|
11 | 11 | using System.Diagnostics.CodeAnalysis; |
12 | 12 | using System.Globalization; |
13 | 13 | using System.IO; |
| 14 | +using System.Linq; |
14 | 15 | using System.Management.Automation.Internal; |
15 | | -using System.Management.Automation.Runspaces; |
16 | | -using System.Runtime.InteropServices; |
| 16 | +using System.Runtime.CompilerServices; |
17 | 17 | using System.Runtime.Serialization; |
18 | 18 | using System.Text; |
19 | 19 | using System.Threading; |
20 | 20 | using System.Threading.Tasks; |
21 | 21 | using System.Xml; |
| 22 | +using Microsoft.PowerShell.Commands; |
22 | 23 | using Microsoft.PowerShell.Telemetry; |
| 24 | +using Microsoft.Win32; |
23 | 25 | using Dbg = System.Management.Automation.Diagnostics; |
24 | 26 |
|
25 | 27 | namespace System.Management.Automation |
@@ -194,27 +196,211 @@ internal NativeCommandExitException(string path, int exitCode, int processId, st |
194 | 196 | /// </summary> |
195 | 197 | internal class NativeCommandProcessor : CommandProcessorBase |
196 | 198 | { |
197 | | - // This is the list of files which will trigger Legacy behavior if |
198 | | - // PSNativeCommandArgumentPassing is set to "Windows". |
199 | | - private static readonly IReadOnlySet<string> s_legacyFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) |
| 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 !UNIX |
| 225 | + /// <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 | + try |
| 253 | + { |
| 254 | + using RegistryKey key = Registry.LocalMachine.OpenSubKey(RegKeyPath); |
| 255 | + subKeyNames = key?.GetSubKeyNames(); |
| 256 | + } |
| 257 | + catch |
| 258 | + { |
| 259 | + return null; |
| 260 | + } |
| 261 | + |
| 262 | + if (subKeyNames is { Length: > 0 }) |
| 263 | + { |
| 264 | + IEnumerable<string> names = subKeyNames.Length <= MaxPackageManagerCount |
| 265 | + ? subKeyNames |
| 266 | + : 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) |
200 | 278 | { |
201 | | - ".js", |
202 | | - ".wsf", |
203 | | - ".cmd", |
204 | | - ".bat", |
205 | | - ".vbs", |
206 | | - }; |
207 | | - |
208 | | - // The following native commands have non-standard behavior with regard to argument passing, |
209 | | - // so we use Legacy argument parsing for them when PSNativeCommandArgumentPassing is set to Windows. |
210 | | - private static readonly IReadOnlySet<string> s_legacyCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase) |
| 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) |
211 | 306 | { |
212 | | - "cmd", |
213 | | - "cscript", |
214 | | - "find", |
215 | | - "sqlcmd", |
216 | | - "wscript", |
217 | | - }; |
| 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 | + else |
| 348 | + { |
| 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 | + else |
| 398 | + { |
| 399 | + newPath.Append(';').Append(newChange); |
| 400 | + } |
| 401 | + } |
| 402 | + } |
| 403 | +#endif |
218 | 404 |
|
219 | 405 | #region ctor/native command properties |
220 | 406 |
|
@@ -262,7 +448,11 @@ internal NativeCommandProcessor(ApplicationInfo applicationInfo, ExecutionContex |
262 | 448 | // Create input writer for providing input to the process. |
263 | 449 | _inputWriter = new ProcessInputWriter(Command); |
264 | 450 |
|
265 | | - _isTranscribing = this.Command.Context.EngineHostInterface.UI.IsTranscribing; |
| 451 | + _isTranscribing = context.EngineHostInterface.UI.IsTranscribing; |
| 452 | + |
| 453 | +#if !UNIX |
| 454 | + _isPackageManager = IsKnownPackageManager(_applicationInfo.Name) && IsPathUpdateFeatureEnabled(context); |
| 455 | +#endif |
266 | 456 | } |
267 | 457 |
|
268 | 458 | /// <summary> |
@@ -418,7 +608,7 @@ internal override void ProcessRecord() |
418 | 608 | /// <summary> |
419 | 609 | /// Process object for the invoked application. |
420 | 610 | /// </summary> |
421 | | - private System.Diagnostics.Process _nativeProcess; |
| 611 | + private Process _nativeProcess; |
422 | 612 |
|
423 | 613 | /// <summary> |
424 | 614 | /// This is used for writing input to the process. |
@@ -560,6 +750,12 @@ private void InitNativeProcess() |
560 | 750 | // must set UseShellExecute to false if we modify the environment block |
561 | 751 | startInfo.UseShellExecute = false; |
562 | 752 | } |
| 753 | + |
| 754 | + if (_isPackageManager) |
| 755 | + { |
| 756 | + _originalUserEnvPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.User); |
| 757 | + _originalSystemEnvPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine); |
| 758 | + } |
563 | 759 | #endif |
564 | 760 |
|
565 | 761 | if (this.Command.Context.CurrentPipelineStopping) |
@@ -898,6 +1094,13 @@ internal override void Complete() |
898 | 1094 | ConsumeAvailableNativeProcessOutput(blocking: true); |
899 | 1095 | _nativeProcess.WaitForExit(); |
900 | 1096 |
|
| 1097 | +#if !UNIX |
| 1098 | + if (_isPackageManager) |
| 1099 | + { |
| 1100 | + UpdateProcessEnvPath(_originalUserEnvPath, _originalSystemEnvPath); |
| 1101 | + } |
| 1102 | +#endif |
| 1103 | + |
901 | 1104 | // Capture screen output if we are transcribing and running stand alone |
902 | 1105 | if (_isTranscribing && (s_supportScreenScrape == true) && _runStandAlone) |
903 | 1106 | { |
@@ -1717,6 +1920,7 @@ private bool IsExecutable(string path) |
1717 | 1920 | #region Minishell Interop |
1718 | 1921 |
|
1719 | 1922 | private bool _isMiniShell = false; |
| 1923 | + |
1720 | 1924 | /// <summary> |
1721 | 1925 | /// Returns true if native command being invoked is mini-shell. |
1722 | 1926 | /// </summary> |
|
0 commit comments