fbuild 1.2.8__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 +390 -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_info_generator.py +624 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +93 -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 +664 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +214 -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 +651 -0
- fbuild/build/orchestrator_esp32.py +878 -0
- fbuild/build/orchestrator_rp2040.py +719 -0
- fbuild/build/orchestrator_stm32.py +696 -0
- fbuild/build/orchestrator_teensy.py +580 -0
- fbuild/build/source_compilation_orchestrator.py +218 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +717 -0
- fbuild/cli_utils.py +314 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +542 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +369 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +42 -0
- fbuild/daemon/async_client.py +531 -0
- fbuild/daemon/client.py +1505 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/configuration_lock.py +865 -0
- fbuild/daemon/daemon.py +585 -0
- fbuild/daemon/daemon_context.py +293 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/firmware_ledger.py +546 -0
- fbuild/daemon/lock_manager.py +508 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +957 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/port_state_manager.py +249 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +18 -0
- fbuild/daemon/processors/build_processor.py +248 -0
- fbuild/daemon/processors/deploy_processor.py +664 -0
- fbuild/daemon/processors/install_deps_processor.py +431 -0
- fbuild/daemon/processors/locking_processor.py +777 -0
- fbuild/daemon/processors/monitor_processor.py +285 -0
- fbuild/daemon/request_processor.py +457 -0
- fbuild/daemon/shared_serial.py +819 -0
- fbuild/daemon/status_manager.py +238 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +21 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +310 -0
- fbuild/deploy/docker_utils.py +315 -0
- fbuild/deploy/monitor.py +519 -0
- fbuild/deploy/qemu_runner.py +603 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/ledger/__init__.py +52 -0
- fbuild/ledger/board_ledger.py +560 -0
- fbuild/output.py +352 -0
- fbuild/packages/__init__.py +66 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +256 -0
- fbuild/packages/concurrent_manager.py +510 -0
- fbuild/packages/downloader.py +518 -0
- fbuild/packages/fingerprint.py +423 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_rp2040.py +349 -0
- fbuild/packages/framework_stm32.py +459 -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 +725 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_rp2040.py +400 -0
- fbuild/packages/platform_stm32.py +581 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +369 -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 +489 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_rp2040.py +436 -0
- fbuild/packages/toolchain_stm32.py +417 -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/platform_configs/rp2040.json +70 -0
- fbuild/platform_configs/rp2350.json +76 -0
- fbuild/platform_configs/stm32f1.json +59 -0
- fbuild/platform_configs/stm32f4.json +63 -0
- fbuild/py.typed +0 -0
- fbuild-1.2.8.dist-info/METADATA +468 -0
- fbuild-1.2.8.dist-info/RECORD +121 -0
- fbuild-1.2.8.dist-info/WHEEL +5 -0
- fbuild-1.2.8.dist-info/entry_points.txt +5 -0
- fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
- fbuild-1.2.8.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
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Locking Request Processor - Handles lock management, firmware queries, and serial session requests.
|
|
3
|
+
|
|
4
|
+
This module provides processors for the new centralized locking mechanism messages:
|
|
5
|
+
- Lock management (acquire, release, status)
|
|
6
|
+
- Firmware ledger queries (check if firmware is current)
|
|
7
|
+
- Serial session management (attach, detach, write, read buffer)
|
|
8
|
+
- Client connection management (connect, heartbeat, disconnect)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import logging
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from fbuild.daemon.messages import (
|
|
18
|
+
ClientConnectRequest,
|
|
19
|
+
ClientDisconnectRequest,
|
|
20
|
+
ClientHeartbeatRequest,
|
|
21
|
+
ClientResponse,
|
|
22
|
+
FirmwareQueryRequest,
|
|
23
|
+
FirmwareQueryResponse,
|
|
24
|
+
FirmwareRecordRequest,
|
|
25
|
+
LockAcquireRequest,
|
|
26
|
+
LockReleaseRequest,
|
|
27
|
+
LockResponse,
|
|
28
|
+
LockStatusRequest,
|
|
29
|
+
LockType,
|
|
30
|
+
SerialAttachRequest,
|
|
31
|
+
SerialBufferRequest,
|
|
32
|
+
SerialDetachRequest,
|
|
33
|
+
SerialSessionResponse,
|
|
34
|
+
SerialWriteRequest,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from fbuild.daemon.daemon_context import DaemonContext
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class GenericResponse:
|
|
43
|
+
"""Generic response for operations without a specific response type."""
|
|
44
|
+
|
|
45
|
+
success: bool
|
|
46
|
+
message: str
|
|
47
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
48
|
+
timestamp: float = field(default_factory=time.time)
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
"""Convert to dictionary for JSON serialization."""
|
|
52
|
+
return {
|
|
53
|
+
"success": self.success,
|
|
54
|
+
"message": self.message,
|
|
55
|
+
"data": self.data,
|
|
56
|
+
"timestamp": self.timestamp,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LockingRequestProcessor:
|
|
61
|
+
"""Processor for lock management requests.
|
|
62
|
+
|
|
63
|
+
Handles lock acquire, release, and status requests using the
|
|
64
|
+
ConfigurationLockManager from the daemon context.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def handle_lock_acquire(
|
|
68
|
+
self,
|
|
69
|
+
request: LockAcquireRequest,
|
|
70
|
+
context: "DaemonContext",
|
|
71
|
+
) -> LockResponse:
|
|
72
|
+
"""Handle a lock acquisition request.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
request: Lock acquisition request
|
|
76
|
+
context: Daemon context with configuration lock manager
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
LockResponse indicating success or failure
|
|
80
|
+
"""
|
|
81
|
+
config_key = (request.project_dir, request.environment, request.port)
|
|
82
|
+
manager = context.configuration_lock_manager
|
|
83
|
+
|
|
84
|
+
logging.info(f"Lock acquire request: client={request.client_id}, " f"config={config_key}, type={request.lock_type.value}")
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
if request.lock_type == LockType.EXCLUSIVE:
|
|
88
|
+
success = manager.acquire_exclusive(
|
|
89
|
+
config_key=config_key,
|
|
90
|
+
client_id=request.client_id,
|
|
91
|
+
description=request.description,
|
|
92
|
+
timeout=request.timeout,
|
|
93
|
+
)
|
|
94
|
+
else: # SHARED_READ
|
|
95
|
+
success = manager.acquire_shared_read(
|
|
96
|
+
config_key=config_key,
|
|
97
|
+
client_id=request.client_id,
|
|
98
|
+
description=request.description,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if success:
|
|
102
|
+
# Track resource attachment for cleanup
|
|
103
|
+
resource_key = f"lock:{request.project_dir}|{request.environment}|{request.port}"
|
|
104
|
+
context.client_manager.attach_resource(request.client_id, resource_key)
|
|
105
|
+
|
|
106
|
+
lock_status = manager.get_lock_status(config_key)
|
|
107
|
+
return LockResponse(
|
|
108
|
+
success=True,
|
|
109
|
+
message=f"Lock acquired ({request.lock_type.value})",
|
|
110
|
+
lock_state=lock_status.get("state", "unknown"),
|
|
111
|
+
holder_count=lock_status.get("holder_count", 0),
|
|
112
|
+
waiting_count=lock_status.get("waiting_count", 0),
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
lock_status = manager.get_lock_status(config_key)
|
|
116
|
+
return LockResponse(
|
|
117
|
+
success=False,
|
|
118
|
+
message="Lock not available",
|
|
119
|
+
lock_state=lock_status.get("state", "unknown"),
|
|
120
|
+
holder_count=lock_status.get("holder_count", 0),
|
|
121
|
+
waiting_count=lock_status.get("waiting_count", 0),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
125
|
+
raise
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logging.error(f"Error acquiring lock: {e}")
|
|
128
|
+
return LockResponse(
|
|
129
|
+
success=False,
|
|
130
|
+
message=f"Error acquiring lock: {e}",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def handle_lock_release(
|
|
134
|
+
self,
|
|
135
|
+
request: LockReleaseRequest,
|
|
136
|
+
context: "DaemonContext",
|
|
137
|
+
) -> LockResponse:
|
|
138
|
+
"""Handle a lock release request.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
request: Lock release request
|
|
142
|
+
context: Daemon context with configuration lock manager
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
LockResponse indicating success or failure
|
|
146
|
+
"""
|
|
147
|
+
config_key = (request.project_dir, request.environment, request.port)
|
|
148
|
+
manager = context.configuration_lock_manager
|
|
149
|
+
|
|
150
|
+
logging.info(f"Lock release request: client={request.client_id}, config={config_key}")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
success = manager.release(config_key, request.client_id)
|
|
154
|
+
|
|
155
|
+
if success:
|
|
156
|
+
# Remove resource tracking
|
|
157
|
+
resource_key = f"lock:{request.project_dir}|{request.environment}|{request.port}"
|
|
158
|
+
context.client_manager.detach_resource(request.client_id, resource_key)
|
|
159
|
+
|
|
160
|
+
lock_status = manager.get_lock_status(config_key)
|
|
161
|
+
|
|
162
|
+
return LockResponse(
|
|
163
|
+
success=success,
|
|
164
|
+
message="Lock released" if success else "No lock held by client",
|
|
165
|
+
lock_state=lock_status.get("state", "unlocked"),
|
|
166
|
+
holder_count=lock_status.get("holder_count", 0),
|
|
167
|
+
waiting_count=lock_status.get("waiting_count", 0),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
171
|
+
raise
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logging.error(f"Error releasing lock: {e}")
|
|
174
|
+
return LockResponse(
|
|
175
|
+
success=False,
|
|
176
|
+
message=f"Error releasing lock: {e}",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def handle_lock_status(
|
|
180
|
+
self,
|
|
181
|
+
request: LockStatusRequest,
|
|
182
|
+
context: "DaemonContext",
|
|
183
|
+
) -> LockResponse:
|
|
184
|
+
"""Handle a lock status query request.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
request: Lock status request
|
|
188
|
+
context: Daemon context with configuration lock manager
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
LockResponse with current lock state
|
|
192
|
+
"""
|
|
193
|
+
config_key = (request.project_dir, request.environment, request.port)
|
|
194
|
+
manager = context.configuration_lock_manager
|
|
195
|
+
|
|
196
|
+
logging.debug(f"Lock status request: config={config_key}")
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
lock_status = manager.get_lock_status(config_key)
|
|
200
|
+
|
|
201
|
+
return LockResponse(
|
|
202
|
+
success=True,
|
|
203
|
+
message="Lock status retrieved",
|
|
204
|
+
lock_state=lock_status.get("state", "unlocked"),
|
|
205
|
+
holder_count=lock_status.get("holder_count", 0),
|
|
206
|
+
waiting_count=lock_status.get("waiting_count", 0),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
210
|
+
raise
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logging.error(f"Error getting lock status: {e}")
|
|
213
|
+
return LockResponse(
|
|
214
|
+
success=False,
|
|
215
|
+
message=f"Error getting lock status: {e}",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class FirmwareRequestProcessor:
|
|
220
|
+
"""Processor for firmware ledger requests.
|
|
221
|
+
|
|
222
|
+
Handles firmware query and record requests using the FirmwareLedger
|
|
223
|
+
from the daemon context.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def handle_firmware_query(
|
|
227
|
+
self,
|
|
228
|
+
request: FirmwareQueryRequest,
|
|
229
|
+
context: "DaemonContext",
|
|
230
|
+
) -> FirmwareQueryResponse:
|
|
231
|
+
"""Handle a firmware query request.
|
|
232
|
+
|
|
233
|
+
Checks if the current firmware on a device matches the expected
|
|
234
|
+
source and build flags hashes.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
request: Firmware query request
|
|
238
|
+
context: Daemon context with firmware ledger
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
FirmwareQueryResponse with firmware status
|
|
242
|
+
"""
|
|
243
|
+
ledger = context.firmware_ledger
|
|
244
|
+
|
|
245
|
+
logging.debug(f"Firmware query request: port={request.port}, " f"source_hash={request.source_hash[:16]}...")
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
# Get deployment info if available
|
|
249
|
+
deployment = ledger.get_deployment(request.port)
|
|
250
|
+
|
|
251
|
+
if deployment:
|
|
252
|
+
needs_redeploy = ledger.needs_redeploy(
|
|
253
|
+
port=request.port,
|
|
254
|
+
source_hash=request.source_hash,
|
|
255
|
+
build_flags_hash=request.build_flags_hash,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# is_current means source and build flags match (no redeploy needed)
|
|
259
|
+
is_current = not needs_redeploy
|
|
260
|
+
|
|
261
|
+
return FirmwareQueryResponse(
|
|
262
|
+
is_current=is_current,
|
|
263
|
+
needs_redeploy=needs_redeploy,
|
|
264
|
+
firmware_hash=deployment.firmware_hash,
|
|
265
|
+
project_dir=deployment.project_dir,
|
|
266
|
+
environment=deployment.environment,
|
|
267
|
+
upload_timestamp=deployment.upload_timestamp,
|
|
268
|
+
message="Firmware info retrieved" if is_current else "Redeploy needed",
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
return FirmwareQueryResponse(
|
|
272
|
+
is_current=False,
|
|
273
|
+
needs_redeploy=True,
|
|
274
|
+
message="No firmware recorded for this port",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
278
|
+
raise
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logging.error(f"Error querying firmware: {e}")
|
|
281
|
+
return FirmwareQueryResponse(
|
|
282
|
+
is_current=False,
|
|
283
|
+
needs_redeploy=True,
|
|
284
|
+
message=f"Error querying firmware: {e}",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def handle_firmware_record(
|
|
288
|
+
self,
|
|
289
|
+
request: FirmwareRecordRequest,
|
|
290
|
+
context: "DaemonContext",
|
|
291
|
+
) -> GenericResponse:
|
|
292
|
+
"""Handle a firmware record request.
|
|
293
|
+
|
|
294
|
+
Records a successful deployment in the firmware ledger.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
request: Firmware record request
|
|
298
|
+
context: Daemon context with firmware ledger
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
GenericResponse indicating success or failure
|
|
302
|
+
"""
|
|
303
|
+
ledger = context.firmware_ledger
|
|
304
|
+
|
|
305
|
+
logging.info(f"Firmware record request: port={request.port}, " f"project={request.project_dir}, env={request.environment}")
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
ledger.record_deployment(
|
|
309
|
+
port=request.port,
|
|
310
|
+
firmware_hash=request.firmware_hash,
|
|
311
|
+
source_hash=request.source_hash,
|
|
312
|
+
project_dir=request.project_dir,
|
|
313
|
+
environment=request.environment,
|
|
314
|
+
build_flags_hash=request.build_flags_hash,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return GenericResponse(
|
|
318
|
+
success=True,
|
|
319
|
+
message="Firmware deployment recorded",
|
|
320
|
+
data={
|
|
321
|
+
"port": request.port,
|
|
322
|
+
"firmware_hash": request.firmware_hash,
|
|
323
|
+
"source_hash": request.source_hash,
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
328
|
+
raise
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logging.error(f"Error recording firmware: {e}")
|
|
331
|
+
return GenericResponse(
|
|
332
|
+
success=False,
|
|
333
|
+
message=f"Error recording firmware: {e}",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class SerialSessionProcessor:
|
|
338
|
+
"""Processor for serial session management requests.
|
|
339
|
+
|
|
340
|
+
Handles serial attach, detach, write, and buffer read requests
|
|
341
|
+
using the SharedSerialManager from the daemon context.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
def handle_serial_attach(
|
|
345
|
+
self,
|
|
346
|
+
request: SerialAttachRequest,
|
|
347
|
+
context: "DaemonContext",
|
|
348
|
+
) -> SerialSessionResponse:
|
|
349
|
+
"""Handle a serial attach request.
|
|
350
|
+
|
|
351
|
+
Attaches a client to a serial session as a reader, or opens
|
|
352
|
+
a new serial port if not already open.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
request: Serial attach request
|
|
356
|
+
context: Daemon context with shared serial manager
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
SerialSessionResponse with session status
|
|
360
|
+
"""
|
|
361
|
+
manager = context.shared_serial_manager
|
|
362
|
+
|
|
363
|
+
logging.info(f"Serial attach request: client={request.client_id}, " f"port={request.port}, as_reader={request.as_reader}")
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
# Check if port is already open
|
|
367
|
+
session_info = manager.get_session_info(request.port) or {}
|
|
368
|
+
if session_info and session_info.get("is_open", False):
|
|
369
|
+
if request.as_reader:
|
|
370
|
+
# Attach as reader to existing session
|
|
371
|
+
success = manager.attach_reader(request.port, request.client_id)
|
|
372
|
+
if success:
|
|
373
|
+
# Track resource attachment
|
|
374
|
+
resource_key = f"serial:{request.port}"
|
|
375
|
+
context.client_manager.attach_resource(request.client_id, resource_key)
|
|
376
|
+
|
|
377
|
+
session_info_new = manager.get_session_info(request.port) or {}
|
|
378
|
+
return SerialSessionResponse(
|
|
379
|
+
success=success,
|
|
380
|
+
message="Attached as reader" if success else "Failed to attach",
|
|
381
|
+
is_open=True,
|
|
382
|
+
reader_count=len(session_info_new.get("reader_client_ids", [])),
|
|
383
|
+
has_writer=session_info_new.get("writer_client_id") is not None,
|
|
384
|
+
buffer_size=session_info_new.get("buffer_size", 0),
|
|
385
|
+
)
|
|
386
|
+
else:
|
|
387
|
+
# Port already open, can't open again
|
|
388
|
+
return SerialSessionResponse(
|
|
389
|
+
success=False,
|
|
390
|
+
message="Port already open by another client",
|
|
391
|
+
is_open=True,
|
|
392
|
+
reader_count=len(session_info.get("reader_client_ids", [])),
|
|
393
|
+
has_writer=session_info.get("writer_client_id") is not None,
|
|
394
|
+
)
|
|
395
|
+
else:
|
|
396
|
+
# Open new port
|
|
397
|
+
success = manager.open_port(
|
|
398
|
+
port=request.port,
|
|
399
|
+
baud_rate=request.baud_rate,
|
|
400
|
+
client_id=request.client_id,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if success:
|
|
404
|
+
# Track resource attachment
|
|
405
|
+
resource_key = f"serial:{request.port}"
|
|
406
|
+
context.client_manager.attach_resource(request.client_id, resource_key)
|
|
407
|
+
|
|
408
|
+
new_session_info = manager.get_session_info(request.port) or {}
|
|
409
|
+
return SerialSessionResponse(
|
|
410
|
+
success=True,
|
|
411
|
+
message="Port opened and attached",
|
|
412
|
+
is_open=True,
|
|
413
|
+
reader_count=len(new_session_info.get("reader_client_ids", [])),
|
|
414
|
+
has_writer=new_session_info.get("writer_client_id") is not None,
|
|
415
|
+
buffer_size=0,
|
|
416
|
+
)
|
|
417
|
+
else:
|
|
418
|
+
return SerialSessionResponse(
|
|
419
|
+
success=False,
|
|
420
|
+
message="Failed to open port",
|
|
421
|
+
is_open=False,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
425
|
+
raise
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logging.error(f"Error in serial attach: {e}")
|
|
428
|
+
return SerialSessionResponse(
|
|
429
|
+
success=False,
|
|
430
|
+
message=f"Error: {e}",
|
|
431
|
+
is_open=False,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def handle_serial_detach(
|
|
435
|
+
self,
|
|
436
|
+
request: SerialDetachRequest,
|
|
437
|
+
context: "DaemonContext",
|
|
438
|
+
) -> SerialSessionResponse:
|
|
439
|
+
"""Handle a serial detach request.
|
|
440
|
+
|
|
441
|
+
Detaches a client from a serial session. Optionally closes the port
|
|
442
|
+
if this is the last reader.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
request: Serial detach request
|
|
446
|
+
context: Daemon context with shared serial manager
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
SerialSessionResponse with session status
|
|
450
|
+
"""
|
|
451
|
+
manager = context.shared_serial_manager
|
|
452
|
+
|
|
453
|
+
logging.info(f"Serial detach request: client={request.client_id}, " f"port={request.port}, close_port={request.close_port}")
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
# Detach reader
|
|
457
|
+
success = manager.detach_reader(request.port, request.client_id)
|
|
458
|
+
|
|
459
|
+
if success:
|
|
460
|
+
# Remove resource tracking
|
|
461
|
+
resource_key = f"serial:{request.port}"
|
|
462
|
+
context.client_manager.detach_resource(request.client_id, resource_key)
|
|
463
|
+
|
|
464
|
+
# Check if we should close the port
|
|
465
|
+
session_info = manager.get_session_info(request.port) or {}
|
|
466
|
+
port_is_open = session_info.get("is_open", False)
|
|
467
|
+
|
|
468
|
+
if request.close_port and port_is_open:
|
|
469
|
+
if len(session_info.get("reader_client_ids", [])) == 0:
|
|
470
|
+
manager.close_port(request.port, request.client_id)
|
|
471
|
+
return SerialSessionResponse(
|
|
472
|
+
success=True,
|
|
473
|
+
message="Detached and port closed",
|
|
474
|
+
is_open=False,
|
|
475
|
+
reader_count=0,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if port_is_open:
|
|
479
|
+
session_info = manager.get_session_info(request.port) or {}
|
|
480
|
+
return SerialSessionResponse(
|
|
481
|
+
success=success,
|
|
482
|
+
message="Detached from session" if success else "Not attached",
|
|
483
|
+
is_open=True,
|
|
484
|
+
reader_count=len(session_info.get("reader_client_ids", [])),
|
|
485
|
+
has_writer=session_info.get("writer_client_id") is not None,
|
|
486
|
+
buffer_size=session_info.get("buffer_size", 0),
|
|
487
|
+
)
|
|
488
|
+
else:
|
|
489
|
+
return SerialSessionResponse(
|
|
490
|
+
success=success,
|
|
491
|
+
message="Detached (port not open)",
|
|
492
|
+
is_open=False,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
496
|
+
raise
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logging.error(f"Error in serial detach: {e}")
|
|
499
|
+
return SerialSessionResponse(
|
|
500
|
+
success=False,
|
|
501
|
+
message=f"Error: {e}",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def handle_serial_write(
|
|
505
|
+
self,
|
|
506
|
+
request: SerialWriteRequest,
|
|
507
|
+
context: "DaemonContext",
|
|
508
|
+
) -> SerialSessionResponse:
|
|
509
|
+
"""Handle a serial write request.
|
|
510
|
+
|
|
511
|
+
Writes data to a serial port. The client must have writer access
|
|
512
|
+
or request to acquire it.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
request: Serial write request (data is base64 encoded)
|
|
516
|
+
context: Daemon context with shared serial manager
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
SerialSessionResponse with write status
|
|
520
|
+
"""
|
|
521
|
+
manager = context.shared_serial_manager
|
|
522
|
+
|
|
523
|
+
logging.debug(f"Serial write request: client={request.client_id}, port={request.port}")
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
# Decode base64 data
|
|
527
|
+
data_bytes = base64.b64decode(request.data)
|
|
528
|
+
|
|
529
|
+
# Check if client has writer access
|
|
530
|
+
session_info = manager.get_session_info(request.port) or {}
|
|
531
|
+
current_writer = session_info.get("writer_client_id") if session_info else None
|
|
532
|
+
|
|
533
|
+
if current_writer != request.client_id:
|
|
534
|
+
if request.acquire_writer:
|
|
535
|
+
# Try to acquire writer access
|
|
536
|
+
acquired = manager.acquire_writer(request.port, request.client_id)
|
|
537
|
+
if not acquired:
|
|
538
|
+
return SerialSessionResponse(
|
|
539
|
+
success=False,
|
|
540
|
+
message="Could not acquire writer access",
|
|
541
|
+
is_open=session_info.get("is_open", False),
|
|
542
|
+
has_writer=session_info.get("writer_client_id") is not None,
|
|
543
|
+
)
|
|
544
|
+
else:
|
|
545
|
+
return SerialSessionResponse(
|
|
546
|
+
success=False,
|
|
547
|
+
message="No writer access and acquire_writer is False",
|
|
548
|
+
is_open=session_info.get("is_open", False),
|
|
549
|
+
has_writer=True,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Write data
|
|
553
|
+
bytes_written = manager.write(request.port, request.client_id, data_bytes)
|
|
554
|
+
|
|
555
|
+
return SerialSessionResponse(
|
|
556
|
+
success=bytes_written > 0,
|
|
557
|
+
message=f"Wrote {bytes_written} bytes",
|
|
558
|
+
is_open=True,
|
|
559
|
+
bytes_written=bytes_written,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
563
|
+
raise
|
|
564
|
+
except Exception as e:
|
|
565
|
+
logging.error(f"Error in serial write: {e}")
|
|
566
|
+
return SerialSessionResponse(
|
|
567
|
+
success=False,
|
|
568
|
+
message=f"Error: {e}",
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
def handle_serial_buffer(
|
|
572
|
+
self,
|
|
573
|
+
request: SerialBufferRequest,
|
|
574
|
+
context: "DaemonContext",
|
|
575
|
+
) -> SerialSessionResponse:
|
|
576
|
+
"""Handle a serial buffer read request.
|
|
577
|
+
|
|
578
|
+
Reads buffered output from a serial session.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
request: Serial buffer request
|
|
582
|
+
context: Daemon context with shared serial manager
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
SerialSessionResponse with buffered lines
|
|
586
|
+
"""
|
|
587
|
+
manager = context.shared_serial_manager
|
|
588
|
+
|
|
589
|
+
logging.debug(f"Serial buffer request: client={request.client_id}, " f"port={request.port}, max_lines={request.max_lines}")
|
|
590
|
+
|
|
591
|
+
try:
|
|
592
|
+
session_info = manager.get_session_info(request.port) or {}
|
|
593
|
+
if not session_info.get("is_open", False):
|
|
594
|
+
return SerialSessionResponse(
|
|
595
|
+
success=False,
|
|
596
|
+
message="Port not open",
|
|
597
|
+
is_open=False,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Read buffer
|
|
601
|
+
lines = manager.read_buffer(
|
|
602
|
+
port=request.port,
|
|
603
|
+
client_id=request.client_id,
|
|
604
|
+
max_lines=request.max_lines,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
session_info = manager.get_session_info(request.port) or {}
|
|
608
|
+
|
|
609
|
+
return SerialSessionResponse(
|
|
610
|
+
success=True,
|
|
611
|
+
message=f"Read {len(lines)} lines",
|
|
612
|
+
is_open=True,
|
|
613
|
+
reader_count=len(session_info.get("reader_client_ids", [])),
|
|
614
|
+
has_writer=session_info.get("writer_client_id") is not None,
|
|
615
|
+
buffer_size=session_info.get("buffer_size", 0),
|
|
616
|
+
lines=lines,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
620
|
+
raise
|
|
621
|
+
except Exception as e:
|
|
622
|
+
logging.error(f"Error in serial buffer read: {e}")
|
|
623
|
+
return SerialSessionResponse(
|
|
624
|
+
success=False,
|
|
625
|
+
message=f"Error: {e}",
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class ClientConnectionProcessor:
|
|
630
|
+
"""Processor for client connection management requests.
|
|
631
|
+
|
|
632
|
+
Handles client connect, heartbeat, and disconnect requests using
|
|
633
|
+
the ClientConnectionManager from the daemon context.
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
def handle_client_connect(
|
|
637
|
+
self,
|
|
638
|
+
request: ClientConnectRequest,
|
|
639
|
+
context: "DaemonContext",
|
|
640
|
+
) -> ClientResponse:
|
|
641
|
+
"""Handle a client connection request.
|
|
642
|
+
|
|
643
|
+
Registers a new client with the daemon.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
request: Client connect request
|
|
647
|
+
context: Daemon context with client manager
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
ClientResponse with registration status
|
|
651
|
+
"""
|
|
652
|
+
manager = context.client_manager
|
|
653
|
+
|
|
654
|
+
logging.info(f"Client connect request: client_id={request.client_id}, pid={request.pid}")
|
|
655
|
+
|
|
656
|
+
try:
|
|
657
|
+
# Build metadata from request
|
|
658
|
+
metadata = {
|
|
659
|
+
"hostname": request.hostname,
|
|
660
|
+
"version": request.version,
|
|
661
|
+
"connect_timestamp": request.timestamp,
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# Register client
|
|
665
|
+
client_info = manager.register_client(
|
|
666
|
+
client_id=request.client_id,
|
|
667
|
+
pid=request.pid,
|
|
668
|
+
metadata=metadata,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
return ClientResponse(
|
|
672
|
+
success=True,
|
|
673
|
+
message="Client registered",
|
|
674
|
+
client_id=client_info.client_id,
|
|
675
|
+
is_registered=True,
|
|
676
|
+
total_clients=manager.get_client_count(),
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
680
|
+
raise
|
|
681
|
+
except Exception as e:
|
|
682
|
+
logging.error(f"Error registering client: {e}")
|
|
683
|
+
return ClientResponse(
|
|
684
|
+
success=False,
|
|
685
|
+
message=f"Error: {e}",
|
|
686
|
+
client_id=request.client_id,
|
|
687
|
+
is_registered=False,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
def handle_client_heartbeat(
|
|
691
|
+
self,
|
|
692
|
+
request: ClientHeartbeatRequest,
|
|
693
|
+
context: "DaemonContext",
|
|
694
|
+
) -> ClientResponse:
|
|
695
|
+
"""Handle a client heartbeat request.
|
|
696
|
+
|
|
697
|
+
Updates the last heartbeat time for a client.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
request: Client heartbeat request
|
|
701
|
+
context: Daemon context with client manager
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
ClientResponse with heartbeat status
|
|
705
|
+
"""
|
|
706
|
+
manager = context.client_manager
|
|
707
|
+
|
|
708
|
+
logging.debug(f"Client heartbeat: client_id={request.client_id}")
|
|
709
|
+
|
|
710
|
+
try:
|
|
711
|
+
success = manager.heartbeat(request.client_id)
|
|
712
|
+
|
|
713
|
+
return ClientResponse(
|
|
714
|
+
success=success,
|
|
715
|
+
message="Heartbeat recorded" if success else "Unknown client",
|
|
716
|
+
client_id=request.client_id,
|
|
717
|
+
is_registered=success,
|
|
718
|
+
total_clients=manager.get_client_count(),
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
722
|
+
raise
|
|
723
|
+
except Exception as e:
|
|
724
|
+
logging.error(f"Error processing heartbeat: {e}")
|
|
725
|
+
return ClientResponse(
|
|
726
|
+
success=False,
|
|
727
|
+
message=f"Error: {e}",
|
|
728
|
+
client_id=request.client_id,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def handle_client_disconnect(
|
|
732
|
+
self,
|
|
733
|
+
request: ClientDisconnectRequest,
|
|
734
|
+
context: "DaemonContext",
|
|
735
|
+
) -> ClientResponse:
|
|
736
|
+
"""Handle a client disconnect request.
|
|
737
|
+
|
|
738
|
+
Unregisters a client and releases all its resources.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
request: Client disconnect request
|
|
742
|
+
context: Daemon context with client manager
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
ClientResponse with disconnect status
|
|
746
|
+
"""
|
|
747
|
+
manager = context.client_manager
|
|
748
|
+
|
|
749
|
+
logging.info(f"Client disconnect request: client_id={request.client_id}, " f"reason={request.reason}")
|
|
750
|
+
|
|
751
|
+
try:
|
|
752
|
+
success = manager.unregister_client(request.client_id)
|
|
753
|
+
|
|
754
|
+
return ClientResponse(
|
|
755
|
+
success=success,
|
|
756
|
+
message="Client disconnected" if success else "Client not found",
|
|
757
|
+
client_id=request.client_id,
|
|
758
|
+
is_registered=False,
|
|
759
|
+
total_clients=manager.get_client_count(),
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
except KeyboardInterrupt: # noqa: KBI002
|
|
763
|
+
raise
|
|
764
|
+
except Exception as e:
|
|
765
|
+
logging.error(f"Error disconnecting client: {e}")
|
|
766
|
+
return ClientResponse(
|
|
767
|
+
success=False,
|
|
768
|
+
message=f"Error: {e}",
|
|
769
|
+
client_id=request.client_id,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# Create singleton instances for easy access
|
|
774
|
+
locking_processor = LockingRequestProcessor()
|
|
775
|
+
firmware_processor = FirmwareRequestProcessor()
|
|
776
|
+
serial_processor = SerialSessionProcessor()
|
|
777
|
+
client_processor = ClientConnectionProcessor()
|