Skip to content

Commit

Permalink
Make program work as a Windows Service, when installed as a service
Browse files Browse the repository at this point in the history
  • Loading branch information
ksmith committed Aug 14, 2023
1 parent 5d4cacb commit 05aa62c
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 14 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,4 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/
/FileWatcher/Properties/launchSettings.json
15 changes: 10 additions & 5 deletions FileWatcher/FileWatcher.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>TE.FileWatcher</RootNamespace>
<AssemblyName>fw</AssemblyName>
<Version>1.4.0</Version>
<Version>1.4.90</Version>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
Expand All @@ -23,9 +23,9 @@
<PackageTags>filesystem file-monitoring folder-monitoring</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageId>TE.FileWatcher</PackageId>
<PackageId>TE.FileWatcher</PackageId>
</PropertyGroup>

<ItemGroup>
<None Include="..\README.md">
<Pack>True</Pack>
Expand All @@ -34,12 +34,17 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.21216.1" />
</ItemGroup>

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
<None Update="Templates\config-template.xml">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
Expand Down
230 changes: 223 additions & 7 deletions FileWatcher/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@
using System.Diagnostics.CodeAnalysis;
using TE.FileWatcher.Configuration;
using TE.FileWatcher.Logging;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using System.CommandLine.IO;
using System.Text;
using System;

namespace TE.FileWatcher
{
/// <summary>
/// The main program class.
/// </summary>
class Program
internal class Program
{
// Success return code
private const int SUCCESS = 0;
Expand All @@ -28,6 +36,27 @@ class Program
/// </returns>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
static int Main(string[] args)
{
if (WindowsServiceHelpers.IsWindowsService())
{
return ServiceMain(args);
}
else
{
return InitWatcher(args);
}
}

/// <summary>
/// Sets up the RootCommand, which parses the command line and runs the filewatcher.
/// This is both used directly from Main, but also indirectly from the service, if running as a Windows Service.
/// </summary>
/// <param name="args"></param>
/// <param name="stoppingToken"></param>
/// <param name="console"></param>
/// <returns></returns>
[RequiresUnreferencedCode("Calls TE.FileWatcher.Configuration.IConfigurationFile.Read()")]
public static int InitWatcher(string[] args, CancellationToken? stoppingToken = null, WindowsBackgroundService.LoggingConsole? console = null)
{
RootCommand rootCommand = new()
{
Expand All @@ -39,10 +68,20 @@ static int Main(string[] args)
aliases: new string[] { "--configFile", "-cf" },
description: "The name of the configuration XML file."),
};
if (stoppingToken != null)
{
rootCommand.AddOption(new Option<CancellationToken?>(
alias: "--stoppingToken",
getDefaultValue: () => stoppingToken,
description: "CancellationToken")
{ IsHidden = true }
);
}
rootCommand.Description = "Monitors files and folders for changes.";
rootCommand.Handler = CommandHandler.Create<string, string>(Run);
rootCommand.Handler = CommandHandler.Create<string, string, CancellationToken?>(Run);

return rootCommand.Invoke(args);
rootCommand.TreatUnmatchedTokensAsErrors = true;
return rootCommand.Invoke(args, console);
}

/// <summary>
Expand All @@ -58,7 +97,7 @@ static int Main(string[] args)
/// Returns 0 if no error occurred, otherwise non-zero.
/// </returns>
[RequiresUnreferencedCode("Calls TE.FileWatcher.Configuration.IConfigurationFile.Read()")]
private static int Run(string folder, string configFile)
internal static int Run(string? folder, string? configFile, CancellationToken? stoppingToken = null)
{
IConfigurationFile config = new XmlFile(folder, configFile);

Expand All @@ -76,7 +115,7 @@ private static int Run(string folder, string configFile)
}

// Run the watcher tasks
if (StartWatchers(watches))
if (StartWatchers(watches, stoppingToken))
{
return SUCCESS;
}
Expand All @@ -86,6 +125,37 @@ private static int Run(string folder, string configFile)
}
}

/// <summary>
/// Runs the file/folder watcher as a Windows Service.
/// </summary>
/// <param name="folder">
/// The folder where the config and notifications files are stored.
/// </param>
/// <param name="configFileName">
/// The name of the configuration file.
/// </param>
/// <returns>
/// Returns 0 if no error occurred, otherwise non-zero.
/// </returns>
[RequiresUnreferencedCode("Calls TE.FileWatcher.Configuration.IConfigurationFile.Read()")]
internal static int ServiceMain(string[] args)
{
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "FileWatcher Service";
});

builder.Services.AddHostedService<WindowsBackgroundService>();

// See: https://github.com/dotnet/runtime/issues/47303
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));

IHost host = builder.Build();
host.Run();
return SUCCESS;
}

/// <summary>
/// Sets the logger.
/// </summary>
Expand Down Expand Up @@ -124,7 +194,7 @@ private static bool SetLogger(Watches watches)
/// <returns>
/// True if the tasks were started and run successfully, otherwise false.
/// </returns>
private static bool StartWatchers(Watches watches)
private static bool StartWatchers(Watches watches, CancellationToken? stoppingToken = null)
{
if (watches == null)
{
Expand All @@ -133,10 +203,156 @@ private static bool StartWatchers(Watches watches)
}

watches.Start();
new AutoResetEvent(false).WaitOne();
if (stoppingToken != null)
{
stoppingToken.Value.WaitHandle.WaitOne();
}
else
{
new AutoResetEvent(false).WaitOne(); // Will never return
}

Logger.WriteLine("All watchers have closed.");
return true;
}

}

public sealed class WindowsBackgroundService : BackgroundService
{
private readonly ILogger<WindowsBackgroundService> _eventLogger;

public WindowsBackgroundService(
ILogger<WindowsBackgroundService> logger) =>
(_eventLogger) = (logger);

[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
_eventLogger.LogInformation($"CommandLineArgs: {Environment.CommandLine}");

LoggingConsole loggingConsole = new LoggingConsole(_eventLogger);
var errorLogWriter = new ErrorLogWriter(_eventLogger);
Console.SetError(errorLogWriter);
var result = await Task.Run( () => Program.InitWatcher(Environment.GetCommandLineArgs(), stoppingToken, loggingConsole));
if (result != 0)
{
errorLogWriter.WriteLine();
_eventLogger.LogError("Process: {ProcessPath}\r\n{Message}", Environment.ProcessPath, $"Service stopped with error: {result}");
Environment.Exit(result);
}
}
catch (TaskCanceledException)
{
// When the stopping token is canceled, for example, a call made from services.msc,
// we shouldn't exit with a non-zero exit code. In other words, this is expected...
}
catch (Exception ex)
{
_eventLogger.LogError(ex, "Process: {ProcessPath}\r\n{Message}", Environment.ProcessPath, ex.Message);

// Terminates this process and returns an exit code to the operating system.
// This is required to avoid the 'BackgroundServiceExceptionBehavior', which
// performs one of two scenarios:
// 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
// 2. When set to "StopHost": will cleanly stop the host, and log errors.
//
// In order for the Windows Service Management system to leverage configured
// recovery options, we need to terminate the process with a non-zero exit code.
Environment.Exit(1);
}
}
public class LoggingConsole : IConsole
{
public readonly ILogger<WindowsBackgroundService> _eventLogger;

public LoggingConsole(ILogger<WindowsBackgroundService> eventLogger)
{
this._eventLogger = eventLogger;
}

public IStandardStreamWriter Out => new LoggingStandardWriter(this);

public bool IsOutputRedirected => true;

public IStandardStreamWriter Error => new LoggingErrorWriter(this);

public bool IsErrorRedirected => true;

public bool IsInputRedirected => false;

private class LoggingStandardWriter : IStandardStreamWriter
{
private readonly LoggingConsole _console;

public LoggingStandardWriter(LoggingConsole console)
{
_console = console;
}

public void Write(string value)
{
if (!string.IsNullOrEmpty(value?.Trim()))
_console._eventLogger.LogDebug("Process: {ProcessPath}\r\n{value}", Environment.ProcessPath, value);
}
}
private class LoggingErrorWriter : IStandardStreamWriter
{
private readonly LoggingConsole _console;

public LoggingErrorWriter(LoggingConsole console)
{
_console = console;
}
public void Write(string value)
{
if (!string.IsNullOrEmpty(value?.Trim()))
_console._eventLogger.LogError("Process: {ProcessPath}\r\n{value}", Environment.ProcessPath, value);
}
}
}
public class ErrorLogWriter : TextWriter
{
public override Encoding Encoding => Encoding.UTF8;

public readonly ILogger<WindowsBackgroundService> _eventLogger;
StringBuilder _message = new StringBuilder();

public ErrorLogWriter(ILogger<WindowsBackgroundService> eventLogger)
{
this._eventLogger = eventLogger;
}

public override void Write(string? value)
{
if (!string.IsNullOrEmpty(value))
_message.Append(value);
}

public override void Write(char ch)
{
_message.Append(ch);
}

public override void WriteLine(string? value)
{
if (!string.IsNullOrEmpty(value?.Trim()))
{
_message.Append(value);
}
WriteLine();
}
public override void WriteLine()
{
if (_message.Length > 0)
{
_eventLogger.LogError("Process: {ProcessPath}\r\n{value}", Environment.ProcessPath, _message);
_message.Clear();
}
}

}
}
}
4 changes: 2 additions & 2 deletions FileWatcher/Properties/PublishProfiles/win-x64.pubxml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net6.0\publish\win-x64</PublishDir>
<PublishDir>bin\Release\net6.0\win-x64\publish</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishReadyToRun>False</PublishReadyToRun>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>
16 changes: 16 additions & 0 deletions FileWatcher/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
},
"EventLog": {
"SourceName": "FileWatcher Service",
"LogName": "Application",
"LogLevel": {
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information",
"TE.FileWatcher.WindowsBackgroundService": "Error"
}
}
}
}

0 comments on commit 05aa62c

Please sign in to comment.