wsctl 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- wsctl-0.1.4/.dockerignore +19 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/CHANGELOG.md +252 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/DESIGN.md +82 -25
- wsctl-0.1.4/PKG-INFO +990 -0
- wsctl-0.1.4/README.md +916 -0
- wsctl-0.1.4/SECURITY.md +75 -0
- wsctl-0.1.4/contrib/docker/Dockerfile +46 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/contrib/docker/docker-compose.yml +10 -7
- wsctl-0.1.4/contrib/docker/entrypoint.sh +24 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/contrib/systemd/wsctl.service +14 -1
- {wsctl-0.1.2 → wsctl-0.1.4}/pyproject.toml +2 -1
- {wsctl-0.1.2 → wsctl-0.1.4}/scripts/e2e/run_all.py +187 -13
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/__init__.py +1 -1
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/cli/client.py +18 -3
- wsctl-0.1.4/src/wsctl/cli/connect.py +241 -0
- wsctl-0.1.4/src/wsctl/cli/daemon.py +445 -0
- wsctl-0.1.4/src/wsctl/cli/main.py +1721 -0
- wsctl-0.1.4/src/wsctl/cli/validate.py +148 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/_winpty.py +19 -6
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/audit.py +35 -12
- wsctl-0.1.4/src/wsctl/core/authcache.py +125 -0
- wsctl-0.1.4/src/wsctl/core/backup.py +168 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/config.py +20 -11
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/fs.py +23 -2
- wsctl-0.1.4/src/wsctl/core/logrotate.py +65 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/metrics.py +26 -5
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/net.py +6 -4
- wsctl-0.1.4/src/wsctl/core/passwords.py +54 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/pty.py +49 -11
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/recording.py +87 -15
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/scrollback.py +19 -2
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/session.py +65 -15
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/store.py +152 -34
- wsctl-0.1.4/src/wsctl/core/user_admin.py +105 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/server/app.py +339 -121
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/server/ws.py +109 -53
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/app.css +41 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/app.js +412 -55
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/index.html +49 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/conftest.py +29 -0
- wsctl-0.1.4/tests/test_audit.py +88 -0
- wsctl-0.1.4/tests/test_authcache.py +101 -0
- wsctl-0.1.4/tests/test_backup.py +122 -0
- wsctl-0.1.4/tests/test_browser.py +684 -0
- wsctl-0.1.4/tests/test_cli.py +388 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_config.py +22 -7
- wsctl-0.1.4/tests/test_daemon.py +188 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_fs.py +16 -2
- wsctl-0.1.4/tests/test_logrotate.py +51 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_metrics.py +19 -0
- wsctl-0.1.4/tests/test_passwords.py +45 -0
- wsctl-0.1.4/tests/test_pty.py +66 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_recording.py +47 -1
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_scrollback.py +12 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_server.py +277 -14
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_session.py +140 -2
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_store.py +148 -16
- wsctl-0.1.4/tests/test_user_admin.py +105 -0
- wsctl-0.1.4/tests/test_validate.py +83 -0
- wsctl-0.1.2/PKG-INFO +0 -681
- wsctl-0.1.2/README.md +0 -607
- wsctl-0.1.2/SECURITY.md +0 -43
- wsctl-0.1.2/contrib/docker/Dockerfile +0 -29
- wsctl-0.1.2/src/wsctl/cli/connect.py +0 -158
- wsctl-0.1.2/src/wsctl/cli/main.py +0 -910
- wsctl-0.1.2/src/wsctl/core/passwords.py +0 -19
- wsctl-0.1.2/tests/test_audit.py +0 -45
- wsctl-0.1.2/tests/test_browser.py +0 -322
- wsctl-0.1.2/tests/test_cli.py +0 -94
- wsctl-0.1.2/tests/test_pty.py +0 -38
- {wsctl-0.1.2 → wsctl-0.1.4}/.gitignore +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/LICENSE +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/THIRD_PARTY_NOTICES.md +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/contrib/nginx/wsctl.conf +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/docs/screenshots/01-login.png +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/docs/screenshots/02-terminal.png +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/docs/screenshots/03-files.png +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/docs/screenshots/04-sessions.png +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/docs/screenshots/05-admin-users.png +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/docs/screenshots/06-admin-audit.png +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/docs/screenshots/07-settings.png +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/__main__.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/cli/__init__.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/__init__.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/logging.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/ratelimit.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/ssh.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/tmux.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/totp.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/core/webhook.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/py.typed +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/server/__init__.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/server/client.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/server/security.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/favicon.svg +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/manifest.webmanifest +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/THIRD_PARTY_NOTICES.txt +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/addon-fit.js +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/addon-image.js +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/addon-image.js.LICENSE.txt +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/addon-web-links.js +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/asciinema-player.css +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/asciinema-player.min.js +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/xterm.css +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/xterm.js +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/src/wsctl/static/vendor/zmodem.js +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_client.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_connect.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_instances.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_load.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_net.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_ratelimit.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_security.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_ssh.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_tmux.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_totp.py +0 -0
- {wsctl-0.1.2 → wsctl-0.1.4}/tests/test_webhook.py +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 构建上下文是仓库根目录(见 contrib/docker/Dockerfile 的说明),
|
|
2
|
+
# 这里排除与镜像无关的大目录,加快构建并避免把本地垃圾拷进去。
|
|
3
|
+
.git
|
|
4
|
+
.github
|
|
5
|
+
.venv
|
|
6
|
+
venv
|
|
7
|
+
__pycache__
|
|
8
|
+
*.pyc
|
|
9
|
+
*.pyo
|
|
10
|
+
.mypy_cache
|
|
11
|
+
.pytest_cache
|
|
12
|
+
.ruff_cache
|
|
13
|
+
dist
|
|
14
|
+
build
|
|
15
|
+
*.egg-info
|
|
16
|
+
docs/screenshots
|
|
17
|
+
data
|
|
18
|
+
*.tar.gz
|
|
19
|
+
*.log
|
|
@@ -5,6 +5,258 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.4] - 2026-09-22
|
|
9
|
+
|
|
10
|
+
Reliability and usability release. Every promise the product makes now holds
|
|
11
|
+
across a restart, a broken disk and a broken config edit; the CLI can log in
|
|
12
|
+
with 2FA and script against JSON. No new terminal protocols.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **CLI can log in to a two-factor account.** `wsctl login --totp 123456`, the
|
|
17
|
+
`WSCTL_TOTP` environment variable, or a prompt when the server asks for a
|
|
18
|
+
code. Previously a 2FA-enabled account made every CLI command unusable.
|
|
19
|
+
- **Share links survive a server restart.** The token, its expiry and its
|
|
20
|
+
writable flag are persisted with the session (`term_sessions.share_*`,
|
|
21
|
+
schema version 4) and rehydrated verbatim on adoption, so a QR code that was
|
|
22
|
+
already handed out keeps working — the same promise the session itself makes
|
|
23
|
+
on the tmux backend. Renaming a session no longer invalidates its link.
|
|
24
|
+
- **Minimum password policy** (non-empty, at least 8 characters), enforced in
|
|
25
|
+
one place (`core/passwords.validate_password`) at the storage boundary, in
|
|
26
|
+
`user_admin`, and therefore by both the HTTP API and the CLI. Existing
|
|
27
|
+
passwords are *not* force-changed; `wsctl doctor` warns instead of locking
|
|
28
|
+
anyone out.
|
|
29
|
+
- **Log rotation** for the background lifecycle. `log_file` / `log_max_bytes` /
|
|
30
|
+
`log_backup_count` rotate by copy-and-truncate, so the append-mode descriptor
|
|
31
|
+
`wsctl start` hands its child stays valid across a rotation (rename-based
|
|
32
|
+
rotation would leave the daemon writing into the rotated file forever).
|
|
33
|
+
- **Web new-session dialog** with name, command, working directory, backend and
|
|
34
|
+
a structured SSH target form (host / user / port / identity / `ssh -o`
|
|
35
|
+
options). `Alt+N` still opens a default shell with one keystroke.
|
|
36
|
+
- **Web admin "config" tab**: the effective configuration, each key labelled
|
|
37
|
+
`热更新` or `需重启`, plus a reload button that reports exactly what changed
|
|
38
|
+
*and* what failed to parse.
|
|
39
|
+
- **`wsctl completion install|show [bash|zsh|fish|powershell]`**. Typer's
|
|
40
|
+
`--install-completion` only ever installs bash; this covers the rest and
|
|
41
|
+
picks the shell from `$SHELL` instead of guessing bash.
|
|
42
|
+
- `GET /api/config` (admin) — the read-only effective-configuration view behind
|
|
43
|
+
the new tab.
|
|
44
|
+
- CLI `--json` on `session list|new|rename`, `user add|list`, `audit` and
|
|
45
|
+
`config show|get`; `wsctl audit` gained `--event` / `--user-id` / `--ip` filters;
|
|
46
|
+
`wsctl session list` now shows the owner's **username** (and the backend and
|
|
47
|
+
liveness) instead of a bare numeric id.
|
|
48
|
+
- `wsctl logs` reads across rotated backups, so a rotation never hides history.
|
|
49
|
+
- `wsctl doctor` additionally reports free disk space on the data partition,
|
|
50
|
+
the password policy in force, the log file and its rotation threshold, and
|
|
51
|
+
the system clock (which TOTP depends on).
|
|
52
|
+
- Metrics `wsctl_pty_input_dropped_total` and `wsctl_recording_failures_total`.
|
|
53
|
+
- Uploads can opt in to replacing an existing file (`overwrite=1`); the web UI
|
|
54
|
+
asks first.
|
|
55
|
+
- File listings report `truncated` and `limit` when a directory exceeds 2000
|
|
56
|
+
entries instead of silently returning everything.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **A broken config edit is no longer silent.** `reload_settings_file` returns
|
|
61
|
+
`(changed, errors)`; `wsctl reload` and `POST /api/config/reload` surface the
|
|
62
|
+
exact TOML/validation error and exit non-zero, and the server logs it.
|
|
63
|
+
Previously an invalid value was discarded and the reload reported "no
|
|
64
|
+
changes", which was indistinguishable from the reload not running.
|
|
65
|
+
- **`config set` preserves trailing comments** on the line it edits
|
|
66
|
+
(`port = 7681 # 监听端口` stays annotated) and no longer mistakes a
|
|
67
|
+
commented-out `# port = 1234` for the real key.
|
|
68
|
+
- **Web terminal connections are bounded.** At most `8` sessions hold a live
|
|
69
|
+
socket (the most recently used ones); the rest appear in the tab bar as
|
|
70
|
+
`未连接` and attach when opened. Previously every session opened a WebSocket
|
|
71
|
+
and an xterm instance at page load, which fell over past a handful.
|
|
72
|
+
- Uploading over an existing file is refused with `409` unless the client
|
|
73
|
+
explicitly opts in. Files were previously truncated without warning.
|
|
74
|
+
- The image in `contrib/docker` builds from the **repository root** and installs
|
|
75
|
+
the local source; `docker compose` refuses to start without
|
|
76
|
+
`WSCTL_ADMIN_PASSWORD` instead of defaulting to `change-me`.
|
|
77
|
+
- CI adds a macOS leg and a Windows import/CLI smoke job, runs the `-m slow`
|
|
78
|
+
load suite, installs `lrzsz` so the ZMODEM round trip is really exercised,
|
|
79
|
+
and builds the Docker image.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **A session whose child never exits can no longer hang the server.**
|
|
84
|
+
`TermSession._finalize` waited unboundedly on `Pty.wait()`; a daemonized
|
|
85
|
+
grandchild holding the terminal open would wedge the read loop's cleanup
|
|
86
|
+
forever and leak the session object. The wait is now bounded and escalates to
|
|
87
|
+
`kill`.
|
|
88
|
+
- **A failed recording no longer claims to be recording.** When the writer hit a
|
|
89
|
+
disk error it silently stopped while `is_recording` stayed true, so the UI
|
|
90
|
+
showed a red dot for a file that was not being written. The recorder now
|
|
91
|
+
closes itself, records the error, and `close()` can no longer race the writer
|
|
92
|
+
thread into a half-closed file.
|
|
93
|
+
- **`import wsctl.core.store` no longer computes an Argon2 hash**, which made
|
|
94
|
+
every CLI invocation — including `wsctl --version` — pay ~120ms before doing
|
|
95
|
+
anything. The timing-equalising dummy hash is computed lazily; combined with
|
|
96
|
+
lazy imports of the server/client dependencies, `wsctl version` went from
|
|
97
|
+
~1.05s to ~0.36s.
|
|
98
|
+
- **Authentication lookups no longer run on the event loop.** The WebSocket
|
|
99
|
+
handshake and its periodic re-check did synchronous SQLite work per
|
|
100
|
+
connection every 5s, as did the maintenance loop's leases/reaping/retention.
|
|
101
|
+
They now run on worker threads, and a short-lived auth cache
|
|
102
|
+
(`core/authcache.py`) turns the re-check into a dict lookup. Revocation stays
|
|
103
|
+
immediate: disabling a user, changing a password or changing a role drops that
|
|
104
|
+
user's cache entries at once.
|
|
105
|
+
- **A dropped keystroke is now reported.** When a session's child stops
|
|
106
|
+
draining its terminal the write path drops input rather than grow without
|
|
107
|
+
bound — but it did so *silently*, which looked like a dead keyboard. The
|
|
108
|
+
client is told once (`终端输入过快,部分按键已丢弃`) and the drops are counted.
|
|
109
|
+
- **A stale `attached` message can no longer revert a rename.** The tab label
|
|
110
|
+
is protected by a rename generation counter, so a reconnect that echoes the
|
|
111
|
+
pre-rename name does not silently undo what the user just typed.
|
|
112
|
+
- **`promptDialog`/`confirmDialog` are singletons.** A second invocation while
|
|
113
|
+
one was open overwrote the input the user was typing into; that is how a
|
|
114
|
+
rename could send the *old* name back to the server and look lost.
|
|
115
|
+
- **A finished ZMODEM transfer always releases the keyboard.** The input lock
|
|
116
|
+
was only cleared by the transfer's end event, so an aborted transfer left the
|
|
117
|
+
terminal accepting no input at all. An inactivity watchdog now force-releases
|
|
118
|
+
it (and `on_retract` does too).
|
|
119
|
+
- `wsctl connect` survives non-JSON text frames (a proxy or middlebox injecting
|
|
120
|
+
one used to tear the connection down with a traceback).
|
|
121
|
+
- **Maintenance reconciliation can no longer mis-kill a live session.**
|
|
122
|
+
`term_session_stop_missing` takes the in-memory session set as its liveness
|
|
123
|
+
list; running its `UPDATE` on a worker thread after taking that snapshot
|
|
124
|
+
widened the window to thread-scheduling latency, so a session created in
|
|
125
|
+
between would be written as `stopped` while running (and never adopted again
|
|
126
|
+
after a restart). The snapshot and that one statement stay in a single
|
|
127
|
+
synchronous stretch on the loop; only snapshot-independent writes are
|
|
128
|
+
threaded.
|
|
129
|
+
- **Argon2 hashing no longer runs under the store lock.** `user_create` and
|
|
130
|
+
`user_set_password` hashed while holding the single store mutex, serialising
|
|
131
|
+
every other database operation — including authentication — behind one
|
|
132
|
+
~100ms CPU-bound hash. The hash is now computed before the lock is taken.
|
|
133
|
+
- **Audit input records only what actually reached the shell.** Keystrokes
|
|
134
|
+
dropped by write backpressure (or a session that had already exited) used to
|
|
135
|
+
be audited as if the command had run. Dropped input is now reported to the
|
|
136
|
+
user and simply not recorded.
|
|
137
|
+
- The terminal connection indicator follows the *visible* tab: a background
|
|
138
|
+
tab reconnecting or being suspended no longer rewrites it to
|
|
139
|
+
"连接中"/"空闲".
|
|
140
|
+
- Metrics named `_total` (`wsctl_audit_dropped_total`,
|
|
141
|
+
`wsctl_audit_write_errors_total`, `wsctl_pty_input_dropped_total`,
|
|
142
|
+
`wsctl_recording_failures_total`) are now exposed with `# TYPE … counter`.
|
|
143
|
+
They were declared `gauge`, which makes `rate()` reject them. The two new
|
|
144
|
+
ones are also genuinely monotonic — a per-session sum would have gone *down*
|
|
145
|
+
whenever a session ended, showing up as counter resets.
|
|
146
|
+
- Metric label values are escaped for the Prometheus text format (a `"` or
|
|
147
|
+
newline in a label produced invalid exposition output).
|
|
148
|
+
- Scrollback eviction no longer leaves an orphaned UTF-8 continuation byte at
|
|
149
|
+
the head of a replayed screen (rendered as a replacement glyph).
|
|
150
|
+
- `wsctl doctor` no longer imports the hashing stack just to print a table.
|
|
151
|
+
|
|
152
|
+
### Testing
|
|
153
|
+
|
|
154
|
+
- New suites for the password policy, the auth cache (including revocation
|
|
155
|
+
guarantees), copy-and-truncate log rotation, recording-failure honesty,
|
|
156
|
+
scrollback UTF-8 alignment and metric label escaping; extended suites for
|
|
157
|
+
share persistence (including "a rename must not invalidate the link"),
|
|
158
|
+
the bounded `Pty.wait`, PTY input drops, the upload overwrite guard, the
|
|
159
|
+
config endpoint and reload errors, TOTP/`--json`/comment-preserving
|
|
160
|
+
`config set`, and `completion show`.
|
|
161
|
+
- The browser suite covers the new-session dialog (including the SSH-required
|
|
162
|
+
validation), the config tab, the upload overwrite prompt, the bounded
|
|
163
|
+
connection budget, and a **real ZMODEM `sz`/`rz` byte-exact round trip**.
|
|
164
|
+
- `scripts/e2e/run_all.py` asserts that a share link keeps working across a
|
|
165
|
+
full server restart, and reports the child's log when a scenario fails to
|
|
166
|
+
start.
|
|
167
|
+
|
|
168
|
+
### Upgrade notes
|
|
169
|
+
|
|
170
|
+
- The database migrates to schema version 4 (adds `share_token`,
|
|
171
|
+
`share_expires`, `share_writable` to `term_sessions`). The migration is
|
|
172
|
+
idempotent, but **back up before downgrading** to 0.1.3 or older —
|
|
173
|
+
`wsctl backup` first, since the older release does not know about these
|
|
174
|
+
columns.
|
|
175
|
+
- New accounts and password changes must be at least 8 characters. Existing
|
|
176
|
+
accounts keep working; `wsctl doctor` reports which ones are below the bar.
|
|
177
|
+
|
|
178
|
+
[0.1.4]: https://github.com/ThzxxArt/wsctl/releases/tag/v0.1.4
|
|
179
|
+
|
|
180
|
+
## [0.1.3] - 2026-09-22
|
|
181
|
+
|
|
182
|
+
Operations and correctness release: a background lifecycle that needs no
|
|
183
|
+
systemd, strict validation of mutually exclusive options, and the last blocking
|
|
184
|
+
paths on the event loop removed. No new terminal protocols.
|
|
185
|
+
|
|
186
|
+
### Added
|
|
187
|
+
|
|
188
|
+
- **Background lifecycle without systemd**: `wsctl start` / `stop` / `restart` /
|
|
189
|
+
`status` / `logs` / `reload`, plus `wsctl serve --daemon`. The child is
|
|
190
|
+
detached (`start_new_session`), its output goes to a per-port log file, and a
|
|
191
|
+
pid file carries a **process-identity fingerprint** (Linux start time, `ps`
|
|
192
|
+
elsewhere) so a recycled PID is never signalled. `start` refuses to clobber a
|
|
193
|
+
live instance (`start --force` replaces a running one); `status --json` is
|
|
194
|
+
machine-readable; `reload` sends `SIGHUP`, which the server now handles (in
|
|
195
|
+
addition to the mtime watcher).
|
|
196
|
+
- **Half-open connection detection**: a WebSocket that has sent nothing for
|
|
197
|
+
`IDLE_TIMEOUT` (120s) is closed with code `4408`. Both clients ping every 25s,
|
|
198
|
+
so a peer that vanished without a FIN is reclaimed instead of lingering.
|
|
199
|
+
- **Declarative CLI validation** (`wsctl/cli/validate.py`): options that cannot
|
|
200
|
+
be combined or that depend on each other now fail fast with exit code 2 and an
|
|
201
|
+
actionable message — `--daemon` vs `--reuse-port`, `--ssl-cert`/`--ssl-key`,
|
|
202
|
+
`serve --backend ssh`, `session new --ssh` vs `--backend`, `--ssh-*` without
|
|
203
|
+
`--ssh`, unknown roles, and more. `config set` now type-checks the value
|
|
204
|
+
against the settings model and validates the whole file before writing.
|
|
205
|
+
- **One set of user invariants for both surfaces** (`wsctl/core/user_admin.py`):
|
|
206
|
+
the CLI can no longer disable, demote or delete the last admin (which could
|
|
207
|
+
lock the instance out), and `wsctl user passwd` now revokes that user's
|
|
208
|
+
existing logins just like the API does.
|
|
209
|
+
- **Consistent backups and restores**: `backup` uses SQLite's online backup API
|
|
210
|
+
(a WAL-mode database copied as a plain file could miss recent commits), and
|
|
211
|
+
`restore` validates the archive, refuses to overwrite without `--force`
|
|
212
|
+
(keeping a `.bak`), and rejects path-traversal members.
|
|
213
|
+
- `wsctl connect` reconnects automatically with exponential backoff and
|
|
214
|
+
re-attaches to the same session (server-side replay restores the screen);
|
|
215
|
+
`--no-reconnect` opts out.
|
|
216
|
+
- `doctor` now reports the running instance, port availability, `SO_REUSEPORT`
|
|
217
|
+
support and background-start capability.
|
|
218
|
+
- Metrics `wsctl_audit_write_errors_total` and `wsctl_maintenance_lag_seconds`.
|
|
219
|
+
|
|
220
|
+
### Changed
|
|
221
|
+
|
|
222
|
+
- **WebSocket close codes are now meaningful**: `4400` bad request, `4401`
|
|
223
|
+
unauthenticated, `4403` forbidden, `4404` session not found, `4409` limit
|
|
224
|
+
reached, `4500` server error. The web client stops reconnecting on a permanent
|
|
225
|
+
failure instead of looping, and a missing session no longer pops the login
|
|
226
|
+
dialog at an already-authenticated user.
|
|
227
|
+
- Session ids are hex (`token_hex`) so they can never start with `-` and be
|
|
228
|
+
mistaken for a CLI option.
|
|
229
|
+
- `--reuse-port` now fails loudly when `SO_REUSEPORT` is unavailable instead of
|
|
230
|
+
silently binding without it (a silent downgrade would defeat zero-downtime
|
|
231
|
+
handovers).
|
|
232
|
+
|
|
233
|
+
### Fixed
|
|
234
|
+
|
|
235
|
+
- **Argon2 verification and hashing no longer run on the event loop**: login,
|
|
236
|
+
user creation and password changes run on a worker thread, so a login storm
|
|
237
|
+
cannot stall every terminal.
|
|
238
|
+
- `Recorder.close()` (which joins its writer thread) no longer blocks the event
|
|
239
|
+
loop; retention purging, recording-capacity checks and directory listings run
|
|
240
|
+
on worker threads.
|
|
241
|
+
- The audit writer survives a failing database write (it logs, counts and keeps
|
|
242
|
+
going instead of dying silently), and concurrent `flush()` calls can no longer
|
|
243
|
+
reorder events.
|
|
244
|
+
- `serve --new` with a `tmux` backend that is not installed no longer crashes the
|
|
245
|
+
server at startup.
|
|
246
|
+
- The web UI's Esc key closes the topmost modal (previously it could close the
|
|
247
|
+
admin panel behind the QR dialog), the QR dialog is a normal, focus-trapped
|
|
248
|
+
modal, password reset uses a masked input, and assigning a hotkey that is
|
|
249
|
+
already taken is rejected with a warning.
|
|
250
|
+
|
|
251
|
+
### Testing
|
|
252
|
+
|
|
253
|
+
- New suites for the validation layer, the pid-file lifecycle, consistent
|
|
254
|
+
backups/restores, and the shared user-admin invariants; WebSocket close-code
|
|
255
|
+
tests; audit-writer failure and ordering tests; a `daemon` end-to-end scenario;
|
|
256
|
+
and browser coverage for the 2FA QR modal.
|
|
257
|
+
|
|
258
|
+
[0.1.3]: https://github.com/ThzxxArt/wsctl/releases/tag/v0.1.3
|
|
259
|
+
|
|
8
260
|
## [0.1.2] - 2026-09-22
|
|
9
261
|
|
|
10
262
|
Polish and robustness release: disk/DB I/O is moved off the event loop, the web
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wsctl 设计文档
|
|
2
2
|
|
|
3
|
-
> 版本:0.1.
|
|
3
|
+
> 版本:0.1.4 · 状态:0.1.0–0.1.4 已发布
|
|
4
4
|
> 作者:ThzxxArt · 许可:MIT
|
|
5
5
|
|
|
6
6
|
## 1. 定位
|
|
@@ -61,8 +61,11 @@ wsctl **不在原始吞吐上对标 ttyd**。纯 Python 无法在 `cat` 大文
|
|
|
61
61
|
- **背压**:每个客户端维护有界发送队列;慢消费者达到上限时丢弃旧帧或断开,
|
|
62
62
|
防止大输出导致服务端内存暴涨。
|
|
63
63
|
- **回收策略**:空闲超时 / 最大生命周期 / 管理员显式 kill,三者之一触发回收。
|
|
64
|
-
- **进程治理**:`start_new_session` + `killpg`,并在会话结束时 `wait()`
|
|
65
|
-
|
|
64
|
+
- **进程治理**:`start_new_session` + `killpg`,并在会话结束时 `wait()` 回收(**有界
|
|
65
|
+
等待,超时强杀**:子进程若被守护化孙进程占住终端而不退,也不能挂死清理路径);
|
|
66
|
+
未安装全局 SIGCHLD handler(避免与 `subprocess` 争抢 PID)。已跟踪会话不残留僵尸进程。
|
|
67
|
+
- **分享也是承诺**:share token / 有效期 / 可写标记与会话一起持久化,服务重启后原样
|
|
68
|
+
恢复(不轮换 token),已分发的二维码与链接继续可用;重命名会话不会使其失效。
|
|
66
69
|
|
|
67
70
|
### 3.1 可选 tmux 后端(跨重启恢复)
|
|
68
71
|
|
|
@@ -134,7 +137,9 @@ users(id, username, password_hash, role[admin|user], totp_secret, disabled, crea
|
|
|
134
137
|
auth_sessions(id, user_id, token_hash, ip, user_agent, exp, last_seen) -- 登录态
|
|
135
138
|
|
|
136
139
|
term_sessions(id, name, owner_id, backend[local|tmux|ssh], command, argv, env,
|
|
137
|
-
cwd, idle_timeout, max_life, status, instance_id,
|
|
140
|
+
cwd, idle_timeout, max_life, status, instance_id,
|
|
141
|
+
share_token, share_expires, share_writable,
|
|
142
|
+
created_at, last_active)
|
|
138
143
|
|
|
139
144
|
instances(id, pid, host, started_at, heartbeat) -- 多实例租约
|
|
140
145
|
|
|
@@ -145,27 +150,45 @@ settings(key, value)
|
|
|
145
150
|
|
|
146
151
|
> 命名约定:`auth_sessions` 指**登录态**,`term_sessions` 指**终端会话**,
|
|
147
152
|
> `instances` 指**服务进程租约**。
|
|
148
|
-
> 模式版本
|
|
153
|
+
> 模式版本 4;旧库通过 `PRAGMA table_info` + `ALTER TABLE` 幂等迁移补齐新列,且索引
|
|
149
154
|
> 在列补齐之后创建(否则升级旧库会因列不存在而失败)。
|
|
155
|
+
>
|
|
156
|
+
> `share_token` / `share_expires` / `share_writable` 持久化分享链接:分享是**承诺**,
|
|
157
|
+
> 必须和会话一样跨重启存活(0.1.4 起)。降级到 0.1.3 及更早版本前请先 `wsctl backup`,
|
|
158
|
+
> 旧版本不认识这几列。
|
|
150
159
|
|
|
151
160
|
## 6. CLI 命令树(已实现)
|
|
152
161
|
|
|
153
162
|
```
|
|
154
|
-
wsctl serve #
|
|
163
|
+
wsctl serve # 前台起服务(--daemon 转后台)
|
|
164
|
+
wsctl start|stop|restart|status|logs|reload # 非 systemd 后台生命周期
|
|
155
165
|
wsctl serve --new "bash" # 启动时顺带开一个会话
|
|
156
166
|
wsctl serve --backend tmux # 默认使用 tmux 后端(跨重启恢复)
|
|
157
|
-
wsctl doctor #
|
|
158
|
-
wsctl session list|new|rename|kill|attach # new 支持 --backend local|tmux|ssh
|
|
167
|
+
wsctl doctor # 环境、配置、运行状态、端口、SO_REUSEPORT、磁盘、时钟自检
|
|
168
|
+
wsctl session list|new|rename|kill|attach # 均支持 --json;new 支持 --backend local|tmux|ssh
|
|
159
169
|
wsctl session record|record-stop|recording
|
|
160
|
-
wsctl connect [url] [-s id] # 瘦客户端:本地 raw
|
|
161
|
-
wsctl login|logout #
|
|
170
|
+
wsctl connect [url] [-s id] # 瘦客户端:本地 raw 终端直连(断线自动重连)
|
|
171
|
+
wsctl login|logout # login 支持 --totp / WSCTL_TOTP(两步验证账号)
|
|
162
172
|
wsctl user add|list|del|passwd|role|disable|enable|totp
|
|
163
|
-
wsctl audit
|
|
164
|
-
wsctl config
|
|
173
|
+
wsctl audit [--event|--user-id|--ip|--json]
|
|
174
|
+
wsctl backup FILE [--include-config] / restore FILE [--force]
|
|
175
|
+
wsctl config show|get|path|edit|set|validate|reload # set 保留行内注释
|
|
176
|
+
wsctl completion install|show [bash|zsh|fish|powershell]
|
|
165
177
|
wsctl version / --version
|
|
166
178
|
```
|
|
167
179
|
|
|
168
|
-
> `config set`
|
|
180
|
+
> `config set` 写入前会校验单项类型与整文件合法性(不合法则拒绝并回滚),并保留被改行
|
|
181
|
+
> 尾部的注释;`config reload`(或 `wsctl reload` 的 SIGHUP)让运行中的服务热加载,
|
|
182
|
+
> **配置写错时会原样打印错误并以非零码退出**,绝不静默忽略。
|
|
183
|
+
>
|
|
184
|
+
> **互斥/依赖强校验**:所有互相冲突或彼此依赖的参数在 `cli/validate.py` 中声明式
|
|
185
|
+
> 定义,非法组合在进程启动前即以退出码 2 报错,而不是静默忽略其一。
|
|
186
|
+
>
|
|
187
|
+
> **后台生命周期**(`cli/daemon.py`):子进程以 `start_new_session` 脱离终端,
|
|
188
|
+
> stdout/stderr 重定向到 `<data_dir>/run/wsctl-<port>.log`(可按大小轮转,
|
|
189
|
+
> copytruncate 语义以保住继承的追加型描述符);pid 文件记录
|
|
190
|
+
> **进程身份指纹**(Linux 取 `/proc/<pid>/stat` 的 starttime),`stop/status` 在
|
|
191
|
+
> 发信号前核对,避免 PID 复用误杀。`--reuse-port` 的多实例热切换不纳入托管。
|
|
169
192
|
|
|
170
193
|
|
|
171
194
|
## 7. 目录结构
|
|
@@ -246,8 +269,21 @@ pyotp segno websockets
|
|
|
246
269
|
| **M25** | 0.1.2 便捷性:`config get/validate`、`doctor --json`、`backup`、终端内 TOTP 二维码、shell 补全、Docker 示例 |
|
|
247
270
|
| **M26** | 0.1.2 设计系统:令牌 + 组件、图标化工具栏 + 移动端折叠、统一 Modal、主题色预览、favicon/manifest、空态/加载态、a11y、跟随系统深浅色 |
|
|
248
271
|
| **M27** | 0.1.2 测试与文档:异步审计/管理 API/TOTP/节流/滑动 TTL 单测 + 浏览器用例扩展;README/DESIGN/CHANGELOG 同步 |
|
|
249
|
-
|
|
250
|
-
|
|
272
|
+
| **M28** | 0.1.3 非 systemd 后台生命周期:`start/stop/restart/status/logs/reload`(`start --force` 先停再启)+ `serve --daemon`;pidfile(进程身份指纹)+ 日志文件 + stale-pid 清理;SIGHUP 配置重载;Windows 明确不支持 |
|
|
273
|
+
| **M29** | 0.1.3 参数互斥/依赖强校验:`cli/validate.py` 声明式规则(conflicts/requires/requires_if/choices);`config set` 类型与整文件校验 + 回滚;`serve --backend`/`--ssl-*`/`--daemon`/`--reuse-port` 组合约束;`--reuse-port` 在平台不支持 `SO_REUSEPORT` 时**明确报错**而非静默降级 |
|
|
274
|
+
| **M29b** | 0.1.3 用户管理不变量同源:`core/user_admin.py`,CLI 与 API 共用(最后管理员保护、不能操作自身、改密/禁用即吊销登录态) |
|
|
275
|
+
| **M30** | 0.1.3 事件循环解阻:argon2 认证/建用户/改密移出循环、`Recorder.close` 非阻塞、保留清理/录制容量/目录列举入线程、AuditWriter 容错与 flush 串行化、维护滞后指标 |
|
|
276
|
+
| **M31** | 0.1.3 连接健壮性:WS 关闭码语义化(4400/4401/4403/4404/4408 空闲超时/4409/4500)+ 前端按码处理(永久失败不再重连、会话不存在不再误弹登录框);**半开连接检测**(服务端 120s 无消息回收,前端与 CLI 每 25s 心跳);`connect` 客户端指数退避自动重连;会话 id 改十六进制(避免以 `-` 开头被当选项) |
|
|
277
|
+
| **M32** | 0.1.3 一致性备份/恢复:`backup` 用 SQLite 在线备份 API(含 WAL)、`restore` 校验归档/拒绝穿越/`--force` 保留 `.bak`;`status --json`;`doctor` 扩展运行状态/端口/reuse-port/后台能力 |
|
|
278
|
+
| **M33** | 0.1.3 前端易用性:密码掩码对话框、QR 弹窗纳入统一 Modal(Esc 关最上层/焦点陷阱)、快捷键冲突检测 |
|
|
279
|
+
| **M34** | 0.1.3 测试与文档:校验层/daemon 生命周期/备份恢复/用户不变量单测 + 关闭码测试 + e2e `daemon` 场景 + 浏览器 QR 用例;README/DESIGN/CHANGELOG 同步 |
|
|
280
|
+
| **M35** | 0.1.4 「说到做到」:CLI TOTP 登录(`--totp`/`WSCTL_TOTP`/交互补录)、密码策略(非空+≥8 位,`core/passwords` 单一入口,Store/user_admin 双闸,不强制改存量)、`_finalize` 有界等待 + 强杀(子进程不退也不再挂死)、**分享 token 持久化**(schema v4 + 收养回填 + 过期清理)、录制诚实性(写失败自闭 + `close` 与写线程互斥)、配置热加载报错(`(changed, errors)` 贯通 API/CLI/日志)、Argon2 惰性化 |
|
|
281
|
+
| **M36** | 0.1.4 「长跑不塌」:认证短缓存(`core/authcache.py`,改密/禁用/改角色即时失效)+ 同步 SQLite 全面出事件循环(WS 握手与周期复查、维护循环批量写、audit/users 查询)、PTY 写缓冲满**反馈**(一次性提示 + `wsctl_pty_input_dropped_total`)、日志轮转(copytruncate 保住继承的追加型 fd)、Web 惰性连接(最多 8 个在线,其余「未连接」按需挂载)、上传防覆盖(409 + 确认后 `overwrite=1`)、`list_dir` 上限 + 截断标记、`connect` 非 JSON 文本帧容错、metrics 标签转义、scrollback UTF-8 边界对齐 |
|
|
282
|
+
| **M37** | 0.1.4 「脚本与界面顺手」:CLI 全线 `--json`(session/user/audit/config show)+ `audit` 筛选 + `session list` 显示用户名/后端/存活、Web 建会话对话框(名称/命令/工作目录/后端/结构化 SSH 表单 + 必填校验)、Web 管理面「配置」页签(`GET /api/config`,标注热更新/需重启 + 重载反馈)、`config set` 保留行内注释且不误改注释行、`wsctl completion install|show`(bash/zsh/fish/powershell,按 `$SHELL` 探测)、`doctor` 增检磁盘/密码策略/日志/系统时钟、CLI 冷启动懒加载(`wsctl version` 1.05s → 0.36s) |
|
|
283
|
+
| **M38** | 0.1.4 部署与工程化:Docker 改**仓库根构建**(装本地源码而非 PyPI 旧版)+ `entrypoint.sh` 首启建号 + compose 强制 `WSCTL_ADMIN_PASSWORD` + `.dockerignore`、CI 加 macOS 腿与 Windows 导入/CLI 冒烟、`-m slow` 压测入 CI、装 `lrzsz` 真机验证 ZMODEM 收发、构建 Docker 镜像冒烟、systemd 单元注明 `ExecStart` 路径来源、e2e 失败时打印子进程日志 |
|
|
284
|
+
| **M39** | 0.1.4 测试与文档:新增密码策略/认证缓存/日志轮转/录制失败/UTF-8/metrics 转义六个套件,扩展分享持久化、有界 `Pty.wait`、PTY 丢输入、上传覆盖、配置端点与重载报错、TOTP/`--json`/注释保留/补全;浏览器覆盖建会话对话框、配置页签、上传覆盖提示、连接预算、**ZMODEM `sz`/`rz` 字节级真机往返**;e2e 断言分享跨重启存活;README/DESIGN/CHANGELOG/SECURITY 同步 |
|
|
285
|
+
|
|
286
|
+
## 10.1 实现状态(截至 0.1.4)
|
|
251
287
|
|
|
252
288
|
**已实现**:M0–M20 全部交付项;二进制 WS 协议、会话与连接解耦、重连回放、
|
|
253
289
|
多用户 RBAC、审计 + `/api/audit`、登录限速、IP allowlist、TOTP、安全响应头、
|
|
@@ -259,8 +295,22 @@ asciinema 录制 + 前端回放、SO_REUSEPORT 零停机重启、Webhook、
|
|
|
259
295
|
`config set`/`reload`、前端浏览器级自动化测试、每会话内存硬上限、
|
|
260
296
|
Sixel 渲染、可选 ZMODEM、终端主题市场。
|
|
261
297
|
|
|
262
|
-
|
|
263
|
-
|
|
298
|
+
**0.1.3 新增**:非 systemd 后台生命周期(pidfile + 身份指纹 + 日志文件 + SIGHUP
|
|
299
|
+
重载)、声明式参数互斥/依赖强校验、CLI 与 API 同源的用户管理不变量、一致性
|
|
300
|
+
备份/恢复(SQLite 在线备份)、WS 关闭码语义化、`connect` 自动重连、事件循环
|
|
301
|
+
解阻(argon2/录制/保留/审计容错)。
|
|
302
|
+
|
|
303
|
+
**0.1.4 新增**:承诺闭环与可靠性收口——**分享链接跨重启存活**(与会话同级的承诺)、
|
|
304
|
+
CLI 两步验证登录、密码策略、子进程不退也不挂死、录制失败自诚实、配置写错必报错、
|
|
305
|
+
认证查询出事件循环 + 短缓存、日志轮转、上传防覆盖、Web 连接预算与 SSH 建会话表单、
|
|
306
|
+
CLI 全线 `--json`、多 shell 补全。**验证深度提升**:ZMODEM 从「集成不破坏 I/O」升级为
|
|
307
|
+
**`sz`/`rz` 字节级真机往返**。
|
|
308
|
+
|
|
309
|
+
**尚未实现**:无。验证边界(诚实声明):
|
|
310
|
+
- ZMODEM 真机往返需要 `lrzsz`(CI 已装并执行;本地缺失时该浏览器用例跳过)。
|
|
311
|
+
- 后台生命周期为 POSIX-only;Windows 下 `serve --daemon`/`start` 明确报错并建议
|
|
312
|
+
服务管理器,CI 仅做导入与 CLI 冒烟(`wsctl[win]` 的 `WinPty` 未端到端验证)。
|
|
313
|
+
- Docker 镜像构建在 CI 的 `docker` job 中验证;本机无 docker 时未构建过。
|
|
264
314
|
|
|
265
315
|
> 0.1.1 起,关闭标签默认只断开连接(detach),终止会话需显式操作(右键菜单 /
|
|
266
316
|
> 会话列表 / `Alt+Shift+W`),与「会话独立于连接」的核心承诺一致。
|
|
@@ -268,19 +318,26 @@ Sixel 渲染、可选 ZMODEM、终端主题市场。
|
|
|
268
318
|
> 0.1.2 起,磁盘/数据库写入全部移出事件循环:审计经 `AuditWriter` 队列批量落盘,
|
|
269
319
|
> 录制在后台线程写文件,上传在线程池落盘;`resolve_auth_session` 节流 `last_seen`。
|
|
270
320
|
|
|
271
|
-
>
|
|
272
|
-
>
|
|
273
|
-
>
|
|
274
|
-
>
|
|
321
|
+
> 0.1.4 起,**读路径**同样出事件循环:WS 握手与周期复查、维护循环的租约/回收/保留、
|
|
322
|
+
> 审计与用户查询均在线程池执行;`resolve_auth_session` 前置一层短缓存
|
|
323
|
+
> (`core/authcache.py`),改密/禁用/改角色/删除时主动失效对应条目,因此
|
|
324
|
+
> 「禁用用户立即踢线」的语义不变。
|
|
325
|
+
|
|
326
|
+
> 回归测试:`scripts/e2e/run_all.py` 提供 8 个后端端到端场景(server / CLI /
|
|
327
|
+
> connect / tmux 重启恢复 **含分享跨重启存活断言** / crash 崩溃收养 /
|
|
328
|
+
> **multiplex 多实例隔离** / **daemon 后台生命周期** / SO_REUSEPORT 优雅重启);
|
|
329
|
+
> `pytest -m browser` 为浏览器级测试(含 ZMODEM 真机往返);`pytest -m slow`
|
|
330
|
+
> 为并发/大输出压测。CI 在独立 job 中运行浏览器测试、慢速压测与 Docker 构建。
|
|
275
331
|
|
|
276
332
|
> 说明:进程回收依赖 `start_new_session` + `killpg` 与 `TermSession` 结束时的
|
|
277
|
-
> `wait()
|
|
278
|
-
>
|
|
333
|
+
> `wait()`(**有界等待,超时强杀**),未安装全局 SIGCHLD handler(避免与
|
|
334
|
+
> `subprocess` 争抢 PID);已跟踪会话不会残留僵尸进程。
|
|
279
335
|
|
|
280
336
|
## 11. Backlog(后续)
|
|
281
337
|
|
|
282
|
-
无(v0.1.
|
|
283
|
-
|
|
338
|
+
无(v0.1.4 计划项已全部落地)。后续可考虑:每用户后端策略(命令级权限隔离)、
|
|
339
|
+
i18n 框架、更多终端协议(Kitty graphics 等)、Windows 服务封装与 `WinPty`
|
|
340
|
+
端到端验证、`--reuse-port` 实例的托管。
|
|
284
341
|
|
|
285
342
|
## 12. 发布
|
|
286
343
|
|