cyclo-manager 1.1.0.dev1__tar.gz → 1.1.0.dev3__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.
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/PKG-INFO +5 -6
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/README.md +4 -5
- cyclo_manager-1.1.0.dev3/cyclo_host_agent/cpu_usage.py +84 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/main.py +14 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/models.py +31 -0
- cyclo_manager-1.1.0.dev3/cyclo_host_agent/routers/files.py +562 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/routers/repos.py +59 -36
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/routers/system_stats.py +17 -7
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/PKG-INFO +5 -6
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/SOURCES.txt +3 -1
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager_cli/__init__.py +1 -1
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/pyproject.toml +1 -1
- cyclo_manager-1.1.0.dev3/tests/test_cpu_usage.py +169 -0
- cyclo_manager-1.1.0.dev1/cyclo_host_agent/routers/files.py +0 -216
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/__init__.py +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/routers/__init__.py +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/routers/update.py +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/dependency_links.txt +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/entry_points.txt +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/requires.txt +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/top_level.txt +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager_cli/cli.py +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager_cli/config/config.yml +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
- {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cyclo-manager
|
|
3
|
-
Version: 1.1.0.
|
|
3
|
+
Version: 1.1.0.dev3
|
|
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
|
|
@@ -137,7 +137,7 @@ The API container reaches it at `/agents/host/host_agent.sock` (see bundled `con
|
|
|
137
137
|
|
|
138
138
|
`cyclo_manager up` installs the `refresh-host-agent` CLI subcommand into sudoers. It is registered as `sys.executable -m cyclo_manager_cli.cli refresh-host-agent` with `SETENV`, so UI-triggered updates can pass the current `PYTHONPATH` to sudo and do not depend on `PATH`. The command is idempotent and refreshes the socket directory, sudoers file, systemd unit, and service state after package upgrades.
|
|
139
139
|
|
|
140
|
-
Repository scanning
|
|
140
|
+
Repository scanning and the Files UI use `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise they use the service user's home directory, except root-only devices with `/data/docker`, where `/data/docker` is used automatically. Only repositories whose `origin` remote belongs to `ROBOTIS-GIT` are returned by the repo list/update endpoints.
|
|
141
141
|
|
|
142
142
|
---
|
|
143
143
|
|
|
@@ -147,8 +147,8 @@ Defined in [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/doc
|
|
|
147
147
|
|
|
148
148
|
| Compose service | Container name | `cyclo_manager up` | Image (example) |
|
|
149
149
|
|-----------------|----------------|--------------------|-----------------|
|
|
150
|
-
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:1.0.
|
|
151
|
-
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:1.0.
|
|
150
|
+
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:1.0.1` |
|
|
151
|
+
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:1.0.1` |
|
|
152
152
|
| `rmw_zenoh` | `zenoh_daemon` | **Created only** | `robotis/zenoh-daemon:latest` |
|
|
153
153
|
| `novnc-server` | `novnc-server` | **Created only** | `robotis/novnc-server:latest` |
|
|
154
154
|
|
|
@@ -217,8 +217,7 @@ So `/agents/ai_worker/s6_agent.sock` in config corresponds to the host path abov
|
|
|
217
217
|
| Variable | Set by | Purpose |
|
|
218
218
|
|----------|--------|---------|
|
|
219
219
|
| **`CYCLO_MANAGER_CONFIG_FILE`** | CLI (`up`, `down`, `update`) | Absolute path to bundled `config.yml` on the host; mounted into the API container |
|
|
220
|
-
| **`CYCLO_HOST_AGENT_WORKSPACE`** | User or CLI-generated systemd unit | Workspace scanned by `cyclo_host_agent` for managed git repositories |
|
|
221
|
-
| **`CYCLO_HOST_AGENT_FILE_ROOT`** | User (optional) | Root directory for the Files UI/API. Defaults to the host agent service user's home (`$HOME`, e.g. `/root` when running as root) |
|
|
220
|
+
| **`CYCLO_HOST_AGENT_WORKSPACE`** | User or CLI-generated systemd unit | Workspace scanned by `cyclo_host_agent` for managed git repositories and used as the Files UI/API root |
|
|
222
221
|
| **`HOSTNAME`** | CLI (default: machine hostname) | Passed to API as `HOST_HOSTNAME` |
|
|
223
222
|
| **`CONFIG_FILE`** | Compose (`/app/config.yml`) | Path inside the API container |
|
|
224
223
|
| **`ROS_DOMAIN_ID`** | **Not** set by CLI | Set inside robot containers (e.g. `~/.bashrc`) so DDS matches your fleet |
|
|
@@ -123,7 +123,7 @@ The API container reaches it at `/agents/host/host_agent.sock` (see bundled `con
|
|
|
123
123
|
|
|
124
124
|
`cyclo_manager up` installs the `refresh-host-agent` CLI subcommand into sudoers. It is registered as `sys.executable -m cyclo_manager_cli.cli refresh-host-agent` with `SETENV`, so UI-triggered updates can pass the current `PYTHONPATH` to sudo and do not depend on `PATH`. The command is idempotent and refreshes the socket directory, sudoers file, systemd unit, and service state after package upgrades.
|
|
125
125
|
|
|
126
|
-
Repository scanning
|
|
126
|
+
Repository scanning and the Files UI use `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise they use the service user's home directory, except root-only devices with `/data/docker`, where `/data/docker` is used automatically. Only repositories whose `origin` remote belongs to `ROBOTIS-GIT` are returned by the repo list/update endpoints.
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
@@ -133,8 +133,8 @@ Defined in [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/doc
|
|
|
133
133
|
|
|
134
134
|
| Compose service | Container name | `cyclo_manager up` | Image (example) |
|
|
135
135
|
|-----------------|----------------|--------------------|-----------------|
|
|
136
|
-
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:1.0.
|
|
137
|
-
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:1.0.
|
|
136
|
+
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:1.0.1` |
|
|
137
|
+
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:1.0.1` |
|
|
138
138
|
| `rmw_zenoh` | `zenoh_daemon` | **Created only** | `robotis/zenoh-daemon:latest` |
|
|
139
139
|
| `novnc-server` | `novnc-server` | **Created only** | `robotis/novnc-server:latest` |
|
|
140
140
|
|
|
@@ -203,8 +203,7 @@ So `/agents/ai_worker/s6_agent.sock` in config corresponds to the host path abov
|
|
|
203
203
|
| Variable | Set by | Purpose |
|
|
204
204
|
|----------|--------|---------|
|
|
205
205
|
| **`CYCLO_MANAGER_CONFIG_FILE`** | CLI (`up`, `down`, `update`) | Absolute path to bundled `config.yml` on the host; mounted into the API container |
|
|
206
|
-
| **`CYCLO_HOST_AGENT_WORKSPACE`** | User or CLI-generated systemd unit | Workspace scanned by `cyclo_host_agent` for managed git repositories |
|
|
207
|
-
| **`CYCLO_HOST_AGENT_FILE_ROOT`** | User (optional) | Root directory for the Files UI/API. Defaults to the host agent service user's home (`$HOME`, e.g. `/root` when running as root) |
|
|
206
|
+
| **`CYCLO_HOST_AGENT_WORKSPACE`** | User or CLI-generated systemd unit | Workspace scanned by `cyclo_host_agent` for managed git repositories and used as the Files UI/API root |
|
|
208
207
|
| **`HOSTNAME`** | CLI (default: machine hostname) | Passed to API as `HOST_HOSTNAME` |
|
|
209
208
|
| **`CONFIG_FILE`** | Compose (`/app/config.yml`) | Path inside the API container |
|
|
210
209
|
| **`ROS_DOMAIN_ID`** | **Not** set by CLI | Set inside robot containers (e.g. `~/.bashrc`) so DDS matches your fleet |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 ROBOTIS CO., LTD.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
#
|
|
17
|
+
# Author: Hyungyu Kim
|
|
18
|
+
|
|
19
|
+
"""Continuous host CPU sampling, independent of HTTP requests."""
|
|
20
|
+
|
|
21
|
+
from collections import deque
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
|
|
25
|
+
import psutil
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
CPU_SAMPLE_INTERVAL_SECONDS = 1.0
|
|
30
|
+
CPU_AVERAGE_SAMPLES = 3
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CpuUsageSampler:
|
|
34
|
+
"""Keep the mean of the latest three consecutive one-second CPU samples."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, interval: float = CPU_SAMPLE_INTERVAL_SECONDS) -> None:
|
|
37
|
+
"""Initialize the sampler without starting a background thread."""
|
|
38
|
+
self._interval = interval
|
|
39
|
+
self._samples: deque[float] = deque(maxlen=CPU_AVERAGE_SAMPLES)
|
|
40
|
+
self._lock = threading.Lock()
|
|
41
|
+
self._stop = threading.Event()
|
|
42
|
+
self._thread: threading.Thread | None = None
|
|
43
|
+
|
|
44
|
+
def start(self) -> None:
|
|
45
|
+
"""Start one sampling thread and discard samples from any previous run."""
|
|
46
|
+
if self._thread is not None and self._thread.is_alive():
|
|
47
|
+
return
|
|
48
|
+
with self._lock:
|
|
49
|
+
self._samples.clear()
|
|
50
|
+
self._stop.clear()
|
|
51
|
+
self._thread = threading.Thread(
|
|
52
|
+
target=self._run, name='host-cpu-sampler', daemon=True,
|
|
53
|
+
)
|
|
54
|
+
self._thread.start()
|
|
55
|
+
|
|
56
|
+
def stop(self) -> None:
|
|
57
|
+
"""Wake and join the sampler on application shutdown."""
|
|
58
|
+
self._stop.set()
|
|
59
|
+
if self._thread is not None:
|
|
60
|
+
self._thread.join()
|
|
61
|
+
self._thread = None
|
|
62
|
+
|
|
63
|
+
def average(self) -> float | None:
|
|
64
|
+
"""Return a cached average, or None before the first complete interval."""
|
|
65
|
+
with self._lock:
|
|
66
|
+
if not self._samples:
|
|
67
|
+
return None
|
|
68
|
+
return round(sum(self._samples) / len(self._samples), 1)
|
|
69
|
+
|
|
70
|
+
def _run(self) -> None:
|
|
71
|
+
while not self._stop.is_set():
|
|
72
|
+
try:
|
|
73
|
+
# Prime and read on the same thread. The initial value is not a sample.
|
|
74
|
+
psutil.cpu_percent(interval=None)
|
|
75
|
+
while not self._stop.wait(self._interval):
|
|
76
|
+
sample = psutil.cpu_percent(interval=None)
|
|
77
|
+
with self._lock:
|
|
78
|
+
self._samples.append(float(sample))
|
|
79
|
+
except Exception:
|
|
80
|
+
# Re-prime after a failure instead of mixing stale measurement intervals.
|
|
81
|
+
with self._lock:
|
|
82
|
+
self._samples.clear()
|
|
83
|
+
logger.exception('Failed to sample host CPU usage')
|
|
84
|
+
self._stop.wait(self._interval)
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
"""Cyclo host agent: FastAPI server on Unix Domain Socket for host-level operations."""
|
|
20
20
|
|
|
21
|
+
import asyncio
|
|
22
|
+
from contextlib import asynccontextmanager
|
|
21
23
|
import logging
|
|
22
24
|
from pathlib import Path
|
|
23
25
|
|
|
@@ -34,10 +36,22 @@ logger = logging.getLogger(__name__)
|
|
|
34
36
|
|
|
35
37
|
SOCKET_PATH = '/var/run/robotis/agent_sockets/host/host_agent.sock'
|
|
36
38
|
|
|
39
|
+
|
|
40
|
+
@asynccontextmanager
|
|
41
|
+
async def lifespan(app: FastAPI):
|
|
42
|
+
"""Run the shared CPU sampler for the lifetime of the host agent."""
|
|
43
|
+
system_stats.cpu_sampler.start()
|
|
44
|
+
try:
|
|
45
|
+
yield
|
|
46
|
+
finally:
|
|
47
|
+
await asyncio.to_thread(system_stats.cpu_sampler.stop)
|
|
48
|
+
|
|
49
|
+
|
|
37
50
|
app = FastAPI(
|
|
38
51
|
title='cyclo_host_agent',
|
|
39
52
|
description='Host agent for Cyclo Manager: repo management.',
|
|
40
53
|
version=__version__,
|
|
54
|
+
lifespan=lifespan,
|
|
41
55
|
)
|
|
42
56
|
|
|
43
57
|
app.include_router(repos.router)
|
|
@@ -160,6 +160,7 @@ class FileTreeEntry(BaseModel):
|
|
|
160
160
|
readonly: bool = False
|
|
161
161
|
hidden: bool = False
|
|
162
162
|
symlink: bool = False
|
|
163
|
+
git_status: str | None = None
|
|
163
164
|
|
|
164
165
|
|
|
165
166
|
class FileTreeResponse(BaseModel):
|
|
@@ -170,6 +171,16 @@ class FileTreeResponse(BaseModel):
|
|
|
170
171
|
entries: list[FileTreeEntry]
|
|
171
172
|
|
|
172
173
|
|
|
174
|
+
class FileSearchResponse(BaseModel):
|
|
175
|
+
"""Response for GET /files/search."""
|
|
176
|
+
|
|
177
|
+
root_path: str
|
|
178
|
+
path: str
|
|
179
|
+
query: str
|
|
180
|
+
entries: list[FileTreeEntry]
|
|
181
|
+
truncated: bool = False
|
|
182
|
+
|
|
183
|
+
|
|
173
184
|
class FileReadResponse(BaseModel):
|
|
174
185
|
"""Response for GET /files/read."""
|
|
175
186
|
|
|
@@ -180,6 +191,15 @@ class FileReadResponse(BaseModel):
|
|
|
180
191
|
readonly: bool = False
|
|
181
192
|
|
|
182
193
|
|
|
194
|
+
class FileDiffResponse(BaseModel):
|
|
195
|
+
"""Response for GET /files/diff."""
|
|
196
|
+
|
|
197
|
+
path: str
|
|
198
|
+
status: str
|
|
199
|
+
original_content: str
|
|
200
|
+
current_content: str
|
|
201
|
+
|
|
202
|
+
|
|
183
203
|
class FileWriteRequest(BaseModel):
|
|
184
204
|
"""Request body for POST /files/write."""
|
|
185
205
|
|
|
@@ -203,6 +223,17 @@ class FileRenameRequest(BaseModel):
|
|
|
203
223
|
new_name: str
|
|
204
224
|
|
|
205
225
|
|
|
226
|
+
class FileUploadResponse(BaseModel):
|
|
227
|
+
"""Response for POST /files/upload."""
|
|
228
|
+
|
|
229
|
+
name: str
|
|
230
|
+
path: str
|
|
231
|
+
size: int
|
|
232
|
+
overwritten: bool = False
|
|
233
|
+
success: bool
|
|
234
|
+
message: str
|
|
235
|
+
|
|
236
|
+
|
|
206
237
|
class FileOperationResponse(BaseModel):
|
|
207
238
|
"""Generic file operation response."""
|
|
208
239
|
|