All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dromara.jpom.build.BuildExecuteManage Maven / Gradle / Ivy

There is a newer version: 2.11.9
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Code Technology Studio
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.dromara.jpom.build;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.SystemClock;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileCopier;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.lang.Tuple;
import cn.hutool.core.map.SafeConcurrentHashMap;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.thread.ExecutorBuilder;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.keepbx.jpom.model.BaseIdModel;
import cn.keepbx.jpom.plugins.IPlugin;
import lombok.Builder;
import lombok.Lombok;
import lombok.extern.slf4j.Slf4j;
import org.dromara.jpom.JpomApplication;
import org.dromara.jpom.common.BaseServerController;
import org.dromara.jpom.common.ServerConst;
import org.dromara.jpom.func.assets.server.MachineDockerServer;
import org.dromara.jpom.func.files.service.FileStorageService;
import org.dromara.jpom.model.data.BuildInfoModel;
import org.dromara.jpom.model.data.CommandExecLogModel;
import org.dromara.jpom.model.data.RepositoryModel;
import org.dromara.jpom.model.docker.DockerInfoModel;
import org.dromara.jpom.model.enums.BuildReleaseMethod;
import org.dromara.jpom.model.enums.BuildStatus;
import org.dromara.jpom.model.log.BuildHistoryLog;
import org.dromara.jpom.model.script.ScriptExecuteLogModel;
import org.dromara.jpom.model.script.ScriptModel;
import org.dromara.jpom.model.user.UserModel;
import org.dromara.jpom.plugin.PluginFactory;
import org.dromara.jpom.service.dblog.BuildInfoService;
import org.dromara.jpom.service.dblog.DbBuildHistoryLogService;
import org.dromara.jpom.service.docker.DockerInfoService;
import org.dromara.jpom.service.script.ScriptExecuteLogServer;
import org.dromara.jpom.service.script.ScriptServer;
import org.dromara.jpom.system.ExtConfigBean;
import org.dromara.jpom.system.extconf.BuildExtConfig;
import org.dromara.jpom.util.AntPathUtil;
import org.dromara.jpom.util.CommandUtil;
import org.dromara.jpom.util.FileUtils;
import org.dromara.jpom.util.LogRecorder;
import org.springframework.util.Assert;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * @author bwcx_jzy
 * @since 2023/3/30
 */
@Builder
@Slf4j
public class BuildExecuteManage implements Runnable {

    /**
     * 构建线程池
     */
    private static ThreadPoolExecutor threadPoolExecutor;


    /**
     * 缓存构建中
     */
    public static final Map BUILD_MANAGE_MAP = new SafeConcurrentHashMap<>();

    private final TaskData taskData;
    private final BuildExtraModule buildExtraModule;
    private final String logId;
    private final BuildExecuteService buildExecuteService;
    private final ScriptServer scriptServer;
    private final ScriptExecuteLogServer scriptExecuteLogServer;
    private final BuildInfoService buildService;
    private final DbBuildHistoryLogService dbBuildHistoryLogService;
    private final DockerInfoService dockerInfoService;
    private final MachineDockerServer machineDockerServer;
    private final BuildExtConfig buildExtConfig;
    private final FileStorageService fileStorageService;
    //
    private Process process;
    private LogRecorder logRecorder;
    private File gitFile;
    private Thread currentThread;

    /**
     * 提交任务时间
     */
    private Long submitTaskTime;


    /**
     * 正在构建的数量
     *
     * @return 构建数量
     */
    public static Set buildKeys() {
        return BUILD_MANAGE_MAP.keySet();
    }

    /**
     * 创建构建线程池
     */
    private synchronized void initPool() {
        if (threadPoolExecutor != null) {
            return;
        }
        ExecutorBuilder executorBuilder = ExecutorBuilder.create();
        int poolSize = buildExtConfig.getPoolSize();
        if (poolSize > 0) {
            executorBuilder.setCorePoolSize(poolSize).setMaxPoolSize(poolSize);
        }
        executorBuilder.useArrayBlockingQueue(Math.max(buildExtConfig.getPoolWaitQueue(), 1));
        executorBuilder.setHandler(new ThreadPoolExecutor.DiscardPolicy() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
                if (r instanceof BuildExecuteManage) {
                    // 取消任务
                    BuildExecuteManage buildExecuteManage = (BuildExecuteManage) r;
                    buildExecuteManage.rejectedExecution();
                } else {
                    log.warn("构建线程池拒绝了未知任务:{}", r.getClass());
                }
            }
        });
        threadPoolExecutor = executorBuilder.build();
        JpomApplication.register("build", threadPoolExecutor);
    }

    /**
     * 提交任务
     */
    public void submitTask() {
        submitTaskTime = SystemClock.now();
        // 创建线程池
        initPool();
        //
        BuildInfoModel buildInfoModel = taskData.buildInfoModel;
        File logFile = BuildUtil.getLogFile(buildInfoModel.getId(), buildInfoModel.getBuildId());
        this.logRecorder = LogRecorder.builder().file(logFile).build();
        //
        int queueSize = threadPoolExecutor.getQueue().size();
        int size = BUILD_MANAGE_MAP.size();
        logRecorder.system("当前构建中任务数:{},队列中任务数:{} {}", size, queueSize,
            size > buildExtConfig.getPoolSize() ? "构建任务开始进入队列等待...." : StrUtil.EMPTY);
        //BuildInfoManage manage = new BuildInfoManage(taskData);
        BUILD_MANAGE_MAP.put(buildInfoModel.getId(), this);
        threadPoolExecutor.execute(this);
    }

    /**
     * 取消任务(拒绝执行)
     */
    private void rejectedExecution() {
        int queueSize = threadPoolExecutor.getQueue().size();
        int limitPoolSize = threadPoolExecutor.getPoolSize();
        int corePoolSize = threadPoolExecutor.getCorePoolSize();
        String format = StrUtil.format("当前构建中任务数:{},队列中任务数:{} 构建任务等待超时或者超出最大等待数量,当前运行中的任务数:{}/{},取消执行当前构建", BUILD_MANAGE_MAP.size(), queueSize, limitPoolSize, corePoolSize);
        logRecorder.system(format);
        this.cancelTask(format);
    }

    /**
     * 取消任务
     */
    private void cancelTask(String desc) {
        Optional.ofNullable(process).ifPresent(Process::destroy);
        Integer buildMode = taskData.buildInfoModel.getBuildMode();
        if (buildMode != null && buildMode == 1) {
            // 容器构建 删除容器
            try {
                Optional.ofNullable(taskData.dockerParameter).ifPresent(parameter -> {
                    IPlugin plugin = PluginFactory.getPlugin(DockerInfoService.DOCKER_PLUGIN_NAME);
                    parameter.put("containerId", taskData.buildContainerId);
                    try {
                        plugin.execute("removeContainer", parameter);
                    } catch (Exception e) {
                        throw Lombok.sneakyThrow(e);
                    }
                });

            } catch (Exception e) {
                log.warn("清理构建资源失败", e);
            }
        }
        String buildId = taskData.buildInfoModel.getId();
        buildExecuteService.updateStatus(buildId, logId, taskData.buildInfoModel.getBuildId(), BuildStatus.Cancel, desc);
        Optional.ofNullable(currentThread).ifPresent(Thread::interrupt);
        BUILD_MANAGE_MAP.remove(buildId);
    }

    /**
     * 打包构建产物
     */
    private String packageFile() {
        BuildInfoModel buildInfoModel = taskData.buildInfoModel;
        Integer buildMode = taskData.buildInfoModel.getBuildMode();
        String resultDirFile = buildInfoModel.getResultDirFile();
        String excludeReleaseAnt = this.buildExtraModule.getExcludeReleaseAnt();
        boolean releaseHideFile = ObjectUtil.defaultIfNull(this.buildExtraModule.getReleaseHideFile(), false);
        List excludeReleaseAnts = StrUtil.splitTrim(excludeReleaseAnt, StrUtil.COMMA);
        ResultDirFileAction resultDirFileAction = ResultDirFileAction.parse(resultDirFile);
        final int[] excludeReleaseAntCount = {0};
        Predicate predicate = file -> {
            if (CollUtil.isEmpty(excludeReleaseAnts)) {
                return true;
            }
            for (String releaseAnt : excludeReleaseAnts) {
                if (AntPathUtil.ANT_PATH_MATCHER.match(releaseAnt, file)) {
                    // 过滤
                    excludeReleaseAntCount[0]++;
                    return false;
                }
            }
            return true;
        };
        if (buildMode != null && buildMode == 1) {
            // 容器构建直接下载到 结果目录
            File toFile = BuildUtil.getHistoryPackageFile(buildInfoModel.getId(), buildInfoModel.getBuildId(), resultDirFileAction.getPath());
            if (!FileUtil.exist(toFile)) {
                String format = StrUtil.format("{} 不存在,处理构建产物失败", resultDirFileAction.getPath());
                logRecorder.systemError(format);
                return format;
            }
            logRecorder.system("备份产物 {} {}", resultDirFileAction.getPath(), buildInfoModel.getBuildId());
            return null;
        }
        if (resultDirFileAction.getType() == ResultDirFileAction.Type.ANT_PATH) {
            // 通配模式
            List paths = AntPathUtil.antPathMatcher(this.gitFile, resultDirFileAction.getPath());
            int matcherSize = CollUtil.size(paths);
            if (matcherSize <= 0) {
                String format = StrUtil.format("{} 没有匹配到任何文件", resultDirFileAction.getPath());
                logRecorder.systemError(format);
                return format;
            }
            logRecorder.system("{} 模糊匹配到 {} 个文件", resultDirFileAction.getPath(), matcherSize);
            String antSubMatch = resultDirFileAction.antSubMatch();
            ResultDirFileAction.AntFileUploadMode antFileUploadMode = resultDirFileAction.getAntFileUploadMode();
            Assert.notNull(antFileUploadMode, "没有配置文件上传模式");
            File historyPackageFile = BuildUtil.getHistoryPackageFile(buildInfoModel.getId(), buildInfoModel.getBuildId(), StrUtil.SLASH);
            int subMatchCount = paths.stream()
                .filter(s -> {
                    // 需要能满足二级匹配
                    return StrUtil.isEmpty(antSubMatch) || AntPathUtil.ANT_PATH_MATCHER.matchStart(antSubMatch + "**", s);
                })
                .filter(predicate)
                .mapToInt(path -> {
                    File toFile;
                    if (antFileUploadMode == ResultDirFileAction.AntFileUploadMode.KEEP_DIR) {
                        // 剔除文件夹层级
                        List list = StrUtil.splitTrim(path, StrUtil.SLASH);
                        int notMathIndex;
                        int pathItemSize = list.size();
                        if (StrUtil.isEmpty(antSubMatch) || StrUtil.equals(antSubMatch, StrUtil.SLASH)) {
                            notMathIndex = 0;
                        } else {
                            notMathIndex = ArrayUtil.INDEX_NOT_FOUND;
                            for (int i = pathItemSize - 1; i >= 0; i--) {
                                String suffix = i == pathItemSize - 1 ? StrUtil.EMPTY : StrUtil.SLASH;
                                String itemS = StrUtil.SLASH + CollUtil.join(CollUtil.sub(list, 0, i + 1), StrUtil.SLASH) + suffix;
                                if (AntPathUtil.ANT_PATH_MATCHER.match(antSubMatch, itemS)) {
                                    notMathIndex = i + 1;
                                    break; // 结束本次循环
                                }
                            }
                            if (notMathIndex == ArrayUtil.INDEX_NOT_FOUND) {
                                return 0;
                            }
                        }
                        // 保留文件夹层级
                        String itemEnd = CollUtil.join(CollUtil.sub(list, notMathIndex, pathItemSize), StrUtil.SLASH);
                        toFile = FileUtil.file(historyPackageFile, itemEnd);
                    } else if (antFileUploadMode == ResultDirFileAction.AntFileUploadMode.SAME_DIR) {
                        toFile = historyPackageFile;
                    } else {
                        throw new IllegalStateException("暂不支持的模式:" + antFileUploadMode);
                    }
                    // 创建文件夹,避免出现文件全部为相关文件名(result)
                    BuildUtil.mkdirHistoryPackageFile(buildInfoModel.getId(), buildInfoModel.getBuildId());
                    File srcFile = FileUtil.file(this.gitFile, path);
                    //
                    FileCopier.create(srcFile, toFile)
                        .setCopyContentIfDir(true)
                        .setOverride(true)
                        .setCopyAttributes(true)
                        .setCopyFilter(file -> releaseHideFile || !file.isHidden())
                        .copy();
                    return 1;
                }).sum();
            if (subMatchCount <= 0) {
                String format = StrUtil.format("{} 没有匹配到任何文件", antSubMatch);
                logRecorder.systemError(format);
                return format;
            }
            logRecorder.system("{} 二级目录模糊匹配到 {} 个文件, 当前文件保留方式 {}", antSubMatch, subMatchCount, antFileUploadMode);
            // 更新产物路径为普通路径
            dbBuildHistoryLogService.updateResultDirFile(this.logId, StrUtil.SLASH);
            buildInfoModel.setResultDirFile(StrUtil.SLASH);
            this.buildExtraModule.setResultDirFile(StrUtil.SLASH);
        } else if (resultDirFileAction.getType() == ResultDirFileAction.Type.ORIGINAL) {
            File file = FileUtil.file(this.gitFile, resultDirFile);
            if (!file.exists()) {
                String format = StrUtil.format("{} 不存在,处理构建产物失败", resultDirFile);
                logRecorder.systemError(format);
                return format;
            }
            BuildUtil.mkdirHistoryPackageFile(buildInfoModel.getId(), buildInfoModel.getBuildId());
            File toFile = BuildUtil.getHistoryPackageFile(buildInfoModel.getId(), buildInfoModel.getBuildId(), resultDirFile);
            //
            String rootDir = FileUtil.getAbsolutePath(gitFile);
            FileCopier.create(file, toFile)
                .setCopyContentIfDir(true)
                .setOverride(true)
                .setCopyAttributes(true)
                .setCopyFilter(file12 -> {
                    if (!releaseHideFile && file12.isHidden()) {
                        return false;
                    }
                    String subPath = FileUtil.subPath(rootDir, file12);
                    subPath = FileUtil.normalize(StrUtil.SLASH + subPath);
                    return predicate.test(subPath);
                })
                .copy();
        }
        if (CollUtil.isNotEmpty(excludeReleaseAnts)) {
            logRecorder.system("{} 累积过滤:{} 个文件 ", excludeReleaseAnt, excludeReleaseAntCount[0]);
        }
        return null;
    }

    /**
     * 准备构建
     *
     * @return false 执行异常需要结束
     */
    private String startReady() {
        BuildInfoModel buildInfoModel = taskData.buildInfoModel;
        this.gitFile = BuildUtil.getSourceById(buildInfoModel.getId());

        Integer delay = taskData.delay;
        logRecorder.system("开始构建 #{} 构建执行路径 : {}", buildInfoModel.getBuildId(), FileUtil.getAbsolutePath(this.gitFile));
        if (delay != null && delay > 0) {
            // 延迟执行
            logRecorder.system("执行等待 {} 秒", delay);
            ThreadUtil.sleep(delay, TimeUnit.SECONDS);
        }
        // 删除缓存
        Boolean cacheBuild = this.buildExtraModule.getCacheBuild();
        if (cacheBuild != null && !cacheBuild) {
            logRecorder.system("删除构建缓存");
            CommandUtil.systemFastDel(this.gitFile);
        }
        //
        taskData.environmentMapBuilder.put("BUILD_ID", this.buildExtraModule.getId());
        taskData.environmentMapBuilder.put("BUILD_NAME", this.buildExtraModule.getName());
        taskData.environmentMapBuilder.put("BUILD_SOURCE_FILE", FileUtil.getAbsolutePath(this.gitFile));
        taskData.environmentMapBuilder.put("BUILD_NUMBER_ID", String.valueOf(this.taskData.buildInfoModel.getBuildId()));
        taskData.environmentMapBuilder.put("BUILD_ORIGINAL_RESULT_DIR_FILE", buildInfoModel.getResultDirFile());
        // 配置的分支名称,可能存在模糊匹配的情况
        taskData.environmentMapBuilder.put("BUILD_CONFIG_BRANCH_NAME", this.taskData.buildInfoModel.getBranchName());
        return null;
    }

    /**
     * 拉取代码后并缓存环境变量
     *
     * @return pull 的结果
     */
    private String pullAndCacheBuildEnv() {
        String pull = this.pull();
        if (pull == null) {
            BuildHistoryLog buildInfoModel = new BuildHistoryLog();
            buildInfoModel.setId(logId);
            buildInfoModel.setBuildEnvCache(taskData.environmentMapBuilder.toDataJsonStr());
            dbBuildHistoryLogService.updateById(buildInfoModel);
        }
        return pull;
    }

    /**
     * 拉取代码
     *
     * @return false 执行异常需要结束
     */
    private String pull() {
        RepositoryModel repositoryModel = taskData.repositoryModel;
        BuildInfoModel buildInfoModel = taskData.buildInfoModel;
        try {
            String msg;
            Integer repoTypeCode = repositoryModel.getRepoType();
            RepositoryModel.RepoType repoType = EnumUtil.likeValueOf(RepositoryModel.RepoType.class, repoTypeCode);
            Boolean checkRepositoryDiff = Optional.ofNullable(taskData.checkRepositoryDiff).orElse(buildExtraModule.getCheckRepositoryDiff());
            String repositoryLastCommitId = buildInfoModel.getRepositoryLastCommitId();
            if (repoType == RepositoryModel.RepoType.Git) {
                // git with password
                IPlugin plugin = PluginFactory.getPlugin("git-clone");
                Map map = repositoryModel.toMap();
                // 指定 clone 深度
                Integer cloneDepth = buildExtraModule.getCloneDepth();
                map.put("depth", cloneDepth);
                if (cloneDepth != null) {
                    // 使用系统
                    map.put("gitProcessType", "SystemGit");
                }
                Tuple tuple = (Tuple) plugin.execute("branchAndTagList", map);
                //GitUtil.getBranchAndTagList(repositoryModel);
                Assert.notNull(tuple, "获取仓库分支失败");
                map.put("reduceProgressRatio", buildExtConfig.getLogReduceProgressRatio());
                map.put("logWriter", logRecorder.getPrintWriter());
                map.put("savePath", gitFile);
                // 模糊匹配 标签
                String branchTagName = buildInfoModel.getBranchTagName();
                String[] result;
                if (StrUtil.isNotEmpty(branchTagName)) {
                    String newBranchTagName = fuzzyMatch(tuple.get(1), branchTagName);
                    if (StrUtil.isEmpty(newBranchTagName)) {
                        String format = StrUtil.format("{} Did not match the corresponding tag", branchTagName);
                        logRecorder.systemError(format);
                        return format;
                    }
                    // author bwcx_jzy 2022.11.28 map.put("branchName", newBranchName);
                    map.put("tagName", newBranchTagName);
                    //author bwcx_jzy 2022.11.28 buildEnv.put("BUILD_BRANCH_NAME", newBranchName);
                    taskData.environmentMapBuilder.put("BUILD_TAG_NAME", newBranchTagName);
                    // 标签拉取模式
                    logRecorder.system("repository tag [{}] clone pull from {}", branchTagName, newBranchTagName);
                    result = (String[]) plugin.execute("pullByTag", map);
                } else {
                    String branchName = buildInfoModel.getBranchName();
                    // 模糊匹配分支
                    String newBranchName = fuzzyMatch(tuple.get(0), branchName);
                    if (StrUtil.isEmpty(newBranchName)) {
                        String format = StrUtil.format("{} Did not match the corresponding branch", branchName);
                        logRecorder.systemError(format);
                        //buildExecuteService.updateStatus(buildInfoModel.getId(), this.logId, this.taskData.buildInfoModel.getBuildId(), BuildStatus.Error);
                        return format;
                    }
                    // 分支模式
                    map.put("branchName", newBranchName);
                    // 真实使用的分支名
                    taskData.environmentMapBuilder.put("BUILD_BRANCH_NAME", newBranchName);
                    logRecorder.system("repository [{}] clone pull from {}", branchName, newBranchName);
                    result = (String[]) plugin.execute("pull", map);
                }
                msg = result[1];
                // 判断hash 码和上次构建是否一致
                if (checkRepositoryDiff != null && checkRepositoryDiff) {
                    if (StrUtil.equals(repositoryLastCommitId, result[0])) {
                        // 如果一致,则不构建
                        String format = StrUtil.format("仓库代码没有任何变动终止本次构建:{} {}", result[0], msg);
                        logRecorder.systemError(format);
                        throw new DiyInterruptException(format);
                    }
                }
                taskData.repositoryLastCommitId = result[0];
            } else if (repoType == RepositoryModel.RepoType.Svn) {
                // svn
                Map map = repositoryModel.toMap();

                IPlugin plugin = PluginFactory.getPlugin("svn-clone");
                String[] result = (String[]) plugin.execute(gitFile, map);
                //msg = SvnKitUtil.checkOut(repositoryModel, gitFile);
                msg = ArrayUtil.get(result, 1);
                // 判断版本号和上次构建是否一致
                if (checkRepositoryDiff != null && checkRepositoryDiff) {
                    if (StrUtil.equals(repositoryLastCommitId, result[0])) {
                        // 如果一致,则不构建
                        String format = StrUtil.format("仓库代码没有任何变动终止本次构建:{}", result[0]);
                        logRecorder.systemError(format);
                        throw new DiyInterruptException(format);
                    }
                }
                taskData.repositoryLastCommitId = result[0];
            } else {
                String format = StrUtil.format("不支持的类型:{}", repoType.getDesc());
                logRecorder.systemError(format);
                return format;
            }
            taskData.environmentMapBuilder.put("BUILD_COMMIT_ID", taskData.repositoryLastCommitId);
            logRecorder.system(msg);
        } catch (Exception e) {
            throw Lombok.sneakyThrow(e);
        }
        // env file
        String attachEnv = this.buildExtraModule.getAttachEnv();
        Opt.ofBlankAble(attachEnv).ifPresent(s -> {
            UrlQuery of = UrlQuery.of(attachEnv, CharsetUtil.CHARSET_UTF_8);
            Map queryMap = of.getQueryMap();
            logRecorder.system("读取附件变量:{} {}", attachEnv, CollUtil.size(queryMap));
            //
            Optional.ofNullable(queryMap).ifPresent(map -> {
                for (Map.Entry entry : map.entrySet()) {
                    taskData.environmentMapBuilder.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
                }
            });
            Map envFileMap = FileUtils.readEnvFile(this.gitFile, s);
            taskData.environmentMapBuilder.putStr(envFileMap);
        });
        // 输出环境变量
        taskData.environmentMapBuilder.eachStr(logRecorder::system);
        return null;
    }

    private String dockerCommand() {
        BuildInfoModel buildInfoModel = taskData.buildInfoModel;
        String script = buildInfoModel.getScript();
        DockerYmlDsl dockerYmlDsl = DockerYmlDsl.build(script);
        String fromTag = dockerYmlDsl.getFromTag();
        // 根据 tag 查询
        List dockerInfoModels = dockerInfoService
            .queryByTag(buildInfoModel.getWorkspaceId(), fromTag);
        Map map = machineDockerServer.dockerParameter(dockerInfoModels);
        Assert.notNull(map, fromTag + " 没有可用的 docker server");
        taskData.dockerParameter = new HashMap<>(map);
        logRecorder.system("use docker {}", map.get("name"));
        logRecorder.info("");
        String workingDir = "/home/jpom/";

        map.put("runsOn", dockerYmlDsl.getRunsOn());
        map.put("workingDir", workingDir);
        map.put("tempDir", JpomApplication.getInstance().getTempPath());
        String buildInfoModelId = buildInfoModel.getId();
        taskData.buildContainerId = "jpom-build-" + buildInfoModelId;
        map.put("dockerName", taskData.buildContainerId);
        map.put("logFile", logRecorder.getFile());
        //
        List copy = ObjectUtil.defaultIfNull(dockerYmlDsl.getCopy(), new ArrayList<>());
        // 将仓库文件上传到容器
        copy.add(FileUtil.getAbsolutePath(this.gitFile) + StrUtil.COLON + workingDir + StrUtil.COLON + "true");
        map.put("copy", copy);
        map.put("binds", ObjectUtil.defaultIfNull(dockerYmlDsl.getBinds(), new ArrayList<>()));

        Map dockerEnv = ObjectUtil.defaultIfNull(dockerYmlDsl.getEnv(), new HashMap<>(10));
        Map env = taskData.environmentMapBuilder.environment();
        env.putAll(dockerEnv);
        env.put("JPOM_BUILD_ID", buildInfoModelId);
        env.put("JPOM_WORKING_DIR", workingDir);
        map.put("env", env);
        map.put("steps", dockerYmlDsl.getSteps());
        // 构建产物
        String resultDirFile = buildInfoModel.getResultDirFile();
        String resultFile = FileUtil.normalize(workingDir + StrUtil.SLASH + resultDirFile);
        map.put("resultFile", resultFile);
        // 产物输出目录
        File toFile = BuildUtil.getHistoryPackageFile(buildInfoModelId, buildInfoModel.getBuildId(), resultDirFile);
        map.put("resultFileOut", FileUtil.getAbsolutePath(toFile));
        IPlugin plugin = PluginFactory.getPlugin(DockerInfoService.DOCKER_PLUGIN_NAME);
        try {
            Object execute = plugin.execute("build", map);
            int resultCode = Convert.toInt(execute, -100);
            // 严格模式
            if (buildExtraModule.strictlyEnforce()) {
                return resultCode == 0 ? null : StrUtil.format("执行命令退出码非0,{}", resultCode);
            }
        } catch (Exception e) {
            logRecorder.error("构建调用容器异常", e);
            return e.getMessage();
        }
        return null;
    }

    /**
     * 执行构建命令
     *
     * @return false 执行异常需要结束
     */
    private String executeCommand() {
        BuildInfoModel buildInfoModel = taskData.buildInfoModel;
        Integer buildMode = buildInfoModel.getBuildMode();
        if (buildMode != null && buildMode == 1) {
            // 容器构建
            return this.dockerCommand();
        }
        String script = buildInfoModel.getScript();
        if (StrUtil.isEmpty(script)) {
            String info = "没有需要执行的命令";
            logRecorder.systemError(info);
            return info;
        }
        if (StrUtil.startWith(script, ServerConst.REF_SCRIPT)) {
            String scriptId = StrUtil.removePrefix(script, ServerConst.REF_SCRIPT);
            ScriptModel keyAndGlobal = scriptServer.getByKey(scriptId);
            Assert.notNull(keyAndGlobal, "请选择正确的脚本");
            script = keyAndGlobal.getContext();
            logRecorder.system("引入脚本内容:{}[{}]", keyAndGlobal.getName(), scriptId);
        }
        Map environment = taskData.environmentMapBuilder.environment();

        InputStream templateInputStream = ExtConfigBean.getConfigResourceInputStream("/exec/template." + CommandUtil.SUFFIX);
        String s1 = IoUtil.readUtf8(templateInputStream);
        try {
            int waitFor = JpomApplication.getInstance()
                .execScript(s1 + script, file -> {
                    try {
                        return CommandUtil.execWaitFor(file, this.gitFile, environment, StrUtil.EMPTY, (s, process) -> logRecorder.info(s));
                    } catch (IOException | InterruptedException e) {
                        throw Lombok.sneakyThrow(e);
                    }
                });
            logRecorder.system("执行脚本的退出码是:{}", waitFor);
            // 判断是否为严格执行
            if (buildExtraModule.strictlyEnforce()) {
                return waitFor == 0 ? null : StrUtil.format("执行命令退出码非0,{}", waitFor);
            }
        } catch (Exception e) {
            logRecorder.error("执行异常", e);
            return e.getMessage();
        }
        return null;
    }

    /**
     * 打包发布
     *
     * @return false 执行需要结束
     */
    private String release() {
        BuildInfoModel buildInfoModel = taskData.buildInfoModel;
        UserModel userModel = taskData.userModel;
        // 发布文件
        ReleaseManage releaseManage = ReleaseManage.builder()
            .buildNumberId(buildInfoModel.getBuildId())
            .buildExtraModule(buildExtraModule)
            .userModel(userModel)
            .logId(logId)
            .dockerInfoService(dockerInfoService)
            .machineDockerServer(machineDockerServer)
            .buildEnv(taskData.environmentMapBuilder)
            .fileStorageService(fileStorageService)
            .buildExecuteService(buildExecuteService)
            .buildExtConfig(buildExtConfig)
            .logRecorder(logRecorder)
            .build();
        try {
            return releaseManage.start(resultFileSize -> taskData.resultFileSize = resultFileSize, buildInfoModel);
        } catch (Exception e) {
            throw Lombok.sneakyThrow(e);
        }
    }

    /**
     * 结束流程
     *
     * @return 流程执行是否成功
     */
    private String finish() {
        BuildInfoModel buildInfoModel1 = taskData.buildInfoModel;
        if (StrUtil.isNotEmpty(taskData.repositoryLastCommitId)) {
            BuildInfoModel buildInfoModel = new BuildInfoModel();
            buildInfoModel.setId(buildInfoModel1.getId());
            buildInfoModel.setRepositoryLastCommitId(taskData.repositoryLastCommitId);
            buildService.updateById(buildInfoModel);
        }
        //
        BuildStatus buildStatus = buildInfoModel1.getReleaseMethod() != BuildReleaseMethod.No.getCode() ? BuildStatus.PubSuccess : BuildStatus.Success;
        buildExecuteService.updateStatus(buildInfoModel1.getId(), this.logId, this.taskData.buildInfoModel.getBuildId(), buildStatus, "任务正常结束");
        // 判断是否保留产物
        Boolean saveBuildFile = this.buildExtraModule.getSaveBuildFile();
        if (saveBuildFile != null && !saveBuildFile) {
            // 删除 产物文件夹
            File historyPackageFile = BuildUtil.getHistoryPackageFile(buildExtraModule.getId(), buildInfoModel1.getBuildId(), StrUtil.SLASH);
            CommandUtil.systemFastDel(historyPackageFile);
            // 被删除后
            this.taskData.resultFileSize = 0L;
        }
        return null;
    }

    private Map createProcess() {
        // 初始化构建流程 准备构建->拉取仓库代码->执行构建命令->打包产物->发布产物->构建结束
        Map suppliers = new LinkedHashMap<>(10);
        suppliers.put("startReady", new IProcessItem() {
            @Override
            public String name() {
                return "准备构建";
            }

            @Override
            public String execute() {
                return BuildExecuteManage.this.startReady();
            }
        });
        suppliers.put("pull", new IProcessItem() {
            @Override
            public String name() {
                return "拉取仓库代码";
            }

            @Override
            public String execute() {
                return BuildExecuteManage.this.pullAndCacheBuildEnv();
            }
        });
        suppliers.put("executeCommand", new IProcessItem() {
            @Override
            public String name() {
                return "执行构建命令";
            }

            @Override
            public String execute() {
                return BuildExecuteManage.this.executeCommand();
            }
        });
        suppliers.put("packageFile", new IProcessItem() {
            @Override
            public String name() {
                return "打包产物";
            }

            @Override
            public String execute() {
                return BuildExecuteManage.this.packageFile();
            }
        });
        suppliers.put("release", new IProcessItem() {
            @Override
            public String name() {
                return "发布产物";
            }

            @Override
            public String execute() {
                return BuildExecuteManage.this.release();
            }
        });
        suppliers.put("finish", new IProcessItem() {
            @Override
            public String name() {
                return "构建结束";
            }

            @Override
            public String execute() {
                return BuildExecuteManage.this.finish();
            }
        });
        return suppliers;
    }

    /**
     * 清理构建资源
     */
    private void clearResources() {
        //
        BuildInfoModel buildInfoModel1 = taskData.buildInfoModel;
        File historyPackageZipFile = BuildUtil.getHistoryPackageZipFile(buildExtraModule.getId(), buildInfoModel1.getBuildId());
        CommandUtil.systemFastDel(historyPackageZipFile);
        // 计算文件占用大小
        File file = logRecorder.getFile();
        long size = FileUtil.size(file);

        BuildHistoryLog buildInfoModel = new BuildHistoryLog();
        buildInfoModel.setId(logId);
        buildInfoModel.setResultFileSize(taskData.resultFileSize);
        buildInfoModel.setBuildLogFileSize(size);
        dbBuildHistoryLogService.updateById(buildInfoModel);
    }

    public void runTask() {
        currentThread = Thread.currentThread();
        logRecorder.system("开始执行构建任务,任务等待时间:{}", DateUtil.formatBetween(SystemClock.now() - submitTaskTime));

        // 判断任务是否被取消
        BuildHistoryLog buildHistoryLog = dbBuildHistoryLogService.getByKey(this.logId);
        if (buildHistoryLog == null) {
            logRecorder.systemError("构建记录丢失,无法继续构建");
            return;
        }
        if (buildHistoryLog.getStatus() == null || buildHistoryLog.getStatus() == BuildStatus.Cancel.getCode()) {
            logRecorder.systemError("构建状态异常或者被取消");
            return;
        }
        BuildInfoModel buildInfoModel = this.taskData.buildInfoModel;
        buildExecuteService.updateStatus(buildInfoModel.getId(), this.logId, buildInfoModel.getBuildId(), BuildStatus.Ing, "开始构建,构建线程执行");
        //
        Map processItemMap = this.createProcess();
        // 依次执行流程,发生异常结束整个流程
        String processName = StrUtil.EMPTY;
        long startTime = SystemClock.now();
        if (taskData.triggerBuildType == 2) {
            // 系统触发构建
            BaseServerController.resetInfo(UserModel.EMPTY);
        } else {
            BaseServerController.resetInfo(taskData.userModel);
        }

        try {
            boolean stop = false;
            for (Map.Entry stringSupplierEntry : processItemMap.entrySet()) {
                processName = stringSupplierEntry.getKey();
                IProcessItem processItem = stringSupplierEntry.getValue();
                //
                long processItemStartTime = SystemClock.now();
                logRecorder.system("开始执行 {}流程", processItem.name());
                String interruptMsg = this.asyncWebHooks(processName);
                if (interruptMsg != null) {
                    // 事件脚本中断构建流程
                    logRecorder.system("执行中断 {} 流程,原因事件脚本中断", processItem.name());
                    this.asyncWebHooks("stop", "process", processName, "statusMsg", interruptMsg);
                    buildExecuteService.updateStatus(buildInfoModel.getId(), this.logId, buildInfoModel.getBuildId(), BuildStatus.Interrupt, interruptMsg);
                    stop = true;
                    break;
                }
                String errorMsg = processItem.execute();
                if (errorMsg != null) {
                    // 有条件结束构建流程
                    logRecorder.systemError("执行异常[{}]流程:{}", processItem.name(), errorMsg);
                    this.asyncWebHooks("stop", "process", processName, "statusMsg", errorMsg);
                    buildExecuteService.updateStatus(buildInfoModel.getId(), this.logId, buildInfoModel.getBuildId(), BuildStatus.Error, errorMsg);
                    stop = true;
                    break;
                }
                logRecorder.system("执行结束 {}流程,耗时:{}", processItem.name(), DateUtil.formatBetween(SystemClock.now() - processItemStartTime));
            }
            if (!stop) { // 没有执行 stop
                this.asyncWebHooks("success");
            }
        } catch (DiyInterruptException diyInterruptException) {
            // 主动中断
            this.asyncWebHooks("stop", "process", processName);
            buildExecuteService.updateStatus(buildInfoModel.getId(), this.logId, buildInfoModel.getBuildId(), BuildStatus.Interrupt, diyInterruptException.getMessage());
        } catch (java.util.concurrent.CancellationException interruptException) {
            // 异常中断
            this.asyncWebHooks("stop", "process", processName);
            buildExecuteService.updateStatus(buildInfoModel.getId(), this.logId, buildInfoModel.getBuildId(), BuildStatus.Interrupt, "系统中断异常");
        } catch (Exception e) {
            buildExecuteService.updateStatus(buildInfoModel.getId(), this.logId, buildInfoModel.getBuildId(), BuildStatus.Error, e.getMessage());
            logRecorder.error("构建失败:" + processName, e);
            this.asyncWebHooks("error", "process", processName, "statusMsg", e.getMessage());
        } finally {
            this.clearResources();
            logRecorder.system("构建结束-累计耗时:{}", DateUtil.formatBetween(SystemClock.now() - startTime));
            this.asyncWebHooks("done");
            BaseServerController.removeAll();
        }
    }

    public void run() {
        BuildInfoModel buildInfoModel = this.taskData.buildInfoModel;
        try {
            this.runTask();
        } catch (Exception e) {
            log.error("构建发生未知错误", e);
        } finally {
            BUILD_MANAGE_MAP.remove(buildInfoModel.getId());
        }
    }

    /**
     * 执行 webhooks 通知
     *
     * @param type  类型
     * @param other 其他参数
     * @return 是否还继续整个构建流程
     */
    private String asyncWebHooks(String type, Object... other) {
        BuildInfoModel buildInfoModel = taskData.buildInfoModel;
        Map map = new HashMap<>(10);
        //
        for (int i = 0; i < other.length; i += 2) {
            map.put(other[i].toString(), other[i + 1]);
        }
        map.put("buildId", buildInfoModel.getId());
        map.put("buildNumberId", this.taskData.buildInfoModel.getBuildId());
        map.put("buildName", buildInfoModel.getName());
        map.put("buildSourceFile", FileUtil.getAbsolutePath(this.gitFile));
        map.put("type", type);
        map.put("triggerBuildType", taskData.triggerBuildType);
        map.put("triggerTime", SystemClock.now());
        String triggerUser = Optional.ofNullable(taskData.userModel).map(BaseIdModel::getId).orElse(UserModel.SYSTEM_ADMIN);
        map.put("triggerUser", triggerUser);
        String resultDirFile = buildExtraModule.getResultDirFile();
        map.put("buildResultDirFile", resultDirFile);
        map.put("buildResultFile", BuildUtil.getHistoryPackageFile(buildInfoModel.getId(), this.taskData.buildInfoModel.getBuildId(), resultDirFile));

        Opt.ofBlankAble(buildInfoModel.getWebhook())
            .ifPresent(s ->
                ThreadUtil.execute(() -> {
                    try {
                        IPlugin plugin = PluginFactory.getPlugin("webhook");
                        plugin.execute(s, map);
                    } catch (Exception e) {
                        log.error("WebHooks 调用错误", e);
                    }
                })
            );
        // 执行对应的事件脚本
        try {
            return this.noticeScript(type, map);
        } catch (Exception e) {
            log.error("noticeScript 调用错误", e);
            logRecorder.error("执行事件脚本错误", e);
            // 执行事件脚本发送异常不终止构建流程
            return null;
        }
    }

    /**
     * 执行事件脚本
     *
     * @param type 事件类型
     * @param map  相关参数
     * @return 是否还继续整个构建流程
     */
    private String noticeScript(String type, Map map) {
        String noticeScriptId = this.buildExtraModule.getNoticeScriptId();
        if (StrUtil.isEmpty(noticeScriptId)) {
            return null;
        }
        List list = StrUtil.splitTrim(noticeScriptId, StrUtil.COMMA);
        for (String noticeScriptIdItem : list) {
            String error = this.noticeScript(noticeScriptIdItem, type, map);
            if (error != null) {
                return error;
            }
        }
        return null;
    }

    /**
     * 执行事件脚本
     *
     * @param noticeScriptId 脚本id
     * @param type           事件类型
     * @param map            相关参数
     * @return 是否还继续整个构建流程
     */
    private String noticeScript(String noticeScriptId, String type, Map map) {
        ScriptModel scriptModel = scriptServer.getByKey(noticeScriptId);
        if (scriptModel == null) {
            logRecorder.systemWarning("事件脚本不存在:{} {}", type, noticeScriptId);
            return null;
        }
        // 判断是否包含需要执行的事件
        if (!StrUtil.containsAnyIgnoreCase(scriptModel.getDescription(), type, "all")) {
            log.warn("忽略执行事件脚本 {} {} {}", type, scriptModel.getName(), noticeScriptId);
            return null;
        }
        logRecorder.system("开始执行事件脚本: {}", type);
        // 环境变量
        Map environment = taskData.environmentMapBuilder.environment(map);
        ScriptExecuteLogModel logModel = scriptExecuteLogServer.create(scriptModel, 3, this.taskData.buildInfoModel.getWorkspaceId());
        File logFile = scriptModel.logFile(logModel.getId());
        File scriptFile = null;
        LogRecorder scriptLog = LogRecorder.builder().file(logFile).build();
        final String[] lastMsg = new String[1];
        try {
            // 创建执行器
            scriptFile = scriptModel.scriptFile();
            scriptExecuteLogServer.updateStatus(logModel.getId(), CommandExecLogModel.Status.ING);
            int waitFor = JpomApplication.getInstance().execScript(scriptModel.getContext(), file -> {
                try {
                    // 输出环境变量
                    taskData.environmentMapBuilder.eachStr(s -> {
                        logRecorder.system(s);
                        scriptLog.info(s);
                    }, map);
                    //
                    return CommandUtil.execWaitFor(file, null, environment, null, (s, process) -> {
                        logRecorder.info(s);
                        scriptLog.info(s);
                        lastMsg[0] = s;
                    });
                } catch (IOException | InterruptedException e) {
                    scriptExecuteLogServer.updateStatus(logModel.getId(), CommandExecLogModel.Status.ERROR);
                    throw Lombok.sneakyThrow(e);
                }
            });
            logRecorder.system("执行 {} 类型脚本的退出码是:{}", type, waitFor);
            scriptExecuteLogServer.updateStatus(logModel.getId(), CommandExecLogModel.Status.DONE, waitFor);
            // 判断是否为严格执行
            if (buildExtraModule.strictlyEnforce() && waitFor != 0) {
                //logRecorder.systemError("严格执行模式,事件脚本返回状态码异常");
                return "严格执行模式,事件脚本返回状态码异常," + waitFor;
            }
            if (StrUtil.startWithIgnoreCase(lastMsg[0], "interrupt " + type)) {
                return "事件脚本中断:" + lastMsg[0];
            }
            return null;
        } finally {
            try {
                FileUtil.del(scriptFile);
            } catch (Exception ignored) {
            }
        }
    }

    /**
     * 取消构建
     *
     * @param id id
     * @return bool
     */
    public static boolean cancelTaskById(String id) {
        return Optional.ofNullable(BuildExecuteManage.BUILD_MANAGE_MAP.get(id)).map(buildExecuteManage1 -> {
            buildExecuteManage1.cancelTask("手动取消任务");
            return true;
        }).orElse(false);
    }

    /**
     * 模糊匹配
     *
     * @param list    待匹配待列表
     * @param pattern 迷糊的表达式
     * @return 匹配到到值
     */
    private static String fuzzyMatch(List list, String pattern) {
        Assert.notEmpty(list, "仓库没有任何分支或者标签");
        if (AntPathUtil.ANT_PATH_MATCHER.isPattern(pattern)) {
            List collect = list.stream().filter(s -> AntPathUtil.ANT_PATH_MATCHER.match(pattern, s)).collect(Collectors.toList());
            return CollUtil.getFirst(collect);
        }
        return pattern;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy