procclean 1.2.0__py3-none-any.whl
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.
- procclean/__init__.py +11 -0
- procclean/__main__.py +22 -0
- procclean/cli/__init__.py +27 -0
- procclean/cli/commands.py +213 -0
- procclean/cli/docs.py +234 -0
- procclean/cli/parser.py +272 -0
- procclean/core/__init__.py +47 -0
- procclean/core/actions.py +46 -0
- procclean/core/constants.py +48 -0
- procclean/core/filters.py +121 -0
- procclean/core/memory.py +22 -0
- procclean/core/models.py +27 -0
- procclean/core/process.py +160 -0
- procclean/formatters/__init__.py +33 -0
- procclean/formatters/columns.py +128 -0
- procclean/formatters/output.py +158 -0
- procclean/tui/__init__.py +6 -0
- procclean/tui/app.py +401 -0
- procclean/tui/app.tcss +87 -0
- procclean/tui/screens.py +79 -0
- procclean-1.2.0.dist-info/METADATA +164 -0
- procclean-1.2.0.dist-info/RECORD +24 -0
- procclean-1.2.0.dist-info/WHEEL +4 -0
- procclean-1.2.0.dist-info/entry_points.txt +3 -0
procclean/cli/parser.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""CLI argument parser."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
from procclean.formatters import get_available_columns
|
|
7
|
+
|
|
8
|
+
from .commands import cmd_groups, cmd_kill, cmd_list, cmd_memory
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
12
|
+
"""Create CLI argument parser.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
argparse.ArgumentParser: Configured argument parser for the CLI.
|
|
16
|
+
"""
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="procclean",
|
|
19
|
+
description="Process cleanup tool with TUI and CLI interfaces.",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"-v",
|
|
23
|
+
"--version",
|
|
24
|
+
action="version",
|
|
25
|
+
version=f"%(prog)s {version('procclean')}",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
29
|
+
|
|
30
|
+
# List command
|
|
31
|
+
list_parser = subparsers.add_parser("list", aliases=["ls"], help="List processes")
|
|
32
|
+
list_parser.add_argument(
|
|
33
|
+
"-f",
|
|
34
|
+
"--format",
|
|
35
|
+
choices=["table", "json", "csv", "md"],
|
|
36
|
+
default="table",
|
|
37
|
+
help="Output format (default: table)",
|
|
38
|
+
)
|
|
39
|
+
list_parser.add_argument(
|
|
40
|
+
"-s",
|
|
41
|
+
"--sort",
|
|
42
|
+
choices=["memory", "mem", "cpu", "pid", "name", "cwd"],
|
|
43
|
+
default="memory",
|
|
44
|
+
help="Sort by field (default: memory)",
|
|
45
|
+
)
|
|
46
|
+
list_parser.add_argument(
|
|
47
|
+
"-a",
|
|
48
|
+
"--ascending",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Sort ascending instead of descending",
|
|
51
|
+
)
|
|
52
|
+
list_parser.add_argument(
|
|
53
|
+
"-F",
|
|
54
|
+
"--filter",
|
|
55
|
+
choices=["killable", "orphans", "high-memory"],
|
|
56
|
+
help="Filter preset: killable (orphans, not tmux, not system), "
|
|
57
|
+
"orphans, high-memory",
|
|
58
|
+
)
|
|
59
|
+
list_parser.add_argument(
|
|
60
|
+
"-k",
|
|
61
|
+
"--killable",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="Shorthand for --filter killable",
|
|
64
|
+
)
|
|
65
|
+
list_parser.add_argument(
|
|
66
|
+
"-o",
|
|
67
|
+
"--orphans",
|
|
68
|
+
action="store_true",
|
|
69
|
+
help="Shorthand for --filter orphans",
|
|
70
|
+
)
|
|
71
|
+
list_parser.add_argument(
|
|
72
|
+
"-m",
|
|
73
|
+
"--high-memory",
|
|
74
|
+
action="store_true",
|
|
75
|
+
help="Shorthand for --filter high-memory",
|
|
76
|
+
)
|
|
77
|
+
list_parser.add_argument(
|
|
78
|
+
"--high-memory-threshold",
|
|
79
|
+
type=float,
|
|
80
|
+
default=500.0,
|
|
81
|
+
metavar="MB",
|
|
82
|
+
help="Threshold for high memory filter (default: 500 MB)",
|
|
83
|
+
)
|
|
84
|
+
list_parser.add_argument(
|
|
85
|
+
"--min-memory",
|
|
86
|
+
type=float,
|
|
87
|
+
default=5.0,
|
|
88
|
+
metavar="MB",
|
|
89
|
+
help="Minimum memory to include (default: 5 MB)",
|
|
90
|
+
)
|
|
91
|
+
list_parser.add_argument(
|
|
92
|
+
"-n",
|
|
93
|
+
"--limit",
|
|
94
|
+
type=int,
|
|
95
|
+
metavar="N",
|
|
96
|
+
help="Limit output to N processes",
|
|
97
|
+
)
|
|
98
|
+
list_parser.add_argument(
|
|
99
|
+
"-c",
|
|
100
|
+
"--columns",
|
|
101
|
+
type=str,
|
|
102
|
+
metavar="COLS",
|
|
103
|
+
help=f"Comma-separated columns ({','.join(get_available_columns())})",
|
|
104
|
+
)
|
|
105
|
+
list_parser.add_argument(
|
|
106
|
+
"--cwd",
|
|
107
|
+
nargs="?",
|
|
108
|
+
const="",
|
|
109
|
+
default=None,
|
|
110
|
+
metavar="PATH",
|
|
111
|
+
help="Filter by cwd (no value = current dir, or specify path/glob)",
|
|
112
|
+
)
|
|
113
|
+
list_parser.set_defaults(func=cmd_list)
|
|
114
|
+
|
|
115
|
+
# Groups command
|
|
116
|
+
groups_parser = subparsers.add_parser(
|
|
117
|
+
"groups", aliases=["g"], help="Show process groups"
|
|
118
|
+
)
|
|
119
|
+
groups_parser.add_argument(
|
|
120
|
+
"-f",
|
|
121
|
+
"--format",
|
|
122
|
+
choices=["table", "json"],
|
|
123
|
+
default="table",
|
|
124
|
+
help="Output format (default: table)",
|
|
125
|
+
)
|
|
126
|
+
groups_parser.add_argument(
|
|
127
|
+
"--min-memory",
|
|
128
|
+
type=float,
|
|
129
|
+
default=5.0,
|
|
130
|
+
metavar="MB",
|
|
131
|
+
help="Minimum memory to include (default: 5 MB)",
|
|
132
|
+
)
|
|
133
|
+
groups_parser.set_defaults(func=cmd_groups)
|
|
134
|
+
|
|
135
|
+
# Kill command
|
|
136
|
+
kill_parser = subparsers.add_parser("kill", help="Kill process(es)")
|
|
137
|
+
kill_parser.add_argument(
|
|
138
|
+
"pids",
|
|
139
|
+
type=int,
|
|
140
|
+
nargs="*",
|
|
141
|
+
metavar="PID",
|
|
142
|
+
help="Process ID(s) to kill (or use filters)",
|
|
143
|
+
)
|
|
144
|
+
kill_parser.add_argument(
|
|
145
|
+
"-f",
|
|
146
|
+
"--force",
|
|
147
|
+
action="store_true",
|
|
148
|
+
help="Force kill (SIGKILL instead of SIGTERM)",
|
|
149
|
+
)
|
|
150
|
+
kill_parser.add_argument(
|
|
151
|
+
"-y",
|
|
152
|
+
"--yes",
|
|
153
|
+
action="store_true",
|
|
154
|
+
help="Skip confirmation prompt",
|
|
155
|
+
)
|
|
156
|
+
kill_parser.add_argument(
|
|
157
|
+
"--cwd",
|
|
158
|
+
nargs="?",
|
|
159
|
+
const="",
|
|
160
|
+
default=None,
|
|
161
|
+
metavar="PATH",
|
|
162
|
+
help="Kill processes in cwd (no value = current dir, or specify path/glob)",
|
|
163
|
+
)
|
|
164
|
+
kill_parser.add_argument(
|
|
165
|
+
"-F",
|
|
166
|
+
"--filter",
|
|
167
|
+
choices=["killable", "orphans", "high-memory"],
|
|
168
|
+
help="Filter preset to select processes",
|
|
169
|
+
)
|
|
170
|
+
kill_parser.add_argument(
|
|
171
|
+
"-k",
|
|
172
|
+
"--killable",
|
|
173
|
+
action="store_true",
|
|
174
|
+
help="Shorthand for --filter killable",
|
|
175
|
+
)
|
|
176
|
+
kill_parser.add_argument(
|
|
177
|
+
"-o",
|
|
178
|
+
"--orphans",
|
|
179
|
+
action="store_true",
|
|
180
|
+
help="Shorthand for --filter orphans",
|
|
181
|
+
)
|
|
182
|
+
kill_parser.add_argument(
|
|
183
|
+
"-m",
|
|
184
|
+
"--high-memory",
|
|
185
|
+
action="store_true",
|
|
186
|
+
help="Shorthand for --filter high-memory",
|
|
187
|
+
)
|
|
188
|
+
kill_parser.add_argument(
|
|
189
|
+
"--min-memory",
|
|
190
|
+
type=float,
|
|
191
|
+
default=5.0,
|
|
192
|
+
metavar="MB",
|
|
193
|
+
help="Minimum memory for filter (default: 5 MB)",
|
|
194
|
+
)
|
|
195
|
+
kill_parser.add_argument(
|
|
196
|
+
"--high-memory-threshold",
|
|
197
|
+
type=float,
|
|
198
|
+
default=500.0,
|
|
199
|
+
metavar="MB",
|
|
200
|
+
help="Threshold for high memory filter (default: 500 MB)",
|
|
201
|
+
)
|
|
202
|
+
kill_parser.add_argument(
|
|
203
|
+
"--preview",
|
|
204
|
+
"--dry-run",
|
|
205
|
+
"--dry",
|
|
206
|
+
action="store_true",
|
|
207
|
+
dest="preview",
|
|
208
|
+
help="Show what would be killed without killing",
|
|
209
|
+
)
|
|
210
|
+
kill_parser.add_argument(
|
|
211
|
+
"-O",
|
|
212
|
+
"--out-format",
|
|
213
|
+
choices=["table", "json", "csv", "md"],
|
|
214
|
+
default="table",
|
|
215
|
+
dest="out_format",
|
|
216
|
+
help="Output format for preview (default: table)",
|
|
217
|
+
)
|
|
218
|
+
kill_parser.add_argument(
|
|
219
|
+
"-s",
|
|
220
|
+
"--sort",
|
|
221
|
+
choices=["memory", "mem", "cpu", "pid", "name", "cwd"],
|
|
222
|
+
default=None,
|
|
223
|
+
help="Sort by field for preview",
|
|
224
|
+
)
|
|
225
|
+
kill_parser.add_argument(
|
|
226
|
+
"-n",
|
|
227
|
+
"--limit",
|
|
228
|
+
type=int,
|
|
229
|
+
metavar="N",
|
|
230
|
+
help="Limit preview output to N processes",
|
|
231
|
+
)
|
|
232
|
+
kill_parser.add_argument(
|
|
233
|
+
"-c",
|
|
234
|
+
"--columns",
|
|
235
|
+
type=str,
|
|
236
|
+
metavar="COLS",
|
|
237
|
+
help=f"Comma-separated columns for preview "
|
|
238
|
+
f"({','.join(get_available_columns())})",
|
|
239
|
+
)
|
|
240
|
+
kill_parser.set_defaults(func=cmd_kill)
|
|
241
|
+
|
|
242
|
+
# Memory command
|
|
243
|
+
memory_parser = subparsers.add_parser(
|
|
244
|
+
"memory", aliases=["mem"], help="Show memory summary"
|
|
245
|
+
)
|
|
246
|
+
memory_parser.add_argument(
|
|
247
|
+
"-f",
|
|
248
|
+
"--format",
|
|
249
|
+
choices=["table", "json"],
|
|
250
|
+
default="table",
|
|
251
|
+
help="Output format (default: table)",
|
|
252
|
+
)
|
|
253
|
+
memory_parser.set_defaults(func=cmd_memory)
|
|
254
|
+
|
|
255
|
+
return parser
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def run_cli(args: list[str] | None = None) -> int:
|
|
259
|
+
"""Run CLI with given args (or sys.argv if None).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
int: Exit status code. Returns ``-1`` when no subcommand is provided to
|
|
263
|
+
signal that the TUI should run.
|
|
264
|
+
"""
|
|
265
|
+
parser = create_parser()
|
|
266
|
+
parsed = parser.parse_args(args)
|
|
267
|
+
|
|
268
|
+
if parsed.command is None:
|
|
269
|
+
# No subcommand - return None to signal TUI should run
|
|
270
|
+
return -1
|
|
271
|
+
|
|
272
|
+
return parsed.func(parsed)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Core process analysis functionality."""
|
|
2
|
+
|
|
3
|
+
from .actions import kill_process, kill_processes
|
|
4
|
+
from .constants import (
|
|
5
|
+
CONFIRM_PREVIEW_LIMIT,
|
|
6
|
+
CRITICAL_SERVICES,
|
|
7
|
+
CWD_MAX_WIDTH,
|
|
8
|
+
CWD_TRUNCATE_WIDTH,
|
|
9
|
+
HIGH_MEMORY_THRESHOLD_MB,
|
|
10
|
+
PREVIEW_LIMIT,
|
|
11
|
+
SYSTEM_EXE_PATHS,
|
|
12
|
+
)
|
|
13
|
+
from .filters import (
|
|
14
|
+
filter_by_cwd,
|
|
15
|
+
filter_high_memory,
|
|
16
|
+
filter_killable,
|
|
17
|
+
filter_orphans,
|
|
18
|
+
is_system_service,
|
|
19
|
+
sort_processes,
|
|
20
|
+
)
|
|
21
|
+
from .memory import get_memory_summary
|
|
22
|
+
from .models import ProcessInfo
|
|
23
|
+
from .process import find_similar_processes, get_cwd, get_process_list, get_tmux_env
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"CONFIRM_PREVIEW_LIMIT",
|
|
27
|
+
"CRITICAL_SERVICES",
|
|
28
|
+
"CWD_MAX_WIDTH",
|
|
29
|
+
"CWD_TRUNCATE_WIDTH",
|
|
30
|
+
"HIGH_MEMORY_THRESHOLD_MB",
|
|
31
|
+
"PREVIEW_LIMIT",
|
|
32
|
+
"SYSTEM_EXE_PATHS",
|
|
33
|
+
"ProcessInfo",
|
|
34
|
+
"filter_by_cwd",
|
|
35
|
+
"filter_high_memory",
|
|
36
|
+
"filter_killable",
|
|
37
|
+
"filter_orphans",
|
|
38
|
+
"find_similar_processes",
|
|
39
|
+
"get_cwd",
|
|
40
|
+
"get_memory_summary",
|
|
41
|
+
"get_process_list",
|
|
42
|
+
"get_tmux_env",
|
|
43
|
+
"is_system_service",
|
|
44
|
+
"kill_process",
|
|
45
|
+
"kill_processes",
|
|
46
|
+
"sort_processes",
|
|
47
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Process kill actions."""
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def kill_process(pid: int, force: bool = False) -> tuple[bool, str]:
|
|
7
|
+
"""Kill a process by PID.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
pid: Process ID to kill.
|
|
11
|
+
force: If True, force kill the process; otherwise, terminate gracefully.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
A tuple of (success, message) indicating whether the operation succeeded and
|
|
15
|
+
providing a human-readable message.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
proc = psutil.Process(pid)
|
|
19
|
+
if force:
|
|
20
|
+
proc.kill()
|
|
21
|
+
else:
|
|
22
|
+
proc.terminate()
|
|
23
|
+
return True, f"Process {pid} terminated"
|
|
24
|
+
except psutil.NoSuchProcess:
|
|
25
|
+
return False, f"Process {pid} not found"
|
|
26
|
+
except psutil.AccessDenied:
|
|
27
|
+
return False, f"Access denied for process {pid}"
|
|
28
|
+
except OSError as e:
|
|
29
|
+
return False, f"Error: {e}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def kill_processes(pids: list[int], force: bool = False) -> list[tuple[int, bool, str]]:
|
|
33
|
+
"""Kill multiple processes.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
pids: Process IDs to kill.
|
|
37
|
+
force: If True, force kill the processes; otherwise, terminate gracefully.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A list of tuples (pid, success, message) for each PID attempted.
|
|
41
|
+
"""
|
|
42
|
+
results: list[tuple[int, bool, str]] = []
|
|
43
|
+
for pid in pids:
|
|
44
|
+
success, msg = kill_process(pid, force)
|
|
45
|
+
results.append((pid, success, msg))
|
|
46
|
+
return results
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Constants for process analysis."""
|
|
2
|
+
|
|
3
|
+
# Display constants
|
|
4
|
+
PREVIEW_LIMIT = 5 # Number of processes to show in previews
|
|
5
|
+
CONFIRM_PREVIEW_LIMIT = 10 # Number of processes to show in confirm dialogs
|
|
6
|
+
CWD_MAX_WIDTH = 35 # Max width for cwd column display
|
|
7
|
+
CWD_TRUNCATE_WIDTH = 32 # Width to keep when truncating cwd
|
|
8
|
+
|
|
9
|
+
# Memory thresholds
|
|
10
|
+
HIGH_MEMORY_THRESHOLD_MB = 500 # Default threshold for high memory filter
|
|
11
|
+
|
|
12
|
+
# System library paths - executables here are system services
|
|
13
|
+
SYSTEM_EXE_PATHS = ("/usr/lib", "/usr/libexec", "/lib")
|
|
14
|
+
|
|
15
|
+
# Critical services in /usr/bin that should never be killed
|
|
16
|
+
# (session managers, audio, shells, display, auth)
|
|
17
|
+
CRITICAL_SERVICES = {
|
|
18
|
+
# Display/session
|
|
19
|
+
"gnome-shell",
|
|
20
|
+
"kwin",
|
|
21
|
+
"plasmashell",
|
|
22
|
+
"mutter",
|
|
23
|
+
# Audio
|
|
24
|
+
"pipewire",
|
|
25
|
+
"pipewire-pulse",
|
|
26
|
+
"wireplumber",
|
|
27
|
+
"pulseaudio",
|
|
28
|
+
# Remote sessions
|
|
29
|
+
"tmux: server",
|
|
30
|
+
"tmux",
|
|
31
|
+
"mosh-server",
|
|
32
|
+
# Shells
|
|
33
|
+
"zsh",
|
|
34
|
+
"-zsh",
|
|
35
|
+
"bash",
|
|
36
|
+
"-bash",
|
|
37
|
+
"ssh",
|
|
38
|
+
"sshd",
|
|
39
|
+
# System
|
|
40
|
+
"systemd",
|
|
41
|
+
"init",
|
|
42
|
+
"dbus-daemon",
|
|
43
|
+
"dbus-broker",
|
|
44
|
+
# Desktop services
|
|
45
|
+
"ibus-daemon",
|
|
46
|
+
"gjs",
|
|
47
|
+
"gnome-keyring-daemon",
|
|
48
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Process filtering and sorting utilities."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
|
|
5
|
+
import psutil
|
|
6
|
+
|
|
7
|
+
from .constants import CRITICAL_SERVICES, SYSTEM_EXE_PATHS
|
|
8
|
+
from .models import ProcessInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_system_service(proc: ProcessInfo) -> bool:
|
|
12
|
+
"""Check if process is a system service that shouldn't be killed.
|
|
13
|
+
|
|
14
|
+
Uses two heuristics:
|
|
15
|
+
1. Exe path in system directories (/usr/lib, /usr/libexec)
|
|
16
|
+
2. Name matches critical services list (shells, audio, display)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
True if the process looks like a system/critical service, otherwise False.
|
|
20
|
+
"""
|
|
21
|
+
# Check exe path - most system services live in /usr/lib
|
|
22
|
+
try:
|
|
23
|
+
exe = psutil.Process(proc.pid).exe() or ""
|
|
24
|
+
if exe.startswith(SYSTEM_EXE_PATHS):
|
|
25
|
+
return True
|
|
26
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
# Check critical services by name
|
|
30
|
+
return proc.name.lower() in {s.lower() for s in CRITICAL_SERVICES}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def filter_orphans(procs: list[ProcessInfo]) -> list[ProcessInfo]:
|
|
34
|
+
"""Filter to only orphaned processes.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
procs: List of processes to filter.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Processes that are marked as orphaned.
|
|
41
|
+
"""
|
|
42
|
+
return [p for p in procs if p.is_orphan]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def filter_killable(procs: list[ProcessInfo]) -> list[ProcessInfo]:
|
|
46
|
+
"""Filter to orphaned processes that are safe to kill.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Processes that are:
|
|
50
|
+
- Orphaned (parent is init/systemd)
|
|
51
|
+
- Not running in tmux
|
|
52
|
+
- Not a system service (GNOME, pipewire, etc.)
|
|
53
|
+
"""
|
|
54
|
+
return [p for p in procs if p.is_orphan_candidate and not is_system_service(p)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def filter_high_memory(
|
|
58
|
+
procs: list[ProcessInfo], threshold_mb: float = 500.0
|
|
59
|
+
) -> list[ProcessInfo]:
|
|
60
|
+
"""Filter to processes using more than threshold memory.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
procs: List of processes to filter.
|
|
64
|
+
threshold_mb: Memory threshold in MB.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Processes whose RSS memory usage is greater than threshold_mb.
|
|
68
|
+
"""
|
|
69
|
+
return [p for p in procs if p.rss_mb > threshold_mb]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def filter_by_cwd(procs: list[ProcessInfo], cwd_path: str) -> list[ProcessInfo]:
|
|
73
|
+
"""Filter processes by current working directory.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
procs: List of processes to filter
|
|
77
|
+
cwd_path: Path to match. If contains '*', uses glob matching.
|
|
78
|
+
Otherwise, uses prefix matching.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Processes whose cwd starts with cwd_path (or matches glob pattern)
|
|
82
|
+
"""
|
|
83
|
+
if "*" in cwd_path or "?" in cwd_path:
|
|
84
|
+
# Glob matching
|
|
85
|
+
return [p for p in procs if p.cwd and fnmatch.fnmatch(p.cwd, cwd_path)]
|
|
86
|
+
# Prefix matching (normalized)
|
|
87
|
+
cwd_path = cwd_path.rstrip("/")
|
|
88
|
+
return [
|
|
89
|
+
p
|
|
90
|
+
for p in procs
|
|
91
|
+
if p.cwd
|
|
92
|
+
and p.cwd != "?"
|
|
93
|
+
and (p.cwd == cwd_path or p.cwd.startswith(cwd_path + "/"))
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def sort_processes(
|
|
98
|
+
procs: list[ProcessInfo],
|
|
99
|
+
sort_by: str = "memory",
|
|
100
|
+
reverse: bool = True,
|
|
101
|
+
) -> list[ProcessInfo]:
|
|
102
|
+
"""Sort processes by given key.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
procs: List of processes to sort
|
|
106
|
+
sort_by: One of 'memory', 'cpu', 'pid', 'name', 'cwd'
|
|
107
|
+
reverse: If True, sort descending (default for numeric)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
A new list of processes sorted by the requested key.
|
|
111
|
+
"""
|
|
112
|
+
sort_keys = {
|
|
113
|
+
"memory": lambda p: p.rss_mb,
|
|
114
|
+
"mem": lambda p: p.rss_mb,
|
|
115
|
+
"cpu": lambda p: p.cpu_percent,
|
|
116
|
+
"pid": lambda p: p.pid,
|
|
117
|
+
"name": lambda p: p.name.lower(),
|
|
118
|
+
"cwd": lambda p: p.cwd.lower() if p.cwd else "",
|
|
119
|
+
}
|
|
120
|
+
key_func = sort_keys.get(sort_by, sort_keys["memory"])
|
|
121
|
+
return sorted(procs, key=key_func, reverse=reverse)
|
procclean/core/memory.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Memory summary utilities."""
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_memory_summary() -> dict:
|
|
7
|
+
"""Get system memory summary.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
dict: A dictionary containing total, used, and available memory in GB,
|
|
11
|
+
memory usage percentage, and swap usage/total in GB.
|
|
12
|
+
"""
|
|
13
|
+
mem = psutil.virtual_memory()
|
|
14
|
+
swap = psutil.swap_memory()
|
|
15
|
+
return {
|
|
16
|
+
"total_gb": mem.total / 1024**3,
|
|
17
|
+
"used_gb": mem.used / 1024**3,
|
|
18
|
+
"free_gb": mem.available / 1024**3,
|
|
19
|
+
"percent": mem.percent,
|
|
20
|
+
"swap_used_gb": swap.used / 1024**3,
|
|
21
|
+
"swap_total_gb": swap.total / 1024**3,
|
|
22
|
+
}
|
procclean/core/models.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Process data models."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ProcessInfo:
|
|
8
|
+
"""Process information data class."""
|
|
9
|
+
|
|
10
|
+
pid: int
|
|
11
|
+
name: str
|
|
12
|
+
cmdline: str
|
|
13
|
+
cwd: str
|
|
14
|
+
ppid: int
|
|
15
|
+
parent_name: str
|
|
16
|
+
rss_mb: float
|
|
17
|
+
cpu_percent: float
|
|
18
|
+
username: str
|
|
19
|
+
create_time: float
|
|
20
|
+
is_orphan: bool
|
|
21
|
+
in_tmux: bool
|
|
22
|
+
status: str
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def is_orphan_candidate(self) -> bool:
|
|
26
|
+
"""Check if process is orphaned (PPID=1 or user systemd)."""
|
|
27
|
+
return self.is_orphan and not self.in_tmux
|