diff --git a/cli/BUILD b/cli/BUILD index ee701a58..ec0c7700 100644 --- a/cli/BUILD +++ b/cli/BUILD @@ -120,6 +120,12 @@ kt_jvm_test( runtime_deps = [":cli-test-lib"], ) +kt_jvm_test( + name = "ProcessStdinHangRegressionTest", + test_class = "com.bazel_diff.process.ProcessStdinHangRegressionTest", + runtime_deps = [":cli-test-lib"], +) + kt_jvm_test( name = "E2ETest", timeout = "long", diff --git a/cli/src/main/kotlin/com/bazel_diff/process/Process.kt b/cli/src/main/kotlin/com/bazel_diff/process/Process.kt index 499e1e4b..26c2866f 100644 --- a/cli/src/main/kotlin/com/bazel_diff/process/Process.kt +++ b/cli/src/main/kotlin/com/bazel_diff/process/Process.kt @@ -57,6 +57,12 @@ suspend fun process( } .start() + // Close the subprocess's stdin so reads from it see EOF immediately. + // Without this, subprocesses that read stdin on startup (e.g. aspect CLI's + // interactive first-run path, #256) block forever waiting for input we + // never send, and waitFor() then blocks forever too. + process.outputStream.close() + // Handles async consumptions before the blocking output handling. if (stdout is Redirect.Consume) { process.inputStream.lineFlow(stdout.consumer) diff --git a/cli/src/test/kotlin/com/bazel_diff/process/ProcessStdinHangRegressionTest.kt b/cli/src/test/kotlin/com/bazel_diff/process/ProcessStdinHangRegressionTest.kt new file mode 100644 index 00000000..98358811 --- /dev/null +++ b/cli/src/test/kotlin/com/bazel_diff/process/ProcessStdinHangRegressionTest.kt @@ -0,0 +1,43 @@ +package com.bazel_diff.process + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +/** + * Regression test for https://github.com/Tinder/bazel-diff/issues/256 + * ("bug: bazel-diff freezes when aspect CLI is installed"). + * + * Before the fix, [process] started subprocesses via `ProcessBuilder.start()` + * without redirecting or closing stdin. Java defaults stdin to `Redirect.PIPE`, + * so the subprocess received an open, never-closed stdin pipe. Any subprocess + * that reads from stdin (the aspect CLI's interactive first-run path, in #256) + * blocked indefinitely on `read()`, and `process.waitFor()` then blocked too — + * matching the `FUTEX_WAIT` strace the original reporter captured. + * + * The fix closes `process.outputStream` immediately after `start()` so the + * subprocess sees EOF on stdin and exits. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class ProcessStdinHangRegressionTest { + + @Test + fun `process does not hang when subprocess reads from stdin`() = runBlocking { + // `cat` with no args reads from stdin until EOF. Before the fix this hung + // forever; after the fix `cat` sees EOF immediately and exits 0. + val result = + withTimeoutOrNull(timeMillis = 5_000) { + process( + "cat", + stdout = Redirect.CAPTURE, + stderr = Redirect.SILENT, + ) + } + + assertNotNull(result, "process() deadlocked — subprocess stdin was not closed (regression of #256).") + assertEquals(0, result.resultCode) + } +}