Skip to content

Commit fa93c0f

Browse files
committed
fix(shell): add 10s/30s kill-on-timeout to ShellRunner.Run/RunPowerShell
Prevents auditpol.exe and other system tools from hanging the test host indefinitely on Intune/corporate machines. Process tree is killed after the timeout and (-1, string.Empty, 'timeout') is returned. Also removes [Fact(Skip=...)] from ToolVersionChecker_CheckAll_ReturnsResults: test gracefully handles missing tools (returns IsInstalled=false per tool) and passes on CI without any tools installed. Timeout raised 10s->60s. Total: 7,479 tweaks, 3,231 tests (0 failures, 0 skipped)
1 parent 861faf5 commit fa93c0f

2 files changed

Lines changed: 44 additions & 11 deletions

File tree

src/RegiLattice.Core/Services/ShellRunner.cs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace RegiLattice.Core;
1212
/// </summary>
1313
public static class ShellRunner
1414
{
15-
/// <summary>Run a process with explicit argument list.</summary>
15+
/// <summary>Run a process with explicit argument list. Kills the process if <paramref name="ct"/> is cancelled.</summary>
1616
public static async Task<(int ExitCode, string StdOut, string StdErr)> RunAsync(
1717
string fileName,
1818
IEnumerable<string> args,
@@ -38,18 +38,50 @@ public static class ShellRunner
3838
proc.Start();
3939
var stdoutTask = proc.StandardOutput.ReadToEndAsync(ct);
4040
var stderrTask = proc.StandardError.ReadToEndAsync(ct);
41-
await proc.WaitForExitAsync(ct).ConfigureAwait(false);
41+
try
42+
{
43+
await proc.WaitForExitAsync(ct).ConfigureAwait(false);
44+
}
45+
catch (OperationCanceledException)
46+
{
47+
try { proc.Kill(entireProcessTree: true); } catch (InvalidOperationException) { }
48+
return (-1, string.Empty, "timeout");
49+
}
4250
return (proc.ExitCode, await stdoutTask.ConfigureAwait(false), await stderrTask.ConfigureAwait(false));
4351
}
4452

4553
/// <summary>Run a PowerShell -Command script.</summary>
4654
public static Task<(int ExitCode, string StdOut, string StdErr)> RunPowerShellAsync(string script, CancellationToken ct = default) =>
4755
RunAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], ct);
4856

49-
/// <summary>Synchronous wrapper for RunAsync (used by TweakDef delegates).</summary>
50-
public static (int ExitCode, string StdOut, string StdErr) Run(string fileName, IEnumerable<string> args) =>
51-
RunAsync(fileName, args).GetAwaiter().GetResult();
57+
/// <summary>
58+
/// Synchronous wrapper for RunAsync (used by TweakDef delegates).
59+
/// Kills the process after <paramref name="timeoutMs"/> milliseconds (default 10 s).
60+
/// </summary>
61+
public static (int ExitCode, string StdOut, string StdErr) Run(string fileName, IEnumerable<string> args, int timeoutMs = 10_000)
62+
{
63+
using var cts = new CancellationTokenSource(timeoutMs);
64+
try
65+
{
66+
return RunAsync(fileName, args, cts.Token).GetAwaiter().GetResult();
67+
}
68+
catch (OperationCanceledException)
69+
{
70+
return (-1, string.Empty, "timeout");
71+
}
72+
}
5273

53-
/// <summary>Synchronous wrapper for RunPowerShellAsync.</summary>
54-
public static (int ExitCode, string StdOut, string StdErr) RunPowerShell(string script) => RunPowerShellAsync(script).GetAwaiter().GetResult();
74+
/// <summary>Synchronous wrapper for RunPowerShellAsync (default 30 s timeout).</summary>
75+
public static (int ExitCode, string StdOut, string StdErr) RunPowerShell(string script, int timeoutMs = 30_000)
76+
{
77+
using var cts = new CancellationTokenSource(timeoutMs);
78+
try
79+
{
80+
return RunPowerShellAsync(script, cts.Token).GetAwaiter().GetResult();
81+
}
82+
catch (OperationCanceledException)
83+
{
84+
return (-1, string.Empty, "timeout");
85+
}
86+
}
5587
}

tests/RegiLattice.GUI.Tests/PackageManagerValidationTests.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,15 @@ public void ToolInfo_NotInstalled_NullVersion()
108108
Assert.False(info.IsInstalled);
109109
}
110110

111-
// Integration test — spawns 16 real processes; excluded from normal test runs.
112-
[Fact(Skip = "Integration: spawns 16 real processes to detect installed tool versions. Run manually only.")]
111+
// Budget: 60s — spawns 16 parallel process checks; all return quickly even when
112+
// tools are not installed (gracefully returns ToolInfo with IsInstalled=false).
113+
[Fact]
113114
public async Task ToolVersionChecker_CheckAll_ReturnsResults()
114115
{
115-
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
116+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
116117
var results = await ToolVersionChecker.CheckAllAsync(cts.Token);
117118
Assert.NotEmpty(results);
118-
Assert.Equal(16, results.Count);
119+
Assert.Equal(16, results.Count); // 16 entries in CheckAllAsync — update if tool list changes
119120
Assert.All(results, r => Assert.NotNull(r.Name));
120121
}
121122

0 commit comments

Comments
 (0)