.kotlin.kotlin-compiler-client-embeddable.2.1.0.source-code.KotlinCompilerClient.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlin-compiler-client-embeddable Show documentation
Show all versions of kotlin-compiler-client-embeddable Show documentation
Kotlin compiler client embeddable
/*
* Copyright 2010-2024 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.daemon.client
import org.jetbrains.kotlin.cli.common.CompilerSystemProperties
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.daemon.common.*
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
import java.io.File
import java.io.PrintStream
import java.net.SocketException
import java.nio.file.Files
import java.rmi.ConnectException
import java.rmi.ConnectIOException
import java.rmi.UnmarshalException
import java.rmi.server.UnicastRemoteObject
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
class CompilationServices(
val incrementalCompilationComponents: IncrementalCompilationComponents? = null,
val lookupTracker: LookupTracker? = null,
val compilationCanceledStatus: CompilationCanceledStatus? = null,
)
data class CompileServiceSession(val compileService: CompileService, val sessionId: Int)
object KotlinCompilerClient {
private const val DAEMON_DEFAULT_STARTUP_TIMEOUT_MS = 10000L
/**
* Defines the number of attempts to find a daemon and connect to it.
* Effectively, it also controls the number of daemon startup attempts as [DAEMON_CONNECT_CYCLE_ATTEMPTS] minus 1.
*/
private const val DAEMON_CONNECT_CYCLE_ATTEMPTS = 4
val verboseReporting = CompilerSystemProperties.COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY.value != null
fun getOrCreateClientFlagFile(daemonOptions: DaemonOptions): File =
// for jps property is passed from IDEA to JPS in KotlinBuildProcessParametersProvider
CompilerSystemProperties.COMPILE_DAEMON_CLIENT_ALIVE_PATH_PROPERTY.value
?.let(String::trimQuotes)
?.takeUnless(String::isBlank)
?.let(::File)
?.takeIf(File::exists)
?: makeAutodeletingFlagFile(baseDir = File(daemonOptions.runFilesPathOrDefault))
fun connectToCompileService(
compilerId: CompilerId,
daemonJVMOptions: DaemonJVMOptions,
daemonOptions: DaemonOptions,
reportingTargets: DaemonReportingTargets,
autostart: Boolean = true,
@Suppress("UNUSED_PARAMETER") checkId: Boolean = true,
): CompileService? {
val flagFile = getOrCreateClientFlagFile(daemonOptions)
return connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, reportingTargets, autostart)
}
fun connectToCompileService(
compilerId: CompilerId,
clientAliveFlagFile: File,
daemonJVMOptions: DaemonJVMOptions,
daemonOptions: DaemonOptions,
reportingTargets: DaemonReportingTargets,
autostart: Boolean = true,
): CompileService? =
connectAndLease(
compilerId,
clientAliveFlagFile,
daemonJVMOptions,
daemonOptions,
reportingTargets,
autostart,
leaseSession = false,
sessionAliveFlagFile = null
)?.compileService
fun connectAndLease(
compilerId: CompilerId,
clientAliveFlagFile: File,
daemonJVMOptions: DaemonJVMOptions,
daemonOptions: DaemonOptions,
reportingTargets: DaemonReportingTargets,
autostart: Boolean,
leaseSession: Boolean,
sessionAliveFlagFile: File? = null,
): CompileServiceSession? {
val ignoredDaemonSessionFiles = mutableSetOf()
var daemonStartupAttemptsCount = 0
val gcAutoConfiguration = GcAutoConfiguration()
val initiatorInfo = InitiatorInformation(clientAliveFlagFile)
return connectLoop(reportingTargets, autostart) { isLastAttempt ->
fun CompileService.tryToLeaseSession(): CompileServiceSession? {
// the newJVMOptions could be checked here for additional parameters, if needed
registerClient(clientAliveFlagFile.absolutePath)
reportingTargets.report(DaemonReportCategory.DEBUG, "connected to the daemon")
if (!leaseSession) return CompileServiceSession(this, CompileService.NO_SESSION)
return when (val leaseSessionResult = leaseCompileSession(sessionAliveFlagFile?.absolutePath)) {
is CompileService.CallResult.Dying -> {
reportingTargets.report(DaemonReportCategory.DEBUG, "the daemon is already dying, skipping it")
null
}
is CompileService.CallResult.Good -> {
val sessionId = leaseSessionResult.get()
reportingTargets.report(DaemonReportCategory.DEBUG, "successfully leased a compile session (id = $sessionId)")
CompileServiceSession(this, sessionId)
}
else -> {
reportingTargets.report(DaemonReportCategory.DEBUG, "got an expected result on attempt to lease a compile session")
// the call to get() below shall lead to an exception throwing
// if it does happen, it indicates real problems,
// so no special handling is required, and it's ok to fail-fast
CompileServiceSession(this, leaseSessionResult.get())
}
}
}
ensureServerHostnameIsSetUp()
val result = tryFindSuitableDaemonOrNewOpts(
File(daemonOptions.runFilesPath),
compilerId,
daemonJVMOptions,
ignoredDaemonSessionFiles,
) { cat, msg -> reportingTargets.report(cat, msg) }
when (result) {
is DaemonSearchResult.Found -> result.compileService.tryToLeaseSession().also {
// the null value here means that the daemon is already dying,
// so we shall query other daemons or start a new one
if (it == null) ignoredDaemonSessionFiles.add(result.runFileMarker)
}
is DaemonSearchResult.NotFound -> {
if (!isLastAttempt && autostart) {
reportingTargets.report(DaemonReportCategory.DEBUG, "trying to start a new compiler daemon")
if (
startDaemon(
compilerId,
result.requiredJvmOptions,
daemonOptions,
reportingTargets,
daemonStartupAttemptsCount++,
gcAutoConfiguration,
initiatorInfo,
)
) {
reportingTargets.report(DaemonReportCategory.DEBUG, "new compiler daemon started, trying to find it")
}
}
null
}
}
}
}
fun shutdownCompileService(compilerId: CompilerId, daemonOptions: DaemonOptions): Unit {
connectToCompileService(
compilerId,
DaemonJVMOptions(),
daemonOptions,
DaemonReportingTargets(out = System.out),
autostart = false,
checkId = false
)
?.shutdown()
}
fun shutdownCompileService(compilerId: CompilerId): Unit {
shutdownCompileService(compilerId, DaemonOptions())
}
fun leaseCompileSession(compilerService: CompileService, aliveFlagPath: String?): Int =
compilerService.leaseCompileSession(aliveFlagPath).get()
fun releaseCompileSession(compilerService: CompileService, sessionId: Int): Unit {
compilerService.releaseCompileSession(sessionId)
}
fun compile(
compilerService: CompileService,
sessionId: Int,
targetPlatform: CompileService.TargetPlatform,
args: Array,
messageCollector: MessageCollector,
outputsCollector: ((File, List) -> Unit)? = null,
compilerMode: CompilerMode = CompilerMode.NON_INCREMENTAL_COMPILER,
reportSeverity: ReportSeverity = ReportSeverity.INFO,
port: Int = SOCKET_ANY_FREE_PORT,
profiler: Profiler = DummyProfiler(),
): Int = profiler.withMeasure(this) {
val services = BasicCompilerServicesWithResultsFacadeServer(messageCollector, outputsCollector, port)
compilerService.compile(
sessionId,
args,
CompilationOptions(
compilerMode,
targetPlatform,
arrayOf(
ReportCategory.COMPILER_MESSAGE.code,
ReportCategory.DAEMON_MESSAGE.code,
ReportCategory.EXCEPTION.code,
ReportCategory.OUTPUT_MESSAGE.code
),
reportSeverity.code,
emptyArray()
),
services,
null
).get()
}
data class ClientOptions(
var stop: Boolean = false,
) : OptionsGroup {
override val mappers: List>
get() = listOf(BoolPropMapper(this, ClientOptions::stop))
}
private fun configureClientOptions(opts: ClientOptions): ClientOptions {
CompilerSystemProperties.COMPILE_DAEMON_CLIENT_OPTIONS_PROPERTY.value?.let {
val unrecognized = it.trimQuotes().split(",").filterExtractProps(opts.mappers, "")
if (unrecognized.any())
throw IllegalArgumentException(
"Unrecognized client options passed via property ${CompilerSystemProperties.COMPILE_DAEMON_CLIENT_OPTIONS_PROPERTY.property}: " + unrecognized.joinToString(
" "
) +
"\nSupported options: " + opts.mappers.joinToString(", ", transform = { it.names.first() })
)
}
return opts
}
private fun configureClientOptions(): ClientOptions = configureClientOptions(ClientOptions())
@JvmStatic
fun main(vararg args: String) {
val compilerId = CompilerId()
val daemonOptions = configureDaemonOptions()
val daemonLaunchingOptions =
configureDaemonJVMOptions(inheritMemoryLimits = true, inheritOtherJvmOptions = false, inheritAdditionalProperties = 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")
detectCompilerClasspath()
?.let { compilerId.compilerClasspath = it }
}
if (compilerId.compilerClasspath.none())
throw IllegalArgumentException("Cannot find compiler jar")
else
println("desired compiler classpath: " + compilerId.compilerClasspath.joinToString(File.pathSeparator))
}
val daemon = connectToCompileService(
compilerId,
daemonLaunchingOptions,
daemonOptions,
DaemonReportingTargets(out = System.out),
autostart = !clientOptions.stop,
checkId = !clientOptions.stop
)
if (daemon == null) {
if (clientOptions.stop) {
System.err.println("No daemon found to shut down")
} else throw Exception("Unable to connect to daemon")
} else when {
clientOptions.stop -> {
println("Shutdown the daemon")
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 ${
daemon.checkCompilerId(
compilerId
)
}"
)
}
else -> {
println("Executing daemon compilation with args: " + filteredArgs.joinToString(" "))
val messageCollector = object : MessageCollector {
var hasErrors = false
override fun clear() {}
override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageSourceLocation?) {
if (severity.isError) {
hasErrors = true
}
println("${severity.name}\t${location?.path ?: ""}:${location?.line ?: ""} \t$message")
}
override fun hasErrors() = hasErrors
}
val outputsCollector = { x: File, y: List -> println("$x $y") }
val servicesFacade = BasicCompilerServicesWithResultsFacadeServer(messageCollector, outputsCollector)
try {
val memBefore = daemon.getUsedMemory().get() / 1024
val startTime = System.nanoTime()
val compilationOptions = CompilationOptions(
CompilerMode.NON_INCREMENTAL_COMPILER,
CompileService.TargetPlatform.JVM,
arrayOf(
ReportCategory.COMPILER_MESSAGE.code,
ReportCategory.DAEMON_MESSAGE.code,
ReportCategory.EXCEPTION.code,
ReportCategory.OUTPUT_MESSAGE.code
),
ReportSeverity.INFO.code,
emptyArray()
)
val res = daemon.compile(
CompileService.NO_SESSION,
filteredArgs.toList().toTypedArray(),
compilationOptions,
servicesFacade,
null
)
val endTime = System.nanoTime()
println("Compilation ${if (res.isGood) "succeeded" else "failed"}, result code: ${res.get()}")
val memAfter = daemon.getUsedMemory().get() / 1024
println("Compilation time: " + TimeUnit.NANOSECONDS.toMillis(endTime - startTime) + " ms")
println("Used memory $memAfter (${"%+d".format(memAfter - memBefore)} kb)")
} finally {
// forcing RMI to unregister all objects and stop
UnicastRemoteObject.unexportObject(servicesFacade, true)
}
}
}
}
fun detectCompilerClasspath(): List? =
CompilerSystemProperties.JAVA_CLASS_PATH.value
?.split(File.pathSeparator)
?.map { File(it).parentFile }
?.distinct()
?.mapNotNull {
it?.walk()
?.firstOrNull { it.name.equals(COMPILER_JAR_NAME, ignoreCase = true) }
}
?.firstOrNull()
?.let { listOf(it.absolutePath) }
// --- Implementation ---------------------------------------
private inline fun connectLoop(
reportingTargets: DaemonReportingTargets, autostart: Boolean, body: (Boolean) -> R?,
): R? = synchronized(this) {
try {
var attempts = 1
while (true) {
val (res, err) = try {
body(attempts >= DAEMON_CONNECT_CYCLE_ATTEMPTS) to null
} catch (e: SocketException) {
null to e
} catch (e: ConnectException) {
null to e
} catch (e: ConnectIOException) {
null to e
} catch (e: UnmarshalException) {
null to e
} catch (e: RuntimeException) {
null to e
}
if (res != null) return res
val errorDetails = err?.let { ", error: ${it.stackTraceToString()}" } ?: ""
if (attempts >= DAEMON_CONNECT_CYCLE_ATTEMPTS || !autostart) {
reportingTargets.report(
DaemonReportCategory.EXCEPTION,
"Failed connecting to the daemon in $attempts retries$errorDetails"
)
} else {
reportingTargets.report(DaemonReportCategory.INFO, "#$attempts retrying connecting to the daemon $errorDetails")
}
if (++attempts > DAEMON_CONNECT_CYCLE_ATTEMPTS || !autostart) {
return null
}
}
} catch (e: Throwable) {
reportingTargets.report(DaemonReportCategory.EXCEPTION, e.toString())
}
return null
}
private sealed interface DaemonSearchResult {
class Found(
val compileService: CompileService,
val runFileMarker: File,
) : DaemonSearchResult
class NotFound(
val requiredJvmOptions: DaemonJVMOptions,
) : DaemonSearchResult
}
private fun tryFindSuitableDaemonOrNewOpts(
registryDir: File,
compilerId: CompilerId,
daemonJVMOptions: DaemonJVMOptions,
ignoredDaemonSessionFiles: Set,
report: (DaemonReportCategory, String) -> Unit,
): DaemonSearchResult {
registryDir.mkdirs()
val timestampMarker = Files.createTempFile(registryDir.toPath(), "kotlin-daemon-client-tsmarker", null).toFile()
val aliveWithMetadata = try {
walkDaemons(registryDir, compilerId, timestampMarker, report = report, filter = { file, _ -> file !in ignoredDaemonSessionFiles }).toList()
} finally {
timestampMarker.delete()
}
val comparator =
compareBy(DaemonJVMOptionsMemoryComparator()) { it.jvmOptions }
.thenBy(FileAgeComparator()) { it.runFile }
val optsCopy = daemonJVMOptions.copy()
// if required options fit into fattest running daemon - return the daemon and required options with memory params set to actual ones in the daemon
return aliveWithMetadata.maxWithOrNull(comparator)?.takeIf { daemonJVMOptions memorywiseFitsInto it.jvmOptions }?.let {
DaemonSearchResult.Found(it.daemon, it.runFile)
}
// else combine all options from running daemon to get fattest option for a new daemon to run
?: DaemonSearchResult.NotFound(aliveWithMetadata.fold(optsCopy) { opts, d -> opts.updateMemoryUpperBounds(d.jvmOptions) })
}
internal data class GcAutoConfiguration(
var shouldAutoConfigureGc: Boolean = true,
val preferredGc: String = "Parallel"
)
private fun getEnvironmentVariablesForTests(reportingTargets: DaemonReportingTargets): Map {
val systemPropertyValue = CompilerSystemProperties.COMPILE_DAEMON_ENVIRONMENT_VARIABLES_FOR_TESTS.value ?: return emptyMap()
return runCatching {
reportingTargets.report(
DaemonReportCategory.EXCEPTION,
"${CompilerSystemProperties.COMPILE_DAEMON_ENVIRONMENT_VARIABLES_FOR_TESTS.property} should be used only for testing!"
)
systemPropertyValue
.split(";")
.map { it.split("=") }
.associate { it[0] to it[1] }
}.getOrNull() ?: emptyMap()
}
private const val JAVA_TOOL_OPTIONS_ENV_VARIABLE = "JAVA_TOOL_OPTIONS"
/**
* Retrieves implicitly passed JVM arguments using the [JAVA_TOOL_OPTIONS_ENV_VARIABLE] environment variable.
* Even though there are some other vendor-specific environment variables available, we intentionally support
* only the one included in the JVMTI specification: https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html#tooloptions
*
* The specification mentions that the environment variables may be disabled or not be supported.
* In that case the worst thing we may face, we won't configure the [GcAutoConfiguration.preferredGc] GC.
* That sounds acceptable.
*/
private fun getImplicitJvmArguments(environmentVariablesForTests: Map) : List {
val javaToolOptions = environmentVariablesForTests[JAVA_TOOL_OPTIONS_ENV_VARIABLE]
?: System.getenv(JAVA_TOOL_OPTIONS_ENV_VARIABLE)
?: return emptyList()
return javaToolOptions.split(" ")
}
private fun startDaemon(
compilerId: CompilerId,
daemonJVMOptions: DaemonJVMOptions,
daemonOptions: DaemonOptions,
reportingTargets: DaemonReportingTargets,
startupAttempt: Int,
gcAutoConfiguration: GcAutoConfiguration,
initiatorInfo: InitiatorInformation,
): Boolean {
val javaExecutable = File(File(CompilerSystemProperties.JAVA_HOME.safeValue, "bin"), "java")
val serverHostname = CompilerSystemProperties.JAVA_RMI_SERVER_HOSTNAME.value
?: error("${CompilerSystemProperties.JAVA_RMI_SERVER_HOSTNAME.property} is not set!")
val platformSpecificOptions = listOf(
// hide daemon window
"-Djava.awt.headless=true",
"-D${CompilerSystemProperties.JAVA_RMI_SERVER_HOSTNAME.property}=$serverHostname"
)
val javaVersion = CompilerSystemProperties.JAVA_VERSION.value?.toIntOrNull()
val javaIllegalAccessWorkaround =
if (javaVersion != null && javaVersion >= 16)
listOf("--add-exports", "java.base/sun.nio.ch=ALL-UNNAMED")
else emptyList()
val environmentVariablesForTests = getEnvironmentVariablesForTests(reportingTargets)
val jvmArguments = daemonJVMOptions.mappers.flatMap { it.toArgs("-") }
if (
(jvmArguments + getImplicitJvmArguments(environmentVariablesForTests))
.any { it == "-XX:-Use${gcAutoConfiguration.preferredGc}GC" || (it.startsWith("-XX:+Use") && it.endsWith("GC")) }
) {
// enable the preferred gc only if it's not explicitly disabled and no other GC is selected
gcAutoConfiguration.shouldAutoConfigureGc = false
}
val additionalOptimizationOptions = listOfNotNull(
"-XX:+UseCodeCacheFlushing",
if (gcAutoConfiguration.shouldAutoConfigureGc) "-XX:+Use${gcAutoConfiguration.preferredGc}GC" else null,
)
// TODO: KT-72161. Investigate IDEA's JSR223 Kotlin Script integration. Possibly transform this into regular arguments
val initiatorInfoAsSystemProperties = listOfNotNull(
initiatorInfo.clientMarkerFile?.absolutePath?.let {
"-D${CompilerSystemProperties.COMPILE_DAEMON_INITIATOR_MARKER_FILE.property}=$it"
}
)
val args = listOf(
javaExecutable.absolutePath, "-cp", compilerId.compilerClasspath.joinToString(File.pathSeparator)
) +
platformSpecificOptions +
jvmArguments +
additionalOptimizationOptions +
initiatorInfoAsSystemProperties +
javaIllegalAccessWorkaround +
COMPILER_DAEMON_CLASS_FQN +
daemonOptions.mappers.flatMap { it.toArgs(COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX) } +
compilerId.mappers.flatMap { it.toArgs(COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX) }
reportingTargets.report(DaemonReportCategory.INFO, "starting the daemon as: " + args.joinToString(" "))
val processBuilder = ProcessBuilder(args)
processBuilder.redirectErrorStream(true)
processBuilder.environment().putAll(environmentVariablesForTests)
val workingDir = File(daemonOptions.runFilesPath).apply { mkdirs() }
processBuilder.directory(workingDir)
// assuming daemon process is deaf and (mostly) silent, so do not handle streams
val daemon = processBuilder.start()
return checkDaemonStartedProperly(daemon, reportingTargets, daemonOptions, startupAttempt, gcAutoConfiguration)
}
/**
* Ensures that the daemon process has started properly.
* Additionally, handles the logging logic in the case of exceptions.
*
* @return `true` if the daemon started properly, `false` otherwise.
*/
private fun checkDaemonStartedProperly(
daemon: Process,
reportingTargets: DaemonReportingTargets,
daemonOptions: DaemonOptions,
startupAttempt: Int,
gcAutoConfiguration: GcAutoConfiguration,
): Boolean {
val isEchoRead = Semaphore(1)
isEchoRead.acquire()
val initMessageListener = DaemonInitMessageListener()
val outputListener = CompositeDaemonErrorReportingOutputListener(
DaemonLastOutputLinesListener(),
DaemonGcAutoConfigurationProblemsListener(gcAutoConfiguration, startupAttempt),
initMessageListener,
)
// `daemonIsAlmostDead == true` implies `initMessageListener.caughtInitMessage == true` (and enables relevant diagnostic reporting)
// However, `initMessageListener.caughtInitMessage == true` does not imply `daemonIsAlmostDead == true`
val daemonIsAlmostDead = AtomicBoolean(false)
val stdoutThread =
thread {
try {
daemon.inputStream
.reader()
.forEachLine {
if (Thread.currentThread().isInterrupted) return@forEachLine
outputListener.onOutputLine(it)
if (initMessageListener.caughtInitMessage) {
reportingTargets.report(
DaemonReportCategory.DEBUG,
"Received the message signalling that the daemon is ready"
)
isEchoRead.release()
return@forEachLine
} else {
reportingTargets.report(DaemonReportCategory.INFO, it, "daemon")
}
}
if (!initMessageListener.caughtInitMessage) {
// That means the stream was fully read, but no "echo" received. The process is crashing.
daemonIsAlmostDead.set(true)
}
} catch (_: InterruptedException) {
// Ignore
} catch (e: Throwable) {
reportingTargets.report(
DaemonReportCategory.EXCEPTION,
"Exception occurred during daemon output processing: ${e.stackTraceToString()}"
)
} finally {
daemon.inputStream.close()
daemon.outputStream.close()
daemon.errorStream.close()
isEchoRead.release()
}
}
try {
// trying to wait for process
val daemonStartupTimeout = CompilerSystemProperties.COMPILE_DAEMON_STARTUP_TIMEOUT_PROPERTY.value?.let {
try {
it.toLong()
} catch (_: Exception) {
reportingTargets.report(
DaemonReportCategory.INFO,
"unable to interpret ${CompilerSystemProperties.COMPILE_DAEMON_STARTUP_TIMEOUT_PROPERTY.property} property ('$it'); using default timeout $DAEMON_DEFAULT_STARTUP_TIMEOUT_MS ms"
)
null
}
} ?: DAEMON_DEFAULT_STARTUP_TIMEOUT_MS
if (daemonOptions.runFilesPath.isNotEmpty()) {
val succeeded = isEchoRead.tryAcquire(daemonStartupTimeout, TimeUnit.MILLISECONDS)
return when {
!isProcessAlive(daemon) || daemonIsAlmostDead.get() -> {
/*
* We know the daemon crashed, but the process might be still running for a bit.
* However, we do not want to wait indefinitely even in this case
*/
val processStateString = if (daemon.waitFor(daemonStartupTimeout, TimeUnit.MILLISECONDS)) {
" with exit code: ${daemon.exitValue()}"
} else {
". The process may remain alive for a bit"
}
reportingTargets.report(
DaemonReportCategory.EXCEPTION,
"The daemon has terminated unexpectedly on startup attempt #${startupAttempt + 1}${processStateString}. ${
outputListener.retrieveProblems().joinToString("\n")
}"
)
false
}
!succeeded -> {
reportingTargets.report(DaemonReportCategory.INFO, "Unable to get response from daemon in $daemonStartupTimeout ms")
false
}
else -> true
}
} else
// without startEcho defined waiting for max timeout
Thread.sleep(daemonStartupTimeout)
return true
} finally {
// assuming that all important output is already done, the rest should be routed to the log by the daemon itself
if (stdoutThread.isAlive) {
// TODO: find better method to stop the thread, but seems it will require asynchronous consuming of the stream
stdoutThread.interrupt()
}
reportingTargets.out?.flush()
}
}
}
data class DaemonReportMessage(val category: DaemonReportCategory, val message: String)
class DaemonReportingTargets(
val out: PrintStream? = null,
val messages: MutableCollection? = null,
val messageCollector: MessageCollector? = null,
val compilerServices: CompilerServicesFacadeBase? = null,
)
internal fun DaemonReportingTargets.report(category: DaemonReportCategory, message: String, source: String? = null) {
val sourceMessage: String by lazy { source?.let { "[$it] $message" } ?: message }
out?.println("${category.name}: $sourceMessage")
messages?.add(DaemonReportMessage(category, sourceMessage))
messageCollector?.let {
when (category) {
DaemonReportCategory.DEBUG -> it.report(CompilerMessageSeverity.LOGGING, sourceMessage)
DaemonReportCategory.INFO -> it.report(CompilerMessageSeverity.INFO, sourceMessage)
DaemonReportCategory.EXCEPTION -> it.report(CompilerMessageSeverity.EXCEPTION, sourceMessage)
}
}
compilerServices?.let {
when (category) {
DaemonReportCategory.DEBUG -> it.report(ReportCategory.DAEMON_MESSAGE, ReportSeverity.DEBUG, message, source)
DaemonReportCategory.INFO -> it.report(ReportCategory.DAEMON_MESSAGE, ReportSeverity.INFO, message, source)
DaemonReportCategory.EXCEPTION -> it.report(ReportCategory.EXCEPTION, ReportSeverity.ERROR, message, source)
}
}
}
internal fun isProcessAlive(process: Process) =
try {
process.exitValue()
false
} catch (e: IllegalThreadStateException) {
true
}