Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Unix: make UseShellExecute execute executables #33052

Merged
merged 19 commits into from
Dec 5, 2018
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal static partial class Interop
{
internal static partial class Sys
{
internal static unsafe void ForkAndExecProcess(
internal static unsafe int ForkAndExecProcess(
string filename, string[] argv, string[] envp, string cwd,
bool redirectStdin, bool redirectStdout, bool redirectStderr,
bool setUser, uint userId, uint groupId,
Expand All @@ -31,17 +31,11 @@ internal static partial class Sys
out lpChildPid, out stdinFd, out stdoutFd, out stderrFd);
if (result != 0)
{
// Normally we'd simply make this method return the result of the native
// call and allow the caller to use GetLastWin32Error. However, we need
// to free the native arrays after calling the function, and doing so
// stomps on the runtime's captured last error. So we need to access the
// error here, and without SetLastWin32Error available, we can't propagate
// the error to the caller via the normal GetLastWin32Error mechanism. We could
// return 0 on success or the GetLastWin32Error value on failure, but that's
// technically ambiguous, in the case of a failure with a 0 errno. Simplest
// solution then is just to throw here the same exception the Process caller
// would have. This can be revisited if we ever have another call site.
throw new Win32Exception();
return Marshal.GetLastWin32Error();
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
return 0;
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ protected virtual void Dispose(bool disposing)
/// </summary>
protected string TestDirectory { get; }

/// <summary>Creates a test directory that is associated with the call site.</summary>
protected string CreateTestDirectory(int? index = null, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0)
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
string path = GetTestFilePath(index, memberName, lineNumber);
Directory.CreateDirectory(path);
return path;
}

/// <summary>Gets a test file full path that is associated with the call site.</summary>
/// <param name="index">An optional index value to use as a suffix on the file name. Typically a loop index.</param>
/// <param name="memberName">The member name of the function calling this method.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
<Compile Include="..\..\Common\src\System\PasteArguments.cs">
<Link>Common\System\PasteArguments.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Windows\Interop.Errors.cs">
<Link>Common\Interop\Windows\Interop.Errors.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup Condition=" '$(TargetsWindows)' == 'true' and '$(TargetGroup)' != 'uap'">
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.EnumProcessModules.cs">
Expand Down Expand Up @@ -223,9 +226,6 @@
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CreatePipe_SafeFileHandle.cs">
<Link>Common\Interop\Windows\kernel32\Interop.CreatePipe.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Windows\Interop.Errors.cs">
<Link>Common\Interop\Windows\Interop.Errors.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.ThreadOptions.cs">
<Link>Common\Interop\Windows\kernel32\Interop.ThreadOptions.cs</Link>
</Compile>
Expand Down Expand Up @@ -346,6 +346,9 @@
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.WaitPid.cs">
<Link>Common\Interop\Unix\Interop.WaitPid.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.Access.cs">
<Link>Common\Interop\Unix\System.Native\Interop.Access.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup Condition=" '$(TargetsLinux)' == 'true'">
<Compile Include="System\Diagnostics\Process.Linux.cs" />
Expand Down
213 changes: 169 additions & 44 deletions src/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,15 +291,66 @@ private bool StartCore(ProcessStartInfo startInfo)
}
}

int childPid, stdinFd, stdoutFd, stderrFd;
int stdinFd = -1, stdoutFd = -1, stderrFd = -1;
string[] envp = CreateEnvp(startInfo);
string cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null;

bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName);
uint userId = 0;
uint groupId = 0;
if (setCredentials)
{
(userId, groupId) = GetUserAndGroupIds(startInfo);
}

if (startInfo.UseShellExecute)
{
string verb = startInfo.Verb;
if (string.IsNullOrEmpty(verb))
{
// Default to 'open'.
verb = "open";
}
else
{
// Verbs are case-insensitive.
verb = verb.ToLowerInvariant();
tmds marked this conversation as resolved.
Show resolved Hide resolved
}

if (verb != "open")
{
throw new Win32Exception(Interop.Errors.ERROR_NO_ASSOCIATION);
}
tmds marked this conversation as resolved.
Show resolved Hide resolved

tmds marked this conversation as resolved.
Show resolved Hide resolved
// On Windows, UseShellExecute of executables and scripts causes those files to be executed.
// To achieve this on Unix, we check if the file is executable (x-bit).
// Some files may have the x-bit set even when they are not executable. This happens for example
// when a Windows filesystem is mounted on Linux. To handle that, treat it as a regular file
// when exec returns ENOEXEC (file format cannot be executed).
bool isExecuting = false;
filename = ResolveExecutableForShellExecute(startInfo.FileName);
tmds marked this conversation as resolved.
Show resolved Hide resolved
if (filename != null)
{
argv = ParseArgv(startInfo);

isExecuting = ForkAndExecProcess(filename, argv, envp, cwd,
startInfo.RedirectStandardInput, startInfo.RedirectStandardOutput, startInfo.RedirectStandardError,
setCredentials, userId, groupId,
out stdinFd, out stdoutFd, out stderrFd,
throwOnNoExec: false); // return false instead of throwing on ENOEXEC
}

// use default program to open file/url
filename = GetPathToOpenFile();
argv = ParseArgv(startInfo, filename);
if (!isExecuting)
{
filename = GetPathToOpenFile();
argv = ParseArgv(startInfo, filename);

ForkAndExecProcess(filename, argv, envp, cwd,
tmds marked this conversation as resolved.
Show resolved Hide resolved
startInfo.RedirectStandardInput, startInfo.RedirectStandardOutput, startInfo.RedirectStandardError,
setCredentials, userId, groupId,
out stdinFd, out stdoutFd, out stderrFd);
}
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
Expand All @@ -309,50 +360,11 @@ private bool StartCore(ProcessStartInfo startInfo)
{
throw new Win32Exception(SR.DirectoryNotValidAsInput);
}
}

if (string.IsNullOrEmpty(filename))
{
throw new Win32Exception(Interop.Error.ENOENT.Info().RawErrno);
}

bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName);
uint userId = 0;
uint groupId = 0;
if (setCredentials)
{
(userId, groupId) = GetUserAndGroupIds(startInfo);
}

// Lock to avoid races with OnSigChild
// By using a ReaderWriterLock we allow multiple processes to start concurrently.
s_processStartLock.EnterReadLock();
try
{
// Invoke the shim fork/execve routine. It will create pipes for all requested
// redirects, fork a child process, map the pipe ends onto the appropriate stdin/stdout/stderr
// descriptors, and execve to execute the requested process. The shim implementation
// is used to fork/execve as executing managed code in a forked process is not safe (only
// the calling thread will transfer, thread IDs aren't stable across the fork, etc.)
Interop.Sys.ForkAndExecProcess(
filename, argv, envp, cwd,
ForkAndExecProcess(filename, argv, envp, cwd,
tmds marked this conversation as resolved.
Show resolved Hide resolved
startInfo.RedirectStandardInput, startInfo.RedirectStandardOutput, startInfo.RedirectStandardError,
setCredentials, userId, groupId,
out childPid,
setCredentials, userId, groupId,
out stdinFd, out stdoutFd, out stderrFd);

// Ensure we'll reap this process.
// note: SetProcessId will set this if we don't set it first.
_waitStateHolder = new ProcessWaitState.Holder(childPid, isNewChild: true);

// Store the child's information into this Process object.
Debug.Assert(childPid >= 0);
SetProcessId(childPid);
SetProcessHandle(new SafeProcessHandle(childPid));
}
finally
{
s_processStartLock.ExitReadLock();
}

// Configure the parent's ends of the redirection streams.
Expand Down Expand Up @@ -381,6 +393,67 @@ private bool StartCore(ProcessStartInfo startInfo)
return true;
}

private bool ForkAndExecProcess(
string filename, string[] argv, string[] envp, string cwd,
bool redirectStdin, bool redirectStdout, bool redirectStderr,
bool setCredentials, uint userId, uint groupId,
out int stdinFd, out int stdoutFd, out int stderrFd,
bool throwOnNoExec = true)
{
if (string.IsNullOrEmpty(filename))
{
throw new Win32Exception(Interop.Error.ENOENT.Info().RawErrno);
}

// Lock to avoid races with OnSigChild
// By using a ReaderWriterLock we allow multiple processes to start concurrently.
s_processStartLock.EnterReadLock();
try
{
int childPid;

// Invoke the shim fork/execve routine. It will create pipes for all requested
// redirects, fork a child process, map the pipe ends onto the appropriate stdin/stdout/stderr
// descriptors, and execve to execute the requested process. The shim implementation
// is used to fork/execve as executing managed code in a forked process is not safe (only
// the calling thread will transfer, thread IDs aren't stable across the fork, etc.)
int errno = Interop.Sys.ForkAndExecProcess(
filename, argv, envp, cwd,
redirectStdin, redirectStdout, redirectStderr,
setCredentials, userId, groupId,
out childPid,
out stdinFd, out stdoutFd, out stderrFd);

if (errno == 0)
{
// Ensure we'll reap this process.
// note: SetProcessId will set this if we don't set it first.
_waitStateHolder = new ProcessWaitState.Holder(childPid, isNewChild: true);

// Store the child's information into this Process object.
Debug.Assert(childPid >= 0);
SetProcessId(childPid);
SetProcessHandle(new SafeProcessHandle(childPid));

return true;
}
else
{
if ((throwOnNoExec == false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: !throwOnNoExec

&& (Interop.Sys.ConvertErrorPlatformToPal(errno) == Interop.Error.ENOEXEC))
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
return false;
}

throw new Win32Exception(errno);
}
}
finally
{
s_processStartLock.ExitReadLock();
}
}

// -----------------------------
// ---- PAL layer ends here ----
// -----------------------------
Expand Down Expand Up @@ -439,6 +512,58 @@ private static string[] CreateEnvp(ProcessStartInfo psi)
return envp;
}

private static string ResolveExecutableForShellExecute(string filename)
{
// Determine if filename points to an executable file.
// filename may be an absolute path, a relative path or a uri.

string resolvedFilename = null;
// filename is an absolute path
if (Path.IsPathRooted(filename))
{
if (File.Exists(filename))
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
resolvedFilename = filename;
}
}
// filename is a uri
else if (Uri.TryCreate(filename, UriKind.Absolute, out Uri uri))
{
if (uri.IsFile && uri.Host == "" && File.Exists(uri.LocalPath))
{
resolvedFilename = uri.LocalPath;
}
}
else
{
string relativeFile = Path.Combine(Directory.GetCurrentDirectory(), filename);
// filename is a relative path in the working directory
if (File.Exists(relativeFile))
{
resolvedFilename = relativeFile;
}
// find filename on PATH
else
{
resolvedFilename = FindProgramInPath(filename);
}
}

if (resolvedFilename == null)
{
return null;
}

if (Interop.Sys.Access(resolvedFilename, Interop.Sys.AccessMode.X_OK) == 0)
{
return resolvedFilename;
}
else
{
return null;
}
}

/// <summary>Resolves a path to the filename passed to ProcessStartInfo. </summary>
/// <param name="filename">The filename.</param>
/// <returns>The resolved path. It can return null in case of URLs.</returns>
Expand Down