Skip to content

Commit 512f48c

Browse files
committed
fix(daemon): tolerate system info refresh failures
1 parent e7945c9 commit 512f48c

4 files changed

Lines changed: 106 additions & 13 deletions

File tree

MCServerLauncher.Daemon/Bootstrap/DaemonServiceComposition.cs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,25 @@ internal static void AttachDaemonLifecycle(HttpService httpService)
7373
daemonReportTimer.AutoReset = true;
7474
daemonReportTimer.Elapsed += async (sender, args) =>
7575
{
76-
var eventService = httpService.Resolver.GetRequiredService<IEventService>();
77-
var cell = httpService.Resolver.GetRequiredService<IAsyncTimedLazyCell<SystemInfo>>();
78-
var systemInfo = await cell.Value;
79-
eventService.OnDaemonReport(new DaemonReport(
80-
systemInfo.Os,
81-
systemInfo.Cpu,
82-
systemInfo.Mem,
83-
systemInfo.Drive,
84-
Application.StartTime.ToUnixTimeMilliSeconds(),
85-
systemInfo.Drives,
86-
systemInfo.DaemonVersion
87-
));
76+
try
77+
{
78+
var eventService = httpService.Resolver.GetRequiredService<IEventService>();
79+
var cell = httpService.Resolver.GetRequiredService<IAsyncTimedLazyCell<SystemInfo>>();
80+
var systemInfo = await cell.Value;
81+
eventService.OnDaemonReport(new DaemonReport(
82+
systemInfo.Os,
83+
systemInfo.Cpu,
84+
systemInfo.Mem,
85+
systemInfo.Drive,
86+
Application.StartTime.ToUnixTimeMilliSeconds(),
87+
systemInfo.Drives,
88+
systemInfo.DaemonVersion
89+
));
90+
}
91+
catch (Exception ex)
92+
{
93+
Serilog.Log.Debug(ex, "[DaemonReport] Failed to refresh daemon report");
94+
}
8895
};
8996
Application.OnStarted += () =>
9097
{

MCServerLauncher.Daemon/Utils/Status/CpuInfoHelper.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel;
2+
using System.Globalization;
23
using System.Runtime.InteropServices;
34
using MCServerLauncher.Common.Helpers;
45
using MCServerLauncher.Common.ProtoType.Status;
@@ -91,7 +92,7 @@ public static async Task<CpuInfo> GetCpuInfo()
9192
var cpuUsage =
9293
await SystemInfoHelper.RunCommandAsync("sh",
9394
"-c \"top -l 1 | grep 'CPU usage' | awk '{print $3}' | tr -d '%'\"")
94-
.MapTask(double.Parse);
95+
.MapTask(ParseMacOsCpuUsage);
9596

9697
var name = await SystemInfoHelper.RunCommandAsync("sysctl", "-n machdep.cpu.brand_string");
9798
var vendor = name.Split(' ')[0];
@@ -113,6 +114,17 @@ await SystemInfoHelper.RunCommandAsync("sh",
113114
throw new NotSupportedException("Unsupported OS");
114115
}
115116

117+
internal static double ParseMacOsCpuUsage(string output)
118+
{
119+
return double.TryParse(
120+
output.Trim(),
121+
NumberStyles.Float,
122+
CultureInfo.InvariantCulture,
123+
out var usage)
124+
? usage
125+
: 0;
126+
}
127+
116128
private static CpuInfo CreateCpuInfo(double usage)
117129
{
118130
return new CpuInfo(Vendor, Name, ThreadCount, usage, CoreCount, ThreadCount);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using MCServerLauncher.Daemon.Utils.Status;
2+
3+
namespace MCServerLauncher.ProtocolTests;
4+
5+
public class SystemInfoFailureToleranceTests
6+
{
7+
[Fact]
8+
[Trait("Category", "Status")]
9+
[Trait("Category", "FailureTolerance")]
10+
public void ParseMacOsCpuUsage_EmptyTopOutput_ReturnsZero()
11+
{
12+
var usage = CpuInfoHelper.ParseMacOsCpuUsage("");
13+
14+
Assert.Equal(0, usage);
15+
}
16+
17+
[Fact]
18+
[Trait("Category", "Status")]
19+
[Trait("Category", "FailureTolerance")]
20+
public void ParseMacOsCpuUsage_PercentageOutput_ParsesValue()
21+
{
22+
var usage = CpuInfoHelper.ParseMacOsCpuUsage("12.34");
23+
24+
Assert.Equal(12.34, usage);
25+
}
26+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Daemon Status Failure Tolerance Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Prevent daemon shutdown or background report refresh from crashing when macOS CPU usage command output is empty or malformed.
6+
7+
**Architecture:** Keep the existing system-info helpers. Add a small parser seam for macOS CPU usage and make the daemon-report timer callback contain system-info refresh failures.
8+
9+
**Tech Stack:** C# 14, .NET 10, xUnit protocol tests.
10+
11+
---
12+
13+
## Touched Areas
14+
15+
- `backend`
16+
- `tests`
17+
- `docs`
18+
19+
## Tasks
20+
21+
### Task 1: Reproduce CPU Usage Parse Failure
22+
23+
**Files:**
24+
- Create: `MCServerLauncher.ProtocolTests/SystemInfoFailureToleranceTests.cs`
25+
26+
- [x] Add a failing test for empty macOS CPU usage output.
27+
- [x] Add a passing baseline test for normal percentage output.
28+
29+
### Task 2: Make Status Refresh Tolerant
30+
31+
**Files:**
32+
- Modify: `MCServerLauncher.Daemon/Utils/Status/CpuInfoHelper.cs`
33+
- Modify: `MCServerLauncher.Daemon/Bootstrap/DaemonServiceComposition.cs`
34+
35+
- [x] Parse macOS CPU usage with `double.TryParse` and return `0` when output is empty or invalid.
36+
- [x] Catch daemon-report timer refresh failures so background status refresh does not become an unhandled exception during shutdown.
37+
38+
### Task 3: Verify
39+
40+
**Commands:**
41+
- [x] `dotnet test MCServerLauncher.ProtocolTests/MCServerLauncher.ProtocolTests.csproj --filter FullyQualifiedName~SystemInfoFailureToleranceTests /m:1`
42+
- [x] `dotnet build MCServerLauncher.Daemon/MCServerLauncher.Daemon.csproj /m:1`
43+
- [x] `dotnet test MCServerLauncher.ProtocolTests/MCServerLauncher.ProtocolTests.csproj /m:1`
44+
- [x] `git diff --check`
45+
46+
## Changelog
47+
48+
- 2026-06-19: Added macOS CPU usage parse fallback and contained daemon-report timer refresh failures.

0 commit comments

Comments
 (0)