super-dsh 0.3.1 → 0.3.2-e
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/client/index.js +135 -14
- package/package.json +1 -1
- package/remote/helper-bin/MANIFEST.json +35 -0
- package/remote/helper-bin/aarch64-apple-darwin/BUILT-VER +1 -0
- package/remote/helper-bin/aarch64-apple-darwin/superd-run +0 -0
- package/remote/helper-bin/aarch64-unknown-linux-musl/BUILT-VER +1 -0
- package/remote/helper-bin/aarch64-unknown-linux-musl/superd-run +0 -0
- package/remote/helper-bin/x86_64-pc-windows-gnu/BUILT-VER +1 -0
- package/remote/helper-bin/x86_64-pc-windows-gnu/superd-run.exe +0 -0
- package/remote/helper-bin/x86_64-unknown-linux-musl/BUILT-VER +1 -0
- package/remote/helper-bin/x86_64-unknown-linux-musl/superd-run +0 -0
- package/remote/hub-pty-pump.mjs +97 -0
- package/remote/plugin.d.ts +523 -199
- package/remote/plugin.js +2530 -768
- package/remote/provenance.json +36 -21
- package/superd-hub/dist/a2a-host.d.ts +11 -3
- package/superd-hub/dist/a2a-host.js +7 -7
- package/superd-hub/dist/a2a-row.js +7 -1
- package/superd-hub/dist/carrier.d.ts +9 -7
- package/superd-hub/dist/carrier.js +3 -3
- package/superd-hub/dist/client/pill-geometry.d.ts +70 -0
- package/superd-hub/dist/client/pill-geometry.js +49 -0
- package/superd-hub/dist/client/switch.d.ts +15 -0
- package/superd-hub/dist/client/switch.js +54 -1
- package/superd-hub/dist/companions.js +8 -6
- package/superd-hub/dist/exec.d.ts +3 -2
- package/superd-hub/dist/exec.js +6 -5
- package/superd-hub/dist/fusion-transport.d.ts +18 -1
- package/superd-hub/dist/fusion-transport.js +44 -5
- package/superd-hub/dist/gateway.js +8 -1
- package/superd-hub/dist/hub-route.d.ts +5 -2
- package/superd-hub/dist/hub-route.js +6 -6
- package/superd-hub/dist/hub-tool.d.ts +1 -1
- package/superd-hub/dist/hub-tool.js +6 -6
- package/superd-hub/dist/remote-provider.js +70 -36
- package/superd-hub/dist/rpc.d.ts +5 -0
- package/superd-hub/dist/rpc.js +20 -1
- package/superd-hub/dist/switch-core.d.ts +41 -0
- package/superd-hub/dist/switch-core.js +40 -0
- package/superd-hub/dist/targets.d.ts +7 -0
- package/superd-hub/dist/world-lifecycle.js +1 -1
- package/superd-hub/lib/client/index.js +135 -14
- package/superd-hub/dist/exec-pty-channel.d.ts +0 -58
- package/superd-hub/dist/exec-pty-channel.js +0 -162
- package/superd-hub/dist/exec-pty-framing.d.ts +0 -34
- package/superd-hub/dist/exec-pty-framing.js +0 -89
- package/superd-hub/dist/exec-pty-session.d.ts +0 -78
- package/superd-hub/dist/exec-pty-session.js +0 -228
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { Utf16TailRing } from '../../remote/plugin.js';
|
|
2
|
-
// PTY session machinery — PORTED from better-dsh src/remote/pty-session.ts
|
|
3
|
-
// (spec §8.2 / D12 "原样照抄"), with the ONE sanctioned adaptation: the
|
|
4
|
-
// subprocess spawn is replaced by the PtyChannel seam (exec-pty-channel.ts) —
|
|
5
|
-
// every state transition (init nonce handshake, FIFO dispatch, P8 two-stage
|
|
6
|
-
// interrupt \x03 + injected 130-marker, 3s grace, group-kill fallback, idle
|
|
7
|
-
// TTL, OUTPUT_HARD_CAP with surrogate-pair-safe slicing, I1 waiter hygiene)
|
|
8
|
-
// is the ported logic, unchanged.
|
|
9
|
-
import { buildInitCommand, createNonceFrameParser, genNonce, wrapPtyCommand } from './exec-pty-framing.js';
|
|
10
|
-
/** spec §五.2 逐字。 */
|
|
11
|
-
export const RECONNECT_NOTICE = '[remote: session reconnected to fresh shell; cwd reset to default]';
|
|
12
|
-
const DEFAULT_INIT_TIMEOUT_SEC = 30;
|
|
13
|
-
const INTERRUPT_GRACE_MS = 3_000;
|
|
14
|
-
/** pty 收集内存安全帽(Ruling P15,与 oneshot HARD_CAP 对齐):模型面只见尾窗,保留尾部。 */
|
|
15
|
-
const OUTPUT_HARD_CAP = 4_000_000;
|
|
16
|
-
export class PtySession {
|
|
17
|
-
opts;
|
|
18
|
-
state = 'cold';
|
|
19
|
-
startPromise = null;
|
|
20
|
-
queue = Promise.resolve();
|
|
21
|
-
idleTimer = null;
|
|
22
|
-
onChunk = null;
|
|
23
|
-
deathWaiters = [];
|
|
24
|
-
inFlight = 0;
|
|
25
|
-
lastSettleAt = 0;
|
|
26
|
-
constructor(opts) {
|
|
27
|
-
this.opts = opts;
|
|
28
|
-
this.opts.channel.onDead(() => this.markDead());
|
|
29
|
-
}
|
|
30
|
-
get sessionState() { return this.state; }
|
|
31
|
-
/** status 面快照:state + busy(有命令在跑)+ idleMs(上次 settle 至今)。 */
|
|
32
|
-
get snapshot() {
|
|
33
|
-
return { state: this.state, busy: this.inFlight > 0, idleMs: this.lastSettleAt > 0 ? Date.now() - this.lastSettleAt : null };
|
|
34
|
-
}
|
|
35
|
-
/** 同一会话命令严格串行(FIFO);死亡/冷态先重启。 */
|
|
36
|
-
dispatch(cmd, dOpts = {}) {
|
|
37
|
-
const attempt = async () => {
|
|
38
|
-
const started = Date.now();
|
|
39
|
-
const reconnected = this.state === 'dead';
|
|
40
|
-
if (this.state !== 'ready')
|
|
41
|
-
await this.start();
|
|
42
|
-
this.inFlight++;
|
|
43
|
-
return await new Promise((resolve) => {
|
|
44
|
-
const nonce = genNonce();
|
|
45
|
-
const ring = new Utf16TailRing(OUTPUT_HARD_CAP);
|
|
46
|
-
let interrupted = false;
|
|
47
|
-
let settled = false;
|
|
48
|
-
let interruptTimer;
|
|
49
|
-
let killTimer;
|
|
50
|
-
const deathWaiter = () => finish({ output: ring.text, exit: null, cwd: null, timedOut: interrupted });
|
|
51
|
-
const cleanup = () => {
|
|
52
|
-
this.onChunk = null;
|
|
53
|
-
// I1(终审):已结算 dispatch 的死亡等待必须摘除——否则闭包钉住整段 output,
|
|
54
|
-
// 长命会话每调用累积一个 waiter(init 路径的 filter 同款形态)
|
|
55
|
-
this.deathWaiters = this.deathWaiters.filter((w) => w !== deathWaiter);
|
|
56
|
-
if (interruptTimer !== undefined)
|
|
57
|
-
clearTimeout(interruptTimer);
|
|
58
|
-
if (killTimer !== undefined)
|
|
59
|
-
clearTimeout(killTimer);
|
|
60
|
-
};
|
|
61
|
-
const finish = (r) => {
|
|
62
|
-
if (settled)
|
|
63
|
-
return;
|
|
64
|
-
settled = true;
|
|
65
|
-
cleanup();
|
|
66
|
-
this.inFlight--;
|
|
67
|
-
this.lastSettleAt = Date.now();
|
|
68
|
-
this.resetIdleTimer();
|
|
69
|
-
resolve({ ...r, outputStats: { stdout: ring.stats() }, reconnected, durationMs: Date.now() - started });
|
|
70
|
-
};
|
|
71
|
-
const parser = createNonceFrameParser(nonce, {
|
|
72
|
-
onOutput: (t) => {
|
|
73
|
-
ring.push(t);
|
|
74
|
-
},
|
|
75
|
-
onFrame: ({ exit, cwd }) => finish({ output: ring.text, exit, cwd, timedOut: interrupted }),
|
|
76
|
-
});
|
|
77
|
-
this.onChunk = (c) => parser.feed(c);
|
|
78
|
-
this.write(wrapPtyCommand(cmd, nonce));
|
|
79
|
-
if (dOpts.stdin !== undefined)
|
|
80
|
-
this.write(dOpts.stdin); // Ruling 11: REPL 式紧随
|
|
81
|
-
interruptTimer = setTimeout(() => {
|
|
82
|
-
interrupted = true;
|
|
83
|
-
// P8 两段打断:\x03 中断前台作业后,交互 bash 会放弃当前命令行的剩余部分
|
|
84
|
-
// (尾部 marker 不再执行);紧随注入的同 nonce 130-marker 行在 bash 回到
|
|
85
|
-
// 读取态后被执行。无论 bash 弃行还是续行,帧都在宽限内到达且 exit=130,
|
|
86
|
-
// 会话存活;若命令 trap 掉 SIGINT,原 marker 先帧、注入行帧后字节被丢弃。
|
|
87
|
-
this.write('\x03');
|
|
88
|
-
this.write(`printf '\\033]133;D;${nonce};130;%s\\007' "$PWD"\n`);
|
|
89
|
-
killTimer = setTimeout(() => {
|
|
90
|
-
// Ruling 8 的 kill 兜底:宽限无帧 = 会话不可恢复,杀整组后收尸
|
|
91
|
-
// (否则 trap-INT 的卡死进程永生,重连后旧组再不可达)
|
|
92
|
-
this.opts.channel.kill();
|
|
93
|
-
this.markDead();
|
|
94
|
-
finish({ output: ring.text, exit: null, cwd: null, timedOut: true });
|
|
95
|
-
}, INTERRUPT_GRACE_MS);
|
|
96
|
-
}, dOpts.timeoutSec === undefined ? 120_000 : dOpts.timeoutSec * 1_000);
|
|
97
|
-
this.deathWaiters.push(deathWaiter);
|
|
98
|
-
});
|
|
99
|
-
};
|
|
100
|
-
const run = this.queue.then(attempt, attempt);
|
|
101
|
-
this.queue = run.then(() => undefined, () => undefined);
|
|
102
|
-
return run;
|
|
103
|
-
}
|
|
104
|
-
/** TTL / row teardown:尽力 exit,1s 后杀组,出池。 */
|
|
105
|
-
async dispose() {
|
|
106
|
-
if (this.state === 'disposed')
|
|
107
|
-
return;
|
|
108
|
-
this.clearIdleTimer();
|
|
109
|
-
this.state = 'disposed';
|
|
110
|
-
this.startPromise = null;
|
|
111
|
-
this.opts.channel.close();
|
|
112
|
-
this.opts.onDead?.();
|
|
113
|
-
}
|
|
114
|
-
write(s) {
|
|
115
|
-
this.opts.channel.write(s);
|
|
116
|
-
}
|
|
117
|
-
async start() {
|
|
118
|
-
if (this.state === 'disposed')
|
|
119
|
-
throw new Error(`[E_SESSION_DISPOSED] remote pty session '${this.opts.key}' was disposed — the pool replaces disposed sessions; this reference is stale`);
|
|
120
|
-
if (this.startPromise === null) {
|
|
121
|
-
this.state = 'starting';
|
|
122
|
-
this.startPromise = this.doStart().catch((err) => {
|
|
123
|
-
this.startPromise = null;
|
|
124
|
-
this.markDead();
|
|
125
|
-
throw err;
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
return this.startPromise;
|
|
129
|
-
}
|
|
130
|
-
async doStart() {
|
|
131
|
-
const initTimeoutSec = this.opts.initTimeoutSec ?? DEFAULT_INIT_TIMEOUT_SEC;
|
|
132
|
-
const initNonce = genNonce();
|
|
133
|
-
await new Promise((resolve, reject) => {
|
|
134
|
-
const timer = setTimeout(() => {
|
|
135
|
-
this.opts.channel.kill();
|
|
136
|
-
reject(new Error(`[E_SESSION_START] remote pty session '${this.opts.key}' did not initialize in ${initTimeoutSec}s` +
|
|
137
|
-
(this.diag().length > 0 ? `; transport stderr tail: ${this.diag()}` : '')));
|
|
138
|
-
}, initTimeoutSec * 1_000);
|
|
139
|
-
const waiter = () => {
|
|
140
|
-
clearTimeout(timer);
|
|
141
|
-
reject(new Error(`[E_SESSION_DIED] remote pty session '${this.opts.key}' died before initializing` +
|
|
142
|
-
(this.diag().length > 0 ? `; transport stderr tail: ${this.diag()}` : '')));
|
|
143
|
-
};
|
|
144
|
-
this.deathWaiters.push(waiter);
|
|
145
|
-
const parser = createNonceFrameParser(initNonce, {
|
|
146
|
-
onFrame: () => {
|
|
147
|
-
clearTimeout(timer);
|
|
148
|
-
this.deathWaiters = this.deathWaiters.filter((w) => w !== waiter);
|
|
149
|
-
if (this.state === 'disposed') {
|
|
150
|
-
// dispose 竞态 init:不得复活已终态会话(否则重挂 idle timer + 幽灵 ready)
|
|
151
|
-
reject(new Error(`[E_SESSION_DISPOSED] remote pty session '${this.opts.key}' disposed during init`));
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
this.state = 'ready';
|
|
155
|
-
this.resetIdleTimer();
|
|
156
|
-
resolve();
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
this.onChunk = (c) => parser.feed(c);
|
|
160
|
-
this.opts.channel.open((chunk) => this.onChunk?.(chunk))
|
|
161
|
-
.catch((err) => {
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
this.deathWaiters = this.deathWaiters.filter((w) => w !== waiter);
|
|
164
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
165
|
-
});
|
|
166
|
-
this.write(buildInitCommand(initNonce));
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
diag() { return this.opts.channel.diag; }
|
|
170
|
-
markDead() {
|
|
171
|
-
if (this.state === 'disposed') {
|
|
172
|
-
// dispose 是终态:不再翻转 state,但在飞 dispatch 的死亡等待仍需立即结算
|
|
173
|
-
// (row teardown 不是超时——不得等 123s 看门狗,也不得错标 timedOut)
|
|
174
|
-
const waiters = this.deathWaiters;
|
|
175
|
-
this.deathWaiters = [];
|
|
176
|
-
for (const w of waiters)
|
|
177
|
-
w();
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
this.state = 'dead';
|
|
181
|
-
this.startPromise = null;
|
|
182
|
-
this.clearIdleTimer();
|
|
183
|
-
const waiters = this.deathWaiters;
|
|
184
|
-
this.deathWaiters = [];
|
|
185
|
-
for (const w of waiters)
|
|
186
|
-
w();
|
|
187
|
-
}
|
|
188
|
-
resetIdleTimer() {
|
|
189
|
-
this.clearIdleTimer();
|
|
190
|
-
this.idleTimer = setTimeout(() => void this.dispose(), this.opts.idleTtlSec * 1_000);
|
|
191
|
-
this.idleTimer.unref();
|
|
192
|
-
}
|
|
193
|
-
clearIdleTimer() {
|
|
194
|
-
if (this.idleTimer !== null) {
|
|
195
|
-
clearTimeout(this.idleTimer);
|
|
196
|
-
this.idleTimer = null;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
/** 会话池:键 → 会话。意外死亡的会话留在池内(下次 dispatch 带 reconnect 提示)。 */
|
|
201
|
-
export class PtyPool {
|
|
202
|
-
defaults;
|
|
203
|
-
sessions = new Map();
|
|
204
|
-
constructor(defaults) {
|
|
205
|
-
this.defaults = defaults;
|
|
206
|
-
}
|
|
207
|
-
get size() { return this.sessions.size; }
|
|
208
|
-
/** status 面:键上的会话快照(无会话 = undefined → "none — dials on first exec")。 */
|
|
209
|
-
inspect(key) {
|
|
210
|
-
return this.sessions.get(key)?.snapshot;
|
|
211
|
-
}
|
|
212
|
-
/** roster 面:池内全部会话。 */
|
|
213
|
-
list() {
|
|
214
|
-
return [...this.sessions.entries()].map(([key, s]) => ({ key, snapshot: s.snapshot }));
|
|
215
|
-
}
|
|
216
|
-
getOrCreate(key, channelOf) {
|
|
217
|
-
let s = this.sessions.get(key);
|
|
218
|
-
if (s === undefined || s.sessionState === 'disposed') {
|
|
219
|
-
s = new PtySession({ key, channel: channelOf(), ...this.defaults, onDead: () => this.sessions.delete(key) });
|
|
220
|
-
this.sessions.set(key, s);
|
|
221
|
-
}
|
|
222
|
-
return s;
|
|
223
|
-
}
|
|
224
|
-
async disposeAll() {
|
|
225
|
-
await Promise.all([...this.sessions.values()].map((s) => s.dispose()));
|
|
226
|
-
this.sessions.clear();
|
|
227
|
-
}
|
|
228
|
-
}
|