Deadlines
- Event log over RPC (MS-EVEN6): the whole read has a deadline (OperationTimeout per log);
the token cancels the EventLogSession's operations and is checked before each
FormatDescription, and the agent stops waiting even if the native call ignores it.
Reported as ServerAccessProblem.Timeout ("svarede ikke i tide over hændelsesloggens
protokol (RPC) inden for N sekunder").
- Every CIM call runs on the thread pool and is awaited with the caller's token; a call
that ignores cancellation is abandoned and the CIM session is closed once it returns.
- Each server cycle has a deadline (new SQLInsight:Servers:CycleTimeout, default 5 min,
1-30 min, at least 2 x OperationTimeout): the areas not read by then are "ikke målbar"
("Serveren svarede ikke inden for N minutter, så indsamlingen blev afbrudt; …"),
logged once, and the slot is freed.
- Scheduled tasks: run information is read within a time budget (CycleTimeout / 3); the
rest keep their state without the last run, and the coverage note says so.
- "Test server" has an overall deadline (WindowsServerTester, default 2 min); areas not
done in time are reported as not answered, so the result is always stored.
Backoff and memory
- A server that does not answer (Unreachable/Timeout, or a timed-out cycle) is backed
off 1, 2, 5, 10, then 15 minutes, reset when it answers.
- Protocol Auto: the protocol that worked (e.g. DCOM after WS-Man was refused, also for
access denied) is tried first for an hour, then WS-Man is tried again; a server whose
event log RPC failed is read over CIM for an hour.
- Over WS-Man with read-only WinRM rights Task Scheduler's GetInfoByName is denied: with
Auto the tasks are read over DCOM instead (remembered for the server); with WSMan forced
the reason says to choose Auto or DCOM. The advice "winrm configSDDL default" (broader
WinRM rights) is gone; texts follow the grant script (-WinRM read right,
-EnableFirewall -AgentAddress), not Remote Management Users.
Events
- Each log continues from its own mark (ServerEventSnapshot.LogReadUntil, a small additive
field in Core): a truncated System log no longer loses the events between its last
read event and the newest Application event.
- CIM fallback: Win32_NTLogEvent is streamed and only the oldest max per log are kept
(memory bounded, deterministic order), consistent with the RPC read and the coverage
text, which now names the logs that had more.
Other
- CimInstances (and method results) are mapped to SQLInsight's types and disposed at once.
- Options are clamped when bound (ServerOptions/SyntheticCheckOptions.Normalize):
OperationTimeout 5 s-5 min, MaxEventsPerCollection 10-10000, MaxWatched 1-200,
MaxParallelServers/Checks 1-32, retention days >= 1, ForecastHistoryDays 1-365,
FailuresBeforeFinding/SlowRunsBeforeFinding 1-99.
- Shutdown: the timer's cancellation is caught, running cycles are awaited before the
throttle is disposed; a cycle's time is taken when it gets its slot; a TaskCanceledException
that is not shutdown no longer ends the service.
- Retention maintenance also runs (hourly) when SQLInsight:Servers:Enabled is false but
synthetic checks are on.
- LocalCimTests run alone (collection without parallelization), share one warmed-up
session, use the same 2 min operation timeout and a 5 min deadline per test, a smaller
workload, and report timings in assertion messages.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
153 lines
8.5 KiB
C#
153 lines
8.5 KiB
C#
using SQLInsight.Core.Servers;
|
|
using SQLInsight.Servers;
|
|
using SQLInsight.Servers.Synthetic;
|
|
|
|
namespace SQLInsight.Servers.Tests;
|
|
|
|
public class ServerErrorTests
|
|
{
|
|
private static ServerAccessException Describe(uint? code, string? kind = null, string message = "Windows' tekst", string? area = ServerArea.Services, string protocol = "WS-Man") =>
|
|
ServerErrors.Describe(code, kind, message, area, "aos01", @"FIRMA\AGENT01$", protocol);
|
|
|
|
[Fact]
|
|
public void Access_denied_names_the_right_and_the_script_never_local_admin()
|
|
{
|
|
var error = Describe(ServerErrors.AccessDenied);
|
|
|
|
Assert.Equal(ServerAccessProblem.AccessDenied, error.Problem);
|
|
Assert.Contains(@"FIRMA\AGENT01$", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("Service Control Manager", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("grant-windows-server.ps1", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("ikke være lokal administrator", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("0x80070005", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("Event Log Readers", Describe(ServerErrors.WbemAccessDenied, area: ServerArea.Events).Message, StringComparison.Ordinal);
|
|
Assert.Contains("Distributed COM Users", Describe(null, "AccessDenied", area: null, protocol: "DCOM").Message, StringComparison.Ordinal);
|
|
var wsman = Describe(null, "AccessDenied", area: null, protocol: "WS-Man").Message;
|
|
Assert.Contains("læseret i WinRM (grant-windows-server.ps1 -WinRM)", wsman, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("Remote Management Users", wsman, StringComparison.Ordinal); // that group may also run PowerShell remotely
|
|
Assert.DoesNotContain("configSDDL", wsman, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_namespace_unreachable_and_timeouts()
|
|
{
|
|
var tasks = Describe(ServerErrors.InvalidNamespace, "InvalidNamespace", area: ServerArea.Tasks);
|
|
Assert.Equal(ServerAccessProblem.NotAvailable, tasks.Problem);
|
|
Assert.Contains("Windows Server 2012", tasks.Message, StringComparison.Ordinal);
|
|
|
|
var rpc = Describe(ServerErrors.RpcUnavailable, "Failed", "The RPC server is unavailable.", area: null, protocol: "DCOM");
|
|
Assert.Equal(ServerAccessProblem.Unreachable, rpc.Problem);
|
|
Assert.Contains("TCP 135", rpc.Message, StringComparison.Ordinal);
|
|
|
|
var winrm = Describe(null, "Failed", "The client cannot connect to the destination specified in the request. ... WinRM ...", area: null);
|
|
Assert.Equal(ServerAccessProblem.Unreachable, winrm.Problem);
|
|
Assert.Contains("5985", winrm.Message, StringComparison.Ordinal);
|
|
|
|
Assert.Equal(ServerAccessProblem.Timeout, Describe(ServerErrors.MiTimeout, "Failed", "Timed out").Problem);
|
|
Assert.Equal(ServerAccessProblem.Other, Describe(0x80004005, "Failed", "Unspecified error").Problem);
|
|
}
|
|
|
|
[Fact]
|
|
public void Hints_by_problem()
|
|
{
|
|
Assert.Contains("tildelingsscriptet", WindowsServerTester.Hint(ServerAccessProblem.AccessDenied), StringComparison.Ordinal);
|
|
Assert.Contains("5985", WindowsServerTester.Hint(ServerAccessProblem.Unreachable), StringComparison.Ordinal);
|
|
Assert.Equal(UnsupportedWindowsServerSource.Reason, WindowsServerTester.Hint(ServerAccessProblem.NotSupported));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_agent_not_on_windows_says_so()
|
|
{
|
|
var error = await Assert.ThrowsAsync<ServerAccessException>(() => new UnsupportedWindowsServerSource().ConnectAsync("aos01", CancellationToken.None));
|
|
var test = await WindowsServerTester.TestAsync(new UnsupportedWindowsServerSource(), "aos01", null, DateTimeOffset.UtcNow, CancellationToken.None);
|
|
|
|
Assert.Equal(ServerAccessProblem.NotSupported, error.Problem);
|
|
Assert.False(test.Reachable);
|
|
Assert.Equal(UnsupportedWindowsServerSource.Reason, test.Error);
|
|
}
|
|
}
|
|
|
|
public class CimQueryTests
|
|
{
|
|
[Fact]
|
|
public void Wql_literals_and_times()
|
|
{
|
|
Assert.Equal(@"AOS60$01", CimQueries.Wql("AOS60$01"));
|
|
Assert.Equal(@"a\\b\'c", CimQueries.Wql(@"a\b'c"));
|
|
Assert.Equal("SELECT Name, DisplayName, State, StartMode, ExitCode FROM Win32_Service WHERE Name = 'O\\'Brien'", CimQueries.SingleServiceQuery("O'Brien"));
|
|
var at = new DateTimeOffset(2026, 10, 2, 14, 5, 6, TimeSpan.FromHours(2));
|
|
Assert.Equal("20261002120506.000000+000", CimQueries.Dmtf(at));
|
|
Assert.Equal("*[System[(Level=1 or Level=2) and TimeCreated[@SystemTime>'2026-10-02T12:05:06.000Z']]]", CimQueries.EventXPath(at));
|
|
Assert.Contains("Logfile = 'System' AND EventType = 1 AND TimeGenerated > '20261002120506.000000+000'", CimQueries.EventQuery("System", at), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Queries_only_read()
|
|
{
|
|
var queries = new[]
|
|
{
|
|
CimQueries.OperatingSystemQuery, CimQueries.ComputerSystemQuery, CimQueries.ProcessorQuery, CimQueries.MemoryQuery,
|
|
CimQueries.VolumeQuery, CimQueries.LogicalDiskQuery, CimQueries.ServiceQuery, CimQueries.TaskQuery,
|
|
};
|
|
|
|
Assert.All(queries, q => Assert.StartsWith("SELECT ", q, StringComparison.Ordinal));
|
|
}
|
|
}
|
|
|
|
public class AxBusinessConnectorTests
|
|
{
|
|
private static readonly BusinessConnectorInstallation Ax2012 = BusinessConnectorLocator.Describe(@"C:\Program Files\Microsoft Dynamics AX\60\BusinessConnector\Bin\Microsoft.Dynamics.BusinessConnectorNet.dll");
|
|
private static readonly BusinessConnectorInstallation Ax2009 = BusinessConnectorLocator.Describe(@"C:\Program Files (x86)\Microsoft Dynamics AX\50\Client\Bin\Microsoft.Dynamics.BusinessConnectorNet.dll");
|
|
|
|
[Fact]
|
|
public void Versions_and_bitness_from_the_path()
|
|
{
|
|
Assert.Equal(("AX 2012", false, false), (Ax2012.Version, Ax2012.Is32Bit, Ax2012.NeedsClr2));
|
|
Assert.Equal(("AX 2009", true, true), (Ax2009.Version, Ax2009.Is32Bit, Ax2009.NeedsClr2));
|
|
}
|
|
|
|
[Fact]
|
|
public void Locator_uses_the_configured_path_first()
|
|
{
|
|
var configured = @"D:\AX\Microsoft.Dynamics.BusinessConnectorNet.dll";
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
Assert.Equal(configured, new BusinessConnectorLocator(configured, p => p == configured).Find()!.Path);
|
|
}
|
|
|
|
Assert.Null(new BusinessConnectorLocator(null, _ => false).Find());
|
|
}
|
|
|
|
[Fact]
|
|
public void Command_picks_powershell_and_never_puts_parameters_on_the_command_line()
|
|
{
|
|
var (file2012, args2012) = AxLogonClient.Command(Ax2012, @"C:\Windows");
|
|
var (file2009, args2009) = AxLogonClient.Command(Ax2009, @"C:\Windows");
|
|
var environment = AxLogonClient.Environment(Ax2012, "DAT", "01@aos01:2712", null);
|
|
|
|
Assert.Equal(@"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe", file2012);
|
|
Assert.DoesNotContain("-Version", args2012);
|
|
Assert.EndsWith(@"WindowsPowerShell\v1.0\powershell.exe", file2009, StringComparison.Ordinal);
|
|
Assert.Equal(["-Version", "2"], args2009.Take(2));
|
|
Assert.Contains("-EncodedCommand", args2012);
|
|
Assert.DoesNotContain(args2012, a => a.Contains("aos01", StringComparison.Ordinal));
|
|
Assert.Equal(("DAT", "01@aos01:2712", string.Empty), (environment["SQLINSIGHT_AX_COMPANY"], environment["SQLINSIGHT_AX_SERVER"], environment["SQLINSIGHT_AX_CONFIG"]));
|
|
var script = System.Text.Encoding.Unicode.GetString(Convert.FromBase64String(args2012[^1]));
|
|
Assert.Contains("Logon(", script, StringComparison.Ordinal);
|
|
Assert.Contains("Logoff()", script, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_the_helpers_answer()
|
|
{
|
|
Assert.Equal((true, 812, "Logon i AX lykkedes."), AxLogonClient.Parse(new ProcessResult(0, "noise\r\nSQLINSIGHT-AX-OK 812\r\n", string.Empty, false), Ax2012));
|
|
var notUser = AxLogonClient.Parse(new ProcessResult(1, "SQLINSIGHT-AX-ERROR You are not a recognized user of Microsoft Dynamics AX.", string.Empty, false), Ax2012);
|
|
Assert.False(notUser.Ok);
|
|
Assert.Contains("ikke bruger i AX", notUser.Message, StringComparison.Ordinal);
|
|
Assert.Contains("PowerShell 2.0", AxLogonClient.Parse(new ProcessResult(1, string.Empty, "Version v2.0.50727 of the .NET Framework is not installed", false), Ax2009).Message,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("i tide", AxLogonClient.Parse(new ProcessResult(-1, string.Empty, string.Empty, true), Ax2012).Message, StringComparison.Ordinal);
|
|
Assert.Contains("kode 3", AxLogonClient.Parse(new ProcessResult(3, string.Empty, string.Empty, false), Ax2012).Message, StringComparison.Ordinal);
|
|
}
|
|
}
|