fix: harden sharkd replay sessions and serve the uncapped frame list

Review-driven session/transport fixes (each reproduced live against
sharkd 4.6.7 before fixing):

- raise the RPC stream limit to 16 MB: a full 1000-row frames page
  measures ~190 KB against the 64 KB StreamReader default, which failed
  the request with a 500 and desynchronized the resident session; a
  line-over-limit ValueError is now treated as a transport failure
- verify JSON-RPC reply ids: a timed-out request's late reply was
  served as the next request's answer; timeouts, dead pipes, malformed
  and stale replies now kill the session for good instead
- make check-spawn atomic under one manager lock: concurrent requests
  for the same pcap double-spawned sharkd and leaked the loser (process
  plus /tmp scratch copy) forever
- refcount sessions and evict idle only (LRU, cap raised 8 -> 16): a
  tag with more sources than the cap respawned every source on every
  request, and concurrent requests could get their session killed
  mid-RPC (spurious 502)
- map FilterError to sharkd's filter rejection (-13002) only; other
  engine failures with a filter set are 502, not a client 400
- detail: accept an optional frame_number to disambiguate
  same-microsecond frames (ts is not unique within a pcap); drop the
  -8003 -> 404 mapping (the range is validated locally, engine errors
  are real faults); a failed hex read is a 404 instead of "hex": null
- a pcap deleted mid-request is a 404, not a 500; the pcap-sized
  scratch copy runs off the event loop; server shutdown kills every
  resident session and drops its scratch directory
- pin the packet-list layout through scratch-HOME Wireshark
  preferences: the column indexes are a contract the server owns
  (protocol-level column negotiation is rejected by sharkd 4.6.x)

Range contract change (WebUI moved to an always-flat list): the merged
frame list is returned in full, deliberately uncapped - truncated and
per-second buckets are removed, frame_count always equals
len(frames), and rendering cost is the client's concern (the window
endpoint remains the incremental path).
This commit is contained in:
YueGuobin committed 2026-09-11 00:55:18 +08:00
1 parent ef5d81498a
commit 5d91ca0efc
6 files changed
+627 -211

No files matched your search

+12 -6
View File
@@ -253,11 +253,12 @@ async def replay_tag_range(
The tag gate applies: every marker under the tag must be paused
(``enabled: false``) — 409 otherwise. The response carries the timeline
bounds, per-source stats, and the full merged frame list while under the
frame cap (5000); above it the list is replaced by per-second buckets.
bounds, per-source stats, and the full merged frame list — deliberately
uncapped (rendering a huge list is the client's concern; the window
endpoint exists for incremental views).
``filter`` is an optional Wireshark display filter applied **before**
counting and slicing — start / end / frame_count / frames | buckets are
counting and slicing — start / end / frame_count / frames are
all computed on the matching frames only. An invalid expression is a 400
carrying sharkd's original error text (for inline display in the UI
filter bar).
@@ -321,6 +322,7 @@ async def replay_tag_frame_detail(
node_id: str,
link_id: str,
marker: str,
frame_number: Optional[int] = None,
project: Project = Depends(dep_project),
) -> dict:
"""
@@ -332,14 +334,18 @@ async def replay_tag_frame_detail(
``children``) — values untouched.
``ts`` must be the exact string from the frame list; ``node_id`` +
``link_id`` + ``marker`` identify the source pcap. The tag gate applies.
Requires sharkd — 501 without it.
``link_id`` + ``marker`` identify the source pcap. ``frame_number``
(optional, from the frame list entry) disambiguates same-microsecond
frames on one link — without it the first ts match decodes. The tag gate
applies. Requires sharkd — 501 without it.
Required privilege: Project.Audit
"""
return await _replay_response(
marker_replay.decode_frame(project, tag, ts, node_id, link_id, marker)
marker_replay.decode_frame(
project, tag, ts, node_id, link_id, marker, frame_number=frame_number
)
)