Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
535d698
first cut at new arg passing which special cases windows
JamesWTruher May 10, 2021
30639d9
Add validation for the proper behavior based on the platform
JamesWTruher May 14, 2021
51f8558
Updates to address code factor issues.
JamesWTruher May 17, 2021
217fba2
additional code factor fixes
JamesWTruher May 17, 2021
4e79ca2
try to reduce complexity of GetProcessStartInfo
JamesWTruher May 17, 2021
b9613d0
fix spelling error
JamesWTruher May 17, 2021
bdabe9e
Additional CodeFactor fixes
JamesWTruher May 17, 2021
7ec84da
Fix a couple of tests now that the default behavior on Windows has ch…
JamesWTruher May 19, 2021
dd95172
Make sure to change the arguments correctly.
JamesWTruher May 25, 2021
90578a5
Add .js and .wsf extensions to the list of specially treated files
JamesWTruher May 25, 2021
60c9d50
Add tests for .cmd, .bat, .vbs, .wsf, .js files.
JamesWTruher May 26, 2021
c3babfd
Add additional validation which should help with debugging test failu…
JamesWTruher May 27, 2021
0e0423c
Additional debugging output to help determine why it works locally bu…
JamesWTruher May 27, 2021
ef89309
Adding explicit call to csript.exe for wsf and vbs files
JamesWTruher May 27, 2021
f2abcfd
Change variable name
JamesWTruher Jun 1, 2021
8c7962b
incorporate feedback from iSazonov
JamesWTruher Jun 14, 2021
e452b07
Update src/System.Management.Automation/engine/CommandBase.cs
JamesWTruher Jul 14, 2021
f8df366
Apply suggestions from code review
rjmholt Jul 20, 2021
7a0b264
Fix comments
rjmholt Jul 20, 2021
72d3dc5
Rework use-legacy test
rjmholt Jul 20, 2021
256b2c4
Use switch expression
rjmholt Jul 20, 2021
25d26ef
Refactor ProcessStartInfo creation
rjmholt Jul 20, 2021
5b9eaa6
Fix parameter ordering
rjmholt Jul 20, 2021
f2a99d1
Use static variable path
rjmholt Jul 20, 2021
a4d8858
Merge pull request #15 from rjmholt/NativeArgPassing002
JamesWTruher Jul 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/System.Management.Automation/engine/CommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,14 @@ public enum NativeArgumentPassingStyle
/// <summary>Use legacy argument parsing via ProcessStartInfo.Arguments.</summary>
Legacy = 0,

/// <summary>Use new style argument parsing via ProcessStartInfo.ArgumentList.</summary>
Standard = 1
/// <summary>Use new style argument passing via ProcessStartInfo.ArgumentList.</summary>
Standard = 1,

/// <summary>
/// Use specific to Windows passing style which is Legacy for selected files on Windows, but
/// Standard for everything else. This is the default behavior for Windows.
/// </summary>
Windows = 2
}
#endregion NativeArgumentPassingStyle

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1320,13 +1320,18 @@ private void AddVariables(IEnumerable<SessionStateVariableEntry> variables)

// If the PSNativeCommandArgumentPassing feature is enabled, create the variable which controls the behavior
// Since the BuiltInVariables list is static, and this should be done dynamically
// we need to do this here.
// we need to do this here. Also, since the defaults are different based on platform we need a
// bit more logic.
if (ExperimentalFeature.IsEnabled("PSNativeCommandArgumentPassing"))
{
NativeArgumentPassingStyle style = NativeArgumentPassingStyle.Standard;
if (Platform.IsWindows) {
style = NativeArgumentPassingStyle.Windows;
}
Variables.Add(
new SessionStateVariableEntry(
SpecialVariables.NativeArgumentPassing,
NativeArgumentPassingStyle.Standard,
style,
RunspaceInit.NativeCommandArgumentPassingDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(NativeArgumentPassingStyle))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ namespace System.Management.Automation
/// </summary>
internal class NativeCommandParameterBinder : ParameterBinderBase
{
private readonly VariablePath s_nativeArgumentPassingVarPath = new VariablePath(SpecialVariables.NativeArgumentPassing);

#region ctor

/// <summary>
Expand Down Expand Up @@ -187,7 +189,7 @@ internal void AddToArgumentList(CommandParameterInternal parameter, string argum
/// <summary>
/// Gets a value indicating whether to use an ArgumentList or string for arguments when invoking a native executable.
/// </summary>
internal bool UseArgumentList
internal NativeArgumentPassingStyle ArgumentPassingStyle
{
get
{
Expand All @@ -197,17 +199,17 @@ internal bool UseArgumentList
{
// This will default to the new behavior if it is set to anything other than Legacy
var preference = LanguagePrimitives.ConvertTo<NativeArgumentPassingStyle>(
Context.GetVariableValue(new VariablePath(SpecialVariables.NativeArgumentPassing), NativeArgumentPassingStyle.Standard));
return preference != NativeArgumentPassingStyle.Legacy;
Context.GetVariableValue(s_nativeArgumentPassingVarPath, NativeArgumentPassingStyle.Standard));
return preference;
}
catch
{
// The value is not convertable send back true
return true;
// The value is not convertable send back Legacy
return NativeArgumentPassingStyle.Legacy;
}
}

return false;
return NativeArgumentPassingStyle.Legacy;
}
}

Expand Down Expand Up @@ -314,7 +316,7 @@ private void AppendOneNativeArgument(ExecutionContext context, CommandParameterI
}
else
{
if (argArrayAst != null && UseArgumentList)
if (argArrayAst != null && ArgumentPassingStyle == NativeArgumentPassingStyle.Standard)
{
// We have a literal array, so take the extent, break it on spaces and add them to the argument list.
foreach (string element in argArrayAst.Extent.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries))
Expand All @@ -331,7 +333,7 @@ private void AppendOneNativeArgument(ExecutionContext context, CommandParameterI
}
}
}
else if (UseArgumentList && currentObj != null)
else if (ArgumentPassingStyle == NativeArgumentPassingStyle.Standard && currentObj != null)
{
// add empty strings to arglist, but not nulls
AddToArgumentList(parameter, arg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ internal string[] ArgumentList
}

/// <summary>
/// Gets a value indicating whether to use the new API for StartInfo.
/// Gets the value indicating what type of native argument binding to use.
/// </summary>
internal bool UseArgumentList
internal NativeArgumentPassingStyle ArgumentPassingStyle
{
get
{
return ((NativeCommandParameterBinder)DefaultParameterBinder).UseArgumentList;
return ((NativeCommandParameterBinder)DefaultParameterBinder).ArgumentPassingStyle;
}
}

Expand Down
153 changes: 117 additions & 36 deletions src/System.Management.Automation/engine/NativeCommandProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,26 @@ internal ProcessOutputObject(object data, MinishellStream stream)
/// </summary>
internal class NativeCommandProcessor : CommandProcessorBase
{
// This is the list of files which will trigger Legacy behavior if
// PSNativeCommandArgumentPassing is set to "Windows".
private static readonly IReadOnlySet<string> s_legacyFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".js",
".wsf",
".cmd",
".bat",
".vbs",
};

// The following native commands have non-standard behavior with regard to argument passing,
// so we use Legacy argument parsing for them when PSNativeCommandArgumentPassing is set to Windows.
private static readonly IReadOnlySet<string> s_legacyCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"cmd",
"cscript",
"wscript",
};

#region ctor/native command properties

/// <summary>
Expand Down Expand Up @@ -474,6 +494,7 @@ private void InitNativeProcess()
// on Windows desktops, see if there is a file association for this command. If so then we'll use that.
string executable = FindExecutable(startInfo.FileName);
bool notDone = true;
// check to see what mode we should be in for argument passing
if (!string.IsNullOrEmpty(executable))
{
isWindowsApplication = IsWindowsApplication(executable);
Expand All @@ -485,7 +506,16 @@ private void InitNativeProcess()

string oldArguments = startInfo.Arguments;
string oldFileName = startInfo.FileName;
startInfo.Arguments = "\"" + startInfo.FileName + "\" " + startInfo.Arguments;
// Check to see whether this executable should be using Legacy mode argument parsing
bool useSpecialArgumentPassing = UseSpecialArgumentPassing(oldFileName);
if (useSpecialArgumentPassing)
{
startInfo.Arguments = "\"" + oldFileName + "\" " + startInfo.Arguments;
}
else
{
startInfo.ArgumentList.Insert(0, oldFileName);
}
startInfo.FileName = executable;
try
{
Expand All @@ -495,7 +525,14 @@ private void InitNativeProcess()
catch (Win32Exception)
{
// Restore the old filename and arguments to try shell execute last...
startInfo.Arguments = oldArguments;
if (useSpecialArgumentPassing)
{
startInfo.Arguments = oldArguments;
}
else
{
startInfo.ArgumentList.RemoveAt(0);
}
startInfo.FileName = oldFileName;
}
}
Expand Down Expand Up @@ -1108,64 +1145,89 @@ private void ProcessOutputRecord(ProcessOutputObject outputValue)
}

/// <summary>
/// Gets the start info for process.
/// Get whether we should treat this executable with special handling and use the legacy passing style.
/// </summary>
/// <param name="redirectOutput"></param>
/// <param name="redirectError"></param>
/// <param name="redirectInput"></param>
/// <param name="soloCommand"></param>
/// <returns></returns>
private ProcessStartInfo GetProcessStartInfo(bool redirectOutput, bool redirectError, bool redirectInput, bool soloCommand)
/// <param name="filePath"></param>
private bool UseSpecialArgumentPassing(string filePath) =>
NativeParameterBinderController.ArgumentPassingStyle switch
{
NativeArgumentPassingStyle.Legacy => true,
NativeArgumentPassingStyle.Windows => ShouldUseLegacyPassingStyle(filePath),
_ => false
};

/// <summary>
/// Gets the ProcessStartInfo for process.
/// </summary>
/// <param name="redirectOutput">A boolean that indicates that, when true, output from the process is redirected to a stream, and otherwise is sent to stdout.</param>
/// <param name="redirectError">A boolean that indicates that, when true, error output from the process is redirected to a stream, and otherwise is sent to stderr.</param>
/// <param name="redirectInput">A boolean that indicates that, when true, input to the process is taken from a stream, and otherwise is taken from stdin.</param>
/// <param name="soloCommand">A boolean that indicates, when true, that the command to be executed is not part of a pipeline, and otherwise indicates that is is.</param>
/// <returns>A ProcessStartInfo object which is the base of the native invocation.</returns>
private ProcessStartInfo GetProcessStartInfo(
bool redirectOutput,
bool redirectError,
bool redirectInput,
bool soloCommand)
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = this.Path;
var startInfo = new ProcessStartInfo
{
FileName = this.Path
};

if (IsExecutable(this.Path))
if (!IsExecutable(this.Path))
{
startInfo.UseShellExecute = false;
if (redirectInput)
if (Platform.IsNanoServer || Platform.IsIoT)
{
startInfo.RedirectStandardInput = true;
// Shell doesn't exist on headless SKUs, so documents cannot be associated with an application.
// Therefore, we cannot run document in this case.
throw InterpreterError.NewInterpreterException(
this.Path,
typeof(RuntimeException),
this.Command.InvocationExtent,
"CantActivateDocumentInPowerShellCore",
ParserStrings.CantActivateDocumentInPowerShellCore,
this.Path);
}

if (redirectOutput)
// We only want to ShellExecute something that is standalone...
if (!soloCommand)
{
startInfo.RedirectStandardOutput = true;
startInfo.StandardOutputEncoding = Console.OutputEncoding;
throw InterpreterError.NewInterpreterException(
this.Path,
typeof(RuntimeException),
this.Command.InvocationExtent,
"CantActivateDocumentInPipeline",
ParserStrings.CantActivateDocumentInPipeline,
this.Path);
}

if (redirectError)
{
startInfo.RedirectStandardError = true;
startInfo.StandardErrorEncoding = Console.OutputEncoding;
}
startInfo.UseShellExecute = true;
}
else
{
if (Platform.IsNanoServer || Platform.IsIoT)
startInfo.UseShellExecute = false;
startInfo.RedirectStandardInput = redirectInput;

if (redirectOutput)
{
// Shell doesn't exist on headless SKUs, so documents cannot be associated with an application.
// Therefore, we cannot run document in this case.
throw InterpreterError.NewInterpreterException(this.Path, typeof(RuntimeException),
this.Command.InvocationExtent, "CantActivateDocumentInPowerShellCore", ParserStrings.CantActivateDocumentInPowerShellCore, this.Path);
startInfo.RedirectStandardOutput = true;
startInfo.StandardOutputEncoding = Console.OutputEncoding;
}

// We only want to ShellExecute something that is standalone...
if (!soloCommand)
if (redirectError)
{
throw InterpreterError.NewInterpreterException(this.Path, typeof(RuntimeException),
this.Command.InvocationExtent, "CantActivateDocumentInPipeline", ParserStrings.CantActivateDocumentInPipeline, this.Path);
startInfo.RedirectStandardError = true;
startInfo.StandardErrorEncoding = Console.OutputEncoding;
}

startInfo.UseShellExecute = true;
}

// For minishell value of -outoutFormat parameter depends on value of redirectOutput.
// So we delay the parameter binding. Do parameter binding for minishell now.
if (_isMiniShell)
{
MinishellParameterBinderController mpc = (MinishellParameterBinderController)NativeParameterBinderController;
mpc.BindParameters(arguments, redirectOutput, this.Command.Context.EngineHostInterface.Name);
mpc.BindParameters(arguments, startInfo.RedirectStandardOutput, this.Command.Context.EngineHostInterface.Name);
startInfo.CreateNoWindow = mpc.NonInteractive;
}

Expand All @@ -1174,7 +1236,8 @@ private ProcessStartInfo GetProcessStartInfo(bool redirectOutput, bool redirectE
// We provide the user a way to select the new behavior via a new preference variable
using (ParameterBinderBase.bindingTracer.TraceScope("BIND NAMED native application line args [{0}]", this.Path))
{
if (!NativeParameterBinderController.UseArgumentList)
// We need to check if we're using legacy argument passing or it's a special case.
if (UseSpecialArgumentPassing(startInfo.FileName))
{
using (ParameterBinderBase.bindingTracer.TraceScope("BIND argument [{0}]", NativeParameterBinderController.Arguments))
{
Expand Down Expand Up @@ -1203,9 +1266,27 @@ private ProcessStartInfo GetProcessStartInfo(bool redirectOutput, bool redirectE
context.EngineSessionState.GetNamespaceCurrentLocation(
context.ProviderNames.FileSystem).ProviderPath;
startInfo.WorkingDirectory = WildcardPattern.Unescape(rawPath);

return startInfo;
}

/// <summary>
/// Determine if we have a special file which will change the way native argument passing
/// is done on Windows. We use legacy behavior for cmd.exe, .bat, .cmd files.
/// </summary>
/// <param name="filePath">The file to use when checking how to pass arguments.</param>
/// <returns>A boolean indicating what passing style should be used.</returns>
private static bool ShouldUseLegacyPassingStyle(string filePath)
{
if (string.IsNullOrEmpty(filePath))
{
return false;
}

return s_legacyFileExtensions.Contains(IO.Path.GetExtension(filePath))
|| s_legacyCommands.Contains(IO.Path.GetFileNameWithoutExtension(filePath));
}

private static bool IsDownstreamOutDefault(Pipe downstreamPipe)
{
Diagnostics.Assert(downstreamPipe != null, "Caller makes sure the passed-in parameter is not null.");
Expand Down
2 changes: 1 addition & 1 deletion test/powershell/Host/ConsoleHost.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ Describe "ConsoleHost unit tests" -tags "Feature" {
$LASTEXITCODE | Should -Be 64
}

It "Empty space command should succeed" {
It "Empty space command should succeed on non-Windows" -skip:$IsWindows {
& $powershell -noprofile -c '' | Should -BeNullOrEmpty
$LASTEXITCODE | Should -Be 0
}
Expand Down
Loading