MegaBites-AI/Windows-powershell
0308
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4#pragma warning disable 1634, 16915 6using System.Collections;7using System.Collections.Concurrent;8using System.Collections.Generic;9using System.ComponentModel;10using System.Diagnostics;11using System.Diagnostics.CodeAnalysis;12using System.Globalization;13using System.IO;14using System.Linq;15using System.Management.Automation.Internal;16using System.Runtime.CompilerServices;17using System.Runtime.Serialization;18using System.Text;19using System.Threading;20using System.Threading.Tasks;21using System.Xml;22using Microsoft.PowerShell.Commands;23using Microsoft.PowerShell.Telemetry;24using Microsoft.Win32;25using Dbg = System.Management.Automation.Diagnostics;26 27namespace System.Management.Automation28{29 /// <summary>30 /// Various types of input/output supported by native commands.31 /// </summary>32 /// <remarks>33 /// Most native commands only support text. Other formats34 /// are supported by minishell35 /// </remarks>36 internal enum NativeCommandIOFormat37 {38 Text,39 Xml40 }41 42 /// <summary>43 /// Different streams produced by minishell output.44 /// </summary>45 internal enum MinishellStream46 {47 Output,48 Error,49 Verbose,50 Warning,51 Debug,52 Progress,53 Information,54 Unknown55 }56 57 /// <summary>58 /// Helper class which holds stream names and also provide conversion59 /// method.60 /// </summary>61 internal static class StringToMinishellStreamConverter62 {63 internal const string OutputStream = "output";64 internal const string ErrorStream = "error";65 internal const string DebugStream = "debug";66 internal const string VerboseStream = "verbose";67 internal const string WarningStream = "warning";68 internal const string ProgressStream = "progress";69 internal const string InformationStream = "information";70 71 internal static MinishellStream ToMinishellStream(string stream)72 {73 Dbg.Assert(stream != null, "caller should validate the parameter");74 75 MinishellStream ms = MinishellStream.Unknown;76 if (OutputStream.Equals(stream, StringComparison.OrdinalIgnoreCase))77 {78 ms = MinishellStream.Output;79 }80 else if (ErrorStream.Equals(stream, StringComparison.OrdinalIgnoreCase))81 {82 ms = MinishellStream.Error;83 }84 else if (DebugStream.Equals(stream, StringComparison.OrdinalIgnoreCase))85 {86 ms = MinishellStream.Debug;87 }88 else if (VerboseStream.Equals(stream, StringComparison.OrdinalIgnoreCase))89 {90 ms = MinishellStream.Verbose;91 }92 else if (WarningStream.Equals(stream, StringComparison.OrdinalIgnoreCase))93 {94 ms = MinishellStream.Warning;95 }96 else if (ProgressStream.Equals(stream, StringComparison.OrdinalIgnoreCase))97 {98 ms = MinishellStream.Progress;99 }100 else if (InformationStream.Equals(stream, StringComparison.OrdinalIgnoreCase))101 {102 ms = MinishellStream.Information;103 }104 105 return ms;106 }107 }108 109 /// <summary>110 /// An output object from the child process.111 /// If it's from the error stream isError will be true.112 /// </summary>113 internal class ProcessOutputObject114 {115 /// <summary>116 /// Get the data from this object.117 /// </summary>118 /// <value>The data</value>119 internal object Data { get; }120 121 /// <summary>122 /// Stream to which data belongs.123 /// </summary>124 internal MinishellStream Stream { get; }125 126 /// <summary>127 /// Build an output object.128 /// </summary>129 /// <param name="data">The data to output.</param>130 /// <param name="stream">Stream to which data belongs.</param>131 internal ProcessOutputObject(object data, MinishellStream stream)132 {133 Data = data;134 Stream = stream;135 }136 }137 138#nullable enable139 /// <summary>140 /// This exception is used by the NativeCommandProcessor to indicate an error141 /// when a native command returns a non-zero exit code.142 /// </summary>143 public sealed class NativeCommandExitException : RuntimeException144 {145 // NOTE:146 // When implementing the native error action preference integration,147 // reusing ApplicationFailedException was rejected.148 // Instead of reusing a type already used in another scenario149 // it was decided instead to use a fresh type to avoid conflating the two scenarios:150 // * ApplicationFailedException: PowerShell was not able to complete execution of the application.151 // * NativeCommandExitException: the application completed execution but returned a non-zero exit code.152 153 #region Constructors154 155 /// <summary>156 /// Initializes a new instance of the <see cref="NativeCommandExitException"/> class with information on the native157 /// command, a specified error message and a specified error ID.158 /// </summary>159 /// <param name="path">The full path of the native command.</param>160 /// <param name="exitCode">The exit code returned by the native command.</param>161 /// <param name="processId">The process ID of the process before it ended.</param>162 /// <param name="message">The error message.</param>163 /// <param name="errorId">The PowerShell runtime error ID.</param>164 internal NativeCommandExitException(string path, int exitCode, int processId, string message, string errorId)165 : base(message)166 {167 SetErrorId(errorId);168 SetErrorCategory(ErrorCategory.NotSpecified);169 Path = path;170 ExitCode = exitCode;171 ProcessId = processId;172 }173 174 #endregion Constructors175 176 /// <summary>177 /// Gets the path of the native command.178 /// </summary>179 public string? Path { get; }180 181 /// <summary>182 /// Gets the exit code returned by the native command.183 /// </summary>184 public int ExitCode { get; }185 186 /// <summary>187 /// Gets the native command's process ID.188 /// </summary>189 public int ProcessId { get; }190 191 }192#nullable restore193 194 /// <summary>195 /// Provides way to create and execute native commands.196 /// </summary>197 internal class NativeCommandProcessor : CommandProcessorBase198 {199 /// <summary>200 /// This is the list of files which will trigger Legacy behavior if 'PSNativeCommandArgumentPassing' is set to "Windows".201 /// </summary>202 private static readonly HashSet<string> s_legacyFileExtensions = new(StringComparer.OrdinalIgnoreCase)203 {204 ".js",205 ".wsf",206 ".cmd",207 ".bat",208 ".vbs",209 };210 211 /// <summary>212 /// This is the list of native commands that have non-standard behavior with regard to argument passing.213 /// We use Legacy argument parsing for them when 'PSNativeCommandArgumentPassing' is set to "Windows".214 /// </summary>215 private static readonly HashSet<string> s_legacyCommands = new(StringComparer.OrdinalIgnoreCase)216 {217 "cmd",218 "cscript",219 "find",220 "sqlcmd",221 "wscript",222 };223 224#if !UNIX225 /// <summary>226 /// List of known package managers pulled from the registry.227 /// </summary>228 private static readonly HashSet<string> s_knownPackageManagers = GetPackageManagerListFromRegistry();229 230 /// <summary>231 /// Indicates whether the Path Update feature is enabled in a given session.232 /// PowerShell sessions could reuse the same thread, so we cannot cache the value with a thread static variable.233 /// </summary>234 private static readonly ConditionalWeakTable<ExecutionContext, string> s_pathUpdateFeatureEnabled = new();235 236 private readonly bool _isPackageManager;237 private string _originalUserEnvPath;238 private string _originalSystemEnvPath;239 240 /// <summary>241 /// Gets the known package managers from the registry.242 /// </summary>243 private static HashSet<string> GetPackageManagerListFromRegistry()244 {245 // We only account for the first 8 package managers. This is the same behavior as in CMD.246 const int MaxPackageManagerCount = 8;247 const string RegKeyPath = @"Software\Microsoft\Command Processor\KnownPackageManagers";248 249 string[] subKeyNames = null;250 HashSet<string> retSet = null;251 252 try253 {254 using RegistryKey key = Registry.LocalMachine.OpenSubKey(RegKeyPath);255 subKeyNames = key?.GetSubKeyNames();256 }257 catch258 {259 return null;260 }261 262 if (subKeyNames is { Length: > 0 })263 {264 IEnumerable<string> names = subKeyNames.Length <= MaxPackageManagerCount265 ? subKeyNames266 : subKeyNames.Take(MaxPackageManagerCount);267 268 retSet = new(names, StringComparer.OrdinalIgnoreCase);269 }270 271 return retSet;272 }273 274 /// <summary>275 /// Check if the given name is a known package manager from the registry list.276 /// </summary>277 private static bool IsKnownPackageManager(string name)278 {279 if (s_knownPackageManagers is null)280 {281 return false;282 }283 284 if (s_knownPackageManagers.Contains(name))285 {286 return true;287 }288 289 int lastDotIndex = name.LastIndexOf('.');290 if (lastDotIndex > 0)291 {292 string nameWithoutExt = name[..lastDotIndex];293 if (s_knownPackageManagers.Contains(nameWithoutExt))294 {295 return true;296 }297 }298 299 return false;300 }301 302 /// <summary>303 /// Check if the Path Update feature is enabled for the given session.304 /// </summary>305 private static bool IsPathUpdateFeatureEnabled(ExecutionContext context)306 {307 // We check only once per session.308 if (s_pathUpdateFeatureEnabled.TryGetValue(context, out string value))309 {310 // The feature is enabled if the value is not null.311 return value is { };312 }313 314 // Disable Path Update if 'EnvironmentProvider' is disabled in the current session, or the current session is restricted.315 bool enabled = context.EngineSessionState.Providers.ContainsKey(EnvironmentProvider.ProviderName)316 && !Utils.IsSessionRestricted(context);317 318 // - Use the static empty string instance to indicate that the feature is enabled.319 // - Use the null value to indicate that the feature is disabled.320 s_pathUpdateFeatureEnabled.TryAdd(context, enabled ? string.Empty : null);321 return enabled;322 }323 324 /// <summary>325 /// Gets the added part of the new string compared to the old string.326 /// </summary>327 private static ReadOnlySpan<char> GetAddedPartOfString(string oldString, string newString)328 {329 if (oldString.Length >= newString.Length)330 {331 // Nothing added or something removed.332 return ReadOnlySpan<char>.Empty;333 }334 335 int index = newString.IndexOf(oldString);336 if (index is -1)337 {338 // The new and old strings are drastically different. Stop trying in this case.339 return ReadOnlySpan<char>.Empty;340 }341 342 if (index > 0)343 {344 // Found the old string at non-zero offset, so something was prepended to the old string.345 return newString.AsSpan(0, index);346 }347 else348 {349 // Found the old string at the beginning of the new string, so something was appended to the old string.350 return newString.AsSpan(oldString.Length);351 }352 }353 354 /// <summary>355 /// Update the process-scope environment variable Path based on the changes in the user-scope and system-scope Path.356 /// </summary>357 /// <param name="oldUserPath">The old value of the user-scope Path retrieved from registry.</param>358 /// <param name="oldSystemPath">The old value of the system-scope Path retrieved from registry.</param>359 private static void UpdateProcessEnvPath(string oldUserPath, string oldSystemPath)360 {361 string newUserEnvPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.User);362 string newSystemEnvPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine);363 string procEnvPath = Environment.GetEnvironmentVariable("Path");364 365 ReadOnlySpan<char> userPathChange = GetAddedPartOfString(oldUserPath, newUserEnvPath).Trim(';');366 ReadOnlySpan<char> systemPathChange = GetAddedPartOfString(oldSystemPath, newSystemEnvPath).Trim(';');367 368 // Add 2 to account for the path separators we may need to add.369 int maxLength = procEnvPath.Length + userPathChange.Length + systemPathChange.Length + 2;370 StringBuilder newPath = null;371 372 if (userPathChange.Length > 0)373 {374 CreateNewProcEnvPath(userPathChange);375 }376 377 if (systemPathChange.Length > 0)378 {379 CreateNewProcEnvPath(systemPathChange);380 }381 382 if (newPath is { Length: > 0 })383 {384 // Update the process env Path.385 Environment.SetEnvironmentVariable("Path", newPath.ToString());386 }387 388 // Helper method to create a new env Path string.389 void CreateNewProcEnvPath(ReadOnlySpan<char> newChange)390 {391 newPath ??= new StringBuilder(procEnvPath, capacity: maxLength);392 393 if (newPath.Length is 0 || newPath[^1] is ';')394 {395 newPath.Append(newChange);396 }397 else398 {399 newPath.Append(';').Append(newChange);400 }401 }402 }403#endif404 405 #region ctor/native command properties406 407 /// <summary>408 /// Information about application which is invoked by this instance of409 /// NativeCommandProcessor.410 /// </summary>411 private readonly ApplicationInfo _applicationInfo;412 413 /// <summary>414 /// Initializes the new instance of NativeCommandProcessor class.415 /// </summary>416 /// <param name="applicationInfo">417 /// The information about the application to run.418 /// </param>419 /// <param name="context">420 /// The execution context for this command.421 /// </param>422 /// <exception cref="ArgumentNullException">423 /// <paramref name="applicationInfo"/> or <paramref name="context"/> is null424 /// </exception>425 internal NativeCommandProcessor(ApplicationInfo applicationInfo, ExecutionContext context)426 : base(applicationInfo)427 {428 if (applicationInfo == null)429 {430 throw PSTraceSource.NewArgumentNullException(nameof(applicationInfo));431 }432 433 _applicationInfo = applicationInfo;434 this._context = context;435 436 this.Command = new NativeCommand();437 this.Command.CommandInfo = applicationInfo;438 this.Command.Context = context;439 this.Command.commandRuntime = this.commandRuntime = new MshCommandRuntime(context, applicationInfo, this.Command);440 441 this.CommandScope = context.EngineSessionState.CurrentScope;442 443 // provide native command a backpointer to this object.444 // When kill is called on the command object,445 // it calls this NCP back to kill the process...446 ((NativeCommand)Command).MyCommandProcessor = this;447 448 // Create input writer for providing input to the process.449 _inputWriter = new ProcessInputWriter(Command);450 451 _isTranscribing = context.EngineHostInterface.UI.IsTranscribing;452 453#if !UNIX454 _isPackageManager = IsKnownPackageManager(_applicationInfo.Name) && IsPathUpdateFeatureEnabled(context);455#endif456 }457 458 /// <summary>459 /// Gets the NativeCommand associated with this command processor.460 /// </summary>461 private NativeCommand nativeCommand462 {463 get464 {465 NativeCommand command = this.Command as NativeCommand;466 Diagnostics.Assert(command != null, "this.Command is created in the constructor.");467 return command;468 }469 }470 471 /// <summary>472 /// Gets or sets the name of the native command.473 /// </summary>474 private string NativeCommandName475 {476 get477 {478 string name = _applicationInfo.Name;479 return name;480 }481 }482 483 /// <summary>484 /// Gets or sets path to the native command.485 /// </summary>486 private string Path487 {488 get489 {490 string path = _applicationInfo.Path;491 return path;492 }493 }494 495 internal NativeCommandProcessor DownStreamNativeCommand { get; set; }496 497 internal bool UpstreamIsNativeCommand { get; set; }498 499 internal BytePipe StdOutDestination { get; set; }500 501 #endregion ctor/native command properties502 503 #region parameter binder504 505 /// <summary>506 /// Parameter binder used by this command processor.507 /// </summary>508 private NativeCommandParameterBinderController _nativeParameterBinderController;509 510 /// <summary>511 /// Gets a new instance of a ParameterBinderController using a NativeCommandParameterBinder.512 /// </summary>513 /// <param name="command">514 /// The native command to be run.515 /// </param>516 /// <returns>517 /// A new parameter binder controller for the specified command.518 /// </returns>519 internal ParameterBinderController NewParameterBinderController(InternalCommand command)520 {521 if (_isMiniShell)522 {523 _nativeParameterBinderController =524 new MinishellParameterBinderController(525 this.nativeCommand);526 }527 else528 {529 _nativeParameterBinderController =530 new NativeCommandParameterBinderController(531 this.nativeCommand);532 }533 534 return _nativeParameterBinderController;535 }536 537 internal NativeCommandParameterBinderController NativeParameterBinderController538 {539 get540 {541 if (_nativeParameterBinderController == null)542 {543 NewParameterBinderController(this.Command);544 }545 546 return _nativeParameterBinderController;547 }548 }549 550 #endregion parameter binder551 552 #region internal overrides553 554 /// <summary>555 /// Prepares the command for execution with the specified CommandParameterInternal.556 /// </summary>557 internal override void Prepare(IDictionary psDefaultParameterValues)558 {559 // Check if the application is minishell560 _isMiniShell = IsMiniShell();561 562 // For minishell parameter binding is done in Complete method because we need563 // to know if output is redirected before we can bind parameters.564 if (!_isMiniShell)565 {566 this.NativeParameterBinderController.BindParameters(arguments);567 }568 569 try570 {571 InitNativeProcess();572 }573 catch (Exception)574 {575 // Do cleanup in case of exception576 CleanUp(killBackgroundProcess: true);577 throw;578 }579 }580 581 /// <summary>582 /// Executes the command. This method assumes that Prepare is already called.583 /// </summary>584 internal override void ProcessRecord()585 {586 try587 {588 // If upstream is a native command it'll be writing directly to our stdin stream589 // so we can skip reading here.590 if (!UpstreamIsNativeCommand)591 {592 while (Read())593 {594 _inputWriter.Add(Command.CurrentPipelineObject);595 }596 }597 598 ConsumeAvailableNativeProcessOutput(blocking: false);599 }600 catch (Exception)601 {602 // Do cleanup in case of exception603 CleanUp(killBackgroundProcess: true);604 throw;605 }606 }607 608 /// <summary>609 /// Process object for the invoked application.610 /// </summary>611 private Process _nativeProcess;612 613 /// <summary>614 /// This is used for writing input to the process.615 /// </summary>616 private readonly ProcessInputWriter _inputWriter = null;617 618 /// <summary>619 /// Is true if this command is to be run "standalone" - that is, with620 /// no redirection.621 /// </summary>622 private bool _runStandAlone;623 624 /// <summary>625 /// Indicate whether we need to consider redirecting the output/error of the current native command.626 /// Usually a windows program which is the last command in a pipeline can be executed as 'background' -- we don't need to capture its output/error streams.627 /// </summary>628 private bool _isRunningInBackground;629 630 /// <summary>631 /// Indicate if we have called 'NotifyBeginApplication()' on the host, so that632 /// we can call the counterpart 'NotifyEndApplication' as appropriate.633 /// </summary>634 private bool _hasNotifiedBeginApplication;635 636 /// <summary>637 /// This output queue helps us keep the output and error (if redirected) order correct.638 /// We could do a blocking read in the Complete block instead,639 /// but then we would not be able to restore the order reasonable.640 /// </summary>641 private BlockingCollection<ProcessOutputObject> _nativeProcessOutputQueue;642 643 private static bool? s_supportScreenScrape = null;644 private readonly bool _isTranscribing;645 private Host.Coordinates _startPosition;646 647 /// <summary>648 /// Object used for synchronization between StopProcessing thread and649 /// Pipeline thread.650 /// </summary>651 private readonly object _sync = new object();652 653 private SemaphoreSlim _processInitialized;654 655 internal async Task WaitForProcessInitializationAsync(CancellationToken cancellationToken)656 {657 SemaphoreSlim processInitialized = _processInitialized;658 if (processInitialized is null)659 {660 lock (_sync)661 {662 processInitialized = _processInitialized ??= new SemaphoreSlim(0, 1);663 }664 }665 666 try667 {668 await processInitialized.WaitAsync(cancellationToken);669 }670 finally671 {672 processInitialized.Release();673 }674 }675 676 /// <summary>677 /// Creates a pipe representing the streaming of unprocessed bytes.678 /// </summary>679 /// <param name="stdout">680 /// The stream that the pipe should represent. <see langword="true" />681 /// for stdout, <see langword="false" /> for stdin.682 /// </param>683 /// <returns>A new byte pipe representing the specified stream.</returns>684 internal BytePipe CreateBytePipe(bool stdout) => new NativeCommandProcessorBytePipe(this, stdout);685 686 /// <summary>687 /// Gets the specified base <see cref="Stream" /> for the underlying688 /// <see cref="Process" />.689 /// </summary>690 /// <param name="stdout">691 /// The stream that should be retrieved. <see langword="true" /> for692 /// stdout, <see langword="false" /> for stdin.693 /// </param>694 /// <returns>The specified <see cref="Stream" />.</returns>695 internal Stream GetStream(bool stdout)696 {697 Debug.Assert(698 _nativeProcess is not null,699 "Caller should verify that initialization has completed before attempting to get the underlying stream.");700 701 return stdout702 ? _nativeProcess.StandardOutput.BaseStream703 : _nativeProcess.StandardInput.BaseStream;704 }705 706 /// <summary>707 /// Executes the native command once all of the input has been gathered.708 /// </summary>709 /// <exception cref="PipelineStoppedException">710 /// The pipeline is stopping711 /// </exception>712 /// <exception cref="ApplicationFailedException">713 /// The native command could not be run714 /// </exception>715 private void InitNativeProcess()716 {717 // Figure out if we're going to run this process "standalone" i.e. without718 // redirecting anything. This is a bit tricky as we always run redirected so719 // we have to see if the redirection is actually being done at the topmost level or not.720 721 // Calculate if input and output are redirected.722 bool redirectOutput;723 bool redirectError;724 bool redirectInput;725 726 _startPosition = new Host.Coordinates();727 728 bool isWindowsApplication = IsWindowsApplication(this.Path);729 CalculateIORedirection(isWindowsApplication, out redirectOutput, out redirectError, out redirectInput);730 731 // Find out if it's the only command in the pipeline.732 bool soloCommand = this.Command.MyInvocation.PipelineLength == 1;733 734 // Get the start info for the process.735 ProcessStartInfo startInfo = GetProcessStartInfo(redirectOutput, redirectError, redirectInput, soloCommand);736 737 // Send Telemetry indicating what argument passing mode we are in.738 ApplicationInsightsTelemetry.SendExperimentalUseData(739 "PSWindowsNativeCommandArgPassing",740 NativeParameterBinderController.ArgumentPassingStyle.ToString());741 742#if !UNIX743 string commandPath = this.Path.ToLowerInvariant();744 if (commandPath.EndsWith("powershell.exe") || commandPath.EndsWith("powershell_ise.exe"))745 {746 // if starting Windows PowerShell, need to remove PowerShell specific segments of PSModulePath747 string psmodulepath = ModuleIntrinsics.GetWindowsPowerShellModulePath();748 startInfo.Environment["PSModulePath"] = psmodulepath;749 750 // must set UseShellExecute to false if we modify the environment block751 startInfo.UseShellExecute = false;752 }753 754 if (_isPackageManager)755 {756 _originalUserEnvPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.User);757 _originalSystemEnvPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine);758 }759#endif760 761 if (this.Command.Context.CurrentPipelineStopping)762 {763 throw new PipelineStoppedException();764 }765 766 Exception exceptionToRethrow = null;767 try768 {769 // Before start the executable, tell the host, which might want to save off the770 // window title or other such state as might be tweaked by the native process.771 Command.Context.EngineHostInterface.NotifyBeginApplication();772 _hasNotifiedBeginApplication = true;773 774 if (_runStandAlone)775 {776 // Store the Raw UI coordinates so that we can scrape the screen after777 // if we are transcribing.778 if (_isTranscribing && (s_supportScreenScrape == true))779 {780 _startPosition = this.Command.Context.EngineHostInterface.UI.RawUI.CursorPosition;781 _startPosition.X = 0;782 }783 }784 785 // Start the process. If stop has been called, throw exception.786 // Note: if StopProcessing is called which this method has the lock,787 // Stop thread will wait for nativeProcess to start.788 // If StopProcessing gets the lock first, then it will set the stopped789 // flag and this method will throw PipelineStoppedException when it gets790 // the lock.791 lock (_sync)792 {793 if (_stopped)794 {795 throw new PipelineStoppedException();796 }797 798 try799 {800 _nativeProcess = new Process() { StartInfo = startInfo };801 _nativeProcess.Start();802 }803 catch (Win32Exception)804 {805#if UNIX806 // On Unix platforms, nothing can be further done, so just throw.807 throw;808#else809 // On headless Windows SKUs, there is no shell to fall back to, so just throw810 if (!Platform.IsWindowsDesktop)811 {812 throw;813 }814 815 // on Windows desktops, see if there is a file association for this command. If so then we'll use that.816 string executable = Interop.Windows.FindExecutable(startInfo.FileName);817 bool notDone = true;818 // check to see what mode we should be in for argument passing819 if (!string.IsNullOrEmpty(executable))820 {821 isWindowsApplication = IsWindowsApplication(executable);822 if (!isWindowsApplication)823 {824 // Allocate a console if there isn't one attached already...825 ConsoleVisibility.AllocateHiddenConsole();826 }827 828 string oldArguments = startInfo.Arguments;829 string oldFileName = startInfo.FileName;830 // Check to see whether this executable should be using Legacy mode argument parsing831 bool useSpecialArgumentPassing = UseSpecialArgumentPassing(oldFileName);832 if (useSpecialArgumentPassing)833 {834 // codeql[cs/microsoft/command-line-injection] - This is expected PowerShell behavior where user inputted paths are supported for the context of this method and the path portion of the argument is escaped. The user assumes trust for the file path specified on the user's system to start process for, and in the case of remoting, restricted remoting security guidelines should be used.835 startInfo.Arguments = "\"" + oldFileName + "\" " + startInfo.Arguments;836 }837 else838 {839 startInfo.ArgumentList.Insert(0, oldFileName);840 }841 startInfo.FileName = executable;842 try843 {844 _nativeProcess.Start();845 notDone = false;846 }847 catch (Win32Exception)848 {849 // Restore the old filename and arguments to try shell execute last...850 if (useSpecialArgumentPassing)851 {852 startInfo.Arguments = oldArguments;853 }854 else855 {856 startInfo.ArgumentList.RemoveAt(0);857 }858 859 // codeql[cs/microsoft/command-line-injection-shell-execution] - This is expected PowerShell behavior where user inputted paths are supported for the context of this method. The user assumes trust for the file path specified on the user's system to retrieve process info for, and in the case of remoting, restricted remoting security guidelines should be used.860 startInfo.FileName = oldFileName;861 }862 }863 // We got here because there was either no executable found for this864 // file or we tried to launch the exe and it failed. In either case865 // we will try launching one last time using ShellExecute...866 if (notDone)867 {868 if (soloCommand && !startInfo.UseShellExecute)869 {870 startInfo.UseShellExecute = true;871 startInfo.RedirectStandardInput = false;872 startInfo.RedirectStandardOutput = false;873 startInfo.RedirectStandardError = false;874 _nativeProcess.Start();875 }876 else877 {878 throw;879 }880 }881#endif882 }883 884 if (UpstreamIsNativeCommand)885 {886 _processInitialized ??= new SemaphoreSlim(0, 1);887 _processInitialized.Release();888 }889 }890 891 if (this.Command.MyInvocation.PipelinePosition < this.Command.MyInvocation.PipelineLength)892 {893 // Never background unless you're at the end of a pipe.894 // Something like895 // ls | notepad | sort.exe896 // should block until the notepad process is terminated.897 _isRunningInBackground = false;898 }899 else900 {901 _isRunningInBackground = true;902 if (!startInfo.UseShellExecute)903 {904 _isRunningInBackground = isWindowsApplication;905 }906 }907 908 try909 {910 // If input is redirected, start input to process.911 if (startInfo.RedirectStandardInput)912 {913 NativeCommandIOFormat inputFormat = NativeCommandIOFormat.Text;914 if (_isMiniShell)915 {916 inputFormat = ((MinishellParameterBinderController)NativeParameterBinderController).InputFormat;917 }918 919 lock (_sync)920 {921 if (!_stopped && !UpstreamIsNativeCommand)922 {923 _inputWriter.Start(_nativeProcess, inputFormat);924 }925 }926 }927 }928 catch (Exception)929 {930 StopProcessing();931 throw;932 }933 934 if (!_isRunningInBackground)935 {936 InitOutputQueue();937 }938 }939 catch (Win32Exception e)940 {941 exceptionToRethrow = e;942 }943 catch (PipelineStoppedException)944 {945 // If we're stopping the process, just rethrow this exception...946 throw;947 }948 catch (Exception e)949 {950 exceptionToRethrow = e;951 }952 953 // An exception was thrown while attempting to run the program954 // so wrap and rethrow it here...955 if (exceptionToRethrow != null)956 {957 // It's a system exception so wrap it in one of ours and re-throw.958 string message = StringUtil.Format(ParserStrings.ProgramFailedToExecute,959 this.NativeCommandName, exceptionToRethrow.Message,960 this.Command.MyInvocation.PositionMessage);961 ApplicationFailedException appFailedException = new ApplicationFailedException(message, exceptionToRethrow);962 963 // There is no need to set this exception here since this exception will eventually be caught by pipeline processor.964 // this.commandRuntime.PipelineProcessor.ExecutionFailed = true;965 966 throw appFailedException;967 }968 }969 970 private AsyncByteStreamTransfer _stdOutByteTransfer;971 972 private void InitOutputQueue()973 {974 // if output is redirected, start reading output of process in queue.975 if (_nativeProcess.StartInfo.RedirectStandardOutput || _nativeProcess.StartInfo.RedirectStandardError)976 {977 lock (_sync)978 {979 if (!_stopped)980 {981 if (CommandRuntime.ErrorMergeTo is MshCommandRuntime.MergeDataStream.Output)982 {983 StdOutDestination = null;984 if (DownStreamNativeCommand is not null)985 {986 DownStreamNativeCommand.UpstreamIsNativeCommand = false;987 DownStreamNativeCommand = null;988 }989 }990 991 _nativeProcessOutputQueue = new BlockingCollection<ProcessOutputObject>();992 // we don't assign the handler to anything, because it's used only for objects marshaling993 BytePipe stdOutDestination = StdOutDestination ?? DownStreamNativeCommand?.CreateBytePipe(stdout: false);994 995 BytePipe stdOutSource = null;996 if (stdOutDestination is not null)997 {998 stdOutSource = CreateBytePipe(stdout: true);999 }1000 1001 _ = new ProcessOutputHandler(1002 _nativeProcess,1003 _nativeProcessOutputQueue,1004 stdOutDestination,1005 stdOutSource,1006 out _stdOutByteTransfer);1007 }1008 }1009 }1010 }1011 1012 private ProcessOutputObject DequeueProcessOutput(bool blocking)1013 {1014 if (blocking)1015 {1016 // If adding was completed and collection is empty (IsCompleted == true)1017 // there is no need to do a blocking Take(), we should just return.1018 if (!_nativeProcessOutputQueue.IsCompleted)1019 {1020 try1021 {1022 // If adding is not complete we need a try {} catch {}1023 // to mitigate a concurrent call to CompleteAdding().1024 return _nativeProcessOutputQueue.Take();1025 }1026 catch (InvalidOperationException)1027 {1028 // It's a normal situation: another thread can mark collection as CompleteAdding1029 // in a concurrent way and we will rise an exception in Take().1030 // Although it's a normal situation it's not the most common path1031 // and will be executed only on the race condtion case.1032 }1033 }1034 1035 return null;1036 }1037 1038 _nativeProcessOutputQueue.TryTake(out ProcessOutputObject record);1039 return record;1040 }1041 1042 /// <summary>1043 /// Read the output from the native process and send it down the line.1044 /// </summary>1045 private void ConsumeAvailableNativeProcessOutput(bool blocking)1046 {1047 if (_isRunningInBackground)1048 {1049 return;1050 }1051 1052 bool stdOutRedirected = _nativeProcess.StartInfo.RedirectStandardOutput;1053 bool stdErrRedirected = _nativeProcess.StartInfo.RedirectStandardError;1054 if (stdOutRedirected && _stdOutByteTransfer is not null)1055 {1056 if (blocking)1057 {1058 _stdOutByteTransfer.EOF.GetAwaiter().GetResult();1059 }1060 1061 if (!stdErrRedirected)1062 {1063 return;1064 }1065 }1066 1067 if (stdOutRedirected || stdErrRedirected)1068 {1069 ProcessOutputObject record;1070 while ((record = DequeueProcessOutput(blocking)) != null)1071 {1072 if (this.Command.Context.CurrentPipelineStopping)1073 {1074 this.StopProcessing();1075 return;1076 }1077 1078 ProcessOutputRecord(record);1079 }1080 }1081 }1082 1083 internal override void Complete()1084 {1085 Exception exceptionToRethrow = null;1086 try1087 {1088 if (!_isRunningInBackground)1089 {1090 // Wait for input writer to finish.1091 if (!UpstreamIsNativeCommand || _nativeProcess.StartInfo.RedirectStandardError)1092 {1093 _inputWriter.Done();1094 }1095 1096 // read all the available output in the blocking way1097 ConsumeAvailableNativeProcessOutput(blocking: true);1098 _nativeProcess.WaitForExit();1099 1100#if !UNIX1101 if (_isPackageManager)1102 {1103 UpdateProcessEnvPath(_originalUserEnvPath, _originalSystemEnvPath);1104 }1105#endif1106 1107 // Capture screen output if we are transcribing and running stand alone1108 if (_isTranscribing && (s_supportScreenScrape == true) && _runStandAlone)1109 {1110 Host.Coordinates endPosition = this.Command.Context.EngineHostInterface.UI.RawUI.CursorPosition;1111 endPosition.X = this.Command.Context.EngineHostInterface.UI.RawUI.BufferSize.Width - 1;1112 1113 // If the end position is before the start position, then capture the entire buffer.1114 if (endPosition.Y < _startPosition.Y)1115 {1116 _startPosition.Y = 0;1117 }1118 1119 Host.BufferCell[,] bufferContents = this.Command.Context.EngineHostInterface.UI.RawUI.GetBufferContents(1120 new Host.Rectangle(_startPosition, endPosition));1121 1122 StringBuilder lineContents = new StringBuilder();1123 StringBuilder bufferText = new StringBuilder();1124 1125 for (int row = 0; row < bufferContents.GetLength(0); row++)1126 {1127 if (row > 0)1128 {1129 bufferText.Append(Environment.NewLine);1130 }1131 1132 lineContents.Clear();1133 for (int column = 0; column < bufferContents.GetLength(1); column++)1134 {1135 lineContents.Append(bufferContents[row, column].Character);1136 }1137 1138 bufferText.Append(lineContents.ToString().TrimEnd(Utils.Separators.SpaceOrTab));1139 }1140 1141 this.Command.Context.InternalHost.UI.TranscribeResult(bufferText.ToString());1142 }1143 1144 this.Command.Context.SetVariable(SpecialVariables.LastExitCodeVarPath, _nativeProcess.ExitCode);1145 if (_nativeProcess.ExitCode == 0)1146 {1147 return;1148 }1149 1150 this.commandRuntime.PipelineProcessor.ExecutionFailed = true;1151 1152 // We send telemetry information only if the feature is enabled.1153 // This shouldn't be done once, because it's a run-time check we should send telemetry every time.1154 // Report on the following conditions:1155 // - The variable is not present1156 // - The value is not set (variable is null)1157 // - The value is set to true or false1158 bool useDefaultSetting;1159 bool nativeErrorActionPreferenceSetting = Command.Context.GetBooleanPreference(1160 SpecialVariables.PSNativeCommandUseErrorActionPreferenceVarPath,1161 defaultPref: false,1162 out useDefaultSetting);1163 1164 // The variable is unset1165 if (useDefaultSetting)1166 {1167 ApplicationInsightsTelemetry.SendExperimentalUseData("PSNativeCommandErrorActionPreference", "unset");1168 return;1169 }1170 1171 // Send the value that was set.1172 ApplicationInsightsTelemetry.SendExperimentalUseData("PSNativeCommandErrorActionPreference", nativeErrorActionPreferenceSetting.ToString());1173 1174 // if it was explicitly set to false, return1175 if (!nativeErrorActionPreferenceSetting)1176 {1177 return;1178 }1179 1180 const string errorId = nameof(CommandBaseStrings.ProgramExitedWithNonZeroCode);1181#if UNIX1182 string hexFormatStr = "0x{0:X2}";1183#else1184 string hexFormatStr = "0x{0:X8}";1185#endif1186 1187 string errorMsg = StringUtil.Format(1188 CommandBaseStrings.ProgramExitedWithNonZeroCode,1189 NativeCommandName,1190 _nativeProcess.ExitCode,1191 string.Format(CultureInfo.InvariantCulture, hexFormatStr, _nativeProcess.ExitCode));1192 1193 var exception = new NativeCommandExitException(1194 Path,1195 _nativeProcess.ExitCode,1196 _nativeProcess.Id,1197 errorMsg,1198 errorId);1199 1200 var errorRecord = new ErrorRecord(exception, errorId, ErrorCategory.NotSpecified, targetObject: Path);