From 61ce7241dd0860e372a8dcfddacd18121187da22 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Thu, 19 Jan 2023 22:31:02 +0100 Subject: [PATCH] New tool runner, based on the new process runner AdbRunner implemented with the new tool runner --- .../Xamarin.Android.Debug/AndroidDevice.cs | 8 +- .../Xamarin.Android.Debug/DebugSession.cs | 3 +- .../DeviceLibrariesCopier.cs | 4 +- .../LddDeviceLibrariesCopier.cs | 3 +- .../NoLddDeviceLibrariesCopier.cs | 4 +- .../Xamarin.Android.Utilities/AdbRunner.cs | 230 ++++++++++++++++++ .../IProcessOutputLogger.cs | 8 +- .../ProcessRunner.cs | 152 ++++++++---- .../ProcessStatus.cs | 14 +- .../Xamarin.Android.Utilities/ToolRunner.cs | 126 ++++++++++ .../XamarinLoggingHelper.cs | 32 ++- 11 files changed, 514 insertions(+), 70 deletions(-) create mode 100644 tools/xadebug/Xamarin.Android.Utilities/AdbRunner.cs create mode 100644 tools/xadebug/Xamarin.Android.Utilities/ToolRunner.cs diff --git a/tools/xadebug/Xamarin.Android.Debug/AndroidDevice.cs b/tools/xadebug/Xamarin.Android.Debug/AndroidDevice.cs index 162b4293caf..3a74e0a5602 100644 --- a/tools/xadebug/Xamarin.Android.Debug/AndroidDevice.cs +++ b/tools/xadebug/Xamarin.Android.Debug/AndroidDevice.cs @@ -47,7 +47,7 @@ class AndroidDevice string outputDir; XamarinLoggingHelper log; - AdbRunner adb; + AdbRunner2 adb; AndroidNdk ndk; public int ApiLevel => apiLevel; @@ -62,9 +62,9 @@ class AndroidDevice public string LldbBaseDir => appLldbBaseDir ?? String.Empty; public string AppDataDir => appDataDir ?? String.Empty; public string? DeviceLddPath => deviceLdd; - public AdbRunner AdbRunner => adb; + public AdbRunner2 AdbRunner => adb; - public AndroidDevice (XamarinLoggingHelper log, AndroidNdk ndk, string outputDir, string adbPath, string packageName, List supportedAbis, string? adbTargetDevice = null) + public AndroidDevice (XamarinLoggingHelper log, IProcessOutputLogger processLogger, AndroidNdk ndk, string outputDir, string adbPath, string packageName, List supportedAbis, string? adbTargetDevice = null) { this.adbPath = adbPath; this.log = log; @@ -73,7 +73,7 @@ public AndroidDevice (XamarinLoggingHelper log, AndroidNdk ndk, string outputDir this.ndk = ndk; this.outputDir = outputDir; - adb = new AdbRunner (log, adbPath, adbTargetDevice); + adb = new AdbRunner2 (log, processLogger, adbPath, adbTargetDevice); } // TODO: implement manual error checking on API 21, since `adb` won't ever return any error code other than 0 - we need to look at the output of any command to determine diff --git a/tools/xadebug/Xamarin.Android.Debug/DebugSession.cs b/tools/xadebug/Xamarin.Android.Debug/DebugSession.cs index 5d5832a048a..d817d5ac851 100644 --- a/tools/xadebug/Xamarin.Android.Debug/DebugSession.cs +++ b/tools/xadebug/Xamarin.Android.Debug/DebugSession.cs @@ -50,7 +50,8 @@ public bool Prepare () ndk = new AndroidNdk (log, parsedOptions.NdkDirPath!, supportedAbis); device = new AndroidDevice ( - log, + log, // general logger + log, // process output logger ndk, workDirectory, parsedOptions.AdbPath, diff --git a/tools/xadebug/Xamarin.Android.Debug/DeviceLibrariesCopier.cs b/tools/xadebug/Xamarin.Android.Debug/DeviceLibrariesCopier.cs index 50ec4bb65ac..73982bfa919 100644 --- a/tools/xadebug/Xamarin.Android.Debug/DeviceLibrariesCopier.cs +++ b/tools/xadebug/Xamarin.Android.Debug/DeviceLibrariesCopier.cs @@ -10,10 +10,10 @@ abstract class DeviceLibraryCopier protected XamarinLoggingHelper Log { get; } protected bool AppIs64Bit { get; } protected string LocalDestinationDir { get; } - protected AdbRunner Adb { get; } + protected AdbRunner2 Adb { get; } protected AndroidDevice Device { get; } - protected DeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device) + protected DeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner2 adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device) { Log = log; Adb = adb; diff --git a/tools/xadebug/Xamarin.Android.Debug/LddDeviceLibrariesCopier.cs b/tools/xadebug/Xamarin.Android.Debug/LddDeviceLibrariesCopier.cs index 2a2fc1348a7..166b611ecd1 100644 --- a/tools/xadebug/Xamarin.Android.Debug/LddDeviceLibrariesCopier.cs +++ b/tools/xadebug/Xamarin.Android.Debug/LddDeviceLibrariesCopier.cs @@ -2,13 +2,12 @@ using System.Collections.Generic; using Xamarin.Android.Utilities; -using Xamarin.Android.Tasks; namespace Xamarin.Android.Debug; class LddDeviceLibraryCopier : DeviceLibraryCopier { - public LddDeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device) + public LddDeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner2 adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device) : base (log, adb, appIs64Bit, localDestinationDir, device) {} diff --git a/tools/xadebug/Xamarin.Android.Debug/NoLddDeviceLibrariesCopier.cs b/tools/xadebug/Xamarin.Android.Debug/NoLddDeviceLibrariesCopier.cs index 31f50651b7d..646891d819a 100644 --- a/tools/xadebug/Xamarin.Android.Debug/NoLddDeviceLibrariesCopier.cs +++ b/tools/xadebug/Xamarin.Android.Debug/NoLddDeviceLibrariesCopier.cs @@ -34,7 +34,7 @@ class NoLddDeviceLibraryCopier : DeviceLibraryCopier "/system/vendor/@LIB@/mediadrm", }; - public NoLddDeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device) + public NoLddDeviceLibraryCopier (XamarinLoggingHelper log, AdbRunner2 adb, bool appIs64Bit, string localDestinationDir, AndroidDevice device) : base (log, adb, appIs64Bit, localDestinationDir, device) {} @@ -61,7 +61,7 @@ public override bool Copy (out string? zygotePath) void AddSharedLibraries (List sharedLibraries, string deviceDirPath, HashSet permittedPaths) { - AdbRunner.OutputLineFilter filterOutErrors = (bool isStdError, string line) => { + AdbRunner2.OutputLineFilter filterOutErrors = (bool isStdError, string line) => { if (!isStdError) { return false; // don't suppress any lines on stdout } diff --git a/tools/xadebug/Xamarin.Android.Utilities/AdbRunner.cs b/tools/xadebug/Xamarin.Android.Utilities/AdbRunner.cs new file mode 100644 index 00000000000..c1c142c5736 --- /dev/null +++ b/tools/xadebug/Xamarin.Android.Utilities/AdbRunner.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Xamarin.Android.Utilities; + +class AdbRunner2 : ToolRunner2 +{ + public delegate bool OutputLineFilter (bool isStdErr, string line); + + sealed class CaptureOutputState + { + public OutputLineFilter? LineFilter; + public CaptureProcessOutputLogger? Logger; + } + + sealed class CaptureProcessOutputLogger : IProcessOutputLogger + { + IProcessOutputLogger? wrappedLogger; + OutputLineFilter? lineFilter; + List lines; + string? stderrPrefix; + string? stdoutPrefix; + + public List Lines => lines; + + public IProcessOutputLogger? WrappedLogger => wrappedLogger; + + public string? StdoutPrefix { + get => stdoutPrefix ?? wrappedLogger?.StdoutPrefix ?? String.Empty; + set => stdoutPrefix = value; + } + + public string? StderrPrefix { + get => stderrPrefix ?? wrappedLogger?.StderrPrefix ?? String.Empty; + set => stderrPrefix = value; + } + + public CaptureProcessOutputLogger (IProcessOutputLogger? wrappedLogger, OutputLineFilter? lineFilter = null) + { + this.wrappedLogger = wrappedLogger; + this.lineFilter = lineFilter; + + lines = new List (); + } + + public void WriteStderr (string text, bool writeLine = true) + { + if (LineFiltered (text, isStdError: true)) { + return; + } + + wrappedLogger?.WriteStderr (text, writeLine); + } + + public void WriteStdout (string text, bool writeLine = true) + { + if (LineFiltered (text, isStdError: false)) { + return; + } + + lines.Add (text); + } + + bool LineFiltered (string text, bool isStdError) + { + if (lineFilter == null) { + return false; + } + + return lineFilter (isStdError, text); + } + } + + string[]? initialParams; + + public AdbRunner2 (ILogger logger, IProcessOutputLogger processOutputLogger, string adbPath, string? deviceSerial = null) + : base (adbPath, logger, processOutputLogger) + { + if (!String.IsNullOrEmpty (deviceSerial)) { + initialParams = new string[] { "-s", deviceSerial }; + } + } + + public async Task Pull (string remotePath, string localPath) + { + var runner = CreateAdbRunner (); + runner.AddArgument ("pull"); + runner.AddArgument (remotePath); + runner.AddArgument (localPath); + + return await RunAdbAsync (runner); + } + + public async Task Push (string localPath, string remotePath) + { + var runner = CreateAdbRunner (); + runner.AddArgument ("push"); + runner.AddArgument (localPath); + runner.AddArgument (remotePath); + + return await RunAdbAsync (runner); + } + + public async Task Install (string apkPath, bool apkIsDebuggable = false, bool replaceExisting = true, bool noStreaming = true) + { + var runner = CreateAdbRunner (); + runner.AddArgument ("install"); + + if (replaceExisting) { + runner.AddArgument ("-r"); + } + + if (apkIsDebuggable) { + runner.AddArgument ("-d"); // Allow version code downgrade + } + + if (noStreaming) { + runner.AddArgument ("--no-streaming"); + } + + runner.AddQuotedArgument (apkPath); + + return await RunAdbAsync (runner); + } + + public async Task<(bool success, string output)> GetAppDataDirectory (string packageName) + { + return await RunAs (packageName, "/system/bin/sh", "-c", "pwd"); + } + + + public async Task<(bool success, string output)> CreateDirectoryAs (string packageName, string directoryPath) + { + return await RunAs (packageName, "mkdir", "-p", directoryPath); + } + + public async Task<(bool success, string output)> GetPropertyValue (string propertyName) + { + var runner = CreateAdbRunner (); + return await Shell ("getprop", propertyName); + } + + public async Task<(bool success, string output)> RunAs (string packageName, string command, params string[] args) + { + if (String.IsNullOrEmpty (packageName)) { + throw new ArgumentException ("must not be null or empty", nameof (packageName)); + } + + var shellArgs = new List { + packageName, + command, + }; + + if (args != null && args.Length > 0) { + shellArgs.AddRange (args); + } + + return await Shell ("run-as", (IEnumerable)shellArgs, lineFilter: null); + } + + public async Task<(bool success, string output)> Shell (string command, List args, OutputLineFilter? lineFilter = null) + { + return await Shell (command, (IEnumerable)args, lineFilter); + } + + public async Task<(bool success, string output)> Shell (string command, params string[] args) + { + return await Shell (command, (IEnumerable)args, lineFilter: null); + } + + public async Task<(bool success, string output)> Shell (OutputLineFilter lineFilter, string command, params string[] args) + { + return await Shell (command, (IEnumerable)args, lineFilter); + } + + async Task<(bool success, string output)> Shell (string command, IEnumerable? args, OutputLineFilter? lineFilter) + { + if (String.IsNullOrEmpty (command)) { + throw new ArgumentException ("must not be null or empty", nameof (command)); + } + + var captureState = new CaptureOutputState { + LineFilter = lineFilter, + }; + + var runner = CreateAdbRunner (captureState); + + runner.AddArgument ("shell"); + runner.AddArgument (command); + runner.AddArguments (args); + + return await CaptureAdbOutput (runner, captureState); + } + + async Task RunAdbAsync (ProcessRunner2 runner) + { + ProcessStatus status = await runner.RunAsync (); + return status.Success; + } + + async Task<(bool success, string output)> CaptureAdbOutput (ProcessRunner2 runner, CaptureOutputState captureState) + { + ProcessStatus status = await runner.RunAsync (); + + string output = captureState.Logger != null ? String.Join (Environment.NewLine, captureState.Logger.Lines) : String.Empty; + return (status.Success, output); + } + + ProcessRunner2 CreateAdbRunner (CaptureOutputState? state = null) => InitProcessRunner (state, initialParams); + + protected override ProcessRunner2 CreateProcessRunner (IProcessOutputLogger consoleProcessLogger, object? state, params string?[]? initialParams) + { + IProcessOutputLogger outputLogger; + + if (state is CaptureOutputState captureState) { + captureState.Logger = new CaptureProcessOutputLogger (consoleProcessLogger, captureState.LineFilter); + outputLogger = captureState.Logger; + } else { + outputLogger = consoleProcessLogger; + } + + outputLogger.StderrPrefix = "adb> "; + ProcessRunner2 ret = base.CreateProcessRunner (outputLogger, initialParams); + + // Let's make sure all the messages we get are in English, since we need to parse some of them to detect problems + ret.Environment["LANG"] = "C"; + return ret; + } +} diff --git a/tools/xadebug/Xamarin.Android.Utilities/IProcessOutputLogger.cs b/tools/xadebug/Xamarin.Android.Utilities/IProcessOutputLogger.cs index e90db418684..3ffb3eda893 100644 --- a/tools/xadebug/Xamarin.Android.Utilities/IProcessOutputLogger.cs +++ b/tools/xadebug/Xamarin.Android.Utilities/IProcessOutputLogger.cs @@ -2,6 +2,10 @@ namespace Xamarin.Android.Utilities; interface IProcessOutputLogger { - void WriteStdout (string text); - void WriteStderr (string text); + IProcessOutputLogger? WrappedLogger { get; } + string? StdoutPrefix { get; set; } + string? StderrPrefix { get; set; } + + void WriteStdout (string text, bool writeLine = true); + void WriteStderr (string text, bool writeLine = true); } diff --git a/tools/xadebug/Xamarin.Android.Utilities/ProcessRunner.cs b/tools/xadebug/Xamarin.Android.Utilities/ProcessRunner.cs index 1fd733f984e..512e9683a26 100644 --- a/tools/xadebug/Xamarin.Android.Utilities/ProcessRunner.cs +++ b/tools/xadebug/Xamarin.Android.Utilities/ProcessRunner.cs @@ -20,10 +20,12 @@ class ProcessRunner2 : IDisposable bool disposed; bool running; List? arguments; + Task? backgroundProcess; public bool CreateWindow { get; set; } public Dictionary Environment { get; } = new Dictionary (StringComparer.Ordinal); public string? FullCommandLine { get; private set; } + public bool LeaveRunning { get; set; } public bool LogRunInfo { get; set; } = true; public bool LogStderr { get; set; } public bool LogStdout { get; set; } @@ -57,6 +59,21 @@ public ProcessRunner2 (string command, IProcessOutputLogger? outputLogger = null public void Kill (bool gracefully = true) {} + public void AddArguments (IEnumerable? args) + { + if (args == null) { + return; + } + + foreach (string? a in args) { + if (String.IsNullOrEmpty (a)) { + continue; + } + + AddArgument (a); + } + } + public void AddArgument (string arg) { if (arguments == null) { @@ -76,22 +93,66 @@ public void AddQuotedArgument (string arg) /// public ProcessStatus Run () { + return Run (ProcessTimeout); + } + + public ProcessStatus Run (TimeSpan processTimeout) + { + MarkRunning (); + try { - return DoRun (PrepareForRun ()); + return DoRun (processTimeout); } finally { MarkNotRunning (); } } - ProcessStatus DoRun (ProcessStartInfo psi) + ProcessStatus DoRun (TimeSpan processTimeout) { - ManualResetEventSlim? stdout_done = null; - ManualResetEventSlim? stderr_done = null; + 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}"; + + ManualResetEventSlim? stderr_done = null; if (LogStderr) { stderr_done = new ManualResetEventSlim (false); } + ManualResetEventSlim? stdout_done = null; if (LogStdout) { stdout_done = new ManualResetEventSlim (false); } @@ -137,7 +198,7 @@ ProcessStatus DoRun (ProcessStartInfo psi) process.BeginOutputReadLine (); } - int timeout = ProcessTimeout == TimeSpan.MaxValue ? -1 : (int)ProcessTimeout.TotalMilliseconds; + int timeout = processTimeout == TimeSpan.MaxValue ? -1 : (int)processTimeout.TotalMilliseconds; bool exited = process.WaitForExit (timeout); if (!exited) { logger?.ErrorLine ($"Process '{FullCommandLine}' timed out after {ProcessTimeout}"); @@ -170,11 +231,42 @@ public Task RunAsync () /// /// Run process in background, calling the 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. + /// process but without us actively monitoring them or awaiting their completion. By default the process will run without a timeout (the + /// property is ignored). Timeout can be changed by setting the parameter to anything other than TimeSpan.MaxValue /// - public void RunInBackground (Action completionHandler) + public void RunInBackground (Action completionHandler, TimeSpan? processTimeout = null) { - ProcessStartInfo psi = PrepareForRun (); + backgroundProcess = new Task ( + () => Run (processTimeout ?? TimeSpan.MaxValue), + TaskCreationOptions.LongRunning + ).ContinueWith ( + (Task task) => { + ProcessStatus status; + if (task.IsFaulted) { + status = new ProcessStatus (task.Exception!); + } else { + status = new ProcessStatus (); + } + completionHandler (this, status); + return status; + }, TaskContinuationOptions.OnlyOnFaulted + ).ContinueWith ( + (Task task) => { + completionHandler (this, task.Result); + return task.Result; + }, + TaskContinuationOptions.OnlyOnRanToCompletion + ).ContinueWith ( + (Task task) => { + var status = new ProcessStatus (); + completionHandler (this, status); + return status; + }, + TaskContinuationOptions.OnlyOnCanceled + ); + + backgroundProcess.ConfigureAwait (false); + backgroundProcess.Start (); } protected virtual void Dispose (bool disposing) @@ -198,50 +290,6 @@ public void Dispose () 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) { diff --git a/tools/xadebug/Xamarin.Android.Utilities/ProcessStatus.cs b/tools/xadebug/Xamarin.Android.Utilities/ProcessStatus.cs index 8d6e352bf3d..9ef291305a9 100644 --- a/tools/xadebug/Xamarin.Android.Utilities/ProcessStatus.cs +++ b/tools/xadebug/Xamarin.Android.Utilities/ProcessStatus.cs @@ -1,14 +1,22 @@ +using System; + namespace Xamarin.Android.Utilities; class ProcessStatus { - public int ExitCode { get; } = -1; - public bool Exited { get; } = false; - public bool Success { get; } = false; + public int ExitCode { get; } = -1; + public bool Exited { get; } = false; + public bool Success { get; } = false; + public Exception? Exception { get; } public ProcessStatus () {} + public ProcessStatus (Exception ex) + { + Exception = ex; + } + public ProcessStatus (int exitCode, bool exited, bool success) { ExitCode = exitCode; diff --git a/tools/xadebug/Xamarin.Android.Utilities/ToolRunner.cs b/tools/xadebug/Xamarin.Android.Utilities/ToolRunner.cs new file mode 100644 index 00000000000..90561688101 --- /dev/null +++ b/tools/xadebug/Xamarin.Android.Utilities/ToolRunner.cs @@ -0,0 +1,126 @@ +using System; + +namespace Xamarin.Android.Utilities; + +abstract class ToolRunner2 : IDisposable +{ + sealed class ConsoleProcessLogger : IProcessOutputLogger + { + bool echoStdout; + bool echoStderr; + IProcessOutputLogger wrappedLogger; + ILogger logger; + + public IProcessOutputLogger? WrappedLogger => wrappedLogger; + public string? StdoutPrefix { get; set; } + public string? StderrPrefix { get; set; } = "stderr> "; + + public ConsoleProcessLogger (ILogger logger, IProcessOutputLogger wrappedLogger, bool echoStdout, bool echoStderr) + { + this.logger = logger; + this.wrappedLogger = wrappedLogger; + this.echoStdout = echoStdout; + this.echoStderr = echoStderr; + } + + public void WriteStderr (string text, bool writeNewline) + { + if (echoStderr) { + string message = $"{GetPrefix (StderrPrefix)}{text}"; + if (writeNewline) { + logger.ErrorLine (message); + } else { + logger.Error (message); + } + } + + wrappedLogger.WriteStderr (text, writeNewline); + } + + public void WriteStdout (string text, bool writeNewline) + { + if (echoStdout) { + string message = $"{GetPrefix (StdoutPrefix)}{text}"; + if (writeNewline) { + logger.MessageLine (message); + } else { + logger.Message (message); + } + } + + wrappedLogger.WriteStdout (text, writeNewline); + } + + string GetPrefix (string? prefix) => prefix ?? String.Empty; + } + + static readonly TimeSpan DefaultProcessTimeout = TimeSpan.FromMinutes (15); + + bool disposed; + ILogger logger; + IProcessOutputLogger processOutputLogger; + + protected ILogger Log => logger; + protected IProcessOutputLogger OutputLogger => processOutputLogger; + + public string ToolPath { get; } + public bool EchoCmdAndArguments { get; set; } = true; + public bool EchoStandardError { get; set; } = true; + public bool EchoStandardOutput { get; set; } + public TimeSpan ProcessTimeout { get; set; } = DefaultProcessTimeout; + + protected ToolRunner2 (string toolPath, ILogger logger, IProcessOutputLogger processOutputLogger) + { + if (String.IsNullOrEmpty (toolPath)) { + throw new ArgumentException ("must not be null or empty", nameof (toolPath)); + } + + this.logger = logger; + this.processOutputLogger = processOutputLogger; + + ToolPath = toolPath; + } + + ~ToolRunner2 () + { + Dispose (disposing: false); + } + + protected ProcessRunner2 InitProcessRunner (object? state, params string?[]? initialParams) + { + var consoleLogger = new ConsoleProcessLogger (logger, processOutputLogger, EchoStandardOutput, EchoStandardError); + return CreateProcessRunner (consoleLogger, state, initialParams); + } + + protected virtual ProcessRunner2 CreateProcessRunner (IProcessOutputLogger consoleProcessLogger, object? state, params string?[]? initialParams) + { + var runner = new ProcessRunner2 (ToolPath, consoleProcessLogger, logger) { + ProcessTimeout = ProcessTimeout, + LogRunInfo = EchoCmdAndArguments, + LogStdout = true, + LogStderr = true, + }; + + runner.AddArguments (initialParams); + return runner; + } + + protected virtual void Dispose (bool disposing) + { + if (disposed) { + return; + } + + if (disposing) { + // TODO: dispose managed state (managed objects) + } + + disposed = true; + } + + public void Dispose () + { + Dispose (disposing: true); + GC.SuppressFinalize (this); + } +} diff --git a/tools/xadebug/Xamarin.Android.Utilities/XamarinLoggingHelper.cs b/tools/xadebug/Xamarin.Android.Utilities/XamarinLoggingHelper.cs index 4d5227d71ad..a17aa6d8018 100644 --- a/tools/xadebug/Xamarin.Android.Utilities/XamarinLoggingHelper.cs +++ b/tools/xadebug/Xamarin.Android.Utilities/XamarinLoggingHelper.cs @@ -12,7 +12,7 @@ enum LogLevel Debug } -class XamarinLoggingHelper : ILogger +class XamarinLoggingHelper : ILogger, IProcessOutputLogger { static readonly object consoleLock = new object (); string? logFilePath = null; @@ -42,6 +42,22 @@ public string? LogFilePath { } } + public IProcessOutputLogger? WrappedLogger => null; + + public string? StdoutPrefix { + get => String.Empty; + set { + // no op + } + } + + public string? StderrPrefix { + get => "stderr> "; + set { + // no op + } + } + public void Message (string? message) { Log (LogLevel.Message, message); @@ -202,5 +218,17 @@ public void LogWarning (string message, params object[] messageArgs) WarningLine (String.Format (message, messageArgs)); } } -#endregion + + public void WriteStdout (string text, bool writeLine = true) + { + LogToFile ($"{StdoutPrefix}{text}{GetNewline (writeLine)}"); + } + + public void WriteStderr (string text, bool writeLine = true) + { + LogToFile ($"{StderrPrefix}{text}{GetNewline (writeLine)}"); + } + + string GetNewline (bool yes) => yes ? Environment.NewLine : String.Empty; + #endregion }