Moving daemon files, renaming namespaces, modules and jar
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="kotlin-runtime" level="project" />
|
||||
<orderEntry type="module" module-name="daemon-common" />
|
||||
<orderEntry type="module" module-name="frontend.java" />
|
||||
<orderEntry type="module" module-name="util" />
|
||||
<orderEntry type="library" name="native-platform-uberjar" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.daemon.common.CompilerCallbackServicesFacade
|
||||
import org.jetbrains.kotlin.daemon.common.LoopbackNetworkInterface
|
||||
import org.jetbrains.kotlin.daemon.common.SOCKET_ANY_FREE_PORT
|
||||
import org.jetbrains.kotlin.incremental.components.LookupInfo
|
||||
import org.jetbrains.kotlin.incremental.components.LookupTracker
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.JvmPackagePartProto
|
||||
import org.jetbrains.kotlin.modules.TargetId
|
||||
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
|
||||
import java.rmi.server.UnicastRemoteObject
|
||||
|
||||
|
||||
class CompilerCallbackServicesFacadeServer(
|
||||
val incrementalCompilationComponents: IncrementalCompilationComponents? = null,
|
||||
val compilationCancelledStatus: CompilationCanceledStatus? = null,
|
||||
port: Int = SOCKET_ANY_FREE_PORT
|
||||
) : CompilerCallbackServicesFacade,
|
||||
UnicastRemoteObject(port, LoopbackNetworkInterface.clientLoopbackSocketFactory, LoopbackNetworkInterface.serverLoopbackSocketFactory)
|
||||
{
|
||||
override fun hasIncrementalCaches(): Boolean = incrementalCompilationComponents != null
|
||||
|
||||
override fun hasLookupTracker(): Boolean = incrementalCompilationComponents != null
|
||||
|
||||
override fun hasCompilationCanceledStatus(): Boolean = compilationCancelledStatus != null
|
||||
|
||||
// TODO: consider replacing NPE with other reporting, although NPE here means most probably incorrect usage
|
||||
|
||||
override fun incrementalCache_getObsoletePackageParts(target: TargetId): Collection<String> = incrementalCompilationComponents!!.getIncrementalCache(target).getObsoletePackageParts()
|
||||
|
||||
override fun incrementalCache_getObsoleteMultifileClassFacades(target: TargetId): Collection<String> = incrementalCompilationComponents!!.getIncrementalCache(target).getObsoleteMultifileClasses()
|
||||
|
||||
override fun incrementalCache_getMultifileFacadeParts(target: TargetId, internalName: String): Collection<String>? = incrementalCompilationComponents!!.getIncrementalCache(target).getStableMultifileFacadeParts(internalName)
|
||||
|
||||
override fun incrementalCache_getMultifileFacade(target: TargetId, partInternalName: String): String? = incrementalCompilationComponents!!.getIncrementalCache(target).getMultifileFacade(partInternalName)
|
||||
|
||||
override fun incrementalCache_getPackagePartData(target: TargetId, fqName: String): JvmPackagePartProto? = incrementalCompilationComponents!!.getIncrementalCache(target).getPackagePartData(fqName)
|
||||
|
||||
override fun incrementalCache_getModuleMappingData(target: TargetId): ByteArray? = incrementalCompilationComponents!!.getIncrementalCache(target).getModuleMappingData()
|
||||
|
||||
override fun incrementalCache_registerInline(target: TargetId, fromPath: String, jvmSignature: String, toPath: String) {
|
||||
incrementalCompilationComponents!!.getIncrementalCache(target).registerInline(fromPath, jvmSignature, toPath)
|
||||
}
|
||||
|
||||
override fun incrementalCache_getClassFilePath(target: TargetId, internalClassName: String): String = incrementalCompilationComponents!!.getIncrementalCache(target).getClassFilePath(internalClassName)
|
||||
|
||||
override fun incrementalCache_close(target: TargetId) {
|
||||
incrementalCompilationComponents!!.getIncrementalCache(target).close()
|
||||
}
|
||||
|
||||
override fun lookupTracker_requiresPosition() = incrementalCompilationComponents!!.getLookupTracker().requiresPosition
|
||||
|
||||
override fun lookupTracker_record(lookups: Collection<LookupInfo>) {
|
||||
val lookupTracker = incrementalCompilationComponents!!.getLookupTracker()
|
||||
|
||||
for (it in lookups) {
|
||||
lookupTracker.record(it.filePath, it.position, it.scopeFqName, it.scopeKind, it.name)
|
||||
}
|
||||
}
|
||||
|
||||
private val lookupTracker_isDoNothing: Boolean = incrementalCompilationComponents != null && incrementalCompilationComponents.getLookupTracker() == LookupTracker.DO_NOTHING
|
||||
|
||||
override fun lookupTracker_isDoNothing(): Boolean = lookupTracker_isDoNothing
|
||||
|
||||
override fun compilationCanceledStatus_checkCanceled() {
|
||||
compilationCancelledStatus!!.checkCanceled()
|
||||
}
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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 net.rubygrapefruit.platform.Native
|
||||
import net.rubygrapefruit.platform.ProcessLauncher
|
||||
import org.jetbrains.kotlin.daemon.common.*
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
|
||||
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.check
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.io.PrintStream
|
||||
import java.rmi.server.UnicastRemoteObject
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
class CompilationServices(
|
||||
val incrementalCompilationComponents: IncrementalCompilationComponents? = null,
|
||||
val compilationCanceledStatus: CompilationCanceledStatus? = null
|
||||
)
|
||||
|
||||
|
||||
object KotlinCompilerClient {
|
||||
|
||||
val DAEMON_DEFAULT_STARTUP_TIMEOUT_MS = 10000L
|
||||
val DAEMON_CONNECT_CYCLE_ATTEMPTS = 3
|
||||
|
||||
val verboseReporting = System.getProperty(COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY) != null
|
||||
|
||||
|
||||
fun connectToCompileService(compilerId: CompilerId,
|
||||
daemonJVMOptions: DaemonJVMOptions,
|
||||
daemonOptions: DaemonOptions,
|
||||
reportingTargets: DaemonReportingTargets,
|
||||
autostart: Boolean = true,
|
||||
checkId: Boolean = true
|
||||
): CompileService? {
|
||||
fun newFlagFile(): File {
|
||||
val flagFile = File.createTempFile("kotlin-compiler-client-", "-is-running")
|
||||
flagFile.deleteOnExit()
|
||||
return flagFile
|
||||
}
|
||||
|
||||
val flagFile = System.getProperty(COMPILE_DAEMON_CLIENT_ALIVE_PATH_PROPERTY)
|
||||
?.let { it.trimQuotes() }
|
||||
?.check { !it.isBlank() }
|
||||
?.let { File(it) }
|
||||
?.check { it.exists() }
|
||||
?: newFlagFile()
|
||||
return connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, reportingTargets, autostart)
|
||||
}
|
||||
|
||||
fun connectToCompileService(compilerId: CompilerId,
|
||||
clientAliveFlagFile: File,
|
||||
daemonJVMOptions: DaemonJVMOptions,
|
||||
daemonOptions: DaemonOptions,
|
||||
reportingTargets: DaemonReportingTargets,
|
||||
autostart: Boolean = true
|
||||
): CompileService? {
|
||||
|
||||
var attempts = 0
|
||||
try {
|
||||
while (attempts++ < DAEMON_CONNECT_CYCLE_ATTEMPTS) {
|
||||
val (service, newJVMOptions) = tryFindSuitableDaemonOrNewOpts(File(daemonOptions.runFilesPath), compilerId, daemonJVMOptions, { cat, msg -> reportingTargets.report(cat, msg) })
|
||||
if (service != null) {
|
||||
// the newJVMOptions could be checked here for additional parameters, if needed
|
||||
service.registerClient(clientAliveFlagFile.absolutePath)
|
||||
reportingTargets.report(DaemonReportCategory.DEBUG, "connected to the daemon")
|
||||
return service
|
||||
}
|
||||
reportingTargets.report(DaemonReportCategory.DEBUG, "no suitable daemon found")
|
||||
if (autostart) {
|
||||
startDaemon(compilerId, newJVMOptions, daemonOptions, reportingTargets)
|
||||
reportingTargets.report(DaemonReportCategory.DEBUG, "new daemon started, trying to find it")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
reportingTargets.report(DaemonReportCategory.EXCEPTION, e.toString())
|
||||
}
|
||||
return 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 compile(compilerService: CompileService,
|
||||
sessionId: Int,
|
||||
targetPlatform: CompileService.TargetPlatform,
|
||||
args: Array<out String>,
|
||||
out: OutputStream,
|
||||
port: Int = SOCKET_ANY_FREE_PORT,
|
||||
operationsTracer: RemoteOperationsTracer? = null
|
||||
): Int {
|
||||
val outStrm = RemoteOutputStreamServer(out, port = port)
|
||||
return compilerService.remoteCompile(sessionId, targetPlatform, args, CompilerCallbackServicesFacadeServer(port = port), outStrm, CompileService.OutputFormat.PLAIN, outStrm, operationsTracer).get()
|
||||
}
|
||||
|
||||
|
||||
fun incrementalCompile(compileService: CompileService,
|
||||
sessionId: Int,
|
||||
targetPlatform: CompileService.TargetPlatform,
|
||||
args: Array<out String>,
|
||||
callbackServices: CompilationServices,
|
||||
compilerOut: OutputStream,
|
||||
daemonOut: OutputStream,
|
||||
port: Int = SOCKET_ANY_FREE_PORT,
|
||||
profiler: Profiler = DummyProfiler(),
|
||||
operationsTracer: RemoteOperationsTracer? = null
|
||||
): Int = profiler.withMeasure(this) {
|
||||
compileService.remoteIncrementalCompile(
|
||||
sessionId,
|
||||
targetPlatform,
|
||||
args,
|
||||
CompilerCallbackServicesFacadeServer(incrementalCompilationComponents = callbackServices.incrementalCompilationComponents,
|
||||
compilationCancelledStatus = callbackServices.compilationCanceledStatus,
|
||||
port = port),
|
||||
RemoteOutputStreamServer(compilerOut, port),
|
||||
CompileService.OutputFormat.XML,
|
||||
RemoteOutputStreamServer(daemonOut, port),
|
||||
operationsTracer).get()
|
||||
}
|
||||
|
||||
val COMPILE_DAEMON_CLIENT_OPTIONS_PROPERTY: String = "kotlin.daemon.client.options"
|
||||
data class ClientOptions(
|
||||
var stop: Boolean = false
|
||||
) : OptionsGroup {
|
||||
override val mappers: List<PropMapper<*, *, *>>
|
||||
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
|
||||
fun main(vararg args: String) {
|
||||
val compilerId = CompilerId()
|
||||
val daemonOptions = configureDaemonOptions()
|
||||
val daemonLaunchingOptions = configureDaemonJVMOptions(inheritMemoryLimits = true, 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 outStrm = RemoteOutputStreamServer(System.out)
|
||||
val servicesFacade = CompilerCallbackServicesFacadeServer()
|
||||
try {
|
||||
val memBefore = daemon.getUsedMemory().get() / 1024
|
||||
val startTime = System.nanoTime()
|
||||
|
||||
val res = daemon.remoteCompile(CompileService.NO_SESSION, CompileService.TargetPlatform.JVM, filteredArgs.toArrayList().toTypedArray(), servicesFacade, outStrm, CompileService.OutputFormat.PLAIN, outStrm, null)
|
||||
|
||||
val endTime = System.nanoTime()
|
||||
println("Compilation result code: $res")
|
||||
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)
|
||||
UnicastRemoteObject.unexportObject(outStrm, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun detectCompilerClasspath(): List<String>? =
|
||||
System.getProperty("java.class.path")
|
||||
?.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 fun DaemonReportingTargets.report(category: DaemonReportCategory, message: String, source: String = "daemon client") {
|
||||
if (category == DaemonReportCategory.DEBUG && !verboseReporting) return
|
||||
out?.println("[$source] ${category.name}: $message")
|
||||
messages?.add(DaemonReportMessage(category, "[$source] $message"))
|
||||
}
|
||||
|
||||
|
||||
private fun tryFindSuitableDaemonOrNewOpts(registryDir: File, compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, report: (DaemonReportCategory, String) -> Unit): Pair<CompileService?, DaemonJVMOptions> {
|
||||
val aliveWithOpts = walkDaemons(registryDir, compilerId, report = report)
|
||||
.map { Pair(it, it.getDaemonJVMOptions()) }
|
||||
.filter { it.second.isGood }
|
||||
.sortedWith(compareBy(DaemonJVMOptionsMemoryComparator().reversed(), { it.second.get() }))
|
||||
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 aliveWithOpts.firstOrNull()?.check { daemonJVMOptions memorywiseFitsInto it.second.get() }?.let {
|
||||
Pair(it.first, optsCopy.updateMemoryUpperBounds(it.second.get()))
|
||||
}
|
||||
// else combine all options from running daemon to get fattest option for a new daemon to run
|
||||
?: Pair(null, aliveWithOpts.fold(optsCopy, { opts, d -> opts.updateMemoryUpperBounds(d.second.get()) }))
|
||||
}
|
||||
|
||||
|
||||
private fun startDaemon(compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, reportingTargets: DaemonReportingTargets) {
|
||||
val javaExecutable = File(File(System.getProperty("java.home"), "bin"), "java")
|
||||
val platformSpecificOptions = listOf("-Djava.awt.headless=true") // hide daemon window
|
||||
val args = listOf(
|
||||
javaExecutable.absolutePath, "-cp", compilerId.compilerClasspath.joinToString(File.pathSeparator)) +
|
||||
platformSpecificOptions +
|
||||
daemonJVMOptions.mappers.flatMap { it.toArgs("-") } +
|
||||
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.DEBUG, "starting the daemon as: " + args.joinToString(" "))
|
||||
val processBuilder = ProcessBuilder(args)
|
||||
processBuilder.redirectErrorStream(true)
|
||||
// assuming daemon process is deaf and (mostly) silent, so do not handle streams
|
||||
val daemonLauncher = Native.get(ProcessLauncher::class.java)
|
||||
val daemon = daemonLauncher.start(processBuilder)
|
||||
|
||||
var isEchoRead = Semaphore(1)
|
||||
isEchoRead.acquire()
|
||||
|
||||
val stdoutThread =
|
||||
thread {
|
||||
try {
|
||||
daemon.inputStream
|
||||
.reader()
|
||||
.forEachLine {
|
||||
if (daemonOptions.runFilesPath.isNotEmpty() && it.contains(daemonOptions.runFilesPath)) {
|
||||
isEchoRead.release()
|
||||
return@forEachLine
|
||||
}
|
||||
reportingTargets.report(DaemonReportCategory.DEBUG, it, "daemon")
|
||||
}
|
||||
}
|
||||
finally {
|
||||
daemon.inputStream.close()
|
||||
daemon.outputStream.close()
|
||||
daemon.errorStream.close()
|
||||
}
|
||||
}
|
||||
try {
|
||||
// trying to wait for process
|
||||
val daemonStartupTimeout = System.getProperty(COMPILE_DAEMON_STARTUP_TIMEOUT_PROPERTY)?.let {
|
||||
try {
|
||||
it.toLong()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
reportingTargets.report(DaemonReportCategory.INFO, "unable to interpret ${COMPILE_DAEMON_STARTUP_TIMEOUT_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)
|
||||
if (!isProcessAlive(daemon))
|
||||
throw Exception("Daemon terminated unexpectedly")
|
||||
if (!succeeded)
|
||||
throw Exception("Unable to get response from daemon in $daemonStartupTimeout ms")
|
||||
}
|
||||
else
|
||||
// without startEcho defined waiting for max timeout
|
||||
Thread.sleep(daemonStartupTimeout)
|
||||
}
|
||||
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.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class DaemonReportMessage(public val category: DaemonReportCategory, public val message: String)
|
||||
|
||||
class DaemonReportingTargets(public val out: PrintStream? = null, public val messages: MutableCollection<DaemonReportMessage>? = null)
|
||||
|
||||
|
||||
internal fun isProcessAlive(process: Process) =
|
||||
try {
|
||||
process.exitValue()
|
||||
false
|
||||
}
|
||||
catch (e: IllegalThreadStateException) {
|
||||
true
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.daemon.common.LoopbackNetworkInterface
|
||||
import org.jetbrains.kotlin.daemon.common.SOCKET_ANY_FREE_PORT
|
||||
import org.jetbrains.kotlin.daemon.common.RemoteOutputStream
|
||||
import org.jetbrains.kotlin.daemon.common.SOCKET_ANY_FREE_PORT
|
||||
import java.io.OutputStream
|
||||
import java.rmi.server.UnicastRemoteObject
|
||||
|
||||
|
||||
class RemoteOutputStreamServer(val out: OutputStream, port: Int = SOCKET_ANY_FREE_PORT)
|
||||
: RemoteOutputStream,
|
||||
UnicastRemoteObject(port, LoopbackNetworkInterface.clientLoopbackSocketFactory, LoopbackNetworkInterface.serverLoopbackSocketFactory)
|
||||
{
|
||||
override fun close() {
|
||||
out.close()
|
||||
}
|
||||
|
||||
override fun write(data: ByteArray, offset: Int, length: Int) {
|
||||
out.write(data, offset, length)
|
||||
}
|
||||
|
||||
override fun write(dataByte: Int) {
|
||||
out.write(dataByte)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="kotlin-runtime" level="project" />
|
||||
<orderEntry type="module" module-name="cli-common" />
|
||||
<orderEntry type="module" module-name="frontend.java" />
|
||||
<orderEntry type="module" module-name="util" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.common
|
||||
|
||||
import java.io.File
|
||||
import java.rmi.ConnectException
|
||||
import java.rmi.registry.LocateRegistry
|
||||
|
||||
|
||||
internal val MAX_PORT_NUMBER = 0xffff
|
||||
|
||||
|
||||
enum class DaemonReportCategory {
|
||||
DEBUG, INFO, EXCEPTION;
|
||||
}
|
||||
|
||||
|
||||
fun makeRunFilenameString(timestamp: String, digest: String, port: String, escapeSequence: String = ""): String =
|
||||
"${COMPILE_DAEMON_DEFAULT_FILES_PREFIX}$escapeSequence.$timestamp$escapeSequence.$digest$escapeSequence.$port$escapeSequence.run"
|
||||
|
||||
|
||||
fun makePortFromRunFilenameExtractor(digest: String): (String) -> Int? {
|
||||
val regex = makeRunFilenameString(timestamp = "[0-9TZ:\\.\\+-]+", digest = digest, port = "(\\d+)", escapeSequence = "\\").toRegex()
|
||||
return { regex.find(it)
|
||||
?.groups?.get(1)
|
||||
?.value?.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun walkDaemons(registryDir: File,
|
||||
compilerId: CompilerId,
|
||||
filter: (File, Int) -> Boolean = { f, p -> true },
|
||||
report: (DaemonReportCategory, String) -> Unit = { cat, msg -> }
|
||||
): Sequence<CompileService> {
|
||||
val classPathDigest = compilerId.compilerClasspath.map { File(it).absolutePath }.distinctStringsDigest().toHexString()
|
||||
val portExtractor = makePortFromRunFilenameExtractor(classPathDigest)
|
||||
return registryDir.walk()
|
||||
.map { Pair(it, portExtractor(it.name)) }
|
||||
.filter { it.second != null && filter(it.first, it.second!!) }
|
||||
.map {
|
||||
assert(it.second!! > 0 && it.second!! < MAX_PORT_NUMBER)
|
||||
report(DaemonReportCategory.DEBUG, "found daemon on port ${it.second}, trying to connect")
|
||||
val daemon = tryConnectToDaemon(it.second!!, report)
|
||||
// cleaning orphaned file; note: daemon should shut itself down if it detects that the run file is deleted
|
||||
if (daemon == null && !it.first.delete()) {
|
||||
report(DaemonReportCategory.INFO, "WARNING: unable to delete seemingly orphaned file '${it.first.absolutePath}', cleanup recommended")
|
||||
}
|
||||
daemon
|
||||
}
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
|
||||
private inline fun tryConnectToDaemon(port: Int, report: (DaemonReportCategory, String) -> Unit): CompileService? {
|
||||
try {
|
||||
val daemon = LocateRegistry.getRegistry(LoopbackNetworkInterface.loopbackInetAddressName, port)
|
||||
?.lookup(COMPILER_SERVICE_RMI_NAME)
|
||||
if (daemon != null)
|
||||
return daemon as? CompileService ?:
|
||||
throw ClassCastException("Unable to cast compiler service, actual class received: ${daemon.javaClass}")
|
||||
report(DaemonReportCategory.EXCEPTION, "daemon not found")
|
||||
}
|
||||
catch (e: ConnectException) {
|
||||
report(DaemonReportCategory.EXCEPTION, "cannot connect to registry: " + (e.cause?.message ?: e.message ?: "unknown exception"))
|
||||
// ignoring it - processing below
|
||||
}
|
||||
return null
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.common
|
||||
|
||||
import java.io.Serializable
|
||||
import java.rmi.Remote
|
||||
import java.rmi.RemoteException
|
||||
|
||||
interface CompileService : Remote {
|
||||
|
||||
enum class OutputFormat : Serializable {
|
||||
PLAIN,
|
||||
XML
|
||||
}
|
||||
|
||||
enum class TargetPlatform : Serializable {
|
||||
JVM,
|
||||
JS
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NO_SESSION: Int = 0
|
||||
}
|
||||
|
||||
sealed class CallResult<out R> : Serializable {
|
||||
|
||||
class Good<R>(val result: R) : CallResult<R>() {
|
||||
override fun get(): R = result
|
||||
override fun equals(other: Any?): Boolean = other is Good<*> && this.result == other.result
|
||||
override fun hashCode(): Int = this.javaClass.hashCode() + (result?.hashCode() ?: 1)
|
||||
}
|
||||
class Ok : CallResult<Nothing>() {
|
||||
override fun get(): Nothing = throw IllegalStateException("Gey is inapplicable to Ok call result")
|
||||
override fun equals(other: Any?): Boolean = other is Ok
|
||||
override fun hashCode(): Int = this.javaClass.hashCode() + 1 // avoiding clash with the hash of class itself
|
||||
}
|
||||
class Dying : CallResult<Nothing>() {
|
||||
override fun get(): Nothing = throw IllegalStateException("Service is dying")
|
||||
override fun equals(other: Any?): Boolean = other is Dying
|
||||
override fun hashCode(): Int = this.javaClass.hashCode() + 1 // see comment to Ok.hashCode
|
||||
}
|
||||
class Error(val message: String) : CallResult<Nothing>() {
|
||||
override fun get(): Nothing = throw Exception(message)
|
||||
override fun equals(other: Any?): Boolean = other is Error && this.message == other.message
|
||||
override fun hashCode(): Int = this.javaClass.hashCode() + message.hashCode()
|
||||
}
|
||||
|
||||
val isGood: Boolean get() = this is Good<*>
|
||||
|
||||
abstract fun get(): R
|
||||
}
|
||||
|
||||
// TODO: remove!
|
||||
@Throws(RemoteException::class)
|
||||
fun checkCompilerId(expectedCompilerId: CompilerId): Boolean
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun getUsedMemory(): CallResult<Long>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun getDaemonOptions(): CallResult<DaemonOptions>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun getDaemonJVMOptions(): CallResult<DaemonJVMOptions>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun registerClient(aliveFlagPath: String?): CallResult<Nothing>
|
||||
|
||||
// TODO: consider adding another client alive checking mechanism, e.g. socket/port
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun getClients(): CallResult<List<String>>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun leaseCompileSession(aliveFlagPath: String?): CallResult<Int>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun releaseCompileSession(sessionId: Int): CallResult<Nothing>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun shutdown(): CallResult<Nothing>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun scheduleShutdown(graceful: Boolean): CallResult<Boolean>
|
||||
|
||||
// TODO: consider adding async version of shutdown and release
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun remoteCompile(
|
||||
sessionId: Int,
|
||||
targetPlatform: TargetPlatform,
|
||||
args: Array<out String>,
|
||||
servicesFacade: CompilerCallbackServicesFacade,
|
||||
compilerOutputStream: RemoteOutputStream,
|
||||
outputFormat: OutputFormat,
|
||||
serviceOutputStream: RemoteOutputStream,
|
||||
operationsTracer: RemoteOperationsTracer?
|
||||
): CallResult<Int>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun remoteIncrementalCompile(
|
||||
sessionId: Int,
|
||||
targetPlatform: TargetPlatform,
|
||||
args: Array<out String>,
|
||||
servicesFacade: CompilerCallbackServicesFacade,
|
||||
compilerOutputStream: RemoteOutputStream,
|
||||
compilerOutputFormat: OutputFormat,
|
||||
serviceOutputStream: RemoteOutputStream,
|
||||
operationsTracer: RemoteOperationsTracer?
|
||||
): CallResult<Int>
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.common
|
||||
|
||||
import org.jetbrains.kotlin.incremental.components.LookupInfo
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.JvmPackagePartProto
|
||||
import org.jetbrains.kotlin.modules.TargetId
|
||||
import java.rmi.Remote
|
||||
import java.rmi.RemoteException
|
||||
|
||||
|
||||
/**
|
||||
* common facade for compiler services exposed from client process (e.g. JPS) to the compiler running on daemon
|
||||
* the reason for having common facade is attempt to reduce number of connections between client and daemon
|
||||
* Note: non-standard naming convention used to denote combining several entities in one facade - prefix <entityName>_ is used for every function belonging to the entity
|
||||
*/
|
||||
interface CompilerCallbackServicesFacade : Remote {
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun hasIncrementalCaches(): Boolean
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun hasLookupTracker(): Boolean
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun hasCompilationCanceledStatus(): Boolean
|
||||
|
||||
// ----------------------------------------------------
|
||||
// IncrementalCache
|
||||
@Throws(RemoteException::class)
|
||||
fun incrementalCache_getObsoletePackageParts(target: TargetId): Collection<String>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun incrementalCache_getObsoleteMultifileClassFacades(target: TargetId): Collection<String>
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun incrementalCache_getMultifileFacade(target: TargetId, partInternalName: String): String?
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun incrementalCache_getPackagePartData(target: TargetId, fqName: String): JvmPackagePartProto?
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun incrementalCache_getModuleMappingData(target: TargetId): ByteArray?
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun incrementalCache_registerInline(target: TargetId, fromPath: String, jvmSignature: String, toPath: String)
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun incrementalCache_getClassFilePath(target: TargetId, internalClassName: String): String
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun incrementalCache_close(target: TargetId)
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun incrementalCache_getMultifileFacadeParts(target: TargetId, internalName: String): Collection<String>?
|
||||
|
||||
// ----------------------------------------------------
|
||||
// LookupTracker
|
||||
@Throws(RemoteException::class)
|
||||
fun lookupTracker_requiresPosition(): Boolean
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun lookupTracker_record(lookups: Collection<LookupInfo>)
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun lookupTracker_isDoNothing(): Boolean
|
||||
|
||||
// ----------------------------------------------------
|
||||
// CompilationCanceledStatus
|
||||
@Throws(RemoteException::class)
|
||||
fun compilationCanceledStatus_checkCanceled(): Unit
|
||||
}
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.common
|
||||
|
||||
import org.jetbrains.kotlin.cli.common.KOTLIN_COMPILER_ENVIRONMENT_KEEPALIVE_PROPERTY
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
|
||||
|
||||
val COMPILER_JAR_NAME: String = "kotlin-compiler.jar"
|
||||
val COMPILER_SERVICE_RMI_NAME: String = "KotlinJvmCompilerService"
|
||||
val COMPILER_DAEMON_CLASS_FQN: String = "org.jetbrains.kotlin.daemon.CompileDaemon"
|
||||
val COMPILE_DAEMON_FIND_PORT_ATTEMPTS: Int = 10
|
||||
val COMPILE_DAEMON_PORTS_RANGE_START: Int = 17001
|
||||
val COMPILE_DAEMON_PORTS_RANGE_END: Int = 18000
|
||||
val COMPILE_DAEMON_ENABLED_PROPERTY: String = "kotlin.daemon.enabled"
|
||||
val COMPILE_DAEMON_JVM_OPTIONS_PROPERTY: String = "kotlin.daemon.jvm.options"
|
||||
val COMPILE_DAEMON_OPTIONS_PROPERTY: String = "kotlin.daemon.options"
|
||||
val COMPILE_DAEMON_CLIENT_ALIVE_PATH_PROPERTY: String = "kotlin.daemon.client.alive.path"
|
||||
val COMPILE_DAEMON_LOG_PATH_PROPERTY: String = "kotlin.daemon.log.path"
|
||||
val COMPILE_DAEMON_REPORT_PERF_PROPERTY: String = "kotlin.daemon.perf"
|
||||
val COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY: String = "kotlin.daemon.verbose"
|
||||
val COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX: String = "--daemon-"
|
||||
val COMPILE_DAEMON_STARTUP_TIMEOUT_PROPERTY: String = "kotlin.daemon.startup.timeout"
|
||||
val COMPILE_DAEMON_DEFAULT_FILES_PREFIX: String = "kotlin-daemon"
|
||||
val COMPILE_DAEMON_TIMEOUT_INFINITE_S: Int = 0
|
||||
val COMPILE_DAEMON_DEFAULT_IDLE_TIMEOUT_S: Int = 7200 // 2 hours
|
||||
val COMPILE_DAEMON_DEFAULT_UNUSED_TIMEOUT_S: Int = 60
|
||||
val COMPILE_DAEMON_DEFAULT_SHUTDOWN_DELAY_MS: Long = 1000L // 1 sec
|
||||
val COMPILE_DAEMON_MEMORY_THRESHOLD_INFINITE: Long = 0L
|
||||
val COMPILE_DAEMON_FORCE_SHUTDOWN_DEFAULT_TIMEOUT_MS: Long = 10000L // 10 secs
|
||||
val COMPILE_DAEMON_TIMEOUT_INFINITE_MS: Long = 0L
|
||||
|
||||
val COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH: String get() =
|
||||
FileSystem.getRuntimeStateFilesPath("kotlin", "daemon")
|
||||
|
||||
val CLASSPATH_ID_DIGEST = "MD5"
|
||||
|
||||
|
||||
open class PropMapper<C, V, P : KMutableProperty1<C, V>>(val dest: C,
|
||||
val prop: P,
|
||||
val names: List<String> = listOf(prop.name),
|
||||
val fromString: (String) -> V,
|
||||
val toString: ((V) -> String?) = { it.toString() },
|
||||
val skipIf: ((V) -> Boolean) = { false },
|
||||
val mergeDelimiter: String? = null) {
|
||||
open fun toArgs(prefix: String = COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX): List<String> =
|
||||
when {
|
||||
skipIf(prop.get(dest)) -> listOf<String>()
|
||||
mergeDelimiter != null -> listOf(listOfNotNull(prefix + names.first(), toString(prop.get(dest))).joinToString(mergeDelimiter))
|
||||
else -> listOfNotNull(prefix + names.first(), toString(prop.get(dest)))
|
||||
}
|
||||
|
||||
open fun apply(s: String) = prop.set(dest, fromString(s))
|
||||
}
|
||||
|
||||
|
||||
class NullablePropMapper<C, V : Any?, P : KMutableProperty1<C, V>>(dest: C,
|
||||
prop: P,
|
||||
names: List<String> = listOf(),
|
||||
fromString: ((String) -> V),
|
||||
toString: ((V) -> String?) = { it.toString() },
|
||||
skipIf: ((V) -> Boolean) = { it == null },
|
||||
mergeDelimiter: String? = null)
|
||||
: PropMapper<C, V, P>(dest = dest, prop = prop, names = if (names.any()) names else listOf(prop.name),
|
||||
fromString = fromString, toString = toString, skipIf = skipIf, mergeDelimiter = mergeDelimiter)
|
||||
|
||||
|
||||
class StringPropMapper<C, P : KMutableProperty1<C, String>>(dest: C,
|
||||
prop: P,
|
||||
names: List<String> = listOf(),
|
||||
fromString: ((String) -> String) = { it },
|
||||
toString: ((String) -> String?) = { it.toString() },
|
||||
skipIf: ((String) -> Boolean) = { it.isEmpty() },
|
||||
mergeDelimiter: String? = null)
|
||||
: PropMapper<C, String, P>(dest = dest, prop = prop, names = if (names.any()) names else listOf(prop.name),
|
||||
fromString = fromString, toString = toString, skipIf = skipIf, mergeDelimiter = mergeDelimiter)
|
||||
|
||||
|
||||
class BoolPropMapper<C, P : KMutableProperty1<C, Boolean>>(dest: C, prop: P, names: List<String> = listOf())
|
||||
: PropMapper<C, Boolean, P>(dest = dest, prop = prop, names = if (names.any()) names else listOf(prop.name),
|
||||
fromString = { true }, toString = { null }, skipIf = { !prop.get(dest) })
|
||||
|
||||
|
||||
class RestPropMapper<C, P : KMutableProperty1<C, MutableCollection<String>>>(dest: C, prop: P)
|
||||
: PropMapper<C, MutableCollection<String>, P>(dest = dest, prop = prop, toString = { null }, fromString = { arrayListOf() }) {
|
||||
override fun toArgs(prefix: String): List<String> = prop.get(dest).map { prefix + it }
|
||||
override fun apply(s: String) = add(s)
|
||||
fun add(s: String) {
|
||||
prop.get(dest).add(s)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// helper function combining find with map, useful for the cases then there is a calculation performed in find, which is nice to return along with
|
||||
// found value; mappingPredicate should return the pair of boolean compare predicate result and transformation value, we want to get along with found value
|
||||
inline fun <T, R : Any> Iterable<T>.findWithTransform(mappingPredicate: (T) -> Pair<Boolean, R?>): R? {
|
||||
for (element in this) {
|
||||
val (found, mapped) = mappingPredicate(element)
|
||||
if (found) return mapped
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
// filter-like function, takes list of propmappers, bound to properties of concrete objects, iterates over receiver, extract matching values via appropriate
|
||||
// mappers into bound properties; if restParser is given, adds all non-matching elements to it, otherwise return them as an iterable
|
||||
// note bound properties mutation!
|
||||
fun Iterable<String>.filterExtractProps(propMappers: List<PropMapper<*, *, *>>, prefix: String, restParser: RestPropMapper<*, *>? = null): Iterable<String> {
|
||||
|
||||
val iter = iterator()
|
||||
val rest = arrayListOf<String>()
|
||||
|
||||
while (iter.hasNext()) {
|
||||
val param = iter.next()
|
||||
val (propMapper, matchingOption) = propMappers.findWithTransform { mapper ->
|
||||
mapper.names
|
||||
.firstOrNull { param.startsWith(prefix + it) }
|
||||
.let { Pair(it != null, Pair(mapper, it)) }
|
||||
} ?: Pair(null, null)
|
||||
|
||||
when {
|
||||
propMapper != null -> {
|
||||
val optionLength = prefix.length + matchingOption!!.length
|
||||
when {
|
||||
propMapper is BoolPropMapper<*, *> -> {
|
||||
if (param.length > optionLength)
|
||||
throw IllegalArgumentException("Invalid switch option '$param', expecting $prefix$matchingOption without arguments")
|
||||
propMapper.apply("")
|
||||
}
|
||||
param.length > optionLength ->
|
||||
if (param[optionLength] != '=') {
|
||||
if (propMapper.mergeDelimiter == null)
|
||||
throw IllegalArgumentException("Invalid option syntax '$param', expecting $prefix$matchingOption[= ]<arg>")
|
||||
propMapper.apply(param.substring(optionLength))
|
||||
}
|
||||
else {
|
||||
propMapper.apply(param.substring(optionLength + 1))
|
||||
}
|
||||
else -> {
|
||||
if (!iter.hasNext()) throw IllegalArgumentException("Expecting argument for the option $prefix$matchingOption")
|
||||
propMapper.apply(iter.next())
|
||||
}
|
||||
}
|
||||
}
|
||||
restParser != null && param.startsWith(prefix) ->
|
||||
restParser.add(param.removePrefix(prefix))
|
||||
else -> rest.add(param)
|
||||
}
|
||||
}
|
||||
return rest
|
||||
}
|
||||
|
||||
|
||||
fun String.trimQuotes() = trim('"','\'')
|
||||
|
||||
|
||||
interface OptionsGroup : Serializable {
|
||||
val mappers: List<PropMapper<*, *, *>>
|
||||
}
|
||||
|
||||
fun Iterable<String>.filterExtractProps(vararg groups: OptionsGroup, prefix: String): Iterable<String> =
|
||||
filterExtractProps(groups.flatMap { it.mappers }, prefix)
|
||||
|
||||
|
||||
data class DaemonJVMOptions(
|
||||
var maxMemory: String = "",
|
||||
var maxPermSize: String = "",
|
||||
var reservedCodeCacheSize: String = "",
|
||||
var jvmParams: MutableCollection<String> = arrayListOf()
|
||||
) : OptionsGroup {
|
||||
|
||||
override val mappers: List<PropMapper<*, *, *>>
|
||||
get() = listOf(StringPropMapper(this, DaemonJVMOptions::maxMemory, listOf("Xmx"), mergeDelimiter = ""),
|
||||
StringPropMapper(this, DaemonJVMOptions::maxPermSize, listOf("XX:MaxPermSize"), mergeDelimiter = "="),
|
||||
StringPropMapper(this, DaemonJVMOptions::reservedCodeCacheSize, listOf("XX:ReservedCodeCacheSize"), mergeDelimiter = "="),
|
||||
restMapper)
|
||||
|
||||
val restMapper: RestPropMapper<*, *>
|
||||
get() = RestPropMapper(this, DaemonJVMOptions::jvmParams)
|
||||
}
|
||||
|
||||
|
||||
data class DaemonOptions(
|
||||
var runFilesPath: String = COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH,
|
||||
var autoshutdownMemoryThreshold: Long = COMPILE_DAEMON_MEMORY_THRESHOLD_INFINITE,
|
||||
var autoshutdownIdleSeconds: Int = COMPILE_DAEMON_DEFAULT_IDLE_TIMEOUT_S,
|
||||
var autoshutdownUnusedSeconds: Int = COMPILE_DAEMON_DEFAULT_UNUSED_TIMEOUT_S,
|
||||
var shutdownDelayMilliseconds: Long = COMPILE_DAEMON_DEFAULT_SHUTDOWN_DELAY_MS,
|
||||
var forceShutdownTimeoutMilliseconds: Long = COMPILE_DAEMON_FORCE_SHUTDOWN_DEFAULT_TIMEOUT_MS,
|
||||
var verbose: Boolean = false,
|
||||
var reportPerf: Boolean = false
|
||||
) : OptionsGroup {
|
||||
|
||||
override val mappers: List<PropMapper<*, *, *>>
|
||||
get() = listOf(PropMapper(this, DaemonOptions::runFilesPath, fromString = { it.trimQuotes() }),
|
||||
PropMapper(this, DaemonOptions::autoshutdownMemoryThreshold, fromString = { it.toLong() }, skipIf = { it == 0L }, mergeDelimiter = "="),
|
||||
// TODO: implement "use default" value without specifying default, so if client and server uses different defaults, it should not lead to many params in the cmd line; use 0 for it and used different val for infinite
|
||||
PropMapper(this, DaemonOptions::autoshutdownIdleSeconds, fromString = { it.toInt() }, skipIf = { it == 0 }, mergeDelimiter = "="),
|
||||
PropMapper(this, DaemonOptions::autoshutdownUnusedSeconds, fromString = { it.toInt() }, skipIf = { it == COMPILE_DAEMON_DEFAULT_UNUSED_TIMEOUT_S }, mergeDelimiter = "="),
|
||||
PropMapper(this, DaemonOptions::shutdownDelayMilliseconds, fromString = { it.toLong() }, skipIf = { it == COMPILE_DAEMON_DEFAULT_SHUTDOWN_DELAY_MS }, mergeDelimiter = "="),
|
||||
PropMapper(this, DaemonOptions::forceShutdownTimeoutMilliseconds, fromString = { it.toLong() }, skipIf = { it == COMPILE_DAEMON_FORCE_SHUTDOWN_DEFAULT_TIMEOUT_MS }, mergeDelimiter = "="),
|
||||
BoolPropMapper(this, DaemonOptions::verbose),
|
||||
BoolPropMapper(this, DaemonOptions::reportPerf))
|
||||
}
|
||||
|
||||
// TODO: consider implementing generic approach to it or may be replace getters with ones returning default if necessary
|
||||
val DaemonOptions.runFilesPathOrDefault: String
|
||||
get() = if (runFilesPath.isBlank()) COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH else runFilesPath
|
||||
|
||||
|
||||
fun Iterable<String>.distinctStringsDigest(): ByteArray =
|
||||
MessageDigest.getInstance(CLASSPATH_ID_DIGEST)
|
||||
.digest(this.distinct().sorted().joinToString("").toByteArray())
|
||||
|
||||
fun ByteArray.toHexString(): String = joinToString("", transform = { "%02x".format(it) })
|
||||
|
||||
|
||||
data class CompilerId(
|
||||
var compilerClasspath: List<String> = listOf(),
|
||||
var compilerVersion: String = ""
|
||||
) : OptionsGroup {
|
||||
|
||||
override val mappers: List<PropMapper<*, *, *>>
|
||||
get() = listOf(PropMapper(this, CompilerId::compilerClasspath, toString = { it.joinToString(File.pathSeparator) }, fromString = { it.trimQuotes().split(File.pathSeparator) }),
|
||||
StringPropMapper(this, CompilerId::compilerVersion))
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun makeCompilerId(vararg paths: File): CompilerId = makeCompilerId(paths.asIterable())
|
||||
|
||||
@JvmStatic
|
||||
fun makeCompilerId(paths: Iterable<File>): CompilerId =
|
||||
CompilerId(compilerClasspath = paths.map { it.absolutePath })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun isDaemonEnabled(): Boolean = System.getProperty(COMPILE_DAEMON_ENABLED_PROPERTY) != null
|
||||
|
||||
|
||||
fun configureDaemonJVMOptions(opts: DaemonJVMOptions,
|
||||
vararg additionalParams: String,
|
||||
inheritMemoryLimits: Boolean,
|
||||
inheritAdditionalProperties: Boolean
|
||||
): DaemonJVMOptions {
|
||||
// note: sequence matters, explicit override in COMPILE_DAEMON_JVM_OPTIONS_PROPERTY should be done after inputArguments processing
|
||||
if (inheritMemoryLimits) {
|
||||
ManagementFactory.getRuntimeMXBean().inputArguments.filterExtractProps(opts.mappers, "-")
|
||||
}
|
||||
System.getProperty(COMPILE_DAEMON_JVM_OPTIONS_PROPERTY)?.let {
|
||||
opts.jvmParams.addAll(
|
||||
it.trimQuotes()
|
||||
.split("(?<!\\\\),".toRegex()) // using independent non-capturing group with negative lookahead zero length assertion to split only on non-escaped commas
|
||||
.map { it.replace("\\\\(.)".toRegex(), "$1") } // de-escaping characters escaped by backslash, straightforward, without exceptions
|
||||
.filterExtractProps(opts.mappers, "-", opts.restMapper))
|
||||
}
|
||||
|
||||
opts.jvmParams.addAll(additionalParams)
|
||||
if (inheritAdditionalProperties) {
|
||||
System.getProperty(COMPILE_DAEMON_LOG_PATH_PROPERTY)?.let { opts.jvmParams.add("D${COMPILE_DAEMON_LOG_PATH_PROPERTY}=\"$it\"") }
|
||||
System.getProperty(KOTLIN_COMPILER_ENVIRONMENT_KEEPALIVE_PROPERTY)?.let { opts.jvmParams.add("D${KOTLIN_COMPILER_ENVIRONMENT_KEEPALIVE_PROPERTY}") }
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
|
||||
fun configureDaemonJVMOptions(vararg additionalParams: String,
|
||||
inheritMemoryLimits: Boolean,
|
||||
inheritAdditionalProperties: Boolean
|
||||
): DaemonJVMOptions =
|
||||
configureDaemonJVMOptions(DaemonJVMOptions(),
|
||||
additionalParams = *additionalParams,
|
||||
inheritMemoryLimits = inheritMemoryLimits,
|
||||
inheritAdditionalProperties = inheritAdditionalProperties)
|
||||
|
||||
|
||||
fun configureDaemonOptions(opts: DaemonOptions): DaemonOptions {
|
||||
System.getProperty(COMPILE_DAEMON_OPTIONS_PROPERTY)?.let {
|
||||
val unrecognized = it.trimQuotes().split(",").filterExtractProps(opts.mappers, "")
|
||||
if (unrecognized.any())
|
||||
throw IllegalArgumentException(
|
||||
"Unrecognized daemon options passed via property ${COMPILE_DAEMON_OPTIONS_PROPERTY}: " + unrecognized.joinToString(" ") +
|
||||
"\nSupported options: " + opts.mappers.joinToString(", ", transform = { it.names.first() }))
|
||||
}
|
||||
System.getProperty(COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY)?.let { opts.verbose = true }
|
||||
System.getProperty(COMPILE_DAEMON_REPORT_PERF_PROPERTY)?.let { opts.reportPerf = true }
|
||||
return opts
|
||||
}
|
||||
|
||||
|
||||
fun configureDaemonOptions(): DaemonOptions = configureDaemonOptions(DaemonOptions())
|
||||
|
||||
|
||||
private val humanizedMemorySizeRegex = "(\\d+)([kmg]?)".toRegex()
|
||||
|
||||
private fun String.memToBytes(): Long? =
|
||||
humanizedMemorySizeRegex
|
||||
.matchEntire(this.trim().toLowerCase())
|
||||
?.groups?.let { match ->
|
||||
match[1]?.value?.let {
|
||||
it.toLong() *
|
||||
when (match[2]?.value) {
|
||||
"k" -> 1 shl 10
|
||||
"m" -> 1 shl 20
|
||||
"g" -> 1 shl 30
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val daemonJVMOptionsMemoryProps =
|
||||
listOf(DaemonJVMOptions::maxMemory, DaemonJVMOptions::maxPermSize, DaemonJVMOptions::reservedCodeCacheSize)
|
||||
|
||||
infix fun DaemonJVMOptions.memorywiseFitsInto(other: DaemonJVMOptions): Boolean =
|
||||
daemonJVMOptionsMemoryProps
|
||||
.all { (it.get(this).memToBytes() ?: 0) <= (it.get(other).memToBytes() ?: 0) }
|
||||
|
||||
fun compareDaemonJVMOptionsMemory(left: DaemonJVMOptions, right: DaemonJVMOptions): Int {
|
||||
val props = daemonJVMOptionsMemoryProps.map { Pair(it.get(left).memToBytes() ?: 0, it.get(right).memToBytes() ?: 0) }
|
||||
return when {
|
||||
props.all { it.first == it.second } -> 0
|
||||
props.all { it.first <= it.second } -> -1
|
||||
props.all { it.first >= it.second } -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
class DaemonJVMOptionsMemoryComparator : Comparator<DaemonJVMOptions> {
|
||||
override fun compare(left: DaemonJVMOptions, right: DaemonJVMOptions): Int = compareDaemonJVMOptionsMemory(left, right)
|
||||
}
|
||||
|
||||
|
||||
fun DaemonJVMOptions.updateMemoryUpperBounds(other: DaemonJVMOptions): DaemonJVMOptions {
|
||||
daemonJVMOptionsMemoryProps
|
||||
.forEach { if ((it.get(this).memToBytes() ?: 0) < (it.get(other).memToBytes() ?: 0)) it.set(this, it.get(other)) }
|
||||
return this
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.common
|
||||
|
||||
import java.io.File
|
||||
|
||||
enum class OSKind {
|
||||
Windows,
|
||||
OSX,
|
||||
Unix,
|
||||
Unknown;
|
||||
|
||||
companion object {
|
||||
val current: OSKind = System.getProperty("os.name").toLowerCase().let {
|
||||
when {
|
||||
// partly taken from http://www.code4copy.com/java/post/detecting-os-type-in-java
|
||||
it.startsWith("windows") -> Windows
|
||||
it.startsWith("mac os") -> OSX
|
||||
it.contains("unix") -> Unix
|
||||
it.startsWith("linux") -> Unix
|
||||
it.contains("bsd") -> Unix
|
||||
it.startsWith("irix") -> Unix
|
||||
it.startsWith("mpe/ix") -> Unix
|
||||
it.startsWith("aix") -> Unix
|
||||
it.startsWith("hp-ux") -> Unix
|
||||
it.startsWith("sunos") -> Unix
|
||||
it.startsWith("sun os") -> Unix
|
||||
it.startsWith("solaris") -> Unix
|
||||
else -> Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.orDefault(v: String): String =
|
||||
if (this == null || this.isBlank()) v else this
|
||||
|
||||
// Note links to OS recommendations for storing various kinds of files
|
||||
// Windows: http://www.microsoft.com/security/portal/mmpc/shared/variables.aspx
|
||||
// unix (freedesktop): http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
// OS X: https://developer.apple.com/library/mac/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/AccessingFilesandDirectories/AccessingFilesandDirectories.html
|
||||
|
||||
object FileSystem {
|
||||
|
||||
val userHomePath: String get() = System.getProperty("user.home")
|
||||
val tempPath: String get() = System.getProperty("java.io.tmpdir")
|
||||
|
||||
val logFilesPath: String get() = tempPath
|
||||
|
||||
val runtimeStateFilesBasePath: String get() = when (OSKind.current) {
|
||||
OSKind.Windows -> System.getenv("LOCALAPPDATA").orDefault(tempPath)
|
||||
OSKind.OSX -> userHomePath + "/Library/Application Support"
|
||||
OSKind.Unix -> System.getenv("XDG_DATA_HOME").orDefault(userHomePath + "/.local/share")
|
||||
OSKind.Unknown -> tempPath
|
||||
}
|
||||
|
||||
fun getRuntimeStateFilesPath(vararg names: String): String {
|
||||
assert(names.any())
|
||||
val base = File(runtimeStateFilesBasePath)
|
||||
// if base is not suitable, take home dir as a base and ensure the first name is prefixed with "." -
|
||||
// this will work ok as a fallback solution on most systems
|
||||
val dir = if (base.exists() && base.isDirectory) names.fold(base, { r, v -> File(r, v) })
|
||||
else names.drop(1)
|
||||
.fold(File(userHomePath, names.first().let { if (it.startsWith(".")) it else ".$it" }),
|
||||
{ r, v -> File(r, v) })
|
||||
return if ((dir.exists() && dir.isDirectory) || dir.mkdirs()) dir.absolutePath
|
||||
else tempPath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.common
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.Serializable
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.rmi.RemoteException
|
||||
import java.rmi.registry.LocateRegistry
|
||||
import java.rmi.registry.Registry
|
||||
import java.rmi.server.RMIClientSocketFactory
|
||||
import java.rmi.server.RMIServerSocketFactory
|
||||
import java.util.*
|
||||
|
||||
|
||||
val SOCKET_ANY_FREE_PORT = 0
|
||||
|
||||
object LoopbackNetworkInterface {
|
||||
|
||||
val IPV4_LOOPBACK_INET_ADDRESS = "127.0.0.1"
|
||||
val IPV6_LOOPBACK_INET_ADDRESS = "::1"
|
||||
|
||||
val SERVER_SOCKET_BACKLOG_SIZE = 10 // size of the requests queue for daemon services, so far seems that we don't need any big numbers here
|
||||
// but if we'll start getting "connection refused" errors, that could be the first place to try to fix it
|
||||
|
||||
val serverLoopbackSocketFactory by lazy { ServerLoopbackSocketFactory() }
|
||||
val clientLoopbackSocketFactory by lazy { ClientLoopbackSocketFactory() }
|
||||
|
||||
// TODO switch to InetAddress.getLoopbackAddress on java 7+
|
||||
val loopbackInetAddressName by lazy {
|
||||
try {
|
||||
if (InetAddress.getLocalHost() is Inet6Address) IPV6_LOOPBACK_INET_ADDRESS else IPV4_LOOPBACK_INET_ADDRESS
|
||||
}
|
||||
catch (e: IOException) {
|
||||
// getLocalHost may fail for unknown reasons in some situations, the fallback is to assume IPv4 for now
|
||||
// TODO consider some other ways to detect default to IPv6 addresses in this case
|
||||
IPV4_LOOPBACK_INET_ADDRESS
|
||||
}
|
||||
}
|
||||
|
||||
// base socket factories by default don't implement equals properly (see e.g. http://stackoverflow.com/questions/21555710/rmi-and-jmx-socket-factories)
|
||||
// so implementing it in derived classes using the fact that they are singletons
|
||||
|
||||
class ServerLoopbackSocketFactory : RMIServerSocketFactory, Serializable {
|
||||
override fun equals(other: Any?): Boolean = other === this || super.equals(other)
|
||||
override fun hashCode(): Int = super.hashCode()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun createServerSocket(port: Int): ServerSocket = ServerSocket(port, SERVER_SOCKET_BACKLOG_SIZE, InetAddress.getByName(loopbackInetAddressName))
|
||||
}
|
||||
|
||||
|
||||
class ClientLoopbackSocketFactory : RMIClientSocketFactory, Serializable {
|
||||
override fun equals(other: Any?): Boolean = other === this || super.equals(other)
|
||||
override fun hashCode(): Int = super.hashCode()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun createSocket(host: String, port: Int): Socket = Socket(InetAddress.getByName(loopbackInetAddressName), port)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val portSelectionRng = Random()
|
||||
|
||||
fun findPortAndCreateRegistry(attempts: Int, portRangeStart: Int, portRangeEnd: Int) : Pair<Registry, Int> {
|
||||
var i = 0
|
||||
var lastException: RemoteException? = null
|
||||
|
||||
while (i++ < attempts) {
|
||||
val port = portSelectionRng.nextInt(portRangeEnd - portRangeStart) + portRangeStart
|
||||
try {
|
||||
return Pair(LocateRegistry.createRegistry(port, LoopbackNetworkInterface.clientLoopbackSocketFactory, LoopbackNetworkInterface.serverLoopbackSocketFactory), port)
|
||||
}
|
||||
catch (e: RemoteException) {
|
||||
// assuming that the port is already taken
|
||||
lastException = e
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("Cannot find free port in $attempts attempts", lastException)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.common
|
||||
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.lang.management.ThreadMXBean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
interface PerfCounters {
|
||||
val count: Long
|
||||
val time: Long
|
||||
val threadTime: Long
|
||||
val threadUserTime: Long
|
||||
val memory: Long
|
||||
|
||||
fun addMeasurement(time: Long = 0, thread: Long = 0, threadUser: Long = 0, memory: Long = 0)
|
||||
}
|
||||
|
||||
interface Profiler {
|
||||
fun getCounters(): Map<Any?, PerfCounters>
|
||||
fun getTotalCounters(): PerfCounters
|
||||
|
||||
fun<R> withMeasure(obj: Any?, body: () -> R): R
|
||||
}
|
||||
|
||||
|
||||
open class SimplePerfCounters : PerfCounters {
|
||||
private val _count: AtomicLong = AtomicLong(0L)
|
||||
private val _time: AtomicLong = AtomicLong(0L)
|
||||
private val _threadTime: AtomicLong = AtomicLong(0L)
|
||||
private val _threadUserTime: AtomicLong = AtomicLong(0L)
|
||||
private val _memory: AtomicLong = AtomicLong(0L)
|
||||
|
||||
override val count: Long get() = _count.get()
|
||||
override val time: Long get() = _time.get()
|
||||
override val threadTime: Long get() = _threadTime.get()
|
||||
override val threadUserTime: Long get() = _threadUserTime.get()
|
||||
override val memory: Long get() = _memory.get()
|
||||
|
||||
override fun addMeasurement(time: Long, thread: Long, threadUser: Long, memory: Long) {
|
||||
_count.incrementAndGet()
|
||||
_time.addAndGet(time)
|
||||
_threadTime.addAndGet(thread)
|
||||
_threadUserTime.addAndGet(threadUser)
|
||||
_memory.addAndGet(memory)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SimplePerfCountersWithTotal(val totalRef: PerfCounters) : SimplePerfCounters() {
|
||||
override fun addMeasurement(time: Long, thread: Long, threadUser: Long, memory: Long) {
|
||||
super.addMeasurement(time, thread, threadUser, memory)
|
||||
totalRef.addMeasurement(time, thread, threadUser, memory)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun ThreadMXBean.threadCpuTime() = if (isCurrentThreadCpuTimeSupported) currentThreadCpuTime else 0L
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun ThreadMXBean.threadUserTime() = if (isCurrentThreadCpuTimeSupported) currentThreadUserTime else 0L
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun usedMemory(withGC: Boolean): Long {
|
||||
if (withGC) {
|
||||
System.gc()
|
||||
}
|
||||
val rt = Runtime.getRuntime()
|
||||
return (rt.totalMemory() - rt.freeMemory())
|
||||
}
|
||||
|
||||
|
||||
inline fun<R> withMeasureWallTime(perfCounters: PerfCounters, body: () -> R): R {
|
||||
val startTime = System.nanoTime()
|
||||
val res = body()
|
||||
perfCounters.addMeasurement(time = System.nanoTime() - startTime) // TODO: add support for time wrapping
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
inline fun<R> withMeasureWallAndThreadTimes(perfCounters: PerfCounters, threadMXBean: ThreadMXBean, body: () -> R): R {
|
||||
val startTime = System.nanoTime()
|
||||
val startThreadTime = threadMXBean.threadCpuTime()
|
||||
val startThreadUserTime = threadMXBean.threadUserTime()
|
||||
|
||||
val res = body()
|
||||
|
||||
// TODO: add support for time wrapping
|
||||
perfCounters.addMeasurement(time = System.nanoTime() - startTime,
|
||||
thread = threadMXBean.threadCpuTime() - startThreadTime,
|
||||
threadUser = threadMXBean.threadUserTime() - startThreadUserTime)
|
||||
return res
|
||||
}
|
||||
|
||||
inline fun<R> withMeasureWallAndThreadTimes(perfCounters: PerfCounters, body: () -> R): R = withMeasureWallAndThreadTimes(perfCounters, ManagementFactory.getThreadMXBean(), body)
|
||||
|
||||
|
||||
inline fun<R> withMeasureWallAndThreadTimesAndMemory(perfCounters: PerfCounters, withGC: Boolean = false, threadMXBean: ThreadMXBean, body: () -> R): R {
|
||||
val startMem = usedMemory(withGC)
|
||||
val startTime = System.nanoTime()
|
||||
val startThreadTime = threadMXBean.threadCpuTime()
|
||||
val startThreadUserTime = threadMXBean.threadUserTime()
|
||||
|
||||
val res = body()
|
||||
|
||||
// TODO: add support for time wrapping
|
||||
perfCounters.addMeasurement(time = System.nanoTime() - startTime,
|
||||
thread = threadMXBean.threadCpuTime() - startThreadTime,
|
||||
threadUser = threadMXBean.threadUserTime() - startThreadUserTime,
|
||||
memory = usedMemory(withGC) - startMem)
|
||||
return res
|
||||
}
|
||||
|
||||
inline fun<R> withMeasureWallAndThreadTimesAndMemory(perfCounters: PerfCounters, withGC: Boolean, body: () -> R): R =
|
||||
withMeasureWallAndThreadTimesAndMemory(perfCounters, withGC, ManagementFactory.getThreadMXBean(), body)
|
||||
|
||||
|
||||
class DummyProfiler : Profiler {
|
||||
override fun getCounters(): Map<Any?, PerfCounters> = mapOf(null to SimplePerfCounters())
|
||||
override fun getTotalCounters(): PerfCounters = SimplePerfCounters()
|
||||
|
||||
override final inline fun <R> withMeasure(obj: Any?, body: () -> R): R = body()
|
||||
}
|
||||
|
||||
|
||||
abstract class TotalProfiler : Profiler {
|
||||
|
||||
val total = SimplePerfCounters()
|
||||
val threadMXBean = ManagementFactory.getThreadMXBean()
|
||||
|
||||
override fun getCounters(): Map<Any?, PerfCounters> = mapOf()
|
||||
override fun getTotalCounters(): PerfCounters = total
|
||||
}
|
||||
|
||||
|
||||
class WallTotalProfiler : TotalProfiler() {
|
||||
override final inline fun <R> withMeasure(obj: Any?, body: () -> R): R = withMeasureWallTime(total, body)
|
||||
}
|
||||
|
||||
|
||||
class WallAndThreadTotalProfiler : TotalProfiler() {
|
||||
override final inline fun <R> withMeasure(obj: Any?, body: () -> R): R = withMeasureWallAndThreadTimes(total, threadMXBean, body)
|
||||
}
|
||||
|
||||
|
||||
class WallAndThreadAndMemoryTotalProfiler(val withGC: Boolean) : TotalProfiler() {
|
||||
override final inline fun <R> withMeasure(obj: Any?, body: () -> R): R = withMeasureWallAndThreadTimesAndMemory(total, withGC, threadMXBean, body)
|
||||
}
|
||||
|
||||
|
||||
class WallAndThreadByClassProfiler() : TotalProfiler() {
|
||||
|
||||
val counters = hashMapOf<Any?, SimplePerfCountersWithTotal>()
|
||||
|
||||
override fun getCounters(): Map<Any?, PerfCounters> = counters
|
||||
|
||||
override final inline fun <R> withMeasure(obj: Any?, body: () -> R): R =
|
||||
withMeasureWallAndThreadTimes(counters.getOrPut(obj?.javaClass?.name, { SimplePerfCountersWithTotal(total) }), threadMXBean, body)
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.common
|
||||
|
||||
import java.rmi.Remote
|
||||
import java.rmi.RemoteException
|
||||
|
||||
interface RemoteOperationsTracer : Remote {
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun before(id: String)
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun after(id: String)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.common
|
||||
|
||||
import java.rmi.Remote
|
||||
import java.rmi.RemoteException
|
||||
|
||||
interface RemoteOutputStream : Remote {
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun close()
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun write(data: ByteArray, offset: Int, length: Int)
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun write(dataByte: Int)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="kotlin-runtime" level="project" />
|
||||
<orderEntry type="module" module-name="cli" />
|
||||
<orderEntry type="module" module-name="daemon-common" />
|
||||
<orderEntry type="module" module-name="frontend.java" />
|
||||
<orderEntry type="module" module-name="util.runtime" />
|
||||
<orderEntry type="module" module-name="util" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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
|
||||
|
||||
import org.jetbrains.kotlin.cli.common.CLICompiler
|
||||
import org.jetbrains.kotlin.cli.js.K2JSCompiler
|
||||
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
|
||||
import org.jetbrains.kotlin.daemon.common.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.io.PrintStream
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.net.URLClassLoader
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.jar.Manifest
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogManager
|
||||
import java.util.logging.Logger
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
val DAEMON_PERIODIC_CHECK_INTERVAL_MS = 1000L
|
||||
|
||||
|
||||
class LogStream(name: String) : OutputStream() {
|
||||
|
||||
val log by lazy { Logger.getLogger(name) }
|
||||
|
||||
val lineBuf = StringBuilder()
|
||||
|
||||
override fun write(byte: Int) {
|
||||
if (byte.toChar() == '\n') flush()
|
||||
else lineBuf.append(byte.toChar())
|
||||
}
|
||||
|
||||
override fun flush() {
|
||||
log.info(lineBuf.toString())
|
||||
lineBuf.setLength(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object CompileDaemon {
|
||||
|
||||
init {
|
||||
val logTime: String = SimpleDateFormat("yyyy-MM-dd.HH-mm-ss-SSS").format(Date())
|
||||
val (logPath: String, fileIsGiven: Boolean) =
|
||||
System.getProperty(COMPILE_DAEMON_LOG_PATH_PROPERTY)?.trimQuotes()?.let { Pair(it, File(it).isFile) } ?: Pair("%t", false)
|
||||
val cfg: String =
|
||||
"handlers = java.util.logging.FileHandler\n" +
|
||||
"java.util.logging.FileHandler.level = ALL\n" +
|
||||
"java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter\n" +
|
||||
"java.util.logging.FileHandler.encoding = UTF-8\n" +
|
||||
"java.util.logging.FileHandler.limit = ${if (fileIsGiven) 0 else (1 shl 20)}\n" + // if file is provided - disabled, else - 1Mb
|
||||
"java.util.logging.FileHandler.count = ${if (fileIsGiven) 1 else 3}\n" +
|
||||
"java.util.logging.FileHandler.append = $fileIsGiven\n" +
|
||||
"java.util.logging.FileHandler.pattern = ${if (fileIsGiven) logPath else (logPath + File.separator + "${COMPILE_DAEMON_DEFAULT_FILES_PREFIX}.$logTime.%u%g.log")}\n" +
|
||||
"java.util.logging.SimpleFormatter.format = %1\$tF %1\$tT.%1\$tL [%3\$s] %4\$s: %5\$s\\n\n"
|
||||
|
||||
LogManager.getLogManager().readConfiguration(cfg.byteInputStream())
|
||||
}
|
||||
|
||||
val log by lazy { Logger.getLogger("daemon") }
|
||||
|
||||
private fun loadVersionFromResource(): String? {
|
||||
(CompileDaemon::class.java.classLoader as? URLClassLoader)
|
||||
?.findResource("META-INF/MANIFEST.MF")
|
||||
?.let {
|
||||
try {
|
||||
return Manifest(it.openStream()).mainAttributes.getValue("Implementation-Version") ?: null
|
||||
}
|
||||
catch (e: IOException) {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
|
||||
log.info("Kotlin compiler daemon version " + (loadVersionFromResource() ?: "<unknown>"))
|
||||
log.info("daemon JVM args: " + ManagementFactory.getRuntimeMXBean().inputArguments.joinToString(" "))
|
||||
log.info("daemon args: " + args.joinToString(" "))
|
||||
|
||||
val compilerId = CompilerId()
|
||||
val daemonOptions = DaemonOptions()
|
||||
|
||||
try {
|
||||
val daemonJVMOptions = configureDaemonJVMOptions(inheritMemoryLimits = true, inheritAdditionalProperties = true)
|
||||
|
||||
val filteredArgs = args.asIterable().filterExtractProps(compilerId, daemonOptions, prefix = COMPILE_DAEMON_CMDLINE_OPTIONS_PREFIX)
|
||||
|
||||
if (filteredArgs.any()) {
|
||||
val helpLine = "usage: <daemon> <compilerId options> <daemon options>"
|
||||
log.info(helpLine)
|
||||
println(helpLine)
|
||||
throw IllegalArgumentException("Unknown arguments: " + filteredArgs.joinToString(" "))
|
||||
}
|
||||
|
||||
log.info("starting daemon")
|
||||
|
||||
// TODO: find minimal set of permissions and restore security management
|
||||
// note: may be not needed anymore since (hopefully) server is now loopback-only
|
||||
// if (System.getSecurityManager() == null)
|
||||
// System.setSecurityManager (RMISecurityManager())
|
||||
//
|
||||
// setDaemonPermissions(daemonOptions.port)
|
||||
|
||||
val (registry, port) = findPortAndCreateRegistry(COMPILE_DAEMON_FIND_PORT_ATTEMPTS, COMPILE_DAEMON_PORTS_RANGE_START, COMPILE_DAEMON_PORTS_RANGE_END)
|
||||
|
||||
val compilerSelector = object : CompilerSelector {
|
||||
private val jvm by lazy { K2JVMCompiler() }
|
||||
private val js by lazy { K2JSCompiler() }
|
||||
override fun get(targetPlatform: CompileService.TargetPlatform): CLICompiler<*> = when (targetPlatform) {
|
||||
CompileService.TargetPlatform.JVM -> jvm
|
||||
CompileService.TargetPlatform.JS -> js
|
||||
}
|
||||
}
|
||||
// timer with a daemon thread, meaning it should not prevent JVM to exit normally
|
||||
val timer = Timer(true)
|
||||
val compilerService = CompileServiceImpl(registry = registry,
|
||||
compiler = compilerSelector,
|
||||
compilerId = compilerId,
|
||||
daemonOptions = daemonOptions,
|
||||
daemonJVMOptions = daemonJVMOptions,
|
||||
port = port,
|
||||
timer = timer,
|
||||
onShutdown = {
|
||||
if (daemonOptions.forceShutdownTimeoutMilliseconds != COMPILE_DAEMON_TIMEOUT_INFINITE_MS) {
|
||||
// running a watcher thread that ensures that if the daemon is not exited normally (may be due to RMI leftovers), it's forced to exit
|
||||
timer.schedule(daemonOptions.forceShutdownTimeoutMilliseconds) {
|
||||
cancel()
|
||||
log.info("force JVM shutdown")
|
||||
System.exit(0)
|
||||
}
|
||||
}
|
||||
else {
|
||||
timer.cancel()
|
||||
}
|
||||
})
|
||||
|
||||
if (daemonOptions.runFilesPath.isNotEmpty())
|
||||
println(daemonOptions.runFilesPath)
|
||||
|
||||
// this supposed to stop redirected streams reader(s) on the client side and prevent some situations with hanging threads, but doesn't work reliably
|
||||
// TODO: implement more reliable scheme
|
||||
System.out.close()
|
||||
System.err.close()
|
||||
|
||||
System.setErr(PrintStream(LogStream("stderr")))
|
||||
System.setOut(PrintStream(LogStream("stdout")))
|
||||
}
|
||||
catch (e: Exception) {
|
||||
System.err.println("Exception: " + e.message)
|
||||
e.printStackTrace(System.err)
|
||||
// repeating it to log for the cases when stderr is not redirected yet
|
||||
log.log(Level.INFO, "Exception: ", e)
|
||||
// TODO consider exiting without throwing
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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
|
||||
|
||||
import com.intellij.openapi.vfs.impl.ZipHandler
|
||||
import org.jetbrains.kotlin.cli.common.CLICompiler
|
||||
import org.jetbrains.kotlin.cli.common.ExitCode
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
|
||||
import org.jetbrains.kotlin.config.Services
|
||||
import org.jetbrains.kotlin.daemon.common.*
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
|
||||
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.check
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
import java.rmi.NoSuchObjectException
|
||||
import java.rmi.registry.Registry
|
||||
import java.rmi.server.UnicastRemoteObject
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.concurrent.write
|
||||
|
||||
fun nowSeconds() = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime())
|
||||
|
||||
interface CompilerSelector {
|
||||
operator fun get(targetPlatform: CompileService.TargetPlatform): CLICompiler<*>
|
||||
}
|
||||
|
||||
interface EventManger {
|
||||
fun onCompilationFinished(f : () -> Unit)
|
||||
}
|
||||
|
||||
private class EventMangerImpl : EventManger {
|
||||
private val onCompilationFinished = arrayListOf<() -> Unit>()
|
||||
|
||||
override fun onCompilationFinished(f: () -> Unit) {
|
||||
onCompilationFinished.add(f)
|
||||
}
|
||||
|
||||
fun fireCompilationFinished() {
|
||||
onCompilationFinished.forEach { it() }
|
||||
}
|
||||
}
|
||||
|
||||
class CompileServiceImpl(
|
||||
val registry: Registry,
|
||||
val compiler: CompilerSelector,
|
||||
val compilerId: CompilerId,
|
||||
val daemonOptions: DaemonOptions,
|
||||
val daemonJVMOptions: DaemonJVMOptions,
|
||||
val port: Int,
|
||||
val timer: Timer,
|
||||
val onShutdown: () -> Unit
|
||||
) : CompileService {
|
||||
|
||||
// wrapped in a class to encapsulate alive check logic
|
||||
private class ClientOrSessionProxy(val aliveFlagPath: String?) {
|
||||
val registered = nowSeconds()
|
||||
val secondsSinceRegistered: Long get() = nowSeconds() - registered
|
||||
val isAlive: Boolean get() = aliveFlagPath?.let { File(it).exists() } ?: true // assuming that if no file was given, the client is alive
|
||||
}
|
||||
|
||||
private val sessionsIdCounter = AtomicInteger(0)
|
||||
private val compilationsCounter = AtomicInteger(0)
|
||||
private val internalRng = Random()
|
||||
|
||||
private val classpathWatcher = LazyClasspathWatcher(compilerId.compilerClasspath)
|
||||
|
||||
enum class Aliveness {
|
||||
// !!! ordering of values is used in state comparison
|
||||
Dying, LastSession, Alive
|
||||
}
|
||||
|
||||
// TODO: encapsulate operations on state here
|
||||
private val state = object {
|
||||
|
||||
val clientProxies: MutableSet<ClientOrSessionProxy> = hashSetOf()
|
||||
val sessions: MutableMap<Int, ClientOrSessionProxy> = hashMapOf()
|
||||
|
||||
val delayedShutdownQueued = AtomicBoolean(false)
|
||||
|
||||
var alive = AtomicInteger(Aliveness.Alive.ordinal)
|
||||
}
|
||||
|
||||
@Volatile private var _lastUsedSeconds = nowSeconds()
|
||||
val lastUsedSeconds: Long get() = if (rwlock.isWriteLocked || rwlock.readLockCount - rwlock.readHoldCount > 0) nowSeconds() else _lastUsedSeconds
|
||||
|
||||
private val log by lazy { Logger.getLogger("compiler") }
|
||||
|
||||
private val rwlock = ReentrantReadWriteLock()
|
||||
|
||||
private var runFile: File
|
||||
|
||||
init {
|
||||
val runFileDir = File(daemonOptions.runFilesPathOrDefault)
|
||||
runFileDir.mkdirs()
|
||||
runFile = File(runFileDir,
|
||||
makeRunFilenameString(timestamp = "%tFT%<tH-%<tM-%<tS.%<tLZ".format(Calendar.getInstance(TimeZone.getTimeZone("Z"))),
|
||||
digest = compilerId.compilerClasspath.map { File(it).absolutePath }.distinctStringsDigest().toHexString(),
|
||||
port = port.toString()))
|
||||
try {
|
||||
if (!runFile.createNewFile()) throw Exception("createNewFile returned false")
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("Unable to create run file '${runFile.absolutePath}'", e)
|
||||
}
|
||||
runFile.deleteOnExit()
|
||||
}
|
||||
|
||||
// RMI-exposed API
|
||||
|
||||
override fun getDaemonOptions(): CompileService.CallResult<DaemonOptions> = ifAlive { daemonOptions }
|
||||
|
||||
override fun getDaemonJVMOptions(): CompileService.CallResult<DaemonJVMOptions> = ifAlive { daemonJVMOptions }
|
||||
|
||||
override fun registerClient(aliveFlagPath: String?): CompileService.CallResult<Nothing> = ifAlive_Nothing {
|
||||
synchronized(state.clientProxies) {
|
||||
state.clientProxies.add(ClientOrSessionProxy(aliveFlagPath))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getClients(): CompileService.CallResult<List<String>> = ifAlive {
|
||||
synchronized(state.clientProxies) {
|
||||
state.clientProxies.map { it.aliveFlagPath }.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: consider tying a session to a client and use this info to cleanup
|
||||
override fun leaseCompileSession(aliveFlagPath: String?): CompileService.CallResult<Int> = ifAlive(minAliveness = Aliveness.Alive) {
|
||||
// fighting hypothetical integer wrapping
|
||||
var newId = sessionsIdCounter.incrementAndGet()
|
||||
val session = ClientOrSessionProxy(aliveFlagPath)
|
||||
for (attempt in 1..100) {
|
||||
if (newId != CompileService.NO_SESSION) {
|
||||
synchronized(state.sessions) {
|
||||
if (!state.sessions.containsKey(newId)) {
|
||||
state.sessions.put(newId, session)
|
||||
log.info("leased a new session $newId, client alive file: $aliveFlagPath")
|
||||
return@ifAlive newId
|
||||
}
|
||||
}
|
||||
}
|
||||
// assuming wrap, jumping to random number to reduce probability of further clashes
|
||||
newId = sessionsIdCounter.addAndGet(internalRng.nextInt())
|
||||
}
|
||||
throw IllegalStateException("Invalid state or algorithm error")
|
||||
}
|
||||
|
||||
override fun releaseCompileSession(sessionId: Int) = ifAlive_Nothing(minAliveness = Aliveness.LastSession) {
|
||||
synchronized(state.sessions) {
|
||||
state.sessions.remove(sessionId)
|
||||
log.info("cleaning after session $sessionId")
|
||||
clearJarCache()
|
||||
if (state.sessions.isEmpty()) {
|
||||
// TODO: and some goes here
|
||||
}
|
||||
}
|
||||
timer.schedule(0) {
|
||||
periodicAndAfterSessionCheck()
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkCompilerId(expectedCompilerId: CompilerId): Boolean =
|
||||
(compilerId.compilerVersion.isEmpty() || compilerId.compilerVersion == expectedCompilerId.compilerVersion) &&
|
||||
(compilerId.compilerClasspath.all { expectedCompilerId.compilerClasspath.contains(it) }) &&
|
||||
!classpathWatcher.isChanged
|
||||
|
||||
override fun getUsedMemory(): CompileService.CallResult<Long> = ifAlive { usedMemory(withGC = true) }
|
||||
|
||||
override fun shutdown(): CompileService.CallResult<Nothing> = ifAliveExclusive_Nothing(minAliveness = Aliveness.LastSession, ignoreCompilerChanged = true) {
|
||||
shutdownImpl()
|
||||
}
|
||||
|
||||
override fun scheduleShutdown(graceful: Boolean): CompileService.CallResult<Boolean> = ifAlive(minAliveness = Aliveness.Alive) {
|
||||
if (!graceful || state.alive.compareAndSet(Aliveness.Alive.ordinal, Aliveness.LastSession.ordinal)) {
|
||||
timer.schedule(0) {
|
||||
ifAliveExclusive(minAliveness = Aliveness.LastSession, ignoreCompilerChanged = true) {
|
||||
if (!graceful || state.sessions.isEmpty()) {
|
||||
shutdownImpl()
|
||||
}
|
||||
else {
|
||||
log.info("Some sessions are active, waiting for them to finish")
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
else false
|
||||
}
|
||||
|
||||
override fun remoteCompile(sessionId: Int,
|
||||
targetPlatform: CompileService.TargetPlatform,
|
||||
args: Array<out String>,
|
||||
servicesFacade: CompilerCallbackServicesFacade,
|
||||
compilerOutputStream: RemoteOutputStream,
|
||||
outputFormat: CompileService.OutputFormat,
|
||||
serviceOutputStream: RemoteOutputStream,
|
||||
operationsTracer: RemoteOperationsTracer?
|
||||
): CompileService.CallResult<Int> =
|
||||
doCompile(sessionId, args, compilerOutputStream, serviceOutputStream, operationsTracer) { printStream, eventManager, profiler ->
|
||||
when (outputFormat) {
|
||||
CompileService.OutputFormat.PLAIN -> compiler[targetPlatform].exec(printStream, *args)
|
||||
CompileService.OutputFormat.XML -> compiler[targetPlatform].execAndOutputXml(printStream, createCompileServices(servicesFacade, eventManager, profiler), *args)
|
||||
}
|
||||
}
|
||||
|
||||
override fun remoteIncrementalCompile(sessionId: Int,
|
||||
targetPlatform: CompileService.TargetPlatform,
|
||||
args: Array<out String>,
|
||||
servicesFacade: CompilerCallbackServicesFacade,
|
||||
compilerOutputStream: RemoteOutputStream,
|
||||
compilerOutputFormat: CompileService.OutputFormat,
|
||||
serviceOutputStream: RemoteOutputStream,
|
||||
operationsTracer: RemoteOperationsTracer?
|
||||
): CompileService.CallResult<Int> =
|
||||
doCompile(sessionId, args, compilerOutputStream, serviceOutputStream, operationsTracer) { printStream, eventManager, profiler ->
|
||||
when (compilerOutputFormat) {
|
||||
CompileService.OutputFormat.PLAIN -> throw NotImplementedError("Only XML output is supported in remote incremental compilation")
|
||||
CompileService.OutputFormat.XML -> compiler[targetPlatform].execAndOutputXml(printStream, createCompileServices(servicesFacade, eventManager, profiler), *args)
|
||||
}
|
||||
}
|
||||
|
||||
// internal implementation stuff
|
||||
|
||||
// TODO: consider matching compilerId coming from outside with actual one
|
||||
// private val selfCompilerId by lazy {
|
||||
// CompilerId(
|
||||
// compilerClasspath = System.getProperty("java.class.path")
|
||||
// ?.split(File.pathSeparator)
|
||||
// ?.map { File(it) }
|
||||
// ?.filter { it.exists() }
|
||||
// ?.map { it.absolutePath }
|
||||
// ?: listOf(),
|
||||
// compilerVersion = loadKotlinVersionFromResource()
|
||||
// )
|
||||
// }
|
||||
|
||||
init {
|
||||
// assuming logically synchronized
|
||||
try {
|
||||
// cleanup for the case of incorrect restart and many other situations
|
||||
UnicastRemoteObject.unexportObject(this, false)
|
||||
}
|
||||
catch (e: NoSuchObjectException) {
|
||||
// ignoring if object already exported
|
||||
}
|
||||
|
||||
val stub = UnicastRemoteObject.exportObject(this, port, LoopbackNetworkInterface.clientLoopbackSocketFactory, LoopbackNetworkInterface.serverLoopbackSocketFactory) as CompileService
|
||||
registry.rebind (COMPILER_SERVICE_RMI_NAME, stub);
|
||||
|
||||
timer.schedule(0) {
|
||||
initiateElections()
|
||||
}
|
||||
timer.schedule(delay = DAEMON_PERIODIC_CHECK_INTERVAL_MS, period = DAEMON_PERIODIC_CHECK_INTERVAL_MS) {
|
||||
try {
|
||||
periodicAndAfterSessionCheck()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
System.err.println("Exception in timer thread: " + e.message)
|
||||
e.printStackTrace(System.err)
|
||||
log.log(Level.SEVERE, "Exception in timer thread", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun periodicAndAfterSessionCheck() {
|
||||
|
||||
ifAlive_Nothing(minAliveness = Aliveness.LastSession) {
|
||||
|
||||
// 1. check if unused for a timeout - shutdown
|
||||
if (shutdownCondition({ daemonOptions.autoshutdownUnusedSeconds != COMPILE_DAEMON_TIMEOUT_INFINITE_S && compilationsCounter.get() == 0 && nowSeconds() - lastUsedSeconds > daemonOptions.autoshutdownUnusedSeconds },
|
||||
"Unused timeout exceeded ${daemonOptions.autoshutdownUnusedSeconds}s, shutting down")) {
|
||||
shutdown()
|
||||
}
|
||||
else {
|
||||
synchronized(state.sessions) {
|
||||
// 2. check if any session hanged - clean
|
||||
// making copy of the list before calling release
|
||||
state.sessions.filterValues { !it.isAlive }.keys.toArrayList()
|
||||
}.forEach { releaseCompileSession(it) }
|
||||
|
||||
// 3. check if in graceful shutdown state and all sessions are closed
|
||||
if (shutdownCondition({ state.alive.get() == Aliveness.LastSession.ordinal && state.sessions.none()}, "All sessions finished, shutting down")) {
|
||||
shutdown()
|
||||
}
|
||||
|
||||
// 4. clean dead clients, then check if any left - conditional shutdown (with small delay)
|
||||
synchronized(state.clientProxies) { state.clientProxies.removeAll(state.clientProxies.filter { !it.isAlive }) }
|
||||
if (state.clientProxies.isEmpty() && compilationsCounter.get() > 0 && !state.delayedShutdownQueued.get()) {
|
||||
log.info("No more clients left, delayed shutdown in ${daemonOptions.shutdownDelayMilliseconds}ms")
|
||||
shutdownWithDelay()
|
||||
}
|
||||
// 5. check idle timeout - shutdown
|
||||
if (shutdownCondition({ daemonOptions.autoshutdownIdleSeconds != COMPILE_DAEMON_TIMEOUT_INFINITE_S && nowSeconds() - lastUsedSeconds > daemonOptions.autoshutdownIdleSeconds },
|
||||
"Idle timeout exceeded ${daemonOptions.autoshutdownIdleSeconds}s, shutting down") ||
|
||||
// 6. discovery file removed - shutdown
|
||||
shutdownCondition({ !runFile.exists() }, "Run file removed, shutting down") ||
|
||||
// 7. compiler changed (seldom check) - shutdown
|
||||
// TODO: could be too expensive anyway, consider removing this check
|
||||
shutdownCondition({ classpathWatcher.isChanged }, "Compiler changed")) {
|
||||
shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initiateElections() {
|
||||
|
||||
ifAlive_Nothing {
|
||||
|
||||
val aliveWithOpts = walkDaemons(File(daemonOptions.runFilesPathOrDefault), compilerId, filter = { f, p -> p != port }, report = { lvl, msg -> log.info(msg) })
|
||||
.map { Pair(it, it.getDaemonJVMOptions()) }
|
||||
.filter { it.second.isGood }
|
||||
.sortedWith(compareBy(DaemonJVMOptionsMemoryComparator().reversed(), { it.second.get() }))
|
||||
if (aliveWithOpts.any()) {
|
||||
val fattestOpts = aliveWithOpts.first().second.get()
|
||||
// second part of the condition means that we prefer other daemon if is "equal" to the current one
|
||||
if (fattestOpts memorywiseFitsInto daemonJVMOptions && !(daemonJVMOptions memorywiseFitsInto fattestOpts)) {
|
||||
// all others are smaller that me, take overs' clients and shut them down
|
||||
aliveWithOpts.forEach {
|
||||
it.first.getClients().check { it.isGood }?.let {
|
||||
it.get().forEach { registerClient(it) }
|
||||
}
|
||||
it.first.scheduleShutdown(true)
|
||||
}
|
||||
}
|
||||
else if (daemonJVMOptions memorywiseFitsInto fattestOpts) {
|
||||
// there is at least one bigger, handover my clients to it and shutdown
|
||||
scheduleShutdown(true)
|
||||
aliveWithOpts.first().first.let { fattest ->
|
||||
getClients().check { it.isGood }?.let {
|
||||
it.get().forEach { fattest.registerClient(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
// else - do nothing, all daemons are staying
|
||||
// TODO: implement some behaviour here, e.g.:
|
||||
// - shutdown/takeover smaller daemon
|
||||
// - run (or better persuade client to run) a bigger daemon (in fact may be even simple shutdown will do, because of client's daemon choosing logic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shutdownImpl() {
|
||||
log.info("Shutdown started")
|
||||
state.alive.set(Aliveness.Dying.ordinal)
|
||||
UnicastRemoteObject.unexportObject(this, true)
|
||||
log.info("Shutdown complete")
|
||||
onShutdown()
|
||||
}
|
||||
|
||||
private fun shutdownWithDelay() {
|
||||
state.delayedShutdownQueued.set(true)
|
||||
val currentCompilationsCount = compilationsCounter.get()
|
||||
timer.schedule(daemonOptions.shutdownDelayMilliseconds) {
|
||||
state.delayedShutdownQueued.set(false)
|
||||
if (currentCompilationsCount == compilationsCounter.get()) {
|
||||
log.fine("Execute delayed shutdown")
|
||||
shutdown()
|
||||
}
|
||||
else {
|
||||
log.info("Cancel delayed shutdown due to new client")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun shutdownCondition(check: () -> Boolean, message: String): Boolean {
|
||||
val res = check()
|
||||
if (res) {
|
||||
log.info(message)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
private fun doCompile(sessionId: Int,
|
||||
args: Array<out String>,
|
||||
compilerMessagesStreamProxy: RemoteOutputStream,
|
||||
serviceOutputStreamProxy: RemoteOutputStream,
|
||||
operationsTracer: RemoteOperationsTracer?,
|
||||
body: (PrintStream, EventManger, Profiler) -> ExitCode): CompileService.CallResult<Int> =
|
||||
ifAlive {
|
||||
|
||||
operationsTracer?.before("compile")
|
||||
compilationsCounter.incrementAndGet()
|
||||
val rpcProfiler = if (daemonOptions.reportPerf) WallAndThreadTotalProfiler() else DummyProfiler()
|
||||
val eventManger = EventMangerImpl()
|
||||
val compilerMessagesStream = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(compilerMessagesStreamProxy, rpcProfiler), 4096))
|
||||
val serviceOutputStream = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(serviceOutputStreamProxy, rpcProfiler), 4096))
|
||||
try {
|
||||
checkedCompile(args, serviceOutputStream, rpcProfiler) {
|
||||
val res = body(compilerMessagesStream, eventManger, rpcProfiler).code
|
||||
_lastUsedSeconds = nowSeconds()
|
||||
res
|
||||
}
|
||||
}
|
||||
finally {
|
||||
serviceOutputStream.flush()
|
||||
compilerMessagesStream.flush()
|
||||
eventManger.fireCompilationFinished()
|
||||
operationsTracer?.after("compile")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCompileServices(facade: CompilerCallbackServicesFacade, eventManger: EventManger, rpcProfiler: Profiler): Services {
|
||||
val builder = Services.Builder()
|
||||
if (facade.hasIncrementalCaches() || facade.hasLookupTracker()) {
|
||||
builder.register(IncrementalCompilationComponents::class.java, RemoteIncrementalCompilationComponentsClient(facade, eventManger, rpcProfiler))
|
||||
}
|
||||
if (facade.hasCompilationCanceledStatus()) {
|
||||
builder.register(CompilationCanceledStatus::class.java, RemoteCompilationCanceledStatusClient(facade, rpcProfiler))
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
||||
private fun<R> checkedCompile(args: Array<out String>, serviceOut: PrintStream, rpcProfiler: Profiler, body: () -> R): R {
|
||||
try {
|
||||
if (args.none())
|
||||
throw IllegalArgumentException("Error: empty arguments list.")
|
||||
log.info("Starting compilation with args: " + args.joinToString(" "))
|
||||
|
||||
val profiler = if (daemonOptions.reportPerf) WallAndThreadAndMemoryTotalProfiler(withGC = false) else DummyProfiler()
|
||||
|
||||
val res = profiler.withMeasure(null, body)
|
||||
|
||||
val endMem = if (daemonOptions.reportPerf) usedMemory(withGC = false) else 0L
|
||||
|
||||
log.info("Done with result " + res.toString())
|
||||
|
||||
if (daemonOptions.reportPerf) {
|
||||
fun Long.ms() = TimeUnit.NANOSECONDS.toMillis(this)
|
||||
fun Long.kb() = this / 1024
|
||||
val pc = profiler.getTotalCounters()
|
||||
val rpc = rpcProfiler.getTotalCounters()
|
||||
|
||||
"PERF: Compile on daemon: ${pc.time.ms()} ms; thread: user ${pc.threadUserTime.ms()} ms, sys ${(pc.threadTime - pc.threadUserTime).ms()} ms; rpc: ${rpc.count} calls, ${rpc.time.ms()} ms, thread ${rpc.threadTime.ms()} ms; memory: ${endMem.kb()} kb (${"%+d".format(pc.memory.kb())} kb)".let {
|
||||
serviceOut.println(it)
|
||||
log.info(it)
|
||||
}
|
||||
|
||||
// this will only be reported if if appropriate (e.g. ByClass) profiler is used
|
||||
for ((obj, counters) in rpcProfiler.getCounters()) {
|
||||
"PERF: rpc by $obj: ${counters.count} calls, ${counters.time.ms()} ms, thread ${counters.threadTime.ms()} ms".let {
|
||||
serviceOut.println(it)
|
||||
log.info(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
// TODO: consider possibilities to handle OutOfMemory
|
||||
catch (e: Exception) {
|
||||
log.info("Error: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearJarCache() {
|
||||
ZipHandler.clearFileAccessorCache()
|
||||
val classloader = javaClass.classLoader
|
||||
// TODO: replace the following code with direct call to CoreJarFileSystem.<clearCache> as soon as it will be available (hopefully in 15.02)
|
||||
try {
|
||||
KotlinCoreEnvironment.applicationEnvironment?.jarFileSystem.let { jarfs ->
|
||||
val jarfsClass = classloader.loadClass("com.intellij.openapi.vfs.impl.jar.CoreJarFileSystem")
|
||||
val privateHandlersField = jarfsClass.getDeclaredField("myHandlers")
|
||||
privateHandlersField.isAccessible = true
|
||||
privateHandlersField.get(jarfs)?.let {
|
||||
val clearMethod = privateHandlersField.type.getMethod("clear")
|
||||
if (clearMethod != null) {
|
||||
clearMethod.invoke(it)
|
||||
log.info("successfully cleared com.intellij.openapi.vfs.impl.jar.CoreJarFileSystem.myHandlers")
|
||||
}
|
||||
else {
|
||||
log.info("unable to access com.intellij.openapi.vfs.impl.jar.CoreJarFileSystem.myHandlers.clear")
|
||||
}
|
||||
} ?: log.info("unable to access CoreJarFileSystem.myHandlers (${privateHandlersField.get(jarfs)})")
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
log.log(Level.SEVERE, "error clearing CoreJarFileSystem", e)
|
||||
}
|
||||
}
|
||||
|
||||
// copied (with edit) from gradle plugin
|
||||
private fun callVoidStaticMethod(classFqName: String, methodName: String) {
|
||||
// compiler classloader == current classloader for now
|
||||
// TODO: consider abstracting classloader, for easier changing it for a compiler
|
||||
val cls = this.javaClass.classLoader.loadClass(classFqName)
|
||||
|
||||
val method = cls.getMethod(methodName)
|
||||
|
||||
method.invoke(null)
|
||||
}
|
||||
|
||||
private fun<R> ifAlive(minAliveness: Aliveness = Aliveness.Alive, ignoreCompilerChanged: Boolean = false, body: () -> R): CompileService.CallResult<R> = rwlock.read {
|
||||
ifAliveChecksImpl(minAliveness, ignoreCompilerChanged) { CompileService.CallResult.Good(body()) }
|
||||
}
|
||||
|
||||
// TODO: find how to implement it without using unique name for this variant; making name deliberately ugly meanwhile
|
||||
private fun ifAlive_Nothing(minAliveness: Aliveness = Aliveness.Alive, ignoreCompilerChanged: Boolean = false, body: () -> Unit): CompileService.CallResult<Nothing> = rwlock.read {
|
||||
ifAliveChecksImpl(minAliveness, ignoreCompilerChanged) {
|
||||
body()
|
||||
CompileService.CallResult.Ok()
|
||||
}
|
||||
}
|
||||
|
||||
private fun<R> ifAliveExclusive(minAliveness: Aliveness = Aliveness.Alive, ignoreCompilerChanged: Boolean = false, body: () -> R): CompileService.CallResult<R> = rwlock.write {
|
||||
ifAliveChecksImpl(minAliveness, ignoreCompilerChanged) { CompileService.CallResult.Good(body()) }
|
||||
}
|
||||
|
||||
// see comment to ifAliveNothing
|
||||
private fun<R> ifAliveExclusive_Nothing(minAliveness: Aliveness = Aliveness.Alive, ignoreCompilerChanged: Boolean = false, body: () -> Unit): CompileService.CallResult<R> = rwlock.write {
|
||||
ifAliveChecksImpl(minAliveness, ignoreCompilerChanged) {
|
||||
body()
|
||||
CompileService.CallResult.Ok()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
inline private fun<R> ifAliveChecksImpl(minAliveness: Aliveness = Aliveness.Alive, ignoreCompilerChanged: Boolean = false, body: () -> CompileService.CallResult<R>): CompileService.CallResult<R> =
|
||||
when {
|
||||
state.alive.get() < minAliveness.ordinal -> CompileService.CallResult.Dying()
|
||||
!ignoreCompilerChanged && classpathWatcher.isChanged -> {
|
||||
log.info("Compiler changed, scheduling shutdown")
|
||||
timer.schedule(0) { shutdown() }
|
||||
CompileService.CallResult.Dying()
|
||||
}
|
||||
else -> {
|
||||
try {
|
||||
body()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
log.log(Level.SEVERE, "Exception", e)
|
||||
CompileService.CallResult.Error(e.message ?: "unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.security.DigestInputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
val CLASSPATH_FILE_ID_DIGEST = "MD5"
|
||||
val DEFAULT_CLASSPATH_WATCH_PERIOD_MS = 1000L
|
||||
val DEFAULT_CLASSPATH_DIGEST_WATCH_PERIOD_MS = 300000L // 5 min
|
||||
|
||||
|
||||
/**
|
||||
* Class for lazy (on demand) check if any relevant file in the classpath is changed
|
||||
* poor-man watcher in the absence of NIO
|
||||
* TODO: replace with NIO watching when switching to java 7+
|
||||
*/
|
||||
class LazyClasspathWatcher(classpath: Iterable<String>,
|
||||
val checkPeriod: Long = DEFAULT_CLASSPATH_WATCH_PERIOD_MS,
|
||||
val digestCheckPeriod: Long = DEFAULT_CLASSPATH_DIGEST_WATCH_PERIOD_MS) {
|
||||
|
||||
private data class FileId(val file: File, val lastModified: Long, val digest: ByteArray)
|
||||
|
||||
private val fileIdsLock = Semaphore(1) // a barrier for ensuring ids are initialized, using semaphore to allow modifications from another thread
|
||||
private var fileIds: ArrayList<FileId>? = null
|
||||
private val lastChangedStatus = AtomicBoolean(false)
|
||||
private val lastUpdate = AtomicLong(0)
|
||||
private val lastDigestUpdate = AtomicLong(0)
|
||||
private val log by lazy { Logger.getLogger("classpath watcher") }
|
||||
|
||||
init {
|
||||
// locking before entering thread in order to avoid racing with isChanged
|
||||
fileIdsLock.acquire()
|
||||
thread(daemon = true, start = true) {
|
||||
try {
|
||||
fileIds = classpath
|
||||
.map { File(it) }
|
||||
.asSequence()
|
||||
.flatMap { FileTreeWalk(it, filter = ::isClasspathFile) }
|
||||
.map { FileId(it, it.lastModified(), it.md5Digest()) }
|
||||
.toArrayList()
|
||||
val nowMs = TimeUnit.MILLISECONDS.toMillis(System.nanoTime())
|
||||
lastUpdate.set(nowMs)
|
||||
lastDigestUpdate.set(nowMs)
|
||||
}
|
||||
catch (e: IOException) {
|
||||
log.log(Level.WARNING, "Error on walking classpath", e)
|
||||
// ignoring it for now
|
||||
}
|
||||
finally {
|
||||
fileIdsLock.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isChanged: Boolean get() {
|
||||
if (lastChangedStatus.get()) return true
|
||||
val nowMs = TimeUnit.MILLISECONDS.toMillis(System.nanoTime())
|
||||
if (nowMs - lastUpdate.get() < checkPeriod) return false
|
||||
|
||||
val checkDigest = nowMs - lastDigestUpdate.get() > digestCheckPeriod
|
||||
// making sure that fieldIds are initialized
|
||||
fileIdsLock.acquire()
|
||||
fileIdsLock.release()
|
||||
val changed =
|
||||
fileIds?.find {
|
||||
try {
|
||||
if (!it.file.exists()) {
|
||||
log.info("cp changed: ${it.file} doesn't exist any more")
|
||||
true
|
||||
}
|
||||
// if last modified changed or if enforced by param - checking the digest
|
||||
else if ((it.file.lastModified() != it.lastModified || checkDigest) && !Arrays.equals(it.digest, it.file.md5Digest())) {
|
||||
log.info("cp changed: ${it.file} digests differ")
|
||||
true
|
||||
}
|
||||
else false
|
||||
}
|
||||
catch (e: IOException) {
|
||||
log.log(Level.INFO, "cp changed: ${it.file} access throws the exception", e)
|
||||
true // io error considered as change
|
||||
}
|
||||
} != null
|
||||
lastUpdate.set(TimeUnit.MILLISECONDS.toMillis(System.nanoTime()))
|
||||
if (checkDigest) lastDigestUpdate.set(lastUpdate.get())
|
||||
|
||||
return changed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun isClasspathFile(file: File): Boolean = file.isFile && listOf("class", "jar").contains(file.extension.toLowerCase())
|
||||
|
||||
fun File.md5Digest(): ByteArray {
|
||||
val md = MessageDigest.getInstance(CLASSPATH_FILE_ID_DIGEST)
|
||||
DigestInputStream(inputStream(), md).use {
|
||||
val buf = ByteArray(1024)
|
||||
while (it.read(buf) != -1) {}
|
||||
it.close()
|
||||
}
|
||||
return md.digest()
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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
|
||||
|
||||
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
|
||||
import org.jetbrains.kotlin.daemon.common.CompilerCallbackServicesFacade
|
||||
import org.jetbrains.kotlin.daemon.common.DummyProfiler
|
||||
import org.jetbrains.kotlin.daemon.common.Profiler
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
val CANCELED_STATUS_CHECK_THRESHOLD_NS = TimeUnit.MILLISECONDS.toNanos(100)
|
||||
|
||||
class RemoteCompilationCanceledStatusClient(val facade: CompilerCallbackServicesFacade, val profiler: Profiler = DummyProfiler()): CompilationCanceledStatus {
|
||||
@Volatile var lastChecked: Long = System.nanoTime()
|
||||
override fun checkCanceled() {
|
||||
val curNanos = System.nanoTime()
|
||||
if (curNanos - lastChecked > CANCELED_STATUS_CHECK_THRESHOLD_NS) {
|
||||
profiler.withMeasure(this) {
|
||||
facade.compilationCanceledStatus_checkCanceled()
|
||||
}
|
||||
lastChecked = curNanos
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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
|
||||
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCache
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.JvmPackagePartProto
|
||||
import org.jetbrains.kotlin.modules.TargetId
|
||||
import org.jetbrains.kotlin.daemon.common.CompilerCallbackServicesFacade
|
||||
import org.jetbrains.kotlin.daemon.common.DummyProfiler
|
||||
import org.jetbrains.kotlin.daemon.common.Profiler
|
||||
|
||||
class RemoteIncrementalCacheClient(val facade: CompilerCallbackServicesFacade, val target: TargetId, val profiler: Profiler = DummyProfiler()): IncrementalCache {
|
||||
|
||||
override fun getObsoletePackageParts(): Collection<String> = profiler.withMeasure(this) { facade.incrementalCache_getObsoletePackageParts(target) }
|
||||
|
||||
override fun getObsoleteMultifileClasses(): Collection<String> = profiler.withMeasure(this) { facade.incrementalCache_getObsoleteMultifileClassFacades(target) }
|
||||
|
||||
override fun getStableMultifileFacadeParts(facadeInternalName: String): Collection<String>? = profiler.withMeasure(this) { facade.incrementalCache_getMultifileFacadeParts(target, facadeInternalName) }
|
||||
|
||||
override fun getPackagePartData(fqName: String): JvmPackagePartProto? = profiler.withMeasure(this) { facade.incrementalCache_getPackagePartData(target, fqName) }
|
||||
|
||||
override fun getMultifileFacade(partInternalName: String): String? = profiler.withMeasure(this) { facade.incrementalCache_getMultifileFacade(target, partInternalName) }
|
||||
|
||||
override fun getModuleMappingData(): ByteArray? = profiler.withMeasure(this) { facade.incrementalCache_getModuleMappingData(target) }
|
||||
|
||||
override fun registerInline(fromPath: String, jvmSignature: String, toPath: String) {
|
||||
profiler.withMeasure(this) { facade.incrementalCache_registerInline(target, fromPath, jvmSignature, toPath) }
|
||||
}
|
||||
|
||||
override fun getClassFilePath(internalClassName: String): String = profiler.withMeasure(this) { facade.incrementalCache_getClassFilePath(target,internalClassName) }
|
||||
|
||||
override fun close(): Unit = profiler.withMeasure(this) { facade.incrementalCache_close(target) }
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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
|
||||
|
||||
import org.jetbrains.kotlin.incremental.components.LookupTracker
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCache
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
|
||||
import org.jetbrains.kotlin.modules.TargetId
|
||||
import org.jetbrains.kotlin.daemon.common.CompilerCallbackServicesFacade
|
||||
import org.jetbrains.kotlin.daemon.common.DummyProfiler
|
||||
import org.jetbrains.kotlin.daemon.common.Profiler
|
||||
|
||||
|
||||
class RemoteIncrementalCompilationComponentsClient(val facade: CompilerCallbackServicesFacade, eventManger: EventManger, val profiler: Profiler = DummyProfiler()) : IncrementalCompilationComponents {
|
||||
val remoteLookupTrackerClient = RemoteLookupTrackerClient(facade, eventManger, profiler)
|
||||
|
||||
override fun getIncrementalCache(target: TargetId): IncrementalCache = RemoteIncrementalCacheClient(facade, target, profiler)
|
||||
|
||||
override fun getLookupTracker(): LookupTracker = remoteLookupTrackerClient
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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
|
||||
|
||||
import org.jetbrains.kotlin.incremental.components.LookupInfo
|
||||
import org.jetbrains.kotlin.incremental.components.LookupTracker
|
||||
import org.jetbrains.kotlin.incremental.components.Position
|
||||
import org.jetbrains.kotlin.incremental.components.ScopeKind
|
||||
import org.jetbrains.kotlin.daemon.common.CompilerCallbackServicesFacade
|
||||
import org.jetbrains.kotlin.daemon.common.DummyProfiler
|
||||
import org.jetbrains.kotlin.daemon.common.Profiler
|
||||
|
||||
|
||||
class RemoteLookupTrackerClient(val facade: CompilerCallbackServicesFacade, val eventManger: EventManger, val profiler: Profiler = DummyProfiler()) : LookupTracker {
|
||||
private val isDoNothing = profiler.withMeasure(this) { facade.lookupTracker_isDoNothing() }
|
||||
|
||||
private val lookups = hashSetOf<LookupInfo>()
|
||||
|
||||
override val requiresPosition: Boolean = profiler.withMeasure(this) { facade.lookupTracker_requiresPosition() }
|
||||
|
||||
override fun record(filePath: String, position: Position, scopeFqName: String, scopeKind: ScopeKind, name: String) {
|
||||
if (isDoNothing) return
|
||||
|
||||
lookups.add(LookupInfo(filePath, position, scopeFqName, scopeKind, name))
|
||||
}
|
||||
|
||||
init {
|
||||
eventManger.onCompilationFinished { flush() }
|
||||
}
|
||||
|
||||
private fun flush() {
|
||||
if (isDoNothing || lookups.isEmpty()) return
|
||||
|
||||
profiler.withMeasure(this) {
|
||||
facade.lookupTracker_record(lookups)
|
||||
}
|
||||
|
||||
lookups.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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
|
||||
|
||||
import org.jetbrains.kotlin.daemon.common.DummyProfiler
|
||||
import org.jetbrains.kotlin.daemon.common.Profiler
|
||||
import org.jetbrains.kotlin.daemon.common.RemoteOutputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
class RemoteOutputStreamClient(val remote: RemoteOutputStream, val profiler: Profiler = DummyProfiler()): OutputStream() {
|
||||
override fun write(data: ByteArray) {
|
||||
profiler.withMeasure(this) { remote.write(data, 0, data.size) }
|
||||
}
|
||||
|
||||
override fun write(data: ByteArray, offset: Int, length: Int) {
|
||||
profiler.withMeasure(this) { remote.write(data, offset, length) }
|
||||
}
|
||||
|
||||
override fun write(byte: Int) {
|
||||
profiler.withMeasure(this) { remote.write(byte) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user