Implement `LinuxInhibitorTask`.
MODIFY
WalletWasabi.Gui/Global.cs
WalletWasabi.Gui/Global.cs
@@ -144,7 +144,16 @@ namespace WalletWasabi.Gui
await LegalChecker.InitializeAsync(HostedServices.Get<UpdateChecker>()).ConfigureAwait(false);
cancel.ThrowIfCancellationRequested();
- HostedServices.Register<SystemAwakeChecker>(new SystemAwakeChecker(WalletManager), "System Awake Checker");
+ SystemAwakeChecker? systemAwakeChecker = await SystemAwakeChecker.CreateAsync(WalletManager).ConfigureAwait(false);
+
+ if (systemAwakeChecker is not null)
+ {
+ HostedServices.Register<SystemAwakeChecker>(systemAwakeChecker, "System Awake Checker");
+ }
+ else
+ {
+ Logger.LogInfo("System Awake Checker is not available on this platform.");
+ }
if (Config.UseTor && Network != Network.RegTest)
{

ADD
WalletWasabi.Tests/UnitTests/Helpers/PowerSaving/LinuxInhibitorTests.cs
+using Moq;
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Helpers.PowerSaving;
+using WalletWasabi.Microservices;
+using Xunit;
+using static WalletWasabi.Helpers.PowerSaving.LinuxInhibitorTask;
+
+namespace WalletWasabi.Tests.UnitTests.Helpers.PowerSaving
+{
+ /// <summary>
+ /// Tests for <see cref="LinuxInhibitorTask"/> class.
+ /// </summary>
+ public class LinuxInhibitorTests
+ {
+ private const string DefaultReason = "CJ is in progress";
+
+ [Fact]
+ public async Task TestAvailabilityAsync()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ bool isAvailable = await LinuxInhibitorTask.IsSystemdInhibitSupportedAsync();
+ Assert.True(isAvailable);
+ }
+ }
+
+ [Fact]
+ public async Task CancelBehaviorAsync()
+ {
+ Mock<ProcessAsync> mockProcess = new(MockBehavior.Strict, new ProcessStartInfo());
+ mockProcess.Setup(p => p.WaitForExitAsync(It.IsAny<CancellationToken>(), It.IsAny<bool>()))
+ .Returns((CancellationToken cancellationToken, bool killOnCancel) => Task.Delay(Timeout.Infinite, cancellationToken));
+ mockProcess.Setup(p => p.HasExited).Returns(false);
+ mockProcess.Setup(p => p.Kill());
+
+ LinuxInhibitorTask psTask = new(InhibitWhat.All, TimeSpan.FromSeconds(10), DefaultReason, mockProcess.Object);
+
+ // Task was started and as such it cannot be done yet.
+ Assert.False(psTask.IsDone);
+
+ // Explicitly ask to stop the task.
+ await psTask.StopAsync();
+
+ // Now the task must be finished.
+ Assert.True(psTask.IsDone);
+
+ // Prolong after exit must fail.
+ Assert.False(psTask.Prolong(TimeSpan.FromSeconds(5)));
+
+ mockProcess.VerifyAll();
+ }
+ }
+}

ADD
WalletWasabi/Helpers/IPowerSavingInhibitorTask.cs
+using System;
+using System.Threading.Tasks;
+
+namespace WalletWasabi.Helpers
+{
+ /// <summary>Task that makes sure that computer does not go to sleep, switch to idle state, etc.</summary>
+ public interface IPowerSavingInhibitorTask
+ {
+ /// <remarks>Useful to know whether a new task needs to be created.</remarks>
+ bool IsDone { get; }
+
+ /// <summary>Prolongs the interval how long the power saving should be postponed by.</summary>
+ /// <returns><c>true</c> when prolonging of the power saving task succeeded, <c>false</c> when the task is already stopped.</returns>
+ bool Prolong(TimeSpan timeSpan);
+
+ /// <summary>Stop the internal power saving mechanism.</summary>
+ /// <remarks>Once the task is stopped, computer may go idle/reboot, etc. Operating systems' power saving timers may be reseted or not - we provide no guarantees here.</remarks>
+ Task StopAsync();
+ }
+}

ADD
WalletWasabi/Helpers/PowerSaving/LinuxInhibitorTask.cs
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Logging;
+using WalletWasabi.Microservices;
+
+namespace WalletWasabi.Helpers.PowerSaving
+{
+ /// <summary><c>systemd-inhibitor</c> API wrapper.</summary>
+ /// <remarks>Only works on Linux machines that use systemd.</remarks>
+ /// <seealso href="https://www.freedesktop.org/wiki/Software/systemd/inhibit/"/>
+ public class LinuxInhibitorTask : IPowerSavingInhibitorTask
+ {
+ /// <remarks>Guarded by <see cref="StateLock"/>.</remarks>
+ private bool _isDone;
+
+ /// <remarks>Use the constructor only in tests.</remarks>
+ internal LinuxInhibitorTask(InhibitWhat what, TimeSpan period, string reason, ProcessAsync process)
+ {
+ What = what;
+ BasePeriod = period;
+ Reason = reason;
+ Process = process;
+ Cts = new CancellationTokenSource(period);
+
+ Task = WaitAsync();
+ }
+
+ /// <summary>Linux GUI environments.</summary>
+ public enum GraphicalEnvironment
+ {
+ Gnome,
+ Mate,
+ Other,
+ }
+
+ [Flags]
+ public enum InhibitWhat
+ {
+ /// <summary>
+ /// Inhibits that the system goes into idle mode, possibly resulting in automatic system
+ /// suspend or shutdown depending on configuration.
+ /// </summary>
+ Idle = 1,
+
+ /// <summary>Inhibits system suspend and hibernation requested by (unprivileged) users.</summary>
+ Sleep = 2,
+
+ /// <summary>Inhibits high-level system power-off and reboot requested by (unprivileged) users.</summary>
+ Shutdown = 4,
+
+ All = Idle | Sleep | Shutdown
+ }
+
+ /// <remarks>Guards <see cref="_isDone"/>.</remarks>
+ private object StateLock { get; } = new();
+
+ public InhibitWhat What { get; }
+
+ /// <remarks>It holds: inhibitorEndTime = now + BasePeriod + ProlongInterval.</remarks>
+ public TimeSpan BasePeriod { get; }
+
+ /// <summary>Reason why the power saving is inhibited.</summary>
+ public string Reason { get; }
+ private ProcessAsync Process { get; }
+ private CancellationTokenSource Cts { get; }
+ private TaskCompletionSource StoppedTcs { get; } = new();
+ private Task Task { get; }
+
+ /// <inheritdoc/>
+ public bool IsDone
+ {
+ get
+ {
+ lock (StateLock)
+ {
+ return _isDone;
+ }
+ }
+ }
+
+ private async Task WaitAsync()
+ {
+ try
+ {
+ await Process.WaitForExitAsync(Cts.Token).ConfigureAwait(false);
+
+ // This should be hit only when somebody externally kills the systemd-inhibit process.
+ Logger.LogError("Linux inhibit process ended prematurely.");
+ }
+ catch (OperationCanceledException)
+ {
+ Logger.LogTrace("Elapsed time limit for the inhibitor task to live.");
+ }
+ finally
+ {
+ if (!Process.HasExited)
+ {
+ // Process cannot stop on its own so we know it is actually running.
+ try
+ {
+ Process.Kill();
+ Logger.LogWarning($"XXX: Inhibit task was killed.");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogTrace($"Failed to kill the process. It might have finished already.", ex);
+ }
+ }
+
+ lock (StateLock)
+ {
+ Cts.Cancel();
+ Cts.Dispose();
+ _isDone = true;
+ StoppedTcs.SetResult();
+ }
+
+ Logger.LogTrace("Inhibit task is finished.");
+ Logger.LogWarning($"XXX: Inhibit task is finished.");
+ }
+ }
+
+ /// <inheritdoc/>
+ public bool Prolong(TimeSpan period)
+ {
+ string logMessage = "N/A";
+
+ try
+ {
+ lock (StateLock)
+ {
+ if (!_isDone && !Cts.IsCancellationRequested)
+ {
+ // This does nothing when cancellation of CTS is already requested.
+ Cts.CancelAfter(period);
+ logMessage = $"Power saving task was prolonged to: {DateTime.UtcNow.Add(period)}";
+ return !Cts.IsCancellationRequested;
+ }
+
+ logMessage = "Power saving task is already finished.";
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex);
+ return false;
+ }
+ finally
+ {
+ Logger.LogWarning($"XXX: {logMessage}");
+ Logger.LogTrace(logMessage);
+ }
+ }
+
+ public Task StopAsync()
+ {
+ lock (StateLock)
+ {
+ if (!_isDone)
+ {
+ Cts.Cancel();
+ }
+ }
+
+ return StoppedTcs.Task;
+ }
+
+ public static Task<bool> IsSystemdInhibitSupportedAsync()
+ {
+ return IsCommandSupportedAsync("systemd-inhibit");
+ }
+
+ public static Task<bool> IsGnomeSessionInhibitSupportedAsync()
+ {
+ return IsCommandSupportedAsync("gnome-session-inhibit");
+ }
+
+ public static Task<bool> IsMateSessionInhibitSupportedAsync()
+ {
+ return IsCommandSupportedAsync("mate-session-inhibit");
+ }
+
+ private static async Task<bool> IsCommandSupportedAsync(string command)
+ {
+ try
+ {
+ using CancellationTokenSource cts = new(TimeSpan.FromSeconds(5));
+ ProcessStartInfo processStartInfo = GetProcessStartInfo(command, "--help");
+ Process process = System.Diagnostics.Process.Start(processStartInfo)!;
+
+ await process.WaitForExitAsync(cts.Token).ConfigureAwait(false);
+ bool success = process.ExitCode == 0;
+ Logger.LogDebug($"{command} is {(success ? "supported" : "NOT supported")}.");
+
+ return success;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to find out whether {command} is supported or not.", ex);
+ }
+
+ return false;
+ }
+
+ /// <remarks><paramref name="reason"/> cannot contain apostrophe characters.</remarks>
+ public static LinuxInhibitorTask Create(InhibitWhat what, TimeSpan basePeriod, string reason, GraphicalEnvironment gui = GraphicalEnvironment.Other)
+ {
+ // Make sure that the systemd-inhibit is terminated once the parent process (WW) finishes.
+ string innerCommand = $"tail --pid={Environment.ProcessId} -f /dev/null";
+ string command;
+ string arguments;
+
+ // systemd-inhibit command by default does not seem to inhibit idle behavior on Ubuntu 20.04 (and probably most others).
+ // That's why we use gnome-session-inhibit when we can.
+ if (gui == GraphicalEnvironment.Gnome)
+ {
+ string inhibitArgument = ConstructInhibitArgument(what);
+ command = "gnome-session-inhibit";
+ arguments = $"--reason \"{reason}\" --inhibit {inhibitArgument} {innerCommand}";
+ }
+ else if (gui == GraphicalEnvironment.Mate)
+ {
+ string inhibitArgument = ConstructInhibitArgument(what);
+ command = $"mate-session-inhibit";
+ arguments = $"--reason \"{reason}\" --inhibit {inhibitArgument} {innerCommand}";
+ }
+ else
+ {
+ string whatArgument = ConstructSystemdWhatArgument(what);
+ command = $"systemd-inhibit";
+ arguments = $"--why=\"{reason}\" --what=\"{whatArgument}\" --mode=block {innerCommand}";
+ }
+
+ Logger.LogWarning($"XXX: shell command to invoke: {command}");
+ ProcessStartInfo startInfo = GetProcessStartInfo(command, arguments);
+
+ ProcessAsync process = new(startInfo);
+ process.Start();
+ LinuxInhibitorTask task = new(what, basePeriod, reason, process);
+
+ return task;
+ }
+
+ /// <summary>Constructs argument <c>--inhibit</c> value for <c>gnome-session-inhibit</c> or <c>mate-session-inhibit</c> command.</summary>
+ /// <remarks>The possible values are "logout", "switch-user", "suspend", "idle", "automount".</remarks>
+ private static string ConstructInhibitArgument(InhibitWhat what)
+ {
+ List<string> whatList = new();
+
+ if (what.HasFlag(InhibitWhat.Idle))
+ {
+ whatList.Add("idle");
+ }
+
+ if (what.HasFlag(InhibitWhat.Sleep))
+ {
+ whatList.Add("suspend");
+ }
+
+ string whatArgument = string.Join(':', whatList);
+ return whatArgument;
+ }
+
+ /// <summary>Constructs argument <c>--what</c> value for <c>systemd-inhibit</c> command.</summary>
+ private static string ConstructSystemdWhatArgument(InhibitWhat what)
+ {
+ List<string> whatList = new();
+
+ if (what.HasFlag(InhibitWhat.Idle))
+ {
+ whatList.Add("idle");
+ }
+
+ if (what.HasFlag(InhibitWhat.Sleep))
+ {
+ whatList.Add("sleep");
+ }
+
+ if (what.HasFlag(InhibitWhat.Shutdown))
+ {
+ whatList.Add("shutdown");
+ }
+
+ string whatArgument = string.Join(':', whatList);
+ return whatArgument;
+ }
+
+ private static ProcessStartInfo GetProcessStartInfo(string command, string arguments)
+ {
+ return new()
+ {
+ FileName = command,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ }
+ }
+}

MODIFY
WalletWasabi/Microservices/ProcessAsync.cs
WalletWasabi/Microservices/ProcessAsync.cs
@@ -36,7 +36,7 @@ namespace WalletWasabi.Microservices
public int ExitCode => Process.ExitCode;
/// <inheritdoc cref="Process.HasExited"/>
- public bool HasExited => Process.HasExited;
+ public virtual bool HasExited => Process.HasExited;
/// <inheritdoc cref="Process.Id"/>
public int Id => Process.Id;
@@ -69,7 +69,7 @@ namespace WalletWasabi.Microservices
}
/// <inheritdoc cref="Process.Kill()"/>
- public void Kill()
+ public virtual void Kill()
{
Process.Kill();
}

MODIFY
WalletWasabi/Services/SystemAwakeChecker.cs
WalletWasabi/Services/SystemAwakeChecker.cs
using System;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Bases;
using WalletWasabi.Helpers;
+using WalletWasabi.Helpers.PowerSaving;
+using WalletWasabi.Logging;
using WalletWasabi.Wallets;
+using static WalletWasabi.Helpers.PowerSaving.LinuxInhibitorTask;
namespace WalletWasabi.Services
{
public class SystemAwakeChecker : PeriodicRunner
{
- public SystemAwakeChecker(WalletManager walletManager) : base(TimeSpan.FromMinutes(1))
+ private const string Reason = "CoinJoin is in progress.";
+ private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(5);
+
+ private volatile IPowerSavingInhibitorTask? _powerSavingTask;
+
+ private SystemAwakeChecker(WalletManager walletManager, Func<Task<IPowerSavingInhibitorTask>>? taskFactory) : base(TimeSpan.FromMinutes(1))
{
WalletManager = walletManager;
+ TaskFactory = taskFactory;
}
private WalletManager WalletManager { get; }
+ public Func<Task<IPowerSavingInhibitorTask>>? TaskFactory { get; }
+
+ /// <summary>Checks whether we support awake state prolonging for the current platform.</summary>
+ public static async Task<SystemAwakeChecker?> CreateAsync(WalletManager walletManager)
+ {
+ Func<Task<IPowerSavingInhibitorTask>>? taskFactory = null;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ // Either we have systemd or not.
+ bool isSystemd = await LinuxInhibitorTask.IsSystemdInhibitSupportedAsync().ConfigureAwait(false);
+ if (!isSystemd)
+ {
+ return null;
+ }
+
+ GraphicalEnvironment gui = GraphicalEnvironment.Other;
+
+ // Specialization for GNOME.
+ if (await LinuxInhibitorTask.IsMateSessionInhibitSupportedAsync().ConfigureAwait(false))
+ {
+ gui = GraphicalEnvironment.Mate;
+ }
+ else if (await LinuxInhibitorTask.IsGnomeSessionInhibitSupportedAsync().ConfigureAwait(false))
+ {
+ gui = GraphicalEnvironment.Gnome;
+ }
+
+ taskFactory = () => Task.FromResult<IPowerSavingInhibitorTask>(LinuxInhibitorTask.Create(InhibitWhat.All, Timeout, Reason, gui));
+ }
+
+ return new SystemAwakeChecker(walletManager, taskFactory);
+ }
protected override async Task ActionAsync(CancellationToken cancel)
{
+ IPowerSavingInhibitorTask? task = _powerSavingTask;
+
if (WalletManager.AnyCoinJoinInProgress())
+ {
+ Logger.LogWarning($"XXX: CJ is in progress, make sure to prolong awake state.");
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ if (task is not null)
+ {
+ Logger.LogWarning($"XXX: Prolong power saving prevention task by one minute.");
+ task.Prolong(TimeSpan.FromMinutes(1));
+ }
+ else
+ {
+ Logger.LogWarning($"XXX: Create new power saving prevention task.");
+ _powerSavingTask = await TaskFactory!().ConfigureAwait(false);
+ }
+ }
+ else
{
await EnvironmentHelpers.ProlongSystemAwakeAsync().ConfigureAwait(false);
}
}
+ else
+ {
+ if (task is not null)
+ {
+ Logger.LogWarning($"XXX: Stop task.");
+ await task.StopAsync().ConfigureAwait(false);
+ }
+ }
+ }
}
}

Implement `LinuxInhibitorTask`.