cyclo-manager 0.2.0.dev0__tar.gz → 0.2.0.dev2__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 (20) hide show
  1. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/PKG-INFO +1 -1
  2. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_host_agent/routers/repos.py +31 -26
  3. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_host_agent/routers/update.py +18 -56
  4. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager.egg-info/PKG-INFO +1 -1
  5. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager_cli/cli.py +18 -2
  6. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/pyproject.toml +1 -1
  7. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/setup.cfg +1 -1
  8. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/README.md +0 -0
  9. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_host_agent/__init__.py +0 -0
  10. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_host_agent/main.py +0 -0
  11. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_host_agent/models.py +0 -0
  12. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_host_agent/routers/__init__.py +0 -0
  13. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager.egg-info/SOURCES.txt +0 -0
  14. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager.egg-info/dependency_links.txt +0 -0
  15. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager.egg-info/entry_points.txt +0 -0
  16. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager.egg-info/requires.txt +0 -0
  17. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager.egg-info/top_level.txt +0 -0
  18. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager_cli/__init__.py +0 -0
  19. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager_cli/config/config.yml +0 -0
  20. {cyclo_manager-0.2.0.dev0 → cyclo_manager-0.2.0.dev2}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cyclo-manager
3
- Version: 0.2.0.dev0
3
+ Version: 0.2.0.dev2
4
4
  Summary: cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack.
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -22,6 +22,7 @@ import asyncio
22
22
  import json
23
23
  import os
24
24
  import re
25
+ import subprocess
25
26
  import urllib.request
26
27
  from pathlib import Path
27
28
  from typing import Optional
@@ -86,20 +87,23 @@ def _fmt_cmd(cmd: str, output: str) -> str:
86
87
 
87
88
  # ── git helpers ────────────────────────────────────────────────────────────────
88
89
 
89
- async def _git(args: list[str], cwd: Optional[Path] = None) -> tuple[int, str, str]:
90
- proc = await asyncio.create_subprocess_exec(
91
- 'git', *args,
92
- cwd=str(cwd) if cwd else None,
93
- stdout=asyncio.subprocess.PIPE,
94
- stderr=asyncio.subprocess.PIPE,
95
- )
90
+ def _git_sync(args: list[str], cwd: Optional[Path] = None) -> tuple[int, str, str]:
96
91
  try:
97
- stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=GIT_TIMEOUT)
98
- except asyncio.TimeoutError:
99
- proc.kill()
100
- await proc.wait()
101
- raise
102
- return proc.returncode, stdout.decode(), stderr.decode()
92
+ result = subprocess.run(
93
+ ['git', *args],
94
+ cwd=str(cwd) if cwd else None,
95
+ capture_output=True,
96
+ timeout=GIT_TIMEOUT,
97
+ )
98
+ return result.returncode, result.stdout.decode(), result.stderr.decode()
99
+ except subprocess.TimeoutExpired:
100
+ return 1, '', 'git timed out'
101
+ except Exception as e:
102
+ return 1, '', str(e)
103
+
104
+
105
+ async def _git(args: list[str], cwd: Optional[Path] = None) -> tuple[int, str, str]:
106
+ return await asyncio.to_thread(_git_sync, args, cwd)
103
107
 
104
108
 
105
109
  async def _repo_info(repo_path: Path) -> RepoInfo:
@@ -279,24 +283,25 @@ async def _update_reset(repo_path: Path, preserve_files: list[str]) -> UpdateRes
279
283
 
280
284
  # ── container helper ───────────────────────────────────────────────────────────
281
285
 
282
- async def _run_container_sh(repo_path: Path, action: str) -> tuple[bool, str]:
286
+ def _run_container_sh_sync(repo_path: Path, action: str) -> tuple[bool, str]:
283
287
  script = repo_path / 'docker' / 'container.sh'
284
288
  if not script.exists():
285
289
  return False, f'container.sh not found at {script}'
286
- proc = await asyncio.create_subprocess_exec(
287
- 'bash', str(script), action,
288
- cwd=str(repo_path / 'docker'),
289
- stdin=asyncio.subprocess.PIPE,
290
- stdout=asyncio.subprocess.PIPE,
291
- stderr=asyncio.subprocess.PIPE,
292
- )
293
290
  try:
294
- stdout, stderr = await asyncio.wait_for(proc.communicate(input=b'y\n'), timeout=300.0)
295
- except asyncio.TimeoutError:
296
- proc.kill()
297
- await proc.wait()
291
+ result = subprocess.run(
292
+ ['bash', str(script), action],
293
+ cwd=str(repo_path / 'docker'),
294
+ input=b'y\n',
295
+ capture_output=True,
296
+ timeout=300.0,
297
+ )
298
+ return result.returncode == 0, (result.stdout.decode() + result.stderr.decode()).strip()
299
+ except subprocess.TimeoutExpired:
298
300
  return False, 'Timeout waiting for container.sh'
299
- return proc.returncode == 0, (stdout.decode() + stderr.decode()).strip()
301
+
302
+
303
+ async def _run_container_sh(repo_path: Path, action: str) -> tuple[bool, str]:
304
+ return await asyncio.to_thread(_run_container_sh_sync, repo_path, action)
300
305
 
301
306
 
302
307
  # ── endpoints ──────────────────────────────────────────────────────────────────
@@ -42,16 +42,15 @@ def _fmt(cmd: str, out: str) -> str:
42
42
  return f'$ {cmd}\n{out.strip()}' if out.strip() else f'$ {cmd}'
43
43
 
44
44
 
45
- def _restart_self() -> None:
46
- """Restart the host agent after a short delay so new binary takes effect."""
47
- time.sleep(2)
45
+
46
+ def _run_cmd(args: list[str], timeout: float) -> tuple[int, str]:
48
47
  try:
49
- subprocess.run(
50
- ['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'],
51
- check=True,
52
- )
48
+ result = subprocess.run(args, capture_output=True, timeout=timeout)
49
+ return result.returncode, (result.stdout.decode() + result.stderr.decode()).strip()
50
+ except subprocess.TimeoutExpired:
51
+ return 1, 'timed out'
53
52
  except Exception as e:
54
- logger.error('Failed to restart host agent: %s', e)
53
+ return 1, str(e)
55
54
 
56
55
 
57
56
  async def _run_install_and_up(cyclo_exe: str, pip_exe: str) -> None:
@@ -60,50 +59,24 @@ async def _run_install_and_up(cyclo_exe: str, pip_exe: str) -> None:
60
59
 
61
60
  # pip install -U
62
61
  _update_status['phase'] = 'installing'
63
- try:
64
- proc = await asyncio.create_subprocess_exec(
65
- pip_exe, 'install', '-U', PYPI_PACKAGE,
66
- stdout=asyncio.subprocess.PIPE,
67
- stderr=asyncio.subprocess.PIPE,
68
- )
69
- stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
70
- out = (stdout.decode() + stderr.decode()).strip()
71
- _update_status['install_output'] = _fmt(f'pip install -U {PYPI_PACKAGE}', out)
72
- if proc.returncode != 0:
73
- _update_status['phase'] = 'error'
74
- _update_status['error'] = f'pip install failed (exit {proc.returncode})'
75
- return
76
- except asyncio.TimeoutError:
62
+ rc, out = await asyncio.to_thread(_run_cmd, [pip_exe, 'install', '-U', PYPI_PACKAGE], 120)
63
+ _update_status['install_output'] = _fmt(f'pip install -U {PYPI_PACKAGE}', out)
64
+ if rc != 0:
77
65
  _update_status['phase'] = 'error'
78
- _update_status['error'] = 'pip install timed out'
66
+ _update_status['error'] = f'pip install failed (exit {rc})'
79
67
  return
80
68
 
81
69
  # cyclo up
82
70
  _update_status['phase'] = 'starting'
83
- try:
84
- proc = await asyncio.create_subprocess_exec(
85
- cyclo_exe, 'up',
86
- stdout=asyncio.subprocess.PIPE,
87
- stderr=asyncio.subprocess.PIPE,
88
- )
89
- stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
90
- out = (stdout.decode() + stderr.decode()).strip()
91
- _update_status['up_output'] = _fmt('cyclo_manager up', out)
92
- if proc.returncode != 0:
93
- _update_status['phase'] = 'error'
94
- _update_status['error'] = f'cyclo_manager up failed (exit {proc.returncode})'
95
- return
96
- except asyncio.TimeoutError:
71
+ rc, out = await asyncio.to_thread(_run_cmd, [cyclo_exe, 'up'], 300)
72
+ _update_status['up_output'] = _fmt('cyclo_manager up', out)
73
+ if rc != 0:
97
74
  _update_status['phase'] = 'error'
98
- _update_status['error'] = 'cyclo_manager up timed out'
75
+ _update_status['error'] = f'cyclo_manager up failed (exit {rc})'
99
76
  return
100
77
 
101
78
  _update_status['phase'] = 'done'
102
79
 
103
- # Restart self so new binary takes effect
104
- loop = asyncio.get_event_loop()
105
- await loop.run_in_executor(None, _restart_self)
106
-
107
80
 
108
81
  @router.post('/update')
109
82
  async def start_update() -> dict:
@@ -127,20 +100,9 @@ async def start_update() -> dict:
127
100
 
128
101
  _update_status = {'phase': 'stopping', 'install_output': '', 'up_output': '', 'error': ''}
129
102
 
130
- # cyclo down (synchronous — server goes down here)
131
- down_output = ''
132
- try:
133
- proc = await asyncio.create_subprocess_exec(
134
- cyclo_exe, 'down',
135
- stdout=asyncio.subprocess.PIPE,
136
- stderr=asyncio.subprocess.PIPE,
137
- )
138
- stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30)
139
- down_output = _fmt('cyclo_manager down', (stdout.decode() + stderr.decode()))
140
- except asyncio.TimeoutError:
141
- down_output = '$ cyclo_manager down\n(timed out)'
142
- except Exception as e:
143
- down_output = f'$ cyclo_manager down\n(error: {e})'
103
+ # cyclo down
104
+ rc, out = await asyncio.to_thread(_run_cmd, [cyclo_exe, 'down'], 30)
105
+ down_output = _fmt('cyclo_manager down', out)
144
106
 
145
107
  # Continue install + up in background
146
108
  asyncio.create_task(_run_install_and_up(cyclo_exe, pip_exe))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cyclo-manager
3
- Version: 0.2.0.dev0
3
+ Version: 0.2.0.dev2
4
4
  Summary: cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack.
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -80,7 +80,8 @@ def _create_host_agent() -> int:
80
80
  )
81
81
  return 1
82
82
 
83
- user_home = Path.home()
83
+ current_user = os.environ.get('SUDO_USER') or os.environ.get('USER') or os.getlogin()
84
+ user_home = Path(f'/home/{current_user}') if current_user else Path.home()
84
85
  service_content = f"""\
85
86
  [Unit]
86
87
  Description=Cyclo Host Agent
@@ -88,6 +89,7 @@ After=network.target
88
89
 
89
90
  [Service]
90
91
  Type=simple
92
+ User={current_user}
91
93
  ExecStart={agent_exe}
92
94
  Environment=HOME={user_home}
93
95
  Restart=always
@@ -121,7 +123,12 @@ WantedBy=multi-user.target
121
123
 
122
124
  def cmd_up(args: argparse.Namespace) -> int:
123
125
  """Start API + UI; create zenoh and noVNC containers without starting them."""
124
- if not _check_host_agent():
126
+ if _check_host_agent():
127
+ try:
128
+ subprocess.run(['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'], check=True)
129
+ except (subprocess.CalledProcessError, FileNotFoundError):
130
+ print('Warning: Failed to restart host agent service.', file=sys.stderr)
131
+ else:
125
132
  if _create_host_agent() != 0:
126
133
  print('Warning: Failed to install host agent service.')
127
134
 
@@ -230,6 +237,15 @@ def cmd_update(args: argparse.Namespace) -> int:
230
237
  except subprocess.CalledProcessError as e:
231
238
  return e.returncode
232
239
 
240
+ print('Restarting host agent...')
241
+ try:
242
+ subprocess.run(
243
+ ['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'],
244
+ check=True,
245
+ )
246
+ except (subprocess.CalledProcessError, FileNotFoundError):
247
+ print('Warning: Failed to restart host agent service.', file=sys.stderr)
248
+
233
249
  print('cyclo_manager update completed.')
234
250
  return 0
235
251
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cyclo-manager"
7
- version = "0.2.0.dev0"
7
+ version = "0.2.0.dev2"
8
8
  description = "cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = cyclo-manager
3
- version = 0.2.0dev
3
+ version = 0.2.0de2
4
4
 
5
5
  [egg_info]
6
6
  egg_base = .