What you wrote is a standard Console app. A service shall expose Start and Stop facilities to be correctly interpreted by Windows (or by SystemD equivalently).
Your core loop of the main should be hosted on a IHostedService, or a Worker service.
Give a look at [here][1] to figure out what you have to do.
I give you here below a plausible Program.cs file that I wrote some time ago for a Linux service (there is actually no difference with Windows services, just remove the .UseSystemD()
call).
/// <summary>
/// Remote service App for Monday Parser and Database manager.
/// </summary>
public static class Program
{
private static readonly ILogger logger = new LoggerConfiguration()
.ReadFrom.Configuration(MondayConfiguration.Configuration, sectionName: "AppLog")
.CreateLogger().ForContext("Origin", "MondayService");
/// <summary>
/// Helper to create hosting environment.
/// </summary>
/// <param name="args">
/// command line arguments if any- no management is occurring now.
/// </param>
/// <returns>
/// </returns>
public static IHostBuilder CreateWebHostBuilder(string[] args)
{
string curDir = MondayConfiguration.DefineCurrentDir();
IConfigurationRoot config = new ConfigurationBuilder()
// .SetBasePath(Directory.GetCurrentDirectory())
.SetBasePath(curDir)
.AddJsonFile("servicelocationoptions.json", optional: false, reloadOnChange: true)
#if DEBUG
.AddJsonFile("appSettings.Debug.json")
#else
.AddJsonFile("appSettings.json")
#endif
.Build();
return Host.CreateDefaultBuilder(args)
.UseContentRoot(curDir)
.ConfigureAppConfiguration((_, configuration) =>
{
configuration
.AddIniFile("appSettings.ini", optional: true, reloadOnChange: true)
#if DEBUG
.AddJsonFile("appSettings.Debug.json")
#else
.AddJsonFile("appSettings.json")
#endif
.AddJsonFile("servicelocationoptions.json", optional: false, reloadOnChange: true);
})
.UseSerilog((_, services, configuration) => configuration
.ReadFrom.Configuration(config, sectionName: "AppLog")// (context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.WriteTo.Console())
// .UseSerilog(MondayConfiguration.Logger)
.ConfigureServices((hostContext, services) =>
{
services
.Configure<ServiceLocationOptions>(hostContext.Configuration.GetSection(key: nameof(ServiceLocationOptions)))
.Configure<HostOptions>(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(30));
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
ServiceLocationOptions? locationOptions = config.GetSection(nameof(ServiceLocationOptions)).Get<ServiceLocationOptions>();
string url = locationOptions?.HttpBase + "*:" + locationOptions?.Port;
webBuilder.UseUrls(url);
})
.UseSystemd();
}
Here below you find the main implementation of the service, I added a lot of XML to show you all the exception you may get when moving things around in the service and which methods should be implmented as a minimum to have the service to work.
If you remove all the variables that you don't understand, you will remain with a working skeleton of a windows service running simply a ping service. Try it.
/// <summary>
/// Main Worker class for managing the service inside SystemD.
/// </summary>
public partial class MondayService : BackgroundService
{
/// <summary>
/// Initializes a new instance of the <see cref="MondayService"/> class. Std ctr.
/// </summary>
/// <param name="mondayhub">
/// </param>
/// <param name="containerServer">
/// </param>
public MondayService(IHubContext<MondayHub, IMondayServiceHub> mondayhub,
IFeatureContainer containerServer)
{
_containerServer = containerServer;
_dbManager = _containerServer.DbManager;
_parser = _containerServer.Parser;
_syslogQueue = _containerServer.SyslogQueue;
_segmentManager = _containerServer.SegmentManager;
_orderManager = _containerServer.OrderManager;
while (!MondayConfiguration.SerilogFactoryReady)
{
Thread.Sleep(20);
}
// _logger = MondayConfiguration.LoggerFactory.CreateLogger("");
_logger = new LoggerConfiguration().ReadFrom.Configuration(MondayConfiguration.Configuration, sectionName: "AppLog").CreateLogger().ForContext("Origin", "MondayService");
_mondayHub = mondayhub;
}
/// <summary>
/// Setup activities for the Monday service.
/// </summary>
/// <param name="cancellationToken">
/// </param>
/// <returns>
/// </returns>
/// <exception cref="OverflowException">
/// <paramref><name>value</name></paramref> is less than <see cref="TimeSpan.MinValue"/>
/// or greater than <see cref="TimeSpan.MaxValue"/>.
/// -or- value is <see><cref>System.Double.PositiveInfinity</cref></see> .
/// -or- value is <see cref="double.NegativeInfinity"/>.
/// </exception>
/// <exception cref="AggregateException">
/// The task was canceled. The <see><cref>InnerExceptions</cref></see> collection
/// contains a <see cref="TaskCanceledException"/> object.
/// -or- An exception was thrown during the execution of the task. The
/// <see><cref>InnerExceptions</cref></see> collection contains information about the
/// exception or exceptions.
/// </exception>
/// <exception cref="TaskCanceledException">
/// The task has been canceled.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The <paramref><name>function</name></paramref> parameter was <see langword="null"/>.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The <see cref="CancellationTokenSource"/> associated with <paramref
/// name="cancellationToken"/> was disposed.
/// </exception>
/// <exception cref="IOException">
/// destFileName already exists and overwrite is <see langword="false"/>.
/// -or- An I/O error has occurred, e.g. while copying the file across disk volumes.
/// </exception>
/// <exception cref="UnauthorizedAccessException">
/// The caller does not have the required permission.
/// </exception>
/// <exception cref="SecurityException">
/// The caller does not have the required permission.
/// </exception>
/// <exception cref="FileNotFoundException">
/// <paramref><name>sourceFileName</name></paramref> was not found.
/// </exception>
public override Task StartAsync(CancellationToken cancellationToken)
{
_logger.DebugInfo().Information("MondayService: Starting StartAsync");
var parserRenewed = false;
if (_parser is null)
{
_parser = new();
_parser.OnParserSocketDataArrived +=
(sender, e) => _containerServer.BroadcastParserSocketDataViaSignalR(sender, e);
parserRenewed = true;
Task.Run(ParserSubscriptions, cancellationToken);
_logger.DebugInfo().Information("MondayService: Instantiating again the parser inside StartAsync");
}
if (_dbManager is null || parserRenewed)
{
_dbManager = new(_parser);
_logger.DebugInfo().Information("MondayService: Instantiating again the db manager inside StartAsync");
_dbManager.ConnectToParserSocket();
_dbManager.OnFilteredSocketDataArrived +=
(sender, e) => _containerServer.BroadcastFilteredSocketDataViaSignalR(sender, e);
if (!_tagsDataSavedOnce)
{
// ReSharper disable once ExceptionNotDocumented
Tags.SaveAll();
_tagsDataSavedOnce = true;
}
}
if (_segmentManager is null)
{
_segmentManager = new(_parser, _dbManager);
_segmentManager.ConnectToParserSocket(_parser);
_segmentManager.OnSegmentClosure += (sender, e) => _containerServer.BroadcastSegmentDataViaSignalR(sender, e);
_logger.DebugInfo().Information("MondayService: Instantiating again the segment manager inside StartAsync");
}
if (_orderManager is null)
{
_orderManager = new(_parser);
_orderManager.OnOrderManagerEvent +=
(sender, e) => _containerServer.BroadcastOrderManagerEventsViaSignalR(sender, e);
_logger.DebugInfo().Information("MondayService: Instantiating again the order manager inside StartAsync");
}
_logger.DebugInfo().Information("MondayService: Completing StartAsync");
return base.StartAsync(cancellationToken);
}
/// <summary>
/// Graceful shutdown and disposal of Monday service (parser and database manager comprised).
/// </summary>
/// <param name="cancellationToken">
/// </param>
/// <returns>
/// </returns>
/// <exception cref="IOException">
/// destFileName already exists and overwrite is <see langword="false"/>.
/// -or- An I/O error has occurred, e.g. while copying the file across disk volumes.
/// </exception>
/// <exception cref="UnauthorizedAccessException">
/// The caller does not have the required permission.
/// </exception>
/// <exception cref="AggregateException">
/// The task was canceled. The <see><cref>InnerExceptions</cref></see> collection
/// contains a <see cref="TaskCanceledException"/> object.
/// -or- An exception was thrown during the execution of the task. The
/// <see><cref>InnerExceptions</cref></see> collection contains information about the
/// exception or exceptions.
/// </exception>
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.DebugInfo().Information("MondayService: Starting StopAsync");
if (!_tagsDataSavedOnce)
{
Tags.SaveAll();
_tagsDataSavedOnce = true;
}
_logger.DebugInfo().Information("Stopping Monday Service hosted on Linux.");
if (_parser is not null) await _parser.UnsubscribeAllAsync();
foreach (string ex in Tags.Exchanges.ExchangeNames.ToList())
{
_parser?.DeactivateRest(ex);
}
_parser?.Dispose();
_dbManager?.Dispose();
_orderManager?.Dispose();
_segmentManager?.Dispose();
_logger.DebugInfo().Information("MondayService: Completing StopAsync");
await base.StopAsync(cancellationToken);
}
/// <summary>
/// Core loop of the service. Here all the assets are instantiated and managed up to
/// final disposal. This instantiates the SignalR service and manages it.
/// </summary>
/// <param name="stoppingToken">
/// </param>
/// <returns>
/// </returns>
/// <exception cref="TaskCanceledException">
/// The task has been canceled.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref><name>delay</name></paramref> represents a negative time interval other
/// than <see langword="TimeSpan.FromMilliseconds(-1)"/>.
/// -or- The <paramref><name>delay</name></paramref> argument's <see
/// cref="P:System.TimeSpan.TotalMilliseconds"/> property is greater than <see cref="int.MaxValue"/>.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The provided <paramref><name>cancellationToken</name></paramref> has already been disposed.
/// </exception>
/// <exception cref="OverflowException">
/// <paramref><name>value</name></paramref> is less than <see cref="TimeSpan.MinValue"/>
/// or greater than <see cref="TimeSpan.MaxValue"/>.
/// -or- <paramref><name>value</name></paramref> is <see cref="double.PositiveInfinity"/>.
/// -or- <paramref><name>value</name></paramref> is <see cref="double.NegativeInfinity"/>.
/// </exception>
/// <exception cref="IOException">
/// destFileName already exists and overwrite is <see langword="false"/>.
/// -or- An I/O error has occurred, e.g. while copying the file across disk volumes.
/// </exception>
/// <exception cref="UnauthorizedAccessException">
/// The caller does not have the required permission.
/// </exception>
/// <exception cref="SecurityException">
/// The caller does not have the required permission.
/// </exception>
/// <exception cref="FileNotFoundException">
/// <paramref><name>sourceFileName</name></paramref> was not found.
/// </exception>
/// <exception cref="DirectoryNotFoundException">
/// The specified path is invalid (for example, it is on an unmapped drive).
/// </exception>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.DebugInfo().Information("Monday Service starting at {Now}", DateTimeOffset.Now);
while (!stoppingToken.IsCancellationRequested)
{
Task tlog = FetchLogAsync(stoppingToken);
Task? tping = null;
int seconds = DateTimeOffset.Now.Second;
int minutes = DateTimeOffset.Now.Minute;
// logging a ping every 5 minutes
if (seconds < 5 && minutes % 1 == 0)
{
tping = Ping();
if (!_tagsDataSavedOnce)
{
Tags.SaveAll();
_tagsDataSavedOnce = true;
}
}
// looping every 5 seconds
var tLoop = Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
if (tping is null)
{
await Task.WhenAll(tlog, tLoop);
}
else
{
await Task.WhenAll(tlog, tLoop, tping);
}
}
_logger.DebugInfo().Information("Monday Service stopping at {Now}", DateTimeOffset.Now);
}
[MustUseReturnValue]
private Task FetchLogAsync(CancellationToken stoppingToken)
{
var ok = true;
while (ok)
{
try
{
ok = _containerServer.SyslogQueue.Reader.TryRead(
out (LogLevel lvl, string line) item);
if (ok)
{
switch (item.lvl)
{
case LogLevel.Trace:
case LogLevel.Debug:
_logger.DebugInfo().Debug("{FetchedMessage}", item.line);
break;
case LogLevel.Information:
_logger.DebugInfo().Information("{FetchedMessage}", item.line);
break;
case LogLevel.Warning:
_logger.DebugInfo().Warning("{FetchedMessage}", item.line);
break;
case LogLevel.Error:
_logger.DebugInfo().Error("{FetchedMessage}", item.line);
break;
case LogLevel.Critical:
_logger.Fatal("{FetchedMessage}", item.line);
break;
case LogLevel.None:
break;
}
}
if (stoppingToken.IsCancellationRequested)
{
ok = false;
}
}
catch
{
ok = false;
}
}
return Task.CompletedTask;
}
private Task<CallResult<UpdateSubscription>> ParserSubscriptions()
{
Guard.Against.Null(nameof(_parser));
return _parser!.SubscribeFromSettingsAsync();
}
private async Task Ping()
{
await _containerServer.SyslogQueue.Writer.WriteAsync((
LogLevel.Information,
$"Monday Service active at: {DateTime.UtcNow.ToLocalTime()}"));
}
/// <summary>
/// This is a debug utility to check whether the service creates too many ThreaPpool threads.
/// </summary>
public static class ProcessTracker
{
static ProcessTracker()
{
}
/// <summary>
/// See https://stackoverflow.com/questions/31633541/clrmd-throws-exception-when-creating-runtime/31745689#31745689
/// </summary>
/// <returns>
/// </returns>
public static string Scan()
{
StringBuilder sb = new();
StringBuilder answer = new();
answer.Append("Active Threads").Append(Environment.NewLine);
// Create the data target. This tells us the versions of CLR loaded in the target process.
int countThread = 0;
var pid = Environment.ProcessId;
using (var dataTarget = DataTarget.AttachToProcess(pid, 5000, AttachFlag.Passive))
{
// Note I just take the first version of CLR in the process. You can loop over
// every loaded CLR to handle the SxS case where both desktop CLR and .Net Core
// are loaded in the process.
ClrInfo version = dataTarget.ClrVersions[0];
var runtime = version.CreateRuntime();
// Walk each thread in the process.
foreach (ClrThread thread in runtime.Threads)
{
try
{
sb = new();
// The ClrRuntime.Threads will also report threads which have recently
// died, but their underlying data structures have not yet been cleaned
// up. This can potentially be useful in debugging (!threads displays
// this information with XXX displayed for their OS thread id). You
// cannot walk the stack of these threads though, so we skip them here.
if (!thread.IsAlive)
continue;
sb.Append("Thread ").AppendFormat("{0:X}", thread.OSThreadId).Append(':');
countThread++;
// Each thread tracks a "last thrown exception". This is the exception
// object which !threads prints. If that exception object is present, we
// will display some basic exception data here. Note that you can get
// the stack trace of the exception with ClrHeapException.StackTrace (we
// don't do that here).
ClrException? currException = thread.CurrentException;
if (currException is ClrException ex)
{
sb.Append("Exception: ")
.AppendFormat("{0:X}", ex.Address)
.Append(" (").Append(ex.Type.Name)
.Append("), HRESULT=")
.AppendFormat("{0:X}", ex.HResult)
.AppendLine();
}
// Walk the stack of the thread and print output similar to !ClrStack.
sb.AppendLine(" ------> Managed Call stack:");
var collection = thread.EnumerateStackTrace().ToList();
foreach (ClrStackFrame frame in collection)
{
// Note that CLRStackFrame currently only has three pieces of data:
// stack pointer, instruction pointer, and frame name (which comes
// from ToString). Future versions of this API will allow you to get
// the type/function/module of the method (instead of just the
// name). This is not yet implemented.
sb.Append(" ").Append(frame).AppendLine();
}
}
catch
{
//skip to the next
}
finally
{
answer.Append(sb);
}
}
}
answer.Append(Environment.NewLine).Append(" Total thread listed: ").Append(countThread);
return answer.ToString();
}
}
}
The main core loop is pinging a queue for logging. It is just a way to let something run over the service permanently while other classes do their work (in this case they are almost all based on EAP).
Definitely, look at MSDN at first. It seems that you are missing the basics.
[1]: https://learn.microsoft.com/en-us/dotnet/core/extensions/workers