├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── pytest.ini ├── .gitignore ├── .coveragerc ├── MANIFEST.in ├── Makefile ├── setup.cfg ├── tox.ini ├── strace_process_tree_example_output.txt ├── setup.py ├── README.rst ├── CHANGES.rst ├── release.mk ├── strace_process_tree_example_verbose_output.txt ├── strace_process_tree.py ├── LICENSE └── tests.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: mgedmin 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests.py 3 | addopts = -ra 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | build/ 3 | dist/ 4 | tmp/ 5 | *.pyc 6 | *.pyo 7 | __pycache__/ 8 | .coverage 9 | .tox/ 10 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = strace_process_tree 3 | branch = true 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: nocover 8 | if __name__ == .__main__.: 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include .gitignore 2 | include *.mk 3 | include *.py 4 | include *.rst 5 | include *.txt 6 | include *.yml 7 | include .coveragerc 8 | include LICENSE 9 | include Makefile 10 | include pytest.ini 11 | include tox.ini 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: 3 | @echo "Nothing to build." 4 | 5 | .PHONY: test 6 | test: ##: run tests 7 | tox -p auto 8 | 9 | .PHONY: coverage 10 | coverage: ##: measure test coverage 11 | tox -e coverage 12 | 13 | .PHONY: flake8 14 | flake8: ##: check for style problems 15 | tox -e flake8 16 | 17 | 18 | FILE_WITH_VERSION = strace_process_tree.py 19 | include release.mk 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [zest.releaser] 2 | python-file-with-version = strace_process_tree.py 3 | create-wheel = yes 4 | 5 | [flake8] 6 | extend-ignore = E501 7 | # https://pep8.readthedocs.org/en/latest/intro.html#error-codes 8 | # E501: line too long (82 > 79 characters) 9 | 10 | [isort] 11 | # from X import ( 12 | # a, 13 | # b, 14 | # ) 15 | multi_line_output = 3 16 | include_trailing_comma = true 17 | lines_after_imports = 2 18 | reverse_relative = true 19 | default_section = THIRDPARTY 20 | known_first_party = strace_process_tree 21 | # known_third_party = pytest, ... 22 | # skip = filename... 23 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py310,py311,py312,py313,py314,pypy3 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | # NB: pytest {posargs} and python -m pytest {posargs} produce observable 8 | # differences in argparse's understanding of the program name. 9 | commands = 10 | python -m pytest {posargs} 11 | 12 | [testenv:coverage] 13 | deps = 14 | {[testenv]deps} 15 | coverage 16 | commands = 17 | coverage run -m pytest 18 | coverage report -m 19 | 20 | [testenv:flake8] 21 | deps = flake8 22 | skip_install = true 23 | commands = flake8 setup.py strace_process_tree.py tests.py 24 | 25 | [testenv:isort] 26 | deps = isort 27 | skip_install = true 28 | commands = isort {posargs: -c --diff setup.py strace_process_tree.py tests.py} 29 | 30 | [testenv:check-manifest] 31 | deps = check-manifest 32 | skip_install = true 33 | commands = check-manifest {posargs} 34 | 35 | [testenv:check-python-versions] 36 | deps = check-python-versions 37 | skip_install = true 38 | commands = check-python-versions {posargs} 39 | -------------------------------------------------------------------------------- /strace_process_tree_example_output.txt: -------------------------------------------------------------------------------- 1 | 25510 make binary-package 2 | ├─25511 /bin/sh -c 'dpkg-parsechangelog | awk '\''$1 == "Source:" { print $2 }'\''' 3 | │ ├─25512 dpkg-parsechangelog 4 | │ │ └─25514 tail -n 40 debian/changelog 5 | │ └─25513 awk '$1 == "Source:" { print $2 }' 6 | ├─25515 /bin/sh -c 'dpkg-parsechangelog | awk '\''$1 == "Version:" { print $2 }'\''' 7 | │ ├─25516 dpkg-parsechangelog 8 | │ │ └─25518 tail -n 40 debian/changelog 9 | │ └─25517 awk '$1 == "Version:" { print $2 }' 10 | ├─25519 /bin/sh -c 'dpkg-parsechangelog | grep ^Date: | cut -d: -f 2- | date --date="$(cat)" +%Y-%m-%d' 11 | │ ├─25520 dpkg-parsechangelog 12 | │ │ └─25525 tail -n 40 debian/changelog 13 | │ ├─25521 grep ^Date: 14 | │ ├─25522 cut -d: -f 2- 15 | │ └─25523 date --date=" Thu, 18 Jan 2018 23:39:51 +0200" +%Y-%m-%d 16 | │ └─25524 cat 17 | └─25526 /bin/sh -c 'dpkg-parsechangelog | awk '\''$1 == "Distribution:" { print $2 }'\''' 18 | ├─25527 dpkg-parsechangelog 19 | │ └─25529 tail -n 40 debian/changelog 20 | └─25528 awk '$1 == "Distribution:" { print $2 }' 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import ast 3 | import os 4 | import re 5 | 6 | from setuptools import setup 7 | 8 | 9 | here = os.path.dirname(__file__) 10 | with open(os.path.join(here, "README.rst")) as f: 11 | long_description = f.read() 12 | 13 | metadata = {} 14 | with open(os.path.join(here, "strace_process_tree.py")) as f: 15 | rx = re.compile("(__version__|__author__|__url__|__licence__) = (.*)") 16 | for line in f: 17 | m = rx.match(line) 18 | if m: 19 | metadata[m.group(1)] = ast.literal_eval(m.group(2)) 20 | version = metadata["__version__"] 21 | 22 | setup( 23 | name="strace-process-tree", 24 | version=version, 25 | author="Marius Gedminas", 26 | author_email="marius@gedmin.as", 27 | url="https://github.com/mgedmin/strace-process-tree", 28 | description="Produce a process tree from an strace log", 29 | long_description=long_description, 30 | long_description_content_type='text/x-rst', 31 | keywords="strace log process tree", 32 | classifiers=[ 33 | "Development Status :: 5 - Production/Stable", 34 | "Environment :: Console", 35 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 36 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 37 | "Operating System :: OS Independent", 38 | "Programming Language :: Python", 39 | "Programming Language :: Python :: 3.10", 40 | "Programming Language :: Python :: 3.11", 41 | "Programming Language :: Python :: 3.12", 42 | "Programming Language :: Python :: 3.13", 43 | "Programming Language :: Python :: 3.14", 44 | "Programming Language :: Python :: Implementation :: CPython", 45 | "Programming Language :: Python :: Implementation :: PyPy", 46 | ], 47 | license="GPL v2 or v3", 48 | python_requires=">=3.10", 49 | 50 | py_modules=["strace_process_tree"], 51 | zip_safe=False, 52 | entry_points={ 53 | "console_scripts": [ 54 | "strace-process-tree = strace_process_tree:main", 55 | ], 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | strace-process-tree 2 | =================== 3 | 4 | .. image:: https://github.com/mgedmin/strace-process-tree/actions/workflows/build.yml/badge.svg?branch=master 5 | :target: https://github.com/mgedmin/strace-process-tree/actions 6 | 7 | 8 | Reads strace -f output and produces a process tree. Example :: 9 | 10 | $ strace -f -e trace=process -s 1024 -o /tmp/trace.out make binary-package 11 | ... 12 | 13 | $ strace-process-tree /tmp/trace.out 14 | 25510 make binary-package 15 | ├─25511 /bin/sh -c 'dpkg-parsechangelog | awk '\''$1 == "Source:" { print $2 }'\''' 16 | │ ├─25512 dpkg-parsechangelog 17 | │ │ └─25514 tail -n 40 debian/changelog 18 | │ └─25513 awk '$1 == "Source:" { print $2 }' 19 | ├─25515 /bin/sh -c 'dpkg-parsechangelog | awk '\''$1 == "Version:" { print $2 }'\''' 20 | │ ├─25516 dpkg-parsechangelog 21 | │ │ └─25518 tail -n 40 debian/changelog 22 | │ └─25517 awk '$1 == "Version:" { print $2 }' 23 | ├─25519 /bin/sh -c 'dpkg-parsechangelog | grep ^Date: | cut -d: -f 2- | date --date="$(cat)" +%Y-%m-%d' 24 | │ ├─25520 dpkg-parsechangelog 25 | │ │ └─25525 tail -n 40 debian/changelog 26 | │ ├─25521 grep ^Date: 27 | │ ├─25522 cut -d: -f 2- 28 | │ └─25523 date --date=" Thu, 18 Jan 2018 23:39:51 +0200" +%Y-%m-%d 29 | │ └─25524 cat 30 | └─25526 /bin/sh -c 'dpkg-parsechangelog | awk '\''$1 == "Distribution:" { print $2 }'\''' 31 | ├─25527 dpkg-parsechangelog 32 | │ └─25529 tail -n 40 debian/changelog 33 | └─25528 awk '$1 == "Distribution:" { print $2 }' 34 | 35 | 36 | Installation 37 | ------------ 38 | 39 | Use your favourite pip wrapper to install strace-process-tree, e.g. 40 | 41 | pipx install strace-process-tree 42 | 43 | 44 | Synopsis 45 | -------- 46 | 47 | Usage: strace-process-tree [-h] [--version] [-c] [-C] [-U] [-A] [-v] filename 48 | 49 | Read strace -f output and produce a process tree. Recommended strace options 50 | for best results: 51 | 52 | strace -f -ttt -e trace=process -s 1024 -o FILENAME COMMAND 53 | 54 | positional arguments: 55 | filename strace log to parse (use - to read stdin) 56 | 57 | optional arguments: 58 | -h, --help show this help message and exit 59 | --version show program's version number and exit 60 | -c, --color force color output 61 | -C, --no-color disable color output 62 | -U, --unicode force Unicode output 63 | -A, --ascii force ASCII output 64 | -v, --verbose more verbose output 65 | 66 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # NB: this name is used in the status badge 2 | name: build 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | workflow_dispatch: 12 | schedule: 13 | - cron: "0 5 * * 6" # 5:00 UTC every Saturday 14 | 15 | jobs: 16 | build: 17 | name: Python ${{ matrix.python-version }} on ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | 20 | strategy: 21 | matrix: 22 | python-version: 23 | - "3.10" 24 | - "3.11" 25 | - "3.12" 26 | - "3.13" 27 | - "3.14" 28 | - "pypy3.10" 29 | os: 30 | - ubuntu-latest 31 | - windows-latest 32 | 33 | steps: 34 | - name: Git clone 35 | uses: actions/checkout@v4 36 | 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: "${{ matrix.python-version }}" 41 | cache: pip 42 | cache-dependency-path: | 43 | setup.py 44 | 45 | - name: Install dependencies 46 | run: | 47 | python -m pip install -U pip 48 | python -m pip install -U setuptools wheel 49 | python -m pip install -U pytest coverage coveralls 50 | python -m pip install -e . 51 | 52 | - name: Run tests 53 | run: coverage run -m pytest 54 | 55 | - name: Check test coverage 56 | run: | 57 | coverage report -m --fail-under=${{ contains(fromJSON('["3.8", "3.9"]'), matrix.python-version) && 99 || 100 }} 58 | coverage xml 59 | 60 | - name: Report to coveralls 61 | uses: coverallsapp/github-action@v2 62 | with: 63 | file: coverage.xml 64 | 65 | lint: 66 | name: ${{ matrix.toxenv }} 67 | runs-on: ubuntu-latest 68 | 69 | strategy: 70 | matrix: 71 | toxenv: 72 | - flake8 73 | - isort 74 | - check-manifest 75 | - check-python-versions 76 | 77 | steps: 78 | - name: Git clone 79 | uses: actions/checkout@v4 80 | 81 | - name: Set up Python ${{ env.default_python || '3.12' }} 82 | uses: actions/setup-python@v5 83 | with: 84 | python-version: "${{ env.default_python || '3.12' }}" 85 | cache: pip 86 | cache-dependency-path: | 87 | tox.ini 88 | 89 | - name: Install dependencies 90 | run: | 91 | python -m pip install -U pip 92 | python -m pip install -U setuptools wheel 93 | python -m pip install -U tox 94 | 95 | - name: Run ${{ matrix.toxenv }} 96 | run: python -m tox -e ${{ matrix.toxenv }} 97 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | 5 | 1.5.3 (unreleased) 6 | ------------------ 7 | 8 | - Drop support for Python 3.8 and 3.9. 9 | 10 | 11 | 1.5.2 (2025-10-14) 12 | ------------------ 13 | 14 | - Add support for Python 3.14. 15 | - Drop support for Python 3.7. 16 | - Fix parsing ``execve("command", ["args", ...], [/* N vars */)]`` with a 17 | ``...`` in the argv array (`issue 12 18 | `_). 19 | 20 | 21 | 1.5.1 (2024-10-09) 22 | ------------------ 23 | 24 | - Add support for Python 3.13. 25 | 26 | 27 | 1.5.0 (2024-04-19) 28 | ------------------ 29 | 30 | - Add support for Python 3.12. 31 | - Recognize the clone3 system call (`issue 11 32 | `_). 33 | 34 | 35 | 1.4.0 (2023-06-27) 36 | ------------------ 37 | 38 | * Fix parsing ``/* 1 var */`` (`issue 9 39 | `_). 40 | * Removed support for Python 2. 41 | 42 | 43 | 1.3.0 (2023-05-24) 44 | ------------------ 45 | 46 | * Support the NO_COLOR environment variable for disabling color autodetection 47 | (see https://no-color.org/). 48 | * Fix parsing '<... syscall resumed>)' lines without a space in front of 49 | the closing parenthesis (`issue 5 50 | `_). 51 | 52 | 53 | 1.2.1 (2022-10-28) 54 | ------------------ 55 | 56 | * Add support for Python 3.8, 3.9, 3.10, and 3.11. 57 | * Drop support for Python 3.5 and 3.6. 58 | * Show line numbers when complaining about malformed input lines. 59 | * Handle "[pid NNN]" prefixes with more than one space. 60 | 61 | 62 | 1.2.0 (2019-08-23) 63 | ------------------ 64 | 65 | * Colorize the output if your terminal supports color. 66 | * Command-line options --color/--no-color if you don't want autodetection. 67 | * Use ASCII art if your locale does not support UTF-8. 68 | * Command-line options --ascii/--unicode if you don't want autodetection. 69 | * Speed up strace log parsing by 40%. 70 | 71 | 72 | 1.1.0 (2019-08-22) 73 | ------------------ 74 | 75 | * Show process running times when the strace log has timestamps 76 | (i.e. -t/-tt/ -ttt was passed to strace). 77 | * Fix tree construction to avoid duplicating processes when execve() 78 | shows up in the log before the parent's clone() resumes. 79 | 80 | 81 | 1.0.0 (2019-08-21) 82 | ------------------ 83 | 84 | * Moved to its own repository on GitHub, added a README and this changelog. 85 | * First release to PyPI. 86 | 87 | 88 | 0.9.0 (2019-08-01) 89 | ------------------ 90 | 91 | * Use Python 3 by default. 92 | 93 | 94 | 0.8.0 (2019-06-05) 95 | ------------------ 96 | 97 | * Parse more strace log variations: pids shown as "[pid NNN]", timestamps 98 | formatted as "HH:MM:SS.ssss" (strace -t/-tt versus -ttt that we already 99 | handled). 100 | 101 | 102 | 0.7.0 (2019-04-10) 103 | ------------------ 104 | 105 | * Do not lose information on repeated execve() calls. 106 | 107 | 108 | 0.6.2 (2019-04-10) 109 | ------------------ 110 | 111 | * PEP-8 and slight readability refactoring. 112 | 113 | 114 | 0.6.1 (2018-05-19) 115 | ------------------ 116 | 117 | * New strace in Ubuntu 18.04 LTS formats its log files differently. 118 | * Recognize fork(). 119 | 120 | 121 | 0.6.0 (2018-01-19) 122 | ------------------ 123 | 124 | * Use argparse, add help message. 125 | * Better error reporting. 126 | * Print just the command lines instead of execve() system call arguments 127 | (pass -v/--verbose if you want to see full execve() calls like before). 128 | * execve() is more important than clone(). 129 | * Distinguish threads from forks. 130 | * This was the last version released as a Gist. Newer versions were available 131 | from `my scripts repository 132 | `__. 133 | 134 | 135 | 0.5.1 (2016-12-07) 136 | ------------------ 137 | 138 | * Strip trailing whitespace in output. 139 | 140 | 141 | 0.5.0 (2015-12-01) 142 | ------------------ 143 | 144 | * Handle strace -T output. 145 | * Simplify clone() args in output. 146 | 147 | 148 | 0.4.0 (2015-11-18) 149 | ------------------ 150 | 151 | * Support vfork() and fork(). 152 | 153 | 154 | 0.3.0 (2015-11-13) 155 | ------------------ 156 | 157 | * Support optional timestamps (strace -ttt). 158 | 159 | 160 | 0.2.3 (2014-11-14) 161 | ------------------ 162 | 163 | * Recommend strace options in --help message. 164 | * Add a file containing example output. 165 | 166 | 167 | 0.2.2 (2013-05-29) 168 | ------------------ 169 | 170 | * Fix strace files that have two spaces between pid and event. 171 | 172 | 173 | 0.2.1 (2013-02-27) 174 | ------------------ 175 | 176 | * Add output example. 177 | * Fix incorrect assumption that strace files always had two spaces between the 178 | pid and the event. 179 | 180 | 181 | 0.2 (2013-02-15) 182 | ---------------- 183 | 184 | * Add Unicode line art. 185 | 186 | 187 | 0.1 (2013-02-14) 188 | ---------------- 189 | 190 | * First public release as a GitHub Gist at 191 | https://gist.github.com/mgedmin/4953427 192 | -------------------------------------------------------------------------------- /release.mk: -------------------------------------------------------------------------------- 1 | # release.mk version 2.2.3 (2024-10-10) 2 | # 3 | # Helpful Makefile rules for releasing Python packages. 4 | # https://github.com/mgedmin/python-project-skel 5 | 6 | # You might want to change these 7 | FILE_WITH_VERSION ?= setup.py 8 | FILE_WITH_CHANGELOG ?= CHANGES.rst 9 | CHANGELOG_DATE_FORMAT ?= %Y-%m-%d 10 | CHANGELOG_FORMAT ?= $(changelog_ver) ($(changelog_date)) 11 | DISTCHECK_DIFF_OPTS ?= $(DISTCHECK_DIFF_DEFAULT_OPTS) 12 | 13 | # These should be fine 14 | PYTHON ?= python3 15 | PYPI_PUBLISH ?= rm -rf dist && $(PYTHON) -m build && twine check dist/* && twine upload dist/* 16 | LATEST_RELEASE_MK_URL = https://raw.githubusercontent.com/mgedmin/python-project-skel/master/release.mk 17 | DISTCHECK_DIFF_DEFAULT_OPTS = -x PKG-INFO -x setup.cfg -x '*.egg-info' -x .github -I'^\#' 18 | 19 | # These should be fine, as long as you use Git 20 | VCS_GET_LATEST ?= git pull 21 | VCS_STATUS ?= git status --porcelain 22 | VCS_EXPORT ?= git archive --format=tar --prefix=tmp/tree/ HEAD | tar -xf - 23 | VCS_TAG ?= git tag -s $(changelog_ver) -m \"Release $(changelog_ver)\" 24 | VCS_COMMIT_AND_PUSH ?= git commit -av -m "Post-release version bump" && git push && git push --tags 25 | 26 | # These are internal implementation details 27 | changelog_ver = `$(PYTHON) setup.py --version` 28 | changelog_date = `LC_ALL=C date +'$(CHANGELOG_DATE_FORMAT)'` 29 | 30 | # Tweaking the look of 'make help'; most of these are awk literals and need the quotes 31 | HELP_INDENT = "" 32 | HELP_PREFIX = "make " 33 | HELP_WIDTH = 24 34 | HELP_SEPARATOR = " \# " 35 | HELP_SECTION_SEP = "\n" 36 | 37 | .PHONY: help 38 | help: 39 | @grep -Eh -e '^[a-zA-Z0-9_ -]+:.*?##: .*$$' -e '^##:' $(MAKEFILE_LIST) \ 40 | | awk 'BEGIN {FS = "(^|:[^#]*)##: "; section=""}; \ 41 | /^##:/ {printf "%s%s\n%s", section, $$2, $(HELP_SECTION_SEP); section=$(HELP_SECTION_SEP)} \ 42 | /^[^#]/ {printf "%s\033[36m%-$(HELP_WIDTH)s\033[0m%s%s\n", \ 43 | $(HELP_INDENT), $(HELP_PREFIX) $$1, $(HELP_SEPARATOR), $$2}' 44 | 45 | .PHONY: dist 46 | dist: 47 | $(PYTHON) -m build 48 | 49 | # Provide a default 'make check' to be the same as 'make test', since that's 50 | # what 80% of my projects use, but make it possible to override. Now 51 | # overriding Make rules is painful, so instead of a regular rule definition 52 | # you'll have to override the check_recipe macro. 53 | .PHONY: check 54 | check: 55 | $(check_recipe) 56 | 57 | ifndef check_recipe 58 | define check_recipe = 59 | @$(MAKE) test 60 | endef 61 | endif 62 | 63 | .PHONY: distcheck 64 | distcheck: distcheck-vcs distcheck-sdist 65 | 66 | .PHONY: distcheck-vcs 67 | distcheck-vcs: 68 | ifndef FORCE 69 | # Bit of a chicken-and-egg here, but if the tree is unclean, make 70 | # distcheck-sdist will fail. 71 | @test -z "`$(VCS_STATUS) 2>&1`" || { echo; echo "Your working tree is not clean:" 1>&2; $(VCS_STATUS) 1>&2; exit 1; } 72 | endif 73 | 74 | # NB: do not use $(MAKE) in rules with multiple shell commands joined by && 75 | # because then make -n distcheck will actually run those instead of just 76 | # printing what it does 77 | 78 | # TBH this could (and probably should) be replaced by check-manifest 79 | 80 | .PHONY: distcheck-sdist 81 | distcheck-sdist: dist 82 | pkg_and_version=`$(PYTHON) setup.py --name|tr A-Z.- a-z__`-`$(PYTHON) setup.py --version` && \ 83 | rm -rf tmp && \ 84 | mkdir tmp && \ 85 | $(VCS_EXPORT) && \ 86 | cd tmp && \ 87 | tar -xzf ../dist/$$pkg_and_version.tar.gz && \ 88 | diff -ur $$pkg_and_version tree $(DISTCHECK_DIFF_OPTS) && \ 89 | cd $$pkg_and_version && \ 90 | make dist check && \ 91 | cd .. && \ 92 | mkdir one two && \ 93 | cd one && \ 94 | tar -xzf ../../dist/$$pkg_and_version.tar.gz && \ 95 | cd ../two/ && \ 96 | tar -xzf ../$$pkg_and_version/dist/$$pkg_and_version.tar.gz && \ 97 | cd .. && \ 98 | diff -ur one two -x SOURCES.txt -I'^#:' && \ 99 | cd .. && \ 100 | rm -rf tmp && \ 101 | echo "sdist seems to be ok" 102 | 103 | .PHONY: check-latest-rules 104 | check-latest-rules: 105 | ifndef FORCE 106 | @curl -s $(LATEST_RELEASE_MK_URL) | cmp -s release.mk || { printf "\nYour release.mk does not match the latest version at\n$(LATEST_RELEASE_MK_URL)\n\n" 1>&2; exit 1; } 107 | endif 108 | 109 | .PHONY: check-latest-version 110 | check-latest-version: 111 | $(VCS_GET_LATEST) 112 | 113 | .PHONY: check-version-number 114 | check-version-number: 115 | @$(PYTHON) setup.py --version | grep -qv dev || { \ 116 | echo "Please remove the 'dev' suffix from the version number in $(FILE_WITH_VERSION)"; exit 1; } 117 | 118 | .PHONY: check-long-description 119 | check-long-description: 120 | @$(PYTHON) setup.py --long-description | rst2html --exit-status=2 > /dev/null 121 | 122 | .PHONY: check-changelog 123 | check-changelog: 124 | @ver_and_date="$(CHANGELOG_FORMAT)" && \ 125 | grep -q "^$$ver_and_date$$" $(FILE_WITH_CHANGELOG) || { \ 126 | echo "$(FILE_WITH_CHANGELOG) has no entry for $$ver_and_date"; exit 1; } 127 | 128 | 129 | # NB: the Makefile that includes release.mk may want to add additional 130 | # dependencies to the releasechecklist target, but I want 'make distcheck' to 131 | # happen last, so that's why I put it into the recipe and not at the end of the 132 | # list of dependencies. 133 | 134 | .PHONY: releasechecklist 135 | releasechecklist: check-latest-rules check-latest-version check-version-number check-long-description check-changelog 136 | $(MAKE) distcheck 137 | 138 | .PHONY: release 139 | release: releasechecklist do-release ##: prepare a new PyPI release 140 | 141 | .PHONY: do-release 142 | do-release: 143 | $(release_recipe) 144 | 145 | define default_release_recipe_publish_and_tag = 146 | # I'm chicken so I won't actually do these things yet 147 | @echo "Please run" 148 | @echo 149 | @echo " $(PYPI_PUBLISH)" 150 | @echo " $(VCS_TAG)" 151 | @echo 152 | endef 153 | define default_release_recipe_increment_and_push = 154 | @echo "Please increment the version number in $(FILE_WITH_VERSION)" 155 | @echo "and add a new empty entry at the top of the changelog in $(FILE_WITH_CHANGELOG), then" 156 | @echo 157 | @echo ' $(VCS_COMMIT_AND_PUSH)' 158 | @echo 159 | endef 160 | ifndef release_recipe 161 | define release_recipe = 162 | $(default_release_recipe_publish_and_tag) 163 | $(default_release_recipe_increment_and_push) 164 | endef 165 | endif 166 | -------------------------------------------------------------------------------- /strace_process_tree_example_verbose_output.txt: -------------------------------------------------------------------------------- 1 | $ strace-process-tree zdaemon-py3.GIT/TRACE3 2 | 6184 execve("bin/test", ["bin/test", "-pvc", "-t", "README"], [/* 65 vars */]) 3 | ├─6196 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -p 'echo hello world' "...], [/* 67 vars */]) 4 | │ └─6197 execve("./zdaemon", ["./zdaemon", "-p", "echo hello world", "fg"], [/* 67 vars */]) 5 | │ └─6199 execve("/bin/echo", ["echo", "hello", "world"], [/* 67 vars */]) 6 | ├─6200 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -p 'sleep 100' start"], [/* 67 vars */]) 7 | │ └─6201 execve("./zdaemon", ["./zdaemon", "-p", "sleep 100", "start"], [/* 67 vars */]) 8 | │ └─6205 execve("/usr/bin/python3.3", ["/usr/bin/python3.3", "./zdaemon", "-S", "schema.xml", "-b", "10", "-s", "zdsock", "-m", "0o22", "-x", "0,2", "sleep", "100"], [/* 68 vars */]) 9 | │ └─6212 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f163fcab9d0) 10 | │ ├─6213 clone(child_stack=0x7f163e4b4ff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f163e4b59d0, tls=0x7f163e4b5700, child_tidptr=0x7f163e4b59d0) 11 | │ └─6214 execve("/bin/sleep", ["sleep", "100"], [/* 67 vars */]) 12 | ├─6215 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -p 'sleep 100' status"], [/* 67 vars */]) 13 | │ └─6216 execve("./zdaemon", ["./zdaemon", "-p", "sleep 100", "status"], [/* 67 vars */]) 14 | ├─6217 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -p 'sleep 100' stop"], [/* 67 vars */]) 15 | │ └─6218 execve("./zdaemon", ["./zdaemon", "-p", "sleep 100", "stop"], [/* 67 vars */]) 16 | ├─6220 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -p 'sleep 100' status"], [/* 67 vars */]) 17 | │ └─6221 execve("./zdaemon", ["./zdaemon", "-p", "sleep 100", "status"], [/* 67 vars */]) 18 | ├─6225 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf start"], [/* 67 vars */]) 19 | │ └─6226 execve("./zdaemon", ["./zdaemon", "-Cconf", "start"], [/* 67 vars */]) 20 | │ └─6253 execve("/usr/bin/python3.3", ["/usr/bin/python3.3", "./zdaemon", "-S", "/home/mg/src/zdaemon-py3.GIT/src"..., "-C", "conf", "sleep", "100"], [/* 68 vars */]) 21 | │ └─6262 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fd2a97bf9d0) 22 | │ ├─6263 clone(child_stack=0x7fd2a7fc8ff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fd2a7fc99d0, tls=0x7fd2a7fc9700, child_tidptr=0x7fd2a7fc99d0) 23 | │ └─6264 execve("/bin/sleep", ["sleep", "100"], [/* 67 vars */]) 24 | ├─6269 execve("/bin/sh", ["/bin/sh", "-c", "ls"], [/* 67 vars */]) 25 | │ └─6270 execve("/bin/ls", ["ls"], [/* 67 vars */]) 26 | ├─6271 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf stop"], [/* 67 vars */]) 27 | │ └─6272 execve("./zdaemon", ["./zdaemon", "-Cconf", "stop"], [/* 67 vars */]) 28 | ├─6278 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf start"], [/* 67 vars */]) 29 | │ └─6279 execve("./zdaemon", ["./zdaemon", "-Cconf", "start"], [/* 67 vars */]) 30 | │ └─6288 execve("/usr/bin/python3.3", ["/usr/bin/python3.3", "./zdaemon", "-S", "/home/mg/src/zdaemon-py3.GIT/src"..., "-C", "conf", "sleep", "100"], [/* 68 vars */]) 31 | │ └─6294 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f44cb0e79d0) 32 | │ ├─6295 clone(child_stack=0x7f44c98f0ff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f44c98f19d0, tls=0x7f44c98f1700, child_tidptr=0x7f44c98f19d0) 33 | │ └─6296 execve("/bin/sleep", ["sleep", "100"], [/* 67 vars */]) 34 | ├─6298 execve("/bin/sh", ["/bin/sh", "-c", "ls"], [/* 67 vars */]) 35 | │ └─6300 execve("/bin/ls", ["ls"], [/* 67 vars */]) 36 | ├─6301 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf stop"], [/* 67 vars */]) 37 | │ └─6302 execve("./zdaemon", ["./zdaemon", "-Cconf", "stop"], [/* 67 vars */]) 38 | ├─6304 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf start 100"], [/* 67 vars */]) 39 | │ └─6305 execve("./zdaemon", ["./zdaemon", "-Cconf", "start", "100"], [/* 67 vars */]) 40 | │ └─6314 execve("/usr/bin/python3.3", ["/usr/bin/python3.3", "./zdaemon", "-S", "/home/mg/src/zdaemon-py3.GIT/src"..., "-C", "conf", "sleep", "100"], [/* 68 vars */]) 41 | │ └─6322 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff8f14959d0) 42 | │ ├─6323 clone(child_stack=0x7ff8efc9eff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7ff8efc9f9d0, tls=0x7ff8efc9f700, child_tidptr=0x7ff8efc9f9d0) 43 | │ └─6324 execve("/bin/sleep", ["sleep", "100"], [/* 67 vars */]) 44 | ├─6333 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf status"], [/* 67 vars */]) 45 | │ └─6334 execve("./zdaemon", ["./zdaemon", "-Cconf", "status"], [/* 67 vars */]) 46 | ├─6346 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf stop"], [/* 67 vars */]) 47 | │ └─6347 execve("./zdaemon", ["./zdaemon", "-Cconf", "stop"], [/* 67 vars */]) 48 | ├─6348 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf fg"], [/* 67 vars */]) 49 | │ └─6349 execve("./zdaemon", ["./zdaemon", "-Cconf", "fg"], [/* 67 vars */]) 50 | │ └─6350 execve("/usr/bin/env", ["env"], [/* 68 vars */]) 51 | ├─6351 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf start"], [/* 67 vars */]) 52 | │ └─6352 execve("./zdaemon", ["./zdaemon", "-Cconf", "start"], [/* 67 vars */]) 53 | │ └─6354 execve("/usr/bin/python3.3", ["/usr/bin/python3.3", "./zdaemon", "-S", "/home/mg/src/zdaemon-py3.GIT/src"..., "-C", "conf", "tail", "-f", "data"], [/* 68 vars */]) 54 | │ └─6380 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb4506119d0) 55 | │ ├─6381 clone(child_stack=0x7fb44ee1aff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fb44ee1b9d0, tls=0x7fb44ee1b700, child_tidptr=0x7fb44ee1b9d0) 56 | │ └─6382 execve("/usr/bin/tail", ["tail", "-f", "data"], [/* 67 vars */]) 57 | └─6399 execve("/bin/sh", ["/bin/sh", "-c", "./zdaemon -Cconf reopen_transcri"...], [/* 67 vars */]) 58 | └─6400 execve("./zdaemon", ["./zdaemon", "-Cconf", "reopen_transcript"], [/* 67 vars */]) 59 | -------------------------------------------------------------------------------- /strace_process_tree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | Usage: 5 | strace-process-tree filename 6 | 7 | Read strace -f output and produce a process tree. 8 | 9 | Recommended strace options for best results: 10 | 11 | strace -f -e trace=process -s 1024 -o filename.out command args 12 | 13 | """ 14 | 15 | import argparse 16 | import os 17 | import re 18 | import string 19 | import sys 20 | from collections import defaultdict, namedtuple 21 | from contextlib import nullcontext 22 | from functools import partial 23 | 24 | 25 | __version__ = '1.5.3.dev0' 26 | __author__ = 'Marius Gedminas ' 27 | __url__ = "https://github.com/mgedmin/strace-process-tree" 28 | __licence__ = 'GPL v2 or v3' # or ask me for MIT 29 | 30 | 31 | Tree = namedtuple('Tree', 'trunk, fork, end, space') 32 | 33 | 34 | class Theme(object): 35 | 36 | default_styles = dict( 37 | tree_style='normal', 38 | pid='red', 39 | process='green', 40 | time_range='blue', 41 | ) 42 | 43 | ascii_tree = Tree( 44 | ' | ', 45 | ' |-', 46 | ' `-', 47 | ' ', 48 | ) 49 | 50 | unicode_tree = Tree( 51 | ' │ ', 52 | ' ├─', 53 | ' └─', 54 | ' ', 55 | ) 56 | 57 | def __new__(cls, color=None, unicode=None): 58 | if cls is Theme: 59 | if color is None: 60 | color = cls.should_use_color() 61 | if color: 62 | cls = AnsiTheme 63 | else: 64 | cls = PlainTheme 65 | return object.__new__(cls) 66 | 67 | def __init__(self, color=None, unicode=None): 68 | if unicode is None: 69 | unicode = self.can_unicode() 70 | self.tree = self.unicode_tree if unicode else self.ascii_tree 71 | self.styles = dict(self.default_styles) 72 | 73 | @classmethod 74 | def should_use_color(cls): 75 | return ( 76 | cls.is_terminal() 77 | and cls.terminal_supports_color() 78 | and not cls.user_dislikes_color() 79 | ) 80 | 81 | @classmethod 82 | def is_terminal(cls): 83 | return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() 84 | 85 | @classmethod 86 | def terminal_supports_color(cls): 87 | return (os.environ.get('TERM') or 'dumb') != 'dumb' 88 | 89 | @classmethod 90 | def user_dislikes_color(cls): 91 | # https://no-color.org/ 92 | return bool(os.environ.get('NO_COLOR')) 93 | 94 | @classmethod 95 | def can_unicode(cls): 96 | return getattr(sys.stdout, 'encoding', None) == 'UTF-8' 97 | 98 | def _format(self, prefix, suffix, text): 99 | if not text: 100 | return '' 101 | return '{}{}{}'.format(prefix, text, suffix) 102 | 103 | def _no_format(self, text): 104 | return text or '' 105 | 106 | def __getattr__(self, attr): 107 | if attr not in self.styles: 108 | raise AttributeError(attr) 109 | style = self.styles[attr] 110 | if style == 'normal': 111 | _format = self._no_format 112 | else: 113 | prefix = self.ctlseq[style] 114 | suffix = self.ctlseq['normal'] 115 | _format = partial(self._format, prefix, suffix) 116 | setattr(self, attr, _format) 117 | return _format 118 | 119 | 120 | class PlainTheme(Theme): 121 | 122 | def __getattr__(self, attr): 123 | if attr not in self.styles: 124 | raise AttributeError(attr) 125 | _format = self._no_format 126 | setattr(self, attr, _format) 127 | return _format 128 | 129 | 130 | class AnsiTheme(Theme): 131 | 132 | ctlseq = dict( 133 | normal='\033[m', 134 | red='\033[31m', 135 | green='\033[32m', 136 | blue='\033[34m', 137 | ) 138 | 139 | 140 | Event = namedtuple('Event', 'pid, timestamp, event') 141 | 142 | 143 | def parse_timestamp(timestamp): 144 | if ':' in timestamp: 145 | h, m, s = timestamp.split(':') 146 | return (float(h) * 60 + float(m)) * 60 + float(s) 147 | else: 148 | return float(timestamp) 149 | 150 | 151 | RESUMED_PREFIX = re.compile(r'<... \w+ resumed> ?') 152 | UNFINISHED_SUFFIX = ' ' 153 | DURATION_SUFFIX = re.compile(r' <\d+(?:\.\d+)?>$') 154 | PID = re.compile(r'^\[pid +(\d+)\]') 155 | TIMESTAMP = re.compile(r'^\d+(?::\d+:\d+)?(?:\.\d+)?\s+') 156 | IGNORE = re.compile(r'^$|^strace: Process \d+ attached$') 157 | 158 | 159 | def events(stream): 160 | pending = {} 161 | for n, line in enumerate(stream, 1): 162 | line = line.strip() 163 | if line.startswith('[pid'): 164 | line = PID.sub(r'\1', line) 165 | pid, space, event = line.partition(' ') 166 | try: 167 | pid = int(pid) 168 | except ValueError: 169 | if IGNORE.match(line): 170 | continue 171 | raise SystemExit( 172 | "This does not look like a log file produced by strace -f:\n\n" 173 | " %s\n\n" 174 | "There should've been a PID at the beginning of line %d." 175 | % (line, n)) 176 | event = event.lstrip() 177 | timestamp = None 178 | if event[:1].isdigit(): 179 | m = TIMESTAMP.match(event) 180 | if m is not None: 181 | timestamp = parse_timestamp(m.group()) 182 | event = event[m.end():] 183 | if event.endswith('>'): 184 | e, sp, d = event.rpartition(' <') 185 | if DURATION_SUFFIX.match(sp + d): 186 | event = e 187 | if event.startswith('<...'): 188 | m = RESUMED_PREFIX.match(event) 189 | if m is not None: 190 | pending_event, timestamp = pending.pop(pid) 191 | event = pending_event + event[m.end():] 192 | if event.endswith(UNFINISHED_SUFFIX): 193 | pending[pid] = (event[:-len(UNFINISHED_SUFFIX)], timestamp) 194 | else: 195 | yield Event(pid, timestamp, event) 196 | 197 | 198 | Process = namedtuple('Process', 'pid, seq, name, parent') 199 | 200 | 201 | class ProcessTree(object): 202 | def __init__(self): 203 | self.processes = {} # map pid to Process 204 | self.start_time = {} # map Process to seconds 205 | self.exit_time = {} # map Process to seconds 206 | self.children = defaultdict(set) 207 | # Invariant: every Process appears exactly once in 208 | # self.children[some_parent]. 209 | 210 | def add_child(self, ppid, pid, name, timestamp): 211 | parent = self.processes.get(ppid) 212 | if parent is None: 213 | # This can happen when we attach to a running process and so miss 214 | # the initial execve() call that would have given it a name. 215 | parent = Process(pid=ppid, seq=0, name=None, parent=None) 216 | self.children[None].add(parent) 217 | # NB: it's possible that strace saw code executing in the child process 218 | # before the parent's clone() returned a value, so we might already 219 | # have a self.processes[pid]. 220 | old_process = self.processes.get(pid) 221 | if old_process is not None: 222 | self.children[old_process.parent].remove(old_process) 223 | child = old_process._replace(parent=parent) 224 | else: 225 | # We pass seq=0 here and seq=1 in handle_exec() because 226 | # conceptually clone() happens before execve(), but we must be 227 | # ready to handle these two events in either order. 228 | child = Process(pid=pid, seq=0, name=name, parent=parent) 229 | self.processes[pid] = child 230 | self.children[parent].add(child) 231 | # The timestamp of clone() is always going to be earlier than the 232 | # timestamp of execve() so we use unconditional assignment here but a 233 | # setdefault() in handle_exec(). 234 | self.start_time[child] = timestamp 235 | 236 | def handle_exec(self, pid, name, timestamp): 237 | old_process = self.processes.get(pid) 238 | if old_process: 239 | new_process = old_process._replace(seq=old_process.seq + 1, 240 | name=name) 241 | if old_process.seq == 0 and not self.children[old_process]: 242 | # Drop the child process if it did nothing interesting between 243 | # fork() and exec(). 244 | self.children[old_process.parent].remove(old_process) 245 | else: 246 | new_process = Process(pid=pid, seq=1, name=name, parent=None) 247 | self.processes[pid] = new_process 248 | self.children[new_process.parent].add(new_process) 249 | self.start_time.setdefault(new_process, timestamp) 250 | 251 | def handle_exit(self, pid, timestamp): 252 | process = self.processes.get(pid) 253 | if process: 254 | # process may be None when we attach to a running process and 255 | # see it exit before it does any clone()/execve() calls 256 | self.exit_time[process] = timestamp 257 | 258 | def _format_time_range(self, start_time, exit_time): 259 | if start_time is not None and exit_time is not None: 260 | return '[{duration:.1f}s @{start_time:.1f}s]'.format( 261 | start_time=start_time, 262 | duration=exit_time - start_time 263 | ) 264 | elif start_time: # skip both None and 0 please 265 | return '[@{start_time:.1f}s]'.format( 266 | start_time=start_time, 267 | ) 268 | else: 269 | return '' 270 | 271 | def _format_process_name(self, theme, name, indent, cs, ccs, padding): 272 | lines = (name or '').split('\n') 273 | return '\n{indent}{tree}{padding}'.format( 274 | indent=indent, 275 | tree=theme.tree_style(cs + ccs), 276 | padding=padding, 277 | ).join( 278 | theme.process(line) 279 | for line in lines 280 | ) 281 | 282 | def _format(self, theme, processes, indent='', level=0): 283 | r = [] 284 | for n, process in enumerate(processes): 285 | if level == 0: 286 | s, cs = '', '' 287 | elif n < len(processes) - 1: 288 | s, cs = theme.tree.fork, theme.tree.trunk 289 | else: 290 | s, cs = theme.tree.end, theme.tree.space 291 | children = sorted(self.children[process]) 292 | if children: 293 | ccs = theme.tree.trunk 294 | else: 295 | ccs = theme.tree.space 296 | time_range = self._format_time_range( 297 | self.start_time.get(process), 298 | self.exit_time.get(process), 299 | ) 300 | title = '{pid} {name} {time_range}'.format( 301 | pid=theme.pid(process.pid or ''), 302 | name=self._format_process_name( 303 | theme, process.name, indent, cs, ccs, theme.tree.space), 304 | time_range=theme.time_range(time_range), 305 | ).rstrip() 306 | r.append(indent + (theme.tree_style(s) + title).rstrip() + '\n') 307 | r.append(self._format(theme, children, indent+cs, level+1)) 308 | 309 | return ''.join(r) 310 | 311 | def format(self, theme): 312 | return self._format(theme, sorted(self.children[None])) 313 | 314 | def __str__(self): 315 | return self.format(PlainTheme(unicode=True)) 316 | 317 | 318 | def simplify_syscall(event): 319 | # clone(child_stack=0x..., flags=FLAGS, parent_tidptr=..., tls=..., 320 | # child_tidptr=...) => clone(FLAGS) 321 | if event.startswith(('clone(', 'clone3(')): 322 | event = re.sub('[(].*(?:, |{)flags=([^,]*), .*[)]', r'(\1)', event) 323 | return event.rstrip() 324 | 325 | 326 | def extract_command_line(event): 327 | # execve("/usr/bin/foo", ["foo", "bar"], [/* 45 vars */]) => foo bar 328 | # execve("/usr/bin/foo", ["foo", "bar"], [/* 1 var */]) => foo bar 329 | if event.startswith(('clone(', 'clone3(')): 330 | if 'CLONE_THREAD' in event: 331 | return '(thread)' 332 | elif 'flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD' in event: 333 | return '(fork)' 334 | else: 335 | return '...' 336 | elif event.startswith('execve('): 337 | command = event.strip() 338 | command = re.sub(r'^execve\([^[]*\[', '', command) 339 | command = re.sub(r'\], (0x[0-9a-f]+ )?\[?/\* \d+ vars? \*/\]?\)$', '', 340 | command) 341 | command = parse_argv(command) 342 | return format_command(command) 343 | else: 344 | return event.rstrip() 345 | 346 | 347 | ESCAPES = { 348 | 'n': '\n', 349 | 'r': '\r', 350 | 't': '\t', 351 | 'b': '\b', 352 | '0': '\0', 353 | 'a': '\a', 354 | } 355 | 356 | 357 | def parse_argv(s): 358 | # '"foo", "bar"..., "baz", "\""' => ['foo', 'bar...', 'baz', '"'] 359 | it = iter(s + ",") 360 | args = [] 361 | for c in it: 362 | if c == ' ': 363 | continue 364 | if c == '.': 365 | c = next(it) 366 | assert c == ".", c 367 | c = next(it) 368 | assert c == ".", c 369 | c = next(it) 370 | args.append(Ellipsis) 371 | assert c == ',', (c, s) 372 | continue 373 | assert c == '"', c 374 | arg = [] 375 | for c in it: # pragma: no branch -- loop will execute at least once 376 | if c == '"': 377 | break 378 | if c == '\\': 379 | c = next(it) 380 | arg.append(ESCAPES.get(c, c)) 381 | else: 382 | arg.append(c) 383 | c = next(it) 384 | if c == ".": 385 | arg.append('...') 386 | c = next(it) 387 | assert c == ".", c 388 | c = next(it) 389 | assert c == ".", c 390 | c = next(it) 391 | args.append(''.join(arg)) 392 | assert c == ',', (c, s) 393 | return args 394 | 395 | 396 | SHELL_SAFE_CHARS = set(string.ascii_letters + string.digits + '%+,-./:=@^_~') 397 | SHELL_SAFE_QUOTED = SHELL_SAFE_CHARS | set("!#&'()*;<>?[]{|} \t\n") 398 | 399 | 400 | def format_command(command): 401 | return ' '.join(map(pushquote, ( 402 | '...' if arg is Ellipsis else 403 | arg if all(c in SHELL_SAFE_CHARS for c in arg) else 404 | '"%s"' % arg if all(c in SHELL_SAFE_QUOTED for c in arg) else 405 | "'%s'" % arg.replace("'", "'\\''") 406 | for arg in command 407 | ))) 408 | 409 | 410 | def pushquote(arg): 411 | # Change "--foo=bar" to --foo="bar" because that looks better to human eyes 412 | return re.sub('''^(['"])(--[a-zA-Z0-9_-]+)=''', r'\2=\1', arg) 413 | 414 | 415 | def parse_stream(event_stream, mogrifier=extract_command_line): 416 | tree = ProcessTree() 417 | first_timestamp = None 418 | for e in event_stream: 419 | timestamp = e.timestamp 420 | if timestamp is not None: 421 | if first_timestamp is None: 422 | first_timestamp = e.timestamp 423 | timestamp -= first_timestamp 424 | if e.event.startswith('execve('): 425 | args, equal, result = e.event.rpartition(' = ') 426 | if result == '0': 427 | name = mogrifier(args) 428 | tree.handle_exec(e.pid, name, timestamp) 429 | if e.event.startswith(('clone(', 'clone3(', 'fork(', 'vfork(')): 430 | args, equal, result = e.event.rpartition(' = ') 431 | # if clone() fails, the event will look like this: 432 | # clone(...) = -1 EPERM (Operation not permitted) 433 | # and it will fail the result.isdigit() check 434 | if result.isdigit(): 435 | child_pid = int(result) 436 | name = mogrifier(args) 437 | tree.add_child(e.pid, child_pid, name, timestamp) 438 | if e.event.startswith('+++ exited with '): 439 | tree.handle_exit(e.pid, timestamp) 440 | return tree 441 | 442 | 443 | def open_arg(arg: str): 444 | if arg == '-': 445 | return nullcontext(sys.stdin) 446 | else: 447 | return open(arg) 448 | 449 | 450 | def main(): 451 | parser = argparse.ArgumentParser( 452 | description=""" 453 | Read strace -f output and produce a process tree. 454 | 455 | Recommended strace options for best results: 456 | 457 | strace -f -ttt -e trace=process -s 1024 -o FILENAME COMMAND 458 | """) 459 | parser.add_argument('--version', action='version', version=__version__) 460 | parser.add_argument('-c', '--color', action='store_true', default=None, 461 | help='force color output') 462 | parser.add_argument('-C', '--no-color', action='store_false', dest='color', 463 | help='disable color output') 464 | parser.add_argument('-U', '--unicode', action='store_true', default=None, 465 | help='force Unicode output') 466 | parser.add_argument('-A', '--ascii', action='store_false', dest='unicode', 467 | help='force ASCII output') 468 | parser.add_argument('-v', '--verbose', action='store_true', 469 | help='more verbose output') 470 | parser.add_argument('filename', 471 | help='strace log to parse (use - to read stdin)') 472 | args = parser.parse_args() 473 | 474 | mogrifier = simplify_syscall if args.verbose else extract_command_line 475 | 476 | with open_arg(args.filename) as fp: 477 | tree = parse_stream(events(fp), mogrifier) 478 | 479 | theme = Theme(color=args.color, unicode=args.unicode) 480 | print(tree.format(theme).rstrip()) 481 | 482 | 483 | if __name__ == '__main__': 484 | main() 485 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from io import StringIO 4 | 5 | import pytest 6 | 7 | import strace_process_tree as stp 8 | 9 | 10 | class FakeStdout: 11 | def isatty(self): 12 | return True 13 | 14 | 15 | @pytest.fixture(autouse=True) 16 | def fix_argv0(monkeypatch): 17 | # argparse in Python 3.14 is a clever little monkey that looks at 18 | # sys.modules['__main__'].__spec__ to figure out if the script was started 19 | # with python -m modname instead of by running a script.py, and if so it 20 | # ignores sys.argv[0] completely. 21 | monkeypatch.setattr(sys.modules['__main__'], '__spec__', None) 22 | 23 | 24 | def test_Theme_is_terminal_no_it_is_not(capsys): 25 | assert not stp.Theme.is_terminal() 26 | 27 | 28 | def test_Theme_is_terminal_yes_it_is(monkeypatch): 29 | monkeypatch.setattr(sys, 'stdout', FakeStdout()) 30 | assert stp.Theme.is_terminal() 31 | 32 | 33 | def test_Theme_terminal_supports_color_no(monkeypatch): 34 | monkeypatch.setenv('TERM', 'dumb') 35 | assert not stp.Theme.terminal_supports_color() 36 | 37 | 38 | def test_Theme_terminal_supports_color_yes(monkeypatch): 39 | monkeypatch.setenv('TERM', 'xterm') 40 | assert stp.Theme.terminal_supports_color() 41 | 42 | 43 | def test_Theme_no_color_unset(monkeypatch): 44 | monkeypatch.delenv('NO_COLOR', raising=False) 45 | assert not stp.Theme.user_dislikes_color() 46 | 47 | 48 | def test_Theme_no_color_blank(monkeypatch): 49 | monkeypatch.setenv('NO_COLOR', '') 50 | assert not stp.Theme.user_dislikes_color() 51 | 52 | 53 | def test_Theme_no_color_nonblank(monkeypatch): 54 | monkeypatch.setenv('NO_COLOR', 'please') 55 | assert stp.Theme.user_dislikes_color() 56 | 57 | 58 | def test_Theme_autodetection_color_yes(monkeypatch): 59 | monkeypatch.setattr(sys, 'stdout', FakeStdout()) 60 | monkeypatch.setenv('TERM', 'xterm') 61 | monkeypatch.delenv('NO_COLOR', raising=False) 62 | assert isinstance(stp.Theme(), stp.AnsiTheme) 63 | 64 | 65 | def test_Theme_autodetection_color_no(monkeypatch): 66 | monkeypatch.setattr(sys, 'stdout', FakeStdout()) 67 | monkeypatch.setenv('TERM', 'dumb') 68 | assert isinstance(stp.Theme(), stp.PlainTheme) 69 | 70 | 71 | def test_Theme_autodetection_color_disabled(monkeypatch): 72 | monkeypatch.setattr(sys, 'stdout', FakeStdout()) 73 | monkeypatch.setenv('TERM', 'xterm') 74 | monkeypatch.setenv('NO_COLOR', '1') 75 | assert isinstance(stp.Theme(), stp.PlainTheme) 76 | 77 | 78 | def test_PlainTheme_bad_style(): 79 | with pytest.raises(AttributeError): 80 | stp.PlainTheme().waterfall("oOoOoO") 81 | 82 | 83 | def test_AnsiTheme_bad_style(): 84 | with pytest.raises(AttributeError): 85 | stp.AnsiTheme().waterfall("oOoOoO") 86 | 87 | 88 | def test_AnsiTheme_good_style(): 89 | theme = stp.AnsiTheme() 90 | assert theme.pid('PID') == '\033[31mPID\033[m' 91 | 92 | 93 | def test_AnsiTheme_empty_text(): 94 | theme = stp.AnsiTheme() 95 | assert theme.pid('') == '' 96 | 97 | 98 | @pytest.mark.parametrize(['value', 'expected'], [ 99 | ('123', 123), 100 | ('123.045', 123.045), 101 | ('01:02:03', 3723), 102 | ('01:02:03.045', 3723.045), 103 | ]) 104 | def test_parse_timestamp(value, expected): 105 | assert stp.parse_timestamp(value) == expected 106 | 107 | 108 | def test_events(): 109 | # events() does several things: 110 | # - extracts the pid if present 111 | # - extracts timestamps if present 112 | # - extracts the system call 113 | # - strips durations if present 114 | # - assembles system calls split across several lines with <... resumed> 115 | log_lines = [ 116 | 'strace: Process 27369 attached', 117 | '27369 13:53:26.881056 execve("bin/test", ["bin/test", "-pvc", "-t", "allowhosts.txt"], 0x7fffa04e8ba0 /* 71 vars */) = 0 <0.000832>', 118 | '27369 13:53:26.884089 arch_prctl(ARCH_SET_FS, 0x7fbb38e89740) = 0 <0.000008>', 119 | '27369 13:53:27.213383 clone( ', 120 | '27370 13:53:27.214709 execve("/bin/sh", ["sh", "-c", "uname -p 2> /dev/null"], 0x55842eb789e0 /* 72 vars */ ', 121 | '27369 13:53:27.214872 <... clone resumed> child_stack=0x7fbb371faff0, flags=CLONE_VM|CLONE_VFORK|SIGCHLD) = 27370 <0.001466>', 122 | '27370 13:53:27.214899 <... execve resumed> ) = 0 <0.000132>', 123 | '27370 13:53:27.216357 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=27371, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---', 124 | '27370 13:53:27.216395 exit_group(0) = ?', 125 | '27370 13:53:27.216441 +++ exited with 0 +++', 126 | ] 127 | result = list(stp.events(log_lines)) 128 | assert result == [ 129 | (27369, 50006.881056, 'execve("bin/test", ["bin/test", "-pvc", "-t", "allowhosts.txt"], 0x7fffa04e8ba0 /* 71 vars */) = 0'), 130 | (27369, 50006.884089, 'arch_prctl(ARCH_SET_FS, 0x7fbb38e89740) = 0'), 131 | (27369, 50007.213383, 'clone(child_stack=0x7fbb371faff0, flags=CLONE_VM|CLONE_VFORK|SIGCHLD) = 27370'), 132 | (27370, 50007.214709, 'execve("/bin/sh", ["sh", "-c", "uname -p 2> /dev/null"], 0x55842eb789e0 /* 72 vars */) = 0'), 133 | (27370, 50007.216357, '--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=27371, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---'), 134 | (27370, 50007.216395, 'exit_group(0) = ?'), 135 | (27370, 50007.216441, '+++ exited with 0 +++'), 136 | ] 137 | 138 | 139 | def test_events_split_vfork(): 140 | # Regression test for https://github.com/mgedmin/strace-process-tree/issues/5 141 | log_lines = [ 142 | '230473 02:47:49 vfork( ', 143 | '230474 02:47:49 execve("/home/jtojnar/Projects/tartan/scripts/tartan-build", ["tartan-build", "gcc", "-Isrc/commands/rpm2cpio.p", "-Isrc/commands", "-I../src/commands", "-I.", "-I..", "-I/nix/store/kfizydssj99lf8f6dbmrfa5hap1162m5-glib-2.76.2-dev/include/glib-2.0", "-I/nix/store/763gsnl7y7nj6v98fp1l380ddvyr6zqv-glib-2.76.2/lib/glib-2.0/include", "-fdiagnostics-color=always", "-D_FILE_OFFSET_BITS=64", "-Wall", "-Winvalid-pch", "-O0", "-g", "-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_38", "-Wall", "-Wextra", "-Wcast-align", "-Wmissing-prototypes", "-Wnested-externs", "-Wpointer-arith", "-Wformat-security", "-Wno-unused-parameter", "-MD", "-MQ", "src/commands/rpm2cpio.p/rpm2cpio.c.o", "-MF", "src/commands/rpm2cpio.p/rpm2cpio.c.o.d", "-o", "src/commands/rpm2cpio.p/rpm2cpio.c.o", "-c", "../src/commands/rpm2cpio.c"], 0x2507640 /* 163 vars */ ', 144 | '230473 02:47:49 <... vfork resumed>) = 230474', 145 | '230474 02:47:49 <... execve resumed>) = 0', 146 | ] 147 | result = list(stp.events(log_lines)) 148 | assert result == [ 149 | stp.Event(230473, 10069.0, 'vfork() = 230474'), 150 | stp.Event(230474, 10069.0, 'execve("/home/jtojnar/Projects/tartan/scripts/tartan-build", ["tartan-build", "gcc", "-Isrc/commands/rpm2cpio.p", "-Isrc/commands", "-I../src/commands", "-I.", "-I..", "-I/nix/store/kfizydssj99lf8f6dbmrfa5hap1162m5-glib-2.76.2-dev/include/glib-2.0", "-I/nix/store/763gsnl7y7nj6v98fp1l380ddvyr6zqv-glib-2.76.2/lib/glib-2.0/include", "-fdiagnostics-color=always", "-D_FILE_OFFSET_BITS=64", "-Wall", "-Winvalid-pch", "-O0", "-g", "-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_38", "-Wall", "-Wextra", "-Wcast-align", "-Wmissing-prototypes", "-Wnested-externs", "-Wpointer-arith", "-Wformat-security", "-Wno-unused-parameter", "-MD", "-MQ", "src/commands/rpm2cpio.p/rpm2cpio.c.o", "-MF", "src/commands/rpm2cpio.p/rpm2cpio.c.o.d", "-o", "src/commands/rpm2cpio.p/rpm2cpio.c.o", "-c", "../src/commands/rpm2cpio.c"], 0x2507640 /* 163 vars */) = 0'), 151 | ] 152 | 153 | 154 | def test_events_result_unavailable(): 155 | # Regression test for https://github.com/mgedmin/strace-process-tree/issues/12 156 | log_lines = [ 157 | '230473 02:47:49 futex( ', 158 | '230473 02:47:49 <... futex resumed>) = ? ', 159 | ] 160 | result = list(stp.events(log_lines)) 161 | assert result == [ 162 | stp.Event(230473, 10069.0, 'futex() = ? '), 163 | ] 164 | 165 | 166 | def test_events_special_pid_format(): 167 | log_lines = [ 168 | '[pid 27369] execve("bin/test", ["bin/test", "-pvc", "-t", "allowhosts.txt"], 0x7fffa04e8ba0 /* 71 vars */) = 0', 169 | '[pid 123] execve("bin/test", ["bin/test", "-pvc", "-t", "allowhosts.txt"], 0x7fffa04e8ba0 /* 71 vars */) = 0', 170 | ] 171 | result = list(stp.events(log_lines)) 172 | assert result == [ 173 | (27369, None, 'execve("bin/test", ["bin/test", "-pvc", "-t", "allowhosts.txt"], 0x7fffa04e8ba0 /* 71 vars */) = 0'), 174 | (123, None, 'execve("bin/test", ["bin/test", "-pvc", "-t", "allowhosts.txt"], 0x7fffa04e8ba0 /* 71 vars */) = 0'), 175 | ] 176 | 177 | 178 | def test_events_special_cases_that_cannot_really_happen(): 179 | log_lines = [ 180 | '27369 42', 181 | '27369 arch_prctl(ARCH_SET_FS, 0x7fef1c205140) = 0 ', 182 | '27369 <... no really why am I doin this', 183 | ] 184 | result = list(stp.events(log_lines)) 185 | assert result == [ 186 | (27369, None, '42'), 187 | (27369, None, 'arch_prctl(ARCH_SET_FS, 0x7fef1c205140) = 0 '), 188 | (27369, None, '<... no really why am I doin this'), 189 | ] 190 | 191 | 192 | def test_events_bad_file_format(): 193 | log_lines = [ 194 | 'Hello this is a text file and not an strace log file at all actually.', 195 | ] 196 | with pytest.raises(SystemExit) as ctx: 197 | list(stp.events(log_lines)) 198 | assert 'line 1.' in str(ctx.value) 199 | 200 | 201 | def test_ProcessTree(): 202 | pt = stp.ProcessTree() 203 | pt.handle_exec(42, 'foo', None) 204 | assert str(pt) == '42 foo\n' 205 | 206 | 207 | def test_ProcessTree_simple_child(): 208 | pt = stp.ProcessTree() 209 | pt.handle_exec(42, 'foo', None) 210 | pt.add_child(42, 43, 'bar', None) 211 | pt.add_child(42, 44, 'baz', None) 212 | assert str(pt) == ( 213 | '42 foo\n' 214 | ' ├─43 bar\n' 215 | ' └─44 baz\n' 216 | ) 217 | 218 | 219 | def test_ProcessTree_fork_then_exec(): 220 | pt = stp.ProcessTree() 221 | pt.handle_exec(42, 'foo', None) 222 | pt.add_child(42, 43, 'fork()', None) 223 | pt.handle_exec(43, 'bar', None) 224 | assert str(pt) == ( 225 | '42 foo\n' 226 | ' └─43 bar\n' 227 | ) 228 | 229 | 230 | def test_ProcessTree_exec_then_fork(): 231 | pt = stp.ProcessTree() 232 | pt.handle_exec(42, 'foo', None) 233 | pt.handle_exec(43, 'bar', None) 234 | pt.add_child(42, 43, 'fork()', None) 235 | assert str(pt) == ( 236 | '42 foo\n' 237 | ' └─43 bar\n' 238 | ) 239 | 240 | 241 | def test_ProcessTree_unknown_parent_pid_and_name(): 242 | pt = stp.ProcessTree() 243 | pt.add_child(None, 43, 'bar', None) 244 | pt.add_child(None, 44, 'baz', None) 245 | assert str(pt) == ( 246 | '\n' 247 | ' ├─43 bar\n' 248 | ' └─44 baz\n' 249 | ) 250 | 251 | 252 | def test_ProcessTree_unknown_parent_pid(): 253 | pt = stp.ProcessTree() 254 | pt.handle_exec(None, 'foo', None) 255 | pt.add_child(None, 43, 'bar', None) 256 | pt.add_child(None, 44, 'baz', None) 257 | assert str(pt) == ( 258 | ' foo\n' 259 | ' ├─43 bar\n' 260 | ' └─44 baz\n' 261 | ) 262 | 263 | 264 | def test_ProcessTree_exec_twice(): 265 | pt = stp.ProcessTree() 266 | pt.handle_exec(42, 'foo', None) 267 | pt.add_child(42, 43, 'bar', None) 268 | pt.handle_exec(43, 'qux', None) 269 | assert str(pt) == ( 270 | '42 foo\n' 271 | ' └─43 qux\n' 272 | ) 273 | 274 | 275 | def test_ProcessTree_exec_twice_with_children(): 276 | pt = stp.ProcessTree() 277 | pt.handle_exec(42, 'foo', None) 278 | pt.add_child(42, 43, 'bar', None) 279 | pt.add_child(43, 44, 'baz', None) 280 | pt.handle_exec(43, 'qux', None) 281 | assert str(pt) == ( 282 | '42 foo\n' 283 | ' ├─43 bar\n' 284 | ' │ └─44 baz\n' 285 | ' └─43 qux\n' 286 | ) 287 | 288 | 289 | def test_ProcessTree_start_time_known_exit_time_not_known(): 290 | pt = stp.ProcessTree() 291 | pt.handle_exec(42, 'foo', None) 292 | pt.add_child(42, 43, 'bar', 24) 293 | assert str(pt) == ( 294 | '42 foo\n' 295 | ' └─43 bar [@24.0s]\n' 296 | ) 297 | 298 | 299 | def test_ProcessTree_handle_exit_unknown_pid(): 300 | pt = stp.ProcessTree() 301 | pt.handle_exit(42, 1775.45) 302 | 303 | 304 | def test_simplify_syscall(): 305 | assert stp.simplify_syscall( 306 | 'exit_group(0) ' 307 | ) == ( 308 | 'exit_group(0)' 309 | ) 310 | assert stp.simplify_syscall( 311 | 'clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fbb38e89a10)' 312 | ) == ( 313 | 'clone(CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD)' 314 | ) 315 | assert stp.simplify_syscall( 316 | 'clone(child_stack=0x7fbb3690dfb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fbb3690e9d0, tls=0x7fbb3690e700, child_tidptr=0x7fbb3690e9d0)' 317 | ) == ( 318 | 'clone(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID)' 319 | ) 320 | assert stp.simplify_syscall( 321 | 'clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f1e00bff910, parent_tid=0x7f1e00bff910, exit_signal=0, stack=0x7f1e003ff000, stack_size=0x7feb40, tls=0x7f1e00bff640} => {parent_tid=[1942]}, 88)' 322 | ) == ( 323 | 'clone3(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID)' 324 | ) 325 | 326 | 327 | def test_extract_command_line(): 328 | assert stp.extract_command_line( 329 | 'exit_group(0) ' 330 | ) == ( 331 | 'exit_group(0)' 332 | ) 333 | assert stp.extract_command_line( 334 | 'execve("/usr/bin/foo", ["foo", "bar"], [/* 45 vars */])' 335 | ) == ( 336 | 'foo bar' 337 | ) 338 | assert stp.extract_command_line( 339 | 'execve("/usr/bin/foo", ["short", "env"], [/* 1 var */])' 340 | ) == ( 341 | 'short env' 342 | ) 343 | assert stp.extract_command_line( 344 | 'clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fbb38e89a10)' 345 | ) == ( 346 | '(fork)' 347 | ) 348 | assert stp.extract_command_line( 349 | 'clone(child_stack=0x7fbb3690dfb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fbb3690e9d0, tls=0x7fbb3690e700, child_tidptr=0x7fbb3690e9d0)' 350 | ) == ( 351 | '(thread)' 352 | ) 353 | assert stp.extract_command_line( 354 | 'clone(...)' 355 | ) == ( 356 | '...' 357 | ) 358 | 359 | 360 | def test_parse_argv(): 361 | assert stp.parse_argv('"foo"') == ["foo"] 362 | assert stp.parse_argv('"foo", "bar"') == ["foo", "bar"] 363 | assert stp.parse_argv(r'"foo", "bar"..., "baz\t", "\""') == [ 364 | "foo", "bar...", "baz\t", '"', 365 | ] 366 | assert stp.parse_argv('"foo", "bar", ...') == ["foo", "bar", ...] 367 | 368 | 369 | def test_format_command(): 370 | assert stp.format_command(["foo", "bar"]) == "foo bar" 371 | assert stp.format_command(["foo", "bar baz"]) == 'foo "bar baz"' 372 | assert stp.format_command(["foo", "bar`baz's"]) == r"foo 'bar`baz'\''s'" 373 | assert stp.format_command(["foo", "bar", ...]) == "foo bar ..." 374 | 375 | 376 | def test_pushquote(): 377 | assert stp.pushquote('"--foo=bar"') == '--foo="bar"' 378 | 379 | 380 | def test_parse_stream(): 381 | tree = stp.parse_stream([ 382 | stp.Event(42, 1262372451.579, 'execve("/tmp/test.sh", ["/tmp/test.sh"], 0x7ffc5be66b48 /* 71 vars */) = 0'), 383 | stp.Event(42, 1262372451.975, 'clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fea1237a850) = 43'), 384 | stp.Event(43, 1262372452.001, 'execve("/usr/bin/printf", ["/usr/bin/printf", "hi"], 0x557884c640a8 /* 71 vars */) = 0'), 385 | stp.Event(43, 1262372452.073, 'exit_group(0) = ?'), 386 | stp.Event(43, 1262372452.074, '+++ exited with 0 +++'), 387 | ]) 388 | assert str(tree) == ( 389 | "42 /tmp/test.sh\n" 390 | " └─43 /usr/bin/printf hi [0.1s @0.4s]\n" 391 | ) 392 | 393 | 394 | def test_parse_stream_exec_error(): 395 | tree = stp.parse_stream([ 396 | stp.Event(42, 1262372451.579, 'execve("/tmp/test.sh", ["/tmp/test.sh"], 0x7ffc5be66b48 /* 71 vars */) = 0'), 397 | stp.Event(42, 1262372451.975, 'clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fea1237a850) = 43'), 398 | stp.Event(43, 1262372452.001, 'execve("/usr/bin/printf", ["/usr/bin/printf", "hi"], 0x557884c640a8 /* 71 vars */) = -1 ENOENT (No such file or directory)'), 399 | ]) 400 | assert str(tree) == ( 401 | "42 /tmp/test.sh\n" 402 | " └─43 (fork) [@0.4s]\n" 403 | ) 404 | 405 | 406 | def test_parse_stream_clone_error(): 407 | tree = stp.parse_stream([ 408 | stp.Event(42, 1262372451.579, 'execve("/tmp/test.sh", ["/tmp/test.sh"], 0x7ffc5be66b48 /* 71 vars */) = 0'), 409 | stp.Event(42, 1262372451.975, 'clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fea1237a850) = -1 EPERM (Operation not permitted)'), 410 | ]) 411 | assert str(tree) == ( 412 | "42 /tmp/test.sh\n" 413 | ) 414 | 415 | 416 | def test_open_arg(monkeypatch): 417 | monkeypatch.setattr(sys, 'stdin', StringIO('hello world')) 418 | with stp.open_arg('-') as fp: 419 | assert fp.read() == 'hello world' 420 | 421 | 422 | def test_main_no_args(monkeypatch, capsys): 423 | monkeypatch.setattr(sys, 'argv', ['strace-process-tree']) 424 | with pytest.raises(SystemExit): 425 | stp.main() 426 | output = capsys.readouterr().err 427 | assert output.startswith( 428 | 'usage: strace-process-tree [-h] [--version] [-c] [-C] [-U] [-A] [-v] filename\n' 429 | 'strace-process-tree: error:' 430 | ), output 431 | 432 | 433 | def test_main_help(monkeypatch, capsys): 434 | monkeypatch.setattr(sys, 'argv', ['strace-process-tree', '--help']) 435 | with pytest.raises(SystemExit): 436 | stp.main() 437 | output = capsys.readouterr().out 438 | assert output.startswith( 439 | 'usage: strace-process-tree' 440 | ) 441 | 442 | 443 | def test_main(monkeypatch, tmp_path, capsys): 444 | filename = tmp_path / "example.log" 445 | filename.write_text( 446 | u'29900 execve("/tmp/test.sh", ["/tmp/test.sh"], 0x7ffc5be66b48 /* 71 vars */) = 0\n' 447 | u'29900 arch_prctl(ARCH_SET_FS, 0x7fea1237a580) = 0\n' 448 | u'29900 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fea1237a850) = 29901\n' 449 | u'29900 wait4(-1, \n' 450 | u'29901 execve("/usr/bin/printf", ["/usr/bin/printf", "hi\\\\n"], 0x557884c640a8 /* 71 vars */) = 0\n' 451 | u'29901 arch_prctl(ARCH_SET_FS, 0x7f52d9e64580) = 0\n' 452 | u'29901 exit_group(0) = ?\n' 453 | u'29901 +++ exited with 0 +++\n' 454 | u'29900 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29901\n' 455 | u'29900 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=29901, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---\n' 456 | u'29900 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fea1237a850) = 29902\n' 457 | u'29900 wait4(-1, \n' 458 | u'29902 execve("/tmp/child.sh", ["/tmp/child.sh"], 0x557884c640a8 /* 71 vars */) = 0\n' 459 | u'29902 arch_prctl(ARCH_SET_FS, 0x7f3125dd8580) = 0\n' 460 | u'29902 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3125dd8850) = 29903\n' 461 | u'29902 wait4(-1, \n' 462 | u'29903 execve("/usr/bin/printf", ["/usr/bin/printf", "one\\\\n"], 0x560fc7c870a8 /* 71 vars */) = 0\n' 463 | u'29903 arch_prctl(ARCH_SET_FS, 0x7f1cc7344580) = 0\n' 464 | u'29903 exit_group(0) = ?\n' 465 | u'29903 +++ exited with 0 +++\n' 466 | u'29902 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29903\n' 467 | u'29902 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=29903, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---\n' 468 | u'29902 execve("/tmp/another.sh", ["/tmp/another.sh"], 0x560fc7c870d8 /* 71 vars */) = 0\n' 469 | u'29902 arch_prctl(ARCH_SET_FS, 0x7fb887202580) = 0\n' 470 | u'29902 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb887202850) = 29904\n' 471 | u'29902 wait4(-1, \n' 472 | u'29904 execve("/bin/true", ["/bin/true"], 0x563a7aa1d0a8 /* 71 vars */) = 0\n' 473 | u'29904 arch_prctl(ARCH_SET_FS, 0x7f2242adc580) = 0\n' 474 | u'29904 exit_group(0) = ?\n' 475 | u'29904 +++ exited with 0 +++\n' 476 | u'29902 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29904\n' 477 | u'29902 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=29904, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---\n' 478 | u'29902 exit_group(0) = ?\n' 479 | u'29902 +++ exited with 0 +++\n' 480 | u'29900 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29902\n' 481 | u'29900 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=29902, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---\n' 482 | u'29900 exit_group(0) = ?\n' 483 | u'29900 +++ exited with 0 +++\n' 484 | ) 485 | monkeypatch.setattr(sys, 'argv', ['strace-process-tree', str(filename)]) 486 | stp.main() 487 | output = capsys.readouterr().out 488 | assert output == ( 489 | u"29900 /tmp/test.sh\n" 490 | u" ├─29901 /usr/bin/printf 'hi\\n'\n" 491 | u" ├─29902 /tmp/child.sh\n" 492 | u" │ └─29903 /usr/bin/printf 'one\\n'\n" 493 | u" └─29902 /tmp/another.sh\n" 494 | u" └─29904 /bin/true\n" 495 | ) 496 | 497 | 498 | def test_main_force_color(monkeypatch, tmp_path, capsys): 499 | filename = tmp_path / "example.log" 500 | filename.write_text( 501 | u'29900 execve("/tmp/test.sh", ["/tmp/test.sh"], 0x7ffc5be66b48 /* 71 vars */) = 0\n' 502 | ) 503 | monkeypatch.setattr(sys, 'argv', ['strace-process-tree', '-c', str(filename)]) 504 | stp.main() 505 | output = capsys.readouterr().out 506 | assert output == ( 507 | u"\033[31m29900\033[m \033[32m/tmp/test.sh\033[m\n" 508 | ) 509 | 510 | 511 | def test_main_force_no_color(monkeypatch, tmp_path, capsys): 512 | filename = tmp_path / "example.log" 513 | filename.write_text( 514 | u'29900 execve("/tmp/test.sh", ["/tmp/test.sh"], 0x7ffc5be66b48 /* 71 vars */) = 0\n' 515 | ) 516 | monkeypatch.setattr(sys, 'argv', ['strace-process-tree', '--no-color', str(filename)]) 517 | monkeypatch.setattr(stp.Theme, 'should_use_color', lambda: True) 518 | stp.main() 519 | output = capsys.readouterr().out 520 | assert output == ( 521 | u"29900 /tmp/test.sh\n" 522 | ) 523 | --------------------------------------------------------------------------------