Skip to content

Commit

Permalink
Merge branch 'johan/pxtree' into python
Browse files Browse the repository at this point in the history
  • Loading branch information
walles committed Oct 2, 2023
2 parents 723057b + e037567 commit 18dd82c
Show file tree
Hide file tree
Showing 13 changed files with 379 additions and 18 deletions.
16 changes: 16 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ going on.
``px`` I use for figuring out things like "do I still have any `Flutter`_
processes running in the background"?

``pxtree`` can be used as ``watch --color pxtree brew`` to figure out what
`Homebrew`_ is doing.

Output
======

Expand Down Expand Up @@ -45,6 +48,17 @@ expect from ``ptop``, with explanations below the screenshot:
* A help text on the bottom hints you how to search / filter (interactively),
change sort order or how to pick processes for further inspection or killing.

``pxtree``
----------

|pxtree screenshot|

* Note how search hits are highlighted in **bold**
* Note how PIDs (process IDs) are printed by default
* Note how multiple processes with the same names are coalesced and printed with
the count in parentheses
* Note how the process names make sense (``lsp_server.py`` rather than ``python3``)

``px``
-------------
Running just ``px`` lists all running processes, with the most interesting ones last.
Expand Down Expand Up @@ -361,6 +375,7 @@ DONE
.. _how to install: #installation
.. _Bubblemon: https://walles.github.io/bubblemon/
.. _Flutter: https://flutter.dev
.. _Homebrew: https://brew.sh
.. _Debian 10 Buster: https://wiki.debian.org/DebianBuster
.. _Ubuntu 19.04 Disco: https://launchpad.net/ubuntu/disco/
.. _Homebrew: https://brew.sh
Expand All @@ -378,3 +393,4 @@ DONE
.. |macOS CI Status| image:: https://github.com/walles/px/actions/workflows/macos-ci.yml/badge.svg
:target: https://github.com/walles/px/actions/workflows/macos-ci.yml?query=branch%3Apython
.. |ptop screenshot| image:: doc/ptop-screenshot.png
.. |pxtree screenshot| image:: doc/pxtree-screenshot.png
3 changes: 2 additions & 1 deletion doc/ptop.1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
.Os
.Sh NAME
.Nm ptop
.Nd display and update sorted information about processes
.Nd display sorted information about processes
.Sh SYNOPSIS
.\" FIXME: Other man pages don't need to use \p to break lines here,
.\" and use the Nm macro for the command name. Why can't we?
Expand Down Expand Up @@ -72,6 +72,7 @@ Perl
.El
.Sh SEE ALSO
.Xr px 1 ,
.Xr pxtree 1 ,
.Xr top 1
.Sh HOMEPAGE
.Nm
Expand Down
3 changes: 2 additions & 1 deletion doc/px.1
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ Various shells
Perl
.El
.Sh SEE ALSO
.Xr ptop 1
.Xr ptop 1 ,
.Xr pxtree 1
.Sh HOMEPAGE
.Nm
lives at http://github.com/walles/px
Binary file added doc/pxtree-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions doc/pxtree.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
.Dd October 2, 2023
.Dt PXTREE 1
.Os
.Sh NAME
.Nm pxtree
.Nd display running processes in a tree
.Sh SYNOPSIS
.\" FIXME: Other man pages don't need to use \p to break lines here,
.\" and use the Nm macro for the command name. Why can't we?
.Ic pxtree [ --debug ] [ SEARCH ]
.Sh DESCRIPTION
.Nm
lists all running processes in a tree.
.Pp
Process names are followed by their PIDs in parentheses. If multiple processes
have the same names, they will be coalesced into one entry marked with (3×) in
bold, where
.Sy 3
in this case is the number of duplicates.
.Pp
The optional
.Cm SEARCH
parameter can be:
.Bl -bullet
.It
A PID (Process ID)
.It
A process name or part of one
.It
A user name
.El
.Pp
Any search hits will be highlighted in bold. All parents and children of all
search hits will always be listed.
.Pp
With the
.Fl -debug
flag, extra log messages are sometimes printed after
.Nm
finishes.
.Sh PROCESS NAMING
.Nm
tries to be helpful about naming processes, and avoid printing names
of various VMs.
.Pp
For example, if you do
.Sy java -jar foo.jar ,
.Nm
will show this process as
.Sy foo.jar
rather than
.Sy java .
.El
.Sh SEE ALSO
.Xr px 1 ,
.Xr ptop 1
.Sh HOMEPAGE
.Nm
lives at http://github.com/walles/px
7 changes: 5 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ echo "sudo install px.pex ${PXPREFIX}/px"
sudo install "${TEMPFILE}" "${PXPREFIX}/px"
echo "sudo ln px ${PXPREFIX}/ptop"
sudo ln -sf px "${PXPREFIX}/ptop"
echo "sudo ln px ${PXPREFIX}/pxtree"
sudo ln -sf px "${PXPREFIX}/pxtree"

rm -f "${TEMPFILE}"

echo
echo "Installation done, now run one or both of:"
echo " px"
echo "Installation done, now run one or all of:"
echo " ptop"
echo " pxtree"
echo " px"
31 changes: 28 additions & 3 deletions px/px.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
px [--debug] [--sort=cpupercent] [--no-username] [filter string]
px [--debug] [--no-pager] [--color] <PID>
px [--debug] --top [filter string]
px [--debug] --tree [filter string]
px --install
px --help
px --version
Expand All @@ -27,9 +28,13 @@
invoked px, rather than from when each process started. This gives you a picture
of which processes are most active right now.
In --tree mode, a process tree is shown. Filtering is supported, try
"px --tree firefox" for example.
--top: Show a continuously refreshed process list
--tree: Print a process tree
--debug: Print debug logs (if any) after running
--install: Install /usr/local/bin/px and /usr/local/bin/ptop
--install: Install px, ptop and pxtree in /usr/local/bin/
--no-pager: Print PID info to stdout rather than to a pager
--sort=cpupercent: Order processes by CPU percentage only
--no-username: Don't show the username column in px output
Expand Down Expand Up @@ -76,6 +81,7 @@ def install(argv: List[str]) -> None:

px_install.install(px_pex, "/usr/local/bin/px")
px_install.install(px_pex, "/usr/local/bin/ptop")
px_install.install(px_pex, "/usr/local/bin/pxtree")


# This is the setup.py entry point
Expand Down Expand Up @@ -171,6 +177,7 @@ def _main(argv: List[str]) -> None:
with_color: Optional[bool] = None
with_username = True
top: bool = False
tree: bool = False
sort_cpupercent: bool = False

while "--no-pager" in argv:
Expand All @@ -193,6 +200,12 @@ def _main(argv: List[str]) -> None:
if os.path.basename(argv[0]).endswith("top"):
top = True

while "--tree" in argv:
tree = True
argv.remove("--tree")
if os.path.basename(argv[0]).endswith("tree"):
tree = True

while "--sort=cpupercent" in argv:
sort_cpupercent = True
argv.remove("--sort=cpupercent")
Expand All @@ -204,25 +217,37 @@ def _main(argv: List[str]) -> None:

if len(argv) > 2:
sys.stderr.write("ERROR: Expected zero or one argument but got more\n\n")
print(__doc__)
print(__doc__, file=sys.stderr)
sys.exit(1)

search = ""
if len(argv) == 2:
if argv[1].startswith("--"):
sys.stderr.write(f"ERROR: Unknown argument: {argv[1]}\n\n")
print(__doc__)
print(__doc__, file=sys.stderr)
sys.exit(1)

search = argv[1]

if top and tree:
sys.stderr.write("ERROR: --top and --tree are mutually exclusive\n\n")
print(__doc__, file=sys.stderr)
sys.exit(1)

if top:
# Pulling px_top in on demand like this improves test result caching
from . import px_top # pylint: disable=import-outside-toplevel

px_top.top(search=search)
return

if tree:
# Pulling px_tree in on demand like this improves test result caching
from . import px_tree # pylint: disable=import-outside-toplevel

px_tree.tree(search=search)
return

try:
pid = int(search)
if not with_pager:
Expand Down
2 changes: 1 addition & 1 deletion px/px_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def match(self, string, require_exact_user=True):
See px_process_test.test_match() for the exact definition of how the
matching is done.
"""
if string is None:
if not string:
return True

if self.username == string:
Expand Down
134 changes: 134 additions & 0 deletions px/px_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from . import px_process
from . import px_terminal

from typing import Set, List, Optional, Iterable


def tree(search: str) -> None:
"""Print a process tree"""
for line in _generate_tree(px_process.get_all(), search):
print(line)


def _generate_tree(processes: List[px_process.PxProcess], search: str) -> List[str]:
# Only print subtrees needed for showing all search hits and their children.
#
# We do that by starting at the search hits and walking up the tree from all
# of them, collecting each PID we see along the way. Then when we render the
# tree, we only render those PIDs.
show_pids: Set[int] = set()
if search:
for process in processes:
if not process.match(search):
continue

_mark_children(process, show_pids)

if process and process.parent:
process = process.parent
while process:
if process.pid in show_pids:
break
show_pids.add(process.pid)
if not process.parent:
break
process = process.parent

if not processes:
return []

lines = []
coalescer = Coalescer(0)
lines += coalescer.submit(processes[0], search)
lines += coalescer.flush()
return lines + _generate_child_tree(processes[0].children, 1, search, show_pids)


def _mark_children(process: px_process.PxProcess, show_pids: Set[int]) -> None:
"""Recursively mark all children of a search hit as needing to be shown"""
show_pids.add(process.pid)
for child in process.children:
_mark_children(child, show_pids)


class Coalescer:
def __init__(self, indent: int) -> None:
self._base: Optional[px_process.PxProcess] = None
self._count = 0
self._indent = indent

def submit(self, process: px_process.PxProcess, search: str) -> List[str]:
"""Returns an array of zero or more lines to be printed"""
is_search_hit = search and process.match(search)
has_children = bool(process.children)
is_candidate = not is_search_hit and not has_children

# If we can coalesce this, do it!
if self._base and is_candidate and self._base.command == process.command:
self._count += 1
return []
if not self._base and is_candidate:
self._base = process
self._count = 1
return []

return_me = []

# Otherwise, print the coalesced line if we have one
if self._base:
return_me += self.flush()

# Can we coalesce this line?
if is_candidate:
self._base = process
self._count = 1
return return_me

# And print the current line
if is_search_hit:
return_me.append(
f"{' ' * self._indent}{px_terminal.bold(process.command)}({process.pid})"
)
else:
return_me.append(f"{' ' * self._indent}{process.command}({process.pid})")

return return_me

def flush(self) -> List[str]:
if not self._base:
return []

assert self._count > 0

return_me: str
if self._count == 1:
return_me = f"{' ' * self._indent}{self._base.command}({self._base.pid})"
else:
return_me = f"{' ' * self._indent}{self._base.command}... ({px_terminal.bold(f'{self._count}×')})"

self._base = None
self._count = 0

return [return_me]


def _generate_child_tree(
children: Iterable[px_process.PxProcess],
indent: int,
search: str,
show_pids: Set[int],
) -> List[str]:
lines = []

coalescer = Coalescer(indent)
for child in sorted(
children, key=lambda p: (p.command.lower(), bool(p.children), p.pid)
):
if show_pids and child.pid not in show_pids:
continue

lines += coalescer.submit(child, search)
lines += _generate_child_tree(child.children, indent + 1, search, show_pids)

lines += coalescer.flush()
return lines
5 changes: 5 additions & 0 deletions pxtree.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

# Run pxtree from current sources

PYTHONPATH=px:$(echo env/lib/python*/site-packages) python3 -m px.px --tree "$@"
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@
"pytest",
],
entry_points={
"console_scripts": ["px = px.px:main", "ptop = px.px:main"],
"console_scripts": [
"px = px.px:main",
"ptop = px.px:main",
"pxtree = px.px:main",
],
}
# Note that we're by design *not* installing man pages here.
# Using "data_files=" only puts the man pages in the egg file,
Expand Down
Loading

0 comments on commit 18dd82c

Please sign in to comment.