Windows下的通用进程守护程序(持续更新中),高仿supervisor

Window下的通用守护进程

Windows下的通用进程守护程序(持续更新中),高仿supervisor

是的,有这个需求。

曾经,我也觉得没必要,然而,现实很残酷。

比如开发了.net core的web程序,明明可以很好的部署在IIS下,但领导偏不,他就想启动为不可见的Console程序。如果是一个console程序,那么还可以借助exe转service的技术,把它转换成windows service服务,好像也没啥大不了,怕就怕竟然又多个。.

多个console启动在服务端虽然有些不好看,忍忍是不是就没事了?不,如果程序写的不好崩溃了咋办?这…容我思考下,感觉好像需要写个守护程序…

哎,部署在Linux下不行吗?使用supervisor不行吗?好像也可以,好像又不行。

好吧,那就准备操练一把,写个通用的守护进程程序吧。

守护配置

目标,守护多个进程。
士兵,windows 服务。
军粮:守护配置。

用户可以灵活配置守护,如同Supervisor配置一样。配置内容如下:

[program:WebTest]
# 项目根目录
directory=C:\Users\source\repos\Framework\WebTest\WebTest\bin\Debug\net6.0
# 启动执行命令
command=C:\Users\source\repos\Framework\WebTest\WebTest\bin\Debug\net6.0\WebTest.exe
stderr_logfile=d:\test_stderr.log
stdout_logfile=d:\test_stdout.log
arguments=
env=

有了这个配置,你就可以轻松配置被守护的程序,执行目录,参数以及环境变量了。当然也可以重定向输出日志和错误日志。

下载

当前版本实现了基本的守护功能,程序自动启动为 guard service服务,自我测试,运行良好。
下载文件在 此

使用了dotnet的一体化打包模式,不需要安装.net6运行环境。因此包稍微大了点。

dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true /p:PublishTrimmed=true

具体实现逻辑

首先选用了TopShelf类库包,这样就减轻了开发windows service的复杂度和安装命令解析的复杂度。
因此,exe执行文件可以带一系列参数进行服务的安装和卸载工作。

 service install
        Installs the service into the service control manager

    service install -username:joe -password:bob --autostart
        Installs the service using the specified username/password and
        configures the service to start automatically at machine startup

    service uninstall
        Uninstalls the service

    service install -instance:001
        Installs the service, appending the instance name to the service name
        so that the service can be installed multiple times. You may need to
        tweak the log4net.config to make this play nicely with the log files.

我写了个直接运行的批处理文件,解包后可以看到是RunAsService.bat,内容如下:

call WbtGuardService.exe install --autostart 
echo start guard service...
call WbtGuardService.exe start

预留了开关web服务的配置(包含端口号),可以修改appsettings.json文件的配置

{
  "Kestrel": {
    "Endpoints": {
      "Http": {"Url": "http://*:8088"}     
    }
  },
  "EnableWeb": "true", 
  "UserName": "admin",
  "Password": "admin",
  "CheckInterval": 20000,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

内部核心的代码,无非是包装了process的调用方法。代码如下:

private Process StartProcess()
        {
            _logger.Info($"开始程序 {this.config.Name}...");
            var bDir = !string.IsNullOrEmpty(this.config.Directory);
            Process p = Process.GetProcessesByName(config.Name)?.FirstOrDefault();
            if (p == null)
            {
                var startInfo =  new ProcessStartInfo
                {
                    FileName = this.config.Command,
                    UseShellExecute = false,
                    RedirectStandardOutput = stdoutStream != null,
                    RedirectStandardError = stderrorStream != null,
                    WorkingDirectory = bDir ? this.config.Directory : AppDomain.CurrentDomain.BaseDirectory,
                    Arguments = this.config.Arguments,
                    CreateNoWindow = true,                   
                    //StandardOutputEncoding = Encoding.UTF8,
                    //StandardErrorEncoding = Encoding.UTF8,
                };
                foreach (var (key, value) in this.config.GetEnvironmentVariables())
                {
                    if (value is not null)
                    {
                        startInfo.Environment[key] = value;
                    }
                    else
                    {
                        // Null value means we should remove the variable
                        // https://github.com/Tyrrrz/CliWrap/issues/109
                        // https://github.com/dotnet/runtime/issues/34446
                        startInfo.Environment.Remove(key);
                    }
                }
                Console.WriteLine($"start {config.Name}  ....");
                p = new Process
                {
                    StartInfo =startInfo,
                };
                
                p.ErrorDataReceived += P_ErrorDataReceived;
                p.OutputDataReceived += P_OutputDataReceived;

                try
                {
                    if (!p.Start())
                    {
                       
                    }

                    if (stdoutStream != null) p.BeginOutputReadLine();
                    if (stderrorStream != null) p.BeginErrorReadLine();
                    _logger.Info($"程序 {this.config.Name} 启动成功.");
                }
                catch (Win32Exception ex)
                {
                    
                }
            }

            return p;
        }

欢迎评论

如果你想用,有场景用,欢迎评论,如果评论超过十个,那么我就放在github上开源。

看看这个场景是真需求,还是假需求。

反正我不信真的有人用…

好吧,又产生了一个轮子。