加强对异常请求头的支持
智能大石头 authored at 2025-06-18 10:09:11
8.89 KiB
Stardust
using System.Text.RegularExpressions;
using NewLife;
using NewLife.Log;

namespace StarAgent.Managers;

/// <summary>发布站点到nginx</summary>
/// <remarks>
/// 
/// </remarks>
internal class NginxDeploy
{
    #region 属性
    /// <summary>nginx配置文件</summary>
    public String SiteFile { get; set; } = String.Empty;

    /// <summary>nginx配置路径</summary>
    public String ConfigPath { get; set; } = String.Empty;

    /// <summary>配置文件扩展名</summary>
    public String Extension { get; set; } = String.Empty;

    private static String _nginxConfig = String.Empty;
    private static String _nginxExtension = ".conf";
    #endregion

    #region 构造
    static NginxDeploy()
    {
        Init();
    }

    public NginxDeploy()
    {
        // 初始化时,nginx配置目录和扩展名已经确定
        ConfigPath = _nginxConfig;
        Extension = _nginxExtension;
    }

    private static void Init()
    {
        // 初始化nginx配置
        var output = "nginx".Execute("-t", 5_000);
        if (!output.IsNullOrEmpty())
        {
            // 匹配nginx.conf路径
            var match = Regex.Match(output, @"nginx: configuration file (.+nginx\.conf) ");
            if (match.Success)
            {
                var nginxConfPath = match.Groups[1].Value.Trim();

                // 2. 读取nginx.conf,查找include行
                if (!String.IsNullOrEmpty(nginxConfPath) && File.Exists(nginxConfPath))
                {
                    var lines = File.ReadAllLines(nginxConfPath);
                    foreach (var line in lines)
                    {
                        var trimmed = line.Trim();
                        if (!trimmed.StartsWith("#") && trimmed.StartsWith("include"))
                        {
                            // 解析include路径
                            var parts = trimmed.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
                            if (parts.Length >= 2)
                            {
                                var includePath = parts[1].TrimEnd(';');
                                // 例如 include /etc/nginx/conf.d/*.conf
                                var dir = Path.GetDirectoryName(includePath);
                                var ext = Path.GetExtension(includePath);
                                if (!String.IsNullOrEmpty(dir) && Directory.Exists(dir))
                                {
                                    _nginxConfig = dir;
                                    _nginxExtension = String.IsNullOrEmpty(ext) ? ".conf" : ext;
                                    break;
                                }
                            }
                        }
                    }

                    if (!_nginxConfig.IsNullOrEmpty()) return;
                }
            }
        }

        // 这里可以加载默认的nginx配置模板

        // 常见nginx配置文件路径
        var commonPaths = new[]
        {
            "/etc/nginx/conf.d",
            "/etc/nginx/sites-enabled",
            "/usr/local/nginx/conf/vhost",
            "/usr/local/nginx/conf.d",
            "C:\\nginx\\conf\\vhost",
            "C:\\nginx\\conf\\sites-enabled",
            "C:\\nginx\\conf\\conf.d",
            "D:\\nginx\\conf\\vhost",
            "D:\\nginx\\conf\\sites-enabled",
            "D:\\nginx\\conf\\conf.d"
        };


        String? bestDir = null;
        String? bestExt = null;
        var maxCount = 0;

        foreach (var path in commonPaths)
        {
            if (Directory.Exists(path))
            {
                // 查找所有配置文件
                var files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly)
                    .Where(f => f.EndsWithIgnoreCase(".conf", ".nginx"))
                    .ToList();

                if (files.Count > 0)
                {
                    // 统计后缀名出现次数
                    var extGroup = files.GroupBy(f => Path.GetExtension(f).ToLower())
                        .OrderByDescending(g => g.Count())
                        .FirstOrDefault();

                    if (extGroup != null && extGroup.Count() > maxCount)
                    {
                        bestDir = path;
                        bestExt = extGroup.Key;
                        maxCount = extGroup.Count();
                    }
                }
            }
        }

        if (!String.IsNullOrEmpty(bestDir))
        {
            _nginxConfig = bestDir;
            _nginxExtension = bestExt ?? ".conf";
        }
        else
        {
            // 没有找到站点配置文件,使用默认
            _nginxConfig = commonPaths.FirstOrDefault(Directory.Exists) ?? "";
            _nginxExtension = ".conf";
        }
    }
    #endregion

    #region 方法
    /// <summary>检测nginx配置文件</summary>
    /// <remarks>
    /// 检测nginx配置文件是否存在nginx配置文件,以.nginx或者.conf结尾,且内部内容包含nginx特征
    /// 目录下可能有多个nginx配置文件,返回所有符合条件的配置文件
    /// </remarks>
    public static IEnumerable<NginxDeploy> DetectNginxConfig(String path)
    {
        //todo: 检测nginx配置文件是否存在nginx配置文件,以.nginx或者.conf结尾,且内部内容包含nginx特征
        // 目录下可能有多个nginx配置文件,返回所有符合条件的配置文件
        if (String.IsNullOrEmpty(path) || !Directory.Exists(path)) yield break;

        var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories)
            .Where(f => f.EndsWithIgnoreCase(".nginx", ".conf"));

        foreach (var file in files)
        {
            // 简单判断是否为nginx配置文件
            var content = File.ReadAllText(file);
            if (content.Contains("server") && content.Contains("listen") && content.Contains("location"))
            {
                yield return new NginxDeploy { SiteFile = file, };
            }
        }
    }

    /// <summary>发布站点到nginx</summary>
    public Boolean Publish()
    {
        // 判断配置文件是否存在,然后拷贝到nginx配置目录,并重新加载nginx配置
        if (String.IsNullOrEmpty(SiteFile) || String.IsNullOrEmpty(ConfigPath))
            throw new InvalidOperationException("配置文件或nginx路径未设置");

        if (!File.Exists(SiteFile))
            throw new FileNotFoundException("配置文件不存在", SiteFile);

        // 目标nginx配置目录
        var targetDir = ConfigPath;
        if (String.IsNullOrEmpty(targetDir) || !Directory.Exists(targetDir))
            throw new DirectoryNotFoundException("nginx配置目录不存在");

        // 拷贝配置文件
        var targetFile = targetDir.CombinePath(Path.GetFileName(SiteFile));
        if (!Extension.IsNullOrEmpty()) targetFile = Path.ChangeExtension(targetFile, Extension);

        // 目标文件不存在,或者两者MD5不一致时,才进行拷贝
        if (File.Exists(targetFile) && File.ReadAllText(SiteFile).MD5() == File.ReadAllText(targetFile).MD5())
            return false;

        WriteLog("正在发布站点配置到nginx:{0} => {1}", SiteFile, targetFile);
        File.Copy(SiteFile, targetFile, true);

        // 重新加载nginx配置
        WriteLog("正在重新加载nginx配置...");
        if (Runtime.Windows)
            targetDir.CombinePath("../nginx.exe").Run("-s reload", 5_000);
        else
            "nginx".Run("-s reload", 5_000);
        WriteLog("nginx配置重新加载完成。");

        return true;
    }

    /// <summary>
    /// 重新加载nginx配置
    /// </summary>
    private void ReloadNginx(String nginxDir)
    {
        // 尝试查找nginx可执行文件
        var exeNames = new[] { "nginx.exe", "nginx" };
        String? exePath = null;
        foreach (var exe in exeNames)
        {
            var path = Path.Combine(nginxDir, exe);
            if (File.Exists(path))
            {
                exePath = path;
                break;
            }
        }

        if (exePath == null)
        {
            // 可能nginx在PATH中,尝试直接调用
            exePath = exeNames.First();
        }

        try
        {
            var psi = new System.Diagnostics.ProcessStartInfo
            {
                FileName = exePath,
                Arguments = "-s reload",
                WorkingDirectory = nginxDir,
                CreateNoWindow = true,
                UseShellExecute = false
            };
            using (var proc = System.Diagnostics.Process.Start(psi))
            {
                proc?.WaitForExit(5000);
            }
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("重载nginx配置失败: " + ex.Message, ex);
        }
    }
    #endregion

    #region 日志
    public ILog Log { get; set; } = XTrace.Log;

    public void WriteLog(String format, params Object[] args) => Log?.Info(format, args);
    #endregion
}