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
@@ -1,457 +1,457 @@
1
- """
2
- Request Processor - Template method pattern for daemon request handling.
3
-
4
- This module provides the RequestProcessor abstract base class which implements
5
- the Template Method pattern to eliminate code duplication across build, deploy,
6
- and monitor request handlers. It handles all common concerns (lock management,
7
- status updates, error handling) while allowing subclasses to implement only
8
- the operation-specific business logic.
9
- """
10
-
11
- import logging
12
- import time
13
- from abc import ABC, abstractmethod
14
- from contextlib import ExitStack
15
- from typing import TYPE_CHECKING, Any
16
-
17
- from fbuild.daemon.lock_manager import LockAcquisitionError
18
- from fbuild.daemon.messages import DaemonState, OperationType
19
-
20
- if TYPE_CHECKING:
21
- from fbuild.daemon.daemon_context import DaemonContext
22
- from fbuild.daemon.messages import BuildRequest, DeployRequest, MonitorRequest
23
-
24
-
25
- class RequestProcessor(ABC):
26
- """Abstract base class for processing daemon requests.
27
-
28
- This class implements the Template Method pattern to handle all common
29
- concerns of request processing:
30
- - Request validation
31
- - Lock acquisition (port and/or project locks)
32
- - Status updates (started, in-progress, completed, failed)
33
- - Error handling and cleanup
34
- - Operation tracking
35
-
36
- Subclasses only need to implement:
37
- - get_operation_type(): Return the OperationType
38
- - get_required_locks(): Specify which locks are needed
39
- - execute_operation(): Implement the actual business logic
40
-
41
- Example:
42
- >>> class BuildRequestProcessor(RequestProcessor):
43
- ... def get_operation_type(self) -> OperationType:
44
- ... return OperationType.BUILD
45
- ...
46
- ... def get_required_locks(self, request, context):
47
- ... return {"project": request.project_dir}
48
- ...
49
- ... def execute_operation(self, request, context):
50
- ... # Actual build logic here
51
- ... result = build_project(request.project_dir)
52
- ... return result.success
53
- """
54
-
55
- def process_request(
56
- self,
57
- request: "BuildRequest | DeployRequest | MonitorRequest",
58
- context: "DaemonContext",
59
- ) -> bool:
60
- """Process a request using the template method pattern.
61
-
62
- This is the main entry point that coordinates the entire request
63
- processing lifecycle. It handles all boilerplate while calling
64
- abstract methods for operation-specific logic.
65
-
66
- Args:
67
- request: The request to process (BuildRequest, DeployRequest, or MonitorRequest)
68
- context: The daemon context containing all subsystems
69
-
70
- Returns:
71
- True if operation succeeded, False otherwise
72
-
73
- Lifecycle:
74
- 1. Validate request
75
- 2. Acquire required locks (project and/or port)
76
- 3. Mark operation as in progress
77
- 4. Update status to starting state
78
- 5. Execute operation (abstract method)
79
- 6. Update status based on result
80
- 7. Release locks and cleanup
81
-
82
- Example:
83
- >>> processor = BuildRequestProcessor()
84
- >>> success = processor.process_request(build_request, daemon_context)
85
- """
86
- logging.info(f"Processing {self.get_operation_type().value} request {request.request_id}: " + f"env={request.environment}, project={request.project_dir}")
87
-
88
- # Validate request
89
- if not self.validate_request(request, context):
90
- self._update_status(
91
- context,
92
- DaemonState.FAILED,
93
- "Request validation failed",
94
- request=request,
95
- exit_code=1,
96
- )
97
- return False
98
-
99
- # Use ExitStack to manage multiple locks as context managers
100
- # We store the result to return after lock release and status update
101
- result: bool = False
102
- exception_to_reraise: BaseException | None = None
103
-
104
- with ExitStack() as lock_stack:
105
- # Acquire required locks
106
- if not self._acquire_locks(request, context, lock_stack):
107
- return False
108
-
109
- try:
110
- # Mark operation in progress
111
- with context.operation_lock:
112
- context.operation_in_progress = True
113
-
114
- # Update status to starting state
115
- self._update_status(
116
- context,
117
- self.get_starting_state(),
118
- self.get_starting_message(request),
119
- request=request,
120
- request_started_at=time.time(),
121
- operation_type=self.get_operation_type(),
122
- )
123
-
124
- # Execute the operation (implemented by subclass)
125
- success = self.execute_operation(request, context)
126
-
127
- # Update final status
128
- if success:
129
- self._update_status(
130
- context,
131
- DaemonState.COMPLETED,
132
- self.get_success_message(request),
133
- request=request,
134
- exit_code=0,
135
- operation_in_progress=False,
136
- )
137
- else:
138
- self._update_status(
139
- context,
140
- DaemonState.FAILED,
141
- self.get_failure_message(request),
142
- request=request,
143
- exit_code=1,
144
- operation_in_progress=False,
145
- )
146
-
147
- result = success
148
-
149
- except KeyboardInterrupt as ki:
150
- import _thread
151
-
152
- _thread.interrupt_main()
153
- exception_to_reraise = ki
154
- except Exception as e:
155
- import traceback
156
-
157
- logging.error(f"{self.get_operation_type().value} exception: {e}")
158
- logging.error(f"Traceback:\n{traceback.format_exc()}")
159
- self._update_status(
160
- context,
161
- DaemonState.FAILED,
162
- f"{self.get_operation_type().value} exception: {e}",
163
- request=request,
164
- exit_code=1,
165
- operation_in_progress=False,
166
- )
167
- result = False
168
- finally:
169
- # Mark operation complete
170
- with context.operation_lock:
171
- context.operation_in_progress = False
172
-
173
- # After locks are released (ExitStack has exited), update status to reflect
174
- # the new lock state. This ensures the status file shows locks as released.
175
- # We read the current status and re-write it to capture the updated lock state.
176
- try:
177
- current_status = context.status_manager.read_status()
178
- context.status_manager.update_status(
179
- state=current_status.state,
180
- message=current_status.message,
181
- environment=getattr(current_status, "environment", request.environment),
182
- project_dir=getattr(current_status, "project_dir", request.project_dir),
183
- request_id=getattr(current_status, "request_id", request.request_id),
184
- caller_pid=getattr(current_status, "caller_pid", request.caller_pid),
185
- caller_cwd=getattr(current_status, "caller_cwd", request.caller_cwd),
186
- exit_code=getattr(current_status, "exit_code", None),
187
- )
188
- except KeyboardInterrupt as ke:
189
- import _thread
190
-
191
- _thread.interrupt_main()
192
- raise ke
193
- except Exception as e:
194
- logging.warning(f"Failed to update status after lock release: {e}")
195
-
196
- # Re-raise KeyboardInterrupt if it was caught
197
- if exception_to_reraise is not None:
198
- import _thread
199
-
200
- _thread.interrupt_main()
201
- raise exception_to_reraise
202
-
203
- return result
204
-
205
- @abstractmethod
206
- def get_operation_type(self) -> OperationType:
207
- """Get the operation type for this processor.
208
-
209
- Returns:
210
- OperationType enum value (BUILD, DEPLOY, MONITOR, etc.)
211
- """
212
- pass
213
-
214
- @abstractmethod
215
- def get_required_locks(
216
- self,
217
- request: "BuildRequest | DeployRequest | MonitorRequest",
218
- context: "DaemonContext",
219
- ) -> dict[str, str]:
220
- """Specify which locks are required for this operation.
221
-
222
- Returns:
223
- Dictionary with lock types as keys and resource identifiers as values.
224
- Valid keys: "project" (for project_dir), "port" (for serial port)
225
-
226
- Examples:
227
- Build only needs project lock:
228
- return {"project": request.project_dir}
229
-
230
- Deploy needs both project and port locks:
231
- return {"project": request.project_dir, "port": request.port}
232
-
233
- Monitor only needs port lock:
234
- return {"port": request.port}
235
- """
236
- pass
237
-
238
- @abstractmethod
239
- def execute_operation(
240
- self,
241
- request: "BuildRequest | DeployRequest | MonitorRequest",
242
- context: "DaemonContext",
243
- ) -> bool:
244
- """Execute the actual operation logic.
245
-
246
- This is the core business logic that subclasses must implement.
247
- All boilerplate (locks, status updates, error handling) is handled
248
- by the base class.
249
-
250
- Args:
251
- request: The request being processed
252
- context: The daemon context with all subsystems
253
-
254
- Returns:
255
- True if operation succeeded, False otherwise
256
-
257
- Example:
258
- >>> def execute_operation(self, request, context):
259
- ... # Build the project
260
- ... orchestrator = BuildOrchestratorAVR(verbose=request.verbose)
261
- ... result = orchestrator.build(
262
- ... project_dir=Path(request.project_dir),
263
- ... env_name=request.environment,
264
- ... clean=request.clean_build,
265
- ... )
266
- ... return result.success
267
- """
268
- pass
269
-
270
- def validate_request(
271
- self,
272
- request: "BuildRequest | DeployRequest | MonitorRequest",
273
- context: "DaemonContext",
274
- ) -> bool:
275
- """Validate the request before processing.
276
-
277
- Default implementation always returns True. Override to add validation.
278
-
279
- Args:
280
- request: The request to validate
281
- context: The daemon context
282
-
283
- Returns:
284
- True if request is valid, False otherwise
285
- """
286
- return True
287
-
288
- def get_starting_state(self) -> DaemonState:
289
- """Get the daemon state when operation starts.
290
-
291
- Default implementation uses BUILDING. Override for different operations.
292
-
293
- Returns:
294
- DaemonState enum value for operation start
295
- """
296
- operation_type = self.get_operation_type()
297
- if operation_type == OperationType.BUILD:
298
- return DaemonState.BUILDING
299
- elif operation_type == OperationType.DEPLOY or operation_type == OperationType.BUILD_AND_DEPLOY:
300
- return DaemonState.DEPLOYING
301
- elif operation_type == OperationType.MONITOR:
302
- return DaemonState.MONITORING
303
- else:
304
- return DaemonState.BUILDING
305
-
306
- def get_starting_message(self, request: "BuildRequest | DeployRequest | MonitorRequest") -> str:
307
- """Get the status message when operation starts.
308
-
309
- Args:
310
- request: The request being processed
311
-
312
- Returns:
313
- Human-readable status message
314
- """
315
- operation_type = self.get_operation_type()
316
- if operation_type == OperationType.BUILD:
317
- return f"Building {request.environment}"
318
- elif operation_type == OperationType.DEPLOY or operation_type == OperationType.BUILD_AND_DEPLOY:
319
- return f"Deploying {request.environment}"
320
- elif operation_type == OperationType.MONITOR:
321
- return f"Monitoring {request.environment}"
322
- else:
323
- return f"Processing {request.environment}"
324
-
325
- def get_success_message(self, request: "BuildRequest | DeployRequest | MonitorRequest") -> str:
326
- """Get the status message on success.
327
-
328
- Args:
329
- request: The request that was processed
330
-
331
- Returns:
332
- Human-readable success message
333
- """
334
- operation_type = self.get_operation_type()
335
- if operation_type == OperationType.BUILD:
336
- return "Build successful"
337
- elif operation_type == OperationType.DEPLOY or operation_type == OperationType.BUILD_AND_DEPLOY:
338
- return "Deploy successful"
339
- elif operation_type == OperationType.MONITOR:
340
- return "Monitor completed"
341
- else:
342
- return "Operation successful"
343
-
344
- def get_failure_message(self, request: "BuildRequest | DeployRequest | MonitorRequest") -> str:
345
- """Get the status message on failure.
346
-
347
- Args:
348
- request: The request that failed
349
-
350
- Returns:
351
- Human-readable failure message
352
- """
353
- operation_type = self.get_operation_type()
354
- if operation_type == OperationType.BUILD:
355
- return "Build failed"
356
- elif operation_type == OperationType.DEPLOY or operation_type == OperationType.BUILD_AND_DEPLOY:
357
- return "Deploy failed"
358
- elif operation_type == OperationType.MONITOR:
359
- return "Monitor failed"
360
- else:
361
- return "Operation failed"
362
-
363
- def _acquire_locks(
364
- self,
365
- request: "BuildRequest | DeployRequest | MonitorRequest",
366
- context: "DaemonContext",
367
- lock_stack: ExitStack,
368
- ) -> bool:
369
- """Acquire all required locks for the operation.
370
-
371
- Args:
372
- request: The request being processed
373
- context: The daemon context
374
- lock_stack: ExitStack to manage lock lifetimes
375
-
376
- Returns:
377
- True if all locks acquired, False if any lock is unavailable
378
- """
379
- required_locks = self.get_required_locks(request, context)
380
- operation_type = self.get_operation_type()
381
- operation_desc = f"{operation_type.value} for {request.environment}"
382
-
383
- # Acquire project lock if needed
384
- if "project" in required_locks:
385
- project_dir = required_locks["project"]
386
- try:
387
- lock_stack.enter_context(
388
- context.lock_manager.acquire_project_lock(
389
- project_dir,
390
- blocking=False,
391
- operation_id=request.request_id,
392
- description=operation_desc,
393
- )
394
- )
395
- except LockAcquisitionError as e:
396
- logging.warning(f"Project lock unavailable: {e}")
397
- self._update_status(
398
- context,
399
- DaemonState.FAILED,
400
- str(e),
401
- request=request,
402
- )
403
- return False
404
-
405
- # Acquire port lock if needed
406
- if "port" in required_locks:
407
- port = required_locks["port"]
408
- if port: # Only acquire if port is not None/empty
409
- try:
410
- lock_stack.enter_context(
411
- context.lock_manager.acquire_port_lock(
412
- port,
413
- blocking=False,
414
- operation_id=request.request_id,
415
- description=operation_desc,
416
- )
417
- )
418
- except LockAcquisitionError as e:
419
- logging.warning(f"Port lock unavailable: {e}")
420
- self._update_status(
421
- context,
422
- DaemonState.FAILED,
423
- str(e),
424
- request=request,
425
- )
426
- return False
427
-
428
- return True
429
-
430
- def _update_status(
431
- self,
432
- context: "DaemonContext",
433
- state: DaemonState,
434
- message: str,
435
- request: "BuildRequest | DeployRequest | MonitorRequest",
436
- **kwargs: Any,
437
- ) -> None:
438
- """Update daemon status file.
439
-
440
- Args:
441
- context: The daemon context
442
- state: New daemon state
443
- message: Status message
444
- request: The request being processed
445
- **kwargs: Additional fields for status update
446
- """
447
- # Use the status manager from context
448
- context.status_manager.update_status(
449
- state=state,
450
- message=message,
451
- environment=request.environment,
452
- project_dir=request.project_dir,
453
- request_id=request.request_id,
454
- caller_pid=request.caller_pid,
455
- caller_cwd=request.caller_cwd,
456
- **kwargs,
457
- )
1
+ """
2
+ Request Processor - Template method pattern for daemon request handling.
3
+
4
+ This module provides the RequestProcessor abstract base class which implements
5
+ the Template Method pattern to eliminate code duplication across build, deploy,
6
+ and monitor request handlers. It handles all common concerns (lock management,
7
+ status updates, error handling) while allowing subclasses to implement only
8
+ the operation-specific business logic.
9
+ """
10
+
11
+ import logging
12
+ import time
13
+ from abc import ABC, abstractmethod
14
+ from contextlib import ExitStack
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from fbuild.daemon.lock_manager import LockAcquisitionError
18
+ from fbuild.daemon.messages import DaemonState, OperationType
19
+
20
+ if TYPE_CHECKING:
21
+ from fbuild.daemon.daemon_context import DaemonContext
22
+ from fbuild.daemon.messages import BuildRequest, DeployRequest, MonitorRequest
23
+
24
+
25
+ class RequestProcessor(ABC):
26
+ """Abstract base class for processing daemon requests.
27
+
28
+ This class implements the Template Method pattern to handle all common
29
+ concerns of request processing:
30
+ - Request validation
31
+ - Lock acquisition (port and/or project locks)
32
+ - Status updates (started, in-progress, completed, failed)
33
+ - Error handling and cleanup
34
+ - Operation tracking
35
+
36
+ Subclasses only need to implement:
37
+ - get_operation_type(): Return the OperationType
38
+ - get_required_locks(): Specify which locks are needed
39
+ - execute_operation(): Implement the actual business logic
40
+
41
+ Example:
42
+ >>> class BuildRequestProcessor(RequestProcessor):
43
+ ... def get_operation_type(self) -> OperationType:
44
+ ... return OperationType.BUILD
45
+ ...
46
+ ... def get_required_locks(self, request, context):
47
+ ... return {"project": request.project_dir}
48
+ ...
49
+ ... def execute_operation(self, request, context):
50
+ ... # Actual build logic here
51
+ ... result = build_project(request.project_dir)
52
+ ... return result.success
53
+ """
54
+
55
+ def process_request(
56
+ self,
57
+ request: "BuildRequest | DeployRequest | MonitorRequest",
58
+ context: "DaemonContext",
59
+ ) -> bool:
60
+ """Process a request using the template method pattern.
61
+
62
+ This is the main entry point that coordinates the entire request
63
+ processing lifecycle. It handles all boilerplate while calling
64
+ abstract methods for operation-specific logic.
65
+
66
+ Args:
67
+ request: The request to process (BuildRequest, DeployRequest, or MonitorRequest)
68
+ context: The daemon context containing all subsystems
69
+
70
+ Returns:
71
+ True if operation succeeded, False otherwise
72
+
73
+ Lifecycle:
74
+ 1. Validate request
75
+ 2. Acquire required locks (project and/or port)
76
+ 3. Mark operation as in progress
77
+ 4. Update status to starting state
78
+ 5. Execute operation (abstract method)
79
+ 6. Update status based on result
80
+ 7. Release locks and cleanup
81
+
82
+ Example:
83
+ >>> processor = BuildRequestProcessor()
84
+ >>> success = processor.process_request(build_request, daemon_context)
85
+ """
86
+ logging.info(f"Processing {self.get_operation_type().value} request {request.request_id}: " + f"env={request.environment}, project={request.project_dir}")
87
+
88
+ # Validate request
89
+ if not self.validate_request(request, context):
90
+ self._update_status(
91
+ context,
92
+ DaemonState.FAILED,
93
+ "Request validation failed",
94
+ request=request,
95
+ exit_code=1,
96
+ )
97
+ return False
98
+
99
+ # Use ExitStack to manage multiple locks as context managers
100
+ # We store the result to return after lock release and status update
101
+ result: bool = False
102
+ exception_to_reraise: BaseException | None = None
103
+
104
+ with ExitStack() as lock_stack:
105
+ # Acquire required locks
106
+ if not self._acquire_locks(request, context, lock_stack):
107
+ return False
108
+
109
+ try:
110
+ # Mark operation in progress
111
+ with context.operation_lock:
112
+ context.operation_in_progress = True
113
+
114
+ # Update status to starting state
115
+ self._update_status(
116
+ context,
117
+ self.get_starting_state(),
118
+ self.get_starting_message(request),
119
+ request=request,
120
+ request_started_at=time.time(),
121
+ operation_type=self.get_operation_type(),
122
+ )
123
+
124
+ # Execute the operation (implemented by subclass)
125
+ success = self.execute_operation(request, context)
126
+
127
+ # Update final status
128
+ if success:
129
+ self._update_status(
130
+ context,
131
+ DaemonState.COMPLETED,
132
+ self.get_success_message(request),
133
+ request=request,
134
+ exit_code=0,
135
+ operation_in_progress=False,
136
+ )
137
+ else:
138
+ self._update_status(
139
+ context,
140
+ DaemonState.FAILED,
141
+ self.get_failure_message(request),
142
+ request=request,
143
+ exit_code=1,
144
+ operation_in_progress=False,
145
+ )
146
+
147
+ result = success
148
+
149
+ except KeyboardInterrupt as ki:
150
+ import _thread
151
+
152
+ _thread.interrupt_main()
153
+ exception_to_reraise = ki
154
+ except Exception as e:
155
+ import traceback
156
+
157
+ logging.error(f"{self.get_operation_type().value} exception: {e}")
158
+ logging.error(f"Traceback:\n{traceback.format_exc()}")
159
+ self._update_status(
160
+ context,
161
+ DaemonState.FAILED,
162
+ f"{self.get_operation_type().value} exception: {e}",
163
+ request=request,
164
+ exit_code=1,
165
+ operation_in_progress=False,
166
+ )
167
+ result = False
168
+ finally:
169
+ # Mark operation complete
170
+ with context.operation_lock:
171
+ context.operation_in_progress = False
172
+
173
+ # After locks are released (ExitStack has exited), update status to reflect
174
+ # the new lock state. This ensures the status file shows locks as released.
175
+ # We read the current status and re-write it to capture the updated lock state.
176
+ try:
177
+ current_status = context.status_manager.read_status()
178
+ context.status_manager.update_status(
179
+ state=current_status.state,
180
+ message=current_status.message,
181
+ environment=getattr(current_status, "environment", request.environment),
182
+ project_dir=getattr(current_status, "project_dir", request.project_dir),
183
+ request_id=getattr(current_status, "request_id", request.request_id),
184
+ caller_pid=getattr(current_status, "caller_pid", request.caller_pid),
185
+ caller_cwd=getattr(current_status, "caller_cwd", request.caller_cwd),
186
+ exit_code=getattr(current_status, "exit_code", None),
187
+ )
188
+ except KeyboardInterrupt as ke:
189
+ import _thread
190
+
191
+ _thread.interrupt_main()
192
+ raise ke
193
+ except Exception as e:
194
+ logging.warning(f"Failed to update status after lock release: {e}")
195
+
196
+ # Re-raise KeyboardInterrupt if it was caught
197
+ if exception_to_reraise is not None:
198
+ import _thread
199
+
200
+ _thread.interrupt_main()
201
+ raise exception_to_reraise
202
+
203
+ return result
204
+
205
+ @abstractmethod
206
+ def get_operation_type(self) -> OperationType:
207
+ """Get the operation type for this processor.
208
+
209
+ Returns:
210
+ OperationType enum value (BUILD, DEPLOY, MONITOR, etc.)
211
+ """
212
+ pass
213
+
214
+ @abstractmethod
215
+ def get_required_locks(
216
+ self,
217
+ request: "BuildRequest | DeployRequest | MonitorRequest",
218
+ context: "DaemonContext",
219
+ ) -> dict[str, str]:
220
+ """Specify which locks are required for this operation.
221
+
222
+ Returns:
223
+ Dictionary with lock types as keys and resource identifiers as values.
224
+ Valid keys: "project" (for project_dir), "port" (for serial port)
225
+
226
+ Examples:
227
+ Build only needs project lock:
228
+ return {"project": request.project_dir}
229
+
230
+ Deploy needs both project and port locks:
231
+ return {"project": request.project_dir, "port": request.port}
232
+
233
+ Monitor only needs port lock:
234
+ return {"port": request.port}
235
+ """
236
+ pass
237
+
238
+ @abstractmethod
239
+ def execute_operation(
240
+ self,
241
+ request: "BuildRequest | DeployRequest | MonitorRequest",
242
+ context: "DaemonContext",
243
+ ) -> bool:
244
+ """Execute the actual operation logic.
245
+
246
+ This is the core business logic that subclasses must implement.
247
+ All boilerplate (locks, status updates, error handling) is handled
248
+ by the base class.
249
+
250
+ Args:
251
+ request: The request being processed
252
+ context: The daemon context with all subsystems
253
+
254
+ Returns:
255
+ True if operation succeeded, False otherwise
256
+
257
+ Example:
258
+ >>> def execute_operation(self, request, context):
259
+ ... # Build the project
260
+ ... orchestrator = BuildOrchestratorAVR(verbose=request.verbose)
261
+ ... result = orchestrator.build(
262
+ ... project_dir=Path(request.project_dir),
263
+ ... env_name=request.environment,
264
+ ... clean=request.clean_build,
265
+ ... )
266
+ ... return result.success
267
+ """
268
+ pass
269
+
270
+ def validate_request(
271
+ self,
272
+ request: "BuildRequest | DeployRequest | MonitorRequest",
273
+ context: "DaemonContext",
274
+ ) -> bool:
275
+ """Validate the request before processing.
276
+
277
+ Default implementation always returns True. Override to add validation.
278
+
279
+ Args:
280
+ request: The request to validate
281
+ context: The daemon context
282
+
283
+ Returns:
284
+ True if request is valid, False otherwise
285
+ """
286
+ return True
287
+
288
+ def get_starting_state(self) -> DaemonState:
289
+ """Get the daemon state when operation starts.
290
+
291
+ Default implementation uses BUILDING. Override for different operations.
292
+
293
+ Returns:
294
+ DaemonState enum value for operation start
295
+ """
296
+ operation_type = self.get_operation_type()
297
+ if operation_type == OperationType.BUILD:
298
+ return DaemonState.BUILDING
299
+ elif operation_type == OperationType.DEPLOY or operation_type == OperationType.BUILD_AND_DEPLOY:
300
+ return DaemonState.DEPLOYING
301
+ elif operation_type == OperationType.MONITOR:
302
+ return DaemonState.MONITORING
303
+ else:
304
+ return DaemonState.BUILDING
305
+
306
+ def get_starting_message(self, request: "BuildRequest | DeployRequest | MonitorRequest") -> str:
307
+ """Get the status message when operation starts.
308
+
309
+ Args:
310
+ request: The request being processed
311
+
312
+ Returns:
313
+ Human-readable status message
314
+ """
315
+ operation_type = self.get_operation_type()
316
+ if operation_type == OperationType.BUILD:
317
+ return f"Building {request.environment}"
318
+ elif operation_type == OperationType.DEPLOY or operation_type == OperationType.BUILD_AND_DEPLOY:
319
+ return f"Deploying {request.environment}"
320
+ elif operation_type == OperationType.MONITOR:
321
+ return f"Monitoring {request.environment}"
322
+ else:
323
+ return f"Processing {request.environment}"
324
+
325
+ def get_success_message(self, request: "BuildRequest | DeployRequest | MonitorRequest") -> str:
326
+ """Get the status message on success.
327
+
328
+ Args:
329
+ request: The request that was processed
330
+
331
+ Returns:
332
+ Human-readable success message
333
+ """
334
+ operation_type = self.get_operation_type()
335
+ if operation_type == OperationType.BUILD:
336
+ return "Build successful"
337
+ elif operation_type == OperationType.DEPLOY or operation_type == OperationType.BUILD_AND_DEPLOY:
338
+ return "Deploy successful"
339
+ elif operation_type == OperationType.MONITOR:
340
+ return "Monitor completed"
341
+ else:
342
+ return "Operation successful"
343
+
344
+ def get_failure_message(self, request: "BuildRequest | DeployRequest | MonitorRequest") -> str:
345
+ """Get the status message on failure.
346
+
347
+ Args:
348
+ request: The request that failed
349
+
350
+ Returns:
351
+ Human-readable failure message
352
+ """
353
+ operation_type = self.get_operation_type()
354
+ if operation_type == OperationType.BUILD:
355
+ return "Build failed"
356
+ elif operation_type == OperationType.DEPLOY or operation_type == OperationType.BUILD_AND_DEPLOY:
357
+ return "Deploy failed"
358
+ elif operation_type == OperationType.MONITOR:
359
+ return "Monitor failed"
360
+ else:
361
+ return "Operation failed"
362
+
363
+ def _acquire_locks(
364
+ self,
365
+ request: "BuildRequest | DeployRequest | MonitorRequest",
366
+ context: "DaemonContext",
367
+ lock_stack: ExitStack,
368
+ ) -> bool:
369
+ """Acquire all required locks for the operation.
370
+
371
+ Args:
372
+ request: The request being processed
373
+ context: The daemon context
374
+ lock_stack: ExitStack to manage lock lifetimes
375
+
376
+ Returns:
377
+ True if all locks acquired, False if any lock is unavailable
378
+ """
379
+ required_locks = self.get_required_locks(request, context)
380
+ operation_type = self.get_operation_type()
381
+ operation_desc = f"{operation_type.value} for {request.environment}"
382
+
383
+ # Acquire project lock if needed
384
+ if "project" in required_locks:
385
+ project_dir = required_locks["project"]
386
+ try:
387
+ lock_stack.enter_context(
388
+ context.lock_manager.acquire_project_lock(
389
+ project_dir,
390
+ blocking=False,
391
+ operation_id=request.request_id,
392
+ description=operation_desc,
393
+ )
394
+ )
395
+ except LockAcquisitionError as e:
396
+ logging.warning(f"Project lock unavailable: {e}")
397
+ self._update_status(
398
+ context,
399
+ DaemonState.FAILED,
400
+ str(e),
401
+ request=request,
402
+ )
403
+ return False
404
+
405
+ # Acquire port lock if needed
406
+ if "port" in required_locks:
407
+ port = required_locks["port"]
408
+ if port: # Only acquire if port is not None/empty
409
+ try:
410
+ lock_stack.enter_context(
411
+ context.lock_manager.acquire_port_lock(
412
+ port,
413
+ blocking=False,
414
+ operation_id=request.request_id,
415
+ description=operation_desc,
416
+ )
417
+ )
418
+ except LockAcquisitionError as e:
419
+ logging.warning(f"Port lock unavailable: {e}")
420
+ self._update_status(
421
+ context,
422
+ DaemonState.FAILED,
423
+ str(e),
424
+ request=request,
425
+ )
426
+ return False
427
+
428
+ return True
429
+
430
+ def _update_status(
431
+ self,
432
+ context: "DaemonContext",
433
+ state: DaemonState,
434
+ message: str,
435
+ request: "BuildRequest | DeployRequest | MonitorRequest",
436
+ **kwargs: Any,
437
+ ) -> None:
438
+ """Update daemon status file.
439
+
440
+ Args:
441
+ context: The daemon context
442
+ state: New daemon state
443
+ message: Status message
444
+ request: The request being processed
445
+ **kwargs: Additional fields for status update
446
+ """
447
+ # Use the status manager from context
448
+ context.status_manager.update_status(
449
+ state=state,
450
+ message=message,
451
+ environment=request.environment,
452
+ project_dir=request.project_dir,
453
+ request_id=request.request_id,
454
+ caller_pid=request.caller_pid,
455
+ caller_cwd=request.caller_cwd,
456
+ **kwargs,
457
+ )