mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 06:59:42 +02:00
fix(remote): stop the flush losing a chunk, and reset the host form's wake fields
Own review pass over the PR:
- `_flush` took the chunk out of the buffer only AFTER awaiting the write. Input
arriving during that await is enqueued (`waking` is still set, so it takes the
buffer path), and the 4 KB cap then drops the OLDEST chunk — which is the one
already on its way to the pane. The `shift()` that followed removed the NEXT
chunk instead, so the drop-oldest bookkeeping silently lost a chunk that was
never written, while the log line blamed the one that was. The chunk is now
removed before the await and re-inserted at the FRONT on a failed write, so the
order of the queue behind it is preserved. Regression test: a chunk enqueued
during the first write of a full buffer must still reach the pane (red against
the old order).
- `showCreateCaseModal()` reset the remote-host form fields but not the two new
wake inputs, so one host's MAC/command carried over into the next host that
form saved.
- The banner's pre-poll `wakeConfigured` labelled a command-only host as 'mac'.
Nothing reads the distinction, but the field is documented as which path is
configured, so it says the truth until the first poll corrects it.
- Stale `resolveRemote` comment ("only for sessions that have no usable target of
their own"): after the host config became authoritative in both directions it is
consulted on the TTL regardless.
This commit is contained in:
+9
-1
@@ -690,14 +690,22 @@ export class RemoteWakeRegistry {
|
||||
private async _flush(state: WakeState, session: WakeableSession): Promise<void> {
|
||||
while (state.pending.length > 0) {
|
||||
const chunk = state.pending[0];
|
||||
// Take the chunk OUT before awaiting the write. Input arriving during the await is
|
||||
// enqueued by `handleInput` (a wake is still in flight, so it takes the buffer
|
||||
// path), and `appendBoundedPending` may then drop the OLDEST chunk to stay under
|
||||
// the cap — which would be this one, already on its way to the pane. Shifting
|
||||
// afterwards removed the NEXT chunk instead, so the drop-oldest bookkeeping lost a
|
||||
// chunk that was never written while the log line blamed the one that was.
|
||||
state.pending = state.pending.slice(1);
|
||||
const ok = await session.writeViaMux(chunk).catch(() => false);
|
||||
if (!ok) {
|
||||
// Retain it, IN ORDER: a failed write must not reorder the queue behind it.
|
||||
state.pending = [chunk, ...state.pending];
|
||||
this.deps.log?.(
|
||||
`[RemoteWake] flush failed for session ${session.id} — ${state.pending.length} chunk(s) retained`
|
||||
);
|
||||
return;
|
||||
}
|
||||
state.pending.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +110,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
state.label = session.remote.label || 'Remote host';
|
||||
// Text from the session payload first (instant, no round trip), corrected by the
|
||||
// poll — a session whose wake config was added after launch only knows it after
|
||||
// the server resolves host config.
|
||||
state.wakeConfigured = session.remote.wakeMac || session.remote.wakeCommand ? 'mac' : 'none';
|
||||
// the server resolves host config. The kind matters: the payload can say WHICH
|
||||
// path is configured, so a command-only host is not mislabelled 'mac' until the
|
||||
// first poll lands.
|
||||
state.wakeConfigured = session.remote.wakeMac ? 'mac' : session.remote.wakeCommand ? 'command' : 'none';
|
||||
this._renderHostWakeBanner();
|
||||
}
|
||||
this._pollHostReachability();
|
||||
|
||||
@@ -2474,6 +2474,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
'remoteHostSocksProxy',
|
||||
'remoteHostJumpHost',
|
||||
'remoteHostExtraSshOptions',
|
||||
// Wake-on-LAN: they belong to the HOST being configured, so leaving them filled in
|
||||
// would carry one host's MAC/command onto the next host this form saves.
|
||||
'remoteHostWakeMac',
|
||||
'remoteHostWakeCommand',
|
||||
];
|
||||
remoteFields.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
|
||||
@@ -866,8 +866,9 @@ export function registerSessionRoutes(
|
||||
log: (message) => console.log(message),
|
||||
// The session's `remote` block is a launch-time snapshot, so a wake target
|
||||
// configured later (banner's config dialog, or a hand-edited remote-hosts.json)
|
||||
// is resolved here — throttled by the registry, and only for sessions that
|
||||
// have no usable target of their own.
|
||||
// is resolved here — throttled by the registry, and the host config is
|
||||
// authoritative in BOTH directions (removing the field turns the feature off
|
||||
// for a live session too).
|
||||
resolveRemote: async (session) => {
|
||||
const remote = session.remote;
|
||||
if (!remote) return undefined;
|
||||
|
||||
@@ -372,6 +372,38 @@ describe('RemoteWakeRegistry', () => {
|
||||
expect(h.registry.pendingBytes('sess-1')).toBe(3);
|
||||
});
|
||||
|
||||
it('flushes the chunk it is writing out of the buffer first, so a concurrent enqueue cannot drop a different one', async () => {
|
||||
// Input arriving DURING the flush is enqueued (`waking` is still set), and the cap
|
||||
// then drops the OLDEST chunk — the one already on its way to the pane. Shifting the
|
||||
// buffer after the write removed the NEXT chunk instead, so the drop-oldest
|
||||
// bookkeeping lost a chunk that was never written.
|
||||
const h = harness();
|
||||
h.probe.mockResolvedValue(false);
|
||||
let release: (() => void) | undefined;
|
||||
h.waitUntilReady.mockImplementation(
|
||||
() =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
release = () => resolve(true);
|
||||
})
|
||||
);
|
||||
|
||||
const big = 'a'.repeat(REMOTE_WAKE_PENDING_MAX_BYTES - 10);
|
||||
await h.registry.handleInput(h.session, big);
|
||||
await h.registry.handleInput(h.session, 'bbbbbbbbbb'); // fills the cap exactly
|
||||
// The third chunk arrives while the FIRST write is in flight, which is what pushes
|
||||
// the buffer over the cap mid-flush.
|
||||
h.writeViaMux.mockImplementationOnce(async () => {
|
||||
await h.registry.handleInput(h.session, 'c');
|
||||
return true;
|
||||
});
|
||||
|
||||
release?.();
|
||||
await h.registry.wake(h.session);
|
||||
|
||||
expect(h.writeViaMux.mock.calls.map((c) => c[0])).toEqual([big, 'bbbbbbbbbb', 'c']);
|
||||
expect(h.registry.pendingBytes('sess-1')).toBe(0);
|
||||
});
|
||||
|
||||
it('ensureAwake blocks only for the wait path and returns true without a wake command', async () => {
|
||||
const h = harness({ remote: { hostId: 'x', label: 'X', host: '10.0.0.9' } });
|
||||
await expect(h.registry.ensureAwake(h.session)).resolves.toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user