├── .github └── workflows │ ├── codecov.yml │ ├── python-publish.yml │ └── pythons-tests.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── pgtree.plugin.bash ├── pgtree.plugin.ksh ├── pgtree.plugin.zsh ├── pgtree ├── __init__.py ├── _version.py ├── pgtree └── pgtree.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests └── test_pgtree.py /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov pgtree 2 | on: [push, pull_request] 3 | jobs: 4 | run: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v3 9 | - name: Set up Python 3.10 10 | uses: actions/setup-python@v4 11 | with: 12 | python-version: '3.10' 13 | - name: Install dependencies 14 | run: pip install -r requirements.txt 15 | - name: Run tests and collect coverage 16 | run: pytest --cov 17 | - name: Upload coverage to Codecov 18 | env: 19 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 20 | uses: codecov/codecov-action@v3 21 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: __token__ 28 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.github/workflows/pythons-tests.yml: -------------------------------------------------------------------------------- 1 | name: pythons tests 2 | on: [push, pull_request] 3 | jobs: 4 | run: 5 | runs-on: ubuntu-20.04 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | python-version: ["3.6", "3.9", "3.10", "3.11"] 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python ${{ matrix.python-version }} 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | - name: Install dependencies 17 | run: pip install pytest 18 | - name: Run tests and collect coverage 19 | run: pytest 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eggs 2 | build 3 | dist 4 | pgtree.egg-info 5 | __pycache__ 6 | *.pyc 7 | .coverage 8 | coverage.xml 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # .travis.yml 2 | 3 | language: python 4 | python: 5 | - "2.6" 6 | - "2.7" 7 | - "3.6" 8 | cache: pip 9 | #dist: xenial 10 | dist: trusty 11 | install: 12 | # - pip install lint codecov incremental 13 | script: 14 | - python pgtree/pgtree.py 15 | - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then exit 0; fi 16 | - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then exit 0; fi 17 | - pip install lint coverage pytest pytest-cov incremental 18 | - pylint pgtree/pgtree.py ; echo done 19 | - python -m unittest discover -s tests/ 20 | - pytest --cov 21 | - curl -Os https://uploader.codecov.io/latest/linux/codecov 22 | - chmod +x codecov 23 | after_success: 24 | - bash <(curl -s https://codecov.io/bash) 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | joknarf@free.fr. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 joknarf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | nose = "*" 10 | pylint = "*" 11 | 12 | [requires] 13 | python_version = "3.6" 14 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "316a6a3aa71cfc3733726fee35a6c01a7a1c113c2e5b277632c95995a146806b" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "astroid": { 20 | "hashes": [ 21 | "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", 22 | "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" 23 | ], 24 | "version": "==2.4.2" 25 | }, 26 | "isort": { 27 | "hashes": [ 28 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 29 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 30 | ], 31 | "version": "==4.3.21" 32 | }, 33 | "lazy-object-proxy": { 34 | "hashes": [ 35 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 36 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 37 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 38 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 39 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 40 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 41 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 42 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 43 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 44 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 45 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 46 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 47 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 48 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 49 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 50 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 51 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 52 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 53 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 54 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 55 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 56 | ], 57 | "version": "==1.4.3" 58 | }, 59 | "mccabe": { 60 | "hashes": [ 61 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 62 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 63 | ], 64 | "version": "==0.6.1" 65 | }, 66 | "nose": { 67 | "hashes": [ 68 | "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", 69 | "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", 70 | "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" 71 | ], 72 | "index": "pypi", 73 | "version": "==1.3.7" 74 | }, 75 | "pylint": { 76 | "hashes": [ 77 | "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", 78 | "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" 79 | ], 80 | "index": "pypi", 81 | "version": "==2.5.3" 82 | }, 83 | "six": { 84 | "hashes": [ 85 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 86 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 87 | ], 88 | "version": "==1.15.0" 89 | }, 90 | "toml": { 91 | "hashes": [ 92 | "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", 93 | "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" 94 | ], 95 | "version": "==0.10.1" 96 | }, 97 | "typed-ast": { 98 | "hashes": [ 99 | "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", 100 | "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", 101 | "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", 102 | "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", 103 | "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", 104 | "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", 105 | "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", 106 | "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", 107 | "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", 108 | "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", 109 | "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", 110 | "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", 111 | "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", 112 | "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", 113 | "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", 114 | "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", 115 | "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", 116 | "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", 117 | "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", 118 | "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", 119 | "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" 120 | ], 121 | "markers": "implementation_name == 'cpython' and python_version < '3.8'", 122 | "version": "==1.4.1" 123 | }, 124 | "wrapt": { 125 | "hashes": [ 126 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 127 | ], 128 | "version": "==1.12.1" 129 | } 130 | }, 131 | "develop": {} 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Codecov](https://codecov.io/github/joknarf/pgtree/coverage.svg?branch=master)](https://codecov.io/gh/joknarf/pgtree) 2 | [![Upload Python Package](https://github.com/joknarf/pgtree/workflows/Upload%20Python%20Package/badge.svg)](https://github.com/joknarf/pgtree/actions?query=workflow%3A%22Upload+Python+Package%22) 3 | [![Pypi version](https://img.shields.io/pypi/v/pgtree.svg)](https://pypi.org/project/pgtree/) 4 | [![Downloads](https://pepy.tech/badge/pgtree)](https://pepy.tech/project/pgtree) 5 | [![Python versions](https://img.shields.io/badge/python-2.3+%20|%203.x-blue.svg)](https://shields.io/) 6 | [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/) 7 | 8 | 9 | # pgtree 10 | Unix process hierachy tree display for specific processes (kind of mixed pgrep + pstree) 11 | 12 | pgtree is also able to send signal to found processes and all their children 13 | 14 | The purpose is to have the tool working out of the box on any Unix box, using the default OS python installed, without installing anything else. 15 | The code must be compatible with python 2.x + 3.x 16 | 17 | Should work on any Unix that can execute : 18 | ``` 19 | # /usr/bin/pgrep 20 | # /usr/bin/ps ax -o pid,ppid,stime,user,ucomm,args 21 | ``` 22 | 23 | if `pgrep` command not available (AIX), pgtree uses built-in pgrep (`-f -i -x -u ` supported). 24 | 25 | `-T` option to display threads only works if `ps ax -T -o spid,ppid` available on system (ubuntu/redhat...) 26 | 27 | _pgtree Tested on various versions of RedHat / CentOS / Ubuntu / Debian / Suse / FreeBSD / ArchLinux / MacOS / Solaris / AIX including old versions_ 28 | 29 | _(uses -o fname on Solaris)_ 30 | 31 | ## Installation 32 | FYI, the `pgtree/pgtree.py` is standalone and can be directly copied/used anywhere without any installation. 33 | 34 | installation using pip: 35 | ``` 36 | # pip install pgtree 37 | ``` 38 | 39 | ## Usage 40 | ``` 41 | # pgtree -h 42 | usage: pgtree.py [-W] [-RIya] [-C ] [-O ] [-c|-k|-K] [-1|-p ,...|] 43 | 44 | -I : use -o uid instead of -o user for ps command 45 | (if uid/user mapping is broken ps command can be stuck) 46 | -c : display processes and children only 47 | -k : kill -TERM processes and children 48 | -K : kill -KILL processes and children 49 | -y : do not ask for confirmation to kill 50 | -R : force use of internal pgrep 51 | -C : color preference : y/yes/always or n/no/never (default auto) 52 | -w : tty wrap text : y/yes or n/no (default y) 53 | -W : watch and follow process tree every 2s 54 | -a : use ascii characters 55 | -T : display threads (ps -T) 56 | -O [,psfield,...] : display multiple instead of 'stime' in output 57 | must be valid with ps -o command 58 | 59 | by default display full process hierarchy (parents + children of selected processes) 60 | 61 | -p : select processes pids to display hierarchy (default 0) 62 | -1 : display hierachy children of pid 1 (not including pid 0) 63 | : use pgrep to select processes (see pgrep -h) 64 | 65 | found pids are prefixed with ▶ 66 | ``` 67 | ## Examples 68 | show all parents and children of processes matching `bash` 69 | 70 | # pgtree bash 71 | 72 | show processes matching `bash` and their children 73 | 74 | # pgtree -c bash 75 | 76 | kill all `sh` processes of user joknarf and their children 77 | 78 | #pgtree -k -u joknarf -x sh 79 | 80 | Customize ps output fields: 81 | 82 | image 83 | 84 | Put default options in PGTREE env variable: 85 | ``` 86 | # export PGTREE='-1 -O %cpu,stime -C y' 87 | # pgtree 88 | ``` 89 | 90 | Use watch utility to follow process tree: 91 | ``` 92 | # pgtree -W bash 93 | ``` 94 | ![image](https://user-images.githubusercontent.com/10117818/215317322-7df4559c-ccf4-41f6-b008-55d1fc8f0bb7.png) 95 | 96 | ## Demo 97 | 98 | output 99 | 100 | -------------------------------------------------------------------------------- /pgtree.plugin.bash: -------------------------------------------------------------------------------- 1 | alias pgtree="$(cd "${BASH_SOURCE%/*}";pwd)/pgtree/pgtree" -------------------------------------------------------------------------------- /pgtree.plugin.ksh: -------------------------------------------------------------------------------- 1 | alias pgtree="$(cd "${.sh.file%/*}";pwd)/pgtree/pgtree" 2 | -------------------------------------------------------------------------------- /pgtree.plugin.zsh: -------------------------------------------------------------------------------- 1 | alias pgtree="$(cd "${0%/*}";pwd)/pgtree/pgtree" 2 | -------------------------------------------------------------------------------- /pgtree/__init__.py: -------------------------------------------------------------------------------- 1 | # pgtree package 2 | __author__='Franck Jouvanceau' 3 | 4 | from .pgtree import Proctree, Treedisplay, runcmd, main -------------------------------------------------------------------------------- /pgtree/_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides pgtree version information. 3 | """ 4 | 5 | # This file is auto-generated! Do not edit! 6 | # Use `python -m incremental.update pgtree` to change this file. 7 | 8 | from incremental import Version 9 | 10 | __version__ = Version("pgtree", 1, 1, 1) 11 | __all__ = ["__version__"] 12 | -------------------------------------------------------------------------------- /pgtree/pgtree: -------------------------------------------------------------------------------- 1 | pgtree.py -------------------------------------------------------------------------------- /pgtree/pgtree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # coding: utf-8 3 | # pylint: disable=C0114,C0413,R0902,C0209 4 | # determine available python executable 5 | # determine ps -o options 6 | _='''' 7 | #[ "$1" = -W ] && shift && exec watch -x -c -- "$0" -C y "$@" 8 | export LANG=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf8 9 | PGT_PGREP=$(type -p pgrep) 10 | ps -p $$ -o ucomm >/dev/null 2>&1 && PGT_COMM=ucomm 11 | [ ! "$PGT_COMM" ] && ps -p $$ -o comm >/dev/null 2>&1 && PGT_COMM=comm 12 | [ "$PGT_COMM" ] && { 13 | ps -p $$ -o stime >/dev/null 2>&1 && PGT_STIME=stime 14 | [ ! "$PGT_STIME" ] && ps -p $$ -o start >/dev/null 2>&1 && PGT_STIME=start 15 | [ ! "$PGT_STIME" ] && PGT_STIME=time 16 | } 17 | # busybox no -p option 18 | [ ! "$PGT_COMM" ] && ! ps -p $$ >/dev/null 2>&1 && PGT_COMM=comm && PGT_STIME=time 19 | export PGT_COMM PGT_STIME PGT_PGREP 20 | python=$(type -p python || type -p python3 || type -p python2) 21 | [ "$python" ] && exec $python "$0" "$@" 22 | echo "ERROR: cannot find python interpreter" >&2 23 | exit 1 24 | ''' 25 | 26 | """ 27 | Program for showing the hierarchy of specific processes on a Unix computer. 28 | Like pstree but with searching for specific processes with pgrep first and display 29 | hierarchy of matching processes (parents and children) 30 | should work on any Unix supporting commands : 31 | # pgrep 32 | # ps ax -o pid,ppid,comm,args 33 | (RedHat/CentOS/Fedora/Ubuntu/Suse/Solaris...) 34 | Compatible python 2 / 3 35 | 36 | Example: 37 | $ ./pgtree.py sshd 38 | 1 (root) [init] /init 39 | └─6 (root) [init] /init 40 | └─144 (root) [systemd] /lib/systemd/systemd --system-unit=basic.target 41 | ► └─483 (root) [sshd] /usr/sbin/sshd -D 42 | ► └─1066 (root) [sshd] sshd: joknarf [priv] 43 | ► └─1181 (joknarf) [sshd] sshd: joknarf@pts/1 44 | └─1182 (joknarf) [bash] -bash 45 | ├─1905 (joknarf) [sleep] sleep 60 46 | └─1906 (joknarf) [top] top 47 | 48 | Reminder for compatibility with old python versions: 49 | no dict comprehension or dict simili comprehension 50 | no f string or string .format() 51 | no inline if 52 | """ 53 | 54 | __author__ = "Franck Jouvanceau" 55 | __copyright__ = "Copyright 2020, Franck Jouvanceau" 56 | __license__ = "MIT" 57 | 58 | import sys 59 | import os 60 | import platform 61 | import getopt 62 | import re 63 | try: 64 | import time 65 | except ImportError: 66 | pass 67 | 68 | # impossible detection using ps for AIX/MacOS 69 | # stime is not start time of process 70 | system = platform.system() 71 | PS_OPTION = 'ax' 72 | if system in ['AIX', 'Darwin']: 73 | os.environ['PGT_STIME'] = 'start' 74 | elif system == 'SunOS': # ps ax -o not supported 75 | PS_OPTION = '-e' 76 | os.environ['PGT_COMM'] = 'fname' # comm header width not respected 77 | 78 | def runcmd(cmd): 79 | """run command""" 80 | pipe = os.popen(cmd, 'r') 81 | std_out = pipe.read() 82 | res = pipe.close() 83 | return res, std_out.rstrip('\n') 84 | 85 | def ask(prompt): 86 | """input text""" 87 | try: 88 | answer = raw_input(prompt) 89 | except NameError: 90 | answer = input(prompt) 91 | return answer 92 | 93 | # pylint: disable=R0903 94 | class Treedisplay: 95 | """Tree display attributes""" 96 | #COLOR_FG = "\x1b[38;5;" # 256 colors 97 | COLOR_FG = "\x1b[01;" # 16 colors more compatible 98 | COLOR_RESET = "\x1b[0m" 99 | 100 | def __init__(self, use_ascii=False, use_color=False): 101 | """choose tree characters""" 102 | if use_ascii: 103 | self.selected = '>' 104 | self.child = '|_' 105 | self.notchild = '| ' 106 | self.lastchild = '\\_' 107 | else: 108 | self.selected = '►' # ⇒ 🠖 🡆 ➤ ➥ ► ▶ 109 | self.child = '├─' 110 | self.notchild = '│ ' 111 | self.lastchild = '└─' 112 | self.use_color = use_color 113 | self.colors = { 114 | 'pid': '34', # 12 115 | 'user': '33', # 3 116 | 'comm': '32', # 2 117 | '%cpu': '31', 118 | 'vsz': '35', 119 | '%mem': '35', 120 | 'time': '35', 121 | 'default': '36', # 8 122 | } 123 | 124 | def colorize(self, field, value): 125 | """colorize fields""" 126 | if not self.use_color: 127 | return value 128 | if field in self.colors: 129 | return self.COLOR_FG + self.colors[field] + "m" + value + self.COLOR_RESET 130 | return self.colorize('default', value) 131 | 132 | 133 | class Proctree: 134 | """ 135 | Manage process tree of pids 136 | Proctree([ 'pid1', 'pid2' ]) 137 | """ 138 | 139 | # pylint: disable=R0913 140 | def __init__(self, use_uid=False, use_ascii=False, use_color=False, 141 | pid_zero=True, opt_fields=None, threads=False): 142 | """constructor""" 143 | self.pids = [] 144 | self.ps_info = {} # ps command info stored 145 | self.children = {} # children of pid 146 | self.selected_pids = [] # pids and their children 147 | self.pids_tree = {} 148 | self.top_parents = [] 149 | self.treedisp = Treedisplay(use_ascii, use_color) 150 | self.ps_fields = self.get_fields(opt_fields, use_uid, threads) 151 | self.get_psinfo(pid_zero) 152 | 153 | def get_fields(self, opt_fields=None, use_uid=False, threads=False): 154 | """ Get ps fields from OS / optionnal fields """ 155 | if use_uid: 156 | user = 'uid' 157 | else: 158 | user = 'user' 159 | if threads: 160 | pid = 'spid' 161 | else: 162 | pid = 'pid' 163 | if not opt_fields or not os.environ.get('PGT_COMM'): 164 | opt_fields = [os.environ.get('PGT_STIME') or 'stime'] 165 | 166 | return [pid, 'ppid', user, os.environ.get('PGT_COMM') or 'ucomm'] + opt_fields 167 | 168 | def run_ps(self, widths): 169 | """ 170 | run ps command detected setting columns widths 171 | guess columns for ps command not supporting -o (mingw/msys2) 172 | """ 173 | if os.environ.get('PGT_COMM'): 174 | ps_cmd = 'ps ' + PS_OPTION + ' ' + ' '.join( 175 | ['-o '+ o +'='+ widths[i]*'-' for i,o in enumerate(self.ps_fields)] 176 | ) + ' -o args' 177 | err, ps_out = runcmd(ps_cmd) 178 | if err: 179 | print('Error: executing ps ' + PS_OPTION + ' -o ' + ",".join(self.ps_fields)) 180 | sys.exit(1) 181 | return ps_out.splitlines() 182 | _, out = runcmd('ps aux') # try to use header to guess columns 183 | out = out.splitlines() 184 | if not 'PPID' in out[0]: 185 | _, out = runcmd('ps -ef') 186 | out = out.splitlines() 187 | ps_out = [] 188 | fields = {} 189 | for i,field in enumerate(out[0].strip().lower().split()): 190 | field = re.sub("command|cmd", "args", field) 191 | field = re.sub("uid", "user", field) 192 | fields[field] = i 193 | if not 'ppid' in fields: 194 | print("Error: command 'ps aux' does not provides PPID") 195 | sys.exit(1) 196 | fields["ucomm"] = len(fields) 197 | for line in out: 198 | ps_info = line.strip().split(None, len(fields)-2) 199 | if "stime" in fields: 200 | if ps_info[fields["stime"]] in ["Jan","Feb","Mar","Apr","May","Jun", 201 | "Jui","Aug","Sep","Oct","Nov","Dec"]: 202 | ps_info = line.strip().split(None, len(fields)-1) 203 | ps_info[fields["stime"]] += ps_info.pop(fields["stime"]+1) 204 | ps_info.append(os.path.basename(ps_info[fields["args"]].split()[0])) 205 | ps_out.append(' '.join( 206 | [('%-'+ str(widths[i]) +'s') % ps_info[fields[opt]] 207 | for i,opt in enumerate(self.ps_fields)] + [ps_info[fields["args"]]] 208 | )) 209 | return ps_out 210 | 211 | def get_psinfo(self, pid_zero): 212 | """parse unix ps command""" 213 | widths = [30, 30, 30, 130] + [50 for i in self.ps_fields[4:]] 214 | ps_out = self.run_ps(widths) 215 | pid_z = ["0", "0"] + self.ps_fields[2:] 216 | ps_out[0] = ' '.join( 217 | [('%-'+ str(widths[i]) +'s') % opt for i,opt in enumerate(pid_z)] + ['args'] 218 | ) 219 | ps_opts = ['pid', 'ppid', 'user', 'comm'] + self.ps_fields[4:] 220 | for line in ps_out: 221 | infos = {} 222 | col = 0 223 | for i,field in enumerate(ps_opts): 224 | infos[field] = line[col:col+widths[i]].strip() 225 | col = col + widths[i] + 1 226 | infos['args'] = line[col:len(line)] 227 | infos['comm'] = os.path.basename(infos['comm']) 228 | pid = infos['pid'] 229 | ppid = infos['ppid'] 230 | if pid == str(os.getpid()): 231 | continue 232 | if ppid == pid: 233 | ppid = '-1' 234 | infos['ppid'] = '-1' 235 | if ppid not in self.children: 236 | self.children[ppid] = [] 237 | self.children[ppid].append(pid) 238 | self.ps_info[pid] = infos 239 | if not self.ps_info.get('1'): 240 | self.ps_info['1'] = self.ps_info['0'] 241 | if not pid_zero: 242 | del self.ps_info['0'] 243 | del self.children['0'] 244 | 245 | def pgrep(self, argv): 246 | """mini built-in pgrep if pgrep command not available 247 | [-f] [-x] [-i] [-u ] [pattern]""" 248 | if "PGT_PGREP" not in os.environ or os.environ["PGT_PGREP"]: 249 | _, pgrep = runcmd('pgrep ' +' '.join(argv)) 250 | return pgrep.split("\n") 251 | 252 | try: 253 | opts, args = getopt.getopt(argv, "ifxu:") 254 | except getopt.GetoptError: 255 | print("bad pgrep parameters") 256 | sys.exit(2) 257 | psfield = "comm" 258 | flag = 0 259 | exact = False 260 | user = ".*" 261 | pattern = ".*" 262 | for opt, arg in opts: 263 | if opt == "-f": 264 | psfield = "args" 265 | elif opt == "-i": 266 | flag = re.IGNORECASE 267 | elif opt == "-x": 268 | exact = True 269 | elif opt == "-u": 270 | user = arg 271 | 272 | if args: 273 | pattern = args[0] 274 | if exact: 275 | pattern = "^" + pattern + "$" 276 | pids = [] 277 | for pid,info in self.ps_info.items(): 278 | if pid == '0': 279 | continue 280 | if re.search(pattern, info[psfield], flag) and \ 281 | re.match(user, info["user"]): 282 | pids.append(pid) 283 | return pids 284 | 285 | 286 | def get_parents(self): 287 | """get parents list of pids""" 288 | last_ppid = None 289 | for pid in self.pids: 290 | if pid not in self.ps_info: 291 | continue 292 | while pid in self.ps_info: 293 | ppid = self.ps_info[pid]['ppid'] 294 | if ppid not in self.pids_tree: 295 | self.pids_tree[ppid] = [] 296 | if pid not in self.pids_tree[ppid]: 297 | self.pids_tree[ppid].append(pid) 298 | last_ppid = pid 299 | pid = ppid 300 | if last_ppid not in self.top_parents: 301 | self.top_parents.append(last_ppid) 302 | 303 | # recursive 304 | def children2tree(self, pids): 305 | """build children tree""" 306 | for pid in pids: 307 | if pid in self.pids_tree: 308 | continue 309 | if pid in self.children: 310 | self.pids_tree[pid] = self.children[pid] 311 | self.children2tree(self.children[pid]) 312 | 313 | def build_tree(self): 314 | """build process tree""" 315 | self.children2tree(self.pids) 316 | self.get_parents() 317 | 318 | def print_proc(self, pid, pre, print_it, last): 319 | """display process information with indent/tree/colors""" 320 | next_p = '' 321 | ppre = pre 322 | if pid in self.pids: 323 | print_it = True 324 | ppre = self.treedisp.selected + pre[1:] 325 | if print_it: 326 | self.selected_pids.insert(0, pid) 327 | if pre == ' ': # head of hierarchy 328 | curr_p = next_p = ' ' 329 | elif last: # last child 330 | curr_p = self.treedisp.lastchild 331 | next_p = ' ' 332 | else: # not last child 333 | curr_p = self.treedisp.child 334 | next_p = self.treedisp.notchild 335 | ps_info = self.treedisp.colorize('pid', pid.ljust(5)) + \ 336 | self.treedisp.colorize('user', ' (' + self.ps_info[pid]['user'] + ') ') + \ 337 | self.treedisp.colorize('comm', '[' + self.ps_info[pid]['comm'] + '] ') 338 | ps_info += ' '.join( 339 | [self.treedisp.colorize(f, self.ps_info[pid][f]) for f in self.ps_fields[4:]] 340 | ) 341 | ps_info += ' ' + self.ps_info[pid]['args'] 342 | output = ppre + curr_p + ps_info 343 | print(output) 344 | return (next_p, print_it) 345 | 346 | # recursive 347 | def _print_tree(self, pids, print_it=True, pre=' '): 348 | """display wonderful process tree""" 349 | for idx, pid in enumerate(pids): 350 | (next_p, print_children) = self.print_proc(pid, pre, print_it, idx == len(pids)-1) 351 | if pid in self.pids_tree: 352 | self._print_tree(self.pids_tree[pid], print_children, pre+next_p) 353 | 354 | def print_tree(self, pids=None, child_only=False, sig=0, confirmed=False): 355 | """display full or children only process tree""" 356 | if pids: 357 | self.pids = pids 358 | else: 359 | if '0' in self.children: 360 | self.pids = ['0'] 361 | else: 362 | self.pids = ['1'] 363 | self.build_tree() 364 | if sig: 365 | self.kill_with_children(sig=sig, confirmed=confirmed) 366 | else: 367 | self._print_tree(self.top_parents, not child_only) 368 | 369 | def kill_with_children(self, sig=15, confirmed=False): 370 | """kill processes and children with signal""" 371 | self._print_tree(self.top_parents, False) 372 | if not self.selected_pids: 373 | return 374 | print("kill "+" ".join(self.selected_pids)) 375 | if not confirmed: 376 | answer = ask('Confirm (y/[n]) ? ') 377 | if answer != 'y': 378 | return 379 | for pid in self.selected_pids: 380 | try: 381 | os.kill(int(pid), sig) 382 | except ProcessLookupError: 383 | continue 384 | except PermissionError: 385 | print('kill ' + pid + ': Permission error') 386 | continue 387 | 388 | def colored(opt): 389 | """colored output or not""" 390 | if opt in ('y', 'yes', 'always'): 391 | opt = True 392 | elif opt in ('n', 'no', 'never'): 393 | opt = False 394 | elif opt == 'auto': 395 | opt = sys.stdout.isatty() 396 | return opt 397 | 398 | def wrap_text(opt): 399 | """wrap/nowrap text on tty (default wrap with tty)""" 400 | if opt in ('y', 'yes'): 401 | opt = True 402 | elif opt in ('n', 'no'): 403 | opt = False 404 | 405 | if sys.stdout.isatty() and opt: 406 | sys.stdout.write("\x1b[?7l") # rmam 407 | after = "\x1b[?7h" # smam 408 | else: 409 | after = '' 410 | return after 411 | 412 | def pgtree(options, psfields, pgrep_args): 413 | """ Display process tree from options """ 414 | ptree = Proctree(use_uid='-I' in options, 415 | use_ascii='-a' in options, 416 | use_color=colored(options['-C']), 417 | pid_zero='-1' not in options, 418 | opt_fields=psfields, 419 | threads='-T' in options) 420 | 421 | found = None 422 | if '-p' in options: 423 | found = options['-p'].split(',') 424 | elif pgrep_args: 425 | found = ptree.pgrep(pgrep_args) 426 | return (ptree, found) 427 | 428 | def watch_pgtree(options, psfields, pgrep_args, sig): 429 | """ follow process hierarchy """ 430 | while True: 431 | try: 432 | (ptree, found) = pgtree(options, psfields, pgrep_args) 433 | cur_time = time.strftime("%c", time.localtime()) 434 | sys.stdout.write("\033c") 435 | wrap_text(options['-w']) 436 | sys.stdout.write("Every 2.0s: " + ' '.join(sys.argv) + " " + cur_time + "\n\n") 437 | ptree.print_tree(pids=found, child_only='-c' in options, sig=sig, 438 | confirmed='-y' in options) 439 | if time.sleep(2): 440 | break 441 | except KeyboardInterrupt: 442 | break 443 | 444 | 445 | def main(argv): 446 | """pgtree command line""" 447 | global PS_OPTION 448 | usage = """ 449 | usage: pgtree.py [-W] [-RIya] [-C ] [-O ] [-c|-k|-K] [-1|-p ,...|] 450 | 451 | -I : use -o uid instead of -o user for ps command 452 | (if uid/user mapping is broken ps command can be stuck) 453 | -c : display processes and children only 454 | -k : kill -TERM processes and children 455 | -K : kill -KILL processes and children 456 | -y : do not ask for confirmation to kill 457 | -R : force use of internal pgrep 458 | -C : color preference : y/yes/always or n/no/never (default auto) 459 | -w : tty wrap text : y/yes or n/no (default y) 460 | -W : watch and follow process tree every 2s 461 | -a : use ascii characters 462 | -T : display threads (ps -T) 463 | -O [,psfield,...] : display multiple instead of 'stime' in output 464 | must be valid with ps -o command 465 | 466 | by default display full process hierarchy (parents + children of selected processes) 467 | 468 | -p : select processes pids to display hierarchy (default 0) 469 | -1 : display hierachy children of pid 1 (not including pid 0) 470 | : use pgrep to select processes (see pgrep -h) 471 | 472 | found pids are prefixed with ► 473 | """ 474 | 475 | # allow options after pattern : pgtree mysearch -fc 476 | if len(argv) > 1 and argv[0][0] != '-': 477 | argv.append(argv.pop(0)) 478 | if 'PGTREE' in os.environ: 479 | argv = os.environ["PGTREE"].split(' ') + argv 480 | try: 481 | opts, args = getopt.getopt(argv, 482 | "W1IRckKfxvinoyaTp:u:U:g:G:P:s:t:F:O:C:w:", 483 | ["ns=", "nslist="]) 484 | except getopt.GetoptError: 485 | print(usage) 486 | sys.exit(2) 487 | 488 | sig = 0 489 | pgrep_args = [] 490 | options = {} 491 | psfields = None 492 | options['-C'] = 'auto' 493 | options['-w'] = 'yes' 494 | for opt, arg in opts: 495 | options[opt] = arg 496 | if opt == "-k": 497 | sig = 15 498 | elif opt == "-K": 499 | sig = 9 500 | elif opt == "-O": 501 | psfields = arg.split(',') 502 | elif opt == "-R": 503 | os.environ["PGT_PGREP"] = "" 504 | elif opt == "-T": 505 | PS_OPTION += " -T" 506 | elif opt in ("-f", "-x", "-v", "-i", "-n", "-o"): 507 | pgrep_args.append(opt) 508 | elif opt in ("-u", "-U", "-g", "-G", "-P", "-s", "-t", "-F", "--ns", "--nslist"): 509 | pgrep_args += [opt, arg] 510 | pgrep_args += args 511 | after = wrap_text(options['-w']) 512 | if '-W' in options: 513 | watch_pgtree(options, psfields, pgrep_args, sig) 514 | else: 515 | (ptree, found) = pgtree(options, psfields, pgrep_args) 516 | ptree.print_tree(pids=found, child_only='-c' in options, sig=sig, 517 | confirmed='-y' in options) 518 | sys.stdout.write(after) 519 | 520 | 521 | if __name__ == '__main__': 522 | main(sys.argv[1:]) 523 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==22.2.0 2 | coverage==7.2.2 3 | iniconfig==2.0.0 4 | packaging==23.0 5 | pluggy==1.0.0 6 | py==1.11.0 7 | pyparsing==3.0.9 8 | pytest==7.2.2 9 | pytest-cov==4.0.0 10 | tomli==2.0.1 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Install pgtree package""" 3 | from setuptools import setup 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setup( 9 | name="pgtree", 10 | author="joknarf", 11 | author_email="joknarf@gmail.com", 12 | description="Unix process tree search", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/joknarf/pgtree", 16 | packages=["pgtree"], 17 | scripts=["pgtree/pgtree"], 18 | use_incremental=True, 19 | setup_requires=['incremental'], 20 | install_requires=[], 21 | classifiers=[ 22 | "Programming Language :: Python :: 3", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: POSIX", 25 | "Topic :: Utilities", 26 | ], 27 | keywords="shell pstree pgrep process tree", 28 | license="MIT", 29 | ) 30 | -------------------------------------------------------------------------------- /tests/test_pgtree.py: -------------------------------------------------------------------------------- 1 | """pgtree tests""" 2 | import os 3 | import sys 4 | import unittest 5 | from unittest.mock import patch 6 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 7 | import pgtree 8 | #from unittest.mock import MagicMock, Mock, patch 9 | os.environ['PGT_COMM'] = 'ucomm' 10 | os.environ['PGT_STIME'] = 'stime' 11 | 12 | class ProctreeTest(unittest.TestCase): 13 | """tests for pgtree""" 14 | @patch('os.kill') 15 | @patch('pgtree.pgtree.runcmd') 16 | def test_tree1(self, mock_runcmd, mock_kill): 17 | """test""" 18 | ps_out = 4*(130*'-'+' ') 19 | print("tree: =======") 20 | ps_out = 4*(130*'-'+' ') + "\n" 21 | ps_out += f'{"1":>30} {"0":>30} {"root":<30} {"init":<130} {"Aug12":<50} /init\n' 22 | ps_out += f'{"10":>30} {"1":>30} {"joknarf":<30} {"bash":<130} {"Aug12":<50} -bash\n' 23 | ps_out += f'{"20":>30} {"10":>30} {"joknarf":<30} {"sleep":<130} {"10:10":<50} /bin/sleep 60\n' 24 | ps_out += f'{"30":>30} {"10":>30} {"joknarf":<30} {"top":<130} {"10:10":<50} /bin/top\n' 25 | ps_out += f'{"40":>30} {"1":>30} {"root":<30} {"bash":<130} {"11:01":<50} -bash' 26 | print(ps_out) 27 | mock_runcmd.return_value = 0, ps_out 28 | mock_kill.return_value = True 29 | ptree = pgtree.Proctree() 30 | 31 | children = { 32 | '-1': ['0'], 33 | '0': ['1'], 34 | '1': ['10', '40'], 35 | '10': ['20', '30'], 36 | } 37 | ps_info = { 38 | '0': { 39 | 'pid': '0', 40 | 'ppid': '-1', 41 | 'stime': 'stime', 42 | 'user': 'user', 43 | 'comm': 'ucomm', 44 | 'args': 'args', 45 | }, 46 | '1': { 47 | 'pid': '1', 48 | 'ppid': '0', 49 | 'stime': 'Aug12', 50 | 'user': 'root', 51 | 'comm': 'init', 52 | 'args': '/init', 53 | }, 54 | '10': { 55 | 'pid': '10', 56 | 'ppid': '1', 57 | 'stime': 'Aug12', 58 | 'user': 'joknarf', 59 | 'comm': 'bash', 60 | 'args': '-bash', 61 | }, 62 | '20': { 63 | 'pid': '20', 64 | 'ppid': '10', 65 | 'stime': '10:10', 66 | 'user': 'joknarf', 67 | 'comm': 'sleep', 68 | 'args': '/bin/sleep 60', 69 | }, 70 | '30': { 71 | 'pid': '30', 72 | 'ppid': '10', 73 | 'stime': '10:10', 74 | 'user': 'joknarf', 75 | 'comm': 'top', 76 | 'args': '/bin/top', 77 | }, 78 | '40': { 79 | 'pid': '40', 80 | 'ppid': '1', 81 | 'stime': '11:01', 82 | 'user': 'root', 83 | 'comm': 'bash', 84 | 'args': '-bash', 85 | }, 86 | } 87 | pids_tree = { 88 | '10': ['20', '30'], 89 | '1': ['10'], 90 | '0': ['1'], 91 | '-1': ['0'], 92 | } 93 | selected_pids = ['30', '20', '10'] 94 | ptree.print_tree(pids=['10'], child_only=True) 95 | print(ptree.pids_tree) 96 | self.assertEqual(ptree.children, children) 97 | self.maxDiff = None 98 | self.assertEqual(ptree.ps_info, ps_info) 99 | self.assertEqual(ptree.pids_tree, pids_tree) 100 | self.assertEqual(ptree.selected_pids, selected_pids) 101 | ptree.print_tree(pids=['10'], child_only=True, sig=15, confirmed=True) 102 | 103 | def test_main(self): 104 | """test""" 105 | print('main =========') 106 | pgtree.main([]) 107 | 108 | def test_main2(self): 109 | """test""" 110 | print('main2 ========') 111 | pgtree.main(['-c', '-u', 'root', '-f', 'sshd']) 112 | pgtree.main(['sshd', '-cf', '-u', 'root']) 113 | 114 | @patch('os.kill') 115 | def test_main3(self, mock_kill): 116 | """test""" 117 | print('main3 ========') 118 | mock_kill.return_value = True 119 | pgtree.main(['-k', '-p', '1111']) 120 | pgtree.main(['-K', '-p', '1111']) 121 | 122 | def test_main4(self): 123 | """test""" 124 | print('main4 ========') 125 | try: 126 | pgtree.main(['-h']) 127 | except SystemExit: 128 | pass 129 | 130 | @patch('builtins.input') 131 | def test_main5(self, mock_input): 132 | """test""" 133 | print('main5 ========') 134 | mock_input.return_value = 'n' 135 | pgtree.main(['-k', 'sshd']) 136 | 137 | def test_main6(self): 138 | """test""" 139 | print('main6 ========') 140 | pgtree.main(['-a']) 141 | 142 | def test_main7(self): 143 | """test""" 144 | print('main7 ========') 145 | pgtree.main(['-C', 'y', '-O', '%cpu', 'init']) 146 | 147 | def test_ospgrep(self): 148 | """pgrep os""" 149 | print("test os pgrep") 150 | pgtree.main(['-C','y','-w','n','-f', '-i', '-u', 'root', '-x', '-t', 'pts/1', 'bash']) 151 | pgtree.main("-1") 152 | 153 | @patch.dict(os.environ, {"PGT_PGREP": "", "PGTREE": "-1"}) 154 | def test_pgrep(self): 155 | """pgrep built-in""" 156 | print("test pgrep built-in") 157 | try: 158 | pgtree.main(['-R','-t', 'pts/1']) 159 | except SystemExit: 160 | pass 161 | pgtree.main(['-I','-C','n','-w','n','-f', '-i', '-u', 'root', '-x', '/sbin/init']) 162 | 163 | @patch('time.sleep') 164 | def test_watch(self, mock_sleep): 165 | """watch built-in""" 166 | print("test watch built-in") 167 | mock_sleep.return_value = True 168 | pgtree.main(['-W', 'bash']) 169 | 170 | @patch.dict(os.environ, {"PGT_COMM": "", "PGT_STIME": ""}) 171 | def test_simpleps(self): 172 | pgtree.main([]) 173 | 174 | def test_psfail(self): 175 | """test""" 176 | print('psfail ========') 177 | try: 178 | pgtree.main(['-O abcd']) 179 | except SystemExit: 180 | pass 181 | 182 | def test_threads(self): 183 | pgtree.main(["-T"]) 184 | --------------------------------------------------------------------------------