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,285 @@
|
|
|
1
|
+
"""The effect executor and its file table (design D7/D10).
|
|
2
|
+
|
|
3
|
+
A pure machine never touches the world; it returns effects. The
|
|
4
|
+
:class:`EffectExecutor` interprets those effects against the real filestore,
|
|
5
|
+
transport, and indication/message plumbing, and owns the one place where open
|
|
6
|
+
OS file handles live: the ``tid -> handle`` :class:`FileTable` (design D10).
|
|
7
|
+
|
|
8
|
+
The executor is deliberately *thin logic, real I/O*: each ``execute`` call maps
|
|
9
|
+
exactly one :class:`~cfdp.pure.outputs.Output` effect to its side effect. The
|
|
10
|
+
:class:`~cfdp.shell.dispatcher.Dispatcher` owns the exception boundary around
|
|
11
|
+
these calls (design D4/D7), so the executor itself just does the work and lets
|
|
12
|
+
exceptions propagate.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .. import logger
|
|
16
|
+
from ..event import EventType
|
|
17
|
+
from ..filestore.base import FileHandle
|
|
18
|
+
from ..meta import execute_filestore_requests
|
|
19
|
+
from ..pure.outputs import (
|
|
20
|
+
CancelTimer,
|
|
21
|
+
CloseFile,
|
|
22
|
+
EmitIndication,
|
|
23
|
+
ExecuteFilestoreRequests,
|
|
24
|
+
Finalize,
|
|
25
|
+
InternalEvent,
|
|
26
|
+
OpenSource,
|
|
27
|
+
OpenTemp,
|
|
28
|
+
Output,
|
|
29
|
+
PostUserMessage,
|
|
30
|
+
RequestChecksum,
|
|
31
|
+
ResumeTimer,
|
|
32
|
+
ScheduleEvent,
|
|
33
|
+
SendPdu,
|
|
34
|
+
StartTimer,
|
|
35
|
+
SuspendTimer,
|
|
36
|
+
TimerKind,
|
|
37
|
+
WriteSegment,
|
|
38
|
+
)
|
|
39
|
+
from ..pure.transaction import Transaction, TransactionId
|
|
40
|
+
|
|
41
|
+
# Which EventType a given timer kind injects on expiry (design D6). The pure
|
|
42
|
+
# machine emits ``StartTimer(kind, interval)`` without naming the event; the
|
|
43
|
+
# shell owns this fixed kind -> event mapping so the machine stays agnostic of
|
|
44
|
+
# how expiry re-enters the FIFO.
|
|
45
|
+
_TIMER_EXPIRY_EVENT = {
|
|
46
|
+
TimerKind.INACTIVITY: EventType.E27_INACTIVITY_TIMEOUT,
|
|
47
|
+
TimerKind.ACK: EventType.E25_ACK_TIMEOUT,
|
|
48
|
+
TimerKind.NAK: EventType.E26_NAK_TIMEOUT,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FileTable:
|
|
53
|
+
"""The ``tid -> open handle`` map (design D10).
|
|
54
|
+
|
|
55
|
+
A machine that never opened a file has no entry, so cleanup is a no-op for
|
|
56
|
+
it — this kills the class of ``AttributeError`` the old ``shutdown()`` could
|
|
57
|
+
raise (design D10 / issue 01).
|
|
58
|
+
|
|
59
|
+
Thread-safety invariant (issue #21): ``get(tid)`` is read by the async
|
|
60
|
+
checksum worker thread while the event thread calls ``set``/``pop``/
|
|
61
|
+
``cleanup``. This is lock-free-safe because the receive-side lifecycle
|
|
62
|
+
guarantees the worker's read never overlaps a close: the handle stays in the
|
|
63
|
+
table until the transaction is reaped, and reaping happens only after the
|
|
64
|
+
checksum result (E42) has been processed, i.e. after the worker has finished.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
self._handles: dict[TransactionId, FileHandle] = {}
|
|
69
|
+
|
|
70
|
+
def get(self, tid: TransactionId) -> FileHandle | None:
|
|
71
|
+
return self._handles.get(tid)
|
|
72
|
+
|
|
73
|
+
def set(self, tid: TransactionId, handle) -> None:
|
|
74
|
+
self._handles[tid] = handle
|
|
75
|
+
|
|
76
|
+
def pop(self, tid: TransactionId):
|
|
77
|
+
return self._handles.pop(tid, None)
|
|
78
|
+
|
|
79
|
+
def __contains__(self, tid: TransactionId) -> bool:
|
|
80
|
+
return tid in self._handles
|
|
81
|
+
|
|
82
|
+
def close_all(self) -> None:
|
|
83
|
+
"""Close and forget every open handle (entity shutdown / reaping)."""
|
|
84
|
+
for tid, handle in list(self._handles.items()):
|
|
85
|
+
try:
|
|
86
|
+
if handle is not None:
|
|
87
|
+
handle.close()
|
|
88
|
+
except Exception:
|
|
89
|
+
logger.exception("Error closing file handle for %s", tid)
|
|
90
|
+
self._handles.pop(tid, None)
|
|
91
|
+
|
|
92
|
+
def cleanup(self, tid: TransactionId) -> None:
|
|
93
|
+
"""Close and forget the handle for one transaction, if any."""
|
|
94
|
+
handle = self._handles.pop(tid, None)
|
|
95
|
+
if handle is not None:
|
|
96
|
+
try:
|
|
97
|
+
handle.close()
|
|
98
|
+
except Exception:
|
|
99
|
+
logger.exception("Error closing file handle for %s", tid)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class EffectExecutor:
|
|
103
|
+
"""Interprets effect outputs against real I/O (design D7).
|
|
104
|
+
|
|
105
|
+
Owns the :class:`FileTable`; opens/writes/finalizes/closes files, encodes and
|
|
106
|
+
sends PDUs, and fans indications and user messages out through the entity's
|
|
107
|
+
existing indication methods and message queue.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self, entity, file_table=None, timer_service=None, checksum_service=None
|
|
112
|
+
):
|
|
113
|
+
self.entity = entity
|
|
114
|
+
self.files = file_table if file_table is not None else FileTable()
|
|
115
|
+
# The wall-clock timer table (design D6 / issue #17). Owns real timers
|
|
116
|
+
# per (tid, kind) and injects the expiry event onto the entity FIFO.
|
|
117
|
+
self.timers = timer_service
|
|
118
|
+
# The async-checksum worker (design R1 / issue #21). Computes a whole-file
|
|
119
|
+
# checksum off the event thread and injects E42_CHECKSUM_READY. Optional
|
|
120
|
+
# so the Class-1 shell-seam unit tests (which never request a checksum via
|
|
121
|
+
# the executor) can construct an executor without one.
|
|
122
|
+
self.checksums = checksum_service
|
|
123
|
+
# Direct-to-destination bookkeeping (write_to_destination_directly).
|
|
124
|
+
# ``_direct_dest`` maps a tid to the destination filename its handle was
|
|
125
|
+
# opened on (so a failed transfer's partial file can be deleted);
|
|
126
|
+
# ``_direct_committed`` marks tids whose data was verified/Finalized, so
|
|
127
|
+
# the following CloseFile keeps the file instead of deleting it.
|
|
128
|
+
self._direct_dest: dict[TransactionId, str] = {}
|
|
129
|
+
self._direct_committed: set[TransactionId] = set()
|
|
130
|
+
|
|
131
|
+
# -- the single execute door ------------------------------------------- #
|
|
132
|
+
|
|
133
|
+
def execute(
|
|
134
|
+
self, tid: TransactionId, transaction: Transaction, effect: Output
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Execute one effect for the machine owning ``tid``.
|
|
137
|
+
|
|
138
|
+
Timer effects are handled by the dispatcher/timer service, not here;
|
|
139
|
+
internal events are re-enqueued by the dispatcher. This method covers
|
|
140
|
+
only the world-touching effects the Class-1 machines emit.
|
|
141
|
+
"""
|
|
142
|
+
if isinstance(effect, SendPdu):
|
|
143
|
+
self.entity.transport.send(effect.pdu.encode())
|
|
144
|
+
|
|
145
|
+
elif isinstance(effect, OpenSource):
|
|
146
|
+
self.files.set(tid, self.entity.filestore.open(transaction.source_filename))
|
|
147
|
+
|
|
148
|
+
elif isinstance(effect, OpenTemp):
|
|
149
|
+
dest_name = transaction.destination_filename
|
|
150
|
+
if self.entity.write_to_destination_directly and dest_name:
|
|
151
|
+
# Direct-to-destination mode: write incoming data straight into
|
|
152
|
+
# the target file instead of a temp file, so a remote-service
|
|
153
|
+
# filestore can show download progress. Open read/write (``w+b``)
|
|
154
|
+
# so the async checksum can read the file back before Finished.
|
|
155
|
+
# If the destination is not yet known (Class-2 recovery with a
|
|
156
|
+
# lost Metadata, where only the machine holds the resolved name),
|
|
157
|
+
# fall back to a temp file and the normal Finalize copy.
|
|
158
|
+
self.files.set(tid, self.entity.filestore.open(dest_name, "w+b"))
|
|
159
|
+
self._direct_dest[tid] = dest_name
|
|
160
|
+
else:
|
|
161
|
+
self.files.set(tid, self.entity.filestore.open_tempfile())
|
|
162
|
+
|
|
163
|
+
elif isinstance(effect, WriteSegment):
|
|
164
|
+
handle = self.files.get(tid)
|
|
165
|
+
handle.seek(effect.offset)
|
|
166
|
+
handle.write(effect.data)
|
|
167
|
+
|
|
168
|
+
elif isinstance(effect, ExecuteFilestoreRequests):
|
|
169
|
+
if effect.requests:
|
|
170
|
+
execute_filestore_requests(self.entity, effect.requests)
|
|
171
|
+
|
|
172
|
+
elif isinstance(effect, Finalize):
|
|
173
|
+
self._finalize(tid, transaction, effect.destination_filename)
|
|
174
|
+
|
|
175
|
+
elif isinstance(effect, CloseFile):
|
|
176
|
+
handle = self.files.get(tid)
|
|
177
|
+
if handle is not None:
|
|
178
|
+
handle.close()
|
|
179
|
+
if tid in self._direct_dest:
|
|
180
|
+
# Direct-to-destination mode: the target file was written in
|
|
181
|
+
# place. If the transfer never reached Finalize (checksum/size
|
|
182
|
+
# fault, cancel, abandon), delete the partial file so a failed
|
|
183
|
+
# transfer never leaves a corrupt file behind. Pop the tracking
|
|
184
|
+
# so the second CloseFile on the happy path is a plain no-op.
|
|
185
|
+
dest_name = self._direct_dest.pop(tid)
|
|
186
|
+
if tid not in self._direct_committed:
|
|
187
|
+
try:
|
|
188
|
+
self.entity.filestore.delete_file(dest_name)
|
|
189
|
+
except Exception:
|
|
190
|
+
logger.exception(
|
|
191
|
+
"Error deleting partial direct-write file for %s", tid
|
|
192
|
+
)
|
|
193
|
+
self._direct_committed.discard(tid)
|
|
194
|
+
# Mirror the old machine: close_tempfile does NOT drop the handle,
|
|
195
|
+
# so a double CloseFile on the happy EOF path is a no-op the second
|
|
196
|
+
# time (the file object tolerates a repeat close). The handle is
|
|
197
|
+
# only dropped from the table when the transaction is reaped, so the
|
|
198
|
+
# QueryPort can still read the temp file between close and reap.
|
|
199
|
+
|
|
200
|
+
elif isinstance(effect, EmitIndication):
|
|
201
|
+
self._emit_indication(effect)
|
|
202
|
+
|
|
203
|
+
elif isinstance(effect, PostUserMessage):
|
|
204
|
+
self.entity._messages_to_user_queue.put(effect.message)
|
|
205
|
+
|
|
206
|
+
elif isinstance(effect, StartTimer):
|
|
207
|
+
self.timers.start(
|
|
208
|
+
tid, effect.kind, effect.interval, _TIMER_EXPIRY_EVENT[effect.kind]
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
elif isinstance(effect, CancelTimer):
|
|
212
|
+
self.timers.cancel(tid, effect.kind)
|
|
213
|
+
|
|
214
|
+
elif isinstance(effect, SuspendTimer):
|
|
215
|
+
self.timers.suspend(tid, effect.kind)
|
|
216
|
+
|
|
217
|
+
elif isinstance(effect, ResumeTimer):
|
|
218
|
+
self.timers.resume(tid, effect.kind)
|
|
219
|
+
|
|
220
|
+
elif isinstance(effect, RequestChecksum):
|
|
221
|
+
# Compute the whole-file checksum off the event thread (issue #21);
|
|
222
|
+
# the result returns as an injected E42_CHECKSUM_READY event.
|
|
223
|
+
self.checksums.request(
|
|
224
|
+
tid, transaction, effect.checksum_type, effect.from_temp_handle
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
elif isinstance(effect, (InternalEvent, ScheduleEvent)):
|
|
228
|
+
# Should be routed by the dispatcher, never reach the executor.
|
|
229
|
+
raise TypeError("internal events are not executor effects")
|
|
230
|
+
|
|
231
|
+
else:
|
|
232
|
+
raise TypeError(f"unknown effect: {effect!r}")
|
|
233
|
+
|
|
234
|
+
# -- file lifecycle ---------------------------------------------------- #
|
|
235
|
+
|
|
236
|
+
def _finalize(
|
|
237
|
+
self,
|
|
238
|
+
tid: TransactionId,
|
|
239
|
+
transaction: Transaction,
|
|
240
|
+
destination_filename=None,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Copy the completed temp file to its destination (receive side).
|
|
243
|
+
|
|
244
|
+
``destination_filename`` from the :class:`Finalize` effect wins when the
|
|
245
|
+
machine resolved it from a late-recovered Metadata (Class-2 recovery);
|
|
246
|
+
otherwise fall back to the frozen transaction's filename (the Class-1
|
|
247
|
+
path, where Metadata arrives first).
|
|
248
|
+
"""
|
|
249
|
+
if tid in self._direct_dest:
|
|
250
|
+
# Direct-to-destination mode: the data is already in the target
|
|
251
|
+
# file, so there is nothing to copy. Mark it committed so the
|
|
252
|
+
# following CloseFile keeps the file instead of deleting it.
|
|
253
|
+
self._direct_committed.add(tid)
|
|
254
|
+
return
|
|
255
|
+
dest_name = destination_filename or transaction.destination_filename
|
|
256
|
+
handle = self.files.get(tid)
|
|
257
|
+
if handle is None:
|
|
258
|
+
# No temp handle for this transaction (issue #30): raise a clean,
|
|
259
|
+
# descriptive error BEFORE opening dest, so the dispatcher's exception
|
|
260
|
+
# boundary faults cleanly rather than AttributeError-ing on ``None``
|
|
261
|
+
# (and no destination fd is opened, so none can leak).
|
|
262
|
+
raise RuntimeError(
|
|
263
|
+
f"cannot finalize {tid}: no open temp file handle to copy from"
|
|
264
|
+
)
|
|
265
|
+
dest = self.entity.filestore.open(dest_name, "wb")
|
|
266
|
+
try:
|
|
267
|
+
handle.seek(0)
|
|
268
|
+
chunk_size = 65536
|
|
269
|
+
while True:
|
|
270
|
+
chunk = handle.read(chunk_size)
|
|
271
|
+
if not chunk:
|
|
272
|
+
break
|
|
273
|
+
dest.write(chunk)
|
|
274
|
+
finally:
|
|
275
|
+
# Always close the destination handle, on every path (a mid-copy
|
|
276
|
+
# read/write error included), so a failed finalize never leaks an fd.
|
|
277
|
+
dest.close()
|
|
278
|
+
|
|
279
|
+
# -- indication fan-out ------------------------------------------------ #
|
|
280
|
+
|
|
281
|
+
def _emit_indication(self, effect):
|
|
282
|
+
method = getattr(self.entity, "_indication_" + effect.kind, None)
|
|
283
|
+
if method is None:
|
|
284
|
+
raise TypeError(f"unknown indication kind: {effect.kind!r}")
|
|
285
|
+
method(*effect.args)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""The ``tid -> machine`` registry and machine creation (design D7/D9).
|
|
2
|
+
|
|
3
|
+
Owns the map of live pure machines, builds them (folding config into a frozen
|
|
4
|
+
:class:`~cfdp.pure.config.TransactionConfig` snapshot, design D9, and injecting a
|
|
5
|
+
per-transaction :class:`~cfdp.shell.query_port.EntityQueryPort`, design D3), and
|
|
6
|
+
tracks completion for TTL reaping.
|
|
7
|
+
|
|
8
|
+
Two creation doors:
|
|
9
|
+
|
|
10
|
+
* :meth:`create_sender` — from a local ``put`` request.
|
|
11
|
+
* :meth:`create_receiver` — from an inbound Metadata/Filedata/EOF toward the
|
|
12
|
+
receiver.
|
|
13
|
+
|
|
14
|
+
Both Class-1 (``UNACKNOWLEDGED``) and Class-2 (``ACKNOWLEDGED``) pure machines
|
|
15
|
+
are built here (issue #20 wired Class-2 through the shell). These are the only
|
|
16
|
+
machines (the old machines were removed at the cutover, issue #22).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from ..constants import TransmissionMode
|
|
20
|
+
from ..pure import (
|
|
21
|
+
EntityDefaults,
|
|
22
|
+
PutOverrides,
|
|
23
|
+
TransactionConfig,
|
|
24
|
+
)
|
|
25
|
+
from ..pure import (
|
|
26
|
+
Transaction as PureTransaction,
|
|
27
|
+
)
|
|
28
|
+
from ..pure.machines import Receiver1 as PureReceiver1
|
|
29
|
+
from ..pure.machines import Receiver2 as PureReceiver2
|
|
30
|
+
from ..pure.machines import Sender1 as PureSender1
|
|
31
|
+
from ..pure.machines import Sender2 as PureSender2
|
|
32
|
+
from ..pure.transaction import TransactionId
|
|
33
|
+
from .query_port import EntityQueryPort
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MachineRegistry:
|
|
37
|
+
"""The live pure-machine map plus creation and reaping (design D7)."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, entity, file_table, machines=None):
|
|
40
|
+
self._entity = entity
|
|
41
|
+
self._files = file_table
|
|
42
|
+
# Share the entity's machines dict so every lookup path
|
|
43
|
+
# (cancel/suspend/resume/report/freeze/thaw, is_complete, reaping) sees
|
|
44
|
+
# the machines uniformly.
|
|
45
|
+
self.machines = machines if machines is not None else {}
|
|
46
|
+
|
|
47
|
+
# -- membership -------------------------------------------------------- #
|
|
48
|
+
|
|
49
|
+
def get(self, tid: TransactionId):
|
|
50
|
+
return self.machines.get(tid)
|
|
51
|
+
|
|
52
|
+
def __contains__(self, tid: TransactionId) -> bool:
|
|
53
|
+
return tid in self.machines
|
|
54
|
+
|
|
55
|
+
# -- config snapshot (design D9) --------------------------------------- #
|
|
56
|
+
|
|
57
|
+
def _entity_defaults(self):
|
|
58
|
+
return EntityDefaults(
|
|
59
|
+
transaction_inactivity_limit=self._entity.transaction_inactivity_limit,
|
|
60
|
+
ack_timer_interval=self._entity.positive_ack_timer_interval,
|
|
61
|
+
ack_timer_expiration_limit=self._entity.positive_ack_timer_expiration_limit,
|
|
62
|
+
nak_timer_interval=self._entity.nak_timer_interval,
|
|
63
|
+
nak_timer_expiration_limit=self._entity.nak_timer_expiration_limit,
|
|
64
|
+
maximum_file_segment_length=self._entity.maximum_file_segment_length,
|
|
65
|
+
inter_pdu_delay=self._entity.inter_pdu_delay,
|
|
66
|
+
fault_handlers=dict(self._entity.default_fault_handlers),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _resolve_config(self, remote_config, put_overrides=None):
|
|
70
|
+
return TransactionConfig.resolve(
|
|
71
|
+
self._entity_defaults(), remote_config, put_overrides
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# -- creation doors ---------------------------------------------------- #
|
|
75
|
+
|
|
76
|
+
def create_sender(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
seq_number,
|
|
80
|
+
destination_id,
|
|
81
|
+
source_filename,
|
|
82
|
+
destination_filename,
|
|
83
|
+
checksum_type,
|
|
84
|
+
transmission_mode,
|
|
85
|
+
maximum_file_segment_length,
|
|
86
|
+
messages_to_user=None,
|
|
87
|
+
filestore_requests=None,
|
|
88
|
+
fault_handler_overrides=None,
|
|
89
|
+
remote_config=None,
|
|
90
|
+
):
|
|
91
|
+
"""Build a pure sender for a local ``put`` and register it.
|
|
92
|
+
|
|
93
|
+
Picks :class:`PureSender1` for ``UNACKNOWLEDGED`` and :class:`PureSender2`
|
|
94
|
+
for ``ACKNOWLEDGED`` (issue #20); both take the same
|
|
95
|
+
``(transaction, config, query_port, messages_to_user, filestore_requests)``
|
|
96
|
+
constructor.
|
|
97
|
+
"""
|
|
98
|
+
transaction = PureTransaction(
|
|
99
|
+
source_entity_id=self._entity.entity_id,
|
|
100
|
+
seq_number=seq_number,
|
|
101
|
+
destination_entity_id=destination_id,
|
|
102
|
+
transmission_mode=transmission_mode,
|
|
103
|
+
source_filename=source_filename,
|
|
104
|
+
destination_filename=destination_filename,
|
|
105
|
+
checksum_type=checksum_type,
|
|
106
|
+
)
|
|
107
|
+
put_overrides = PutOverrides(
|
|
108
|
+
maximum_file_segment_length=maximum_file_segment_length,
|
|
109
|
+
fault_handler_overrides=fault_handler_overrides,
|
|
110
|
+
)
|
|
111
|
+
config = self._resolve_config(remote_config, put_overrides)
|
|
112
|
+
query_port = EntityQueryPort(self._entity, transaction, self._files)
|
|
113
|
+
sender_cls = (
|
|
114
|
+
PureSender2
|
|
115
|
+
if transmission_mode == TransmissionMode.ACKNOWLEDGED
|
|
116
|
+
else PureSender1
|
|
117
|
+
)
|
|
118
|
+
machine = sender_cls(
|
|
119
|
+
transaction,
|
|
120
|
+
config,
|
|
121
|
+
query_port,
|
|
122
|
+
messages_to_user=messages_to_user,
|
|
123
|
+
filestore_requests=filestore_requests,
|
|
124
|
+
)
|
|
125
|
+
self.machines[transaction.id] = machine
|
|
126
|
+
return machine, transaction
|
|
127
|
+
|
|
128
|
+
def create_receiver(self, pdu, remote_config=None):
|
|
129
|
+
"""Build a pure receiver from an inbound toward-receiver PDU.
|
|
130
|
+
|
|
131
|
+
Picks :class:`PureReceiver1` for ``UNACKNOWLEDGED`` and
|
|
132
|
+
:class:`PureReceiver2` for ``ACKNOWLEDGED`` (issue #20), keyed off the
|
|
133
|
+
inbound PDU header's transmission mode.
|
|
134
|
+
"""
|
|
135
|
+
source_entity_id = pdu.pdu_header.source_entity_id
|
|
136
|
+
seq_number = pdu.pdu_header.transaction_seq_number
|
|
137
|
+
destination_entity_id = pdu.pdu_header.destination_entity_id
|
|
138
|
+
transmission_mode = pdu.pdu_header.transmission_mode
|
|
139
|
+
# Metadata carries the filenames + checksum type; Filedata/EOF-first do
|
|
140
|
+
# not, matching the old core._run_action_for_incoming_pdu split.
|
|
141
|
+
source_filename = getattr(pdu, "source_filename", None)
|
|
142
|
+
destination_filename = getattr(pdu, "destination_filename", None)
|
|
143
|
+
checksum_type = getattr(pdu, "checksum_type", None)
|
|
144
|
+
kwargs = {}
|
|
145
|
+
if checksum_type is not None:
|
|
146
|
+
kwargs["checksum_type"] = checksum_type
|
|
147
|
+
transaction = PureTransaction(
|
|
148
|
+
source_entity_id=source_entity_id,
|
|
149
|
+
seq_number=seq_number,
|
|
150
|
+
destination_entity_id=destination_entity_id,
|
|
151
|
+
transmission_mode=transmission_mode,
|
|
152
|
+
source_filename=source_filename,
|
|
153
|
+
destination_filename=destination_filename,
|
|
154
|
+
**kwargs,
|
|
155
|
+
)
|
|
156
|
+
config = self._resolve_config(remote_config)
|
|
157
|
+
query_port = EntityQueryPort(self._entity, transaction, self._files)
|
|
158
|
+
receiver_cls = (
|
|
159
|
+
PureReceiver2
|
|
160
|
+
if transmission_mode == TransmissionMode.ACKNOWLEDGED
|
|
161
|
+
else PureReceiver1
|
|
162
|
+
)
|
|
163
|
+
machine = receiver_cls(transaction, config, query_port)
|
|
164
|
+
self.machines[transaction.id] = machine
|
|
165
|
+
return machine, transaction
|
cfdp/shell/query_port.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""The synchronous, local-only query port implementation (design D3/D5/D10).
|
|
2
|
+
|
|
3
|
+
A pure machine calls its injected :class:`~cfdp.pure.query_port.QueryPort`
|
|
4
|
+
synchronously mid-decision for local + fast reads only: source size/checksum,
|
|
5
|
+
sequential source segments, temp-file checksum read-back, and transport
|
|
6
|
+
readiness. :class:`EntityQueryPort` backs those against the entity's real
|
|
7
|
+
filestore/transport and the shell-held file handle (the send-side source handle
|
|
8
|
+
or the receive-side temp handle) in the :class:`~cfdp.shell.effect_executor.FileTable`.
|
|
9
|
+
|
|
10
|
+
One port instance is bound per transaction: it captures the ``tid`` and the
|
|
11
|
+
frozen :class:`~cfdp.pure.transaction.Transaction` so the machine's calls resolve
|
|
12
|
+
to that transaction's filenames and handle.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ..pure.transaction import Transaction
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EntityQueryPort:
|
|
19
|
+
"""A :class:`~cfdp.pure.query_port.QueryPort` bound to one transaction.
|
|
20
|
+
|
|
21
|
+
``read_segment``/temp ``checksum`` read the handle the
|
|
22
|
+
:class:`~cfdp.shell.effect_executor.EffectExecutor` opened for this ``tid``
|
|
23
|
+
(design D10: the shell owns the fd, the machine holds only positions).
|
|
24
|
+
``size``/source ``checksum`` open the source afresh, mirroring the old
|
|
25
|
+
``Transaction`` read helpers exactly.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, entity, transaction: Transaction, file_table) -> None:
|
|
29
|
+
self._entity = entity
|
|
30
|
+
self._transaction = transaction
|
|
31
|
+
self._files = file_table
|
|
32
|
+
self._tid = transaction.id
|
|
33
|
+
|
|
34
|
+
def is_file(self, filename: str) -> bool:
|
|
35
|
+
return self._entity.filestore.is_file(filename)
|
|
36
|
+
|
|
37
|
+
def size(self, filename: str) -> int:
|
|
38
|
+
try:
|
|
39
|
+
return self._entity.filestore.get_size(filename)
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
def checksum(self, checksum_type: int) -> int:
|
|
44
|
+
"""Checksum the transaction's current file.
|
|
45
|
+
|
|
46
|
+
Receive side: read back the shell-held temp handle (design D10). Send
|
|
47
|
+
side (no temp handle in the table): open the source afresh, exactly as
|
|
48
|
+
the old ``Transaction.get_file_checksum`` did.
|
|
49
|
+
"""
|
|
50
|
+
handle = self._files.get(self._tid)
|
|
51
|
+
if handle is not None:
|
|
52
|
+
return handle.calculate_checksum(checksum_type)
|
|
53
|
+
source = self._transaction.source_filename
|
|
54
|
+
if not source:
|
|
55
|
+
return 0
|
|
56
|
+
fh = self._entity.filestore.open(source)
|
|
57
|
+
try:
|
|
58
|
+
return fh.calculate_checksum(checksum_type)
|
|
59
|
+
finally:
|
|
60
|
+
fh.close()
|
|
61
|
+
|
|
62
|
+
def read_segment(self, offset: int, length: int) -> tuple[int, bytes]:
|
|
63
|
+
handle = self._files.get(self._tid)
|
|
64
|
+
if handle is None:
|
|
65
|
+
raise ValueError("File handle not defined")
|
|
66
|
+
handle.seek(offset)
|
|
67
|
+
return offset, handle.read(length)
|
|
68
|
+
|
|
69
|
+
def transport_ready(self) -> bool:
|
|
70
|
+
return self._entity.transport.is_ready()
|