@hyzyn/dsh-docker 0.9.2 → 0.9.3
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.
- package/README.en.md +78 -7
- package/README.md +91 -13
- package/client-src/docker.css +22 -3
- package/client-src/index.js +448 -56
- package/client-src/log-stream.js +15 -0
- package/client-src/log-window.js +244 -0
- package/client.js +45 -26
- package/lib/index.d.ts +33 -0
- package/lib/index.js +218 -34
- package/lib/index.js.map +1 -1
- package/lib/ssh-exec.d.ts +57 -7
- package/lib/ssh-exec.js +227 -78
- package/lib/ssh-exec.js.map +1 -1
- package/package.json +3 -3
- package/scripts/client-smoke.mjs +126 -10
- package/scripts/log-perf.mjs +327 -10
package/README.en.md
CHANGED
|
@@ -250,7 +250,7 @@ Inside the panel:
|
|
|
250
250
|
**SSE push** (`GET /api/dsh-docker/logs/stream`, where the server runs
|
|
251
251
|
`docker logs --follow`) — new log lines are appended as they arrive and polling stops; `FOLLOW` and
|
|
252
252
|
`AUTO REFRESH` are mutually exclusive (opening the stream stops polling and greys out the switch), and closing it
|
|
253
|
-
returns to snapshots with an immediate refresh. Streaming logs keep the last **5000 lines / 4MB** (a ring buffer that drops the oldest on either cap; newline-free oversized output is force-split so memory stays bounded). Chunks render at most every **150ms** (no per-chunk re-render on chatty containers) and rows carry stable ids, so
|
|
253
|
+
returns to snapshots with an immediate refresh. Streaming logs keep the last **5000 lines / 4MB** (a ring buffer that drops the oldest on either cap; newline-free oversized output is force-split so memory stays bounded). Chunks render at most every **150ms** (no per-chunk re-render on chatty containers) and rows carry stable ids. The body is **windowed (D152)**: only the visible rows plus 24 rows of overscan on each side are mounted (about 230 nodes for 5000 rows, previously ~10000), the rest is represented by top/bottom padding — under a sustained 20k lines/s flood the max event-loop lag drops from 70–97ms to 6ms and >50ms long frames from 25–26 to **0**. Row heights are **measured** (not estimated) and the window anchors to the tail while pinned, so auto-scroll / scroll-up pause / "back to bottom" keep exact geometry; scrolling up through history is compensated by a top-of-viewport anchor recorded **across commits**, so it does not jump even while the ring buffer keeps evicting old rows (or the aggregate view inserts a row by timestamp). **Switching streams / containers / refreshing a snapshot invalidates that whole generation of measured heights and anchors (D155)**: snapshot row ids are positional (`'s'+index`), so the same id is different content after a refresh — keeping them would compute the padding from the wrong generation's heights (measured: `scrollHeight` inflated by 35644px), so the scrollbar, jumps and "which row is at the top" all describe the wrong generation. The view does not truncate: whatever `LINES` selects is scrollable and exported — it is simply not all mounted at once (bounded by the buffer and the host output cap). Auto-scroll to bottom,
|
|
254
254
|
drops the oldest and hints once); filtering / level colouring share exactly the same rendering as snapshots. It
|
|
255
255
|
auto-scrolls to the bottom, pauses when the user scrolls up and floats a
|
|
256
256
|
"back to bottom" button; a status line in the top right shows the connection state, and a stream that ends
|
|
@@ -259,8 +259,15 @@ Inside the panel:
|
|
|
259
259
|
carries `tail` to backfill history, while every reconnect uses `tail=0` — new lines only, **never replaying
|
|
260
260
|
history** (auto-reconnect reuses the URL with its `tail`, so the server pushes the last `tail` lines again as
|
|
261
261
|
if they were new, and the log grows a duplicated block). When the host-side backpressure queue (8MB)
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
**Host-side backpressure (D153)**: each stream buffers at most 8MB; the text tail streams (logs, pulls)
|
|
263
|
+
drop the oldest frames and keep the newest when they overflow, sending a `skip{frames,bytes}` notice so the UI
|
|
264
|
+
can say "a stretch is missing, following continues" — the stream is **not** torn down (the old policy sent
|
|
265
|
+
`end{reason:output-limit}` and closed, which left the panel stuck in "connection lost" while the container kept
|
|
266
|
+
flooding). The skip accounting is **exact**: when more frames are dropped while a queued `skip` notice has not
|
|
267
|
+
been written yet, the server updates that notice **in place** (D156 — no gap goes unreported); the client
|
|
268
|
+
**accumulates** consecutive skips and shows "N batches skipped on this connection", resetting on a successful
|
|
269
|
+
reconnect (D157). Structured streams (stats, events) still close and reconnect, because a missing sample/event is
|
|
270
|
+
semantically wrong.
|
|
264
271
|
Connecting / switching pages / closing the panel all close the `EventSource`.
|
|
265
272
|
- **Overview**: `docker inspect`'s authoritative data — state and health, exit code, restart count and policy,
|
|
266
273
|
port mappings, mounts (including read-only flags), networks and IPs, entrypoint and command, and the latest
|
|
@@ -427,6 +434,22 @@ The "Overview" page lays out all targets on one screen: one counter card per tar
|
|
|
427
434
|
|
|
428
435
|

|
|
429
436
|
|
|
437
|
+
Where the settings surface **lives** depends on the DSH version, but it is always the **same form**
|
|
438
|
+
(same settings namespace, same write path) — the data and behaviour are identical:
|
|
439
|
+
|
|
440
|
+
| Host | Where the settings are |
|
|
441
|
+
|---|---|
|
|
442
|
+
| `0.2.0-rc.1` and later | **Directly below the description on the plugin detail page** (`plugins.bundle.config`, key = **this package's own** bundle name) — no extra ">" step into a sub-page |
|
|
443
|
+
| `0.1.6` line | Plugins sidebar → the bundle's **row** → the ">" row detail (`plugins.row.config`) |
|
|
444
|
+
| `≤0.1.5` | Settings → Plugins → the plugin's collapsible card (`settings.plugin.item`) |
|
|
445
|
+
| All of the above | Settings → **plugin configuration**, the same entry (`settings.kit.item`, provided by `@hyzyn/dsh-kit-settings`) |
|
|
446
|
+
|
|
447
|
+
On hosts that have the bundle slot, **this package's** row entry is **no longer registered** — two entries
|
|
448
|
+
for one form only makes people think there are two sets of settings; hosts without it (the bundle slot
|
|
449
|
+
missing) fall back to the row detail automatically, with no loss of function. The **aggregate bundle
|
|
450
|
+
`@hyzyn/dsh-all` is the exception**: its `plugins.bundle.config` key is shared by every plugin (a second
|
|
451
|
+
registrant throws), so it has no inline slot and keeps using its row entry.
|
|
452
|
+
|
|
430
453
|
Configuration lives in the settings namespace `docker`, i.e. the `docker:` section of `~/.dsh/settings.yaml`
|
|
431
454
|
(`$DSH_HOME/settings.yaml`; DSH's settings file is provided by the host's `dsh-settings-file`). The composition
|
|
432
455
|
config in the plugin line acts as the schema's `base`, which the settings layer overrides; the HTTP
|
|
@@ -616,6 +639,19 @@ The four streams differ only in "executor + end reason":
|
|
|
616
639
|
no-cache`, `connection: keep-alive`, with `flushHeaders()` immediately after writing them (the host's gzip
|
|
617
640
|
explicitly skips `text/event-stream`, so nothing is buffered).
|
|
618
641
|
- Heartbeat: one `: ping` comment frame every 15s (ignored by clients per the SSE spec).
|
|
642
|
+
- **Chunk coalescing (D151)**: the log and pull streams merge upstream chunks over a 50ms window before pushing
|
|
643
|
+
them (strict ordering within a channel, stdout before stderr, an immediate flush once 256KB has accumulated), so a
|
|
644
|
+
`line` frame's `d` / `e` may carry **several lines**. The client already splits on arbitrary chunks, so the
|
|
645
|
+
semantics are unchanged — what disappears is "one SSE frame per stdout chunk" (with a chatty container that is
|
|
646
|
+
thousands of frames per second, and every frame's fixed cost lands on the browser's main thread). The `end` frame
|
|
647
|
+
always comes after the last batch of `line` frames in the window.
|
|
648
|
+
- **Backpressure and overflow (D153)**: when the client cannot keep up, frames go into an in-memory queue
|
|
649
|
+
(≤ 8MB per stream). The log and pull streams (text tails) drop the oldest whole frames and keep the newest on
|
|
650
|
+
overflow, pushing a `skip{frames,bytes}` notice (`skip` itself is never dropped) — the stream stays alive. When
|
|
651
|
+
more frames are dropped while a queued `skip` has not been written yet, its counters are updated **in place**
|
|
652
|
+
(D156 — reporting only the first gap would undercount). The
|
|
653
|
+
stats and events streams send `end{reason:output-limit}` and reconnect instead. Also, `write()` returning false
|
|
654
|
+
only means "past the high-water mark": a frame is **never written twice** (that duplication was a real bug).
|
|
619
655
|
- Teardown: the client disconnects → the executor is aborted immediately (locally `SIGTERM`, then `SIGKILL` if it has
|
|
620
656
|
not exited in 2s; over SSH that exec channel is closed and the pooled connection is kept for reuse), writing no
|
|
621
657
|
frame at all; the plugin is disabled / the config is hot-updated / it is uninstalled → the server wraps up on its
|
|
@@ -876,14 +912,49 @@ ring buffer / action labels / debounce** (pure logic through the `__events` test
|
|
|
876
912
|
the entry label when the sidebar is collapsed (`data-sidebar-collapsed`).
|
|
877
913
|
Verification that needs a real daemon follows the manual checklist below.
|
|
878
914
|
|
|
879
|
-
`test/logs-stream.test.ts` (
|
|
915
|
+
`test/logs-stream.test.ts` (37 cases, run by the root `pnpm test`) covers four layers of the live log stream:
|
|
880
916
|
`logsStream`'s argv construction and `assertRef` allowlist, the single-line JSON encapsulation of SSE frames
|
|
881
917
|
(newlines / multi-byte), the local stream lifecycle (fake spawn: multi-byte across chunks, the SIGTERM→SIGKILL
|
|
882
918
|
ladder, close resolve, spawn error) plus the SSH long stream's busy-count pairing / sweeper skip, and the route
|
|
883
919
|
layer's event sequence / heartbeat / silent abort when the client disconnects / uniform wrap-up when the plugin is
|
|
884
|
-
disabled
|
|
885
|
-
|
|
886
|
-
`
|
|
920
|
+
disabled — plus **chunk coalescing** (D151: ten chunks produce a single `line` frame; when the stream ends before the
|
|
921
|
+
window elapses, `end` still comes after those rows) and **exact skip accounting** (D156: frames dropped while a queued
|
|
922
|
+
`skip` has not been written yet must still be reported — a 46-frame ledger where every frame is either delivered or
|
|
923
|
+
accounted for). The coalescer's own rules (window merge / flush on the size cap /
|
|
924
|
+
idempotent flush / dispose stops the timer) live in `test/sse-coalesce.test.ts` (6 cases).
|
|
925
|
+
|
|
926
|
+
`test/log-window.test.ts` (17 cases) covers the **pure windowing logic** (D152, `client-src/log-window.js`):
|
|
927
|
+
measured heights in the cache vs the estimate for rows never measured, window placement and self-consistent
|
|
928
|
+
padding for the non-pinned case, the pinned case anchoring to the tail with zero bottom padding (the premise
|
|
929
|
+
for exact stick-to-bottom), `reanchor` only compensating for height changes above the anchor, `offsetOf` and
|
|
930
|
+
"compensation after head eviction = the evicted height" (the cross-generation anchor math behind D154), cache
|
|
931
|
+
FIFO eviction and empty-list edges. The real-DOM layer (measuring, anchor correction, pinning) is verified by the
|
|
932
|
+
performance gate in real Chrome; offline there is also the `__logWindow` seam in `scripts/client-smoke.mjs`.
|
|
933
|
+
|
|
934
|
+
`pnpm --filter @hyzyn/dsh-docker perf:logs` (`scripts/log-perf.mjs`) is the **browser-side performance gate for the
|
|
935
|
+
log page**: a real Chrome measures four scenarios — the 5000-row first paint (time / DOM size), the 20000-row FOLLOW
|
|
936
|
+
burst (processing time and max event-loop lag), the **event flood** (the same 6.2k lines/s as 1 line per event vs 50
|
|
937
|
+
lines per event — this is the per-event fixed cost D151 removed) and the **sustained flood** (20k lines/s for 6s; once
|
|
938
|
+
the buffer is full every frame evicts and inserts, which is the "the page janks as soon as logs get fast" users
|
|
939
|
+
report). It also checks two things users tend to misread: the snapshot must show the yellow "truncated" banner whenever
|
|
940
|
+
the "Output limit (KB)" byte cap bites (otherwise "LINES = 1000 returned only 985 rows" looks like data loss — that
|
|
941
|
+
985 is docker's own record granularity), it also asserts that "scroll-up pauses following" reacts to **real gestures only** (wheel / touch /
|
|
942
|
+
scrollbar / keyboard: a gesture must pause and one click must resume, while positional drift with no gesture must
|
|
943
|
+
*not* stop following), and it simulates the **backlog → reconnect** path (D153/D152: after a host-side `output-limit` the body must
|
|
944
|
+
still show rows, following must not silently stop, and after the automatic reconnect the last mounted row must be
|
|
945
|
+
the newest), it **parks mid-history during a flood** (D154: while the ring buffer keeps evicting, the top visible row
|
|
946
|
+
must stay the same row for 1.2s — uncompensated eviction makes reading history drift like an automatic rewind), it checks the **generation switch** (D155: a snapshot refreshed into a generation of taller wrapped rows and then back to normal rows must have its padding computed from the *current* generation — a stale cache inflates `scrollHeight` by 35644px), and **checks the window is
|
|
947
|
+
correct**: the snapshot content is deterministic, so scrolling to the top must show `seq=0` as the first row,
|
|
948
|
+
scrolling to the bottom `seq=4999` as the last, and after the flood the last mounted row must be the newest one
|
|
949
|
+
(stick-to-bottom still following). Over budget means a non-zero exit. Measured after D152: 5000-row snapshot
|
|
950
|
+
179ms / **229 body nodes** (previously ~10000) with 53–57 rows mounted; 20k-row burst 933ms / max lag **2ms**
|
|
951
|
+
(previously 52–97ms); event flood 10ms vs 7ms; sustained 20k lines/s × 6s max lag **6ms** with **0** frames >50ms
|
|
952
|
+
(previously 70–97ms / 25–26) and 213 nodes. The budget also caps mounted rows at 200 — if windowing breaks, that
|
|
953
|
+
turns red immediately. It depends on the tty preview fixtures
|
|
954
|
+
(generated by `node packages/tty/scripts/preview.mjs`) and **skips with exit 0** when the fixtures or Chrome are
|
|
955
|
+
missing — it is an npm script, not a required vitest entry.
|
|
956
|
+
|
|
957
|
+
`test/streams.test.ts` (35 cases) covers the **stats stream / event stream / pull stream / networks and volumes /
|
|
887
958
|
generic SSE infrastructure**: `statsStream` without `--no-stream` (the same construction point as the snapshot),
|
|
888
959
|
`pullStream`'s `assertImageRef` allowlist, `/stats/stream` normalising line-by-line JSON (including half lines
|
|
889
960
|
across chunks) into `stats` events with the same shape as the `/stats` snapshot, heartbeats, silent abort when the
|
package/README.md
CHANGED
|
@@ -224,14 +224,28 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
224
224
|
`AUTO REFRESH` 互斥(开流自动停轮询、开关置灰),关闭即回到快照并立即刷新。
|
|
225
225
|
流式日志保留最近 **5000 行 / 4MB**(环形缓冲,行数或字节超限都丢最旧并提示
|
|
226
226
|
一次;没有换行符的超长输出会被强制分片,内存恒有界);分片到达按 **150ms 合帧**
|
|
227
|
-
渲染(话痨容器也不再逐条重绘),行 key 用单调 id
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
渲染(话痨容器也不再逐条重绘),行 key 用单调 id。
|
|
228
|
+
**正文按窗口化挂载(D152)**:DOM 里只留「可见 + 上下各 24 行 overscan」的几十行
|
|
229
|
+
(实测 5000 行时约 230 个节点,此前是全量 ~10000),其余用上下垫高占位——持续洪泛
|
|
230
|
+
20k 行/秒时事件循环最大延迟从 70~97ms 降到 6ms、>50ms 长帧从 25~26 个降到 **0**。
|
|
231
|
+
行高是**实测**的(不是估算),贴底时窗口锚在列表尾部,所以「自动贴底」「上滚暂停」
|
|
232
|
+
「回到底部」的几何判定依旧精确;往上滚历史时按可见区顶行做**跨帧记锚**的修正——
|
|
233
|
+
洪泛里缓冲持续淘汰旧行、或聚合视图按时间插行,都不会跳。**换流 / 换容器 / 快照刷新
|
|
234
|
+
会整代作废这批实测高度与锚点(D155)**:快照行 id 是位置寻址(`'s'+index`),刷新后
|
|
235
|
+
同一个 id 就是另一行内容——不换代就会拿另一代的高度算垫高(实测滚动高虚高 35644px),
|
|
236
|
+
滚动条、跳转、「滚到顶看到第几行」全部按错的一代算。
|
|
237
|
+
显示层**不截断**:`LINES` 选多少就是多少——全部行都能滚到、都能导出(真正的闸是
|
|
238
|
+
缓冲与宿主的「输出上限(KB)」),只是不再同时挂在 DOM 里。自动滚动到底部,用户向上滚动时暂停并浮出
|
|
230
239
|
「回到底部」按钮;右上状态行显示连接状态,容器退出导致流自然结束时自动切回快照刷新。**断线重连由插件自己管**(不依赖
|
|
231
240
|
EventSource 的自动重连):首连带 tail 补历史,重连一律 tail=0——只补新行、**不
|
|
232
241
|
重放历史**(自动重连会复用带 tail 的 URL,服务端就会把最后 tail 行当新行重推一遍,
|
|
233
|
-
|
|
234
|
-
|
|
242
|
+
日志里凭空多出一段重复)。**宿主侧背压(D153)**:单条流的暂存队列上限 8MB;日志与拉取
|
|
243
|
+
这类**文本尾部流**溢出时丢最旧的帧、留最新,并推一条 `skip{frames,bytes}` 让界面说明
|
|
244
|
+
「中间断了一截,跟随继续」——**不再掐流**(老策略是补一条 `end{reason:output-limit}` 收尾,
|
|
245
|
+
客户端只能重连,积压期间面板就是「连接中断」在打转)。skip 的账目是**精确**的:skip 帧还在
|
|
246
|
+
队列里没落地时又发生丢弃,服务端**原地更新**它的计数(D156,一段缺口都不吞);客户端把
|
|
247
|
+
多批 skip **累加**显示成「本次连接共跳过 N 批」,重连成功即清零(D157)。统计 / 事件这类结构化流仍走收尾重连
|
|
248
|
+
(少一个采样/事件在语义上是错的,宁可让客户端补一次)。
|
|
235
249
|
连接 / 切页 / 关面板都会关闭 `EventSource`。
|
|
236
250
|
- **概览**:`docker inspect` 的权威数据——状态与健康、退出码、重启次数与策略、
|
|
237
251
|
端口映射、挂载(含只读标记)、网络与 IP、entrypoint 与命令、最近一次健康
|
|
@@ -397,6 +411,21 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
397
411
|
|
|
398
412
|

|
|
399
413
|
|
|
414
|
+
设置面在宿主里的**位置**随 DSH 版本变,但**始终是同一份表单**(同一个 settings 命名空间、
|
|
415
|
+
同一个写入通道),配置数据与行为逐字节相同:
|
|
416
|
+
|
|
417
|
+
| 宿主 | 设置面在哪 |
|
|
418
|
+
|---|---|
|
|
419
|
+
| `0.2.0-rc.1` 起 | **插件详情页「说明」正下方**(`plugins.bundle.config`,key = **本包自己的** bundle 包名)——设置就在详情页里,不必再点一次「>」进二级页 |
|
|
420
|
+
| `0.1.6` 线 | 侧边栏「插件」→ 该 bundle 的**行** → 「>」行详情(`plugins.row.config`) |
|
|
421
|
+
| `≤0.1.5` | 设置 → 插件 → 该插件的可折叠卡片(`settings.plugin.item`) |
|
|
422
|
+
| 以上各版 | 设置 → **插件配置** 里的同一条目(`settings.kit.item`,由 `@hyzyn/dsh-kit-settings` 提供) |
|
|
423
|
+
|
|
424
|
+
新宿主上 bundle 槽可用时**不再注册本包那份 row 槽**——同一份表单两个入口只会让人以为有两套设置;
|
|
425
|
+
旧宿主(bundle 槽不存在)自动回退到行详情,功能一点不减。**聚合包 `@hyzyn/dsh-all` 例外**:它的
|
|
426
|
+
`plugins.bundle.config` key 是所有插件共享的(重复注册会直接抛错),所以挂不了内联位,它在聚合
|
|
427
|
+
安装下继续走 row 入口。
|
|
428
|
+
|
|
400
429
|
配置落在 settings 命名空间 `docker`,即 `~/.dsh/settings.yaml` 的 `docker:`
|
|
401
430
|
段(`$DSH_HOME/settings.yaml`;DSH 的 settings 文件由宿主 `dsh-settings-file`
|
|
402
431
|
提供)。插件行里的 composition 配置作为 schema `base` 打底,settings 层覆盖
|
|
@@ -580,6 +609,16 @@ abort)、客户端断开静默中止。各自只差执行器与结束原因:
|
|
|
580
609
|
no-cache`、`connection: keep-alive`,写头后立即 `flushHeaders()`(宿主 gzip
|
|
581
610
|
对 `text/event-stream` 显式跳过,不会缓冲)。
|
|
582
611
|
- 心跳:每 15s 一帧 `: ping` 注释(SSE 规范里客户端忽略)。
|
|
612
|
+
- **背压与溢出(D153)**:客户端消费不过来时帧进内存队列(每条流 ≤ 8MB);日志 / 拉取流溢出
|
|
613
|
+
时丢最旧的整帧、只留最新,并推一条 `skip{frames,bytes}`(`skip` 帧自身永不丢)——**流不断**;
|
|
614
|
+
skip 排队期间再发生丢弃时**原地更新**这条 skip 的账目(D156,两段缺口只报出一段就是少报)。
|
|
615
|
+
统计 / 事件流溢出时补 `end{reason:output-limit}` 收尾,由客户端重连。另外 `write()` 返回
|
|
616
|
+
false 只代表「越过高水位」,帧**不会被重写**(老实现的重复帧就是这里来的)。
|
|
617
|
+
- **分片合帧(D151)**:日志流与拉取流把上游分片按 50ms 窗口合并后再推(同一通道严格保序、
|
|
618
|
+
stdout 先于 stderr、攒满 256KB 立刻推),所以 `line` 帧的 `d` / `e` 里可能有**多行**——
|
|
619
|
+
客户端本来就是按「任意分片」切行的,语义不变,只是「一个 stdout chunk 一帧」没了(话痨
|
|
620
|
+
容器每秒几千帧时,每帧的固定开销全砸在浏览器主线程上)。`end` 帧一定排在窗口里最后一批
|
|
621
|
+
`line` 之后。
|
|
583
622
|
- 清理:客户端断开 → 立即中止执行器(本机 `SIGTERM`,2s 未退再 `SIGKILL`;
|
|
584
623
|
SSH 关闭该 exec channel、连接池连接保留复用),不写任何帧;插件禁用 / 配置
|
|
585
624
|
热更新 / 卸载 → 服务端主动收尾(abort + `end`)。
|
|
@@ -652,6 +691,17 @@ abort)、客户端断开静默中止。各自只差执行器与结束原因:
|
|
|
652
691
|
都是带指向性的提示,而不是 ssh2 的原始文案。若你的 sshd 调过 `MaxSessions`
|
|
653
692
|
(`sshd -T | grep maxsessions`),当前上限是编译期常量,需要跟着改就提 issue。
|
|
654
693
|
折叠右侧栏标签会主动断流、把通道还回去。
|
|
694
|
+
(`D150`)这条约束另有两道补救:① **短命令按目标排队** —— 每条连接同时最多 2 条
|
|
695
|
+
并发(`DSH_DOCKER_SHORT_CHANNELS` 可覆盖,仅测试 / 排障),多出来的等前一条收尾,
|
|
696
|
+
不再出现「一次点击(概览 inspect + 日志快照 + 统计快照)就去撞 10 的硬上限」;
|
|
697
|
+
② **额度满会自愈** —— 远端回 `Channel open failure: open failed`(sshd 侧原文
|
|
698
|
+
`error: no more sessions`)时,插件丢掉并**关闭**那条连接、重建一条再重试一次,
|
|
699
|
+
宿主日志留一行 `通道额度已满:已重建连接重试`。之所以必须重建:这条连接**不会自己
|
|
700
|
+
恢复**(中止长流时发的 `signal('KILL')` 在部分 sshd 上被拒绝,而 sshd 在子进程仍活着
|
|
701
|
+
时延迟释放 session 槽)。代价是该连接上正在跟随的流会断开、由面板 SSE 自动重连
|
|
702
|
+
(日志流按 `D133` 用 `tail=0` 续尾)。**根治办法在 sshd 一侧**:把那台主机的
|
|
703
|
+
`MaxSessions` 调大 —— 例如 `/etc/ssh/sshd_config.d/10-dsh-maxsessions.conf` 写一行
|
|
704
|
+
`MaxSessions 50`,再 `sshd -t && systemctl reload ssh`(reload 只影响新连接)。
|
|
655
705
|
- **docker CLI 版本差异**:解析走 `--format '{{json .}}'`,字段随版本增减,
|
|
656
706
|
解析器一律降级而不抛异常(例如 `State` 缺失就从 `Status` 推导状态,健康态
|
|
657
707
|
从 `(healthy)` / `(unhealthy)` 提取);缺字段时对应列可能为空,需要权威
|
|
@@ -829,20 +879,48 @@ loopback 403(含三条流路由)、容器名 / 镜像引用注入尝试被
|
|
|
829
879
|
卡片 key 等于命名空间 `docker`、找不到宿主侧边栏时安静降级且卸载可重复调用,ttyConnbar 集成的四条路径(连接簿名命中 / host:port 命中 / 未配置主机不加按钮 / tty 未安装静默跳过),FOLLOW 的 SSE 订阅与「回到底部」交互(静态断言),**镜像详情 / 拉取流 / 删除 / prune 入口**、**统计 FOLLOW + sparkline 钩子**、**Compose 分组与聚合日志**、**「活动」条装配 + 事件环形缓冲 / 动作标签 / 防抖**(纯逻辑经 `__events` 测试缝),以及侧边栏折叠态(`data-sidebar-collapsed`)隐藏入口标签的样式规则。
|
|
830
880
|
需要真 daemon 的验证走下面的手工清单。
|
|
831
881
|
|
|
832
|
-
`test/logs-stream.test.ts`(
|
|
882
|
+
`test/logs-stream.test.ts`(37 例,随根 `pnpm test` 跑)覆盖日志实时流的四层:
|
|
833
883
|
`logsStream` 的 argv 构造与 `assertRef` 白名单、SSE 帧的单行 JSON 封装(换行 /
|
|
834
884
|
多字节)、本地流生命周期(假 spawn:跨 chunk 多字节、SIGTERM→SIGKILL 阶梯、
|
|
835
885
|
close resolve、spawn error)与 SSH 长流的 busy 计数配对 / sweeper 跳过、以及
|
|
836
886
|
路由层的事件序列 / 心跳 / 客户端断开静默中止 / 插件禁用统一收尾,以及
|
|
837
|
-
**`tail=0`
|
|
887
|
+
**`tail=0` 只跟随不补历史**(D133)、**背压不重写已写过的帧**与**积压时丢最旧 + `skip` 通知、
|
|
888
|
+
不掐流**(D153)、**skip 账目精确**(D156:skip 排队期间再丢弃,两段缺口都要报——
|
|
889
|
+
46 块逐块对账「每一帧要么送达、要么被 skip 记账」)、
|
|
890
|
+
以及**分片合帧**(D151:十个分片只出一个 `line` 帧;未到窗口就收尾时 `end` 仍排在
|
|
891
|
+
这些行之后)。合帧器本身的规则(窗口合并 / 攒满即推 / flush 幂等 / dispose 停表)在
|
|
892
|
+
`test/sse-coalesce.test.ts`(6 例)。
|
|
893
|
+
|
|
894
|
+
`test/log-window.test.ts`(17 例)覆盖**窗口化挂载的纯逻辑**(D152,`client-src/log-window.js`):
|
|
895
|
+
实测高度进缓存而没量过的行用估算、`layout` 非贴底时的窗口定位与上下垫高自洽、贴底时锚到
|
|
896
|
+
列表尾部且下垫为 0(贴底判定精确的前提)、`reanchor` 只在锚点上方行高变化时产生等量补偿、
|
|
897
|
+
`offsetOf` 与「淘汰头部后补偿量 = 被淘汰行总高」(D154 跨代锚点的数学)、
|
|
898
|
+
缓存 FIFO 淘汰与空表边界。真 DOM 那一层(量高、锚点修正、贴底钉住)由性能门禁在真 Chrome
|
|
899
|
+
里验证,离线侧另有 `scripts/client-smoke.mjs` 的 `__logWindow` 缝。
|
|
838
900
|
|
|
839
901
|
`pnpm --filter @hyzyn/dsh-docker perf:logs`(`scripts/log-perf.mjs`)是**日志页的浏览器侧
|
|
840
|
-
性能门禁**:用真 Chrome
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
902
|
+
性能门禁**:用真 Chrome 量四个场景——「5000 行首屏耗时 / DOM 规模」、「FOLLOW 突发 20000 行
|
|
903
|
+
的处理耗时与事件循环最大延迟」、「事件洪泛」(同一 6.2k 行/秒下每事件 1 行 vs 每事件 50 行,
|
|
904
|
+
量的就是 D151 合掉的那笔每事件固定开销)、以及「**持续洪泛** 20k 行/秒 × 6s」的稳态
|
|
905
|
+
(缓冲满了之后每帧都在淘汰 + 插入,这才是用户报的「日志一快整页就卡」),并**校验两件容易被误解的事**:① 快照被「输出上限(KB)」字节闸截断时**必须有**「已截断」黄色横幅
|
|
906
|
+
(否则「LINES 选 1000 只回来 985 行」会被当成丢行——实测那 985 行是 docker 自己的记录粒度,不是丢行);
|
|
907
|
+
② 「上滚暂停跟随」只认**真手势**(wheel / 触摸 / 滚动条 / 键盘):真手势上滚必须出现
|
|
908
|
+
「回到底部」且点一下能恢复,没有手势的位置漂移(洪泛里的夹紧/抖动)**必须不停跟随**;
|
|
909
|
+
③ **洪泛中驻留历史**(D154):跟随流没停、缓冲还在淘汰最旧的行时驻留 1.2s,可见区顶行
|
|
910
|
+
必须还是同一行(环形缓冲淘汰的位移没补偿的话,读历史会自动「倒带」);**换代**
|
|
911
|
+
(D155):快照先刷成一代会折行的高行并扫位量进缓存、再刷回普通行,垫高必须由**这一代**
|
|
912
|
+
的高度算出来(不作废缓存则滚动高虚高 35644px);
|
|
913
|
+
④ **校验窗口算得对**:
|
|
914
|
+
快照内容是确定的,滚到顶第一行必须是 `seq=0`、滚到底最后一行必须是 `seq=4999`,洪泛结束后
|
|
915
|
+
最后一行必须是最新行(贴底跟随还生效)。超预算即非零退出。D152 之后的实测:
|
|
916
|
+
5000 行快照 179ms / **229 个正文节点**(此前 ~10000)、挂 53~57 行;突发 20k 行 933ms /
|
|
917
|
+
最大延迟 **2ms**(此前 52~97ms);事件洪泛 10ms vs 7ms;持续洪泛 20k 行/秒 × 6s
|
|
918
|
+
最大延迟 **6ms**、>50ms 长帧 **0 个**(此前 70~97ms / 25~26 个)、213 节点。预算里另有一条
|
|
919
|
+
「挂载行数 ≤ 200」——虚拟滚动失效会立刻变红。它依赖 tty 的预览夹具
|
|
920
|
+
(`node packages/tty/scripts/preview.mjs` 生成),夹具或 Chrome 缺失时**跳过并 exit 0**
|
|
921
|
+
—— 它是 npm script,不是 vitest 必跑项。
|
|
922
|
+
|
|
923
|
+
`test/streams.test.ts`(35 例)覆盖**统计流 / 事件流 / 拉取流 / 网络卷 / 通用 SSE 基建**:
|
|
846
924
|
`statsStream` 不带 `--no-stream`(与快照同一构造点)、`pullStream` 的
|
|
847
925
|
`assertImageRef` 白名单、`/stats/stream` 把逐行 JSON(含跨 chunk 的半行)归一成
|
|
848
926
|
`stats` 事件且形状与 `/stats` 快照一致、心跳、客户端断开静默中止、
|
package/client-src/docker.css
CHANGED
|
@@ -1114,6 +1114,12 @@
|
|
|
1114
1114
|
font-family: var(--dk-mono);
|
|
1115
1115
|
font-size: 12px;
|
|
1116
1116
|
line-height: 1.6;
|
|
1117
|
+
/*
|
|
1118
|
+
* 关掉浏览器的滚动锚定(D152):窗口化会把垫高与行**成批地换掉**,Chrome 的锚定
|
|
1119
|
+
* 会挑一个节点把 scrollTop 拽回去,跟「贴底钉住 / 锚点修正」互相打架——表现就是
|
|
1120
|
+
* 位置漂一下、甚至整屏空白。位置上的一切由 useLogRows 负责,不需要浏览器再猜。
|
|
1121
|
+
*/
|
|
1122
|
+
overflow-anchor: none;
|
|
1117
1123
|
}
|
|
1118
1124
|
|
|
1119
1125
|
.dk_logLine {
|
|
@@ -1131,9 +1137,11 @@
|
|
|
1131
1137
|
*
|
|
1132
1138
|
* D63 根治(第二轮)走了另一条路:行 key 用单调 id + 缓冲行数/字节双限 +
|
|
1133
1139
|
* 150ms 合帧(见 index.js 与 log-buffer.js)——渲染频率与 chunk 速率解耦、
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1140
|
+
* 内存有界。D152 起再叠一层**窗口化挂载**:DOM 里只留可见的几十行 + 上下垫高
|
|
1141
|
+
* (`.dk_logPad`),「显示层不截断」落在**数据**上——全部行都能滚到、都能导出,
|
|
1142
|
+
* 只是不再同时挂在 DOM 里。精确 scrollHeight 由实测行高 + 贴底锚定保住(见
|
|
1143
|
+
* log-window.js),所以贴底判定依旧成立;content-visibility 依旧没有开的理由
|
|
1144
|
+
* (D91 的估算高度会把它打穿)。
|
|
1137
1145
|
*/
|
|
1138
1146
|
}
|
|
1139
1147
|
|
|
@@ -1143,6 +1151,17 @@
|
|
|
1143
1151
|
border-radius: 2px;
|
|
1144
1152
|
}
|
|
1145
1153
|
|
|
1154
|
+
/*
|
|
1155
|
+
* 窗口化挂载的上下垫高(D152):没挂出来的行由它占位,高度 = 那些行的前缀和。
|
|
1156
|
+
* 必须 `flex: none` / 不参与换行——它是纯占位,不该被压缩,也不该被选中或读屏读到
|
|
1157
|
+
* (元素上带 aria-hidden)。`content-visibility` 依旧不要开(D91)。
|
|
1158
|
+
*/
|
|
1159
|
+
.dk_logPad {
|
|
1160
|
+
flex: none;
|
|
1161
|
+
pointer-events: none;
|
|
1162
|
+
user-select: none;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1146
1165
|
.dk_logLevel {
|
|
1147
1166
|
margin-right: var(--dk-gap-sm);
|
|
1148
1167
|
font-weight: 600;
|