Files
orca/native/windows-cli-launcher/OrcaCliLauncher.cs
T
Neil 6954398d53 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
2026-08-02 22:11:25 -07:00

130 lines
4.5 KiB
C#

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
internal static class OrcaCliLauncher
{
private static int Main(string[] args)
{
try
{
string launcherDirectory = Path.GetDirectoryName(typeof(OrcaCliLauncher).Assembly.Location);
string resourcesDirectory = Directory.GetParent(launcherDirectory).FullName;
string appDirectory = Directory.GetParent(resourcesDirectory).FullName;
string electronPath = Path.Combine(appDirectory, "Orca.exe");
string cliPath = Path.Combine(
resourcesDirectory,
"app.asar.unpacked",
"out",
"cli",
"index.js"
);
if (!File.Exists(electronPath))
{
Console.Error.WriteLine("Unable to locate Orca.exe next to \"{0}\"", resourcesDirectory);
return 1;
}
if (!File.Exists(cliPath))
{
Console.Error.WriteLine("Unable to locate the Orca CLI entrypoint at \"{0}\"", cliPath);
return 1;
}
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = electronPath,
Arguments = BuildArguments(cliPath, args),
UseShellExecute = false
};
// Why: launching without cmd.exe preserves embedded newlines while matching the
// packaged batch launcher's Electron-as-Node environment contract.
// Why: reading ProcessStartInfo.EnvironmentVariables copies the process block into a
// case-insensitive dictionary and throws when it holds both PATH and Path
// (stablyai/orca#12046); mutating our own env leaves the child inheriting it verbatim.
MoveEnvironmentVariable("NODE_OPTIONS", "ORCA_NODE_OPTIONS");
MoveEnvironmentVariable("NODE_REPL_EXTERNAL_MODULE", "ORCA_NODE_REPL_EXTERNAL_MODULE");
Environment.SetEnvironmentVariable("ELECTRON_RUN_AS_NODE", "1");
Environment.SetEnvironmentVariable("ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER", "1");
string requestedCliCommand = Environment.GetEnvironmentVariable("ORCA_CLI_COMMAND");
Environment.SetEnvironmentVariable(
"ORCA_CLI_COMMAND",
requestedCliCommand == "orca-ide" ? "orca-ide" : "orca"
);
using (Process child = Process.Start(startInfo))
{
child.WaitForExit();
return child.ExitCode;
}
}
catch (Exception error)
{
Console.Error.WriteLine("Unable to start the Orca CLI: {0}", error.Message);
return 1;
}
}
private static void MoveEnvironmentVariable(string sourceName, string targetName)
{
string value = Environment.GetEnvironmentVariable(sourceName);
Environment.SetEnvironmentVariable(sourceName, null);
// Why: a null value clears the target, matching the previous unconditional Remove.
Environment.SetEnvironmentVariable(targetName, value);
}
private static string BuildArguments(string cliPath, string[] args)
{
StringBuilder commandLine = new StringBuilder(QuoteArgument(cliPath));
foreach (string arg in args)
{
commandLine.Append(' ');
commandLine.Append(QuoteArgument(arg));
}
return commandLine.ToString();
}
private static string QuoteArgument(string value)
{
bool requiresQuotes = value.Length == 0;
for (int index = 0; index < value.Length && !requiresQuotes; index += 1)
{
requiresQuotes = value[index] == '"' || Char.IsWhiteSpace(value[index]);
}
if (!requiresQuotes)
{
return value;
}
StringBuilder quoted = new StringBuilder("\"");
int backslashCount = 0;
foreach (char character in value)
{
if (character == '\\')
{
backslashCount += 1;
continue;
}
if (character == '"')
{
quoted.Append('\\', backslashCount * 2 + 1);
quoted.Append('"');
}
else
{
quoted.Append('\\', backslashCount);
quoted.Append(character);
}
backslashCount = 0;
}
quoted.Append('\\', backslashCount * 2);
quoted.Append('"');
return quoted.ToString();
}
}