Remove ProcessExtensions and wrap useful bits into ProcessAsync class.
MODIFY
WalletWasabi.Gui/CrashReport/CrashReporter.cs
WalletWasabi.Gui/CrashReport/CrashReporter.cs
@@ -32,7 +32,7 @@ namespace WalletWasabi.Gui.CrashReport
var args = $"crashreport -attempt=\"{Attempts + 1}\" -exception=\"{Base64ExceptionString}\"";
ProcessBridge processBridge = new ProcessBridge(Process.GetCurrentProcess().MainModule.FileName);
- processBridge.Start(args, false);
+ processBridge.Start(args, openConsole: false);
}
catch (Exception ex)
{

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
+{
+ public class ProcessAsyncTest
+ {
+ [Fact]
+ public async void SendCommandImmediateCancelAsync()
+ {
+ await Assert.ThrowsAsync<TaskCanceledException>(async () =>
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
+
+ var startInfo = new ProcessStartInfo()
+ {
+ FileName = MicroserviceHelpers.GetBinaryPath("bitcoind"),
+ Arguments = "-testnet=1"
+ };
+
+ using var process = new ProcessAsync(startInfo);
+ process.Start();
+
+ // killOnCancel is necessary on Windows. It seems, it ignores Process.Close() call.
+ await process.WaitForExitAsync(cts.Token, killOnCancel: true);
+ });
+ }
+ }
+}

ADD
WalletWasabi.Tests/UnitTests/Microservices/ProcessBridgeTest.cs
+using System;
+using System.Threading;
+using WalletWasabi.Microservices;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.Microservices
+{
+ public class ProcessBridgeTest
+ {
+ [Fact]
+ public async void SendCommandSuccessAsync()
+ {
+ using var cts = new CancellationTokenSource();
+
+ var processBridge = new ProcessBridge(MicroserviceHelpers.GetBinaryPath("hwi"));
+ (string response, int exitCode) p = await processBridge.SendCommandAsync("--help", openConsole: false, cts.Token);
+
+ Assert.Equal(0, p.exitCode);
+ Assert.Equal("{\"error\": \"Help text requested\", \"code\": -17}" + Environment.NewLine, p.response);
+ }
+ }
+}

MODIFY
WalletWasabi/BitcoinCore/CoreNode.cs
WalletWasabi/BitcoinCore/CoreNode.cs
@@ -188,7 +188,7 @@ namespace WalletWasabi.BitcoinCore
{
var arguments = "-version";
var bridge = new BitcoindProcessBridge();
- var (responseString, exitCode) = await bridge.SendCommandAsync(arguments, false, cancel).ConfigureAwait(false);
+ var (responseString, exitCode) = await bridge.SendCommandAsync(arguments, openConsole:false, cancel).ConfigureAwait(false);
if (exitCode != 0)
{

MODIFY
WalletWasabi/BitcoinCore/Processes/BitcoindRpcProcessBridge.cs
WalletWasabi/BitcoinCore/Processes/BitcoindRpcProcessBridge.cs
@@ -4,13 +4,11 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
-using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.BitcoinCore.Configuration;
using WalletWasabi.Helpers;
-using WalletWasabi.Interfaces;
using WalletWasabi.Logging;
using WalletWasabi.Microservices;
@@ -37,15 +35,21 @@ namespace WalletWasabi.BitcoinCore.Processes
public PidFile PidFile { get; }
private int? CachedPid { get; set; }
+ private ProcessAsync _process = null;
+
public async Task StartAsync(CancellationToken cancel)
{
var ptcv = PrintToConsole ? 1 : 0;
- using var process = Start($"{NetworkTranslator.GetCommandLineArguments(Network)} -datadir={DataDir} -printtoconsole={ptcv}", false);
+ _process = new ProcessAsync(CreateProcessInstance($"{NetworkTranslator.GetCommandLineArguments(Network)} -datadir={DataDir} -printtoconsole={ptcv}", openConsole: false));
+ _process.Start();
- await PidFile.WriteFileAsync(process.Id).ConfigureAwait(false);
- CachedPid = process.Id;
+ await PidFile.WriteFileAsync(_process.Id).ConfigureAwait(false);
+ CachedPid = _process.Id;
string latestFailureMessage = null;
+
+ try
+ {
while (true)
{
var ex = await RpcClient.TestAsync().ConfigureAwait(false);
@@ -60,9 +64,9 @@ namespace WalletWasabi.BitcoinCore.Processes
Logger.LogInfo($"Bitcoin Core is not yet ready... Reason: {latestFailureMessage}");
}
- if (process is null || process.HasExited)
+ if (_process is null || _process.HasExited)
{
- throw new BitcoindException($"Failed to start daemon, location: '{process?.StartInfo.FileName} {process?.StartInfo.Arguments}'", ex);
+ throw new BitcoindException($"Failed to start daemon, location: '{_process?.StartInfo.FileName} {_process?.StartInfo.Arguments}'", ex);
}
if (cancel.IsCancellationRequested)
@@ -74,6 +78,12 @@ namespace WalletWasabi.BitcoinCore.Processes
await Task.Delay(100).ConfigureAwait(false); // So to leave some breathing room before the next check.
}
}
+ catch (Exception)
+ {
+ _process?.Dispose();
+ throw;
+ }
+ }
/// <param name="onlyOwned">Only stop if this node owns the process.</param>
public async Task StopAsync(bool onlyOwned)
@@ -86,12 +96,11 @@ namespace WalletWasabi.BitcoinCore.Processes
using CancellationTokenSource cts = new CancellationTokenSource(reasonableCoreShutdownTimeout);
int? pid = await PidFile.TryReadAsync().ConfigureAwait(false);
- // If the cached pid is pid, then we own the process.
+ // If the cached PID is PID, then we own the process.
if (pid.HasValue && (!onlyOwned || CachedPid == pid))
{
try
{
- using Process process = Process.GetProcessById(pid.Value);
try
{
await RpcClient.StopAsync().ConfigureAwait(false);
@@ -99,13 +108,14 @@ namespace WalletWasabi.BitcoinCore.Processes
}
catch (Exception ex)
{
- process.Kill();
+ _process.Kill();
Logger.LogDebug(ex);
}
- await process.WaitForExitAsync(cts.Token).ConfigureAwait(false);
+ await _process.WaitForExitAsync(cts.Token).ConfigureAwait(false);
}
finally
{
+ _process?.Dispose();
PidFile.TryDelete();
}
}

MODIFY
WalletWasabi/BitcoinCore/Processes/PidFile.cs
WalletWasabi/BitcoinCore/Processes/PidFile.cs
@@ -26,7 +26,7 @@ namespace WalletWasabi.BitcoinCore.Processes
}
/// <summary>
- /// Full path to PID file.
+ /// Gets full path to PID file.
/// </summary>
public string FilePath { get; }

DELETE
WalletWasabi/Extensions/ProcessExtensions.cs
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using WalletWasabi.Logging;
-
-namespace System.Diagnostics
-{
- public static class ProcessExtensions
- {
- private static Dictionary<int, TaskCompletionSource<bool>> AwaitingTaskCompletionSources { get; } = new Dictionary<int, TaskCompletionSource<bool>>();
- private static object AwaitingTaskCompletionSourcesLock { get; } = new object();
-
- public static async Task WaitForExitAsync(this Process process, CancellationToken cancel)
- {
- if (process.HasExited)
- {
- return;
- }
-
- // https://stackoverflow.com/a/12858633
- var tcs = new TaskCompletionSource<bool>();
- try
- {
- lock (AwaitingTaskCompletionSourcesLock)
- {
- AwaitingTaskCompletionSources.Add(process.Id, tcs);
- process.EnableRaisingEvents = true;
- process.Exited += Process_Exited;
- }
-
- try
- {
- await tcs.Task.WithAwaitCancellationAsync(cancel).ConfigureAwait(false);
- }
- catch (OperationCanceledException ex)
- {
- if (!process.HasExited) // Final check.
- {
- // Originally it was TaskCanceledException here, so I don't want to change the behavior.
- throw new TaskCanceledException("Waiting for process exiting was canceled.", ex, cancel);
- }
- }
- }
- finally
- {
- lock (AwaitingTaskCompletionSourcesLock)
- {
- process.Exited -= Process_Exited;
- AwaitingTaskCompletionSources.Remove(process.Id);
- }
- }
- }
-
- private static void Process_Exited(object sender, EventArgs e)
- {
- try
- {
- lock (AwaitingTaskCompletionSourcesLock)
- {
- var proc = sender as Process;
- TaskCompletionSource<bool> tcs = AwaitingTaskCompletionSources[proc.Id];
- tcs.SetResult(true);
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, nameof(ProcessExtensions));
- }
- }
- }
-}

MODIFY
WalletWasabi/Helpers/EnvironmentHelpers.cs
WalletWasabi/Helpers/EnvironmentHelpers.cs
@@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Logging;
+using WalletWasabi.Microservices;
namespace WalletWasabi.Helpers
{
@@ -169,8 +170,7 @@ namespace WalletWasabi.Helpers
{
var escapedArgs = cmd.Replace("\"", "\\\"");
- using var process = Process.Start(
- new ProcessStartInfo
+ var startInfo = new ProcessStartInfo
{
FileName = "/bin/sh",
Arguments = $"-c \"{escapedArgs}\"",
@@ -178,16 +178,22 @@ namespace WalletWasabi.Helpers
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
- }
- );
+ };
+
if (waitForExit)
{
+ using var process = new ProcessAsync(startInfo);
+ process.Start();
+
await process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
- var exitCode = process.ExitCode;
- if (exitCode != 0)
+
+ if (process.ExitCode != 0)
{
- Logger.LogError($"{nameof(ShellExecAsync)} command: {cmd} exited with exit code: {exitCode}, instead of 0.");
+ Logger.LogError($"{nameof(ShellExecAsync)} command: {cmd} exited with exit code: {process.ExitCode}, instead of 0.");
}
+ } else
+ {
+ using var process = Process.Start(startInfo);
}
}

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
+{
+ public class ProcessAsync : IDisposable
+ {
+ private readonly TaskCompletionSource<bool> TaskCompletionSource;
+ private readonly Process Process;
+
+ /// <summary>
+ /// To detect redundant calls.
+ /// </summary>
+ private bool _disposed = false;
+
+ public ProcessAsync(ProcessStartInfo startInfo) : this(new Process() { StartInfo = startInfo })
+ {
+ }
+
+ public ProcessAsync(Process process)
+ {
+ TaskCompletionSource = new TaskCompletionSource<bool>();
+
+ Process = process;
+ Process.EnableRaisingEvents = true;
+ Process.Exited += (object sender, EventArgs e) => TaskCompletionSource.TrySetResult(true);
+ }
+
+ /// <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 StreamWriter => Process.StandardInput;
+
+ /// <inheritdoc cref="Process.StandardOutput"/>
+ public StreamReader StandardOutput => Process.StandardOutput;
+
+ /// <inheritdoc cref="Process.Start()"/>
+ public void Start()
+ {
+ try
+ {
+ Process.Start();
+ }
+ catch
+ {
+ 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();
+ }
+
+ /// <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 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(() => TaskCompletionSource.TrySetCanceled()))
+ {
+ await TaskCompletionSource.Task;
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ if (killOnCancel)
+ {
+ if (!Process.HasExited)
+ {
+ try
+ {
+ Process.Kill(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?.Dispose();
+ }
+
+ _disposed = true;
+ }
+
+ public void Dispose()
+ {
+ // Dispose of unmanaged resources.
+ Dispose(true);
+
+ // Suppress finalization.
+ GC.SuppressFinalize(this);
+ }
+ }
+}

MODIFY
WalletWasabi/Microservices/ProcessBridge.cs
WalletWasabi/Microservices/ProcessBridge.cs
using System;
-using System.Collections.Generic;
+using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Helpers;
@@ -16,6 +15,7 @@ namespace WalletWasabi.Microservices
public ProcessBridge(string processPath)
{
ProcessPath = Guard.NotNullOrEmptyOrWhitespace(nameof(processPath), processPath);
+
if (!File.Exists(ProcessPath))
{
throw new FileNotFoundException($"{Path.GetFileNameWithoutExtension(ProcessPath)} is not found.", ProcessPath);
@@ -26,50 +26,58 @@ namespace WalletWasabi.Microservices
public Process Start(string arguments, bool openConsole)
{
- var redirectStandardOutput = !openConsole;
- var useShellExecute = openConsole;
- var createNoWindow = !openConsole;
- var windowStyle = openConsole ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden;
-
- if (openConsole && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- throw new PlatformNotSupportedException($"{RuntimeInformation.OSDescription} is not supported.");
- }
-
- ProcessStartInfo startInfo = new ProcessStartInfo
- {
- FileName = ProcessPath,
- Arguments = arguments,
- RedirectStandardOutput = redirectStandardOutput,
- UseShellExecute = useShellExecute,
- CreateNoWindow = createNoWindow,
- WindowStyle = windowStyle
- };
-
+ var process = CreateProcessInstance(arguments, openConsole);
try
{
- return Process.Start(startInfo);
+ process.Start();
}
catch
{
- Logger.LogInfo($"{nameof(startInfo.FileName)}: {startInfo.FileName}");
- Logger.LogInfo($"{nameof(startInfo.Arguments)}: {startInfo.Arguments}");
- Logger.LogInfo($"{nameof(startInfo.RedirectStandardOutput)}: {startInfo.RedirectStandardOutput}");
- Logger.LogInfo($"{nameof(startInfo.UseShellExecute)}: {startInfo.UseShellExecute}");
- Logger.LogInfo($"{nameof(startInfo.CreateNoWindow)}: {startInfo.CreateNoWindow}");
- Logger.LogInfo($"{nameof(startInfo.WindowStyle)}: {startInfo.WindowStyle}");
-
+ 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;
}
+ return process;
}
public async Task<(string response, int exitCode)> SendCommandAsync(string arguments, bool openConsole, CancellationToken cancel)
{
- using var process = Start(arguments, openConsole);
- await process.WaitForExitAsync(cancel).ConfigureAwait(false);
- string responseString = openConsole ? string.Empty : await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
+ // "using" makes sure the process exits at the end of this method.
+ using var process = new ProcessAsync(CreateProcessInstance(arguments, openConsole));
+
+ process.Start();
+ await process.WaitForExitAsync(cancel);
+
+ string responseString = openConsole ? string.Empty : await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
return (responseString, exitCode: process.ExitCode);
}
+
+ protected Process CreateProcessInstance(string arguments, bool openConsole)
+ {
+ var redirectStandardOutput = !openConsole;
+ var useShellExecute = openConsole;
+ var createNoWindow = !openConsole;
+ var windowStyle = openConsole ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden;
+
+ if (openConsole && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ throw new PlatformNotSupportedException($"{RuntimeInformation.OSDescription} is not supported.");
+ }
+
+ var p = new Process();
+ p.StartInfo.FileName = ProcessPath;
+ p.StartInfo.Arguments = arguments;
+ p.StartInfo.RedirectStandardOutput = redirectStandardOutput;
+ p.StartInfo.UseShellExecute = useShellExecute;
+ p.StartInfo.CreateNoWindow = createNoWindow;
+ p.StartInfo.WindowStyle = windowStyle;
+
+ return p;
+ }
}
}

Remove ProcessExtensions and wrap useful bits into ProcessAsync class.