Compare commits

...
Author SHA1 Message Date
Filip Hejmowski 49a21bcf30 fix(cli): give the Windows service a hidden console 2026-10-02 01:48:57 +02:00
5 changed files with 48 additions and 1 deletions

No files matched your search

+7
View File
@@ -16,6 +16,13 @@
"./run": "./src/run/index.ts",
"./server-process": "./src/server-process.ts"
},
"imports": {
"#hidden-console": {
"bun": "./src/services/hidden-console.bun.ts",
"node": "./src/services/hidden-console.node.ts",
"default": "./src/services/hidden-console.bun.ts"
}
},
"scripts": {
"build": "bun run script/build.ts",
"build:node": "bun run script/build-node.ts",
+6 -1
View File
@@ -12,6 +12,7 @@ import { PersistentPty } from "@opencode/schema/persistent-pty"
import { HttpServer } from "effect/unstable/http"
import { Env } from "./env"
import { ServiceConfig } from "./services/service-config"
import { HiddenConsole } from "./services/hidden-console"
import { RetainedImage } from "./services/retained-image"
import { ServiceRegistration } from "./services/service-registration"
import { WebUi } from "./services/web-ui"
@@ -52,7 +53,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
Effect.mapError(() => new Error("Invalid PTY restart handoff")),
)
const global = yield* Global.Service
if (options.mode === "service") yield* Effect.sync(() => process.chdir(global.home))
if (options.mode === "service") {
yield* Effect.sync(() => process.chdir(global.home))
const status = yield* Effect.sync(() => HiddenConsole.attach())
if (status !== undefined) yield* Effect.logWarning("could not attach a hidden console", { status })
}
return yield* Effect.scoped(
Effect.gen(function* () {
const foreground = options.mode === "default"
@@ -0,0 +1,30 @@
import { dlopen, ptr } from "bun:ffi"
import os from "node:os"
// STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE
const standardHandles = [-10, -11, -12]
/**
* A detached service starts without a console, so Windows opens a visible window for every
* console child spawned without CREATE_NO_WINDOW (plugin code, hooks, npm's git). Allocating a
* windowless console gives those children one to inherit instead.
*
* Returns the failing HRESULT, or undefined when the console was attached or is unsupported.
*/
export function attach() {
if (process.platform !== "win32") return
// AllocConsoleWithOptions requires Windows 11 24H2 / Server 2025 (build 26100).
if (Number(os.release().split(".")[2]) < 26100) return
const kernel32 = dlopen("kernel32.dll", {
AllocConsoleWithOptions: { args: ["ptr", "ptr"], returns: "i32" },
GetStdHandle: { args: ["i32"], returns: "i64" },
SetStdHandle: { args: ["i32", "i64"], returns: "i32" },
})
// Allocation may point the standard handles at the new console; keep the pipes the spawner reads.
const handles = standardHandles.map((id) => [id, kernel32.symbols.GetStdHandle(id)] as const)
// ALLOC_CONSOLE_OPTIONS { mode: ALLOC_CONSOLE_MODE_NO_WINDOW, useShowWindow: FALSE, showWindow: 0 }
const status = kernel32.symbols.AllocConsoleWithOptions(ptr(new Int32Array([2, 0, 0])), null)
handles.forEach(([id, handle]) => kernel32.symbols.SetStdHandle(id, handle))
kernel32.close()
return status === 0 ? undefined : status
}
@@ -0,0 +1,4 @@
// The Node build has no console binding yet, so its service keeps the console-less behavior.
export function attach(): number | undefined {
return undefined
}
@@ -0,0 +1 @@
export * as HiddenConsole from "#hidden-console"