Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

macOS - sleep inhibiting II #9707

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions WalletWasabi.Tests/UnitTests/Clients/PreventSleepTests.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,42 +1,30 @@
using Moq;
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.
/// Tests for <see cref="BaseInhibitorTask"/> class.
/// </summary>
public class LinuxInhibitorTests
public class BaseInhibitorTaskTests
{
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());
mockProcess.Setup(p => p.Kill(It.IsAny<bool>()));

LinuxInhibitorTask psTask = new(InhibitWhat.All, TimeSpan.FromSeconds(10), DefaultReason, mockProcess.Object);
TestInhibitorClass psTask = new(TimeSpan.FromSeconds(10), DefaultReason, mockProcess.Object);

// Task was started and as such it cannot be done yet.
Assert.False(psTask.IsDone);
Expand All @@ -52,4 +40,12 @@ public async Task CancelBehaviorAsync()

mockProcess.VerifyAll();
}

public class TestInhibitorClass : BaseInhibitorTask
{
public TestInhibitorClass(TimeSpan period, string reason, ProcessAsync process)
: base(period, reason, process)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using WalletWasabi.Helpers.PowerSaving;
using Xunit;

namespace WalletWasabi.Tests.UnitTests.Helpers.PowerSaving;

/// <summary>
/// Tests for <see cref="LinuxInhibitorTask"/> class.
/// </summary>
public class LinuxInhibitorTaskTests
{
[Fact]
public async Task TestAvailabilityAsync()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
bool isAvailable = await LinuxInhibitorTask.IsSystemdInhibitSupportedAsync();
Assert.True(isAvailable);
}
}
}
37 changes: 1 addition & 36 deletions WalletWasabi/Helpers/EnvironmentHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,6 @@ namespace WalletWasabi.Helpers;

public static class EnvironmentHelpers
{
[Flags]
private enum EXECUTION_STATE : uint
{
ES_AWAYMODE_REQUIRED = 0x00000040,
ES_CONTINUOUS = 0x80000000,
ES_DISPLAY_REQUIRED = 0x00000002,
ES_SYSTEM_REQUIRED = 0x00000001
}

// appName, dataDir
private static ConcurrentDictionary<string, string> DataDirDict { get; } = new ConcurrentDictionary<string, string>();

Expand Down Expand Up @@ -121,7 +112,7 @@ public static string GetDefaultBitcoinCoreDataDirOrEmptyString()
// This method removes the path and file extension.
//
// Given Wasabi releases are currently built using Windows, the generated assemblies contain
// the hardcoded "C:\Users\User\Desktop\WalletWasabi\.......\FileName.cs" string because that
// the hard coded "C:\Users\User\Desktop\WalletWasabi\.......\FileName.cs" string because that
// is the real path of the file, it doesn't matter what OS was targeted.
// In Windows and Linux that string is a valid path and that means Path.GetFileNameWithoutExtension
// can extract the file name but in the case of OSX the same string is not a valid path so, it assumes
Expand Down Expand Up @@ -232,30 +223,4 @@ public static string GetExecutablePath()
var fluentExecutable = Path.Combine(fullBaseDir, assemblyName);
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"{fluentExecutable}.exe" : $"{fluentExecutable}";
}

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);

/// <summary>
/// Reset the system sleep timer, this method has to be called from time to time to prevent sleep.
/// It does not prevent the display to turn off.
/// </summary>
public static async Task ProlongSystemAwakeAsync()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Reset the system sleep timer.
var result = SetThreadExecutionState(EXECUTION_STATE.ES_SYSTEM_REQUIRED);
if (result == 0)
{
throw new InvalidOperationException("SetThreadExecutionState failed.");
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// Prevent macOS system from idle sleep and keep it for 1 second. This will reset the idle sleep timer.
string shellCommand = $"caffeinate -i -t 1";
await ShellExecAsync(shellCommand, waitForExit: true).ConfigureAwait(false);
}
}
}
173 changes: 173 additions & 0 deletions WalletWasabi/Helpers/PowerSaving/BaseInhibitorTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Logging;
using WalletWasabi.Microservices;

namespace WalletWasabi.Helpers.PowerSaving;

public class BaseInhibitorTask : IPowerSavingInhibitorTask
{
/// <remarks>Guarded by <see cref="StateLock"/>.</remarks>
private bool _isDone;

protected BaseInhibitorTask(TimeSpan period, string reason, ProcessAsync process)
{
BasePeriod = period;
Reason = reason;
Process = process;
Cts = new CancellationTokenSource(period);

_ = WaitAsync();
}

/// <remarks>Guards <see cref="_isDone"/>.</remarks>
protected object StateLock { get; } = new();

/// <inheritdoc/>
public bool IsDone
{
get
{
lock (StateLock)
{
return _isDone;
}
}
}

/// <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 async Task WaitAsync()
{
try
{
await Process.WaitForExitAsync(Cts.Token).ConfigureAwait(false);

// This should be hit only when somebody externally kills the inhibiting process.
Logger.LogError("Inhibit process ended prematurely.");
}
catch (OperationCanceledException)
{
Logger.LogTrace("Elapsed time limit for the inhibitor task to live.");
}
kiminuo marked this conversation as resolved.
Show resolved Hide resolved
finally
{
if (!Process.HasExited)
{
// Process cannot stop on its own so we know it is actually running.
try
{
Process.Kill(entireProcessTree: true);
Logger.LogTrace("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.");
}
}

/// <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.LogTrace(logMessage);
}
}

public Task StopAsync()
{
lock (StateLock)
{
if (!_isDone)
{
Cts.Cancel();
}
}

return StoppedTcs.Task;
}

/// <summary>
/// Checks whether <paramref name="command"/> can be invoked with <c>--help</c> argument
/// to find out if the command is available on the machine.
/// </summary>
protected 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.LogDebug($"Failed to find out whether {command} is supported or not.");
Logger.LogTrace(ex);
}

return false;
}

protected static ProcessStartInfo GetProcessStartInfo(string command, string arguments)
{
return new()
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
}
}
Loading