< Summary - Combined Code Coverage

Information
Class: NLightning.Application.NLTG.Utilities.DaemonUtils
Assembly: NLightning.Application.NLTG
File(s): /home/runner/work/nlightning/nlightning/src/NLightning.Application.NLTG/Utilities/DaemonUtils.cs
Tag: 30_15166811759
Line coverage
0%
Covered lines: 0
Uncovered lines: 162
Coverable lines: 162
Total lines: 397
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 66
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
StartDaemonIfRequested(...)0%342180%
StartWindowsDaemon(...)0%7280%
StartMacOsDaemon(...)0%210140%
StartUnixDaemon(...)0%7280%
IsRunningAsDaemon()100%210%
GetPidFilePath(...)100%210%
StopDaemon(...)0%110100%
SendSignal(...)100%210%
SendCtrlEvent(...)100%210%
Fork()0%2040%
Setsid()0%2040%

File(s)

/home/runner/work/nlightning/nlightning/src/NLightning.Application.NLTG/Utilities/DaemonUtils.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Runtime.InteropServices;
 3using System.Text;
 4using Microsoft.Extensions.Configuration;
 5using Serilog;
 6
 7namespace NLightning.Application.NLTG.Utilities;
 8
 9using Constants;
 10
 11public partial class DaemonUtils
 12{
 13    /// <summary>
 14    /// Starts the application as a daemon process if requested
 15    /// </summary>
 16    /// <param name="args">Command line arguments</param>
 17    /// <param name="configuration">Configuration</param>
 18    /// <param name="pidFilePath">Path where to store the PID file</param>
 19    /// <param name="logger">Logger for startup messages</param>
 20    /// <returns>True if the parent process should exit, false to continue execution</returns>
 21    public static bool StartDaemonIfRequested(string[] args, IConfiguration configuration, string pidFilePath, ILogger l
 22    {
 23        // Check if we're already running as a daemon child process
 024        if (IsRunningAsDaemon())
 25        {
 026            return false; // Continue execution as daemon child
 27        }
 28
 29        // Check command line args (highest priority)
 030        var isDaemonRequested = Array.Exists(args, arg =>
 031            arg.Equals("--daemon", StringComparison.OrdinalIgnoreCase) ||
 032            arg.Equals("--daemon=true", StringComparison.OrdinalIgnoreCase));
 33
 34        // Check environment variable (middle priority)
 035        if (!isDaemonRequested)
 36        {
 037            var envDaemon = Environment.GetEnvironmentVariable("NLTG_DAEMON");
 038            isDaemonRequested = !string.IsNullOrEmpty(envDaemon) &&
 039                                (envDaemon.Equals("true", StringComparison.OrdinalIgnoreCase) ||
 040                                 envDaemon.Equals("1", StringComparison.OrdinalIgnoreCase));
 41        }
 42
 43        // Check configuration file (lowest priority)
 044        if (!isDaemonRequested)
 45        {
 046            isDaemonRequested = configuration.GetValue<bool>("Node:Daemon");
 47        }
 48
 049        if (!isDaemonRequested)
 50        {
 051            return false; // Continue normal execution
 52        }
 53
 054        logger.Information("Daemon mode requested, starting background process");
 55
 56        // Platform-specific daemon implementation
 057        return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
 058            ? StartWindowsDaemon(args, pidFilePath, logger)
 059            : RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
 060                ? StartMacOsDaemon(args, pidFilePath, logger) // Special implementation for macOS to avoid fork() issues
 061                : StartUnixDaemon(pidFilePath, logger); // Linux and other Unix systems
 62    }
 63
 64    private static bool StartWindowsDaemon(string[] args, string pidFilePath, ILogger logger)
 65    {
 66        try
 67        {
 68            // Create a new process info
 069            var startInfo = new ProcessStartInfo
 070            {
 071                FileName = Process.GetCurrentProcess().MainModule?.FileName,
 072                UseShellExecute = true,
 073                CreateNoWindow = true,
 074                WindowStyle = ProcessWindowStyle.Hidden,
 075                WorkingDirectory = Environment.CurrentDirectory
 076            };
 77
 78            // Copy all args except --daemon
 079            foreach (var arg in args)
 80            {
 081                if (!arg.StartsWith("--daemon", StringComparison.OrdinalIgnoreCase))
 82                {
 083                    startInfo.ArgumentList.Add(arg);
 84                }
 85            }
 86
 87            // Add special flag to indicate we're already in daemon mode
 088            startInfo.ArgumentList.Add("--daemon-child");
 89
 90            // Start the new process
 091            var process = Process.Start(startInfo);
 092            if (process == null)
 93            {
 094                logger.Error("Failed to start daemon process");
 095                return false;
 96            }
 97
 98            // Write PID to file
 099            File.WriteAllText(pidFilePath, process.Id.ToString());
 100
 0101            logger.Information("Daemon started with PID {PID}", process.Id);
 0102            return true; // Parent should exit
 103        }
 0104        catch (Exception ex)
 105        {
 0106            logger.Error(ex, "Error starting daemon process");
 0107            return false;
 108        }
 0109    }
 110
 111    /// <summary>
 112    /// Start daemon on macOS - uses a different approach than Linux to avoid fork() issues
 113    /// </summary>
 114    private static bool StartMacOsDaemon(string[] args, string pidFilePath, ILogger logger)
 115    {
 116        try
 117        {
 0118            logger.Information("Using macOS-specific daemon startup");
 119
 120            // Build the command line
 0121            var processPath = Process.GetCurrentProcess().MainModule?.FileName;
 0122            var arguments = new StringBuilder();
 123
 124            // Add all the original arguments except --daemon
 0125            foreach (var arg in args)
 126            {
 0127                if (arg.StartsWith("--daemon", StringComparison.OrdinalIgnoreCase))
 128                {
 129                    continue;
 130                }
 131
 132                // Quote the argument if it contains spaces
 0133                if (arg.Contains(' '))
 134                {
 0135                    arguments.Append($"\"{arg}\" ");
 136                }
 137                else
 138                {
 0139                    arguments.Append($"{arg} ");
 140                }
 141            }
 142
 143            // Add daemon-child argument
 0144            arguments.Append("--daemon-child");
 145
 146            // Create a shell script to launch the process and disown it
 0147            var scriptPath = Path.Combine(Path.GetTempPath(), $"nltg_daemon_{Guid.NewGuid()}.sh");
 148
 149            // Write the shell script
 0150            var scriptContent = $"""
 0151                                 #!/bin/bash
 0152                                 # Auto-generated daemon launcher for NLTG
 0153                                 nohup "{processPath}" {arguments} > /dev/null 2>&1 &
 0154                                 echo $! > "{pidFilePath}"
 0155
 0156                                 """;
 0157            File.WriteAllText(scriptPath, scriptContent);
 158
 159            // Make the script executable
 0160            var chmodProcess = Process.Start(new ProcessStartInfo
 0161            {
 0162                FileName = "chmod",
 0163                Arguments = $"+x \"{scriptPath}\"",
 0164                UseShellExecute = false,
 0165                CreateNoWindow = true
 0166            });
 0167            chmodProcess?.WaitForExit();
 168
 169            // Run the script
 0170            var scriptProcess = Process.Start(new ProcessStartInfo
 0171            {
 0172                FileName = "/bin/bash",
 0173                Arguments = $"\"{scriptPath}\"",
 0174                UseShellExecute = false,
 0175                CreateNoWindow = true
 0176            });
 0177            scriptProcess?.WaitForExit();
 178
 179            // Clean up the script file
 180            try
 181            {
 0182                File.Delete(scriptPath);
 0183            }
 0184            catch
 185            {
 186                // Ignore cleanup errors
 0187            }
 188
 189            // Verify PID file was created
 0190            if (File.Exists(pidFilePath))
 191            {
 0192                var pidContent = File.ReadAllText(pidFilePath).Trim();
 0193                logger.Information("macOS daemon started with PID {PID}", pidContent);
 0194                return true;
 195            }
 196
 0197            logger.Warning("PID file not created, daemon may not have started correctly");
 0198            return true; // Parent still exits even if there might be an issue
 199        }
 0200        catch (Exception ex)
 201        {
 0202            logger.Error(ex, "Error starting macOS daemon process");
 0203            return false;
 204        }
 0205    }
 206
 207    private static bool StartUnixDaemon(string pidFilePath, ILogger logger)
 208    {
 209        try
 210        {
 211            // First fork
 0212            var pid = Fork();
 213            switch (pid)
 214            {
 215                case < 0:
 0216                    logger.Error("First fork failed");
 0217                    return false;
 218                case > 0:
 219                    // Parent process exits
 0220                    logger.Information("Forked first process with PID {PID}", pid);
 0221                    return true;
 222            }
 223
 224            // Detach from terminal
 0225            _ = Setsid();
 226
 227            // Second fork
 0228            pid = Fork();
 229            switch (pid)
 230            {
 231                case < 0:
 0232                    logger.Error("Second fork failed");
 0233                    return false;
 234                case > 0:
 235                    // Exit the intermediate process
 0236                    Environment.Exit(0);
 237                    break;
 238            }
 239
 240            // Child process continues
 241            // Change working directory
 0242            Directory.SetCurrentDirectory("/");
 243
 244            // Close standard file descriptors
 0245            Console.SetIn(StreamReader.Null);
 0246            Console.SetOut(StreamWriter.Null);
 0247            Console.SetError(StreamWriter.Null);
 248
 249            // Write PID file
 0250            var currentPid = Environment.ProcessId;
 0251            File.WriteAllText(pidFilePath, currentPid.ToString());
 252
 0253            return false; // Continue execution in the child
 254        }
 0255        catch (Exception ex)
 256        {
 0257            logger.Error(ex, "Error starting Unix daemon process");
 0258            return false;
 259        }
 0260    }
 261
 262    /// <summary>
 263    /// Checks if this process is already running as daemon
 264    /// </summary>
 265    public static bool IsRunningAsDaemon()
 266    {
 0267        return Array.Exists(Environment.GetCommandLineArgs(),
 0268            arg => arg.Equals("--daemon-child", StringComparison.OrdinalIgnoreCase));
 269    }
 270
 271    /// <summary>
 272    /// Gets the path for the PID file
 273    /// </summary>
 274    public static string GetPidFilePath(string network)
 275    {
 0276        var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
 0277        var networkDir = Path.Combine(homeDir, DaemonConstants.DAEMON_FOLDER, network);
 0278        Directory.CreateDirectory(networkDir); // Ensure directory exists
 0279        return Path.Combine(networkDir, DaemonConstants.PID_FILE);
 280    }
 281
 282    /// <summary>
 283    /// Stops a running daemon if it exists
 284    /// </summary>
 285    public static bool StopDaemon(string pidFilePath, ILogger logger)
 286    {
 287        try
 288        {
 0289            if (!File.Exists(pidFilePath))
 290            {
 0291                logger.Warning("PID file not found, daemon may not be running");
 0292                return false;
 293            }
 294
 0295            var pidText = File.ReadAllText(pidFilePath).Trim();
 0296            if (!int.TryParse(pidText, out var pid))
 297            {
 0298                logger.Error("Invalid PID in file: {PidText}", pidText);
 0299                return false;
 300            }
 301
 302            try
 303            {
 0304                var process = Process.GetProcessById(pid);
 0305                logger.Information("Stopping daemon process with PID {PID}", pid);
 306
 307                // Send SIGTERM instead of Kill for graceful shutdown
 0308                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 309                {
 310                    // Windows - send Ctrl+C or use taskkill /PID {pid} /F
 0311                    SendCtrlEvent(process);
 312                }
 313                else
 314                {
 315                    // Unix/macOS - send SIGTERM
 0316                    SendSignal(pid, 15); // SIGTERM is 15
 317                }
 318
 319                // Wait for exit
 0320                var exited = process.WaitForExit(TimeSpan.FromSeconds(10));
 0321                if (exited)
 322                {
 0323                    logger.Information("Daemon process stopped successfully");
 0324                    File.Delete(pidFilePath);
 0325                    return true;
 326                }
 327
 328                // If graceful shutdown fails, force kill as last resort
 0329                logger.Warning("Daemon process did not exit gracefully, forcing termination");
 0330                process.Kill();
 0331                exited = process.WaitForExit(5000);
 0332                if (exited)
 333                {
 0334                    File.Delete(pidFilePath);
 0335                    return true;
 336                }
 337
 0338                return false;
 339            }
 0340            catch (ArgumentException)
 341            {
 0342                logger.Warning("No process found with PID {PID}, removing stale PID file", pid);
 0343                File.Delete(pidFilePath);
 0344                return false;
 345            }
 346        }
 0347        catch (Exception ex)
 348        {
 0349            logger.Error(ex, "Error stopping daemon");
 0350            return false;
 351        }
 0352    }
 353
 354    private static void SendSignal(int pid, int signal)
 355    {
 0356        Process.Start("kill", $"-{signal} {pid}").WaitForExit();
 0357    }
 358
 359    private static void SendCtrlEvent(Process process)
 360    {
 0361        Process.Start("taskkill", $"/PID {process.Id}").WaitForExit();
 0362    }
 363
 364    #region Native Methods
 365
 366    [LibraryImport("libc")]
 367    private static partial int fork();
 368
 369    [LibraryImport("libc")]
 370    private static partial int setsid();
 371
 372    private static int Fork()
 373    {
 374        // If not on Unix, simulate fork by returning -1
 0375        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
 0376            !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
 377        {
 0378            return -1;
 379        }
 380
 0381        return fork();
 382    }
 383
 384    private static int Setsid()
 385    {
 386        // If not on Unix, simulate setsid by returning -1
 0387        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
 0388            !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
 389        {
 0390            return -1;
 391        }
 392
 0393        return setsid();
 394    }
 395
 396    #endregion
 397}