From 677061e908f9449adf89a488708447702d37f85e Mon Sep 17 00:00:00 2001 From: Tareq Imbasher Date: Tue, 1 Oct 2024 19:00:42 +0300 Subject: [PATCH] Make .NET server stop if Tauri app exits ungracefully --- .../NetPad.Apps.App/ParentProcessTracker.cs | 69 +++++++++++ src/Apps/NetPad.Apps.App/Program.cs | 114 +++++++++--------- src/Apps/NetPad.Apps.App/ProgramArgs.cs | 61 ++++++++++ src/Apps/NetPad.Apps.App/ProgramExitCode.cs | 11 ++ src/Apps/NetPad.Apps.App/RunMode.cs | 7 ++ src/Apps/NetPad.Apps.App/Startup.cs | 8 +- .../src-tauri/src/dotnet_server_manager.rs | 38 +++++- 7 files changed, 242 insertions(+), 66 deletions(-) create mode 100644 src/Apps/NetPad.Apps.App/ParentProcessTracker.cs create mode 100644 src/Apps/NetPad.Apps.App/ProgramArgs.cs create mode 100644 src/Apps/NetPad.Apps.App/ProgramExitCode.cs create mode 100644 src/Apps/NetPad.Apps.App/RunMode.cs diff --git a/src/Apps/NetPad.Apps.App/ParentProcessTracker.cs b/src/Apps/NetPad.Apps.App/ParentProcessTracker.cs new file mode 100644 index 00000000..25bcb35d --- /dev/null +++ b/src/Apps/NetPad.Apps.App/ParentProcessTracker.cs @@ -0,0 +1,69 @@ +using System.Diagnostics; +using Microsoft.Extensions.Hosting; + +namespace NetPad; + +/// +/// Used to exit this program when the parent host process that started it exits. +/// +public static class ParentProcessTracker +{ + private static bool _initialized; + private static IHost? _thisHost; + + public static void ExitWhenParentProcessExists(int parentPid) + { + if (_initialized) + { + throw new InvalidOperationException("Already initialized"); + } + + Process? parentProcess = null; + + try + { + parentProcess = Process.GetProcessById(parentPid); + parentProcess.EnableRaisingEvents = true; + } + catch + { + // ignore + } + + if (parentProcess != null) + { + parentProcess.Exited += (_, _) => + { + try + { + _thisHost?.StopAsync(TimeSpan.FromSeconds(10)); + } + catch + { + // ignore + } + finally + { + Environment.Exit((int)ProgramExitCode.Success); + } + }; + + _initialized = true; + } + else + { + Console.WriteLine($"Parent process with ID '{parentPid}' is not running"); + Environment.Exit((int)ProgramExitCode.ParentProcessIsNotRunning); + } + } + + public static void SetThisHost(IHost host) + { + if (!_initialized) + { + throw new InvalidOperationException("Not initialized"); + } + + _thisHost = host; + } +} diff --git a/src/Apps/NetPad.Apps.App/Program.cs b/src/Apps/NetPad.Apps.App/Program.cs index 0e1ea1d8..79649f54 100644 --- a/src/Apps/NetPad.Apps.App/Program.cs +++ b/src/Apps/NetPad.Apps.App/Program.cs @@ -8,9 +8,6 @@ using NetPad.Application; using NetPad.Apps; using NetPad.Apps.Shells; -using NetPad.Apps.Shells.Electron; -using NetPad.Apps.Shells.Tauri; -using NetPad.Apps.Shells.Web; using NetPad.Configuration; using NetPad.Swagger; using Serilog; @@ -19,21 +16,48 @@ namespace NetPad; public static class Program { - internal static IShell Shell { get; private set; } = null!; - private static bool _isSwaggerCodeGenMode; + internal static IShell? Shell; - public static async Task Main(string[] args) + public static async Task Main(string[] rawArgs) { - if (args.Contains("--swagger")) + ProgramExitCode result; + var args = new ProgramArgs(rawArgs); + + if (args.RunMode == RunMode.SwaggerGen) { - _isSwaggerCodeGenMode = true; - return await GenerateSwaggerClientCodeAsync(args); + result = await GenerateSwaggerClientCodeAsync(args); + } + else + { + try + { + RunApp(args); + result = ProgramExitCode.Success; + } + catch (IOException ioException) when (ioException.Message.ContainsIgnoreCase("address already in use")) + { + Console.WriteLine($"Another instance is already running. {ioException.Message}"); + Shell?.ShowErrorDialog( + $"{AppIdentifier.AppName} Already Running", + $"{AppIdentifier.AppName} is already running. You cannot open multiple instances of {AppIdentifier.AppName}."); + result = ProgramExitCode.PortUnavailable; + } + catch (Exception ex) + { + Console.WriteLine($"Host terminated unexpectedly with error:\n{ex}"); + Log.Fatal(ex, "Host terminated unexpectedly"); + result = ProgramExitCode.UnexpectedError; + } + finally + { + Log.CloseAndFlush(); + } } - return RunApp(args); + return (int)result; } - private static async Task GenerateSwaggerClientCodeAsync(string[] args) + private static async Task GenerateSwaggerClientCodeAsync(ProgramArgs args) { try { @@ -65,83 +89,61 @@ private static async Task GenerateSwaggerClientCodeAsync(string[] args) { var content = await response.Content.ReadAsStringAsync(); Console.WriteLine(content); - return 1; + return ProgramExitCode.SwaggerGenError; } } await host.StopAsync(); - return 0; + return ProgramExitCode.Success; } catch (Exception e) { Console.WriteLine(e); - return 1; + return ProgramExitCode.SwaggerGenError; } } - private static int RunApp(string[] args) + private static void RunApp(ProgramArgs args) { - try + if (args.ParentPid.HasValue) { - Shell = CreateShell(args); - - var builder = CreateHostBuilder(args); + ParentProcessTracker.ExitWhenParentProcessExists(args.ParentPid.Value); + } - builder.ConfigureServices(s => s.AddSingleton(Shell)); + Shell = args.CreateShell(); - var host = builder.Build(); + var builder = CreateHostBuilder(args); - host.Run(); - return 0; - } - catch (IOException ioException) when (ioException.Message.ContainsIgnoreCase("address already in use")) - { - Console.WriteLine($"Another instance is already running. {ioException.Message}"); - Shell.ShowErrorDialog( - $"{AppIdentifier.AppName} Already Running", - $"{AppIdentifier.AppName} is already running. You cannot open multiple instances of {AppIdentifier.AppName}."); - return 1; - } - catch (Exception ex) - { - Console.WriteLine($"Host terminated unexpectedly with error:\n{ex}"); - Log.Fatal(ex, "Host terminated unexpectedly"); - return 1; - } - finally - { - Log.CloseAndFlush(); - } - } + builder.ConfigureServices(s => s.AddSingleton(Shell)); - private static IShell CreateShell(string[] args) - { - if (args.Any(a => a.ContainsIgnoreCase("/ELECTRONPORT"))) - { - return new ElectronShell(); - } + var host = builder.Build(); - if (args.Any(a => a.EqualsIgnoreCase("--tauri"))) + if (args.ParentPid.HasValue) { - return new TauriShell(); + ParentProcessTracker.SetThisHost(host); } - return new WebBrowserShell(); + host.Run(); } - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) + private static IHostBuilder CreateHostBuilder(ProgramArgs args) => + Host.CreateDefaultBuilder(args.Raw) .ConfigureAppConfiguration(config => { config.AddJsonFile("appsettings.Local.json", true); }) .UseSerilog((ctx, config) => { ConfigureLogging(config, ctx.Configuration); }) .ConfigureWebHostDefaults(webBuilder => { - if (_isSwaggerCodeGenMode) + if (args.RunMode == RunMode.SwaggerGen) { webBuilder.UseStartup(); } else { - Shell.ConfigureWebHost(webBuilder, args); + if (Shell == null) + { + throw new Exception("Shell has not been initialized"); + } + + Shell.ConfigureWebHost(webBuilder, args.Raw); webBuilder.UseStartup(); } }); diff --git a/src/Apps/NetPad.Apps.App/ProgramArgs.cs b/src/Apps/NetPad.Apps.App/ProgramArgs.cs new file mode 100644 index 00000000..b3be6c6e --- /dev/null +++ b/src/Apps/NetPad.Apps.App/ProgramArgs.cs @@ -0,0 +1,61 @@ +using System.Linq; +using NetPad.Apps.Shells; +using NetPad.Apps.Shells.Electron; +using NetPad.Apps.Shells.Tauri; +using NetPad.Apps.Shells.Web; + +namespace NetPad; + +public class ProgramArgs +{ + public ProgramArgs(string[] args) + { + Raw = args; + + RunMode = args.Contains("--swagger") ? RunMode.SwaggerGen : RunMode.Normal; + + var parentPidArg = Array.IndexOf(args, "--parent-pid"); + if (parentPidArg >= 0 && parentPidArg + 1 < args.Length) + { + if (int.TryParse(args[parentPidArg + 1], out var parentPid)) + { + ParentPid = parentPid; + } + else + { + Console.WriteLine($"Invalid parent pid: {args[parentPidArg + 1]}"); + Environment.Exit((int)ProgramExitCode.InvalidParentProcessPid); + } + } + } + + /// + /// The raw args passed to the program. + /// + public string[] Raw { get; } + + /// + /// The pid of the process that started this program. + /// + public int? ParentPid { get; } + + /// + /// The mode to run the program in. + /// + public RunMode RunMode { get; } + + public IShell CreateShell() + { + if (Raw.Any(a => a.ContainsIgnoreCase("/ELECTRONPORT"))) + { + return new ElectronShell(); + } + + if (Raw.Any(a => a.EqualsIgnoreCase("--tauri"))) + { + return new TauriShell(); + } + + return new WebBrowserShell(); + } +} diff --git a/src/Apps/NetPad.Apps.App/ProgramExitCode.cs b/src/Apps/NetPad.Apps.App/ProgramExitCode.cs new file mode 100644 index 00000000..e45e1130 --- /dev/null +++ b/src/Apps/NetPad.Apps.App/ProgramExitCode.cs @@ -0,0 +1,11 @@ +namespace NetPad; + +public enum ProgramExitCode +{ + Success = 0, + UnexpectedError = 1, + SwaggerGenError = 2, + PortUnavailable = 3, + InvalidParentProcessPid = 4, + ParentProcessIsNotRunning = 5, +} diff --git a/src/Apps/NetPad.Apps.App/RunMode.cs b/src/Apps/NetPad.Apps.App/RunMode.cs new file mode 100644 index 00000000..0e2135a4 --- /dev/null +++ b/src/Apps/NetPad.Apps.App/RunMode.cs @@ -0,0 +1,7 @@ +namespace NetPad; + +public enum RunMode +{ + Normal, + SwaggerGen +} diff --git a/src/Apps/NetPad.Apps.App/Startup.cs b/src/Apps/NetPad.Apps.App/Startup.cs index 761473fa..171ef1b7 100644 --- a/src/Apps/NetPad.Apps.App/Startup.cs +++ b/src/Apps/NetPad.Apps.App/Startup.cs @@ -50,7 +50,7 @@ public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironm Console.WriteLine($" - Environment: {webHostEnvironment.EnvironmentName}"); Console.WriteLine($" - WebRootPath: {webHostEnvironment.WebRootPath}"); Console.WriteLine($" - ContentRootPath: {webHostEnvironment.ContentRootPath}"); - Console.WriteLine($" - Shell: {Program.Shell.GetType().Name}"); + Console.WriteLine($" - Shell: {Program.Shell?.GetType().Name}"); } public IConfiguration Configuration { get; } @@ -163,7 +163,7 @@ public void ConfigureServices(IServiceCollection services) #endif // Allow Shell to add/modify any service registrations it needs - Program.Shell.ConfigureServices(services); + Program.Shell?.ConfigureServices(services); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) @@ -195,7 +195,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); - Program.Shell.ConfigureRequestPipeline(app, env); + Program.Shell?.ConfigureRequestPipeline(app, env); app.UseEndpoints(endpoints => { @@ -214,7 +214,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) #endif }); - Program.Shell.Initialize(app, env); + Program.Shell?.Initialize(app, env); } private static void InitializeHostInfo(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/src/Apps/NetPad.Apps.Shells.Tauri/TauriApp/src-tauri/src/dotnet_server_manager.rs b/src/Apps/NetPad.Apps.Shells.Tauri/TauriApp/src-tauri/src/dotnet_server_manager.rs index 80941692..bd031708 100644 --- a/src/Apps/NetPad.Apps.Shells.Tauri/TauriApp/src-tauri/src/dotnet_server_manager.rs +++ b/src/Apps/NetPad.Apps.Shells.Tauri/TauriApp/src-tauri/src/dotnet_server_manager.rs @@ -18,18 +18,42 @@ pub struct DotNetServerManager { impl DotNetServerManager { pub fn start_backend(&mut self, app_handle: &tauri::AppHandle) -> Result<(), String> { - let executable_path = app_handle + let exe_ext = if std::env::consts::OS == "windows" { + ".exe" + } else { + "" + }; + + let mut executable_path = app_handle .path() .resolve( - "resources/netpad-server/NetPad.Apps.App", + format!("resources/netpad-server/NetPad.Apps.App{exe_ext}"), BaseDirectory::Resource, ) .unwrap(); - let working_dir = app_handle - .path() - .resolve("resources/netpad-server", BaseDirectory::Resource) - .unwrap(); + if !executable_path.exists() { + // If running standalone app and resources folder is in same dir as executable + if let Ok(current_exe) = std::env::current_exe() { + executable_path = current_exe; + executable_path.pop(); + executable_path.push("resources"); + executable_path.push("netpad-server"); + executable_path.push(format!("NetPad.Apps.App{exe_ext}")); + } + + if !executable_path.exists() { + let msg = format!( + ".NET server executable was not found at path: '{}'", + executable_path.display() + ); + log::error!("{msg}"); + return Err(msg); + } + } + + let mut working_dir = executable_path.clone(); + working_dir.pop(); log::info!( "Starting .NET server backend at path: '{}' with working dir: '{}'", @@ -39,6 +63,8 @@ impl DotNetServerManager { let mut cmd = Command::new(executable_path); cmd.arg("--tauri"); + cmd.arg("--parent-pid"); + cmd.arg(std::process::id().to_string()); cmd.current_dir(dunce::canonicalize(working_dir).unwrap()); #[cfg(target_os = "windows")] {