fbuild 1.2.8__py3-none-any.whl → 1.2.15__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.
- fbuild/__init__.py +5 -1
- fbuild/build/configurable_compiler.py +49 -6
- fbuild/build/configurable_linker.py +14 -9
- fbuild/build/orchestrator_esp32.py +6 -3
- fbuild/build/orchestrator_rp2040.py +6 -2
- fbuild/cli.py +300 -5
- fbuild/config/ini_parser.py +13 -1
- fbuild/daemon/__init__.py +11 -0
- fbuild/daemon/async_client.py +5 -4
- fbuild/daemon/async_client_lib.py +1543 -0
- fbuild/daemon/async_protocol.py +825 -0
- fbuild/daemon/async_server.py +2100 -0
- fbuild/daemon/client.py +425 -13
- fbuild/daemon/configuration_lock.py +13 -13
- fbuild/daemon/connection.py +508 -0
- fbuild/daemon/connection_registry.py +579 -0
- fbuild/daemon/daemon.py +517 -164
- fbuild/daemon/daemon_context.py +72 -1
- fbuild/daemon/device_discovery.py +477 -0
- fbuild/daemon/device_manager.py +821 -0
- fbuild/daemon/error_collector.py +263 -263
- fbuild/daemon/file_cache.py +332 -332
- fbuild/daemon/firmware_ledger.py +46 -123
- fbuild/daemon/lock_manager.py +508 -508
- fbuild/daemon/messages.py +431 -0
- fbuild/daemon/operation_registry.py +288 -288
- fbuild/daemon/processors/build_processor.py +34 -1
- fbuild/daemon/processors/deploy_processor.py +1 -3
- fbuild/daemon/processors/locking_processor.py +7 -7
- fbuild/daemon/request_processor.py +457 -457
- fbuild/daemon/shared_serial.py +7 -7
- fbuild/daemon/status_manager.py +238 -238
- fbuild/daemon/subprocess_manager.py +316 -316
- fbuild/deploy/docker_utils.py +182 -2
- fbuild/deploy/monitor.py +1 -1
- fbuild/deploy/qemu_runner.py +71 -13
- fbuild/ledger/board_ledger.py +46 -122
- fbuild/output.py +238 -2
- fbuild/packages/library_compiler.py +15 -5
- fbuild/packages/library_manager.py +12 -6
- fbuild-1.2.15.dist-info/METADATA +569 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/RECORD +46 -39
- fbuild-1.2.8.dist-info/METADATA +0 -468
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/WHEEL +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/entry_points.txt +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/licenses/LICENSE +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client-side daemon connection object.
|
|
3
|
+
|
|
4
|
+
This module provides the DaemonConnection class which represents a client's
|
|
5
|
+
connection to the fbuild daemon. Each call to connect_daemon() creates a NEW
|
|
6
|
+
connection with a unique ID - connections are NOT singletons.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from fbuild.daemon.connection import connect_daemon
|
|
10
|
+
|
|
11
|
+
# Using context manager (recommended)
|
|
12
|
+
with connect_daemon(Path("./project"), "esp32dev") as conn:
|
|
13
|
+
conn.install_dependencies()
|
|
14
|
+
conn.build(clean=True)
|
|
15
|
+
conn.deploy(port="/dev/ttyUSB0")
|
|
16
|
+
conn.monitor()
|
|
17
|
+
|
|
18
|
+
# Manual lifecycle management
|
|
19
|
+
conn = connect_daemon(Path("./project"), "esp32dev")
|
|
20
|
+
try:
|
|
21
|
+
conn.build()
|
|
22
|
+
finally:
|
|
23
|
+
conn.close()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import _thread
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import socket
|
|
30
|
+
import time
|
|
31
|
+
import uuid
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from threading import Thread
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from fbuild.daemon.messages import (
|
|
37
|
+
ClientConnectRequest,
|
|
38
|
+
ClientDisconnectRequest,
|
|
39
|
+
ClientHeartbeatRequest,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_daemon_dir(dev_mode: bool) -> Path:
|
|
44
|
+
"""Get daemon directory based on dev mode setting.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
dev_mode: Whether to use development mode directory.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Path to daemon directory.
|
|
51
|
+
"""
|
|
52
|
+
if dev_mode:
|
|
53
|
+
return Path.cwd() / ".fbuild" / "daemon_dev"
|
|
54
|
+
else:
|
|
55
|
+
return Path.home() / ".fbuild" / "daemon"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DaemonConnection:
|
|
59
|
+
"""Client-side connection to the fbuild daemon.
|
|
60
|
+
|
|
61
|
+
Represents a single client connection with a unique ID. Each connection
|
|
62
|
+
maintains its own heartbeat thread and can perform operations independently.
|
|
63
|
+
|
|
64
|
+
IMPORTANT: This is NOT a singleton. Each call to connect_daemon() creates
|
|
65
|
+
a new DaemonConnection instance with a unique connection_id.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
connection_id: Unique UUID for this connection.
|
|
69
|
+
project_dir: Path to the project directory.
|
|
70
|
+
environment: Build environment name.
|
|
71
|
+
dev_mode: Whether using development mode daemon.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
project_dir: Path,
|
|
77
|
+
environment: str,
|
|
78
|
+
dev_mode: bool | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Initialize a new daemon connection.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
project_dir: Path to project directory.
|
|
84
|
+
environment: Build environment name.
|
|
85
|
+
dev_mode: Use dev mode daemon. Auto-detects from FBUILD_DEV_MODE if None.
|
|
86
|
+
"""
|
|
87
|
+
self.connection_id: str = str(uuid.uuid4())
|
|
88
|
+
self.project_dir: Path = Path(project_dir).resolve()
|
|
89
|
+
self.environment: str = environment
|
|
90
|
+
|
|
91
|
+
# Auto-detect dev mode from environment variable if not specified
|
|
92
|
+
if dev_mode is None:
|
|
93
|
+
self.dev_mode: bool = os.environ.get("FBUILD_DEV_MODE") == "1"
|
|
94
|
+
else:
|
|
95
|
+
self.dev_mode = dev_mode
|
|
96
|
+
|
|
97
|
+
self._closed: bool = False
|
|
98
|
+
self._heartbeat_thread: Thread | None = None
|
|
99
|
+
self._heartbeat_interval: float = 10.0 # seconds between heartbeats
|
|
100
|
+
|
|
101
|
+
# Get daemon directory based on dev mode
|
|
102
|
+
self._daemon_dir = _get_daemon_dir(self.dev_mode)
|
|
103
|
+
|
|
104
|
+
# Send connect message to daemon
|
|
105
|
+
self._send_connect()
|
|
106
|
+
|
|
107
|
+
# Start heartbeat thread
|
|
108
|
+
self._start_heartbeat()
|
|
109
|
+
|
|
110
|
+
def __enter__(self) -> "DaemonConnection":
|
|
111
|
+
"""Context manager entry.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
This connection instance.
|
|
115
|
+
"""
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def __exit__(self, _exc_type: type[BaseException] | None, _exc_val: BaseException | None, _exc_tb: Any) -> None:
|
|
119
|
+
"""Context manager exit - closes the connection."""
|
|
120
|
+
self.close()
|
|
121
|
+
|
|
122
|
+
def _start_heartbeat(self) -> None:
|
|
123
|
+
"""Start the background heartbeat thread.
|
|
124
|
+
|
|
125
|
+
The heartbeat thread sends periodic heartbeats to the daemon to indicate
|
|
126
|
+
this connection is still alive. If heartbeats stop, the daemon will
|
|
127
|
+
eventually clean up this connection's resources.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def heartbeat_loop() -> None:
|
|
131
|
+
while not self._closed:
|
|
132
|
+
try:
|
|
133
|
+
self._send_heartbeat()
|
|
134
|
+
except KeyboardInterrupt:
|
|
135
|
+
_thread.interrupt_main()
|
|
136
|
+
break # Exit heartbeat loop on interrupt
|
|
137
|
+
except Exception:
|
|
138
|
+
# Silently ignore heartbeat failures - daemon may not be ready
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# Sleep in small increments to allow faster exit when closed
|
|
142
|
+
sleep_time = 0.0
|
|
143
|
+
while sleep_time < self._heartbeat_interval and not self._closed:
|
|
144
|
+
time.sleep(0.5)
|
|
145
|
+
sleep_time += 0.5
|
|
146
|
+
|
|
147
|
+
self._heartbeat_thread = Thread(target=heartbeat_loop, daemon=True)
|
|
148
|
+
self._heartbeat_thread.start()
|
|
149
|
+
|
|
150
|
+
def _send_heartbeat(self) -> None:
|
|
151
|
+
"""Send a heartbeat message to the daemon.
|
|
152
|
+
|
|
153
|
+
Writes a heartbeat file to the daemon directory that the daemon
|
|
154
|
+
will pick up during its polling cycle.
|
|
155
|
+
"""
|
|
156
|
+
if self._closed:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
request = ClientHeartbeatRequest(
|
|
160
|
+
client_id=self.connection_id,
|
|
161
|
+
timestamp=time.time(),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
heartbeat_file = self._daemon_dir / f"heartbeat_{self.connection_id}.json"
|
|
165
|
+
self._daemon_dir.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
|
|
167
|
+
# Atomic write using temp file
|
|
168
|
+
temp_file = heartbeat_file.with_suffix(".tmp")
|
|
169
|
+
try:
|
|
170
|
+
with open(temp_file, "w") as f:
|
|
171
|
+
json.dump(request.to_dict(), f)
|
|
172
|
+
temp_file.replace(heartbeat_file)
|
|
173
|
+
except KeyboardInterrupt:
|
|
174
|
+
_thread.interrupt_main()
|
|
175
|
+
raise
|
|
176
|
+
except Exception:
|
|
177
|
+
# Best effort - don't fail if we can't write heartbeat
|
|
178
|
+
if temp_file.exists():
|
|
179
|
+
temp_file.unlink(missing_ok=True)
|
|
180
|
+
|
|
181
|
+
def _send_connect(self) -> None:
|
|
182
|
+
"""Send a connect message to register with the daemon.
|
|
183
|
+
|
|
184
|
+
Called during __init__ to register this connection with the daemon.
|
|
185
|
+
"""
|
|
186
|
+
request = ClientConnectRequest(
|
|
187
|
+
client_id=self.connection_id,
|
|
188
|
+
pid=os.getpid(),
|
|
189
|
+
hostname=socket.gethostname(),
|
|
190
|
+
version=self._get_version(),
|
|
191
|
+
timestamp=time.time(),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
connect_file = self._daemon_dir / f"connect_{self.connection_id}.json"
|
|
195
|
+
self._daemon_dir.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
|
|
197
|
+
# Atomic write
|
|
198
|
+
temp_file = connect_file.with_suffix(".tmp")
|
|
199
|
+
try:
|
|
200
|
+
with open(temp_file, "w") as f:
|
|
201
|
+
json.dump(request.to_dict(), f)
|
|
202
|
+
temp_file.replace(connect_file)
|
|
203
|
+
except KeyboardInterrupt:
|
|
204
|
+
_thread.interrupt_main()
|
|
205
|
+
raise
|
|
206
|
+
except Exception:
|
|
207
|
+
# Best effort - daemon may not be running yet
|
|
208
|
+
if temp_file.exists():
|
|
209
|
+
temp_file.unlink(missing_ok=True)
|
|
210
|
+
|
|
211
|
+
def _send_disconnect(self) -> None:
|
|
212
|
+
"""Send a disconnect message to notify daemon of graceful close.
|
|
213
|
+
|
|
214
|
+
Called during close() to notify the daemon to clean up resources
|
|
215
|
+
associated with this connection.
|
|
216
|
+
"""
|
|
217
|
+
request = ClientDisconnectRequest(
|
|
218
|
+
client_id=self.connection_id,
|
|
219
|
+
reason="graceful_close",
|
|
220
|
+
timestamp=time.time(),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
disconnect_file = self._daemon_dir / f"disconnect_{self.connection_id}.json"
|
|
224
|
+
self._daemon_dir.mkdir(parents=True, exist_ok=True)
|
|
225
|
+
|
|
226
|
+
# Atomic write
|
|
227
|
+
temp_file = disconnect_file.with_suffix(".tmp")
|
|
228
|
+
try:
|
|
229
|
+
with open(temp_file, "w") as f:
|
|
230
|
+
json.dump(request.to_dict(), f)
|
|
231
|
+
temp_file.replace(disconnect_file)
|
|
232
|
+
except KeyboardInterrupt:
|
|
233
|
+
_thread.interrupt_main()
|
|
234
|
+
raise
|
|
235
|
+
except Exception:
|
|
236
|
+
# Best effort
|
|
237
|
+
if temp_file.exists():
|
|
238
|
+
temp_file.unlink(missing_ok=True)
|
|
239
|
+
|
|
240
|
+
# Clean up heartbeat file if it exists
|
|
241
|
+
heartbeat_file = self._daemon_dir / f"heartbeat_{self.connection_id}.json"
|
|
242
|
+
heartbeat_file.unlink(missing_ok=True)
|
|
243
|
+
|
|
244
|
+
# Clean up connect file if it exists
|
|
245
|
+
connect_file = self._daemon_dir / f"connect_{self.connection_id}.json"
|
|
246
|
+
connect_file.unlink(missing_ok=True)
|
|
247
|
+
|
|
248
|
+
def _get_version(self) -> str:
|
|
249
|
+
"""Get the fbuild version string.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Version string from fbuild package.
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
from fbuild import __version__
|
|
256
|
+
|
|
257
|
+
return __version__
|
|
258
|
+
except ImportError:
|
|
259
|
+
return "unknown"
|
|
260
|
+
|
|
261
|
+
def _check_closed(self) -> None:
|
|
262
|
+
"""Raise RuntimeError if connection is closed.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
RuntimeError: If the connection has been closed.
|
|
266
|
+
"""
|
|
267
|
+
if self._closed:
|
|
268
|
+
raise RuntimeError(f"DaemonConnection {self.connection_id} is closed. Cannot perform operations on a closed connection.")
|
|
269
|
+
|
|
270
|
+
def close(self) -> None:
|
|
271
|
+
"""Gracefully close the connection.
|
|
272
|
+
|
|
273
|
+
Stops the heartbeat thread, sends a disconnect message to the daemon,
|
|
274
|
+
and marks the connection as closed. Safe to call multiple times.
|
|
275
|
+
"""
|
|
276
|
+
if self._closed:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
self._closed = True
|
|
280
|
+
|
|
281
|
+
# Send disconnect message to daemon
|
|
282
|
+
self._send_disconnect()
|
|
283
|
+
|
|
284
|
+
# Wait for heartbeat thread to stop (with timeout)
|
|
285
|
+
if self._heartbeat_thread is not None:
|
|
286
|
+
self._heartbeat_thread.join(timeout=1.0)
|
|
287
|
+
self._heartbeat_thread = None
|
|
288
|
+
|
|
289
|
+
# =========================================================================
|
|
290
|
+
# Operation Methods
|
|
291
|
+
# =========================================================================
|
|
292
|
+
|
|
293
|
+
def install_dependencies(
|
|
294
|
+
self,
|
|
295
|
+
verbose: bool = False,
|
|
296
|
+
timeout: float = 1800,
|
|
297
|
+
) -> bool:
|
|
298
|
+
"""Install project dependencies (toolchain, framework, libraries).
|
|
299
|
+
|
|
300
|
+
This pre-downloads and caches all dependencies required for a build
|
|
301
|
+
without actually compiling.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
verbose: Enable verbose output.
|
|
305
|
+
timeout: Maximum wait time in seconds (default: 30 minutes).
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
True if dependencies installed successfully, False otherwise.
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
RuntimeError: If the connection is closed.
|
|
312
|
+
"""
|
|
313
|
+
self._check_closed()
|
|
314
|
+
|
|
315
|
+
from fbuild.daemon.client import InstallDependenciesRequestHandler
|
|
316
|
+
|
|
317
|
+
handler = InstallDependenciesRequestHandler(
|
|
318
|
+
project_dir=self.project_dir,
|
|
319
|
+
environment=self.environment,
|
|
320
|
+
verbose=verbose,
|
|
321
|
+
timeout=timeout,
|
|
322
|
+
)
|
|
323
|
+
return handler.execute()
|
|
324
|
+
|
|
325
|
+
def build(
|
|
326
|
+
self,
|
|
327
|
+
clean: bool = False,
|
|
328
|
+
verbose: bool = False,
|
|
329
|
+
timeout: float = 1800,
|
|
330
|
+
) -> bool:
|
|
331
|
+
"""Build the project.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
clean: Whether to perform a clean build.
|
|
335
|
+
verbose: Enable verbose build output.
|
|
336
|
+
timeout: Maximum wait time in seconds (default: 30 minutes).
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
True if build successful, False otherwise.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
RuntimeError: If the connection is closed.
|
|
343
|
+
"""
|
|
344
|
+
self._check_closed()
|
|
345
|
+
|
|
346
|
+
from fbuild.daemon.client import BuildRequestHandler
|
|
347
|
+
|
|
348
|
+
handler = BuildRequestHandler(
|
|
349
|
+
project_dir=self.project_dir,
|
|
350
|
+
environment=self.environment,
|
|
351
|
+
clean_build=clean,
|
|
352
|
+
verbose=verbose,
|
|
353
|
+
timeout=timeout,
|
|
354
|
+
)
|
|
355
|
+
return handler.execute()
|
|
356
|
+
|
|
357
|
+
def deploy(
|
|
358
|
+
self,
|
|
359
|
+
port: str | None = None,
|
|
360
|
+
clean: bool = False,
|
|
361
|
+
monitor_after: bool = False,
|
|
362
|
+
monitor_timeout: float | None = None,
|
|
363
|
+
monitor_halt_on_error: str | None = None,
|
|
364
|
+
monitor_halt_on_success: str | None = None,
|
|
365
|
+
monitor_expect: str | None = None,
|
|
366
|
+
monitor_show_timestamp: bool = False,
|
|
367
|
+
timeout: float = 1800,
|
|
368
|
+
) -> bool:
|
|
369
|
+
"""Deploy (build and upload) the project to a device.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
port: Serial port for upload (auto-detect if None).
|
|
373
|
+
clean: Whether to perform a clean build.
|
|
374
|
+
monitor_after: Whether to start serial monitor after deploy.
|
|
375
|
+
monitor_timeout: Timeout for monitor in seconds.
|
|
376
|
+
monitor_halt_on_error: Pattern to halt on error.
|
|
377
|
+
monitor_halt_on_success: Pattern to halt on success.
|
|
378
|
+
monitor_expect: Expected pattern to check.
|
|
379
|
+
monitor_show_timestamp: Prefix output with elapsed time.
|
|
380
|
+
timeout: Maximum wait time in seconds (default: 30 minutes).
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
True if deploy successful, False otherwise.
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
RuntimeError: If the connection is closed.
|
|
387
|
+
"""
|
|
388
|
+
self._check_closed()
|
|
389
|
+
|
|
390
|
+
from fbuild.daemon.client import DeployRequestHandler
|
|
391
|
+
|
|
392
|
+
handler = DeployRequestHandler(
|
|
393
|
+
project_dir=self.project_dir,
|
|
394
|
+
environment=self.environment,
|
|
395
|
+
port=port,
|
|
396
|
+
clean_build=clean,
|
|
397
|
+
monitor_after=monitor_after,
|
|
398
|
+
monitor_timeout=monitor_timeout,
|
|
399
|
+
monitor_halt_on_error=monitor_halt_on_error,
|
|
400
|
+
monitor_halt_on_success=monitor_halt_on_success,
|
|
401
|
+
monitor_expect=monitor_expect,
|
|
402
|
+
monitor_show_timestamp=monitor_show_timestamp,
|
|
403
|
+
timeout=timeout,
|
|
404
|
+
)
|
|
405
|
+
return handler.execute()
|
|
406
|
+
|
|
407
|
+
def monitor(
|
|
408
|
+
self,
|
|
409
|
+
port: str | None = None,
|
|
410
|
+
baud_rate: int | None = None,
|
|
411
|
+
halt_on_error: str | None = None,
|
|
412
|
+
halt_on_success: str | None = None,
|
|
413
|
+
expect: str | None = None,
|
|
414
|
+
timeout: float | None = None,
|
|
415
|
+
show_timestamp: bool = False,
|
|
416
|
+
) -> bool:
|
|
417
|
+
"""Start serial monitoring.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
port: Serial port (auto-detect if None).
|
|
421
|
+
baud_rate: Serial baud rate (use config default if None).
|
|
422
|
+
halt_on_error: Pattern to halt on error.
|
|
423
|
+
halt_on_success: Pattern to halt on success.
|
|
424
|
+
expect: Expected pattern to check at timeout/success.
|
|
425
|
+
timeout: Maximum monitoring time in seconds.
|
|
426
|
+
show_timestamp: Prefix output lines with elapsed time.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
True if monitoring completed successfully, False otherwise.
|
|
430
|
+
|
|
431
|
+
Raises:
|
|
432
|
+
RuntimeError: If the connection is closed.
|
|
433
|
+
"""
|
|
434
|
+
self._check_closed()
|
|
435
|
+
|
|
436
|
+
from fbuild.daemon.client import MonitorRequestHandler
|
|
437
|
+
|
|
438
|
+
handler = MonitorRequestHandler(
|
|
439
|
+
project_dir=self.project_dir,
|
|
440
|
+
environment=self.environment,
|
|
441
|
+
port=port,
|
|
442
|
+
baud_rate=baud_rate,
|
|
443
|
+
halt_on_error=halt_on_error,
|
|
444
|
+
halt_on_success=halt_on_success,
|
|
445
|
+
expect=expect,
|
|
446
|
+
timeout=timeout,
|
|
447
|
+
show_timestamp=show_timestamp,
|
|
448
|
+
)
|
|
449
|
+
return handler.execute()
|
|
450
|
+
|
|
451
|
+
def get_status(self) -> dict[str, Any]:
|
|
452
|
+
"""Get current daemon status.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Dictionary with daemon status information including:
|
|
456
|
+
- running: Whether daemon is running
|
|
457
|
+
- state: Current daemon state
|
|
458
|
+
- message: Status message
|
|
459
|
+
- pid: Daemon process ID
|
|
460
|
+
- locks: Lock information
|
|
461
|
+
|
|
462
|
+
Raises:
|
|
463
|
+
RuntimeError: If the connection is closed.
|
|
464
|
+
"""
|
|
465
|
+
self._check_closed()
|
|
466
|
+
|
|
467
|
+
from fbuild.daemon.client import get_daemon_status
|
|
468
|
+
|
|
469
|
+
return get_daemon_status()
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def connect_daemon(
|
|
473
|
+
project_dir: Path | str,
|
|
474
|
+
environment: str,
|
|
475
|
+
dev_mode: bool | None = None,
|
|
476
|
+
) -> DaemonConnection:
|
|
477
|
+
"""Create a new daemon connection.
|
|
478
|
+
|
|
479
|
+
Each call creates a NEW connection with a unique ID. This is NOT a
|
|
480
|
+
singleton - multiple connections can exist simultaneously for different
|
|
481
|
+
projects or environments.
|
|
482
|
+
|
|
483
|
+
Usage:
|
|
484
|
+
# Using context manager (recommended)
|
|
485
|
+
with connect_daemon(Path("./project"), "esp32dev") as conn:
|
|
486
|
+
conn.build()
|
|
487
|
+
conn.deploy()
|
|
488
|
+
|
|
489
|
+
# Manual lifecycle
|
|
490
|
+
conn = connect_daemon(Path("./project"), "esp32dev")
|
|
491
|
+
try:
|
|
492
|
+
conn.build()
|
|
493
|
+
finally:
|
|
494
|
+
conn.close()
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
project_dir: Path to project directory.
|
|
498
|
+
environment: Build environment name.
|
|
499
|
+
dev_mode: Use dev mode daemon. Auto-detects from FBUILD_DEV_MODE if None.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
New DaemonConnection instance.
|
|
503
|
+
"""
|
|
504
|
+
return DaemonConnection(
|
|
505
|
+
project_dir=Path(project_dir),
|
|
506
|
+
environment=environment,
|
|
507
|
+
dev_mode=dev_mode,
|
|
508
|
+
)
|