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
cfdp/core.py
ADDED
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import queue
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from . import logger
|
|
9
|
+
from .constants import (
|
|
10
|
+
DEFAULT_ACK_TIMER_EXPIRATION_LIMIT,
|
|
11
|
+
DEFAULT_ACK_TIMER_LIMIT,
|
|
12
|
+
DEFAULT_FAULT_HANDLERS,
|
|
13
|
+
DEFAULT_INACTIVITY_TIMEOUT,
|
|
14
|
+
DEFAULT_MAX_FILE_SEGMENT_LEN,
|
|
15
|
+
DEFAULT_NAK_TIMER_EXPIRATION_LIMIT,
|
|
16
|
+
DEFAULT_NAK_TIMER_INTERVAL,
|
|
17
|
+
ChecksumType,
|
|
18
|
+
ConditionCode,
|
|
19
|
+
Direction,
|
|
20
|
+
DirectiveCode,
|
|
21
|
+
DirectiveSubTypeCode,
|
|
22
|
+
MachineState,
|
|
23
|
+
PduTypeCode,
|
|
24
|
+
TransactionStatus,
|
|
25
|
+
TransmissionMode,
|
|
26
|
+
)
|
|
27
|
+
from .event import Event, EventType
|
|
28
|
+
from .meta import process_user_message as meta_process_user_message
|
|
29
|
+
from .pdu import (
|
|
30
|
+
AckPdu,
|
|
31
|
+
EofPdu,
|
|
32
|
+
FiledataPdu,
|
|
33
|
+
FinishedPdu,
|
|
34
|
+
KeepAlivePdu,
|
|
35
|
+
MetadataPdu,
|
|
36
|
+
NakPdu,
|
|
37
|
+
PduHeader,
|
|
38
|
+
PromptPdu,
|
|
39
|
+
)
|
|
40
|
+
from .pure.transaction import TransactionId
|
|
41
|
+
from .shell import Dispatcher, EffectExecutor, FileTable, MachineRegistry
|
|
42
|
+
from .shell.checksum_service import ChecksumService
|
|
43
|
+
from .shell.timer_service import TimerService
|
|
44
|
+
from .transaction_handle import TransactionHandle
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from .filestore.base import VirtualFileStore
|
|
48
|
+
from .transport.base import Transport
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CfdpEntity:
|
|
52
|
+
"""The public CFDP entity — the one object callers construct and drive.
|
|
53
|
+
|
|
54
|
+
A thin facade over the functional-core / imperative-shell stack (design
|
|
55
|
+
``docs/design/machine-decoupling.md``): it holds the shell collaborators
|
|
56
|
+
(``Dispatcher``, ``MachineRegistry``, ``EffectExecutor``, ``TimerService``,
|
|
57
|
+
``ChecksumService``, ``FileTable``) and exposes the user-facing verbs
|
|
58
|
+
(``put``, ``cancel``, ``suspend``, ``resume``, ``report``, ``freeze``,
|
|
59
|
+
``thaw``, ``start``/``stop``). One entity serves both roles (sender and
|
|
60
|
+
receiver) for many concurrent transactions.
|
|
61
|
+
|
|
62
|
+
``self.machines`` is the live ``tid -> machine`` map, keyed by transaction
|
|
63
|
+
id — a ``(source_entity_id, seq_number)`` tuple; the ``MachineRegistry``
|
|
64
|
+
shares this same dict.
|
|
65
|
+
|
|
66
|
+
Constructor knobs:
|
|
67
|
+
|
|
68
|
+
* ``entity_id`` — this entity's CFDP id (protocol-opaque).
|
|
69
|
+
* ``filestore`` — the :class:`~cfdp.filestore.base.VirtualFileStore` backing
|
|
70
|
+
file reads/writes.
|
|
71
|
+
* ``transport`` — the :class:`~cfdp.transport.base.Transport` used to send
|
|
72
|
+
and receive PDUs.
|
|
73
|
+
* ``*_indication_required`` — per-indication opt-in flags controlling which
|
|
74
|
+
optional indications fire.
|
|
75
|
+
* ``default_fault_handlers`` — condition-code -> action map applied when a
|
|
76
|
+
remote/put config does not override a handler.
|
|
77
|
+
* ``transaction_inactivity_limit`` — seconds of silence before the
|
|
78
|
+
inactivity timer faults a transaction.
|
|
79
|
+
* ``positive_ack_timer_interval`` / ``positive_ack_timer_expiration_limit``
|
|
80
|
+
— ACK-timer period (seconds) and its expiration count before faulting.
|
|
81
|
+
* ``nak_timer_interval`` / ``nak_timer_expiration_limit`` — NAK-timer period
|
|
82
|
+
(seconds) and its expiration count before faulting.
|
|
83
|
+
* ``maximum_file_segment_length`` — max Filedata payload bytes per PDU.
|
|
84
|
+
* ``inter_pdu_delay`` — seconds to pace between outbound file-data segments
|
|
85
|
+
(0.0 = as fast as the transport accepts).
|
|
86
|
+
* ``completed_transaction_ttl`` — seconds a completed machine is retained
|
|
87
|
+
before reaping.
|
|
88
|
+
* ``remote_entity_configs`` — optional ``{entity_id: RemoteEntityConfig}``
|
|
89
|
+
per-peer overrides.
|
|
90
|
+
|
|
91
|
+
Every knob has a spec-derived default; only ``entity_id``, ``filestore``,
|
|
92
|
+
and ``transport`` are required.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# Minimum yield inserted before re-enqueuing the file-data pump when the
|
|
96
|
+
# transport is not writable, so an unready link idles rather than spinning
|
|
97
|
+
# (design R4 / issue #20). Read in trigger_event.
|
|
98
|
+
_BACKPRESSURE_YIELD = 0.01
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
entity_id: Any,
|
|
103
|
+
filestore: VirtualFileStore,
|
|
104
|
+
transport: Transport,
|
|
105
|
+
eof_sent_indication_required: bool = False,
|
|
106
|
+
eof_received_indication_required: bool = False,
|
|
107
|
+
filesegment_received_indication_required: bool = False,
|
|
108
|
+
transaction_finished_indication_required: bool = False,
|
|
109
|
+
suspended_indication_required: bool = False,
|
|
110
|
+
resumed_indication_required: bool = False,
|
|
111
|
+
default_fault_handlers: dict = DEFAULT_FAULT_HANDLERS,
|
|
112
|
+
transaction_inactivity_limit: float = DEFAULT_INACTIVITY_TIMEOUT,
|
|
113
|
+
positive_ack_timer_interval: float = DEFAULT_ACK_TIMER_LIMIT,
|
|
114
|
+
positive_ack_timer_expiration_limit: int = DEFAULT_ACK_TIMER_EXPIRATION_LIMIT,
|
|
115
|
+
nak_timer_interval: float = DEFAULT_NAK_TIMER_INTERVAL,
|
|
116
|
+
nak_timer_expiration_limit: int = DEFAULT_NAK_TIMER_EXPIRATION_LIMIT,
|
|
117
|
+
maximum_file_segment_length: int = DEFAULT_MAX_FILE_SEGMENT_LEN,
|
|
118
|
+
inter_pdu_delay: float = 0.0,
|
|
119
|
+
completed_transaction_ttl: float = 60.0,
|
|
120
|
+
remote_entity_configs: dict | None = None,
|
|
121
|
+
write_to_destination_directly: bool = False,
|
|
122
|
+
) -> None:
|
|
123
|
+
self.entity_id = entity_id
|
|
124
|
+
self.filestore = filestore
|
|
125
|
+
self.transport = transport
|
|
126
|
+
self.eof_sent_indication_required = eof_sent_indication_required
|
|
127
|
+
self.eof_received_indication_required = eof_received_indication_required
|
|
128
|
+
self.filesegment_received_indication_required = (
|
|
129
|
+
filesegment_received_indication_required
|
|
130
|
+
)
|
|
131
|
+
self.transaction_finished_indication_required = (
|
|
132
|
+
transaction_finished_indication_required
|
|
133
|
+
)
|
|
134
|
+
self.suspended_indication_required = suspended_indication_required
|
|
135
|
+
self.resumed_indication_required = resumed_indication_required
|
|
136
|
+
self.default_fault_handlers = default_fault_handlers
|
|
137
|
+
self.transaction_inactivity_limit = transaction_inactivity_limit
|
|
138
|
+
self.positive_ack_timer_interval = positive_ack_timer_interval
|
|
139
|
+
self.positive_ack_timer_expiration_limit = positive_ack_timer_expiration_limit
|
|
140
|
+
self.nak_timer_interval = nak_timer_interval
|
|
141
|
+
self.nak_timer_expiration_limit = nak_timer_expiration_limit
|
|
142
|
+
self.maximum_file_segment_length = maximum_file_segment_length
|
|
143
|
+
self.inter_pdu_delay = inter_pdu_delay
|
|
144
|
+
self.completed_transaction_ttl = completed_transaction_ttl
|
|
145
|
+
# Receive-side option: write incoming file data straight into the
|
|
146
|
+
# destination file instead of buffering to a temp file and copying it at
|
|
147
|
+
# Finished time. Lets a remote-service filestore show download progress.
|
|
148
|
+
# Interpreted entirely in the shell (EffectExecutor); the pure machines
|
|
149
|
+
# are unaware of it. Default False preserves the temp-file behaviour.
|
|
150
|
+
self.write_to_destination_directly = write_to_destination_directly
|
|
151
|
+
self._remote_entity_configs = remote_entity_configs or {}
|
|
152
|
+
self._transaction_seq_counter = 0
|
|
153
|
+
self._seq_lock = threading.Lock()
|
|
154
|
+
self.machines: dict[TransactionId, Any] = {}
|
|
155
|
+
self._completion_times: dict[TransactionId, float] = {}
|
|
156
|
+
|
|
157
|
+
self._event_queue: queue.Queue = queue.Queue()
|
|
158
|
+
# Typed Any, not Thread: start() attaches a dynamic ``.kill`` flag to the
|
|
159
|
+
# thread object that the handler loops poll, so these are not plain
|
|
160
|
+
# threading.Thread instances. Retyping that shutdown mechanism is out of
|
|
161
|
+
# scope for the annotation pass.
|
|
162
|
+
self._event_thread: Any = None
|
|
163
|
+
|
|
164
|
+
self._messages_to_user_queue: queue.Queue = queue.Queue()
|
|
165
|
+
self._message_thread: Any = None
|
|
166
|
+
|
|
167
|
+
self._started = False
|
|
168
|
+
self._transaction_inter_pdu_delays: dict[TransactionId, float] = {}
|
|
169
|
+
self._completion_events: dict[TransactionId, threading.Event] = {}
|
|
170
|
+
|
|
171
|
+
# Live file-data pacing timers (issue #31). The E1 re-enqueue paces via a
|
|
172
|
+
# ``threading.Timer``; untracked, one could fire after ``stop()`` and push
|
|
173
|
+
# onto a drained queue / leave an unjoined thread (tripping the suite's
|
|
174
|
+
# active-thread-count assertions). We track them so ``stop()`` can cancel
|
|
175
|
+
# and join every pending one. Guarded by its own lock: timers are armed on
|
|
176
|
+
# the event thread but fire (and self-remove) on their own timer threads.
|
|
177
|
+
self._pacing_timers: set[threading.Timer] = set()
|
|
178
|
+
self._pacing_lock = threading.Lock()
|
|
179
|
+
|
|
180
|
+
# Functional-core / imperative-shell stack (design
|
|
181
|
+
# docs/design/machine-decoupling.md; issues #16 / #20 / #22). Both
|
|
182
|
+
# Class-1 (UNACKNOWLEDGED) and Class-2 (ACKNOWLEDGED) transfers route
|
|
183
|
+
# through the pure machines + shell collaborators. The registry shares
|
|
184
|
+
# this entity's ``machines`` dict so every lookup path sees the machines
|
|
185
|
+
# uniformly.
|
|
186
|
+
self._file_table = FileTable()
|
|
187
|
+
# The wall-clock timer table (design D6 / issue #17). Owns real timers
|
|
188
|
+
# per (tid, kind); an expiry re-enters via trigger_event onto the shared
|
|
189
|
+
# FIFO (design D8). The executor drives it from StartTimer/CancelTimer/
|
|
190
|
+
# SuspendTimer/ResumeTimer effects.
|
|
191
|
+
self._timer_service = TimerService(self)
|
|
192
|
+
# The async-checksum worker (design R1 / issue #21). A RequestChecksum
|
|
193
|
+
# effect computes the whole-file checksum off the event thread and
|
|
194
|
+
# injects E42_CHECKSUM_READY onto the shared FIFO, so a large-file
|
|
195
|
+
# checksum no longer stalls every other transaction on the dispatcher.
|
|
196
|
+
self._checksum_service = ChecksumService(self, self._file_table)
|
|
197
|
+
self._effect_executor = EffectExecutor(
|
|
198
|
+
self, self._file_table, self._timer_service, self._checksum_service
|
|
199
|
+
)
|
|
200
|
+
self._machine_registry = MachineRegistry(
|
|
201
|
+
self, self._file_table, machines=self.machines
|
|
202
|
+
)
|
|
203
|
+
self._dispatcher = Dispatcher(
|
|
204
|
+
self, self._machine_registry, self._effect_executor
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Callback attributes — set to a callable to receive indications
|
|
208
|
+
self.on_transaction_finished = None
|
|
209
|
+
self.on_metadata_received = None
|
|
210
|
+
self.on_eof_sent = None
|
|
211
|
+
self.on_eof_received = None
|
|
212
|
+
self.on_filesegment_received = None
|
|
213
|
+
self.on_suspended = None
|
|
214
|
+
self.on_resumed = None
|
|
215
|
+
self.on_report = None
|
|
216
|
+
self.on_fault = None
|
|
217
|
+
self.on_abandoned = None
|
|
218
|
+
self.on_unrecognized_message = None
|
|
219
|
+
|
|
220
|
+
def start(self) -> None:
|
|
221
|
+
"""Begin the event thread, message thread, and transport receive loop."""
|
|
222
|
+
if self._started:
|
|
223
|
+
return
|
|
224
|
+
self._started = True
|
|
225
|
+
self._event_thread = threading.Thread(target=self._event_handler, daemon=True)
|
|
226
|
+
self._event_thread.kill = False
|
|
227
|
+
self._message_thread = threading.Thread(
|
|
228
|
+
target=self._message_handler, daemon=True
|
|
229
|
+
)
|
|
230
|
+
self._message_thread.kill = False
|
|
231
|
+
self._event_thread.start()
|
|
232
|
+
self._message_thread.start()
|
|
233
|
+
self.transport.start(self.pdu_received)
|
|
234
|
+
|
|
235
|
+
def stop(self) -> None:
|
|
236
|
+
"""Set the kill flag, send sentinels, and join both threads."""
|
|
237
|
+
if not self._started:
|
|
238
|
+
return
|
|
239
|
+
self._started = False
|
|
240
|
+
self._event_thread.kill = True
|
|
241
|
+
self._message_thread.kill = True
|
|
242
|
+
self._event_queue.put(("stop",))
|
|
243
|
+
self._messages_to_user_queue.put(0)
|
|
244
|
+
self._event_thread.join()
|
|
245
|
+
self._message_thread.join()
|
|
246
|
+
# Cancel and join every pending file-data pacing timer (issue #31) so
|
|
247
|
+
# none fires after shutdown to push onto the drained queue or leave an
|
|
248
|
+
# unjoined thread. Done after the event thread is joined, so no new
|
|
249
|
+
# pacing timer can be armed here.
|
|
250
|
+
with self._pacing_lock:
|
|
251
|
+
pending = list(self._pacing_timers)
|
|
252
|
+
self._pacing_timers.clear()
|
|
253
|
+
for timer in pending:
|
|
254
|
+
timer.cancel()
|
|
255
|
+
timer.join(timeout=1)
|
|
256
|
+
# Cancel every outstanding wall-clock timer so no timer thread leaks
|
|
257
|
+
# past shutdown (design D6 / issue #17). Done after the event thread is
|
|
258
|
+
# joined so no start/cancel can race an in-flight timer here.
|
|
259
|
+
self._timer_service.stop()
|
|
260
|
+
# Join any in-flight checksum worker so none leaks past shutdown
|
|
261
|
+
# (design R1 / issue #21). After the event thread is joined no new
|
|
262
|
+
# RequestChecksum can be issued, so this drains the last workers.
|
|
263
|
+
self._checksum_service.stop()
|
|
264
|
+
|
|
265
|
+
def _increment_transaction_seq_counter(self):
|
|
266
|
+
with self._seq_lock:
|
|
267
|
+
self._transaction_seq_counter += 1
|
|
268
|
+
return self._transaction_seq_counter
|
|
269
|
+
|
|
270
|
+
def _invoke_callback(self, cb, *args):
|
|
271
|
+
if cb is None:
|
|
272
|
+
return
|
|
273
|
+
try:
|
|
274
|
+
cb(*args)
|
|
275
|
+
except Exception:
|
|
276
|
+
logger.exception("Unhandled exception in callback")
|
|
277
|
+
|
|
278
|
+
def trigger_event(self, event, pdu=None):
|
|
279
|
+
if event.type == EventType.E1_SEND_FILE_DATA:
|
|
280
|
+
delay = self._transaction_inter_pdu_delays.get(
|
|
281
|
+
event.transaction.id, self.inter_pdu_delay
|
|
282
|
+
)
|
|
283
|
+
# Backpressure-aware rescheduling (design R4 / issue #20). The pure
|
|
284
|
+
# file-data pump re-emits E1_SEND_FILE_DATA every step even when the
|
|
285
|
+
# transport could not accept a segment. Left
|
|
286
|
+
# unchecked, an unwritable link would spin the event thread at 100%
|
|
287
|
+
# CPU re-servicing E1s that send nothing. Here — the one place that
|
|
288
|
+
# owns the E1 re-enqueue timing — we detect an unready transport and
|
|
289
|
+
# apply a minimum yield so the pump idles instead of busy-spinning.
|
|
290
|
+
# FIFO re-enqueue already gives inter-transaction fairness (design
|
|
291
|
+
# D8); this only fixes the CPU spin.
|
|
292
|
+
if delay <= 0 and not self._transport_ready():
|
|
293
|
+
delay = self._BACKPRESSURE_YIELD
|
|
294
|
+
if delay > 0:
|
|
295
|
+
self._schedule_paced_enqueue(delay, [event, pdu])
|
|
296
|
+
return
|
|
297
|
+
self._event_queue.put([event, pdu])
|
|
298
|
+
|
|
299
|
+
def _schedule_paced_enqueue(self, delay, item):
|
|
300
|
+
"""Enqueue ``item`` after ``delay`` on a tracked timer (issue #31).
|
|
301
|
+
|
|
302
|
+
The timer is registered before it starts and self-removes when it fires,
|
|
303
|
+
so a pending pacing timer is visible to ``stop()`` (which cancels and
|
|
304
|
+
joins every one) and a timer that fires normally does not accumulate.
|
|
305
|
+
"""
|
|
306
|
+
holder = {}
|
|
307
|
+
|
|
308
|
+
def _fire():
|
|
309
|
+
with self._pacing_lock:
|
|
310
|
+
self._pacing_timers.discard(holder["timer"])
|
|
311
|
+
self._event_queue.put(item)
|
|
312
|
+
|
|
313
|
+
timer = threading.Timer(delay, _fire)
|
|
314
|
+
holder["timer"] = timer
|
|
315
|
+
with self._pacing_lock:
|
|
316
|
+
self._pacing_timers.add(timer)
|
|
317
|
+
timer.start()
|
|
318
|
+
|
|
319
|
+
def _transport_ready(self):
|
|
320
|
+
"""Best-effort local transport writability check (never raises)."""
|
|
321
|
+
is_ready = getattr(self.transport, "is_ready", None)
|
|
322
|
+
if is_ready is None:
|
|
323
|
+
return True
|
|
324
|
+
try:
|
|
325
|
+
return is_ready()
|
|
326
|
+
except Exception:
|
|
327
|
+
return True
|
|
328
|
+
|
|
329
|
+
def _get_remote_config(self, destination_entity_id):
|
|
330
|
+
return self._remote_entity_configs.get(destination_entity_id)
|
|
331
|
+
|
|
332
|
+
def _resolve_id(self, transaction_id):
|
|
333
|
+
if isinstance(transaction_id, TransactionHandle):
|
|
334
|
+
return transaction_id.id
|
|
335
|
+
return transaction_id
|
|
336
|
+
|
|
337
|
+
def is_complete(self, transaction_id) -> bool:
|
|
338
|
+
tid = self._resolve_id(transaction_id)
|
|
339
|
+
evt = self._completion_events.get(tid)
|
|
340
|
+
if evt is None:
|
|
341
|
+
# No record of this transaction — treat as complete
|
|
342
|
+
return True
|
|
343
|
+
return evt.is_set()
|
|
344
|
+
|
|
345
|
+
def _reap_expired_machines(self):
|
|
346
|
+
now = time.monotonic()
|
|
347
|
+
expired = [
|
|
348
|
+
tid
|
|
349
|
+
for tid, ts in list(self._completion_times.items())
|
|
350
|
+
if now - ts >= self.completed_transaction_ttl
|
|
351
|
+
]
|
|
352
|
+
for tid in expired:
|
|
353
|
+
self.machines.pop(tid, None)
|
|
354
|
+
self._completion_times.pop(tid, None)
|
|
355
|
+
self._transaction_inter_pdu_delays.pop(tid, None)
|
|
356
|
+
self._completion_events.pop(tid, None)
|
|
357
|
+
# Close and forget any file handle the shell held for a reaped
|
|
358
|
+
# transaction (design D10). No-op for a transaction that never opened
|
|
359
|
+
# a file.
|
|
360
|
+
self._file_table.cleanup(tid)
|
|
361
|
+
# Drop any outstanding wall-clock timers for the reaped transaction
|
|
362
|
+
# so no timer thread outlives it (design D6 / issue #17).
|
|
363
|
+
self._timer_service.cancel_all(tid)
|
|
364
|
+
|
|
365
|
+
def _event_handler(self):
|
|
366
|
+
thread = threading.current_thread()
|
|
367
|
+
while not thread.kill:
|
|
368
|
+
item = None
|
|
369
|
+
try:
|
|
370
|
+
item = self._event_queue.get(timeout=1)
|
|
371
|
+
except queue.Empty:
|
|
372
|
+
self._reap_expired_machines()
|
|
373
|
+
continue
|
|
374
|
+
if thread.kill:
|
|
375
|
+
break
|
|
376
|
+
try:
|
|
377
|
+
if isinstance(item, tuple):
|
|
378
|
+
self._handle_control_message(item)
|
|
379
|
+
else:
|
|
380
|
+
# Normal [event, pdu] from trigger_event() or timers
|
|
381
|
+
event, pdu = item
|
|
382
|
+
self._dispatch_event(event, pdu)
|
|
383
|
+
except Exception:
|
|
384
|
+
logger.exception("Unhandled exception in event handler")
|
|
385
|
+
self._reap_expired_machines()
|
|
386
|
+
|
|
387
|
+
def _dispatch_event(self, event, pdu):
|
|
388
|
+
machine = self.machines.get(event.transaction.id)
|
|
389
|
+
if not machine:
|
|
390
|
+
return
|
|
391
|
+
# The pure machine has a single input door and reads any inbound PDU off
|
|
392
|
+
# ``event.pdu``. The Dispatcher walks its returned output list (execute
|
|
393
|
+
# effects inline, re-enqueue internal events) and reports completion via
|
|
394
|
+
# _on_pure_machine_complete.
|
|
395
|
+
event.pdu = pdu
|
|
396
|
+
self._dispatcher.dispatch(machine, event)
|
|
397
|
+
|
|
398
|
+
def _on_pure_machine_complete(self, tid):
|
|
399
|
+
"""Mark a pure-machine transaction complete (called by the Dispatcher)."""
|
|
400
|
+
if tid not in self._completion_times:
|
|
401
|
+
self._completion_times[tid] = time.monotonic()
|
|
402
|
+
# Cancel any armed timers immediately on completion rather than waiting
|
|
403
|
+
# for the TTL reap: an ACK/NAK/inactivity timer that fires into an
|
|
404
|
+
# already-completed machine would inject a stale event. ``cancel_all``
|
|
405
|
+
# on an already-empty tid is a safe no-op, so this never double-cancels
|
|
406
|
+
# harmfully with the reap path (which also calls ``cancel_all``).
|
|
407
|
+
self._timer_service.cancel_all(tid)
|
|
408
|
+
ce = self._completion_events.get(tid)
|
|
409
|
+
if ce is not None:
|
|
410
|
+
ce.set()
|
|
411
|
+
|
|
412
|
+
def _handle_control_message(self, item):
|
|
413
|
+
tag = item[0]
|
|
414
|
+
|
|
415
|
+
if tag == "stop":
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
elif tag == "pdu_raw":
|
|
419
|
+
_, data = item
|
|
420
|
+
try:
|
|
421
|
+
pdu = self._get_pdu_object(data)
|
|
422
|
+
except Exception:
|
|
423
|
+
logger.exception("Failed to decode incoming PDU")
|
|
424
|
+
return
|
|
425
|
+
mode = pdu.pdu_header.transmission_mode
|
|
426
|
+
direction = pdu.pdu_header.direction
|
|
427
|
+
transaction_id = pdu.pdu_header.transaction_id
|
|
428
|
+
existing = self.machines.get(transaction_id)
|
|
429
|
+
if existing and existing.transmission_mode == mode:
|
|
430
|
+
if existing.state is not MachineState.COMPLETED:
|
|
431
|
+
self._send_pdu_to_state_machine(existing, pdu)
|
|
432
|
+
else:
|
|
433
|
+
del self.machines[transaction_id]
|
|
434
|
+
self._completion_times.pop(transaction_id, None)
|
|
435
|
+
ce = self._completion_events.pop(transaction_id, None)
|
|
436
|
+
if ce is not None:
|
|
437
|
+
ce.set()
|
|
438
|
+
self._run_action_for_incoming_pdu(
|
|
439
|
+
mode, direction, transaction_id, pdu
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
self._run_action_for_incoming_pdu(mode, direction, transaction_id, pdu)
|
|
443
|
+
|
|
444
|
+
# If handling this PDU left no live machine owning the transaction,
|
|
445
|
+
# nothing will ever drive it to completion. ``pdu_received`` pre-
|
|
446
|
+
# registers a completion event for every inbound PDU (so is_complete
|
|
447
|
+
# is False while it is queued), but a *stateless* response — e.g.
|
|
448
|
+
# Ack-ing a Finished-cancel that a peer legitimately retransmits after
|
|
449
|
+
# we already completed and reaped (issue #23) — would otherwise leave
|
|
450
|
+
# that freshly-created event unset forever, flipping is_complete back
|
|
451
|
+
# to False. Mark it complete: no owner means done.
|
|
452
|
+
if transaction_id not in self.machines:
|
|
453
|
+
ce = self._completion_events.get(transaction_id)
|
|
454
|
+
if ce is not None:
|
|
455
|
+
ce.set()
|
|
456
|
+
|
|
457
|
+
elif tag == "cancel_tid":
|
|
458
|
+
_, tid = item
|
|
459
|
+
machine = self.machines.get(tid)
|
|
460
|
+
if machine:
|
|
461
|
+
self._dispatch_event(
|
|
462
|
+
Event(machine.transaction, EventType.E33_RECEIVED_CANCEL_REQUEST),
|
|
463
|
+
None,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
elif tag == "suspend_tid":
|
|
467
|
+
_, tid = item
|
|
468
|
+
machine = self.machines.get(tid)
|
|
469
|
+
if machine:
|
|
470
|
+
self._dispatch_event(
|
|
471
|
+
Event(machine.transaction, EventType.E31_RECEIVED_SUSPEND_REQUEST),
|
|
472
|
+
None,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
elif tag == "resume_tid":
|
|
476
|
+
_, tid = item
|
|
477
|
+
machine = self.machines.get(tid)
|
|
478
|
+
if machine:
|
|
479
|
+
self._dispatch_event(
|
|
480
|
+
Event(machine.transaction, EventType.E32_RECEIVED_RESUME_REQUEST),
|
|
481
|
+
None,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
elif tag == "report_tid":
|
|
485
|
+
_, tid = item
|
|
486
|
+
machine = self.machines.get(tid)
|
|
487
|
+
if machine:
|
|
488
|
+
self._dispatch_event(
|
|
489
|
+
Event(machine.transaction, EventType.E34_RECEIVED_REPORT_REQUEST),
|
|
490
|
+
None,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
elif tag == "freeze_entity":
|
|
494
|
+
_, receiving_entity_id = item
|
|
495
|
+
for transaction_id, machine in list(self.machines.items()):
|
|
496
|
+
if transaction_id[0] == self.entity_id:
|
|
497
|
+
if machine.transaction.destination_entity_id == receiving_entity_id:
|
|
498
|
+
self._dispatch_event(
|
|
499
|
+
Event(machine.transaction, EventType.E40_RECEIVED_FREEZE),
|
|
500
|
+
None,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
elif tag == "thaw_entity":
|
|
504
|
+
_, receiving_entity_id = item
|
|
505
|
+
for transaction_id, machine in list(self.machines.items()):
|
|
506
|
+
if transaction_id[0] == self.entity_id:
|
|
507
|
+
if machine.transaction.destination_entity_id == receiving_entity_id:
|
|
508
|
+
self._dispatch_event(
|
|
509
|
+
Event(machine.transaction, EventType.E41_RECEIVED_THAW),
|
|
510
|
+
None,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
def _message_handler(self):
|
|
514
|
+
thread = threading.current_thread()
|
|
515
|
+
while not thread.kill:
|
|
516
|
+
try:
|
|
517
|
+
message = self._messages_to_user_queue.get(timeout=1)
|
|
518
|
+
if thread.kill:
|
|
519
|
+
break
|
|
520
|
+
logger.info("Processing user message %s" % str(message))
|
|
521
|
+
self.process_user_message(message)
|
|
522
|
+
except queue.Empty:
|
|
523
|
+
pass
|
|
524
|
+
except Exception:
|
|
525
|
+
logger.exception("Unhandled exception in message handler")
|
|
526
|
+
|
|
527
|
+
""" CFDP service requests """
|
|
528
|
+
|
|
529
|
+
def put(
|
|
530
|
+
self,
|
|
531
|
+
destination_id,
|
|
532
|
+
source_filename=None,
|
|
533
|
+
destination_filename=None,
|
|
534
|
+
segmentation_control=None, # not implemented
|
|
535
|
+
fault_handler_overrides=None,
|
|
536
|
+
flow_label=None, # not implemented
|
|
537
|
+
transmission_mode=TransmissionMode.ACKNOWLEDGED,
|
|
538
|
+
closure_requested=False, # not implemented
|
|
539
|
+
messages_to_user=None,
|
|
540
|
+
filestore_requests=None,
|
|
541
|
+
checksum_type=ChecksumType.MODULAR,
|
|
542
|
+
maximum_file_segment_length: int | None = None,
|
|
543
|
+
) -> TransactionHandle:
|
|
544
|
+
"""Initiate a file-delivery transaction.
|
|
545
|
+
|
|
546
|
+
Parameters
|
|
547
|
+
----------
|
|
548
|
+
destination_id : int
|
|
549
|
+
Entity ID of the receiving entity.
|
|
550
|
+
source_filename : str or None
|
|
551
|
+
VFS path of the file to send. ``None`` sends a metadata-only PDU.
|
|
552
|
+
destination_filename : str or None
|
|
553
|
+
VFS path at the receiver where the file will be written.
|
|
554
|
+
transmission_mode : TransmissionMode
|
|
555
|
+
``ACKNOWLEDGED`` (Class 2, default) or ``UNACKNOWLEDGED`` (Class 1).
|
|
556
|
+
checksum_type : ChecksumType
|
|
557
|
+
Checksum algorithm to use (default: MODULAR).
|
|
558
|
+
messages_to_user : list or None
|
|
559
|
+
Optional TLV messages to include in the Metadata PDU.
|
|
560
|
+
filestore_requests : list or None
|
|
561
|
+
Optional filestore-request TLVs.
|
|
562
|
+
maximum_file_segment_length : int or None
|
|
563
|
+
Override the entity-level segment length for this transaction.
|
|
564
|
+
"""
|
|
565
|
+
if closure_requested:
|
|
566
|
+
raise NotImplementedError("closure_requested is not yet implemented")
|
|
567
|
+
if flow_label:
|
|
568
|
+
raise NotImplementedError("flow_label is not yet implemented")
|
|
569
|
+
if segmentation_control:
|
|
570
|
+
raise NotImplementedError("segmentation_control is not yet implemented")
|
|
571
|
+
|
|
572
|
+
seq_number = self._increment_transaction_seq_counter()
|
|
573
|
+
|
|
574
|
+
remote_cfg = self._get_remote_config(destination_id)
|
|
575
|
+
effective_delay = (
|
|
576
|
+
remote_cfg.inter_pdu_delay
|
|
577
|
+
if remote_cfg is not None and remote_cfg.inter_pdu_delay is not None
|
|
578
|
+
else self.inter_pdu_delay
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# Build a pure sender via the registry (which folds config into a frozen
|
|
582
|
+
# snapshot and injects the query port). Both Class-1 (UNACKNOWLEDGED) and
|
|
583
|
+
# Class-2 (ACKNOWLEDGED) route here (issues #16 / #20).
|
|
584
|
+
resolved_segment_length = (
|
|
585
|
+
maximum_file_segment_length
|
|
586
|
+
if maximum_file_segment_length
|
|
587
|
+
else self.maximum_file_segment_length
|
|
588
|
+
)
|
|
589
|
+
machine, transaction = self._machine_registry.create_sender(
|
|
590
|
+
seq_number=seq_number,
|
|
591
|
+
destination_id=destination_id,
|
|
592
|
+
source_filename=source_filename,
|
|
593
|
+
destination_filename=destination_filename,
|
|
594
|
+
checksum_type=checksum_type,
|
|
595
|
+
transmission_mode=transmission_mode,
|
|
596
|
+
maximum_file_segment_length=resolved_segment_length,
|
|
597
|
+
messages_to_user=messages_to_user,
|
|
598
|
+
filestore_requests=filestore_requests,
|
|
599
|
+
fault_handler_overrides=fault_handler_overrides,
|
|
600
|
+
remote_config=remote_cfg,
|
|
601
|
+
)
|
|
602
|
+
self._completion_events[transaction.id] = threading.Event()
|
|
603
|
+
self._transaction_inter_pdu_delays[transaction.id] = effective_delay
|
|
604
|
+
self.trigger_event(Event(transaction, EventType.E0_ENTERED_STATE))
|
|
605
|
+
self.trigger_event(Event(transaction, EventType.E30_RECEIVED_PUT_REQUEST))
|
|
606
|
+
return TransactionHandle(self, transaction.id)
|
|
607
|
+
|
|
608
|
+
def cancel(self, transaction_id: Any) -> None:
|
|
609
|
+
self._event_queue.put(("cancel_tid", self._resolve_id(transaction_id)))
|
|
610
|
+
|
|
611
|
+
def suspend(self, transaction_id: Any) -> None:
|
|
612
|
+
self._event_queue.put(("suspend_tid", self._resolve_id(transaction_id)))
|
|
613
|
+
|
|
614
|
+
def resume(self, transaction_id: Any) -> None:
|
|
615
|
+
self._event_queue.put(("resume_tid", self._resolve_id(transaction_id)))
|
|
616
|
+
|
|
617
|
+
def report(self, transaction_id: Any) -> None:
|
|
618
|
+
self._event_queue.put(("report_tid", self._resolve_id(transaction_id)))
|
|
619
|
+
|
|
620
|
+
""" CFDP indication dispatch — called from machine base class """
|
|
621
|
+
|
|
622
|
+
def _as_handle(self, transaction_id):
|
|
623
|
+
if isinstance(transaction_id, TransactionHandle):
|
|
624
|
+
return transaction_id
|
|
625
|
+
return TransactionHandle(self, transaction_id)
|
|
626
|
+
|
|
627
|
+
def _indication_transaction(self, transaction_id):
|
|
628
|
+
logger.info("[%s] Transaction indication" % str(transaction_id))
|
|
629
|
+
|
|
630
|
+
def _indication_eof_sent(self, transaction_id):
|
|
631
|
+
logger.info("[%s] EOF sent indication" % str(transaction_id))
|
|
632
|
+
self._invoke_callback(self.on_eof_sent, self._as_handle(transaction_id))
|
|
633
|
+
|
|
634
|
+
def _indication_transaction_finished(
|
|
635
|
+
self,
|
|
636
|
+
transaction_id,
|
|
637
|
+
condition_code,
|
|
638
|
+
file_status,
|
|
639
|
+
delivery_code,
|
|
640
|
+
filestore_responses=None,
|
|
641
|
+
status_report=None,
|
|
642
|
+
):
|
|
643
|
+
logger.info("[%s] Transaction finished indication" % str(transaction_id))
|
|
644
|
+
self._invoke_callback(
|
|
645
|
+
self.on_transaction_finished,
|
|
646
|
+
self._as_handle(transaction_id),
|
|
647
|
+
condition_code,
|
|
648
|
+
file_status,
|
|
649
|
+
delivery_code,
|
|
650
|
+
filestore_responses,
|
|
651
|
+
status_report,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
def _indication_metadata_received(
|
|
655
|
+
self,
|
|
656
|
+
transaction_id,
|
|
657
|
+
source_entity_id,
|
|
658
|
+
file_size=None,
|
|
659
|
+
source_filename=None,
|
|
660
|
+
destination_filename=None,
|
|
661
|
+
messages_to_user=None,
|
|
662
|
+
):
|
|
663
|
+
logger.info(
|
|
664
|
+
"[%s] Metadata received indication (%s bytes)"
|
|
665
|
+
% (str(transaction_id), str(file_size))
|
|
666
|
+
)
|
|
667
|
+
self._invoke_callback(
|
|
668
|
+
self.on_metadata_received,
|
|
669
|
+
self._as_handle(transaction_id),
|
|
670
|
+
source_entity_id,
|
|
671
|
+
file_size,
|
|
672
|
+
source_filename,
|
|
673
|
+
destination_filename,
|
|
674
|
+
messages_to_user,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
def _indication_filesegment_received(self, transaction_id, offset, length):
|
|
678
|
+
logger.info(
|
|
679
|
+
"[%s] Filesegment received indication (offset %s, length %s)"
|
|
680
|
+
% (transaction_id, offset, length)
|
|
681
|
+
)
|
|
682
|
+
self._invoke_callback(
|
|
683
|
+
self.on_filesegment_received,
|
|
684
|
+
self._as_handle(transaction_id),
|
|
685
|
+
offset,
|
|
686
|
+
length,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
def _indication_suspended(self, transaction_id, condition_code):
|
|
690
|
+
logger.info("[%s] Suspended indication" % str(transaction_id))
|
|
691
|
+
self._invoke_callback(
|
|
692
|
+
self.on_suspended, self._as_handle(transaction_id), condition_code
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
def _indication_resumed(self, transaction_id, progress):
|
|
696
|
+
logger.info("[%s] Resumed indication" % str(transaction_id))
|
|
697
|
+
self._invoke_callback(
|
|
698
|
+
self.on_resumed, self._as_handle(transaction_id), progress
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
def _indication_report(self, transaction_id, status_report):
|
|
702
|
+
logger.info("[%s] Report indication" % str(transaction_id))
|
|
703
|
+
self._invoke_callback(
|
|
704
|
+
self.on_report, self._as_handle(transaction_id), status_report
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
def _indication_fault(self, transaction_id, condition_code, progress):
|
|
708
|
+
logger.info("[%s] Fault indication" % str(transaction_id))
|
|
709
|
+
self._invoke_callback(
|
|
710
|
+
self.on_fault, self._as_handle(transaction_id), condition_code, progress
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
def _indication_abandoned(self, transaction_id, condition_code, progress):
|
|
714
|
+
logger.info("[%s] Abandoned indication" % str(transaction_id))
|
|
715
|
+
self._invoke_callback(
|
|
716
|
+
self.on_abandoned, self._as_handle(transaction_id), condition_code, progress
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
def _indication_eof_received(self, transaction_id):
|
|
720
|
+
logger.info("[%s] EOF received indication" % str(transaction_id))
|
|
721
|
+
self._invoke_callback(self.on_eof_received, self._as_handle(transaction_id))
|
|
722
|
+
|
|
723
|
+
""" Link state procedures """
|
|
724
|
+
|
|
725
|
+
def freeze(self, receiving_entity_id) -> None:
|
|
726
|
+
self._event_queue.put(("freeze_entity", receiving_entity_id))
|
|
727
|
+
|
|
728
|
+
def thaw(self, receiving_entity_id) -> None:
|
|
729
|
+
self._event_queue.put(("thaw_entity", receiving_entity_id))
|
|
730
|
+
|
|
731
|
+
def pdu_received(self, data) -> None:
|
|
732
|
+
"""Queue raw PDU bytes for processing on the event thread (CCSDS 720.2-G-3 §5.4).
|
|
733
|
+
|
|
734
|
+
Pre-creates a completion event so is_complete() returns False (not True)
|
|
735
|
+
for transactions that are queued but not yet processed.
|
|
736
|
+
"""
|
|
737
|
+
try:
|
|
738
|
+
pdu_header = PduHeader.decode(data)
|
|
739
|
+
tid = pdu_header.transaction_id
|
|
740
|
+
self._completion_events.setdefault(tid, threading.Event())
|
|
741
|
+
except Exception:
|
|
742
|
+
# A header we cannot even decode enough to key a completion event
|
|
743
|
+
# (truncated/corrupt bytes). Processing still enqueues the raw PDU
|
|
744
|
+
# below, where _get_pdu_object surfaces the decode failure; this
|
|
745
|
+
# pre-registration is best-effort, so log and carry on rather than
|
|
746
|
+
# dropping the PDU silently.
|
|
747
|
+
logger.debug("Could not pre-register completion event for incoming PDU")
|
|
748
|
+
self._event_queue.put(("pdu_raw", data))
|
|
749
|
+
|
|
750
|
+
def _get_pdu_object(self, raw_pdu):
|
|
751
|
+
"Take the raw input and return proper PDU object"
|
|
752
|
+
|
|
753
|
+
pdu_header = PduHeader.decode(raw_pdu)
|
|
754
|
+
|
|
755
|
+
# determine type of PDU
|
|
756
|
+
if pdu_header.pdu_type == PduTypeCode.FILE_DATA:
|
|
757
|
+
pdu = FiledataPdu.decode(raw_pdu)
|
|
758
|
+
|
|
759
|
+
elif pdu_header.pdu_type == PduTypeCode.FILE_DIRECTIVE:
|
|
760
|
+
code = raw_pdu[len(pdu_header)]
|
|
761
|
+
|
|
762
|
+
if code == DirectiveCode.EOF:
|
|
763
|
+
pdu = EofPdu.decode(raw_pdu)
|
|
764
|
+
|
|
765
|
+
elif code == DirectiveCode.FINISHED:
|
|
766
|
+
pdu = FinishedPdu.decode(raw_pdu)
|
|
767
|
+
|
|
768
|
+
elif code == DirectiveCode.ACK:
|
|
769
|
+
pdu = AckPdu.decode(raw_pdu)
|
|
770
|
+
|
|
771
|
+
elif code == DirectiveCode.METADATA:
|
|
772
|
+
pdu = MetadataPdu.decode(raw_pdu)
|
|
773
|
+
|
|
774
|
+
elif code == DirectiveCode.NAK:
|
|
775
|
+
pdu = NakPdu.decode(raw_pdu)
|
|
776
|
+
|
|
777
|
+
elif code == DirectiveCode.PROMPT:
|
|
778
|
+
pdu = PromptPdu.decode(raw_pdu)
|
|
779
|
+
|
|
780
|
+
elif code == DirectiveCode.KEEP_ALIVE:
|
|
781
|
+
pdu = KeepAlivePdu.decode(raw_pdu)
|
|
782
|
+
|
|
783
|
+
else:
|
|
784
|
+
raise ValueError(f"unknown file-directive code: {code!r}")
|
|
785
|
+
|
|
786
|
+
else:
|
|
787
|
+
raise ValueError(f"unknown PDU type: {pdu_header.pdu_type!r}")
|
|
788
|
+
|
|
789
|
+
return pdu
|
|
790
|
+
|
|
791
|
+
def _send_pdu_to_state_machine(self, machine, pdu):
|
|
792
|
+
if isinstance(pdu, MetadataPdu):
|
|
793
|
+
self.trigger_event(
|
|
794
|
+
Event(machine.transaction, EventType.E10_RECEIVED_METADATA), pdu
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
elif isinstance(pdu, FiledataPdu):
|
|
798
|
+
self.trigger_event(
|
|
799
|
+
Event(machine.transaction, EventType.E11_RECEIVED_FILEDATA), pdu
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
elif isinstance(pdu, EofPdu):
|
|
803
|
+
if pdu.condition_code == ConditionCode.NO_ERROR:
|
|
804
|
+
self.trigger_event(
|
|
805
|
+
Event(machine.transaction, EventType.E12_RECEIVED_EOF_NO_ERROR), pdu
|
|
806
|
+
)
|
|
807
|
+
else:
|
|
808
|
+
self.trigger_event(
|
|
809
|
+
Event(machine.transaction, EventType.E13_RECEIVED_EOF_CANCEL), pdu
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
elif isinstance(pdu, AckPdu):
|
|
813
|
+
if pdu.directive_code_ack == DirectiveCode.EOF:
|
|
814
|
+
self.trigger_event(
|
|
815
|
+
Event(machine.transaction, EventType.E14_RECEIVED_ACK_EOF), pdu
|
|
816
|
+
)
|
|
817
|
+
elif pdu.directive_code_ack == DirectiveCode.FINISHED:
|
|
818
|
+
self.trigger_event(
|
|
819
|
+
Event(machine.transaction, EventType.E18_RECEIVED_ACK_FINISHED), pdu
|
|
820
|
+
)
|
|
821
|
+
else:
|
|
822
|
+
raise NotImplementedError
|
|
823
|
+
|
|
824
|
+
elif isinstance(pdu, FinishedPdu):
|
|
825
|
+
if pdu.condition_code == ConditionCode.NO_ERROR:
|
|
826
|
+
self.trigger_event(
|
|
827
|
+
Event(
|
|
828
|
+
machine.transaction, EventType.E16_RECEIVED_FINISHED_NO_ERROR
|
|
829
|
+
),
|
|
830
|
+
pdu,
|
|
831
|
+
)
|
|
832
|
+
else:
|
|
833
|
+
self.trigger_event(
|
|
834
|
+
Event(machine.transaction, EventType.E17_RECEIVED_FINISHED_CANCEL),
|
|
835
|
+
pdu,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
elif isinstance(pdu, NakPdu):
|
|
839
|
+
self.trigger_event(
|
|
840
|
+
Event(machine.transaction, EventType.E15_RECEIVED_NAK), pdu
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
else:
|
|
844
|
+
raise TypeError(f"unroutable PDU type: {type(pdu).__name__}")
|
|
845
|
+
|
|
846
|
+
def _run_action_for_incoming_pdu(self, mode, direction, transaction_id, pdu):
|
|
847
|
+
"""See CCSDS 720.2-G-3, Chapter 5.4, Table 5-5"""
|
|
848
|
+
|
|
849
|
+
if (
|
|
850
|
+
direction == Direction.TOWARD_SENDER
|
|
851
|
+
and mode == TransmissionMode.UNACKNOWLEDGED
|
|
852
|
+
):
|
|
853
|
+
# ignore
|
|
854
|
+
pass
|
|
855
|
+
|
|
856
|
+
elif (
|
|
857
|
+
direction == Direction.TOWARD_SENDER
|
|
858
|
+
and mode == TransmissionMode.ACKNOWLEDGED
|
|
859
|
+
):
|
|
860
|
+
if isinstance(pdu, FinishedPdu):
|
|
861
|
+
logger.debug(f"[{transaction_id}] Send Ack Finished")
|
|
862
|
+
ack_pdu = AckPdu(
|
|
863
|
+
direction=Direction.TOWARD_RECEIVER,
|
|
864
|
+
transmission_mode=pdu.pdu_header.transmission_mode,
|
|
865
|
+
source_entity_id=pdu.pdu_header.source_entity_id,
|
|
866
|
+
transaction_seq_number=pdu.pdu_header.transaction_seq_number,
|
|
867
|
+
destination_entity_id=pdu.pdu_header.destination_entity_id,
|
|
868
|
+
#
|
|
869
|
+
directive_code_ack=DirectiveCode.FINISHED,
|
|
870
|
+
directive_subtype_code=DirectiveSubTypeCode.ACK_FINISHED,
|
|
871
|
+
condition_code=ConditionCode.NO_ERROR,
|
|
872
|
+
transaction_status=TransactionStatus.UNDEFINED,
|
|
873
|
+
)
|
|
874
|
+
self.transport.send(ack_pdu.encode())
|
|
875
|
+
|
|
876
|
+
elif direction == Direction.TOWARD_RECEIVER:
|
|
877
|
+
# start a new state machine
|
|
878
|
+
if (
|
|
879
|
+
isinstance(pdu, MetadataPdu)
|
|
880
|
+
or isinstance(pdu, FiledataPdu)
|
|
881
|
+
or isinstance(pdu, EofPdu)
|
|
882
|
+
):
|
|
883
|
+
# Build a pure receiver via the registry. It picks Receiver1
|
|
884
|
+
# (UNACKNOWLEDGED) or Receiver2 (ACKNOWLEDGED) off the inbound PDU
|
|
885
|
+
# header (issues #16 / #20).
|
|
886
|
+
remote_cfg = self._get_remote_config(pdu.pdu_header.source_entity_id)
|
|
887
|
+
machine, transaction = self._machine_registry.create_receiver(
|
|
888
|
+
pdu, remote_config=remote_cfg
|
|
889
|
+
)
|
|
890
|
+
self._completion_events.setdefault(transaction.id, threading.Event())
|
|
891
|
+
self.trigger_event(Event(transaction, EventType.E0_ENTERED_STATE))
|
|
892
|
+
self._send_pdu_to_state_machine(machine, pdu)
|
|
893
|
+
|
|
894
|
+
def process_user_message(self, message):
|
|
895
|
+
meta_process_user_message(self, message)
|