mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
fix(remote): merge-time fixes for Wake-on-LAN
The MAC-count limit lived in two places that disagreed. RemoteHostSchema.wakeMac's 128-character cap admits seven comma-separated MACs while parseMacList takes at most four, all-or-nothing, so a five-MAC value validated, was written to remote-hosts.json, and then resolved to NO wake target: POST /api/sessions/:id/wake answered "No wake-on-LAN target configured for this host" and the banner offered "Configure WoL" for a host the user had just configured. MAX_WAKE_MACS now lives in src/config/remote-wake-limits.ts and both sides refine against it. Its own module because src/remote-wake.ts is import-fenced to session-routes.ts and server.ts (the wiring guard that stops a watcher waking a host), and because schemas.ts must not drag dgram/net/child_process into every request-validating module. The documented 40 s request budget also omitted the wake's own cost. A `command` target is bounded by REMOTE_WAKE_COMMAND_TIMEOUT_MS and runs BEFORE the readiness poll, so a slow one pushed a wakeCommand host's worst case to ~68 s, past the 60 s proxy_read_timeout the budget exists to stay under. _wakeAndWait now subtracts the wake's measured elapsed time from the readiness budget, floored at one poll interval so a wake that ate the whole budget still gets one probe. A magic packet is effectively instant and is unaffected, which is why live testing never saw it. Also: the two new endpoints are documented in docs/api-reference.md with the import fence stated as the rule it is, CLAUDE.md's frontend module count moves to 34, and the release changesets carry the Thanks section. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @fileoverview Limits shared between Wake-on-LAN parsing and its request schema.
|
||||
*
|
||||
* Its own module because `src/remote-wake.ts` is import-fenced: only
|
||||
* `web/routes/session-routes.ts` and `web/server.ts` may import it, so that no
|
||||
* watcher or boot-recovery path can WAKE a host (pinned by the wiring guard in
|
||||
* `test/remote-wake.test.ts`). `web/schemas.ts` needs the same MAC-count limit and
|
||||
* must not become a third importer, and it would drag `dgram`/`net`/`child_process`
|
||||
* into every module that validates a request body. A plain constant satisfies both.
|
||||
*/
|
||||
|
||||
/**
|
||||
* How many comma-separated MACs one `wakeMac` may carry.
|
||||
*
|
||||
* ⚠ Single source for `parseMacList()` and `RemoteHostSchema.wakeMac`. The two used
|
||||
* to disagree: the schema's 128-character cap admits seven MACs while the parser
|
||||
* rejected more than four all-or-nothing, so a five-MAC value validated, persisted to
|
||||
* `remote-hosts.json`, and then resolved to NO wake target. The host read as
|
||||
* unconfigured and the banner offered "Configure WoL" for a host the user had just
|
||||
* configured, which is the worst shape a validation gap can take: accepted, stored,
|
||||
* silently inert.
|
||||
*/
|
||||
export const MAX_WAKE_MACS = 4;
|
||||
+21
-2
@@ -37,6 +37,7 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import dgram from 'node:dgram';
|
||||
import net from 'node:net';
|
||||
import { MAX_WAKE_MACS } from './config/remote-wake-limits.js';
|
||||
|
||||
/** Minimum spacing between two reachability probes for the same session. */
|
||||
export const REMOTE_WAKE_PROBE_MIN_INTERVAL_MS = 30_000;
|
||||
@@ -54,6 +55,10 @@ export const REMOTE_WAKE_READY_TIMEOUT_MS = 90_000;
|
||||
* the browser reports a failure for a session that exists. The budget has to cover
|
||||
* the WHOLE request, not just the wait: 40 s here + the 1.5 s reachability probe +
|
||||
* the tmux prereq probe's own 15 s timeout = 56.5 s worst case, still under 60 s.
|
||||
* ⚠ The wake ITSELF counts against this, which the original arithmetic omitted: a
|
||||
* `command` target can spend REMOTE_WAKE_COMMAND_TIMEOUT_MS before the readiness poll
|
||||
* begins, which would have made the real worst case ~68 s. `_wakeAndWait` therefore
|
||||
* subtracts the wake's measured elapsed time from this budget rather than adding to it.
|
||||
* A warm S3 resume measures ~12 s, so 40 s is >3× the observed wake.
|
||||
*/
|
||||
export const REMOTE_WAKE_REQUEST_READY_TIMEOUT_MS = 40_000;
|
||||
@@ -200,7 +205,7 @@ export function resolveWakeTarget(remote: WakeableRemote | undefined): WakeTarge
|
||||
* Parse a comma-separated MAC list into byte arrays. Pure; returns null when any
|
||||
* entry is malformed (all-or-nothing, so a typo cannot half-arm a host).
|
||||
*/
|
||||
export function parseMacList(value: string, maxMacs = 4): number[][] | null {
|
||||
export function parseMacList(value: string, maxMacs = MAX_WAKE_MACS): number[][] | null {
|
||||
const parts = value
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
@@ -669,6 +674,7 @@ export class RemoteWakeRegistry {
|
||||
});
|
||||
this.deps.log?.(`[RemoteWake] waking ${remote.label} (${remote.host}) via ${target.kind} ${forWhat}`);
|
||||
|
||||
const wakeStartedAt = Date.now();
|
||||
const woke = await this.deps.wake(target);
|
||||
if (!woke) {
|
||||
this.deps.log?.(
|
||||
@@ -676,8 +682,21 @@ export class RemoteWakeRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
// The request-scoped budget has to cover the WHOLE request, and the wake is part
|
||||
// of it. A `command` target is bounded by REMOTE_WAKE_COMMAND_TIMEOUT_MS, so a slow
|
||||
// one burned 10 s before the readiness poll even started and pushed a wakeCommand
|
||||
// host's worst case to ~68 s, past the 60 s proxy_read_timeout this budget exists to
|
||||
// stay under. A magic packet is effectively instant, so this subtracts nothing there.
|
||||
// Floored at one poll interval so a wake that ate the whole budget still gets one
|
||||
// probe rather than being declared unreachable without asking.
|
||||
const wakeElapsedMs = Date.now() - wakeStartedAt;
|
||||
const readyTimeoutMs =
|
||||
opts.timeoutMs === undefined
|
||||
? undefined
|
||||
: Math.max(REMOTE_WAKE_READY_INTERVAL_MS, opts.timeoutMs - wakeElapsedMs);
|
||||
|
||||
const ready = await this.deps.waitUntilReady(remote, {
|
||||
timeoutMs: opts.timeoutMs,
|
||||
timeoutMs: readyTimeoutMs,
|
||||
signal: this.shutdown.signal,
|
||||
});
|
||||
if (!ready) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '../config/terminal-history.js';
|
||||
import { MAX_EDITABLE_BYTES } from '../config/file-editing.js';
|
||||
import { MIN_MATCH_LENGTH, MAX_MATCH_LENGTH } from '../config/agent-wait.js';
|
||||
import { MAX_WAKE_MACS } from '../config/remote-wake-limits.js';
|
||||
import { enabledCliIds, enabledClis } from '../config/cli-registry/registry.js';
|
||||
import type { SessionMode } from '../types.js';
|
||||
|
||||
@@ -760,6 +761,12 @@ export const RemoteHostSchema = z.object({
|
||||
/^[0-9a-fA-F]{2}([:-][0-9a-fA-F]{2}){5}(\s*,\s*[0-9a-fA-F]{2}([:-][0-9a-fA-F]{2}){5})*$/,
|
||||
'Wake MAC must be one or more MAC addresses, comma-separated'
|
||||
)
|
||||
// ⚠ The character cap admits seven MACs while parseMacList takes at most
|
||||
// MAX_WAKE_MACS, all-or-nothing. Without this the extra ones validated, persisted,
|
||||
// and then resolved to NO wake target, so the host read as unconfigured.
|
||||
.refine((value) => value.split(',').length <= MAX_WAKE_MACS, {
|
||||
message: `Wake MAC accepts at most ${MAX_WAKE_MACS} comma-separated addresses`,
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user