fbuild 1.1.0__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.
Potentially problematic release.
This version of fbuild might be problematic. Click here for more details.
- fbuild/__init__.py +0 -0
- fbuild/assets/example.txt +1 -0
- fbuild/build/__init__.py +117 -0
- fbuild/build/archive_creator.py +186 -0
- fbuild/build/binary_generator.py +444 -0
- fbuild/build/build_component_factory.py +131 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +98 -0
- fbuild/build/compilation_executor.py +422 -0
- fbuild/build/compiler.py +165 -0
- fbuild/build/compiler_avr.py +574 -0
- fbuild/build/configurable_compiler.py +612 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +186 -0
- fbuild/build/library_dependency_processor.py +185 -0
- fbuild/build/linker.py +708 -0
- fbuild/build/orchestrator.py +67 -0
- fbuild/build/orchestrator_avr.py +656 -0
- fbuild/build/orchestrator_esp32.py +797 -0
- fbuild/build/orchestrator_teensy.py +543 -0
- fbuild/build/source_compilation_orchestrator.py +220 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +566 -0
- fbuild/cli_utils.py +312 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +457 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +209 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +34 -0
- fbuild/daemon/client.py +929 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/daemon.py +474 -0
- fbuild/daemon/daemon_context.py +196 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/lock_manager.py +270 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +301 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +12 -0
- fbuild/daemon/processors/build_processor.py +157 -0
- fbuild/daemon/processors/deploy_processor.py +327 -0
- fbuild/daemon/processors/monitor_processor.py +146 -0
- fbuild/daemon/request_processor.py +401 -0
- fbuild/daemon/status_manager.py +216 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +17 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +314 -0
- fbuild/deploy/monitor.py +495 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/packages/__init__.py +53 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +249 -0
- fbuild/packages/downloader.py +366 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_teensy.py +346 -0
- fbuild/packages/github_utils.py +96 -0
- fbuild/packages/header_trampoline_cache.py +394 -0
- fbuild/packages/library_compiler.py +203 -0
- fbuild/packages/library_manager.py +549 -0
- fbuild/packages/library_manager_esp32.py +413 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +325 -0
- fbuild/packages/sdk_utils.py +231 -0
- fbuild/packages/toolchain.py +436 -0
- fbuild/packages/toolchain_binaries.py +196 -0
- fbuild/packages/toolchain_esp32.py +484 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_teensy.py +404 -0
- fbuild/platform_configs/esp32.json +150 -0
- fbuild/platform_configs/esp32c2.json +144 -0
- fbuild/platform_configs/esp32c3.json +143 -0
- fbuild/platform_configs/esp32c5.json +151 -0
- fbuild/platform_configs/esp32c6.json +151 -0
- fbuild/platform_configs/esp32p4.json +149 -0
- fbuild/platform_configs/esp32s3.json +151 -0
- fbuild/platform_configs/imxrt1062.json +56 -0
- fbuild-1.1.0.dist-info/METADATA +447 -0
- fbuild-1.1.0.dist-info/RECORD +93 -0
- fbuild-1.1.0.dist-info/WHEEL +5 -0
- fbuild-1.1.0.dist-info/entry_points.txt +5 -0
- fbuild-1.1.0.dist-info/licenses/LICENSE +21 -0
- fbuild-1.1.0.dist-info/top_level.txt +2 -0
- fbuild_lint/__init__.py +0 -0
- fbuild_lint/ruff_plugins/__init__.py +0 -0
- fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
fbuild/daemon/client.py
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fbuild Daemon Client
|
|
3
|
+
|
|
4
|
+
Client interface for requesting deploy and monitor operations from the daemon.
|
|
5
|
+
Handles daemon lifecycle, request submission, and progress monitoring.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import _thread
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import psutil
|
|
19
|
+
|
|
20
|
+
from fbuild.daemon.messages import (
|
|
21
|
+
BuildRequest,
|
|
22
|
+
DaemonState,
|
|
23
|
+
DaemonStatus,
|
|
24
|
+
DeployRequest,
|
|
25
|
+
MonitorRequest,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Daemon configuration (must match daemon settings)
|
|
29
|
+
DAEMON_NAME = "fbuild_daemon"
|
|
30
|
+
DAEMON_DIR = Path.home() / ".fbuild" / "daemon"
|
|
31
|
+
PID_FILE = DAEMON_DIR / f"{DAEMON_NAME}.pid"
|
|
32
|
+
STATUS_FILE = DAEMON_DIR / "daemon_status.json"
|
|
33
|
+
BUILD_REQUEST_FILE = DAEMON_DIR / "build_request.json"
|
|
34
|
+
DEPLOY_REQUEST_FILE = DAEMON_DIR / "deploy_request.json"
|
|
35
|
+
MONITOR_REQUEST_FILE = DAEMON_DIR / "monitor_request.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_daemon_running() -> bool:
|
|
39
|
+
"""Check if daemon is running, clean up stale PID files.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
True if daemon is running, False otherwise
|
|
43
|
+
"""
|
|
44
|
+
if not PID_FILE.exists():
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with open(PID_FILE) as f:
|
|
49
|
+
pid = int(f.read().strip())
|
|
50
|
+
|
|
51
|
+
# Check if process exists
|
|
52
|
+
if psutil.pid_exists(pid):
|
|
53
|
+
return True
|
|
54
|
+
else:
|
|
55
|
+
# Stale PID file - remove it
|
|
56
|
+
print(f"Removing stale PID file: {PID_FILE}")
|
|
57
|
+
PID_FILE.unlink()
|
|
58
|
+
return False
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
_thread.interrupt_main()
|
|
61
|
+
raise
|
|
62
|
+
except Exception:
|
|
63
|
+
# Corrupted PID file - remove it
|
|
64
|
+
try:
|
|
65
|
+
PID_FILE.unlink(missing_ok=True)
|
|
66
|
+
except KeyboardInterrupt:
|
|
67
|
+
_thread.interrupt_main()
|
|
68
|
+
raise
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def start_daemon() -> None:
|
|
75
|
+
"""Start the daemon process."""
|
|
76
|
+
daemon_script = Path(__file__).parent / "daemon.py"
|
|
77
|
+
|
|
78
|
+
if not daemon_script.exists():
|
|
79
|
+
raise RuntimeError(f"Daemon script not found: {daemon_script}")
|
|
80
|
+
|
|
81
|
+
# Start daemon in background
|
|
82
|
+
subprocess.Popen(
|
|
83
|
+
[sys.executable, str(daemon_script)],
|
|
84
|
+
stdout=subprocess.DEVNULL,
|
|
85
|
+
stderr=subprocess.DEVNULL,
|
|
86
|
+
stdin=subprocess.DEVNULL,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def read_status_file() -> DaemonStatus:
|
|
91
|
+
"""Read current daemon status with corruption recovery.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
DaemonStatus object (or default status if file doesn't exist or corrupted)
|
|
95
|
+
"""
|
|
96
|
+
if not STATUS_FILE.exists():
|
|
97
|
+
return DaemonStatus(
|
|
98
|
+
state=DaemonState.UNKNOWN,
|
|
99
|
+
message="Status file not found",
|
|
100
|
+
updated_at=time.time(),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
with open(STATUS_FILE) as f:
|
|
105
|
+
data = json.load(f)
|
|
106
|
+
|
|
107
|
+
# Parse into typed DaemonStatus
|
|
108
|
+
return DaemonStatus.from_dict(data)
|
|
109
|
+
|
|
110
|
+
except (json.JSONDecodeError, ValueError):
|
|
111
|
+
# Corrupted JSON - return default status
|
|
112
|
+
return DaemonStatus(
|
|
113
|
+
state=DaemonState.UNKNOWN,
|
|
114
|
+
message="Status file corrupted (invalid JSON)",
|
|
115
|
+
updated_at=time.time(),
|
|
116
|
+
)
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
_thread.interrupt_main()
|
|
119
|
+
raise
|
|
120
|
+
except Exception:
|
|
121
|
+
return DaemonStatus(
|
|
122
|
+
state=DaemonState.UNKNOWN,
|
|
123
|
+
message="Failed to read status",
|
|
124
|
+
updated_at=time.time(),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def write_request_file(request_file: Path, request: Any) -> None:
|
|
129
|
+
"""Atomically write request file.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
request_file: Path to request file
|
|
133
|
+
request: Request object (DeployRequest or MonitorRequest)
|
|
134
|
+
"""
|
|
135
|
+
DAEMON_DIR.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
# Atomic write using temporary file
|
|
138
|
+
temp_file = request_file.with_suffix(".tmp")
|
|
139
|
+
with open(temp_file, "w") as f:
|
|
140
|
+
json.dump(request.to_dict(), f, indent=2)
|
|
141
|
+
|
|
142
|
+
# Atomic rename
|
|
143
|
+
temp_file.replace(request_file)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def display_status(status: DaemonStatus, prefix: str = " ") -> None:
|
|
147
|
+
"""Display status update to user.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
status: DaemonStatus object
|
|
151
|
+
prefix: Line prefix for indentation
|
|
152
|
+
"""
|
|
153
|
+
# Show current operation if available, otherwise use message
|
|
154
|
+
display_text = status.current_operation or status.message
|
|
155
|
+
|
|
156
|
+
if status.state == DaemonState.DEPLOYING:
|
|
157
|
+
print(f"{prefix}📦 {display_text}", flush=True)
|
|
158
|
+
elif status.state == DaemonState.MONITORING:
|
|
159
|
+
print(f"{prefix}👁️ {display_text}", flush=True)
|
|
160
|
+
elif status.state == DaemonState.BUILDING:
|
|
161
|
+
print(f"{prefix}🔨 {display_text}", flush=True)
|
|
162
|
+
elif status.state == DaemonState.COMPLETED:
|
|
163
|
+
print(f"{prefix}✅ {display_text}", flush=True)
|
|
164
|
+
elif status.state == DaemonState.FAILED:
|
|
165
|
+
print(f"{prefix}❌ {display_text}", flush=True)
|
|
166
|
+
else:
|
|
167
|
+
print(f"{prefix}ℹ️ {display_text}", flush=True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def ensure_daemon_running() -> bool:
|
|
171
|
+
"""Ensure daemon is running, start if needed.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if daemon is running or started successfully, False otherwise
|
|
175
|
+
"""
|
|
176
|
+
if is_daemon_running():
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
# If we reach here, daemon is not running (stale PID was cleaned by is_daemon_running)
|
|
180
|
+
# Clear stale status file to prevent race condition where client reads old status
|
|
181
|
+
# from previous daemon run before new daemon writes fresh status
|
|
182
|
+
if STATUS_FILE.exists():
|
|
183
|
+
try:
|
|
184
|
+
STATUS_FILE.unlink()
|
|
185
|
+
except KeyboardInterrupt:
|
|
186
|
+
_thread.interrupt_main()
|
|
187
|
+
raise
|
|
188
|
+
except Exception:
|
|
189
|
+
pass # Best effort - continue even if delete fails
|
|
190
|
+
|
|
191
|
+
print("🔗 Starting fbuild daemon...")
|
|
192
|
+
start_daemon()
|
|
193
|
+
|
|
194
|
+
# Wait up to 10 seconds for daemon to start and write fresh status
|
|
195
|
+
for _ in range(10):
|
|
196
|
+
if is_daemon_running():
|
|
197
|
+
# Daemon is running - check if status file is fresh
|
|
198
|
+
status = read_status_file()
|
|
199
|
+
if status.state != DaemonState.UNKNOWN:
|
|
200
|
+
# Valid status received from new daemon
|
|
201
|
+
print("✅ Daemon started successfully")
|
|
202
|
+
return True
|
|
203
|
+
time.sleep(1)
|
|
204
|
+
|
|
205
|
+
print("❌ Failed to start daemon")
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def request_build(
|
|
210
|
+
project_dir: Path,
|
|
211
|
+
environment: str,
|
|
212
|
+
clean_build: bool = False,
|
|
213
|
+
verbose: bool = False,
|
|
214
|
+
timeout: float = 1800,
|
|
215
|
+
) -> bool:
|
|
216
|
+
"""Request a build operation from the daemon.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
project_dir: Project directory
|
|
220
|
+
environment: Build environment
|
|
221
|
+
clean_build: Whether to perform clean build
|
|
222
|
+
verbose: Enable verbose build output
|
|
223
|
+
timeout: Maximum wait time in seconds (default: 30 minutes)
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if build successful, False otherwise
|
|
227
|
+
"""
|
|
228
|
+
handler = BuildRequestHandler(
|
|
229
|
+
project_dir=project_dir,
|
|
230
|
+
environment=environment,
|
|
231
|
+
clean_build=clean_build,
|
|
232
|
+
verbose=verbose,
|
|
233
|
+
timeout=timeout,
|
|
234
|
+
)
|
|
235
|
+
return handler.execute()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _display_monitor_summary(project_dir: Path) -> None:
|
|
239
|
+
"""Display monitor summary from JSON file.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
project_dir: Project directory where summary file is located
|
|
243
|
+
"""
|
|
244
|
+
summary_file = project_dir / ".fbuild" / "monitor_summary.json"
|
|
245
|
+
if not summary_file.exists():
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
with open(summary_file, "r", encoding="utf-8") as f:
|
|
250
|
+
summary = json.load(f)
|
|
251
|
+
|
|
252
|
+
print("\n" + "=" * 50)
|
|
253
|
+
print("Monitor Summary")
|
|
254
|
+
print("=" * 50)
|
|
255
|
+
|
|
256
|
+
# Display expect pattern result
|
|
257
|
+
if summary.get("expect_pattern"):
|
|
258
|
+
pattern = summary["expect_pattern"]
|
|
259
|
+
found = summary.get("expect_found", False)
|
|
260
|
+
status = "FOUND ✓" if found else "NOT FOUND ✗"
|
|
261
|
+
print(f'Expected pattern: "{pattern}" - {status}')
|
|
262
|
+
|
|
263
|
+
# Display halt on error pattern result
|
|
264
|
+
if summary.get("halt_on_error_pattern"):
|
|
265
|
+
pattern = summary["halt_on_error_pattern"]
|
|
266
|
+
found = summary.get("halt_on_error_found", False)
|
|
267
|
+
status = "FOUND ✗" if found else "NOT FOUND ✓"
|
|
268
|
+
print(f'Error pattern: "{pattern}" - {status}')
|
|
269
|
+
|
|
270
|
+
# Display halt on success pattern result
|
|
271
|
+
if summary.get("halt_on_success_pattern"):
|
|
272
|
+
pattern = summary["halt_on_success_pattern"]
|
|
273
|
+
found = summary.get("halt_on_success_found", False)
|
|
274
|
+
status = "FOUND ✓" if found else "NOT FOUND ✗"
|
|
275
|
+
print(f'Success pattern: "{pattern}" - {status}')
|
|
276
|
+
|
|
277
|
+
# Display statistics
|
|
278
|
+
lines = summary.get("lines_processed", 0)
|
|
279
|
+
elapsed = summary.get("elapsed_time", 0.0)
|
|
280
|
+
exit_reason = summary.get("exit_reason", "unknown")
|
|
281
|
+
|
|
282
|
+
print(f"Lines processed: {lines}")
|
|
283
|
+
print(f"Time elapsed: {elapsed:.2f}s")
|
|
284
|
+
|
|
285
|
+
# Translate exit_reason to user-friendly text
|
|
286
|
+
reason_text = {
|
|
287
|
+
"timeout": "Timeout reached",
|
|
288
|
+
"expect_found": "Expected pattern found",
|
|
289
|
+
"halt_error": "Error pattern detected",
|
|
290
|
+
"halt_success": "Success pattern detected",
|
|
291
|
+
"interrupted": "Interrupted by user",
|
|
292
|
+
"error": "Serial port error",
|
|
293
|
+
}.get(exit_reason, exit_reason)
|
|
294
|
+
|
|
295
|
+
print(f"Exit reason: {reason_text}")
|
|
296
|
+
print("=" * 50)
|
|
297
|
+
|
|
298
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
299
|
+
raise
|
|
300
|
+
except Exception:
|
|
301
|
+
# Silently fail - don't disrupt the user experience
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ============================================================================
|
|
306
|
+
# REQUEST HANDLER ARCHITECTURE
|
|
307
|
+
# ============================================================================
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class BaseRequestHandler(ABC):
|
|
311
|
+
"""Base class for handling daemon requests with common functionality.
|
|
312
|
+
|
|
313
|
+
Implements the template method pattern to eliminate duplication across
|
|
314
|
+
build, deploy, and monitor request handlers.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
def __init__(self, project_dir: Path, environment: str, timeout: float = 1800):
|
|
318
|
+
"""Initialize request handler.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
project_dir: Project directory
|
|
322
|
+
environment: Build environment
|
|
323
|
+
timeout: Maximum wait time in seconds (default: 30 minutes)
|
|
324
|
+
"""
|
|
325
|
+
self.project_dir = project_dir
|
|
326
|
+
self.environment = environment
|
|
327
|
+
self.timeout = timeout
|
|
328
|
+
self.start_time = 0.0
|
|
329
|
+
self.last_message: str | None = None
|
|
330
|
+
self.monitoring_started = False
|
|
331
|
+
self.output_file_position = 0
|
|
332
|
+
|
|
333
|
+
@abstractmethod
|
|
334
|
+
def create_request(self) -> BuildRequest | DeployRequest | MonitorRequest:
|
|
335
|
+
"""Create the specific request object.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Request object (BuildRequest, DeployRequest, or MonitorRequest)
|
|
339
|
+
"""
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
@abstractmethod
|
|
343
|
+
def get_request_file(self) -> Path:
|
|
344
|
+
"""Get the request file path.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Path to request file
|
|
348
|
+
"""
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
@abstractmethod
|
|
352
|
+
def get_operation_name(self) -> str:
|
|
353
|
+
"""Get the operation name for display.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Operation name (e.g., "Build", "Deploy", "Monitor")
|
|
357
|
+
"""
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
@abstractmethod
|
|
361
|
+
def get_operation_emoji(self) -> str:
|
|
362
|
+
"""Get the operation emoji for display.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Operation emoji (e.g., "🔨", "📦", "👁️")
|
|
366
|
+
"""
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
def should_tail_output(self) -> bool:
|
|
370
|
+
"""Check if output file should be tailed.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
True if output should be tailed, False otherwise
|
|
374
|
+
"""
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
def on_monitoring_started(self) -> None:
|
|
378
|
+
"""Hook called when monitoring phase starts."""
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
def on_completion(self, elapsed: float) -> None:
|
|
382
|
+
"""Hook called on successful completion.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
elapsed: Elapsed time in seconds
|
|
386
|
+
"""
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
def on_failure(self, status: DaemonStatus, elapsed: float) -> None:
|
|
390
|
+
"""Hook called on failure.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
status: Current daemon status
|
|
394
|
+
elapsed: Elapsed time in seconds
|
|
395
|
+
"""
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
def print_submission_info(self) -> None:
|
|
399
|
+
"""Print request submission information."""
|
|
400
|
+
print(f"\n📤 Submitting {self.get_operation_name().lower()} request...")
|
|
401
|
+
print(f" Project: {self.project_dir}")
|
|
402
|
+
print(f" Environment: {self.environment}")
|
|
403
|
+
|
|
404
|
+
def tail_output_file(self) -> None:
|
|
405
|
+
"""Tail the output file and print new lines."""
|
|
406
|
+
output_file = self.project_dir / ".fbuild" / "monitor_output.txt"
|
|
407
|
+
if output_file.exists():
|
|
408
|
+
try:
|
|
409
|
+
with open(output_file, "r", encoding="utf-8", errors="replace") as f:
|
|
410
|
+
f.seek(self.output_file_position)
|
|
411
|
+
new_lines = f.read()
|
|
412
|
+
if new_lines:
|
|
413
|
+
print(new_lines, end="", flush=True)
|
|
414
|
+
self.output_file_position = f.tell()
|
|
415
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
416
|
+
raise
|
|
417
|
+
except Exception:
|
|
418
|
+
pass # Ignore read errors
|
|
419
|
+
|
|
420
|
+
def read_remaining_output(self) -> None:
|
|
421
|
+
"""Read any remaining output from output file."""
|
|
422
|
+
if not self.monitoring_started:
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
output_file = self.project_dir / ".fbuild" / "monitor_output.txt"
|
|
426
|
+
if output_file.exists():
|
|
427
|
+
try:
|
|
428
|
+
with open(output_file, "r", encoding="utf-8", errors="replace") as f:
|
|
429
|
+
f.seek(self.output_file_position)
|
|
430
|
+
new_lines = f.read()
|
|
431
|
+
if new_lines:
|
|
432
|
+
print(new_lines, end="", flush=True)
|
|
433
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
434
|
+
raise
|
|
435
|
+
except Exception:
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
def handle_keyboard_interrupt(self, request_id: str) -> bool:
|
|
439
|
+
"""Handle keyboard interrupt with background option.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
request_id: Request ID for cancellation
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
False (operation not completed or cancelled)
|
|
446
|
+
"""
|
|
447
|
+
print("\n\n⚠️ Interrupted by user (Ctrl-C)")
|
|
448
|
+
response = input("Keep operation running in background? (y/n): ").strip().lower()
|
|
449
|
+
|
|
450
|
+
if response in ("y", "yes"):
|
|
451
|
+
print("\n✅ Operation continues in background")
|
|
452
|
+
print(" Check status: fbuild daemon status")
|
|
453
|
+
print(" Stop daemon: fbuild daemon stop")
|
|
454
|
+
return False
|
|
455
|
+
else:
|
|
456
|
+
print("\n🛑 Requesting daemon to stop operation...")
|
|
457
|
+
cancel_file = DAEMON_DIR / f"cancel_{request_id}.signal"
|
|
458
|
+
cancel_file.touch()
|
|
459
|
+
print(" Operation cancellation requested")
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
def execute(self) -> bool:
|
|
463
|
+
"""Execute the request and monitor progress.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
True if operation successful, False otherwise
|
|
467
|
+
"""
|
|
468
|
+
# Ensure daemon is running
|
|
469
|
+
if not ensure_daemon_running():
|
|
470
|
+
return False
|
|
471
|
+
|
|
472
|
+
# Print submission info
|
|
473
|
+
self.print_submission_info()
|
|
474
|
+
|
|
475
|
+
# Create and submit request
|
|
476
|
+
request = self.create_request()
|
|
477
|
+
write_request_file(self.get_request_file(), request)
|
|
478
|
+
print(f" Request ID: {request.request_id}")
|
|
479
|
+
print(" ✅ Submitted\n")
|
|
480
|
+
|
|
481
|
+
# Monitor progress
|
|
482
|
+
print(f"{self.get_operation_emoji()} {self.get_operation_name()} Progress:")
|
|
483
|
+
self.start_time = time.time()
|
|
484
|
+
|
|
485
|
+
while True:
|
|
486
|
+
try:
|
|
487
|
+
elapsed = time.time() - self.start_time
|
|
488
|
+
|
|
489
|
+
# Check timeout
|
|
490
|
+
if elapsed > self.timeout:
|
|
491
|
+
print(f"\n❌ {self.get_operation_name()} timeout ({self.timeout}s)")
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
# Read status
|
|
495
|
+
status = read_status_file()
|
|
496
|
+
|
|
497
|
+
# Display progress when message changes
|
|
498
|
+
if status.message != self.last_message:
|
|
499
|
+
display_status(status)
|
|
500
|
+
self.last_message = status.message
|
|
501
|
+
|
|
502
|
+
# Handle monitoring phase
|
|
503
|
+
if self.should_tail_output() and status.state == DaemonState.MONITORING:
|
|
504
|
+
if not self.monitoring_started:
|
|
505
|
+
self.monitoring_started = True
|
|
506
|
+
print() # Blank line before serial output
|
|
507
|
+
self.on_monitoring_started()
|
|
508
|
+
|
|
509
|
+
if self.monitoring_started and self.should_tail_output():
|
|
510
|
+
self.tail_output_file()
|
|
511
|
+
|
|
512
|
+
# Check completion
|
|
513
|
+
if status.state == DaemonState.COMPLETED:
|
|
514
|
+
if status.request_id == request.request_id:
|
|
515
|
+
self.read_remaining_output()
|
|
516
|
+
self.on_completion(elapsed)
|
|
517
|
+
print(f"\n✅ {self.get_operation_name()} completed in {elapsed:.1f}s")
|
|
518
|
+
return True
|
|
519
|
+
|
|
520
|
+
elif status.state == DaemonState.FAILED:
|
|
521
|
+
if status.request_id == request.request_id:
|
|
522
|
+
self.read_remaining_output()
|
|
523
|
+
self.on_failure(status, elapsed)
|
|
524
|
+
print(f"\n❌ {self.get_operation_name()} failed: {status.message}")
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
# Sleep before next poll
|
|
528
|
+
poll_interval = 0.1 if self.monitoring_started else 0.5
|
|
529
|
+
time.sleep(poll_interval)
|
|
530
|
+
|
|
531
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
532
|
+
return self.handle_keyboard_interrupt(request.request_id)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class BuildRequestHandler(BaseRequestHandler):
|
|
536
|
+
"""Handler for build requests."""
|
|
537
|
+
|
|
538
|
+
def __init__(
|
|
539
|
+
self,
|
|
540
|
+
project_dir: Path,
|
|
541
|
+
environment: str,
|
|
542
|
+
clean_build: bool = False,
|
|
543
|
+
verbose: bool = False,
|
|
544
|
+
timeout: float = 1800,
|
|
545
|
+
):
|
|
546
|
+
"""Initialize build request handler.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
project_dir: Project directory
|
|
550
|
+
environment: Build environment
|
|
551
|
+
clean_build: Whether to perform clean build
|
|
552
|
+
verbose: Enable verbose build output
|
|
553
|
+
timeout: Maximum wait time in seconds
|
|
554
|
+
"""
|
|
555
|
+
super().__init__(project_dir, environment, timeout)
|
|
556
|
+
self.clean_build = clean_build
|
|
557
|
+
self.verbose = verbose
|
|
558
|
+
|
|
559
|
+
def create_request(self) -> BuildRequest:
|
|
560
|
+
"""Create build request."""
|
|
561
|
+
return BuildRequest(
|
|
562
|
+
project_dir=str(self.project_dir.absolute()),
|
|
563
|
+
environment=self.environment,
|
|
564
|
+
clean_build=self.clean_build,
|
|
565
|
+
verbose=self.verbose,
|
|
566
|
+
caller_pid=os.getpid(),
|
|
567
|
+
caller_cwd=os.getcwd(),
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
def get_request_file(self) -> Path:
|
|
571
|
+
"""Get build request file path."""
|
|
572
|
+
return BUILD_REQUEST_FILE
|
|
573
|
+
|
|
574
|
+
def get_operation_name(self) -> str:
|
|
575
|
+
"""Get operation name."""
|
|
576
|
+
return "Build"
|
|
577
|
+
|
|
578
|
+
def get_operation_emoji(self) -> str:
|
|
579
|
+
"""Get operation emoji."""
|
|
580
|
+
return "🔨"
|
|
581
|
+
|
|
582
|
+
def print_submission_info(self) -> None:
|
|
583
|
+
"""Print build submission information."""
|
|
584
|
+
super().print_submission_info()
|
|
585
|
+
if self.clean_build:
|
|
586
|
+
print(" Clean build: Yes")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class DeployRequestHandler(BaseRequestHandler):
|
|
590
|
+
"""Handler for deploy requests."""
|
|
591
|
+
|
|
592
|
+
def __init__(
|
|
593
|
+
self,
|
|
594
|
+
project_dir: Path,
|
|
595
|
+
environment: str,
|
|
596
|
+
port: str | None = None,
|
|
597
|
+
clean_build: bool = False,
|
|
598
|
+
monitor_after: bool = False,
|
|
599
|
+
monitor_timeout: float | None = None,
|
|
600
|
+
monitor_halt_on_error: str | None = None,
|
|
601
|
+
monitor_halt_on_success: str | None = None,
|
|
602
|
+
monitor_expect: str | None = None,
|
|
603
|
+
timeout: float = 1800,
|
|
604
|
+
):
|
|
605
|
+
"""Initialize deploy request handler.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
project_dir: Project directory
|
|
609
|
+
environment: Build environment
|
|
610
|
+
port: Serial port (optional)
|
|
611
|
+
clean_build: Whether to perform clean build
|
|
612
|
+
monitor_after: Whether to start monitor after deploy
|
|
613
|
+
monitor_timeout: Timeout for monitor
|
|
614
|
+
monitor_halt_on_error: Pattern to halt on error
|
|
615
|
+
monitor_halt_on_success: Pattern to halt on success
|
|
616
|
+
monitor_expect: Expected pattern to check
|
|
617
|
+
timeout: Maximum wait time in seconds
|
|
618
|
+
"""
|
|
619
|
+
super().__init__(project_dir, environment, timeout)
|
|
620
|
+
self.port = port
|
|
621
|
+
self.clean_build = clean_build
|
|
622
|
+
self.monitor_after = monitor_after
|
|
623
|
+
self.monitor_timeout = monitor_timeout
|
|
624
|
+
self.monitor_halt_on_error = monitor_halt_on_error
|
|
625
|
+
self.monitor_halt_on_success = monitor_halt_on_success
|
|
626
|
+
self.monitor_expect = monitor_expect
|
|
627
|
+
|
|
628
|
+
def create_request(self) -> DeployRequest:
|
|
629
|
+
"""Create deploy request."""
|
|
630
|
+
return DeployRequest(
|
|
631
|
+
project_dir=str(self.project_dir.absolute()),
|
|
632
|
+
environment=self.environment,
|
|
633
|
+
port=self.port,
|
|
634
|
+
clean_build=self.clean_build,
|
|
635
|
+
monitor_after=self.monitor_after,
|
|
636
|
+
monitor_timeout=self.monitor_timeout,
|
|
637
|
+
monitor_halt_on_error=self.monitor_halt_on_error,
|
|
638
|
+
monitor_halt_on_success=self.monitor_halt_on_success,
|
|
639
|
+
monitor_expect=self.monitor_expect,
|
|
640
|
+
caller_pid=os.getpid(),
|
|
641
|
+
caller_cwd=os.getcwd(),
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def get_request_file(self) -> Path:
|
|
645
|
+
"""Get deploy request file path."""
|
|
646
|
+
return DEPLOY_REQUEST_FILE
|
|
647
|
+
|
|
648
|
+
def get_operation_name(self) -> str:
|
|
649
|
+
"""Get operation name."""
|
|
650
|
+
return "Deploy"
|
|
651
|
+
|
|
652
|
+
def get_operation_emoji(self) -> str:
|
|
653
|
+
"""Get operation emoji."""
|
|
654
|
+
return "📦"
|
|
655
|
+
|
|
656
|
+
def should_tail_output(self) -> bool:
|
|
657
|
+
"""Check if output should be tailed."""
|
|
658
|
+
return self.monitor_after
|
|
659
|
+
|
|
660
|
+
def print_submission_info(self) -> None:
|
|
661
|
+
"""Print deploy submission information."""
|
|
662
|
+
super().print_submission_info()
|
|
663
|
+
if self.port:
|
|
664
|
+
print(f" Port: {self.port}")
|
|
665
|
+
|
|
666
|
+
def on_completion(self, elapsed: float) -> None:
|
|
667
|
+
"""Handle completion with monitor summary."""
|
|
668
|
+
if self.monitoring_started:
|
|
669
|
+
_display_monitor_summary(self.project_dir)
|
|
670
|
+
|
|
671
|
+
def on_failure(self, status: DaemonStatus, elapsed: float) -> None:
|
|
672
|
+
"""Handle failure with monitor summary."""
|
|
673
|
+
if self.monitoring_started:
|
|
674
|
+
_display_monitor_summary(self.project_dir)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class MonitorRequestHandler(BaseRequestHandler):
|
|
678
|
+
"""Handler for monitor requests."""
|
|
679
|
+
|
|
680
|
+
def __init__(
|
|
681
|
+
self,
|
|
682
|
+
project_dir: Path,
|
|
683
|
+
environment: str,
|
|
684
|
+
port: str | None = None,
|
|
685
|
+
baud_rate: int | None = None,
|
|
686
|
+
halt_on_error: str | None = None,
|
|
687
|
+
halt_on_success: str | None = None,
|
|
688
|
+
expect: str | None = None,
|
|
689
|
+
timeout: float | None = None,
|
|
690
|
+
):
|
|
691
|
+
"""Initialize monitor request handler.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
project_dir: Project directory
|
|
695
|
+
environment: Build environment
|
|
696
|
+
port: Serial port (optional)
|
|
697
|
+
baud_rate: Serial baud rate (optional)
|
|
698
|
+
halt_on_error: Pattern to halt on error
|
|
699
|
+
halt_on_success: Pattern to halt on success
|
|
700
|
+
expect: Expected pattern to check
|
|
701
|
+
timeout: Maximum monitoring time in seconds
|
|
702
|
+
"""
|
|
703
|
+
super().__init__(project_dir, environment, timeout or 3600)
|
|
704
|
+
self.port = port
|
|
705
|
+
self.baud_rate = baud_rate
|
|
706
|
+
self.halt_on_error = halt_on_error
|
|
707
|
+
self.halt_on_success = halt_on_success
|
|
708
|
+
self.expect = expect
|
|
709
|
+
self.monitor_timeout = timeout
|
|
710
|
+
|
|
711
|
+
def create_request(self) -> MonitorRequest:
|
|
712
|
+
"""Create monitor request."""
|
|
713
|
+
return MonitorRequest(
|
|
714
|
+
project_dir=str(self.project_dir.absolute()),
|
|
715
|
+
environment=self.environment,
|
|
716
|
+
port=self.port,
|
|
717
|
+
baud_rate=self.baud_rate,
|
|
718
|
+
halt_on_error=self.halt_on_error,
|
|
719
|
+
halt_on_success=self.halt_on_success,
|
|
720
|
+
expect=self.expect,
|
|
721
|
+
timeout=self.monitor_timeout,
|
|
722
|
+
caller_pid=os.getpid(),
|
|
723
|
+
caller_cwd=os.getcwd(),
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
def get_request_file(self) -> Path:
|
|
727
|
+
"""Get monitor request file path."""
|
|
728
|
+
return MONITOR_REQUEST_FILE
|
|
729
|
+
|
|
730
|
+
def get_operation_name(self) -> str:
|
|
731
|
+
"""Get operation name."""
|
|
732
|
+
return "Monitor"
|
|
733
|
+
|
|
734
|
+
def get_operation_emoji(self) -> str:
|
|
735
|
+
"""Get operation emoji."""
|
|
736
|
+
return "👁️"
|
|
737
|
+
|
|
738
|
+
def should_tail_output(self) -> bool:
|
|
739
|
+
"""Check if output should be tailed."""
|
|
740
|
+
return True
|
|
741
|
+
|
|
742
|
+
def print_submission_info(self) -> None:
|
|
743
|
+
"""Print monitor submission information."""
|
|
744
|
+
super().print_submission_info()
|
|
745
|
+
if self.port:
|
|
746
|
+
print(f" Port: {self.port}")
|
|
747
|
+
if self.baud_rate:
|
|
748
|
+
print(f" Baud rate: {self.baud_rate}")
|
|
749
|
+
if self.monitor_timeout:
|
|
750
|
+
print(f" Timeout: {self.monitor_timeout}s")
|
|
751
|
+
|
|
752
|
+
def on_completion(self, elapsed: float) -> None:
|
|
753
|
+
"""Handle completion with monitor summary."""
|
|
754
|
+
if self.monitoring_started:
|
|
755
|
+
_display_monitor_summary(self.project_dir)
|
|
756
|
+
|
|
757
|
+
def on_failure(self, status: DaemonStatus, elapsed: float) -> None:
|
|
758
|
+
"""Handle failure with monitor summary."""
|
|
759
|
+
if self.monitoring_started:
|
|
760
|
+
_display_monitor_summary(self.project_dir)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def request_deploy(
|
|
764
|
+
project_dir: Path,
|
|
765
|
+
environment: str,
|
|
766
|
+
port: str | None = None,
|
|
767
|
+
clean_build: bool = False,
|
|
768
|
+
monitor_after: bool = False,
|
|
769
|
+
monitor_timeout: float | None = None,
|
|
770
|
+
monitor_halt_on_error: str | None = None,
|
|
771
|
+
monitor_halt_on_success: str | None = None,
|
|
772
|
+
monitor_expect: str | None = None,
|
|
773
|
+
timeout: float = 1800,
|
|
774
|
+
) -> bool:
|
|
775
|
+
"""Request a deploy operation from the daemon.
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
project_dir: Project directory
|
|
779
|
+
environment: Build environment
|
|
780
|
+
port: Serial port (optional, auto-detect if None)
|
|
781
|
+
clean_build: Whether to perform clean build
|
|
782
|
+
monitor_after: Whether to start monitor after deploy
|
|
783
|
+
monitor_timeout: Timeout for monitor (if monitor_after=True)
|
|
784
|
+
monitor_halt_on_error: Pattern to halt on error (if monitor_after=True)
|
|
785
|
+
monitor_halt_on_success: Pattern to halt on success (if monitor_after=True)
|
|
786
|
+
monitor_expect: Expected pattern to check at timeout/success (if monitor_after=True)
|
|
787
|
+
timeout: Maximum wait time in seconds (default: 30 minutes)
|
|
788
|
+
|
|
789
|
+
Returns:
|
|
790
|
+
True if deploy successful, False otherwise
|
|
791
|
+
"""
|
|
792
|
+
handler = DeployRequestHandler(
|
|
793
|
+
project_dir=project_dir,
|
|
794
|
+
environment=environment,
|
|
795
|
+
port=port,
|
|
796
|
+
clean_build=clean_build,
|
|
797
|
+
monitor_after=monitor_after,
|
|
798
|
+
monitor_timeout=monitor_timeout,
|
|
799
|
+
monitor_halt_on_error=monitor_halt_on_error,
|
|
800
|
+
monitor_halt_on_success=monitor_halt_on_success,
|
|
801
|
+
monitor_expect=monitor_expect,
|
|
802
|
+
timeout=timeout,
|
|
803
|
+
)
|
|
804
|
+
return handler.execute()
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def request_monitor(
|
|
808
|
+
project_dir: Path,
|
|
809
|
+
environment: str,
|
|
810
|
+
port: str | None = None,
|
|
811
|
+
baud_rate: int | None = None,
|
|
812
|
+
halt_on_error: str | None = None,
|
|
813
|
+
halt_on_success: str | None = None,
|
|
814
|
+
expect: str | None = None,
|
|
815
|
+
timeout: float | None = None,
|
|
816
|
+
) -> bool:
|
|
817
|
+
"""Request a monitor operation from the daemon.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
project_dir: Project directory
|
|
821
|
+
environment: Build environment
|
|
822
|
+
port: Serial port (optional, auto-detect if None)
|
|
823
|
+
baud_rate: Serial baud rate (optional)
|
|
824
|
+
halt_on_error: Pattern to halt on (error detection)
|
|
825
|
+
halt_on_success: Pattern to halt on (success detection)
|
|
826
|
+
expect: Expected pattern to check at timeout/success
|
|
827
|
+
timeout: Maximum monitoring time in seconds
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
True if monitoring successful, False otherwise
|
|
831
|
+
"""
|
|
832
|
+
handler = MonitorRequestHandler(
|
|
833
|
+
project_dir=project_dir,
|
|
834
|
+
environment=environment,
|
|
835
|
+
port=port,
|
|
836
|
+
baud_rate=baud_rate,
|
|
837
|
+
halt_on_error=halt_on_error,
|
|
838
|
+
halt_on_success=halt_on_success,
|
|
839
|
+
expect=expect,
|
|
840
|
+
timeout=timeout,
|
|
841
|
+
)
|
|
842
|
+
return handler.execute()
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def stop_daemon() -> bool:
|
|
846
|
+
"""Stop the daemon gracefully.
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
True if daemon was stopped, False otherwise
|
|
850
|
+
"""
|
|
851
|
+
if not is_daemon_running():
|
|
852
|
+
print("Daemon is not running")
|
|
853
|
+
return False
|
|
854
|
+
|
|
855
|
+
# Create shutdown signal file
|
|
856
|
+
shutdown_file = DAEMON_DIR / "shutdown.signal"
|
|
857
|
+
shutdown_file.touch()
|
|
858
|
+
|
|
859
|
+
# Wait for daemon to exit
|
|
860
|
+
print("Stopping daemon...")
|
|
861
|
+
for _ in range(10):
|
|
862
|
+
if not is_daemon_running():
|
|
863
|
+
print("✅ Daemon stopped")
|
|
864
|
+
return True
|
|
865
|
+
time.sleep(1)
|
|
866
|
+
|
|
867
|
+
print("⚠️ Daemon did not stop gracefully")
|
|
868
|
+
return False
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def get_daemon_status() -> dict[str, Any]:
|
|
872
|
+
"""Get current daemon status.
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
Dictionary with daemon status information
|
|
876
|
+
"""
|
|
877
|
+
status: dict[str, Any] = {
|
|
878
|
+
"running": is_daemon_running(),
|
|
879
|
+
"pid_file_exists": PID_FILE.exists(),
|
|
880
|
+
"status_file_exists": STATUS_FILE.exists(),
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if PID_FILE.exists():
|
|
884
|
+
try:
|
|
885
|
+
with open(PID_FILE) as f:
|
|
886
|
+
status["pid"] = int(f.read().strip())
|
|
887
|
+
except KeyboardInterrupt:
|
|
888
|
+
_thread.interrupt_main()
|
|
889
|
+
raise
|
|
890
|
+
except Exception:
|
|
891
|
+
status["pid"] = None
|
|
892
|
+
|
|
893
|
+
if STATUS_FILE.exists():
|
|
894
|
+
daemon_status = read_status_file()
|
|
895
|
+
# Convert DaemonStatus to dict for JSON serialization
|
|
896
|
+
status["current_status"] = daemon_status.to_dict()
|
|
897
|
+
|
|
898
|
+
return status
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def main() -> int:
|
|
902
|
+
"""Command-line interface for client."""
|
|
903
|
+
import argparse
|
|
904
|
+
|
|
905
|
+
parser = argparse.ArgumentParser(description="fbuild Daemon Client")
|
|
906
|
+
parser.add_argument("--status", action="store_true", help="Show daemon status")
|
|
907
|
+
parser.add_argument("--stop", action="store_true", help="Stop the daemon")
|
|
908
|
+
|
|
909
|
+
args = parser.parse_args()
|
|
910
|
+
|
|
911
|
+
if args.status:
|
|
912
|
+
status = get_daemon_status()
|
|
913
|
+
print("Daemon Status:")
|
|
914
|
+
print(json.dumps(status, indent=2))
|
|
915
|
+
return 0
|
|
916
|
+
|
|
917
|
+
if args.stop:
|
|
918
|
+
return 0 if stop_daemon() else 1
|
|
919
|
+
|
|
920
|
+
parser.print_help()
|
|
921
|
+
return 1
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
if __name__ == "__main__":
|
|
925
|
+
try:
|
|
926
|
+
sys.exit(main())
|
|
927
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
928
|
+
print("\nInterrupted by user")
|
|
929
|
+
sys.exit(130)
|