Merge branch 'master' into v3.1
大石头 authored at 2024-07-13 19:03:18
13.68 KiB
Stardust
using System.Diagnostics;
using NewLife;
using NewLife.Log;
using Stardust.Models;
using static System.Runtime.InteropServices.JavaScript.JSType;

#if !NET40
using TaskEx = System.Threading.Tasks.Task;
#endif

namespace Stardust.Managers;

/// <summary>运行处理器</summary>
public class ExeHandler : IServiceHandler, ITracerFeature, ILogFeature
{
    #region 属性
    /// <summary>服务控制器</summary>
    public IServiceController Controller { get; set; } = null!;
    #endregion

    /// <summary>启动服务</summary>
    /// <returns></returns>
    public Boolean Start(ServiceInfo service)
    {
        // 修正路径
        var workDir = service.WorkingDirectory;
        var file = service.FileName?.Trim();
        if (file.IsNullOrEmpty())
        {
            WriteLog("应用[{0}]文件名为空", Name);
            return false;
        }

        if (file.Contains('/') || file.Contains('\\'))
        {
            file = file.GetFullPath();
        }
        if (workDir.IsNullOrEmpty()) workDir = Path.GetDirectoryName(file);
        if (workDir.IsNullOrEmpty())
        {
            WriteLog("应用[{0}]工作目录为空", Name);
            return false;
        }

        workDir = workDir.GetFullPath();
        _fileName = null;
        _workdir = workDir;

        var args = service.Arguments?.Trim();
        WriteLog("启动应用:{0} {1} workDir={2} Mode={3} Times={4}", file, args, workDir, service.Mode, _error);
        if (service.MaxMemory > 0) WriteLog("内存限制:{0:n0}M", service.MaxMemory);

        var src = service.ZipFile ?? file;
        using var span = Tracer?.NewSpan("StartService", service);
        try
        {
            Process? p;
            var isZip = src.EndsWithIgnoreCase(".zip");

            // 在环境变量中设置BasePath,不用担心影响当前进程,因为PathHelper仅读取一次
            //Environment.SetEnvironmentVariable("BasePath", workDir);

            // 工作模式
            switch (service.Mode)
            {
                case ServiceModes.Default:
                case ServiceModes.Multiple:
                    break;
                case ServiceModes.Extract:
                    WriteLog("解压后不运行,外部主机(如IIS)将托管应用");
                    Extract(src, args, workDir, false);
                    Running = true;
                    return true;
                case ServiceModes.ExtractAndRun:
                    WriteLog("解压后在工作目录运行");
                    var deploy = Extract(src, args, workDir, false);
                    if (deploy == null) throw new Exception("解压缩失败");

                    //file ??= deploy.ExecuteFile;
                    var runfile = deploy.FindExeFile(workDir);
                    file = runfile?.FullName;
                    if (file.IsNullOrEmpty()) throw new Exception("无法找到启动文件");

                    args = deploy.Arguments;
                    //_fileName = deploy.ExecuteFile;
                    isZip = false;
                    break;
                case ServiceModes.RunOnce:
                    //service.Enable = false;
                    break;
                default:
                    break;
            }

            if (isZip)
                p = RunZip(file, args, workDir, service);
            else
                p = RunExe(file, args, workDir, service);

            if (p == null) return false;

            WriteLog("启动成功 PID={0}/{1}", p.Id, p.ProcessName);

            if (service.Mode == ServiceModes.RunOnce)
            {
                WriteLog("单次运行完成,禁用该应用服务");
                service.Enable = false;
                Running = false;

                return true;
            }

            // 记录进程信息,避免宿主重启后无法继续管理
            SetProcess(p);
            Running = true;

            StartTime = DateTime.Now;

            // 定时检查文件是否有改变
            StartMonitor();

            // 此时还不能清零,因为进程可能不稳定,待定时器检测可靠后清零
            //_error = 0;

            return true;
        }
        catch (Exception ex)
        {
            span?.SetError(ex, null);
            Log?.Write(LogLevel.Error, "{0}", ex);
            WriteEvent("error", ex.ToString());
        }

        return false;
    }

    /// <summary>停止服务</summary>
    /// <param name="reason"></param>
    public void Stop(String reason)
    {
        using var span = Tracer?.NewSpan("StopService", $"{Info?.Name} reason={reason}");
        _timer.TryDispose();
        _timer = null;

        try
        {
            if (!p.GetHasExited() && p.CloseMainWindow())
            {
                WriteLog("已发送关闭窗口消息,等待目标进程退出");

                for (var i = 0; i < 50 && !p.GetHasExited(); i++)
                {
                    Thread.Sleep(200);
                }
            }
        }
        catch { }

        // 优雅关闭进程
        if (!p.GetHasExited())
        {
            WriteLog("优雅退出进程:PID={0}/{1},最大等待{2}毫秒", p.Id, p.ProcessName, 50 * 200);
            p.SafetyKill(50, 200);
        }

        try
        {
            if (!p.GetHasExited())
            {
                WriteLog("强行结束进程 PID={0}/{1}", p.Id, p.ProcessName);
                p.ForceKill();
            }

            if (p.GetHasExited()) WriteLog("进程[PID={0}]已退出!ExitCode={1}", p.Id, p.ExitCode);
        }
        catch (Exception ex)
        {
            WriteLog("进程[PID={0}]退出失败!{1}", p.Id, ex.Message);
            span?.SetError(ex, null);
        }
    }

    /// <summary>检查服务是否正常</summary>
    /// <returns></returns>
    public Boolean Check()
    {
        using var span = Tracer?.NewSpan("CheckService", inf);

        // 获取当前进程Id
        var mypid = Process.GetCurrentProcess().Id;

        // 判断进程存在
        var p = Process;
        if (p != null && p.GetHasExited())
        {
            WriteLog("应用[{0}/{1}]已退出!", p.Id, Name);

            p = null;
            Process = null;
            Running = false;
        }

        // 进程存在,常规判断内存
        if (p != null) p = CheckMaxMemory(p, inf);

        // 进程不存在,但Id存在
        if (p == null && ProcessId > 0 && ProcessId != mypid)
        {
            span?.AppendTag($"GetProcessById({ProcessId})");
            try
            {
                p = Process.GetProcessById(ProcessId);

                var exited = p == null || p.GetHasExited();

                // 这里的进程名可能是 dotnet/java,照样可以使用
                if (p != null && !exited && p.ProcessName == ProcessName) return TakeOver(p, $"按[Id={ProcessId}]查找");
            }
            catch (Exception ex)
            {
                span?.SetError(ex, null);

                if (ex is not ArgumentException)
                {
                    Log?.Error("{0}", ex);
                    WriteEvent("error", ex.ToString());
                }
            }

            p = null;
            ProcessId = 0;
        }

        // 进程不存在,但名称存在
        if (p == null && !ProcessName.IsNullOrEmpty() && inf.Mode != ServiceModes.Multiple)
        {
            if (ProcessName.EqualIgnoreCase("dotnet", "java"))
            {
                var target = _fileName ?? inf.FileName;
                if (target.EqualIgnoreCase("dotnet", "java"))
                {
                    var ss = inf.Arguments?.Split(' ');
                    if (ss != null) target = ss.FirstOrDefault(e => e.EndsWithIgnoreCase(".dll", ".jar"));
                }
                if (!target.IsNullOrEmpty())
                {
                    //target = Path.GetFileName(target);
                    span?.AppendTag($"GetProcessesByFile({target}) ProcessName={ProcessName}");

                    // 遍历所有进程,从命令行参数中找到启动文件名一致的进程
                    foreach (var item in Process.GetProcesses())
                    {
                        if (item.Id == mypid || item.GetHasExited()) continue;
                        if (!item.ProcessName.EqualIgnoreCase(ProcessName)) continue;

                        var name = StarHelper.GetProcessName(item);
                        if (!name.IsNullOrEmpty())
                        {
                            span?.AppendTag($"id={item.Id} name={name}");

                            // target有可能是文件全路径,此时需要比对无后缀文件名
                            if (name.EqualIgnoreCase(target, Path.GetFileNameWithoutExtension(target)))
                                return TakeOver(item, $"按[{ProcessName} {target}]查找");
                        }
                    }
                }
            }
            else
            {
                span?.AppendTag($"GetProcessesByName({ProcessName})");

                var ps = Process.GetProcessesByName(ProcessName).Where(e => e.Id != mypid && !e.GetHasExited()).ToArray();
                if (ps.Length > 0) return TakeOver(ps[0], $"按[Name={ProcessName}]查找");
            }
        }

        // 准备启动进程
        var rs = Start();

        // 检测并上报性能
        p = Process;
        if (p != null && EventProvider is StarClient client)
        {
            if (_appInfo == null || _appInfo.Id != p.Id)
                _appInfo = new AppInfo(p) { AppName = inf.Name };
            else
                _appInfo.Refresh();

            TaskEx.Run(() => client.AppPing(_appInfo));
        }

        return rs;
    }

    private Process? RunExe(String file, String? args, String workDir, ServiceInfo service)
    {
        //WriteLog("拉起进程:{0} {1}", file, args);

        var fileName = workDir.CombinePath(file).GetFullPath();
        if (!fileName.AsFile().Exists)
        {
            fileName = file;
        }

        if (file.EndsWithIgnoreCase(".dll"))
        {
            args = $"{fileName} {args}";
            fileName = "dotnet";
        }
        else if (file.EndsWithIgnoreCase(".jar"))
        {
            args = $"{fileName} {args}";
            fileName = "java";
        }

        var si = new ProcessStartInfo
        {
            FileName = fileName,
            Arguments = args ?? "",
            WorkingDirectory = workDir,

            // false时目前控制台合并到当前控制台,一起退出;
            // true时目标控制台独立窗口,不会一起退出;
            UseShellExecute = false,
        };
        si.EnvironmentVariables["BasePath"] = workDir;

        // 环境变量。不能用于ShellExecute
        if (service.Environments.IsNullOrEmpty() && !si.UseShellExecute)
        {
            foreach (var item in service.Environments.SplitAsDictionary("=", ";"))
            {
                if (!item.Key.IsNullOrEmpty())
                    si.EnvironmentVariables[item.Key] = item.Value;
            }
        }

        if (Runtime.Linux)
        {
            // Linux下,需要给予可执行权限
            Process.Start("chmod", $"+x {fileName}").WaitForExit(5_000);
        }

        // 指定用户时,以特定用户启动进程
        var user = service.UserName;
        if (!user.IsNullOrEmpty())
        {
            // 在前台桌面中打开应用
            if (user == "$")
            {
                if (!Runtime.Windows)
                    user = null;
                else
                {
                    var desktop = new Desktop();
                }
            }

            si.UserName = user;
            //si.UseShellExecute = false;

            // 在Linux系统中,改变目录所属用户
            if (Runtime.Linux)
            {
                if (!user.Contains(':')) user = $"{user}:{user}";
                //Process.Start("chown", $"-R {user} {si.WorkingDirectory}");
                Process.Start("chown", $"-R {user} {si.WorkingDirectory.CombinePath("../").GetBasePath()}").WaitForExit(5_000);
            }
        }

        // 如果出现超过一次的重启,则打开调试模式,截取控制台输出到日志
        if (_error > 1)
        {
            // UseShellExecute 必须 false,以便于后续重定向输出流
            si.UseShellExecute = false;
            si.RedirectStandardError = true;
            si.RedirectStandardOutput = true;
        }

        //// 在进程的环境变量中设置BasePath
        //if (!si.UseShellExecute)
        //    si.EnvironmentVariables["BasePath"] = si.WorkingDirectory;

        WriteLog("工作目录: {0}", si.WorkingDirectory);
        WriteLog("启动文件: {0}", si.FileName);
        WriteLog("启动参数: {0}", si.Arguments);
        if (!si.UserName.IsNullOrEmpty())
            WriteLog("启动用户:{0}", si.UserName);

        var p = Process.Start(si);
        if (StartWait > 0 && p != null && p.WaitForExit(StartWait) && p.ExitCode != 0)
        {
            WriteLog("启动失败!ExitCode={0}", p.ExitCode);

            if (si.RedirectStandardError)
            {
                var rs = p.StandardOutput.ReadToEnd();
                if (!rs.IsNullOrEmpty()) WriteLog(rs);

                rs = p.StandardError.ReadToEnd();
                if (!rs.IsNullOrEmpty()) WriteLog(rs);
            }

            return null;
        }

        _fileName ??= file;

        return p;
    }

    #region 日志
    /// <summary>性能追踪</summary>
    public ITracer? Tracer { get; set; }

    /// <summary>日志</summary>
    public ILog Log { get; set; } = Logger.Null;

    /// <summary>写日志</summary>
    /// <param name="format"></param>
    /// <param name="args"></param>
    public void WriteLog(String format, params Object?[] args) => Log?.Info(format, args);
    #endregion
}