diff --git a/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java b/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java index 97319c5..2913762 100644 --- a/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java +++ b/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java @@ -20,10 +20,14 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Scanner; +import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.beans.factory.InitializingBean; @@ -55,6 +59,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean { private static final String DEFAULT_BRANCH = "master"; private static final long CMD_TIMEOUT_SECONDS = 120; + private static final long SYNC_TOTAL_TIMEOUT_MINUTES = 30; // 整体同步超时 private static final int PARALLEL = 6; // 并发数 private final FileOperationHelper fileOperationHelper; @@ -214,8 +219,11 @@ public class GitSyncPlugin implements Plugin, InitializingBean { final String repoPath = proj.getPath(); final String repoName = extractName(repoPath); final String targetBranch = branch; + final int taskNum = i + 1; // 固定序号,用于定位卡住的项 pool.submit(() -> { + // 开始处理时立即打印,方便定位卡住的项目 + System.out.printf("[%d/%d] 处理中 %s ...%n", taskNum, total, repoName); try { SyncResult result = syncOneRepo(repoPath, targetBranch, baseUrlMap); resultMap.put(repoPath, result); @@ -226,13 +234,13 @@ public class GitSyncPlugin implements Plugin, InitializingBean { case DAMAGED: tag = "损坏"; break; default: tag = "失败"; break; } - System.out.printf("[%d/%d] %s >> %s%n", done, total, repoName, tag); + System.out.printf("[%d/%d] %s >> %s (完成 %d/%d)%n", taskNum, total, repoName, tag, done, total); } catch (Exception e) { SyncResult r = SyncResult.FAILED; r.error = e.getMessage(); resultMap.put(repoPath, r); int done = completed.incrementAndGet(); - System.out.printf("[%d/%d] %s >> 异常: %s%n", done, total, repoName, e.getMessage()); + System.out.printf("[%d/%d] %s >> 异常: %s (完成 %d/%d)%n", taskNum, total, repoName, e.getMessage(), done, total); } finally { latch.countDown(); } @@ -241,7 +249,12 @@ public class GitSyncPlugin implements Plugin, InitializingBean { pool.shutdown(); try { - latch.await(); + boolean allDone = latch.await(SYNC_TOTAL_TIMEOUT_MINUTES, TimeUnit.MINUTES); + if (!allDone) { + pool.shutdownNow(); + System.err.println("\n!!! 同步超时(超过 " + SYNC_TOTAL_TIMEOUT_MINUTES + " 分钟),强制终止剩余任务 !!!"); + // 未完成的任务不再等待,继续输出已有结果 + } } catch (InterruptedException e) { Thread.currentThread().interrupt(); pool.shutdownNow(); @@ -522,49 +535,71 @@ public class GitSyncPlugin implements Plugin, InitializingBean { // ==================== Git 命令执行 ==================== private CmdResult runGit(String workingDir, String command) { - CmdResult result = new CmdResult(); - try { - ProcessBuilder pb = isWindows() - ? new ProcessBuilder("cmd.exe", "/c", command) - : new ProcessBuilder("bash", "-c", command); + // 用 Future 包一层,防止 readLine() 因进程卡死而永久阻塞 + ExecutorService singleExecutor = Executors.newSingleThreadExecutor(); + Future future = singleExecutor.submit(() -> { + CmdResult result = new CmdResult(); + try { + ProcessBuilder pb = isWindows() + ? new ProcessBuilder("cmd.exe", "/c", command) + : new ProcessBuilder("bash", "-c", command); - if (workingDir != null) { - pb.directory(new File(workingDir)); - } - pb.redirectErrorStream(true); - - Process process = pb.start(); - StringBuilder output = new StringBuilder(); - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); + if (workingDir != null) { + pb.directory(new File(workingDir)); } - } + pb.redirectErrorStream(true); - boolean finished = process.waitFor(CMD_TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!finished) { - process.destroyForcibly(); + Process process = pb.start(); + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + boolean finished = process.waitFor(CMD_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + result.ok = false; + result.error = "命令超时(" + CMD_TIMEOUT_SECONDS + "s)"; + return result; + } + + int exitCode = process.exitValue(); + result.ok = (exitCode == 0); + result.output = output.toString().trim(); + if (!result.ok) { + String detail = output.toString().trim(); + result.error = detail.isEmpty() + ? "exit code " + exitCode + : "exit code " + exitCode + ": " + detail; + } + } catch (Exception e) { result.ok = false; - result.error = "命令超时"; - return result; + result.error = e.getMessage(); } + return result; + }); - int exitCode = process.exitValue(); - result.ok = (exitCode == 0); - result.output = output.toString().trim(); - if (!result.ok) { - String detail = output.toString().trim(); - result.error = detail.isEmpty() - ? "exit code " + exitCode - : "exit code " + exitCode + ": " + detail; - } - } catch (Exception e) { + try { + // 整体超时比内部 waitFor 多给 10 秒缓冲 + return future.get(CMD_TIMEOUT_SECONDS + 10, TimeUnit.SECONDS); + } catch (TimeoutException e) { + future.cancel(true); + CmdResult result = new CmdResult(); result.ok = false; - result.error = e.getMessage(); + result.error = "命令超时(" + (CMD_TIMEOUT_SECONDS + 10) + "s,进程被强制终止)"; + return result; + } catch (InterruptedException | ExecutionException | CancellationException e) { + CmdResult result = new CmdResult(); + result.ok = false; + result.error = "执行异常: " + e.getMessage(); + return result; + } finally { + singleExecutor.shutdownNow(); } - return result; } private String getRemoteUrl(String repoPath, Map baseUrlMap) { diff --git a/src/main/resources/config/damaged_repos.cache b/src/main/resources/config/damaged_repos.cache index 5741e3d..ef0aaea 100644 --- a/src/main/resources/config/damaged_repos.cache +++ b/src/main/resources/config/damaged_repos.cache @@ -1,11 +1 @@ branch:master -ft-loan-core|D:/bestpay-srdcloud/ft-loan-core|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core -cf-asset-core|D:/bestpay-srdcloud/cf-asset-core|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core -cf-boss|D:/bestpay-srdcloud/cf-boss|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core -cf-credit-prod-batch|D:/bestpay-srdcloud/cf-credit-prod-batch|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core -cf-instalment-mix|D:/bestpay-srdcloud/cf-instalment-mix|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core -cf-marketing-manage|D:/bestpay-srdcloud/cf-marketing-manage|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core -cf-risk-control-core|D:/bestpay-srdcloud/cf-risk-control-core|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core -cf-risktool|D:/bestpay-srdcloud/cf-risktool|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core -cf-thirdpartybiz-prod|D:/bestpay-srdcloud/cf-thirdpartybiz-prod|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core -cf-trade-core|D:/bestpay-srdcloud/cf-trade-core|master|checkout 失败: exit code 128|https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/cf/cf-trade-core diff --git a/src/main/resources/config/代码仓库项目路径.txt b/src/main/resources/config/代码仓库项目路径.txt index cb7826a..66baacc 100644 --- a/src/main/resources/config/代码仓库项目路径.txt +++ b/src/main/resources/config/代码仓库项目路径.txt @@ -15,7 +15,6 @@ D:/bestpay-srdcloud/ft-risk-control-core=https://srd17321104074@code.srdcloud.cn D:/bestpay-srdcloud/ft-settlement-core=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/ft-wechatstage-prod=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-agent-admin=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ -D:/bestpay-srdcloud/cf-asset-core=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-batch-creditpayment=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-batch-creditpayment-plus=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-blockchain-prod=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ @@ -32,7 +31,6 @@ D:/bestpay-srdcloud/cf-credit-bizfront=https://srd17321104074@code.srdcloud.cn/b D:/bestpay-srdcloud/cf-credit-bizfront-plus=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-credit-front=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-credit-prod=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ -D:/bestpay-srdcloud/cf-credit-prod-batch=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-credit-prod-manage=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-creditstagescore=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-credituse-core=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ @@ -44,7 +42,6 @@ D:/bestpay-srdcloud/cf-grant-core=https://srd17321104074@code.srdcloud.cn/bestpa D:/bestpay-srdcloud/cf-info-collection=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-instalment-admin=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-instalment-batch=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ -D:/bestpay-srdcloud/cf-instalment-mix=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-invoice-core=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-lending-management-core=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-loancapital-core=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ @@ -55,7 +52,6 @@ D:/bestpay-srdcloud/cf-organmng-core=https://srd17321104074@code.srdcloud.cn/bes D:/bestpay-srdcloud/cf-precredit-core=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-productcfg-core-plus=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-protocol-core=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ -D:/bestpay-srdcloud/cf-risk-control-core=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-risktool=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-scheduler=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/ D:/bestpay-srdcloud/cf-search=https://srd17321104074@code.srdcloud.cn/bestpay-cf/cf/