diff --git a/compiler/rmi/kotlinr/src/org/jetbrains/kotlin/rmi/kotlinr/KotlinCompilerClient.kt b/compiler/rmi/kotlinr/src/org/jetbrains/kotlin/rmi/kotlinr/KotlinCompilerClient.kt index e7b857115c4..d02a8b7017e 100644 --- a/compiler/rmi/kotlinr/src/org/jetbrains/kotlin/rmi/kotlinr/KotlinCompilerClient.kt +++ b/compiler/rmi/kotlinr/src/org/jetbrains/kotlin/rmi/kotlinr/KotlinCompilerClient.kt @@ -17,7 +17,6 @@ package org.jetbrains.kotlin.rmi.kotlinr import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents -import org.jetbrains.kotlin.modules.TargetId import org.jetbrains.kotlin.progress.CompilationCanceledStatus import org.jetbrains.kotlin.rmi.* import java.io.* @@ -150,6 +149,7 @@ public object KotlinCompilerClient { } } + public val COMPILE_DAEMON_CLIENT_OPTIONS_PROPERTY: String = "kotlin.daemon.client.options" data class ClientOptions( public var stop: Boolean = false ) : OptionsGroup { @@ -157,30 +157,34 @@ public object KotlinCompilerClient { get() = listOf(BoolPropMapper(this, ClientOptions::stop)) } + private fun configureClientOptions(opts: ClientOptions): ClientOptions { + System.getProperty(COMPILE_DAEMON_CLIENT_OPTIONS_PROPERTY)?.let { + val unrecognized = it.trimQuotes().split(",").filterExtractProps(opts.mappers, "") + if (unrecognized.any()) + throw IllegalArgumentException( + "Unrecognized client options passed via property $COMPILE_DAEMON_OPTIONS_PROPERTY: " + unrecognized.joinToString(" ") + + "\nSupported options: " + opts.mappers.joinToString(", ", transform = { it.names.first() })) + } + return opts + } + + private fun configureClientOptions(): ClientOptions = configureClientOptions(ClientOptions()) + @JvmStatic public fun main(vararg args: String) { val compilerId = CompilerId() - val daemonOptions = DaemonOptions() - val daemonLaunchingOptions = DaemonJVMOptions() - val clientOptions = ClientOptions() + val daemonOptions = configureDaemonOptions() + val daemonLaunchingOptions = configureDaemonJVMOptions(true) + val clientOptions = configureClientOptions() val filteredArgs = args.asIterable().filterExtractProps(compilerId, daemonOptions, daemonLaunchingOptions, clientOptions, prefix = COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX) if (!clientOptions.stop) { if (compilerId.compilerClasspath.none()) { // attempt to find compiler to use System.err.println("compiler wasn't explicitly specified, attempt to find appropriate jar") - System.getProperty("java.class.path") - ?.split(File.pathSeparator) - ?.map { File(it).parentFile } - ?.distinct() - ?.map { - it?.walk() - ?.firstOrNull { it.name.equals(COMPILER_JAR_NAME, ignoreCase = true) } - } - ?.filterNotNull() - ?.firstOrNull() - ?.let { compilerId.compilerClasspath = listOf(it.absolutePath) } + detectCompilerClasspath() + ?.let { compilerId.compilerClasspath = it } } if (compilerId.compilerClasspath.none()) throw IllegalArgumentException("Cannot find compiler jar") @@ -204,6 +208,10 @@ public object KotlinCompilerClient { daemon.shutdown() println("Daemon shut down successfully") } + filteredArgs.none() -> { + // so far used only in tests + println("Warning: empty arguments list, only daemon check is performed: checkCompilerId() returns ${checkCompilerId(daemon, compilerId)}") + } else -> { println("Executing daemon compilation with args: " + filteredArgs.joinToString(" ")) val outStrm = RemoteOutputStreamServer(System.out) @@ -228,6 +236,19 @@ public object KotlinCompilerClient { } } + public fun detectCompilerClasspath(): List? = + System.getProperty("java.class.path") + ?.split(File.pathSeparator) + ?.map { File(it).parentFile } + ?.distinct() + ?.map { + it?.walk() + ?.firstOrNull { it.name.equals(COMPILER_JAR_NAME, ignoreCase = true) } + } + ?.filterNotNull() + ?.firstOrNull() + ?.let { listOf(it.absolutePath) } + // --- Implementation --------------------------------------- fun DaemonReportingTargets.report(category: DaemonReportCategory, message: String, source: String = "daemon client") { @@ -335,7 +356,7 @@ public object KotlinCompilerClient { } ?: DAEMON_DEFAULT_STARTUP_TIMEOUT_MS if (daemonOptions.runFilesPath.isNotEmpty()) { val succeeded = isEchoRead.tryAcquire(daemonStartupTimeout, TimeUnit.MILLISECONDS) - if (!daemon.isAlive()) + if (!isProcessAlive(daemon)) throw Exception("Daemon terminated unexpectedly") if (!succeeded) throw Exception("Unable to get response from daemon in $daemonStartupTimeout ms") @@ -413,11 +434,12 @@ public data class DaemonReportMessage(public val category: DaemonReportCategory, public class DaemonReportingTargets(public val out: PrintStream? = null, public val messages: MutableCollection? = null) -internal fun Process.isAlive() = +internal fun isProcessAlive(process: Process) = try { - this.exitValue() + process.exitValue() false } catch (e: IllegalThreadStateException) { true } + diff --git a/compiler/tests/org/jetbrains/kotlin/daemon/CompilerDaemonTest.kt b/compiler/tests/org/jetbrains/kotlin/daemon/CompilerDaemonTest.kt index e819307ca78..8d4583f6d60 100644 --- a/compiler/tests/org/jetbrains/kotlin/daemon/CompilerDaemonTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/daemon/CompilerDaemonTest.kt @@ -25,6 +25,10 @@ import org.jetbrains.kotlin.rmi.kotlinr.KotlinCompilerClient import org.jetbrains.kotlin.test.JetTestUtils import java.io.ByteArrayOutputStream import java.io.File +import kotlin.concurrent.thread + + +val TIMEOUT_DAEMON_RUNNER_EXIT_MS = 10000L public class CompilerDaemonTest : KotlinIntegrationTestBase() { @@ -32,6 +36,8 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() { val compilerClassPath = listOf( File(KotlinIntegrationTestBase.getCompilerLib(), "kotlin-compiler.jar")) + val daemonClientClassPath = listOf( File(KotlinIntegrationTestBase.getCompilerLib(), "kotlinr.jar"), + File(KotlinIntegrationTestBase.getCompilerLib(), "kotlin-compiler.jar")) val compilerId by lazy(LazyThreadSafetyMode.NONE) { CompilerId.makeCompilerId(compilerClassPath) } private fun compileOnDaemon(compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, vararg args: String): CompilerResults { @@ -178,6 +184,53 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() { logFile2.assertLogContainsSequence("Shutdown complete") logFile2.delete() } + + + /** Testing that running daemon in the child process doesn't block on s child process.waitFor() + * that may happen on windows if simple processBuilder.start is used due to handles inheritance: + * - process A starts process B using ProcessBuilder and waits for it using process.waitFor() + * - process B starts daemon and exits + * - due to default behavior of CreateProcess on windows, the handles of process B are inherited by the daemon + * (in particular handles of stdin/out/err) and therefore these handles remain open while daemon is running + * - (seems) due to the way how waiting for process is implemented, waitFor() hangs until daemon is killed + * This seems a known problem, e.g. gradle uses a library with native code that prevents io handles inheritance when launching it's daemon + * (the same solution is used in kotlin daemon client - see next commit) + */ + public fun testDaemonExecutionViaIntermediateProcess() { + val clientAliveFile = createTempFile("kotlin-daemon-transitive-run-test", ".run") + val runFilesPath = File(tmpdir, getTestName(true)).absolutePath + val daemonOptions = DaemonOptions(runFilesPath = runFilesPath, clientAliveFlagPath = clientAliveFile.absolutePath) + val args = listOf( + File(File(System.getProperty("java.home"), "bin"), "java").absolutePath, + "-D$COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY", + "-cp", + daemonClientClassPath.joinToString(File.pathSeparator) { it.absolutePath }, + KotlinCompilerClient::class.qualifiedName!!) + + daemonOptions.mappers.flatMap { it.toArgs(COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX) } + + compilerId.mappers.flatMap { it.toArgs(COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX) } + + File(getHelloAppBaseDir(), "hello.kt").absolutePath + try { + KotlinCompilerClient.shutdownCompileService(compilerId, daemonOptions) + var resOutput: String? = null + var resCode: Int? = null + // running intermediate process (daemon command line controller) that executes the daemon + val runnerProcess = ProcessBuilder(args).redirectErrorStream(true).start() + thread { + resOutput = runnerProcess.inputStream.reader().readText() + } + val waitThread = thread { + resCode = runnerProcess.waitFor() + } + waitThread.join(TIMEOUT_DAEMON_RUNNER_EXIT_MS) + + TestCase.assertFalse("process.waitFor() hangs:\n$resOutput", waitThread.isAlive) + TestCase.assertEquals("Compilation failed:\n$resOutput", 0, resCode) + } + finally { + if (clientAliveFile.exists()) + clientAliveFile.delete() + } + } }