using System.Diagnostics;
using Dpz.Core.Service.Mediator.Features.Code.Commands;
using Hangfire;
using JetBrains.Annotations;
using LibGit2Sharp;
using Mediator;

namespace Dpz.Core.Web.Jobs.Hangfire;

/// <summary>
/// 同步代码任务执行器
/// </summary>
[UsedImplicitly]
public class SyncCodeActivator(
    IMediator mediator,
    IConfiguration configuration,
    ILogger<SyncCodeActivator> logger
) : JobActivator
{
    private static readonly SemaphoreSlim SyncLock = new(1, 1);

    [ProlongExpirationTime]
    public async Task SyncAsync()
    {
        // 检查是否有实例正在运行
        if (!await SyncLock.WaitAsync(0))
        {
            logger.LogWarning("同步代码任务已有实例正在执行,跳过本次执行");
            return;
        }

        try
        {
            var sourceCodePath = configuration["CodeView:SourceCodeRoot"];
            if (string.IsNullOrWhiteSpace(sourceCodePath) || !Directory.Exists(sourceCodePath))
            {
                logger.LogError("源码目录:{Path},未配置或不存在", sourceCodePath);
                return;
            }

            // Git 拉取
            try
            {
                logger.LogInformation("正在拉取源代码");
                var cancelTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(30));
                var st = new Stopwatch();
                st.Start();

                // 尝试清理可能存在的锁文件
                CleanupGitLockFiles(sourceCodePath);

                using (var repo = new Repository(sourceCodePath))
                {
                    try
                    {
                        Commands.Checkout(repo, "master");
                        cancelTokenSource.Token.ThrowIfCancellationRequested();
                    }
                    catch (LockedFileException)
                    {
                        logger.LogWarning("检测到 Git 索引锁定,尝试清理锁文件后重试");
                        CleanupGitLockFiles(sourceCodePath);
                        cancelTokenSource.Token.ThrowIfCancellationRequested();
                        Commands.Checkout(repo, "master");
                        cancelTokenSource.Token.ThrowIfCancellationRequested();
                    }

                    var options = new PullOptions { FetchOptions = new FetchOptions() };
                    var signature = new Signature(
                        new Identity("dpz.core", "service@dpangzi.com"),
                        DateTimeOffset.Now
                    );

                    cancelTokenSource.Token.ThrowIfCancellationRequested();
                    var result = Commands.Pull(repo, signature, options);

                    if (result?.Status == MergeStatus.UpToDate)
                    {
                        logger.LogInformation("源代码已经是最新的");
                    }
                }

                st.Stop();
                logger.LogInformation(
                    "源代码拉取完毕,耗时:{ElapsedMilliseconds}ms",
                    st.ElapsedMilliseconds
                );
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "拉取源代码失败");
                return;
            }

            // 代码同步
            try
            {
                logger.LogInformation("正在同步代码到数据库");
                await mediator.Send(new SyncCodeFileSystemEntriesRequest());
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "同步代码到数据库失败");
            }
        }
        finally
        {
            SyncLock.Release();
        }
    }

    /// <summary>
    /// 清理 Git 锁文件
    /// </summary>
    /// <param name="repositoryPath">仓库路径</param>
    private void CleanupGitLockFiles(string repositoryPath)
    {
        try
        {
            var gitDir = Path.Combine(repositoryPath, ".git");
            var indexLockPath = Path.Combine(gitDir, "index.lock");

            if (File.Exists(indexLockPath))
            {
                logger.LogWarning("发现残留的 index.lock 文件,正在删除:{Path}", indexLockPath);
                File.Delete(indexLockPath);
                logger.LogInformation("index.lock 文件已删除");
            }

            // 也检查 HEAD.lock
            var headLockPath = Path.Combine(gitDir, "HEAD.lock");
            if (File.Exists(headLockPath))
            {
                logger.LogWarning("发现残留的 HEAD.lock 文件,正在删除:{Path}", headLockPath);
                File.Delete(headLockPath);
                logger.LogInformation("HEAD.lock 文件已删除");
            }
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "清理 Git 锁文件时发生异常");
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个基于 Hangfire 的后台任务执行器,用于定期同步 Git 仓库代码并将代码信息同步到数据库。


核心功能

1. 类定义与依赖注入

public class SyncCodeActivator(
    IMediator mediator,
    IConfiguration configuration,
    ILogger<SyncCodeActivator> logger
) : JobActivator
  • 继承自 JobActivator(Hangfire 的任务激活器基类)
  • 使用主构造函数注入三个依赖:
    • IMediator:用于发送命令(MediatR 模式)
    • IConfiguration:读取配置信息
    • ILogger:日志记录

2. 并发控制

private static readonly SemaphoreSlim SyncLock = new(1, 1);
  • 使用信号量确保同一时刻只有一个任务实例运行
  • 如果已有实例在执行,新任务会直接跳过:
if (!await SyncLock.WaitAsync(0))
{
    logger.LogWarning("同步代码任务已有实例正在执行,跳过本次执行");
    return;
}

3. 主同步流程 SyncAsync()

步骤 1:验证源码目录

var sourceCodePath = configuration["CodeView:SourceCodeRoot"];
if (string.IsNullOrWhiteSpace(sourceCodePath) || !Directory.Exists(sourceCodePath))
{
    logger.LogError("源码目录:{Path},未配置或不存在", sourceCodePath);
    return;
}

从配置中读取 CodeView:SourceCodeRoot,检查路径是否有效。


步骤 2:Git 拉取最新代码

using (var repo = new Repository(sourceCodePath))
{
    Commands.Checkout(repo, "master");
    var result = Commands.Pull(repo, signature, options);
}

关键处理:

  • 超时控制:设置 30 分钟超时

    var cancelTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(30));
    
  • 锁文件处理:如果遇到 LockedFileException(Git 索引被锁定),会:

    1. 调用 CleanupGitLockFiles() 清理残留锁文件
    2. 重新尝试 checkout
  • 拉取结果判断

    if (result?.Status == MergeStatus.UpToDate)
    {
        logger.LogInformation("源代码已经是最新的");
    }
    

步骤 3:同步到数据库

await mediator.Send(new SyncCodeFileSystemEntriesRequest());

通过 MediatR 发送命令,将文件系统中的代码信息同步到数据库。


4. 锁文件清理 CleanupGitLockFiles()

清理可能导致 Git 操作失败的残留锁文件:

var indexLockPath = Path.Combine(gitDir, "index.lock");
var headLockPath = Path.Combine(gitDir, "HEAD.lock");

这些锁文件通常在 Git 操作异常终止时产生,会阻止后续操作。


关键设计亮点

特性实现方式
防并发SemaphoreSlim 确保单实例运行
健壮性自动清理 Git 锁文件 + 重试机制
超时保护30 分钟 CancellationToken
性能监控使用 Stopwatch 记录耗时
异常隔离Git 拉取失败不影响数据库同步的尝试
Hangfire 特性[ProlongExpirationTime] 防止长时间任务被误删

典型使用场景

适用于需要定期从 Git 仓库同步代码并建立代码索引的系统,如:

  • 代码搜索平台
  • 代码审查系统
  • 代码统计分析工具
  • 在线代码浏览器

通过 Hangfire 配置定时任务(如每小时执行),自动保持代码库最新状态。

评论加载中...