bdo-toolkit 1.0.0__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.
Files changed (48) hide show
  1. bdo_toolkit/__init__.py +87 -0
  2. bdo_toolkit/_async_sessions.py +651 -0
  3. bdo_toolkit/_capture_backend.py +194 -0
  4. bdo_toolkit/_capture_options.py +68 -0
  5. bdo_toolkit/_capture_runtime.py +626 -0
  6. bdo_toolkit/_deposit_origin.py +1599 -0
  7. bdo_toolkit/_engine.py +327 -0
  8. bdo_toolkit/_framing.py +904 -0
  9. bdo_toolkit/_profile_runtime.py +157 -0
  10. bdo_toolkit/_protocol.py +386 -0
  11. bdo_toolkit/_reassembly.py +654 -0
  12. bdo_toolkit/_specs.py +285 -0
  13. bdo_toolkit/_storage_destination_validation.py +167 -0
  14. bdo_toolkit/_storage_hydration.py +241 -0
  15. bdo_toolkit/_version.py +3 -0
  16. bdo_toolkit/calibration.py +3223 -0
  17. bdo_toolkit/capture.py +1713 -0
  18. bdo_toolkit/character_state.py +3506 -0
  19. bdo_toolkit/cli.py +948 -0
  20. bdo_toolkit/diagnostics.py +51 -0
  21. bdo_toolkit/events.py +214 -0
  22. bdo_toolkit/filters.py +105 -0
  23. bdo_toolkit/item_state.py +48 -0
  24. bdo_toolkit/origin_learning.py +779 -0
  25. bdo_toolkit/profiles.py +370 -0
  26. bdo_toolkit/py.typed +1 -0
  27. bdo_toolkit/remote_profiles.py +358 -0
  28. bdo_toolkit/solare/__init__.py +50 -0
  29. bdo_toolkit/solare/_constants.py +94 -0
  30. bdo_toolkit/solare/_detail_learning.py +1437 -0
  31. bdo_toolkit/solare/_details.py +796 -0
  32. bdo_toolkit/solare/_discovery.py +1212 -0
  33. bdo_toolkit/solare/_live_tracker.py +472 -0
  34. bdo_toolkit/solare/_replay_capture.py +182 -0
  35. bdo_toolkit/solare/_result.py +441 -0
  36. bdo_toolkit/solare/_scanner.py +203 -0
  37. bdo_toolkit/solare/_validation.py +11 -0
  38. bdo_toolkit/solare/async_session.py +444 -0
  39. bdo_toolkit/solare/models.py +806 -0
  40. bdo_toolkit/solare/replay.py +62 -0
  41. bdo_toolkit/solare/session.py +1051 -0
  42. bdo_toolkit/writers.py +30 -0
  43. bdo_toolkit-1.0.0.dist-info/METADATA +143 -0
  44. bdo_toolkit-1.0.0.dist-info/RECORD +48 -0
  45. bdo_toolkit-1.0.0.dist-info/WHEEL +5 -0
  46. bdo_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
  47. bdo_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  48. bdo_toolkit-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,472 @@
1
+ """Structured progress tracking for opcode-agnostic Solare capture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections import Counter, deque
7
+ from collections.abc import Callable
8
+ from dataclasses import replace
9
+ from typing import Deque, Optional
10
+
11
+ from bdo_toolkit._protocol import BDOFrame
12
+
13
+ from ._constants import (
14
+ DISCOVERY_MAX_FRAME_LENGTH,
15
+ DISCOVERY_MIN_FAMILY_FRAMES,
16
+ DISCOVERY_MIN_FRAME_LENGTH,
17
+ DISCOVERY_MIN_RICH_RECORDS,
18
+ DISCOVERY_RETENTION_MAX_BYTES,
19
+ DISCOVERY_RETENTION_MAX_FRAMES,
20
+ LIVE_CANDIDATE_IDLE_SECONDS,
21
+ )
22
+ from ._discovery import (
23
+ _DiscoveryAnalysisCache,
24
+ DiscoveredSolareFamily,
25
+ SolareDiscoveryResult,
26
+ _family_key,
27
+ _rank_reset_starts,
28
+ discover_solare,
29
+ )
30
+ from .models import SolareUpdate, SolareUpdateKind
31
+
32
+
33
+ def _discovery_score(result: SolareDiscoveryResult) -> tuple[int, int, int]:
34
+ ranked = result.rich or result.ranked_candidate
35
+ partial = result.overall or result.partial_overall
36
+ if result.confirmed:
37
+ stage = 5
38
+ elif result.rich is not None and result.partial_overall is not None:
39
+ stage = 4
40
+ elif result.rich is not None:
41
+ stage = 3
42
+ elif ranked is not None and partial is not None:
43
+ stage = 2
44
+ elif ranked is not None:
45
+ stage = 1
46
+ elif result.menu_context is not None:
47
+ stage = 0
48
+ else:
49
+ stage = -1
50
+ return (
51
+ stage,
52
+ ranked.record_count if ranked is not None else 0,
53
+ partial.record_count if partial is not None else 0,
54
+ )
55
+
56
+
57
+ def _compact_discovery(result: SolareDiscoveryResult) -> SolareDiscoveryResult:
58
+ """Drop raw frame ownership while retaining compact structural evidence."""
59
+
60
+ def compact(
61
+ family: Optional[DiscoveredSolareFamily],
62
+ ) -> Optional[DiscoveredSolareFamily]:
63
+ if family is None:
64
+ return None
65
+ return replace(family, frames=())
66
+
67
+ return replace(
68
+ result,
69
+ rich=compact(result.rich),
70
+ overall=compact(result.overall),
71
+ ranked_candidate=compact(result.ranked_candidate),
72
+ partial_overall=compact(result.partial_overall),
73
+ )
74
+
75
+
76
+ class LiveSolareDiscoveryTracker:
77
+ """Refresh structural discovery at useful milestones and emit updates."""
78
+
79
+ def __init__(self, emit: Callable[[SolareUpdate], None]) -> None:
80
+ self._emit = emit
81
+ self._frames: Deque[BDOFrame] = deque()
82
+ self._retained_bytes = 0
83
+ self._peak_retained_frame_count = 0
84
+ self._peak_retained_bytes = 0
85
+ self._evicted_frame_count = 0
86
+ self._evicted_bytes = 0
87
+ self._observed_frame_count = 0
88
+ self._rollover_warned = False
89
+ self._analysis_cache = _DiscoveryAnalysisCache()
90
+ self._family_counts: Counter[tuple[object, int, int, int]] = Counter()
91
+ self._retained_family_counts: Counter[
92
+ tuple[object, int, int, int]
93
+ ] = Counter()
94
+ self._family_recent: dict[
95
+ tuple[object, int, int, int], Deque[BDOFrame]
96
+ ] = {}
97
+ self._family_targets: dict[tuple[object, int, int, int], set[int]] = {}
98
+ self._announced_menu = False
99
+ self._announced_ranked_players = 300
100
+ self._announced_rich: Optional[tuple[object, ...]] = None
101
+ self._announced_cross_check = 0
102
+ self._announced_confirmed = False
103
+ self._confirmed_frames: Optional[tuple[BDOFrame, ...]] = None
104
+ self._dirty = False
105
+ self._tail_finalized = False
106
+ self._last_candidate_activity_at: Optional[float] = None
107
+ self.result = SolareDiscoveryResult(0, ())
108
+ self._best_result = self.result
109
+
110
+ def observe(self, frame: BDOFrame) -> None:
111
+ if self._confirmed_frames is not None:
112
+ return
113
+ if not (
114
+ frame.flag == 0
115
+ and DISCOVERY_MIN_FRAME_LENGTH
116
+ <= frame.length
117
+ <= DISCOVERY_MAX_FRAME_LENGTH
118
+ ):
119
+ return
120
+
121
+ self._last_candidate_activity_at = time.monotonic()
122
+ rolled_over = self._make_room_for(frame)
123
+ if self.complete:
124
+ return
125
+ self._frames.append(frame)
126
+ self._retained_bytes += frame.length
127
+ self._observed_frame_count += 1
128
+ self._update_peaks()
129
+ self._dirty = True
130
+ self._tail_finalized = False
131
+ family_key = _family_key(frame)
132
+ self._family_counts[family_key] += 1
133
+ self._retained_family_counts[family_key] += 1
134
+ family_count = self._family_counts[family_key]
135
+
136
+ recent = self._family_recent.setdefault(family_key, deque(maxlen=10))
137
+ recent.append(frame)
138
+ if len(recent) == 10 and _rank_reset_starts(tuple(recent)) == (0,):
139
+ first_count = family_count - 9
140
+ targets = self._family_targets.setdefault(family_key, set())
141
+ targets.update(
142
+ {
143
+ first_count + 49,
144
+ first_count + 199,
145
+ first_count + 249,
146
+ first_count + 299,
147
+ first_count + 309,
148
+ }
149
+ )
150
+ reset_interval = family_count in self._family_targets.get(
151
+ family_key,
152
+ (),
153
+ )
154
+ if reset_interval:
155
+ self._family_targets[family_key].discard(family_count)
156
+
157
+ ranked_known = self.result.ranked_candidate is not None
158
+ rich_interval = family_count in {
159
+ DISCOVERY_MIN_RICH_RECORDS // 2,
160
+ 250,
161
+ 300,
162
+ 310,
163
+ }
164
+ related_interval = (
165
+ ranked_known
166
+ and family_count >= DISCOVERY_MIN_FAMILY_FRAMES
167
+ and family_count in {10, 50}
168
+ )
169
+ if (
170
+ family_count == 24
171
+ or rich_interval
172
+ or related_interval
173
+ or reset_interval
174
+ ) and not rolled_over:
175
+ self.refresh(end_of_burst=False)
176
+
177
+ @property
178
+ def complete(self) -> bool:
179
+ return self._confirmed_frames is not None
180
+
181
+ @property
182
+ def confirmed_frames(self) -> Optional[tuple[BDOFrame, ...]]:
183
+ """Exact first structurally confirmed window, latched permanently."""
184
+
185
+ return self._confirmed_frames
186
+
187
+ @property
188
+ def retained_frames(self) -> tuple[BDOFrame, ...]:
189
+ """Current bounded discovery input, or the exact confirmed window."""
190
+
191
+ return tuple(self._frames)
192
+
193
+ @property
194
+ def retained_frame_count(self) -> int:
195
+ return len(self._frames)
196
+
197
+ @property
198
+ def retained_bytes(self) -> int:
199
+ return self._retained_bytes
200
+
201
+ @property
202
+ def peak_retained_frame_count(self) -> int:
203
+ return self._peak_retained_frame_count
204
+
205
+ @property
206
+ def peak_retained_bytes(self) -> int:
207
+ return self._peak_retained_bytes
208
+
209
+ @property
210
+ def evicted_frame_count(self) -> int:
211
+ return self._evicted_frame_count
212
+
213
+ @property
214
+ def evicted_bytes(self) -> int:
215
+ return self._evicted_bytes
216
+
217
+ @property
218
+ def observed_frame_count(self) -> int:
219
+ return self._observed_frame_count
220
+
221
+ @property
222
+ def history_rolled_over(self) -> bool:
223
+ return self._rollover_warned
224
+
225
+ def refresh(
226
+ self,
227
+ *,
228
+ end_of_burst: bool = True,
229
+ ) -> SolareDiscoveryResult:
230
+ if self._confirmed_frames is not None:
231
+ return self.result
232
+ if not self._dirty and (not end_of_burst or self._tail_finalized):
233
+ return self.result
234
+ candidate = discover_solare(
235
+ self._frames,
236
+ allow_trailing_overall=end_of_burst,
237
+ _analysis_cache=self._analysis_cache,
238
+ )
239
+ candidate = replace(
240
+ candidate,
241
+ scanned_frames=self._observed_frame_count,
242
+ )
243
+ self._dirty = False
244
+ self._tail_finalized = end_of_burst
245
+ if candidate.confirmed:
246
+ self._analysis_cache.clear()
247
+ self.result = candidate
248
+ assert candidate.rich is not None
249
+ assert candidate.overall is not None
250
+ selected: list[BDOFrame] = []
251
+ seen: set[int] = set()
252
+ for frame in (*candidate.rich.frames, *candidate.overall.frames):
253
+ if id(frame) in seen:
254
+ continue
255
+ seen.add(id(frame))
256
+ selected.append(frame)
257
+ for frame in self._frames:
258
+ if id(frame) not in seen:
259
+ self._evicted_frame_count += 1
260
+ self._evicted_bytes += frame.length
261
+ self._confirmed_frames = tuple(selected)
262
+ self._frames = deque(self._confirmed_frames)
263
+ self._retained_bytes = sum(
264
+ frame.length for frame in self._confirmed_frames
265
+ )
266
+ # The selected discovery result and ``_frames`` are the only
267
+ # intentional owners after confirmation. The rolling reset
268
+ # indexes are useful only while acquiring and must not keep
269
+ # evicted or unrelated packet buffers alive behind the public
270
+ # retained-frame/byte accounting.
271
+ self._family_counts.clear()
272
+ self._retained_family_counts.clear()
273
+ self._family_recent.clear()
274
+ self._family_targets.clear()
275
+ else:
276
+ compact = _compact_discovery(candidate)
277
+ if _discovery_score(compact) >= _discovery_score(self._best_result):
278
+ self._best_result = compact
279
+ self.result = replace(
280
+ self._best_result,
281
+ scanned_frames=self._observed_frame_count,
282
+ )
283
+ self._announce_progress()
284
+ return self.result
285
+
286
+ def service_candidate_idle(self, now: float) -> bool:
287
+ """Finalize a quiet candidate tail despite unrelated game traffic.
288
+
289
+ Returns whether this call changed the tracker from provisional to
290
+ structurally complete. The ordinary discovery checks—including rich-
291
+ prefix rejection—remain authoritative at this boundary.
292
+ """
293
+
294
+ if (
295
+ self.complete
296
+ or self._tail_finalized
297
+ or self._last_candidate_activity_at is None
298
+ or now - self._last_candidate_activity_at
299
+ < LIVE_CANDIDATE_IDLE_SECONDS
300
+ ):
301
+ return False
302
+ self.refresh()
303
+ return self.complete
304
+
305
+ def _make_room_for(self, frame: BDOFrame) -> bool:
306
+ if (
307
+ len(self._frames) < DISCOVERY_RETENTION_MAX_FRAMES
308
+ and self._retained_bytes + frame.length
309
+ <= DISCOVERY_RETENTION_MAX_BYTES
310
+ ):
311
+ return False
312
+
313
+ # Evaluate the complete retained window before making room. The
314
+ # low-water marks retain more than the largest structural snapshot, so
315
+ # the incoming frame can still complete a candidate without ownership
316
+ # ever exceeding the advertised hard limits.
317
+ self.refresh(end_of_burst=False)
318
+ if self.complete:
319
+ return True
320
+
321
+ target_frames = DISCOVERY_RETENTION_MAX_FRAMES - max(
322
+ 64,
323
+ DISCOVERY_RETENTION_MAX_FRAMES // 8,
324
+ )
325
+ target_bytes = (DISCOVERY_RETENTION_MAX_BYTES * 7) // 8
326
+ self._analysis_cache.clear()
327
+ while self._frames and (
328
+ len(self._frames) >= target_frames
329
+ or self._retained_bytes + frame.length > target_bytes
330
+ ):
331
+ evicted = self._frames.popleft()
332
+ self._retained_bytes -= evicted.length
333
+ self._evicted_frame_count += 1
334
+ self._evicted_bytes += evicted.length
335
+ key = _family_key(evicted)
336
+ self._retained_family_counts[key] -= 1
337
+ recent = self._family_recent.get(key)
338
+ if recent is not None and any(item is evicted for item in recent):
339
+ kept = (item for item in recent if item is not evicted)
340
+ replacement: Deque[BDOFrame] = deque(kept, maxlen=10)
341
+ if replacement:
342
+ self._family_recent[key] = replacement
343
+ else:
344
+ self._family_recent.pop(key, None)
345
+ if self._retained_family_counts[key] <= 0:
346
+ self._retained_family_counts.pop(key, None)
347
+ self._family_counts.pop(key, None)
348
+ self._family_recent.pop(key, None)
349
+ self._family_targets.pop(key, None)
350
+
351
+ if not self._rollover_warned:
352
+ self._rollover_warned = True
353
+ self._emit(
354
+ SolareUpdate(
355
+ kind=SolareUpdateKind.WARNING,
356
+ message=(
357
+ "Solare discovery history reached its bounded retention "
358
+ "window; older candidates were discarded after evaluation"
359
+ ),
360
+ )
361
+ )
362
+ self._update_peaks()
363
+ return True
364
+
365
+ def _update_peaks(self) -> None:
366
+ self._peak_retained_frame_count = max(
367
+ self._peak_retained_frame_count,
368
+ len(self._frames),
369
+ )
370
+ self._peak_retained_bytes = max(
371
+ self._peak_retained_bytes,
372
+ self._retained_bytes,
373
+ )
374
+
375
+ def _announce_progress(self) -> None:
376
+ result = self.result
377
+ menu = result.menu_context
378
+ if menu is not None and not self._announced_menu:
379
+ self._announced_menu = True
380
+ self._emit(
381
+ SolareUpdate(
382
+ kind=SolareUpdateKind.MENU_CONTEXT,
383
+ message=(
384
+ "Solare menu/history-like traffic was observed, but no "
385
+ "leaderboard load was sent; if the Leaderboard was "
386
+ "already opened, restart the game and retry capture "
387
+ "before opening it"
388
+ ),
389
+ )
390
+ )
391
+
392
+ ranked = result.ranked_candidate
393
+ if (
394
+ not result.confirmed
395
+ and result.partial_overall is None
396
+ and ranked is not None
397
+ and ranked.record_count >= self._announced_ranked_players + 100
398
+ ):
399
+ self._announced_ranked_players = (
400
+ ranked.record_count // 100
401
+ ) * 100
402
+ self._emit(
403
+ SolareUpdate(
404
+ kind=SolareUpdateKind.RANKED_PROGRESS,
405
+ message=(
406
+ f"{ranked.record_count} unique ranked players recovered; "
407
+ "waiting for class balance and the top-100 cross-check"
408
+ ),
409
+ ranked_players=ranked.record_count,
410
+ )
411
+ )
412
+
413
+ rich = result.rich
414
+ if rich is not None:
415
+ identity = (
416
+ rich.flow,
417
+ rich.frame_length,
418
+ rich.record_stride,
419
+ rich.name_offset,
420
+ rich.rank_offset,
421
+ rich.class_offset,
422
+ )
423
+ if identity != self._announced_rich:
424
+ self._announced_rich = identity
425
+ self._emit(
426
+ SolareUpdate(
427
+ kind=SolareUpdateKind.RICH_CANDIDATE,
428
+ message=(
429
+ f"structurally valid rich table: {rich.record_count} "
430
+ f"players across {len(rich.class_counts)} class groups"
431
+ ),
432
+ ranked_players=rich.record_count,
433
+ )
434
+ )
435
+
436
+ partial = result.partial_overall
437
+ if (
438
+ not result.confirmed
439
+ and ranked is not None
440
+ and partial is not None
441
+ and self._announced_cross_check == 0
442
+ ):
443
+ self._announced_cross_check = result.exact_cross_check
444
+ self._emit(
445
+ SolareUpdate(
446
+ kind=SolareUpdateKind.CROSS_CHECK,
447
+ message=(
448
+ f"{result.exact_cross_check}/{partial.record_count} shared "
449
+ "overall rank/name pairs match; continuing for 100 "
450
+ "overall entries"
451
+ ),
452
+ ranked_players=ranked.record_count,
453
+ overall_players=partial.record_count,
454
+ exact_cross_check=result.exact_cross_check,
455
+ )
456
+ )
457
+
458
+ if result.confirmed and not self._announced_confirmed:
459
+ self._announced_confirmed = True
460
+ assert result.rich is not None
461
+ assert result.overall is not None
462
+ self._emit(
463
+ SolareUpdate(
464
+ kind=SolareUpdateKind.SNAPSHOT_CONFIRMED,
465
+ message=(
466
+ "opcode-agnostic Arena of Solare leaderboard confirmed"
467
+ ),
468
+ ranked_players=result.rich.record_count,
469
+ overall_players=result.overall.record_count,
470
+ exact_cross_check=result.exact_cross_check,
471
+ )
472
+ )
@@ -0,0 +1,182 @@
1
+ """TCP reassembly and health accounting shared by Solare replay/live APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterable
6
+ from typing import Optional
7
+
8
+ from bdo_toolkit._capture_backend import validate_server_ports
9
+ from bdo_toolkit._protocol import BDOFrame
10
+ from bdo_toolkit._reassembly import FlowManager
11
+
12
+ from ._constants import (
13
+ DISCOVERY_MAX_FRAME_LENGTH,
14
+ DISCOVERY_MIN_FRAME_LENGTH,
15
+ SOLARE_MAX_ACTIVE_FLOWS,
16
+ SOLARE_MAX_PENDING_BYTES,
17
+ SOLARE_MAX_PENDING_SEGMENTS,
18
+ )
19
+ from ._scanner import SolareDiscoveryStreamScanner
20
+ from .models import SolareCaptureHealth
21
+
22
+
23
+ class SolareFrameCollector:
24
+ """Collect generic server frames without selecting a Solare opcode.
25
+
26
+ This object implements the segment-consumer protocol used by pcap replay
27
+ and live packet capture. It deliberately counts only inbound payload on
28
+ the selected game-server ports, matching the bytes that can contribute to
29
+ discovery.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ ports: Iterable[int],
35
+ on_frame: Optional[Callable[[BDOFrame], None]] = None,
36
+ on_traffic: Optional[Callable[[], None]] = None,
37
+ *,
38
+ retain_frames: bool = True,
39
+ ) -> None:
40
+ self.ports = validate_server_ports(ports)
41
+ self.frames: list[BDOFrame] = []
42
+ self.payload_segments = 0
43
+ self.payload_bytes = 0
44
+ self.tcp_gap_resets = 0
45
+ self.flow_state_evictions = 0
46
+ self.synchronized_messages = 0
47
+ self._retention_enabled = True
48
+ self._retain_frames = retain_frames
49
+ self._on_frame = on_frame
50
+ self._on_traffic = on_traffic
51
+ self._manager = FlowManager(
52
+ server_ports=self.ports,
53
+ scanner_factory=lambda: SolareDiscoveryStreamScanner(
54
+ self._accept_frame,
55
+ self._count_gap_reset,
56
+ ),
57
+ max_flows=SOLARE_MAX_ACTIVE_FLOWS,
58
+ max_pending_segments=SOLARE_MAX_PENDING_SEGMENTS,
59
+ max_pending_bytes=SOLARE_MAX_PENDING_BYTES,
60
+ on_flow_eviction=self._count_flow_eviction,
61
+ track_flow_generations=True,
62
+ )
63
+
64
+ def _accept_frame(self, frame: BDOFrame) -> None:
65
+ self.synchronized_messages += 1
66
+ if not self._retention_enabled:
67
+ return
68
+ if self._on_frame is not None:
69
+ self._on_frame(frame)
70
+ if (
71
+ self._retain_frames
72
+ and frame.flag == 0
73
+ and DISCOVERY_MIN_FRAME_LENGTH
74
+ <= frame.length
75
+ <= DISCOVERY_MAX_FRAME_LENGTH
76
+ ):
77
+ self.frames.append(frame)
78
+
79
+ def stop_retaining(self) -> None:
80
+ """Freeze discovery input after the first snapshot is latched.
81
+
82
+ Packet capture and health accounting may continue (for example with
83
+ ``stop_on_complete=False``), but later frames cannot change the atomic
84
+ result and therefore must not grow memory without bound.
85
+ """
86
+
87
+ self._retention_enabled = False
88
+
89
+ def _count_gap_reset(self) -> None:
90
+ self.tcp_gap_resets += 1
91
+
92
+ def _count_flow_eviction(self) -> None:
93
+ self.flow_state_evictions += 1
94
+
95
+ def process_tcp_segment(
96
+ self,
97
+ *,
98
+ source_ip: str,
99
+ source_port: int,
100
+ destination_ip: str,
101
+ destination_port: int,
102
+ sequence: int,
103
+ payload: bytes,
104
+ timestamp: float,
105
+ syn: bool = False,
106
+ rst: bool = False,
107
+ fin: bool = False,
108
+ ) -> None:
109
+ if source_port in self.ports and payload:
110
+ self.payload_segments += 1
111
+ self.payload_bytes += len(payload)
112
+ if self._on_traffic is not None:
113
+ self._on_traffic()
114
+ self._manager.process_tcp_segment(
115
+ source_ip=source_ip,
116
+ source_port=source_port,
117
+ destination_ip=destination_ip,
118
+ destination_port=destination_port,
119
+ sequence=sequence,
120
+ payload=payload,
121
+ timestamp=timestamp,
122
+ syn=syn,
123
+ rst=rst,
124
+ fin=fin,
125
+ )
126
+
127
+ def finish(self) -> None:
128
+ self._manager.finish()
129
+
130
+ def service_gaps(self, now: float) -> int:
131
+ """Advance TCP gap clocks after the live decode queue is drained."""
132
+
133
+ return self._manager.service_gaps(now)
134
+
135
+ def health(
136
+ self,
137
+ *,
138
+ saved_packets: int = 0,
139
+ pcap_received: Optional[int] = None,
140
+ pcap_dropped: Optional[int] = None,
141
+ pcap_interface_dropped: Optional[int] = None,
142
+ capture_buffer_bytes: Optional[int] = None,
143
+ retained_large_messages: Optional[int] = None,
144
+ candidate_messages_observed: int = 0,
145
+ candidate_frames_retained: int = 0,
146
+ candidate_bytes_retained: int = 0,
147
+ peak_candidate_frames: int = 0,
148
+ peak_candidate_bytes: int = 0,
149
+ candidate_frames_evicted: int = 0,
150
+ candidate_bytes_evicted: int = 0,
151
+ candidate_history_rolled_over: bool = False,
152
+ packet_queue_peak: int = 0,
153
+ packet_queue_overflows: int = 0,
154
+ flow_state_evictions: int = 0,
155
+ ) -> SolareCaptureHealth:
156
+ return SolareCaptureHealth(
157
+ payload_segments=self.payload_segments,
158
+ payload_bytes=self.payload_bytes,
159
+ synchronized_messages=self.synchronized_messages,
160
+ retained_large_messages=(
161
+ len(self.frames)
162
+ if retained_large_messages is None
163
+ else retained_large_messages
164
+ ),
165
+ tcp_gap_resets=self.tcp_gap_resets,
166
+ pcap_received=pcap_received,
167
+ pcap_dropped=pcap_dropped,
168
+ pcap_interface_dropped=pcap_interface_dropped,
169
+ capture_buffer_bytes=capture_buffer_bytes,
170
+ saved_packets=saved_packets,
171
+ candidate_messages_observed=candidate_messages_observed,
172
+ candidate_frames_retained=candidate_frames_retained,
173
+ candidate_bytes_retained=candidate_bytes_retained,
174
+ peak_candidate_frames=peak_candidate_frames,
175
+ peak_candidate_bytes=peak_candidate_bytes,
176
+ candidate_frames_evicted=candidate_frames_evicted,
177
+ candidate_bytes_evicted=candidate_bytes_evicted,
178
+ candidate_history_rolled_over=candidate_history_rolled_over,
179
+ packet_queue_peak=packet_queue_peak,
180
+ packet_queue_overflows=packet_queue_overflows,
181
+ flow_state_evictions=self.flow_state_evictions + flow_state_evictions,
182
+ )