Skip to content

Commit 7760167

Browse files
committed
fix: address remaining PR 84 review threads
1 parent 4736732 commit 7760167

File tree

4 files changed

+205
-19
lines changed

4 files changed

+205
-19
lines changed

DotPilot.Tests/Chat/ViewModels/ChatModelTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,34 @@ public async Task RefreshIgnoresCancellationDuringWorkspaceProbe()
100100
(await model.FeedbackMessage).Should().BeEmpty();
101101
}
102102

103+
[Test]
104+
public async Task RefreshInvalidatesTheFleetProviderSnapshotAfterProviderChanges()
105+
{
106+
using var commandScope = CodexCliTestScope.Create(nameof(ChatModelTests));
107+
commandScope.WriteVersionCommand("codex", "codex version 1.0.0");
108+
commandScope.WriteCodexMetadata("gpt-5.4", "gpt-5.4");
109+
await using var fixture = await CreateFixtureAsync();
110+
var model = ActivatorUtilities.CreateInstance<ChatModel>(fixture.Provider);
111+
112+
var initialBoard = await model.FleetBoard;
113+
initialBoard.Should().NotBeNull();
114+
initialBoard!.Providers.Should().Contain(provider =>
115+
provider.DisplayName == "Codex" &&
116+
provider.StatusLabel == "Disabled");
117+
118+
(await fixture.WorkspaceState.UpdateProviderAsync(
119+
new UpdateProviderPreferenceCommand(AgentProviderKind.Codex, true),
120+
CancellationToken.None)).ShouldSucceed();
121+
122+
await model.Refresh(CancellationToken.None);
123+
124+
var refreshedBoard = await model.FleetBoard;
125+
refreshedBoard.Should().NotBeNull();
126+
refreshedBoard!.Providers.Should().Contain(provider =>
127+
provider.DisplayName == "Codex" &&
128+
provider.StatusLabel == "Ready");
129+
}
130+
103131
[Test]
104132
public async Task FleetBoardShowsTheActiveSessionWhileStreamingAndClearsAfterCompletion()
105133
{
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System.Diagnostics;
2+
using DotPilot.Core.ChatSessions;
3+
using DotPilot.Core.ControlPlaneDomain;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace DotPilot.Tests.Host.Power;
7+
8+
public sealed class DesktopSleepPreventionServiceTests
9+
{
10+
[Test]
11+
public async Task ServiceTracksSessionActivityLifecycle()
12+
{
13+
if (OperatingSystem.IsLinux() && !CommandExists("systemd-inhibit"))
14+
{
15+
Assert.Ignore("systemd-inhibit is not available on this machine.");
16+
}
17+
18+
if (OperatingSystem.IsMacOS() && !CommandExists("caffeinate"))
19+
{
20+
Assert.Ignore("caffeinate is not available on this machine.");
21+
}
22+
23+
var services = new ServiceCollection();
24+
services.AddLogging();
25+
services.AddSingleton(TimeProvider.System);
26+
services.AddAgentSessions(new AgentSessionStorageOptions
27+
{
28+
UseInMemoryDatabase = true,
29+
InMemoryDatabaseName = Guid.NewGuid().ToString("N"),
30+
});
31+
services.AddSingleton<DesktopSleepPreventionService>();
32+
33+
await using var provider = services.BuildServiceProvider();
34+
var monitor = provider.GetRequiredService<ISessionActivityMonitor>();
35+
var sleepPrevention = provider.GetRequiredService<DesktopSleepPreventionService>();
36+
37+
using var lease = monitor.BeginActivity(
38+
new SessionActivityDescriptor(
39+
SessionId.New(),
40+
"Sleep prevention session",
41+
AgentProfileId.New(),
42+
"Sleep agent",
43+
"Debug Provider"));
44+
45+
await WaitForAsync(static service => service.IsSleepPreventionActive, sleepPrevention);
46+
47+
lease.Dispose();
48+
49+
await WaitForAsync(static service => !service.IsSleepPreventionActive, sleepPrevention);
50+
}
51+
52+
private static async Task WaitForAsync(
53+
Func<DesktopSleepPreventionService, bool> predicate,
54+
DesktopSleepPreventionService service)
55+
{
56+
var timeoutAt = DateTimeOffset.UtcNow.AddSeconds(5);
57+
while (DateTimeOffset.UtcNow < timeoutAt)
58+
{
59+
if (predicate(service))
60+
{
61+
return;
62+
}
63+
64+
await Task.Delay(50);
65+
}
66+
67+
predicate(service).Should().BeTrue();
68+
}
69+
70+
private static bool CommandExists(string commandName)
71+
{
72+
try
73+
{
74+
var startInfo = new ProcessStartInfo
75+
{
76+
FileName = "sh",
77+
RedirectStandardOutput = true,
78+
RedirectStandardError = true,
79+
UseShellExecute = false,
80+
CreateNoWindow = true,
81+
};
82+
startInfo.ArgumentList.Add("-c");
83+
startInfo.ArgumentList.Add($"command -v {commandName}");
84+
85+
using var process = Process.Start(startInfo);
86+
if (process is null)
87+
{
88+
return false;
89+
}
90+
91+
process.WaitForExit(2000);
92+
return process.ExitCode == 0;
93+
}
94+
catch
95+
{
96+
return false;
97+
}
98+
}
99+
}

DotPilot/Host/Power/DesktopSleepPreventionService.cs

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public sealed class DesktopSleepPreventionService : IDisposable
1818
private readonly Lock gate = new();
1919
private Process? inhibitorProcess;
2020
private bool isSleepPreventionActive;
21+
private bool isSleepPreventionPending;
22+
private long stateVersion;
2123

2224
public DesktopSleepPreventionService(
2325
ISessionActivityMonitor sessionActivityMonitor,
@@ -71,62 +73,68 @@ private void ApplySessionActivityState()
7173

7274
private void AcquireSleepPrevention()
7375
{
74-
lock (gate)
76+
var acquisition = TryBeginAcquisition();
77+
if (!acquisition.ShouldAcquire)
7578
{
76-
if (isSleepPreventionActive)
77-
{
78-
return;
79-
}
79+
return;
8080
}
8181

8282
try
8383
{
8484
if (OperatingSystem.IsWindows())
8585
{
8686
AcquireWindowsSleepPrevention();
87-
SetActiveState("SetThreadExecutionState");
87+
CompleteWindowsAcquisition(acquisition.Version, "SetThreadExecutionState");
8888
return;
8989
}
9090

9191
if (OperatingSystem.IsMacOS())
9292
{
93-
SetProcessState(StartMacOsInhibitorProcess());
93+
CompleteProcessAcquisition(acquisition.Version, StartMacOsInhibitorProcess());
9494
return;
9595
}
9696

9797
if (OperatingSystem.IsLinux())
9898
{
99-
SetProcessState(StartLinuxInhibitorProcess());
99+
CompleteProcessAcquisition(acquisition.Version, StartLinuxInhibitorProcess());
100+
return;
100101
}
102+
103+
CancelPendingAcquisition(acquisition.Version);
101104
}
102105
catch (Exception exception)
103106
{
104107
ShellSleepPreventionLog.AcquireFailed(logger, exception);
108+
CancelPendingAcquisition(acquisition.Version);
105109
ReleaseSleepPrevention();
106110
}
107111
}
108112

109113
private void ReleaseSleepPrevention()
110114
{
111115
Process? processToStop;
116+
bool shouldReleaseWindows;
112117
bool wasActive;
113118

114119
lock (gate)
115120
{
116-
if (!isSleepPreventionActive && inhibitorProcess is null)
121+
stateVersion++;
122+
if (!isSleepPreventionActive && !isSleepPreventionPending && inhibitorProcess is null)
117123
{
118124
return;
119125
}
120126

121-
if (OperatingSystem.IsWindows() && isSleepPreventionActive)
122-
{
123-
ReleaseWindowsSleepPrevention();
124-
}
125-
127+
shouldReleaseWindows = OperatingSystem.IsWindows() && isSleepPreventionActive;
126128
processToStop = inhibitorProcess;
127129
inhibitorProcess = null;
128130
wasActive = isSleepPreventionActive;
129131
isSleepPreventionActive = false;
132+
isSleepPreventionPending = false;
133+
}
134+
135+
if (shouldReleaseWindows)
136+
{
137+
ReleaseWindowsSleepPrevention();
130138
}
131139

132140
StopProcess(processToStop);
@@ -139,29 +147,79 @@ private void ReleaseSleepPrevention()
139147
StateChanged?.Invoke(this, EventArgs.Empty);
140148
}
141149

142-
private void SetProcessState(Process process)
150+
private (bool ShouldAcquire, long Version) TryBeginAcquisition()
151+
{
152+
lock (gate)
153+
{
154+
if (isSleepPreventionActive || isSleepPreventionPending)
155+
{
156+
return (false, stateVersion);
157+
}
158+
159+
stateVersion++;
160+
isSleepPreventionPending = true;
161+
return (true, stateVersion);
162+
}
163+
}
164+
165+
private void CompleteProcessAcquisition(long version, Process process)
143166
{
167+
var acquired = false;
144168
lock (gate)
145169
{
146-
inhibitorProcess = process;
147-
isSleepPreventionActive = true;
170+
if (isSleepPreventionPending && !isSleepPreventionActive && version == stateVersion)
171+
{
172+
inhibitorProcess = process;
173+
isSleepPreventionActive = true;
174+
isSleepPreventionPending = false;
175+
acquired = true;
176+
}
177+
}
178+
179+
if (!acquired)
180+
{
181+
StopProcess(process);
182+
return;
148183
}
149184

150185
ShellSleepPreventionLog.Acquired(logger, process.ProcessName);
151186
StateChanged?.Invoke(this, EventArgs.Empty);
152187
}
153188

154-
private void SetActiveState(string mechanism)
189+
private void CompleteWindowsAcquisition(long version, string mechanism)
155190
{
191+
var acquired = false;
156192
lock (gate)
157193
{
158-
isSleepPreventionActive = true;
194+
if (isSleepPreventionPending && !isSleepPreventionActive && version == stateVersion)
195+
{
196+
isSleepPreventionActive = true;
197+
isSleepPreventionPending = false;
198+
acquired = true;
199+
}
200+
}
201+
202+
if (!acquired)
203+
{
204+
ReleaseWindowsSleepPrevention();
205+
return;
159206
}
160207

161208
ShellSleepPreventionLog.Acquired(logger, mechanism);
162209
StateChanged?.Invoke(this, EventArgs.Empty);
163210
}
164211

212+
private void CancelPendingAcquisition(long version)
213+
{
214+
lock (gate)
215+
{
216+
if (isSleepPreventionPending && !isSleepPreventionActive && version == stateVersion)
217+
{
218+
isSleepPreventionPending = false;
219+
}
220+
}
221+
}
222+
165223
private static void AcquireWindowsSleepPrevention()
166224
{
167225
var result = SetThreadExecutionState(EsContinuous | EsSystemRequired);

DotPilot/Presentation/Chat/ViewModels/ChatModel.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public async ValueTask Refresh(CancellationToken cancellationToken)
9494
return;
9595
}
9696

97+
fleetProviderSnapshotStale = true;
9798
_workspaceRefresh.Raise();
9899
_sessionRefresh.Raise();
99100
await EnsureSelectedChatAsync(cancellationToken);

0 commit comments

Comments
 (0)