sqlinsight-pro/src/SQLInsight.Core/Servers/ServerTasks.cs
Søren Eggert Lundsteen Olsen 2345b199fa Review 1.5: disk forecasts, 24-hour event counts, performance gaps and per-user tasks in Core
- DiskForecast.EnoughHistory: false for "ingen målinger endnu" and "for lidt historik",
  so a rule can say "ikke målbar" instead of "fyldes ikke op". Days are counted from
  now (a last point older than its hour is subtracted); FullAt takes the time the
  forecast was made; Describe says "fuld nu" at 0 days.
- ServerEventGroup.Buckets: counts per clock hour, and Recent sums only the hours with
  events in the last 24 hours (FirstAt within the window), so a recurring error no
  longer adds up from when it was first seen. Older groups without buckets still merge.
- ServerPerformanceMath.Sample takes the interval: after a gap of more than three
  intervals CPU and pages/s are null rather than the gap's average stamped now.
  Commit percent is computed in floating point.
- TaskResults.IsUserOrUpdaterTask (a user's SID in the name, OneDrive/Edge/Google
  updaters, User_Feed_Synchronization), HasUserSid and Display (SID replaced by a
  short hash for titles and subjects).
- Tests: forecast from real history, irregular sampling, identical timestamps,
  a shrunk volume, days from now; an error every ten hours over three days; a
  sample after a gap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:45:44 +02:00

108 lines
4.9 KiB
C#

using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace SQLInsight.Core.Servers;
/// <summary>Task Scheduler's result codes (LastTaskResult) in Danish, and which of them mean the task failed.</summary>
public static partial class TaskResults
{
public const uint Success = 0;
public const uint Ready = 0x41300;
public const uint Running = 0x41301;
public const uint Disabled = 0x41302;
public const uint HasNotRun = 0x41303;
public const uint NoMoreRuns = 0x41304;
public const uint TerminatedByUser = 0x41306;
public const uint Queued = 0x41325;
public const uint AlreadyRunning = 0x8004131F;
/// <summary>Whether the last run ended badly. Running, queued, never run and "already running" are not failures.</summary>
public static bool IsFailure(uint? result) => result is { } r && r is not (Success or Ready or Running or Disabled or HasNotRun or NoMoreRuns or Queued or AlreadyRunning);
public static string Hex(uint result) => "0x" + result.ToString("X", CultureInfo.InvariantCulture);
public static string Describe(uint result) => result switch
{
Success => "Fuldført (0x0)",
Ready => "Klar (0x41300)",
Running => "Kører nu (0x41301)",
Disabled => "Deaktiveret (0x41302)",
HasNotRun => "Har ikke kørt endnu (0x41303)",
NoMoreRuns => "Ingen flere planlagte kørsler (0x41304)",
TerminatedByUser => "Afbrudt af en bruger eller fordi den kørte for længe (0x41306)",
Queued => "I kø (0x41325)",
AlreadyRunning => "Kørte allerede, en ny kørsel blev ikke startet (0x8004131F)",
0x1 => "Programmet sluttede med fejlkode 1 (typisk en fejl i scriptet eller programmet) (0x1)",
0x2 => "Filen blev ikke fundet (0x2)",
0xC000013A => "Programmet blev afbrudt (Ctrl+C eller nedlukning) (0xC000013A)",
0x80070002 => "Filen blev ikke fundet (0x80070002)",
0x80070005 => "Adgang nægtet: kontoen, opgaven kører som, mangler rettigheder (0x80070005)",
0x8007052E => "Logon fejlede: forkert brugernavn eller adgangskode til opgavens konto (0x8007052E)",
0x800710E0 => "Opgaven blev afvist (fx fordi computeren kørte på batteri eller kontoen ikke var logget på) (0x800710E0)",
0x80041326 => "Opgaven er deaktiveret (0x80041326)",
_ => "Sluttede med fejlkode " + Hex(result),
};
/// <summary>Whether a task is Microsoft's own (\Microsoft\…), which is not watched unless asked for.</summary>
public static bool IsMicrosoftTask(string path)
{
ArgumentNullException.ThrowIfNull(path);
return path.StartsWith(@"\Microsoft\", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Tasks that programs create per user or to update themselves, in the root folder rather than under \Microsoft\:
/// they are not the server's own work, and their failures are rarely anyone's business. A name, or a prefix ending in *.
/// </summary>
public static readonly IReadOnlyList<string> DefaultIgnoredNames =
[
"OneDrive Standalone Update Task*",
"OneDrive Per-Machine Standalone Update Task*",
"OneDrive Reporting Task*",
"OneDrive Startup Task*",
"MicrosoftEdgeUpdateTask*",
"GoogleUpdateTask*",
"GoogleUpdaterTask*",
"User_Feed_Synchronization-*",
];
/// <summary>
/// Whether a task belongs to one user (its name carries the user's SID, S-1-5-21-…) or is a program's own updater
/// (<see cref="DefaultIgnoredNames"/>). Such tasks are not watched unless asked for.
/// </summary>
public static bool IsUserOrUpdaterTask(string path)
{
ArgumentNullException.ThrowIfNull(path);
if (path.Contains("S-1-5-21-", StringComparison.OrdinalIgnoreCase))
{
return true;
}
var name = path[(path.LastIndexOf('\\') + 1)..];
return DefaultIgnoredNames.Any(p => p.EndsWith('*')
? name.StartsWith(p[..^1], StringComparison.OrdinalIgnoreCase)
: name.Equals(p, StringComparison.OrdinalIgnoreCase));
}
/// <summary>Whether the task's path names a user by SID.</summary>
public static bool HasUserSid(string path)
{
ArgumentNullException.ThrowIfNull(path);
return Sid().IsMatch(path);
}
/// <summary>
/// The task's path for titles and subjects: a user's SID is replaced by "bruger-" and a short hash of it, which tells
/// two users' tasks apart without naming the user.
/// </summary>
public static string Display(string path)
{
ArgumentNullException.ThrowIfNull(path);
return Sid().Replace(path, m => "bruger-" + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(m.Value.ToUpperInvariant())))[..8].ToLowerInvariant());
}
[GeneratedRegex(@"S-1-5-21(-[0-9]+)+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex Sid();
}