sysgraph 0.0.16__tar.gz → 0.0.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. sysgraph-0.0.18/PKG-INFO +159 -0
  2. sysgraph-0.0.18/README.md +128 -0
  3. sysgraph-0.0.18/pyproject.toml +66 -0
  4. sysgraph-0.0.18/src/sysgraph/__init__.py +1 -0
  5. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph/app.py +6 -1
  6. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph/constants.py +2 -2
  7. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph/discovery.py +41 -12
  8. sysgraph-0.0.18/src/sysgraph/dist/assets/index-BLbtXZbQ.css +1 -0
  9. sysgraph-0.0.18/src/sysgraph/dist/assets/index-Dq5MCDEE.js +737 -0
  10. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph/dist/index.html +6 -4
  11. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph/main.py +4 -1
  12. sysgraph-0.0.18/src/sysgraph/temp/congress_network/compute_vc.py +31 -0
  13. sysgraph-0.0.18/src/sysgraph/temp/congress_network/convert_to_graph_format.py +85 -0
  14. sysgraph-0.0.18/src/sysgraph/temp/congress_network/histogram_weights.py +36 -0
  15. sysgraph-0.0.18/src/sysgraph/temp/congress_network/viral_centrality.py +120 -0
  16. sysgraph-0.0.18/src/sysgraph/temp/voles_network/convert_voles_to_graph_format.py +89 -0
  17. sysgraph-0.0.18/src/sysgraph/tests/test_discovery.py +19 -0
  18. sysgraph-0.0.18/src/sysgraph/tests/test_graph.py +132 -0
  19. sysgraph-0.0.18/src/sysgraph.egg-info/PKG-INFO +159 -0
  20. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph.egg-info/SOURCES.txt +9 -4
  21. sysgraph-0.0.18/src/sysgraph.egg-info/top_level.txt +2 -0
  22. sysgraph-0.0.16/PKG-INFO +0 -142
  23. sysgraph-0.0.16/README.md +0 -99
  24. sysgraph-0.0.16/pyproject.toml +0 -19
  25. sysgraph-0.0.16/setup.py +0 -91
  26. sysgraph-0.0.16/src/sysgraph/__init__.py +0 -1
  27. sysgraph-0.0.16/src/sysgraph/dist/assets/index-D4kwmDWy.css +0 -1
  28. sysgraph-0.0.16/src/sysgraph/dist/assets/index-D5t1fx8g.js +0 -735
  29. sysgraph-0.0.16/src/sysgraph.egg-info/PKG-INFO +0 -142
  30. sysgraph-0.0.16/src/sysgraph.egg-info/not-zip-safe +0 -1
  31. sysgraph-0.0.16/src/sysgraph.egg-info/top_level.txt +0 -1
  32. {sysgraph-0.0.16 → sysgraph-0.0.18}/LICENSE +0 -0
  33. {sysgraph-0.0.16 → sysgraph-0.0.18}/MANIFEST.in +0 -0
  34. {sysgraph-0.0.16 → sysgraph-0.0.18}/setup.cfg +0 -0
  35. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph/__main__.py +0 -0
  36. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph/dist/assets/icon-TKtfQOgj.png +0 -0
  37. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph/graph.py +0 -0
  38. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph/model.py +0 -0
  39. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph.egg-info/dependency_links.txt +0 -0
  40. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph.egg-info/entry_points.txt +0 -0
  41. {sysgraph-0.0.16 → sysgraph-0.0.18}/src/sysgraph.egg-info/requires.txt +0 -0
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: sysgraph
3
+ Version: 0.0.18
4
+ Summary: Visualizer for processes and their interconnections
5
+ Author-email: Eugene Gubenkov <gubenkoved@gmail.com>
6
+ License: MIT
7
+ Project-URL: Source, https://github.com/gubenkoved/sysgraph
8
+ Project-URL: Issues, https://github.com/gubenkoved/sysgraph/issues
9
+ Keywords: process,visualization,graph,ipc,system,monitoring,cross-platform,macos,linux
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Topic :: System :: Monitoring
18
+ Classifier: Topic :: System :: Systems Administration
19
+ Classifier: Environment :: Web Environment
20
+ Classifier: Framework :: FastAPI
21
+ Classifier: Intended Audience :: Developers
22
+ Classifier: Intended Audience :: System Administrators
23
+ Requires-Python: >=3.12
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: psutil
27
+ Requires-Dist: coloredlogs
28
+ Requires-Dist: fastapi>=0.95
29
+ Requires-Dist: uvicorn[standard]>=0.20
30
+ Dynamic: license-file
31
+
32
+ # sysgraph
33
+
34
+ An interactive force-directed **network graph visualizer** for the browser — with two modes:
35
+
36
+ - **Import any graph** — load any JSON graph (nodes + edges) to explore and visualize it interactively
37
+ - **Live process graph** — discover running OS processes and their inter-process communication channels in real time (cross-platform; richest on Linux)
38
+
39
+ [![PyPI](https://img.shields.io/pypi/v/sysgraph)](https://pypi.org/project/sysgraph/)
40
+ ![Python](https://img.shields.io/badge/python-%3E%3D3.12-blue)
41
+ ![License](https://img.shields.io/badge/license-MIT-green)
42
+
43
+ ## Features
44
+
45
+ - **Import any graph** — load a JSON file with nodes and edges to visualize any network, social graph, dependency tree, or dataset
46
+ - **Export/Import** — save and reload graph snapshots as JSON; use the sample at [`data/simplest-graph.json`](data/simplest-graph.json) as a format reference
47
+ - **Interactive graph** — force-directed graph rendered in the browser with zoom, pan, drag, and search
48
+ - **Fuzzy search** — find nodes by any property
49
+ - **Adjacency filtering** — right-click a node to show only its neighbors
50
+ - **Configurable** — tune d3 force parameters, colors, and type filters via the settings panel
51
+ - **Process discovery** — enumerates running OS processes and their parent-child relationships (cross-platform via psutil)
52
+ - **IPC visualization** — discovers TCP/UDP connections (all platforms), Unix domain sockets and pipes (Linux only)
53
+ - **Real-time** — fetch the latest process graph on demand via the web UI
54
+
55
+ ## Demo
56
+
57
+ [Demo](https://github.com/user-attachments/assets/7d19daca-042c-43f1-bedd-4d74344e1e89)
58
+
59
+ ## Requirements
60
+
61
+ - **Python ≥ 3.12**
62
+ - **Linux, macOS, or Windows** — process and network discovery works on all platforms via psutil; Unix domain sockets and pipe discovery require Linux
63
+ - Root/sudo recommended for full process visibility on Linux/macOS
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ pip install sysgraph
69
+ ```
70
+
71
+ ## Usage
72
+
73
+ ```bash
74
+ # Start the web server (default: http://localhost:8000)
75
+ sysgraph
76
+
77
+ # Specify a custom port
78
+ sysgraph --port 9000
79
+
80
+ # Or run as a module
81
+ python -m sysgraph
82
+ ```
83
+
84
+ Open your browser to the displayed URL.
85
+
86
+ ### Visualize your own graph
87
+
88
+ Use the **Import** button in the UI to load any JSON file in the following format:
89
+
90
+ ```json
91
+ {
92
+ "nodes": [
93
+ {"id": "1", "type": "person", "properties": {"name": "Alice"}},
94
+ {"id": "2", "type": "person", "properties": {"name": "Bob"}}
95
+ ],
96
+ "edges": [
97
+ {"source_id": "1", "target_id": "2", "type": "knows", "properties": {}}
98
+ ]
99
+ }
100
+ ```
101
+
102
+ See [`data/simplest-graph.json`](data/simplest-graph.json) for a minimal example.
103
+
104
+ ### Live process graph
105
+
106
+ For full visibility into all processes and their connections, run with elevated privileges (Linux/macOS):
107
+
108
+ ```bash
109
+ sudo sysgraph
110
+ ```
111
+
112
+ ## Docker
113
+
114
+ ```bash
115
+ docker run --rm -it --pid=host --net=host gubenkoved/sysgraph
116
+ ```
117
+
118
+ The `--pid=host` and `--net=host` flags allow the container to see host processes and network connections.
119
+
120
+ ## How It Works
121
+
122
+ 1. The **browser frontend** renders interactive force-directed graphs using [force-graph](https://github.com/vasturiano/force-graph) with d3 physics simulation.
123
+ 2. Graphs can be **imported from JSON** directly in the browser, or fetched live from the backend.
124
+ 3. The **FastAPI backend** uses `psutil` to discover processes and network connections (cross-platform), plus Linux-specific APIs (`/proc`, `ss`) for pipe and Unix domain socket discovery, building a graph served via `GET /api/graph`.
125
+
126
+ ## Development
127
+
128
+ ### Prerequisites
129
+ - Python ≥ 3.12, Docker (for frontend builds)
130
+ - Node.js 22 runs inside Docker; no host installation required
131
+
132
+ ### Backend
133
+ ```bash
134
+ python3 -m venv .venv && source .venv/bin/activate
135
+ pip install -e . && pip install -r requirements-dev.in
136
+ python src/sysgraph/app.py # → http://localhost:8000
137
+ ```
138
+
139
+ ### Frontend (TypeScript + Vite)
140
+ ```bash
141
+ ./scripts/build-ui.sh # production build → src/sysgraph/dist/
142
+ ./scripts/dev-ui.sh # Vite dev server with HMR on :5173
143
+ ./scripts/typecheck-ui.sh # TypeScript type checking
144
+ ./scripts/lint-ui.sh # Biome linter (pass --fix to auto-fix)
145
+ ```
146
+
147
+ ### Tests
148
+ ```bash
149
+ pytest src/sysgraph/tests/
150
+ ```
151
+
152
+ ### Python linting
153
+ ```bash
154
+ ./scripts/lint.sh # ruff check + ruff format + isort
155
+ ```
156
+
157
+ ## License
158
+
159
+ [MIT](LICENSE)
@@ -0,0 +1,128 @@
1
+ # sysgraph
2
+
3
+ An interactive force-directed **network graph visualizer** for the browser — with two modes:
4
+
5
+ - **Import any graph** — load any JSON graph (nodes + edges) to explore and visualize it interactively
6
+ - **Live process graph** — discover running OS processes and their inter-process communication channels in real time (cross-platform; richest on Linux)
7
+
8
+ [![PyPI](https://img.shields.io/pypi/v/sysgraph)](https://pypi.org/project/sysgraph/)
9
+ ![Python](https://img.shields.io/badge/python-%3E%3D3.12-blue)
10
+ ![License](https://img.shields.io/badge/license-MIT-green)
11
+
12
+ ## Features
13
+
14
+ - **Import any graph** — load a JSON file with nodes and edges to visualize any network, social graph, dependency tree, or dataset
15
+ - **Export/Import** — save and reload graph snapshots as JSON; use the sample at [`data/simplest-graph.json`](data/simplest-graph.json) as a format reference
16
+ - **Interactive graph** — force-directed graph rendered in the browser with zoom, pan, drag, and search
17
+ - **Fuzzy search** — find nodes by any property
18
+ - **Adjacency filtering** — right-click a node to show only its neighbors
19
+ - **Configurable** — tune d3 force parameters, colors, and type filters via the settings panel
20
+ - **Process discovery** — enumerates running OS processes and their parent-child relationships (cross-platform via psutil)
21
+ - **IPC visualization** — discovers TCP/UDP connections (all platforms), Unix domain sockets and pipes (Linux only)
22
+ - **Real-time** — fetch the latest process graph on demand via the web UI
23
+
24
+ ## Demo
25
+
26
+ [Demo](https://github.com/user-attachments/assets/7d19daca-042c-43f1-bedd-4d74344e1e89)
27
+
28
+ ## Requirements
29
+
30
+ - **Python ≥ 3.12**
31
+ - **Linux, macOS, or Windows** — process and network discovery works on all platforms via psutil; Unix domain sockets and pipe discovery require Linux
32
+ - Root/sudo recommended for full process visibility on Linux/macOS
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install sysgraph
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ # Start the web server (default: http://localhost:8000)
44
+ sysgraph
45
+
46
+ # Specify a custom port
47
+ sysgraph --port 9000
48
+
49
+ # Or run as a module
50
+ python -m sysgraph
51
+ ```
52
+
53
+ Open your browser to the displayed URL.
54
+
55
+ ### Visualize your own graph
56
+
57
+ Use the **Import** button in the UI to load any JSON file in the following format:
58
+
59
+ ```json
60
+ {
61
+ "nodes": [
62
+ {"id": "1", "type": "person", "properties": {"name": "Alice"}},
63
+ {"id": "2", "type": "person", "properties": {"name": "Bob"}}
64
+ ],
65
+ "edges": [
66
+ {"source_id": "1", "target_id": "2", "type": "knows", "properties": {}}
67
+ ]
68
+ }
69
+ ```
70
+
71
+ See [`data/simplest-graph.json`](data/simplest-graph.json) for a minimal example.
72
+
73
+ ### Live process graph
74
+
75
+ For full visibility into all processes and their connections, run with elevated privileges (Linux/macOS):
76
+
77
+ ```bash
78
+ sudo sysgraph
79
+ ```
80
+
81
+ ## Docker
82
+
83
+ ```bash
84
+ docker run --rm -it --pid=host --net=host gubenkoved/sysgraph
85
+ ```
86
+
87
+ The `--pid=host` and `--net=host` flags allow the container to see host processes and network connections.
88
+
89
+ ## How It Works
90
+
91
+ 1. The **browser frontend** renders interactive force-directed graphs using [force-graph](https://github.com/vasturiano/force-graph) with d3 physics simulation.
92
+ 2. Graphs can be **imported from JSON** directly in the browser, or fetched live from the backend.
93
+ 3. The **FastAPI backend** uses `psutil` to discover processes and network connections (cross-platform), plus Linux-specific APIs (`/proc`, `ss`) for pipe and Unix domain socket discovery, building a graph served via `GET /api/graph`.
94
+
95
+ ## Development
96
+
97
+ ### Prerequisites
98
+ - Python ≥ 3.12, Docker (for frontend builds)
99
+ - Node.js 22 runs inside Docker; no host installation required
100
+
101
+ ### Backend
102
+ ```bash
103
+ python3 -m venv .venv && source .venv/bin/activate
104
+ pip install -e . && pip install -r requirements-dev.in
105
+ python src/sysgraph/app.py # → http://localhost:8000
106
+ ```
107
+
108
+ ### Frontend (TypeScript + Vite)
109
+ ```bash
110
+ ./scripts/build-ui.sh # production build → src/sysgraph/dist/
111
+ ./scripts/dev-ui.sh # Vite dev server with HMR on :5173
112
+ ./scripts/typecheck-ui.sh # TypeScript type checking
113
+ ./scripts/lint-ui.sh # Biome linter (pass --fix to auto-fix)
114
+ ```
115
+
116
+ ### Tests
117
+ ```bash
118
+ pytest src/sysgraph/tests/
119
+ ```
120
+
121
+ ### Python linting
122
+ ```bash
123
+ ./scripts/lint.sh # ruff check + ruff format + isort
124
+ ```
125
+
126
+ ## License
127
+
128
+ [MIT](LICENSE)
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sysgraph"
7
+ dynamic = ["version"]
8
+ description = "Visualizer for processes and their interconnections"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Eugene Gubenkov", email = "gubenkoved@gmail.com" }]
12
+ requires-python = ">=3.12"
13
+ keywords = ["process", "visualization", "graph", "ipc", "system", "monitoring", "cross-platform", "macos", "linux"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Operating System :: MacOS",
21
+ "Operating System :: Microsoft :: Windows",
22
+ "Topic :: System :: Monitoring",
23
+ "Topic :: System :: Systems Administration",
24
+ "Environment :: Web Environment",
25
+ "Framework :: FastAPI",
26
+ "Intended Audience :: Developers",
27
+ "Intended Audience :: System Administrators",
28
+ ]
29
+ dependencies = [
30
+ "psutil",
31
+ "coloredlogs",
32
+ "fastapi>=0.95",
33
+ "uvicorn[standard]>=0.20",
34
+ ]
35
+
36
+ [project.urls]
37
+ Source = "https://github.com/gubenkoved/sysgraph"
38
+ Issues = "https://github.com/gubenkoved/sysgraph/issues"
39
+
40
+ [project.scripts]
41
+ sysgraph = "sysgraph.app:main"
42
+
43
+ [tool.setuptools.dynamic]
44
+ version = { attr = "sysgraph.__version__" }
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.setuptools.package-data]
50
+ sysgraph = ["dist/*", "dist/**/*"]
51
+
52
+ [tool.ruff]
53
+ line-length = 79
54
+ target-version = "py312"
55
+ # Tune ruff rules as needed. Ruff supports many checks and can also format (`ruff format`).
56
+ # See https://beta.ruff.rs/docs/configuration/ for config options.
57
+ exclude = [".venv", "build", "dist", "__pycache__"]
58
+
59
+ [tool.isort]
60
+ profile = "black"
61
+ multi_line_output = 3
62
+ include_trailing_comma = true
63
+ force_grid_wrap = 0
64
+ use_parentheses = true
65
+ ensure_newline_before_comments = true
66
+ line_length = 88
@@ -0,0 +1 @@
1
+ __version__ = "0.0.18"
@@ -18,9 +18,14 @@ from sysgraph.discovery import build_graph
18
18
  LOGGER = logging.getLogger(__name__)
19
19
 
20
20
 
21
+ LOG_FORMAT = (
22
+ "%(asctime)s.%(msecs)03d %(name)s[%(process)d] %(levelname)s %(message)s"
23
+ )
24
+
25
+
21
26
  @asynccontextmanager
22
27
  async def lifespan(app):
23
- coloredlogs.install()
28
+ coloredlogs.install(fmt=LOG_FORMAT)
24
29
  logging.info(f"init for process with PID {os.getpid()}")
25
30
  yield
26
31
  logging.info(f"shutdown for process with PID {os.getpid()}")
@@ -20,8 +20,8 @@ def process_node_id(pid: int) -> str:
20
20
  return f"{NODE_PROCESS}::{pid}"
21
21
 
22
22
 
23
- def uds_node_id(inode: int) -> str:
24
- return f"{NODE_UDS}::{inode}"
23
+ def uds_node_id(key: str) -> str:
24
+ return f"{NODE_UDS}::{key}"
25
25
 
26
26
 
27
27
  def pipe_node_id(inode: str) -> str:
@@ -2,6 +2,8 @@ import logging
2
2
  import os
3
3
  import re
4
4
  import subprocess
5
+ import sys
6
+ import time
5
7
  from collections import defaultdict
6
8
  from concurrent.futures import ThreadPoolExecutor
7
9
  from pathlib import Path
@@ -71,7 +73,7 @@ def discover_processes() -> list[Process]:
71
73
  if mem_info:
72
74
  p.memory_rss = mem_info.rss
73
75
  p.memory_vms = mem_info.vms
74
- p.memory_shared = mem_info.shared
76
+ p.memory_shared = getattr(mem_info, "shared", None)
75
77
 
76
78
  p.environment = info.get("environ")
77
79
 
@@ -81,15 +83,18 @@ def discover_processes() -> list[Process]:
81
83
 
82
84
 
83
85
  def discover_unix_sockets() -> list[UnixDomainSocket]:
84
- """Discover Unix Domain Sockets as reported by kernel.
86
+ """Discover Unix Domain Sockets by parsing ``ss -xp`` output.
85
87
 
86
- Note however that connection with UDS will have at least 2 records reported
87
- with opposite local/peer inodes. They can be further groupped to
88
- find connected pairs of processes.
88
+ This is a Linux-only feature. On other platforms an empty list is
89
+ returned without raising an error.
89
90
 
90
91
  Returns:
91
- list[UnixDomainSocket]: _description_
92
+ list[UnixDomainSocket]: Discovered UDS sockets.
92
93
  """
94
+ if sys.platform != "linux":
95
+ LOGGER.debug("UDS discovery skipped (only supported on Linux)")
96
+ return []
97
+
93
98
  result = subprocess.run(
94
99
  ["ss", "-xp"], capture_output=True, text=True, check=False
95
100
  )
@@ -98,11 +103,15 @@ def discover_unix_sockets() -> list[UnixDomainSocket]:
98
103
 
99
104
  # example line:
100
105
  # users:(("dbus-daemon",pid=1950,fd=12))
101
- def parse_proccess(s: str) -> list[UnixDomainSocketProcRef]:
106
+ def parse_proccess(
107
+ s: str,
108
+ ) -> list[UnixDomainSocketProcRef]:
102
109
  processes = []
103
110
 
104
111
  for match in re.finditer(
105
- r'\("(?P<name>[^"]+)",pid=(?P<pid>[0-9]+),fd=(?P<fd>[0-9]+)\)', s
112
+ r'\("(?P<name>[^"]+)",pid=(?P<pid>[0-9]+),'
113
+ r"fd=(?P<fd>[0-9]+)\)",
114
+ s,
106
115
  ):
107
116
  ref = UnixDomainSocketProcRef(pid=int(match.group("pid")))
108
117
  ref.name = match.group("name")
@@ -140,7 +149,8 @@ def discover_connected_uds(
140
149
  ) -> list[UnixDomainSocketConnection]:
141
150
  """Discover connected UDS pairs from the list of discovered UDS sockets.
142
151
 
143
- Each connection will have two UDS sockets with opposite local/peer inodes.
152
+ Each connection will have two UDS sockets with opposite local/peer
153
+ inodes.
144
154
 
145
155
  Args:
146
156
  sockets (list[UnixDomainSocket]): List of discovered UDS sockets.
@@ -158,7 +168,10 @@ def discover_connected_uds(
158
168
  for uds in sockets:
159
169
  if (uds.peer_inode, uds.local_inode) in inode_map:
160
170
  peer_uds = inode_map[(uds.peer_inode, uds.local_inode)]
161
- if (uds.local_inode, uds.peer_inode) not in visited and (
171
+ if (
172
+ uds.local_inode,
173
+ uds.peer_inode,
174
+ ) not in visited and (
162
175
  uds.peer_inode,
163
176
  uds.local_inode,
164
177
  ) not in visited:
@@ -174,8 +187,18 @@ def discover_connected_uds(
174
187
 
175
188
 
176
189
  def get_processes_open_files() -> dict[int, list[ProcessOpenFile]]:
177
- """Read open file descriptors directly from /proc instead of spawning lsof."""
190
+ """Read open file descriptors from /proc to discover pipes.
191
+
192
+ This is a Linux-only feature that reads ``/proc/[pid]/fd`` and
193
+ ``/proc/[pid]/fdinfo``. On other platforms an empty mapping is
194
+ returned without raising an error.
195
+ """
178
196
  result_map: dict[int, list[ProcessOpenFile]] = defaultdict(list)
197
+
198
+ if sys.platform != "linux":
199
+ LOGGER.debug("pipe discovery skipped (only supported on Linux)")
200
+ return result_map
201
+
179
202
  proc_path = Path("/proc")
180
203
 
181
204
  for pid_dir in proc_path.iterdir():
@@ -305,7 +328,7 @@ def _add_uds_nodes(
305
328
  label = f"uds:[{inode}]"
306
329
 
307
330
  node = graph.add_node(
308
- uds_node_id(inode),
331
+ uds_node_id(str(inode)),
309
332
  NODE_UDS,
310
333
  properties={
311
334
  "label": label,
@@ -513,6 +536,8 @@ def _add_network_nodes(
513
536
 
514
537
 
515
538
  def build_graph(discover_uds_connectivity: bool = True) -> Graph:
539
+ started_at = time.monotonic()
540
+
516
541
  graph = Graph()
517
542
 
518
543
  # run independent discovery steps in parallel (all I/O-bound)
@@ -532,4 +557,8 @@ def build_graph(discover_uds_connectivity: bool = True) -> Graph:
532
557
  _add_pipe_nodes(graph, open_files_map, pid_to_node)
533
558
  _add_network_nodes(graph, processes, all_net_connections, pid_to_node)
534
559
 
560
+ LOGGER.info(
561
+ f"discovery completed in {time.monotonic() - started_at:.2f} seconds"
562
+ )
563
+
535
564
  return graph
@@ -0,0 +1 @@
1
+ :root{--z-toolbar: 100;--z-search-help: 150;--z-context-menu: 200;--z-loading-overlay: 300;--z-details-panel: 50;--z-selection-overlay: 10;--z-version-badge: 10;--z-zoom-indicator: 10;--bg-toolbar: rgba(255, 255, 255, .95);--bg-panel: rgba(255, 255, 255, .97);--border-subtle: rgba(0, 0, 0, .08);--border-light: rgba(0, 0, 0, .1);--border-medium: rgba(0, 0, 0, .12);--shadow-xs: rgba(0, 0, 0, .08);--shadow-sm: rgba(0, 0, 0, .14);--shadow-md: rgba(0, 0, 0, .15);--accent-primary: #1a56db;--accent-info: #1a73e8;--accent-danger: #dc2626;--toolbar-height: 48px;--spacing-xs: 4px;--spacing-sm: 6px;--spacing-md: 8px;--spacing-lg: 16px;--font-family: "Ubuntu", "Segoe UI", "Arial", sans-serif;--font-size-xs: 11px;--font-size-sm: 12px;--font-size-md: 13px;--radius-sm: 4px;--radius-md: 8px;--radius-lg: 12px;--button-height: 28px;--icon-button-size: 24px;--tp-base-background-color: hsla(230, 5%, 90%, 1);--tp-base-shadow-color: hsla(0, 0%, 0%, .1);--tp-button-background-color: hsla(230, 7%, 75%, 1);--tp-button-background-color-active: hsla(230, 7%, 60%, 1);--tp-button-background-color-focus: hsla(230, 7%, 65%, 1);--tp-button-background-color-hover: hsla(230, 7%, 70%, 1);--tp-button-foreground-color: hsla(230, 10%, 30%, 1);--tp-container-background-color: hsla(230, 15%, 30%, .2);--tp-container-background-color-active: hsla(230, 15%, 30%, .32);--tp-container-background-color-focus: hsla(230, 15%, 30%, .28);--tp-container-background-color-hover: hsla(230, 15%, 30%, .24);--tp-container-foreground-color: hsla(230, 10%, 30%, 1);--tp-groove-foreground-color: hsla(230, 15%, 30%, .1);--tp-input-background-color: hsla(230, 15%, 30%, .1);--tp-input-background-color-active: hsla(230, 15%, 30%, .22);--tp-input-background-color-focus: hsla(230, 15%, 30%, .18);--tp-input-background-color-hover: hsla(230, 15%, 30%, .14);--tp-input-foreground-color: hsla(230, 10%, 30%, 1);--tp-label-foreground-color: hsla(230, 10%, 30%, .7);--tp-monitor-background-color: hsla(230, 15%, 30%, .1);--tp-monitor-foreground-color: hsla(230, 10%, 30%, .5)}html,body{height:100%;margin:0;overflow:hidden;font-family:var(--font-family)}#toolbar{position:fixed;top:0;left:0;right:0;height:var(--toolbar-height);z-index:var(--z-toolbar);background:var(--bg-toolbar);border-bottom:1px solid var(--border-light);display:flex;align-items:center;padding:0 var(--spacing-lg);gap:var(--spacing-sm);box-shadow:0 1px 3px var(--shadow-xs)}#toolbar md-outlined-button{--md-outlined-button-container-height: var(--button-height);--md-outlined-button-label-text-size: var(--font-size-sm);--md-outlined-button-label-text-weight: 500;--md-outlined-button-icon-size: 16px;--md-outlined-button-leading-space: 10px;--md-outlined-button-trailing-space: 12px;--md-outlined-button-icon-offset: -2px;--md-outlined-button-container-shape: var(--radius-sm)}md-outlined-button.active{--md-outlined-button-container-color: #d0e1ff;--md-outlined-button-label-text-color: var(--accent-primary);--md-outlined-button-outline-color: var(--accent-primary);--md-outlined-button-icon-color: var(--accent-primary)}md-outlined-button.danger{--md-outlined-button-label-text-color: var(--accent-danger);--md-outlined-button-outline-color: var(--accent-danger);--md-outlined-button-icon-color: var(--accent-danger)}.toolbar-separator{width:1px;height:var(--icon-button-size);background:var(--border-light)}#searchInput{width:240px;height:var(--button-height);--md-outlined-text-field-container-shape: var(--radius-sm);--md-outlined-text-field-top-space: var(--spacing-xs);--md-outlined-text-field-bottom-space: var(--spacing-xs);--md-outlined-text-field-input-text-size: var(--font-size-sm);--md-outlined-text-field-label-text-size: var(--font-size-sm)}#searchHelpTrigger{--md-icon-button-icon-size: 18px;--md-icon-button-state-layer-height: var(--button-height);--md-icon-button-state-layer-width: var(--button-height);height:var(--button-height);width:var(--button-height)}.search-help-anchor{position:relative;display:inline-flex}#searchHelp{display:none;position:absolute;top:100%;left:0;margin-top:var(--spacing-xs);z-index:var(--z-search-help);border:1px solid var(--border-medium);border-radius:var(--radius-md);padding:10px 14px;box-shadow:0 4px 12px var(--shadow-md);font-size:var(--font-size-sm);line-height:1.5;background:#1e293b;color:#fff}#searchHelp.open{display:block}#searchHelp code{background:#ffffff1f;padding:1px var(--spacing-xs);border-radius:3px}#content{position:fixed;top:var(--toolbar-height);left:0;right:0;bottom:0}#graph{width:100%;height:100%;position:relative}.details-panel{position:absolute;top:var(--spacing-md);left:var(--spacing-md);width:fit-content;max-width:420px;height:auto;min-width:200px;min-height:120px;max-height:min(350px,calc(100% - var(--spacing-lg)));z-index:var(--z-details-panel);background:var(--bg-panel);box-shadow:0 2px 12px var(--shadow-sm);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);display:none;flex-direction:column;overflow:hidden;resize:both}.details-panel.open{display:flex}.details-panel .panel-header{display:flex;align-items:center;height:var(--button-height);padding:0 2px 0 10px;border-bottom:1px solid var(--border-subtle);flex-shrink:0;cursor:grab;-webkit-user-select:none;user-select:none}.details-panel .panel-header:active{cursor:grabbing}.details-panel .panel-title{font-size:var(--font-size-sm);font-weight:500;color:#333;flex:1}.details-panel md-icon-button{--md-icon-button-icon-size: 16px;--md-icon-button-state-layer-height: var(--icon-button-size);--md-icon-button-state-layer-width: var(--icon-button-size);height:var(--icon-button-size);width:var(--icon-button-size)}.details-panel .panel-body{flex:1;overflow-y:auto;min-height:0;padding:var(--spacing-md);font-family:var(--font-family)}#detailsPanel{border-left:3px solid var(--accent-info)}#detailsPanel .panel-header{background:#1a73e80f}#contextMenu{display:none;position:fixed;z-index:var(--z-context-menu);background:var(--bg-panel);border:1px solid var(--border-medium);border-radius:var(--spacing-sm);padding:var(--spacing-xs) 0;box-shadow:0 4px 12px var(--shadow-md);min-width:160px;font-size:var(--font-size-md)}.context-menu-item{padding:var(--spacing-sm) 14px;cursor:pointer;-webkit-user-select:none;user-select:none}.context-menu-item:hover{background:#0000000f}.context-menu-divider{height:1px;margin:var(--spacing-xs) 0;background:var(--border-medium)}#loading-overlay{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:var(--z-loading-overlay);background:#0000008c;color:#fff;font-family:var(--font-family);font-size:14px;padding:8px 18px;border-radius:var(--radius-md);pointer-events:none}#loading-overlay.visible{display:block}.tp-rotv{font-family:var(--font-family)!important;opacity:.9}#settingsPane{max-height:calc(100% - 30px);position:absolute;overflow-y:auto;right:10px;top:10px}#version-badge{position:fixed;bottom:var(--spacing-sm);right:var(--spacing-md);font-size:var(--font-size-xs);color:#0000004d;pointer-events:none;z-index:var(--z-version-badge);font-family:var(--font-family)}#zoom-indicator{position:fixed;bottom:var(--spacing-md);left:var(--spacing-md);z-index:var(--z-zoom-indicator);display:flex;align-items:center;gap:0;background:var(--bg-toolbar);border:1px solid var(--border-subtle);border-radius:var(--radius-md);box-shadow:0 1px 4px var(--shadow-xs);padding:0 var(--spacing-xs)}#zoom-indicator md-icon-button{--md-icon-button-icon-size: 16px;--md-icon-button-state-layer-height: var(--button-height);--md-icon-button-state-layer-width: var(--button-height);height:var(--button-height);width:var(--button-height)}#zoomLevel{font-size:var(--font-size-xs);font-family:var(--font-family);color:#555;min-width:34px;text-align:center;-webkit-user-select:none;user-select:none;cursor:pointer;border-radius:var(--radius-sm);padding:2px var(--spacing-xs);transition:background .1s}#zoomLevel:hover{background:#0000000f}.muted{color:#666}hr{border:none;border-top:1px solid var(--border-subtle);margin:var(--spacing-sm) 0}