| File: src\Shared\ProcessStartInfoHelper.cs | Web Access |
| Project: src\src\Aspire.Hosting.RemoteHost\Aspire.Hosting.RemoteHost.csproj (Aspire.Hosting.RemoteHost) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; internal static class ProcessStartInfoHelper { private const string CommandEnvironmentVariable = "ASPIRE_COMMAND_SHIM_PATH"; private const string ArgumentEnvironmentVariablePrefix = "ASPIRE_COMMAND_SHIM_ARGUMENT_"; /// <summary> /// Configures an executable and its arguments, including Windows batch shims. /// </summary> public static void SetCommand(ProcessStartInfo startInfo, string command, IEnumerable<string> args, bool isWindows) { startInfo.Arguments = ""; #if !NETFRAMEWORK startInfo.ArgumentList.Clear(); #endif if (isWindows && (command.EndsWith(".cmd", StringComparison.OrdinalIgnoreCase) || command.EndsWith(".bat", StringComparison.OrdinalIgnoreCase))) { startInfo.FileName = Environment.GetEnvironmentVariable("ComSpec") ?? "cmd.exe"; SetEnvironmentVariable(startInfo, CommandEnvironmentVariable, command); var commandLine = new StringBuilder($"\"%{CommandEnvironmentVariable}%\""); var index = 0; foreach (var arg in args) { if (arg.Length == 0) { // Windows omits empty environment values. An undefined %VARIABLE% remains // literal in cmd's command line, so represent empty arguments directly. commandLine.Append(" \"\""); } else { var variable = ArgumentEnvironmentVariablePrefix + index.ToString(CultureInfo.InvariantCulture); SetEnvironmentVariable(startInfo, variable, EncodeQuotedWindowsArgument(arg, forBatchShim: true)); commandLine.Append(" \"%").Append(variable).Append("%\""); } index++; } // Expand child-only variables once so paths like C:\apps\%TEMP%\host.mts // retain literal percent-delimited text. /V:OFF also preserves literal '!'. // /S /C strips the outer quotes, leaving: "%ASPIRE_COMMAND_SHIM_PATH%" "%ASPIRE_COMMAND_SHIM_ARGUMENT_0%" // https://learn.microsoft.com/windows-server/administration/windows-commands/cmd startInfo.Arguments = $"/D /V:OFF /S /C \"{commandLine}\""; } else { startInfo.FileName = command; #if NETFRAMEWORK startInfo.Arguments = string.Join(" ", args.Select(QuoteWindowsArgument)); #else foreach (var arg in args) { startInfo.ArgumentList.Add(arg); } #endif } } private static void SetEnvironmentVariable(ProcessStartInfo startInfo, string name, string value) { #if NETFRAMEWORK startInfo.EnvironmentVariables[name] = value; #else startInfo.Environment[name] = value; #endif } #if NETFRAMEWORK private static string QuoteWindowsArgument(string argument) { if (argument.Length > 0 && !argument.Any(static character => char.IsWhiteSpace(character) || character == '"')) { return argument; } return $"\"{EncodeQuotedWindowsArgument(argument, forBatchShim: false)}\""; } #endif private static string EncodeQuotedWindowsArgument(string argument, bool forBatchShim) { var result = new StringBuilder(argument.Length); var backslashCount = 0; foreach (var character in argument) { if (character == '\\') { backslashCount++; continue; } if (character == '"') { // cmd does not recognize backslash-escaped quotes. For batch shims, // a "quoted" & value becomes "a ""quoted"" & value", keeping shell // metacharacters quoted while the native child restores the literal quotes. // CRT parsing also requires doubled backslashes before quotes and the // closing delimiter; native executables use the usual backslash escape. // https://learn.microsoft.com/cpp/c-language/parsing-c-command-line-arguments result.Append('\\', (backslashCount * 2) + (forBatchShim ? 0 : 1)); if (forBatchShim) { result.Append('"'); } result.Append('"'); backslashCount = 0; continue; } result.Append('\\', backslashCount); backslashCount = 0; result.Append(character); } result.Append('\\', backslashCount * 2); return result.ToString(); } }