Prechádzať zdrojové kódy

Merge pull request #400 from nickbabcock/sc

Replace Topshelf with direct windows service
Nick Babcock 1 rok pred
rodič
commit
07e044dd7c

+ 19 - 0
OhmGraphite.Test/OhmCliTest.cs

@@ -0,0 +1,19 @@
+using System.Threading.Tasks;
+using Xunit;
+
+namespace OhmGraphite.Test
+{
+    public class OhmCliTest
+    {
+        [Fact]
+        public async Task CanExecuteCliVersion() {
+            await OhmCli.Execute(new[] { "--version" });
+        }
+
+        [Fact]
+        public async Task CanExecuteCliStatusWithOldFlags()
+        {
+            await OhmCli.Execute(new[] { "status", "-servicename" });
+        }
+    }
+}

+ 217 - 0
OhmGraphite/OhmCli.cs

@@ -0,0 +1,217 @@
+using Microsoft.Extensions.Hosting.WindowsServices;
+using Microsoft.Extensions.Hosting;
+using NLog;
+using System;
+using System.CommandLine;
+using System.Configuration;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using System.ServiceProcess;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace OhmGraphite
+{
+    public static class OhmCli
+    {
+        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+        public static async Task Execute(string[] args)
+        {
+            var serviceName = "OhmGraphite";
+            var description = "Expose hardware sensor data to Graphite / InfluxDB / Prometheus / Postgres / Timescaledb";
+            var configOption = new Option<FileInfo>(name: "--config", description: "OhmGraphite configuration file");
+            var rootCommand = new RootCommand(description)
+            {
+                // To maintain compabitility with old topshelf installations, we should ignore
+                // command flags like " -displayname"
+                TreatUnmatchedTokensAsErrors = false
+            };
+
+            rootCommand.AddOption(configOption);
+            rootCommand.SetHandler(async (context) => await OhmCommand(context, configOption));
+
+            var runCommand = new Command("run", "Runs the service from the command line (default)");
+            runCommand.AddOption(configOption);
+            runCommand.SetHandler(async (context) => await OhmCommand(context, configOption));
+            rootCommand.AddCommand(runCommand);
+
+            var statusCommand = new Command("status", "Queries the status of the service");
+            statusCommand.SetHandler((context) =>
+            {
+                if (OperatingSystem.IsWindows())
+                {
+                    var service = new ServiceController(serviceName);
+                    Console.WriteLine(service.Status);
+                }
+            });
+            rootCommand.AddCommand(statusCommand);
+
+            var stopCommand = new Command("stop", "Stops the service if it is running");
+            stopCommand.SetHandler((context) =>
+            {
+                if (OperatingSystem.IsWindows())
+                {
+                    var service = new ServiceController(serviceName);
+                    service.Stop();
+                    service.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(10));
+                    if (service.Status != ServiceControllerStatus.Stopped)
+                    {
+                        Console.Error.WriteLine($"Unable to stop {serviceName}");
+                    }
+                    else
+                    {
+                        Console.WriteLine($"{serviceName} stopped");
+                    }
+                }
+            });
+            rootCommand.AddCommand(stopCommand);
+
+            var startCommand = new Command("start", "Starts the service if it is not already running");
+            startCommand.SetHandler((context) =>
+            {
+                if (OperatingSystem.IsWindows())
+                {
+                    var service = new ServiceController(serviceName);
+                    service.Start();
+                    service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10));
+                    if (service.Status != ServiceControllerStatus.Running)
+                    {
+                        Console.Error.WriteLine($"Unable to start {serviceName}");
+                    }
+                    else
+                    {
+                        Console.WriteLine($"{serviceName} started");
+                    }
+                }
+            });
+            rootCommand.AddCommand(startCommand);
+
+            var installCommand = new Command("install", "Installs the service");
+            installCommand.AddOption(configOption);
+            installCommand.SetHandler((context) =>
+            {
+                {
+                    var procInfo = new ProcessStartInfo();
+                    procInfo.ArgumentList.Add("create");
+                    procInfo.ArgumentList.Add(serviceName);
+
+                    var configFile = context.ParseResult.GetValueForOption(configOption);
+                    var configService = configFile == null ? "" : $@" --config ""{configFile.FullName}""";
+                    procInfo.ArgumentList.Add($@"binpath=""{Environment.ProcessPath}"" {configService}");
+                    procInfo.ArgumentList.Add(@"DisplayName=Ohm Graphite");
+                    procInfo.ArgumentList.Add("start=auto");
+                    ScCommand(procInfo);
+                }
+
+                {
+                    var procInfo = new ProcessStartInfo();
+                    procInfo.ArgumentList.Add("description");
+                    procInfo.ArgumentList.Add(serviceName);
+                    procInfo.ArgumentList.Add(description);
+
+                    ScCommand(procInfo);
+                }
+            });
+            rootCommand.AddCommand(installCommand);
+
+            var uninstallCommand = new Command("uninstall", "Uninstalls the service");
+            uninstallCommand.SetHandler((context) =>
+            {
+                var procInfo = new ProcessStartInfo
+                {
+                    Arguments = $"delete {serviceName}",
+                };
+
+                ScCommand(procInfo);
+            });
+            rootCommand.AddCommand(uninstallCommand);
+
+            await rootCommand.InvokeAsync(args);
+        }
+
+        static async Task OhmCommand(System.CommandLine.Invocation.InvocationContext context, Option<FileInfo> configOption)
+        {
+            var configFile = context.ParseResult.GetValueForOption(configOption);
+            var configPath = configFile == null ? string.Empty : configFile.FullName;
+            var configDisplay = configFile == null ? "default" : configFile.Name;
+            var config = Logger.LogFunction($"parse config {configDisplay}", () => MetricConfig.ParseAppSettings(CreateConfiguration(configPath)));
+
+            if (WindowsServiceHelpers.IsWindowsService())
+            {
+                var builder = Host.CreateDefaultBuilder()
+                    .UseWindowsService(options =>
+                    {
+                        options.ServiceName = "OhmGraphite";
+                    })
+                    .ConfigureServices((hostContext, services) =>
+                    {
+                        services.AddSingleton(config);
+                        services.AddHostedService<Worker>();
+                    });
+
+                builder.Build().Run();
+            }
+            else
+            {
+                var token = context.GetCancellationToken();
+                var worker = new Worker(config);
+                await worker.StartAsync(token);
+                await worker.ExecuteTask;
+            }
+        }
+
+        private static int ScCommand(ProcessStartInfo procInfo)
+        {
+            procInfo.FileName = "sc.exe";
+            procInfo.RedirectStandardOutput = true;
+            procInfo.RedirectStandardError = true;
+            procInfo.CreateNoWindow = true;
+            procInfo.UseShellExecute = false;
+
+            using var process = new Process() { StartInfo = procInfo };
+            process.Start();
+            process.WaitForExit();
+            var error = process.StandardError.ReadToEnd();
+            var stdout = process.StandardOutput.ReadToEnd();
+
+            if (!string.IsNullOrEmpty(error))
+            {
+                Console.WriteLine(error);
+            }
+
+            if (!string.IsNullOrEmpty(stdout))
+            {
+                Console.WriteLine(stdout);
+            }
+
+            return process.ExitCode;
+        }
+
+        private static IAppConfig CreateConfiguration(string configPath)
+        {
+            if (string.IsNullOrEmpty(configPath))
+            {
+                // https://github.com/dotnet/runtime/issues/13051#issuecomment-510267727
+                var processModule = Process.GetCurrentProcess().MainModule;
+                if (processModule != null)
+                {
+                    var pt = processModule.FileName;
+                    var fn = Path.Join(Path.GetDirectoryName(pt), "OhmGraphite.exe.config");
+                    var configMap1 = new ExeConfigurationFileMap { ExeConfigFilename = fn };
+                    var config1 = ConfigurationManager.OpenMappedExeConfiguration(configMap1, ConfigurationUserLevel.None);
+                    return new CustomConfig(config1);
+                }
+            }
+
+            if (!File.Exists(configPath))
+            {
+                throw new ApplicationException($"unable to detect config: ${configPath}");
+            }
+
+            var configMap = new ExeConfigurationFileMap { ExeConfigFilename = configPath };
+            var config = ConfigurationManager.OpenMappedExeConfiguration(configMap, ConfigurationUserLevel.None);
+            return new CustomConfig(config);
+        }
+    }
+}

+ 5 - 3
OhmGraphite/OhmGraphite.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
@@ -37,11 +37,13 @@
   <ItemGroup>
     <PackageReference Include="LibreHardwareMonitorLib" Version="0.9.3-pre243" />
     <PackageReference Include="InfluxDB.Client" Version="4.13.0" />
+    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.1" />
+    <PackageReference Include="NLog" Version="5.2.4" />
     <PackageReference Include="Npgsql" Version="7.0.4" />
     <PackageReference Include="prometheus-net" Version="6.0.0" />
+    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
     <PackageReference Include="System.Configuration.ConfigurationManager" Version="7.0.0" />
-    <PackageReference Include="TopShelf" Version="4.3.0" />
-    <PackageReference Include="Topshelf.NLog" Version="4.3.0" />
     <PackageReference Include="InfluxDB.LineProtocol" Version="1.1.1" />
   </ItemGroup>
 

+ 3 - 127
OhmGraphite/Program.cs

@@ -1,136 +1,12 @@
-using System;
-using System.Configuration;
-using System.Diagnostics;
-using System.IO;
-using System.Reflection;
-using NLog;
-using LibreHardwareMonitor.Hardware;
-using Prometheus;
-using Topshelf;
+using System.Threading.Tasks;
 
 namespace OhmGraphite
 {
     internal class Program
     {
-        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
-
-        private static void Main()
-        {
-            string configPath = string.Empty;
-            bool showVersion = false;
-            HostFactory.Run(x =>
-            {
-                x.Service<IManage>(s =>
-                {
-                    s.ConstructUsing(name =>
-                    {
-                        if (showVersion)
-                        {
-                            var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
-                            Console.WriteLine(version ?? "no version detected");
-                            Environment.Exit(0);
-                        }
-
-                        var configDisplay = string.IsNullOrEmpty(configPath) ? "default" : configPath;
-                        var config = Logger.LogFunction($"parse config {configDisplay}", () => MetricConfig.ParseAppSettings(CreateConfiguration(configPath)));
-
-                        var computer = new Computer
-                        {
-                            IsGpuEnabled = config.EnabledHardware.Gpu,
-                            IsMotherboardEnabled = config.EnabledHardware.Motherboard,
-                            IsCpuEnabled = config.EnabledHardware.Cpu,
-                            IsMemoryEnabled = config.EnabledHardware.Ram,
-                            IsNetworkEnabled = config.EnabledHardware.Network,
-                            IsStorageEnabled = config.EnabledHardware.Storage,
-                            IsControllerEnabled = config.EnabledHardware.Controller,
-                            IsPsuEnabled = config.EnabledHardware.Psu,
-                            IsBatteryEnabled = config.EnabledHardware.Battery,
-                        };
-
-                        var collector = new SensorCollector(computer, config);
-                        return CreateManager(config, collector);
-                    });
-                    s.WhenStarted(tc => tc.Start());
-                    s.WhenStopped(tc => tc.Dispose());
-                });
-
-                // Allow one to specify a command line argument when running interactively
-                x.AddCommandLineDefinition("config", v => configPath = v);
-                x.AddCommandLineSwitch("version", v => showVersion = v);
-                x.UseNLog();
-                x.RunAsLocalSystem();
-                x.SetDescription(
-                    "Extract hardware sensor data and exports it to a given host and port in a graphite compatible format");
-                x.SetDisplayName("Ohm Graphite");
-                x.SetServiceName("OhmGraphite");
-                x.OnException(ex => Logger.Error(ex, "OhmGraphite TopShelf encountered an error"));
-            });
-        }
-
-        private static IAppConfig CreateConfiguration(string configPath)
-        {
-            if (string.IsNullOrEmpty(configPath))
-            {
-                // https://github.com/dotnet/runtime/issues/13051#issuecomment-510267727
-                var processModule = Process.GetCurrentProcess().MainModule;
-                if (processModule != null)
-                {
-                    var pt = processModule.FileName;
-                    var fn = Path.Join(Path.GetDirectoryName(pt), "OhmGraphite.exe.config");
-                    var configMap1 = new ExeConfigurationFileMap { ExeConfigFilename = fn };
-                    var config1 = ConfigurationManager.OpenMappedExeConfiguration(configMap1, ConfigurationUserLevel.None);
-                    return new CustomConfig(config1);
-                }
-            }
-
-            if (!File.Exists(configPath))
-            {
-                throw new ApplicationException($"unable to detect config: ${configPath}");
-            }
-
-            var configMap = new ExeConfigurationFileMap { ExeConfigFilename = configPath };
-            var config = ConfigurationManager.OpenMappedExeConfiguration(configMap, ConfigurationUserLevel.None);
-            return new CustomConfig(config);
-        }
-
-        private static IManage CreateManager(MetricConfig config, SensorCollector collector)
+        private static async Task Main(string[] args)
         {
-            var hostname = config.LookupName();
-            double seconds = config.Interval.TotalSeconds;
-            if (config.Graphite != null)
-            {
-                Logger.Info(
-                    $"Graphite host: {config.Graphite.Host} port: {config.Graphite.Port} interval: {seconds} tags: {config.Graphite.Tags}");
-                var writer = new GraphiteWriter(config.Graphite.Host,
-                    config.Graphite.Port,
-                    hostname,
-                    config.Graphite.Tags);
-                return new MetricTimer(config.Interval, collector, writer);
-            }
-            else if (config.Prometheus != null)
-            {
-                Logger.Info($"Prometheus port: {config.Prometheus.Port}");
-                var registry = PrometheusCollection.SetupDefault(collector);
-                var server = new MetricServer(config.Prometheus.Host, config.Prometheus.Port, url: config.Prometheus.Path, registry: registry, useHttps: config.Prometheus.UseHttps);
-                return new PrometheusServer(server, collector);
-            }
-            else if (config.Timescale != null)
-            {
-                var writer = new TimescaleWriter(config.Timescale.Connection, config.Timescale.SetupTable, hostname);
-                return new MetricTimer(config.Interval, collector, writer);
-            }
-            else if (config.Influx != null)
-            {
-                Logger.Info($"Influxdb address: {config.Influx.Address} db: {config.Influx.Db}");
-                var writer = new InfluxWriter(config.Influx, hostname);
-                return new MetricTimer(config.Interval, collector, writer);
-            }
-            else
-            {
-                Logger.Info($"Influx2 address: {config.Influx2.Options.Url}");
-                var writer = new Influx2Writer(config.Influx2, hostname);
-                return new MetricTimer(config.Interval, collector, writer);
-            }
+            await OhmCli.Execute(args);
         }
     }
 }

+ 89 - 0
OhmGraphite/Worker.cs

@@ -0,0 +1,89 @@
+using NLog;
+using LibreHardwareMonitor.Hardware;
+using Prometheus;
+using System.Threading.Tasks;
+using System.Threading;
+using Microsoft.Extensions.Hosting;
+
+namespace OhmGraphite
+{
+    class Worker : BackgroundService
+    {
+        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+        private readonly MetricConfig config;
+        public Worker(MetricConfig config)
+        {
+            this.config = config;
+        }
+
+        protected override Task ExecuteAsync(CancellationToken stoppingToken)
+        {
+            return Task.Run(() =>
+            {
+                using var app = CreateOhmGraphite(config);
+                app.Start();
+                stoppingToken.WaitHandle.WaitOne();
+            }, stoppingToken);
+        }
+
+        private static IManage CreateOhmGraphite(MetricConfig config)
+        {
+            var computer = new Computer
+            {
+                IsGpuEnabled = config.EnabledHardware.Gpu,
+                IsMotherboardEnabled = config.EnabledHardware.Motherboard,
+                IsCpuEnabled = config.EnabledHardware.Cpu,
+                IsMemoryEnabled = config.EnabledHardware.Ram,
+                IsNetworkEnabled = config.EnabledHardware.Network,
+                IsStorageEnabled = config.EnabledHardware.Storage,
+                IsControllerEnabled = config.EnabledHardware.Controller,
+                IsPsuEnabled = config.EnabledHardware.Psu,
+                IsBatteryEnabled = config.EnabledHardware.Battery,
+            };
+
+            var collector = new SensorCollector(computer, config);
+            return CreateManager(config, collector);
+        }
+
+        private static IManage CreateManager(MetricConfig config, SensorCollector collector)
+        {
+            var hostname = config.LookupName();
+            double seconds = config.Interval.TotalSeconds;
+            if (config.Graphite != null)
+            {
+                Logger.Info(
+                    $"Graphite host: {config.Graphite.Host} port: {config.Graphite.Port} interval: {seconds} tags: {config.Graphite.Tags}");
+                var writer = new GraphiteWriter(config.Graphite.Host,
+                    config.Graphite.Port,
+                    hostname,
+                    config.Graphite.Tags);
+                return new MetricTimer(config.Interval, collector, writer);
+            }
+            else if (config.Prometheus != null)
+            {
+                Logger.Info($"Prometheus port: {config.Prometheus.Port}");
+                var registry = PrometheusCollection.SetupDefault(collector);
+                var server = new MetricServer(config.Prometheus.Host, config.Prometheus.Port, url: config.Prometheus.Path, registry: registry, useHttps: config.Prometheus.UseHttps);
+                return new PrometheusServer(server, collector);
+            }
+            else if (config.Timescale != null)
+            {
+                var writer = new TimescaleWriter(config.Timescale.Connection, config.Timescale.SetupTable, hostname);
+                return new MetricTimer(config.Interval, collector, writer);
+            }
+            else if (config.Influx != null)
+            {
+                Logger.Info($"Influxdb address: {config.Influx.Address} db: {config.Influx.Db}");
+                var writer = new InfluxWriter(config.Influx, hostname);
+                return new MetricTimer(config.Interval, collector, writer);
+            }
+            else
+            {
+                Logger.Info($"Influx2 address: {config.Influx2.Options.Url}");
+                var writer = new Influx2Writer(config.Influx2, hostname);
+                return new MetricTimer(config.Interval, collector, writer);
+            }
+        }
+    }
+}