修复gradle8使用Transform第一个构建中断第二次构建失败的问题:java.io.IOException: Unable to delete directory xxxx\build

问题描述

使用了gradle编译插件,编译插件使用的是Transform处理字节码,如果第一次ctrl+c中断或者其它原因中断,下次再次构建会出现build文件夹清理不了的问题

Execution failed for task ':my-module:my-submodule:clean'.
> java.io.IOException: Unable to delete directory 'C:\Dev\myproject\my-module\my-submodule\build'
    Failed to delete some children. This might happen because a process has files open or has its working directory set in the target directory.
    - C:\Dev\myproject\my-module\my-submodule\build\libs\my-module.my-submodule.jar
    - C:\Dev\myproject\my-module\my-submodule\build\libs

相似问题

https://github.com/gradle/gradle/issues/26912
gradle的github项目上有人反馈这个问题,但不是gradle的问题,是编译插件的问题。

复现问题

  • 使用gradle命令构建app
gradle assembledebug
  • 进入transfrom的taskAction方法后,终止命令,注意不要强制杀死进程
  • 马上执行下一个构建任务
gradle clean
gradle assembledebug

编写了一段java程序,用于模拟复现和修复问题:



import java.io.BufferedReader;
import java.io.File;
import java.lang.Thread;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 运行前,需要确保系统配置了正确的gradle版本、java版本
 * 监控Gradle命令执行并在检测到特定字符串后终止并重试
 * 注意::不能放到unitTest或者androidTest文件夹里面作为单元测试,会有gradle的影响
 * 需要作为java程序单独使用java命令执行mian函数
 * 1.编译:javac GradleTaskKillAndRebuildTest.java
 * 2.运行:java GradleTaskKillAndRebuildTest
 */
public class GradleTaskKillAndRebuildTest {
    private static final String GRADLE_COMMAND = "gradle assembledebug";
  //查找日志,适当时机模拟退出
    private static final String TARGET_STRING = "[Plugin]";
    private static final String TARGET_STRING2 = "transform started";

    public static void main(String[] args) throws Exception {
        File path = new File("");
        String exitDaemonCommand = "cmd /c gradle --stop";
        String cleanCommand = "cmd /c cd " + path.getAbsolutePath() + " && gradle clean";
        String buildCommand = "cmd /c cd " + path.getAbsolutePath() + " && " + GRADLE_COMMAND;

        // 初始,先退出之前的后台进程
        executeCommand(true, exitDaemonCommand);

        // 删除build目录
        File buildDir = new File("./app/build");
        System.out.println(buildDir.getAbsolutePath());
        deleteBuildDirectory(buildDir);

        // 第一次构建:清理+构建中断
        executeCommand(true, cleanCommand);
        boolean foundAndExit = execAndFoundAndKill(buildCommand);
        if (!foundAndExit) {
            System.err.println("执行并查找线程启动标识失败");
            System.exit(2);
            return;
        }

        //等前面的执行完成
        Thread.sleep(5000L);

        // 第二次构建:清理+正常构建
        Pair<Boolean, String> cleanResult = executeCommand(true, cleanCommand);
        if (!cleanResult.first) {
            System.err.println("第二次构建清理失败");
            System.exit(41);
            return;
        }
        Pair<Boolean, String> checkResult = executeCommand(true, buildCommand);
        System.out.println("测试结果:" + checkResult.first);
        System.exit(checkResult.first ? 0 : 1);
    }

    /**
     * 执行命令并返回输出结果
     */
    private static Pair<Boolean, String> executeCommand(boolean print, String command) {
        Process process;
        try {
            process = Runtime.getRuntime().exec(command);
        } catch (IOException e) {
            System.err.println("执行命令失败: " + e.getMessage());
            e.printStackTrace();
            return new Pair<>(false, "");
        }

        ExecutorService executor = Executors.newSingleThreadExecutor();
        StringBuilder output = new StringBuilder();

        Runnable task1 = () -> {
            // 读取标准输出
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line = reader.readLine();
                while (line != null) {
                    if (print) {
                        System.out.println(line);
                    }
                    output.append(line).append(System.lineSeparator());
                    line = reader.readLine();
                }
            } catch (IOException e) {
                System.err.println("读取标准输出时出错: " + e.getMessage());
            }
        };

        Runnable task2 = () -> {
            // 读取错误输出
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line = reader.readLine();
                while (line != null) {
                    if (print) {
                        System.out.println(line); // 打印输出行
                    }
                    output.append(line).append(System.lineSeparator());
                    line = reader.readLine();
                }
            } catch (IOException e) {
                System.err.println("读取错误输出时出错: " + e.getMessage());
            }
        };

        executor.submit(task1);
        executor.submit(task2);
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.HOURS); // 等待足够长的时间让任务完成
            int ret = process.waitFor();
            return new Pair<>(ret == 0, output.toString());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("等待进程完成时被中断: " + e.getMessage());
            return new Pair<>(false, output.toString());
        }
    }

    /**
     * 执行gradle命令,找到特点字符,并终止,返回true则执行正常
     */
    private static boolean execAndFoundAndKill(String gradleCommand) {
        System.out.println("执行Gradle命令  " + gradleCommand);
        try {
            Process process = Runtime.getRuntime().exec(gradleCommand);
            ExecutorService executor = Executors.newSingleThreadExecutor();

            // 启动线程读取输出并监控目标字符串
            boolean targetFound = monitorProcessOutput(process, executor);

            if (targetFound) {
                System.out.println("发现目标字符串: " + TARGET_STRING + ",正在终止进程...");
                process.destroy();
                if (process.waitFor(5, TimeUnit.SECONDS)) {
                    System.out.println("进程已成功终止");
                } else {
                    System.out.println("强制终止进程");
                    process.destroyForcibly();
                }
                return true;
            } else {
                // 如果没有找到目标字符串,等待进程自然结束
                int exitCode = process.waitFor();
                System.out.println("进程正常结束,退出码: " + exitCode);
            }
        } catch (Exception e) {
            System.err.println("执行命令时出错: " + e.getMessage());
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 监控进程输出,查找目标字符串
     * @param process 要监控的进程
     * @param executor 线程池用于异步读取输出
     * @return 是否找到目标字符串
     * @throws InterruptedException 线程中断异常
     */
    private static boolean monitorProcessOutput(Process process, ExecutorService executor) throws InterruptedException {
        AtomicBoolean targetFound = new AtomicBoolean(false);
        Object lock = new Object();
        boolean shouldContinueMonitoring = true;

        // 读取标准输出
        executor.submit(() -> readStream(process.getInputStream(), targetFound, lock));

        // 读取错误输出
        executor.submit(() -> readStream(process.getErrorStream(), targetFound, lock));

        // 定期检查是否找到目标字符串
        while (shouldContinueMonitoring && !targetFound.get()) {
            synchronized (lock) {
                if (targetFound.get()) {
                    shouldContinueMonitoring = false;
                } else {
                    try {
                        lock.wait(100); // 每100ms检查一次
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        shouldContinueMonitoring = false;
                    }
                }
            }

            // 检查进程是否已经结束
            if (shouldContinueMonitoring) {
                try {
                    if (process.waitFor(10, TimeUnit.MILLISECONDS)) {
                        shouldContinueMonitoring = false; // 进程已结束
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    shouldContinueMonitoring = false;
                }
            }
        }

        executor.shutdown();
        executor.awaitTermination(2, TimeUnit.SECONDS);

        return targetFound.get();
    }

    /**
     * 读取输入流并检查目标字符串
     */
    private static void readStream(
            InputStream inputStream,
            AtomicBoolean targetFound,
            Object lock
    ) {
        boolean found = false;
        int foundNextStep = 0;
        try {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
                String line = reader.readLine();
                while (line != null && !targetFound.get()) {
                    System.out.println(line); // 打印输出行
                    if (line.contains(TARGET_STRING) && line.contains(TARGET_STRING2)) {
                        found = true;
                    }
                    if (found) {
                        foundNextStep++;
                    }
                    if (foundNextStep >= 4) {
                        synchronized (lock) {
                            targetFound.set(true);
                            lock.notifyAll();
                        }
                    }
                    line = reader.readLine();
                }
            }
        } catch (Exception e) {
            System.err.println("读取流时出错: " + e.getMessage());
        }
    }

    /**
     * 删除当前目录下的build目录
     */
    private static void deleteBuildDirectory(File buildDir) {
        if (buildDir.exists() && buildDir.isDirectory()) {
            System.out.println("正在删除build目录...");
            if (deleteDirectoryRecursively(buildDir)) {
                System.out.println("build目录删除成功");
            } else {
                System.out.println("build目录删除失败");
            }
        } else {
            System.out.println("build目录不存在,无需删除");
        }
    }

    /**
     * 递归删除目录及其内容
     */
    private static boolean deleteDirectoryRecursively(File dir) {
        // 先删除所有子文件和子目录
        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    deleteDirectoryRecursively(file);
                } else {
                    // System.out.println("删除文件:" + file.getAbsolutePath());
                    file.delete();
                }
            }
        }
        // 删除空目录
        return dir.delete();
    }

    /**
     * 简单的Pair类,用于同时返回两个值
     */
    private static class Pair<T1, T2> {
        public final T1 first;
        public final T2 second;

        public Pair(T1 first, T2 second) {
            this.first = first;
            this.second = second;
        }
    }
}

产生原因

终止gradle任务,不一定是强制杀死jvm进程,而是等待jvm全部用户线程(isDaemon = false)结束(如:jenkins终止gradle打包任务时)
由于transform的操作属于用户线程,所以终止任务后,仍然需要等待transform执行完成。
执行clean时,使用

gradle --status

可以看到,一共有两个进程在busy工作状态,一个是clean任务,第二个是已发出停止但未真正停止的进程
image

未停止的进程还在占用build文件夹、transform输出的classes文件,所以不能被clean任务删除

验证

打印日志到文件,注意加上pid

  1. 监听gradle构建事件buildFinished,将构建结束事件打印日志到文件
  2. transform开始事件打印日志到文件
  3. 监听gradle构建事件projectsLoaded,将构建解析完成事件打印到日志文件

经过上述“复现问题”的步骤,你会发现下面类似的日志:

  1. gradle assembledebug(pid=1) => projectsLoaded
  2. gradle assembledebug (pid=1) => transform started
  3. gradle clean (pid=2) => projectsLoaded
  4. gradle clean (pid=2) => build failure: java.io.IOException: Unable to delete directory xxxx\build
  5. gradle assembledebug(pid=1) => buildFinished

解决方法

完善gradle插件的优雅退出

  • 添加addShutdownHook监听退出事件,回收关闭资源
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            // 这里会被回调
            System.err.println("收到优雅停止信号,开始收尾...");
            // 1. 释放资源、刷日志、关闭连接
            // 2. 记录状态、删除临时文件
            System.err.println("收尾完成,JVM 即将退出");
        }));

  • 将非必要等待的线程,标记为守护线程(非用户线程不会被等待结束)
  Thread thread = new Thread();
  thread.isDaemon = true
  thread .start()

强制杀掉jvm进程

  1. transform开始时,记录当前进程pid,写到一个固定的文件(用于标记构建进行中)
DXGradlePlugin.pidFile = File("./myBuild.pid")
val pid = ProcessHandle.current().pid()
DXGradlePlugin.pidFile.writeText("$pid")
  1. buildFinished事件(构建完成后)删除pid文件(标记构建完成)
    DXGradlePlugin.pidFile.delete()

3.每次启动时(如projectsEvaluated事件配置完成后)检测未完成的pid,等待或强制杀死进程

//gradle配置完成
override fun projectsEvaluated(gradle: Gradle) {
            val startWaitTime = System.currentTimeMillis()
            var hasTryKill = false
            while (true) {
                if (!flagFile.exists()) {
                    //标记文件不存在
                    break
                }
                if (!hasTryKill && System.currentTimeMillis() - startWaitTime >= 10000) {
                    //等10秒后,尝试杀掉进程
                    hasTryKill = true
                    val pid = flagFile.readText()
                   // kill busy pid:$pid"
                    val command = "cmd /c taskkill /PID $pid /T /F"
                    var ret = Runtime.getRuntime().exec(command).waitFor()
                    //标记文件不存在
                    if (ret == 0) {
                        flagFile.delete()
                    } else {
                        val command2 = "kill -9 $pid"
                        ret = Runtime.getRuntime().exec(command2).waitFor()
                    }
                   // "kill busy pid ret:$ret")
                    if (ret == 0) {
                        flagFile.delete()
                        break
                    }
                }
                if (System.currentTimeMillis() - startWaitTime >= 60000) {
                    //等待超时,1分钟
                   //"wait other running take time out")
                    break
                 }
                 // "wait other running take(exit task)")
                  Thread.sleep(1000)
            }
            
        }

修复结果

修改完成后,重新执行:
java GradleTaskKillAndRebuildTest

清理成功:
image

最终测试成功:
image

posted @ 2025-09-25 14:54  georgeyang  阅读(19)  评论(0)    收藏  举报