Skip to content

Commit b177fb7

Browse files
committed
Fix release smoke-test exit
1 parent c058fef commit b177fb7

2 files changed

Lines changed: 178 additions & 74 deletions

File tree

App.xaml.cs

Lines changed: 100 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -63,50 +63,22 @@ public App()
6363
protected override void OnStartup(StartupEventArgs e)
6464
{
6565
// Parse command line arguments early so special startup modes can short-circuit normal flow.
66-
bool startMinimized = false;
67-
bool isAutostart = false;
68-
bool isSmokeTest = false;
69-
bool registerLaunchTask = false;
70-
bool launchedViaTask = false;
71-
#if DEBUG
72-
bool isTestMode = false;
73-
#endif
66+
var startupMode = StartupMode.Parse(e.Args);
7467
bool effectiveStartMinimized = false;
7568
ApplicationSettingsModel? loadedSettings = null;
7669

77-
foreach (var arg in e.Args)
70+
effectiveStartMinimized = startupMode.StartMinimized;
71+
72+
if (startupMode.IsSmokeTest)
7873
{
79-
switch (arg.ToLowerInvariant())
80-
{
81-
#if DEBUG
82-
case "--test":
83-
isTestMode = true;
84-
break;
85-
#endif
86-
case "--smoke-test":
87-
isSmokeTest = true;
88-
break;
89-
case "--start-minimized":
90-
startMinimized = true;
91-
break;
92-
case "--autostart":
93-
isAutostart = true;
94-
break;
95-
case "--startup": // Alternative startup argument
96-
isAutostart = true;
97-
startMinimized = true;
98-
break;
99-
case RegisterLaunchTaskArgument:
100-
registerLaunchTask = true;
101-
break;
102-
case LaunchedViaTaskArgument:
103-
launchedViaTask = true;
104-
break;
105-
}
74+
var smokeLogger = this.ServiceProvider.GetRequiredService<ILogger<App>>();
75+
var smokeTestResult = this.RunSmokeTestWithTimeout(smokeLogger, TimeSpan.FromSeconds(10));
76+
Environment.ExitCode = smokeTestResult;
77+
this.Shutdown(smokeTestResult);
78+
Environment.Exit(smokeTestResult);
79+
return;
10680
}
10781

108-
effectiveStartMinimized = startMinimized;
109-
11082
// Set up global exception handlers first
11183
AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException;
11284
this.DispatcherUnhandledException += this.OnDispatcherUnhandledException;
@@ -130,14 +102,14 @@ protected override void OnStartup(StartupEventArgs e)
130102
}
131103
else
132104
{
133-
if (launchedViaTask)
105+
if (startupMode.LaunchedViaTask)
134106
{
135107
logger.LogError("Application was launched via managed task marker but is still not elevated.");
136108
}
137109
#if DEBUG
138-
else if (!isSmokeTest && !isTestMode)
110+
else if (!startupMode.IsTestMode)
139111
#else
140-
else if (!isSmokeTest)
112+
else
141113
#endif
142114
{
143115
var launchedElevatedInstance = Task.Run(async () => await elevatedTaskService.TryRunLaunchTaskAsync()).GetAwaiter().GetResult();
@@ -148,7 +120,7 @@ protected override void OnStartup(StartupEventArgs e)
148120
return;
149121
}
150122

151-
if (!registerLaunchTask)
123+
if (!startupMode.RegisterLaunchTask)
152124
{
153125
logger.LogInformation("Managed elevated launch task is unavailable. Requesting one-time elevation to bootstrap persistent launch.");
154126
var restartInitiated = Task.Run(async () => await elevationService.RestartWithElevation(new[] { RegisterLaunchTaskArgument })).GetAwaiter().GetResult();
@@ -160,43 +132,38 @@ protected override void OnStartup(StartupEventArgs e)
160132
}
161133

162134
#if DEBUG
163-
if (!isSmokeTest && !isTestMode)
135+
if (!startupMode.IsTestMode)
164136
#else
165-
if (!isSmokeTest)
137+
if (true)
166138
#endif
167139
{
168140
logger.LogError("ThreadPilot requires administrator privileges and cannot continue without elevation.");
169141
this.ShowElevationRequiredMessage();
170142
this.Shutdown(1);
171143
return;
172144
}
173-
174-
logger.LogWarning("Application is running without administrator privileges in smoke test mode.");
175145
}
176146

177147
// Enforce single-instance after elevation bootstrap logic to avoid mutex races during handoff.
178-
if (!isSmokeTest)
148+
bool createdNew;
149+
this.singleInstanceMutex = new Mutex(initiallyOwned: true, name: "Global\\ThreadPilot_SingleInstance", createdNew: out createdNew);
150+
if (!createdNew)
179151
{
180-
bool createdNew;
181-
this.singleInstanceMutex = new Mutex(initiallyOwned: true, name: "Global\\ThreadPilot_SingleInstance", createdNew: out createdNew);
182-
if (!createdNew)
183-
{
184-
System.Windows.MessageBox.Show(
185-
"ThreadPilot is already running.",
186-
"Instance already open",
187-
MessageBoxButton.OK,
188-
MessageBoxImage.Information);
152+
System.Windows.MessageBox.Show(
153+
"ThreadPilot is already running.",
154+
"Instance already open",
155+
MessageBoxButton.OK,
156+
MessageBoxImage.Information);
189157

190-
this.Shutdown();
191-
return;
192-
}
158+
this.Shutdown();
159+
return;
193160
}
194161

195162
base.OnStartup(e);
196163

197164
// Check for test mode
198165
#if DEBUG
199-
if (isTestMode)
166+
if (startupMode.IsTestMode)
200167
{
201168
// Run in console test mode
202169
AllocConsole();
@@ -209,13 +176,6 @@ protected override void OnStartup(StartupEventArgs e)
209176
}
210177
#endif
211178

212-
if (isSmokeTest)
213-
{
214-
var smokeTestResult = Task.Run(async () => await this.RunSmokeTestAsync(logger)).GetAwaiter().GetResult();
215-
this.Shutdown(smokeTestResult);
216-
return;
217-
}
218-
219179
try
220180
{
221181
var settingsService = this.ServiceProvider.GetRequiredService<IApplicationSettingsService>();
@@ -226,7 +186,7 @@ protected override void OnStartup(StartupEventArgs e)
226186
var settings = settingsService.Settings;
227187
loadedSettings = settings;
228188
localizationService.ApplyLanguage(settings.Language);
229-
effectiveStartMinimized = startMinimized || settings.StartMinimized;
189+
effectiveStartMinimized = startupMode.StartMinimized || settings.StartMinimized;
230190
var useDarkTheme = settings.HasUserThemePreference
231191
? settings.UseDarkTheme
232192
: themeService.GetSystemUsesDarkTheme();
@@ -258,7 +218,7 @@ protected override void OnStartup(StartupEventArgs e)
258218
throw new InvalidOperationException("MainWindow could not be created");
259219
}
260220

261-
var startupWindowBehavior = StartupWindowBehavior.Resolve(isAutostart, effectiveStartMinimized);
221+
var startupWindowBehavior = StartupWindowBehavior.Resolve(startupMode.IsAutostart, effectiveStartMinimized);
262222
var showStartupSuggestion = loadedSettings != null
263223
&& StartupMinimizedSuggestionPolicy.ShouldShow(loadedSettings, startupWindowBehavior);
264224
mainWindow.ConfigureStartupMode(
@@ -301,16 +261,33 @@ protected override void OnStartup(StartupEventArgs e)
301261
}
302262
}
303263

304-
private async Task<int> RunSmokeTestAsync(ILogger logger)
264+
private int RunSmokeTestWithTimeout(ILogger logger, TimeSpan timeout)
265+
{
266+
var smokeTestTask = Task.Run(() => this.RunSmokeTest(logger));
267+
if (smokeTestTask.Wait(timeout))
268+
{
269+
return smokeTestTask.GetAwaiter().GetResult();
270+
}
271+
272+
logger.LogError("ThreadPilot smoke test timed out after {TimeoutSeconds} seconds", timeout.TotalSeconds);
273+
return 2;
274+
}
275+
276+
private int RunSmokeTest(ILogger logger)
305277
{
306278
try
307279
{
308280
logger.LogInformation("Starting ThreadPilot smoke test");
309281

310-
var settingsService = this.ServiceProvider.GetRequiredService<IApplicationSettingsService>();
311-
await settingsService.LoadSettingsAsync().ConfigureAwait(false);
312-
_ = this.ServiceProvider.GetRequiredService<ProcessViewModel>();
313-
_ = this.ServiceProvider.GetRequiredService<PowerPlanViewModel>();
282+
_ = this.ServiceProvider.GetRequiredService<ILoggerFactory>();
283+
_ = this.ServiceProvider.GetRequiredService<IApplicationSettingsService>();
284+
_ = this.ServiceProvider.GetRequiredService<IThemeService>();
285+
_ = this.ServiceProvider.GetRequiredService<ILocalizationService>();
286+
287+
if (!System.IO.Directory.Exists(AppContext.BaseDirectory))
288+
{
289+
throw new InvalidOperationException("Application base directory was not found.");
290+
}
314291

315292
logger.LogInformation("ThreadPilot smoke test completed successfully");
316293
return 0;
@@ -322,6 +299,55 @@ private async Task<int> RunSmokeTestAsync(ILogger logger)
322299
}
323300
}
324301

302+
private readonly struct StartupMode
303+
{
304+
public bool StartMinimized { get; init; }
305+
306+
public bool IsAutostart { get; init; }
307+
308+
public bool IsSmokeTest { get; init; }
309+
310+
public bool RegisterLaunchTask { get; init; }
311+
312+
public bool LaunchedViaTask { get; init; }
313+
314+
public bool IsTestMode { get; init; }
315+
316+
public static StartupMode Parse(IEnumerable<string> args)
317+
{
318+
var mode = default(StartupMode);
319+
foreach (var arg in args)
320+
{
321+
switch (arg.ToLowerInvariant())
322+
{
323+
case "--test":
324+
mode = mode with { IsTestMode = true };
325+
break;
326+
case "--smoke-test":
327+
mode = mode with { IsSmokeTest = true };
328+
break;
329+
case "--start-minimized":
330+
mode = mode with { StartMinimized = true };
331+
break;
332+
case "--autostart":
333+
mode = mode with { IsAutostart = true };
334+
break;
335+
case "--startup":
336+
mode = mode with { IsAutostart = true, StartMinimized = true };
337+
break;
338+
case RegisterLaunchTaskArgument:
339+
mode = mode with { RegisterLaunchTask = true };
340+
break;
341+
case LaunchedViaTaskArgument:
342+
mode = mode with { LaunchedViaTask = true };
343+
break;
344+
}
345+
}
346+
347+
return mode;
348+
}
349+
}
350+
325351
protected override void OnExit(ExitEventArgs e)
326352
{
327353
AppDomain.CurrentDomain.UnhandledException -= this.OnUnhandledException;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
namespace ThreadPilot.Core.Tests
2+
{
3+
public sealed class AppSmokeTestStartupTests
4+
{
5+
[Fact]
6+
public void OnStartup_HandlesSmokeTestBeforeElevationSingleInstanceAndWindowStartup()
7+
{
8+
var source = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "App.xaml.cs"));
9+
10+
var smokeTestBranchIndex = source.IndexOf("if (startupMode.IsSmokeTest)", StringComparison.Ordinal);
11+
var elevationIndex = source.IndexOf("GetRequiredService<IElevationService>", StringComparison.Ordinal);
12+
var mutexIndex = source.IndexOf("Global\\\\ThreadPilot_SingleInstance", StringComparison.Ordinal);
13+
var baseStartupIndex = source.IndexOf("base.OnStartup(e);", StringComparison.Ordinal);
14+
var mainWindowIndex = source.IndexOf("GetRequiredService<MainWindow>", StringComparison.Ordinal);
15+
16+
Assert.NotEqual(-1, smokeTestBranchIndex);
17+
Assert.True(smokeTestBranchIndex < elevationIndex);
18+
Assert.True(smokeTestBranchIndex < mutexIndex);
19+
Assert.True(smokeTestBranchIndex < baseStartupIndex);
20+
Assert.True(smokeTestBranchIndex < mainWindowIndex);
21+
}
22+
23+
[Fact]
24+
public void SmokeTestMode_ExitsTheProcessAfterShutdownToAvoidDispatcherOrTimerHangs()
25+
{
26+
var source = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "App.xaml.cs"));
27+
28+
var smokeTestBranch = ExtractSection(
29+
source,
30+
"if (startupMode.IsSmokeTest)",
31+
" // Set up global exception handlers first");
32+
33+
Assert.Contains("this.Shutdown(smokeTestResult);", smokeTestBranch, StringComparison.Ordinal);
34+
Assert.Contains("Environment.Exit(smokeTestResult);", smokeTestBranch, StringComparison.Ordinal);
35+
}
36+
37+
[Fact]
38+
public void RunSmokeTest_DoesNotResolveUiViewModelsOrMainWindow()
39+
{
40+
var source = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "App.xaml.cs"));
41+
var smokeTestMethod = ExtractSection(
42+
source,
43+
"private int RunSmokeTest",
44+
"protected override void OnExit");
45+
46+
Assert.DoesNotContain("ProcessViewModel", smokeTestMethod, StringComparison.Ordinal);
47+
Assert.DoesNotContain("PowerPlanViewModel", smokeTestMethod, StringComparison.Ordinal);
48+
Assert.DoesNotContain("MainWindow", smokeTestMethod, StringComparison.Ordinal);
49+
}
50+
51+
private static string ExtractSection(string source, string startMarker, string endMarker)
52+
{
53+
var startIndex = source.IndexOf(startMarker, StringComparison.Ordinal);
54+
Assert.NotEqual(-1, startIndex);
55+
56+
var endIndex = source.IndexOf(endMarker, startIndex, StringComparison.Ordinal);
57+
Assert.NotEqual(-1, endIndex);
58+
59+
return source[startIndex..endIndex];
60+
}
61+
62+
private static string GetRepositoryRoot()
63+
{
64+
var directory = new DirectoryInfo(AppContext.BaseDirectory);
65+
while (directory != null && !File.Exists(Path.Combine(directory.FullName, "ThreadPilot.csproj")))
66+
{
67+
directory = directory.Parent;
68+
}
69+
70+
if (directory == null)
71+
{
72+
throw new InvalidOperationException("Repository root was not found.");
73+
}
74+
75+
return directory.FullName;
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)