procclean 1.2.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/PKG-INFO +164 -0
- procclean-1.2.0/README.md +137 -0
- procclean-1.2.0/pyproject.toml +89 -0
- procclean-1.2.0/src/procclean/__init__.py +11 -0
- procclean-1.2.0/src/procclean/__main__.py +22 -0
- procclean-1.2.0/src/procclean/cli/__init__.py +27 -0
- procclean-1.2.0/src/procclean/cli/commands.py +213 -0
- procclean-1.2.0/src/procclean/cli/docs.py +234 -0
- procclean-1.2.0/src/procclean/cli/parser.py +272 -0
- procclean-1.2.0/src/procclean/core/__init__.py +47 -0
- procclean-1.2.0/src/procclean/core/actions.py +46 -0
- procclean-1.2.0/src/procclean/core/constants.py +48 -0
- procclean-1.2.0/src/procclean/core/filters.py +121 -0
- procclean-1.2.0/src/procclean/core/memory.py +22 -0
- procclean-1.2.0/src/procclean/core/models.py +27 -0
- procclean-1.2.0/src/procclean/core/process.py +160 -0
- procclean-1.2.0/src/procclean/formatters/__init__.py +33 -0
- procclean-1.2.0/src/procclean/formatters/columns.py +128 -0
- procclean-1.2.0/src/procclean/formatters/output.py +158 -0
- procclean-1.2.0/src/procclean/tui/__init__.py +6 -0
- procclean-1.2.0/src/procclean/tui/app.py +401 -0
- procclean-1.2.0/src/procclean/tui/app.tcss +87 -0
- procclean-1.2.0/src/procclean/tui/screens.py +79 -0
procclean-1.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: procclean
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them
|
|
5
|
+
Keywords: cleanup,kill,memory,orphan,process,terminal,tui
|
|
6
|
+
Author: Kaj Kowalski
|
|
7
|
+
Author-email: Kaj Kowalski <info@kajkowalski.nl>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: System :: Monitoring
|
|
18
|
+
Classifier: Topic :: System :: Systems Administration
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Dist: psutil>=7.2.1
|
|
21
|
+
Requires-Dist: tabulate>=0.9.0
|
|
22
|
+
Requires-Dist: textual>=7.0.0
|
|
23
|
+
Requires-Python: >=3.14
|
|
24
|
+
Project-URL: Homepage, https://procclean.kjanat.com
|
|
25
|
+
Project-URL: Repository, https://github.com/kjanat/procclean
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="logo/procclean-transparent.svg" alt="procclean" width="500">
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<em>Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them.</em>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<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
|
+
<img src="https://img.shields.io/badge/python-3.14%2B-blue" alt="Python 3.14+">
|
|
40
|
+
<img src="https://img.shields.io/badge/platform-linux-lightgrey" alt="Linux">
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Memory overview** - Real-time total/used/free/swap display
|
|
46
|
+
- **Multiple views** - All processes, Orphaned, Process Groups, High Memory
|
|
47
|
+
(>500MB)
|
|
48
|
+
- **Orphan detection** - Finds processes whose parent died (PPID=1)
|
|
49
|
+
- **Tmux awareness** - Won't flag tmux processes as orphan candidates
|
|
50
|
+
- **Batch operations** - Select multiple processes and kill them at once
|
|
51
|
+
- **Process grouping** - Find duplicate/similar processes consuming resources
|
|
52
|
+
- **CLI mode** - Scriptable commands with JSON/CSV/Markdown output
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv tool install git+https://github.com/kjanat/procclean
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or run directly:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uvx git+https://github.com/kjanat/procclean
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### TUI Mode (default)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
procclean
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### CLI Commands
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
procclean list # List processes (table)
|
|
78
|
+
procclean list -f json|csv|md # Different output formats
|
|
79
|
+
procclean list -s mem|cpu|pid|name|cwd # Sort by field
|
|
80
|
+
procclean list -o # Orphans only
|
|
81
|
+
procclean list -m # High memory only
|
|
82
|
+
procclean list -k # Killable orphans only
|
|
83
|
+
procclean list --cwd # Filter by current directory
|
|
84
|
+
procclean list --cwd /path/to/dir # Filter by specific cwd
|
|
85
|
+
|
|
86
|
+
procclean groups # Show process groups
|
|
87
|
+
|
|
88
|
+
procclean kill <PID> [PID...] # Kill process(es)
|
|
89
|
+
procclean kill -f <PID> # Force kill (SIGKILL)
|
|
90
|
+
procclean kill --cwd /path -y # Kill all in cwd (skip confirm)
|
|
91
|
+
procclean kill -k -y # Kill all killable orphans
|
|
92
|
+
procclean kill -k --preview # Preview what would be killed
|
|
93
|
+
|
|
94
|
+
procclean mem # Show memory summary
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## TUI Keybindings
|
|
98
|
+
|
|
99
|
+
| Key | Action |
|
|
100
|
+
| ------- | ----------------------- |
|
|
101
|
+
| `q` | Quit |
|
|
102
|
+
| `r` | Refresh |
|
|
103
|
+
| `k` | Kill selected (SIGTERM) |
|
|
104
|
+
| `K` | Force kill (SIGKILL) |
|
|
105
|
+
| `o` | Show orphans |
|
|
106
|
+
| `a` | Show all |
|
|
107
|
+
| `g` | Show groups |
|
|
108
|
+
| `w` | Filter by selected cwd |
|
|
109
|
+
| `W` | Clear cwd filter |
|
|
110
|
+
| `Space` | Toggle selection |
|
|
111
|
+
| `s` | Select all visible |
|
|
112
|
+
| `c` | Clear selection |
|
|
113
|
+
| `1` | Sort by memory |
|
|
114
|
+
| `2` | Sort by CPU |
|
|
115
|
+
| `3` | Sort by PID |
|
|
116
|
+
| `4` | Sort by name |
|
|
117
|
+
| `5` | Sort by cwd |
|
|
118
|
+
| `!` | Reverse sort order |
|
|
119
|
+
|
|
120
|
+
## Views
|
|
121
|
+
|
|
122
|
+
- **All Processes** - All user processes sorted by memory usage
|
|
123
|
+
- **Orphaned** - Processes with PPID=1 (parent died)
|
|
124
|
+
- **Process Groups** - Similar processes grouped together
|
|
125
|
+
- **High Memory** - Processes using >500MB RAM
|
|
126
|
+
|
|
127
|
+
## Output Formats
|
|
128
|
+
|
|
129
|
+
CLI supports multiple output formats via `-f`:
|
|
130
|
+
|
|
131
|
+
- `table` - Human-readable table (default)
|
|
132
|
+
- `json` - JSON array for scripting
|
|
133
|
+
- `csv` - CSV for spreadsheets
|
|
134
|
+
- `md` - Markdown table
|
|
135
|
+
|
|
136
|
+
## Requirements
|
|
137
|
+
|
|
138
|
+
- Python 3.14+
|
|
139
|
+
- Linux (uses `/proc` filesystem)
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
git clone https://github.com/kjanat/procclean
|
|
145
|
+
cd procclean
|
|
146
|
+
uv sync
|
|
147
|
+
uv run pre-commit install
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Run tests:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uv run pytest
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
[MIT][license]
|
|
159
|
+
|
|
160
|
+
<!--link definitions-->
|
|
161
|
+
|
|
162
|
+
[license]: https://github.com/kjanat/procclean/blob/master/LICENSE "MIT License"
|
|
163
|
+
|
|
164
|
+
<!--markdownlint-disable-file MD033 MD041-->
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo/procclean-transparent.svg" alt="procclean" width="500">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<em>Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them.</em>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<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
|
+
<img src="https://img.shields.io/badge/python-3.14%2B-blue" alt="Python 3.14+">
|
|
13
|
+
<img src="https://img.shields.io/badge/platform-linux-lightgrey" alt="Linux">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Memory overview** - Real-time total/used/free/swap display
|
|
19
|
+
- **Multiple views** - All processes, Orphaned, Process Groups, High Memory
|
|
20
|
+
(>500MB)
|
|
21
|
+
- **Orphan detection** - Finds processes whose parent died (PPID=1)
|
|
22
|
+
- **Tmux awareness** - Won't flag tmux processes as orphan candidates
|
|
23
|
+
- **Batch operations** - Select multiple processes and kill them at once
|
|
24
|
+
- **Process grouping** - Find duplicate/similar processes consuming resources
|
|
25
|
+
- **CLI mode** - Scriptable commands with JSON/CSV/Markdown output
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install git+https://github.com/kjanat/procclean
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or run directly:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uvx git+https://github.com/kjanat/procclean
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### TUI Mode (default)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
procclean
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### CLI Commands
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
procclean list # List processes (table)
|
|
51
|
+
procclean list -f json|csv|md # Different output formats
|
|
52
|
+
procclean list -s mem|cpu|pid|name|cwd # Sort by field
|
|
53
|
+
procclean list -o # Orphans only
|
|
54
|
+
procclean list -m # High memory only
|
|
55
|
+
procclean list -k # Killable orphans only
|
|
56
|
+
procclean list --cwd # Filter by current directory
|
|
57
|
+
procclean list --cwd /path/to/dir # Filter by specific cwd
|
|
58
|
+
|
|
59
|
+
procclean groups # Show process groups
|
|
60
|
+
|
|
61
|
+
procclean kill <PID> [PID...] # Kill process(es)
|
|
62
|
+
procclean kill -f <PID> # Force kill (SIGKILL)
|
|
63
|
+
procclean kill --cwd /path -y # Kill all in cwd (skip confirm)
|
|
64
|
+
procclean kill -k -y # Kill all killable orphans
|
|
65
|
+
procclean kill -k --preview # Preview what would be killed
|
|
66
|
+
|
|
67
|
+
procclean mem # Show memory summary
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## TUI Keybindings
|
|
71
|
+
|
|
72
|
+
| Key | Action |
|
|
73
|
+
| ------- | ----------------------- |
|
|
74
|
+
| `q` | Quit |
|
|
75
|
+
| `r` | Refresh |
|
|
76
|
+
| `k` | Kill selected (SIGTERM) |
|
|
77
|
+
| `K` | Force kill (SIGKILL) |
|
|
78
|
+
| `o` | Show orphans |
|
|
79
|
+
| `a` | Show all |
|
|
80
|
+
| `g` | Show groups |
|
|
81
|
+
| `w` | Filter by selected cwd |
|
|
82
|
+
| `W` | Clear cwd filter |
|
|
83
|
+
| `Space` | Toggle selection |
|
|
84
|
+
| `s` | Select all visible |
|
|
85
|
+
| `c` | Clear selection |
|
|
86
|
+
| `1` | Sort by memory |
|
|
87
|
+
| `2` | Sort by CPU |
|
|
88
|
+
| `3` | Sort by PID |
|
|
89
|
+
| `4` | Sort by name |
|
|
90
|
+
| `5` | Sort by cwd |
|
|
91
|
+
| `!` | Reverse sort order |
|
|
92
|
+
|
|
93
|
+
## Views
|
|
94
|
+
|
|
95
|
+
- **All Processes** - All user processes sorted by memory usage
|
|
96
|
+
- **Orphaned** - Processes with PPID=1 (parent died)
|
|
97
|
+
- **Process Groups** - Similar processes grouped together
|
|
98
|
+
- **High Memory** - Processes using >500MB RAM
|
|
99
|
+
|
|
100
|
+
## Output Formats
|
|
101
|
+
|
|
102
|
+
CLI supports multiple output formats via `-f`:
|
|
103
|
+
|
|
104
|
+
- `table` - Human-readable table (default)
|
|
105
|
+
- `json` - JSON array for scripting
|
|
106
|
+
- `csv` - CSV for spreadsheets
|
|
107
|
+
- `md` - Markdown table
|
|
108
|
+
|
|
109
|
+
## Requirements
|
|
110
|
+
|
|
111
|
+
- Python 3.14+
|
|
112
|
+
- Linux (uses `/proc` filesystem)
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
git clone https://github.com/kjanat/procclean
|
|
118
|
+
cd procclean
|
|
119
|
+
uv sync
|
|
120
|
+
uv run pre-commit install
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Run tests:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
uv run pytest
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
[MIT][license]
|
|
132
|
+
|
|
133
|
+
<!--link definitions-->
|
|
134
|
+
|
|
135
|
+
[license]: https://github.com/kjanat/procclean/blob/master/LICENSE "MIT License"
|
|
136
|
+
|
|
137
|
+
<!--markdownlint-disable-file MD033 MD041-->
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#:schema https://json.schemastore.org/pyproject.json
|
|
2
|
+
#:tombi schema.strict = true
|
|
3
|
+
|
|
4
|
+
[project]
|
|
5
|
+
name = "procclean"
|
|
6
|
+
version = "1.2.0"
|
|
7
|
+
description = "Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "Kaj Kowalski", email = "info@kajkowalski.nl" }]
|
|
12
|
+
keywords = ["cleanup", "kill", "memory", "orphan", "process", "terminal", "tui"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: System Administrators",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Topic :: System :: Monitoring",
|
|
23
|
+
"Topic :: System :: Systems Administration",
|
|
24
|
+
"Topic :: Utilities",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"psutil>=7.2.1",
|
|
28
|
+
"tabulate>=0.9.0",
|
|
29
|
+
"textual>=7.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://procclean.kjanat.com"
|
|
34
|
+
Repository = "https://github.com/kjanat/procclean"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
procclean = "procclean:main"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"pre-commit>=4.5.1",
|
|
42
|
+
]
|
|
43
|
+
lint = [
|
|
44
|
+
"pylint>=4.0.4",
|
|
45
|
+
"ruff>=0.14.10",
|
|
46
|
+
"tombi>=0.7.14",
|
|
47
|
+
"ty>=0.0.8",
|
|
48
|
+
]
|
|
49
|
+
mypy = [
|
|
50
|
+
"mypy>=1.19.1",
|
|
51
|
+
"types-psutil>=7.2.1.20251231",
|
|
52
|
+
"types-tabulate>=0.9.0.20241207",
|
|
53
|
+
]
|
|
54
|
+
site = [
|
|
55
|
+
"markdown-it-py[plugins]>=4.0.0",
|
|
56
|
+
"mkdocs>=1.6.1",
|
|
57
|
+
"mkdocs-material[imaging]>=9.7.1",
|
|
58
|
+
"mkdocs-rich-argparse",
|
|
59
|
+
]
|
|
60
|
+
test = [
|
|
61
|
+
"pytest>=9.0.2",
|
|
62
|
+
"pytest-asyncio>=1.3.0",
|
|
63
|
+
"pytest-cov>=7.0.0",
|
|
64
|
+
"pytest-xdist[psutil]>=3.8.0",
|
|
65
|
+
]
|
|
66
|
+
tui = [
|
|
67
|
+
"textual-dev>=1.8.0",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[build-system]
|
|
71
|
+
requires = ["uv_build>=0.9.21,<0.10.0"]
|
|
72
|
+
build-backend = "uv_build"
|
|
73
|
+
|
|
74
|
+
[tool.pytest]
|
|
75
|
+
addopts = ["-n", "logical"]
|
|
76
|
+
asyncio_mode = "strict"
|
|
77
|
+
|
|
78
|
+
[tool.uv]
|
|
79
|
+
environments = ["sys_platform == 'linux'"]
|
|
80
|
+
default-groups = "all"
|
|
81
|
+
|
|
82
|
+
[tool.uv.sources]
|
|
83
|
+
mkdocs-rich-argparse = { git = "https://github.com/kjanat/mkdocs_rich_argparse.git", branch = "prog-style" }
|
|
84
|
+
|
|
85
|
+
[[tool.uv.index]]
|
|
86
|
+
name = "testpypi"
|
|
87
|
+
url = "https://test.pypi.org/simple/"
|
|
88
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
89
|
+
explicit = true
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Process cleanup TUI application."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
__version__ = version("procclean")
|
|
6
|
+
|
|
7
|
+
# Re-export main entry point and core types
|
|
8
|
+
from procclean.__main__ import main
|
|
9
|
+
from procclean.core import ProcessInfo
|
|
10
|
+
|
|
11
|
+
__all__ = ["ProcessInfo", "__version__", "main"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Entry point for procclean - runs as python -m procclean or via console script."""
|
|
2
|
+
|
|
3
|
+
from .cli import run_cli
|
|
4
|
+
from .tui import ProcessCleanerApp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
"""Dispatch to CLI or run TUI.
|
|
9
|
+
|
|
10
|
+
Raises:
|
|
11
|
+
SystemExit: When CLI command returns non-zero exit code.
|
|
12
|
+
"""
|
|
13
|
+
result = run_cli()
|
|
14
|
+
if result == -1:
|
|
15
|
+
# No subcommand - run TUI
|
|
16
|
+
ProcessCleanerApp().run()
|
|
17
|
+
else:
|
|
18
|
+
raise SystemExit(result)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""CLI interface for procclean."""
|
|
2
|
+
|
|
3
|
+
# Internal helpers - exported for testing
|
|
4
|
+
from .commands import (
|
|
5
|
+
_confirm_kill,
|
|
6
|
+
_do_preview,
|
|
7
|
+
_get_kill_targets,
|
|
8
|
+
cmd_groups,
|
|
9
|
+
cmd_kill,
|
|
10
|
+
cmd_list,
|
|
11
|
+
cmd_memory,
|
|
12
|
+
get_filtered_processes,
|
|
13
|
+
)
|
|
14
|
+
from .parser import create_parser, run_cli
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"_confirm_kill",
|
|
18
|
+
"_do_preview",
|
|
19
|
+
"_get_kill_targets",
|
|
20
|
+
"cmd_groups",
|
|
21
|
+
"cmd_kill",
|
|
22
|
+
"cmd_list",
|
|
23
|
+
"cmd_memory",
|
|
24
|
+
"create_parser",
|
|
25
|
+
"get_filtered_processes",
|
|
26
|
+
"run_cli",
|
|
27
|
+
]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""CLI command handlers."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich import print # pylint: disable=redefined-builtin
|
|
9
|
+
|
|
10
|
+
from procclean.core import (
|
|
11
|
+
PREVIEW_LIMIT,
|
|
12
|
+
filter_by_cwd,
|
|
13
|
+
filter_high_memory,
|
|
14
|
+
filter_killable,
|
|
15
|
+
filter_orphans,
|
|
16
|
+
find_similar_processes,
|
|
17
|
+
get_memory_summary,
|
|
18
|
+
get_process_list,
|
|
19
|
+
kill_processes,
|
|
20
|
+
sort_processes,
|
|
21
|
+
)
|
|
22
|
+
from procclean.formatters import format_output
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
26
|
+
"""List processes command.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
int: Exit code (0 on success).
|
|
30
|
+
"""
|
|
31
|
+
procs = get_filtered_processes(args)
|
|
32
|
+
|
|
33
|
+
# Apply sorting
|
|
34
|
+
reverse = not args.ascending
|
|
35
|
+
procs = sort_processes(procs, sort_by=args.sort, reverse=reverse)
|
|
36
|
+
|
|
37
|
+
# Limit output
|
|
38
|
+
if args.limit:
|
|
39
|
+
procs = procs[: args.limit]
|
|
40
|
+
|
|
41
|
+
# Parse columns
|
|
42
|
+
columns = args.columns.split(",") if args.columns else None
|
|
43
|
+
|
|
44
|
+
print(format_output(procs, args.format, columns=columns))
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cmd_groups(args: argparse.Namespace) -> int:
|
|
49
|
+
"""Show grouped processes command.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
int: Exit code (0 on success).
|
|
53
|
+
"""
|
|
54
|
+
procs = get_process_list(min_memory_mb=args.min_memory)
|
|
55
|
+
groups = find_similar_processes(procs)
|
|
56
|
+
|
|
57
|
+
if not groups:
|
|
58
|
+
print("No process groups found.")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
if args.format == "json":
|
|
62
|
+
data = {
|
|
63
|
+
cmd: [
|
|
64
|
+
{"pid": p.pid, "name": p.name, "rss_mb": round(p.rss_mb, 2)}
|
|
65
|
+
for p in group_procs
|
|
66
|
+
]
|
|
67
|
+
for cmd, group_procs in groups.items()
|
|
68
|
+
}
|
|
69
|
+
print(json.dumps(data, indent=2))
|
|
70
|
+
else:
|
|
71
|
+
for cmd, group_procs in sorted(
|
|
72
|
+
groups.items(), key=lambda x: sum(p.rss_mb for p in x[1]), reverse=True
|
|
73
|
+
):
|
|
74
|
+
total_mb = sum(p.rss_mb for p in group_procs)
|
|
75
|
+
print(f"\n{cmd} ({len(group_procs)} processes, {total_mb:.1f} MB total)")
|
|
76
|
+
for p in sorted(group_procs, key=lambda x: x.rss_mb, reverse=True):
|
|
77
|
+
print(f" PID {p.pid}: {p.rss_mb:.1f} MB")
|
|
78
|
+
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_filtered_processes(args: argparse.Namespace) -> list:
|
|
83
|
+
"""Get processes with all filters from args applied.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
list: Filtered list of processes.
|
|
87
|
+
"""
|
|
88
|
+
procs = get_process_list(min_memory_mb=getattr(args, "min_memory", 5.0))
|
|
89
|
+
|
|
90
|
+
# Apply cwd filter
|
|
91
|
+
if getattr(args, "cwd", None) is not None:
|
|
92
|
+
cwd_path = args.cwd or str(Path.cwd())
|
|
93
|
+
procs = filter_by_cwd(procs, cwd_path)
|
|
94
|
+
|
|
95
|
+
# Apply preset filters
|
|
96
|
+
filt = getattr(args, "filter", None)
|
|
97
|
+
threshold = getattr(args, "high_memory_threshold", 500.0)
|
|
98
|
+
if filt == "killable" or getattr(args, "killable", False):
|
|
99
|
+
procs = filter_killable(procs)
|
|
100
|
+
elif filt == "orphans" or getattr(args, "orphans", False):
|
|
101
|
+
procs = filter_orphans(procs)
|
|
102
|
+
elif filt == "high-memory" or getattr(args, "high_memory", False):
|
|
103
|
+
procs = filter_high_memory(procs, threshold_mb=threshold)
|
|
104
|
+
|
|
105
|
+
return procs
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _get_kill_targets(args: argparse.Namespace) -> list:
|
|
109
|
+
"""Get target processes for kill command from PIDs or filters.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
list: Target processes to kill.
|
|
113
|
+
"""
|
|
114
|
+
if args.pids:
|
|
115
|
+
all_procs = get_process_list(min_memory_mb=0)
|
|
116
|
+
pid_set = set(args.pids)
|
|
117
|
+
procs = [p for p in all_procs if p.pid in pid_set]
|
|
118
|
+
found_pids = {p.pid for p in procs}
|
|
119
|
+
for pid in args.pids:
|
|
120
|
+
if pid not in found_pids:
|
|
121
|
+
print(f"Warning: PID {pid} not found")
|
|
122
|
+
return procs
|
|
123
|
+
return get_filtered_processes(args)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _do_preview(args: argparse.Namespace, procs: list) -> int:
|
|
127
|
+
"""Show preview of what would be killed.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
int: Exit code (0 on success).
|
|
131
|
+
"""
|
|
132
|
+
if hasattr(args, "sort") and args.sort:
|
|
133
|
+
procs = sort_processes(procs, sort_by=args.sort, reverse=True)
|
|
134
|
+
if hasattr(args, "limit") and args.limit:
|
|
135
|
+
procs = procs[: args.limit]
|
|
136
|
+
columns = args.columns.split(",") if getattr(args, "columns", None) else None
|
|
137
|
+
fmt = getattr(args, "out_format", "table")
|
|
138
|
+
print(format_output(procs, fmt, columns=columns))
|
|
139
|
+
print(f"\n{len(procs)} process(es) would be killed.")
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _confirm_kill(args: argparse.Namespace, procs: list) -> bool:
|
|
144
|
+
"""Prompt for kill confirmation.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
args: Parsed CLI arguments.
|
|
148
|
+
procs: Processes that would be killed.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True if the kill action is confirmed (or confirmation is skipped), otherwise
|
|
152
|
+
False.
|
|
153
|
+
"""
|
|
154
|
+
if args.yes or not sys.stdin.isatty():
|
|
155
|
+
return True
|
|
156
|
+
action = "FORCE KILL" if args.force else "terminate"
|
|
157
|
+
print(f"About to {action} {len(procs)} process(es):")
|
|
158
|
+
for p in procs[:PREVIEW_LIMIT]:
|
|
159
|
+
print(f" {p.pid}: {p.name} ({p.rss_mb:.1f} MB)")
|
|
160
|
+
if len(procs) > PREVIEW_LIMIT:
|
|
161
|
+
print(f" ... and {len(procs) - PREVIEW_LIMIT} more")
|
|
162
|
+
try:
|
|
163
|
+
response = input("Continue? [y/N] ")
|
|
164
|
+
return response.lower() in {"y", "yes"}
|
|
165
|
+
except EOFError:
|
|
166
|
+
return True # Non-interactive
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cmd_kill(args: argparse.Namespace) -> int:
|
|
170
|
+
"""Kill processes command.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
int: Exit code (0 on success).
|
|
174
|
+
"""
|
|
175
|
+
procs = _get_kill_targets(args)
|
|
176
|
+
if not procs:
|
|
177
|
+
print("No processes match the filters.")
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
if getattr(args, "preview", False):
|
|
181
|
+
return _do_preview(args, procs)
|
|
182
|
+
|
|
183
|
+
if not _confirm_kill(args, procs):
|
|
184
|
+
print("Aborted.")
|
|
185
|
+
return 1
|
|
186
|
+
|
|
187
|
+
results = kill_processes([p.pid for p in procs], force=args.force)
|
|
188
|
+
exit_code = 0
|
|
189
|
+
for _, success, msg in results:
|
|
190
|
+
status = "OK" if success else "FAILED"
|
|
191
|
+
print(f"[{status}] {msg}")
|
|
192
|
+
if not success:
|
|
193
|
+
exit_code = 1
|
|
194
|
+
return exit_code
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def cmd_memory(args: argparse.Namespace) -> int:
|
|
198
|
+
"""Show memory summary command.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
int: Exit code (0 on success).
|
|
202
|
+
"""
|
|
203
|
+
mem = get_memory_summary()
|
|
204
|
+
|
|
205
|
+
if args.format == "json":
|
|
206
|
+
print(json.dumps(mem, indent=2))
|
|
207
|
+
else:
|
|
208
|
+
print(f"Total: {mem['total_gb']:.2f} GB")
|
|
209
|
+
print(f"Used: {mem['used_gb']:.2f} GB ({mem['percent']:.1f}%)")
|
|
210
|
+
print(f"Free: {mem['free_gb']:.2f} GB")
|
|
211
|
+
print(f"Swap: {mem['swap_used_gb']:.2f} / {mem['swap_total_gb']:.2f} GB")
|
|
212
|
+
|
|
213
|
+
return 0
|