procclean 1.2.0__tar.gz → 1.3.0__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.
- {procclean-1.2.0 → procclean-1.3.0}/PKG-INFO +13 -6
- {procclean-1.2.0 → procclean-1.3.0}/README.md +12 -5
- {procclean-1.2.0 → procclean-1.3.0}/pyproject.toml +2 -1
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/__init__.py +10 -1
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/filters.py +15 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/models.py +1 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/process.py +29 -5
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/tui/app.py +12 -2
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/__init__.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/__main__.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/cli/__init__.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/cli/commands.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/cli/docs.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/cli/parser.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/actions.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/constants.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/memory.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/formatters/__init__.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/formatters/columns.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/formatters/output.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/tui/__init__.py +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/tui/app.tcss +0 -0
- {procclean-1.2.0 → procclean-1.3.0}/src/procclean/tui/screens.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: procclean
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them
|
|
5
5
|
Keywords: cleanup,kill,memory,orphan,process,terminal,tui
|
|
6
6
|
Author: Kaj Kowalski
|
|
@@ -26,7 +26,7 @@ Project-URL: Repository, https://github.com/kjanat/procclean
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
28
|
<p align="center">
|
|
29
|
-
<img src="logo/procclean-transparent.svg" alt="procclean" width="500">
|
|
29
|
+
<img src="https://raw.github.com/kjanat/procclean/master/logo/procclean-transparent.svg" alt="procclean" width="500">
|
|
30
30
|
</p>
|
|
31
31
|
|
|
32
32
|
<p align="center">
|
|
@@ -34,8 +34,9 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
</p>
|
|
35
35
|
|
|
36
36
|
<p align="center">
|
|
37
|
+
<a href="https://pypi.org/project/procclean/"><img src="https://img.shields.io/pypi/v/procclean" alt="PyPI"></a>
|
|
37
38
|
<a href="https://github.com/kjanat/procclean/blob/master/LICENSE"><img src="https://img.shields.io/github/license/kjanat/procclean" alt="License"></a>
|
|
38
|
-
<a href="https://
|
|
39
|
+
<a href="https://procclean.kjanat.com"><img src="https://img.shields.io/badge/docs-mkdocs-blue" alt="Docs"></a>
|
|
39
40
|
<img src="https://img.shields.io/badge/python-3.14%2B-blue" alt="Python 3.14+">
|
|
40
41
|
<img src="https://img.shields.io/badge/platform-linux-lightgrey" alt="Linux">
|
|
41
42
|
</p>
|
|
@@ -54,13 +55,19 @@ Description-Content-Type: text/markdown
|
|
|
54
55
|
## Installation
|
|
55
56
|
|
|
56
57
|
```bash
|
|
57
|
-
|
|
58
|
+
pip install procclean
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
Or
|
|
61
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
|
-
|
|
64
|
+
uv tool install procclean
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Run without installing:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uvx procclean
|
|
64
71
|
```
|
|
65
72
|
|
|
66
73
|
## Usage
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="logo/procclean-transparent.svg" alt="procclean" width="500">
|
|
2
|
+
<img src="https://raw.github.com/kjanat/procclean/master/logo/procclean-transparent.svg" alt="procclean" width="500">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
+
<a href="https://pypi.org/project/procclean/"><img src="https://img.shields.io/pypi/v/procclean" alt="PyPI"></a>
|
|
10
11
|
<a href="https://github.com/kjanat/procclean/blob/master/LICENSE"><img src="https://img.shields.io/github/license/kjanat/procclean" alt="License"></a>
|
|
11
|
-
<a href="https://
|
|
12
|
+
<a href="https://procclean.kjanat.com"><img src="https://img.shields.io/badge/docs-mkdocs-blue" alt="Docs"></a>
|
|
12
13
|
<img src="https://img.shields.io/badge/python-3.14%2B-blue" alt="Python 3.14+">
|
|
13
14
|
<img src="https://img.shields.io/badge/platform-linux-lightgrey" alt="Linux">
|
|
14
15
|
</p>
|
|
@@ -27,13 +28,19 @@
|
|
|
27
28
|
## Installation
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
|
-
|
|
31
|
+
pip install procclean
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
Or
|
|
34
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
|
-
|
|
37
|
+
uv tool install procclean
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Run without installing:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uvx procclean
|
|
37
44
|
```
|
|
38
45
|
|
|
39
46
|
## Usage
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "procclean"
|
|
6
|
-
version = "1.
|
|
6
|
+
version = "1.3.0"
|
|
7
7
|
description = "Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.14"
|
|
@@ -41,6 +41,7 @@ dev = [
|
|
|
41
41
|
"pre-commit>=4.5.1",
|
|
42
42
|
]
|
|
43
43
|
lint = [
|
|
44
|
+
"dprint-py>=0.51.1.0",
|
|
44
45
|
"pylint>=4.0.4",
|
|
45
46
|
"ruff>=0.14.10",
|
|
46
47
|
"tombi>=0.7.14",
|
|
@@ -15,12 +15,19 @@ from .filters import (
|
|
|
15
15
|
filter_high_memory,
|
|
16
16
|
filter_killable,
|
|
17
17
|
filter_orphans,
|
|
18
|
+
filter_stale,
|
|
18
19
|
is_system_service,
|
|
19
20
|
sort_processes,
|
|
20
21
|
)
|
|
21
22
|
from .memory import get_memory_summary
|
|
22
23
|
from .models import ProcessInfo
|
|
23
|
-
from .process import
|
|
24
|
+
from .process import (
|
|
25
|
+
find_similar_processes,
|
|
26
|
+
get_cwd,
|
|
27
|
+
get_process_list,
|
|
28
|
+
get_tmux_env,
|
|
29
|
+
is_exe_deleted,
|
|
30
|
+
)
|
|
24
31
|
|
|
25
32
|
__all__ = [
|
|
26
33
|
"CONFIRM_PREVIEW_LIMIT",
|
|
@@ -35,11 +42,13 @@ __all__ = [
|
|
|
35
42
|
"filter_high_memory",
|
|
36
43
|
"filter_killable",
|
|
37
44
|
"filter_orphans",
|
|
45
|
+
"filter_stale",
|
|
38
46
|
"find_similar_processes",
|
|
39
47
|
"get_cwd",
|
|
40
48
|
"get_memory_summary",
|
|
41
49
|
"get_process_list",
|
|
42
50
|
"get_tmux_env",
|
|
51
|
+
"is_exe_deleted",
|
|
43
52
|
"is_system_service",
|
|
44
53
|
"kill_process",
|
|
45
54
|
"kill_processes",
|
|
@@ -69,6 +69,21 @@ def filter_high_memory(
|
|
|
69
69
|
return [p for p in procs if p.rss_mb > threshold_mb]
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
def filter_stale(procs: list[ProcessInfo]) -> list[ProcessInfo]:
|
|
73
|
+
"""Filter to processes with deleted/updated executables.
|
|
74
|
+
|
|
75
|
+
These are processes running outdated binaries after a package update.
|
|
76
|
+
Common on rolling release distros (Arch, Manjaro) after system updates.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
procs: List of processes to filter.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Processes whose executable has been deleted or replaced.
|
|
83
|
+
"""
|
|
84
|
+
return [p for p in procs if p.exe_deleted]
|
|
85
|
+
|
|
86
|
+
|
|
72
87
|
def filter_by_cwd(procs: list[ProcessInfo], cwd_path: str) -> list[ProcessInfo]:
|
|
73
88
|
"""Filter processes by current working directory.
|
|
74
89
|
|
|
@@ -44,6 +44,26 @@ def get_cwd(pid: int) -> str:
|
|
|
44
44
|
return "?"
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def is_exe_deleted(pid: int) -> bool:
|
|
48
|
+
"""Check if process executable has been deleted or updated.
|
|
49
|
+
|
|
50
|
+
This happens when a package is updated while the process is running.
|
|
51
|
+
The process continues with the old binary in memory, but the exe symlink
|
|
52
|
+
shows "(deleted)" suffix.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
pid: Process ID.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if the executable file was deleted/updated, False otherwise.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
exe_link = Path(f"/proc/{pid}/exe").readlink()
|
|
62
|
+
return str(exe_link).endswith("(deleted)")
|
|
63
|
+
except (PermissionError, FileNotFoundError, ProcessLookupError):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
47
67
|
def get_process_list(
|
|
48
68
|
sort_by: str = "memory",
|
|
49
69
|
filter_user: str | None = None,
|
|
@@ -93,19 +113,22 @@ def get_process_list(
|
|
|
93
113
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
94
114
|
parent_name = "?"
|
|
95
115
|
|
|
96
|
-
# Check if orphaned (
|
|
97
|
-
|
|
116
|
+
# Check if orphaned (reparented to PID 1 system init)
|
|
117
|
+
# Note:
|
|
118
|
+
# ppid != 1 with parent "systemd" means user session service, NOT orphan
|
|
119
|
+
is_orphan = ppid == 1
|
|
98
120
|
|
|
99
121
|
cmdline = " ".join(info["cmdline"] or [])[:200]
|
|
100
122
|
if not cmdline:
|
|
101
123
|
cmdline = info["name"]
|
|
102
124
|
|
|
125
|
+
pid = info["pid"]
|
|
103
126
|
processes.append(
|
|
104
127
|
ProcessInfo(
|
|
105
|
-
pid=
|
|
128
|
+
pid=pid,
|
|
106
129
|
name=info["name"],
|
|
107
130
|
cmdline=cmdline,
|
|
108
|
-
cwd=get_cwd(
|
|
131
|
+
cwd=get_cwd(pid),
|
|
109
132
|
ppid=ppid,
|
|
110
133
|
parent_name=parent_name,
|
|
111
134
|
rss_mb=rss_mb,
|
|
@@ -113,8 +136,9 @@ def get_process_list(
|
|
|
113
136
|
username=info["username"],
|
|
114
137
|
create_time=info["create_time"] or 0,
|
|
115
138
|
is_orphan=is_orphan,
|
|
116
|
-
in_tmux=get_tmux_env(
|
|
139
|
+
in_tmux=get_tmux_env(pid) if is_orphan else False,
|
|
117
140
|
status=info["status"] or "?",
|
|
141
|
+
exe_deleted=is_exe_deleted(pid),
|
|
118
142
|
)
|
|
119
143
|
)
|
|
120
144
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
@@ -32,7 +32,7 @@ from procclean.core import (
|
|
|
32
32
|
from .screens import ConfirmKillScreen
|
|
33
33
|
|
|
34
34
|
# Type aliases
|
|
35
|
-
ViewType = Literal["all", "orphans", "groups", "high-mem"]
|
|
35
|
+
ViewType = Literal["all", "orphans", "stale", "groups", "high-mem"]
|
|
36
36
|
SortKey = Literal["memory", "cpu", "pid", "name", "cwd"]
|
|
37
37
|
|
|
38
38
|
|
|
@@ -53,6 +53,7 @@ class ProcessCleanerApp(App):
|
|
|
53
53
|
Binding("k", "kill_selected", "Kill"),
|
|
54
54
|
Binding("K", "force_kill_selected", "Force Kill"),
|
|
55
55
|
Binding("o", "show_orphans", "Orphans"),
|
|
56
|
+
Binding("t", "show_stale", "Stale"),
|
|
56
57
|
Binding("a", "show_all", "All"),
|
|
57
58
|
Binding("g", "show_groups", "Groups"),
|
|
58
59
|
Binding("w", "filter_cwd", "Filter CWD"),
|
|
@@ -93,6 +94,7 @@ class ProcessCleanerApp(App):
|
|
|
93
94
|
yield OptionList(
|
|
94
95
|
Option("All Processes", id="view-all"),
|
|
95
96
|
Option("Orphaned", id="view-orphans"),
|
|
97
|
+
Option("Stale (Updated)", id="view-stale"),
|
|
96
98
|
Option("Process Groups", id="view-groups"),
|
|
97
99
|
Option("High Memory (>500MB)", id="view-high-mem"),
|
|
98
100
|
id="view-selector",
|
|
@@ -184,6 +186,8 @@ class ProcessCleanerApp(App):
|
|
|
184
186
|
|
|
185
187
|
if self.current_view == "orphans":
|
|
186
188
|
procs = [p for p in self.processes if p.is_orphan]
|
|
189
|
+
elif self.current_view == "stale":
|
|
190
|
+
procs = [p for p in self.processes if p.exe_deleted]
|
|
187
191
|
elif self.current_view == "high-mem":
|
|
188
192
|
procs = [p for p in self.processes if p.rss_mb > HIGH_MEMORY_THRESHOLD_MB]
|
|
189
193
|
elif self.current_view == "groups":
|
|
@@ -205,7 +209,8 @@ class ProcessCleanerApp(App):
|
|
|
205
209
|
selected = "[X]" if proc.pid in self.selected_pids else "[ ]"
|
|
206
210
|
orphan_marker = " [orphan]" if proc.is_orphan else ""
|
|
207
211
|
tmux_marker = " [tmux]" if proc.in_tmux else ""
|
|
208
|
-
|
|
212
|
+
stale_marker = " [stale]" if proc.exe_deleted else ""
|
|
213
|
+
status = f"{proc.status}{orphan_marker}{tmux_marker}{stale_marker}"
|
|
209
214
|
|
|
210
215
|
cwd = proc.cwd or "?"
|
|
211
216
|
if len(cwd) > CWD_MAX_WIDTH:
|
|
@@ -240,6 +245,7 @@ class ProcessCleanerApp(App):
|
|
|
240
245
|
view_map: dict[str, ViewType] = {
|
|
241
246
|
"view-all": "all",
|
|
242
247
|
"view-orphans": "orphans",
|
|
248
|
+
"view-stale": "stale",
|
|
243
249
|
"view-groups": "groups",
|
|
244
250
|
"view-high-mem": "high-mem",
|
|
245
251
|
}
|
|
@@ -306,6 +312,10 @@ class ProcessCleanerApp(App):
|
|
|
306
312
|
"""Switch to orphans view."""
|
|
307
313
|
self.current_view = "orphans"
|
|
308
314
|
|
|
315
|
+
def action_show_stale(self) -> None:
|
|
316
|
+
"""Switch to stale processes view (deleted/updated executables)."""
|
|
317
|
+
self.current_view = "stale"
|
|
318
|
+
|
|
309
319
|
def action_show_all(self) -> None:
|
|
310
320
|
"""Switch to all processes view."""
|
|
311
321
|
self.current_view = "all"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|