lsoph 0.0.1__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.
- lsoph-0.0.1/LICENSE.md +7 -0
- lsoph-0.0.1/PKG-INFO +92 -0
- lsoph-0.0.1/README.md +65 -0
- lsoph-0.0.1/pyproject.toml +48 -0
- lsoph-0.0.1/src/lsoph/__init__.py +0 -0
- lsoph-0.0.1/src/lsoph/backend/__init__.py +0 -0
- lsoph-0.0.1/src/lsoph/backend/lsof.py +329 -0
- lsoph-0.0.1/src/lsoph/backend/psutil.py +357 -0
- lsoph-0.0.1/src/lsoph/backend/strace.py +604 -0
- lsoph-0.0.1/src/lsoph/backend/strace_cmd.py +426 -0
- lsoph-0.0.1/src/lsoph/cli.py +218 -0
- lsoph-0.0.1/src/lsoph/monitor.py +566 -0
- lsoph-0.0.1/src/lsoph/ui/__init__.py +0 -0
- lsoph-0.0.1/src/lsoph/ui/app.py +464 -0
- lsoph-0.0.1/src/lsoph/ui/detail_screen.py +84 -0
- lsoph-0.0.1/src/lsoph/ui/log_screen.py +98 -0
- lsoph-0.0.1/src/lsoph/util/__init__.py +0 -0
- lsoph-0.0.1/src/lsoph/util/pid.py +147 -0
- lsoph-0.0.1/src/lsoph/util/short_path.py +83 -0
- lsoph-0.0.1/src/lsoph/util/versioned.py +61 -0
lsoph-0.0.1/LICENSE.md
ADDED
lsoph-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lsoph
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: An interactive lsof alternative
|
|
5
|
+
Author-email: Gareth Davidson <gaz@bitplane.net>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: Public Domain
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
License-File: LICENSE.md
|
|
13
|
+
Requires-Dist: psutil~=7.0
|
|
14
|
+
Requires-Dist: textual~=3.1
|
|
15
|
+
Requires-Dist: flake8 ; extra == "dev"
|
|
16
|
+
Requires-Dist: pre-commit ; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest ; extra == "dev"
|
|
18
|
+
Requires-Dist: coverage ; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov ; extra == "dev"
|
|
20
|
+
Requires-Dist: build ; extra == "dev"
|
|
21
|
+
Requires-Dist: twine ; extra == "dev"
|
|
22
|
+
Project-URL: Bug Tracker, https://github.com/bitplane/lsoph/issues
|
|
23
|
+
Project-URL: Homepage, https://bitplane.net/dev/python/lsoph
|
|
24
|
+
Project-URL: Source Code, https://github.com/bitplane/lsoph
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
|
|
27
|
+
# ðĪ `lsoph` - list open files using genai slop!
|
|
28
|
+
|
|
29
|
+
TUI that lists open files for a given process.
|
|
30
|
+
|
|
31
|
+
usage:
|
|
32
|
+
|
|
33
|
+
```shell
|
|
34
|
+
uvx lsoph
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Why?
|
|
38
|
+
Because I thought it'd only take a couple of hours, and I'd heard good
|
|
39
|
+
things about Gemini 2.5 Pro, so did it as a coding test.
|
|
40
|
+
|
|
41
|
+
It descended into madness over the course of a weekend. Vibe coders,
|
|
42
|
+
I never realised how bad you have it!
|
|
43
|
+
|
|
44
|
+
### Gemini 2.5 Pro
|
|
45
|
+
|
|
46
|
+
* â Writes more code than a Java consultancy that's paid by LoC.
|
|
47
|
+
* ðĪĄ Defends against every type of exception, even import errors; belt,
|
|
48
|
+
braces and elasticated waist.
|
|
49
|
+
* ð Its trousers still fall down.
|
|
50
|
+
* ð§ą Hard codes special cases and unreachable logic.
|
|
51
|
+
* ðĨ Will put verbose debug logging in your hottest loops.
|
|
52
|
+
* ð Starts at the complexity ceiling, and manages to climb higher with
|
|
53
|
+
every change.
|
|
54
|
+
* â
It needs to be BEST CORRECT, with the pig-headed stubbornness of
|
|
55
|
+
`class UnwaveringPigsHead(basemodel)`.
|
|
56
|
+
* ð Leaves passive aggressive comments in your code if you abuse it enough,
|
|
57
|
+
and doesn't like to tidy up.
|
|
58
|
+
* ðŠĶ It can't write test cases, or testable code.
|
|
59
|
+
* ðĢ Carried by an enormous context window and rapid generation speed,
|
|
60
|
+
then the wheels come off.
|
|
61
|
+
|
|
62
|
+
### GPT 4o and 4.5
|
|
63
|
+
|
|
64
|
+
* ðĐ Can't take the volume of dogshit produced by Gemini (but to be fair who
|
|
65
|
+
can?)
|
|
66
|
+
* ðĪ Gets lazy because it's got no context window left, or because Sama is
|
|
67
|
+
saving all his GPUs. Probably both.
|
|
68
|
+
* ðĨą Attention slips, it forgets where its up to and then hallucinates all
|
|
69
|
+
the details.
|
|
70
|
+
* ðĪĨ Sycophantmaxxer, but still ignores your requests.
|
|
71
|
+
* ð Can actually write unit tests.
|
|
72
|
+
* ðŽ Has actually stopped being such an aggressively "safety focused" PR
|
|
73
|
+
bellend.
|
|
74
|
+
* ð A classic case of being down with the kids, a move that's absolute chefs
|
|
75
|
+
kiss.
|
|
76
|
+
|
|
77
|
+
### Claude 3.7
|
|
78
|
+
|
|
79
|
+
* ðŦ It has none of the tools that GPT has, none of the mental models that
|
|
80
|
+
Gemini has.
|
|
81
|
+
* ð― Still pisses all over them from a great height.
|
|
82
|
+
* ð Decent eye for aesthetics.
|
|
83
|
+
* ðŠ Has a better window size than GPT, and can focus attention better too.
|
|
84
|
+
* ð Mostly does as its told.
|
|
85
|
+
* ðĐ Still can't write good code.
|
|
86
|
+
* ðĪ No banter game whatsoever.
|
|
87
|
+
|
|
88
|
+
## Summary
|
|
89
|
+
|
|
90
|
+
In the kingdom of the token generators, the one-eyed Claude is king.
|
|
91
|
+
|
|
92
|
+
|
lsoph-0.0.1/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ðĪ `lsoph` - list open files using genai slop!
|
|
2
|
+
|
|
3
|
+
TUI that lists open files for a given process.
|
|
4
|
+
|
|
5
|
+
usage:
|
|
6
|
+
|
|
7
|
+
```shell
|
|
8
|
+
uvx lsoph
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Why?
|
|
12
|
+
Because I thought it'd only take a couple of hours, and I'd heard good
|
|
13
|
+
things about Gemini 2.5 Pro, so did it as a coding test.
|
|
14
|
+
|
|
15
|
+
It descended into madness over the course of a weekend. Vibe coders,
|
|
16
|
+
I never realised how bad you have it!
|
|
17
|
+
|
|
18
|
+
### Gemini 2.5 Pro
|
|
19
|
+
|
|
20
|
+
* â Writes more code than a Java consultancy that's paid by LoC.
|
|
21
|
+
* ðĪĄ Defends against every type of exception, even import errors; belt,
|
|
22
|
+
braces and elasticated waist.
|
|
23
|
+
* ð Its trousers still fall down.
|
|
24
|
+
* ð§ą Hard codes special cases and unreachable logic.
|
|
25
|
+
* ðĨ Will put verbose debug logging in your hottest loops.
|
|
26
|
+
* ð Starts at the complexity ceiling, and manages to climb higher with
|
|
27
|
+
every change.
|
|
28
|
+
* â
It needs to be BEST CORRECT, with the pig-headed stubbornness of
|
|
29
|
+
`class UnwaveringPigsHead(basemodel)`.
|
|
30
|
+
* ð Leaves passive aggressive comments in your code if you abuse it enough,
|
|
31
|
+
and doesn't like to tidy up.
|
|
32
|
+
* ðŠĶ It can't write test cases, or testable code.
|
|
33
|
+
* ðĢ Carried by an enormous context window and rapid generation speed,
|
|
34
|
+
then the wheels come off.
|
|
35
|
+
|
|
36
|
+
### GPT 4o and 4.5
|
|
37
|
+
|
|
38
|
+
* ðĐ Can't take the volume of dogshit produced by Gemini (but to be fair who
|
|
39
|
+
can?)
|
|
40
|
+
* ðĪ Gets lazy because it's got no context window left, or because Sama is
|
|
41
|
+
saving all his GPUs. Probably both.
|
|
42
|
+
* ðĨą Attention slips, it forgets where its up to and then hallucinates all
|
|
43
|
+
the details.
|
|
44
|
+
* ðĪĨ Sycophantmaxxer, but still ignores your requests.
|
|
45
|
+
* ð Can actually write unit tests.
|
|
46
|
+
* ðŽ Has actually stopped being such an aggressively "safety focused" PR
|
|
47
|
+
bellend.
|
|
48
|
+
* ð A classic case of being down with the kids, a move that's absolute chefs
|
|
49
|
+
kiss.
|
|
50
|
+
|
|
51
|
+
### Claude 3.7
|
|
52
|
+
|
|
53
|
+
* ðŦ It has none of the tools that GPT has, none of the mental models that
|
|
54
|
+
Gemini has.
|
|
55
|
+
* ð― Still pisses all over them from a great height.
|
|
56
|
+
* ð Decent eye for aesthetics.
|
|
57
|
+
* ðŠ Has a better window size than GPT, and can focus attention better too.
|
|
58
|
+
* ð Mostly does as its told.
|
|
59
|
+
* ðĐ Still can't write good code.
|
|
60
|
+
* ðĪ No banter game whatsoever.
|
|
61
|
+
|
|
62
|
+
## Summary
|
|
63
|
+
|
|
64
|
+
In the kingdom of the token generators, the one-eyed Claude is king.
|
|
65
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "lsoph"
|
|
3
|
+
description = "An interactive lsof alternative"
|
|
4
|
+
version = "0.0.1"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Gareth Davidson", email = "gaz@bitplane.net" }
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"License :: Public Domain", # WTFPL
|
|
14
|
+
"Operating System :: POSIX :: Linux",
|
|
15
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
"psutil~=7.0",
|
|
20
|
+
"textual~=3.1"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"flake8",
|
|
26
|
+
"pre-commit",
|
|
27
|
+
"pytest",
|
|
28
|
+
"coverage",
|
|
29
|
+
"pytest-cov",
|
|
30
|
+
"build",
|
|
31
|
+
"twine"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
"Homepage" = "https://bitplane.net/dev/python/lsoph"
|
|
36
|
+
"Source Code" = "https://github.com/bitplane/lsoph"
|
|
37
|
+
"Bug Tracker" = "https://github.com/bitplane/lsoph/issues"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
build-backend = "flit_core.buildapi"
|
|
41
|
+
requires = ["flit_core >=3.2,<4"]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
lsoph = "lsoph.cli:main"
|
|
45
|
+
|
|
46
|
+
[tool.isort]
|
|
47
|
+
profile = "black"
|
|
48
|
+
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
from typing import Dict, Iterator, List, Optional, Set, Tuple
|
|
7
|
+
|
|
8
|
+
from lsoph.monitor import Monitor
|
|
9
|
+
from lsoph.util.pid import get_descendants
|
|
10
|
+
|
|
11
|
+
# Setup logging - using the same pattern as other modules
|
|
12
|
+
logging.basicConfig(
|
|
13
|
+
level=os.environ.get("LOGLEVEL", "WARNING").upper(),
|
|
14
|
+
format="%(levelname)s:%(name)s:%(message)s",
|
|
15
|
+
)
|
|
16
|
+
log = logging.getLogger("lsoph.backend.lsof") # Use package-aware logger name
|
|
17
|
+
|
|
18
|
+
# Regular expressions for parsing lsof output
|
|
19
|
+
# Format: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
|
20
|
+
LSOF_LINE_RE = re.compile(
|
|
21
|
+
r"^(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$"
|
|
22
|
+
)
|
|
23
|
+
FD_TYPE_RE = re.compile(r"(\d+)([rwu])?")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _parse_fd(fd_str: str) -> Tuple[Optional[int], str]:
|
|
27
|
+
"""Parse the FD column from lsof output."""
|
|
28
|
+
if fd_str in ("cwd", "rtd", "txt", "mem"):
|
|
29
|
+
log.debug(f"Special FD type: {fd_str}")
|
|
30
|
+
return None, fd_str
|
|
31
|
+
|
|
32
|
+
match = FD_TYPE_RE.match(fd_str)
|
|
33
|
+
if match:
|
|
34
|
+
fd = int(match.group(1))
|
|
35
|
+
mode = match.group(2) or ""
|
|
36
|
+
log.debug(f"Parsed FD {fd} with mode '{mode}'")
|
|
37
|
+
return fd, mode
|
|
38
|
+
|
|
39
|
+
log.debug(f"Unparsable FD string: {fd_str}")
|
|
40
|
+
return None, fd_str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _run_lsof(pids: Optional[List[int]] = None) -> Iterator[Dict]:
|
|
44
|
+
"""Run lsof and yield parsed output lines."""
|
|
45
|
+
cmd = ["lsof", "-n", "-F", "pcftn"] # p=pid, c=command, f=fd, t=type, n=name
|
|
46
|
+
|
|
47
|
+
if pids:
|
|
48
|
+
cmd.extend(["-p", ",".join(map(str, pids))])
|
|
49
|
+
|
|
50
|
+
log.info(f"Running lsof command: {' '.join(cmd)}")
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Redirect stderr to /dev/null to suppress tracefs warnings
|
|
54
|
+
with open(os.devnull, "w") as devnull:
|
|
55
|
+
proc = subprocess.Popen(
|
|
56
|
+
cmd, stdout=subprocess.PIPE, stderr=devnull, text=True
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
current_record = {}
|
|
60
|
+
for line in proc.stdout:
|
|
61
|
+
line = line.strip()
|
|
62
|
+
if not line:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
field_type = line[0]
|
|
66
|
+
value = line[1:]
|
|
67
|
+
|
|
68
|
+
if field_type == "p" and current_record:
|
|
69
|
+
# New process, yield previous record
|
|
70
|
+
yield current_record
|
|
71
|
+
current_record = {"pid": int(value)}
|
|
72
|
+
elif field_type == "p":
|
|
73
|
+
current_record = {"pid": int(value)}
|
|
74
|
+
elif field_type == "c":
|
|
75
|
+
current_record["command"] = value
|
|
76
|
+
elif field_type == "f":
|
|
77
|
+
current_record["fd_str"] = value
|
|
78
|
+
fd, mode = _parse_fd(value)
|
|
79
|
+
current_record["fd"] = fd
|
|
80
|
+
current_record["mode"] = mode
|
|
81
|
+
elif field_type == "t":
|
|
82
|
+
current_record["type"] = value
|
|
83
|
+
elif field_type == "n":
|
|
84
|
+
current_record["path"] = value
|
|
85
|
+
log.debug(
|
|
86
|
+
f"Complete record: PID {current_record.get('pid')}, FD {current_record.get('fd')}, path {value}"
|
|
87
|
+
)
|
|
88
|
+
yield current_record
|
|
89
|
+
current_record = {
|
|
90
|
+
"pid": current_record["pid"],
|
|
91
|
+
"command": current_record["command"],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Yield final record if any
|
|
95
|
+
if current_record and "path" in current_record:
|
|
96
|
+
log.debug(f"Final record: {current_record}")
|
|
97
|
+
yield current_record
|
|
98
|
+
|
|
99
|
+
except subprocess.SubprocessError as e:
|
|
100
|
+
log.error(f"Error running lsof: {e}")
|
|
101
|
+
except Exception as e:
|
|
102
|
+
log.exception(f"Unexpected error in _run_lsof: {e}")
|
|
103
|
+
finally:
|
|
104
|
+
if proc.stdout:
|
|
105
|
+
proc.stdout.close()
|
|
106
|
+
exit_code = proc.wait()
|
|
107
|
+
log.debug(f"lsof process exited with code: {exit_code}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _process_lsof_data(record: Dict, monitor: Monitor, timestamp: float) -> None:
|
|
111
|
+
"""Process a parsed lsof record and update the monitor."""
|
|
112
|
+
pid = record.get("pid")
|
|
113
|
+
path = record.get("path")
|
|
114
|
+
fd = record.get("fd")
|
|
115
|
+
|
|
116
|
+
if not (pid and path):
|
|
117
|
+
log.debug(f"Incomplete record missing pid or path: {record}")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# If fd is None, this is a special file like cwd, not an open fd
|
|
121
|
+
if fd is None:
|
|
122
|
+
fd_str = record.get("fd_str", "unknown")
|
|
123
|
+
log.debug(f"Special file {fd_str} for PID {pid}: {path}")
|
|
124
|
+
monitor.stat(pid, path, True, timestamp)
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# For regular files, add as open
|
|
128
|
+
log.debug(f"Processing open file: PID {pid}, FD {fd}, path {path}")
|
|
129
|
+
monitor.open(pid, path, fd, True, timestamp)
|
|
130
|
+
|
|
131
|
+
# Check mode to determine if it's being read/written
|
|
132
|
+
mode = record.get("mode", "")
|
|
133
|
+
if "r" in mode:
|
|
134
|
+
log.debug(f"File has read mode: PID {pid}, FD {fd}")
|
|
135
|
+
monitor.read(pid, fd, path, True, timestamp)
|
|
136
|
+
if "w" in mode:
|
|
137
|
+
log.debug(f"File has write mode: PID {pid}, FD {fd}")
|
|
138
|
+
monitor.write(pid, fd, path, True, timestamp)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _attach_with_descendants(pids: List[int], monitor: Monitor) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Attach to processes, their descendants, and monitor their open files using lsof.
|
|
144
|
+
This version tracks child processes by periodically checking for descendants.
|
|
145
|
+
"""
|
|
146
|
+
log.info(f"Attaching to initial PIDs: {pids}")
|
|
147
|
+
|
|
148
|
+
# Keep track of all PIDs we're monitoring (including descendants)
|
|
149
|
+
all_pids = set(pids)
|
|
150
|
+
|
|
151
|
+
# Keep track of file descriptors we've seen for each PID
|
|
152
|
+
# to detect closures on subsequent runs
|
|
153
|
+
seen_fds: Dict[int, Set[Tuple[int, str]]] = {}
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
poll_interval = 1.0 # seconds
|
|
157
|
+
child_check_interval = 5.0 # check for new children every 5 polls
|
|
158
|
+
poll_count = 0
|
|
159
|
+
|
|
160
|
+
log.info(f"Starting lsof polling with interval: {poll_interval} seconds")
|
|
161
|
+
|
|
162
|
+
while True:
|
|
163
|
+
timestamp = time.time()
|
|
164
|
+
poll_count += 1
|
|
165
|
+
|
|
166
|
+
# Check for descendants periodically
|
|
167
|
+
if poll_count % child_check_interval < 1:
|
|
168
|
+
log.debug(f"Checking for new child processes of {pids}")
|
|
169
|
+
new_pids = set()
|
|
170
|
+
for parent_pid in pids:
|
|
171
|
+
try:
|
|
172
|
+
descendants = get_descendants(parent_pid)
|
|
173
|
+
for child_pid in descendants:
|
|
174
|
+
if child_pid not in all_pids:
|
|
175
|
+
log.info(f"Found new child process: {child_pid}")
|
|
176
|
+
new_pids.add(child_pid)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
log.debug(
|
|
179
|
+
f"Error checking descendants for PID {parent_pid}: {e}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Add any new PIDs to our tracking set
|
|
183
|
+
if new_pids:
|
|
184
|
+
log.info(
|
|
185
|
+
f"Adding {len(new_pids)} new child processes to monitoring"
|
|
186
|
+
)
|
|
187
|
+
all_pids.update(new_pids)
|
|
188
|
+
|
|
189
|
+
# Convert set back to list for lsof
|
|
190
|
+
current_pids = list(all_pids)
|
|
191
|
+
current_fds: Dict[int, Set[Tuple[int, str]]] = {}
|
|
192
|
+
|
|
193
|
+
record_count = 0
|
|
194
|
+
for record in _run_lsof(current_pids):
|
|
195
|
+
record_count += 1
|
|
196
|
+
pid = record.get("pid")
|
|
197
|
+
fd = record.get("fd")
|
|
198
|
+
path = record.get("path")
|
|
199
|
+
|
|
200
|
+
if pid and fd is not None and path:
|
|
201
|
+
if pid not in current_fds:
|
|
202
|
+
current_fds[pid] = set()
|
|
203
|
+
current_fds[pid].add((fd, path))
|
|
204
|
+
|
|
205
|
+
_process_lsof_data(record, monitor, timestamp)
|
|
206
|
+
|
|
207
|
+
log.debug(f"Processed {record_count} lsof records")
|
|
208
|
+
|
|
209
|
+
# Detect closed files (present in seen_fds but not in current_fds)
|
|
210
|
+
close_count = 0
|
|
211
|
+
for pid, fd_paths in seen_fds.items():
|
|
212
|
+
current_pid_fds = current_fds.get(pid, set())
|
|
213
|
+
for fd, path in fd_paths:
|
|
214
|
+
if (fd, path) not in current_pid_fds:
|
|
215
|
+
log.debug(
|
|
216
|
+
f"Detected closed file: PID {pid}, FD {fd}, path {path}"
|
|
217
|
+
)
|
|
218
|
+
monitor.close(pid, fd, True, timestamp)
|
|
219
|
+
close_count += 1
|
|
220
|
+
|
|
221
|
+
if close_count > 0:
|
|
222
|
+
log.debug(f"Detected {close_count} closed files")
|
|
223
|
+
|
|
224
|
+
seen_fds = current_fds
|
|
225
|
+
log.debug(f"Sleeping for {poll_interval} seconds")
|
|
226
|
+
time.sleep(poll_interval)
|
|
227
|
+
|
|
228
|
+
except KeyboardInterrupt:
|
|
229
|
+
log.info("lsof monitoring interrupted by user")
|
|
230
|
+
except Exception as e:
|
|
231
|
+
log.exception(f"Unexpected error in lsof attach: {e}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def attach(pids: List[int], monitor: Monitor) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Standard attach function which monitors the specified PIDs without tracking descendants.
|
|
237
|
+
For command execution with descendants tracking, use _attach_with_descendants.
|
|
238
|
+
"""
|
|
239
|
+
log.info(f"Attaching to PIDs: {pids}")
|
|
240
|
+
|
|
241
|
+
# Keep track of file descriptors we've seen for each PID
|
|
242
|
+
# to detect closures on subsequent runs
|
|
243
|
+
seen_fds: Dict[int, Set[Tuple[int, str]]] = {}
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
poll_interval = 1.0 # seconds
|
|
247
|
+
log.info(f"Starting lsof polling with interval: {poll_interval} seconds")
|
|
248
|
+
|
|
249
|
+
while True:
|
|
250
|
+
timestamp = time.time()
|
|
251
|
+
current_fds: Dict[int, Set[Tuple[int, str]]] = {}
|
|
252
|
+
|
|
253
|
+
record_count = 0
|
|
254
|
+
for record in _run_lsof(pids):
|
|
255
|
+
record_count += 1
|
|
256
|
+
pid = record.get("pid")
|
|
257
|
+
fd = record.get("fd")
|
|
258
|
+
path = record.get("path")
|
|
259
|
+
|
|
260
|
+
if pid and fd is not None and path:
|
|
261
|
+
if pid not in current_fds:
|
|
262
|
+
current_fds[pid] = set()
|
|
263
|
+
current_fds[pid].add((fd, path))
|
|
264
|
+
|
|
265
|
+
_process_lsof_data(record, monitor, timestamp)
|
|
266
|
+
|
|
267
|
+
log.debug(f"Processed {record_count} lsof records")
|
|
268
|
+
|
|
269
|
+
# Detect closed files (present in seen_fds but not in current_fds)
|
|
270
|
+
close_count = 0
|
|
271
|
+
for pid, fd_paths in seen_fds.items():
|
|
272
|
+
current_pid_fds = current_fds.get(pid, set())
|
|
273
|
+
for fd, path in fd_paths:
|
|
274
|
+
if (fd, path) not in current_pid_fds:
|
|
275
|
+
log.debug(
|
|
276
|
+
f"Detected closed file: PID {pid}, FD {fd}, path {path}"
|
|
277
|
+
)
|
|
278
|
+
monitor.close(pid, fd, True, timestamp)
|
|
279
|
+
close_count += 1
|
|
280
|
+
|
|
281
|
+
if close_count > 0:
|
|
282
|
+
log.debug(f"Detected {close_count} closed files")
|
|
283
|
+
|
|
284
|
+
seen_fds = current_fds
|
|
285
|
+
log.debug(f"Sleeping for {poll_interval} seconds")
|
|
286
|
+
time.sleep(poll_interval)
|
|
287
|
+
|
|
288
|
+
except KeyboardInterrupt:
|
|
289
|
+
log.info("lsof monitoring interrupted by user")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
log.exception(f"Unexpected error in lsof attach: {e}")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def run(command: List[str], monitor: Monitor) -> None:
|
|
295
|
+
"""Run a command and monitor its open files using lsof."""
|
|
296
|
+
log.info(f"Running command: {' '.join(command)}")
|
|
297
|
+
|
|
298
|
+
proc = None
|
|
299
|
+
try:
|
|
300
|
+
log.debug("Launching subprocess")
|
|
301
|
+
proc = subprocess.Popen(command)
|
|
302
|
+
pid = proc.pid
|
|
303
|
+
log.info(f"Command started with PID: {pid}")
|
|
304
|
+
|
|
305
|
+
# Track the parent process and all its descendants
|
|
306
|
+
pids = [pid]
|
|
307
|
+
# Use a custom attach function to track descendants
|
|
308
|
+
_attach_with_descendants(pids, monitor)
|
|
309
|
+
|
|
310
|
+
except subprocess.SubprocessError as e:
|
|
311
|
+
log.error(f"Error launching command: {e}")
|
|
312
|
+
except Exception as e:
|
|
313
|
+
log.exception(f"Unexpected error in lsof run: {e}")
|
|
314
|
+
finally:
|
|
315
|
+
if proc and proc.poll() is None:
|
|
316
|
+
log.info(f"Terminating command process (PID: {proc.pid})")
|
|
317
|
+
proc.terminate()
|
|
318
|
+
try:
|
|
319
|
+
proc.wait(timeout=1)
|
|
320
|
+
log.debug(
|
|
321
|
+
f"Process terminated gracefully with exit code: {proc.returncode}"
|
|
322
|
+
)
|
|
323
|
+
except subprocess.TimeoutExpired:
|
|
324
|
+
log.debug(
|
|
325
|
+
f"Process did not terminate gracefully, killing PID {proc.pid}"
|
|
326
|
+
)
|
|
327
|
+
proc.kill()
|
|
328
|
+
proc.wait()
|
|
329
|
+
log.debug(f"Process killed with exit code: {proc.returncode}")
|