Skip to content

Commit

Permalink
Watch for parent process exit (#333)
Browse files Browse the repository at this point in the history
* Add dependent process watcher service

Adds an IHostedService that will watch one or more dependent processes and call IHostApplicationLifetime.StopApplication when a dependent process exits

* Use dependent process watcher in example hosts and template

Registers the dependent process watcher hosted service if the `AppStoreConnect:Adapter:Host:ParentPid` configuration setting value is greater than zero.
  • Loading branch information
wazzamatazz authored May 30, 2023
1 parent 5ad2b0c commit 894bf94
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 3 deletions.
6 changes: 6 additions & 0 deletions examples/ExampleHostedAdapter/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
builder.Configuration
.AddJsonFile(Constants.AdapterSettingsFilePath, false, true);

// Parent PID. If specified, we will gracefully shut down if the parent process exits.
var pid = builder.Configuration.GetValue<int>("AppStoreConnect:Adapter:Host:ParentPid");
if (pid > 0) {
builder.Services.AddDependentProcessWatcher(pid);
}

// Host instance ID.
var instanceId = builder.Configuration.GetValue<string>("AppStoreConnect:Adapter:Host:InstanceId");
if (string.IsNullOrWhiteSpace(instanceId)) {
Expand Down
6 changes: 6 additions & 0 deletions examples/MinimalApiExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@

var builder = WebApplication.CreateBuilder(args);

// Parent PID. If specified, we will gracefully shut down if the parent process exits.
var pid = builder.Configuration.GetValue<int>("AppStoreConnect:Adapter:Host:ParentPid");
if (pid > 0) {
builder.Services.AddDependentProcessWatcher(pid);
}

builder.Services
.AddLocalization()
.AddProblemDetails();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Reflection;

using DataCore.Adapter;
Expand Down Expand Up @@ -63,7 +62,7 @@ public static IAdapterConfigurationBuilder AddDataCoreAdapterAspNetCoreServices(
/// <paramref name="hostInfo"/> is <see langword="null"/>.
/// </exception>
public static IAdapterConfigurationBuilder AddHostInfo(
this IAdapterConfigurationBuilder builder,
this IAdapterConfigurationBuilder builder,
HostInfo hostInfo
) {
if (builder == null) {
Expand Down Expand Up @@ -310,7 +309,7 @@ public static IAdapterConfigurationBuilder AddHostInfo(this IAdapterConfiguratio
.WithName(entryAssembly?.GetName()?.FullName)
.WithVersion(entryAssembly?.GetInformationalVersion())
.WithVendor(sp.GetService<VendorInfo>() ?? entryAssembly?.GetCustomAttribute<VendorInfoAttribute>()?.CreateVendorInfo());

AddOperatingSystemHostInfoProperty(hostInfoBuilder);
AddContainerHostInfoProperty(hostInfoBuilder);

Expand Down Expand Up @@ -507,6 +506,70 @@ public static IAdapterConfigurationBuilder AddDefaultAspNetCoreServices(this IAd
return builder;
}


/// <summary>
/// Registers an <see cref="IHostedService"/> that will request a graceful application
/// shutdown when any of the specified processes exit.
/// </summary>
/// <param name="services">
/// The <see cref="IServiceCollection"/>.
/// </param>
/// <param name="pid">
/// The PID of the first process to watch.
/// </param>
/// <param name="additionalPids">
/// The PID of any additional processes to watch.
/// </param>
/// <returns>
/// The <see cref="IServiceCollection"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="services"/> is <see langword="null"/>.
/// </exception>
/// <remarks>
/// This method is intended to allow adapter host applications that are started by an
/// external application such as App Store Connect to exit if the external application
/// exits without stopping the adapter host.
/// </remarks>
public static IServiceCollection AddDependentProcessWatcher(this IServiceCollection services, int pid, params int[] additionalPids) => AddDependentProcessWatcher(services, new[] { pid }.Concat(additionalPids));


/// <summary>
/// Registers an <see cref="IHostedService"/> that will request a graceful application
/// shutdown when any of the specified processes exit.
/// </summary>
/// <param name="services">
/// The <see cref="IServiceCollection"/>.
/// </param>
/// <param name="pids">
/// The PIDs of the processes to watch.
/// </param>
/// <returns>
/// The <see cref="IServiceCollection"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="services"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="pids"/> is <see langword="null"/>.
/// </exception>
/// <remarks>
/// This method is intended to allow adapter host applications that are started by an
/// external application such as App Store Connect to exit if the external application
/// exits without stopping the adapter host.
/// </remarks>
public static IServiceCollection AddDependentProcessWatcher(this IServiceCollection services, IEnumerable<int> pids) {
if (services == null) {
throw new ArgumentNullException(nameof(services));
}
if (pids == null) {
throw new ArgumentNullException(nameof(pids));
}

services.AddHostedService(sp => ActivatorUtilities.CreateInstance<DependentProcessWatcher>(sp, pids));
return services;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace DataCore.Adapter.AspNetCore.Internal {

/// <summary>
/// <see cref="IHostedService"/> that watches a set of dependent processes and will request
/// that the application gracefully exits if any of the dependent processes exit.
/// </summary>
internal sealed partial class DependentProcessWatcher : BackgroundService {

/// <summary>
/// The <see cref="IHostApplicationLifetime"/> that is used to request graceful shutdown
/// if required.
/// </summary>
private readonly IHostApplicationLifetime _hostApplicationLifetime;

/// <summary>
/// Logging.
/// </summary>
private readonly ILogger<DependentProcessWatcher> _logger;

/// <summary>
/// The process IDs to watch.
/// </summary>
private readonly int[] _pids;


/// <summary>
/// Creates a new <see cref="DependentProcessWatcher"/> instance.
/// </summary>
/// <param name="pids">
/// The process IDs to watch. Note that specifying a PID that does not exist will result
/// in immediate shutdown of the host application when <see cref="ExecuteAsync"/> is called.
/// </param>
/// <param name="hostApplicationLifetime">
/// The <see cref="IHostApplicationLifetime"/>.
/// </param>
/// <param name="logger">
/// The <see cref="ILogger"/>.
/// </param>
/// <exception cref="ArgumentNullException">
/// <paramref name="pids"/> is <see langword="null"/>.
/// </exception>
/// <remarks>
/// Specifying a PID that does not exist will result in immediate shutdown of the host
/// application when <see cref="ExecuteAsync"/> is called.
/// </remarks>
public DependentProcessWatcher(IEnumerable<int> pids, IHostApplicationLifetime hostApplicationLifetime, ILogger<DependentProcessWatcher> logger) {
_logger = logger;
_hostApplicationLifetime = hostApplicationLifetime;
_pids = pids?.ToArray() ?? throw new ArgumentNullException(nameof(pids));
}


/// <inheritdoc/>
protected override Task ExecuteAsync(CancellationToken stoppingToken) {
foreach (var pid in _pids) {
var process = Process.GetProcessById(pid);
if (process == null) {
LogProcessNotFound(pid);
_hostApplicationLifetime.StopApplication();
break;
}

var name = process.ProcessName;

LogWatchingProcess(pid, name);

process.EnableRaisingEvents = true;
process.Exited += (sender, args) => {
if (!stoppingToken.IsCancellationRequested) {
LogProcessExited(pid, name);
_hostApplicationLifetime.StopApplication();
}
};
}

return Task.CompletedTask;
}


[LoggerMessage(0, LogLevel.Information, "Watching dependent process '{name}' (PID: {pid}).")]
partial void LogWatchingProcess(int pid, string name);

[LoggerMessage(1, LogLevel.Warning, "Dependent process '{name}' (PID: {pid}) has exited.")]
partial void LogProcessExited(int pid, string name);

[LoggerMessage(2, LogLevel.Warning, "Dependent process {pid} does not exist or has already exited.")]
partial void LogProcessNotFound(int pid);

}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#nullable enable
static Microsoft.Extensions.DependencyInjection.CommonAdapterConfigurationExtensions.AddDependentProcessWatcher(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, int pid, params int[]! additionalPids) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.Extensions.DependencyInjection.CommonAdapterConfigurationExtensions.AddDependentProcessWatcher(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Collections.Generic.IEnumerable<int>! pids) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.Extensions.DependencyInjection.CommonAdapterConfigurationExtensions.AddHostInfo(this DataCore.Adapter.DependencyInjection.IAdapterConfigurationBuilder! builder, System.Action<DataCore.Adapter.Common.HostInfoBuilder!>! configure) -> DataCore.Adapter.DependencyInjection.IAdapterConfigurationBuilder!
static Microsoft.Extensions.DependencyInjection.CommonAdapterConfigurationExtensions.AddHostInfo(this DataCore.Adapter.DependencyInjection.IAdapterConfigurationBuilder! builder, System.Action<System.IServiceProvider!, DataCore.Adapter.Common.HostInfoBuilder!>! configure) -> DataCore.Adapter.DependencyInjection.IAdapterConfigurationBuilder!
static Microsoft.Extensions.DependencyInjection.CommonAdapterConfigurationExtensions.WithInstanceId(this DataCore.Adapter.Common.HostInfoBuilder! builder, string! instanceId) -> DataCore.Adapter.Common.HostInfoBuilder!
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
// Our adapter settings are stored in adaptersettings.json.
builder.Configuration.AddJsonFile(Constants.AdapterSettingsFilePath, false, true);

// Parent PID. If specified, we will gracefully shut down if the parent process exits.
var pid = builder.Configuration.GetValue<int>("AppStoreConnect:Adapter:Host:ParentPid");
if (pid > 0) {
builder.Services.AddDependentProcessWatcher(pid);
}

// Host instance ID.
var instanceId = builder.Configuration.GetValue<string>("AppStoreConnect:Adapter:Host:InstanceId");
if (string.IsNullOrWhiteSpace(instanceId)) {
Expand Down

0 comments on commit 894bf94

Please sign in to comment.