@agfpd/iapeer 0.2.70 → 0.2.71
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/package.json +1 -1
- package/src/supervisor/daemon.test.ts +36 -0
- package/src/supervisor/daemon.ts +37 -16
package/package.json
CHANGED
|
@@ -174,4 +174,40 @@ d('supervisor daemon (Bun-native pty, tick runtime)', () => {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
})
|
|
177
|
+
|
|
178
|
+
// FINDING-2 — restore the SERVE geometry on last detach. The daemon resizes the child pty to a
|
|
179
|
+
// client's size on attach and (before this) never restored it, so an unattached session stuck at a
|
|
180
|
+
// departed client's size → the fixed-geometry warm-deliver/ready-gate viewport model mismatched the
|
|
181
|
+
// pty. Drive it with a child that prints its live pty winsize ("ROWS COLS") to the pane-log: attach
|
|
182
|
+
// small → pty shrinks; detach → pty restores to the serve geometry.
|
|
183
|
+
test('FINDING-2: last detach restores the serve geometry (no sticking at the client size)', async () => {
|
|
184
|
+
const gSession = 'ptsgeo'
|
|
185
|
+
const paneLog = join(runDir, 'geo-pane.log')
|
|
186
|
+
const lastSize = (): string => {
|
|
187
|
+
const t = existsSync(paneLog) ? readFileSync(paneLog, 'utf8') : ''
|
|
188
|
+
const m = [...t.matchAll(/(\d+) (\d+)/g)]
|
|
189
|
+
return m.length ? `${m[m.length - 1][1]}x${m[m.length - 1][2]}` : ''
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const r = startSupervisorDaemon({
|
|
193
|
+
session: gSession,
|
|
194
|
+
runtime: 'tick',
|
|
195
|
+
runDir,
|
|
196
|
+
// serve geometry 200x50 (distinct from defaults); child reports its live pty winsize as "rows cols"
|
|
197
|
+
serve: { argv: ['sh', '-c', 'while :; do stty size 2>/dev/null; sleep 0.3; done'], env: { PATH: process.env.PATH ?? '' }, cwd: runDir, paneLogPath: paneLog, cols: 200, rows: 50 },
|
|
198
|
+
})
|
|
199
|
+
expect(r.state).toBe('started')
|
|
200
|
+
expect(await waitFor(() => lastSize() === '50 200'.replace(' ', 'x'), 6000)).toBe(true) // at serve geometry
|
|
201
|
+
const c = await attach(runDir, gSession, 90, 30) // attach at a DIFFERENT size → pty resizes to it
|
|
202
|
+
expect(await waitFor(() => lastSize() === '30x90', 6000)).toBe(true) // pty shrank to the client size
|
|
203
|
+
c.close() // last client gone
|
|
204
|
+
expect(await waitFor(() => lastSize() === '50x200', 6000)).toBe(true) // RESTORED to serve geometry
|
|
205
|
+
} finally {
|
|
206
|
+
try {
|
|
207
|
+
killSession(runDir, gSession)
|
|
208
|
+
} catch {
|
|
209
|
+
/* */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
})
|
|
177
213
|
})
|
package/src/supervisor/daemon.ts
CHANGED
|
@@ -90,6 +90,13 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
90
90
|
const { session, runtime, runDir } = opts
|
|
91
91
|
let cols = opts.cols ?? 80
|
|
92
92
|
let rows = opts.rows ?? 30
|
|
93
|
+
// The SERVE geometry — the size the session is hosted at (HOST_COLS×HOST_ROWS for a pty-host peer),
|
|
94
|
+
// immutable. `cols`/`rows` above track the CURRENT (client-driven) size. A client attaches at its own
|
|
95
|
+
// terminal size and the pty resizes to match; on LAST detach we restore the serve geometry (below) so
|
|
96
|
+
// the unattached session never sticks at a departed client's size — otherwise the warm-deliver /
|
|
97
|
+
// ready-gate viewport model (built at the fixed serve geometry) mismatches the actually-resized pty.
|
|
98
|
+
const serveCols = cols
|
|
99
|
+
const serveRows = rows
|
|
93
100
|
const sock = sockPath(runDir, session)
|
|
94
101
|
const pid = pidPath(runDir, session)
|
|
95
102
|
|
|
@@ -217,28 +224,40 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
217
224
|
sendClient(s, frame(FRAME_DATA, Buffer.from(snap, 'utf8')))
|
|
218
225
|
}
|
|
219
226
|
|
|
227
|
+
// Resize the model + child pty to (c, r). Shared by client FRAME_RESIZE and the last-detach restore.
|
|
228
|
+
const applyGeometry = (c: number, r: number): void => {
|
|
229
|
+
cols = c
|
|
230
|
+
rows = r
|
|
231
|
+
try {
|
|
232
|
+
xterm.resize(c, r)
|
|
233
|
+
child.terminal.resize(c, r)
|
|
234
|
+
} catch {
|
|
235
|
+
/* transient */
|
|
236
|
+
}
|
|
237
|
+
// Best-effort SIGWINCH nudge for a SIGWINCH-reactive child (forces a repaint at the new size); NOT
|
|
238
|
+
// load-bearing for an attached client (the complete snapshot repaint is), but for the unattached
|
|
239
|
+
// restore it is what makes the child redraw into the pane-log at the serve geometry.
|
|
240
|
+
try {
|
|
241
|
+
child.terminal.resize(c, Math.max(1, r - 1))
|
|
242
|
+
child.terminal.resize(c, r)
|
|
243
|
+
} catch {
|
|
244
|
+
/* */
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Restore the serve geometry when the last client detaches, so the unattached session never sticks at
|
|
248
|
+
// a departed client's size (FINDING-2 — would mismatch the fixed-geometry warm-deliver/ready-gate
|
|
249
|
+
// viewport model). No-op when already at serve geometry or a client is still attached.
|
|
250
|
+
const restoreServeGeometryIfIdle = (): void => {
|
|
251
|
+
if (clients.size === 0 && (cols !== serveCols || rows !== serveRows)) applyGeometry(serveCols, serveRows)
|
|
252
|
+
}
|
|
253
|
+
|
|
220
254
|
const onClientFrame = (s: { write(b: Buffer): number }, type: number, payload: Buffer): void => {
|
|
221
255
|
if (type === FRAME_DATA) {
|
|
222
256
|
child.terminal.write(payload) // keystrokes → child
|
|
223
257
|
} else if (type === FRAME_RESIZE) {
|
|
224
258
|
const sz = parseSize(payload)
|
|
225
|
-
cols
|
|
226
|
-
rows = sz.rows
|
|
227
|
-
try {
|
|
228
|
-
xterm.resize(cols, rows)
|
|
229
|
-
child.terminal.resize(cols, rows)
|
|
230
|
-
} catch {
|
|
231
|
-
/* transient */
|
|
232
|
-
}
|
|
259
|
+
applyGeometry(sz.cols, sz.rows)
|
|
233
260
|
repaint(s) // instant paint from the snapshot (delivered COMPLETE via backpressure)
|
|
234
|
-
// Best-effort SIGWINCH nudge for a SIGWINCH-reactive child; NOT load-bearing (the complete
|
|
235
|
-
// snapshot above is the real freshness fix).
|
|
236
|
-
try {
|
|
237
|
-
child.terminal.resize(cols, Math.max(1, rows - 1))
|
|
238
|
-
child.terminal.resize(cols, rows)
|
|
239
|
-
} catch {
|
|
240
|
-
/* */
|
|
241
|
-
}
|
|
242
261
|
}
|
|
243
262
|
}
|
|
244
263
|
|
|
@@ -266,11 +285,13 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
266
285
|
clients.delete(s)
|
|
267
286
|
writers.delete(s)
|
|
268
287
|
syncAttached()
|
|
288
|
+
restoreServeGeometryIfIdle() // last client gone → un-stick the pty from its size
|
|
269
289
|
},
|
|
270
290
|
error(s: object) {
|
|
271
291
|
clients.delete(s)
|
|
272
292
|
writers.delete(s)
|
|
273
293
|
syncAttached()
|
|
294
|
+
restoreServeGeometryIfIdle()
|
|
274
295
|
},
|
|
275
296
|
},
|
|
276
297
|
})
|