mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment The packaged Windows `orca.exe` launcher read `ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the case-sensitive process block into a case-insensitive dictionary via `.Add`. An inherited block carrying both `PATH` and `Path` threw `ArgumentException: Item has already been added. Key in dictionary: 'PATH'`, so every `orca` invocation exited 1 before Electron started (native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67). The launcher now mutates its own environment with `Environment.SetEnvironmentVariable` and never touches either `ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment block and the child inherits the live one verbatim. Orca was also minting the duplicate itself. `applyTerminalAttributionEnv` read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows PTY that inherited `Path` got a second spelling; which one the child resolved was non-deterministic. `createLaunchEnv` did the same and, because its read always missed on Windows, shipped Agent Teams terminals a `PATH` containing only the tmux shim dir. `resolvePathEnvKey` (extracted from the existing precedent in windows-environment-path.ts) now drives every PATH read and write in the PTY env pipeline, and attribution collapses Windows onto the single OS-resolved spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior is unchanged and a case-sensitive POSIX `Path` variable is never touched. Closes #12046 * test(windows): track the launcher's own-environment marker The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the old dictionary writes and failed. Co-authored-by: Orca <help@stably.ai> * fix(windows): follow the host block's PATH spelling on sparse daemon env patches Resolving a path-less Windows env to `Path` handed the daemon's own `{...process.env, ...opts.env}` merge both spellings when the host block spelt `PATH`. Fall back to the host block's own key, and collapse again inside the daemon since that merge happens after attribution. Co-authored-by: Orca <help@stably.ai> * fix(windows): resolve the live PATH spelling by block order, not casing Win32 resolves a duplicated variable by taking the first case-insensitive match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first preference targeted the shadowed spelling on the reporter's own `["PATH","Path"]` block. Drop the attribution-side collapse with it: it deleted the other spelling's value, and deleting the live key promotes the shadowed one, so an env that stripped down to empty lost both. * chore: drop unrelated merge formatting --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
124 lines
3.8 KiB
C#
124 lines
3.8 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
internal static class DuplicatePathProcessLauncher
|
|
{
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
private struct StartupInfo
|
|
{
|
|
public int cb;
|
|
public string lpReserved;
|
|
public string lpDesktop;
|
|
public string lpTitle;
|
|
public int dwX;
|
|
public int dwY;
|
|
public int dwXSize;
|
|
public int dwYSize;
|
|
public int dwXCountChars;
|
|
public int dwYCountChars;
|
|
public int dwFillAttribute;
|
|
public int dwFlags;
|
|
public short wShowWindow;
|
|
public short cbReserved2;
|
|
public IntPtr lpReserved2;
|
|
public IntPtr hStdInput;
|
|
public IntPtr hStdOutput;
|
|
public IntPtr hStdError;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct ProcessInformation
|
|
{
|
|
public IntPtr hProcess;
|
|
public IntPtr hThread;
|
|
public int dwProcessId;
|
|
public int dwThreadId;
|
|
}
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
private static extern bool CreateProcess(
|
|
string applicationName,
|
|
StringBuilder commandLine,
|
|
IntPtr processAttributes,
|
|
IntPtr threadAttributes,
|
|
bool inheritHandles,
|
|
uint creationFlags,
|
|
IntPtr environment,
|
|
string currentDirectory,
|
|
ref StartupInfo startupInfo,
|
|
out ProcessInformation processInformation
|
|
);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode);
|
|
|
|
[DllImport("kernel32.dll")]
|
|
private static extern bool CloseHandle(IntPtr handle);
|
|
|
|
private static int Main(string[] args)
|
|
{
|
|
List<string> entries = new List<string>();
|
|
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
|
|
{
|
|
if (!String.Equals((string)entry.Key, "PATH", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
entries.Add((string)entry.Key + "=" + (string)entry.Value);
|
|
}
|
|
}
|
|
entries.Add("PATH=C:\\live");
|
|
entries.Add("Path=C:\\shadowed");
|
|
entries.Add("ORCA_TEST_OUTPUT=" + args[1]);
|
|
|
|
IntPtr environment = Marshal.StringToHGlobalUni(String.Join("\0", entries.ToArray()) + "\0\0");
|
|
StartupInfo startupInfo = new StartupInfo();
|
|
startupInfo.cb = Marshal.SizeOf(startupInfo);
|
|
ProcessInformation processInformation;
|
|
try
|
|
{
|
|
bool started = CreateProcess(
|
|
args[0],
|
|
new StringBuilder("\"" + args[0] + "\""),
|
|
IntPtr.Zero,
|
|
IntPtr.Zero,
|
|
false,
|
|
0x00000400,
|
|
environment,
|
|
null,
|
|
ref startupInfo,
|
|
out processInformation
|
|
);
|
|
if (!started)
|
|
{
|
|
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FreeHGlobal(environment);
|
|
}
|
|
|
|
try
|
|
{
|
|
WaitForSingleObject(processInformation.hProcess, 0xffffffff);
|
|
uint exitCode;
|
|
if (!GetExitCodeProcess(processInformation.hProcess, out exitCode))
|
|
{
|
|
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
}
|
|
return (int)exitCode;
|
|
}
|
|
finally
|
|
{
|
|
CloseHandle(processInformation.hThread);
|
|
CloseHandle(processInformation.hProcess);
|
|
}
|
|
}
|
|
}
|