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.
@@ -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
- _cpu_percent = psutil.cpu_percent(interval=5.0)
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))
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.12.dev1
3
+ Version: 1.4.15.dev3
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -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=KBZ1gEZmvt1J4XR1FpwaCleg3ayMUckV-yZYhnXlSCU,719
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=07wxG_55JMy3yQ9TXCBldW9h43qCW3U8rv2yzGMx4FM,44757
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=7tBYNEY8EBGAPIMT606BqeHnyMOQIZVlQYpH7me26LY,97962
18
- portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
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=Sd25UGM7edWDTIiatZvA2Dh3-4NOzfIDmixG0Fz5m0U,51343
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=AKh7IbwptlLYrbSw5f-DHigvlaKHsg9lDP-lkAUm8cE,10755
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.12.dev1.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
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.12.dev1.dist-info/METADATA,sha256=o7psDA6l4eoRJLFQipCS3XcgsqTLxNMf39xhh6qYJ00,13051
94
- portacode-1.4.12.dev1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
95
- portacode-1.4.12.dev1.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
96
- portacode-1.4.12.dev1.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
97
- portacode-1.4.12.dev1.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5