This solution is now available as a nuget package. The repository with the documentation is available on github.
This article, the second solution in particular, describes the mechanics of the solution in the nuget package. Additionally in the Before we start - prepare your system section you will find useful information on how to create a systemd service that creates /var/run/yourapp/
directory with the necessary write permissions for you.
Content
Before we start - prepare your system
For the purpose of this article I assume you followed Microsoft's tutorial on how to create a service from ASP.NET Core application on Linux. That mainly means you already have a service file called /etc/systemd/system/kestrel-helloapp.service
created.
PID files are stored in the /var/run/
directory. ASP.NET Core applications should run under different user than root
(e.g., under www-data
) and that means they are not allowed to write to /var/run
. We could manually create a subfolder and change it's owner but, on Ubuntu, it will disappear after next reboot. To solve this it is possible to modify the service definition file and add RuntimeDirectory=helloapp
line. Thanks to this line systemd will create helloapp
subdirectory in /var/run
on every service start.
It is also worth it to add PIDFile=/var/run/helloapp/helloapp.pid
line to the service definition file. This won't create a pid file for us unfortunately 😉 It tells systemd to cleanup PID file when service exits so it will double check for us that the file is gone when the app is stopped.
Your file, comparing to what's on Microsoft's site, should now looks like this:
[Unit]
Description=Example .NET Web API App running on Ubuntu
[Service]
WorkingDirectory=/var/www/helloapp
RuntimeDirectory=helloapp
ExecStart=/usr/bin/dotnet /var/www/helloapp/helloapp.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
User=www-data
PIDFile=/var/run/helloapp/helloapp.pid
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target
First solution - IHostApplicationLifetime in Startup file
IHostApplicationLifetime
allows to subscribe to application lifetime events like ApplicationStarted
, ApplicationStopping
or ApplicationStopped
. It can be injected the same way as any other service. For us a good place is Configure
method in Startup.cs
.
public class Startup
{
// [...]
public void Configure(/* ... */ IHostApplicationLifetime appLifetime)
{
// [...]
if (env.IsDevelopment())
{
// [...]
}
else
{
// [...]
HandlePidFile(appLifetime);
}
// [...]
}
private void HandlePidFile(IHostApplicationLifetime appLifetime)
{
string pidFile = Configuration.GetValue<string>("PidFile");
if (string.IsNullOrWhiteSpace(pidFile))
return;
WritePidFile(pidFile);
appLifetime.ApplicationStopped.Register(() => RemovePidFile(pidFile));
}
private void WritePidFile(string pidFile)
{
var processId = Environment.ProcessId.ToString();
File.WriteAllText(pidFile, processId);
}
private void RemovePidFile(string pidFile)
{
File.Delete(pidFile);
}
}
The above code injects IHostApplicationLifetime
into Configure
method. Then, for any other than development environment, retrieves PID file path from appsettings.json
file and creates that file by writing current process ID to it. It also registers ApplicationStopped
event handler that removes the file when the application is exiting.
Second solution - IHostApplicationLifetime in dedicated IHostedService
I think this solution is more elegant one because it doesn't pollutes the Startup file. Instead we are going to use a dedicated IHostedService
. Hosted services are services that are automatically started by ASP.NET on application start and are notified to stop on application shutdown. For more on this you can check this page. Implementation of such service may look as following:
public class PidFileHostedService : IHostedService
{
private readonly ILogger<PidFileHostedService> logger;
private readonly IConfiguration configuration;
private readonly IWebHostEnvironment env;
private bool isPidFileCreated = false;
private string pidFile = string.Empty;
public PidFileHostedService(ILogger<PidFileHostedService> logger,
IConfiguration configuration,
IWebHostEnvironment env)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
this.env = env ?? throw new ArgumentNullException(nameof(env));
}
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
if (env.IsDevelopment())
return;
pidFile = configuration.GetValue<string>("PidFile");
if (string.IsNullOrWhiteSpace(pidFile))
return;
await WritePidFile();
isPidFileCreated = true;
}
catch (Exception ex)
{
logger.LogError(ex, $"Unexpected error when starting {nameof(PidFileHostedService)}", null);
}
}
private async Task WritePidFile()
{
var processId = Environment.ProcessId.ToString();
await File.WriteAllTextAsync(pidFile, processId);
}
public Task StopAsync(CancellationToken cancellationToken)
{
try
{
if (isPidFileCreated)
File.Delete(pidFile);
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error when deleting PID file", null);
}
return Task.CompletedTask;
}
}
The logic here is exactly the same as in the previous solution. All necessary interfaces are registered in the DI by default in ASP.NET. The only thing needed to be done for this service to be fired is to register it in Startup.cs
:
services.AddHostedService<PidFileHostedService>();
Comments
There are no comments yet