From cbdd91a2d59257d5bbc97a23cf301797571c5e1a Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 22 Jul 2025 14:18:06 +1000 Subject: [PATCH 1/4] Tidy up shut-down procedure: stop shipping as quickly as possible so that in-flight batches aren't hard-cancelled --- src/SeqCli/Forwarder/Channel/ForwardingChannel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/SeqCli/Forwarder/Channel/ForwardingChannel.cs b/src/SeqCli/Forwarder/Channel/ForwardingChannel.cs index e5e10d40..7fb97d3a 100644 --- a/src/SeqCli/Forwarder/Channel/ForwardingChannel.cs +++ b/src/SeqCli/Forwarder/Channel/ForwardingChannel.cs @@ -51,7 +51,9 @@ public ForwardingChannel(BufferAppender appender, BufferReader reader, Bookmark reader.AdvanceTo(bookmarkValue.Value); } - while (true) + // Stopping shipping is a priority during shut-down, the work represented by the persistent buffer is unbounded + // so leaving it un-shipped avoids messier hard cancellation if we can't complete the work in time. + while (!_stop.IsCancellationRequested) { if (_hardCancel.IsCancellationRequested) return; From f2156a7a6d9df2ee2014a92475e67643439ccd4a Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 22 Jul 2025 14:45:34 +1000 Subject: [PATCH 2/4] Add some top level exception handling to `ForwardingChannel`'s pump tasks, opportunistically fix some path issues and simplify `forwarder install`. --- seqcli.sln.DotSettings | 1 + .../Cli/Commands/Forwarder/InstallCommand.cs | 293 ++++++------------ .../Forwarder/Channel/ForwardingChannel.cs | 77 +++-- .../Forwarder/Channel/ForwardingChannelMap.cs | 4 +- src/SeqCli/Forwarder/ForwarderModule.cs | 5 +- 5 files changed, 146 insertions(+), 234 deletions(-) diff --git a/seqcli.sln.DotSettings b/seqcli.sln.DotSettings index 93bc904b..6985c87f 100644 --- a/seqcli.sln.DotSettings +++ b/seqcli.sln.DotSettings @@ -38,4 +38,5 @@ True True True + True True \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs index 03d6d32e..e201e136 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs @@ -17,9 +17,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Runtime.InteropServices; using System.Security.AccessControl; -using System.ServiceProcess; using System.Threading.Tasks; using SeqCli.Cli.Features; using SeqCli.Config; @@ -29,234 +27,129 @@ // ReSharper disable once ClassNeverInstantiated.Global -namespace SeqCli.Cli.Commands.Forwarder -{ - [Command("forwarder", "install", "Install the forwarder as a Windows service", IsPreview = true)] - [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] - class InstallCommand : Command - { - readonly StoragePathFeature _storagePath; - readonly ServiceCredentialsFeature _serviceCredentials; - readonly ListenUriFeature _listenUri; - - bool _setup; +namespace SeqCli.Cli.Commands.Forwarder; - public InstallCommand() - { - _storagePath = Enable(); - _listenUri = Enable(); - _serviceCredentials = Enable(); +[Command("forwarder", "install", "Install the forwarder as a Windows service", IsPreview = true)] +[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] +class InstallCommand : Command +{ + readonly StoragePathFeature _storagePath; + readonly ServiceCredentialsFeature _serviceCredentials; + readonly ListenUriFeature _listenUri; - Options.Add( - "setup", - "Install and start the service only if it does not exist; otherwise reconfigure the binary location", - _ => _setup = true); - } + public InstallCommand() + { + _storagePath = Enable(); + _listenUri = Enable(); + _serviceCredentials = Enable(); + } - string ServiceUsername => _serviceCredentials.IsUsernameSpecified ? _serviceCredentials.Username : "NT AUTHORITY\\LocalService"; + string ServiceUsername => _serviceCredentials.IsUsernameSpecified ? _serviceCredentials.Username : "NT AUTHORITY\\LocalService"; - protected override Task Run() + protected override Task Run() + { + try { - try - { - if (!_setup) - { - Install(); - return Task.FromResult(0); - } - - var exit = Setup(); - if (exit == 0) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("Setup completed successfully."); - Console.ResetColor(); - } - return Task.FromResult(exit); - } - catch (DirectoryNotFoundException dex) - { - Console.WriteLine("Could not install the service, directory not found: " + dex.Message); - return Task.FromResult(-1); - } - catch (Exception ex) - { - Console.WriteLine("Could not install the service: " + ex.Message); - return Task.FromResult(-1); - } + Install(); + return Task.FromResult(0); } - - int Setup() + catch (DirectoryNotFoundException dex) { - ServiceController controller; - try - { - Console.WriteLine($"Checking the status of the {SeqCliForwarderWindowsService.WindowsServiceName} service..."); - - controller = new ServiceController(SeqCliForwarderWindowsService.WindowsServiceName); - Console.WriteLine("Status is {0}", controller.Status); - } - catch (InvalidOperationException) - { - Install(); - var controller2 = new ServiceController(SeqCliForwarderWindowsService.WindowsServiceName); - return Start(controller2); - } - - Console.WriteLine("Service is installed; checking path and dependency configuration..."); - Reconfigure(controller); - - return controller.Status != ServiceControllerStatus.Running ? Start(controller) : 0; + Console.WriteLine("Could not install the service, directory not found: " + dex.Message); + return Task.FromResult(-1); } - - static void Reconfigure(ServiceController controller) + catch (Exception ex) { - var sc = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "sc.exe"); - if (0 != CaptiveProcess.Run(sc, "config \"" + controller.ServiceName + "\" depend= Winmgmt/Tcpip/CryptSvc", Console.WriteLine, Console.WriteLine)) - Console.WriteLine("Could not reconfigure service dependencies; ignoring."); - - if (!ServiceConfiguration.GetServiceBinaryPath(controller, out var path)) - return; - - var current = "\"" + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Program.BinaryName) + "\""; - if (path.StartsWith(current)) - return; - - var seqRun = path.IndexOf(Program.BinaryName + "\" run", StringComparison.OrdinalIgnoreCase); - if (seqRun == -1) - { - Console.WriteLine("Current binary path is an unrecognized format."); - return; - } - - Console.WriteLine("Existing service binary path is: {0}", path); - - var trimmed = path.Substring((seqRun + Program.BinaryName + " ").Length); - var newPath = current + trimmed; - Console.WriteLine("Updating service binary path configuration to: {0}", newPath); - - var escaped = newPath.Replace("\"", "\\\""); - if (0 != CaptiveProcess.Run(sc, "config \"" + controller.ServiceName + "\" binPath= \"" + escaped + "\"", Console.WriteLine, Console.WriteLine)) - { - Console.WriteLine("Could not reconfigure service path; ignoring."); - return; - } - - Console.WriteLine("Service binary path reconfigured successfully."); + Console.WriteLine("Could not install the service: " + ex.Message); + return Task.FromResult(-1); } + } - static int Start(ServiceController controller) - { - controller.Start(); - - if (controller.Status != ServiceControllerStatus.Running) - { - Console.WriteLine("Waiting up to 60 seconds for the service to start (currently: " + controller.Status + ")..."); - controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(60)); - } - - if (controller.Status == ServiceControllerStatus.Running) - { - Console.WriteLine("Started."); - return 0; - } + void Install() + { + Console.WriteLine("Installing service..."); - Console.WriteLine("The service hasn't started successfully."); - return -1; + Console.WriteLine($"Updating the configuration in {_storagePath.ConfigFilePath}..."); + var config = SeqCliConfig.ReadFromFile(_storagePath.ConfigFilePath); + + if (!string.IsNullOrEmpty(_listenUri.ListenUri)) + { + config.Forwarder.Api.ListenUri = _listenUri.ListenUri; + SeqCliConfig.WriteToFile(config, _storagePath.ConfigFilePath); } - [DllImport("shlwapi.dll")] - static extern bool PathIsNetworkPath(string pszPath); - - void Install() + if (_serviceCredentials.IsUsernameSpecified) { - Console.WriteLine("Installing service..."); - - if (PathIsNetworkPath(_storagePath.StorageRootPath)) - throw new ArgumentException("Seq requires a local (or SAN) storage location; network shares are not supported."); - - Console.WriteLine($"Updating the configuration in {_storagePath.ConfigFilePath}..."); - var config = SeqCliConfig.ReadFromFile(_storagePath.ConfigFilePath); - - if (!string.IsNullOrEmpty(_listenUri.ListenUri)) - { - config.Forwarder.Api.ListenUri = _listenUri.ListenUri; - SeqCliConfig.WriteToFile(config, _storagePath.ConfigFilePath); - } + if (!_serviceCredentials.IsPasswordSpecified) + throw new ArgumentException( + "If a service user account is specified, a password for the account must also be specified."); - if (_serviceCredentials.IsUsernameSpecified) - { - if (!_serviceCredentials.IsPasswordSpecified) - throw new ArgumentException( - "If a service user account is specified, a password for the account must also be specified."); - - // https://technet.microsoft.com/en-us/library/cc794944(v=ws.10).aspx - Console.WriteLine($"Ensuring {_serviceCredentials.Username} is granted 'Log on as a Service' rights..."); - AccountRightsHelper.EnsureServiceLogOnRights(_serviceCredentials.Username); - } + // https://technet.microsoft.com/en-us/library/cc794944(v=ws.10).aspx + Console.WriteLine($"Ensuring {_serviceCredentials.Username} is granted 'Log on as a Service' rights..."); + AccountRightsHelper.EnsureServiceLogOnRights(_serviceCredentials.Username); + } - Console.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.StorageRootPath}..."); - GiveFullControl(_storagePath.StorageRootPath); + Console.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.StorageRootPath}..."); + GiveFullControl(_storagePath.StorageRootPath); - Console.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.InternalLogPath}..."); - GiveFullControl(_storagePath.InternalLogPath); + Console.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.InternalLogPath}..."); + GiveFullControl(_storagePath.InternalLogPath); - var listenUri = MakeListenUriReservationPattern(config.Forwarder.Api.ListenUri); - Console.WriteLine($"Adding URL reservation at {listenUri} for {ServiceUsername}..."); - var netshResult = CaptiveProcess.Run("netsh", $"http add urlacl url={listenUri} user=\"{ServiceUsername}\"", Console.WriteLine, Console.WriteLine); - if (netshResult != 0) - Console.WriteLine($"Could not add URL reservation for {listenUri}: `netsh` returned {netshResult}; ignoring"); + var listenUri = MakeListenUriReservationPattern(config.Forwarder.Api.ListenUri); + Console.WriteLine($"Adding URL reservation at {listenUri} for {ServiceUsername}..."); + var netshResult = CaptiveProcess.Run("netsh", $"http add urlacl url={listenUri} user=\"{ServiceUsername}\"", Console.WriteLine, Console.WriteLine); + if (netshResult != 0) + Console.WriteLine($"Could not add URL reservation for {listenUri}: `netsh` returned {netshResult}; ignoring"); - var exePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, Program.BinaryName); - var forwarderRunCmdline = $"\"{exePath}\" run --storage=\"{_storagePath.StorageRootPath}\""; + var exePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, Program.BinaryName); + var forwarderRunCmdline = $"\"{exePath}\" forwarder run --pre --storage=\"{_storagePath.StorageRootPath}\""; - var binPath = forwarderRunCmdline.Replace("\"", "\\\""); + var binPath = forwarderRunCmdline.Replace("\"", "\\\""); - var scCmdline = "create \"" + SeqCliForwarderWindowsService.WindowsServiceName + "\"" + - " binPath= \"" + binPath + "\"" + - " start= auto" + - " depend= Winmgmt/Tcpip/CryptSvc"; + var scCmdline = "create \"" + SeqCliForwarderWindowsService.WindowsServiceName + "\"" + + " binPath= \"" + binPath + "\"" + + " start= auto" + + " depend= Winmgmt/Tcpip/CryptSvc"; - if (_serviceCredentials.IsUsernameSpecified) - scCmdline += $" obj= {_serviceCredentials.Username} password= {_serviceCredentials.Password}"; + if (_serviceCredentials.IsUsernameSpecified) + scCmdline += $" obj= {_serviceCredentials.Username} password= {_serviceCredentials.Password}"; - var sc = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "sc.exe"); - if (0 != CaptiveProcess.Run(sc, scCmdline, Console.WriteLine, Console.WriteLine)) - { - throw new ArgumentException("Service setup failed"); - } + var sc = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "sc.exe"); + if (0 != CaptiveProcess.Run(sc, scCmdline, Console.WriteLine, Console.WriteLine)) + { + throw new ArgumentException("Service setup failed"); + } - Console.WriteLine("Setting service restart policy..."); - if (0 != CaptiveProcess.Run(sc, $"failure \"{SeqCliForwarderWindowsService.WindowsServiceName}\" actions= restart/60000/restart/60000/restart/60000// reset= 600000", Console.WriteLine, Console.WriteLine)) - Console.WriteLine("Could not set service restart policy; ignoring"); - Console.WriteLine("Setting service description..."); - if (0 != CaptiveProcess.Run(sc, $"description \"{SeqCliForwarderWindowsService.WindowsServiceName}\" \"Durable storage and forwarding of application log events\"", Console.WriteLine, Console.WriteLine)) - Console.WriteLine("Could not set service description; ignoring"); + Console.WriteLine("Setting service restart policy..."); + if (0 != CaptiveProcess.Run(sc, $"failure \"{SeqCliForwarderWindowsService.WindowsServiceName}\" actions= restart/60000/restart/60000/restart/60000// reset= 600000", Console.WriteLine, Console.WriteLine)) + Console.WriteLine("Could not set service restart policy; ignoring"); + Console.WriteLine("Setting service description..."); + if (0 != CaptiveProcess.Run(sc, $"description \"{SeqCliForwarderWindowsService.WindowsServiceName}\" \"Durable storage and forwarding of application log events\"", Console.WriteLine, Console.WriteLine)) + Console.WriteLine("Could not set service description; ignoring"); - Console.WriteLine("Service installed successfully."); - } + Console.WriteLine("Service installed successfully."); + } - void GiveFullControl(string target) - { - if (target == null) throw new ArgumentNullException(nameof(target)); + void GiveFullControl(string target) + { + if (target == null) throw new ArgumentNullException(nameof(target)); - if (!Directory.Exists(target)) - Directory.CreateDirectory(target); + if (!Directory.Exists(target)) + Directory.CreateDirectory(target); - var storageInfo = new DirectoryInfo(target); - var storageAccessControl = storageInfo.GetAccessControl(); - storageAccessControl.AddAccessRule(new FileSystemAccessRule(ServiceUsername, - FileSystemRights.FullControl, AccessControlType.Allow)); - storageInfo.SetAccessControl(storageAccessControl); - } + var storageInfo = new DirectoryInfo(target); + var storageAccessControl = storageInfo.GetAccessControl(); + storageAccessControl.AddAccessRule(new FileSystemAccessRule(ServiceUsername, + FileSystemRights.FullControl, AccessControlType.Allow)); + storageInfo.SetAccessControl(storageAccessControl); + } - static string MakeListenUriReservationPattern(string uri) - { - var listenUri = uri.Replace("localhost", "+"); - if (!listenUri.EndsWith("/")) - listenUri += "/"; - return listenUri; - } + static string MakeListenUriReservationPattern(string uri) + { + var listenUri = uri.Replace("localhost", "+"); + if (!listenUri.EndsWith("/")) + listenUri += "/"; + return listenUri; } } diff --git a/src/SeqCli/Forwarder/Channel/ForwardingChannel.cs b/src/SeqCli/Forwarder/Channel/ForwardingChannel.cs index 7fb97d3a..1d3573ff 100644 --- a/src/SeqCli/Forwarder/Channel/ForwardingChannel.cs +++ b/src/SeqCli/Forwarder/Channel/ForwardingChannel.cs @@ -5,6 +5,7 @@ using Seq.Api; using SeqCli.Forwarder.Storage; using SeqCli.Ingestion; +using Serilog; namespace SeqCli.Forwarder.Channel; @@ -29,49 +30,67 @@ public ForwardingChannel(BufferAppender appender, BufferReader reader, Bookmark _writer = channel.Writer; _writeWorker = Task.Run(async () => { - await foreach (var entry in channel.Reader.ReadAllAsync(hardCancel)) + try { - try + await foreach (var entry in channel.Reader.ReadAllAsync(hardCancel)) { - // TODO: chunk sizes, max chunks, ingestion log - appender.TryAppend(entry.Data.AsSpan(), 100_000_000); - entry.CompletionSource.SetResult(); - } - catch (Exception e) - { - entry.CompletionSource.TrySetException(e); + try + { + // TODO: chunk sizes, max chunks, ingestion log + appender.TryAppend(entry.Data.AsSpan(), 100_000_000); + entry.CompletionSource.SetResult(); + } + catch (Exception e) + { + entry.CompletionSource.TrySetException(e); + } } } + catch (Exception ex) + { + // We don't loop here; the exception was unexpected, so it's either hard cancellation or an + // unknown condition that could cause CPU-burning hot looping. + Log.ForContext().Fatal(ex, "Forwarding ingest reader failed and exited"); + } }, cancellationToken: hardCancel); _readWorker = Task.Run(async () => { - if (bookmark.TryGet(out var bookmarkValue)) + try { - reader.AdvanceTo(bookmarkValue.Value); - } - - // Stopping shipping is a priority during shut-down, the work represented by the persistent buffer is unbounded - // so leaving it un-shipped avoids messier hard cancellation if we can't complete the work in time. - while (!_stop.IsCancellationRequested) - { - if (_hardCancel.IsCancellationRequested) return; - - if (!reader.TryFillBatch(1024 * 1024, out var batch)) + if (bookmark.TryGet(out var bookmarkValue)) { - await Task.Delay(100, hardCancel); - continue; + reader.AdvanceTo(bookmarkValue.Value); } + + // Stopping shipping is a priority during shut-down, the work represented by the persistent buffer is unbounded + // so leaving it un-shipped avoids messier hard cancellation if we can't complete the work in time. + while (!_stop.IsCancellationRequested) + { + if (_hardCancel.IsCancellationRequested) return; - await LogShipper.ShipBuffer(connection, apiKey, batch.Value.AsArraySegment(), SendFailureHandling.Retry); + if (!reader.TryFillBatch(1024 * 1024, out var batch)) + { + await Task.Delay(100, hardCancel); + continue; + } - if (bookmark.TrySet(new BufferPosition(batch.Value.ReaderHead.ChunkId, - batch.Value.ReaderHead.Offset))) - { - reader.AdvanceTo(batch.Value.ReaderHead); - } + await LogShipper.ShipBuffer(connection, apiKey, batch.Value.AsArraySegment(), SendFailureHandling.Retry); + + if (bookmark.TrySet(new BufferPosition(batch.Value.ReaderHead.ChunkId, + batch.Value.ReaderHead.Offset))) + { + reader.AdvanceTo(batch.Value.ReaderHead); + } - batch.Value.Return(); + batch.Value.Return(); + } + } + catch (Exception ex) + { + // We don't loop here; the exception was unexpected, so it's either hard cancellation or an + // unknown condition that could cause CPU-burning hot looping. + Log.ForContext().Fatal(ex, "Forwarding log shipper failed and exited"); } }, cancellationToken: hardCancel); } diff --git a/src/SeqCli/Forwarder/Channel/ForwardingChannelMap.cs b/src/SeqCli/Forwarder/Channel/ForwardingChannelMap.cs index 08132ceb..f4cd09ff 100644 --- a/src/SeqCli/Forwarder/Channel/ForwardingChannelMap.cs +++ b/src/SeqCli/Forwarder/Channel/ForwardingChannelMap.cs @@ -35,7 +35,7 @@ ForwardingChannel OpenOrCreateChannel(string? apiKey, string name) var storePath = Path.Combine(_bufferPath, name); var store = new SystemStoreDirectory(storePath); - Log.Information("Opening local buffer in {StorePath}", storePath); + Log.ForContext().Information("Opening local buffer in {StorePath}", storePath); return new ForwardingChannel( BufferAppender.Open(store), @@ -71,7 +71,7 @@ public ForwardingChannel Get(string? apiKey) public async Task StopAsync() { - Log.Information("Flushing log buffers"); + Log.ForContext().Information("Flushing log buffers"); _shutdownTokenSource.CancelAfter(TimeSpan.FromSeconds(30)); diff --git a/src/SeqCli/Forwarder/ForwarderModule.cs b/src/SeqCli/Forwarder/ForwarderModule.cs index 960c01d1..70a6af86 100644 --- a/src/SeqCli/Forwarder/ForwarderModule.cs +++ b/src/SeqCli/Forwarder/ForwarderModule.cs @@ -23,7 +23,6 @@ using SeqCli.Forwarder.Web.Host; using Serilog; using Serilog.Formatting; -using Serilog.Formatting.Display; using Serilog.Templates; namespace SeqCli.Forwarder; @@ -52,13 +51,13 @@ protected override void Load(ContainerBuilder builder) if (_config.Forwarder.Diagnostics.ExposeIngestionLog) { - Log.Warning("Configured to expose ingestion log via HTTP API"); + Log.ForContext().Warning("Configured to expose ingestion log via HTTP API"); builder.RegisterType().As(); var ingestionLogTemplate = "[{@t:o} {@l:u3}] {@m}\n"; if (_config.Forwarder.Diagnostics.IngestionLogShowDetail) { - Log.Warning("Including full client, payload, and error detail in the ingestion log"); + Log.ForContext().Warning("Including full client, payload, and error detail in the ingestion log"); ingestionLogTemplate += "{#if ClientHostIP is not null}Client IP address: {ClientHostIP}\n{#end}" + "{#if DocumentStart is not null}First {StartToLog} characters of payload: {DocumentStart:l}\n{#end}" + From 6cf31db1518f1defcaa3aa3618d8f9f52aec7a56 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 22 Jul 2025 14:47:01 +1000 Subject: [PATCH 3/4] Clean up in `forwarder install` command --- seqcli.sln.DotSettings | 2 ++ src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/seqcli.sln.DotSettings b/seqcli.sln.DotSettings index 6985c87f..4474106a 100644 --- a/seqcli.sln.DotSettings +++ b/seqcli.sln.DotSettings @@ -34,9 +34,11 @@ True True True + True True True True True True + True True \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs index e201e136..ba32f466 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs @@ -101,7 +101,7 @@ void Install() if (netshResult != 0) Console.WriteLine($"Could not add URL reservation for {listenUri}: `netsh` returned {netshResult}; ignoring"); - var exePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, Program.BinaryName); + var exePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Program.BinaryName); var forwarderRunCmdline = $"\"{exePath}\" forwarder run --pre --storage=\"{_storagePath.StorageRootPath}\""; var binPath = forwarderRunCmdline.Replace("\"", "\\\""); @@ -117,7 +117,7 @@ void Install() var sc = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "sc.exe"); if (0 != CaptiveProcess.Run(sc, scCmdline, Console.WriteLine, Console.WriteLine)) { - throw new ArgumentException("Service setup failed"); + throw new ArgumentException("Service setup failed."); } Console.WriteLine("Setting service restart policy..."); @@ -132,8 +132,6 @@ void Install() void GiveFullControl(string target) { - if (target == null) throw new ArgumentNullException(nameof(target)); - if (!Directory.Exists(target)) Directory.CreateDirectory(target); @@ -147,7 +145,7 @@ void GiveFullControl(string target) static string MakeListenUriReservationPattern(string uri) { var listenUri = uri.Replace("localhost", "+"); - if (!listenUri.EndsWith("/")) + if (!listenUri.EndsWith('/')) listenUri += "/"; return listenUri; } From e6ba3ff1ee5dce999e90dfb4fc57c458e2f91f8b Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 23 Jul 2025 10:37:43 +1000 Subject: [PATCH 4/4] Fix bad merge --- src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs index b4806570..cbdcfde1 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs @@ -25,6 +25,8 @@ using SeqCli.Forwarder.ServiceProcess; using SeqCli.Forwarder.Util; +namespace SeqCli.Cli.Commands.Forwarder; + // ReSharper disable once ClassNeverInstantiated.Global [Command("forwarder", "install", "Install the forwarder as a Windows service", Visibility = FeatureVisibility.Preview)]