forked from dotnet/android
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for background processes + TPL, attempt #3
BackgroundProcessManager will probably go away, too clunky. ProcessRunner2 should provide a much better way to do it.
- Loading branch information
Showing
6 changed files
with
385 additions
and
1 deletion.
There are no files selected for viewing
68 changes: 68 additions & 0 deletions
68
tools/xadebug/Xamarin.Android.Utilities/BackgroundProcessManager.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
|
||
using Xamarin.Android.Tasks; | ||
|
||
namespace Xamarin.Android.Utilities; | ||
|
||
class BackgroundProcessManager : IDisposable | ||
{ | ||
readonly object runnersLock = new object (); | ||
readonly List<ToolRunner> runners; | ||
|
||
bool disposed; | ||
|
||
public BackgroundProcessManager () | ||
{ | ||
runners = new List<ToolRunner> (); | ||
Console.CancelKeyPress += ConsoleCanceled; | ||
} | ||
|
||
// TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources | ||
~BackgroundProcessManager () | ||
{ | ||
Dispose (disposing: false); | ||
} | ||
|
||
protected virtual void Dispose (bool disposing) | ||
{ | ||
if (!disposed) { | ||
if (disposing) { | ||
// TODO: dispose managed state (managed objects) | ||
} | ||
|
||
FinishAllTasks (); | ||
disposed = true; | ||
} | ||
} | ||
|
||
public void Dispose () | ||
{ | ||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | ||
Dispose (disposing: true); | ||
GC.SuppressFinalize (this); | ||
} | ||
|
||
public void Add (ToolRunner runner) | ||
{ | ||
// Task continuation = task.ContinueWith (TaskFailed, TaskContinuationOptions.OnlyOnFaulted); | ||
// tasks.Add (task); | ||
// tasks.Add (continuation); | ||
lock (runnersLock) { | ||
runners.Add (runner); | ||
} | ||
} | ||
|
||
void TaskFailed (Task task) | ||
{ | ||
} | ||
|
||
void FinishAllTasks () | ||
{ | ||
} | ||
|
||
void ConsoleCanceled (object? sender, ConsoleCancelEventArgs args) | ||
{ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using System; | ||
|
||
namespace Xamarin.Android.Utilities; | ||
|
||
interface ILogger | ||
{ | ||
void Message (string? message); | ||
void MessageLine (string? message = null); | ||
void Warning (string? message); | ||
void WarningLine (string? message = null); | ||
void Error (string? message); | ||
void ErrorLine (string? message = null); | ||
void Info (string? message); | ||
void InfoLine (string? message = null); | ||
void Debug (string? message); | ||
void DebugLine (string? message = null); | ||
void Status (string label, string text); | ||
void StatusLine (string label, string text); | ||
void Log (LogLevel level, string? message); | ||
void LogLine (LogLevel level, string? message, ConsoleColor color); | ||
void Log (LogLevel level, string? message, ConsoleColor color); | ||
} |
7 changes: 7 additions & 0 deletions
7
tools/xadebug/Xamarin.Android.Utilities/IProcessOutputLogger.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace Xamarin.Android.Utilities; | ||
|
||
interface IProcessOutputLogger | ||
{ | ||
void WriteStdout (string text); | ||
void WriteStderr (string text); | ||
} |
269 changes: 269 additions & 0 deletions
269
tools/xadebug/Xamarin.Android.Utilities/ProcessRunner.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,269 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace Xamarin.Android.Utilities; | ||
|
||
class ProcessRunner2 : IDisposable | ||
{ | ||
static readonly TimeSpan DefaultProcessTimeout = TimeSpan.FromMinutes (5); | ||
static readonly TimeSpan DefaultOutputTimeout = TimeSpan.FromSeconds (10); | ||
|
||
readonly object runLock = new object (); | ||
readonly IProcessOutputLogger? outputLogger; | ||
readonly ILogger? logger; | ||
readonly string command; | ||
|
||
bool disposed; | ||
bool running; | ||
List<string>? arguments; | ||
|
||
public bool CreateWindow { get; set; } | ||
public Dictionary<string, string> Environment { get; } = new Dictionary<string, string> (StringComparer.Ordinal); | ||
public string? FullCommandLine { get; private set; } | ||
public bool LogRunInfo { get; set; } = true; | ||
public bool LogStderr { get; set; } | ||
public bool LogStdout { get; set; } | ||
public bool MakeProcessGroupLeader { get; set; } | ||
public TimeSpan ProcessTimeout { get; set; } = DefaultProcessTimeout; | ||
public Encoding StandardOutputEncoding { get; set; } = Encoding.Default; | ||
public Encoding StandardErrorEncoding { get; set; } = Encoding.Default; | ||
public TimeSpan StandardOutputTimeout { get; set; } = DefaultOutputTimeout; | ||
public TimeSpan StandardErrorTimeout { get; set; } = DefaultOutputTimeout; | ||
public Action<ProcessStartInfo>? CustomizeStartInfo { get; set; } | ||
public bool UseShell { get; set; } | ||
public ProcessWindowStyle WindowStyle { get; set; } = ProcessWindowStyle.Hidden; | ||
public string? WorkingDirectory { get; set; } | ||
|
||
public ProcessRunner2 (string command, IProcessOutputLogger? outputLogger = null, ILogger? logger = null) | ||
{ | ||
if (String.IsNullOrEmpty (command)) { | ||
throw new ArgumentException ("must not be null or empty", nameof (command)); | ||
} | ||
|
||
this.command = command; | ||
this.outputLogger = outputLogger; | ||
this.logger = logger; | ||
} | ||
|
||
~ProcessRunner2 () | ||
{ | ||
Dispose (disposing: false); | ||
} | ||
|
||
public void Kill (bool gracefully = true) | ||
{} | ||
|
||
public void AddArgument (string arg) | ||
{ | ||
if (arguments == null) { | ||
arguments = new List<string> (); | ||
} | ||
|
||
arguments.Add (arg); | ||
} | ||
|
||
public void AddQuotedArgument (string arg) | ||
{ | ||
AddArgument ($"\"{arg}\""); | ||
} | ||
|
||
/// <summary> | ||
/// Run process synchronously on the calling thread | ||
/// </summary> | ||
public ProcessStatus Run () | ||
{ | ||
try { | ||
return DoRun (PrepareForRun ()); | ||
} finally { | ||
MarkNotRunning (); | ||
} | ||
} | ||
|
||
ProcessStatus DoRun (ProcessStartInfo psi) | ||
{ | ||
ManualResetEventSlim? stdout_done = null; | ||
ManualResetEventSlim? stderr_done = null; | ||
|
||
if (LogStderr) { | ||
stderr_done = new ManualResetEventSlim (false); | ||
} | ||
|
||
if (LogStdout) { | ||
stdout_done = new ManualResetEventSlim (false); | ||
} | ||
|
||
if (LogRunInfo) { | ||
logger?.DebugLine ($"Running: {FullCommandLine}"); | ||
} | ||
|
||
var process = new Process { | ||
StartInfo = psi | ||
}; | ||
|
||
try { | ||
process.Start (); | ||
} catch (System.ComponentModel.Win32Exception ex) { | ||
if (logger != null) { | ||
logger.ErrorLine ($"Process failed to start: {ex.Message}"); | ||
logger.DebugLine (ex.ToString ()); | ||
} | ||
|
||
return new ProcessStatus (); | ||
} | ||
|
||
if (psi.RedirectStandardError) { | ||
process.ErrorDataReceived += (object sender, DataReceivedEventArgs e) => { | ||
if (e.Data != null) { | ||
outputLogger!.WriteStderr (e.Data ?? String.Empty); | ||
} else { | ||
stderr_done!.Set (); | ||
} | ||
}; | ||
process.BeginErrorReadLine (); | ||
} | ||
|
||
if (psi.RedirectStandardOutput) { | ||
process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => { | ||
if (e.Data != null) { | ||
outputLogger!.WriteStdout (e.Data ?? String.Empty); | ||
} else { | ||
stdout_done!.Set (); | ||
} | ||
}; | ||
process.BeginOutputReadLine (); | ||
} | ||
|
||
int timeout = ProcessTimeout == TimeSpan.MaxValue ? -1 : (int)ProcessTimeout.TotalMilliseconds; | ||
bool exited = process.WaitForExit (timeout); | ||
if (!exited) { | ||
logger?.ErrorLine ($"Process '{FullCommandLine}' timed out after {ProcessTimeout}"); | ||
process.Kill (); | ||
} | ||
|
||
// See: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=netframework-4.7.2#System_Diagnostics_Process_WaitForExit) | ||
if (psi.RedirectStandardError || psi.RedirectStandardOutput) { | ||
process.WaitForExit (); | ||
} | ||
|
||
if (stderr_done != null) { | ||
stderr_done.Wait (StandardErrorTimeout); | ||
} | ||
|
||
if (stdout_done != null) { | ||
stdout_done.Wait (StandardOutputTimeout); | ||
} | ||
|
||
return new ProcessStatus (process.ExitCode, exited, process.ExitCode == 0); | ||
} | ||
|
||
/// <summary> | ||
/// Run process in a separate thread. The caller is responsible for awaiting on the returned <c>Task</c> | ||
/// </summary> | ||
public Task<ProcessStatus> RunAsync () | ||
{ | ||
return Task.Run (() => Run ()); | ||
} | ||
|
||
/// <summary> | ||
/// Run process in background, calling the <param ref="completionHandler"/> on completion. This is meant to be used for processes which are to run under control of our | ||
/// process but without us actively monitoring them or awaiting their completion. | ||
/// </summary> | ||
public void RunInBackground (Action<ProcessRunner2, ProcessStatus> completionHandler) | ||
{ | ||
ProcessStartInfo psi = PrepareForRun (); | ||
} | ||
|
||
protected virtual void Dispose (bool disposing) | ||
{ | ||
if (disposed) { | ||
return; | ||
} | ||
|
||
if (disposing) { | ||
// TODO: dispose managed state (managed objects) | ||
} | ||
|
||
// TODO: free unmanaged resources (unmanaged objects) and override finalizer | ||
// TODO: set large fields to null | ||
disposed = true; | ||
} | ||
|
||
public void Dispose () | ||
{ | ||
Dispose (disposing: true); | ||
GC.SuppressFinalize (this); | ||
} | ||
|
||
ProcessStartInfo PrepareForRun () | ||
{ | ||
MarkRunning (); | ||
|
||
var psi = new ProcessStartInfo (command) { | ||
CreateNoWindow = !CreateWindow, | ||
RedirectStandardError = LogStderr, | ||
RedirectStandardOutput = LogStdout, | ||
UseShellExecute = UseShell, | ||
WindowStyle = WindowStyle, | ||
}; | ||
|
||
if (arguments != null && arguments.Count > 0) { | ||
psi.Arguments = String.Join (" ", arguments); | ||
} | ||
|
||
if (Environment.Count > 0) { | ||
foreach (var kvp in Environment) { | ||
psi.Environment.Add (kvp.Key, kvp.Value); | ||
} | ||
} | ||
|
||
if (!String.IsNullOrEmpty (WorkingDirectory)) { | ||
psi.WorkingDirectory = WorkingDirectory; | ||
} | ||
|
||
if (psi.RedirectStandardError) { | ||
StandardErrorEncoding = StandardErrorEncoding; | ||
} | ||
|
||
if (psi.RedirectStandardOutput) { | ||
StandardOutputEncoding = StandardOutputEncoding; | ||
} | ||
|
||
if (CustomizeStartInfo != null) { | ||
CustomizeStartInfo (psi); | ||
} | ||
|
||
EnsureValidConfig (psi); | ||
|
||
FullCommandLine = $"{psi.FileName} {psi.Arguments}"; | ||
return psi; | ||
} | ||
|
||
void MarkRunning () | ||
{ | ||
lock (runLock) { | ||
if (running) { | ||
throw new InvalidOperationException ("Process already running"); | ||
} | ||
|
||
running = true; | ||
} | ||
} | ||
|
||
void MarkNotRunning () | ||
{ | ||
lock (runLock) { | ||
running = false; | ||
} | ||
} | ||
|
||
void EnsureValidConfig (ProcessStartInfo psi) | ||
{ | ||
if ((psi.RedirectStandardOutput || psi.RedirectStandardError) && outputLogger == null) { | ||
throw new InvalidOperationException ("Process output logger must be set in order to capture standard output streams"); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
namespace Xamarin.Android.Utilities; | ||
|
||
class ProcessStatus | ||
{ | ||
public int ExitCode { get; } = -1; | ||
public bool Exited { get; } = false; | ||
public bool Success { get; } = false; | ||
|
||
public ProcessStatus () | ||
{} | ||
|
||
public ProcessStatus (int exitCode, bool exited, bool success) | ||
{ | ||
ExitCode = exitCode; | ||
Exited = exited; | ||
Success = success; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters