#develop (short for SharpDevelop) is a free IDE for .NET programming languages.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

619 lines
20 KiB

// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ICSharpCode.Core;
using Microsoft.Win32.SafeHandles;
using ICSharpCode.SharpDevelop.Gui;
using ICSharpCode.SharpDevelop.Util;
using ICSharpCode.SharpDevelop.Workbench;
namespace ICSharpCode.SharpDevelop
{
[Flags]
public enum ProcessCreationFlags
{
None = 0,
/// <summary>
/// Creates a new console instead of inheriting the parent console.
/// </summary>
CreateNewConsole = 0x00000010,
/// <summary>
/// Launches a console application without a console window.
/// </summary>
CreateNoWindow = 0x08000000
}
public interface IProcessRunner : IDisposable
{
Task<int> RunInOutputPadAsync(IOutputCategory outputCategory, string program, params string[] arguments);
string WorkingDirectory { get; set; }
ProcessCreationFlags CreationFlags { get; set; }
IDictionary<string, string> EnvironmentVariables { get; }
bool RedirectStandardOutput { get; set; }
bool RedirectStandardError { get; set; }
bool RedirectStandardOutputAndErrorToSingleStream { get; set; }
void Start(string program, params string[] arguments);
void StartCommandLine(string commandLine);
void Kill();
Task WaitForExitAsync();
Stream StandardOutput { get; }
Stream StandardError { get; }
StreamReader OpenStandardOutputReader();
StreamReader OpenStandardErrorReader();
}
/// <summary>
/// Class for starting external processes.
/// Very similar to System.Diagnostics.Process, but supports binary stdout/stderr (not only text),
/// and allows using the same pipe for both stdout and stderr.
///
/// Also, implements an interface to support mocking in unit tests.
/// </summary>
public class ProcessRunner : IProcessRunner, IDisposable
{
public static Encoding OemEncoding {
get {
return NativeMethods.OemEncoding;
}
}
#region SafeProcessHandle
[SecurityCritical]
sealed class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// this private ctor is required for SafeHandle implementations
SafeProcessHandle() : base(true)
{
}
internal SafeProcessHandle(IntPtr handle) : base(true)
{
base.SetHandle(handle);
}
[SecurityCritical]
protected override bool ReleaseHandle()
{
return NativeMethods.CloseHandle(handle);
}
}
#endregion
#region Native structures
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
protected struct STARTUPINFO
{
public uint cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public SafePipeHandle hStdInput;
public SafePipeHandle hStdOutput;
public SafePipeHandle hStdError;
}
[StructLayout(LayoutKind.Sequential)]
protected struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
#endregion
#region Native methods
[DllImport("kernel32.dll", EntryPoint = "CreateProcess", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool NativeCreateProcess(
string lpApplicationName,
StringBuilder lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandles,
uint dwCreationFlags,
string lpEnvironment,
string lpCurrentDirectory,
[In] ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation
);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool TerminateProcess(SafeProcessHandle processHandle, int exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetExitCodeProcess(SafeProcessHandle processHandle, out int exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("shell32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern unsafe char** CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);
[DllImport("kernel32.dll")]
static extern IntPtr LocalFree(IntPtr hMem);
#endregion
#region CommandLine <-> Argument Array
/// <summary>
/// Decodes a command line into an array of arguments according to the CommandLineToArgvW rules.
/// </summary>
/// <remarks>
/// Command line parsing rules:
/// - 2n backslashes followed by a quotation mark produce n backslashes, and the quotation mark is considered to be the end of the argument.
/// - (2n) + 1 backslashes followed by a quotation mark again produce n backslashes followed by a quotation mark.
/// - n backslashes not followed by a quotation mark simply produce n backslashes.
/// </remarks>
public static unsafe string[] CommandLineToArgumentArray(string commandLine)
{
if (string.IsNullOrEmpty(commandLine))
return new string[0];
int numberOfArgs;
char** arr = CommandLineToArgvW(commandLine, out numberOfArgs);
if (arr == null)
throw new Win32Exception();
try {
string[] result = new string[numberOfArgs];
for (int i = 0; i < numberOfArgs; i++) {
result[i] = new string(arr[i]);
}
return result;
} finally {
// Free memory obtained by CommandLineToArgW.
LocalFree(new IntPtr(arr));
}
}
static readonly char[] charsNeedingQuoting = { ' ', '\t', '\n', '\v', '"' };
/// <summary>
/// Escapes a set of arguments according to the CommandLineToArgvW rules.
/// </summary>
/// <remarks>
/// Command line parsing rules:
/// - 2n backslashes followed by a quotation mark produce n backslashes, and the quotation mark is considered to be the end of the argument.
/// - (2n) + 1 backslashes followed by a quotation mark again produce n backslashes followed by a quotation mark.
/// - n backslashes not followed by a quotation mark simply produce n backslashes.
/// </remarks>
public static string ArgumentArrayToCommandLine(params string[] arguments)
{
if (arguments == null)
return null;
StringBuilder b = new StringBuilder();
for (int i = 0; i < arguments.Length; i++) {
if (i > 0)
b.Append(' ');
AppendArgument(b, arguments[i]);
}
return b.ToString();
}
static void AppendArgument(StringBuilder b, string arg)
{
if (arg.Length > 0 && arg.IndexOfAny(charsNeedingQuoting) < 0) {
b.Append(arg);
} else {
b.Append('"');
for (int j = 0; ; j++) {
int backslashCount = 0;
while (j < arg.Length && arg[j] == '\\') {
backslashCount++;
j++;
}
if (j == arg.Length) {
b.Append('\\', backslashCount * 2);
break;
} else if (arg[j] == '"') {
b.Append('\\', backslashCount * 2 + 1);
b.Append('"');
} else {
b.Append('\\', backslashCount);
b.Append(arg[j]);
}
}
b.Append('"');
}
}
#endregion
#region RunInOutputPad
public async Task<int> RunInOutputPadAsync(IOutputCategory outputCategory, string program, params string[] arguments)
{
RedirectStandardOutputAndErrorToSingleStream = true;
Start(program, arguments);
StringBuilder printedCommandLine = new StringBuilder();
if (WorkingDirectory != null) {
printedCommandLine.Append(WorkingDirectory);
printedCommandLine.Append("> ");
}
printedCommandLine.Append(CommandLine);
outputCategory.AppendLine(printedCommandLine.ToString());
using (TextReader reader = OpenStandardOutputReader()) {
await reader.CopyToAsync(new MessageViewCategoryTextWriter(outputCategory));
}
await WaitForExitAsync();
outputCategory.AppendLine(StringParser.Parse("${res:XML.MainMenu.ToolMenu.ExternalTools.ExitedWithCode} " + this.ExitCode));
return this.ExitCode;
}
#endregion
#region Start Info Properties
/// <summary>
/// Gets or sets the process's working directory.
/// </summary>
public string WorkingDirectory { get; set; }
ProcessCreationFlags creationFlags = ProcessCreationFlags.CreateNoWindow;
public ProcessCreationFlags CreationFlags {
get { return creationFlags; }
set { creationFlags = value; }
}
IDictionary<string, string> environmentVariables;
public IDictionary<string, string> EnvironmentVariables {
get {
if (environmentVariables == null) {
environmentVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (DictionaryEntry e in Environment.GetEnvironmentVariables()) {
environmentVariables.Add((string)e.Key, (string)e.Value);
}
}
return environmentVariables;
}
}
public string CommandLine { get; private set; }
public bool RedirectStandardOutput { get; set; }
public bool RedirectStandardError { get; set; }
/// <summary>
/// Gets whether to use a single stream for both stdout and stderr.
/// </summary>
public bool RedirectStandardOutputAndErrorToSingleStream { get; set; }
#endregion
#region Start
bool wasStarted;
SafeProcessHandle safeProcessHandle;
public void Start(string program, params string[] arguments)
{
StringBuilder commandLine = new StringBuilder();
AppendArgument(commandLine, program);
if (arguments != null) {
for (int i = 0; i < arguments.Length; i++) {
commandLine.Append(' ');
AppendArgument(commandLine, arguments[i]);
}
}
StartCommandLine(commandLine.ToString());
}
public void StartCommandLine(string commandLine)
{
lock (lockObj) {
if (wasStarted)
throw new InvalidOperationException();
DoStart(commandLine);
}
}
protected virtual void DoStart(string commandLine)
{
this.CommandLine = commandLine;
const uint STARTF_USESTDHANDLES = 0x00000100;
const int STD_INPUT_HANDLE = -10;
const int STD_OUTPUT_HANDLE = -11;
const int STD_ERROR_HANDLE = -12;
const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
STARTUPINFO startupInfo = new STARTUPINFO();
startupInfo.cb = (uint)Marshal.SizeOf(typeof(STARTUPINFO));
startupInfo.dwFlags = STARTF_USESTDHANDLES;
// Create pipes
startupInfo.hStdInput = new SafePipeHandle(GetStdHandle(STD_INPUT_HANDLE), ownsHandle: false);
if (RedirectStandardOutput || RedirectStandardOutputAndErrorToSingleStream) {
standardOutput = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable);
startupInfo.hStdOutput = standardOutput.ClientSafePipeHandle;
} else {
startupInfo.hStdOutput = new SafePipeHandle(GetStdHandle(STD_OUTPUT_HANDLE), ownsHandle: false);
}
if (RedirectStandardOutputAndErrorToSingleStream) {
standardError = standardOutput;
startupInfo.hStdError = standardError.ClientSafePipeHandle;
} else if (RedirectStandardError) {
standardError = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable);
startupInfo.hStdError = standardError.ClientSafePipeHandle;
} else {
startupInfo.hStdError = new SafePipeHandle(GetStdHandle(STD_ERROR_HANDLE), ownsHandle: false);
}
uint flags = (uint)this.CreationFlags;
string environmentBlock = null;
if (environmentVariables != null) {
environmentBlock = BuildEnvironmentBlock(environmentVariables);
flags |= CREATE_UNICODE_ENVIRONMENT;
}
PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION();
try {
CreateProcess(null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, true, flags, environmentBlock, WorkingDirectory, ref startupInfo, out processInfo);
wasStarted = true;
} finally {
if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != new IntPtr(-1)) {
safeProcessHandle = new SafeProcessHandle(processInfo.hProcess);
}
if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != new IntPtr(-1)) {
NativeMethods.CloseHandle(processInfo.hThread);
}
// Dispose the client side handles of the pipe.
// They got copied into the new process, we don't need our local copies anymore.
startupInfo.hStdInput.Dispose();
startupInfo.hStdOutput.Dispose();
startupInfo.hStdError.Dispose();
if (!wasStarted) {
// In case of error, dispose the server side of the pipes as well
if (standardOutput != null) {
standardOutput.Dispose();
standardOutput = null;
}
if (standardError != null) {
standardError.Dispose();
standardError = null;
}
}
}
//StartStreamCopyAfterProcessCreation();
}
static string BuildEnvironmentBlock(IEnumerable<KeyValuePair<string, string>> environment)
{
StringBuilder b = new StringBuilder();
foreach (var pair in environment.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase)) {
b.Append(pair.Key);
b.Append('=');
b.Append(pair.Value);
b.Append('\0');
}
b.Append('\0');
return b.ToString();
}
protected virtual void CreateProcess(
string lpApplicationName,
StringBuilder lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
string lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation)
{
if (!NativeCreateProcess(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags,
lpEnvironment, lpCurrentDirectory, ref lpStartupInfo, out lpProcessInformation)) {
throw new Win32Exception();
}
}
#endregion
public void Dispose()
{
if (safeProcessHandle != null)
safeProcessHandle.Dispose();
if (standardOutput != null)
standardOutput.Dispose();
if (standardError != null)
standardError.Dispose();
}
#region HasExited / ExitCode / Kill
public bool HasExited {
get { return WaitForExit(0); }
}
/// <summary>
/// Gets the process exit code.
/// </summary>
public int ExitCode {
get {
if (!WaitForExit(0))
throw new InvalidOperationException("Process has not yet exited");
return exitCode; // WaitForExit has the side effect of setting exitCode
}
}
/// <summary>
/// Sends the kill signal to the process.
/// Does not wait for the process to complete to exit after being killed.
/// </summary>
public void Kill()
{
if (!wasStarted)
throw new InvalidOperationException("Process was not started");
if (!TerminateProcess(safeProcessHandle, -1)) {
int err = Marshal.GetLastWin32Error();
// If TerminateProcess fails, maybe it's because the process has already exited.
if (!WaitForExit(0))
throw new Win32Exception(err);
}
}
#endregion
#region WaitForExit
sealed class ProcessWaitHandle : WaitHandle
{
public ProcessWaitHandle(SafeProcessHandle processHandle)
{
var currentProcess = new HandleRef(this, NativeMethods.GetCurrentProcess());
SafeWaitHandle safeWaitHandle;
if (!NativeMethods.DuplicateHandle(currentProcess, processHandle, currentProcess, out safeWaitHandle, 0, false, NativeMethods.DUPLICATE_SAME_ACCESS)) {
throw new Win32Exception();
}
base.SafeWaitHandle = safeWaitHandle;
}
}
bool hasExited;
int exitCode;
public void WaitForExit()
{
WaitForExit(Timeout.Infinite);
}
public bool WaitForExit(int millisecondsTimeout)
{
if (hasExited)
return true;
if (!wasStarted)
throw new InvalidOperationException("Process was not yet started");
if (safeProcessHandle.IsClosed)
throw new ObjectDisposedException("ProcessRunner");
using (var waitHandle = new ProcessWaitHandle(safeProcessHandle)) {
if (waitHandle.WaitOne(millisecondsTimeout, false)) {
if (!GetExitCodeProcess(safeProcessHandle, out exitCode))
throw new Win32Exception();
// Wait until the output is processed
// if (standardOutputTask != null)
// standardOutputTask.Wait();
// if (standardErrorTask != null)
// standardErrorTask.Wait();
hasExited = true;
}
}
return hasExited;
}
readonly object lockObj = new object();
TaskCompletionSource<object> waitForExitTCS;
ProcessWaitHandle waitForExitAsyncWaitHandle;
RegisteredWaitHandle waitForExitAsyncRegisteredWaitHandle;
/// <summary>
/// Asynchronously waits for the process to exit.
/// </summary>
public Task WaitForExitAsync()
{
if (hasExited)
return Task.FromResult(true);
if (!wasStarted)
throw new InvalidOperationException("Process was not yet started");
if (safeProcessHandle.IsClosed)
throw new ObjectDisposedException("ProcessRunner");
lock (lockObj) {
if (waitForExitTCS == null) {
waitForExitTCS = new TaskCompletionSource<object>();
waitForExitAsyncWaitHandle = new ProcessWaitHandle(safeProcessHandle);
waitForExitAsyncRegisteredWaitHandle = ThreadPool.RegisterWaitForSingleObject(waitForExitAsyncWaitHandle, WaitForExitAsyncCallback, null, -1, true);
}
return waitForExitTCS.Task;
}
}
void WaitForExitAsyncCallback(object context, bool wasSignaled)
{
waitForExitAsyncRegisteredWaitHandle.Unregister(null);
waitForExitAsyncRegisteredWaitHandle = null;
waitForExitAsyncWaitHandle.Close();
waitForExitAsyncWaitHandle = null;
// Wait until the output is processed
// if (standardOutputTask != null)
// await standardOutputTask;
// if (standardErrorTask != null)
// await standardErrorTask;
waitForExitTCS.SetResult(null);
}
#endregion
#region StandardOutput/StandardError
AnonymousPipeServerStream standardOutput;
AnonymousPipeServerStream standardError;
public Stream StandardOutput {
get {
if (standardOutput == null)
throw new InvalidOperationException(wasStarted ? "stdout was not redirected" : "Process not yet started");
return standardOutput;
}
}
public Stream StandardError {
get {
if (standardError == null)
throw new InvalidOperationException(wasStarted ? "stderr was not redirected" : "Process not yet started");
return standardError;
}
}
/// <summary>
/// Opens a text reader around the standard output.
/// </summary>
public StreamReader OpenStandardOutputReader()
{
return new StreamReader(this.StandardOutput, OemEncoding);
}
/// <summary>
/// Opens a text reader around the standard error.
/// </summary>
public StreamReader OpenStandardErrorReader()
{
return new StreamReader(this.StandardError, OemEncoding);
}
#endregion
}
}