< Summary - Combined Code Coverage

Information
Class: NLightning.Daemon.Utilities.DaemonUtils
Assembly: NLightning.Daemon
File(s): /home/runner/work/NLightning/NLightning/src/NLightning.Daemon/Utilities/DaemonUtils.cs
Tag: 57_24045730253
Line coverage
0%
Covered lines: 0
Uncovered lines: 193
Coverable lines: 193
Total lines: 440
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
ShowUsage()100%210%
IsStopRequested(...)100%210%
IsStatusRequested(...)100%210%
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.Daemon/Utilities/DaemonUtils.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Runtime.InteropServices;
 3using System.Text;
 4using Microsoft.Extensions.Configuration;
 5using Serilog;
 6
 7namespace NLightning.Daemon.Utilities;
 8
 9using Contracts.Constants;
 10
 11public partial class DaemonUtils
 12{
 13    public static void ShowUsage()
 14    {
 015        Console.WriteLine("NLTG - NLightning Daemon");
 016        Console.WriteLine("Usage:");
 017        Console.WriteLine("  nltg [options]");
 018        Console.WriteLine("  nltg --stop         Stop a running daemon");
 019        Console.WriteLine("  nltg --status       Show daemon status");
 020        Console.WriteLine();
 021        Console.WriteLine("Options:");
 022        Console.WriteLine("  --network, -n <network>    Network to use (mainnet, testnet, regtest) [default: mainnet]");
 023        Console.WriteLine("  --config, -c <path>        Path to custom configuration file");
 024        Console.WriteLine("  --daemon <true|false>      Run as a daemon [default: false]");
 025        Console.WriteLine("  --stop                     Stop a running daemon");
 026        Console.WriteLine("  --status                   Show daemon status information");
 027        Console.WriteLine("  --help, -h, -?             Show this help message");
 028        Console.WriteLine();
 029        Console.WriteLine("Environment Variables:");
 030        Console.WriteLine("  NLTG_NETWORK               Network to use");
 031        Console.WriteLine("  NLTG_CONFIG                Path to custom configuration file");
 032        Console.WriteLine("  NLTG_DAEMON                Run as a daemon");
 033        Console.WriteLine();
 034        Console.WriteLine("Configuration File:");
 035        Console.WriteLine("  Default path: ~/.nltg/{network}/appsettings.json");
 036        Console.WriteLine("  Settings:");
 037        Console.WriteLine("  {");
 038        Console.WriteLine("    \"Daemon\": true,         # Run as a background daemon");
 039        Console.WriteLine("    ... other settings ...");
 040        Console.WriteLine("  }");
 041        Console.WriteLine();
 042        Console.WriteLine("PID file location: ~/.nltg/{network}/nltg.pid");
 043    }
 44
 45    public static bool IsStopRequested(string[] args)
 46    {
 047        return args.Any(arg =>
 048                            arg.Equals("--stop", StringComparison.OrdinalIgnoreCase));
 49    }
 50
 51    public static bool IsStatusRequested(string[] args)
 52    {
 053        return args.Any(arg =>
 054                            arg.Equals("--status", StringComparison.OrdinalIgnoreCase));
 55    }
 56
 57    /// <summary>
 58    /// Starts the application as a daemon process if requested
 59    /// </summary>
 60    /// <param name="args">Command line arguments</param>
 61    /// <param name="configuration">Configuration</param>
 62    /// <param name="pidFilePath">Path where to store the PID file</param>
 63    /// <param name="logger">Logger for startup messages</param>
 64    /// <returns>True if the parent process should exit, false to continue execution</returns>
 65    public static bool StartDaemonIfRequested(string[] args, IConfiguration configuration, string pidFilePath,
 66                                              ILogger logger)
 67    {
 68        // Check if we're already running as a daemon child process
 069        if (IsRunningAsDaemon())
 70        {
 071            return false; // Continue execution as a daemon child
 72        }
 73
 74        // Check command line args (the highest priority)
 075        var isDaemonRequested = Array.Exists(args, arg =>
 076                                                 arg.Equals("--daemon", StringComparison.OrdinalIgnoreCase) ||
 077                                                 arg.Equals("--daemon=true", StringComparison.OrdinalIgnoreCase));
 78
 79        // Check environment variable (middle priority)
 080        if (!isDaemonRequested)
 81        {
 082            var envDaemon = Environment.GetEnvironmentVariable("NLTG_DAEMON");
 083            isDaemonRequested = !string.IsNullOrEmpty(envDaemon) &&
 084                                (envDaemon.Equals("true", StringComparison.OrdinalIgnoreCase) ||
 085                                 envDaemon.Equals("1", StringComparison.OrdinalIgnoreCase));
 86        }
 87
 88        // Check configuration file (lowest priority)
 089        if (!isDaemonRequested)
 90        {
 091            isDaemonRequested = configuration.GetValue<bool>("Node:Daemon");
 92        }
 93
 094        if (!isDaemonRequested)
 95        {
 096            return false; // Continue normal execution
 97        }
 98
 099        logger.Information("Daemon mode requested, starting background process");
 100
 101        // Platform-specific daemon implementation
 0102        return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
 0103                   ? StartWindowsDaemon(args, pidFilePath, logger)
 0104                   : RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
 0105                       ? StartMacOsDaemon(args, pidFilePath,
 0106                                          logger) // Special implementation for macOS to avoid fork() issues
 0107                       : StartUnixDaemon(pidFilePath, logger); // Linux and other Unix systems
 108    }
 109
 110    private static bool StartWindowsDaemon(string[] args, string pidFilePath, ILogger logger)
 111    {
 112        try
 113        {
 114            // Create a new process info
 0115            var startInfo = new ProcessStartInfo
 0116            {
 0117                FileName = Process.GetCurrentProcess().MainModule?.FileName,
 0118                UseShellExecute = true,
 0119                CreateNoWindow = true,
 0120                WindowStyle = ProcessWindowStyle.Hidden,
 0121                WorkingDirectory = Environment.CurrentDirectory
 0122            };
 123
 124            // Copy all args except --daemon
 0125            foreach (var arg in args)
 126            {
 0127                if (!arg.StartsWith("--daemon", StringComparison.OrdinalIgnoreCase))
 128                {
 0129                    startInfo.ArgumentList.Add(arg);
 130                }
 131            }
 132
 133            // Add a special flag to indicate we're already in daemon mode
 0134            startInfo.ArgumentList.Add("--daemon-child");
 135
 136            // Start the new process
 0137            var process = Process.Start(startInfo);
 0138            if (process == null)
 139            {
 0140                logger.Error("Failed to start daemon process");
 0141                return false;
 142            }
 143
 144            // Write PID to file
 0145            File.WriteAllText(pidFilePath, process.Id.ToString());
 146
 0147            logger.Information("Daemon started with PID {PID}", process.Id);
 0148            return true; // Parent should exit
 149        }
 0150        catch (Exception ex)
 151        {
 0152            logger.Error(ex, "Error starting daemon process");
 0153            return false;
 154        }
 0155    }
 156
 157    /// <summary>
 158    /// Start daemon on macOS - uses a different approach than Linux to avoid fork() issues
 159    /// </summary>
 160    private static bool StartMacOsDaemon(string[] args, string pidFilePath, ILogger logger)
 161    {
 162        try
 163        {
 0164            logger.Information("Using macOS-specific daemon startup");
 165
 166            // Build the command line
 0167            var processPath = Process.GetCurrentProcess().MainModule?.FileName;
 0168            var arguments = new StringBuilder();
 169
 170            // Add all the original arguments except --daemon
 0171            foreach (var arg in args)
 172            {
 0173                if (arg.StartsWith("--daemon", StringComparison.OrdinalIgnoreCase))
 174                {
 175                    continue;
 176                }
 177
 178                // Quote the argument if it contains spaces
 0179                if (arg.Contains(' '))
 180                {
 0181                    arguments.Append($"\"{arg}\" ");
 182                }
 183                else
 184                {
 0185                    arguments.Append($"{arg} ");
 186                }
 187            }
 188
 189            // Add daemon-child argument
 0190            arguments.Append("--daemon-child");
 191
 192            // Create a shell script to launch the process and disown it
 0193            var scriptPath = Path.Combine(Path.GetTempPath(), $"nltg_daemon_{Guid.NewGuid()}.sh");
 194
 195            // Write the shell script
 0196            var scriptContent = $"""
 0197                                 #!/bin/bash
 0198                                 # Auto-generated daemon launcher for NLTG
 0199                                 nohup "{processPath}" {arguments} > /dev/null 2>&1 &
 0200                                 echo $! > "{pidFilePath}"
 0201
 0202                                 """;
 0203            File.WriteAllText(scriptPath, scriptContent);
 204
 205            // Make the script executable
 0206            var chmodProcess = Process.Start(new ProcessStartInfo
 0207            {
 0208                FileName = "chmod",
 0209                Arguments = $"+x \"{scriptPath}\"",
 0210                UseShellExecute = false,
 0211                CreateNoWindow = true
 0212            });
 0213            chmodProcess?.WaitForExit();
 214
 215            // Run the script
 0216            var scriptProcess = Process.Start(new ProcessStartInfo
 0217            {
 0218                FileName = "/bin/bash",
 0219                Arguments = $"\"{scriptPath}\"",
 0220                UseShellExecute = false,
 0221                CreateNoWindow = true
 0222            });
 0223            scriptProcess?.WaitForExit();
 224
 225            // Clean up the script file
 226            try
 227            {
 0228                File.Delete(scriptPath);
 0229            }
 0230            catch
 231            {
 232                // Ignore cleanup errors
 0233            }
 234
 235            // Verify the PID file was created
 0236            if (File.Exists(pidFilePath))
 237            {
 0238                var pidContent = File.ReadAllText(pidFilePath).Trim();
 0239                logger.Information("macOS daemon started with PID {PID}", pidContent);
 0240                return true;
 241            }
 242
 0243            logger.Warning("PID file not created, daemon may not have started correctly");
 0244            return true; // Parent still exits even if there might be an issue
 245        }
 0246        catch (Exception ex)
 247        {
 0248            logger.Error(ex, "Error starting macOS daemon process");
 0249            return false;
 250        }
 0251    }
 252
 253    private static bool StartUnixDaemon(string pidFilePath, ILogger logger)
 254    {
 255        try
 256        {
 257            // First fork
 0258            var pid = Fork();
 259            switch (pid)
 260            {
 261                case < 0:
 0262                    logger.Error("First fork failed");
 0263                    return false;
 264                case > 0:
 265                    // Parent process exits
 0266                    logger.Information("Forked first process with PID {PID}", pid);
 0267                    return true;
 268            }
 269
 270            // Detach from terminal
 0271            _ = Setsid();
 272
 273            // Second fork
 0274            pid = Fork();
 275            switch (pid)
 276            {
 277                case < 0:
 0278                    logger.Error("Second fork failed");
 0279                    return false;
 280                case > 0:
 281                    // Exit the intermediate process
 0282                    Environment.Exit(0);
 283                    break;
 284            }
 285
 286            // Child process continues
 287            // Change working directory
 0288            Directory.SetCurrentDirectory("/");
 289
 290            // Close standard file descriptors
 0291            Console.SetIn(StreamReader.Null);
 0292            Console.SetOut(StreamWriter.Null);
 0293            Console.SetError(StreamWriter.Null);
 294
 295            // Write the PID file
 0296            var currentPid = Environment.ProcessId;
 0297            File.WriteAllText(pidFilePath, currentPid.ToString());
 298
 0299            return false; // Continue execution in the child
 300        }
 0301        catch (Exception ex)
 302        {
 0303            logger.Error(ex, "Error starting Unix daemon process");
 0304            return false;
 305        }
 0306    }
 307
 308    /// <summary>
 309    /// Checks if this process is already running as daemon
 310    /// </summary>
 311    public static bool IsRunningAsDaemon()
 312    {
 0313        return Array.Exists(Environment.GetCommandLineArgs(),
 0314                            arg => arg.Equals("--daemon-child", StringComparison.OrdinalIgnoreCase));
 315    }
 316
 317    /// <summary>
 318    /// Gets the path for the PID file
 319    /// </summary>
 320    public static string GetPidFilePath(string configPath)
 321    {
 0322        return Path.Combine(configPath, NodeConstants.PidFile);
 323    }
 324
 325    /// <summary>
 326    /// Stops a running daemon if it exists
 327    /// </summary>
 328    public static bool StopDaemon(string pidFilePath, ILogger logger)
 329    {
 330        try
 331        {
 0332            if (!File.Exists(pidFilePath))
 333            {
 0334                logger.Warning("PID file not found, daemon may not be running");
 0335                return false;
 336            }
 337
 0338            var pidText = File.ReadAllText(pidFilePath).Trim();
 0339            if (!int.TryParse(pidText, out var pid))
 340            {
 0341                logger.Error("Invalid PID in file: {PidText}", pidText);
 0342                return false;
 343            }
 344
 345            try
 346            {
 0347                var process = Process.GetProcessById(pid);
 0348                logger.Information("Stopping daemon process with PID {PID}", pid);
 349
 350                // Send SIGTERM instead of Kill for graceful shutdown
 0351                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 352                {
 353                    // Windows - send Ctrl+C or use taskkill /PID {pid} /F
 0354                    SendCtrlEvent(process);
 355                }
 356                else
 357                {
 358                    // Unix/macOS - send SIGTERM
 0359                    SendSignal(pid, 15); // SIGTERM is 15
 360                }
 361
 362                // Wait for exit
 0363                var exited = process.WaitForExit(TimeSpan.FromSeconds(10));
 0364                if (exited)
 365                {
 0366                    logger.Information("Daemon process stopped successfully");
 0367                    File.Delete(pidFilePath);
 0368                    return true;
 369                }
 370
 371                // If a graceful shutdown fails, force kill as last resort
 0372                logger.Warning("Daemon process did not exit gracefully, forcing termination");
 0373                process.Kill();
 0374                exited = process.WaitForExit(5000);
 0375                if (exited)
 376                {
 0377                    File.Delete(pidFilePath);
 0378                    return true;
 379                }
 380
 0381                return false;
 382            }
 0383            catch (ArgumentException)
 384            {
 0385                logger.Warning("No process found with PID {PID}, removing stale PID file", pid);
 0386                File.Delete(pidFilePath);
 0387                return false;
 388            }
 389        }
 0390        catch (Exception ex)
 391        {
 0392            logger.Error(ex, "Error stopping daemon");
 0393            return false;
 394        }
 0395    }
 396
 397    private static void SendSignal(int pid, int signal)
 398    {
 0399        Process.Start("kill", $"-{signal} {pid}").WaitForExit();
 0400    }
 401
 402    private static void SendCtrlEvent(Process process)
 403    {
 0404        Process.Start("taskkill", $"/PID {process.Id}").WaitForExit();
 0405    }
 406
 407    #region Native Methods
 408
 409    [LibraryImport("libc")]
 410    private static partial int fork();
 411
 412    [LibraryImport("libc")]
 413    private static partial int setsid();
 414
 415    private static int Fork()
 416    {
 417        // If not on Unix, simulate the fork by returning -1
 0418        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
 0419            !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
 420        {
 0421            return -1;
 422        }
 423
 0424        return fork();
 425    }
 426
 427    private static int Setsid()
 428    {
 429        // If not on Unix, simulate setsid by returning -1
 0430        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
 0431            !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
 432        {
 0433            return -1;
 434        }
 435
 0436        return setsid();
 437    }
 438
 439    #endregion
 440}