diff --git a/src/Queil.Ring.Configuration/ConfigSet.cs b/src/Queil.Ring.Configuration/ConfigSet.cs index 6069d9b..977920f 100644 --- a/src/Queil.Ring.Configuration/ConfigSet.cs +++ b/src/Queil.Ring.Configuration/ConfigSet.cs @@ -17,7 +17,7 @@ public ConfigSet(string path, Dictionary bareConfigs) } Path = path; - Flavours = [..bareConfigs.Values.SelectMany(x => x.Tags)]; + Flavours = [.. bareConfigs.Values.SelectMany(x => x.Tags)]; } public HashSet Flavours { get; } diff --git a/src/Queil.Ring.DotNet.Cli/Abstractions/DetailsExtractors.cs b/src/Queil.Ring.DotNet.Cli/Abstractions/DetailsExtractors.cs index a34c072..760e91a 100644 --- a/src/Queil.Ring.DotNet.Cli/Abstractions/DetailsExtractors.cs +++ b/src/Queil.Ring.DotNet.Cli/Abstractions/DetailsExtractors.cs @@ -14,7 +14,7 @@ public static RunnableDetails Extract(IRunnableConfig cfg) var details = cfg switch { CsProjRunnable c => New((DetailsKeys.CsProjPath, c.FullPath)), - _ => new Dictionary() + _ => [] }; if (cfg.FriendlyName != null) details.Add(DetailsKeys.FriendlyName, cfg.FriendlyName); diff --git a/src/Queil.Ring.DotNet.Cli/Abstractions/IRunnable.cs b/src/Queil.Ring.DotNet.Cli/Abstractions/IRunnable.cs index de0bc15..2911098 100644 --- a/src/Queil.Ring.DotNet.Cli/Abstractions/IRunnable.cs +++ b/src/Queil.Ring.DotNet.Cli/Abstractions/IRunnable.cs @@ -10,6 +10,7 @@ public interface IRunnable string UniqueId { get; } State State { get; } IReadOnlyDictionary Details { get; } + Task ConfigureAsync(CancellationToken token); Task RunAsync(CancellationToken token); Task TerminateAsync(); event EventHandler OnHealthCheckCompleted; diff --git a/src/Queil.Ring.DotNet.Cli/Abstractions/Runnable.cs b/src/Queil.Ring.DotNet.Cli/Abstractions/Runnable.cs index 2430af8..ea4db91 100644 --- a/src/Queil.Ring.DotNet.Cli/Abstractions/Runnable.cs +++ b/src/Queil.Ring.DotNet.Cli/Abstractions/Runnable.cs @@ -19,7 +19,7 @@ namespace Queil.Ring.DotNet.Cli.Abstractions; public abstract class Runnable : IRunnable where TConfig : IRunnableConfig { - private readonly Dictionary _details = new(); + private readonly Dictionary _details = []; private readonly Fsm _fsm = new(); private readonly ILogger> _logger; protected readonly ISender Sender; @@ -45,14 +45,19 @@ protected Runnable(TConfig config, ILogger> logger, public event EventHandler? OnInitExecuted; public IReadOnlyDictionary Details => _details; - public async Task RunAsync(CancellationToken token) + public async Task ConfigureAsync(CancellationToken token) { using var _ = _logger.BeginScope(this.ToScope()); var fsm = await InitFsm(token); - await fsm.FireAsync(Trigger.Init); } + public async Task RunAsync(CancellationToken token) + { + using var _ = _logger.BeginScope(this.ToScope()); + await _fsm.FireAsync(Trigger.Start); + } + public async Task TerminateAsync() { using var _ = _logger.BeginScope(this.ToScope()); @@ -91,6 +96,8 @@ private async Task InitFsm(CancellationToken token) .Ignore(Trigger.NoOp) .Ignore(Trigger.HcOk) .Ignore(Trigger.HcUnhealthy) + .Ignore(Trigger.Stop) + .Ignore(Trigger.Destroy) .Permit(Trigger.Init, State.Idle); _fsm.Configure(State.Idle) @@ -198,7 +205,6 @@ private async Task InitCoreAsync(CancellationToken token) _logger.LogContextDebug(_context); _logger.LogDebug(LogEventStatus.OK); await Sender.EnqueueAsync(Message.RunnableInitiated(UniqueId), token); - await _fsm.FireAsync(Trigger.Start); } catch (Exception ex) { diff --git a/src/Queil.Ring.DotNet.Cli/Infrastructure/RingConfiguration.cs b/src/Queil.Ring.DotNet.Cli/Infrastructure/RingConfiguration.cs index 238ccac..c8cbb6e 100644 --- a/src/Queil.Ring.DotNet.Cli/Infrastructure/RingConfiguration.cs +++ b/src/Queil.Ring.DotNet.Cli/Infrastructure/RingConfiguration.cs @@ -75,7 +75,7 @@ public string? ConfigPath public class InitHookConfig { public string? Command { get; set; } - public string[] Args { get; set; } = Array.Empty(); + public string[] Args { get; set; } = []; } public class HooksConfiguration diff --git a/src/Queil.Ring.DotNet.Cli/Infrastructure/WebApplicationExtensions.cs b/src/Queil.Ring.DotNet.Cli/Infrastructure/WebApplicationExtensions.cs index b8a4ff1..862673e 100644 --- a/src/Queil.Ring.DotNet.Cli/Infrastructure/WebApplicationExtensions.cs +++ b/src/Queil.Ring.DotNet.Cli/Infrastructure/WebApplicationExtensions.cs @@ -19,11 +19,11 @@ public static async Task RunRingAsync(this WebApplication app) await app.Services.GetRequiredService().CloneWorkspaceRepos(c.WorkspacePath, c.OutputDir); break; case ConfigDump: - { - var debugView = ((IConfigurationRoot)app.Services.GetRequiredService()).GetDebugView(); - Console.WriteLine(debugView); - break; - } + { + var debugView = ((IConfigurationRoot)app.Services.GetRequiredService()).GetDebugView(); + Console.WriteLine(debugView); + break; + } case HeadlessOptions: case ConsoleOptions: await app.RunAsync(); diff --git a/src/Queil.Ring.DotNet.Cli/Infrastructure/WsClient.cs b/src/Queil.Ring.DotNet.Cli/Infrastructure/WsClient.cs index e47b06f..192f27a 100644 --- a/src/Queil.Ring.DotNet.Cli/Infrastructure/WsClient.cs +++ b/src/Queil.Ring.DotNet.Cli/Infrastructure/WsClient.cs @@ -80,38 +80,38 @@ private async Task AckLongRunning(CancellationToken token) try { while (await _channel.Reader.WaitToReadAsync(token)) - while (_channel.Reader.TryPeek(out var peek)) - if (peek.IsCompleted) - { - if (!_channel.Reader.TryRead(out var task)) continue; - - var ack = await task; - if (!logger.IsEnabled(LogLevel.Debug)) - { - await Ws.SendAckAsync(ack, token); - } - else + while (_channel.Reader.TryPeek(out var peek)) + if (peek.IsCompleted) { - var sendTask = Ws.SendAckAsync(ack, token); - using (logger.WithSentScope(false, M.ACK)) + if (!_channel.Reader.TryRead(out var task)) continue; + + var ack = await task; + if (!logger.IsEnabled(LogLevel.Debug)) { - logger.LogDebug("{Payload} {Id} ({TaskId})", ack, Id, task.Id); + await Ws.SendAckAsync(ack, token); } - - await sendTask.ContinueWith(_ => + else { - using (logger.WithSentScope(true, M.ACK)) + var sendTask = Ws.SendAckAsync(ack, token); + using (logger.WithSentScope(false, M.ACK)) { - logger.LogDebug("{Payload} {Id} ({TaskId})", ack, Id, - task.Id); + logger.LogDebug("{Payload} {Id} ({TaskId})", ack, Id, task.Id); } - }, TaskContinuationOptions.OnlyOnRanToCompletion); + + await sendTask.ContinueWith(_ => + { + using (logger.WithSentScope(true, M.ACK)) + { + logger.LogDebug("{Payload} {Id} ({TaskId})", ack, Id, + task.Id); + } + }, TaskContinuationOptions.OnlyOnRanToCompletion); + } + } + else + { + await Task.Delay(TimeSpan.FromMilliseconds(100), token); } - } - else - { - await Task.Delay(TimeSpan.FromMilliseconds(100), token); - } } catch (OperationCanceledException) { diff --git a/src/Queil.Ring.DotNet.Cli/Logging/ScopeLoggingExtensions.cs b/src/Queil.Ring.DotNet.Cli/Logging/ScopeLoggingExtensions.cs index d814599..54bc956 100644 --- a/src/Queil.Ring.DotNet.Cli/Logging/ScopeLoggingExtensions.cs +++ b/src/Queil.Ring.DotNet.Cli/Logging/ScopeLoggingExtensions.cs @@ -28,14 +28,14 @@ public static void LogContextDebug(this ILogger logger, object context) } public static IDisposable? WithClientScope(this ILogger logger) => logger.BeginScope(new Scope - { [Scope.UniqueIdKey] = "PROTOCOL", [Scope.LogEventKey] = "CLIENT" }); + { [Scope.UniqueIdKey] = "PROTOCOL", [Scope.LogEventKey] = "CLIENT" }); public static IDisposable? WithReceivedScope(this ILogger logger, M protocolEvent) => logger.BeginScope(new Scope { [Scope.UniqueIdKey] = protocolEvent, [Scope.LogEventKey] = "<<" }); public static IDisposable? WithSentScope(this ILogger logger, bool isDelivered, M protocolEvent) => logger.BeginScope(new Scope - { [Scope.UniqueIdKey] = protocolEvent, [Scope.LogEventKey] = isDelivered ? ">>" : "->" }); + { [Scope.UniqueIdKey] = protocolEvent, [Scope.LogEventKey] = isDelivered ? ">>" : "->" }); public static IDisposable? WithHostScope(this ILogger logger, LogEvent logEvent) => logger.WithScope("HOST", logEvent); @@ -45,13 +45,13 @@ public static void LogContextDebug(this ILogger logger, object context) public static IDisposable? WithTaskScope(this ILogger logger, string runtimeId, string taskId) => logger.BeginScope(new Scope - { [Scope.UniqueIdKey] = $"{runtimeId}:{taskId}", [Scope.LogEventKey] = "TASK" }); + { [Scope.UniqueIdKey] = $"{runtimeId}:{taskId}", [Scope.LogEventKey] = "TASK" }); public static IDisposable? WithLogErrorScope(this ILogger logger) => logger.BeginScope(new Scope - { [Scope.LogEventKey] = $"{Red}ERROR" }); + { [Scope.LogEventKey] = $"{Red}ERROR" }); public static IDisposable? WithLogInfoScope(this ILogger logger) => logger.BeginScope(new Scope - { [Scope.LogEventKey] = $"{Gray}LOG" }); + { [Scope.LogEventKey] = $"{Gray}LOG" }); } diff --git a/src/Queil.Ring.DotNet.Cli/Program.cs b/src/Queil.Ring.DotNet.Cli/Program.cs index 0b8f1e3..094a995 100644 --- a/src/Queil.Ring.DotNet.Cli/Program.cs +++ b/src/Queil.Ring.DotNet.Cli/Program.cs @@ -112,9 +112,9 @@ static string Ring(string ver) => .Where(t => typeof(IRunnable).IsAssignableFrom(t)).ToList(); var configMap = (from r in runnableTypes - let cfg = r.GetProperty(nameof(Runnable.Config)) - where cfg != null - select (RunnableType: r, ConfigType: cfg.PropertyType)) + let cfg = r.GetProperty(nameof(Runnable.Config)) + where cfg != null + select (RunnableType: r, ConfigType: cfg.PropertyType)) .ToDictionary(x => x.ConfigType, x => x.RunnableType); foreach (var (_, rt) in configMap) container.Register(rt, rt, new PerRequestLifeTime()); diff --git a/src/Queil.Ring.DotNet.Cli/Runnables/Dotnet/DotnetContext.cs b/src/Queil.Ring.DotNet.Cli/Runnables/Dotnet/DotnetContext.cs index a53ecc3..6f30aa3 100644 --- a/src/Queil.Ring.DotNet.Cli/Runnables/Dotnet/DotnetContext.cs +++ b/src/Queil.Ring.DotNet.Cli/Runnables/Dotnet/DotnetContext.cs @@ -11,7 +11,7 @@ namespace Queil.Ring.DotNet.Cli.Runnables.Dotnet; public class DotnetContext : ICsProjContext, ITrackRetries, ITrackProcessId, ITrackProcessOutput { public string ExePath => Path.ChangeExtension(EntryAssemblyPath, "exe"); - public Dictionary Env { get; set; } = new(); + public Dictionary Env { get; set; } = []; public string CsProjPath { get; set; } public string WorkingDir { get; set; } public string TargetFramework { get; set; } diff --git a/src/Queil.Ring.DotNet.Cli/Runnables/Kustomize/KustomizeContext.cs b/src/Queil.Ring.DotNet.Cli/Runnables/Kustomize/KustomizeContext.cs index 7ccd7c9..59febd5 100644 --- a/src/Queil.Ring.DotNet.Cli/Runnables/Kustomize/KustomizeContext.cs +++ b/src/Queil.Ring.DotNet.Cli/Runnables/Kustomize/KustomizeContext.cs @@ -1,13 +1,12 @@ namespace Queil.Ring.DotNet.Cli.Runnables.Kustomize; -using System; using Abstractions.Context; public class KustomizeContext : ITrackRetries { public string KustomizationDir { get; set; } public string CachePath { get; set; } - public Namespace[] Namespaces { get; set; } = Array.Empty(); + public Namespace[] Namespaces { get; set; } = []; public int ConsecutiveFailures { get; set; } public int TotalFailures { get; set; } } @@ -15,5 +14,5 @@ public class KustomizeContext : ITrackRetries public class Namespace { public string Name { get; set; } - public string[] Pods { get; set; } = Array.Empty(); + public string[] Pods { get; set; } = []; } diff --git a/src/Queil.Ring.DotNet.Cli/Runnables/Kustomize/KustomizeRunnable.cs b/src/Queil.Ring.DotNet.Cli/Runnables/Kustomize/KustomizeRunnable.cs index 9958826..e323b24 100644 --- a/src/Queil.Ring.DotNet.Cli/Runnables/Kustomize/KustomizeRunnable.cs +++ b/src/Queil.Ring.DotNet.Cli/Runnables/Kustomize/KustomizeRunnable.cs @@ -1,4 +1,4 @@ -using static Queil.Ring.DotNet.Cli.Tools.ToolExtensions; +using static Queil.Ring.DotNet.Cli.Tools.RunProcess; using KustomizeConfig = Queil.Ring.Configuration.Runnables.Kustomize; namespace Queil.Ring.DotNet.Cli.Runnables.Kustomize; diff --git a/src/Queil.Ring.DotNet.Cli/Tools/DockerCompose.cs b/src/Queil.Ring.DotNet.Cli/Tools/DockerCompose.cs index 8863bb3..9a5325b 100644 --- a/src/Queil.Ring.DotNet.Cli/Tools/DockerCompose.cs +++ b/src/Queil.Ring.DotNet.Cli/Tools/DockerCompose.cs @@ -1,6 +1,5 @@ namespace Queil.Ring.DotNet.Cli.Tools; -using System; using System.Threading; using System.Threading.Tasks; using Abstractions.Tools; @@ -9,22 +8,22 @@ namespace Queil.Ring.DotNet.Cli.Tools; public class DockerCompose(ILogger logger) : ITool { public string Command { get; set; } = "docker-compose"; - public string[] DefaultArgs { get; set; } = Array.Empty(); + public string[] DefaultArgs { get; set; } = []; public ILogger Logger { get; } = logger; public async Task RmAsync(string composeFilePath, CancellationToken token) => - await this.RunAsync(["-f", $"\"{composeFilePath}\"", "rm", "-f"], wait: true, token: token); + await this.RunAsync(["-f", $"\"{composeFilePath}\"", "rm", "-f"], foreground: true, token: token); public async Task PullAsync(string composeFilePath, CancellationToken token) => - await this.RunAsync(["-f", $"\"{composeFilePath}\"", "pull"], wait: true, token: token); + await this.RunAsync(["-f", $"\"{composeFilePath}\"", "pull"], foreground: true, token: token); public async Task UpAsync(string composeFilePath, CancellationToken token) => - await this.RunAsync(["-f", $"\"{composeFilePath}\"", "up", "--force-recreate"], wait: true, + await this.RunAsync(["-f", $"\"{composeFilePath}\"", "up", "--force-recreate"], foreground: true, token: token); public async Task DownAsync(string composeFilePath, CancellationToken token) => - await this.RunAsync(["-f", $"\"{composeFilePath}\"", "down"], wait: true, token: token); + await this.RunAsync(["-f", $"\"{composeFilePath}\"", "down"], foreground: true, token: token); public async Task StopAsync(string composeFilePath, CancellationToken token) => - await this.RunAsync(["-f", $"\"{composeFilePath}\"", "stop"], wait: true, token: token); + await this.RunAsync(["-f", $"\"{composeFilePath}\"", "stop"], foreground: true, token: token); } diff --git a/src/Queil.Ring.DotNet.Cli/Tools/DotnetCliBundle.cs b/src/Queil.Ring.DotNet.Cli/Tools/DotnetCliBundle.cs index 0db86e7..3018328 100644 --- a/src/Queil.Ring.DotNet.Cli/Tools/DotnetCliBundle.cs +++ b/src/Queil.Ring.DotNet.Cli/Tools/DotnetCliBundle.cs @@ -16,7 +16,7 @@ public class DotnetCliBundle(ProcessRunner processRunner, ILogger DefaultEnvVars = new() { ["ASPNETCORE_ENVIRONMENT"] = "Development" }; public ILogger Logger { get; } = logger; public string Command { get; set; } = "dotnet"; - public string[] DefaultArgs { get; set; } = Array.Empty(); + public string[] DefaultArgs { get; set; } = []; public async Task RunAsync(DotnetContext ctx, CancellationToken token, string[]? urls = null) { @@ -47,6 +47,6 @@ void HandleUrls() } public async Task BuildAsync(string csProjFile, CancellationToken token) => - await this.RunAsync(["build", csProjFile, "-v:q", "/nologo", "/nodereuse:false"], wait: true, + await this.RunAsync(["build", csProjFile, "-v:q", "/nologo", "/nodereuse:false"], foreground: true, token: token); } diff --git a/src/Queil.Ring.DotNet.Cli/Tools/GitClone.cs b/src/Queil.Ring.DotNet.Cli/Tools/GitClone.cs index a5dfb37..4bccfee 100644 --- a/src/Queil.Ring.DotNet.Cli/Tools/GitClone.cs +++ b/src/Queil.Ring.DotNet.Cli/Tools/GitClone.cs @@ -20,7 +20,7 @@ public partial class GitClone(ILogger logger, IOptions(); + public string[] DefaultArgs { get; set; } = []; public ILogger Logger { get; } = logger; public string ResolveFullClonePath(IFromGit gitCfg, string? rootPathOverride = null) @@ -45,7 +45,7 @@ public string ResolveFullClonePath(IFromGit gitCfg, string? rootPathOverride = n private Func> Git(params string[] args) { return token => this.TryAsync(3, TimeSpan.FromSeconds(10), - t => t.RunAsync(args, wait: true, token: token), token); + t => t.RunAsync(args, foreground: true, token: token), token); } public async Task CloneOrPullAsync(IFromGit gitCfg, CancellationToken token, bool shallow = false, @@ -66,7 +66,7 @@ public async Task CloneOrPullAsync(IFromGit gitCfg, CancellationT if (shallow) { - var remoteBranchName = BranchRegex().Match(output.Output); + var remoteBranchName = BranchRegex().Match(output.Output.Split(Environment.NewLine)[0]); if (!remoteBranchName.Success) throw new InvalidOperationException( $"Could not get branch name from git status output: {output.Output}"); diff --git a/src/Queil.Ring.DotNet.Cli/Tools/Kubectl.cs b/src/Queil.Ring.DotNet.Cli/Tools/Kubectl.cs index e886784..2661afe 100644 --- a/src/Queil.Ring.DotNet.Cli/Tools/Kubectl.cs +++ b/src/Queil.Ring.DotNet.Cli/Tools/Kubectl.cs @@ -17,15 +17,15 @@ public class KubectlBundle( IOptions config) : ITool { - private readonly string[] _allowedContexts = config.Value.Kubernetes.AllowedContexts ?? Array.Empty(); + private readonly string[] _allowedContexts = config.Value.Kubernetes.AllowedContexts ?? []; public string Command { get; set; } = "kubectl"; - public string[] DefaultArgs { get; set; } = Array.Empty(); + public string[] DefaultArgs { get; set; } = []; public ILogger Logger { get; } = logger; public async Task EnsureContextIsAllowed(CancellationToken token) { - var result = await this.RunAsync(["config", "current-context"], wait: true, token: token); + var result = await this.RunAsync(["config", "current-context"], foreground: true, token: token); var currentContext = result.Output; if (!_allowedContexts.Contains(currentContext)) throw new InvalidOperationException( @@ -35,8 +35,12 @@ public async Task EnsureContextIsAllowed(CancellationToken token) public async Task IsValidManifestAsync(string filePath, CancellationToken token) { var result = await this.RunAsync([ - "apply", "--validate=true", "--dry-run=client", "-f", $"\"{filePath}\"" - ], wait: true, token: token); + "apply", + "--validate=true", + "--dry-run=client", + "-f", + $"\"{filePath}\"" + ], foreground: true, token: token); return result.IsSuccess; } @@ -48,7 +52,7 @@ public async Task ApplyJsonPathAsync(string path, string jsonPath { await EnsureContextIsAllowed(token); return await this.RunAsync(["apply", "-o", $"jsonpath=\"{jsonPath}\"", "-f", $"\"{path}\""], - wait: true, token: token); + foreground: true, token: token); } public async Task GetPods(string nameSpace) @@ -68,7 +72,12 @@ public async Task DeleteAsync(string path, CancellationToken _) using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); await EnsureContextIsAllowed(cts.Token); return await this.RunAsync([ - "delete", "--ignore-not-found", "--wait=false", "--now=true", "-f", $"\"{path}\"" - ], wait: true, token: cts.Token); + "delete", + "--ignore-not-found", + "--wait=false", + "--now=true", + "-f", + $"\"{path}\"" + ], foreground: true, token: cts.Token); } } diff --git a/src/Queil.Ring.DotNet.Cli/Tools/KustomizeTool.cs b/src/Queil.Ring.DotNet.Cli/Tools/KustomizeTool.cs index fa2add1..e0d6755 100644 --- a/src/Queil.Ring.DotNet.Cli/Tools/KustomizeTool.cs +++ b/src/Queil.Ring.DotNet.Cli/Tools/KustomizeTool.cs @@ -1,6 +1,5 @@ namespace Queil.Ring.DotNet.Cli.Tools; -using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -10,12 +9,12 @@ namespace Queil.Ring.DotNet.Cli.Tools; public class KustomizeTool(ILogger logger) : ITool { public string Command { get; set; } = "kustomize"; - public string[] DefaultArgs { get; set; } = Array.Empty(); + public string[] DefaultArgs { get; set; } = []; public ILogger Logger { get; } = logger; public async Task BuildAsync(string kustomizeDir, string outputFilePath, CancellationToken token) { - var output = await this.RunAsync(["build", kustomizeDir], wait: true, token: token); + var output = await this.RunAsync(["build", kustomizeDir], foreground: true, token: token); await File.WriteAllTextAsync(outputFilePath, output.Output, token); return output; } diff --git a/src/Queil.Ring.DotNet.Cli/Tools/ProcessRunner.cs b/src/Queil.Ring.DotNet.Cli/Tools/ProcessRunner.cs index 3111d2b..102d4e2 100644 --- a/src/Queil.Ring.DotNet.Cli/Tools/ProcessRunner.cs +++ b/src/Queil.Ring.DotNet.Cli/Tools/ProcessRunner.cs @@ -1,12 +1,11 @@ namespace Queil.Ring.DotNet.Cli.Tools; -using System; using Abstractions.Tools; using Microsoft.Extensions.Logging; public class ProcessRunner(ILogger logger) : ITool { public string Command { get; set; } - public string[] DefaultArgs { get; set; } = Array.Empty(); + public string[] DefaultArgs { get; set; } = []; public ILogger Logger { get; } = logger; } diff --git a/src/Queil.Ring.DotNet.Cli/Tools/ToolExtensions.cs b/src/Queil.Ring.DotNet.Cli/Tools/RunProcess.cs similarity index 90% rename from src/Queil.Ring.DotNet.Cli/Tools/ToolExtensions.cs rename to src/Queil.Ring.DotNet.Cli/Tools/RunProcess.cs index a94f5c8..2d70d08 100644 --- a/src/Queil.Ring.DotNet.Cli/Tools/ToolExtensions.cs +++ b/src/Queil.Ring.DotNet.Cli/Tools/RunProcess.cs @@ -14,7 +14,7 @@ namespace Queil.Ring.DotNet.Cli.Tools; using Logging; using Microsoft.Extensions.Logging; -public static class ToolExtensions +public static class RunProcess { //TODO: this should be configurable private static readonly string[] FailureWords = ["err", "error", "fail"]; @@ -55,14 +55,14 @@ public static async Task RunAsync(this ITool tool, string? workingDirectory = null, IDictionary? envVars = null, Action? onErrorData = null, - bool wait = false, + bool foreground = false, bool captureStdOut = true, CancellationToken token = default) { var procUid = Guid.NewGuid().ToString("n").Remove(10); try { - var allArgs = string.Join(" ", tool.DefaultArgs.Concat(args ?? Array.Empty())); + var allArgs = string.Join(" ", tool.DefaultArgs.Concat(args ?? [])); var sb = new StringBuilder(); var s = new ProcessStartInfo @@ -86,29 +86,15 @@ public static async Task RunAsync(this ITool tool, tool.Logger.LogDebug("{procUid} - Starting process: {Tool} {Args} ({ProcessWorkingDir})", procUid, tool.Command, allArgs, workingDirectory ?? ringWorkingDir); - var p = Process.Start(s); - - if (p == null) - { - tool.Logger.LogError("{procUid} - Process failed: {Tool} {Args} ({ProcessWorkingDir})", procUid, - tool.Command, allArgs, workingDirectory ?? ringWorkingDir); - return new ExecutionInfo(); - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + var tcs = new TaskCompletionSource(); + var p = new Process { - p.TrackAsChild(); - } - - p.EnableRaisingEvents = true; - p.OutputDataReceived += OnData; + StartInfo = s, + EnableRaisingEvents = true + }; + if (!foreground) p.OutputDataReceived += OnData; p.ErrorDataReceived += OnError; - p.BeginOutputReadLine(); - p.BeginErrorReadLine(); - - tool.Logger.LogDebug("{procUid} - Process started: {Pid}", procUid, p.Id); - - var tcs = new TaskCompletionSource(); + p.Exited += OnExit; token.Register(() => { @@ -123,12 +109,25 @@ public static async Task RunAsync(this ITool tool, } }); - p.Exited += OnExit; + p.Start(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + p.TrackAsChild(); + } + + if (!foreground) p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + + tool.Logger.LogDebug("{procUid} - Process started: {Pid}", procUid, p.Id); ExecutionInfo result; - if (wait) - result = await tcs.Task; + if (foreground) + { + await p.WaitForExitAsync(token); + result = new ExecutionInfo(p.Id, p.ExitCode, (await p.StandardOutput.ReadToEndAsync(token)).Trim('\r', '\n', ' ', '\t'), tcs.Task); + } else result = new ExecutionInfo(p.Id, null, sb.ToString().Trim('\r', '\n', ' ', '\t'), tcs.Task); @@ -159,7 +158,6 @@ void OnExit(object? sender, EventArgs _) e.Exited -= OnExit; tcs.TrySetResult(new ExecutionInfo(e.Id, e.ExitCode, sb.ToString().Trim('\r', '\n', ' ', '\t'), tcs.Task)); - e.Dispose(); } void OnError(object _, DataReceivedEventArgs x) diff --git a/src/Queil.Ring.DotNet.Cli/Tools/Windows/ChildProcessTracker.cs b/src/Queil.Ring.DotNet.Cli/Tools/Windows/ChildProcessTracker.cs index f39fbe3..a0c4f88 100644 --- a/src/Queil.Ring.DotNet.Cli/Tools/Windows/ChildProcessTracker.cs +++ b/src/Queil.Ring.DotNet.Cli/Tools/Windows/ChildProcessTracker.cs @@ -49,7 +49,7 @@ static ChildProcessTracker() // close the job handle, and when that happens, we want the child processes to // be killed, too. var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION - { LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE }; + { LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE }; var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION { BasicLimitInformation = info }; diff --git a/src/Queil.Ring.DotNet.Cli/Tools/Windows/IISExpressExe.cs b/src/Queil.Ring.DotNet.Cli/Tools/Windows/IISExpressExe.cs index 538f666..10d82fc 100644 --- a/src/Queil.Ring.DotNet.Cli/Tools/Windows/IISExpressExe.cs +++ b/src/Queil.Ring.DotNet.Cli/Tools/Windows/IISExpressExe.cs @@ -1,6 +1,5 @@ namespace Queil.Ring.DotNet.Cli.Tools.Windows; -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -10,7 +9,7 @@ namespace Queil.Ring.DotNet.Cli.Tools.Windows; public class IISExpressExe(ILogger logger) : ITool { public string Command { get; set; } = @"C:\Program Files\IIS Express\iisexpress.exe"; - public string[] DefaultArgs { get; set; } = Array.Empty(); + public string[] DefaultArgs { get; set; } = []; public ILogger Logger { get; } = logger; private void OnError(string error) diff --git a/src/Queil.Ring.DotNet.Cli/Workspace/RunnableContainer.cs b/src/Queil.Ring.DotNet.Cli/Workspace/RunnableContainer.cs index 6c93a99..b8bfcaa 100644 --- a/src/Queil.Ring.DotNet.Cli/Workspace/RunnableContainer.cs +++ b/src/Queil.Ring.DotNet.Cli/Workspace/RunnableContainer.cs @@ -46,6 +46,8 @@ public static async Task CreateAsync(IRunnableConfig cfg, return container; } + public Task ConfigureAsync() => Runnable.ConfigureAsync(_aggregateCts.Token); + public void Start() { Task = Runnable.RunAsync(_aggregateCts.Token); diff --git a/src/Queil.Ring.DotNet.Cli/Workspace/WorkspaceInitHook.cs b/src/Queil.Ring.DotNet.Cli/Workspace/WorkspaceInitHook.cs index a3b24d6..e88d22d 100644 --- a/src/Queil.Ring.DotNet.Cli/Workspace/WorkspaceInitHook.cs +++ b/src/Queil.Ring.DotNet.Cli/Workspace/WorkspaceInitHook.cs @@ -29,7 +29,7 @@ public async Task RunAsync(CancellationToken token) if (_configured) { _logger.LogDebug("Executing Workspace Init Hook"); - await _runner.RunAsync(wait: true, token: token); + await _runner.RunAsync(foreground: true, token: token); } else { diff --git a/src/Queil.Ring.DotNet.Cli/Workspace/WorkspaceLauncher.cs b/src/Queil.Ring.DotNet.Cli/Workspace/WorkspaceLauncher.cs index 6ba5b9a..ce1118d 100644 --- a/src/Queil.Ring.DotNet.Cli/Workspace/WorkspaceLauncher.cs +++ b/src/Queil.Ring.DotNet.Cli/Workspace/WorkspaceLauncher.cs @@ -256,6 +256,7 @@ private async Task AddAsync(string id, IRunnableConfig cfg, TimeSpan delay, bool _runnables.TryAdd(id, container); + await container.ConfigureAsync(); if (start) container.Start(); } @@ -266,7 +267,19 @@ private async Task RemoveAsync(string key) Interlocked.Decrement(ref _initCounter); container.Runnable.OnHealthCheckCompleted -= OnPublishStatus; container.Runnable.OnInitExecuted -= OnRunnableInitExecuted; - await container.DisposeAsync(); + try + { + await container.DisposeAsync(); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Runnable disposal failed"); + return false; + } return true; } @@ -285,7 +298,7 @@ private void OnPublishStatus(object? sender, EventArgs args) private void PublishStatusCore(ServerState serverState, bool force) { var state = serverState == ServerState.IDLE ? WorkspaceState.NONE : - !_runnables.Any() ? WorkspaceState.IDLE : + _runnables.IsEmpty ? WorkspaceState.IDLE : _runnables.Values.Select(x => x.Runnable) .All(r => r.State == State.ProbingHealth || r.State == State.Healthy) ? WorkspaceState.HEALTHY : WorkspaceState.DEGRADED; diff --git a/tests/resources/basic/netcore.toml b/tests/resources/basic/netcore.toml index 7264206..f549b72 100644 --- a/tests/resources/basic/netcore.toml +++ b/tests/resources/basic/netcore.toml @@ -2,3 +2,8 @@ sshRepoUrl = "git@github.com:queil/k8s-debug-poc.git" csproj = "src/k8s-debug-poc.fsproj" tags = ["poc"] + +[aspnetcore.tasks.build] + bringDown = true + command = "dotnet" + args = ["build"]