sleep-pc:使用.NET Native AOT构建的Windows定时休眠工具
背景:让电脑休眠!
让笔记本电脑进入休眠状态似乎比想象中困难得多。无论是半夜自动唤醒加热背包的MacBook,还是拒绝休眠的Windows笔记本,我总是遇到各种问题。
某个周末,我正与后一个问题斗争:无法让Windows笔记本在Windows Media Player Legacy播放完播放列表后休眠。作为习惯在后台播放视频入睡的人,这非常令人烦恼…
我尝试了所有故障排除方法:电源计划设置正确,运行并探索了powercfg。最终我厌倦了,编写了一个小应用,在给定时间后强制笔记本电脑休眠。
使用SetSuspendState让电脑休眠
最初版本只需几分钟就完成了:
1
2
3
4
5
6
7
8
9
10
11
|
using System.Runtime.InteropServices;
var wait = TimeSpan.FromSeconds(60 * 60); // 1小时
Console.WriteLine("Waiting for {wait}");
Thread.Sleep(wait);
Console.WriteLine("Sleeping!");
SetSuspendState(false, false, false);
[DllImport("PowrProf.dll", SetLastError = true)]
static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent);
|
这个简单程序休眠固定时间(对我来说是1小时),然后通过P/Invoke调用PowrProf.dll中的SetSuspendState方法(仅限Windows),让笔记本电脑休眠。
虽然这里设置hibernate=false,但根据系统设置,Windows通常仍会休眠,特别是在启用混合睡眠时。对我来说这会休眠,这正合我意。
添加更多功能
考虑到这只是个小工具,我的想法是:
- 添加适当的命令行参数解析
- 使用新的.NET 10工具支持打包为Native AOT工具
- 美化控制台输出
- 允许"试运行"选项
使用ConsoleAppFramework创建控制台应用
有很多选项可供选择:
- System.CommandLine
- Spectre.Console.Cli
- ConsoleAppFramework
ConsoleAppFramework v5是零依赖、零开销、零反射、零分配、AOT安全的CLI框架,由C#源生成器提供支持;实现极高的性能、最快的启动时间(使用NativeAOT)和最小的二进制大小。
将概念验证转换为"真正"的控制台应用非常简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
using System.ComponentModel.DataAnnotations;
using ConsoleAppFramework;
using System.Runtime.InteropServices;
await ConsoleApp.RunAsync(args, App.Countdown);
static partial class App
{
/// <summary>
/// 在发送计算机到休眠前等待指定的秒数
/// </summary>
/// <param name="sleepDelaySeconds">-s|--seconds, 计算机进入休眠前的时间(秒)。默认为1小时</param>
/// <param name="dryRun">如果为true,打印消息而不是休眠</param>
/// <param name="ct">用于取消执行</param>
/// <returns></returns>
public static async Task Countdown(
[Range(1, 99 * 60 * 60)]uint sleepDelaySeconds = 60 * 60,
bool dryRun = false,
CancellationToken ct = default)
{
var wait = TimeSpan.FromSeconds(sleepDelaySeconds);
Console.WriteLine("Waiting for {wait}");
await Task.Delay(wait, ct);
Console.WriteLine("Sleeping!");
if (!dryRun)
{
SetSuspendState(false, false, false);
}
}
[DllImport("PowrProf.dll", SetLastError = true)]
static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent);
}
|
添加ConsoleAppFramework支持只需:
- 调用源生成的ConsoleApp.RunAsync()方法
- 用XML注释装饰目标方法,描述命令、参数和验证要求
添加Native AOT支持
添加Native AOT支持很简单,因为ConsoleAppFramework已支持Native AOT,且应用没有做太多其他事情。我在项目文件中添加了<PublishAot>true</PublishAot>设置,没有构建警告。
我切换到新的[LibraryImport]源生成器属性:
1
2
3
4
5
6
|
[LibraryImport("PowrProf.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)] // 返回实际上是BOOL (int)
private static partial bool SetSuspendState(
[MarshalAs(UnmanagedType.U1)] bool hibernate,
[MarshalAs(UnmanagedType.U1)] bool forceCritical,
[MarshalAs(UnmanagedType.U1)] bool disableWakeEvent);
|
使用dotnet publish -r win-x64 -c Release发布Native AOT版本后,生成的sleep-pc.exe文件为3.3MB—对于包含所有运行时组件的.NET应用来说还不错!
打包为Native AOT工具
我选择了"折衷包"方法:
- “根"sleep-pc.nupkg包含工具的.NET 8框架相关构建,为.NET 10 SDK用户指向平台特定版本
- 打包的应用具有
<RollForward>Major</RollForward>,如果用户安装了.NET 9而没有.NET 8运行时,它将在.NET 9上运行
- 平台特定包仅包含Native AOT资源,在用户安装.NET 10+时使用
最终.csproj文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>SleepPc</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- NuGet/工具设置 -->
<PackAsTool>true</PackAsTool>
<ToolCommandName>sleep-pc</ToolCommandName>
<PackageId>sleep-pc</PackageId>
<PackageVersion>0.1.0</PackageVersion>
<Authors>Andrew Lock</Authors>
<Description>在给定时间后发送Windows笔记本电脑休眠的工具</Description>
<PackageTags>sleep;timer;windows;tool</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RollForward>Major</RollForward>
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
</PropertyGroup>
<!-- 支持NuGet工具的条件框架版本 -->
<PropertyGroup Condition="$(RuntimeIdentifier) != ''">
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="$(RuntimeIdentifier) == ''">
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework) == 'net10.0'">
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<PublishAot>true</PublishAot>
<!-- Native AOT大小优化位 -->
<DebuggerSupport>false</DebuggerSupport>
<EventSourceSupport>false</EventSourceSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetricsSupport>false</MetricsSupport>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<StripSymbols>true</StripSymbols>
<InvariantGlobalization>true</InvariantGlobalization>
<IlcDisableReflection>true</IlcDisableReflection>
<IlcTrimMetadata>true</IlcTrimMetadata>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<!-- 用于Sizoscope分析 -->
<IlcGenerateMstatFile>true</IlcGenerateMstatFile>
<IlcGenerateDgmlFile>true</IlcGenerateDgmlFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConsoleAppFramework" Version="5.5.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
|
改进控制台输出
我决定不使用Spectre.Console,因为它技术上不兼容AOT,而且我只需要少量动态功能。我只想添加倒计时器,而不是让应用停留在Thread.Sleep()或Task.Wait()中。
通过在Console.WriteLine()中发送退格来"替换"控制台中的行:
1
2
3
4
5
6
7
|
var wait = TimeSpan.FromSeconds(sleepDelaySeconds);
// 写入初始文本
Console.Write("⏰ 剩余时间: ");
// 发送一堆退格后跟格式化文本
Console.Write("\b\b\b\b\b\b\b\b{0:hh\\:mm\\:ss}", wait);
|
运行时,控制台看起来在原地更新。虽然不完美,但对我来说足够好。
为了让倒计时正常运行,我们必须切换到循环以确保定期更新控制台:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
// 计算结束时间以避免漂移
var wait = TimeSpan.FromSeconds(sleepDelaySeconds);
var deadline = DateTimeOffset.UtcNow.Add(wait);
var dryRunText = (dryRun ? " (试运行)" : "");
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine($@"将在 {wait:hh\:mm\:ss} 后休眠,时间 {deadline:dd MMM yy HH:mm:ss}{dryRunText}");
Console.WriteLine("按 ctrl-c 取消");
Console.WriteLine();
Console.Write("⏰ 剩余时间: ");
while (!ct.IsCancellationRequested)
{
// 更新时钟
Console.Write("\b\b\b\b\b\b\b\b{0:hh\\:mm\\:ss}", wait);
try
{
await Task.Delay(1_000, ct);
}
catch
{
// 用户取消 (Ctrl+C)
goto canceled;
}
wait = deadline - DateTimeOffset.UtcNow;
if (wait <= TimeSpan.Zero)
{
Console.WriteLine();
Console.WriteLine($"💤 达到截止时间,正在休眠{dryRunText}");
if (!dryRun)
{
try
{
SetSuspendState(false, false, false);
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ 触发休眠错误: {ex}");
}
}
return;
}
}
canceled:
Console.WriteLine();
Console.WriteLine("❌ 休眠已取消");
|
总结
在本文中,我描述了一个小型.NET工具,用于在计时器到期后强制Windows PC进入休眠。我从展示用于让笔记本电脑休眠的Win32 API和小型概念验证应用开始,然后将此实现扩展为Native AOT编译应用,打包为.NET工具,并添加改进的控制台输出。