mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-07 16:17:25 +03:00
fix(core): keep multi-line commands intact for Store PowerShell 7 (#62522)
This commit is contained in:
committed by
GitHub
Unverified
parent
2b3a0d4410
commit
145e948159
@@ -8,10 +8,12 @@ import { ChildProcess, ChildProcessSpawner } from "effect/process"
|
||||
import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import { Teardown } from "@opencode/util/teardown"
|
||||
import { appExecutionAlias } from "../fixture/app-execution-alias"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const live = LayerNode.compile(CrossSpawnSpawner.node)
|
||||
const fx = testEffect(live)
|
||||
const pwsh = appExecutionAlias("pwsh.exe")
|
||||
|
||||
function js(code: string, opts?: ChildProcess.CommandOptions) {
|
||||
return ChildProcess.make("node", ["-e", code], opts)
|
||||
@@ -579,5 +581,19 @@ describe("cross-spawn spawner", () => {
|
||||
expect(code).toBe(ChildProcessSpawner.ExitCode(0))
|
||||
}),
|
||||
)
|
||||
|
||||
// Skipped where PowerShell 7 is not installed from the Microsoft Store for the user; see the fixture.
|
||||
const storePwsh = pwsh ? fx.live : fx.live.skip
|
||||
storePwsh(
|
||||
"keeps every line of a command run through an app execution alias",
|
||||
Effect.gen(function* () {
|
||||
const out = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) =>
|
||||
svc.string(
|
||||
ChildProcess.make(pwsh!.file, ["-NoProfile", "-NonInteractive", "-Command", "echo one\necho two"]),
|
||||
),
|
||||
)
|
||||
expect(out.trim().split(/\r?\n/)).toEqual(["one", "two"])
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,14 +2,16 @@ import { existsSync, lstatSync, readdirSync } from "fs"
|
||||
import path from "path"
|
||||
|
||||
// Tests cannot create an app execution alias (an IO_REPARSE_TAG_APPEXECLINK reparse point), so they borrow one that
|
||||
// a Microsoft Store app installed for the current user. Returns undefined when the machine has none.
|
||||
export function appExecutionAlias() {
|
||||
// a Microsoft Store app installed for the current user. Returns undefined when the machine has none, or none with
|
||||
// the requested file name.
|
||||
export function appExecutionAlias(file?: string) {
|
||||
if (process.platform !== "win32" || !process.env.LOCALAPPDATA) return
|
||||
const dir = path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps")
|
||||
if (!existsSync(dir)) return
|
||||
const name = readdirSync(dir, { withFileTypes: true })
|
||||
.filter((entry) => entry.name.toLowerCase().endsWith(".exe"))
|
||||
.map((entry) => entry.name)
|
||||
.filter((entry) => file === undefined || entry.toLowerCase() === file)
|
||||
.find((entry) => lstatSync(path.join(dir, entry), { throwIfNoEntry: false })?.isSymbolicLink())
|
||||
return name ? { dir, name, file: path.join(dir, name) } : undefined
|
||||
}
|
||||
|
||||
@@ -370,11 +370,23 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
|
||||
return { stdout, stderr, all: Stream.merge(stdout, stderr) }
|
||||
}
|
||||
|
||||
const launchProcess = (command: ChildProcess.StandardCommand, opts: NodeChildProcess.SpawnOptions) =>
|
||||
// A Microsoft Store app execution alias, such as Store PowerShell 7 in %LOCALAPPDATA%\Microsoft\WindowsApps, is a
|
||||
// reparse point that stat rejects with EACCES. cross-spawn then cannot find it and runs it through
|
||||
// `cmd.exe /d /s /c`, which drops everything after the first newline of the command line. Launch the target of
|
||||
// the link instead, read on every spawn because a Store update moves it.
|
||||
const executable = (file: string) => {
|
||||
if (globalThis.process.platform !== "win32" || !path.isAbsolute(file)) return Effect.succeed(file)
|
||||
return fs.readLink(file).pipe(
|
||||
Effect.map((target) => path.resolve(path.dirname(file), target)),
|
||||
Effect.orElseSucceed(() => file),
|
||||
)
|
||||
}
|
||||
|
||||
const launchProcess = (command: ChildProcess.StandardCommand, file: string, opts: NodeChildProcess.SpawnOptions) =>
|
||||
Effect.callback<Spawned, PlatformError.PlatformError>((resume) => {
|
||||
const closed = Deferred.makeUnsafe<readonly [code: number | null, signal: NodeJS.Signals | null]>()
|
||||
const exited = Deferred.makeUnsafe<readonly [code: number | null, signal: NodeJS.Signals | null]>()
|
||||
const proc = launch(command.command, command.args.map(unwrap), opts)
|
||||
const proc = launch(file, command.args.map(unwrap), opts)
|
||||
let settle = () => {}
|
||||
let end = false
|
||||
let exit: readonly [code: number | null, signal: NodeJS.Signals | null] | undefined
|
||||
@@ -420,7 +432,7 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
|
||||
opts: NodeChildProcess.SpawnOptions,
|
||||
) {
|
||||
yield* Effect.logInfo("spawning process", { command: command.command, args: command.args, cwd: opts.cwd })
|
||||
const [proc, closed, exited, settle] = yield* launchProcess(command, opts)
|
||||
const [proc, closed, exited, settle] = yield* launchProcess(command, yield* executable(command.command), opts)
|
||||
const stopOutput = yield* Deferred.make<void>()
|
||||
// Register before process release so the deadline remains active during termination.
|
||||
yield* Effect.forkScoped(
|
||||
|
||||
Reference in New Issue
Block a user