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.
Files changed (23) hide show
  1. {procclean-1.2.0 → procclean-1.3.0}/PKG-INFO +13 -6
  2. {procclean-1.2.0 → procclean-1.3.0}/README.md +12 -5
  3. {procclean-1.2.0 → procclean-1.3.0}/pyproject.toml +2 -1
  4. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/__init__.py +10 -1
  5. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/filters.py +15 -0
  6. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/models.py +1 -0
  7. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/process.py +29 -5
  8. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/tui/app.py +12 -2
  9. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/__init__.py +0 -0
  10. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/__main__.py +0 -0
  11. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/cli/__init__.py +0 -0
  12. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/cli/commands.py +0 -0
  13. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/cli/docs.py +0 -0
  14. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/cli/parser.py +0 -0
  15. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/actions.py +0 -0
  16. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/constants.py +0 -0
  17. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/core/memory.py +0 -0
  18. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/formatters/__init__.py +0 -0
  19. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/formatters/columns.py +0 -0
  20. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/formatters/output.py +0 -0
  21. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/tui/__init__.py +0 -0
  22. {procclean-1.2.0 → procclean-1.3.0}/src/procclean/tui/app.tcss +0 -0
  23. {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.2.0
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://github.com/kjanat/procclean/releases"><img src="https://img.shields.io/github/v/release/kjanat/procclean" alt="Release"></a>
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
- uv tool install git+https://github.com/kjanat/procclean
58
+ pip install procclean
58
59
  ```
59
60
 
60
- Or run directly:
61
+ Or with [uv](https://docs.astral.sh/uv/):
61
62
 
62
63
  ```bash
63
- uvx git+https://github.com/kjanat/procclean
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://github.com/kjanat/procclean/releases"><img src="https://img.shields.io/github/v/release/kjanat/procclean" alt="Release"></a>
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
- uv tool install git+https://github.com/kjanat/procclean
31
+ pip install procclean
31
32
  ```
32
33
 
33
- Or run directly:
34
+ Or with [uv](https://docs.astral.sh/uv/):
34
35
 
35
36
  ```bash
36
- uvx git+https://github.com/kjanat/procclean
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.2.0"
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 find_similar_processes, get_cwd, get_process_list, get_tmux_env
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
 
@@ -20,6 +20,7 @@ class ProcessInfo:
20
20
  is_orphan: bool
21
21
  in_tmux: bool
22
22
  status: str
23
+ exe_deleted: bool = False # True if executable was deleted/updated
23
24
 
24
25
  @property
25
26
  def is_orphan_candidate(self) -> bool:
@@ -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 (parent is init/systemd)
97
- is_orphan = ppid == 1 or parent_name in {"systemd", "init"}
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=info["pid"],
128
+ pid=pid,
106
129
  name=info["name"],
107
130
  cmdline=cmdline,
108
- cwd=get_cwd(info["pid"]),
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(info["pid"]) if is_orphan else False,
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
- status = f"{proc.status}{orphan_marker}{tmux_marker}"
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"