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
fbuild/daemon/shared_serial.py
CHANGED
|
@@ -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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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:
|
fbuild/daemon/status_manager.py
CHANGED
|
@@ -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
|
+
)
|