Skip to content

Commit a791547

Browse files
committed
Harden desktop proof strict-mode checks
1 parent c9ed151 commit a791547

2 files changed

Lines changed: 92 additions & 12 deletions

File tree

dungeon/scripts/record-desktop-proof.mjs

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ function parseArgs(argv) {
3737
.split(',')
3838
.map((item) => item.trim())
3939
.filter(Boolean),
40+
closeBlockerProcesses: (process.env.SEO_DUNGEON_DESKTOP_PROOF_CLOSE_BLOCKER_PROCESSES || '')
41+
.split(',')
42+
.map((item) => item.trim())
43+
.filter(Boolean),
44+
allowFallbackProject: process.env.SEO_DUNGEON_DESKTOP_PROOF_ALLOW_FALLBACK_PROJECT === '1',
4045
allowForegroundMismatch: process.env.SEO_DUNGEON_DESKTOP_PROOF_ALLOW_FOREGROUND_MISMATCH === '1',
4146
};
4247

@@ -71,6 +76,13 @@ function parseArgs(argv) {
7176
if (!processName) throw new Error('--blocker-process cannot be empty.');
7277
options.blockerProcesses.push(processName);
7378
}
79+
else if (token === '--close-blocker-process') {
80+
const processName = readValue().trim();
81+
if (!processName) throw new Error('--close-blocker-process cannot be empty.');
82+
options.closeBlockerProcesses.push(processName);
83+
options.blockerProcesses.push(processName);
84+
}
85+
else if (token === '--allow-fallback-project') options.allowFallbackProject = true;
7486
else if (token === '--allow-foreground-mismatch') options.allowForegroundMismatch = true;
7587
else if (token === '--help' || token === '-h') options.help = true;
7688
else throw new Error(`Unknown option: ${token}`);
@@ -102,7 +114,8 @@ function usage() {
102114
' node scripts/record-desktop-proof.mjs [--output-dir path] [--domain seodungeon.com] [--project E:\\seo-dungeon-website]',
103115
' [--browser-x 960] [--browser-y 0] [--browser-width 960] [--browser-height 1040] [--fake-codex|--real-codex]',
104116
' [--browser-command "..."] [--codex-command "..."] [--command-timeout-ms 120000]',
105-
' [--minimize-known-blockers] [--hide-known-blockers] [--blocker-process CapCut] [--allow-foreground-mismatch]',
117+
' [--minimize-known-blockers] [--hide-known-blockers] [--blocker-process CapCut] [--close-blocker-process Taskmgr]',
118+
' [--allow-fallback-project] [--allow-foreground-mismatch]',
106119
'',
107120
'Records a full-desktop RC-008 rehearsal with SEO Dungeon in a headed browser window.',
108121
'This does not automate the Codex desktop UI. Put Codex on the left before running for side-by-side proof framing.',
@@ -480,9 +493,31 @@ if ($pids.Count -gt 0) {
480493
return result.stdout.trim().split(/\r?\n/).filter(Boolean);
481494
}
482495

483-
async function assertBrowserForeground(options, logOutput = []) {
496+
async function closeBlockerProcess(processName, pid, logOutput = []) {
497+
const safeProcessName = String(processName || '').replace(/'/g, "''");
498+
const safePid = Number(pid);
499+
if (!safeProcessName || !Number.isInteger(safePid) || safePid <= 0) {
500+
throw new Error(`Invalid blocker process close request: ${processName}:${pid}`);
501+
}
502+
const script = `
503+
$p = Get-Process -Id ${safePid} -ErrorAction Stop
504+
if ($p.ProcessName -ne '${safeProcessName}') {
505+
Write-Error "PID ${safePid} is $($p.ProcessName), not ${safeProcessName}"
506+
exit 3
507+
}
508+
Stop-Process -Id ${safePid} -Force
509+
Write-Output "$($p.ProcessName):$($p.Id)"
510+
`;
511+
const result = await runTool('powershell', ['-NoProfile', '-Command', script], 8000);
512+
logOutput.push(`\n[close-blocker-process] code=${result.code}\n${result.stdout}${result.stderr}\n`);
513+
if (result.code !== 0) throw new Error(`Could not close blocker process:\n${result.stderr || result.stdout}`);
514+
return result.stdout.trim().split(/\r?\n/).filter(Boolean);
515+
}
516+
517+
async function assertBrowserForeground(options, logOutput = [], blockerActions = []) {
484518
if (options.minimizeKnownBlockers) {
485-
await minimizeKnownBlockers(logOutput, options.hideKnownBlockers, options.blockerProcesses);
519+
blockerActions.push(...(await minimizeKnownBlockers(logOutput, options.hideKnownBlockers, options.blockerProcesses))
520+
.map((target) => ({ action: options.hideKnownBlockers ? 'hide' : 'minimize', target })));
486521
}
487522
await focusBrowserWindowByTitle('SEO Dungeon Desktop Proof', logOutput);
488523
await new Promise((resolve) => setTimeout(resolve, 350));
@@ -492,7 +527,18 @@ async function assertBrowserForeground(options, logOutput = []) {
492527
foregroundInfo.processName &&
493528
options.blockerProcesses.some((name) => name.toLowerCase() === foregroundInfo.processName.toLowerCase())
494529
) {
495-
await minimizeKnownBlockers(logOutput, options.hideKnownBlockers, options.blockerProcesses);
530+
blockerActions.push(...(await minimizeKnownBlockers(logOutput, options.hideKnownBlockers, options.blockerProcesses))
531+
.map((target) => ({ action: options.hideKnownBlockers ? 'hide' : 'minimize', target })));
532+
await focusBrowserWindowByTitle('SEO Dungeon Desktop Proof', logOutput);
533+
await new Promise((resolve) => setTimeout(resolve, 350));
534+
foregroundInfo = await getForegroundWindowInfo(logOutput);
535+
}
536+
if (
537+
foregroundInfo.processName &&
538+
options.closeBlockerProcesses.some((name) => name.toLowerCase() === foregroundInfo.processName.toLowerCase())
539+
) {
540+
blockerActions.push(...(await closeBlockerProcess(foregroundInfo.processName, foregroundInfo.pid, logOutput))
541+
.map((target) => ({ action: 'close', target })));
496542
await focusBrowserWindowByTitle('SEO Dungeon Desktop Proof', logOutput);
497543
await new Promise((resolve) => setTimeout(resolve, 350));
498544
foregroundInfo = await getForegroundWindowInfo(logOutput);
@@ -579,15 +625,24 @@ async function main() {
579625
let vite;
580626
let recorder;
581627
let foregroundBeforeCapture = '';
628+
let bridgeHealth = null;
629+
let watchedEvent = null;
630+
let sendJson = null;
631+
const blockerActions = [];
582632
let failureError = null;
583633

584634
fs.mkdirSync(outputDir, { recursive: true });
585635
fs.mkdirSync(fallbackProject, { recursive: true });
586636
fs.writeFileSync(path.join(fallbackProject, 'README.md'), '# SEO Dungeon Desktop Proof Fallback Project\n', 'utf8');
587637
writeFakeCodexAppServer(fakeCodexAppServer, options);
588638

589-
const projectPath = fs.existsSync(options.projectPath)
590-
? path.resolve(options.projectPath)
639+
const requestedProjectPath = path.resolve(options.projectPath);
640+
const projectPathExists = fs.existsSync(options.projectPath);
641+
if (!projectPathExists && !options.fakeCodex && !options.allowFallbackProject) {
642+
throw new Error(`Project path does not exist for real-Codex proof: ${options.projectPath}`);
643+
}
644+
const projectPath = projectPathExists
645+
? requestedProjectPath
591646
: fallbackProject;
592647

593648
try {
@@ -596,6 +651,7 @@ async function main() {
596651
env: {
597652
...process.env,
598653
SEO_DUNGEON_BRIDGE_PORT: String(bridgePort),
654+
SEO_DUNGEON_BRIDGE_STRICT_PORT: '1',
599655
SEO_DUNGEON_ALLOWED_ORIGINS: origin,
600656
...(options.fakeCodex
601657
? {
@@ -616,7 +672,10 @@ async function main() {
616672
vite.stdout.on('data', (chunk) => viteOutput.push(chunk.toString()));
617673
vite.stderr.on('data', (chunk) => viteOutput.push(chunk.toString()));
618674

619-
await waitForHttp(`http://127.0.0.1:${bridgePort}/health`, 'bridge', bridgeOutput, bridge);
675+
const bridgeHealthResponse = await waitForHttp(`http://127.0.0.1:${bridgePort}/health`, 'bridge', bridgeOutput, bridge);
676+
bridgeHealth = await bridgeHealthResponse.json();
677+
assert.equal(bridgeHealth.ok, true);
678+
assert.equal(bridgeHealth.supportsRemoteControl, true);
620679
await waitForHttp(origin, 'vite', viteOutput, vite);
621680

622681
browser = await chromium.launch({
@@ -649,7 +708,7 @@ async function main() {
649708
await page.locator('#danger-mode-toggle').click();
650709
}
651710
await positionBrowserWindow(page, options);
652-
foregroundBeforeCapture = await assertBrowserForeground(options, ffmpegOutput);
711+
foregroundBeforeCapture = await assertBrowserForeground(options, ffmpegOutput, blockerActions);
653712

654713
recorder = startDesktopRecorder({ capturePath: rawVideoPath, fps: options.fps, ffmpegOutput });
655714
await new Promise((resolve) => setTimeout(resolve, 800));
@@ -680,7 +739,7 @@ async function main() {
680739
fs.writeFileSync(watchOutputPath, watchResult.stdout, 'utf8');
681740
assert.equal(watchResult.code, 0, watchResult.stdout);
682741
const watchLines = parseJsonLines(watchResult.stdout);
683-
const watchedEvent = watchLines.find((line) => line.type === 'session-event');
742+
watchedEvent = watchLines.find((line) => line.type === 'session-event');
684743
assert.equal(watchedEvent?.event?.command, options.browserCommand);
685744
assert.equal(watchedEvent?.event?.projectPath, projectPath);
686745

@@ -694,7 +753,7 @@ async function main() {
694753
'--json',
695754
'--wait',
696755
'--timeout',
697-
'15000',
756+
String(options.commandTimeoutMs),
698757
'--project',
699758
projectPath,
700759
'--profile',
@@ -705,7 +764,7 @@ async function main() {
705764
], { bridgeWs, origin, timeoutMs: options.commandTimeoutMs + 3000 });
706765
fs.writeFileSync(sendOutputPath, sendResult.stdout, 'utf8');
707766
assert.equal(sendResult.code, 0, sendResult.stdout);
708-
const sendJson = JSON.parse(sendResult.stdout);
767+
sendJson = JSON.parse(sendResult.stdout);
709768
assert.equal(sendJson.ok, true);
710769
assert.equal(sendJson.waitEvent?.status, 'complete');
711770
await waitForLedger(page, new RegExp(`Remote codex-cli: ${escapeRegExp(options.codexCommand)}`, 'i'), 'desktop proof helper command mirrored', options.commandTimeoutMs);
@@ -739,11 +798,19 @@ async function main() {
739798
projectPath,
740799
browserCommand: options.browserCommand,
741800
codexCommand: options.codexCommand,
801+
commandTimeoutMs: options.commandTimeoutMs,
742802
usedFallbackProject: projectPath === fallbackProject,
803+
allowFallbackProject: options.allowFallbackProject,
743804
fakeCodex: options.fakeCodex,
805+
codexTransport: bridgeHealth?.defaultCodexTransport || null,
806+
codexCliOverride: options.fakeCodex ? process.execPath : (process.env.SEO_DUNGEON_CODEX_CLI || null),
807+
codexArgsOverride: options.fakeCodex ? `"${fakeCodexAppServer}"` : (process.env.SEO_DUNGEON_CODEX_ARGS || null),
808+
bridgeHealth,
744809
minimizedKnownBlockers: options.minimizeKnownBlockers,
745810
hiddenKnownBlockers: options.hideKnownBlockers,
746811
blockerProcesses: options.blockerProcesses,
812+
closeBlockerProcesses: options.closeBlockerProcesses,
813+
blockerActions,
747814
allowForegroundMismatch: options.allowForegroundMismatch,
748815
foregroundBeforeCapture,
749816
browserWindow: {
@@ -792,11 +859,23 @@ async function main() {
792859
projectPath,
793860
browserCommand: options.browserCommand,
794861
codexCommand: options.codexCommand,
862+
commandTimeoutMs: options.commandTimeoutMs,
795863
usedFallbackProject: projectPath === fallbackProject,
864+
allowFallbackProject: options.allowFallbackProject,
796865
fakeCodex: options.fakeCodex,
866+
codexTransport: bridgeHealth?.defaultCodexTransport || null,
867+
codexCliOverride: options.fakeCodex ? process.execPath : (process.env.SEO_DUNGEON_CODEX_CLI || null),
868+
codexArgsOverride: options.fakeCodex ? `"${fakeCodexAppServer}"` : (process.env.SEO_DUNGEON_CODEX_ARGS || null),
869+
bridgeHealth,
870+
watchedEvent,
871+
sendCommandId: sendJson?.data?.commandId || null,
872+
sendEvent: sendJson?.data?.event || null,
873+
waitEvent: sendJson?.waitEvent || null,
797874
minimizedKnownBlockers: options.minimizeKnownBlockers,
798875
hiddenKnownBlockers: options.hideKnownBlockers,
799876
blockerProcesses: options.blockerProcesses,
877+
closeBlockerProcesses: options.closeBlockerProcesses,
878+
blockerActions,
800879
foregroundBeforeCapture,
801880
browserWindow: {
802881
x: options.browserX,

dungeon/server/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const path = require('path');
55
const fs = require('fs');
66

77
const DEFAULT_PORT = Number(process.env.SEO_DUNGEON_BRIDGE_PORT || 3003);
8+
const STRICT_PORT = process.env.SEO_DUNGEON_BRIDGE_STRICT_PORT === '1';
89
let PORT = DEFAULT_PORT;
910

1011
// Project root: server/ -> dungeon/ -> seo-dungeon/
@@ -2396,7 +2397,7 @@ function startBridge() {
23962397
function listenWithFallback(port, attemptsLeft = 24) {
23972398
const onError = (err) => {
23982399
server.off('listening', onListening);
2399-
if (err.code === 'EADDRINUSE' && attemptsLeft > 0) {
2400+
if (err.code === 'EADDRINUSE' && attemptsLeft > 0 && !STRICT_PORT) {
24002401
const nextPort = port + 1;
24012402
console.warn(`Bridge port ${port} is busy; trying ${nextPort}.`);
24022403
setTimeout(() => listenWithFallback(nextPort, attemptsLeft - 1), 80);

0 commit comments

Comments
 (0)