Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions seqcli.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=STORAGEPATH/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=subcommand/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=syslogdt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tcpip/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tokenizes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=trailingindent/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unawaited/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unclosable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=urlacl/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=winmgmt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xmpweb/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
292 changes: 91 additions & 201 deletions src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,246 +17,136 @@
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;
using SeqCli.Forwarder.Cli.Features;
using SeqCli.Forwarder.ServiceProcess;
using SeqCli.Forwarder.Util;

namespace SeqCli.Cli.Commands.Forwarder;

// ReSharper disable once ClassNeverInstantiated.Global

namespace SeqCli.Cli.Commands.Forwarder
[Command("forwarder", "install", "Install the forwarder as a Windows service", Visibility = FeatureVisibility.Preview)]
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
class InstallCommand : Command
{
[Command("forwarder", "install", "Install the forwarder as a Windows service", Visibility = FeatureVisibility.Preview)]
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
class InstallCommand : Command
readonly StoragePathFeature _storagePath;
readonly ServiceCredentialsFeature _serviceCredentials;
readonly ListenUriFeature _listenUri;
public InstallCommand()
{
readonly StoragePathFeature _storagePath;
readonly ServiceCredentialsFeature _serviceCredentials;
readonly ListenUriFeature _listenUri;

bool _setup;

public InstallCommand()
{
_storagePath = Enable<StoragePathFeature>();
_listenUri = Enable<ListenUriFeature>();
_serviceCredentials = Enable<ServiceCredentialsFeature>();

Options.Add(
"setup",
"Install and start the service only if it does not exist; otherwise reconfigure the binary location",
_ => _setup = true);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Seq codebase, an earlier version of which this came from, includes a --setup flag on its service install command to handle reconfiguration during MSI upgrades, in case the user chose a different path to install the Seq binaries.

seqcli forwarder doesn't need that - it's sufficient to be able to forwarder uninstall and forwarder install at the command line, if the seqcli binaries move.

}
_storagePath = Enable<StoragePathFeature>();
_listenUri = Enable<ListenUriFeature>();
_serviceCredentials = Enable<ServiceCredentialsFeature>();
}

string ServiceUsername => _serviceCredentials.IsUsernameSpecified ? _serviceCredentials.Username : "NT AUTHORITY\\LocalService";
string ServiceUsername => _serviceCredentials.IsUsernameSpecified ? _serviceCredentials.Username : "NT AUTHORITY\\LocalService";

protected override Task<int> Run()
protected override Task<int> 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.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);
}
if (!_serviceCredentials.IsPasswordSpecified)
throw new ArgumentException(
"If a service user account is specified, a password for the account must also be specified.");

Console.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.StorageRootPath}...");
GiveFullControl(_storagePath.StorageRootPath);

Console.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.InternalLogPath}...");
GiveFullControl(_storagePath.InternalLogPath);
// 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);
}

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");
Console.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.StorageRootPath}...");
GiveFullControl(_storagePath.StorageRootPath);

var exePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, Program.BinaryName);
var forwarderRunCmdline = $"\"{exePath}\" run --storage=\"{_storagePath.StorageRootPath}\"";
Console.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.InternalLogPath}...");
GiveFullControl(_storagePath.InternalLogPath);

var binPath = forwarderRunCmdline.Replace("\"", "\\\"");
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 scCmdline = "create \"" + SeqCliForwarderWindowsService.WindowsServiceName + "\"" +
" binPath= \"" + binPath + "\"" +
" start= auto" +
" depend= Winmgmt/Tcpip/CryptSvc";
var exePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Program.BinaryName);
var forwarderRunCmdline = $"\"{exePath}\" forwarder run --pre --storage=\"{_storagePath.StorageRootPath}\"";

if (_serviceCredentials.IsUsernameSpecified)
scCmdline += $" obj= {_serviceCredentials.Username} password= {_serviceCredentials.Password}";
var binPath = forwarderRunCmdline.Replace("\"", "\\\"");

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 scCmdline = "create \"" + SeqCliForwarderWindowsService.WindowsServiceName + "\"" +
" binPath= \"" + binPath + "\"" +
" start= auto" +
" depend= Winmgmt/Tcpip/CryptSvc";

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");
if (_serviceCredentials.IsUsernameSpecified)
scCmdline += $" obj= {_serviceCredentials.Username} password= {_serviceCredentials.Password}";

Console.WriteLine("Service installed successfully.");
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.");
}

void GiveFullControl(string target)
{
if (target == null) throw new ArgumentNullException(nameof(target));
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");

if (!Directory.Exists(target))
Directory.CreateDirectory(target);
Console.WriteLine("Service installed successfully.");
}

var storageInfo = new DirectoryInfo(target);
var storageAccessControl = storageInfo.GetAccessControl();
storageAccessControl.AddAccessRule(new FileSystemAccessRule(ServiceUsername,
FileSystemRights.FullControl, AccessControlType.Allow));
storageInfo.SetAccessControl(storageAccessControl);
}
void GiveFullControl(string 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);
}

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;
}
}

Expand Down
Loading