using System.Diagnostics;
using System.Globalization;
using SQLInsight.Rules.Lifecycle;
using SQLInsight.Updates;
namespace SQLInsight.Agent.Updates;
///
/// Command line:
///
/// - update check: show what the feed offers.
/// - update apply [--feed <url|mappe>]: download and install the newest version now (administrator).
/// - apply-update: for the scheduled update task; installs a staged version when the mode is Automatic and
/// the maintenance window is open.
///
/// Installing runs from a copy of the program in the updater's folder, so no file in the installation is in use.
///
public static class UpdateCommands
{
private const string RunnerFlag = "--runner";
public static bool Handles(string[] args) => args.FirstOrDefault() is "update" or "apply-update";
public static async Task RunAsync(string[] args)
{
ArgumentNullException.ThrowIfNull(args);
var agentFolder = Option(args, "--install-dir") ?? AppContext.BaseDirectory;
var settings = LoadOptions(agentFolder);
if (Option(args, "--feed") is { } feed)
{
settings.Feed = feed;
}
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
try
{
return (args.ElementAtOrDefault(0), args.ElementAtOrDefault(1)) switch
{
("update", "check") => await CheckAsync(settings, http).ConfigureAwait(false),
("update", "apply") => await ApplyNowAsync(args, settings, agentFolder, http).ConfigureAwait(false),
("apply-update", _) when args.Contains(RunnerFlag) => await RunApplierAsync(args, settings, agentFolder).ConfigureAwait(false),
("apply-update", _) => ScheduledApply(args, settings, agentFolder),
_ => Usage(),
};
}
catch (Exception ex) when (ex is UpdateException or IOException or UnauthorizedAccessException or FormatException)
{
Console.Error.WriteLine(ex.Message);
return 2;
}
}
private static async Task CheckAsync(UpdateOptions settings, HttpClient http)
{
var checker = Checker(settings, http);
var manifest = (await checker.FetchAsync(CancellationToken.None).ConfigureAwait(false)).Manifest;
var installed = AppVersion.Running;
var pack = RulePack.Embedded();
Console.WriteLine($"Feed: {settings.Feed} ({settings.Channel}, udgivet {manifest.PublishedAt:yyyy-MM-dd HH:mm} UTC)");
Console.WriteLine($"Installeret: {installed}");
Console.WriteLine($"Tilgængelig: {manifest.Release?.Version ?? "-"}{(UpdateChecker.NewerRelease(manifest, installed) is { Security: true } ? " (sikkerhedsopdatering)" : string.Empty)}");
Console.WriteLine($"Regelpakke: {manifest.RulePack?.Version ?? "-"} (indbygget {pack.Version})");
return 0;
}
/// Administrator: download and install now, regardless of mode and maintenance window.
private static async Task ApplyNowAsync(string[] args, UpdateOptions settings, string agentFolder, HttpClient http)
{
var checker = Checker(settings, http);
var manifest = await checker.FetchAsync(CancellationToken.None).ConfigureAwait(false);
if (!await checker.StageAsync(manifest, AppVersion.Running, CancellationToken.None).ConfigureAwait(false))
{
Console.WriteLine($"Version {AppVersion.Running} er den nyeste på {settings.Channel}-kanalen.");
return 0;
}
Console.WriteLine($"Version {manifest.Manifest.Release!.Version} er hentet og verificeret.");
return LaunchRunner(args, settings, agentFolder);
}
/// Scheduled task: only in Automatic mode, only in the maintenance window, only if something is staged.
private static int ScheduledApply(string[] args, UpdateOptions settings, string agentFolder)
{
if (settings.Application != AppUpdateMode.Automatic)
{
Console.WriteLine($"Automatisk installation er slået fra (Application = {settings.Application}).");
return 0;
}
if (!settings.InMaintenanceWindow(TimeOnly.FromDateTime(DateTime.Now)))
{
Console.WriteLine($"Uden for vedligeholdelsesvinduet {settings.MaintenanceWindow}.");
return 0;
}
if (!File.Exists(Path.Combine(settings.Directories.Pending, UpdateFeed.ManifestFile)))
{
Console.WriteLine("Ingen opdatering venter.");
return 0;
}
return LaunchRunner(args, settings, agentFolder);
}
///
/// Copies the program to the updater's folder and runs apply-update --runner from there. On Windows the
/// installation's files are locked while this process runs, so it does not wait; on Linux it waits, so a systemd
/// oneshot unit does not kill the updater when it exits.
///
private static int LaunchRunner(string[] args, UpdateOptions settings, string agentFolder)
{
var runner = Path.Combine(settings.UpdaterRoot(agentFolder), "runner");
if (Directory.Exists(runner))
{
Directory.Delete(runner, recursive: true);
}
foreach (var file in Directory.EnumerateFiles(agentFolder, "*", SearchOption.AllDirectories))
{
var target = Path.Combine(runner, Path.GetRelativePath(agentFolder, file));
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
File.Copy(file, target);
}
var host = Environment.ProcessPath ?? throw new InvalidOperationException("Programmets sti er ukendt.");
var viaDotnet = Path.GetFileNameWithoutExtension(host).Equals("dotnet", StringComparison.OrdinalIgnoreCase);
var start = new ProcessStartInfo(viaDotnet ? host : Path.Combine(runner, Path.GetFileName(host))) { UseShellExecute = false };
if (viaDotnet)
{
start.ArgumentList.Add(Path.Combine(runner, "SQLInsight.Agent.dll"));
}
string[] runnerArgs = ["apply-update", RunnerFlag, "--install-dir", agentFolder, "--wait-pid", Environment.ProcessId.ToString(CultureInfo.InvariantCulture)];
foreach (var arg in runnerArgs.Concat(Option(args, "--feed") is { } feed ? ["--feed", feed] : []))
{
start.ArgumentList.Add(arg);
}
using var process = Process.Start(start) ?? throw new UpdateException("Opdateringen kunne ikke startes.");
if (OperatingSystem.IsWindows())
{
Console.WriteLine($"Opdateringen kører nu i baggrunden. Resultatet skrives i {settings.Directories.ResultFile} og vises i webbrugerfladen.");
return 0;
}
process.WaitForExit();
return process.ExitCode;
}
private static async Task RunApplierAsync(string[] args, UpdateOptions settings, string agentFolder)
{
// On Windows the process that started us holds the installation's files open until it exits.
if (OperatingSystem.IsWindows() && int.TryParse(Option(args, "--wait-pid"), NumberStyles.None, CultureInfo.InvariantCulture, out var parent))
{
try
{
using var launcher = Process.GetProcessById(parent);
using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(1));
await launcher.WaitForExitAsync(timeout.Token).ConfigureAwait(false);
}
catch (Exception ex) when (ex is ArgumentException or OperationCanceledException)
{
// Already gone, or still running after a minute: go ahead, a locked file triggers a rollback.
}
}
var root = settings.UpdaterRoot(agentFolder);
var logFile = Path.Combine(root, "update.log");
void Log(string line)
{
var stamped = $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss} {line}";
Console.WriteLine(stamped);
File.AppendAllText(logFile, stamped + Environment.NewLine);
}
var applier = new UpdateApplier(
new UpdateApplierOptions
{
Channel = settings.Channel,
PrivateRoot = Path.Combine(root, "work"),
Components = settings.ComponentsFor(agentFolder),
HealthCheckDuration = settings.HealthCheckDuration,
},
settings.Directories, settings.Keys, ServiceControl.ForCurrentOs(), TimeProvider.System, Log);
var result = await applier.ApplyAsync(AppVersion.Running, CancellationToken.None).ConfigureAwait(false);
return result.Outcome is UpdateOutcome.Applied or UpdateOutcome.NothingToDo ? 0 : 2;
}
private static UpdateChecker Checker(UpdateOptions settings, HttpClient http)
{
if (string.IsNullOrWhiteSpace(settings.Feed))
{
throw new UpdateException("Der er intet opdateringsfeed. Angiv SQLInsight:Updates:Feed eller --feed .");
}
return new UpdateChecker(new UpdateFeed(settings.Feed, http), settings.Keys, settings.Directories, settings.Channel, TimeProvider.System);
}
private static UpdateOptions LoadOptions(string agentFolder)
{
var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production";
var configuration = new ConfigurationBuilder()
.SetBasePath(agentFolder)
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile($"appsettings.{environment}.json", optional: true)
.AddEnvironmentVariables()
.Build();
return configuration.GetSection(UpdateOptions.SectionName).Get() ?? new UpdateOptions();
}
private static string? Option(string[] args, string name)
{
var i = Array.IndexOf(args, name);
return i >= 0 && i + 1 < args.Length ? args[i + 1] : null;
}
private static int Usage()
{
Console.Error.WriteLine("""
SQLInsight.Agent update check Vis hvad opdateringsfeedet tilbyder
SQLInsight.Agent update apply [--feed ] Hent og installér nyeste version nu (administrator)
SQLInsight.Agent apply-update Til den planlagte opdateringsopgave
""");
return 1;
}
}