fix(engine): a render on Windows opens no console windows (#6613)

* ci(windows): probe that a render opens no visible window

* ci(windows): the probe names what opened each window

* ci(windows): the probe spawns each binary alone, with and without detached

* ci(windows): the probe logs the processes started during the render

* ci(windows): the probe also spawns Chrome one consoleless hop down

* ci(windows): the probe logs each child started without windowsHide

* ci(windows): the probe logs every child process with its windowsHide and detached

* fix(engine): Chrome keeps the CLI's console on Windows, so a render opens no window

* fix(cli): every child the CLI starts on Windows is hidden unless it opts out

* ci(windows): the probe bisects Chrome's flags and keeps each command's output

* ci(windows): the probe names the Chrome process behind each window

* ci(windows): the probe loads a page in Chrome directly and one hidden hop down

* ci(windows): the probe prints the Chrome main command line and renders with the reporter's flags

* ci(windows): the probe narrows the CLI's Chrome arguments to the ones that open a window

* ci(windows): the probe launches the CLI's Chrome arguments one hidden hop down

* ci(windows): the probe launches Chrome through the engine's browser pool

* ci(windows): the probe varies what the engine passes to Puppeteer

* fix(engine): renders on Windows launch Chrome with a Puppeteer that keeps it attached

* refactor(cli): hideChildConsoles copies its arguments and keeps one comment line
This commit is contained in:
Miguel Ángel
2026-10-28 07:48:40 +00:00
committed by GitHub
parent 1d97ed24c6
commit 6cf4b56697
22 changed files with 356 additions and 65 deletions
+92
View File
@@ -0,0 +1,92 @@
# Runs agent-facing commands from a launcher with no console (like an app that starts an agent), and fails if any
# of them opens a visible top-level window.
param([Parameter(Mandatory)][string]$Cli, [Parameter(Mandatory)][string]$Project)
$ErrorActionPreference = 'Stop'
Add-Type @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
public static class Windows {
delegate bool EnumProc(IntPtr h, IntPtr p);
[DllImport("user32.dll")] static extern bool EnumWindows(EnumProc f, IntPtr p);
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassName(IntPtr h, StringBuilder s, int n);
public static List<string[]> Visible() {
var rows = new List<string[]>();
EnumWindows((h, p) => {
if (!IsWindowVisible(h)) return true;
uint pid; GetWindowThreadProcessId(h, out pid);
var t = new StringBuilder(256); var c = new StringBuilder(256);
GetWindowText(h, t, 256); GetClassName(h, c, 256);
rows.Add(new[] { h.ToString(), pid.ToString(), c.ToString(), t.ToString() });
return true;
}, IntPtr.Zero);
return rows;
}
}
'@
function Snapshot {
$seen = @{}
foreach ($w in [Windows]::Visible()) {
$proc = Get-Process -Id ([int]$w[1]) -ErrorAction SilentlyContinue
$seen[$w[0]] = [pscustomobject]@{ Process = $proc.ProcessName; Id = $w[1]; Class = $w[2]; Title = $w[3]; Cmd = '' }
}
$seen
}
# Starts `node launcher.mjs <cli> <cwd> <seconds> <args...>` and reports every window that appears meanwhile.
function Watch([string]$Name, [int]$Seconds, [string[]]$CliArgs) {
$baseline = Snapshot
$p = Start-Process -FilePath node -ArgumentList (@($launcher, $Cli, $Project, $Seconds) + $CliArgs) -WindowStyle Hidden -PassThru
$found = @{}
while (-not $p.HasExited) {
foreach ($entry in (Snapshot).GetEnumerator()) {
if (-not $baseline.ContainsKey($entry.Key) -and -not $found.ContainsKey($entry.Key)) {
$row = $entry.Value
if ($row.Process -match 'chrome') {
$q = Get-CimInstance Win32_Process -Filter "ProcessId=$($row.Id)" -ErrorAction SilentlyContinue
$parent = if ($q) { Get-CimInstance Win32_Process -Filter "ProcessId=$($q.ParentProcessId)" -ErrorAction SilentlyContinue }
$row.Cmd = "pid=$($row.Id) parent=[$($parent.Name) $($parent.ProcessId) main=$((($parent.CommandLine -replace '^.*chrome-headless-shell.exe"? ',''))) ] cmd=[$((("$($q.CommandLine)") -replace '^.*chrome-headless-shell.exe"? ','').Substring(0, [Math]::Min(260, ("$($q.CommandLine)" -replace '^.*chrome-headless-shell.exe"? ','').Length)))]"
}
$found[$entry.Key] = $row
}
}
Start-Sleep -Milliseconds 50
}
$p.WaitForExit()
Write-Host ("{0,-10} exit={1} new windows={2}" -f $Name, $p.ExitCode, $found.Count)
if ($p.ExitCode -ne 0) { Get-Content (Join-Path $env:RUNNER_TEMP "command.log") -Tail 12 | ForEach-Object { Write-Host " | $_" } }
$found.Values | ForEach-Object { Write-Host " $($_.Process) class=$($_.Class) title=[$($_.Title)] $($_.Cmd)" }
[pscustomobject]@{ Name = $Name; Exit = $p.ExitCode; Windows = $found.Count }
}
# The launcher has no console and no window; windowsHide only reaches the CLI itself, never what the CLI starts.
$launcher = Join-Path $env:RUNNER_TEMP 'launch-cli.mjs'
@'
import { spawn } from "node:child_process";
import { openSync } from "node:fs";
const [cli, cwd, seconds, ...args] = process.argv.slice(2);
const log = openSync(process.env.RUNNER_TEMP + "\\command.log", "w");
const child = spawn(process.execPath, [cli, ...args], { cwd, windowsHide: true, stdio: ["ignore", log, log] });
const stop = Number(seconds) > 0 ? setTimeout(() => child.kill(), Number(seconds) * 1000) : undefined;
child.on("exit", (code) => { clearTimeout(stop); process.exit(code ?? 0); });
'@ | Set-Content -Encoding utf8 $launcher
$runs = @(
(Watch 'render' 0 @('render', '--fps', '30', '--quality', 'draft', '--workers', '2', '--output', 'renders/probe.mp4')),
(Watch 'check' 0 @('check')),
(Watch 'check-warm' 0 @('check')),
(Watch 'snapshot' 0 @('snapshot', '--frames', '2')),
(Watch 'doctor' 0 @('doctor')),
(Watch 'preview' 20 @('preview', '--port', '3571'))
)
$bad = $runs | Where-Object { $_.Windows -gt 0 }
if ($bad) { throw "windows opened by: $(($bad | ForEach-Object Name) -join ', ')" }
$failed = $runs | Where-Object { $_.Name -in @('render', 'snapshot') -and $_.Exit -ne 0 }
if ($failed) { throw "commands failed: $(($failed | ForEach-Object Name) -join ', ')" }
@@ -0,0 +1,56 @@
name: Windows window probe
permissions:
contents: read
env:
HYPERFRAMES_NO_TELEMETRY: "1"
on:
pull_request:
paths:
- .github/workflows/windows-window-probe.yml
- .github/scripts/windows-window-probe.ps1
workflow_dispatch:
jobs:
probe:
name: Agent commands open no window on windows-latest
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: ./.github/actions/install-ffmpeg-windows
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- run: bun install --frozen-lockfile --linker=hoisted
shell: pwsh
- name: Build the CLI
shell: pwsh
run: |
bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
bun run --cwd packages/core build
bun run --filter @hyperframes/player build
bun run --filter @hyperframes/studio build
bun run --filter @hyperframes/inspector build
bun run --filter @hyperframes/cli build
- name: Scaffold the canary
shell: pwsh
env:
HYPERFRAMES_SKIP_SKILLS: "1"
run: |
$dir = "$env:RUNNER_TEMP\probe"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Set-Location $dir
node "$env:GITHUB_WORKSPACE\packages\cli\dist\cli.js" init canary --example blank --non-interactive --skip-skills
Copy-Item "$env:GITHUB_WORKSPACE\.github\workflows\fixtures\windows-canary.html" "canary\index.html" -Force
- name: Install Chrome before watching
shell: pwsh
run: |
Set-Location "$env:RUNNER_TEMP\probe\canary"
node "$env:GITHUB_WORKSPACE\packages\cli\dist\cli.js" browser ensure
- name: Run the commands while watching for windows
shell: pwsh
run: .github/scripts/windows-window-probe.ps1 -Cli "$env:GITHUB_WORKSPACE\packages\cli\dist\cli.js" -Project "$env:RUNNER_TEMP\probe\canary"
+20 -52
View File
@@ -27,7 +27,7 @@
},
"packages/aws-lambda": {
"name": "@hyperframes/aws-lambda",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@aws-sdk/client-ecr": "^3.1146.0",
"@aws-sdk/client-lambda": "^3.1146.0",
@@ -63,7 +63,7 @@
},
"packages/cli": {
"name": "@hyperframes/cli",
"version": "0.8.194",
"version": "0.8.210",
"bin": {
"hyperframes": "./bin/hyperframes.mjs",
"hyperframes-localize-fonts": "./bin/hyperframes-localize-fonts.mjs",
@@ -118,7 +118,7 @@
},
"packages/core": {
"name": "@hyperframes/core",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@chenglou/pretext": "^0.0.5",
"@hyperframes/lint": "workspace:*",
@@ -163,15 +163,15 @@
},
"packages/engine": {
"name": "@hyperframes/engine",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@hono/node-server": "^2.0.5",
"@hyperframes/core": "workspace:^",
"@hyperframes/parsers": "workspace:^",
"hono": "^4.6.0",
"linkedom": "^0.18.12",
"puppeteer": "^25.8.0",
"puppeteer-core": "^25.8.0",
"puppeteer": "~25.11.0",
"puppeteer-core": "^25.11.0",
},
"devDependencies": {
"@types/node": "^25.0.10",
@@ -182,7 +182,7 @@
},
"packages/gcp-cloud-run": {
"name": "@hyperframes/gcp-cloud-run",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@google-cloud/storage": "^7.14.0",
"@google-cloud/workflows": "^4.2.0",
@@ -203,7 +203,7 @@
},
"packages/inspector": {
"name": "@hyperframes/inspector",
"version": "0.8.194",
"version": "0.8.210",
"bin": {
"hyperframes-inspector": "./dist/server.js",
},
@@ -226,7 +226,7 @@
},
"packages/lint": {
"name": "@hyperframes/lint",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@hyperframes/parsers": "workspace:*",
"acorn": "^8.17.0",
@@ -246,7 +246,7 @@
},
"packages/parsers": {
"name": "@hyperframes/parsers",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@babel/parser": "^7.27.0",
"acorn": "^8.17.0",
@@ -267,7 +267,7 @@
},
"packages/player": {
"name": "@hyperframes/player",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@hyperframes/core": "workspace:*",
},
@@ -282,7 +282,7 @@
},
"packages/producer": {
"name": "@hyperframes/producer",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@hono/node-server": "^2.0.5",
"@hyperframes/core": "workspace:*",
@@ -293,8 +293,8 @@
"hono": "^4.6.0",
"linkedom": "^0.18.12",
"postcss": "^8.4.0",
"puppeteer": "^25.2.1",
"puppeteer-core": "^25.2.1",
"puppeteer": "~25.11.0",
"puppeteer-core": "^25.11.0",
},
"devDependencies": {
"@hyperframes/aws-lambda": "workspace:*",
@@ -308,7 +308,7 @@
},
"packages/sdk": {
"name": "@hyperframes/sdk",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@hyperframes/core": "workspace:*",
"@hyperframes/parsers": "workspace:*",
@@ -338,7 +338,7 @@
},
"packages/shader-transitions": {
"name": "@hyperframes/shader-transitions",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"html2canvas": "^1.4.1",
},
@@ -351,7 +351,7 @@
},
"packages/studio": {
"name": "@hyperframes/studio",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@base-ui/react": "1.7.0",
"@codemirror/autocomplete": "^6.20.1",
@@ -405,7 +405,7 @@
},
"packages/studio-server": {
"name": "@hyperframes/studio-server",
"version": "0.8.194",
"version": "0.8.210",
"dependencies": {
"@hyperframes/core": "workspace:*",
"@hyperframes/parsers": "workspace:*",
@@ -1553,7 +1553,7 @@
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
@@ -1955,7 +1955,7 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"puppeteer": ["puppeteer@25.8.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "lilconfig": "^3.1.3", "puppeteer-core": "25.8.0", "typed-query-selector": "^2.12.2" }, "bin": { "puppeteer": "lib/puppeteer/node/cli.js" } }, "sha512-3gcUJ+Jfodb5zNa/lWLZukBUwYiRIwAc8WRICoqfi+ZYmNWqpsPFyanTU3Gw/lhgII9aotVbAmhT6/NKHWgyUA=="],
"puppeteer": ["puppeteer@25.11.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.2", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1680125", "lilconfig": "^3.1.3", "puppeteer-core": "25.11.0", "typed-query-selector": "^2.12.2" }, "bin": { "puppeteer": "lib/puppeteer/node/cli.js" } }, "sha512-n+psxZFH/qlJ3gwTZMxTLUZoE98FAOEjT/BSxHXvPmNCGAfN/kq0UnIZ8m9RB4rAVvJr7zRHUnKKg98GLMUyyQ=="],
"puppeteer-core": ["puppeteer-core@25.11.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.2", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1680125", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.3", "ws": "^8.21.3" } }, "sha512-Ujym7JbxoAqanaxGcHgmTomfhC01HfDCSuGZ8Hs9boHrDjzbNVMnFRJo2KtbDT9LCRgeJoFnkn9JQZ41eEUrLw=="],
@@ -2343,12 +2343,6 @@
"path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"puppeteer/@puppeteer/browsers": ["@puppeteer/browsers@3.2.1", "", { "dependencies": { "modern-tar": "^0.8.0", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A=="],
"puppeteer/devtools-protocol": ["devtools-protocol@0.0.1666840", "", {}, "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg=="],
"puppeteer/puppeteer-core": ["puppeteer-core@25.8.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.1" } }, "sha512-LDOrawV8vfCVk+yLj2ozvajNP4Sv3OV9y3Tpiyy2g2Z+aQlbcozP6KJfI4iSBq7YQER+86ihEtPa5ioiZyWxMQ=="],
"puppeteer-core/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -2447,12 +2441,6 @@
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"puppeteer/@puppeteer/browsers/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
"puppeteer/puppeteer-core/webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="],
"puppeteer/puppeteer-core/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
"tsup/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
"tsup/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
@@ -2633,12 +2621,6 @@
"google-gax/retry-request/teeny-request/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"puppeteer/@puppeteer/browsers/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
"puppeteer/@puppeteer/browsers/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"puppeteer/@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
"@puppeteer/browsers/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -2650,19 +2632,5 @@
"google-gax/retry-request/teeny-request/http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"google-gax/retry-request/teeny-request/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"puppeteer/@puppeteer/browsers/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"puppeteer/@puppeteer/browsers/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"puppeteer/@puppeteer/browsers/yargs/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"puppeteer/@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"puppeteer/@puppeteer/browsers/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"puppeteer/@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"puppeteer/@puppeteer/browsers/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
}
}
@@ -1,3 +1,4 @@
import "./utils/hideChildConsoles.js";
import { checkForUpdate } from "./utils/updateCheck.js";
import { refreshSkillsCache } from "./utils/skillsUpdateCheck.js";
+1
View File
@@ -31,6 +31,7 @@ for (const stream of [process.stdout, process.stderr]) {
// probe lands in a directory that does not contain the bundled worker.
// We emit the worker entry next to cli.js (see tsup.config.ts) and tell
// the pool where to find it via the published env-var override.
import "./utils/hideChildConsoles.js";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
+1
View File
@@ -1,4 +1,5 @@
// fallow-ignore-file unused-file
import "./utils/hideChildConsoles.js";
import { injectDeterministicFontFaces } from "@hyperframes/producer";
import { runFontLocalize, stampFontVersions } from "./fontLocalize.js";
import { PRODUCER_VERSION, VERSION } from "./version.js";
+1
View File
@@ -1,3 +1,4 @@
import "./utils/hideChildConsoles.js";
import { writeFileSync } from "node:fs";
import { isAbsolute, join } from "node:path";
+1
View File
@@ -1,3 +1,4 @@
import "./utils/hideChildConsoles.js";
import { constants, setPriority } from "node:os";
import { runBrowserInstall } from "./browser/installRun.js";
import { ensureBrowser, releaseOwnedBrowserInstallLock } from "./browser/manager.js";
+1 -1
View File
@@ -72,7 +72,7 @@ const openWithBundle = (bundleId: string, dir: string): boolean =>
/** Single-instance app: a running copy is forwarded the folder. Only a refused spawn counts as failed. */
function launchApp(executable: string, dir: string): boolean {
try {
const child = spawn(executable, [dir], { detached: true, stdio: "ignore" });
const child = spawn(executable, [dir], { detached: true, stdio: "ignore", windowsHide: false });
child.on("error", () => {});
child.unref();
return true;
@@ -0,0 +1,88 @@
import { promisify } from "node:util";
import { describe, expect, it, vi } from "vitest";
import { hideChildConsoles } from "./hideChildConsoles.js";
function fakeModule() {
const calls: unknown[][] = [];
const record = (name: string) => vi.fn((...args: unknown[]) => calls.push([name, ...args]));
const module: Record<string, unknown> = {};
for (const name of [
"spawn",
"spawnSync",
"exec",
"execSync",
"execFile",
"execFileSync",
"fork",
]) {
module[name] = record(name);
}
const execFile = module.execFile as Record<symbol, unknown>;
execFile[promisify.custom] = vi.fn((...args: unknown[]) =>
calls.push(["execFile.promisified", ...args]),
);
return { module, calls };
}
type Entry = "spawn" | "spawnSync" | "exec" | "execSync" | "execFile" | "execFileSync" | "fork";
type Calls = Record<Entry, (...args: unknown[]) => unknown>;
const run = (platform: NodeJS.Platform, use: (m: Calls) => void) => {
const { module, calls } = fakeModule();
hideChildConsoles(module, platform);
use(module as Calls);
return calls;
};
describe("hideChildConsoles", () => {
it("hides a console on every way of passing the options", () => {
const callback = () => {};
const calls = run("win32", (m) => {
m.spawn("a");
m.spawn("a", ["x"]);
m.spawnSync("a", ["x"], { cwd: "." });
m.execFile("a", ["x"], callback);
m.exec("a", callback);
m.execSync("a", { timeout: 1 });
m.execFileSync("a", { timeout: 1 });
m.fork("a", ["x"], {});
});
expect(
calls.map(([name, , ...rest]) => [
name,
rest.find((a) => a && typeof a === "object" && !Array.isArray(a)),
]),
).toEqual([
["spawn", { windowsHide: true }],
["spawn", { windowsHide: true }],
["spawnSync", { windowsHide: true, cwd: "." }],
["execFile", { windowsHide: true }],
["exec", { windowsHide: true }],
["execSync", { windowsHide: true, timeout: 1 }],
["execFileSync", { windowsHide: true, timeout: 1 }],
["fork", { windowsHide: true }],
]);
expect(calls[3]?.at(-1)).toBe(callback);
});
it("leaves a caller that wants the window alone", () => {
const calls = run("win32", (m) =>
m.spawn("browser", [], { windowsHide: false, detached: true }),
);
expect(calls[0]?.[3]).toEqual({ windowsHide: false, detached: true });
});
it("hides the promisified execFile too", async () => {
const { module, calls } = fakeModule();
hideChildConsoles(module, "win32");
await promisify(module.execFile as never)("a", ["x"]);
expect(calls[0]).toEqual(["execFile.promisified", "a", ["x"], { windowsHide: true }]);
});
it("does nothing off Windows", () => {
const { module } = fakeModule();
const before = { ...module };
hideChildConsoles(module, "linux");
expect(module).toEqual(before);
});
});
@@ -0,0 +1,42 @@
import childProcess from "node:child_process";
import { syncBuiltinESMExports } from "node:module";
import { promisify } from "node:util";
type Spawner = (command: string, ...rest: unknown[]) => unknown;
type ChildProcessModule = Record<string, unknown>;
const ENTRY_POINTS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync", "fork"];
/** The call's arguments with `windowsHide: true` set unless the caller chose either way. */
function hidden(command: string, rest: unknown[]): unknown[] {
const args = [...rest];
const at = args.findIndex((arg) => arg && typeof arg === "object" && !Array.isArray(arg));
if (at >= 0) {
args[at] = { windowsHide: true, ...(args[at] as object) };
} else {
const callback = args.findIndex((arg) => typeof arg === "function");
args.splice(callback < 0 ? args.length : callback, 0, { windowsHide: true });
}
return [command, ...args];
}
/** Windows opens a console window for every child that does not say `windowsHide`; `false` opts out. */
export function hideChildConsoles(
target: ChildProcessModule = childProcess as unknown as ChildProcessModule,
platform: NodeJS.Platform = process.platform,
): void {
if (platform !== "win32") return;
for (const name of ENTRY_POINTS) {
const original = target[name] as Spawner & { [promisify.custom]?: Spawner };
const wrapped: Spawner & { [promisify.custom]?: Spawner } = (command, ...rest) =>
original(...(hidden(command, rest) as [string, ...unknown[]]));
const custom = original[promisify.custom];
if (custom)
wrapped[promisify.custom] = (command, ...rest) =>
custom(...(hidden(command, rest) as [string]));
target[name] = wrapped;
}
if (target === (childProcess as unknown as ChildProcessModule)) syncBuiltinESMExports();
}
hideChildConsoles();
+1 -1
View File
@@ -29,7 +29,7 @@ describe("openBrowser", () => {
"--remote-debugging-port=9222",
"http://localhost:3002",
],
{ detached: true, stdio: "ignore" },
{ detached: true, stdio: "ignore", windowsHide: false },
);
expect(unref).toHaveBeenCalledOnce();
});
+1
View File
@@ -75,6 +75,7 @@ export function openBrowser(url: string, options: OpenBrowserOptions = {}): void
const child = spawn(options.browserPath, args, {
detached: true,
stdio: "ignore",
windowsHide: false,
});
child.on("error", () => {});
child.unref();
+1
View File
@@ -1,3 +1,4 @@
import "./utils/hideChildConsoles.js";
import { randomBytes } from "node:crypto";
import { unlinkSync } from "node:fs";
import { connect, createServer, type Socket } from "node:net";
+1
View File
@@ -1,3 +1,4 @@
import "./utils/hideChildConsoles.js";
import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { dirname, join } from "node:path";
+1
View File
@@ -1,3 +1,4 @@
import "../utils/hideChildConsoles.js";
import { createRequire } from "node:module";
import {
droppedSpeechGaps,
+2 -2
View File
@@ -99,8 +99,8 @@
"@hyperframes/parsers": "workspace:^",
"hono": "^4.6.0",
"linkedom": "^0.18.12",
"puppeteer": "^25.8.0",
"puppeteer-core": "^25.8.0"
"puppeteer": "~25.11.0",
"puppeteer-core": "^25.11.0"
},
"devDependencies": {
"@types/node": "^25.0.10",
+9 -4
View File
@@ -65,10 +65,15 @@ function supportsSettsDuration(binary = getFfmpegBinary()): Promise<boolean> {
let supported = settsDurationSupport.get(binary);
if (!supported) {
supported = new Promise((resolve) => {
execFile(binary, ["-hide_banner", "-h", "bsf=setts"], (error, stdout) => {
// A binary that cannot even print help fails the encode with its own error, not this one.
resolve(error !== null || stdout.includes("duration"));
});
execFile(
binary,
["-hide_banner", "-h", "bsf=setts"],
{ windowsHide: true },
(error, stdout) => {
// A binary that cannot even print help fails the encode with its own error, not this one.
resolve(error !== null || stdout.includes("duration"));
},
);
});
settsDurationSupport.set(binary, supported);
}
@@ -69,8 +69,12 @@ export function shellStarts(binary: string, timeoutMs = 10_000): boolean {
if (answer === undefined) {
const probe = ["--no-sandbox", "--dump-dom", "about:blank"];
answer =
spawnSync(binary, probe, { stdio: "ignore", timeout: timeoutMs, killSignal: "SIGKILL" })
?.status === 0;
spawnSync(binary, probe, {
stdio: "ignore",
timeout: timeoutMs,
killSignal: "SIGKILL",
windowsHide: true,
})?.status === 0;
if (!answer) debug("skipping %s: it did not start", binary);
starts.set(binary, answer);
}
@@ -0,0 +1,24 @@
import { createRequire } from "node:module";
import { describe, expect, it } from "vitest";
// @puppeteer/browsers before 3.2.2 started Chrome detached on every platform. On Windows a detached Chrome has no
// console, so Windows Terminal opens a window for each Chrome process of a render.
const FIRST_SAFE = [3, 2, 2];
function launcherVersion(): number[] {
const fromEngine = createRequire(new URL("../../package.json", import.meta.url));
const fromPuppeteer = createRequire(fromEngine.resolve("puppeteer/package.json"));
const fromCore = createRequire(fromPuppeteer.resolve("puppeteer-core/package.json"));
return (fromCore("@puppeteer/browsers/package.json") as { version: string }).version
.split(".")
.map(Number);
}
describe("the Puppeteer the engine launches Chrome with", () => {
it("does not start Chrome detached on Windows", () => {
const version = launcherVersion();
const behind = FIRST_SAFE.findIndex((part, i) => version[i] !== part);
const atLeast = behind === -1 || (version[behind] ?? 0) > (FIRST_SAFE[behind] ?? 0);
expect(atLeast, `@puppeteer/browsers ${version.join(".")}`).toBe(true);
});
});
+4 -1
View File
@@ -474,7 +474,10 @@ function ffprobeMajor(): Promise<number | undefined> {
runtimeFfprobeMajor ??= new Promise((resolve) => {
let out = "";
const binary = getFfprobeBinary();
const proc = spawn(binary, ["-version"], { stdio: ["ignore", "pipe", "ignore"] });
const proc = spawn(binary, ["-version"], {
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true,
});
proc.stdout.on("data", (chunk) => (out += String(chunk)));
proc.on("error", () => resolve(undefined));
proc.on("close", () => resolve(Number(/version n?(\d+)/.exec(out)?.[1]) || undefined));
+2 -2
View File
@@ -84,8 +84,8 @@
"hono": "^4.6.0",
"linkedom": "^0.18.12",
"postcss": "^8.4.0",
"puppeteer": "^25.2.1",
"puppeteer-core": "^25.2.1"
"puppeteer": "~25.11.0",
"puppeteer-core": "^25.11.0"
},
"devDependencies": {
"@hyperframes/aws-lambda": "workspace:*",