myspace-cli 1.5.0__tar.gz → 1.6.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.
- {myspace_cli-1.5.0 → myspace_cli-1.6.0}/PKG-INFO +4 -1
- {myspace_cli-1.5.0 → myspace_cli-1.6.0}/README.md +3 -0
- {myspace_cli-1.5.0 → myspace_cli-1.6.0}/pyproject.toml +6 -3
- {myspace_cli-1.5.0 → myspace_cli-1.6.0/src}/myspace_cli.egg-info/PKG-INFO +4 -1
- myspace_cli-1.6.0/src/myspace_cli.egg-info/SOURCES.txt +12 -0
- myspace_cli-1.6.0/src/space_cli/__init__.py +10 -0
- myspace_cli-1.6.0/src/space_cli/index_store.py +93 -0
- myspace_cli-1.6.0/src/space_cli/space_analyzer.py +419 -0
- myspace_cli-1.6.0/src/space_cli/space_cli.py +632 -0
- myspace_cli-1.5.0/myspace_cli.egg-info/SOURCES.txt +0 -9
- myspace_cli-1.5.0/space_cli.py +0 -293
- {myspace_cli-1.5.0 → myspace_cli-1.6.0}/setup.cfg +0 -0
- {myspace_cli-1.5.0 → myspace_cli-1.6.0/src}/myspace_cli.egg-info/dependency_links.txt +0 -0
- {myspace_cli-1.5.0 → myspace_cli-1.6.0/src}/myspace_cli.egg-info/entry_points.txt +0 -0
- {myspace_cli-1.5.0 → myspace_cli-1.6.0/src}/myspace_cli.egg-info/requires.txt +0 -0
- {myspace_cli-1.5.0 → myspace_cli-1.6.0/src}/myspace_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: myspace-cli
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.6.0
|
4
4
|
Summary: A macOS disk space analysis CLI: health, index, app usage, big files.
|
5
5
|
Author-email: Your Name <you@example.com>
|
6
6
|
License: MIT
|
@@ -61,6 +61,9 @@ python3 -m space_cli
|
|
61
61
|
# 如果要使用高级的功能,请使用更复杂的命令行参数,可以运行help
|
62
62
|
space-cli --help
|
63
63
|
|
64
|
+
# 升级到最新的版本
|
65
|
+
pip install --upgrade myspace-cli
|
66
|
+
|
64
67
|
```
|
65
68
|
|
66
69
|
### 方法2:直接使用
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "myspace-cli"
|
7
|
-
version = "1.
|
7
|
+
version = "1.6.0"
|
8
8
|
description = "A macOS disk space analysis CLI: health, index, app usage, big files."
|
9
9
|
readme = "README.md"
|
10
10
|
requires-python = ">=3.8"
|
@@ -26,8 +26,11 @@ Issues = "https://github.com/kennyz/space-cli/issues"
|
|
26
26
|
[project.scripts]
|
27
27
|
space-cli = "space_cli:main"
|
28
28
|
|
29
|
-
[tool.setuptools]
|
30
|
-
|
29
|
+
[tool.setuptools.packages.find]
|
30
|
+
where = ["src"]
|
31
|
+
|
32
|
+
[tool.setuptools.package-dir]
|
33
|
+
"" = "src"
|
31
34
|
|
32
35
|
[project.optional-dependencies]
|
33
36
|
mcp = ["mcp>=1,<2"]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: myspace-cli
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.6.0
|
4
4
|
Summary: A macOS disk space analysis CLI: health, index, app usage, big files.
|
5
5
|
Author-email: Your Name <you@example.com>
|
6
6
|
License: MIT
|
@@ -61,6 +61,9 @@ python3 -m space_cli
|
|
61
61
|
# 如果要使用高级的功能,请使用更复杂的命令行参数,可以运行help
|
62
62
|
space-cli --help
|
63
63
|
|
64
|
+
# 升级到最新的版本
|
65
|
+
pip install --upgrade myspace-cli
|
66
|
+
|
64
67
|
```
|
65
68
|
|
66
69
|
### 方法2:直接使用
|
@@ -0,0 +1,12 @@
|
|
1
|
+
README.md
|
2
|
+
pyproject.toml
|
3
|
+
src/myspace_cli.egg-info/PKG-INFO
|
4
|
+
src/myspace_cli.egg-info/SOURCES.txt
|
5
|
+
src/myspace_cli.egg-info/dependency_links.txt
|
6
|
+
src/myspace_cli.egg-info/entry_points.txt
|
7
|
+
src/myspace_cli.egg-info/requires.txt
|
8
|
+
src/myspace_cli.egg-info/top_level.txt
|
9
|
+
src/space_cli/__init__.py
|
10
|
+
src/space_cli/index_store.py
|
11
|
+
src/space_cli/space_analyzer.py
|
12
|
+
src/space_cli/space_cli.py
|
@@ -0,0 +1,93 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
"""
|
4
|
+
IndexStore - 目录大小索引缓存管理器
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import json
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import List, Tuple, Dict
|
11
|
+
from datetime import datetime, timedelta
|
12
|
+
|
13
|
+
|
14
|
+
class IndexStore:
|
15
|
+
"""简单的目录大小索引缓存管理器"""
|
16
|
+
|
17
|
+
def __init__(self, index_file: str = None):
|
18
|
+
home = str(Path.home())
|
19
|
+
cache_dir = os.path.join(home, ".spacecli")
|
20
|
+
os.makedirs(cache_dir, exist_ok=True)
|
21
|
+
self.index_file = index_file or os.path.join(cache_dir, "index.json")
|
22
|
+
self._data: Dict = {}
|
23
|
+
self._loaded = False
|
24
|
+
|
25
|
+
def load(self) -> None:
|
26
|
+
if self._loaded:
|
27
|
+
return
|
28
|
+
if os.path.exists(self.index_file):
|
29
|
+
try:
|
30
|
+
with open(self.index_file, "r", encoding="utf-8") as f:
|
31
|
+
self._data = json.load(f)
|
32
|
+
except Exception:
|
33
|
+
self._data = {}
|
34
|
+
self._loaded = True
|
35
|
+
|
36
|
+
def save(self) -> None:
|
37
|
+
try:
|
38
|
+
with open(self.index_file, "w", encoding="utf-8") as f:
|
39
|
+
json.dump(self._data, f, ensure_ascii=False, indent=2)
|
40
|
+
except Exception:
|
41
|
+
pass
|
42
|
+
|
43
|
+
def _key(self, root_path: str) -> str:
|
44
|
+
return os.path.abspath(root_path)
|
45
|
+
|
46
|
+
def get(self, root_path: str) -> Dict:
|
47
|
+
self.load()
|
48
|
+
return self._data.get(self._key(root_path))
|
49
|
+
|
50
|
+
def set(self, root_path: str, entries: List[Tuple[str, int]]) -> None:
|
51
|
+
self.load()
|
52
|
+
now_iso = datetime.utcnow().isoformat()
|
53
|
+
self._data[self._key(root_path)] = {
|
54
|
+
"updated_at": now_iso,
|
55
|
+
"entries": [{"path": p, "size": s} for p, s in entries],
|
56
|
+
}
|
57
|
+
self.save()
|
58
|
+
|
59
|
+
def is_fresh(self, root_path: str, ttl_hours: int) -> bool:
|
60
|
+
self.load()
|
61
|
+
rec = self._data.get(self._key(root_path))
|
62
|
+
if not rec:
|
63
|
+
return False
|
64
|
+
try:
|
65
|
+
updated_at = datetime.fromisoformat(rec.get("updated_at"))
|
66
|
+
return datetime.utcnow() - updated_at <= timedelta(hours=ttl_hours)
|
67
|
+
except Exception:
|
68
|
+
return False
|
69
|
+
|
70
|
+
# 命名缓存(非路径键),适合应用分析等聚合结果
|
71
|
+
def get_named(self, name: str) -> Dict:
|
72
|
+
self.load()
|
73
|
+
return self._data.get(name)
|
74
|
+
|
75
|
+
def set_named(self, name: str, entries: List[Tuple[str, int]]) -> None:
|
76
|
+
self.load()
|
77
|
+
now_iso = datetime.utcnow().isoformat()
|
78
|
+
self._data[name] = {
|
79
|
+
"updated_at": now_iso,
|
80
|
+
"entries": [{"name": p, "size": s} for p, s in entries],
|
81
|
+
}
|
82
|
+
self.save()
|
83
|
+
|
84
|
+
def is_fresh_named(self, name: str, ttl_hours: int) -> bool:
|
85
|
+
self.load()
|
86
|
+
rec = self._data.get(name)
|
87
|
+
if not rec:
|
88
|
+
return False
|
89
|
+
try:
|
90
|
+
updated_at = datetime.fromisoformat(rec.get("updated_at"))
|
91
|
+
return datetime.utcnow() - updated_at <= timedelta(hours=ttl_hours)
|
92
|
+
except Exception:
|
93
|
+
return False
|
@@ -0,0 +1,419 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
"""
|
4
|
+
SpaceAnalyzer - 磁盘空间分析器
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import sys
|
9
|
+
import subprocess
|
10
|
+
import heapq
|
11
|
+
import time
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import List, Tuple, Dict
|
14
|
+
from .index_store import IndexStore
|
15
|
+
|
16
|
+
|
17
|
+
class SpaceAnalyzer:
|
18
|
+
"""磁盘空间分析器"""
|
19
|
+
|
20
|
+
def __init__(self):
|
21
|
+
self.warning_threshold = 80 # 警告阈值百分比
|
22
|
+
self.critical_threshold = 90 # 严重阈值百分比
|
23
|
+
# 忽略的目录列表, 这些目录时系统目录,不需要分析
|
24
|
+
self.ignore_dir_list = [
|
25
|
+
"/System", # 系统目录
|
26
|
+
"/Volumes", # 外部挂载卷
|
27
|
+
"/private", # 私有目录
|
28
|
+
".Trash", # 垃圾桶
|
29
|
+
".localized", # 本地化目录
|
30
|
+
]
|
31
|
+
|
32
|
+
|
33
|
+
def get_disk_usage(self, path: str = "/") -> Dict:
|
34
|
+
"""获取磁盘使用情况"""
|
35
|
+
try:
|
36
|
+
statvfs = os.statvfs(path)
|
37
|
+
|
38
|
+
# 计算磁盘空间信息
|
39
|
+
total_bytes = statvfs.f_frsize * statvfs.f_blocks
|
40
|
+
free_bytes = statvfs.f_frsize * statvfs.f_bavail
|
41
|
+
used_bytes = total_bytes - free_bytes
|
42
|
+
|
43
|
+
# 计算百分比
|
44
|
+
usage_percent = (used_bytes / total_bytes) * 100
|
45
|
+
|
46
|
+
return {
|
47
|
+
'total': total_bytes,
|
48
|
+
'used': used_bytes,
|
49
|
+
'free': free_bytes,
|
50
|
+
'usage_percent': usage_percent,
|
51
|
+
'path': path
|
52
|
+
}
|
53
|
+
except Exception as e:
|
54
|
+
print(f"错误:无法获取磁盘使用情况 - {e}")
|
55
|
+
return None
|
56
|
+
|
57
|
+
def get_disk_health_status(self, usage_info: Dict) -> Tuple[str, str]:
|
58
|
+
"""评估磁盘健康状态"""
|
59
|
+
if not usage_info:
|
60
|
+
return "未知", "无法获取磁盘信息"
|
61
|
+
|
62
|
+
usage_percent = usage_info['usage_percent']
|
63
|
+
|
64
|
+
if usage_percent >= self.critical_threshold:
|
65
|
+
return "严重", "磁盘空间严重不足!请立即清理磁盘空间"
|
66
|
+
elif usage_percent >= self.warning_threshold:
|
67
|
+
return "警告", "磁盘空间不足,建议清理一些文件"
|
68
|
+
else:
|
69
|
+
return "良好", "磁盘空间充足"
|
70
|
+
|
71
|
+
def format_bytes(self, bytes_value: int) -> str:
|
72
|
+
"""格式化字节数为人类可读格式"""
|
73
|
+
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
74
|
+
if bytes_value < 1024.0:
|
75
|
+
return f"{bytes_value:.1f} {unit}"
|
76
|
+
bytes_value /= 1024.0
|
77
|
+
return f"{bytes_value:.1f} PB"
|
78
|
+
|
79
|
+
def get_directory_size(self, path: str) -> int:
|
80
|
+
"""高性能计算目录大小。
|
81
|
+
|
82
|
+
优先使用 macOS 的 du -sk(以 KiB 为单位,速度快,原生命令可处理边界情况),
|
83
|
+
若 du 调用失败则回退到基于 os.scandir 的非递归遍历实现(避免 os.walk 的函数调用开销)。
|
84
|
+
"""
|
85
|
+
# 优先尝试 du -sk(BSD du 在 macOS 可用)。
|
86
|
+
try:
|
87
|
+
# du 输出形如: "<kib>\t<path>\n"
|
88
|
+
result = subprocess.run([
|
89
|
+
'du', '-sk', path
|
90
|
+
], capture_output=True, text=True, check=True)
|
91
|
+
out = result.stdout.strip().split('\t', 1)[0].strip()
|
92
|
+
kib = int(out)
|
93
|
+
return kib * 1024
|
94
|
+
except Exception:
|
95
|
+
# du 不可用或失败时回退到 Python 实现
|
96
|
+
pass
|
97
|
+
|
98
|
+
total_size = 0
|
99
|
+
# 基于栈的迭代遍历,避免递归栈与 os.walk 的额外开销
|
100
|
+
stack = [path]
|
101
|
+
while stack:
|
102
|
+
current = stack.pop()
|
103
|
+
try:
|
104
|
+
with os.scandir(current) as it:
|
105
|
+
for entry in it:
|
106
|
+
# 跳过符号链接,避免循环与跨文件系统问题
|
107
|
+
try:
|
108
|
+
if entry.is_symlink():
|
109
|
+
continue
|
110
|
+
if entry.is_file(follow_symlinks=False):
|
111
|
+
try:
|
112
|
+
total_size += entry.stat(follow_symlinks=False).st_size
|
113
|
+
except (OSError, FileNotFoundError, PermissionError):
|
114
|
+
continue
|
115
|
+
elif entry.is_dir(follow_symlinks=False):
|
116
|
+
stack.append(entry.path)
|
117
|
+
except (OSError, FileNotFoundError, PermissionError):
|
118
|
+
continue
|
119
|
+
except (OSError, FileNotFoundError, PermissionError):
|
120
|
+
# 无法进入该目录则跳过
|
121
|
+
continue
|
122
|
+
return total_size
|
123
|
+
|
124
|
+
def analyze_largest_files(self, root_path: str = "/", top_n: int = 50,
|
125
|
+
min_size_bytes: int = 0) -> List[Tuple[str, int]]:
|
126
|
+
"""扫描并返回体积最大的文件列表"""
|
127
|
+
print("正在扫描大文件,这可能需要一些时间...")
|
128
|
+
heap: List[Tuple[int, str]] = [] # 最小堆 (size, path)
|
129
|
+
scanned = 0
|
130
|
+
try:
|
131
|
+
for dirpath, dirnames, filenames in os.walk(root_path):
|
132
|
+
|
133
|
+
# 过滤以ignore_dir_list中的目录开头的文件
|
134
|
+
if any(dirpath.startswith(dir) for dir in self.ignore_dir_list):
|
135
|
+
continue
|
136
|
+
|
137
|
+
# 进度提示:单行覆盖当前目录
|
138
|
+
dirpath_display = dirpath[-80:] # 截取最后50个字符
|
139
|
+
if dirpath_display == "":
|
140
|
+
dirpath_display = dirpath
|
141
|
+
sys.stdout.write(f"\r\033[K-> 正在读取: \033[36m{dirpath_display}\033[0m")
|
142
|
+
sys.stdout.flush()
|
143
|
+
for filename in filenames:
|
144
|
+
filepath = os.path.join(dirpath, filename)
|
145
|
+
try:
|
146
|
+
size = os.path.getsize(filepath)
|
147
|
+
except (OSError, FileNotFoundError, PermissionError):
|
148
|
+
continue
|
149
|
+
if size < min_size_bytes:
|
150
|
+
continue
|
151
|
+
if len(heap) < top_n:
|
152
|
+
heapq.heappush(heap, (size, filepath))
|
153
|
+
else:
|
154
|
+
if size > heap[0][0]:
|
155
|
+
heapq.heapreplace(heap, (size, filepath))
|
156
|
+
scanned += 1
|
157
|
+
if scanned % 500 == 0:
|
158
|
+
dirpath_display = dirpath[-80:] # 截取最后50个字符
|
159
|
+
if dirpath_display == "":
|
160
|
+
dirpath_display = dirpath
|
161
|
+
# 间隔性进度输出(单行覆盖)
|
162
|
+
sys.stdout.write(f"\r\033[K-> 正在读取: \033[36m{dirpath_display}\033[0m 已扫描: \033[32m{scanned}\033[0m")
|
163
|
+
sys.stdout.flush()
|
164
|
+
except KeyboardInterrupt:
|
165
|
+
print("\n用户中断扫描,返回当前结果...")
|
166
|
+
except Exception as e:
|
167
|
+
print(f"扫描时出错: {e}")
|
168
|
+
finally:
|
169
|
+
sys.stdout.write("\n")
|
170
|
+
sys.stdout.flush()
|
171
|
+
# 转换为按体积降序列表
|
172
|
+
result = sorted([(p, s) for s, p in heap], key=lambda x: x[1], reverse=False)
|
173
|
+
result.sort(key=lambda x: x[1])
|
174
|
+
result = sorted([(p, s) for s, p in heap], key=lambda x: x[1])
|
175
|
+
# 正确:按 size 降序
|
176
|
+
result = sorted([(p, s) for s, p in heap], key=lambda x: x[1], reverse=True)
|
177
|
+
# 以上为了避免编辑器误合并,最终以最后一行排序为准
|
178
|
+
return result
|
179
|
+
|
180
|
+
def analyze_largest_directories(self, root_path: str = "/", max_depth: int = 2, top_n: int = 20,
|
181
|
+
index: IndexStore = None, use_index: bool = True,
|
182
|
+
reindex: bool = False, index_ttl_hours: int = 24,
|
183
|
+
prompt: bool = True) -> List[Tuple[str, int]]:
|
184
|
+
"""分析占用空间最大的目录(支持索引缓存)"""
|
185
|
+
# 索引命中
|
186
|
+
if use_index and index and not reindex and index.is_fresh(root_path, index_ttl_hours):
|
187
|
+
cached = index.get(root_path)
|
188
|
+
if cached and cached.get("entries"):
|
189
|
+
if prompt and sys.stdin.isatty():
|
190
|
+
try:
|
191
|
+
ans = input("检测到最近索引,是否使用缓存结果而不重新索引?[Y/n]: ").strip().lower()
|
192
|
+
if ans in ("", "y", "yes"):
|
193
|
+
return [(e["path"], int(e["size"])) for e in cached["entries"]][:top_n]
|
194
|
+
except EOFError:
|
195
|
+
pass
|
196
|
+
else:
|
197
|
+
return [(e["path"], int(e["size"])) for e in cached["entries"]][:top_n]
|
198
|
+
|
199
|
+
print("正在分析目录大小,这可能需要一些时间...")
|
200
|
+
|
201
|
+
|
202
|
+
|
203
|
+
directory_sizes = []
|
204
|
+
|
205
|
+
try:
|
206
|
+
# 获取根目录下的直接子目录
|
207
|
+
for item in os.listdir(root_path):
|
208
|
+
item_path = os.path.join(root_path, item)
|
209
|
+
|
210
|
+
# 跳过隐藏文件和系统文件
|
211
|
+
if item.startswith('.') and item not in ['.Trash', '.localized']:
|
212
|
+
continue
|
213
|
+
|
214
|
+
if item_path in self.ignore_dir_list:
|
215
|
+
continue
|
216
|
+
|
217
|
+
if os.path.isdir(item_path):
|
218
|
+
try:
|
219
|
+
# 进度提示:当前正在读取的目录(单行覆盖)
|
220
|
+
sys.stdout.write(f"\r\033[K-> 正在读取: \033[36m{item_path}\033[0m")
|
221
|
+
sys.stdout.flush()
|
222
|
+
size = self.get_directory_size(item_path)
|
223
|
+
directory_sizes.append((item_path, size))
|
224
|
+
#print(f"已分析: {item_path} ({self.format_bytes(size)})")
|
225
|
+
print(f" ({self.format_bytes(size)})\033[0m")
|
226
|
+
except (OSError, PermissionError):
|
227
|
+
print(f"跳过无法访问的目录: {item_path}")
|
228
|
+
continue
|
229
|
+
# 结束时换行,避免后续输出粘连在同一行
|
230
|
+
sys.stdout.write("\n")
|
231
|
+
sys.stdout.flush()
|
232
|
+
|
233
|
+
# 按大小排序
|
234
|
+
directory_sizes.sort(key=lambda x: x[1], reverse=True)
|
235
|
+
# 写入索引
|
236
|
+
if index:
|
237
|
+
try:
|
238
|
+
index.set(root_path, directory_sizes)
|
239
|
+
except Exception:
|
240
|
+
pass
|
241
|
+
return directory_sizes[:top_n]
|
242
|
+
|
243
|
+
except Exception as e:
|
244
|
+
print(f"分析目录时出错: {e}")
|
245
|
+
return []
|
246
|
+
|
247
|
+
def get_system_info(self) -> Dict:
|
248
|
+
"""获取系统信息(包括 CPU、内存、GPU、硬盘等硬件信息)"""
|
249
|
+
system_info = {}
|
250
|
+
|
251
|
+
try:
|
252
|
+
# 获取系统版本信息
|
253
|
+
result = subprocess.run(['sw_vers'], capture_output=True, text=True)
|
254
|
+
for line in result.stdout.split('\n'):
|
255
|
+
if ':' in line:
|
256
|
+
key, value = line.split(':', 1)
|
257
|
+
system_info[key.strip()] = value.strip()
|
258
|
+
except Exception:
|
259
|
+
system_info["ProductName"] = "macOS"
|
260
|
+
system_info["ProductVersion"] = "未知"
|
261
|
+
|
262
|
+
try:
|
263
|
+
# 获取 CPU 信息
|
264
|
+
cpu_result = subprocess.run(['sysctl', '-n', 'machdep.cpu.brand_string'],
|
265
|
+
capture_output=True, text=True)
|
266
|
+
if cpu_result.returncode == 0:
|
267
|
+
system_info["CPU"] = cpu_result.stdout.strip()
|
268
|
+
|
269
|
+
# 获取 CPU 核心数
|
270
|
+
cores_result = subprocess.run(['sysctl', '-n', 'hw.ncpu'],
|
271
|
+
capture_output=True, text=True)
|
272
|
+
if cores_result.returncode == 0:
|
273
|
+
system_info["CPU核心数"] = cores_result.stdout.strip()
|
274
|
+
|
275
|
+
except Exception:
|
276
|
+
system_info["CPU"] = "未知"
|
277
|
+
system_info["CPU核心数"] = "未知"
|
278
|
+
|
279
|
+
try:
|
280
|
+
# 获取内存信息
|
281
|
+
mem_result = subprocess.run(['sysctl', '-n', 'hw.memsize'],
|
282
|
+
capture_output=True, text=True)
|
283
|
+
if mem_result.returncode == 0:
|
284
|
+
mem_bytes = int(mem_result.stdout.strip())
|
285
|
+
system_info["内存"] = self.format_bytes(mem_bytes)
|
286
|
+
# 计算内存使用率(基于 vm_stat 与页面大小)
|
287
|
+
try:
|
288
|
+
pagesize_res = subprocess.run(['sysctl', '-n', 'hw.pagesize'], capture_output=True, text=True)
|
289
|
+
pagesize = int(pagesize_res.stdout.strip()) if pagesize_res.returncode == 0 else 4096
|
290
|
+
vm_res = subprocess.run(['vm_stat'], capture_output=True, text=True)
|
291
|
+
if vm_res.returncode == 0:
|
292
|
+
import re
|
293
|
+
page_counts = {}
|
294
|
+
for line in vm_res.stdout.splitlines():
|
295
|
+
if ':' not in line:
|
296
|
+
continue
|
297
|
+
key, val = line.split(':', 1)
|
298
|
+
m = re.search(r"(\d+)", val.replace('.', ''))
|
299
|
+
if not m:
|
300
|
+
continue
|
301
|
+
count = int(m.group(1))
|
302
|
+
page_counts[key.strip()] = count
|
303
|
+
|
304
|
+
free_pages = page_counts.get('Pages free', 0) + page_counts.get('Pages speculative', 0)
|
305
|
+
active_pages = page_counts.get('Pages active', 0)
|
306
|
+
inactive_pages = page_counts.get('Pages inactive', 0)
|
307
|
+
wired_pages = page_counts.get('Pages wired down', 0) or page_counts.get('Pages wired', 0)
|
308
|
+
compressed_pages = page_counts.get('Pages occupied by compressor', 0)
|
309
|
+
|
310
|
+
used_pages = active_pages + inactive_pages + wired_pages + compressed_pages
|
311
|
+
total_pages = used_pages + free_pages
|
312
|
+
if total_pages > 0:
|
313
|
+
usage_percent = (used_pages / total_pages) * 100.0
|
314
|
+
system_info["内存使用率"] = f"{usage_percent:.1f}%"
|
315
|
+
except Exception:
|
316
|
+
# 安静失败,不影响主流程
|
317
|
+
print("计算内存使用率失败")
|
318
|
+
pass
|
319
|
+
except Exception:
|
320
|
+
system_info["内存"] = "未知"
|
321
|
+
|
322
|
+
|
323
|
+
try:
|
324
|
+
# 获取启动时间
|
325
|
+
boot_result = subprocess.run(['uptime'], capture_output=True, text=True)
|
326
|
+
if boot_result.returncode == 0:
|
327
|
+
uptime_line = boot_result.stdout.strip()
|
328
|
+
system_info["运行时间"] = uptime_line
|
329
|
+
except Exception:
|
330
|
+
system_info["运行时间"] = "未知"
|
331
|
+
|
332
|
+
# 添加 space-cli 版本信息
|
333
|
+
try:
|
334
|
+
# 简单解析 pyproject.toml 文件中的版本号
|
335
|
+
with open('pyproject.toml', 'r', encoding='utf-8') as f:
|
336
|
+
content = f.read()
|
337
|
+
# 查找 version = "x.x.x" 行
|
338
|
+
import re
|
339
|
+
version_match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
340
|
+
if version_match:
|
341
|
+
system_info['SpaceCli Version'] = version_match.group(1)
|
342
|
+
else:
|
343
|
+
system_info['SpaceCli Version'] = '未知'
|
344
|
+
except Exception:
|
345
|
+
system_info['SpaceCli Version'] = '未知'
|
346
|
+
|
347
|
+
return system_info
|
348
|
+
|
349
|
+
def memory_cleanup(self) -> Dict:
|
350
|
+
"""执行内存释放优化"""
|
351
|
+
cleanup_results = {
|
352
|
+
"purged_memory": 0,
|
353
|
+
"cleared_caches": [],
|
354
|
+
"freed_swap": 0,
|
355
|
+
"errors": []
|
356
|
+
}
|
357
|
+
|
358
|
+
try:
|
359
|
+
|
360
|
+
# 2. 强制垃圾回收
|
361
|
+
print("🔄 正在执行垃圾回收...")
|
362
|
+
import gc
|
363
|
+
collected = gc.collect()
|
364
|
+
cleanup_results["purged_memory"] += collected
|
365
|
+
|
366
|
+
# 3. 清理 Python 缓存
|
367
|
+
print("🗑️ 正在清理 Python 缓存...")
|
368
|
+
try:
|
369
|
+
# 清理 __pycache__ 目录
|
370
|
+
import shutil
|
371
|
+
for root, dirs, files in os.walk('/tmp'):
|
372
|
+
for dir_name in dirs:
|
373
|
+
if dir_name == '__pycache__':
|
374
|
+
cache_path = os.path.join(root, dir_name)
|
375
|
+
try:
|
376
|
+
shutil.rmtree(cache_path)
|
377
|
+
cleanup_results["cleared_caches"].append(f"Python缓存: {cache_path}")
|
378
|
+
except Exception:
|
379
|
+
pass
|
380
|
+
except Exception as e:
|
381
|
+
cleanup_results["errors"].append(f"Python缓存清理失败: {e}")
|
382
|
+
|
383
|
+
# 4. 清理临时文件
|
384
|
+
print("📁 正在清理临时文件...")
|
385
|
+
temp_dirs = ['/tmp', '/var/tmp']
|
386
|
+
for temp_dir in temp_dirs:
|
387
|
+
if os.path.exists(temp_dir):
|
388
|
+
try:
|
389
|
+
for item in os.listdir(temp_dir):
|
390
|
+
item_path = os.path.join(temp_dir, item)
|
391
|
+
# 只清理超过1小时的文件
|
392
|
+
if os.path.isfile(item_path):
|
393
|
+
file_age = time.time() - os.path.getmtime(item_path)
|
394
|
+
if file_age > 3600: # 1小时
|
395
|
+
try:
|
396
|
+
os.remove(item_path)
|
397
|
+
cleanup_results["cleared_caches"].append(f"临时文件: {item_path}")
|
398
|
+
except Exception:
|
399
|
+
pass
|
400
|
+
except Exception as e:
|
401
|
+
cleanup_results["errors"].append(f"临时文件清理失败: {e}")
|
402
|
+
|
403
|
+
# 5. 尝试释放交换空间(需要管理员权限)
|
404
|
+
print("💾 正在尝试释放交换空间...(需要登录密码授权此操作,此操作不会保存密码)")
|
405
|
+
try:
|
406
|
+
# 检查交换使用情况
|
407
|
+
swap_result = subprocess.run(['sysctl', 'vm.swapusage'],
|
408
|
+
capture_output=True, text=True)
|
409
|
+
if swap_result.returncode == 0:
|
410
|
+
# 尝试释放未使用的交换空间
|
411
|
+
subprocess.run(['sudo', 'purge'], capture_output=True, text=True)
|
412
|
+
cleanup_results["freed_swap"] = 1
|
413
|
+
except Exception as e:
|
414
|
+
cleanup_results["errors"].append(f"交换空间释放失败: {e}")
|
415
|
+
|
416
|
+
except Exception as e:
|
417
|
+
cleanup_results["errors"].append(f"内存清理过程出错: {e}")
|
418
|
+
|
419
|
+
return cleanup_results
|