portacode 1.4.12.dev1__py3-none-any.whl → 1.4.15.dev3__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.
- portacode/_version.py +2 -2
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +106 -3
- portacode/connection/handlers/__init__.py +6 -0
- portacode/connection/handlers/proxmox_infra.py +713 -48
- portacode/connection/handlers/system_handlers.py +131 -2
- portacode/connection/handlers/test_proxmox_infra.py +13 -0
- portacode/connection/terminal.py +6 -0
- {portacode-1.4.12.dev1.dist-info → portacode-1.4.15.dev3.dist-info}/METADATA +1 -1
- {portacode-1.4.12.dev1.dist-info → portacode-1.4.15.dev3.dist-info}/RECORD +13 -12
- {portacode-1.4.12.dev1.dist-info → portacode-1.4.15.dev3.dist-info}/WHEEL +1 -1
- {portacode-1.4.12.dev1.dist-info → portacode-1.4.15.dev3.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.12.dev1.dist-info → portacode-1.4.15.dev3.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.12.dev1.dist-info → portacode-1.4.15.dev3.dist-info}/top_level.txt +0 -0
|
@@ -10,8 +10,9 @@ import platform
|
|
|
10
10
|
import shutil
|
|
11
11
|
import subprocess
|
|
12
12
|
import threading
|
|
13
|
+
import time
|
|
13
14
|
from pathlib import Path
|
|
14
|
-
from typing import Any, Dict
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
15
16
|
|
|
16
17
|
from portacode import __version__
|
|
17
18
|
import psutil
|
|
@@ -31,11 +32,26 @@ _cpu_percent = 0.0
|
|
|
31
32
|
_cpu_thread = None
|
|
32
33
|
_cpu_lock = threading.Lock()
|
|
33
34
|
|
|
35
|
+
# Cgroup v2 tracking
|
|
36
|
+
_CGROUP_ROOT = Path("/sys/fs/cgroup")
|
|
37
|
+
_cgroup_path: Optional[Path] = None
|
|
38
|
+
_cgroup_v2_supported: Optional[bool] = None
|
|
39
|
+
_CGROUP_CPU_STAT = "cpu.stat"
|
|
40
|
+
_CGROUP_CPU_MAX = "cpu.max"
|
|
41
|
+
_last_cgroup_usage: Optional[int] = None
|
|
42
|
+
_last_cgroup_time: Optional[float] = None
|
|
43
|
+
|
|
34
44
|
def _cpu_monitor():
|
|
35
45
|
"""Background thread to update CPU usage every 5 seconds."""
|
|
36
46
|
global _cpu_percent
|
|
37
47
|
while True:
|
|
38
|
-
|
|
48
|
+
percent = _get_cgroup_cpu_percent()
|
|
49
|
+
if percent is None:
|
|
50
|
+
# Fall back to psutil when cgroup stats are not available yet.
|
|
51
|
+
percent = psutil.cpu_percent(interval=5.0)
|
|
52
|
+
else:
|
|
53
|
+
time.sleep(5.0)
|
|
54
|
+
_cpu_percent = percent
|
|
39
55
|
|
|
40
56
|
def _ensure_cpu_thread():
|
|
41
57
|
"""Ensure CPU monitoring thread is running (singleton)."""
|
|
@@ -130,6 +146,119 @@ def _get_playwright_info() -> Dict[str, Any]:
|
|
|
130
146
|
return result
|
|
131
147
|
|
|
132
148
|
|
|
149
|
+
def _resolve_cgroup_path() -> Path:
|
|
150
|
+
global _cgroup_path
|
|
151
|
+
if _cgroup_path is not None and _cgroup_path.exists():
|
|
152
|
+
return _cgroup_path
|
|
153
|
+
path = _CGROUP_ROOT
|
|
154
|
+
cgroup_file = Path("/proc/self/cgroup")
|
|
155
|
+
if cgroup_file.exists():
|
|
156
|
+
try:
|
|
157
|
+
contents = cgroup_file.read_text()
|
|
158
|
+
except OSError:
|
|
159
|
+
pass
|
|
160
|
+
else:
|
|
161
|
+
for line in contents.splitlines():
|
|
162
|
+
line = line.strip()
|
|
163
|
+
if not line:
|
|
164
|
+
continue
|
|
165
|
+
parts = line.split(":", 2)
|
|
166
|
+
if len(parts) < 3:
|
|
167
|
+
continue
|
|
168
|
+
rel_path = parts[-1].lstrip("/")
|
|
169
|
+
candidate = _CGROUP_ROOT / rel_path
|
|
170
|
+
# Fallback to root path if the relative path is empty
|
|
171
|
+
candidate = candidate if rel_path else _CGROUP_ROOT
|
|
172
|
+
if candidate.exists():
|
|
173
|
+
path = candidate
|
|
174
|
+
break
|
|
175
|
+
_cgroup_path = path
|
|
176
|
+
return _cgroup_path
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _cgroup_file(name: str) -> Path:
|
|
180
|
+
return _resolve_cgroup_path() / name
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _detect_cgroup_v2() -> bool:
|
|
184
|
+
global _cgroup_v2_supported
|
|
185
|
+
if _cgroup_v2_supported is not None:
|
|
186
|
+
return _cgroup_v2_supported
|
|
187
|
+
controllers = _cgroup_file("cgroup.controllers")
|
|
188
|
+
cpu_stat = _cgroup_file(_CGROUP_CPU_STAT)
|
|
189
|
+
_cgroup_v2_supported = controllers.exists() and cpu_stat.exists()
|
|
190
|
+
return _cgroup_v2_supported
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _read_cgroup_cpu_usage() -> Optional[int]:
|
|
194
|
+
path = _cgroup_file(_CGROUP_CPU_STAT)
|
|
195
|
+
try:
|
|
196
|
+
data = path.read_text()
|
|
197
|
+
except (OSError, UnicodeDecodeError):
|
|
198
|
+
return None
|
|
199
|
+
for line in data.splitlines():
|
|
200
|
+
parts = line.strip().split()
|
|
201
|
+
if len(parts) >= 2 and parts[0] == "usage_usec":
|
|
202
|
+
try:
|
|
203
|
+
return int(parts[1])
|
|
204
|
+
except ValueError:
|
|
205
|
+
return None
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _read_cgroup_cpu_limit() -> Optional[float]:
|
|
210
|
+
"""Return the allowed CPU (cores) for this cgroup, if limited."""
|
|
211
|
+
path = _cgroup_file(_CGROUP_CPU_MAX)
|
|
212
|
+
if not path.exists():
|
|
213
|
+
return None
|
|
214
|
+
try:
|
|
215
|
+
data = path.read_text().strip()
|
|
216
|
+
except (OSError, UnicodeDecodeError):
|
|
217
|
+
return None
|
|
218
|
+
parts = data.split()
|
|
219
|
+
if len(parts) < 2:
|
|
220
|
+
return None
|
|
221
|
+
quota, period = parts[0], parts[1]
|
|
222
|
+
if quota == "max":
|
|
223
|
+
return None
|
|
224
|
+
try:
|
|
225
|
+
quota_value = int(quota)
|
|
226
|
+
period_value = int(period)
|
|
227
|
+
except ValueError:
|
|
228
|
+
return None
|
|
229
|
+
if period_value <= 0:
|
|
230
|
+
return None
|
|
231
|
+
return quota_value / period_value
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _get_cgroup_cpu_percent() -> Optional[float]:
|
|
235
|
+
if not _detect_cgroup_v2():
|
|
236
|
+
return None
|
|
237
|
+
usage = _read_cgroup_cpu_usage()
|
|
238
|
+
if usage is None:
|
|
239
|
+
return None
|
|
240
|
+
now = time.monotonic()
|
|
241
|
+
global _last_cgroup_usage, _last_cgroup_time
|
|
242
|
+
prev_usage = _last_cgroup_usage
|
|
243
|
+
prev_time = _last_cgroup_time
|
|
244
|
+
_last_cgroup_usage = usage
|
|
245
|
+
_last_cgroup_time = now
|
|
246
|
+
if prev_usage is None or prev_time is None:
|
|
247
|
+
return None
|
|
248
|
+
delta_usage = usage - prev_usage
|
|
249
|
+
delta_time = now - prev_time
|
|
250
|
+
if delta_time <= 0 or delta_usage < 0:
|
|
251
|
+
return None
|
|
252
|
+
cpu_ratio = (delta_usage / 1_000_000) / delta_time
|
|
253
|
+
limit_cpus = _read_cgroup_cpu_limit()
|
|
254
|
+
if limit_cpus and limit_cpus > 0:
|
|
255
|
+
percent = (cpu_ratio / limit_cpus) * 100.0
|
|
256
|
+
else:
|
|
257
|
+
cpu_count = psutil.cpu_count(logical=True) or 1
|
|
258
|
+
percent = (cpu_ratio / cpu_count) * 100.0
|
|
259
|
+
return max(0.0, min(percent, 100.0))
|
|
260
|
+
|
|
261
|
+
|
|
133
262
|
def _run_probe_command(cmd: list[str]) -> str | None:
|
|
134
263
|
try:
|
|
135
264
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=3)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
|
|
3
|
+
from portacode.connection.handlers.proxmox_infra import _build_bootstrap_steps
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProxmoxInfraHandlerTests(TestCase):
|
|
7
|
+
def test_build_bootstrap_steps_includes_portacode_connect_by_default(self):
|
|
8
|
+
steps = _build_bootstrap_steps("svcuser", "pass", "", include_portacode_connect=True)
|
|
9
|
+
self.assertTrue(any(step.get("name") == "portacode_connect" for step in steps))
|
|
10
|
+
|
|
11
|
+
def test_build_bootstrap_steps_skips_portacode_connect_when_requested(self):
|
|
12
|
+
steps = _build_bootstrap_steps("svcuser", "pass", "", include_portacode_connect=False)
|
|
13
|
+
self.assertFalse(any(step.get("name") == "portacode_connect" for step in steps))
|
portacode/connection/terminal.py
CHANGED
|
@@ -56,6 +56,9 @@ from .handlers import (
|
|
|
56
56
|
CreateProxmoxContainerHandler,
|
|
57
57
|
RevertProxmoxInfraHandler,
|
|
58
58
|
StartPortacodeServiceHandler,
|
|
59
|
+
StartProxmoxContainerHandler,
|
|
60
|
+
StopProxmoxContainerHandler,
|
|
61
|
+
RemoveProxmoxContainerHandler,
|
|
59
62
|
)
|
|
60
63
|
from .handlers.project_aware_file_handlers import (
|
|
61
64
|
ProjectAwareFileWriteHandler,
|
|
@@ -480,6 +483,9 @@ class TerminalManager:
|
|
|
480
483
|
self._command_registry.register(ConfigureProxmoxInfraHandler)
|
|
481
484
|
self._command_registry.register(CreateProxmoxContainerHandler)
|
|
482
485
|
self._command_registry.register(StartPortacodeServiceHandler)
|
|
486
|
+
self._command_registry.register(StartProxmoxContainerHandler)
|
|
487
|
+
self._command_registry.register(StopProxmoxContainerHandler)
|
|
488
|
+
self._command_registry.register(RemoveProxmoxContainerHandler)
|
|
483
489
|
self._command_registry.register(RevertProxmoxInfraHandler)
|
|
484
490
|
self._command_registry.register(UpdatePortacodeHandler)
|
|
485
491
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
|
|
2
2
|
portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
|
|
3
3
|
portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
|
|
4
|
-
portacode/_version.py,sha256=
|
|
4
|
+
portacode/_version.py,sha256=1sfx8JK_QXWbIlvcX2Sn3XEnIw9YLhiiBq9EMvNgnss,719
|
|
5
5
|
portacode/cli.py,sha256=mGLKoZ-T2FBF7IA9wUq0zyG0X9__-A1ao7gajjcVRH8,21828
|
|
6
6
|
portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
|
|
7
7
|
portacode/keypair.py,sha256=0OO4vHDcF1XMxCDqce61xFTlFwlTcmqe5HyGsXFEt7s,5838
|
|
@@ -12,22 +12,23 @@ portacode/connection/README.md,sha256=f9rbuIEKa7cTm9C98rCiBbEtbiIXQU11esGSNhSMiJ
|
|
|
12
12
|
portacode/connection/__init__.py,sha256=atqcVGkViIEd7pRa6cP2do07RJOM0UWpbnz5zXjGktU,250
|
|
13
13
|
portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5weiU,14181
|
|
14
14
|
portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
|
|
15
|
-
portacode/connection/terminal.py,sha256=
|
|
15
|
+
portacode/connection/terminal.py,sha256=oyLPOVLPlUuN_eRvHPGazB51yi8W8JEF3oOEYxucGTE,45069
|
|
16
16
|
portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
|
|
17
|
-
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=
|
|
18
|
-
portacode/connection/handlers/__init__.py,sha256=
|
|
17
|
+
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=5VZSDPvXjtOl-8YZ-2UVtH8WgPnEWe7WB33YPrS8_8Q,104190
|
|
18
|
+
portacode/connection/handlers/__init__.py,sha256=j69jGkf2-mYyCicvYfp2wk8-xB8yqpWktiN5xADXBno,3137
|
|
19
19
|
portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
|
|
20
20
|
portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
|
|
21
21
|
portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZbQw6sF04M3dM6rUV8Q,24477
|
|
22
22
|
portacode/connection/handlers/file_handlers.py,sha256=nAJH8nXnX07xxD28ngLpgIUzcTuRwZBNpEGEKdRqohw,39507
|
|
23
23
|
portacode/connection/handlers/project_aware_file_handlers.py,sha256=AqgMnDqX2893T2NsrvUSCwjN5VKj4Pb2TN0S_SuboOE,9803
|
|
24
24
|
portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
|
|
25
|
-
portacode/connection/handlers/proxmox_infra.py,sha256=
|
|
25
|
+
portacode/connection/handlers/proxmox_infra.py,sha256=mH6xxT9v9X0jwdCBjU3p48lURYisY0T0HPjuzV3eoH8,76070
|
|
26
26
|
portacode/connection/handlers/registry.py,sha256=qXGE60sYEWg6ZtVQzFcZ5YI2XWR6lMgw4hAL9x5qR1I,6181
|
|
27
27
|
portacode/connection/handlers/session.py,sha256=uNGfiO_1B9-_yjJKkpvmbiJhIl6b-UXlT86UTfd6WYE,42219
|
|
28
|
-
portacode/connection/handlers/system_handlers.py,sha256=
|
|
28
|
+
portacode/connection/handlers/system_handlers.py,sha256=fr12QpOr_Z8KYGUU-AYrTQwRPAcrLK85hvj3SEq1Kw8,14757
|
|
29
29
|
portacode/connection/handlers/tab_factory.py,sha256=yn93h6GASjD1VpvW1oqpax3EpoT0r7r97zFXxML1wdA,16173
|
|
30
30
|
portacode/connection/handlers/terminal_handlers.py,sha256=HRwHW1GiqG1NtHVEqXHKaYkFfQEzCDDH6YIlHcb4XD8,11866
|
|
31
|
+
portacode/connection/handlers/test_proxmox_infra.py,sha256=d6iBB4pwAqWWdEGRayLxDEexqCElbGZDJlCB4bXba24,682
|
|
31
32
|
portacode/connection/handlers/update_handler.py,sha256=f2K4LmG4sHJZ3LahzzoRtHBULTKkPUNwuyhwuAAg3RA,2054
|
|
32
33
|
portacode/connection/handlers/project_state/README.md,sha256=trdd4ig6ungmwH5SpbSLfyxbL-QgPlGNU-_XrMEiXtw,10114
|
|
33
34
|
portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL8r_wVs5Tlt6B9J7yQH6yQUt7gc,2541
|
|
@@ -64,7 +65,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
|
|
|
64
65
|
portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
|
|
65
66
|
portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
|
|
66
67
|
portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
|
|
67
|
-
portacode-1.4.
|
|
68
|
+
portacode-1.4.15.dev3.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
68
69
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
69
70
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
70
71
|
test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
|
|
@@ -90,8 +91,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
|
|
|
90
91
|
testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
|
|
91
92
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
92
93
|
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
93
|
-
portacode-1.4.
|
|
94
|
-
portacode-1.4.
|
|
95
|
-
portacode-1.4.
|
|
96
|
-
portacode-1.4.
|
|
97
|
-
portacode-1.4.
|
|
94
|
+
portacode-1.4.15.dev3.dist-info/METADATA,sha256=fhua1K0pTVsxZaun0VkgDDpUBh5AdovF_z9U6_LyktE,13051
|
|
95
|
+
portacode-1.4.15.dev3.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
96
|
+
portacode-1.4.15.dev3.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.15.dev3.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.15.dev3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|