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.
Files changed (47) hide show
  1. fbuild/__init__.py +5 -1
  2. fbuild/build/configurable_compiler.py +49 -6
  3. fbuild/build/configurable_linker.py +14 -9
  4. fbuild/build/orchestrator_esp32.py +6 -3
  5. fbuild/build/orchestrator_rp2040.py +6 -2
  6. fbuild/cli.py +300 -5
  7. fbuild/config/ini_parser.py +13 -1
  8. fbuild/daemon/__init__.py +11 -0
  9. fbuild/daemon/async_client.py +5 -4
  10. fbuild/daemon/async_client_lib.py +1543 -0
  11. fbuild/daemon/async_protocol.py +825 -0
  12. fbuild/daemon/async_server.py +2100 -0
  13. fbuild/daemon/client.py +425 -13
  14. fbuild/daemon/configuration_lock.py +13 -13
  15. fbuild/daemon/connection.py +508 -0
  16. fbuild/daemon/connection_registry.py +579 -0
  17. fbuild/daemon/daemon.py +517 -164
  18. fbuild/daemon/daemon_context.py +72 -1
  19. fbuild/daemon/device_discovery.py +477 -0
  20. fbuild/daemon/device_manager.py +821 -0
  21. fbuild/daemon/error_collector.py +263 -263
  22. fbuild/daemon/file_cache.py +332 -332
  23. fbuild/daemon/firmware_ledger.py +46 -123
  24. fbuild/daemon/lock_manager.py +508 -508
  25. fbuild/daemon/messages.py +431 -0
  26. fbuild/daemon/operation_registry.py +288 -288
  27. fbuild/daemon/processors/build_processor.py +34 -1
  28. fbuild/daemon/processors/deploy_processor.py +1 -3
  29. fbuild/daemon/processors/locking_processor.py +7 -7
  30. fbuild/daemon/request_processor.py +457 -457
  31. fbuild/daemon/shared_serial.py +7 -7
  32. fbuild/daemon/status_manager.py +238 -238
  33. fbuild/daemon/subprocess_manager.py +316 -316
  34. fbuild/deploy/docker_utils.py +182 -2
  35. fbuild/deploy/monitor.py +1 -1
  36. fbuild/deploy/qemu_runner.py +71 -13
  37. fbuild/ledger/board_ledger.py +46 -122
  38. fbuild/output.py +238 -2
  39. fbuild/packages/library_compiler.py +15 -5
  40. fbuild/packages/library_manager.py +12 -6
  41. fbuild-1.2.15.dist-info/METADATA +569 -0
  42. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/RECORD +46 -39
  43. fbuild-1.2.8.dist-info/METADATA +0 -468
  44. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/WHEEL +0 -0
  45. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/entry_points.txt +0 -0
  46. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/licenses/LICENSE +0 -0
  47. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/top_level.txt +0 -0
@@ -234,7 +234,7 @@ class SharedSerialManager:
234
234
  is_last_reader = session.reader_client_ids == {client_id}
235
235
 
236
236
  if not (is_owner or no_readers or is_last_reader):
237
- logging.warning(f"Client {client_id} not allowed to close port {port} " f"(owner: {session.owner_client_id}, readers: {session.reader_client_ids})")
237
+ logging.warning(f"Client {client_id} not allowed to close port {port} (owner: {session.owner_client_id}, readers: {session.reader_client_ids})")
238
238
  return False
239
239
 
240
240
  return self._close_port_internal(port)
@@ -398,7 +398,7 @@ class SharedSerialManager:
398
398
  # Wait with timeout
399
399
  remaining = deadline - time.time()
400
400
  if remaining <= 0:
401
- logging.warning(f"Timeout acquiring writer on {port} for {client_id} " f"(current writer: {session.writer_client_id})")
401
+ logging.warning(f"Timeout acquiring writer on {port} for {client_id} (current writer: {session.writer_client_id})")
402
402
  return False
403
403
 
404
404
  condition.wait(timeout=min(remaining, 0.5))
@@ -421,7 +421,7 @@ class SharedSerialManager:
421
421
  session = self._sessions[port]
422
422
 
423
423
  if session.writer_client_id != client_id:
424
- logging.warning(f"Client {client_id} is not the writer on {port} " f"(current: {session.writer_client_id})")
424
+ logging.warning(f"Client {client_id} is not the writer on {port} (current: {session.writer_client_id})")
425
425
  return False
426
426
 
427
427
  session.writer_client_id = None
@@ -456,7 +456,7 @@ class SharedSerialManager:
456
456
  session = self._sessions[port]
457
457
 
458
458
  if session.writer_client_id != client_id:
459
- logging.warning(f"Client {client_id} cannot write to {port} " f"(current writer: {session.writer_client_id})")
459
+ logging.warning(f"Client {client_id} cannot write to {port} (current writer: {session.writer_client_id})")
460
460
  return -1
461
461
 
462
462
  if port not in self._serial_ports:
@@ -720,7 +720,7 @@ class SharedSerialManager:
720
720
 
721
721
  # Must be the writer to reset
722
722
  if session.writer_client_id != client_id:
723
- logging.warning(f"Client {client_id} cannot reset device on {port} " f"(current writer: {session.writer_client_id})")
723
+ logging.warning(f"Client {client_id} cannot reset device on {port} (current writer: {session.writer_client_id})")
724
724
  return False
725
725
 
726
726
  if port not in self._serial_ports:
@@ -767,7 +767,7 @@ class SharedSerialManager:
767
767
 
768
768
  # Only owner can clear buffer
769
769
  if session.owner_client_id != client_id:
770
- logging.warning(f"Client {client_id} cannot clear buffer on {port} " f"(owner: {session.owner_client_id})")
770
+ logging.warning(f"Client {client_id} cannot clear buffer on {port} (owner: {session.owner_client_id})")
771
771
  return False
772
772
 
773
773
  session.output_buffer.clear()
@@ -796,7 +796,7 @@ class SharedSerialManager:
796
796
 
797
797
  # Only owner can change baud rate
798
798
  if session.owner_client_id != client_id:
799
- logging.warning(f"Client {client_id} cannot set baud rate on {port} " f"(owner: {session.owner_client_id})")
799
+ logging.warning(f"Client {client_id} cannot set baud rate on {port} (owner: {session.owner_client_id})")
800
800
  return False
801
801
 
802
802
  if port not in self._serial_ports:
@@ -1,238 +1,238 @@
1
- """
2
- Status Manager - Centralized status file management for daemon operations.
3
-
4
- This module provides the StatusManager class which handles all status file
5
- I/O operations with proper locking and atomic writes. It eliminates the
6
- scattered update_status() calls throughout daemon.py and provides a clean
7
- API for status management.
8
- """
9
-
10
- import json
11
- import logging
12
- import threading
13
- import time
14
- from pathlib import Path
15
- from typing import TYPE_CHECKING, Any
16
-
17
- from fbuild.daemon.messages import DaemonState, DaemonStatus
18
- from fbuild.daemon.port_state_manager import PortStateManager
19
- from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
20
-
21
- if TYPE_CHECKING:
22
- from fbuild.daemon.lock_manager import ResourceLockManager
23
-
24
-
25
- class StatusManager:
26
- """Manages daemon status file operations.
27
-
28
- This class provides centralized management of the daemon status file,
29
- ensuring:
30
- - Atomic writes (write to temp file + rename)
31
- - Thread-safe operations (internal locking)
32
- - Consistent status structure
33
- - Request ID validation
34
-
35
- The status file is used for communication between the daemon and client,
36
- allowing the client to monitor the progress of operations.
37
-
38
- Example:
39
- >>> manager = StatusManager(status_file_path, daemon_pid=1234)
40
- >>> manager.update_status(
41
- ... DaemonState.BUILDING,
42
- ... "Building firmware",
43
- ... environment="esp32dev",
44
- ... project_dir="/path/to/project"
45
- ... )
46
- >>> status = manager.read_status()
47
- >>> print(status.state)
48
- DaemonState.BUILDING
49
- """
50
-
51
- def __init__(
52
- self,
53
- status_file: Path,
54
- daemon_pid: int,
55
- daemon_started_at: float | None = None,
56
- port_state_manager: PortStateManager | None = None,
57
- lock_manager: "ResourceLockManager | None" = None,
58
- ):
59
- """Initialize the StatusManager.
60
-
61
- Args:
62
- status_file: Path to the status file
63
- daemon_pid: PID of the daemon process
64
- daemon_started_at: Timestamp when daemon started (defaults to now)
65
- port_state_manager: Optional PortStateManager for including port state in status
66
- lock_manager: Optional ResourceLockManager for including lock state in status
67
- """
68
- self.status_file = status_file
69
- self.daemon_pid = daemon_pid
70
- self.daemon_started_at = daemon_started_at if daemon_started_at is not None else time.time()
71
- self._lock = threading.Lock()
72
- self._operation_in_progress = False
73
- self._port_state_manager = port_state_manager
74
- self._lock_manager = lock_manager
75
-
76
- # Ensure parent directory exists
77
- self.status_file.parent.mkdir(parents=True, exist_ok=True)
78
-
79
- def update_status(
80
- self,
81
- state: DaemonState,
82
- message: str,
83
- operation_in_progress: bool | None = None,
84
- **kwargs: Any,
85
- ) -> None:
86
- """Update the status file with current daemon state.
87
-
88
- This method is thread-safe and performs atomic writes to prevent
89
- corruption during concurrent access.
90
-
91
- Args:
92
- state: DaemonState enum value
93
- message: Human-readable status message
94
- operation_in_progress: Whether an operation is in progress (None = use current value)
95
- **kwargs: Additional fields to include in status (e.g., environment, project_dir)
96
-
97
- Example:
98
- >>> manager.update_status(
99
- ... DaemonState.BUILDING,
100
- ... "Building firmware",
101
- ... environment="esp32dev",
102
- ... project_dir="/path/to/project",
103
- ... request_id="build_1234567890",
104
- ... )
105
- """
106
- with self._lock:
107
-
108
- # Update internal operation state if provided
109
- if operation_in_progress is not None:
110
- self._operation_in_progress = operation_in_progress
111
-
112
- # Get port state summary if port_state_manager is available
113
- ports_summary: dict[str, Any] = {}
114
- if self._port_state_manager is not None:
115
- ports_summary = self._port_state_manager.get_ports_summary()
116
-
117
- # Get lock state summary if lock_manager is available
118
- locks_summary: dict[str, Any] = {}
119
- if self._lock_manager is not None:
120
- locks_summary = self._lock_manager.get_lock_details()
121
-
122
- # Create typed DaemonStatus object
123
- status_obj = DaemonStatus(
124
- state=state,
125
- message=message,
126
- updated_at=time.time(),
127
- daemon_pid=self.daemon_pid,
128
- daemon_started_at=self.daemon_started_at,
129
- operation_in_progress=self._operation_in_progress,
130
- ports=ports_summary,
131
- locks=locks_summary,
132
- **kwargs,
133
- )
134
-
135
- logging.debug(f"Writing status to file (additional fields: {len(kwargs)})")
136
- self._write_status_atomic(status_obj.to_dict())
137
-
138
- def read_status(self) -> DaemonStatus:
139
- """Read and parse the status file.
140
-
141
- Returns:
142
- DaemonStatus object with current daemon state
143
-
144
- If the file doesn't exist or is corrupted, returns a default status
145
- indicating the daemon is idle.
146
- """
147
- with self._lock:
148
- if not self.status_file.exists():
149
- return self._get_default_status()
150
-
151
- try:
152
- with open(self.status_file, encoding="utf-8") as f:
153
- data = json.load(f)
154
-
155
- status = DaemonStatus.from_dict(data)
156
- return status
157
-
158
- except KeyboardInterrupt as ke:
159
- handle_keyboard_interrupt_properly(ke)
160
- except (json.JSONDecodeError, ValueError) as e:
161
- logging.warning(f"Corrupted status file detected: {e}")
162
- logging.warning("Creating fresh status file")
163
-
164
- # Write fresh status file
165
- default_status = self._get_default_status()
166
- self._write_status_atomic(default_status.to_dict())
167
- return default_status
168
-
169
- except Exception as e:
170
- logging.error(f"Unexpected error reading status file: {e}")
171
- default_status = self._get_default_status()
172
- self._write_status_atomic(default_status.to_dict())
173
- return default_status
174
-
175
- def set_operation_in_progress(self, in_progress: bool) -> None:
176
- """Set the operation_in_progress flag.
177
-
178
- This is used to track whether the daemon is currently executing
179
- an operation. It's typically set to True when starting an operation
180
- and False when completing or failing.
181
-
182
- Args:
183
- in_progress: Whether an operation is in progress
184
- """
185
- with self._lock:
186
- self._operation_in_progress = in_progress
187
-
188
- def get_operation_in_progress(self) -> bool:
189
- """Get the current operation_in_progress flag.
190
-
191
- Returns:
192
- True if an operation is in progress, False otherwise
193
- """
194
- with self._lock:
195
- return self._operation_in_progress
196
-
197
- def _write_status_atomic(self, status: dict[str, Any]) -> None:
198
- """Write status file atomically to prevent corruption during writes.
199
-
200
- This method writes to a temporary file first, then atomically renames
201
- it to the actual status file. This ensures the status file is never
202
- in a partially-written state.
203
-
204
- Args:
205
- status: Status dictionary to write
206
- """
207
- temp_file = self.status_file.with_suffix(".tmp")
208
- logging.debug(f"Using temp file: {temp_file}")
209
-
210
- try:
211
- logging.debug(f"Writing JSON to temp file ({len(status)} keys)...")
212
- with open(temp_file, "w", encoding="utf-8") as f:
213
- json.dump(status, f, indent=2)
214
- # Atomic rename
215
- temp_file.replace(self.status_file)
216
-
217
- except KeyboardInterrupt: # noqa: KBI002
218
- logging.warning("KeyboardInterrupt during status file write, cleaning up temp file")
219
- temp_file.unlink(missing_ok=True)
220
- raise
221
- except Exception as e:
222
- logging.error(f"Failed to write status file: {e}")
223
- temp_file.unlink(missing_ok=True)
224
-
225
- def _get_default_status(self) -> DaemonStatus:
226
- """Get default idle status.
227
-
228
- Returns:
229
- DaemonStatus object indicating daemon is idle
230
- """
231
- return DaemonStatus(
232
- state=DaemonState.IDLE,
233
- message="Daemon is idle",
234
- updated_at=time.time(),
235
- daemon_pid=self.daemon_pid,
236
- daemon_started_at=self.daemon_started_at,
237
- operation_in_progress=False,
238
- )
1
+ """
2
+ Status Manager - Centralized status file management for daemon operations.
3
+
4
+ This module provides the StatusManager class which handles all status file
5
+ I/O operations with proper locking and atomic writes. It eliminates the
6
+ scattered update_status() calls throughout daemon.py and provides a clean
7
+ API for status management.
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import threading
13
+ import time
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from fbuild.daemon.messages import DaemonState, DaemonStatus
18
+ from fbuild.daemon.port_state_manager import PortStateManager
19
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
20
+
21
+ if TYPE_CHECKING:
22
+ from fbuild.daemon.lock_manager import ResourceLockManager
23
+
24
+
25
+ class StatusManager:
26
+ """Manages daemon status file operations.
27
+
28
+ This class provides centralized management of the daemon status file,
29
+ ensuring:
30
+ - Atomic writes (write to temp file + rename)
31
+ - Thread-safe operations (internal locking)
32
+ - Consistent status structure
33
+ - Request ID validation
34
+
35
+ The status file is used for communication between the daemon and client,
36
+ allowing the client to monitor the progress of operations.
37
+
38
+ Example:
39
+ >>> manager = StatusManager(status_file_path, daemon_pid=1234)
40
+ >>> manager.update_status(
41
+ ... DaemonState.BUILDING,
42
+ ... "Building firmware",
43
+ ... environment="esp32dev",
44
+ ... project_dir="/path/to/project"
45
+ ... )
46
+ >>> status = manager.read_status()
47
+ >>> print(status.state)
48
+ DaemonState.BUILDING
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ status_file: Path,
54
+ daemon_pid: int,
55
+ daemon_started_at: float | None = None,
56
+ port_state_manager: PortStateManager | None = None,
57
+ lock_manager: "ResourceLockManager | None" = None,
58
+ ):
59
+ """Initialize the StatusManager.
60
+
61
+ Args:
62
+ status_file: Path to the status file
63
+ daemon_pid: PID of the daemon process
64
+ daemon_started_at: Timestamp when daemon started (defaults to now)
65
+ port_state_manager: Optional PortStateManager for including port state in status
66
+ lock_manager: Optional ResourceLockManager for including lock state in status
67
+ """
68
+ self.status_file = status_file
69
+ self.daemon_pid = daemon_pid
70
+ self.daemon_started_at = daemon_started_at if daemon_started_at is not None else time.time()
71
+ self._lock = threading.Lock()
72
+ self._operation_in_progress = False
73
+ self._port_state_manager = port_state_manager
74
+ self._lock_manager = lock_manager
75
+
76
+ # Ensure parent directory exists
77
+ self.status_file.parent.mkdir(parents=True, exist_ok=True)
78
+
79
+ def update_status(
80
+ self,
81
+ state: DaemonState,
82
+ message: str,
83
+ operation_in_progress: bool | None = None,
84
+ **kwargs: Any,
85
+ ) -> None:
86
+ """Update the status file with current daemon state.
87
+
88
+ This method is thread-safe and performs atomic writes to prevent
89
+ corruption during concurrent access.
90
+
91
+ Args:
92
+ state: DaemonState enum value
93
+ message: Human-readable status message
94
+ operation_in_progress: Whether an operation is in progress (None = use current value)
95
+ **kwargs: Additional fields to include in status (e.g., environment, project_dir)
96
+
97
+ Example:
98
+ >>> manager.update_status(
99
+ ... DaemonState.BUILDING,
100
+ ... "Building firmware",
101
+ ... environment="esp32dev",
102
+ ... project_dir="/path/to/project",
103
+ ... request_id="build_1234567890",
104
+ ... )
105
+ """
106
+ with self._lock:
107
+
108
+ # Update internal operation state if provided
109
+ if operation_in_progress is not None:
110
+ self._operation_in_progress = operation_in_progress
111
+
112
+ # Get port state summary if port_state_manager is available
113
+ ports_summary: dict[str, Any] = {}
114
+ if self._port_state_manager is not None:
115
+ ports_summary = self._port_state_manager.get_ports_summary()
116
+
117
+ # Get lock state summary if lock_manager is available
118
+ locks_summary: dict[str, Any] = {}
119
+ if self._lock_manager is not None:
120
+ locks_summary = self._lock_manager.get_lock_details()
121
+
122
+ # Create typed DaemonStatus object
123
+ status_obj = DaemonStatus(
124
+ state=state,
125
+ message=message,
126
+ updated_at=time.time(),
127
+ daemon_pid=self.daemon_pid,
128
+ daemon_started_at=self.daemon_started_at,
129
+ operation_in_progress=self._operation_in_progress,
130
+ ports=ports_summary,
131
+ locks=locks_summary,
132
+ **kwargs,
133
+ )
134
+
135
+ logging.debug(f"Writing status to file (additional fields: {len(kwargs)})")
136
+ self._write_status_atomic(status_obj.to_dict())
137
+
138
+ def read_status(self) -> DaemonStatus:
139
+ """Read and parse the status file.
140
+
141
+ Returns:
142
+ DaemonStatus object with current daemon state
143
+
144
+ If the file doesn't exist or is corrupted, returns a default status
145
+ indicating the daemon is idle.
146
+ """
147
+ with self._lock:
148
+ if not self.status_file.exists():
149
+ return self._get_default_status()
150
+
151
+ try:
152
+ with open(self.status_file, encoding="utf-8") as f:
153
+ data = json.load(f)
154
+
155
+ status = DaemonStatus.from_dict(data)
156
+ return status
157
+
158
+ except KeyboardInterrupt as ke:
159
+ handle_keyboard_interrupt_properly(ke)
160
+ except (json.JSONDecodeError, ValueError) as e:
161
+ logging.warning(f"Corrupted status file detected: {e}")
162
+ logging.warning("Creating fresh status file")
163
+
164
+ # Write fresh status file
165
+ default_status = self._get_default_status()
166
+ self._write_status_atomic(default_status.to_dict())
167
+ return default_status
168
+
169
+ except Exception as e:
170
+ logging.error(f"Unexpected error reading status file: {e}")
171
+ default_status = self._get_default_status()
172
+ self._write_status_atomic(default_status.to_dict())
173
+ return default_status
174
+
175
+ def set_operation_in_progress(self, in_progress: bool) -> None:
176
+ """Set the operation_in_progress flag.
177
+
178
+ This is used to track whether the daemon is currently executing
179
+ an operation. It's typically set to True when starting an operation
180
+ and False when completing or failing.
181
+
182
+ Args:
183
+ in_progress: Whether an operation is in progress
184
+ """
185
+ with self._lock:
186
+ self._operation_in_progress = in_progress
187
+
188
+ def get_operation_in_progress(self) -> bool:
189
+ """Get the current operation_in_progress flag.
190
+
191
+ Returns:
192
+ True if an operation is in progress, False otherwise
193
+ """
194
+ with self._lock:
195
+ return self._operation_in_progress
196
+
197
+ def _write_status_atomic(self, status: dict[str, Any]) -> None:
198
+ """Write status file atomically to prevent corruption during writes.
199
+
200
+ This method writes to a temporary file first, then atomically renames
201
+ it to the actual status file. This ensures the status file is never
202
+ in a partially-written state.
203
+
204
+ Args:
205
+ status: Status dictionary to write
206
+ """
207
+ temp_file = self.status_file.with_suffix(".tmp")
208
+ logging.debug(f"Using temp file: {temp_file}")
209
+
210
+ try:
211
+ logging.debug(f"Writing JSON to temp file ({len(status)} keys)...")
212
+ with open(temp_file, "w", encoding="utf-8") as f:
213
+ json.dump(status, f, indent=2)
214
+ # Atomic rename
215
+ temp_file.replace(self.status_file)
216
+
217
+ except KeyboardInterrupt: # noqa: KBI002
218
+ logging.warning("KeyboardInterrupt during status file write, cleaning up temp file")
219
+ temp_file.unlink(missing_ok=True)
220
+ raise
221
+ except Exception as e:
222
+ logging.error(f"Failed to write status file: {e}")
223
+ temp_file.unlink(missing_ok=True)
224
+
225
+ def _get_default_status(self) -> DaemonStatus:
226
+ """Get default idle status.
227
+
228
+ Returns:
229
+ DaemonStatus object indicating daemon is idle
230
+ """
231
+ return DaemonStatus(
232
+ state=DaemonState.IDLE,
233
+ message="Daemon is idle",
234
+ updated_at=time.time(),
235
+ daemon_pid=self.daemon_pid,
236
+ daemon_started_at=self.daemon_started_at,
237
+ operation_in_progress=False,
238
+ )