xamarin-macios/tools/common/Execution.cs

210 строки
8.0 KiB
C#
Исходник Обычный вид История

/*
* Copyright 2014 Xamarin Inc. All rights reserved.
* Copyright 2019, 2020 Microsoft Corp. All rights reserved.
*
* Authors:
* Rolf Bjarne Kvinge <rolf@xamarin.com>
*
*/
using System;
Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. (#7177) * Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. mono changed how quotes should be escaped when passed to System.Diagnostic.Process, so we need to change accordingly. The main difference is that single quotes don't have to be escaped anymore. This solves problems like this: System.ComponentModel.Win32Exception : ApplicationName='nuget', CommandLine='restore '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable/CellCustomTable.sln' -Verbosity detailed -SolutionDir '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable'', CurrentDirectory='/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories', Native error= Cannot find the specified file at System.Diagnostics.Process.StartWithCreateProcess (System.Diagnostics.ProcessStartInfo startInfo) [0x0029f] in /Users/builder/jenkins/workspace/build-package-osx-mono/2019-08/external/bockbuild/builds/mono-x64/mcs/class/System/System.Diagnostics/Process.cs:778 ref: https://github.com/mono/mono/pull/15047 * Rework process arguments to pass arrays/lists around instead of quoted strings. And then only convert to a string at the very end when we create the Process instance. In the future there will be a ProcessStartInfo.ArgumentList property we can use to give the original array/list of arguments directly to the BCL so that we can avoid quoting at all. These changes gets us almost all the way there already (except that the ArgumentList property isn't available quite yet). We also have to bump to target framework version v4.7.2 from v4.5 in several places because of 'Array.Empty<T> ()' which is now used in more places. * Parse linker flags from LinkWith attributes. * [sampletester] Bump to v4.7.2 for Array.Empty<T> (). * Fix typo. * Rename GetVerbosity -> AddVerbosity. * Remove unnecessary string interpolation. * Remove unused variable. * [mtouch] Simplify code a bit. * Use implicitly typed arrays.
2019-10-14 17:18:46 +03:00
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Xamarin.Utils {
public class Execution {
public string FileName;
public IList<string> Arguments;
public IDictionary<string, string> Environment;
public string WorkingDirectory;
public TimeSpan? Timeout;
public CancellationToken? CancellationToken;
Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. (#7177) * Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. mono changed how quotes should be escaped when passed to System.Diagnostic.Process, so we need to change accordingly. The main difference is that single quotes don't have to be escaped anymore. This solves problems like this: System.ComponentModel.Win32Exception : ApplicationName='nuget', CommandLine='restore '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable/CellCustomTable.sln' -Verbosity detailed -SolutionDir '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable'', CurrentDirectory='/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories', Native error= Cannot find the specified file at System.Diagnostics.Process.StartWithCreateProcess (System.Diagnostics.ProcessStartInfo startInfo) [0x0029f] in /Users/builder/jenkins/workspace/build-package-osx-mono/2019-08/external/bockbuild/builds/mono-x64/mcs/class/System/System.Diagnostics/Process.cs:778 ref: https://github.com/mono/mono/pull/15047 * Rework process arguments to pass arrays/lists around instead of quoted strings. And then only convert to a string at the very end when we create the Process instance. In the future there will be a ProcessStartInfo.ArgumentList property we can use to give the original array/list of arguments directly to the BCL so that we can avoid quoting at all. These changes gets us almost all the way there already (except that the ArgumentList property isn't available quite yet). We also have to bump to target framework version v4.7.2 from v4.5 in several places because of 'Array.Empty<T> ()' which is now used in more places. * Parse linker flags from LinkWith attributes. * [sampletester] Bump to v4.7.2 for Array.Empty<T> (). * Fix typo. * Rename GetVerbosity -> AddVerbosity. * Remove unnecessary string interpolation. * Remove unused variable. * [mtouch] Simplify code a bit. * Use implicitly typed arrays.
2019-10-14 17:18:46 +03:00
public TextWriter Log;
public int ExitCode { get; private set; }
public bool TimedOut { get; private set; }
public TextWriter StandardOutput { get; private set; }
public TextWriter StandardError { get; private set; }
static Thread StartOutputThread (TaskCompletionSource<Execution> tcs, object lockobj, StreamReader reader, TextWriter writer, string thread_name)
Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. (#7177) * Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. mono changed how quotes should be escaped when passed to System.Diagnostic.Process, so we need to change accordingly. The main difference is that single quotes don't have to be escaped anymore. This solves problems like this: System.ComponentModel.Win32Exception : ApplicationName='nuget', CommandLine='restore '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable/CellCustomTable.sln' -Verbosity detailed -SolutionDir '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable'', CurrentDirectory='/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories', Native error= Cannot find the specified file at System.Diagnostics.Process.StartWithCreateProcess (System.Diagnostics.ProcessStartInfo startInfo) [0x0029f] in /Users/builder/jenkins/workspace/build-package-osx-mono/2019-08/external/bockbuild/builds/mono-x64/mcs/class/System/System.Diagnostics/Process.cs:778 ref: https://github.com/mono/mono/pull/15047 * Rework process arguments to pass arrays/lists around instead of quoted strings. And then only convert to a string at the very end when we create the Process instance. In the future there will be a ProcessStartInfo.ArgumentList property we can use to give the original array/list of arguments directly to the BCL so that we can avoid quoting at all. These changes gets us almost all the way there already (except that the ArgumentList property isn't available quite yet). We also have to bump to target framework version v4.7.2 from v4.5 in several places because of 'Array.Empty<T> ()' which is now used in more places. * Parse linker flags from LinkWith attributes. * [sampletester] Bump to v4.7.2 for Array.Empty<T> (). * Fix typo. * Rename GetVerbosity -> AddVerbosity. * Remove unnecessary string interpolation. * Remove unused variable. * [mtouch] Simplify code a bit. * Use implicitly typed arrays.
2019-10-14 17:18:46 +03:00
{
var thread = new Thread (() => {
try {
string line;
while ((line = reader.ReadLine ()) != null) {
lock (lockobj)
writer.WriteLine (line);
}
} catch (Exception e) {
tcs.TrySetException (e);
} finally {
// The Process instance doesn't dispose these streams, which means we need to do it,
// otherwise we can run out of file descriptors while waiting for the GC to kick in.
// Ref: https://bugzilla.xamarin.com/show_bug.cgi?id=43462
reader.Dispose ();
}
}) {
IsBackground = true,
Name = thread_name,
};
thread.Start ();
return thread;
}
public Task<Execution> RunAsync ()
{
var tcs = new TaskCompletionSource<Execution> ();
var lockobj = new object ();
try {
var p = new Process ();
p.StartInfo.FileName = FileName;
p.StartInfo.Arguments = StringUtils.FormatArguments (Arguments);
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardInput = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
if (!string.IsNullOrEmpty (WorkingDirectory))
p.StartInfo.WorkingDirectory = WorkingDirectory;
// mtouch/mmp writes UTF8 data outside of the ASCII range, so we need to make sure
// we read it in the same format. This also means we can't use the events to get
// stdout/stderr, because mono's Process class parses those using Encoding.Default.
p.StartInfo.StandardOutputEncoding = Encoding.UTF8;
p.StartInfo.StandardErrorEncoding = Encoding.UTF8;
if (Environment != null) {
foreach (var kvp in Environment) {
if (kvp.Value == null) {
p.StartInfo.EnvironmentVariables.Remove (kvp.Key);
} else {
p.StartInfo.EnvironmentVariables [kvp.Key] = kvp.Value;
}
}
}
StandardOutput ??= new StringWriter ();
StandardError ??= new StringWriter ();
var thread = new Thread (() => {
try {
if (Log != null) {
if (!string.IsNullOrEmpty (p.StartInfo.WorkingDirectory))
Log.Write ($"cd {StringUtils.Quote (p.StartInfo.WorkingDirectory)} && ");
Log.WriteLine ("{0} {1}", p.StartInfo.FileName, p.StartInfo.Arguments);
}
p.Start ();
var pid = p.Id;
var stdoutThread = StartOutputThread (tcs, lockobj, p.StandardOutput, StandardOutput, $"StandardOutput reader for {p.StartInfo.FileName} (PID: {pid})");
var stderrThread = StartOutputThread (tcs, lockobj, p.StandardError, StandardError, $"StandardError reader for {p.StartInfo.FileName} (PID: {pid})");
CancellationToken?.Register (() => {
// Don't call tcs.TrySetCanceled, that won't return an Execution result to the caller.
try {
p.Kill ();
} catch (Exception ex) {
// The process could be disposed already. Just ignore any exceptions here.
Log?.WriteLine ($"Failed to cancel and kill PID {pid}: {ex.Message}");
}
});
if (Timeout.HasValue) {
if (!p.WaitForExit ((int) Timeout.Value.TotalMilliseconds)) {
Log?.WriteLine ($"Command '{p.StartInfo.FileName} {p.StartInfo.Arguments}' didn't finish in {Timeout.Value.TotalMilliseconds} ms, and will be killed.");
TimedOut = true;
try {
p.Kill ();
} catch (Exception ex) {
// According to the documentation, there can be exceptions here we can't prepare for, so just ignore them.
Log?.WriteLine ($"Failed to kill PID {pid}: {ex.Message}");
}
}
}
// Always call this WaitForExit overload to be make sure the stdout/stderr buffers have been flushed,
// even if we've called the WaitForExit (int) overload
p.WaitForExit ();
ExitCode = p.ExitCode;
stdoutThread.Join (TimeSpan.FromSeconds (1));
stderrThread.Join (TimeSpan.FromSeconds (1));
tcs.TrySetResult (this);
} catch (Exception e) {
tcs.TrySetException (e);
} finally {
p.Dispose ();
}
}) {
IsBackground = true,
Name = $"Thread waiting for {p.StartInfo.FileName} to finish",
};
thread.Start ();
} catch (Exception e) {
tcs.TrySetException (e);
}
return tcs.Task;
}
public static Task<Execution> RunWithCallbacksAsync (string filename, IList<string> arguments, Dictionary<string, string> environment = null, Action<string> standardOutput = null, Action<string> standardError = null, TextWriter log = null, string workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)
{
CallbackWriter outputCallback = null;
CallbackWriter errorCallback = null;
if (standardOutput != null)
outputCallback = new CallbackWriter { Callback = standardOutput };
if (standardOutput == standardError)
errorCallback = outputCallback;
else if (standardError != null)
errorCallback = new CallbackWriter { Callback = standardError };
return RunAsync (filename, arguments, environment, outputCallback, errorCallback, log, workingDirectory, timeout, cancellationToken);
}
public static Task<Execution> RunAsync (string filename, IList<string> arguments, Dictionary<string, string> environment = null, TextWriter standardOutput = null, TextWriter standardError = null, TextWriter log = null, string workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)
{
return new Execution {
FileName = filename,
Arguments = arguments,
Environment = environment,
StandardOutput = standardOutput,
StandardError = standardError,
WorkingDirectory = workingDirectory,
CancellationToken = cancellationToken,
Timeout = timeout,
Log = log,
}.RunAsync ();
}
public static Task<Execution> RunAsync (string filename, IList<string> arguments, Dictionary<string, string> environment = null, bool mergeOutput = false, string workingDirectory = null, TextWriter log = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)
{
var standardOutput = new StringWriter ();
var standardError = mergeOutput ? standardOutput : new StringWriter ();
return RunAsync (filename, arguments, environment, standardOutput, standardError, log, workingDirectory, timeout, cancellationToken);
}
public static Task<Execution> RunWithStringBuildersAsync (string filename, IList<string> arguments, Dictionary<string, string> environment = null, StringBuilder standardOutput = null, StringBuilder standardError = null, TextWriter log = null, string workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)
{
var stdout = standardOutput == null ? null : new StringWriter (standardOutput);
var stderr = standardError == null ? null : (standardOutput == standardError ? stdout : new StringWriter (standardError));
return RunAsync (filename, arguments, environment, stdout, stderr, log, workingDirectory, timeout, cancellationToken);
}
class CallbackWriter : TextWriter {
public Action<string> Callback;
public override void WriteLine (string value)
{
Callback (value);
}
public override Encoding Encoding => Encoding.UTF8;
}
}
}