Add remote repl support to the daemon, refactor repl classes accordingly, simple tests

This commit is contained in:
Ilya Chernikov
2016-09-16 17:38:00 +02:00
parent 042fc4fadc
commit 86ece30330
12 changed files with 648 additions and 111 deletions
@@ -23,25 +23,28 @@ import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read import kotlin.concurrent.read
import kotlin.concurrent.write import kotlin.concurrent.write
open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>) : ReplCompiledEvaluator { open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>, baseClassloader: ClassLoader?) : ReplCompiledEvaluator {
private val classpath = baseClasspath.toMutableList()
private var classLoader: org.jetbrains.kotlin.cli.common.repl.ReplClassLoader = private var classLoader: org.jetbrains.kotlin.cli.common.repl.ReplClassLoader =
org.jetbrains.kotlin.cli.common.repl.ReplClassLoader(URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), Thread.currentThread().contextClassLoader)) org.jetbrains.kotlin.cli.common.repl.ReplClassLoader(URLClassLoader(baseClasspath.map { it.toURI().toURL() }.toTypedArray(), baseClassloader))
private val classLoaderLock = ReentrantReadWriteLock() private val classLoaderLock = ReentrantReadWriteLock()
private class ClassWithInstance(val klass: Class<*>, val instance: Any) private class ClassWithInstance(val klass: Class<*>, val instance: Any)
private val compiledLoadedClassesHistory = arrayListOf<Pair<ReplCodeLine, ClassWithInstance>>() private val compiledLoadedClassesHistory = arrayListOf<Pair<ReplCodeLine, ClassWithInstance>>()
override fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>, compiledClasses: List<CompiledClassData>, hasResult: Boolean): ReplEvalResult { override fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>, compiledClasses: List<CompiledClassData>, hasResult: Boolean, newClasspath: List<File>): ReplEvalResult {
checkAndUpdateReplHistoryCollection(compiledLoadedClassesHistory, history)?.let { checkAndUpdateReplHistoryCollection(compiledLoadedClassesHistory, history)?.let {
return@eval ReplEvalResult.HistoryMismatch(it) return@eval ReplEvalResult.HistoryMismatch(it)
} }
classLoaderLock.read { classLoaderLock.read {
if (newClasspath.isNotEmpty()) {
classLoaderLock.write {
classLoader = org.jetbrains.kotlin.cli.common.repl.ReplClassLoader(URLClassLoader(newClasspath.map { it.toURI().toURL() }.toTypedArray(), classLoader))
}
}
compiledClasses.filter { it.path.endsWith(".class") } compiledClasses.filter { it.path.endsWith(".class") }
.forEach { .forEach {
classLoader.addClass(JvmClassName.byInternalName(it.path.replaceFirst("\\.class$".toRegex(), "")), it.bytes) classLoader.addClass(JvmClassName.byInternalName(it.path.replaceFirst("\\.class$".toRegex(), "")), it.bytes)
@@ -71,13 +74,6 @@ open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>) : ReplCom
return if (hasResult) ReplEvalResult.ValueResult(rv) else ReplEvalResult.UnitResult return if (hasResult) ReplEvalResult.ValueResult(rv) else ReplEvalResult.UnitResult
} }
fun dependenciesAdded(newClasspath: List<File>) {
classLoaderLock.write {
classpath.addAll(newClasspath)
classLoader = org.jetbrains.kotlin.cli.common.repl.ReplClassLoader(URLClassLoader(newClasspath.map { it.toURI().toURL() }.toTypedArray(), classLoader))
}
}
companion object { companion object {
private val SCRIPT_RESULT_FIELD_NAME = "\$\$result" private val SCRIPT_RESULT_FIELD_NAME = "\$\$result"
} }
@@ -16,38 +16,62 @@
package org.jetbrains.kotlin.cli.common.repl package org.jetbrains.kotlin.cli.common.repl
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
import java.io.File import java.io.File
import java.io.Serializable import java.io.Serializable
import java.util.* import java.util.*
data class ReplCodeLine(val no: Int, val code: String) data class ReplCodeLine(val no: Int, val code: String) : Serializable {
companion object {
private val serialVersionUID: Long = 8228357578L // just a random number, should be changed when serialized data is changed
}
}
data class CompiledClassData(val path: String, val bytes: ByteArray) : Serializable { data class CompiledClassData(val path: String, val bytes: ByteArray) : Serializable {
override fun equals(other: Any?): Boolean = (other as? CompiledClassData)?.let { path == it.path && Arrays.equals(bytes, it.bytes) } ?: false override fun equals(other: Any?): Boolean = (other as? CompiledClassData)?.let { path == it.path && Arrays.equals(bytes, it.bytes) } ?: false
override fun hashCode(): Int = path.hashCode() + Arrays.hashCode(bytes) override fun hashCode(): Int = path.hashCode() + Arrays.hashCode(bytes)
companion object {
private val serialVersionUID: Long = 8228357578L // just a random number, should be changed when serialized data is changed
}
} }
sealed class ReplCheckResult : Serializable { sealed class ReplCheckResult : Serializable {
object Ok : ReplCheckResult() object Ok : ReplCheckResult()
object Incomplete : ReplCheckResult() object Incomplete : ReplCheckResult()
class Error(val message: String) : ReplCheckResult() class Error(val message: String, val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION) : ReplCheckResult() {
override fun toString(): String = "Error(message = \"$message\""
}
companion object {
private val serialVersionUID: Long = 8228357578L // just a random number, should be changed when serialized data is changed
}
} }
sealed class ReplCompileResult : Serializable { sealed class ReplCompileResult : Serializable {
class CompiledClasses(val classes: List<CompiledClassData>, val hasResult: Boolean) : ReplCompileResult() class CompiledClasses(val classes: List<CompiledClassData>, val hasResult: Boolean, val newClasspath: List<File>) : ReplCompileResult()
object Incomplete : ReplCompileResult() object Incomplete : ReplCompileResult()
class HistoryMismatch(val lineNo: Int): ReplCompileResult() class HistoryMismatch(val lineNo: Int): ReplCompileResult()
class Error(val message: String) : ReplCompileResult() class Error(val message: String, val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION) : ReplCompileResult() {
override fun toString(): String = "Error(message = \"$message\""
}
companion object {
private val serialVersionUID: Long = 8228357578L // just a random number, should be changed when serialized data is changed
}
} }
sealed class ReplEvalResult : Serializable { sealed class ReplEvalResult : Serializable {
class ValueResult(val value: Any?) : ReplEvalResult() class ValueResult(val value: Any?) : ReplEvalResult() {
override fun toString(): String = "Result: $value"
}
object UnitResult : ReplEvalResult() object UnitResult : ReplEvalResult()
object Incomplete : ReplEvalResult() object Incomplete : ReplEvalResult()
class HistoryMismatch(val lineNo: Int): ReplEvalResult() class HistoryMismatch(val lineNo: Int): ReplEvalResult()
sealed class Error(val message: String) : ReplEvalResult() { sealed class Error(val message: String) : ReplEvalResult() {
class Runtime(message: String) : Error(message) class Runtime(message: String) : Error(message)
class CompileTime(message: String) : Error(message) class CompileTime(message: String, val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION) : Error(message)
override fun toString(): String = "${this.javaClass.kotlin.simpleName}Error(message = \"$message\""
}
companion object {
private val serialVersionUID: Long = 8228357578L // just a random number, should be changed when serialized data is changed
} }
} }
@@ -57,14 +81,11 @@ interface ReplChecker {
interface ReplCompiler : ReplChecker { interface ReplCompiler : ReplChecker {
fun compile(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplCompileResult fun compile(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplCompileResult
// a callback
fun dependenciesAdded(classpath: List<File>)
} }
interface ReplCompiledEvaluator { interface ReplCompiledEvaluator {
fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>, compiledClasses: List<CompiledClassData>, hasResult: Boolean): ReplEvalResult fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>, compiledClasses: List<CompiledClassData>, hasResult: Boolean, newClasspath: List<File>): ReplEvalResult
// override to capture output // override to capture output
fun<T> evalWithIO(body: () -> T): T = body() fun<T> evalWithIO(body: () -> T): T = body()
@@ -75,8 +96,6 @@ interface ReplEvaluator : ReplChecker {
fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplEvalResult fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplEvalResult
fun dependenciesAdded(classpath: List<File>)
// override to capture output // override to capture output
fun<T> evalWithIO(body: () -> T): T = body() fun<T> evalWithIO(body: () -> T): T = body()
} }
@@ -77,8 +77,6 @@ open class GenericReplChecker(
protected var lineState: LineState? = null protected var lineState: LineState? = null
val classpath: MutableList<File> = compilerConfiguration.jvmClasspathRoots.toMutableList()
fun createDiagnosticHolder() = ReplTerminalDiagnosticMessageHolder() fun createDiagnosticHolder() = ReplTerminalDiagnosticMessageHolder()
override fun check(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplCheckResult { override fun check(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplCheckResult {
@@ -108,7 +106,7 @@ open class GenericReplChecker(
} }
abstract class GenericReplCompiler( open class GenericReplCompiler(
disposable: Disposable, disposable: Disposable,
scriptDefinition: KotlinScriptDefinition, scriptDefinition: KotlinScriptDefinition,
compilerConfiguration: CompilerConfiguration, compilerConfiguration: CompilerConfiguration,
@@ -132,7 +130,7 @@ abstract class GenericReplCompiler(
val res = check(codeLine, history) val res = check(codeLine, history)
when (res) { when (res) {
ReplCheckResult.Incomplete -> return@compile ReplCompileResult.Incomplete ReplCheckResult.Incomplete -> return@compile ReplCompileResult.Incomplete
is ReplCheckResult.Error -> return@compile ReplCompileResult.Error(res.message) is ReplCheckResult.Error -> return@compile ReplCompileResult.Error(res.message, res.location)
ReplCheckResult.Ok -> {} // continue ReplCheckResult.Ok -> {} // continue
} }
} }
@@ -140,14 +138,6 @@ abstract class GenericReplCompiler(
} }
val newDependencies = scriptDefinition.getDependenciesFor(psiFile, environment.project, lastDependencies) val newDependencies = scriptDefinition.getDependenciesFor(psiFile, environment.project, lastDependencies)
newDependencies?.let {
logger.debug("found dependencies: ${it.classpath}")
val newCp = environment.updateClasspath(it.classpath.map(::JvmClasspathRoot))
if (newCp != null && newCp.isNotEmpty()) {
logger.debug("new dependencies: $newCp")
dependenciesAdded(newCp)
}
}
if (lastDependencies != newDependencies) { if (lastDependencies != newDependencies) {
lastDependencies = newDependencies lastDependencies = newDependencies
} }
@@ -179,7 +169,9 @@ abstract class GenericReplCompiler(
descriptorsHistory.add(codeLine to scriptDescriptor) descriptorsHistory.add(codeLine to scriptDescriptor)
return ReplCompileResult.CompiledClasses(state.factory.asList().map { CompiledClassData(it.relativePath, it.asByteArray()) }, state.replSpecific.hasResult) return ReplCompileResult.CompiledClasses(state.factory.asList().map { CompiledClassData(it.relativePath, it.asByteArray()) },
state.replSpecific.hasResult,
newDependencies?.let { environment.updateClasspath(it.classpath.map(::JvmClasspathRoot)) } ?: emptyList())
} }
} }
@@ -193,28 +185,26 @@ open class GenericRepl(
disposable: Disposable, disposable: Disposable,
scriptDefinition: KotlinScriptDefinition, scriptDefinition: KotlinScriptDefinition,
compilerConfiguration: CompilerConfiguration, compilerConfiguration: CompilerConfiguration,
messageCollector: MessageCollector messageCollector: MessageCollector,
baseClassloader: ClassLoader?
) : ReplEvaluator, GenericReplCompiler(disposable, scriptDefinition, compilerConfiguration, messageCollector) { ) : ReplEvaluator, GenericReplCompiler(disposable, scriptDefinition, compilerConfiguration, messageCollector) {
private val compiledEvaluator = GenericReplCompiledEvaluator(classpath) private val compiledEvaluator = GenericReplCompiledEvaluator(compilerConfiguration.jvmClasspathRoots, baseClassloader)
override fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplEvalResult { override fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplEvalResult = synchronized(this) {
synchronized(this) { return compileAndEval(this, compiledEvaluator, codeLine, history)
val (compiledClasses, hasResult) = compile(codeLine, history).let {
when (it) {
ReplCompileResult.Incomplete -> return@eval ReplEvalResult.Incomplete
is ReplCompileResult.HistoryMismatch -> return@eval ReplEvalResult.HistoryMismatch(it.lineNo)
is ReplCompileResult.Error -> return@eval ReplEvalResult.Error.CompileTime(it.message)
is ReplCompileResult.CompiledClasses -> it.classes to it.hasResult
}
}
return compiledEvaluator.eval(codeLine, history, compiledClasses, hasResult)
}
}
override fun dependenciesAdded(classpath: List<File>) {
compiledEvaluator.dependenciesAdded(classpath)
} }
} }
fun compileAndEval(replCompiler: ReplCompiler, replCompiledEvaluator: ReplCompiledEvaluator, codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplEvalResult =
replCompiler.compile(codeLine, history).let {
when (it) {
ReplCompileResult.Incomplete -> ReplEvalResult.Incomplete
is ReplCompileResult.HistoryMismatch -> ReplEvalResult.HistoryMismatch(it.lineNo)
is ReplCompileResult.Error -> ReplEvalResult.Error.CompileTime(it.message, it.location)
is ReplCompileResult.CompiledClasses -> replCompiledEvaluator.eval(codeLine, history, it.classes, it.hasResult, it.newClasspath)
}
}
@@ -32,7 +32,8 @@ class KotlinJsr232ScriptEngine(
disposable: Disposable, disposable: Disposable,
private val factory: ScriptEngineFactory, private val factory: ScriptEngineFactory,
private val scriptDefinition: KotlinScriptDefinition, private val scriptDefinition: KotlinScriptDefinition,
private val compilerConfiguration: CompilerConfiguration private val compilerConfiguration: CompilerConfiguration,
baseClassLoader: ClassLoader
) : AbstractScriptEngine(), ScriptEngine { ) : AbstractScriptEngine(), ScriptEngine {
data class MessageCollectorReport(val severity: CompilerMessageSeverity, val message: String, val location: CompilerMessageLocation) data class MessageCollectorReport(val severity: CompilerMessageSeverity, val message: String, val location: CompilerMessageLocation)
@@ -72,7 +73,7 @@ class KotlinJsr232ScriptEngine(
} }
} }
private val repl = object : GenericRepl(disposable, scriptDefinition, compilerConfiguration, messageCollector) {} private val repl = object : GenericRepl(disposable, scriptDefinition, compilerConfiguration, messageCollector, baseClassLoader) {}
private var lineCount = 0 private var lineCount = 0
@@ -49,7 +49,8 @@ class KotlinJsr232StandardScriptEngineFactory: ScriptEngineFactory {
// TODO: addJvmClasspathRoots(config.classpath) // TODO: addJvmClasspathRoots(config.classpath)
put(CommonConfigurationKeys.MODULE_NAME, "kotlin-script") put(CommonConfigurationKeys.MODULE_NAME, "kotlin-script")
put(JVMConfigurationKeys.INCLUDE_RUNTIME, true) put(JVMConfigurationKeys.INCLUDE_RUNTIME, true)
}) },
Thread.currentThread().contextClassLoader)
override fun getOutputStatement(toDisplay: String?): String = "print(\"$toDisplay\")" override fun getOutputStatement(toDisplay: String?): String = "print(\"$toDisplay\")"
override fun getMethodCallSyntax(obj: String, m: String, vararg args: String): String = "$obj.$m(${args.joinToString()})" override fun getMethodCallSyntax(obj: String, m: String, vararg args: String): String = "$obj.$m(${args.joinToString()})"
@@ -11,5 +11,6 @@
<orderEntry type="module" module-name="frontend.java" /> <orderEntry type="module" module-name="frontend.java" />
<orderEntry type="module" module-name="util" /> <orderEntry type="module" module-name="util" />
<orderEntry type="library" name="native-platform-uberjar" level="project" /> <orderEntry type="library" name="native-platform-uberjar" level="project" />
<orderEntry type="module" module-name="cli-common" />
</component> </component>
</module> </module>
@@ -31,7 +31,6 @@ import java.util.concurrent.TimeUnit
import kotlin.comparisons.compareByDescending import kotlin.comparisons.compareByDescending
import kotlin.concurrent.thread import kotlin.concurrent.thread
class CompilationServices( class CompilationServices(
val incrementalCompilationComponents: IncrementalCompilationComponents? = null, val incrementalCompilationComponents: IncrementalCompilationComponents? = null,
val compilationCanceledStatus: CompilationCanceledStatus? = null val compilationCanceledStatus: CompilationCanceledStatus? = null
@@ -53,18 +52,12 @@ object KotlinCompilerClient {
autostart: Boolean = true, autostart: Boolean = true,
checkId: Boolean = true checkId: Boolean = true
): CompileService? { ): 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) val flagFile = System.getProperty(COMPILE_DAEMON_CLIENT_ALIVE_PATH_PROPERTY)
?.let { it.trimQuotes() } ?.let(String::trimQuotes)
?.check { !it.isBlank() } ?.check { !it.isBlank() }
?.let { File(it) } ?.let(::File)
?.check { it.exists() } ?.check(File::exists)
?: newFlagFile() ?: makeAutodeletingFlagFile()
return connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, reportingTargets, autostart) return connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, reportingTargets, autostart)
} }
@@ -287,7 +280,7 @@ object KotlinCompilerClient {
val daemonLauncher = Native.get(ProcessLauncher::class.java) val daemonLauncher = Native.get(ProcessLauncher::class.java)
val daemon = daemonLauncher.start(processBuilder) val daemon = daemonLauncher.start(processBuilder)
var isEchoRead = Semaphore(1) val isEchoRead = Semaphore(1)
isEchoRead.acquire() isEchoRead.acquire()
val stdoutThread = val stdoutThread =
@@ -316,7 +309,7 @@ object KotlinCompilerClient {
it.toLong() it.toLong()
} }
catch (e: Exception) { 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") reportingTargets.report(DaemonReportCategory.INFO, "unable to interpret $COMPILE_DAEMON_STARTUP_TIMEOUT_PROPERTY property ('$it'); using default timeout $DAEMON_DEFAULT_STARTUP_TIMEOUT_MS ms")
null null
} }
} ?: DAEMON_DEFAULT_STARTUP_TIMEOUT_MS } ?: DAEMON_DEFAULT_STARTUP_TIMEOUT_MS
@@ -0,0 +1,131 @@
/*
* Copyright 2010-2016 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 com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.cli.common.repl.*
import org.jetbrains.kotlin.daemon.common.CompileService
import org.jetbrains.kotlin.daemon.common.RemoteOperationsTracer
import org.jetbrains.kotlin.daemon.common.SOCKET_ANY_FREE_PORT
import java.io.File
import java.io.InputStream
import java.io.OutputStream
// TODO: reduce number of ports used then SOCKET_ANY_FREE_PORT is passed (same problem with other calls)
open class KotlinRemoteReplClientBase(
disposable: Disposable,
protected val compileService: CompileService,
clientAliveFlagFile: File?,
targetPlatform: CompileService.TargetPlatform,
templateClasspath: List<File>,
templateClassName: String,
compilerMessagesOutputStream: OutputStream,
evalOutputStream: OutputStream? = null,
evalErrorStream: OutputStream? = null,
evalInputStream: InputStream? = null,
port: Int = SOCKET_ANY_FREE_PORT,
operationsTracer: RemoteOperationsTracer? = null
) : ReplChecker {
val sessionId = compileService.leaseReplSession(
clientAliveFlagFile?.absolutePath,
targetPlatform,
CompilerCallbackServicesFacadeServer(port = port),
templateClasspath,
templateClassName,
RemoteOutputStreamServer(compilerMessagesOutputStream, port),
evalOutputStream?.let { RemoteOutputStreamServer(it, port) },
evalErrorStream?.let { RemoteOutputStreamServer(it, port) },
evalInputStream?.let { RemoteInputStreamServer(it, port) },
operationsTracer
).get()
init {
Disposer.register(disposable, Disposable {
compileService.releaseReplSession(sessionId)
})
}
override fun check(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplCheckResult {
return compileService.remoteReplLineCheck(sessionId, codeLine, history.toList()).get()
}
}
class KotlinRemoteReplCompiler(
disposable: Disposable,
compileService: CompileService,
clientAliveFlagFile: File?,
targetPlatform: CompileService.TargetPlatform,
templateClasspath: List<File>,
templateClassName: String,
compilerMessagesOutputStream: OutputStream,
port: Int = SOCKET_ANY_FREE_PORT,
operationsTracer: RemoteOperationsTracer? = null
) : KotlinRemoteReplClientBase(
disposable = disposable,
compileService = compileService,
clientAliveFlagFile = clientAliveFlagFile,
targetPlatform = targetPlatform,
templateClasspath = templateClasspath,
templateClassName = templateClassName,
compilerMessagesOutputStream = compilerMessagesOutputStream,
evalOutputStream = null,
evalErrorStream = null,
evalInputStream = null,
port = port,
operationsTracer = operationsTracer
), ReplCompiler {
override fun compile(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplCompileResult {
return compileService.remoteReplLineCompile(sessionId, codeLine, history.toList()).get()
}
}
class KotlinRemoteReplEvaluator(
disposable: Disposable,
compileService: CompileService,
clientAliveFlagFile: File?,
targetPlatform: CompileService.TargetPlatform,
templateClasspath: List<File>,
templateClassName: String,
compilerMessagesOutputStream: OutputStream,
evalOutputStream: OutputStream?,
evalErrorStream: OutputStream?,
evalInputStream: InputStream?,
port: Int = SOCKET_ANY_FREE_PORT,
operationsTracer: RemoteOperationsTracer? = null
) : KotlinRemoteReplClientBase(
disposable = disposable,
compileService = compileService,
clientAliveFlagFile = clientAliveFlagFile,
targetPlatform = targetPlatform,
templateClasspath = templateClasspath,
templateClassName = templateClassName,
compilerMessagesOutputStream = compilerMessagesOutputStream,
evalOutputStream = evalOutputStream,
evalErrorStream = evalErrorStream,
evalInputStream = evalInputStream,
port = port,
operationsTracer = operationsTracer
), ReplEvaluator {
override fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplEvalResult {
return compileService.remoteReplLineEval(sessionId, codeLine, history.toList()).get()
}
}
@@ -16,6 +16,8 @@
package org.jetbrains.kotlin.daemon.common package org.jetbrains.kotlin.daemon.common
import org.jetbrains.kotlin.cli.common.repl.*
import java.io.File
import java.io.Serializable import java.io.Serializable
import java.rmi.Remote import java.rmi.Remote
import java.rmi.RemoteException import java.rmi.RemoteException
@@ -44,7 +46,7 @@ interface CompileService : Remote {
override fun hashCode(): Int = this.javaClass.hashCode() + (result?.hashCode() ?: 1) override fun hashCode(): Int = this.javaClass.hashCode() + (result?.hashCode() ?: 1)
} }
class Ok : CallResult<Nothing>() { class Ok : CallResult<Nothing>() {
override fun get(): Nothing = throw IllegalStateException("Gey is inapplicable to Ok call result") override fun get(): Nothing = throw IllegalStateException("Get is inapplicable to Ok call result")
override fun equals(other: Any?): Boolean = other is Ok 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 override fun hashCode(): Int = this.javaClass.hashCode() + 1 // avoiding clash with the hash of class itself
} }
@@ -122,4 +124,42 @@ interface CompileService : Remote {
serviceOutputStream: RemoteOutputStream, serviceOutputStream: RemoteOutputStream,
operationsTracer: RemoteOperationsTracer? operationsTracer: RemoteOperationsTracer?
): CallResult<Int> ): CallResult<Int>
@Throws(RemoteException::class)
fun leaseReplSession(
aliveFlagPath: String?,
targetPlatform: CompileService.TargetPlatform,
servicesFacade: CompilerCallbackServicesFacade,
templateClasspath: List<File>,
templateClassName: String,
compilerMessagesOutputStream: RemoteOutputStream,
evalOutputStream: RemoteOutputStream?,
evalErrorStream: RemoteOutputStream?,
evalInputStream: RemoteInputStream?,
operationsTracer: RemoteOperationsTracer?
): CallResult<Int>
@Throws(RemoteException::class)
fun releaseReplSession(sessionId: Int): CallResult<Nothing>
@Throws(RemoteException::class)
fun remoteReplLineCheck(
sessionId: Int,
codeLine: ReplCodeLine,
history: List<ReplCodeLine>
): CallResult<ReplCheckResult>
@Throws(RemoteException::class)
fun remoteReplLineCompile(
sessionId: Int,
codeLine: ReplCodeLine,
history: List<ReplCodeLine>
): CallResult<ReplCompileResult>
@Throws(RemoteException::class)
fun remoteReplLineEval(
sessionId: Int,
codeLine: ReplCodeLine,
history: List<ReplCodeLine>
): CallResult<ReplEvalResult>
} }
@@ -16,11 +16,17 @@
package org.jetbrains.kotlin.daemon package org.jetbrains.kotlin.daemon
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.impl.ZipHandler import com.intellij.openapi.vfs.impl.ZipHandler
import com.intellij.openapi.vfs.impl.jar.CoreJarFileSystem import com.intellij.openapi.vfs.impl.jar.CoreJarFileSystem
import org.jetbrains.kotlin.cli.common.CLICompiler import org.jetbrains.kotlin.cli.common.CLICompiler
import org.jetbrains.kotlin.cli.common.ExitCode import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.KOTLIN_COMPILER_ENVIRONMENT_KEEPALIVE_PROPERTY import org.jetbrains.kotlin.cli.common.KOTLIN_COMPILER_ENVIRONMENT_KEEPALIVE_PROPERTY
import org.jetbrains.kotlin.cli.common.repl.ReplCheckResult
import org.jetbrains.kotlin.cli.common.repl.ReplCodeLine
import org.jetbrains.kotlin.cli.common.repl.ReplCompileResult
import org.jetbrains.kotlin.cli.common.repl.ReplEvalResult
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.Services import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.daemon.common.* import org.jetbrains.kotlin.daemon.common.*
@@ -45,6 +51,8 @@ import kotlin.concurrent.read
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.concurrent.write import kotlin.concurrent.write
const val REMOTE_STREAM_BUFFER_SIZE = 4096
fun nowSeconds() = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime()) fun nowSeconds() = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime())
interface CompilerSelector { interface CompilerSelector {
@@ -83,10 +91,16 @@ class CompileServiceImpl(
} }
// wrapped in a class to encapsulate alive check logic // wrapped in a class to encapsulate alive check logic
private class ClientOrSessionProxy(val aliveFlagPath: String?) { private class ClientOrSessionProxy<out T: Any>(val aliveFlagPath: String?, val data: T? = null, private var disposable: Disposable? = null) {
val registered = nowSeconds() val registered = nowSeconds()
val secondsSinceRegistered: Long get() = nowSeconds() - registered 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 val isAlive: Boolean get() = aliveFlagPath?.let { File(it).exists() } ?: true // assuming that if no file was given, the client is alive
fun dispose() {
disposable?.let {
Disposer.dispose(it)
disposable = null
}
}
} }
private val sessionsIdCounter = AtomicInteger(0) private val sessionsIdCounter = AtomicInteger(0)
@@ -103,8 +117,8 @@ class CompileServiceImpl(
// TODO: encapsulate operations on state here // TODO: encapsulate operations on state here
private val state = object { private val state = object {
val clientProxies: MutableSet<ClientOrSessionProxy> = hashSetOf() val clientProxies: MutableSet<ClientOrSessionProxy<Any>> = hashSetOf()
val sessions: MutableMap<Int, ClientOrSessionProxy> = hashMapOf() val sessions: MutableMap<Int, ClientOrSessionProxy<Any>> = hashMapOf()
val delayedShutdownQueued = AtomicBoolean(false) val delayedShutdownQueued = AtomicBoolean(false)
@@ -155,16 +169,20 @@ class CompileServiceImpl(
// TODO: consider tying a session to a client and use this info to cleanup // 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) { override fun leaseCompileSession(aliveFlagPath: String?): CompileService.CallResult<Int> = ifAlive(minAliveness = Aliveness.Alive) {
leaseSessionImpl(ClientOrSessionProxy<Any>(aliveFlagPath)).apply {
log.info("leased a new session $this, client alive file: $aliveFlagPath")
}
}
private fun<T: Any> leaseSessionImpl(session: ClientOrSessionProxy<T>): Int {
// fighting hypothetical integer wrapping // fighting hypothetical integer wrapping
var newId = sessionsIdCounter.incrementAndGet() var newId = sessionsIdCounter.incrementAndGet()
val session = ClientOrSessionProxy(aliveFlagPath)
for (attempt in 1..100) { for (attempt in 1..100) {
if (newId != CompileService.NO_SESSION) { if (newId != CompileService.NO_SESSION) {
synchronized(state.sessions) { synchronized(state.sessions) {
if (!state.sessions.containsKey(newId)) { if (!state.sessions.containsKey(newId)) {
state.sessions.put(newId, session) state.sessions.put(newId, session)
log.info("leased a new session $newId, client alive file: $aliveFlagPath") return newId
return@ifAlive newId
} }
} }
} }
@@ -176,6 +194,7 @@ class CompileServiceImpl(
override fun releaseCompileSession(sessionId: Int) = ifAlive_Nothing(minAliveness = Aliveness.LastSession) { override fun releaseCompileSession(sessionId: Int) = ifAlive_Nothing(minAliveness = Aliveness.LastSession) {
synchronized(state.sessions) { synchronized(state.sessions) {
state.sessions[sessionId]?.dispose()
state.sessions.remove(sessionId) state.sessions.remove(sessionId)
log.info("cleaning after session $sessionId") log.info("cleaning after session $sessionId")
clearJarCache() clearJarCache()
@@ -248,6 +267,64 @@ class CompileServiceImpl(
} }
} }
override fun leaseReplSession(
aliveFlagPath: String?,
targetPlatform: CompileService.TargetPlatform,
servicesFacade: CompilerCallbackServicesFacade,
templateClasspath: List<File>,
templateClassName: String,
compilerMessagesOutputStream: RemoteOutputStream,
evalOutputStream: RemoteOutputStream?,
evalErrorStream: RemoteOutputStream?,
evalInputStream: RemoteInputStream?,
operationsTracer: RemoteOperationsTracer?
): CompileService.CallResult<Int> = ifAlive_Raw(minAliveness = Aliveness.Alive) {
if (targetPlatform != CompileService.TargetPlatform.JVM)
CompileService.CallResult.Error("Sorry, only JVM target platform is supported now")
else {
val disposable = Disposer.newDisposable()
val repl = KotlinJvmReplService(disposable, templateClasspath, templateClassName, compilerMessagesOutputStream, evalOutputStream, evalErrorStream, evalInputStream, operationsTracer)
val sessionId = leaseSessionImpl(ClientOrSessionProxy(aliveFlagPath, repl, disposable))
CompileService.CallResult.Good(sessionId)
}
}
private fun<R> withValidRepl(sessionId: Int, body: KotlinJvmReplService.() -> R): CompileService.CallResult<R> =
state.sessions[sessionId]?.let {
(it.data as? KotlinJvmReplService?)?.let {
CompileService.CallResult.Good(it.body())
}
} ?: CompileService.CallResult.Error("Unknown or invalid session $sessionId")
// TODO: add more checks (e.g. is it a repl session)
override fun releaseReplSession(sessionId: Int): CompileService.CallResult<Nothing> = releaseCompileSession(sessionId)
override fun remoteReplLineCheck(sessionId: Int, codeLine: ReplCodeLine, history: List<ReplCodeLine>): CompileService.CallResult<ReplCheckResult> =
ifAlive_Raw(minAliveness = Aliveness.Alive) {
withValidRepl(sessionId) {
check(codeLine, history)
}
}
override fun remoteReplLineCompile(sessionId: Int, codeLine: ReplCodeLine, history: List<ReplCodeLine>): CompileService.CallResult<ReplCompileResult> =
ifAlive_Raw(minAliveness = Aliveness.Alive) {
withValidRepl(sessionId) {
compile(codeLine, history)
}
}
override fun remoteReplLineEval(
sessionId: Int,
codeLine: ReplCodeLine,
history: List<ReplCodeLine>
): CompileService.CallResult<ReplEvalResult> =
ifAlive_Raw(minAliveness = Aliveness.Alive) {
withValidRepl(sessionId) {
eval(codeLine, history)
}
}
// internal implementation stuff // internal implementation stuff
// TODO: consider matching compilerId coming from outside with actual one // TODO: consider matching compilerId coming from outside with actual one
@@ -302,19 +379,31 @@ class CompileServiceImpl(
shutdown() shutdown()
} }
else { else {
var anyDead = false
var shuttingDown = false
synchronized(state.sessions) { synchronized(state.sessions) {
// 2. check if any session hanged - clean // 2. check if any session hanged - clean
// making copy of the list before calling release state.sessions.filterValues { !it.isAlive }.apply { anyDead = true }.forEach {
state.sessions.filterValues { !it.isAlive }.keys.toList() it.value.dispose()
}.forEach { releaseCompileSession(it) } state.sessions.remove(it.key)
}
}
// 3. check if in graceful shutdown state and all sessions are closed // 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")) { if (shutdownCondition({ state.alive.get() == Aliveness.LastSession.ordinal && state.sessions.none()}, "All sessions finished, shutting down")) {
shutdown() shutdown()
shuttingDown = true
} }
// 4. clean dead clients, then check if any left - conditional shutdown (with small delay) // 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 }) } synchronized(state.clientProxies) {
state.clientProxies.removeAll(
state.clientProxies.filter { !it.isAlive }.apply { anyDead = true }.map {
it.dispose()
it
})
}
if (state.clientProxies.isEmpty() && compilationsCounter.get() > 0 && !state.delayedShutdownQueued.get()) { if (state.clientProxies.isEmpty() && compilationsCounter.get() > 0 && !state.delayedShutdownQueued.get()) {
log.info("No more clients left, delayed shutdown in ${daemonOptions.shutdownDelayMilliseconds}ms") log.info("No more clients left, delayed shutdown in ${daemonOptions.shutdownDelayMilliseconds}ms")
shutdownWithDelay() shutdownWithDelay()
@@ -326,8 +415,14 @@ class CompileServiceImpl(
shutdownCondition({ !runFile.exists() }, "Run file removed, shutting down") || shutdownCondition({ !runFile.exists() }, "Run file removed, shutting down") ||
// 7. compiler changed (seldom check) - shutdown // 7. compiler changed (seldom check) - shutdown
// TODO: could be too expensive anyway, consider removing this check // TODO: could be too expensive anyway, consider removing this check
shutdownCondition({ classpathWatcher.isChanged }, "Compiler changed")) { shutdownCondition({ classpathWatcher.isChanged }, "Compiler changed"))
{
shutdown() shutdown()
shuttingDown = true
}
if (anyDead && !shuttingDown) {
clearJarCache()
} }
} }
} }
@@ -374,6 +469,7 @@ class CompileServiceImpl(
private fun shutdownImpl() { private fun shutdownImpl() {
log.info("Shutdown started") log.info("Shutdown started")
state.alive.set(Aliveness.Dying.ordinal) state.alive.set(Aliveness.Dying.ordinal)
UnicastRemoteObject.unexportObject(this, true) UnicastRemoteObject.unexportObject(this, true)
log.info("Shutdown complete") log.info("Shutdown complete")
onShutdown() onShutdown()
@@ -414,8 +510,8 @@ class CompileServiceImpl(
compilationsCounter.incrementAndGet() compilationsCounter.incrementAndGet()
val rpcProfiler = if (daemonOptions.reportPerf) WallAndThreadTotalProfiler() else DummyProfiler() val rpcProfiler = if (daemonOptions.reportPerf) WallAndThreadTotalProfiler() else DummyProfiler()
val eventManger = EventMangerImpl() val eventManger = EventMangerImpl()
val compilerMessagesStream = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(compilerMessagesStreamProxy, rpcProfiler), 4096)) val compilerMessagesStream = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(compilerMessagesStreamProxy, rpcProfiler), REMOTE_STREAM_BUFFER_SIZE))
val serviceOutputStream = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(serviceOutputStreamProxy, rpcProfiler), 4096)) val serviceOutputStream = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(serviceOutputStreamProxy, rpcProfiler), REMOTE_STREAM_BUFFER_SIZE))
try { try {
checkedCompile(args, serviceOutputStream, rpcProfiler) { checkedCompile(args, serviceOutputStream, rpcProfiler) {
val res = body(compilerMessagesStream, eventManger, rpcProfiler).code val res = body(compilerMessagesStream, eventManger, rpcProfiler).code
@@ -490,17 +586,6 @@ class CompileServiceImpl(
(KotlinCoreEnvironment.applicationEnvironment?.jarFileSystem as? CoreJarFileSystem)?.clearHandlersCache() (KotlinCoreEnvironment.applicationEnvironment?.jarFileSystem as? CoreJarFileSystem)?.clearHandlersCache()
} }
// 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 { 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()) } ifAliveChecksImpl(minAliveness, ignoreCompilerChanged) { CompileService.CallResult.Good(body()) }
} }
@@ -513,6 +598,10 @@ class CompileServiceImpl(
} }
} }
private fun<R> ifAlive_Raw(minAliveness: Aliveness = Aliveness.Alive, ignoreCompilerChanged: Boolean = false, body: () -> CompileService.CallResult<R>): CompileService.CallResult<R> = rwlock.read {
ifAliveChecksImpl(minAliveness, ignoreCompilerChanged) { body() }
}
private fun<R> ifAliveExclusive(minAliveness: Aliveness = Aliveness.Alive, ignoreCompilerChanged: Boolean = false, body: () -> R): CompileService.CallResult<R> = rwlock.write { 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()) } ifAliveChecksImpl(minAliveness, ignoreCompilerChanged) { CompileService.CallResult.Good(body()) }
} }
@@ -0,0 +1,177 @@
/*
* Copyright 2010-2016 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.Disposable
import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.cli.common.repl.*
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots
import org.jetbrains.kotlin.cli.jvm.repl.GenericReplCompiler
import org.jetbrains.kotlin.cli.jvm.repl.compileAndEval
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.daemon.common.DummyProfiler
import org.jetbrains.kotlin.daemon.common.RemoteInputStream
import org.jetbrains.kotlin.daemon.common.RemoteOperationsTracer
import org.jetbrains.kotlin.daemon.common.RemoteOutputStream
import org.jetbrains.kotlin.script.KotlinScriptDefinition
import org.jetbrains.kotlin.script.KotlinScriptDefinitionFromTemplate
import org.jetbrains.kotlin.utils.PathUtil
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.PrintStream
import java.net.URLClassLoader
open class KotlinJvmReplService(
disposable: Disposable,
templateClasspath: List<File>,
templateClassName: String,
compilerOutputStreamProxy: RemoteOutputStream,
evalOutputStream: RemoteOutputStream?,
evalErrorStream: RemoteOutputStream?,
evalInputStream: RemoteInputStream?,
val operationsTracer: RemoteOperationsTracer?
) : ReplCompiler, ReplEvaluator {
protected val compilerMessagesStream = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(compilerOutputStreamProxy, DummyProfiler()), REMOTE_STREAM_BUFFER_SIZE))
protected class KeepFirstErrorMessageCollector(compilerMessagesStream: PrintStream) : MessageCollector {
private val innerCollector = PrintingMessageCollector(compilerMessagesStream, MessageRenderer.WITHOUT_PATHS, false)
internal var firstErrorMessage: String? = null
internal var firstErrorLocation: CompilerMessageLocation? = null
override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation) {
if (firstErrorMessage == null && severity.isError) {
firstErrorMessage = message
firstErrorLocation = location
}
innerCollector.report(severity, message, location)
}
override fun hasErrors(): Boolean = innerCollector.hasErrors()
override fun clear() {
innerCollector.clear()
}
}
protected val messageCollector = KeepFirstErrorMessageCollector(compilerMessagesStream)
protected val configuration = CompilerConfiguration().apply {
addJvmClasspathRoots(PathUtil.getJdkClassesRoots())
addJvmClasspathRoots(PathUtil.getKotlinPathsForCompiler().let { listOf(it.runtimePath, it.reflectPath) })
addJvmClasspathRoots(templateClasspath)
put(CommonConfigurationKeys.MODULE_NAME, "kotlin-script")
}
protected fun makeScriptDefinition(templateClasspath: List<File>, templateClassName: String): KotlinScriptDefinition? {
val classloader = URLClassLoader(templateClasspath.map { it.toURI().toURL() }.toTypedArray(), this.javaClass.classLoader)
try {
val cls = classloader.loadClass(templateClassName)
val def = KotlinScriptDefinitionFromTemplate(cls.kotlin, null, null, emptyMap())
messageCollector.report(
CompilerMessageSeverity.INFO,
"New script definition $templateClassName: files pattern = \"${def.scriptFilePattern}\", resolver = ${def.resolver?.javaClass?.name}",
CompilerMessageLocation.NO_LOCATION
)
return def
}
catch (ex: ClassNotFoundException) {
messageCollector.report(
CompilerMessageSeverity.ERROR, "Cannot find script definition template class $templateClassName", CompilerMessageLocation.NO_LOCATION
)
}
catch (ex: Exception) {
messageCollector.report(
CompilerMessageSeverity.ERROR, "Error processing script definition template $templateClassName: ${ex.message}", CompilerMessageLocation.NO_LOCATION
)
}
return null
}
private val scriptDef = makeScriptDefinition(templateClasspath, templateClassName)
private val replCompiler : GenericReplCompiler? by lazy {
if (scriptDef == null) null
else GenericReplCompiler(disposable, scriptDef, configuration, messageCollector)
}
private val compiledEvaluator : GenericReplCompiledEvaluator by lazy {
if (evalOutputStream == null && evalErrorStream == null && evalInputStream == null)
GenericReplCompiledEvaluator(configuration.jvmClasspathRoots, null)
else object : GenericReplCompiledEvaluator(configuration.jvmClasspathRoots, null) {
val out = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(evalOutputStream!!, DummyProfiler()), REMOTE_STREAM_BUFFER_SIZE))
val err = PrintStream(BufferedOutputStream(RemoteOutputStreamClient(evalErrorStream!!, DummyProfiler()), REMOTE_STREAM_BUFFER_SIZE))
val `in` = BufferedInputStream(RemoteInputStreamClient(evalInputStream!!, DummyProfiler()), REMOTE_STREAM_BUFFER_SIZE)
override fun<T> evalWithIO(body: () -> T): T {
val prevOut = System.out
System.setOut(out)
val prevErr = System.err
System.setErr(err)
val prevIn = System.`in`
System.setIn(`in`)
try {
return body()
}
finally {
System.setIn(prevIn)
System.setErr(prevErr)
System.setOut(prevOut)
}
}
}
}
override fun check(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplCheckResult {
operationsTracer?.before("check")
try {
return replCompiler?.check(codeLine, history)
?: ReplCheckResult.Error(messageCollector.firstErrorMessage ?: "Unknown error",
messageCollector.firstErrorLocation ?: CompilerMessageLocation.NO_LOCATION)
}
finally {
operationsTracer?.after("check")
}
}
override fun compile(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplCompileResult {
operationsTracer?.before("compile")
try {
return replCompiler?.compile(codeLine, history)
?: ReplCompileResult.Error(messageCollector.firstErrorMessage ?: "Unknown error",
messageCollector.firstErrorLocation ?: CompilerMessageLocation.NO_LOCATION)
}
finally {
operationsTracer?.after("compile")
}
}
override fun eval(codeLine: ReplCodeLine, history: Iterable<ReplCodeLine>): ReplEvalResult = synchronized(this) {
operationsTracer?.before("eval")
try {
return replCompiler?.let { compileAndEval(it, compiledEvaluator, codeLine, history) }
?: ReplEvalResult.Error.CompileTime(messageCollector.firstErrorMessage ?: "Unknown error",
messageCollector.firstErrorLocation ?: CompilerMessageLocation.NO_LOCATION)
}
finally {
operationsTracer?.after("eval")
}
}
}
@@ -16,11 +16,11 @@
package org.jetbrains.kotlin.daemon package org.jetbrains.kotlin.daemon
import com.intellij.openapi.util.Disposer
import junit.framework.TestCase
import org.jetbrains.kotlin.cli.AbstractCliTest import org.jetbrains.kotlin.cli.AbstractCliTest
import org.jetbrains.kotlin.daemon.client.CompilerCallbackServicesFacadeServer import org.jetbrains.kotlin.cli.common.repl.*
import org.jetbrains.kotlin.daemon.client.DaemonReportingTargets import org.jetbrains.kotlin.daemon.client.*
import org.jetbrains.kotlin.daemon.client.KotlinCompilerClient
import org.jetbrains.kotlin.daemon.client.RemoteOutputStreamServer
import org.jetbrains.kotlin.daemon.common.* import org.jetbrains.kotlin.daemon.common.*
import org.jetbrains.kotlin.integration.KotlinIntegrationTestBase import org.jetbrains.kotlin.integration.KotlinIntegrationTestBase
import org.jetbrains.kotlin.progress.CompilationCanceledStatus import org.jetbrains.kotlin.progress.CompilationCanceledStatus
@@ -29,8 +29,11 @@ import org.jetbrains.kotlin.test.KotlinTestUtils
import org.jetbrains.kotlin.utils.PathUtil import org.jetbrains.kotlin.utils.PathUtil
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.net.URL
import java.net.URLClassLoader
import java.rmi.server.UnicastRemoteObject import java.rmi.server.UnicastRemoteObject
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.test.fail import kotlin.test.fail
@@ -81,7 +84,7 @@ class CompilerDaemonTest : KotlinIntegrationTestBase() {
val logFile = createTempFile("kotlin-daemon-test.", ".log") val logFile = createTempFile("kotlin-daemon-test.", ".log")
val daemonJVMOptions = configureDaemonJVMOptions("D${COMPILE_DAEMON_LOG_PATH_PROPERTY}=\"${logFile.loggerCompatiblePath}\"", val daemonJVMOptions = configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile.loggerCompatiblePath}\"",
inheritMemoryLimits = false, inheritAdditionalProperties = false) inheritMemoryLimits = false, inheritAdditionalProperties = false)
var daemonShotDown = false var daemonShotDown = false
@@ -159,11 +162,11 @@ class CompilerDaemonTest : KotlinIntegrationTestBase() {
val logFile1 = createTempFile("kotlin-daemon1-test", ".log") val logFile1 = createTempFile("kotlin-daemon1-test", ".log")
val logFile2 = createTempFile("kotlin-daemon2-test", ".log") val logFile2 = createTempFile("kotlin-daemon2-test", ".log")
val daemonJVMOptions1 = val daemonJVMOptions1 =
configureDaemonJVMOptions("D${COMPILE_DAEMON_LOG_PATH_PROPERTY}=\"${logFile1.loggerCompatiblePath}\"", configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile1.loggerCompatiblePath}\"",
inheritMemoryLimits = false, inheritAdditionalProperties = false) inheritMemoryLimits = false, inheritAdditionalProperties = false)
val daemonJVMOptions2 = val daemonJVMOptions2 =
configureDaemonJVMOptions("D${COMPILE_DAEMON_LOG_PATH_PROPERTY}=\"${logFile2.loggerCompatiblePath}\"", configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile2.loggerCompatiblePath}\"",
inheritMemoryLimits = false, inheritAdditionalProperties = false) inheritMemoryLimits = false, inheritAdditionalProperties = false)
assertTrue(logFile1.length() == 0L && logFile2.length() == 0L) assertTrue(logFile1.length() == 0L && logFile2.length() == 0L)
@@ -198,7 +201,7 @@ class CompilerDaemonTest : KotlinIntegrationTestBase() {
val logFile = createTempFile("kotlin-daemon-test", ".log") val logFile = createTempFile("kotlin-daemon-test", ".log")
val daemonJVMOptions = val daemonJVMOptions =
configureDaemonJVMOptions("D${COMPILE_DAEMON_LOG_PATH_PROPERTY}=\"${logFile.loggerCompatiblePath}\"", configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile.loggerCompatiblePath}\"",
inheritMemoryLimits = false, inheritAdditionalProperties = false) inheritMemoryLimits = false, inheritAdditionalProperties = false)
val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true) val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true)
@@ -225,7 +228,7 @@ class CompilerDaemonTest : KotlinIntegrationTestBase() {
val logFile = createTempFile("kotlin-daemon-test", ".log") val logFile = createTempFile("kotlin-daemon-test", ".log")
val daemonJVMOptions = val daemonJVMOptions =
configureDaemonJVMOptions("D${COMPILE_DAEMON_LOG_PATH_PROPERTY}=\"${logFile.loggerCompatiblePath}\"", configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile.loggerCompatiblePath}\"",
inheritMemoryLimits = false, inheritAdditionalProperties = false) inheritMemoryLimits = false, inheritAdditionalProperties = false)
val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true) val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true)
@@ -257,7 +260,7 @@ class CompilerDaemonTest : KotlinIntegrationTestBase() {
val logFile = createTempFile("kotlin-daemon-test", ".log") val logFile = createTempFile("kotlin-daemon-test", ".log")
val daemonJVMOptions = val daemonJVMOptions =
configureDaemonJVMOptions("D${COMPILE_DAEMON_LOG_PATH_PROPERTY}=\"${logFile.loggerCompatiblePath}\"", configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile.loggerCompatiblePath}\"",
inheritMemoryLimits = false, inheritAdditionalProperties = false) inheritMemoryLimits = false, inheritAdditionalProperties = false)
val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true) val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true)
@@ -304,7 +307,7 @@ class CompilerDaemonTest : KotlinIntegrationTestBase() {
val jar = tmpdir.absolutePath + File.separator + "hello.jar" val jar = tmpdir.absolutePath + File.separator + "hello.jar"
val args = listOf( val args = listOf(
File(File(System.getProperty("java.home"), "bin"), "java").absolutePath, File(File(System.getProperty("java.home"), "bin"), "java").absolutePath,
"-D${COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY}", "-D$COMPILE_DAEMON_VERBOSE_REPORT_PROPERTY",
"-cp", "-cp",
daemonClientClassPath.joinToString(File.pathSeparator) { it.absolutePath }, daemonClientClassPath.joinToString(File.pathSeparator) { it.absolutePath },
KotlinCompilerClient::class.qualifiedName!!) + KotlinCompilerClient::class.qualifiedName!!) +
@@ -430,7 +433,7 @@ class CompilerDaemonTest : KotlinIntegrationTestBase() {
val logFile = createTempFile("kotlin-daemon-test", ".log") val logFile = createTempFile("kotlin-daemon-test", ".log")
val daemonJVMOptions = val daemonJVMOptions =
configureDaemonJVMOptions("D${COMPILE_DAEMON_LOG_PATH_PROPERTY}=\"${logFile.loggerCompatiblePath}\"", configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile.loggerCompatiblePath}\"",
inheritMemoryLimits = false, inheritAdditionalProperties = false) inheritMemoryLimits = false, inheritAdditionalProperties = false)
val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true) val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true)
@@ -467,8 +470,69 @@ class CompilerDaemonTest : KotlinIntegrationTestBase() {
logFile.delete() logFile.delete()
} }
} }
fun testDaemonReplLocalEval() {
withFlagFile(getTestName(true), ".alive") { flagFile ->
val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath)
val daemonJVMOptions = configureDaemonJVMOptions(inheritMemoryLimits = false, inheritAdditionalProperties = false)
val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true)
assertNotNull("failed to connect daemon", daemon)
val disposable = Disposer.newDisposable()
val repl = KotlinRemoteReplCompiler(disposable, daemon!!, null, CompileService.TargetPlatform.JVM,
classpathFromClassloader(),
ScriptWithNoParam::class.qualifiedName!!, System.err)
val res1 = repl.check(ReplCodeLine(0, "val x ="), emptyList())
TestCase.assertTrue("Unexpected check results: $res1", res1 is ReplCheckResult.Incomplete)
val codeLine = ReplCodeLine(0, "1 + 2")
val res2 = repl.compile(codeLine, emptyList())
val compileRes = res2 as? ReplCompileResult.CompiledClasses
TestCase.assertNotNull("Unexpected compile result: $res2", compileRes)
val localEvaluator = GenericReplCompiledEvaluator(emptyList(), Thread.currentThread().contextClassLoader)
val res3 = localEvaluator.eval(codeLine, emptyList(), compileRes!!.classes, compileRes.hasResult, compileRes.newClasspath)
val evalRes = res3 as? ReplEvalResult.ValueResult
TestCase.assertNotNull("Unexpected eval result: $res3", evalRes)
TestCase.assertEquals(3, evalRes!!.value)
Disposer.dispose(disposable)
}
}
// fun testDaemonReplRemoteEval() {
// withFlagFile(getTestName(true), ".alive") { flagFile ->
// val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath)
// val daemonJVMOptions = configureDaemonJVMOptions(inheritMemoryLimits = false, inheritAdditionalProperties = false)
// val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true)
// assertNotNull("failed to connect daemon", daemon)
//
// val evalOut = ByteArrayOutputStream()
// val evalErr = ByteArrayOutputStream()
// val evalIn = ByteArrayInputStream("42\n".toByteArray())
//
// val disposable = Disposer.newDisposable()
//
// val repl = KotlinRemoteReplEvaluator(disposable, daemon!!, null, CompileService.TargetPlatform.JVM, emptyList(),
// "<StandardScriptTemplate>", System.err, evalOut, evalErr, evalIn)
//
// val res1 = repl.check(ReplCodeLine(0, "val x ="), emptyList())
// TestCase.assertEquals(ReplCheckResult.Incomplete, res1)
//
// val res2 = repl.eval(ReplCodeLine(0, "1 + 2"), emptyList())
// TestCase.assertEquals(ReplEvalResult.ValueResult(3), res2)
//
// Disposer.dispose(disposable)
// }
// }
} }
// stolen from CompilerFileLimitTest // stolen from CompilerFileLimitTest
internal fun generateLargeKotlinFile(size: Int): String { internal fun generateLargeKotlinFile(size: Int): String {
return buildString { return buildString {
@@ -537,4 +601,39 @@ internal inline fun withFlagFile(prefix: String, suffix: String? = null, body: (
private val File.loggerCompatiblePath: String private val File.loggerCompatiblePath: String
get() = get() =
if (OSKind.current == OSKind.Windows) absolutePath.replace('\\', '/') if (OSKind.current == OSKind.Windows) absolutePath.replace('\\', '/')
else absolutePath else absolutePath
open class TestKotlinScriptDummyDependenciesResolver : ScriptDependenciesResolver {
private val kotlinPaths by lazy { PathUtil.getKotlinPathsForCompiler() }
override fun resolve(script: ScriptContents,
environment: Map<String, Any?>?,
report: (ScriptDependenciesResolver.ReportSeverity, String, ScriptContents.Position?) -> Unit,
previousDependencies: KotlinScriptExternalDependencies?
): Future<KotlinScriptExternalDependencies?>
{
return object : KotlinScriptExternalDependencies {
override val classpath: Iterable<File> = classpathFromClassloader()
override val imports: Iterable<String> = listOf("org.jetbrains.kotlin.scripts.DependsOn", "org.jetbrains.kotlin.scripts.DependsOnTwo")
}.asFuture()
}
}
@ScriptTemplateDefinition(resolver = TestKotlinScriptDummyDependenciesResolver::class)
abstract class ScriptWithNoParam()
internal fun classpathFromClassloader(): List<File> =
(TestKotlinScriptDummyDependenciesResolver::class.java.classLoader as? URLClassLoader)?.urLs
?.mapNotNull { it.toFile() }
?.filter { it.path.contains("out") && it.path.contains("test") }
?: emptyList()
internal fun URL.toFile() =
try {
File(toURI().schemeSpecificPart)
}
catch (e: java.net.URISyntaxException) {
if (protocol != "file") null
else File(file)
}