diff --git a/.github/workflows/build-and-run-tests-from-branch.yml b/.github/workflows/build-and-run-tests-from-branch.yml index a6fb5a3bdc..cb74efaa6d 100644 --- a/.github/workflows/build-and-run-tests-from-branch.yml +++ b/.github/workflows/build-and-run-tests-from-branch.yml @@ -109,7 +109,7 @@ jobs: # The option forces to execute all jobs even though some of them have failed. fail-fast: false matrix: - project: [utbot-api, utbot-cli, utbot-core, utbot-framework-api, utbot-fuzzers, utbot-gradle, utbot-instrumentation, utbot-instrumentation-tests, utbot-intellij, utbot-junit-contest, utbot-sample, utbot-summary, utbot-summary-tests] + project: [utbot-api, utbot-cli, utbot-core, utbot-framework-api, utbot-fuzzers, utbot-gradle, utbot-instrumentation, utbot-instrumentation-tests, utbot-intellij, utbot-junit-contest, utbot-rd, utbot-sample, utbot-summary, utbot-summary-tests] runs-on: ubuntu-20.04 container: unittestbot/java-env:java11-zulu-jdk-gradle7.4.2-kotlinc1.7.0 steps: diff --git a/settings.gradle b/settings.gradle index f1b58b4358..78fb689b9d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,13 @@ +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.name == "rdgen") { + useModule("com.jetbrains.rd:rd-gen:${requested.version}") + } + } + } +} + rootProject.name = 'utbot' include 'utbot-core' @@ -17,4 +27,5 @@ include 'utbot-summary' include 'utbot-gradle' //include 'utbot-maven' include 'utbot-summary-tests' +include 'utbot-rd' diff --git a/utbot-analytics/build.gradle b/utbot-analytics/build.gradle index 5a7e682ea7..ba34f8081b 100644 --- a/utbot-analytics/build.gradle +++ b/utbot-analytics/build.gradle @@ -82,4 +82,5 @@ jar { } duplicatesStrategy = DuplicatesStrategy.EXCLUDE + zip64 = true } \ No newline at end of file diff --git a/utbot-core/src/main/kotlin/org/utbot/common/ProcessUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/ProcessUtil.kt deleted file mode 100644 index fb6e0b4dbd..0000000000 --- a/utbot-core/src/main/kotlin/org/utbot/common/ProcessUtil.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.utbot.common - -import com.sun.jna.Pointer -import com.sun.jna.platform.win32.Kernel32 -import com.sun.jna.platform.win32.WinNT -import java.lang.management.ManagementFactory - -val Process.pid : Long get() = try { - when (javaClass.name) { - "java.lang.UNIXProcess" -> { - val fPid = javaClass.getDeclaredField("pid") - fPid.withAccessibility { fPid.getLong(this) } - - } - "java.lang.Win32Process", "java.lang.ProcessImpl" -> { - val fHandle = javaClass.getDeclaredField("handle") - fHandle.withAccessibility { - val handle = fHandle.getLong(this) - val winntHandle = WinNT.HANDLE() - winntHandle.pointer = Pointer.createConstant(handle) - Kernel32.INSTANCE.GetProcessId(winntHandle).toLong() - } - } - else -> -1 - } -} catch (e: Exception) { -2 } - -fun getCurrentProcessId() = - try { - ManagementFactory.getRuntimeMXBean()?.let { - it.name.split("@")[0].toLong() - } ?: -1 - } catch (t: Throwable) { - -1 - } \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/UtExecutionResult.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/UtExecutionResult.kt index dfb4235b37..b242ab39ec 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/UtExecutionResult.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/UtExecutionResult.kt @@ -52,13 +52,13 @@ class ConcreteExecutionFailureException(cause: Throwable, errorFile: File, val p appendLine("----------------------------------------") appendLine("The child process is dead") appendLine("Cause:\n${cause.message}") - appendLine("Last 20 lines of the error log ${errorFile.absolutePath}:") + appendLine("Last 1000 lines of the error log ${errorFile.absolutePath}:") appendLine("----------------------------------------") errorFile.useLines { lines -> val lastLines = LinkedList() for (line in lines) { lastLines.add(line) - if (lastLines.size > 20) { + if (lastLines.size > 1000) { lastLines.removeFirst() } } diff --git a/utbot-framework/build.gradle b/utbot-framework/build.gradle index e1c18ee7fa..f4949c02e6 100644 --- a/utbot-framework/build.gradle +++ b/utbot-framework/build.gradle @@ -20,6 +20,9 @@ dependencies { //implementation 'junit:junit:4.13.1' api project(':utbot-framework-api') + implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: '2022.3.1' + implementation group: 'com.jetbrains.rd', name: 'rd-core', version: '2022.3.1' + implementation "com.github.UnitTestBot:soot:${soot_commit_hash}" implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: jackson_version diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt index 087df15112..ab7eb7e770 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -39,6 +38,7 @@ import org.utbot.framework.util.jimpleBody import org.utbot.framework.util.runSoot import org.utbot.framework.util.toModel import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.warmup import org.utbot.instrumentation.warmup.Warmup import java.io.File import java.nio.file.Path diff --git a/utbot-instrumentation-tests/build.gradle b/utbot-instrumentation-tests/build.gradle index 9a923850c9..79c2288db0 100644 --- a/utbot-instrumentation-tests/build.gradle +++ b/utbot-instrumentation-tests/build.gradle @@ -14,6 +14,8 @@ dependencies { testImplementation configurations.fetchInstrumentationJar testImplementation project(':utbot-sample') testImplementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacoco_version + implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: '2022.3.1' + implementation group: 'com.jetbrains.rd', name: 'rd-core', version: '2022.3.1' } processResources { diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestCoverageInstrumentation.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestCoverageInstrumentation.kt index 5abd654914..40599120de 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestCoverageInstrumentation.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestCoverageInstrumentation.kt @@ -1,5 +1,6 @@ package org.utbot.examples +import com.jetbrains.rd.util.reactive.RdFault import org.utbot.examples.samples.ExampleClass import org.utbot.examples.statics.substitution.StaticSubstitution import org.utbot.examples.statics.substitution.StaticSubstitutionExamples @@ -74,9 +75,10 @@ class TestCoverageInstrumentation { } assertInstanceOf( - IllegalArgumentException::class.java, + RdFault::class.java, exc.cause!! ) + assertTrue((exc.cause as RdFault).reasonTypeFqn == "IllegalArgumentException") } } @@ -96,9 +98,10 @@ class TestCoverageInstrumentation { } assertInstanceOf( - IllegalArgumentException::class.java, + RdFault::class.java, exc.cause!! ) + assertTrue((exc.cause as RdFault).reasonTypeFqn == "IllegalArgumentException") it.execute(ExampleClass::bar, arrayOf(testObject, 2)) val coverageInfo1 = it.collectCoverage(ExampleClass::class.java) diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeInstrumentation.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeInstrumentation.kt index 9b0b06669e..7e4bc49eb4 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeInstrumentation.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeInstrumentation.kt @@ -1,5 +1,6 @@ package org.utbot.examples +import com.jetbrains.rd.util.reactive.RdFault import org.utbot.examples.samples.ClassWithSameMethodNames import org.utbot.examples.samples.ExampleClass import org.utbot.examples.samples.staticenvironment.StaticExampleClass @@ -46,9 +47,10 @@ class TestInvokeInstrumentation { ) } assertInstanceOf( - IllegalArgumentException::class.java, + RdFault::class.java, exc.cause!! ) + assertTrue((exc.cause as RdFault).reasonTypeFqn == "IllegalArgumentException") } } diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestIsolated.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestIsolated.kt index c86695d4e0..dca3af7c1c 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestIsolated.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestIsolated.kt @@ -1,5 +1,6 @@ package org.utbot.examples +import com.jetbrains.rd.util.reactive.RdFault import org.utbot.examples.samples.ExampleClass import org.utbot.examples.samples.staticenvironment.StaticExampleClass import org.utbot.instrumentation.ConcreteExecutor @@ -52,9 +53,10 @@ class TestIsolated { } assertInstanceOf( - IllegalArgumentException::class.java, + RdFault::class.java, exc.cause!! ) + assertTrue((exc.cause as RdFault).reasonTypeFqn == "IllegalArgumentException") } } diff --git a/utbot-instrumentation/build.gradle b/utbot-instrumentation/build.gradle index f3d8911979..cccb18fe61 100644 --- a/utbot-instrumentation/build.gradle +++ b/utbot-instrumentation/build.gradle @@ -2,6 +2,7 @@ apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" dependencies { api project(':utbot-framework-api') + implementation project(':utbot-rd') implementation group: 'org.ow2.asm', name: 'asm', version: asm_version implementation group: 'org.ow2.asm', name: 'asm-commons', version: asm_version @@ -10,6 +11,10 @@ dependencies { implementation group: 'de.javakaffee', name: 'kryo-serializers', version: kryo_serializers_version implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version + implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: '2022.3.1' + implementation group: 'com.jetbrains.rd', name: 'rd-core', version: '2022.3.1' + + // TODO: this is necessary for inline classes mocking in UtExecutionInstrumentation implementation group: 'org.mockito', name: 'mockito-core', version: '4.2.0' implementation group: 'org.mockito', name: 'mockito-inline', version: '4.2.0' @@ -41,4 +46,4 @@ configurations { artifacts { instrumentationArchive jar -} +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt index 6da1aff7f3..ee878aaecd 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt @@ -1,30 +1,26 @@ package org.utbot.instrumentation -import java.io.Closeable -import java.io.InputStream -import java.util.concurrent.TimeUnit +import com.jetbrains.rd.util.Logger +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import com.jetbrains.rd.util.lifetime.isAlive +import com.jetbrains.rd.util.lifetime.throwIfNotAlive import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import mu.KotlinLogging -import org.utbot.common.bracket -import org.utbot.common.catch -import org.utbot.common.currentThreadInfo -import org.utbot.common.debug -import org.utbot.common.pid -import org.utbot.common.trace import org.utbot.framework.plugin.api.ConcreteExecutionFailureException import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.signature import org.utbot.instrumentation.instrumentation.Instrumentation import org.utbot.instrumentation.process.ChildProcessRunner +import org.utbot.instrumentation.rd.UtInstrumentationProcess +import org.utbot.instrumentation.rd.UtRdLoggerFactory +import org.utbot.instrumentation.rd.generated.InvokeMethodCommandParams import org.utbot.instrumentation.util.ChildProcessError -import org.utbot.instrumentation.util.KryoHelper -import org.utbot.instrumentation.util.Protocol -import org.utbot.instrumentation.util.UnexpectedCommand +import java.io.Closeable +import java.util.concurrent.atomic.AtomicLong import kotlin.concurrent.thread import kotlin.reflect.KCallable import kotlin.reflect.KFunction @@ -73,7 +69,7 @@ class ConcreteExecutorPool(val maxCount: Int = Settings.defaultConcreteExecutorP @Suppress("UNCHECKED_CAST") return executors.firstOrNull { - it.pathsToUserClasses == pathsToUserClasses && it.instrumentation == instrumentation + it.pathsToUserClasses == pathsToUserClasses && it.instrumentation == instrumentation && it.pathsToDependencyClasses == pathsToDependencyClasses } as? ConcreteExecutor ?: ConcreteExecutor.createNew(instrumentation, pathsToUserClasses, pathsToDependencyClasses).apply { executors.addFirst(this) @@ -112,16 +108,24 @@ class ConcreteExecutor> p internal val pathsToUserClasses: String, internal val pathsToDependencyClasses: String ) : Closeable, Executor { + private val ldef: LifetimeDefinition = LifetimeDefinition() + private val childProcessRunner: ChildProcessRunner = ChildProcessRunner() companion object { - const val ERROR_CMD_ID = 0L - var nextCommandId = 1L + + private val sendTimestamp = AtomicLong() + private val receiveTimeStamp = AtomicLong() + val lastSendTimeMs: Long + get() = sendTimestamp.get() + val lastReceiveTimeMs: Long + get() = receiveTimeStamp.get() val defaultPool = ConcreteExecutorPool() var defaultPathsToDependencyClasses = "" - - var lastSendTimeMs = 0L - var lastReceiveTimeMs = 0L + init { + Logger.set(Lifetime.Eternal, UtRdLoggerFactory) + Runtime.getRuntime().addShutdownHook(thread(start = false) { defaultPool.close() }) + } /** * Delegates creation of the concrete executor to [defaultPool], which first searches for existing executor @@ -140,13 +144,92 @@ class ConcreteExecutor> p ) = ConcreteExecutor(instrumentation, pathsToUserClasses, pathsToDependencyClasses) } - private val childProcessRunner = ChildProcessRunner() - var classLoader: ClassLoader? = UtContext.currentContext()?.classLoader //property that signals to executors pool whether it can reuse this executor or not - var alive = true - private set + val alive: Boolean + get() = ldef.isAlive + + private val corMutex = Mutex() + private var processInstance: UtInstrumentationProcess? = null + + // this function is intended to be called under corMutex + private suspend fun regenerate(): UtInstrumentationProcess { + ldef.throwIfNotAlive() + + var proc: UtInstrumentationProcess? = processInstance + + if (proc == null || !proc.lifetime.isAlive) { + proc = UtInstrumentationProcess( + ldef, + childProcessRunner, + instrumentation, + pathsToUserClasses, + pathsToDependencyClasses, + classLoader + ) + processInstance = proc + } + + return proc + } + + /** + * Main entry point for communicating with child process. + * Use this function every time you want to access protocol model. + * This method prepares child process for execution and ensures it is alive before giving it block + * + * @param exclusively if true - executes block under mutex. + * This guarantees that no one can access protocol model - no other calls made before block completes + */ + suspend fun withProcess(exclusively: Boolean = false, block: suspend UtInstrumentationProcess.() -> T): T { + fun throwConcreteIfDead(e: Throwable, proc: UtInstrumentationProcess?) { + if (proc?.lifetime?.isAlive != true) { + throw ConcreteExecutionFailureException(e, + childProcessRunner.errorLogFile, + try { + proc?.run { process.inputStream.bufferedReader().lines().toList() } ?: emptyList() + } catch (e: Exception) { + emptyList() + } + ) + } + } + + sendTimestamp.set(System.currentTimeMillis()) + + var proc: UtInstrumentationProcess? = null + + try { + if (exclusively) { + corMutex.withLock { + proc = regenerate() + return proc!!.block() + } + } + else { + return corMutex.withLock { regenerate().apply { proc = this } }.block() + } + } + catch (e: CancellationException) { + // cancellation can be from 2 causes + // 1. process died, its lifetime terminated, so operation was cancelled + // this clearly indicates child process death -> ConcreteExecutionFailureException + throwConcreteIfDead(e, proc) + // 2. it can be ordinary timeout from coroutine. then just rethrow + throw e + } + catch(e: Throwable) { + // after exception process can either + // 1. be dead because of this exception + throwConcreteIfDead(e, proc) + // 2. might be deliberately thrown and process still can operate + throw ChildProcessError(e) + } + finally { + receiveTimeStamp.set(System.currentTimeMillis()) + } + } /** * Executes [kCallable] in the child process with the supplied [arguments] and [parameters], e.g. static environment. @@ -158,8 +241,6 @@ class ConcreteExecutor> p arguments: Array, parameters: Any? ): TIResult { - restartIfNeeded() - val (clazz, signature) = when (kCallable) { is KFunction<*> -> kCallable.javaMethod?.run { declaringClass to signature } ?: kCallable.javaConstructor?.run { declaringClass to signature } @@ -169,246 +250,45 @@ class ConcreteExecutor> p else -> error("Unknown KCallable: $kCallable") } // actually executableId implements the same logic, but it requires UtContext - val invokeMethodCommand = Protocol.InvokeMethodCommand( - clazz.name, - signature, - arguments.asList(), - parameters - ) - - val cmdId = sendCommand(invokeMethodCommand) - logger.trace("executeAsync, request: $cmdId , $invokeMethodCommand") - try { - val res = awaitCommand, TIResult>(cmdId) { - @Suppress("UNCHECKED_CAST") - it.result as TIResult + return withProcess { + val argumentsByteArray = kryoHelper.writeObject(arguments.asList()) + val parametersByteArray = kryoHelper.writeObject(parameters) + val params = InvokeMethodCommandParams(clazz.name, signature, argumentsByteArray, parametersByteArray) + + val ba = protocolModel.invokeMethodCommand.startSuspending(lifetime, params).result + kryoHelper.readObject(ba) } - logger.trace { "executeAsync, response: $cmdId, $res" } - return res } catch (e: Throwable) { - logger.trace { "executeAsync, response(ERROR): $cmdId, $e" } - throw e - } - } - - /** - * Send command and return its sequential_id - */ - private fun sendCommand(cmd: Protocol.Command): Long { - lastSendTimeMs = System.currentTimeMillis() - - val kryoHelper = state?.kryoHelper ?: error("State is null") - - val res = nextCommandId++ - logger.trace().bracket("Writing $res, $cmd to channel") { - kryoHelper.writeCommand(res, cmd) + logger.trace { "executeAsync, response(ERROR): $e" } + throw e } - return res - } - - fun warmup() { - restartIfNeeded() - sendCommand(Protocol.WarmupCommand()) } - /** - * Restarts the child process if it is not active. - */ - class ConnectorState( - val receiverThread: Thread, - val process: Process, - val kryoHelper: KryoHelper, - val receiveChannel: Channel - ) { - var disposed = false + override fun close() { + forceTerminateProcess() } - data class CommandResult( - val commandId: Long, - val command: Protocol.Command, - val processStdout: InputStream - ) - - var state: ConnectorState? = null - - private fun restartIfNeeded() { - val oldState = state - if (oldState != null && oldState.process.isAlive && !oldState.disposed) return - - logger.debug() - .bracket("restartIfNeeded; instrumentation: '${instrumentation.javaClass.simpleName}',classpath: '$pathsToUserClasses'") { - //stop old thread - oldState?.terminateResources() - - try { - val process = childProcessRunner.start() - val kryoHelper = KryoHelper( - process.inputStream ?: error("Can't get the standard output of the subprocess"), - process.outputStream ?: error("Can't get the standard input of the subprocess") - ) - classLoader?.let { kryoHelper.setKryoClassLoader(it) } - - val readCommandsChannel = Channel(capacity = Channel.UNLIMITED) - val receiverThread = - thread(name = "ConcreteExecutor-${process.pid}-receiver", isDaemon = true, start = false) { - val s = state!! - while (true) { - val cmd = try { - val (commandId, command) = kryoHelper.readLong() to kryoHelper.readCommand() - logger.trace { "receiver: readFromStream: $commandId : $command" } - CommandResult(commandId, command, process.inputStream) - } catch (e: Throwable) { - if (s.disposed) { - break - } - - s.disposed = true - CommandResult( - ERROR_CMD_ID, - Protocol.ExceptionInKryoCommand(e), - process.inputStream - ) - } finally { - lastReceiveTimeMs = System.currentTimeMillis() - } - - try { - // TODO: Java 11 transition -- Sergey will look - readCommandsChannel.trySend(cmd).isSuccess - } catch (e: CancellationException) { - s.disposed = true - - logger.info(e) { "Receiving is canceled in thread ${currentThreadInfo()} while sending command: $cmd" } - break - } catch (e: Throwable) { - s.disposed = true - - logger.error(e) { "Unexpected error while sending to channel in ${currentThreadInfo()}, cmd=$cmd" } - break - } - } + fun forceTerminateProcess() { + runBlocking { + corMutex.withLock { + if (alive) { + try { + processInstance?.run { + protocolModel.stopProcess.start(lifetime, Unit) } - - state = ConnectorState(receiverThread, process, kryoHelper, readCommandsChannel) - receiverThread.start() - - // send classpath - // we don't expect ProcessReadyCommand here - sendCommand( - Protocol.AddPathsCommand( - pathsToUserClasses, - pathsToDependencyClasses - ) - ) - - // send instrumentation - // we don't expect ProcessReadyCommand here - sendCommand(Protocol.SetInstrumentationCommand(instrumentation)) - - } catch (e: Throwable) { - state?.terminateResources() - throw e - } - } - - } - - /** - * Sends [requestCmd] to the ChildProcess. - * If [action] is not null, waits for the response command, performs [action] on it and returns the result. - * This function is helpful for creating extensions for specific instrumentations. - * @see [org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation]. - */ - fun request(requestCmd: T, action: ((Protocol.Command) -> R)): R = runBlocking { - restartIfNeeded() - awaitCommand(sendCommand(requestCmd), action) - } - - /** - * Read next command of type [T] or throw exception - */ - private suspend inline fun awaitCommand( - awaitingCmdId: Long, - action: (T) -> R - ): R { - val s = state ?: error("State is not initialized") - - if (!currentCoroutineContext().isActive) { - logger.warn { "Current coroutine is canceled" } - } - - while (true) { - val (receivedId, cmd, processStdout) = s.receiveChannel.receive() - - if (receivedId == awaitingCmdId || receivedId == ERROR_CMD_ID) { - return when (cmd) { - is T -> action(cmd) - is Protocol.ExceptionInChildProcess -> throw ChildProcessError(cmd.exception) - is Protocol.ExceptionInKryoCommand -> { - // we assume that exception in Kryo means child process death - // and we do not need to check is it alive - throw ConcreteExecutionFailureException( - cmd.exception, - childProcessRunner.errorLogFile, - processStdout.bufferedReader().lines().toList() - ) - } - else -> throw UnexpectedCommand(cmd) + } catch (_: Exception) {} + processInstance = null } - } else if (receivedId > awaitingCmdId) { - logger.error { "BAD: Awaiting id: $awaitingCmdId, received: $receivedId" } - throw UnexpectedCommand(cmd) + ldef.terminate() } } } - // this fun sometimes work improperly - process dies after delay - @Suppress("unused") - private suspend fun checkProcessIsDeadWithTimeout(): Boolean = - if (state?.process?.isAlive == false) { - true - } else { - delay(50) - state?.process?.isAlive == false - } - - private fun ConnectorState.terminateResources() { - if (disposed) - return - - disposed = true - logger.debug { "Terminating resources in ConcreteExecutor.connectorState" } - - if (!process.isAlive) { - return - } - logger.catch { kryoHelper.writeCommand(nextCommandId++, Protocol.StopProcessCommand()) } - logger.catch { kryoHelper.close() } - - logger.catch { receiveChannel.close() } - - logger.catch { process.waitUntilExitWithTimeout() } - } - - override fun close() { - state?.terminateResources() - alive = false - } - - fun forceTerminateProcess() { - state?.process?.destroy() - alive = false - } - } -private fun Process.waitUntilExitWithTimeout() { - try { - if (!waitFor(100, TimeUnit.MICROSECONDS)) { - destroyForcibly() - } - } catch (e: Throwable) { - logger.error(e) { "Error during termination of child process" } +fun ConcreteExecutor<*,*>.warmup() = runBlocking { + withProcess { + protocolModel.warmup.start(lifetime, Unit) } } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt index 73ef995390..1a904a8923 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt @@ -1,6 +1,5 @@ package org.utbot.instrumentation.instrumentation -import org.utbot.instrumentation.util.Protocol import java.lang.instrument.ClassFileTransformer /** @@ -25,16 +24,6 @@ interface Instrumentation : ClassFileTransformer parameters: Any? = null ): TInvocationInstrumentation - /** - * This function will be called from the child process loop every time it receives [Protocol.InstrumentationCommand] from the main process. - * - * @return Handles [cmd] and returns command which should be sent back to the [org.utbot.instrumentation.ConcreteExecutor]. - * If returns `null`, nothing will be sent. - */ - fun handle(cmd: T): Protocol.Command? { - return null - } - /** * Will be called in the very beginning in the child process. */ diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt index afb04d9f05..cc1977296f 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt @@ -1,5 +1,6 @@ package org.utbot.instrumentation.instrumentation.coverage +import kotlinx.coroutines.runBlocking import org.utbot.common.withAccessibility import org.utbot.instrumentation.ConcreteExecutor import org.utbot.instrumentation.Settings @@ -7,12 +8,10 @@ import org.utbot.instrumentation.instrumentation.ArgumentList import org.utbot.instrumentation.instrumentation.Instrumentation import org.utbot.instrumentation.instrumentation.InvokeWithStaticsInstrumentation import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter +import org.utbot.instrumentation.rd.generated.CollectCoverageParams import org.utbot.instrumentation.util.CastProbesArrayException import org.utbot.instrumentation.util.ChildProcessError -import org.utbot.instrumentation.util.InstrumentationException import org.utbot.instrumentation.util.NoProbesArrayException -import org.utbot.instrumentation.util.Protocol -import org.utbot.instrumentation.util.UnexpectedCommand import java.security.ProtectionDomain data class CoverageInfo( @@ -52,7 +51,7 @@ object CoverageInstrumentation : Instrumentation> { /** * Collects coverage from the given [clazz] via reflection. */ - private fun collectCoverageInfo(clazz: Class): CoverageInfo { + fun collectCoverageInfo(clazz: Class): CoverageInfo { val probesFieldName: String = Settings.PROBES_ARRAY_NAME val visitedLinesField = clazz.fields.firstOrNull { it.name == probesFieldName } ?: throw NoProbesArrayException(clazz, Settings.PROBES_ARRAY_NAME) @@ -93,43 +92,15 @@ object CoverageInstrumentation : Instrumentation> { return instrumenter.classByteCode } - - /** - * Collects coverage for the class wrapped in [cmd] if [cmd] is [CollectCoverageCommand]. - * - * @return [CoverageInfoCommand] with wrapped [CoverageInfo] if [cmd] is [CollectCoverageCommand] and `null` otherwise. - */ - override fun handle(cmd: T): Protocol.Command? = when (cmd) { - is CollectCoverageCommand<*> -> try { - CoverageInfoCommand(collectCoverageInfo(cmd.clazz)) - } catch (e: InstrumentationException) { - Protocol.ExceptionInChildProcess(e) - } - else -> null - } } -/** - * This command is sent to the child process from the [ConcreteExecutor] if user wants to collect coverage for the - * [clazz]. - */ -data class CollectCoverageCommand(val clazz: Class) : Protocol.InstrumentationCommand() - -/** - * This command is sent back to the [ConcreteExecutor] with the [coverageInfo]. - */ -data class CoverageInfoCommand(val coverageInfo: CoverageInfo) : Protocol.InstrumentationCommand() - /** * Extension function for the [ConcreteExecutor], which allows to collect the coverage of the given [clazz]. */ -fun ConcreteExecutor, CoverageInstrumentation>.collectCoverage(clazz: Class<*>): CoverageInfo { - val collectCoverageCommand = CollectCoverageCommand(clazz) - return this.request(collectCoverageCommand) { - when (it) { - is CoverageInfoCommand -> it.coverageInfo - is Protocol.ExceptionInChildProcess -> throw ChildProcessError(it.exception) - else -> throw UnexpectedCommand(it) - } +fun ConcreteExecutor, CoverageInstrumentation>.collectCoverage(clazz: Class<*>): CoverageInfo = runBlocking { + withProcess { + val clazzByteArray = kryoHelper.writeObject(clazz) + + kryoHelper.readObject(protocolModel.collectCoverage.startSuspending(lifetime, CollectCoverageParams(clazzByteArray)).coverageInfo) } } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcess.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcess.kt index d3a585ff00..918b533d6a 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcess.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcess.kt @@ -1,12 +1,33 @@ package org.utbot.instrumentation.process +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.impl.RdCall +import com.jetbrains.rd.framework.util.launchChild +import com.jetbrains.rd.util.ILoggerFactory +import com.jetbrains.rd.util.LogLevel +import com.jetbrains.rd.util.Logger +import com.jetbrains.rd.util.defaultLogFormat +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import com.jetbrains.rd.util.lifetime.plusAssign +import com.jetbrains.rd.util.threading.SingleThreadScheduler +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import org.utbot.common.scanForClasses import org.utbot.framework.plugin.api.util.UtContext import org.utbot.instrumentation.agent.Agent import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.rd.childCreatedFileName +import org.utbot.instrumentation.rd.generated.CollectCoverageResult +import org.utbot.instrumentation.rd.generated.InvokeMethodCommandResult +import org.utbot.instrumentation.rd.generated.ProtocolModel +import org.utbot.instrumentation.rd.obtainClientIO +import org.utbot.instrumentation.rd.processSyncDirectory +import org.utbot.instrumentation.rd.signalChildReady import org.utbot.instrumentation.util.KryoHelper -import org.utbot.instrumentation.util.Protocol -import org.utbot.instrumentation.util.UnexpectedCommand +import org.utbot.rd.UtSingleThreadScheduler +import org.utbot.rd.adviseForConditionAsync import java.io.File import java.io.OutputStream import java.io.PrintStream @@ -14,14 +35,15 @@ import java.net.URLClassLoader import java.security.AllPermission import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import kotlin.system.exitProcess +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.TimeUnit import kotlin.system.measureTimeMillis /** * We use this ClassLoader to separate user's classes and our dependency classes. * Our classes won't be instrumented. */ -internal object HandlerClassesLoader : URLClassLoader(emptyArray()) { +private object HandlerClassesLoader : URLClassLoader(emptyArray()) { fun addUrls(urls: Iterable) { urls.forEach { super.addURL(File(it).toURI().toURL()) } } @@ -40,13 +62,38 @@ internal object HandlerClassesLoader : URLClassLoader(emptyArray()) { } } +private typealias ChildProcessLogLevel = LogLevel + +private val logLevel = ChildProcessLogLevel.Trace + // Logging private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS") -private fun log(any: Any?) { - System.err.println(LocalDateTime.now().format(dateFormatter) + " | $any") +private fun log(level: ChildProcessLogLevel, any: () -> Any?) { + if (level < logLevel) + return + + System.err.println(LocalDateTime.now().format(dateFormatter) + " | ${any()}") +} + +private fun logError(any: () -> Any?) { + log(ChildProcessLogLevel.Error, any) +} + +private fun logInfo(any: () -> Any?) { + log(ChildProcessLogLevel.Info, any) +} + +private fun logTrace(any: () -> Any?) { + log(ChildProcessLogLevel.Trace, any) } -private val kryoHelper: KryoHelper = KryoHelper(System.`in`, System.`out`) +private enum class State { + STARTED, + ENDED +} + +private val messageFromMainTimeoutMillis: Long = TimeUnit.SECONDS.toMillis(120) +private val synchronizer: Channel = Channel(1) /** * Command-line option to disable the sandbox @@ -56,7 +103,7 @@ const val DISABLE_SANDBOX_OPTION = "--disable-sandbox" /** * It should be compiled into separate jar file (child_process.jar) and be run with an agent (agent.jar) option. */ -fun main(args: Array) { +suspend fun main(args: Array) = runBlocking { if (!args.contains(DISABLE_SANDBOX_OPTION)) { permissions { // Enable all permissions for instrumentation. @@ -64,141 +111,188 @@ fun main(args: Array) { +AllPermission() } } + // 0 - auto port for server, should not be used here + val port = args.find { it.startsWith(serverPortProcessArgumentTag) } + ?.run { split("=").last().toInt().coerceIn(1..65535) } + ?: throw IllegalArgumentException("No port provided") - // We don't want user code to litter the standard output, so we redirect it. - val tmpStream = PrintStream(object : OutputStream() { - override fun write(b: Int) {} - }) - System.setOut(tmpStream) + val pid = ProcessHandle.current().pid().toInt() + val def = LifetimeDefinition() - val classPaths = readClasspath() - val pathsToUserClasses = classPaths.pathsToUserClasses.split(File.pathSeparatorChar).toSet() - val pathsToDependencyClasses = classPaths.pathsToDependencyClasses.split(File.pathSeparatorChar).toSet() - HandlerClassesLoader.addUrls(pathsToUserClasses) - HandlerClassesLoader.addUrls(pathsToDependencyClasses) - kryoHelper.setKryoClassLoader(HandlerClassesLoader) // Now kryo will use our classloader when it encounters unregistered class. - - log("User classes:" + pathsToUserClasses.joinToString()) - - kryoHelper.use { - UtContext.setUtContext(UtContext(HandlerClassesLoader)).use { - getInstrumentation()?.let { instrumentation -> - Agent.dynamicClassTransformer.transformer = instrumentation // classTransformer is set - Agent.dynamicClassTransformer.addUserPaths(pathsToUserClasses) - instrumentation.init(pathsToUserClasses) - - try { - loop(instrumentation) - } catch (e: Throwable) { - log("Terminating process because exception occured: ${e.stackTraceToString()}") - exitProcess(1) + SingleThreadScheduler(Lifetime.Eternal, "") + + launchChild(Lifetime.Eternal) { + var lastState = State.STARTED + while (true) { + val current: State? = + withTimeoutOrNull(messageFromMainTimeoutMillis) { + synchronizer.receive() + } + if (current == null) { + if (lastState == State.ENDED) { + // process is waiting for command more than expected, better die + logInfo { "terminating lifetime" } + def.terminate() + break } } + else { + lastState = current + } } } -} -private fun send(cmdId: Long, cmd: Protocol.Command) { - try { - kryoHelper.writeCommand(cmdId, cmd) - log("Send << $cmdId") - } catch (e: Exception) { - log("Failed to serialize << $cmdId with exception: ${e.stackTraceToString()}") - log("Writing it to kryo...") - kryoHelper.writeCommand(cmdId, Protocol.ExceptionInChildProcess(e)) - log("Successfuly wrote.") + def.usingNested { lifetime -> + lifetime += { logInfo { "lifetime terminated" } } + try { + logInfo {"pid - $pid"} + initiate(lifetime, port, pid) + } finally { + val syncFile = File(processSyncDirectory, childCreatedFileName(pid)) + + if (syncFile.exists()) { + logInfo { "sync file existed" } + syncFile.delete() + } + } } } -private fun read(cmdId: Long): Protocol.Command { +private fun measureExecutionForTermination(block: () -> T): T = runBlocking { try { - val cmd = kryoHelper.readCommand() - log("Received :> $cmdId") - return cmd - } catch (e: Exception) { - log("Failed to read :> $cmdId with exception: ${e.stackTraceToString()}") - throw e + synchronizer.send(State.STARTED) + return@runBlocking block() + } + finally { + synchronizer.send(State.ENDED) } } -/** - * Main loop. Processes incoming commands. - */ -private fun loop(instrumentation: Instrumentation<*>) { - while (true) { - val cmdId = kryoHelper.readLong() - val cmd = try { - read(cmdId) - } catch (e: Exception) { - send(cmdId, Protocol.ExceptionInChildProcess(e)) - continue - } +private lateinit var pathsToUserClasses: Set +private lateinit var pathsToDependencyClasses: Set +private lateinit var instrumentation: Instrumentation<*> - when (cmd) { - is Protocol.WarmupCommand -> { - val time = measureTimeMillis { - HandlerClassesLoader.scanForClasses("").toList() // here we transform classes - } - System.err.println("warmup finished in $time ms") - } - is Protocol.InvokeMethodCommand -> { - val resultCmd = try { - val clazz = HandlerClassesLoader.loadClass(cmd.className) - val res = instrumentation.invoke( - clazz, - cmd.signature, - cmd.arguments, - cmd.parameters - ) - Protocol.InvocationResultCommand(res) - } catch (e: Throwable) { - System.err.println(e.stackTraceToString()) - Protocol.ExceptionInChildProcess(e) - } - send(cmdId, resultCmd) - } - is Protocol.StopProcessCommand -> { - break - } - is Protocol.InstrumentationCommand -> { - val result = instrumentation.handle(cmd) - result?.let { - send(cmdId, it) - } - } - else -> { - send(cmdId, Protocol.ExceptionInChildProcess(UnexpectedCommand(cmd))) +private fun RdCall.measureExecutionForTermination(block: (T) -> R) { + this.set { it -> + runBlocking { + measureExecutionForTermination { + block(it) } } } } -/** - * Retrieves the actual instrumentation. It is passed from the main process during - * [org.utbot.instrumentation.ConcreteExecutor] instantiation. - */ -private fun getInstrumentation(): Instrumentation<*>? { - val cmdId = kryoHelper.readLong() - return when (val cmd = kryoHelper.readCommand()) { - is Protocol.SetInstrumentationCommand<*> -> { - cmd.instrumentation - } - is Protocol.StopProcessCommand -> null - else -> { - send(cmdId, Protocol.ExceptionInChildProcess(UnexpectedCommand(cmd))) - null +private suspend fun ProtocolModel.setup(kryoHelper: KryoHelper, onStop: () -> Unit) { + warmup.measureExecutionForTermination { + val time = measureTimeMillis { + HandlerClassesLoader.scanForClasses("").toList() // here we transform classes } + logInfo { "warmup finished in $time ms" } + } + invokeMethodCommand.measureExecutionForTermination { params -> + val clazz = HandlerClassesLoader.loadClass(params.classname) + val res = instrumentation.invoke( + clazz, + params.signature, + kryoHelper.readObject(params.arguments), + kryoHelper.readObject(params.parameters) + ) + + logInfo { "sent cmd: $res" } + InvokeMethodCommandResult(kryoHelper.writeObject(res)) + } + setInstrumentation.measureExecutionForTermination { params -> + instrumentation = kryoHelper.readObject(params.instrumentation) + Agent.dynamicClassTransformer.transformer = instrumentation // classTransformer is set + Agent.dynamicClassTransformer.addUserPaths(pathsToUserClasses) + instrumentation.init(pathsToUserClasses) + } + addPaths.measureExecutionForTermination { params -> + pathsToUserClasses = params.pathsToUserClasses.split(File.pathSeparatorChar).toSet() + pathsToDependencyClasses = params.pathsToDependencyClasses.split(File.pathSeparatorChar).toSet() + HandlerClassesLoader.addUrls(pathsToUserClasses) + HandlerClassesLoader.addUrls(pathsToDependencyClasses) + kryoHelper.setKryoClassLoader(HandlerClassesLoader) // Now kryo will use our classloader when it encounters unregistered class. + + logTrace { "User classes:" + pathsToUserClasses.joinToString() } + + UtContext.setUtContext(UtContext(HandlerClassesLoader)) + } + stopProcess.measureExecutionForTermination { + onStop() + } + collectCoverage.measureExecutionForTermination { params -> + val anyClass: Class<*> = kryoHelper.readObject(params.clazz) + val result = (instrumentation as CoverageInstrumentation).collectCoverageInfo(anyClass) + CollectCoverageResult(kryoHelper.writeObject(result)) } } -private fun readClasspath(): Protocol.AddPathsCommand { - val cmdId = kryoHelper.readLong() - return kryoHelper.readCommand().let { cmd -> - if (cmd is Protocol.AddPathsCommand) { - cmd +private suspend fun initiate(lifetime: Lifetime, port: Int, pid: Int) { + // We don't want user code to litter the standard output, so we redirect it. + val tmpStream = PrintStream(object : OutputStream() { + override fun write(b: Int) {} + }) + System.setOut(tmpStream) + + Logger.set(lifetime, object : ILoggerFactory { + override fun getLogger(category: String) = object : Logger { + override fun isEnabled(level: LogLevel): Boolean { + return level >= logLevel + } + + override fun log(level: LogLevel, message: Any?, throwable: Throwable?) { + val msg = defaultLogFormat(category, level, message, throwable) + + log(logLevel) { msg } + } + + } + }) + + val deferred = CompletableDeferred() + lifetime.onTermination { deferred.complete(Unit) } + val kryoHelper = KryoHelper(lifetime) + logInfo { "kryo created" } + + val scheduler = UtSingleThreadScheduler { logInfo(it) } + val clientProtocol = Protocol( + "ChildProcess", + Serializers(), + Identities(IdKind.Client), + scheduler, + SocketWire.Client(lifetime, scheduler, port), + lifetime + ) + logInfo { + "heartbeatAlive - ${clientProtocol.wire.heartbeatAlive.value}, connected - ${ + clientProtocol.wire.connected.value + }" + } + val (sync, protocolModel) = obtainClientIO(lifetime, clientProtocol) + + protocolModel.setup(kryoHelper) { + deferred.complete(Unit) + } + signalChildReady(pid) + logInfo { "IO obtained" } + + val answerFromMainProcess = sync.adviseForConditionAsync(lifetime) { + if (it == "main") { + measureExecutionForTermination { + sync.fire("child") + } + true } else { - send(cmdId, Protocol.ExceptionInChildProcess(UnexpectedCommand(cmd))) - error("No classpath!") + false } } + + try { + answerFromMainProcess.await() + logInfo { "starting instrumenting" } + deferred.await() + } catch (e: Throwable) { + logError { "Terminating process because exception occurred: ${e.stackTraceToString()}" } + } } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcessRunner.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcessRunner.kt index d57ca98033..82ba3ad898 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcessRunner.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcessRunner.kt @@ -1,11 +1,7 @@ package org.utbot.instrumentation.process import mu.KotlinLogging -import org.utbot.common.bracket -import org.utbot.common.debug -import org.utbot.common.firstOrNullResourceIS -import org.utbot.common.getCurrentProcessId -import org.utbot.common.pid +import org.utbot.common.* import org.utbot.common.scanForResourcesContaining import org.utbot.common.utBotTempDirectory import org.utbot.framework.plugin.services.JdkInfoService @@ -14,11 +10,14 @@ import org.utbot.framework.plugin.services.WorkingDirService import org.utbot.instrumentation.Settings import org.utbot.instrumentation.agent.DynamicClassTransformer import java.io.File +import kotlin.random.Random private val logger = KotlinLogging.logger {} -private var processSeqN = 0 +const val serverPortProcessArgumentTag = "serverPort" class ChildProcessRunner { + private val id = Random.nextLong() + private var processSeqN = 0 private val cmds: List by lazy { val debugCmd = listOfNotNull(DEBUG_RUN_CMD.takeIf { Settings.runChildProcessWithDebug} ) @@ -38,22 +37,24 @@ class ChildProcessRunner { var errorLogFile: File = NULL_FILE - fun start(): Process { - logger.debug { "Starting child process: ${cmds.joinToString(" ")}" } + fun start(port: Int): Process { + val portArgument = "$serverPortProcessArgumentTag=$port" + + logger.debug { "Starting child process: ${cmds.joinToString(" ")} $portArgument" } processSeqN++ if (UtSettings.logConcreteExecutionErrors) { UT_BOT_TEMP_DIR.mkdirs() - errorLogFile = File(UT_BOT_TEMP_DIR, "${hashCode()}-${processSeqN}.log") + errorLogFile = File(UT_BOT_TEMP_DIR, "${id}-${processSeqN}.log") } val directory = WorkingDirService.provide().toFile() - val commandsWithOptions = buildList { addAll(cmds) if (UtSettings.disableSandbox) { add(DISABLE_SANDBOX_OPTION) } + add(portArgument) } val processBuilder = ProcessBuilder(commandsWithOptions) @@ -61,7 +62,7 @@ class ChildProcessRunner { .directory(directory) return processBuilder.start().also { - logger.debug { "Process started with PID=${it.pid}" } + logger.debug { "Process started with PID=${it.pid()}" } if (UtSettings.logConcreteExecutionErrors) { logger.debug { "Child process error log: ${errorLogFile.absolutePath}" } @@ -98,7 +99,7 @@ class ChildProcessRunner { run { logger.debug("Trying to find jar in the resources.") val tempDir = utBotTempDirectory.toFile() - val unzippedJarName = "$UTBOT_INSTRUMENTATION-${getCurrentProcessId()}.jar" + val unzippedJarName = "$UTBOT_INSTRUMENTATION-${ProcessHandle.current().pid()}.jar" val instrumentationJarFile = File(tempDir, unzippedJarName) ChildProcessRunner::class.java.classLoader diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentationIO.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentationIO.kt new file mode 100644 index 0000000000..b9ba0ddd26 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentationIO.kt @@ -0,0 +1,44 @@ +package org.utbot.instrumentation.rd + +import com.jetbrains.rd.framework.Protocol +import com.jetbrains.rd.framework.base.static +import com.jetbrains.rd.framework.impl.RdSignal +import com.jetbrains.rd.util.lifetime.Lifetime +import org.utbot.common.utBotTempDirectory +import org.utbot.instrumentation.rd.generated.ProtocolModel +import org.utbot.instrumentation.rd.generated.protocolModel +import org.utbot.rd.pump +import java.io.File + +const val rdProcessDirName = "rdProcessSync" +val processSyncDirectory = File(utBotTempDirectory.toFile(), rdProcessDirName) + +internal suspend fun obtainClientIO(lifetime: Lifetime, protocol: Protocol): Pair, ProtocolModel> { + return protocol.scheduler.pump(lifetime) { + val sync = RdSignal().static(1).apply { + async = true + bind(lifetime, protocol, rdid.toString()) + } + sync to protocol.protocolModel + } +} + +internal fun childCreatedFileName(pid: Int): String { + return "$pid.created" +} + +internal fun signalChildReady(pid: Int) { + processSyncDirectory.mkdirs() + + val signalFile = File(processSyncDirectory, childCreatedFileName(pid)) + + if (signalFile.exists()) { + signalFile.delete() + } + + val created = signalFile.createNewFile() + + if (!created) { + throw IllegalStateException("cannot create signal file") + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/UtInstrumentationProcess.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/UtInstrumentationProcess.kt new file mode 100644 index 0000000000..cb608bfe13 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/UtInstrumentationProcess.kt @@ -0,0 +1,146 @@ +package org.utbot.instrumentation.rd + +import com.jetbrains.rd.framework.base.static +import com.jetbrains.rd.framework.impl.RdSignal +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.isAlive +import kotlinx.coroutines.delay +import mu.KotlinLogging +import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.process.ChildProcessRunner +import org.utbot.instrumentation.rd.generated.AddPathsParams +import org.utbot.instrumentation.rd.generated.ProtocolModel +import org.utbot.instrumentation.rd.generated.SetInstrumentationParams +import org.utbot.instrumentation.rd.generated.protocolModel +import org.utbot.instrumentation.util.KryoHelper +import org.utbot.rd.* +import java.io.File +import java.nio.file.Files +import java.util.concurrent.atomic.AtomicBoolean + +private val logger = KotlinLogging.logger {} +private const val fileWaitTimeoutMillis = 10L + +/** + * Main goals of this class: + * 1. prepare started child process for execution - initializing rd, sending paths and instrumentation + * 2. expose bound model + */ +class UtInstrumentationProcess private constructor( + private val classLoader: ClassLoader?, + private val rdProcess: ProcessWithRdServer +) : ProcessWithRdServer by rdProcess { + private val sync = RdSignal().static(1).apply { async = true } + val kryoHelper = KryoHelper(lifetime.createNested()).apply { + classLoader?.let { setKryoClassLoader(it) } + } + val protocolModel: ProtocolModel + get() = protocol.protocolModel + + private suspend fun init(): UtInstrumentationProcess { + protocol.scheduler.pump(lifetime) { + sync.bind(lifetime, protocol, sync.rdid.toString()) + protocol.protocolModel + } + processSyncDirectory.mkdirs() + + // there 2 stages at rd protocol initialization: + // 1. we need to bind all entities - for ex. generated model and custom signal + // because we cannot operate with unbound + // 2. we need to wait when all that entities bound on the other side + // because when we fire something that is not bound on another side - we will lose this call + // to guarantee 2nd stage success - there is custom simple synchronization: + // 1. child process will create file "${processId}.created" - this indicates that child process is ready to receive messages + // 2. we will test the connection via sync RdSignal + // only then we can successfully start operating + val pid = process.toHandle().pid().toInt() + val syncFile = File(processSyncDirectory, childCreatedFileName(pid)) + + while (lifetime.isAlive) { + if (Files.deleteIfExists(syncFile.toPath())) { + logger.trace { "process $pid: signal file deleted connecting" } + break + } + + delay(fileWaitTimeoutMillis) + } + + val messageFromChild = sync.adviseForConditionAsync(lifetime) { it == "child" } + + while(messageFromChild.isActive) { + sync.fire("main") + delay(10) + } + + lifetime.onTermination { + if (syncFile.exists()) { + logger.trace { "process $pid: on terminating syncFile existed" } + syncFile.delete() + } + } + + return this + } + + companion object { + private suspend fun > invokeImpl( + lifetime: Lifetime, + childProcessRunner: ChildProcessRunner, + instrumentation: TInstrumentation, + pathsToUserClasses: String, + pathsToDependencyClasses: String, + classLoader: ClassLoader? + ): UtInstrumentationProcess { + val rdProcess: ProcessWithRdServer = startUtProcessWithRdServer( + lifetime = lifetime + ) { + childProcessRunner.start(it) + } + logger.trace("rd process started") + val proc = UtInstrumentationProcess( + classLoader, + rdProcess + ).init() + + proc.lifetime.onTermination { + logger.trace { "process is terminating" } + } + + logger.trace("sending add paths") + proc.protocolModel.addPaths.startSuspending( + proc.lifetime, AddPathsParams( + pathsToUserClasses, + pathsToDependencyClasses + ) + ) + + logger.trace("sending instrumentation") + proc.protocolModel.setInstrumentation.startSuspending( + proc.lifetime, SetInstrumentationParams( + proc.kryoHelper.writeObject(instrumentation) + ) + ) + logger.trace("start commands sent") + + return proc + } + + suspend operator fun > invoke( + lifetime: Lifetime, + childProcessRunner: ChildProcessRunner, + instrumentation: TInstrumentation, + pathsToUserClasses: String, + pathsToDependencyClasses: String, + classLoader: ClassLoader? + ): UtInstrumentationProcess = lifetime.createNested().terminateOnException { + invokeImpl( + it, + childProcessRunner, + instrumentation, + pathsToUserClasses, + pathsToDependencyClasses, + classLoader + ) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/UtRdLogger.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/UtRdLogger.kt new file mode 100644 index 0000000000..0b658f7376 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/UtRdLogger.kt @@ -0,0 +1,41 @@ +package org.utbot.instrumentation.rd + +import com.jetbrains.rd.util.ILoggerFactory +import com.jetbrains.rd.util.LogLevel +import com.jetbrains.rd.util.Logger +import com.jetbrains.rd.util.defaultLogFormat +import mu.KotlinLogging + +private val logger = KotlinLogging.logger {} + +object UtRdLoggerFactory : ILoggerFactory { + override fun getLogger(category: String): Logger { + logger.trace { "getting logger for category: $category" } + return UtRdLogger(category) + } +} + +class UtRdLogger(private val category: String) : Logger { + override fun isEnabled(level: LogLevel): Boolean { + return when (level) { + LogLevel.Trace -> logger.isTraceEnabled + LogLevel.Debug -> logger.isDebugEnabled + LogLevel.Info -> logger.isInfoEnabled + LogLevel.Warn -> logger.isWarnEnabled + LogLevel.Error -> logger.isErrorEnabled + LogLevel.Fatal -> logger.isErrorEnabled + } + } + + override fun log(level: LogLevel, message: Any?, throwable: Throwable?) { + val msg = defaultLogFormat(category, level, message, throwable) + when (level) { + LogLevel.Trace -> logger.trace(msg) + LogLevel.Debug -> logger.debug(msg) + LogLevel.Info -> logger.info(msg) + LogLevel.Warn -> logger.warn(msg) + LogLevel.Error -> logger.error(msg) + LogLevel.Fatal -> logger.error(msg) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ProtocolModel.Generated.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ProtocolModel.Generated.kt new file mode 100644 index 0000000000..7a68147b71 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ProtocolModel.Generated.kt @@ -0,0 +1,528 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.instrumentation.rd.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [ProtocolRoot.kt:7] + */ +class ProtocolModel private constructor( + private val _addPaths: RdCall, + private val _warmup: RdCall, + private val _setInstrumentation: RdCall, + private val _invokeMethodCommand: RdCall, + private val _stopProcess: RdCall, + private val _collectCoverage: RdCall +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + serializers.register(AddPathsParams) + serializers.register(SetInstrumentationParams) + serializers.register(InvokeMethodCommandParams) + serializers.register(InvokeMethodCommandResult) + serializers.register(CollectCoverageParams) + serializers.register(CollectCoverageResult) + } + + + @JvmStatic + @JvmName("internalCreateModel") + @Deprecated("Use create instead", ReplaceWith("create(lifetime, protocol)")) + internal fun createModel(lifetime: Lifetime, protocol: IProtocol): ProtocolModel { + @Suppress("DEPRECATION") + return create(lifetime, protocol) + } + + @JvmStatic + @Deprecated("Use protocol.protocolModel or revise the extension scope instead", ReplaceWith("protocol.protocolModel")) + fun create(lifetime: Lifetime, protocol: IProtocol): ProtocolModel { + ProtocolRoot.register(protocol.serializers) + + return ProtocolModel().apply { + identify(protocol.identity, RdId.Null.mix("ProtocolModel")) + bind(lifetime, protocol, "ProtocolModel") + } + } + + + const val serializationHash = -983308496809975144L + + } + override val serializersOwner: ISerializersOwner get() = ProtocolModel + override val serializationHash: Long get() = ProtocolModel.serializationHash + + //fields + + /** + * The main process tells where the child process should search for the classes + */ + val addPaths: RdCall get() = _addPaths + + /** + * Load classes from classpath and instrument them + */ + val warmup: RdCall get() = _warmup + + /** + * The main process sends [instrumentation] to the child process + */ + val setInstrumentation: RdCall get() = _setInstrumentation + + /** + * The main process requests the child process to execute a method with the given [signature], + which declaring class's name is [className]. + @property parameters are the parameters needed for an execution, e.g. static environment + */ + val invokeMethodCommand: RdCall get() = _invokeMethodCommand + + /** + * This command tells the child process to stop + */ + val stopProcess: RdCall get() = _stopProcess + + /** + * This command is sent to the child process from the [ConcreteExecutor] if user wants to collect coverage for the + [clazz] + */ + val collectCoverage: RdCall get() = _collectCoverage + //methods + //initializer + init { + _addPaths.async = true + _warmup.async = true + _setInstrumentation.async = true + _invokeMethodCommand.async = true + _stopProcess.async = true + _collectCoverage.async = true + } + + init { + bindableChildren.add("addPaths" to _addPaths) + bindableChildren.add("warmup" to _warmup) + bindableChildren.add("setInstrumentation" to _setInstrumentation) + bindableChildren.add("invokeMethodCommand" to _invokeMethodCommand) + bindableChildren.add("stopProcess" to _stopProcess) + bindableChildren.add("collectCoverage" to _collectCoverage) + } + + //secondary constructor + private constructor( + ) : this( + RdCall(AddPathsParams, FrameworkMarshallers.Void), + RdCall(FrameworkMarshallers.Void, FrameworkMarshallers.Void), + RdCall(SetInstrumentationParams, FrameworkMarshallers.Void), + RdCall(InvokeMethodCommandParams, InvokeMethodCommandResult), + RdCall(FrameworkMarshallers.Void, FrameworkMarshallers.Void), + RdCall(CollectCoverageParams, CollectCoverageResult) + ) + + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("ProtocolModel (") + printer.indent { + print("addPaths = "); _addPaths.print(printer); println() + print("warmup = "); _warmup.print(printer); println() + print("setInstrumentation = "); _setInstrumentation.print(printer); println() + print("invokeMethodCommand = "); _invokeMethodCommand.print(printer); println() + print("stopProcess = "); _stopProcess.print(printer); println() + print("collectCoverage = "); _collectCoverage.print(printer); println() + } + printer.print(")") + } + //deepClone + override fun deepClone(): ProtocolModel { + return ProtocolModel( + _addPaths.deepClonePolymorphic(), + _warmup.deepClonePolymorphic(), + _setInstrumentation.deepClonePolymorphic(), + _invokeMethodCommand.deepClonePolymorphic(), + _stopProcess.deepClonePolymorphic(), + _collectCoverage.deepClonePolymorphic() + ) + } + //contexts +} +val IProtocol.protocolModel get() = getOrCreateExtension(ProtocolModel::class) { @Suppress("DEPRECATION") ProtocolModel.create(lifetime, this) } + + + +/** + * #### Generated from [ProtocolRoot.kt:8] + */ +data class AddPathsParams ( + val pathsToUserClasses: String, + val pathsToDependencyClasses: String +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = AddPathsParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): AddPathsParams { + val pathsToUserClasses = buffer.readString() + val pathsToDependencyClasses = buffer.readString() + return AddPathsParams(pathsToUserClasses, pathsToDependencyClasses) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: AddPathsParams) { + buffer.writeString(value.pathsToUserClasses) + buffer.writeString(value.pathsToDependencyClasses) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as AddPathsParams + + if (pathsToUserClasses != other.pathsToUserClasses) return false + if (pathsToDependencyClasses != other.pathsToDependencyClasses) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + pathsToUserClasses.hashCode() + __r = __r*31 + pathsToDependencyClasses.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("AddPathsParams (") + printer.indent { + print("pathsToUserClasses = "); pathsToUserClasses.print(printer); println() + print("pathsToDependencyClasses = "); pathsToDependencyClasses.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [ProtocolRoot.kt:28] + */ +data class CollectCoverageParams ( + val clazz: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = CollectCoverageParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): CollectCoverageParams { + val clazz = buffer.readByteArray() + return CollectCoverageParams(clazz) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: CollectCoverageParams) { + buffer.writeByteArray(value.clazz) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as CollectCoverageParams + + if (!(clazz contentEquals other.clazz)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + clazz.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("CollectCoverageParams (") + printer.indent { + print("clazz = "); clazz.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [ProtocolRoot.kt:32] + */ +data class CollectCoverageResult ( + val coverageInfo: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = CollectCoverageResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): CollectCoverageResult { + val coverageInfo = buffer.readByteArray() + return CollectCoverageResult(coverageInfo) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: CollectCoverageResult) { + buffer.writeByteArray(value.coverageInfo) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as CollectCoverageResult + + if (!(coverageInfo contentEquals other.coverageInfo)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + coverageInfo.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("CollectCoverageResult (") + printer.indent { + print("coverageInfo = "); coverageInfo.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [ProtocolRoot.kt:17] + */ +data class InvokeMethodCommandParams ( + val classname: String, + val signature: String, + val arguments: ByteArray, + val parameters: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = InvokeMethodCommandParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): InvokeMethodCommandParams { + val classname = buffer.readString() + val signature = buffer.readString() + val arguments = buffer.readByteArray() + val parameters = buffer.readByteArray() + return InvokeMethodCommandParams(classname, signature, arguments, parameters) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: InvokeMethodCommandParams) { + buffer.writeString(value.classname) + buffer.writeString(value.signature) + buffer.writeByteArray(value.arguments) + buffer.writeByteArray(value.parameters) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as InvokeMethodCommandParams + + if (classname != other.classname) return false + if (signature != other.signature) return false + if (!(arguments contentEquals other.arguments)) return false + if (!(parameters contentEquals other.parameters)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + classname.hashCode() + __r = __r*31 + signature.hashCode() + __r = __r*31 + arguments.contentHashCode() + __r = __r*31 + parameters.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("InvokeMethodCommandParams (") + printer.indent { + print("classname = "); classname.print(printer); println() + print("signature = "); signature.print(printer); println() + print("arguments = "); arguments.print(printer); println() + print("parameters = "); parameters.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [ProtocolRoot.kt:24] + */ +data class InvokeMethodCommandResult ( + val result: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = InvokeMethodCommandResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): InvokeMethodCommandResult { + val result = buffer.readByteArray() + return InvokeMethodCommandResult(result) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: InvokeMethodCommandResult) { + buffer.writeByteArray(value.result) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as InvokeMethodCommandResult + + if (!(result contentEquals other.result)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + result.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("InvokeMethodCommandResult (") + printer.indent { + print("result = "); result.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [ProtocolRoot.kt:13] + */ +data class SetInstrumentationParams ( + val instrumentation: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = SetInstrumentationParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): SetInstrumentationParams { + val instrumentation = buffer.readByteArray() + return SetInstrumentationParams(instrumentation) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: SetInstrumentationParams) { + buffer.writeByteArray(value.instrumentation) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as SetInstrumentationParams + + if (!(instrumentation contentEquals other.instrumentation)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + instrumentation.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("SetInstrumentationParams (") + printer.indent { + print("instrumentation = "); instrumentation.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ProtocolRoot.Generated.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ProtocolRoot.Generated.kt new file mode 100644 index 0000000000..7551ac8c91 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ProtocolRoot.Generated.kt @@ -0,0 +1,58 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.instrumentation.rd.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [ProtocolRoot.kt:5] + */ +class ProtocolRoot private constructor( +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + ProtocolRoot.register(serializers) + ProtocolModel.register(serializers) + } + + + + + + const val serializationHash = -479905474426893924L + + } + override val serializersOwner: ISerializersOwner get() = ProtocolRoot + override val serializationHash: Long get() = ProtocolRoot.serializationHash + + //fields + //methods + //initializer + //secondary constructor + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("ProtocolRoot (") + printer.print(")") + } + //deepClone + override fun deepClone(): ProtocolRoot { + return ProtocolRoot( + ) + } + //contexts +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/InstrumentationException.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/InstrumentationException.kt index 259155fbdf..8935985223 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/InstrumentationException.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/InstrumentationException.kt @@ -24,8 +24,11 @@ class ReadingFromKryoException(e: Throwable) : class WritingToKryoException(e: Throwable) : InstrumentationException("Writing to Kryo exception |> ${e.stackTraceToString()}", e) +/** + * this exception is thrown only in main process. + * currently it means that {e: Throwable} happened in child process, + * but child process still can operate and not dead. + * on child process death - ConcreteExecutionFailureException is thrown +*/ class ChildProcessError(e: Throwable) : - InstrumentationException("Error in the child process |> ${e.stackTraceToString()}", e) - -class UnexpectedCommand(cmd: Protocol.Command) : - InstrumentationException("Got unexpected command: $cmd") + InstrumentationException("Error in the child process |> ${e.stackTraceToString()}", e) \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/KryoHelper.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/KryoHelper.kt index fc7182cb32..f9414ad3c2 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/KryoHelper.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/KryoHelper.kt @@ -9,76 +9,68 @@ import com.esotericsoftware.kryo.kryo5.objenesis.instantiator.ObjectInstantiator import com.esotericsoftware.kryo.kryo5.objenesis.strategy.StdInstantiatorStrategy import com.esotericsoftware.kryo.kryo5.serializers.JavaSerializer import com.esotericsoftware.kryo.kryo5.util.DefaultInstantiatorStrategy +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.throwIfNotAlive import org.utbot.framework.plugin.api.TimeoutException import java.io.ByteArrayOutputStream -import java.io.Closeable -import java.io.InputStream -import java.io.OutputStream /** * Helpful class for working with the kryo. */ class KryoHelper internal constructor( - inputStream: InputStream, - private val outputStream: OutputStream -) : Closeable { - private val temporaryBuffer = ByteArrayOutputStream() - - private val kryoOutput = Output(temporaryBuffer) - private val kryoInput = Input(inputStream) - + private val lifetime: Lifetime +) { + private val outputBuffer = ByteArrayOutputStream() + private val kryoOutput = Output(outputBuffer) + private val kryoInput= Input() private val sendKryo: Kryo = TunedKryo() private val receiveKryo: Kryo = TunedKryo() + init { + lifetime.onTermination { + kryoInput.close() + kryoOutput.close() + } + } + fun setKryoClassLoader(classLoader: ClassLoader) { sendKryo.classLoader = classLoader receiveKryo.classLoader = classLoader } - fun readLong(): Long { - return receiveKryo.readObject(kryoInput, Long::class.java) - } - /** - * Kryo tries to write the [cmd] to the [temporaryBuffer]. - * If no exception occurs, the output is flushed to the [outputStream]. + * Serializes object to ByteArray * - * If an exception occurs, rethrows it wrapped in [WritingToKryoException]. + * @throws WritingToKryoException wraps all exceptions */ - fun writeCommand(id: Long, cmd: T) { + fun writeObject(obj: T): ByteArray { + lifetime.throwIfNotAlive() try { - sendKryo.writeObject(kryoOutput, id) - sendKryo.writeClassAndObject(kryoOutput, cmd) + sendKryo.writeClassAndObject(kryoOutput, obj) kryoOutput.flush() - temporaryBuffer.writeTo(outputStream) - outputStream.flush() + return outputBuffer.toByteArray() } catch (e: Exception) { throw WritingToKryoException(e) } finally { kryoOutput.reset() - temporaryBuffer.reset() + outputBuffer.reset() } } /** - * Kryo tries to read a command. - * - * If an exception occurs, rethrows it wrapped in [ReadingFromKryoException]. + * Deserializes object form ByteArray * - * @return successfully read command. + * @throws ReadingFromKryoException wraps all exceptions */ - fun readCommand(): Protocol.Command = - try { - receiveKryo.readClassAndObject(kryoInput) as Protocol.Command + fun readObject(byteArray: ByteArray): T { + lifetime.throwIfNotAlive() + return try { + kryoInput.buffer = byteArray + receiveKryo.readClassAndObject(kryoInput) as T } catch (e: Exception) { throw ReadingFromKryoException(e) } - - override fun close() { - kryoInput.close() - kryoOutput.close() - outputStream.close() } } @@ -117,11 +109,6 @@ internal class TunedKryo : Kryo() { factory.config.serializeTransient = false factory.config.fieldsCanBeNull = true this.setDefaultSerializer(factory) - - // Registration of the classes of our protocol commands. - Protocol::class.nestedClasses.forEach { - register(it.java) - } } /** diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/Protocol.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/Protocol.kt deleted file mode 100644 index fd58ff3342..0000000000 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/Protocol.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.utbot.instrumentation.util - -import org.utbot.instrumentation.instrumentation.ArgumentList -import org.utbot.instrumentation.instrumentation.Instrumentation - -/** - * This object represents base commands for interprocess communication. - */ -object Protocol { - /** - * Abstract class for all commands. - */ - abstract class Command - - - /** - * The child process sends this command to the main process to indicate readiness. - */ - class ProcessReadyCommand : Command() - - /** - * The main process tells where the child process should search for the classes. - */ - data class AddPathsCommand( - val pathsToUserClasses: String, - val pathsToDependencyClasses: String - ) : Command() - - - /** - * The main process sends [instrumentation] to the child process. - */ - data class SetInstrumentationCommand( - val instrumentation: Instrumentation - ) : Command() - - /** - * The main process requests the child process to execute a method with the given [signature], - * which declaring class's name is [className]. - * - * @property parameters are the parameters needed for an execution, e.g. static environment. - */ - data class InvokeMethodCommand( - val className: String, - val signature: String, - val arguments: ArgumentList, - val parameters: Any?, - ) : Command() - - /** - * The child process returns the result of the invocation to the main process. - */ - data class InvocationResultCommand( - val result: T - ) : Command() - - /** - * Warmup - load classes from classpath and instrument them - */ - class WarmupCommand() : Command() - - /** - * The child process sends this command if unexpected exception was thrown. - * - * @property exception unexpected exception. - */ - data class ExceptionInChildProcess( - val exception: Throwable - ) : Command() - - data class ExceptionInKryoCommand(val exception: Throwable) : Command() - - /** - * This command tells the child process to stop. - */ - class StopProcessCommand : Command() - - /** - * [org.utbot.instrumentation.ConcreteExecutor] can send other commands depending on specific instrumentation. - * This commands will be handled in [Instrumentation.handle] function. - * - * Only inheritors of this abstract class will be passed in [Instrumentation.handle] function. - */ - abstract class InstrumentationCommand : Protocol.Command() - - -} - - diff --git a/utbot-junit-contest/build.gradle b/utbot-junit-contest/build.gradle index cd30e0e386..dca81e93bd 100644 --- a/utbot-junit-contest/build.gradle +++ b/utbot-junit-contest/build.gradle @@ -93,6 +93,7 @@ jar { dependsOn classes } duplicatesStrategy = DuplicatesStrategy.EXCLUDE + zip64 = true } diff --git a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimator.kt b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimator.kt index 8be688929a..eceac3d30f 100644 --- a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimator.kt +++ b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimator.kt @@ -15,7 +15,6 @@ import org.utbot.analytics.Predictors import org.utbot.common.FileUtil import org.utbot.common.bracket import org.utbot.common.info -import org.utbot.common.pid import org.utbot.contest.Paths.classesLists import org.utbot.contest.Paths.dependenciesJars import org.utbot.contest.Paths.evosuiteGeneratedTestsFile @@ -201,7 +200,7 @@ enum class Tool { logger.info { "Started processing $classFqn" } val process = ProcessBuilder(command).redirectErrorStream(true).start() - logger.info { "Pid: ${process.pid}" } + logger.info { "Pid: ${process.pid()}" } process.inputStream.bufferedReader().use { reader -> while (true) { diff --git a/utbot-rd/build.gradle b/utbot-rd/build.gradle new file mode 100644 index 0000000000..19a6b9adce --- /dev/null +++ b/utbot-rd/build.gradle @@ -0,0 +1,127 @@ +plugins { + id 'com.jetbrains.rdgen' version "2022.3.1" +} + +import com.jetbrains.rd.generator.gradle.RdGenExtension +import com.jetbrains.rd.generator.gradle.RdGenTask + + +apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" + +compileKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +configurations { + lifetimedProcessMockCompileClasspath.extendsFrom configurations.compileClasspath + processWithRdServerMockCompileClasspath.extendsFrom configurations.compileClasspath + rdgenModelsCompileClasspath.extendsFrom configurations.compileClasspath +} + +sourceSets { + lifetimedProcessMock { + kotlin { + srcDirs = ["src/main/lifetimedProcessMock"] + } + } + processWithRdServerMock { + kotlin { + srcDirs = ["src/main/processWithRdServerMock"] + } + } + rdgenModels { + kotlin { + srcDirs = ["src/main/rdgen"] + } + } +} + +dependencies { + implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: '2022.3.1' + implementation group: 'com.jetbrains.rd', name: 'rd-core', version: '2022.3.1' + + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version + + processWithRdServerMockImplementation project(':utbot-rd') + + rdgenModelsCompileClasspath group: 'com.jetbrains.rd', name: 'rd-gen', version: '2022.3.1' +} + +task lifetimedProcessMockJar (type: Jar) { + dependsOn lifetimedProcessMockClasses + archiveAppendix.set("lifetimedProcessMock") + + manifest { + attributes( + 'Main-Class': 'org.utbot.rd.tests.LifetimedProcessMockKt' + ) + } + + from configurations.lifetimedProcessMockCompileClasspath.collect { + (it.isDirectory() || !it.exists()) ? it : zipTree(it) + } + sourceSets.lifetimedProcessMock.output + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +task processWithRdServerMockJar (type: Jar) { + dependsOn processWithRdServerMockClasses + archiveAppendix.set("processWithRdServerMock") + + manifest { + attributes( + 'Main-Class': 'org.utbot.rd.tests.ProcessWithRdServerMockKt' + ) + } + + from configurations.processWithRdServerMockCompileClasspath.collect { + (it.isDirectory() || !it.exists()) ? it : zipTree(it) + } + sourceSets.processWithRdServerMock.output + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +test { + dependsOn lifetimedProcessMockJar + dependsOn processWithRdServerMockJar + systemProperty("RD_MOCK_PROCESS", lifetimedProcessMockJar.archiveFile.get().getAsFile().canonicalPath) + systemProperty("PROCESS_WITH_RD_SERVER_MOCK", processWithRdServerMockJar.archiveFile.get().getAsFile().canonicalPath) +} + +task generateProtocolModels(type: RdGenTask) { + def currentProjectDir = project.projectDir + def instrumentationProjectDir = project.rootProject.childProjects["utbot-instrumentation"].projectDir + def hashDir = new File(instrumentationProjectDir, "build/rdgen/hashes/models") + def sourcesDir = new File(currentProjectDir, "src/main/rdgen/org/utbot/rd/models") + def generatedOutputDir = new File(instrumentationProjectDir, "src/main/kotlin/org/utbot/instrumentation/rd/generated") + def rdParams = extensions.getByName("params") as RdGenExtension + + group = "rdgen" + rdParams.verbose = true + rdParams.sources(sourcesDir) + rdParams.hashFolder = hashDir.canonicalPath + // where to search roots + rdParams.packages = "org.utbot.rd.models" + + rdParams.generator { + language = "kotlin" + transform = "symmetric" + root = "org.utbot.rd.models.ProtocolRoot" + + directory = generatedOutputDir.canonicalPath + namespace = "org.utbot.instrumentation.rd.generated" + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/LifetimedProcess.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/LifetimedProcess.kt new file mode 100644 index 0000000000..93e6dba94d --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/LifetimedProcess.kt @@ -0,0 +1,87 @@ +package org.utbot.rd + +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import com.jetbrains.rd.util.lifetime.throwIfNotAlive +import kotlinx.coroutines.delay +import java.util.concurrent.TimeUnit + +/** + * Creates LifetimedProcess. + * If provided lifetime already terminated - throws CancellationException and process is not started. + */ +fun startLifetimedProcess(cmd: List, lifetime: Lifetime? = null): LifetimedProcess { + lifetime?.throwIfNotAlive() + + return ProcessBuilder(cmd).start().toLifetimedProcess(lifetime) +} + +/** + * Creates LifetimedProcess from already running process. + * + * Process will be terminated if provided lifetime is terminated. + */ +fun Process.toLifetimedProcess(lifetime: Lifetime? = null): LifetimedProcess { + return LifetimedProcessIml(this, lifetime) +} + +/** + * Main class goals + * 1. if process terminates - lifetime terminates + * 2. if lifetime terminates - process terminates + */ +interface LifetimedProcess { + val lifetime: Lifetime + val process: Process + fun terminate() +} + +inline fun R.use(block: (R) -> T): T { + try { + return block(this) + } + finally { + terminate() + } +} + +inline fun R.terminateOnException(block: (R) -> T): T { + try { + return block(this) + } + catch(e: Throwable) { + terminate() + throw e + } +} + +const val processKillTimeoutMillis = 100L +const val checkProcessAliveDelay = 100L + +class LifetimedProcessIml(override val process: Process, lifetime: Lifetime? = null): LifetimedProcess { + private val ldef: LifetimeDefinition + + override val lifetime + get() = ldef.lifetime + + init { + ldef = lifetime?.createNested() ?: LifetimeDefinition() + ldef.onTermination { + process.destroy() + + if (process.waitFor(processKillTimeoutMillis, TimeUnit.MILLISECONDS)) + process.destroyForcibly() + } + UtRdCoroutineScope.current.launch(ldef) { + while (process.isAlive) { + delay(checkProcessAliveDelay) + } + + ldef.terminate() + } + } + + override fun terminate() { + ldef.terminate() + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/ProcessWithRdServer.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/ProcessWithRdServer.kt new file mode 100644 index 0000000000..cf475e345a --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/ProcessWithRdServer.kt @@ -0,0 +1,87 @@ +package org.utbot.rd + +import com.jetbrains.rd.framework.Protocol +import com.jetbrains.rd.framework.serverPort +import com.jetbrains.rd.framework.util.NetUtils +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.throwIfNotAlive + +/** + * Process will be terminated if lifetime is not alive + */ +suspend fun Process.withRdServer( + lifetime: Lifetime? = null, + serverFactory: (Lifetime) -> Protocol +): ProcessWithRdServer { + return ProcessWithRdServerImpl(toLifetimedProcess(lifetime)) { + serverFactory(it) + } +} + +suspend fun LifetimedProcess.withRdServer( + serverFactory: (Lifetime) -> Protocol +): ProcessWithRdServer { + return ProcessWithRdServerImpl(this) { + serverFactory(it) + } +} + +/** + * Process will not be started if lifetime is not alive + */ +suspend fun startProcessWithRdServer( + cmd: List, + lifetime: Lifetime? = null, + serverFactory: (Lifetime) -> Protocol +): ProcessWithRdServer { + lifetime?.throwIfNotAlive() + + val child = startLifetimedProcess(cmd, lifetime) + + return child.withRdServer { + serverFactory(it) + } +} + +/** + * Process will not be started if lifetime is not alive + */ +suspend fun startProcessWithRdServer( + processFactory: (Int) -> Process, + lifetime: Lifetime? = null, + serverFactory: (Int, Lifetime) -> Protocol +): ProcessWithRdServer { + lifetime?.throwIfNotAlive() + + val port = NetUtils.findFreePort(0) + + return processFactory(port).withRdServer(lifetime) { + serverFactory(port, it) + } +} + +/** + * Main goals of this class: + * 1. start rd server protocol with child process + * 2. protocol should be bound to process lifetime + */ +interface ProcessWithRdServer : LifetimedProcess { + val protocol: Protocol + val port: Int + get() = protocol.wire.serverPort +} + +class ProcessWithRdServerImpl private constructor( + private val child: LifetimedProcess, + serverFactory: (Lifetime) -> Protocol +) : ProcessWithRdServer, LifetimedProcess by child { + override val protocol = serverFactory(lifetime) + + companion object { + suspend operator fun invoke( + child: LifetimedProcess, serverFactory: (Lifetime) -> Protocol + ): ProcessWithRdServerImpl = ProcessWithRdServerImpl(child, serverFactory).terminateOnException { + it.apply { protocol.wire.connected.adviseForConditionAsync(lifetime).await() } + } + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdCoroutineScope.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdCoroutineScope.kt new file mode 100644 index 0000000000..75216461ed --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdCoroutineScope.kt @@ -0,0 +1,52 @@ +package org.utbot.rd + +import com.jetbrains.rd.framework.util.RdCoroutineScope +import com.jetbrains.rd.framework.util.asCoroutineDispatcher +import com.jetbrains.rd.util.lifetime.Lifetime +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging + +private val logger = KotlinLogging.logger("UtRdCoroutineScope") + +class UtRdCoroutineScope(lifetime: Lifetime) : RdCoroutineScope(lifetime) { + companion object { + private val dispatcher = + UtSingleThreadScheduler("UtRdCoroutineScope dispatcher") { logger.info(it) }.asCoroutineDispatcher + val current = UtRdCoroutineScope(Lifetime.Eternal) + } + + init { + lifetime.bracketIfAlive({ + override(lifetime, this) + logger.info { "UtRdCoroutineScope overridden" } + }, { + logger.info { "UtRdCoroutineScope has been reset" } + }) + lifetime.onTermination { + logger.info("UtRdCoroutineScope disposed") + } + } + + override val defaultDispatcher = dispatcher + + override fun shutdown() { + try { + runBlocking { + coroutineContext[Job]!!.cancelAndJoin() + } + } catch (e: CancellationException) { + // nothing + } catch (e: Throwable) { + logger.error { "error during shutdown: $e" } + } + } + + override fun onException(throwable: Throwable) { + if (throwable !is CancellationException) { + logger.error("Unhandled coroutine throwable: $throwable\n stacktrace: ${throwable.stackTraceToString()}") + } + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdUtil.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdUtil.kt new file mode 100644 index 0000000000..cd3fd6ca0a --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdUtil.kt @@ -0,0 +1,81 @@ +package org.utbot.rd + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.util.NetUtils +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import com.jetbrains.rd.util.lifetime.throwIfNotAlive +import com.jetbrains.rd.util.reactive.IScheduler +import com.jetbrains.rd.util.reactive.ISource +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import mu.KotlinLogging + +private val logger = KotlinLogging.logger {} +private val serverScheduler = UtSingleThreadScheduler { logger.info(it) } + +inline fun LifetimeDefinition.terminateOnException(block: (Lifetime) -> T): T { + try { + return block(this) + } catch (e: Throwable) { + this.terminate() + throw e + } +} + +suspend fun IScheduler.pump(lifetime: Lifetime, block: () -> T): T { + val ldef = lifetime.createNested() + val deferred = CompletableDeferred() + + ldef.onTermination { deferred.cancel() } + deferred.invokeOnCompletion { ldef.terminate() } + + this.invokeOrQueue { + deferred.complete(block()) + } + + return deferred.await() +} + +suspend fun ISource.adviseForConditionAsync(lifetime: Lifetime, condition: (T) -> Boolean): Deferred { + val ldef = lifetime.createNested() + val deferred = CompletableDeferred() + + ldef.onTermination { deferred.cancel() } + deferred.invokeOnCompletion { ldef.terminate() } + + this.advise(ldef) { + if(condition(it)) { + deferred.complete(Unit) + } + } + + return deferred +} + +suspend fun ISource.adviseForConditionAsync(lifetime: Lifetime): Deferred { + return this.adviseForConditionAsync(lifetime) {it} +} + +/** + * Process will not be started if lifetime is not alive + */ +suspend fun startUtProcessWithRdServer( + lifetime: Lifetime? = null, + factory: (Int) -> Process +): ProcessWithRdServer { + lifetime?.throwIfNotAlive() + + val port = NetUtils.findFreePort(0) + + return factory(port).withRdServer(lifetime) { + Protocol( + "Server", + Serializers(), + Identities(IdKind.Server), + serverScheduler, + SocketWire.Server(it, serverScheduler, port, "ServerSocket"), + it + ) + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/UtSingleThreadScheduler.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/UtSingleThreadScheduler.kt new file mode 100644 index 0000000000..4b63b8bdaf --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/UtSingleThreadScheduler.kt @@ -0,0 +1,10 @@ +package org.utbot.rd + +import com.jetbrains.rd.util.threading.SingleThreadSchedulerBase + +class UtSingleThreadScheduler(name: String = "UtRdScheduler", private val log: (() -> String) -> Unit) : + SingleThreadSchedulerBase(name) { + override fun onException(ex: Throwable) { + log { ex.toString() } + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/lifetimedProcessMock/org/utbot/rd/tests/LifetimedProcessMock.kt b/utbot-rd/src/main/lifetimedProcessMock/org/utbot/rd/tests/LifetimedProcessMock.kt new file mode 100644 index 0000000000..f1f472fc7a --- /dev/null +++ b/utbot-rd/src/main/lifetimedProcessMock/org/utbot/rd/tests/LifetimedProcessMock.kt @@ -0,0 +1,12 @@ +package org.utbot.rd.tests + +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking + +fun main(args: Array) { + val length = args.single().toLong() + + runBlocking { + delay(length * 1000) + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/processWithRdServerMock/org/utbot/rd/tests/ProcessWithRdServerMock.kt b/utbot-rd/src/main/processWithRdServerMock/org/utbot/rd/tests/ProcessWithRdServerMock.kt new file mode 100644 index 0000000000..304add54ce --- /dev/null +++ b/utbot-rd/src/main/processWithRdServerMock/org/utbot/rd/tests/ProcessWithRdServerMock.kt @@ -0,0 +1,40 @@ +package org.utbot.rd.tests + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import com.jetbrains.rd.util.reactive.IScheduler +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking + +fun main(args: Array): Unit { + val def = LifetimeDefinition() + val length = args.first().toLong() + val shouldstart = args.last().toBoolean() + val port = args[1].toInt() + val scheduler = object : IScheduler { + override val isActive = true + + override fun flush() {} + + override fun queue(action: () -> Unit) { + action() + } + } + + if (shouldstart) { + val protocol = Protocol( + "TestClient", + Serializers(), + Identities(IdKind.Client), + scheduler, + SocketWire.Client(def, scheduler, port), + def + ) + println(protocol.name) + } + + runBlocking { + delay(length * 1000) + } + def.terminate() +} \ No newline at end of file diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/ProtocolRoot.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/ProtocolRoot.kt new file mode 100644 index 0000000000..ad49e762a0 --- /dev/null +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/ProtocolRoot.kt @@ -0,0 +1,71 @@ +package org.utbot.rd.models + +import com.jetbrains.rd.generator.nova.* + +object ProtocolRoot : Root() + +object ProtocolModel : Ext(ProtocolRoot) { + val AddPathsParams = structdef { + field("pathsToUserClasses", PredefinedType.string) + field("pathsToDependencyClasses", PredefinedType.string) + } + + val SetInstrumentationParams = structdef { + field("instrumentation", array(PredefinedType.byte)) + } + + val InvokeMethodCommandParams = structdef { + field("classname", PredefinedType.string) + field("signature", PredefinedType.string) + field("arguments", array(PredefinedType.byte)) + field("parameters", array(PredefinedType.byte)) + } + + val InvokeMethodCommandResult = structdef { + field("result", array(PredefinedType.byte)) + } + + val CollectCoverageParams = structdef { + field("clazz", array(PredefinedType.byte)) + } + + val CollectCoverageResult = structdef { + field("coverageInfo", array(PredefinedType.byte)) + } + + init { + call("AddPaths", AddPathsParams, PredefinedType.void).apply { + async + documentation = + "The main process tells where the child process should search for the classes" + } + call("Warmup", PredefinedType.void, PredefinedType.void).apply { + async + documentation = + "Load classes from classpath and instrument them" + } + call("SetInstrumentation", SetInstrumentationParams, PredefinedType.void).apply { + async + documentation = + "The main process sends [instrumentation] to the child process" + } + call("InvokeMethodCommand", InvokeMethodCommandParams, InvokeMethodCommandResult).apply { + async + documentation = + "The main process requests the child process to execute a method with the given [signature],\n" + + "which declaring class's name is [className].\n" + + "@property parameters are the parameters needed for an execution, e.g. static environment" + } + call("StopProcess", PredefinedType.void, PredefinedType.void).apply { + async + documentation = + "This command tells the child process to stop" + } + call("CollectCoverage", CollectCoverageParams, CollectCoverageResult).apply { + async + documentation = + "This command is sent to the child process from the [ConcreteExecutor] if user wants to collect coverage for the\n" + + "[clazz]" + } + } +} \ No newline at end of file diff --git a/utbot-rd/src/test/kotlin/org/utbot/rd/tests/LifetimedProcessTest.kt b/utbot-rd/src/test/kotlin/org/utbot/rd/tests/LifetimedProcessTest.kt new file mode 100644 index 0000000000..265413cac7 --- /dev/null +++ b/utbot-rd/src/test/kotlin/org/utbot/rd/tests/LifetimedProcessTest.kt @@ -0,0 +1,100 @@ +package org.utbot.rd.tests + +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import com.jetbrains.rd.util.lifetime.isAlive +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.utbot.rd.LifetimedProcess +import org.utbot.rd.startLifetimedProcess +import java.io.File + +class LifetimedProcessTest { + lateinit var def: LifetimeDefinition + private val parent: Lifetime + get() = def.lifetime + + @BeforeEach + fun initLifetimes() { + def = LifetimeDefinition() + } + + @AfterEach + fun terminateLifetimes() { + def.terminate() + } + + private fun processMockCmd(delayInSeconds: Long): List { + val jar = System.getProperty("RD_MOCK_PROCESS") + val javaHome = System.getProperty("java.home") + val java = File(javaHome, "bin").resolve("java") + + return listOf(java.canonicalPath, "-ea", "-jar", jar, delayInSeconds.toString()) + } + + private fun List.startLifetimedProcessWithAssertion(block: (LifetimedProcess) -> Unit) { + val proc = startLifetimedProcess(this, parent) + + assertProcessAlive(proc) + block(proc) + assertProcessDead(proc) + } + + private fun assertProcessAlive(proc: LifetimedProcess) = runBlocking { + delay(1000) // if proc not started in 1 seconds - something is bad + Assertions.assertTrue(proc.lifetime.isAlive) + Assertions.assertTrue(proc.process.isAlive) + } + + private fun assertProcessDead(proc: LifetimedProcess) = runBlocking { + delay(1000) // if proc is not dead in 1 second - something is bad + Assertions.assertFalse(proc.lifetime.isAlive) + Assertions.assertFalse(proc.process.isAlive) + } + + @Test + fun testProcessLifetimeTermination() { + val cmds = processMockCmd(10) + + cmds.startLifetimedProcessWithAssertion { + it.terminate() + } + Assertions.assertTrue(parent.isAlive) + } + + @Test + fun testParentLifetimeTermination() { + val cmds = processMockCmd(10) + + cmds.startLifetimedProcessWithAssertion { + terminateLifetimes() + } + Assertions.assertFalse(parent.isAlive) + } + + @Test + fun testProcessDeath() { + val cmds = processMockCmd(3) + + cmds.startLifetimedProcessWithAssertion { + runBlocking { + delay(5000) + } + } + Assertions.assertTrue(parent.isAlive) + } + + @Test + fun testProcessKill() { + val cmds = processMockCmd(10) + + cmds.startLifetimedProcessWithAssertion { + it.process.destroyForcibly() + } + Assertions.assertTrue(parent.isAlive) + } +} \ No newline at end of file diff --git a/utbot-rd/src/test/kotlin/org/utbot/rd/tests/ProcessWithRdServerTest.kt b/utbot-rd/src/test/kotlin/org/utbot/rd/tests/ProcessWithRdServerTest.kt new file mode 100644 index 0000000000..58f5d77cef --- /dev/null +++ b/utbot-rd/src/test/kotlin/org/utbot/rd/tests/ProcessWithRdServerTest.kt @@ -0,0 +1,105 @@ +package org.utbot.rd.tests + +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import com.jetbrains.rd.util.lifetime.isAlive +import kotlinx.coroutines.* +import org.junit.jupiter.api.* +import org.utbot.rd.* +import java.io.File + +class ProcessWithRdServerTest { + lateinit var def: LifetimeDefinition + private val parent: Lifetime + get() = def.lifetime + + @BeforeEach + fun initLifetimes() { + def = LifetimeDefinition() + } + + @AfterEach + fun terminateLifetimes() { + def.terminate() + } + + private fun processMockCmd(delayInSeconds: Long, port: Int, shouldStartProtocol: Boolean): List { + val jar = System.getProperty("PROCESS_WITH_RD_SERVER_MOCK") + val javaHome = System.getProperty("java.home") + val java = File(javaHome, "bin").resolve("java") + + return listOf(java.canonicalPath, "-ea", "-jar", jar, delayInSeconds.toString(), port.toString(), shouldStartProtocol.toString()) + } + + private fun assertProcessAlive(proc: LifetimedProcess) = runBlocking { + delay(1000) // if proc not started in 1 seconds - something is bad + Assertions.assertTrue(proc.lifetime.isAlive) + Assertions.assertTrue(proc.process.isAlive) + } + + private fun assertProcessDead(proc: LifetimedProcess) = runBlocking { + delay(1000) // if proc is not dead in 1 second - something is bad + Assertions.assertFalse(proc.lifetime.isAlive) + Assertions.assertFalse(proc.process.isAlive) + } + + @Test + fun testParentOnly() = runBlocking { + var lifetimedProcess: LifetimedProcess? = null + val exception = assertThrows { + withTimeout(5000) { + startUtProcessWithRdServer(parent) { + val cmds = processMockCmd(3, it, false) + val proc = ProcessBuilder(cmds).start() + val lfProc = proc.toLifetimedProcess(parent) + + assertProcessAlive(lfProc) + lifetimedProcess = lfProc + proc + } + } + } + + Assertions.assertFalse(exception is TimeoutCancellationException) + Assertions.assertTrue(parent.isAlive) + assertProcessDead(lifetimedProcess!!) + } + + @Test + fun testParentWithChild() = runBlocking { + val child = startUtProcessWithRdServer(parent) { + val cmds = processMockCmd(3, it, true) + + ProcessBuilder(cmds).start() + } + + assertProcessAlive(child) + delay(3000) + assertProcessDead(child) + + Assertions.assertTrue(parent.isAlive) + Assertions.assertFalse(child.protocol.lifetime.isAlive) + } + + @Test + fun testCancellation() = runBlocking { + var lifetimedProcess: LifetimedProcess? = null + val exception = assertThrows { + withTimeout(1000) { + startUtProcessWithRdServer(parent) { + val cmds = processMockCmd(3, it, false) + val proc = ProcessBuilder(cmds).start() + val lfProc = proc.toLifetimedProcess(parent) + + assertProcessAlive(lfProc) + lifetimedProcess = lfProc + proc + } + } + } + + Assertions.assertTrue(exception is TimeoutCancellationException) + Assertions.assertTrue(parent.isAlive) + assertProcessDead(lifetimedProcess!!) + } +} \ No newline at end of file