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,893 @@
|
|
|
1
|
+
"""Pure Class-2 (acknowledged) receiver machine (issue #19).
|
|
2
|
+
|
|
3
|
+
A pure function of ``(state, event)`` returning an ordered ``list[Output]``. It
|
|
4
|
+
obeys the machine contract:
|
|
5
|
+
|
|
6
|
+
Receiver2(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 Class-2 receiver is the most complex machine. On top of the Class-1 receiver
|
|
12
|
+
it adds:
|
|
13
|
+
|
|
14
|
+
* **NAK-driven recovery** — it computes a NAK list from its own received-segment
|
|
15
|
+
map (:meth:`_update_nak_list`) and sends NAKs (:class:`SendPdu`) while any gap
|
|
16
|
+
remains; a missing metadata is requested via the ``(0, 0)`` segment request;
|
|
17
|
+
* a **NAK timer** (design D6) driving retransmission, with the expiration-limit
|
|
18
|
+
counter held in machine state and bumped on each ``E26_NAK_TIMEOUT`` via
|
|
19
|
+
:meth:`note_nak_timeout` (mirroring the real ``Timer._expired`` counter bump);
|
|
20
|
+
* an **ACK timer** guarding the Finished/ACK-Finished handshake, whose limit
|
|
21
|
+
counter is likewise machine state, bumped via :meth:`note_ack_timeout`;
|
|
22
|
+
* the **inactivity timer**, restarted on every inbound PDU (5.3.7);
|
|
23
|
+
* **checksum verification** of the received temp file, computed asynchronously
|
|
24
|
+
(issue #21): the machine emits ``RequestChecksum`` and finishes in the
|
|
25
|
+
``E42_CHECKSUM_READY`` continuation so a large-file checksum never blocks the
|
|
26
|
+
dispatcher;
|
|
27
|
+
* the ``ACK(EOF)`` / ``Finished`` / ``ACK(Finished)`` handshake and cancellation
|
|
28
|
+
via a ``TRANSACTION_CANCELLED`` state.
|
|
29
|
+
|
|
30
|
+
Reply PDUs (NAK, ACK, Finished) reuse the sender's first PDU header with the
|
|
31
|
+
direction reversed (CCSDS 5.6.1 l). Rather than mutate the inbound PDU's header
|
|
32
|
+
in place, this machine *copies* the relevant header fields once
|
|
33
|
+
(:meth:`_reuse_senders_first_pdu_header`) and rebuilds each reply PDU from them,
|
|
34
|
+
producing byte-identical output without mutating the inbound PDU (purity).
|
|
35
|
+
|
|
36
|
+
The triggering inbound PDU rides on the single input door as ``event.pdu`` (see
|
|
37
|
+
:class:`cfdp.event.Event`), matching the other receiver machines.
|
|
38
|
+
|
|
39
|
+
Verified by differential trace testing against frozen golden fixtures captured
|
|
40
|
+
from the pre-cutover implementation — see ``tests/test_pure_receiver2.py``. In
|
|
41
|
+
particular the pre-cutover machine closed the received file **twice** on the
|
|
42
|
+
happy path (``close_tempfile`` in SEND_FINISHED then ``shutdown``); this machine
|
|
43
|
+
reproduces both :class:`CloseFile` outputs on purpose (the second close is a
|
|
44
|
+
no-op).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from cfdp.constants import (
|
|
48
|
+
ConditionCode,
|
|
49
|
+
DeliveryCode,
|
|
50
|
+
Direction,
|
|
51
|
+
DirectiveCode,
|
|
52
|
+
DirectiveSubTypeCode,
|
|
53
|
+
FaultHandlerAction,
|
|
54
|
+
FileStatus,
|
|
55
|
+
MachineState,
|
|
56
|
+
TransactionStatus,
|
|
57
|
+
TransmissionMode,
|
|
58
|
+
)
|
|
59
|
+
from cfdp.event import Event, EventType
|
|
60
|
+
from cfdp.pdu import AckPdu, FinishedPdu, NakPdu
|
|
61
|
+
|
|
62
|
+
from .. import indications
|
|
63
|
+
from ..config import TransactionConfig
|
|
64
|
+
from ..outputs import (
|
|
65
|
+
CancelTimer,
|
|
66
|
+
CloseFile,
|
|
67
|
+
ExecuteFilestoreRequests,
|
|
68
|
+
Finalize,
|
|
69
|
+
InternalEvent,
|
|
70
|
+
OpenTemp,
|
|
71
|
+
Output,
|
|
72
|
+
PostUserMessage,
|
|
73
|
+
RequestChecksum,
|
|
74
|
+
SendPdu,
|
|
75
|
+
StartTimer,
|
|
76
|
+
TimerKind,
|
|
77
|
+
WriteSegment,
|
|
78
|
+
)
|
|
79
|
+
from ..query_port import QueryPort
|
|
80
|
+
from ..transaction import Transaction
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Receiver2:
|
|
84
|
+
"""Pure implementation of the Class 2 (acknowledged transfer) receiver."""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
transaction: Transaction,
|
|
89
|
+
config: TransactionConfig,
|
|
90
|
+
query_port: QueryPort,
|
|
91
|
+
) -> None:
|
|
92
|
+
self.transaction = transaction
|
|
93
|
+
self.config = config
|
|
94
|
+
self.query_port = query_port
|
|
95
|
+
|
|
96
|
+
self.transmission_mode = TransmissionMode.ACKNOWLEDGED
|
|
97
|
+
self.state = MachineState.WAIT_FOR_EOF
|
|
98
|
+
self.file_size = None
|
|
99
|
+
|
|
100
|
+
# Request payload received off the inbound Metadata PDU (design D3). The
|
|
101
|
+
# old machine stashes these on the mutable Transaction; here they are
|
|
102
|
+
# machine state.
|
|
103
|
+
self.filestore_requests = None
|
|
104
|
+
self.messages_to_user = None
|
|
105
|
+
self.source_filename = transaction.source_filename
|
|
106
|
+
self.destination_filename = transaction.destination_filename
|
|
107
|
+
self.checksum_type = transaction.checksum_type
|
|
108
|
+
|
|
109
|
+
# Captured first-PDU header fields (design: reuse sender's header,
|
|
110
|
+
# direction reversed). None until the first inbound PDU is seen.
|
|
111
|
+
self._first_header = None
|
|
112
|
+
|
|
113
|
+
# Timer bookkeeping (design D6): the ACK timer's suspend flag and both
|
|
114
|
+
# expiration-limit counters are protocol state on the machine.
|
|
115
|
+
self._ack_timer_suspended = False
|
|
116
|
+
self._ack_expiration_counter = 0
|
|
117
|
+
self._nak_expiration_counter = 0
|
|
118
|
+
|
|
119
|
+
self._initialize()
|
|
120
|
+
|
|
121
|
+
# -- state reset (mirrors Machine.initialize) -------------------------- #
|
|
122
|
+
|
|
123
|
+
def _initialize(self):
|
|
124
|
+
# as per CCSDS 720.2-G-3, Page 5-13, g)
|
|
125
|
+
self.condition_code = ConditionCode.NO_ERROR
|
|
126
|
+
self.delivery_code = DeliveryCode.DATA_INCOMPLETE
|
|
127
|
+
self.frozen = False
|
|
128
|
+
self.metadata_received = False
|
|
129
|
+
self.pdu_received = False
|
|
130
|
+
self.suspended = False
|
|
131
|
+
self.file_status = FileStatus.UNREPORTED
|
|
132
|
+
self.filestore_responses = None
|
|
133
|
+
self.status_report = None
|
|
134
|
+
|
|
135
|
+
self.received_file_size = 0
|
|
136
|
+
# Whether a temp file handle is open (formerly transaction.file_handle
|
|
137
|
+
# truthiness). shutdown() emits CloseFile only if one was opened.
|
|
138
|
+
self._file_open = False
|
|
139
|
+
self.received_file_segments = {}
|
|
140
|
+
self.nak_list = []
|
|
141
|
+
self._progress = 0
|
|
142
|
+
self.received_file_checksum = None
|
|
143
|
+
|
|
144
|
+
# -- the single input door --------------------------------------------- #
|
|
145
|
+
|
|
146
|
+
def update_state(self, event: Event) -> list[Output]:
|
|
147
|
+
"""See state table given in CCSDS 720.2-G-3, Table 5-4.
|
|
148
|
+
|
|
149
|
+
Returns the ordered list of :class:`~cfdp.pure.outputs.Output` produced
|
|
150
|
+
by handling ``event``. The triggering inbound PDU, if any, is read from
|
|
151
|
+
``event.pdu``.
|
|
152
|
+
"""
|
|
153
|
+
self._out: list[Output] = []
|
|
154
|
+
pdu = getattr(event, "pdu", None)
|
|
155
|
+
|
|
156
|
+
# as per CCSDS 720.2-G-3, 5.3.7: any inbound PDU restarts inactivity.
|
|
157
|
+
if pdu is not None:
|
|
158
|
+
self._restart_inactivity_timer()
|
|
159
|
+
|
|
160
|
+
if self.state == MachineState.WAIT_FOR_EOF:
|
|
161
|
+
self._update_wait_for_eof(event, pdu)
|
|
162
|
+
elif self.state == MachineState.GET_MISSING_DATA:
|
|
163
|
+
self._update_get_missing_data(event, pdu)
|
|
164
|
+
elif self.state == MachineState.SEND_FINISHED:
|
|
165
|
+
self._update_send_finished(event, pdu)
|
|
166
|
+
elif self.state == MachineState.TRANSACTION_CANCELLED:
|
|
167
|
+
self._update_transaction_cancelled(event, pdu)
|
|
168
|
+
|
|
169
|
+
out, self._out = self._out, []
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
# -- WAIT_FOR_EOF state ------------------------------------------------ #
|
|
173
|
+
|
|
174
|
+
def _update_wait_for_eof(self, event, pdu):
|
|
175
|
+
if event.type == EventType.E0_ENTERED_STATE:
|
|
176
|
+
self._initialize()
|
|
177
|
+
self._restart_inactivity_timer()
|
|
178
|
+
|
|
179
|
+
elif event.type == EventType.E2_ABANDON_TRANSACTION:
|
|
180
|
+
self._issue_abandoned_indication()
|
|
181
|
+
self._shutdown()
|
|
182
|
+
|
|
183
|
+
elif event.type == EventType.E3_NOTICE_OF_CANCELLATION:
|
|
184
|
+
self.state = MachineState.TRANSACTION_CANCELLED
|
|
185
|
+
self._emit(InternalEvent(EventType.E0_ENTERED_STATE))
|
|
186
|
+
|
|
187
|
+
elif event.type == EventType.E4_NOTICE_OF_SUSPENSION:
|
|
188
|
+
self._notice_of_suspension()
|
|
189
|
+
|
|
190
|
+
elif event.type == EventType.E5_SUSPEND_TIMERS:
|
|
191
|
+
self._suspend_inactivity_timer()
|
|
192
|
+
|
|
193
|
+
elif event.type == EventType.E6_RESUME_TIMERS:
|
|
194
|
+
self._resume_inactivity_timer()
|
|
195
|
+
|
|
196
|
+
elif event.type == EventType.E10_RECEIVED_METADATA:
|
|
197
|
+
self._reuse_senders_first_pdu_header(pdu)
|
|
198
|
+
if not self.metadata_received:
|
|
199
|
+
self._absorb_metadata(pdu)
|
|
200
|
+
|
|
201
|
+
elif event.type == EventType.E11_RECEIVED_FILEDATA:
|
|
202
|
+
self._issue_filesegment_received_indication(
|
|
203
|
+
pdu.segment_offset, len(pdu.file_data)
|
|
204
|
+
)
|
|
205
|
+
self._reuse_senders_first_pdu_header(pdu)
|
|
206
|
+
if not self._file_open:
|
|
207
|
+
self._open_tempfile()
|
|
208
|
+
self._store_file_data(pdu)
|
|
209
|
+
self._update_received_file_size(pdu)
|
|
210
|
+
# NAK list is recomputed lazily at send points (issue #32), not on
|
|
211
|
+
# every stored segment.
|
|
212
|
+
|
|
213
|
+
elif event.type == EventType.E12_RECEIVED_EOF_NO_ERROR:
|
|
214
|
+
self._reuse_senders_first_pdu_header(pdu)
|
|
215
|
+
# Persist the authoritative EOF file size so later NAK recomputes
|
|
216
|
+
# (GET_MISSING_DATA entry / NAK timeout) use it even when Metadata
|
|
217
|
+
# never arrived (issue #32).
|
|
218
|
+
self.file_size = pdu.file_size
|
|
219
|
+
self._update_nak_list(self.file_size)
|
|
220
|
+
self._send_ack_eof()
|
|
221
|
+
|
|
222
|
+
if self._is_file_size_error(pdu.file_size):
|
|
223
|
+
# Honor the configured fault handler: CANCEL/SUSPEND/ABANDON emit
|
|
224
|
+
# an internal event that sets its own state, so we must NOT fall
|
|
225
|
+
# through and overwrite it (issue #25). IGNORE returns True.
|
|
226
|
+
if not self._fault_file_size():
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
self.received_file_checksum = pdu.file_checksum
|
|
230
|
+
|
|
231
|
+
if self._is_nak_list_empty():
|
|
232
|
+
self.state = MachineState.SEND_FINISHED
|
|
233
|
+
else:
|
|
234
|
+
self.state = MachineState.GET_MISSING_DATA
|
|
235
|
+
self._emit(InternalEvent(EventType.E0_ENTERED_STATE))
|
|
236
|
+
|
|
237
|
+
elif event.type == EventType.E13_RECEIVED_EOF_CANCEL:
|
|
238
|
+
self._reuse_senders_first_pdu_header(pdu)
|
|
239
|
+
self.condition_code = pdu.condition_code
|
|
240
|
+
if pdu.fault_location is not None:
|
|
241
|
+
self.status_report = {"fault_location": pdu.fault_location}
|
|
242
|
+
self._send_ack_eof()
|
|
243
|
+
self._issue_transaction_finished_indication()
|
|
244
|
+
self._shutdown()
|
|
245
|
+
|
|
246
|
+
elif event.type == EventType.E27_INACTIVITY_TIMEOUT:
|
|
247
|
+
self._fault_inactivity()
|
|
248
|
+
|
|
249
|
+
elif event.type == EventType.E31_RECEIVED_SUSPEND_REQUEST:
|
|
250
|
+
self._emit(InternalEvent(EventType.E4_NOTICE_OF_SUSPENSION))
|
|
251
|
+
|
|
252
|
+
elif event.type == EventType.E32_RECEIVED_RESUME_REQUEST:
|
|
253
|
+
self._resume_request()
|
|
254
|
+
|
|
255
|
+
elif event.type == EventType.E33_RECEIVED_CANCEL_REQUEST:
|
|
256
|
+
self.condition_code = ConditionCode.CANCEL_REQUEST_RECEIVED
|
|
257
|
+
self._emit(InternalEvent(EventType.E3_NOTICE_OF_CANCELLATION))
|
|
258
|
+
|
|
259
|
+
elif event.type == EventType.E34_RECEIVED_REPORT_REQUEST:
|
|
260
|
+
self._issue_report_indication()
|
|
261
|
+
|
|
262
|
+
elif event.type == EventType.E40_RECEIVED_FREEZE:
|
|
263
|
+
self._freeze()
|
|
264
|
+
|
|
265
|
+
elif event.type == EventType.E41_RECEIVED_THAW:
|
|
266
|
+
self._thaw()
|
|
267
|
+
|
|
268
|
+
# -- GET_MISSING_DATA state -------------------------------------------- #
|
|
269
|
+
|
|
270
|
+
def _update_get_missing_data(self, event, pdu):
|
|
271
|
+
if event.type == EventType.E0_ENTERED_STATE:
|
|
272
|
+
if not self.suspended and not self.frozen:
|
|
273
|
+
self._update_nak_list(self.file_size)
|
|
274
|
+
self._send_nak()
|
|
275
|
+
self._restart_nak_timer()
|
|
276
|
+
|
|
277
|
+
elif event.type == EventType.E2_ABANDON_TRANSACTION:
|
|
278
|
+
self._issue_abandoned_indication()
|
|
279
|
+
self._shutdown()
|
|
280
|
+
|
|
281
|
+
elif event.type == EventType.E3_NOTICE_OF_CANCELLATION:
|
|
282
|
+
self.state = MachineState.TRANSACTION_CANCELLED
|
|
283
|
+
self._emit(InternalEvent(EventType.E0_ENTERED_STATE))
|
|
284
|
+
|
|
285
|
+
elif event.type == EventType.E4_NOTICE_OF_SUSPENSION:
|
|
286
|
+
self._notice_of_suspension()
|
|
287
|
+
|
|
288
|
+
elif event.type == EventType.E5_SUSPEND_TIMERS:
|
|
289
|
+
self._suspend_inactivity_timer()
|
|
290
|
+
|
|
291
|
+
elif event.type == EventType.E6_RESUME_TIMERS:
|
|
292
|
+
self._restart_inactivity_timer()
|
|
293
|
+
|
|
294
|
+
elif event.type == EventType.E10_RECEIVED_METADATA:
|
|
295
|
+
if not self.metadata_received:
|
|
296
|
+
self._absorb_metadata(pdu)
|
|
297
|
+
|
|
298
|
+
elif event.type == EventType.E11_RECEIVED_FILEDATA:
|
|
299
|
+
if not self._file_open:
|
|
300
|
+
self._open_tempfile()
|
|
301
|
+
self._store_file_data(pdu)
|
|
302
|
+
self._update_received_file_size(pdu)
|
|
303
|
+
# NAK list is recomputed lazily at send points (issue #32).
|
|
304
|
+
self._restart_nak_timer()
|
|
305
|
+
|
|
306
|
+
elif event.type == EventType.E12_RECEIVED_EOF_NO_ERROR:
|
|
307
|
+
self._send_ack_eof()
|
|
308
|
+
|
|
309
|
+
elif event.type == EventType.E13_RECEIVED_EOF_CANCEL:
|
|
310
|
+
self.condition_code = pdu.condition_code
|
|
311
|
+
if pdu.fault_location is not None:
|
|
312
|
+
self.status_report = {"fault_location": pdu.fault_location}
|
|
313
|
+
self._send_ack_eof()
|
|
314
|
+
self._issue_transaction_finished_indication()
|
|
315
|
+
self._shutdown()
|
|
316
|
+
|
|
317
|
+
elif event.type == EventType.E26_NAK_TIMEOUT:
|
|
318
|
+
self._restart_nak_timer()
|
|
319
|
+
self._update_nak_list(self.file_size)
|
|
320
|
+
if self._is_nak_list_empty():
|
|
321
|
+
self.state = MachineState.SEND_FINISHED
|
|
322
|
+
self._emit(InternalEvent(EventType.E0_ENTERED_STATE))
|
|
323
|
+
elif not self.suspended and not self.frozen:
|
|
324
|
+
if self._is_nak_limit_reached():
|
|
325
|
+
if not self._fault_nak_limit():
|
|
326
|
+
return
|
|
327
|
+
self._send_nak()
|
|
328
|
+
|
|
329
|
+
elif event.type == EventType.E27_INACTIVITY_TIMEOUT:
|
|
330
|
+
self._fault_inactivity()
|
|
331
|
+
|
|
332
|
+
elif event.type == EventType.E31_RECEIVED_SUSPEND_REQUEST:
|
|
333
|
+
self._emit(InternalEvent(EventType.E4_NOTICE_OF_SUSPENSION))
|
|
334
|
+
|
|
335
|
+
elif event.type == EventType.E32_RECEIVED_RESUME_REQUEST:
|
|
336
|
+
self._resume_request()
|
|
337
|
+
|
|
338
|
+
elif event.type == EventType.E33_RECEIVED_CANCEL_REQUEST:
|
|
339
|
+
self.condition_code = ConditionCode.CANCEL_REQUEST_RECEIVED
|
|
340
|
+
self._emit(InternalEvent(EventType.E3_NOTICE_OF_CANCELLATION))
|
|
341
|
+
|
|
342
|
+
elif event.type == EventType.E34_RECEIVED_REPORT_REQUEST:
|
|
343
|
+
self._issue_report_indication()
|
|
344
|
+
|
|
345
|
+
elif event.type == EventType.E40_RECEIVED_FREEZE:
|
|
346
|
+
self._freeze()
|
|
347
|
+
|
|
348
|
+
elif event.type == EventType.E41_RECEIVED_THAW:
|
|
349
|
+
self._thaw()
|
|
350
|
+
|
|
351
|
+
# -- SEND_FINISHED state ----------------------------------------------- #
|
|
352
|
+
|
|
353
|
+
def _update_send_finished(self, event, pdu):
|
|
354
|
+
if event.type == EventType.E0_ENTERED_STATE:
|
|
355
|
+
self.delivery_code = DeliveryCode.DATA_COMPLETE
|
|
356
|
+
self._cancel_nak_timer()
|
|
357
|
+
|
|
358
|
+
if self._is_file_transfer():
|
|
359
|
+
# Checksum verification is async (issue #21): compute the temp
|
|
360
|
+
# file's checksum off the event thread and finish in the E42
|
|
361
|
+
# continuation below.
|
|
362
|
+
self._emit(
|
|
363
|
+
RequestChecksum(
|
|
364
|
+
checksum_type=self.checksum_type, from_temp_handle=True
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
if self.filestore_requests:
|
|
370
|
+
self._execute_filestore_requests()
|
|
371
|
+
|
|
372
|
+
self._send_finished()
|
|
373
|
+
self._restart_ack_timer()
|
|
374
|
+
|
|
375
|
+
elif event.type == EventType.E42_CHECKSUM_READY:
|
|
376
|
+
self._finish_after_checksum(event.checksum)
|
|
377
|
+
|
|
378
|
+
elif event.type == EventType.E2_ABANDON_TRANSACTION:
|
|
379
|
+
self._issue_abandoned_indication()
|
|
380
|
+
self._shutdown()
|
|
381
|
+
|
|
382
|
+
elif event.type == EventType.E3_NOTICE_OF_CANCELLATION:
|
|
383
|
+
self.state = MachineState.TRANSACTION_CANCELLED
|
|
384
|
+
self._emit(InternalEvent(EventType.E0_ENTERED_STATE))
|
|
385
|
+
|
|
386
|
+
elif event.type == EventType.E4_NOTICE_OF_SUSPENSION:
|
|
387
|
+
self._notice_of_suspension()
|
|
388
|
+
|
|
389
|
+
elif event.type == EventType.E5_SUSPEND_TIMERS:
|
|
390
|
+
self._suspend_inactivity_timer()
|
|
391
|
+
self._suspend_ack_timer()
|
|
392
|
+
|
|
393
|
+
elif event.type == EventType.E6_RESUME_TIMERS:
|
|
394
|
+
self._restart_inactivity_timer()
|
|
395
|
+
self._resume_ack_timer()
|
|
396
|
+
|
|
397
|
+
elif event.type == EventType.E12_RECEIVED_EOF_NO_ERROR:
|
|
398
|
+
self._send_ack_eof()
|
|
399
|
+
|
|
400
|
+
elif event.type == EventType.E13_RECEIVED_EOF_CANCEL:
|
|
401
|
+
self.condition_code = pdu.condition_code
|
|
402
|
+
if pdu.fault_location is not None:
|
|
403
|
+
self.status_report = {"fault_location": pdu.fault_location}
|
|
404
|
+
self._send_ack_eof()
|
|
405
|
+
self._issue_transaction_finished_indication()
|
|
406
|
+
self._shutdown()
|
|
407
|
+
|
|
408
|
+
elif event.type == EventType.E18_RECEIVED_ACK_FINISHED:
|
|
409
|
+
self._issue_transaction_finished_indication()
|
|
410
|
+
self._shutdown()
|
|
411
|
+
|
|
412
|
+
elif event.type == EventType.E25_ACK_TIMEOUT:
|
|
413
|
+
self._restart_ack_timer()
|
|
414
|
+
if self._is_ack_limit_reached():
|
|
415
|
+
self._fault_ack_limit()
|
|
416
|
+
self._send_finished()
|
|
417
|
+
|
|
418
|
+
elif event.type == EventType.E27_INACTIVITY_TIMEOUT:
|
|
419
|
+
self._fault_inactivity()
|
|
420
|
+
|
|
421
|
+
elif event.type == EventType.E31_RECEIVED_SUSPEND_REQUEST:
|
|
422
|
+
self._emit(InternalEvent(EventType.E4_NOTICE_OF_SUSPENSION))
|
|
423
|
+
|
|
424
|
+
elif event.type == EventType.E32_RECEIVED_RESUME_REQUEST:
|
|
425
|
+
self._resume_request()
|
|
426
|
+
|
|
427
|
+
elif event.type == EventType.E33_RECEIVED_CANCEL_REQUEST:
|
|
428
|
+
self.condition_code = ConditionCode.CANCEL_REQUEST_RECEIVED
|
|
429
|
+
self._emit(InternalEvent(EventType.E3_NOTICE_OF_CANCELLATION))
|
|
430
|
+
|
|
431
|
+
elif event.type == EventType.E34_RECEIVED_REPORT_REQUEST:
|
|
432
|
+
self._issue_report_indication()
|
|
433
|
+
|
|
434
|
+
elif event.type == EventType.E40_RECEIVED_FREEZE:
|
|
435
|
+
self._freeze()
|
|
436
|
+
|
|
437
|
+
elif event.type == EventType.E41_RECEIVED_THAW:
|
|
438
|
+
self._thaw()
|
|
439
|
+
|
|
440
|
+
# -- TRANSACTION_CANCELLED state --------------------------------------- #
|
|
441
|
+
|
|
442
|
+
def _update_transaction_cancelled(self, event, pdu):
|
|
443
|
+
if event.type == EventType.E0_ENTERED_STATE:
|
|
444
|
+
self.suspended = False
|
|
445
|
+
self._send_finished()
|
|
446
|
+
self._restart_ack_timer()
|
|
447
|
+
|
|
448
|
+
elif event.type == EventType.E2_ABANDON_TRANSACTION:
|
|
449
|
+
self._issue_abandoned_indication()
|
|
450
|
+
self._shutdown()
|
|
451
|
+
|
|
452
|
+
elif event.type == EventType.E4_NOTICE_OF_SUSPENSION:
|
|
453
|
+
self._notice_of_suspension()
|
|
454
|
+
|
|
455
|
+
elif event.type == EventType.E5_SUSPEND_TIMERS:
|
|
456
|
+
self._suspend_inactivity_timer()
|
|
457
|
+
self._suspend_ack_timer()
|
|
458
|
+
|
|
459
|
+
elif event.type == EventType.E6_RESUME_TIMERS:
|
|
460
|
+
self._restart_inactivity_timer()
|
|
461
|
+
self._resume_ack_timer()
|
|
462
|
+
|
|
463
|
+
elif event.type == EventType.E13_RECEIVED_EOF_CANCEL:
|
|
464
|
+
self.condition_code = pdu.condition_code
|
|
465
|
+
if pdu.fault_location is not None:
|
|
466
|
+
self.status_report = {"fault_location": pdu.fault_location}
|
|
467
|
+
self._send_ack_eof()
|
|
468
|
+
self._issue_transaction_finished_indication()
|
|
469
|
+
self._shutdown()
|
|
470
|
+
|
|
471
|
+
elif event.type == EventType.E18_RECEIVED_ACK_FINISHED:
|
|
472
|
+
if self.condition_code != ConditionCode.NO_ERROR:
|
|
473
|
+
self._issue_transaction_finished_indication()
|
|
474
|
+
self._shutdown()
|
|
475
|
+
|
|
476
|
+
elif event.type == EventType.E25_ACK_TIMEOUT:
|
|
477
|
+
self._restart_ack_timer()
|
|
478
|
+
if self._is_ack_limit_reached():
|
|
479
|
+
self._emit(InternalEvent(EventType.E2_ABANDON_TRANSACTION))
|
|
480
|
+
else:
|
|
481
|
+
self.condition_code = ConditionCode.CANCEL_REQUEST_RECEIVED
|
|
482
|
+
self._send_finished()
|
|
483
|
+
|
|
484
|
+
elif event.type == EventType.E27_INACTIVITY_TIMEOUT:
|
|
485
|
+
self._issue_abandoned_indication()
|
|
486
|
+
self._shutdown()
|
|
487
|
+
|
|
488
|
+
elif event.type == EventType.E31_RECEIVED_SUSPEND_REQUEST:
|
|
489
|
+
self._emit(InternalEvent(EventType.E4_NOTICE_OF_SUSPENSION))
|
|
490
|
+
|
|
491
|
+
elif event.type == EventType.E32_RECEIVED_RESUME_REQUEST:
|
|
492
|
+
self._resume_request()
|
|
493
|
+
|
|
494
|
+
elif event.type == EventType.E34_RECEIVED_REPORT_REQUEST:
|
|
495
|
+
self._issue_report_indication()
|
|
496
|
+
|
|
497
|
+
elif event.type == EventType.E40_RECEIVED_FREEZE:
|
|
498
|
+
self._freeze()
|
|
499
|
+
|
|
500
|
+
elif event.type == EventType.E41_RECEIVED_THAW:
|
|
501
|
+
self._thaw()
|
|
502
|
+
|
|
503
|
+
# -- shared suspend/resume/freeze/thaw helpers ------------------------- #
|
|
504
|
+
|
|
505
|
+
def _notice_of_suspension(self):
|
|
506
|
+
if not self.suspended:
|
|
507
|
+
self._issue_suspended_indication()
|
|
508
|
+
self.suspended = True
|
|
509
|
+
if not self.frozen:
|
|
510
|
+
self._emit(InternalEvent(EventType.E5_SUSPEND_TIMERS))
|
|
511
|
+
|
|
512
|
+
def _resume_request(self):
|
|
513
|
+
if self.suspended:
|
|
514
|
+
self._issue_resumed_indication()
|
|
515
|
+
self.suspended = False
|
|
516
|
+
if not self.frozen:
|
|
517
|
+
self._emit(InternalEvent(EventType.E6_RESUME_TIMERS))
|
|
518
|
+
|
|
519
|
+
def _freeze(self):
|
|
520
|
+
if not self.frozen:
|
|
521
|
+
self.frozen = True
|
|
522
|
+
if not self.suspended:
|
|
523
|
+
self._emit(InternalEvent(EventType.E5_SUSPEND_TIMERS))
|
|
524
|
+
|
|
525
|
+
def _thaw(self):
|
|
526
|
+
if self.frozen:
|
|
527
|
+
self.frozen = False
|
|
528
|
+
if not self.suspended:
|
|
529
|
+
self._emit(InternalEvent(EventType.E6_RESUME_TIMERS))
|
|
530
|
+
|
|
531
|
+
# -- output helper ----------------------------------------------------- #
|
|
532
|
+
|
|
533
|
+
def _emit(self, output):
|
|
534
|
+
self._out.append(output)
|
|
535
|
+
|
|
536
|
+
# -- queries / predicates ---------------------------------------------- #
|
|
537
|
+
|
|
538
|
+
def _is_file_transfer(self):
|
|
539
|
+
return bool(self.source_filename and len(self.source_filename) > 0)
|
|
540
|
+
|
|
541
|
+
def _is_file_size_error(self, file_size):
|
|
542
|
+
return self.received_file_size > file_size
|
|
543
|
+
|
|
544
|
+
def _is_file_checksum_failure(self, expected, calculated):
|
|
545
|
+
# ``calculated`` is the temp-file checksum computed asynchronously by the
|
|
546
|
+
# shell (issue #21) and delivered on E42_CHECKSUM_READY; ``expected`` is
|
|
547
|
+
# the EOF-carried value stashed on ``received_file_checksum``.
|
|
548
|
+
return calculated != expected
|
|
549
|
+
|
|
550
|
+
def _finish_after_checksum(self, calculated_checksum):
|
|
551
|
+
"""E42 continuation: verify checksum, finalize, and send Finished (#21)."""
|
|
552
|
+
if self._is_file_checksum_failure(
|
|
553
|
+
self.received_file_checksum, calculated_checksum
|
|
554
|
+
):
|
|
555
|
+
if not self._fault_file_checksum():
|
|
556
|
+
self._close_tempfile()
|
|
557
|
+
return
|
|
558
|
+
self._copy_tempfile_to_destfile()
|
|
559
|
+
self._close_tempfile()
|
|
560
|
+
self.file_status = FileStatus.RETAINED_SUCCESSFULLY
|
|
561
|
+
|
|
562
|
+
if self.filestore_requests:
|
|
563
|
+
self._execute_filestore_requests()
|
|
564
|
+
|
|
565
|
+
self._send_finished()
|
|
566
|
+
self._restart_ack_timer()
|
|
567
|
+
|
|
568
|
+
def _is_nak_list_empty(self):
|
|
569
|
+
return not self.nak_list
|
|
570
|
+
|
|
571
|
+
# -- NAK list computation (pure, mirrors Machine.update_nak_list) ------- #
|
|
572
|
+
|
|
573
|
+
def _update_nak_list(self, file_size=None):
|
|
574
|
+
self.nak_list = []
|
|
575
|
+
pointer = 0
|
|
576
|
+
if not self.metadata_received:
|
|
577
|
+
self.nak_list.append((0, 0))
|
|
578
|
+
for offset, length in sorted(self.received_file_segments.items()):
|
|
579
|
+
if offset > pointer:
|
|
580
|
+
self.nak_list.append((pointer, offset))
|
|
581
|
+
pointer = offset + length
|
|
582
|
+
if file_size is not None:
|
|
583
|
+
if pointer < file_size:
|
|
584
|
+
self.nak_list.append((pointer, file_size))
|
|
585
|
+
|
|
586
|
+
def _get_segment_requests(self):
|
|
587
|
+
return self.nak_list
|
|
588
|
+
|
|
589
|
+
# -- timer effects (design D6) ----------------------------------------- #
|
|
590
|
+
|
|
591
|
+
def _restart_inactivity_timer(self):
|
|
592
|
+
if not self.suspended:
|
|
593
|
+
self._emit(
|
|
594
|
+
StartTimer(
|
|
595
|
+
kind=TimerKind.INACTIVITY,
|
|
596
|
+
interval=self.config.transaction_inactivity_limit,
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
def _resume_inactivity_timer(self):
|
|
601
|
+
if not self.suspended:
|
|
602
|
+
self._emit(
|
|
603
|
+
StartTimer(
|
|
604
|
+
kind=TimerKind.INACTIVITY,
|
|
605
|
+
interval=self.config.transaction_inactivity_limit,
|
|
606
|
+
)
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
def _suspend_inactivity_timer(self):
|
|
610
|
+
self._emit(CancelTimer(kind=TimerKind.INACTIVITY))
|
|
611
|
+
|
|
612
|
+
def _restart_ack_timer(self):
|
|
613
|
+
self._emit(
|
|
614
|
+
StartTimer(kind=TimerKind.ACK, interval=self.config.ack_timer_interval)
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
def _suspend_ack_timer(self):
|
|
618
|
+
self._ack_timer_suspended = True
|
|
619
|
+
self._emit(CancelTimer(kind=TimerKind.ACK))
|
|
620
|
+
|
|
621
|
+
def _resume_ack_timer(self):
|
|
622
|
+
self._ack_timer_suspended = False
|
|
623
|
+
self._emit(
|
|
624
|
+
StartTimer(kind=TimerKind.ACK, interval=self.config.ack_timer_interval)
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
def _restart_nak_timer(self):
|
|
628
|
+
self._emit(
|
|
629
|
+
StartTimer(kind=TimerKind.NAK, interval=self.config.nak_timer_interval)
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
def _cancel_nak_timer(self):
|
|
633
|
+
self._emit(CancelTimer(kind=TimerKind.NAK))
|
|
634
|
+
|
|
635
|
+
def _is_ack_limit_reached(self):
|
|
636
|
+
limit = self.config.ack_timer_expiration_limit
|
|
637
|
+
if limit is None:
|
|
638
|
+
return False
|
|
639
|
+
return self._ack_expiration_counter >= limit
|
|
640
|
+
|
|
641
|
+
def _is_nak_limit_reached(self):
|
|
642
|
+
limit = self.config.nak_timer_expiration_limit
|
|
643
|
+
if limit is None:
|
|
644
|
+
return False
|
|
645
|
+
return self._nak_expiration_counter >= limit
|
|
646
|
+
|
|
647
|
+
# -- expiration counters (bumped by the shell's timer event) ----------- #
|
|
648
|
+
|
|
649
|
+
def note_ack_timeout(self):
|
|
650
|
+
"""Mirror the real ``Timer._expired`` ACK counter bump (design D6)."""
|
|
651
|
+
self._ack_expiration_counter += 1
|
|
652
|
+
|
|
653
|
+
def note_nak_timeout(self):
|
|
654
|
+
"""Mirror the real ``Timer._expired`` NAK counter bump (design D6)."""
|
|
655
|
+
self._nak_expiration_counter += 1
|
|
656
|
+
|
|
657
|
+
# -- fault handling ---------------------------------------------------- #
|
|
658
|
+
|
|
659
|
+
def _fault_inactivity(self):
|
|
660
|
+
return self._run_fault_handler(
|
|
661
|
+
self.config.get_fault_handler(ConditionCode.INACTIVITY_DETECTED)
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
def _fault_file_size(self):
|
|
665
|
+
return self._run_fault_handler(
|
|
666
|
+
self.config.get_fault_handler(ConditionCode.FILE_SIZE_ERROR)
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
def _fault_file_checksum(self):
|
|
670
|
+
return self._run_fault_handler(
|
|
671
|
+
self.config.get_fault_handler(ConditionCode.FILE_CHECKSUM_FAILURE)
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
def _fault_ack_limit(self):
|
|
675
|
+
return self._run_fault_handler(
|
|
676
|
+
self.config.get_fault_handler(ConditionCode.POSITIVE_ACK_LIMIT_REACHED)
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
def _fault_nak_limit(self):
|
|
680
|
+
return self._run_fault_handler(
|
|
681
|
+
self.config.get_fault_handler(ConditionCode.NAK_LIMIT_REACHED)
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
def _run_fault_handler(self, fault_handler):
|
|
685
|
+
"""Return True if execution should continue (IGNORE), False otherwise."""
|
|
686
|
+
if fault_handler == FaultHandlerAction.CANCEL:
|
|
687
|
+
self._emit(InternalEvent(EventType.E33_RECEIVED_CANCEL_REQUEST))
|
|
688
|
+
return False
|
|
689
|
+
elif fault_handler == FaultHandlerAction.SUSPEND:
|
|
690
|
+
self._emit(InternalEvent(EventType.E31_RECEIVED_SUSPEND_REQUEST))
|
|
691
|
+
return False
|
|
692
|
+
elif fault_handler == FaultHandlerAction.IGNORE:
|
|
693
|
+
return True
|
|
694
|
+
elif fault_handler == FaultHandlerAction.ABANDON:
|
|
695
|
+
self._emit(InternalEvent(EventType.E2_ABANDON_TRANSACTION))
|
|
696
|
+
return False
|
|
697
|
+
else:
|
|
698
|
+
raise ValueError
|
|
699
|
+
|
|
700
|
+
# -- file lifecycle effects -------------------------------------------- #
|
|
701
|
+
|
|
702
|
+
def _open_tempfile(self):
|
|
703
|
+
self._emit(OpenTemp())
|
|
704
|
+
self._file_open = True
|
|
705
|
+
|
|
706
|
+
def _store_file_data(self, pdu):
|
|
707
|
+
self._emit(WriteSegment(offset=pdu.segment_offset, data=bytes(pdu.file_data)))
|
|
708
|
+
self.received_file_segments[pdu.segment_offset] = len(pdu.file_data)
|
|
709
|
+
|
|
710
|
+
def _copy_tempfile_to_destfile(self):
|
|
711
|
+
# Carry the machine-state destination filename: on a lost-then-recovered
|
|
712
|
+
# Metadata (F2 seq 1/6) the frozen transaction has no filename, only the
|
|
713
|
+
# machine does (updated from the late Metadata).
|
|
714
|
+
self._emit(Finalize(destination_filename=self.destination_filename))
|
|
715
|
+
|
|
716
|
+
def _close_tempfile(self):
|
|
717
|
+
# Mirror Machine.close_tempfile: it does NOT clear the handle, so a
|
|
718
|
+
# subsequent shutdown() closes it a second time.
|
|
719
|
+
self._emit(CloseFile())
|
|
720
|
+
|
|
721
|
+
def _update_received_file_size(self, pdu):
|
|
722
|
+
new_size = pdu.segment_offset + len(pdu.file_data)
|
|
723
|
+
if self.received_file_size < new_size:
|
|
724
|
+
self.received_file_size = new_size
|
|
725
|
+
self._progress = new_size
|
|
726
|
+
|
|
727
|
+
def _absorb_metadata(self, pdu):
|
|
728
|
+
"""Absorb a received Metadata PDU into machine state.
|
|
729
|
+
|
|
730
|
+
Shared by the ``WAIT_FOR_EOF`` and ``GET_MISSING_DATA`` E10 handlers so
|
|
731
|
+
they cannot drift (issue #26). Critically it sets ``source_filename`` /
|
|
732
|
+
``destination_filename`` / ``checksum_type`` from the PDU — when the
|
|
733
|
+
receiver was created from a filename-less Filedata/EOF (Metadata lost or
|
|
734
|
+
reordered) the frozen transaction carries none of these, so without this
|
|
735
|
+
the destination filename stays ``None`` (finalize goes nowhere) and the
|
|
736
|
+
checksum type stays the default MODULAR (spurious FILE_CHECKSUM_FAILURE).
|
|
737
|
+
The filenames/messages are set before the metadata-received indication so
|
|
738
|
+
it reports the resolved values.
|
|
739
|
+
"""
|
|
740
|
+
self.source_filename = pdu.source_filename
|
|
741
|
+
self.destination_filename = pdu.destination_filename
|
|
742
|
+
# ``messages_to_user`` is read by the metadata-received indication below;
|
|
743
|
+
# ``filestore_requests`` is (re)set by _process_metadata_options before
|
|
744
|
+
# any read, so it is not assigned here.
|
|
745
|
+
self.messages_to_user = pdu.messages_to_user
|
|
746
|
+
self.checksum_type = pdu.checksum_type
|
|
747
|
+
|
|
748
|
+
self.metadata_received = True
|
|
749
|
+
self.file_size = pdu.file_size
|
|
750
|
+
self._issue_metadata_received_indication(pdu.file_size)
|
|
751
|
+
|
|
752
|
+
if self._is_file_transfer():
|
|
753
|
+
if not self._file_open:
|
|
754
|
+
self._open_tempfile()
|
|
755
|
+
self._process_metadata_options(pdu)
|
|
756
|
+
|
|
757
|
+
def _process_metadata_options(self, pdu):
|
|
758
|
+
self.filestore_requests = pdu.filestore_requests
|
|
759
|
+
self.messages_to_user = pdu.messages_to_user
|
|
760
|
+
for message in self.messages_to_user or []:
|
|
761
|
+
message.originating_transaction = self.transaction
|
|
762
|
+
self._emit(PostUserMessage(message=message))
|
|
763
|
+
|
|
764
|
+
def _execute_filestore_requests(self):
|
|
765
|
+
# Filestore-request execution is a shell effect (design D3/D10): the
|
|
766
|
+
# machine carries the request TLVs but never touches the filestore. The
|
|
767
|
+
# old machine calls ``execute_filestore_requests`` directly here; this
|
|
768
|
+
# machine emits the equivalent command for the EffectExecutor to run.
|
|
769
|
+
self._emit(ExecuteFilestoreRequests(requests=self.filestore_requests))
|
|
770
|
+
|
|
771
|
+
def _shutdown(self):
|
|
772
|
+
# Mirror Receiver2.shutdown: base close (handle only if opened), then
|
|
773
|
+
# all three timers' shutdown() -> CancelTimer, always.
|
|
774
|
+
if self._file_open:
|
|
775
|
+
self._emit(CloseFile())
|
|
776
|
+
self._file_open = False
|
|
777
|
+
self._emit(CancelTimer(kind=TimerKind.INACTIVITY))
|
|
778
|
+
self._emit(CancelTimer(kind=TimerKind.ACK))
|
|
779
|
+
self._emit(CancelTimer(kind=TimerKind.NAK))
|
|
780
|
+
self.state = MachineState.COMPLETED
|
|
781
|
+
|
|
782
|
+
# -- reply-PDU header reuse (CCSDS 5.6.1 l), pure copy) ---------------- #
|
|
783
|
+
|
|
784
|
+
def _reuse_senders_first_pdu_header(self, pdu):
|
|
785
|
+
if not self.pdu_received:
|
|
786
|
+
self.pdu_received = True
|
|
787
|
+
h = pdu.pdu_header
|
|
788
|
+
if h.direction == Direction.TOWARD_RECEIVER:
|
|
789
|
+
direction = Direction.TOWARD_SENDER
|
|
790
|
+
else:
|
|
791
|
+
direction = Direction.TOWARD_RECEIVER
|
|
792
|
+
self._first_header = {
|
|
793
|
+
"direction": direction,
|
|
794
|
+
"transmission_mode": h.transmission_mode,
|
|
795
|
+
"crc_flag": h.crc_flag,
|
|
796
|
+
"large_file_flag": h.large_file_flag,
|
|
797
|
+
"segmentation_control": h.segmentation_control,
|
|
798
|
+
"length_of_entity_ids": h.length_of_entity_ids,
|
|
799
|
+
"segmentation_metadata_flag": h.segmentation_metadata_flag,
|
|
800
|
+
"length_of_seq_number": h.length_of_seq_number,
|
|
801
|
+
"source_entity_id": h.source_entity_id,
|
|
802
|
+
"transaction_seq_number": h.transaction_seq_number,
|
|
803
|
+
"destination_entity_id": h.destination_entity_id,
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
# -- reply PDUs (pure builders) ---------------------------------------- #
|
|
807
|
+
|
|
808
|
+
def _send_nak(self):
|
|
809
|
+
# end_of_scope must cover every segment request. When the tail of the
|
|
810
|
+
# file is missing (up to the whole file when Metadata + all Filedata are
|
|
811
|
+
# lost) the nak_list carries a (pointer, file_size) request whose end is
|
|
812
|
+
# the EOF-declared file_size, which exceeds received_file_size. Scope to
|
|
813
|
+
# the known file_size so the NAK's scope encloses its own requests (a
|
|
814
|
+
# strict peer validates requests lie within [start_of_scope, end_of_scope]);
|
|
815
|
+
# fall back to received_file_size before EOF sets file_size.
|
|
816
|
+
end_of_scope = (
|
|
817
|
+
self.file_size if self.file_size is not None else self.received_file_size
|
|
818
|
+
)
|
|
819
|
+
pdu = NakPdu(
|
|
820
|
+
start_of_scope=0,
|
|
821
|
+
end_of_scope=end_of_scope,
|
|
822
|
+
segment_requests=self._get_segment_requests(),
|
|
823
|
+
**self._first_header,
|
|
824
|
+
)
|
|
825
|
+
self._emit(SendPdu(pdu=pdu))
|
|
826
|
+
|
|
827
|
+
def _send_ack_eof(self):
|
|
828
|
+
pdu = AckPdu(
|
|
829
|
+
directive_code_ack=DirectiveCode.EOF,
|
|
830
|
+
directive_subtype_code=DirectiveSubTypeCode.ACK_OTHERS,
|
|
831
|
+
condition_code=self.condition_code,
|
|
832
|
+
transaction_status=TransactionStatus.UNDEFINED,
|
|
833
|
+
**self._first_header,
|
|
834
|
+
)
|
|
835
|
+
self._emit(SendPdu(pdu=pdu))
|
|
836
|
+
|
|
837
|
+
def _send_finished(self):
|
|
838
|
+
pdu = FinishedPdu(
|
|
839
|
+
condition_code=self.condition_code,
|
|
840
|
+
delivery_code=self.delivery_code,
|
|
841
|
+
file_status=self.file_status,
|
|
842
|
+
filestore_responses=self.filestore_responses,
|
|
843
|
+
fault_location=None,
|
|
844
|
+
**self._first_header,
|
|
845
|
+
)
|
|
846
|
+
self._emit(SendPdu(pdu=pdu))
|
|
847
|
+
|
|
848
|
+
# -- indications ------------------------------------------------------- #
|
|
849
|
+
|
|
850
|
+
def _issue_metadata_received_indication(self, file_size):
|
|
851
|
+
self._emit(
|
|
852
|
+
indications.metadata_received(
|
|
853
|
+
self.transaction.id,
|
|
854
|
+
self.transaction.source_entity_id,
|
|
855
|
+
file_size,
|
|
856
|
+
self.source_filename,
|
|
857
|
+
self.destination_filename,
|
|
858
|
+
self.messages_to_user,
|
|
859
|
+
)
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
def _issue_filesegment_received_indication(self, offset, length):
|
|
863
|
+
self._emit(
|
|
864
|
+
indications.filesegment_received(self.transaction.id, offset, length)
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
def _issue_transaction_finished_indication(self):
|
|
868
|
+
self._emit(
|
|
869
|
+
indications.transaction_finished(
|
|
870
|
+
self.transaction.id,
|
|
871
|
+
self.condition_code,
|
|
872
|
+
self.file_status,
|
|
873
|
+
self.delivery_code,
|
|
874
|
+
self.filestore_responses,
|
|
875
|
+
self.status_report,
|
|
876
|
+
)
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
def _issue_suspended_indication(self):
|
|
880
|
+
self._emit(indications.suspended(self.transaction.id, self.condition_code))
|
|
881
|
+
|
|
882
|
+
def _issue_resumed_indication(self):
|
|
883
|
+
self._emit(indications.resumed(self.transaction.id, self._progress))
|
|
884
|
+
|
|
885
|
+
def _issue_report_indication(self):
|
|
886
|
+
self._emit(indications.report(self.transaction.id, self.status_report))
|
|
887
|
+
|
|
888
|
+
def _issue_abandoned_indication(self):
|
|
889
|
+
self._emit(
|
|
890
|
+
indications.abandoned(
|
|
891
|
+
self.transaction.id, self.condition_code, self._progress
|
|
892
|
+
)
|
|
893
|
+
)
|