Add new async process API that is supposed to replace ProcessBridge API.
ADD
WalletWasabi.Tests/UnitTests/Microservices/ProcessAsyncTest.cs
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Microservices;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.Microservices
+{
+ /// <summary>
+ /// Tests for <see cref="ProcessAsync"/> class.
+ /// </summary>
+ public class ProcessAsyncTest
+ {
+ /// <summary>
+ /// Tests that we can start Bitcoin daemon (testnet) using <see cref="ProcessAsync"/> API.
+ /// </summary>
+ [Fact]
+ public async void SendCommandImmediateCancelAsync()
+ {
+ await Assert.ThrowsAsync<TaskCanceledException>(async () =>
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+
+ var startInfo = new ProcessStartInfo()
+ {
+ FileName = MicroserviceHelpers.GetBinaryPath("bitcoind"),
+ Arguments = "-regtest=1"
+ };
+
+ using var process = new ProcessAsync(startInfo);
+ process.Start();
+
+ await process.WaitForExitAsync(cts.Token, killOnCancel: true);
+ });
+ }
+ }
+}

ADD
WalletWasabi/Microservices/ProcessAsync.cs
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Logging;
+
+namespace WalletWasabi.Microservices
+{
+ /// <summary>
+ /// Async wrapper class for <see cref="System.Diagnostics.Process"/> class that implements <see cref="WaitForExitAsync(CancellationToken, bool)"/>
+ /// to asynchronously wait for a process to exit.
+ /// </summary>
+ /// <remarks><see cref="IDisposable"/> is implemented. Do not forget to use `using` or dispose any instance of this class.</remarks>
+ public class ProcessAsync : IDisposable
+ {
+ /// <summary>
+ /// To detect redundant calls.
+ /// </summary>
+ private bool _disposed = false;
+
+ public ProcessAsync(ProcessStartInfo startInfo) : this(new Process() { StartInfo = startInfo })
+ {
+ }
+
+ private ProcessAsync(Process process)
+ {
+ ProcessExecutionTcs = new TaskCompletionSource<bool>();
+
+ Process = process;
+ Process.EnableRaisingEvents = true;
+ Process.Exited += OnExited;
+ }
+
+ private TaskCompletionSource<bool> ProcessExecutionTcs { get; }
+
+ private Process Process { get; }
+
+ /// <inheritdoc cref="Process.StartInfo"/>
+ public ProcessStartInfo StartInfo => Process.StartInfo;
+
+ /// <inheritdoc cref="Process.ExitCode"/>
+ public int ExitCode => Process.ExitCode;
+
+ /// <inheritdoc cref="Process.HasExited"/>
+ public bool HasExited => Process.HasExited;
+
+ /// <inheritdoc cref="Process.Id"/>
+ public int Id => Process.Id;
+
+ /// <inheritdoc cref="Process.StandardInput"/>
+ public StreamWriter StandardInput => Process.StandardInput;
+
+ /// <inheritdoc cref="Process.StandardOutput"/>
+ public StreamReader StandardOutput => Process.StandardOutput;
+
+ /// <inheritdoc cref="Process.Start()"/>
+ public void Start()
+ {
+ try
+ {
+ Process.Start();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex);
+
+ Logger.LogInfo($"{nameof(Process.StartInfo.FileName)}: {Process.StartInfo.FileName}.");
+ Logger.LogInfo($"{nameof(Process.StartInfo.Arguments)}: {Process.StartInfo.Arguments}.");
+ Logger.LogInfo($"{nameof(Process.StartInfo.RedirectStandardOutput)}: {Process.StartInfo.RedirectStandardOutput}.");
+ Logger.LogInfo($"{nameof(Process.StartInfo.UseShellExecute)}: {Process.StartInfo.UseShellExecute}.");
+ Logger.LogInfo($"{nameof(Process.StartInfo.CreateNoWindow)}: {Process.StartInfo.CreateNoWindow}.");
+ Logger.LogInfo($"{nameof(Process.StartInfo.WindowStyle)}: {Process.StartInfo.WindowStyle}.");
+ throw;
+ }
+ }
+
+ /// <inheritdoc cref="Process.Kill()"/>
+ public void Kill()
+ {
+ Process.Kill();
+ }
+
+ /// <inheritdoc cref="Process.Kill(bool)"/>
+ public void Kill(bool entireProcessTree)
+ {
+ Process.Kill(entireProcessTree);
+ }
+
+ /// <summary>
+ /// Waits until the process either finishes on its own or when user cancels the action.
+ /// </summary>
+ /// <param name="cancel">Cancellation token.</param>
+ /// <param name="killOnCancel">If <c>true</c> the process will be killed (with entire process tree) when this asynchronous action is canceled via <paramref name="cancel"/> token.</param>
+ /// <returns><see cref="Task"/>.</returns>
+ public async Task WaitForExitAsync(CancellationToken cancel, bool killOnCancel = false)
+ {
+ if (Process.HasExited)
+ {
+ return;
+ }
+
+ try
+ {
+ // If this token is already in the canceled state, the delegate will be run immediately and synchronously.
+ using (cancel.Register(() => ProcessExecutionTcs.TrySetCanceled()))
+ {
+ await ProcessExecutionTcs.Task;
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ if (killOnCancel)
+ {
+ if (!Process.HasExited)
+ {
+ try
+ {
+ Process.Kill(entireProcessTree: true);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError($"Could not kill process: {e}.");
+ }
+ }
+ }
+
+ throw new TaskCanceledException("Waiting for process exiting was canceled.", ex, cancel);
+ }
+ }
+
+ // Protected implementation of Dispose pattern.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ // Dispose managed state (managed objects).
+ Process.Exited -= OnExited;
+ Process.Dispose();
+ }
+
+ _disposed = true;
+ }
+
+ public void Dispose()
+ {
+ // Dispose of unmanaged resources.
+ Dispose(true);
+
+ // Suppress finalization.
+ GC.SuppressFinalize(this);
+ }
+
+ private void OnExited(object? sender, EventArgs? e)
+ {
+ ProcessExecutionTcs.TrySetResult(true);
+ }
+ }
+}

ADD
WalletWasabi/Microservices/ProcessInvoker.cs
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace WalletWasabi.Microservices
+{
+ public class ProcessInvoker
+ {
+ public async Task<(string response, int exitCode)> SendCommandAsync(ProcessStartInfo startInfo, CancellationToken token, Action<StreamWriter>? standardInputWriter = null)
+ {
+ using var processAsync = new ProcessAsync(startInfo);
+
+ if (standardInputWriter is { })
+ {
+ processAsync.StartInfo.RedirectStandardInput = true;
+ }
+
+ processAsync.Start();
+
+ if (standardInputWriter is { })
+ {
+ standardInputWriter(processAsync.StandardInput);
+ processAsync.StandardInput.Close();
+ }
+
+ Task<string> readPipeTask = processAsync.StartInfo.UseShellExecute
+ ? Task.FromResult(string.Empty)
+ : processAsync.StandardOutput.ReadToEndAsync();
+
+ await processAsync.WaitForExitAsync(token).ConfigureAwait(false);
+
+ string output = await readPipeTask.ConfigureAwait(false);
+
+ return (output, exitCode: processAsync.ExitCode);
+ }
+ }
+}

ADD
WalletWasabi/Microservices/ProcessStartInfoFactory.cs
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace WalletWasabi.Microservices
+{
+ /// <summary>
+ /// Factory for <see cref="ProcessStartInfo"/> with pre-defined properties as needed in Wasabi Wallet.
+ /// </summary>
+ public class ProcessStartInfoFactory
+ {
+ /// <summary>
+ /// Creates new <see cref="ProcessStartInfo"/> instance.
+ /// </summary>
+ /// <param name="processPath">Path to process.</param>
+ /// <param name="arguments">Process arguments.</param>
+ /// <param name="openConsole">Open console window. Only for Windows platform.</param>
+ /// <returns><see cref="ProcessStartInfo"/> instance.</returns>
+ public static ProcessStartInfo Make(string processPath, string arguments, bool openConsole = false)
+ {
+ ProcessWindowStyle windowStyle;
+
+ if (openConsole)
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ throw new PlatformNotSupportedException($"{RuntimeInformation.OSDescription} is not supported.");
+ }
+
+ windowStyle = ProcessWindowStyle.Normal;
+ }
+ else
+ {
+ windowStyle = ProcessWindowStyle.Hidden;
+ }
+
+ var p = new ProcessStartInfo(fileName: processPath, arguments);
+ p.FileName = processPath;
+ p.Arguments = arguments;
+ p.RedirectStandardOutput = !openConsole;
+ p.UseShellExecute = openConsole;
+ p.CreateNoWindow = !openConsole;
+ p.WindowStyle = windowStyle;
+
+ return p;
+ }
+ }
+}

Add new async process API that is supposed to replace ProcessBridge API.