From 4d809a1954936f6eb636e7434fc3b29db41a0657 Mon Sep 17 00:00:00 2001 From: Shiroha7z Date: Tue, 27 Oct 2026 13:00:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(cli):=20Windows=20=E5=AE=88=E6=8A=A4?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E5=A4=96=E5=A3=B3=E6=94=B9=E7=94=A8=20cmd=20?= =?UTF-8?q?=E9=87=8D=E5=AE=9A=E5=90=91=E8=BE=93=E5=87=BA=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E9=A6=96=E6=AC=A1=E8=BE=93=E5=87=BA=E5=8D=B3=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E5=81=9C=E6=9C=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows PowerShell 5.1 中 OutputDataReceived 脚本块回调运行在没有 Runspace 的线程池线程上,守护进程第一次输出就会抛 PSInvalidOperationException 使外壳崩溃,守护进程及其服务随之退出。改由 cmd.exe 把输出追加到 daemon.log。 --- .changeset/daemon-host-cmd-redirect.md | 5 +++++ packages/cli/src/daemon/host.ts | 29 ++++++++------------------ 2 files changed, 14 insertions(+), 20 deletions(-) create mode 100644 .changeset/daemon-host-cmd-redirect.md diff --git a/.changeset/daemon-host-cmd-redirect.md b/.changeset/daemon-host-cmd-redirect.md new file mode 100644 index 00000000..7649928a --- /dev/null +++ b/.changeset/daemon-host-cmd-redirect.md @@ -0,0 +1,5 @@ +--- +"@sfmc-bds/cli": patch +--- + +修复 Windows 守护进程外壳在守护进程第一次输出时崩溃、连带整个实例停服的问题:改由 cmd.exe 把输出追加到 daemon.log,不再使用 PowerShell 脚本块回调。 diff --git a/packages/cli/src/daemon/host.ts b/packages/cli/src/daemon/host.ts index 97d72104..04a98af4 100644 --- a/packages/cli/src/daemon/host.ts +++ b/packages/cli/src/daemon/host.ts @@ -91,39 +91,28 @@ function launchWindowsOutsideSession(spec: DaemonHostSpec): string | null { const env = carriedEnv(spec, ["PATH", "PATHEXT", "SystemRoot", "USERNAME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP"]); const assignments = Object.entries(env).map(([key, value]) => `$env:${key} = ${psLiteral(value)}`); const log = psLiteral(spec.logPath); - // 事件回调跑在别的线程,用脚本作用域保住日志写入器。入口参数用字符码拼引号,避免转义把路径截断。 + // 不能用 OutputDataReceived 脚本块回调接管输出:Windows PowerShell 5.1 在线程池线程上没有 Runspace, + // 守护进程输出第一行时外壳就会抛 PSInvalidOperationException 崩溃,守护进程随之退出。 + // 改由 cmd.exe 把标准输出和标准错误追加进日志。cmd 的 >> 运行期间只允许别人读、不允许追加, + // 句柄在守护进程退出后立即释放;平台切换里的 waitDaemonLogAppendable 发生在旧守护进程退出之后,不受影响。 + // 路径经环境变量交给 cmd 展开,避免拼进命令行时被引号或特殊字符截断; + // 整行先展开再执行,所以前面的 set 能把 SFMC_DAEMON_LOG 从守护进程环境里去掉,同时不影响重定向目标。 const script = [ "$ErrorActionPreference = 'Stop'", `$env:SFMC_DAEMON_LOG = ${log}`, ...assignments, "New-Item -ItemType Directory -Force -Path (Split-Path -Parent $env:SFMC_DAEMON_LOG) | Out-Null", "Set-Location -LiteralPath $env:SFMC_ROOT", - // StreamWriter(path, append) 默认 FileShare.Read,会挡住下一次平台校验里的追加打开。 - "$mode = [System.IO.FileMode]::Append", - "$access = [System.IO.FileAccess]::Write", - "$share = [System.IO.FileShare]::ReadWrite", - "$stream = New-Object System.IO.FileStream($env:SFMC_DAEMON_LOG, $mode, $access, $share)", - "$inner = New-Object System.IO.StreamWriter($stream)", - "$inner.AutoFlush = $true", - "$script:writer = [System.IO.TextWriter]::Synchronized($inner)", - "Remove-Item Env:SFMC_DAEMON_LOG", "$psi = New-Object System.Diagnostics.ProcessStartInfo", - "$psi.FileName = $env:SFMC_NODE_BINARY", - "$psi.Arguments = ([char]34) + $env:SFMC_DAEMON_ENTRY + ([char]34) + ' --daemon'", + "$psi.FileName = Join-Path $env:SystemRoot 'System32\\cmd.exe'", + `$psi.Arguments = '/d /s /c "set "SFMC_DAEMON_LOG=" & "%SFMC_NODE_BINARY%" "%SFMC_DAEMON_ENTRY%" --daemon >> "%SFMC_DAEMON_LOG%" 2>&1"'`, "$psi.WorkingDirectory = $env:SFMC_ROOT", "$psi.UseShellExecute = $false", "$psi.CreateNoWindow = $true", - "$psi.RedirectStandardOutput = $true", - "$psi.RedirectStandardError = $true", "$proc = New-Object System.Diagnostics.Process", "$proc.StartInfo = $psi", - "$onLine = { param($sender, $lineEvent) if ($null -ne $lineEvent.Data) { $script:writer.WriteLine($lineEvent.Data) } }", - "$proc.add_OutputDataReceived($onLine)", - "$proc.add_ErrorDataReceived({ param($sender, $lineEvent) if ($null -ne $lineEvent.Data) { $script:writer.WriteLine($lineEvent.Data) } })", "[void]$proc.Start()", - "$proc.BeginOutputReadLine()", - "$proc.BeginErrorReadLine()", - "try { $proc.WaitForExit() } finally { if ($script:writer) { $script:writer.Dispose() } }", + "$proc.WaitForExit()", ]; const commandLine = `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script.join("\n"), "utf16le").toString("base64")}`; const run = (hide: boolean) => {