diff --git a/src/dorkbox/executor/Executor.kt b/src/dorkbox/executor/Executor.kt index 8a021c9..ed8d3f3 100644 --- a/src/dorkbox/executor/Executor.kt +++ b/src/dorkbox/executor/Executor.kt @@ -213,12 +213,6 @@ open class Executor { */ private var allowedExitValues: Set? = null - /** - * Timeout for running a process. If the process is running too long a [TimeoutException] is thrown and the process is destroyed. - */ - private var timeout: Long = 0 - private var timeoutUnit: TimeUnit = TimeUnit.SECONDS - /** * Helper for stopping the process in case of timeout or cancellation. */ @@ -279,14 +273,14 @@ open class Executor { private val executingMessageParams: String get() { - var result = "" + builder.command() + var result = builder.command().joinToString(separator = " ") if (builder.directory() != null) { result += " in " + builder.directory() } if (environment.isNotEmpty()) { result += " with environment $environment" } - result += "." + return result } @@ -629,21 +623,6 @@ open class Executor { return this } - /** - * Sets a timeout for the process being executed. When this timeout is reached a [TimeoutException] is thrown and the process is destroyed. - * This only applies to `execute` methods not `start` methods. - * - * @param timeout timeout for running a process. - * @param unit the time unit of the timeout, default is [TimeUnit.SECONDS] - * - * @return This process executor. - */ - fun timeout(timeout: Long, unit: TimeUnit = TimeUnit.SECONDS): Executor { - this.timeout = timeout - this.timeoutUnit = unit - return this - } - /** * Sets the helper for stopping the process in case of timeout or cancellation. * @@ -762,6 +741,7 @@ open class Executor { * * @return This process executor. */ + @JvmOverloads fun redirectOutput(output: OutputStream? = null): Executor { var outputStream = output if (outputStream == null) { @@ -1141,22 +1121,6 @@ open class Executor { return this } - /** - * Changes how most common messages about starting and waiting for processes are actually logged. - * By default **NO OUTPUT** is used. - * - * see http://logback.qos.ch/manual/architecture.html for more info - * logger order goes (from lowest to highest) TRACE->DEBUG->INFO->WARN->ERROR->OFF - * - * @param logger logger instance to use. Will log at whatever the highest level possible for that logger - * - * @return This process executor. - */ - fun setLogger(logger: Logger?): Executor { - this.logger = logger - return this - } - /** * Check the exit value of given process result. This can be used by unit tests. * @@ -1169,6 +1133,22 @@ open class Executor { DeferredProcessResult.checkExit(attributes, result) } + /** + * Changes how most common messages about starting and waiting for processes are actually logged. + * This is configures the Executor **execution logs** to use the Executor log (instead of no logger) + * + * This will use the log at whatever the highest level possible for that logger + * + * see http://logback.qos.ch/manual/architecture.html for more info + * logger order goes (from lowest to highest) TRACE->DEBUG->INFO->WARN->ERROR->OFF + * + * @return This process executor. + */ + fun defaultLogger(): Executor { + this.logger = log + return this + } + /** * If there is high-performance I/O necessary (lots of I/O with the subprocess), then multiple threads * can be used for pumping the I/O. @@ -1185,10 +1165,38 @@ open class Executor { /** * Execute this command as JAVA, using the same JVM as the currently running JVM, as a forked process. * + * Be aware that on MACOS there are two quirks that can both occur! + * - this *must* use the same java installation as pointed to by JAVA_HOME + * - if the macos specific java flag `-Xdock:name` was/is used -- then it *must* be `/usr/bin/java`, even if it's + * a symlink to the same location as JAVA_HOME! + * + * Because of these quirks, on MACOS, if the javaExecutable is not specified, then `/usr/bin/java` will always be used. + * * This should be used last, as the only thing possible from here is [start] and [startAsync] variants */ - fun asJvmProcess(): JvmExecOptions { - jvmExecOptions = JvmExecOptions(this) + @JvmOverloads + fun asJvmProcess(javaExecutable: String? = null): JvmExecOptions { + jvmExecOptions = JvmExecOptions(this, javaExecutable) + return jvmExecOptions!! + } + + /** + * Execute this command as JAVA, using the same JVM as the currently running JVM, as a forked process. + * + * Be aware that on MACOS there are two quirks that can both occur! + * - this *must* use the same java installation as pointed to by JAVA_HOME + * - if the macos specific java flag `-Xdock:name` was/is used -- then it *must* be `/usr/bin/java`, even if it's + * a symlink to the same location as JAVA_HOME! + * + * Because of these quirks, on MACOS, if the javaExecutable is not specified, then `/usr/bin/java` will always be used. + * + * This should be used last, as the only thing possible from here is [start] and [startAsync] variants + */ + fun asJvmProcess(javaExecutable: File): JvmExecOptions { + if (!javaExecutable.canExecute()) { + throw IllegalArgumentException("The java executable if specified, must be exist and be executable. Error with: $javaExecutable") + } + jvmExecOptions = JvmExecOptions(this, javaExecutable.canonicalFile.path) return jvmExecOptions!! } @@ -1215,10 +1223,11 @@ open class Executor { * * @throws IOException an error occurred when process was started. */ + @JvmOverloads @Throws(IOException::class) - fun startAsShellAsync(): DeferredProcessResult { + fun startAsShellAsync(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): DeferredProcessResult { executeAsShell = true - return startAsync() + return startAsync(timeout, timeoutUnit) } /** @@ -1230,13 +1239,17 @@ open class Executor { * * Invoke [DeferredProcessResult.cancel] to destroy the process. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return [DeferredProcessResult] representing the process results (value/completed output-streams/etc) of the finished process. * * @throws IOException an error occurred when process was started. */ + @JvmOverloads @Throws(IOException::class) - fun startAsync(): DeferredProcessResult { - return prepareProcess(true) + fun startAsync(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): DeferredProcessResult { + return prepareProcess(timeout, timeoutUnit, true) } /** @@ -1250,6 +1263,9 @@ open class Executor { * * Calling [SyncProcessResult.output] will result in a non-blocking read of process output. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return results of the finished process (exit code and output, if any) * * @throws IOException an error occurred when process was started or stopped. @@ -1257,12 +1273,13 @@ open class Executor { * @throws TimeoutException timeout set by [.timeout] was reached. * @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]). */ + @JvmOverloads @Throws(IOException::class, InterruptedException::class, TimeoutException::class, InvalidExitValueException::class) - suspend fun startAsShell(): SyncProcessResult { + suspend fun startAsShell(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult { executeAsShell = true @Suppress("BlockingMethodInNonBlockingContext") - return start() + return start(timeout, timeoutUnit) } /** @@ -1276,6 +1293,9 @@ open class Executor { * * Calling [SyncProcessResult.output] will result in a non-blocking read of process output. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return results of the finished process (exit code and output, if any) * * @throws IOException an error occurred when process was started or stopped. @@ -1283,10 +1303,11 @@ open class Executor { * @throws TimeoutException timeout set by [.timeout] was reached. * @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]). */ + @JvmOverloads @Throws(IOException::class, InterruptedException::class, TimeoutException::class, InvalidExitValueException::class) - fun startAsShellBlocking(): SyncProcessResult { + fun startAsShellBlocking(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult { return runBlocking { - startAsShell() + startAsShell(timeout, timeoutUnit) } } @@ -1302,6 +1323,9 @@ open class Executor { * * Calling [SyncProcessResult.output] will result in a non-blocking read of process output. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return results of the finished process (exit code and output, if any) * * @throws IOException an error occurred when process was started or stopped. @@ -1309,14 +1333,15 @@ open class Executor { * @throws TimeoutException timeout set by [.timeout] was reached. * @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]). */ + @JvmOverloads @Throws(IOException::class, InterruptedException::class, TimeoutException::class, InvalidExitValueException::class) - suspend fun start(): SyncProcessResult { + suspend fun start(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult { // we ALWAYS want to block/suspend the current running thread! // This is because we want the same blocking behavior if we have NO timeout or WITH timeout. // Always wait for it to finish. We cannot interrupt this (because we are blocking). // Use startAsync() to interrupt or wait without blocking - return prepareProcess(false).await(timeout, timeoutUnit) + return prepareProcess(timeout, timeoutUnit, false).await(timeout, timeoutUnit) } /** @@ -1328,6 +1353,9 @@ open class Executor { * * Calling [SyncProcessResult.output] will result in a non-blocking read of process output. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return results of the finished process (exit code and output, if any) * * @throws IOException an error occurred when process was started or stopped. @@ -1335,23 +1363,26 @@ open class Executor { * @throws TimeoutException timeout set by [.timeout] was reached. * @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]). */ + @JvmOverloads @Throws(IOException::class, InterruptedException::class, TimeoutException::class, InvalidExitValueException::class) - fun startBlocking(): SyncProcessResult { + fun startBlocking(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult { return runBlocking { - start() + start(timeout, timeoutUnit) } } - /** * Start the process and its stream handlers. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return process the started process. * - * @throws IOException the process or its stream handlers couldn't start (in the latter case we also destroy the process). + * @throws IOException the process or its stream handlers couldn't start (in the latter case we also destroy the process). */ @OptIn(ExperimentalCoroutinesApi::class) - private fun prepareProcess(asyncProcessStart: Boolean): DeferredProcessResult { + private fun prepareProcess(timeout: Long, timeoutUnit: TimeUnit, asyncProcessStart: Boolean): DeferredProcessResult { // Invoke listeners - they can modify this executor listeners.beforeStart(this) val command = builder.command() diff --git a/src/dorkbox/executor/JvmExecOptions.kt b/src/dorkbox/executor/JvmExecOptions.kt index 030d294..2353cd8 100644 --- a/src/dorkbox/executor/JvmExecOptions.kt +++ b/src/dorkbox/executor/JvmExecOptions.kt @@ -22,12 +22,12 @@ import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory import java.io.File import java.io.IOException -import java.util.concurrent.TimeoutException +import java.util.concurrent.* /** * Options for configuring a process to run using the same JVM as the currently launched jvm */ -class JvmExecOptions(private val executor: Executor) { +class JvmExecOptions(private val executor: Executor, private val javaExecutable: String? = null) { companion object { private val log = LoggerFactory.getLogger(JvmExecOptions::class.java) @@ -86,7 +86,7 @@ class JvmExecOptions(private val executor: Executor) { // classpath val additionalClasspath = System.getProperty("java.class.path") if (additionalClasspath.isNotEmpty()) { - builder.append(pathSeparator) // have to add a seperator + builder.append(pathSeparator) // have to add a separator builder.append(additionalClasspath) } } @@ -185,7 +185,8 @@ class JvmExecOptions(private val executor: Executor) { * @return This process executor. */ fun addArg(vararg arguments: String): JvmExecOptions { - executor.addArg(*arguments) + val fixed = Executor.fixArguments(listOf(*arguments)) + mainClassArguments.addAll(fixed) return this } @@ -199,7 +200,8 @@ class JvmExecOptions(private val executor: Executor) { * @return This process executor. */ fun addArg(arguments: Iterable): JvmExecOptions { - executor.addArg(arguments) + val fixed = Executor.fixArguments(arguments) + mainClassArguments.addAll(fixed) return this } @@ -210,6 +212,9 @@ class JvmExecOptions(private val executor: Executor) { * * In the latter cases the process gets destroyed as well. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return exit code of the finished process. * * @throws IOException an error occurred when process was started or stopped. @@ -217,9 +222,10 @@ class JvmExecOptions(private val executor: Executor) { * @throws TimeoutException timeout set by [.timeout] was reached. * @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]). */ + @JvmOverloads @Throws(IOException::class, InterruptedException::class, TimeoutException::class, InvalidExitValueException::class) - suspend fun start(): SyncProcessResult { - return executor.start() + suspend fun start(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult { + return executor.start(timeout, timeoutUnit) } /** @@ -231,13 +237,17 @@ class JvmExecOptions(private val executor: Executor) { * * Invoke [DeferredProcessResult.cancel] to destroy the process. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return [DeferredProcessResult] representing the process results (value/completed outputstreams/etc) of the finished process. * * @throws IOException an error occurred when process was started. */ + @JvmOverloads @Throws(IOException::class) - fun startAsync(): DeferredProcessResult { - return executor.startAsync() + fun startAsync(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): DeferredProcessResult { + return executor.startAsync(timeout, timeoutUnit) } /** @@ -249,6 +259,9 @@ class JvmExecOptions(private val executor: Executor) { * * Calling [SyncProcessResult.output] will result in a non-blocking read of process output. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return results of the finished process (exit code and output, if any) * * @throws IOException an error occurred when process was started or stopped. @@ -256,10 +269,11 @@ class JvmExecOptions(private val executor: Executor) { * @throws TimeoutException timeout set by [.timeout] was reached. * @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]). */ + @JvmOverloads @Throws(IOException::class, InterruptedException::class, TimeoutException::class, InvalidExitValueException::class) - fun startBlocking(): SyncProcessResult { + fun startBlocking(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult { return runBlocking { - start() + start(timeout, timeoutUnit) } } @@ -268,8 +282,11 @@ class JvmExecOptions(private val executor: Executor) { val commandLineArgs = executor.builder.command() val newArgs = mutableListOf() - newArgs.add(javaLocation.absolutePath) - + if (javaExecutable != null) { + newArgs.add(javaExecutable) + } else { + newArgs.add(javaLocation.canonicalFile.path) + } // setup heap information if (initialHeapSizeInMegabytes != 0) { @@ -289,6 +306,10 @@ class JvmExecOptions(private val executor: Executor) { newArgs.addAll(Executor.fixArguments(jvmOptions)) } + // now add the original arguments + newArgs.addAll(commandLineArgs) + + // get the classpath, which is the same as using -cp val classpath: String = getClasspath() @@ -329,9 +350,6 @@ class JvmExecOptions(private val executor: Executor) { newArgs.addAll(Executor.fixArguments(mainClassArguments)) } - // now add the original arguments - newArgs.addAll(commandLineArgs) - // set the arguments executor.builder.command(newArgs) } diff --git a/src/dorkbox/executor/JvmHelper.kt b/src/dorkbox/executor/JvmHelper.kt index 26909a5..f20d1e4 100644 --- a/src/dorkbox/executor/JvmHelper.kt +++ b/src/dorkbox/executor/JvmHelper.kt @@ -40,26 +40,19 @@ object JvmHelper { * Reconstructs the path to the JVM used to launch this process of java. It will always use the "console" version, even on windows. */ fun getJvmPath(): File { - // use the VM in which we're already running --- MAYBE + // use the VM in which we're already running --- MAYBE. // THIS DOES NOT ALWAYS WORK CORRECTLY, especially if the JVM launched is NOT the JVM for which the path is set! - var jvmExecutable = getJvmExecutable(System.getProperty("java.home")) - // Oddly, the Mac OS X specific java flag -Xdock:name will only work if java is launched - // from /usr/bin/java, and not if launched by directly referring to /bin/java, - // even though the former is a symlink to the latter! To work around this, see if the - // desired jvm is in fact pointed to by /usr/bin/java and, if so, use that instead. + // Additionally, dynamic java execution at **runtime** on MACOS does not work anymore. + // You **must** run via java or /usr/bin/java (which use the JAVA_HOME location). + // You **can** directly run via the full executable if it is the location installed in the JAVA_HOME env var + // UNLESS, the macos specific java flag `-Xdock:name` was/is used -- then it must be `/usr/bin/java`, even if it's + // a symlink to the same location as JAVA_HOME! if (Executor.IS_OS_MAC) { - try { - val binDir = File("/usr/bin") - - val javaParentDir = jvmExecutable?.parentFile?.canonicalFile - if (javaParentDir == binDir) { - jvmExecutable = File("/usr/bin/java") - } - } catch (ignored: IOException) { - } + return File("/usr/bin/java") } + var jvmExecutable = getJvmExecutable(System.getProperty("java.home")) if (jvmExecutable == null && Executor.IS_OS_WINDOWS) { // maybe java.library.path System Property has it. We use the first one that matches. System.getProperty("java.library.path").split(";").forEach { diff --git a/src/dorkbox/executor/SshExecOptions.kt b/src/dorkbox/executor/SshExecOptions.kt index 5833aa9..a33f17d 100644 --- a/src/dorkbox/executor/SshExecOptions.kt +++ b/src/dorkbox/executor/SshExecOptions.kt @@ -250,6 +250,9 @@ class SshExecOptions(val executor: Executor) { * * In the latter cases the process gets destroyed as well. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return exit code of the finished process. * * @throws IOException an error occurred when process was started or stopped. @@ -257,8 +260,9 @@ class SshExecOptions(val executor: Executor) { * @throws TimeoutException timeout set by [.timeout] was reached. * @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]). */ - suspend fun start(): SyncProcessResult { - return executor.start() + @JvmOverloads + suspend fun start(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult { + return executor.start(timeout, timeoutUnit) } /** @@ -270,13 +274,17 @@ class SshExecOptions(val executor: Executor) { * * Invoke [DeferredProcessResult.cancel] to destroy the process. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return [DeferredProcessResult] representing the process results (value/completed output-streams/etc) of the finished process. * * @throws IOException an error occurred when process was started. */ + @JvmOverloads @Throws(IOException::class) - fun startAsync(): DeferredProcessResult { - return executor.startAsync() + fun startAsync(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): DeferredProcessResult { + return executor.startAsync(timeout, timeoutUnit) } /** @@ -288,6 +296,9 @@ class SshExecOptions(val executor: Executor) { * * Calling [SyncProcessResult.output] will result in a non-blocking read of process output. * + * @param timeout If specified (non-zero), then if the process is running longer than this + * specified interval, a [TimeoutException] is thrown and the process is destroyed. + * * @return results of the finished process (exit code and output, if any) * * @throws IOException an error occurred when process was started or stopped. @@ -295,10 +306,11 @@ class SshExecOptions(val executor: Executor) { * @throws TimeoutException timeout set by [.timeout] was reached. * @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]). */ + @JvmOverloads @Throws(IOException::class, InterruptedException::class, TimeoutException::class, InvalidExitValueException::class) - fun startBlocking(): SyncProcessResult { + fun startBlocking(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult { return runBlocking { - start() + start(timeout, timeoutUnit) } } }