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.
Files changed (25) hide show
  1. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/PKG-INFO +5 -6
  2. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/README.md +4 -5
  3. cyclo_manager-1.1.0.dev3/cyclo_host_agent/cpu_usage.py +84 -0
  4. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/main.py +14 -0
  5. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/models.py +31 -0
  6. cyclo_manager-1.1.0.dev3/cyclo_host_agent/routers/files.py +562 -0
  7. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/routers/repos.py +59 -36
  8. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/routers/system_stats.py +17 -7
  9. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/PKG-INFO +5 -6
  10. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/SOURCES.txt +3 -1
  11. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager_cli/__init__.py +1 -1
  12. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/pyproject.toml +1 -1
  13. cyclo_manager-1.1.0.dev3/tests/test_cpu_usage.py +169 -0
  14. cyclo_manager-1.1.0.dev1/cyclo_host_agent/routers/files.py +0 -216
  15. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/__init__.py +0 -0
  16. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/routers/__init__.py +0 -0
  17. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_host_agent/routers/update.py +0 -0
  18. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/dependency_links.txt +0 -0
  19. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/entry_points.txt +0 -0
  20. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/requires.txt +0 -0
  21. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager.egg-info/top_level.txt +0 -0
  22. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager_cli/cli.py +0 -0
  23. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager_cli/config/config.yml +0 -0
  24. {cyclo_manager-1.1.0.dev1 → cyclo_manager-1.1.0.dev3}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
  25. {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.dev1
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 uses `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise it uses 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.
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.0` |
151
- | `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:1.0.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 uses `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise it uses 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.
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.0` |
137
- | `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:1.0.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