pycfdp 0.2.2__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.
- cfdp/__init__.py +106 -0
- cfdp/checksum.py +94 -0
- cfdp/constants.py +153 -0
- cfdp/core.py +895 -0
- cfdp/event.py +55 -0
- cfdp/filestore/__init__.py +2 -0
- cfdp/filestore/base.py +99 -0
- cfdp/filestore/native.py +162 -0
- cfdp/meta/__init__.py +3 -0
- cfdp/meta/datafield.py +11 -0
- cfdp/meta/filestore.py +127 -0
- cfdp/meta/message.py +659 -0
- cfdp/pdu/__init__.py +8 -0
- cfdp/pdu/ack.py +90 -0
- cfdp/pdu/eof.py +123 -0
- cfdp/pdu/filedata.py +90 -0
- cfdp/pdu/finished.py +99 -0
- cfdp/pdu/header.py +159 -0
- cfdp/pdu/keep_alive.py +108 -0
- cfdp/pdu/metadata.py +211 -0
- cfdp/pdu/nak.py +220 -0
- cfdp/pdu/prompt.py +75 -0
- cfdp/pure/__init__.py +68 -0
- cfdp/pure/config.py +141 -0
- cfdp/pure/indications.py +130 -0
- cfdp/pure/machines/__init__.py +25 -0
- cfdp/pure/machines/receiver1.py +433 -0
- cfdp/pure/machines/receiver2.py +893 -0
- cfdp/pure/machines/sender1.py +400 -0
- cfdp/pure/machines/sender2.py +774 -0
- cfdp/pure/outputs.py +203 -0
- cfdp/pure/query_port.py +48 -0
- cfdp/pure/transaction.py +43 -0
- cfdp/py.typed +0 -0
- cfdp/remote_entity_config.py +12 -0
- cfdp/shell/__init__.py +44 -0
- cfdp/shell/checksum_service.py +136 -0
- cfdp/shell/dispatcher.py +185 -0
- cfdp/shell/effect_executor.py +285 -0
- cfdp/shell/machine_registry.py +165 -0
- cfdp/shell/query_port.py +70 -0
- cfdp/shell/timer_service.py +252 -0
- cfdp/transaction_handle.py +50 -0
- cfdp/transport/__init__.py +0 -0
- cfdp/transport/base.py +12 -0
- cfdp/transport/spacepacket.py +47 -0
- cfdp/transport/udp.py +90 -0
- cfdp/transport/zmq.py +54 -0
- pycfdp-0.2.2.dist-info/METADATA +76 -0
- pycfdp-0.2.2.dist-info/RECORD +53 -0
- pycfdp-0.2.2.dist-info/WHEEL +5 -0
- pycfdp-0.2.2.dist-info/licenses/LICENSE.txt +19 -0
- pycfdp-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Pure Class-1 (unacknowledged) receiver machine (issue #15).
|
|
2
|
+
|
|
3
|
+
A pure function of ``(state, event)`` returning an ordered ``list[Output]``. It
|
|
4
|
+
obeys the machine contract:
|
|
5
|
+
|
|
6
|
+
Receiver1(transaction, config, query_port)
|
|
7
|
+
update_state(event) -> list[Output]
|
|
8
|
+
|
|
9
|
+
No ``kernel`` reference, no threads, no open file descriptors, no wall clock.
|
|
10
|
+
|
|
11
|
+
The receiver is **PDU-driven**: the triggering inbound PDU rides on the single
|
|
12
|
+
input door as ``event.pdu`` (see :class:`cfdp.event.Event`), so the contract's
|
|
13
|
+
one-door rule holds and no out-of-contract parameter is added. File handling is
|
|
14
|
+
expressed purely as effects — :class:`OpenTemp`, :class:`WriteSegment`,
|
|
15
|
+
:class:`Finalize`, :class:`CloseFile`. The temp-file *size* read-back is a
|
|
16
|
+
synchronous :class:`~cfdp.pure.query_port.QueryPort` call, but *checksum*
|
|
17
|
+
verification is asynchronous (issue #21): the machine emits
|
|
18
|
+
:class:`RequestChecksum` and completes in the ``E42_CHECKSUM_READY`` continuation
|
|
19
|
+
so a large-file checksum never blocks the dispatcher (design D3/D5/D10/R1). The
|
|
20
|
+
machine holds positions and a received-segment map, never a file descriptor.
|
|
21
|
+
|
|
22
|
+
Class 1 has no NAKs and only the inactivity timer, driven as
|
|
23
|
+
:class:`StartTimer` effects.
|
|
24
|
+
|
|
25
|
+
Verified by differential trace testing against frozen golden fixtures captured
|
|
26
|
+
from the pre-cutover implementation — see ``tests/test_pure_receiver1.py``. In
|
|
27
|
+
particular the pre-cutover machine closed the temp file **twice** on the happy
|
|
28
|
+
EOF path (``close_tempfile`` then ``shutdown``); this machine reproduces both
|
|
29
|
+
:class:`CloseFile` outputs on purpose (the second close is a no-op).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from cfdp.constants import (
|
|
33
|
+
ConditionCode,
|
|
34
|
+
DeliveryCode,
|
|
35
|
+
FaultHandlerAction,
|
|
36
|
+
FileStatus,
|
|
37
|
+
MachineState,
|
|
38
|
+
TransmissionMode,
|
|
39
|
+
)
|
|
40
|
+
from cfdp.event import Event, EventType
|
|
41
|
+
|
|
42
|
+
from .. import indications
|
|
43
|
+
from ..config import TransactionConfig
|
|
44
|
+
from ..outputs import (
|
|
45
|
+
CancelTimer,
|
|
46
|
+
CloseFile,
|
|
47
|
+
ExecuteFilestoreRequests,
|
|
48
|
+
Finalize,
|
|
49
|
+
InternalEvent,
|
|
50
|
+
OpenTemp,
|
|
51
|
+
Output,
|
|
52
|
+
PostUserMessage,
|
|
53
|
+
RequestChecksum,
|
|
54
|
+
StartTimer,
|
|
55
|
+
TimerKind,
|
|
56
|
+
WriteSegment,
|
|
57
|
+
)
|
|
58
|
+
from ..query_port import QueryPort
|
|
59
|
+
from ..transaction import Transaction
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Receiver1:
|
|
63
|
+
"""Pure implementation of the Class 1 (unacknowledged transfer) receiver."""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
transaction: Transaction,
|
|
68
|
+
config: TransactionConfig,
|
|
69
|
+
query_port: QueryPort,
|
|
70
|
+
) -> None:
|
|
71
|
+
self.transaction = transaction
|
|
72
|
+
self.config = config
|
|
73
|
+
self.query_port = query_port
|
|
74
|
+
|
|
75
|
+
self.transmission_mode = TransmissionMode.UNACKNOWLEDGED
|
|
76
|
+
self.state = MachineState.WAIT_FOR_MD
|
|
77
|
+
|
|
78
|
+
# Request payload received off the inbound Metadata PDU (design D3). The
|
|
79
|
+
# old machine stashes these on the mutable Transaction via
|
|
80
|
+
# process_metadata_options; here they are machine state.
|
|
81
|
+
self.filestore_requests = None
|
|
82
|
+
self.messages_to_user = None
|
|
83
|
+
|
|
84
|
+
self._initialize()
|
|
85
|
+
|
|
86
|
+
# -- state reset (mirrors Machine.initialize) -------------------------- #
|
|
87
|
+
|
|
88
|
+
def _initialize(self):
|
|
89
|
+
# as per CCSDS 720.2-G-3, Page 5-13, g)
|
|
90
|
+
self.condition_code = ConditionCode.NO_ERROR
|
|
91
|
+
self.delivery_code = DeliveryCode.DATA_INCOMPLETE
|
|
92
|
+
self.frozen = False
|
|
93
|
+
self.metadata_received = False
|
|
94
|
+
self.pdu_received = False
|
|
95
|
+
self.suspended = False
|
|
96
|
+
self.file_status = FileStatus.UNREPORTED
|
|
97
|
+
self.filestore_responses = None
|
|
98
|
+
self.status_report = None
|
|
99
|
+
|
|
100
|
+
self.received_file_size = 0
|
|
101
|
+
# Whether a temp file handle is open (formerly transaction.file_handle
|
|
102
|
+
# truthiness). shutdown() emits CloseFile only if one was opened.
|
|
103
|
+
self._temp_open = False
|
|
104
|
+
self.received_file_segments = {}
|
|
105
|
+
# Progress advances as segments arrive (formerly transaction._progress).
|
|
106
|
+
self._progress = 0
|
|
107
|
+
# Async-checksum verification (issue #21): the EOF-carried checksum to
|
|
108
|
+
# compare against, remembered across the RequestChecksum -> E42 hop while
|
|
109
|
+
# the shell computes the temp-file checksum off the event thread.
|
|
110
|
+
self._expected_checksum = None
|
|
111
|
+
|
|
112
|
+
# -- the single input door --------------------------------------------- #
|
|
113
|
+
|
|
114
|
+
def update_state(self, event: Event) -> list[Output]:
|
|
115
|
+
"""See state table given in CCSDS 720.2-G-3, Table 5-2.
|
|
116
|
+
|
|
117
|
+
Returns the ordered list of :class:`~cfdp.pure.outputs.Output` produced
|
|
118
|
+
by handling ``event``. The triggering inbound PDU, if any, is read from
|
|
119
|
+
``event.pdu``.
|
|
120
|
+
"""
|
|
121
|
+
self._out: list[Output] = []
|
|
122
|
+
pdu = getattr(event, "pdu", None)
|
|
123
|
+
|
|
124
|
+
# as per CCSDS 720.2-G-3, 5.3.7: any inbound PDU restarts inactivity.
|
|
125
|
+
if pdu is not None:
|
|
126
|
+
self._restart_inactivity_timer()
|
|
127
|
+
|
|
128
|
+
if self.state == MachineState.WAIT_FOR_MD:
|
|
129
|
+
self._update_wait_for_md(event, pdu)
|
|
130
|
+
elif self.state == MachineState.WAIT_FOR_EOF:
|
|
131
|
+
self._update_wait_for_eof(event, pdu)
|
|
132
|
+
|
|
133
|
+
out, self._out = self._out, []
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
# -- WAIT_FOR_MD state ------------------------------------------------- #
|
|
137
|
+
|
|
138
|
+
def _update_wait_for_md(self, event, pdu):
|
|
139
|
+
if event.type == EventType.E0_ENTERED_STATE:
|
|
140
|
+
self._initialize()
|
|
141
|
+
self._restart_inactivity_timer()
|
|
142
|
+
|
|
143
|
+
elif event.type == EventType.E2_ABANDON_TRANSACTION:
|
|
144
|
+
self._issue_abandoned_indication()
|
|
145
|
+
self._shutdown()
|
|
146
|
+
|
|
147
|
+
elif event.type == EventType.E3_NOTICE_OF_CANCELLATION:
|
|
148
|
+
self._issue_transaction_finished_indication()
|
|
149
|
+
self._shutdown()
|
|
150
|
+
|
|
151
|
+
elif event.type == EventType.E10_RECEIVED_METADATA:
|
|
152
|
+
self._issue_metadata_received_indication(pdu.file_size)
|
|
153
|
+
if self._is_file_transfer():
|
|
154
|
+
self._open_tempfile()
|
|
155
|
+
self._process_metadata_options(pdu)
|
|
156
|
+
self.state = MachineState.WAIT_FOR_EOF
|
|
157
|
+
|
|
158
|
+
elif event.type == EventType.E12_RECEIVED_EOF_NO_ERROR:
|
|
159
|
+
self._issue_transaction_finished_indication()
|
|
160
|
+
self._shutdown()
|
|
161
|
+
|
|
162
|
+
elif event.type == EventType.E13_RECEIVED_EOF_CANCEL:
|
|
163
|
+
self.condition_code = pdu.condition_code
|
|
164
|
+
if pdu.fault_location is not None:
|
|
165
|
+
self.status_report = {"fault_location": pdu.fault_location}
|
|
166
|
+
self._issue_transaction_finished_indication()
|
|
167
|
+
self._shutdown()
|
|
168
|
+
|
|
169
|
+
elif event.type == EventType.E27_INACTIVITY_TIMEOUT:
|
|
170
|
+
self._restart_inactivity_timer()
|
|
171
|
+
self._fault_inactivity()
|
|
172
|
+
|
|
173
|
+
elif event.type == EventType.E33_RECEIVED_CANCEL_REQUEST:
|
|
174
|
+
self.condition_code = ConditionCode.CANCEL_REQUEST_RECEIVED
|
|
175
|
+
self._emit(InternalEvent(EventType.E3_NOTICE_OF_CANCELLATION))
|
|
176
|
+
|
|
177
|
+
elif event.type == EventType.E34_RECEIVED_REPORT_REQUEST:
|
|
178
|
+
self._issue_report_indication()
|
|
179
|
+
|
|
180
|
+
# -- WAIT_FOR_EOF state ------------------------------------------------ #
|
|
181
|
+
|
|
182
|
+
def _update_wait_for_eof(self, event, pdu):
|
|
183
|
+
if event.type == EventType.E2_ABANDON_TRANSACTION:
|
|
184
|
+
self._issue_abandoned_indication()
|
|
185
|
+
self._shutdown()
|
|
186
|
+
|
|
187
|
+
elif event.type == EventType.E3_NOTICE_OF_CANCELLATION:
|
|
188
|
+
self._issue_transaction_finished_indication()
|
|
189
|
+
self._shutdown()
|
|
190
|
+
|
|
191
|
+
elif event.type == EventType.E11_RECEIVED_FILEDATA:
|
|
192
|
+
self._issue_filesegment_received_indication(
|
|
193
|
+
pdu.segment_offset, len(pdu.file_data)
|
|
194
|
+
)
|
|
195
|
+
if self._is_file_transfer():
|
|
196
|
+
self._store_file_data(pdu)
|
|
197
|
+
self._update_received_file_size(pdu)
|
|
198
|
+
|
|
199
|
+
elif event.type == EventType.E12_RECEIVED_EOF_NO_ERROR:
|
|
200
|
+
self._issue_eof_received_indication()
|
|
201
|
+
|
|
202
|
+
if self._is_file_transfer():
|
|
203
|
+
if self._is_file_size_error(pdu.file_size):
|
|
204
|
+
if not self._fault_file_size():
|
|
205
|
+
self._close_tempfile()
|
|
206
|
+
return
|
|
207
|
+
# Checksum verification is async (issue #21): remember the EOF's
|
|
208
|
+
# checksum, ask the shell to compute the temp-file checksum off
|
|
209
|
+
# the event thread, and finish in the E42 continuation.
|
|
210
|
+
self._expected_checksum = pdu.file_checksum
|
|
211
|
+
self._emit(
|
|
212
|
+
RequestChecksum(
|
|
213
|
+
checksum_type=self.transaction.checksum_type,
|
|
214
|
+
from_temp_handle=True,
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
if self.filestore_requests:
|
|
220
|
+
self._execute_filestore_requests()
|
|
221
|
+
|
|
222
|
+
self._issue_transaction_finished_indication()
|
|
223
|
+
self._shutdown()
|
|
224
|
+
|
|
225
|
+
elif event.type == EventType.E42_CHECKSUM_READY:
|
|
226
|
+
self._finish_after_checksum(event.checksum)
|
|
227
|
+
|
|
228
|
+
elif event.type == EventType.E13_RECEIVED_EOF_CANCEL:
|
|
229
|
+
self.condition_code = pdu.condition_code
|
|
230
|
+
if pdu.fault_location is not None:
|
|
231
|
+
self.status_report = {"fault_location": pdu.fault_location}
|
|
232
|
+
self._issue_transaction_finished_indication()
|
|
233
|
+
self._shutdown()
|
|
234
|
+
|
|
235
|
+
elif event.type == EventType.E27_INACTIVITY_TIMEOUT:
|
|
236
|
+
self._restart_inactivity_timer()
|
|
237
|
+
self._fault_inactivity()
|
|
238
|
+
|
|
239
|
+
elif event.type == EventType.E33_RECEIVED_CANCEL_REQUEST:
|
|
240
|
+
self.condition_code = ConditionCode.CANCEL_REQUEST_RECEIVED
|
|
241
|
+
self._emit(InternalEvent(EventType.E3_NOTICE_OF_CANCELLATION))
|
|
242
|
+
|
|
243
|
+
elif event.type == EventType.E34_RECEIVED_REPORT_REQUEST:
|
|
244
|
+
self._issue_report_indication()
|
|
245
|
+
|
|
246
|
+
# -- output helper ----------------------------------------------------- #
|
|
247
|
+
|
|
248
|
+
def _emit(self, output):
|
|
249
|
+
self._out.append(output)
|
|
250
|
+
|
|
251
|
+
# -- queries / predicates ---------------------------------------------- #
|
|
252
|
+
|
|
253
|
+
def _is_file_transfer(self):
|
|
254
|
+
return bool(
|
|
255
|
+
self.transaction.source_filename
|
|
256
|
+
and len(self.transaction.source_filename) > 0
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _is_file_size_error(self, file_size):
|
|
260
|
+
return self.received_file_size > file_size
|
|
261
|
+
|
|
262
|
+
def _is_file_checksum_failure(self, expected, calculated):
|
|
263
|
+
# ``calculated`` is the temp-file checksum computed asynchronously by the
|
|
264
|
+
# shell (issue #21) and delivered on E42_CHECKSUM_READY; ``expected`` is
|
|
265
|
+
# the EOF-carried value.
|
|
266
|
+
return calculated != expected
|
|
267
|
+
|
|
268
|
+
def _finish_after_checksum(self, calculated_checksum):
|
|
269
|
+
"""E42 continuation: verify checksum, finalize, and finish (issue #21)."""
|
|
270
|
+
if self._is_file_checksum_failure(self._expected_checksum, calculated_checksum):
|
|
271
|
+
if not self._fault_file_checksum():
|
|
272
|
+
self._close_tempfile()
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
self.delivery_code = DeliveryCode.DATA_COMPLETE
|
|
276
|
+
self._copy_tempfile_to_destfile()
|
|
277
|
+
self._close_tempfile()
|
|
278
|
+
self.file_status = FileStatus.RETAINED_SUCCESSFULLY
|
|
279
|
+
|
|
280
|
+
if self.filestore_requests:
|
|
281
|
+
self._execute_filestore_requests()
|
|
282
|
+
|
|
283
|
+
self._issue_transaction_finished_indication()
|
|
284
|
+
self._shutdown()
|
|
285
|
+
|
|
286
|
+
# -- timer effects ----------------------------------------------------- #
|
|
287
|
+
|
|
288
|
+
def _restart_inactivity_timer(self):
|
|
289
|
+
# Mirror Machine.restart_inactivity_timer: only while not suspended.
|
|
290
|
+
if not self.suspended:
|
|
291
|
+
self._emit(
|
|
292
|
+
StartTimer(
|
|
293
|
+
kind=TimerKind.INACTIVITY,
|
|
294
|
+
interval=self.config.transaction_inactivity_limit,
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# -- fault handling ---------------------------------------------------- #
|
|
299
|
+
|
|
300
|
+
def _fault_inactivity(self):
|
|
301
|
+
return self._run_fault_handler(
|
|
302
|
+
self.config.get_fault_handler(ConditionCode.INACTIVITY_DETECTED)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def _fault_file_size(self):
|
|
306
|
+
return self._run_fault_handler(
|
|
307
|
+
self.config.get_fault_handler(ConditionCode.FILE_SIZE_ERROR)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def _fault_file_checksum(self):
|
|
311
|
+
return self._run_fault_handler(
|
|
312
|
+
self.config.get_fault_handler(ConditionCode.FILE_CHECKSUM_FAILURE)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def _run_fault_handler(self, fault_handler):
|
|
316
|
+
"""Return True if execution should continue (IGNORE), False otherwise."""
|
|
317
|
+
if fault_handler == FaultHandlerAction.CANCEL:
|
|
318
|
+
self._emit(InternalEvent(EventType.E33_RECEIVED_CANCEL_REQUEST))
|
|
319
|
+
return False
|
|
320
|
+
elif fault_handler == FaultHandlerAction.SUSPEND:
|
|
321
|
+
self._emit(InternalEvent(EventType.E31_RECEIVED_SUSPEND_REQUEST))
|
|
322
|
+
return False
|
|
323
|
+
elif fault_handler == FaultHandlerAction.IGNORE:
|
|
324
|
+
return True
|
|
325
|
+
elif fault_handler == FaultHandlerAction.ABANDON:
|
|
326
|
+
self._emit(InternalEvent(EventType.E2_ABANDON_TRANSACTION))
|
|
327
|
+
return False
|
|
328
|
+
else:
|
|
329
|
+
raise ValueError
|
|
330
|
+
|
|
331
|
+
# -- file lifecycle effects -------------------------------------------- #
|
|
332
|
+
|
|
333
|
+
def _open_tempfile(self):
|
|
334
|
+
self._emit(OpenTemp())
|
|
335
|
+
self._temp_open = True
|
|
336
|
+
|
|
337
|
+
def _store_file_data(self, pdu):
|
|
338
|
+
self._emit(WriteSegment(offset=pdu.segment_offset, data=bytes(pdu.file_data)))
|
|
339
|
+
self.received_file_segments[pdu.segment_offset] = len(pdu.file_data)
|
|
340
|
+
|
|
341
|
+
def _copy_tempfile_to_destfile(self):
|
|
342
|
+
# Class-1 Metadata always arrives first, so the frozen transaction has
|
|
343
|
+
# the destination filename; carry it explicitly so the effect is
|
|
344
|
+
# self-describing (and matches the recorder's normalized Finalize).
|
|
345
|
+
self._emit(Finalize(destination_filename=self.transaction.destination_filename))
|
|
346
|
+
|
|
347
|
+
def _close_tempfile(self):
|
|
348
|
+
# Mirror Machine.close_tempfile: it does NOT clear the handle, so the
|
|
349
|
+
# subsequent shutdown() sees a live handle and closes it a second time.
|
|
350
|
+
self._emit(CloseFile())
|
|
351
|
+
|
|
352
|
+
def _update_received_file_size(self, pdu):
|
|
353
|
+
new_size = pdu.segment_offset + len(pdu.file_data)
|
|
354
|
+
if self.received_file_size < new_size:
|
|
355
|
+
self.received_file_size = new_size
|
|
356
|
+
self._progress = new_size
|
|
357
|
+
|
|
358
|
+
def _process_metadata_options(self, pdu):
|
|
359
|
+
self.filestore_requests = pdu.filestore_requests
|
|
360
|
+
self.messages_to_user = pdu.messages_to_user
|
|
361
|
+
for message in self.messages_to_user or []:
|
|
362
|
+
message.originating_transaction = self.transaction
|
|
363
|
+
self._emit(PostUserMessage(message=message))
|
|
364
|
+
|
|
365
|
+
def _execute_filestore_requests(self):
|
|
366
|
+
# Filestore-request execution is a shell effect (design D3/D10): the
|
|
367
|
+
# machine carries the request TLVs but never touches the filestore. ``put``
|
|
368
|
+
# permits filestore requests in ANY transmission mode, so a Class-1
|
|
369
|
+
# Metadata can carry them (issue #27); emit the equivalent command for the
|
|
370
|
+
# EffectExecutor to run, mirroring Receiver2._execute_filestore_requests.
|
|
371
|
+
# Both call sites are guarded by ``if self.filestore_requests``, so this is
|
|
372
|
+
# a no-op (no effect) when the transfer carries none — the existing golden
|
|
373
|
+
# fixtures, which never carry filestore requests, are unchanged.
|
|
374
|
+
self._emit(ExecuteFilestoreRequests(requests=self.filestore_requests))
|
|
375
|
+
|
|
376
|
+
def _shutdown(self):
|
|
377
|
+
# Mirror Receiver1.shutdown: first super().shutdown() (base Machine)
|
|
378
|
+
# closes the handle only if one was opened — on the happy EOF path
|
|
379
|
+
# close_tempfile already emitted a CloseFile without clearing the handle,
|
|
380
|
+
# so this emits a second CloseFile, matching the old machine (the
|
|
381
|
+
# differential oracle). Then inactivity_timer.shutdown() cancels the
|
|
382
|
+
# inactivity timer, which the recording timer emits as
|
|
383
|
+
# CancelTimer(INACTIVITY) — always, regardless of whether a file was
|
|
384
|
+
# open.
|
|
385
|
+
if self._temp_open:
|
|
386
|
+
self._emit(CloseFile())
|
|
387
|
+
self._temp_open = False
|
|
388
|
+
self._emit(CancelTimer(kind=TimerKind.INACTIVITY))
|
|
389
|
+
self.state = MachineState.COMPLETED
|
|
390
|
+
|
|
391
|
+
# -- indications ------------------------------------------------------- #
|
|
392
|
+
|
|
393
|
+
def _issue_metadata_received_indication(self, file_size):
|
|
394
|
+
self._emit(
|
|
395
|
+
indications.metadata_received(
|
|
396
|
+
self.transaction.id,
|
|
397
|
+
self.transaction.source_entity_id,
|
|
398
|
+
file_size,
|
|
399
|
+
self.transaction.source_filename,
|
|
400
|
+
self.transaction.destination_filename,
|
|
401
|
+
self.messages_to_user,
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def _issue_filesegment_received_indication(self, offset, length):
|
|
406
|
+
self._emit(
|
|
407
|
+
indications.filesegment_received(self.transaction.id, offset, length)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def _issue_eof_received_indication(self):
|
|
411
|
+
self._emit(indications.eof_received(self.transaction.id))
|
|
412
|
+
|
|
413
|
+
def _issue_transaction_finished_indication(self):
|
|
414
|
+
self._emit(
|
|
415
|
+
indications.transaction_finished(
|
|
416
|
+
self.transaction.id,
|
|
417
|
+
self.condition_code,
|
|
418
|
+
self.file_status,
|
|
419
|
+
self.delivery_code,
|
|
420
|
+
self.filestore_responses,
|
|
421
|
+
self.status_report,
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def _issue_report_indication(self):
|
|
426
|
+
self._emit(indications.report(self.transaction.id, self.status_report))
|
|
427
|
+
|
|
428
|
+
def _issue_abandoned_indication(self):
|
|
429
|
+
self._emit(
|
|
430
|
+
indications.abandoned(
|
|
431
|
+
self.transaction.id, self.condition_code, self._progress
|
|
432
|
+
)
|
|
433
|
+
)
|