- An HTTP answer that breaks off while it is read (HttpIOException/IOException) is a
failed check ("Svaret blev afbrudt, mens det blev læst: …"), not an exception that
made the check rerun every tick. Other unforeseen network errors (IOException,
SocketException, HttpRequestException, AuthenticationException) become
"Forbindelsen fejlede: …"; one that hits the check's deadline is "Intet svar inden
for N sekunder". DNS: an invalid name (ArgumentException) is a failed lookup.
- The HTTP "Adresse" detail (a URL may carry query secrets) and the AX "Object server"
detail are Sensitive, so the API redacts them.
- SyntheticCheckRun's compiler-generated ToString no longer prints the SQL password.
- ProcessRunner (AX Business Connector logon): the PowerShell process tree is killed in
a finally whenever it has not exited, also when the agent stops (before: only on the
check's timeout, leaving an orphaned process and AX session). Output and errors are
capped at 64 K characters each; the rest is read and dropped so the process never
blocks on a full pipe.
- Tests: broken body, password redaction, bounded output, killed on agent stop.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
77 lines
2.9 KiB
C#
77 lines
2.9 KiB
C#
using System.Diagnostics;
|
|
using SQLInsight.Servers.Synthetic;
|
|
|
|
namespace SQLInsight.Servers.Tests;
|
|
|
|
/// <summary>The process the AX logon runs in: its output is bounded, and it never outlives the agent.</summary>
|
|
public class ProcessRunnerTests
|
|
{
|
|
[Fact]
|
|
public async Task Process_output_is_capped_but_read_to_the_end()
|
|
{
|
|
using var reader = new StringReader(new string('x', ProcessRunner.MaxOutputChars * 3) + "end");
|
|
|
|
var text = await ProcessRunner.ReadBoundedAsync(reader, ProcessRunner.MaxOutputChars);
|
|
|
|
Assert.Equal(ProcessRunner.MaxOutputChars, text.Length);
|
|
Assert.Equal(-1, reader.Peek()); // everything was read, so a process never blocks on a full pipe
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_process_is_killed_when_the_agent_stops()
|
|
{
|
|
if (!OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var marker = Path.Combine(Path.GetTempPath(), "sqlinsight-kill-" + Guid.NewGuid().ToString("N") + ".txt");
|
|
var powershell = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), @"WindowsPowerShell\v1.0\powershell.exe");
|
|
using var stop = new CancellationTokenSource(TimeSpan.FromMinutes(2));
|
|
var started = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
// Writes its process id, then would wait for a minute; the agent stops as soon as it has started.
|
|
var stopWhenStarted = Task.Run(async () =>
|
|
{
|
|
while (!File.Exists(marker) && started.Elapsed < TimeSpan.FromMinutes(1))
|
|
{
|
|
await Task.Delay(100);
|
|
}
|
|
|
|
await Task.Delay(500);
|
|
await stop.CancelAsync();
|
|
});
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => new ProcessRunner().RunAsync(powershell,
|
|
["-NoProfile", "-NonInteractive", "-Command", $"Set-Content -LiteralPath '{marker}' -Value $PID; Start-Sleep -Seconds 60"],
|
|
new Dictionary<string, string>(), TimeSpan.FromMinutes(5), stop.Token));
|
|
await stopWhenStarted;
|
|
Assert.True(started.Elapsed < TimeSpan.FromSeconds(55), $"The process was waited for ({started.Elapsed}).");
|
|
|
|
var pid = int.Parse((await File.ReadAllTextAsync(marker)).Trim(), System.Globalization.CultureInfo.InvariantCulture);
|
|
var gone = Stopwatch.StartNew();
|
|
while (IsRunning(pid))
|
|
{
|
|
Assert.True(gone.Elapsed < TimeSpan.FromSeconds(10), $"PowerShell ({pid}) was left running.");
|
|
await Task.Delay(100);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
File.Delete(marker);
|
|
}
|
|
|
|
static bool IsRunning(int pid)
|
|
{
|
|
try
|
|
{
|
|
using var process = Process.GetProcessById(pid);
|
|
return !process.HasExited;
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|