Tweaked timeout/timeoutUnit parameters (are now passed in via start() method calls). Fixed issues with launching JVM processes on macos

master
Robinson 2022-01-20 00:08:35 +01:00
parent 0472d53582
commit 955e4f4294
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
4 changed files with 147 additions and 93 deletions

View File

@ -213,12 +213,6 @@ open class Executor {
*/
private var allowedExitValues: Set<Int>? = 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()

View File

@ -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<String>): 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<String>()
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)
}

View File

@ -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 <java.home>/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 {

View File

@ -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)
}
}
}