CI / Rust checks (push) Canceled after 0s
CI / Docs and registry checks (push) Canceled after 0s
CI / Smoke checks (push) Canceled after 0s
Coverage / Coverage baseline (push) Canceled after 0s
Security / Cargo audit (push) Canceled after 0s
Security / Cargo deny (push) Canceled after 0s
Security / Secret pattern check (push) Canceled after 0s
Security / Dependency review (push) Canceled after 0s
238 lines
7.7 KiB
C#
238 lines
7.7 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.ServiceProcess;
|
|
using System.Threading;
|
|
|
|
namespace AWatchRus
|
|
{
|
|
public sealed class CollectorGuardService : ServiceBase
|
|
{
|
|
private Process child;
|
|
private readonly ServiceOptions options;
|
|
private readonly object sync = new object();
|
|
private bool stopping;
|
|
private string childFileName;
|
|
private string childArguments;
|
|
private DateTime restartWindowStartedUtc = DateTime.UtcNow;
|
|
private int restartCountInWindow;
|
|
|
|
public CollectorGuardService(ServiceOptions options)
|
|
{
|
|
this.options = options;
|
|
ServiceName = options.ServiceName;
|
|
CanStop = true;
|
|
CanShutdown = true;
|
|
}
|
|
|
|
protected override void OnStart(string[] args)
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(options.LogPath));
|
|
Log("service starting");
|
|
|
|
childFileName = string.IsNullOrWhiteSpace(options.ExecPath) ? options.PowerShellPath : options.ExecPath;
|
|
childArguments = string.IsNullOrWhiteSpace(options.ExecPath)
|
|
? string.Format(
|
|
"-NoProfile -ExecutionPolicy Bypass -File \"{0}\" -ConfigPath \"{1}\" -Mode {2} -LoopSeconds {3}",
|
|
options.ScriptPath,
|
|
options.ConfigPath,
|
|
options.Mode,
|
|
options.LoopSeconds)
|
|
: (options.ExecArgs ?? string.Empty);
|
|
|
|
StartChild("initial start");
|
|
}
|
|
|
|
private void StartChild(string reason)
|
|
{
|
|
lock (sync)
|
|
{
|
|
if (stopping)
|
|
{
|
|
return;
|
|
}
|
|
if (child != null && !child.HasExited)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = childFileName,
|
|
Arguments = childArguments,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = false,
|
|
RedirectStandardError = false,
|
|
};
|
|
var process = new Process
|
|
{
|
|
StartInfo = psi,
|
|
EnableRaisingEvents = true,
|
|
};
|
|
process.Exited += ChildExited;
|
|
process.Start();
|
|
lock (sync)
|
|
{
|
|
child = process;
|
|
}
|
|
Log("child pid=" + process.Id + " exec=" + childFileName + " reason=" + reason);
|
|
}
|
|
|
|
private void ChildExited(object sender, EventArgs args)
|
|
{
|
|
var process = sender as Process;
|
|
var exitCode = "unknown";
|
|
try
|
|
{
|
|
if (process != null)
|
|
{
|
|
exitCode = process.ExitCode.ToString();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
lock (sync)
|
|
{
|
|
if (stopping)
|
|
{
|
|
Log("child exited during stop exitCode=" + exitCode);
|
|
return;
|
|
}
|
|
}
|
|
|
|
Log("child exited unexpectedly exitCode=" + exitCode);
|
|
if (!RegisterChildRestart())
|
|
{
|
|
Log("child restart budget exhausted; exiting service for SCM recovery");
|
|
Environment.Exit(1);
|
|
return;
|
|
}
|
|
|
|
var restartThread = new Thread(new ThreadStart(delegate
|
|
{
|
|
Thread.Sleep(Math.Max(1, options.ChildRestartDelaySeconds) * 1000);
|
|
StartChild("child-exit restart");
|
|
}));
|
|
restartThread.IsBackground = true;
|
|
restartThread.Start();
|
|
}
|
|
|
|
private bool RegisterChildRestart()
|
|
{
|
|
lock (sync)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
if ((now - restartWindowStartedUtc).TotalSeconds > options.ChildRestartWindowSeconds)
|
|
{
|
|
restartWindowStartedUtc = now;
|
|
restartCountInWindow = 0;
|
|
}
|
|
restartCountInWindow++;
|
|
Log("child restart budget count=" + restartCountInWindow + " windowSeconds=" + options.ChildRestartWindowSeconds);
|
|
return restartCountInWindow <= options.MaxChildRestartsInWindow;
|
|
}
|
|
}
|
|
|
|
protected override void OnStop()
|
|
{
|
|
StopChild("service stopping");
|
|
}
|
|
|
|
protected override void OnShutdown()
|
|
{
|
|
StopChild("system shutdown");
|
|
}
|
|
|
|
private void StopChild(string reason)
|
|
{
|
|
try
|
|
{
|
|
Log(reason);
|
|
Process process;
|
|
lock (sync)
|
|
{
|
|
stopping = true;
|
|
process = child;
|
|
}
|
|
if (process != null && !process.HasExited)
|
|
{
|
|
process.Kill();
|
|
process.WaitForExit(10000);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try
|
|
{
|
|
Log("stop error: " + ex.Message);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Log(string message)
|
|
{
|
|
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " " + message + Environment.NewLine);
|
|
}
|
|
}
|
|
|
|
public sealed class ServiceOptions
|
|
{
|
|
public string ServiceName = "AWatchRusCollectorGuard";
|
|
public string PowerShellPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32\\WindowsPowerShell\\v1.0\\powershell.exe");
|
|
public string ScriptPath = @"C:\Program Files\AWatch-rus\windows\aw-collector-guard.ps1";
|
|
public string ConfigPath = @"C:\ProgramData\AWatch-rus\deployment-config.json";
|
|
public string Mode = "shadow";
|
|
public int LoopSeconds = 60;
|
|
public string LogPath = @"C:\ProgramData\AWatch-rus\logs\collector-guard-service.log";
|
|
public string ExecPath = null;
|
|
public string ExecArgs = null;
|
|
public int ChildRestartDelaySeconds = 5;
|
|
public int MaxChildRestartsInWindow = 5;
|
|
public int ChildRestartWindowSeconds = 600;
|
|
}
|
|
|
|
internal static class Program
|
|
{
|
|
private static void Main(string[] args)
|
|
{
|
|
var options = Parse(args);
|
|
ServiceBase.Run(new CollectorGuardService(options));
|
|
}
|
|
|
|
private static ServiceOptions Parse(string[] args)
|
|
{
|
|
var options = new ServiceOptions();
|
|
for (var i = 0; i < args.Length; i++)
|
|
{
|
|
var key = args[i].ToLowerInvariant();
|
|
var value = i + 1 < args.Length ? args[i + 1] : null;
|
|
if (value == null || value.StartsWith("--", StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
if (key == "--service-name") options.ServiceName = value;
|
|
else if (key == "--script") options.ScriptPath = value;
|
|
else if (key == "--config") options.ConfigPath = value;
|
|
else if (key == "--mode") options.Mode = value;
|
|
else if (key == "--loop")
|
|
{
|
|
int parsed;
|
|
if (int.TryParse(value, out parsed)) options.LoopSeconds = parsed;
|
|
}
|
|
else if (key == "--log") options.LogPath = value;
|
|
else if (key == "--exec") options.ExecPath = value;
|
|
else if (key == "--args") options.ExecArgs = value;
|
|
i++;
|
|
}
|
|
return options;
|
|
}
|
|
}
|
|
}
|