xamarin-macios/tests/xharness/Microsoft.DotNet.XHarness.i.../Execution/ProcessManager.cs

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

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.DotNet.XHarness.iOS.Shared.Logging;
using Microsoft.DotNet.XHarness.iOS.Shared.Utilities;
using Microsoft.DotNet.XHarness.iOS.Shared.Execution.Mlaunch;
namespace Microsoft.DotNet.XHarness.iOS.Shared.Execution {
public class ProcessManager : IProcessManager {
static readonly Lazy<string> autoDetectedXcodeRoot = new Lazy<string>(DetectXcodePath, LazyThreadSafetyMode.PublicationOnly);
readonly string xcodeRoot;
public string XcodeRoot => xcodeRoot ?? autoDetectedXcodeRoot.Value;
public string MlaunchPath { get; }
Version xcode_version;
public Version XcodeVersion {
get {
if (xcode_version == null) {
var doc = new XmlDocument ();
doc.Load (Path.Combine (XcodeRoot, "Contents", "version.plist"));
xcode_version = Version.Parse (doc.SelectSingleNode ("//key[text() = 'CFBundleShortVersionString']/following-sibling::string").InnerText);
}
return xcode_version;
}
}
public ProcessManager (string xcodeRoot = null, string mlaunchPath = "/Library/Frameworks/Xamarin.iOS.framework/Versions/Current/bin/mlaunch")
{
this.xcodeRoot = xcodeRoot;
MlaunchPath = mlaunchPath;
}
public async Task<ProcessExecutionResult> ExecuteCommandAsync (string filename,
IList<string> args,
ILog log,
TimeSpan timeout,
Dictionary<string, string> environmentVariables = null,
CancellationToken? cancellationToken = null)
{
using var p = new Process ();
p.StartInfo.FileName = filename ?? throw new ArgumentNullException (nameof (filename));
p.StartInfo.Arguments = StringUtils.FormatArguments (args);
return await RunAsync (p, log, timeout, environmentVariables, cancellationToken);
}
public async Task<ProcessExecutionResult> ExecuteCommandAsync (MlaunchArguments args,
ILog log,
TimeSpan timeout,
Dictionary<string, string> environmentVariables = null,
CancellationToken? cancellationToken = null)
{
using var p = new Process ();
return await RunAsync (p, args, log, timeout, environmentVariables, cancellationToken);
}
public Task<ProcessExecutionResult> ExecuteXcodeCommandAsync (string executable, IList<string> args, ILog log, TimeSpan timeout)
{
string filename = Path.Combine (XcodeRoot, "Contents", "Developer", "usr", "bin", executable);
return ExecuteCommandAsync (filename, args, log, timeout: timeout);
}
[DllImport ("/usr/lib/libc.dylib")]
internal static extern int kill (int pid, int sig);
public Task<ProcessExecutionResult> RunAsync (Process process,
ILog log,
TimeSpan? timeout = null,
Dictionary<string, string> environment_variables = null,
CancellationToken? cancellationToken = null,
bool? diagnostics = null)
{
return RunAsync (process, log, log, log, timeout, environment_variables, cancellationToken, diagnostics);
}
public Task<ProcessExecutionResult> RunAsync (Process process,
MlaunchArguments args,
ILog log,
TimeSpan? timeout = null,
Dictionary<string, string> environmentVariables = null,
CancellationToken? cancellationToken = null,
bool? diagnostics = null)
{
if (!args.Any (a => a is SdkRootArgument))
args.Prepend (new SdkRootArgument (XcodeRoot));
process.StartInfo.FileName = MlaunchPath;
process.StartInfo.Arguments = args.AsCommandLine ();
return RunAsync (process, log, timeout, environmentVariables, cancellationToken, diagnostics);
}
public Task<ProcessExecutionResult> RunAsync (Process process,
ILog log,
ILog stdout,
ILog stderr,
TimeSpan? timeout = null,
Dictionary<string, string> environmentVariables = null,
CancellationToken? cancellationToken = null,
bool? diagnostics = null)
{
return RunAsyncInternal (process, log, stdout, stderr, timeout, environmentVariables, cancellationToken, diagnostics);
}
public Task KillTreeAsync (Process process, ILog log, bool? diagnostics = true)
{
return KillTreeAsyncInternal (process.Id, log, diagnostics);
}
public Task KillTreeAsync (int pid, ILog log, bool? diagnostics = true)
{
return KillTreeAsyncInternal (pid, log, diagnostics);
}
static async Task<ProcessExecutionResult> RunAsyncInternal (Process process,
ILog log,
ILog stdout,
ILog stderr,
TimeSpan? timeout = null,
Dictionary<string, string> environmentVariables = null,
CancellationToken? cancellationToken = null,
bool? diagnostics = null)
{
var stdout_completion = new TaskCompletionSource<bool> ();
var stderr_completion = new TaskCompletionSource<bool> ();
var rv = new ProcessExecutionResult ();
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardOutput = true;
// Make cute emojiis show up as cute emojiis in the output instead of ugly text symbols!
process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
process.StartInfo.UseShellExecute = false;
if (environmentVariables != null) {
foreach (var kvp in environmentVariables)
process.StartInfo.EnvironmentVariables [kvp.Key] = kvp.Value;
}
process.OutputDataReceived += (sender, e) => {
if (e.Data != null) {
lock (stdout) {
stdout.WriteLine (e.Data);
stdout.Flush ();
}
} else {
stdout_completion.TrySetResult (true);
}
};
process.ErrorDataReceived += (sender, e) => {
if (e.Data != null) {
lock (stderr) {
stderr.WriteLine (e.Data);
stderr.Flush ();
}
} else {
stderr_completion.TrySetResult (true);
}
};
var sb = new StringBuilder ();
if (process.StartInfo.EnvironmentVariables != null) {
var currentEnvironment = Environment.GetEnvironmentVariables ().Cast<System.Collections.DictionaryEntry> ().ToDictionary ((v) => (string) v.Key, (v) => (string) v.Value, StringComparer.Ordinal);
var processEnvironment = process.StartInfo.EnvironmentVariables.Cast<System.Collections.DictionaryEntry> ().ToDictionary ((v) => (string) v.Key, (v) => (string) v.Value, StringComparer.Ordinal);
var allKeys = currentEnvironment.Keys.Union (processEnvironment.Keys).Distinct ();
foreach (var key in allKeys) {
string a = null, b = null;
currentEnvironment.TryGetValue (key, out a);
processEnvironment.TryGetValue (key, out b);
if (a != b)
sb.Append ($"{key}={StringUtils.Quote (b)} ");
}
}
sb.Append ($"{StringUtils.Quote (process.StartInfo.FileName)} {process.StartInfo.Arguments}");
log.WriteLine (sb);
process.Start ();
var pid = process.Id;
process.BeginErrorReadLine ();
process.BeginOutputReadLine ();
cancellationToken?.Register (() => {
[xharness] Handle Process.HasExited throwing exceptions. (#5239) Apparently Process.HasExited may throw exceptions, so handle those gracefully (by ignoring them completely): Unhandled Exception: System.AggregateException: One or more errors occurred. (No process is associated with this object.) ---> System.InvalidOperationException: No process is associated with this object. at System.Diagnostics.Process.EnsureState (System.Diagnostics.Process+State state) [0x00018] in <d012d9eca6d14975a47488863de2f4c6>:0 at System.Diagnostics.Process.get_HasExited () [0x0000b] in <d012d9eca6d14975a47488863de2f4c6>:0 at (wrapper remoting-invoke-with-check) System.Diagnostics.Process.get_HasExited() at xharness.Process_Extensions+<>c__DisplayClass2_0.<RunAsync>b__2 () [0x00001] in <285dbdcf9e034cd496a3fef953fac640>:0 at System.Threading.CancellationToken.ActionToActionObjShunt (System.Object obj) [0x00000] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationCallbackInfo.ExecutionContextCallback (System.Object obj) [0x00007] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00071] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x0002b] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationCallbackInfo.ExecuteCallback () [0x00024] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork (System.Threading.CancellationCallbackCoreWorkArguments args) [0x00042] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers (System.Boolean throwOnFirstException) [0x000c0] in <96207d0baa204f48a53ad6be05f5ecba>:0 --- End of inner exception stack trace --- at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers (System.Boolean throwOnFirstException) [0x00132] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationTokenSource.NotifyCancellation (System.Boolean throwOnFirstException) [0x0005f] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationTokenSource.Cancel (System.Boolean throwOnFirstException) [0x00006] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationTokenSource.Cancel () [0x00000] in <96207d0baa204f48a53ad6be05f5ecba>:0 at xharness.AppRunner+<>c__DisplayClass73_0.<RunAsync>b__0 (System.Object v) [0x00029] in <285dbdcf9e034cd496a3fef953fac640>:0 at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context (System.Object state) [0x00007] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00071] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem () [0x00021] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.ThreadPoolWorkQueue.Dispatch () [0x00074] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback () [0x00000] in <96207d0baa204f48a53ad6be05f5ecba>:0 ---> (Inner Exception #0) System.InvalidOperationException: No process is associated with this object. at System.Diagnostics.Process.EnsureState (System.Diagnostics.Process+State state) [0x00018] in <d012d9eca6d14975a47488863de2f4c6>:0 at System.Diagnostics.Process.get_HasExited () [0x0000b] in <d012d9eca6d14975a47488863de2f4c6>:0 at (wrapper remoting-invoke-with-check) System.Diagnostics.Process.get_HasExited() at xharness.Process_Extensions+<>c__DisplayClass2_0.<RunAsync>b__2 () [0x00001] in <285dbdcf9e034cd496a3fef953fac640>:0 at System.Threading.CancellationToken.ActionToActionObjShunt (System.Object obj) [0x00000] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationCallbackInfo.ExecutionContextCallback (System.Object obj) [0x00007] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00071] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x0002b] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationCallbackInfo.ExecuteCallback () [0x00024] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork (System.Threading.CancellationCallbackCoreWorkArguments args) [0x00042] in <96207d0baa204f48a53ad6be05f5ecba>:0 at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers (System.Boolean throwOnFirstException) [0x000c0] in <96207d0baa204f48a53ad6be05f5ecba>:0 <---
2018-12-07 17:07:21 +03:00
var hasExited = false;
try {
hasExited = process.HasExited;
} catch {
// Process.HasExited can sometimes throw exceptions, so
// just ignore those and to be safe treat it as the
// process didn't exit (the safe option being to not leave
// processes behind).
}
if (!hasExited) {
stderr.WriteLine ($"Execution of {pid} was cancelled.");
kill (pid, 9);
}
});
if (timeout.HasValue) {
if (!await WaitForExitAsync (process, timeout.Value)) {
await KillTreeAsyncInternal (process.Id, log, diagnostics ?? true);
rv.TimedOut = true;
lock (stderr)
log.WriteLine ($"{pid} Execution timed out after {timeout.Value.TotalSeconds} seconds and the process was killed.");
}
}
await WaitForExitAsync (process);
Task.WaitAll (new Task [] { stderr_completion.Task, stdout_completion.Task }, TimeSpan.FromSeconds (1));
try {
rv.ExitCode = process.ExitCode;
} catch (Exception e) {
rv.ExitCode = 12345678;
log.WriteLine ($"Failed to get ExitCode: {e}");
}
return rv;
}
static async Task KillTreeAsyncInternal (int pid, ILog log, bool? diagnostics = true)
{
var pids = GetChildrenPS (log, pid);
if (diagnostics == true) {
log.WriteLine ($"Pids to kill: {string.Join (", ", pids.Select ((v) => v.ToString ()).ToArray ())}");
using (var ps = new Process ()) {
log.WriteLine ("Writing process list:");
ps.StartInfo.FileName = "ps";
ps.StartInfo.Arguments = "-A -o pid,ruser,ppid,pgid,%cpu=%CPU,%mem=%MEM,flags=FLAGS,lstart,rss,vsz,tty,state,time,command";
await RunAsyncInternal (ps, log, log, log, TimeSpan.FromSeconds (5), diagnostics: false);
}
foreach (var diagnose_pid in pids) {
var template = Path.GetTempFileName ();
try {
var commands = new StringBuilder ();
using (var dbg = new Process ()) {
commands.AppendLine ($"process attach --pid {diagnose_pid}");
commands.AppendLine ("thread list");
commands.AppendLine ("thread backtrace all");
commands.AppendLine ("detach");
commands.AppendLine ("quit");
dbg.StartInfo.FileName = "/usr/bin/lldb";
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
dbg.StartInfo.Arguments = StringUtils.FormatArguments ("--source", template);
File.WriteAllText (template, commands.ToString ());
log.WriteLine ($"Printing backtrace for pid={pid}");
await RunAsyncInternal (dbg, log, log, log, TimeSpan.FromSeconds (30), diagnostics: false);
}
} finally {
try {
File.Delete (template);
} catch {
// Don't care
}
}
}
}
// Send SIGABRT since that produces a crash report
// lldb may fail to attach to system processes, but crash reports will still be produced with potentially helpful stack traces.
for (int i = 0; i < pids.Count; i++)
kill (pids [i], 6);
// send kill -9 anyway as a last resort
for (int i = 0; i < pids.Count; i++)
kill (pids [i], 9);
}
static async Task<bool> WaitForExitAsync (Process process, TimeSpan? timeout = null)
{
if (process.HasExited)
return true;
var tcs = new TaskCompletionSource<bool> ();
void ProcessExited (object sender, EventArgs ea)
{
process.Exited -= ProcessExited;
tcs.TrySetResult (true);
}
process.Exited += ProcessExited;
process.EnableRaisingEvents = true;
// Check if process exited again, in case it exited after we checked
// the last time, but before we attached the event handler.
if (process.HasExited) {
process.Exited -= ProcessExited;
tcs.TrySetResult (true);
return true;
}
if (timeout.HasValue) {
return await tcs.Task.TimeoutAfter (timeout.Value);
} else {
await tcs.Task;
return true;
}
}
static List<int> GetChildrenPS (ILog log, int pid)
{
var list = new List<int> ();
using (Process ps = new Process ()) {
ps.StartInfo.FileName = "ps";
ps.StartInfo.Arguments = "-eo ppid,pid";
ps.StartInfo.UseShellExecute = false;
ps.StartInfo.RedirectStandardOutput = true;
ps.Start ();
string stdout = ps.StandardOutput.ReadToEnd ();
if (!ps.WaitForExit (1000)) {
log.WriteLine ("ps didn't finish in a reasonable amount of time (1 second).");
return list;
}
if (ps.ExitCode != 0)
return list;
stdout = stdout.Trim ();
if (string.IsNullOrEmpty (stdout))
return list;
var dict = new Dictionary<int, List<int>> ();
foreach (string line in stdout.Split (new char [] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)) {
var l = line.Trim ();
var space = l.IndexOf (' ');
if (space <= 0)
continue;
var parent = l.Substring (0, space);
var process = l.Substring (space + 1);
if (int.TryParse (parent, out var parent_id) && int.TryParse (process, out var process_id)) {
if (!dict.TryGetValue (parent_id, out var children))
dict [parent_id] = children = new List<int> ();
children.Add (process_id);
}
}
var queue = new Queue<int> ();
queue.Enqueue (pid);
do {
var parent_id = queue.Dequeue ();
list.Add (parent_id);
if (dict.TryGetValue (parent_id, out var children)) {
foreach (var child in children)
queue.Enqueue (child);
}
} while (queue.Count > 0);
}
return list;
}
static string DetectXcodePath ()
{
using var process = new Process ();
process.StartInfo.FileName = "xcode-select";
process.StartInfo.Arguments = "-p";
var log = new MemoryLog ();
var stdout = new MemoryLog () { Timestamp = false };
var stderr = new ConsoleLog ();
var timeout = TimeSpan.FromSeconds (30);
var result = RunAsyncInternal (process, log, stdout, stderr, timeout).GetAwaiter ().GetResult ();
if (!result.Succeeded)
throw new Exception ("Failed to detect Xcode path from xcode-select!");
// Something like /Applications/Xcode114.app/Contents/Developers
var xcodeRoot = stdout.ToString ().Trim ();
if (string.IsNullOrEmpty (xcodeRoot))
throw new Exception ("Failed to detect Xcode path from xcode-select!");
// We need /Applications/Xcode114.app only
return Path.GetDirectoryName(Path.GetDirectoryName(xcodeRoot));
}
}
}