1783 lines
64 KiB
Kotlin
1783 lines
64 KiB
Kotlin
/*
|
|
* Copyright 2023 dorkbox, llc
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/*
|
|
* Copyright (C) 2014 ZeroTurnaround <support@zeroturnaround.com>
|
|
*
|
|
* Contains fragments of code from Apache Commons Exec, rights owned
|
|
* by Apache Software Foundation (ASF).
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package dorkbox.executor
|
|
|
|
import dorkbox.executor.exceptions.InvalidExitValueException
|
|
import dorkbox.executor.exceptions.ProcessInitException
|
|
import dorkbox.executor.listener.*
|
|
import dorkbox.executor.processResults.NopProcessResult
|
|
import dorkbox.executor.processResults.ProcessResult
|
|
import dorkbox.executor.processResults.SyncProcessResult
|
|
import dorkbox.executor.stop.DestroyProcessStopper
|
|
import dorkbox.executor.stop.NopProcessStopper
|
|
import dorkbox.executor.stop.ProcessStopper
|
|
import dorkbox.executor.stream.CallerLoggerUtil
|
|
import dorkbox.executor.stream.IOStreamHandler
|
|
import dorkbox.executor.stream.NopPumpStreamHandler
|
|
import dorkbox.executor.stream.nopStreams.NopInputStream
|
|
import dorkbox.executor.stream.nopStreams.NopOutputStream
|
|
import dorkbox.executor.stream.slf4j.Slf4jStream
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
import kotlinx.coroutines.runBlocking
|
|
import org.slf4j.Logger
|
|
import org.slf4j.LoggerFactory
|
|
import java.io.*
|
|
import java.util.concurrent.*
|
|
|
|
|
|
|
|
/**
|
|
* Helper for executing a process.
|
|
*
|
|
*
|
|
* It's implemented as a wrapper of [ProcessBuilder] complementing it with additional features such as:
|
|
*
|
|
*
|
|
* * Handling process streams (copied from Commons Exec library).
|
|
* * Destroying process on VM exit (copied from Commons Exec library).
|
|
* * Checking process exit code.
|
|
* * Setting a timeout for running the process and automatically stopping it in case of timeout.
|
|
* * Either waiting for the process to finish ([.execute]) or returning a [Future] ([.start].
|
|
* * Reading the process output stream into a buffer ([.readOutput], [ProcessResult]).
|
|
*
|
|
*
|
|
*
|
|
* The default configuration for executing a process is following:
|
|
*
|
|
* * Process is not automatically destroyed on VM exit.
|
|
* * Error stream is redirected to its output stream. Use [.redirectErrorStream] to override it.
|
|
* * Output stream is pumped to a [NopOutputStream], Use [.streams], [.redirectOutput], or any of the `redirectOutputAs*` methods.to override it.
|
|
* * Any exit code is allowed. Use [.exitValues] to override it.
|
|
* * In case of timeout or cancellation [Process.destroy] is invoked.
|
|
*
|
|
*
|
|
* NOTE: If you are launching using the same classpath as before, and you set the main-classpath to be executed as a
|
|
* "single file source code" file via java11+, YOU CAN GET THE FOLLOWING ERROR.
|
|
*
|
|
* "error: class found on application class path .... "
|
|
*
|
|
* What this REALLY means is:
|
|
*
|
|
* "error: A compiled class <fully qualified class name> already exists on
|
|
* the application classpath and as a result the same class cannot be used
|
|
* as a source for launching single-file source code program".
|
|
*
|
|
* https://mail.openjdk.java.net/pipermail/jdk-dev/2018-June/001438.html
|
|
*
|
|
*
|
|
* @author Rein Raudjärv, Nathan Robinson
|
|
* @see ProcessResult
|
|
*/
|
|
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
|
open class Executor {
|
|
companion object {
|
|
/**
|
|
* Gets the version number.
|
|
*/
|
|
const val version = "3.14"
|
|
|
|
private val log = LoggerFactory.getLogger(Executor::class.java)!!
|
|
val IS_OS_WINDOWS: Boolean
|
|
val IS_OS_MAC: Boolean
|
|
|
|
private const val NORMAL_EXIT_VALUE = 0
|
|
|
|
const val DEFAULT_REDIRECT_ERROR_STREAM = true
|
|
|
|
internal val IO_DISPATCH = CoroutineScope(Dispatchers.IO)
|
|
private val LINE_SEPARATOR = System.getProperty("line.separator")
|
|
|
|
private val EXTRA_SPACE_REGEX = "\\s+".toRegex()
|
|
|
|
// what is the detected shell for this OS?
|
|
private var DEFAULT_SHELL: String? = null
|
|
|
|
init {
|
|
// cannot use any dependencies!
|
|
val osName = System.getProperty("os.name").lowercase()
|
|
IS_OS_WINDOWS = osName.startsWith("win")
|
|
IS_OS_MAC = osName.startsWith("mac") || osName.startsWith("darwin")
|
|
|
|
// Add this project to the updates system, which verifies this class + UUID + version information
|
|
dorkbox.updates.Updates.add(Executor::class.java, "03fcf3762a2b4f68b5e968aaf79f3a72", version)
|
|
}
|
|
|
|
/**
|
|
* Fixes the command line arguments on Windows by replacing empty arguments with `""`. Otherwise these arguments would be just skipped.
|
|
*
|
|
* See:
|
|
* http://bugs.java.com/view_bug.do?bug_id=7028124
|
|
* https://bugs.openjdk.java.net/browse/JDK-6518827
|
|
*/
|
|
internal fun fixArguments(command: Iterable<String>): MutableList<String> {
|
|
if (!IS_OS_WINDOWS) {
|
|
return command.toMutableList()
|
|
}
|
|
|
|
val result = mutableListOf<String>().apply{ addAll(command) }
|
|
val it: MutableListIterator<String> = result.listIterator()
|
|
|
|
while (it.hasNext()) {
|
|
if ("" == it.next()) {
|
|
it.set("\"\"")
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
|
|
/**
|
|
* Quickly run, then read the output of a process. This is very simple. For advanced usage, use the full API.
|
|
*/
|
|
fun run(command: Iterable<String>): String {
|
|
return Executor()
|
|
.command(command)
|
|
.enableRead()
|
|
.startBlocking()
|
|
.output.utf8()
|
|
}
|
|
|
|
/**
|
|
* Quickly run, then read the output of a process. This is very simple. For advanced usage, use the full API.
|
|
*/
|
|
fun run(executable: File, args: Iterable<String>): String {
|
|
return Executor()
|
|
.executable(executable)
|
|
.command(args)
|
|
.enableRead()
|
|
.startBlocking()
|
|
.output.utf8()
|
|
}
|
|
|
|
/**
|
|
* Quickly run, then read the output of a process. This is very simple. For advanced usage, use the full API.
|
|
*/
|
|
fun run(vararg command: String): String {
|
|
return Executor()
|
|
.command(listOf(*command))
|
|
.enableRead()
|
|
.startBlocking()
|
|
.output.utf8()
|
|
}
|
|
|
|
/**
|
|
* Quickly run, then read the output of a process. This is very simple. For advanced usage, use the full API.
|
|
*/
|
|
fun run(executable: File, vararg args: String): String {
|
|
return Executor()
|
|
.executable(executable)
|
|
.command(listOf(*args))
|
|
.enableRead()
|
|
.startBlocking()
|
|
.output.utf8()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process builder used by this executor.
|
|
*/
|
|
internal val builder = ProcessBuilder()
|
|
|
|
/**
|
|
* Environment variables which are added (removed in case of `null` values) to the process being started.
|
|
*/
|
|
val environment: MutableMap<String, String?> = LinkedHashMap()
|
|
|
|
|
|
/**
|
|
* Execute this command as JAVA, using the same JVM as the currently running JVM, as a forked process.
|
|
*/
|
|
private var jvmExecOptions: JvmExecOptions? = null
|
|
|
|
|
|
/**
|
|
* Execute this command on a remote host via SSH
|
|
*/
|
|
private var sshExecOptions: SshExecOptions? = null
|
|
|
|
/**
|
|
* Execute this command a shell command (bash/cmd/etc), instead of directly invoking the command as a forked process.
|
|
*/
|
|
private var executeAsShell: Boolean = false
|
|
|
|
/**
|
|
* Locations to be added to the path when running this executable.
|
|
*
|
|
* When paths are added to the path, [executeAsShell] will be set to true
|
|
*
|
|
* It is important to note, that only setting the path system environment variable WILL NOT affect the path of the running executable!
|
|
*/
|
|
private var pathsToPrepend: MutableList<String> = mutableListOf()
|
|
|
|
/**
|
|
* Set of accepted exit codes or `null` if all exit codes are allowed.
|
|
*/
|
|
private var allowedExitValues: Set<Int>? = null
|
|
|
|
/**
|
|
* Helper for stopping the process in case of timeout or cancellation.
|
|
*/
|
|
private var stopper: ProcessStopper = NopProcessStopper.INSTANCE
|
|
|
|
/**
|
|
* Process stream Handler (copied from Commons Exec library). If [NopPumpStreamHandler] then streams are not handled.
|
|
*/
|
|
private var streams: IOStreamHandler = NopPumpStreamHandler()
|
|
|
|
/**
|
|
* Timeout for closing process' standard streams. In case this timeout is reached we just log a warning but don't throw an error.
|
|
*/
|
|
private var closeTimeout: Long = 0L
|
|
private var closeTimeoutUnit: TimeUnit = TimeUnit.SECONDS
|
|
|
|
/**
|
|
* `true` if the process output should be read to a buffer and returned by [SyncProcessResult] or [DeferredProcessResult]
|
|
*/
|
|
private var readOutput = false
|
|
|
|
/**
|
|
* Sets the new process to inherit the current Java process source and destination I/O stream
|
|
*/
|
|
private var inheritIO = false
|
|
|
|
/**
|
|
* By default, there is only 1 thread for pumping I/O (as necessary) between processes. This setting enables
|
|
* 1 thread per I/O stream that will be pumped
|
|
*/
|
|
private var highPerformanceIO = false
|
|
|
|
/**
|
|
* Process event handlers.
|
|
*/
|
|
private val listeners = CompositeProcessListener()
|
|
|
|
/**
|
|
* Helper for logging messages about starting and waiting for the processes.
|
|
*
|
|
* see http://logback.qos.ch/manual/architecture.html for more info
|
|
* logger order goes (from lowest to highest) TRACE->DEBUG->INFO->WARN->ERROR->OFF
|
|
*/
|
|
private var logger: Logger? = null
|
|
|
|
/**
|
|
* Capture a snapshot of this process executor's main state.
|
|
*/
|
|
private val attributes: ProcessAttributes
|
|
// make a copy
|
|
get() {
|
|
val allowedExit = allowedExitValues
|
|
|
|
return if (allowedExit != null) {
|
|
ProcessAttributes(getCommand(),
|
|
getWorkingDirectory(),
|
|
LinkedHashMap(environment),
|
|
allowedExit.toSet())
|
|
} else {
|
|
ProcessAttributes(getCommand(), getWorkingDirectory(), LinkedHashMap(environment))
|
|
}
|
|
}
|
|
|
|
private val executingMessageParams: String
|
|
get() {
|
|
var result = builder.command().joinToString(separator = " ")
|
|
if (builder.directory() != null) {
|
|
result += " in " + builder.directory()
|
|
}
|
|
if (environment.isNotEmpty()) {
|
|
result += " with environment $environment"
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Creates new [Executor] instance.
|
|
*/
|
|
constructor()
|
|
|
|
/**
|
|
* Creates new [Executor] instance for the given program and its arguments.
|
|
*
|
|
* @param command The iterable containing the program and its arguments.
|
|
*/
|
|
constructor(command: Iterable<String>) {
|
|
command(command)
|
|
}
|
|
|
|
/**
|
|
* Creates new [Executor] instance for the given program and its arguments.
|
|
*
|
|
* @param command A string array containing the program and its arguments.
|
|
*/
|
|
constructor(vararg command: String) {
|
|
command(*command)
|
|
}
|
|
|
|
init {
|
|
// Run in case of any constructor
|
|
exitValueAny()
|
|
|
|
stopper(DestroyProcessStopper.INSTANCE)
|
|
redirectOutput(null)
|
|
redirectError(null)
|
|
destroyer(null)
|
|
|
|
redirectErrorStream(DEFAULT_REDIRECT_ERROR_STREAM)
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns this process executor's operating system program and arguments.
|
|
*
|
|
* The returned list is a copy.
|
|
*
|
|
* @return this process executor's program and its arguments (not `null`).
|
|
*/
|
|
open fun getCommand(): List<String> {
|
|
return ArrayList(builder.command())
|
|
}
|
|
|
|
/**
|
|
* Sets the program and it's arguments which are being executed.
|
|
*
|
|
* @param command The iterable containing the program and its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun command(command: Iterable<String>): Executor {
|
|
val list = command.map { it }
|
|
builder.command().addAll(fixArguments(list))
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the program and its arguments which are being executed.
|
|
*
|
|
* @param command A string array containing the program and its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun command(vararg command: String): Executor {
|
|
builder.command().addAll(fixArguments(listOf(*command)))
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the program and its arguments which are being executed.
|
|
*
|
|
* @param command A file that is the executable
|
|
* @param args A string array containing the arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun command(command: File, vararg args: String): Executor {
|
|
val combined = listOf(command.absolutePath, *args)
|
|
builder.command().addAll(fixArguments(combined))
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Splits string by spaces and passes it to [Executor.command]
|
|
*
|
|
* Note: this method does not handle whitespace escaping,
|
|
* `"mkdir new\ folder"` would be interpreted as `{"mkdir", "new\", "folder"}` command.
|
|
*
|
|
* @param commandWithArgs A string array containing the program and its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun commandSplit(commandWithArgs: String): Executor {
|
|
builder.command().addAll(commandWithArgs.split(EXTRA_SPACE_REGEX))
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the command which will be executed. This allows for the executable and arguments to be set separately.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun executable(exe: String): Executor {
|
|
builder.command().add(0, exe)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the command which will be executed. This allows for the executable and arguments to be set separately.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun executable(exe: File): Executor {
|
|
builder.command().add(0, exe.absolutePath)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @return The executable assigned to this command. If only arguments are set, this will return the first argument.
|
|
*/
|
|
fun getExecutable(): String {
|
|
val command = builder.command()
|
|
if (command.isEmpty()) {
|
|
return ""
|
|
}
|
|
|
|
return command[0]
|
|
}
|
|
|
|
/**
|
|
* Add arguments to an existing command, which will be executed.
|
|
*
|
|
* This does not replace commands, it adds to them
|
|
*
|
|
* @param arguments A string array containing the program and/or its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun addArg(vararg arguments: String): Executor {
|
|
val fixed = fixArguments(listOf(*arguments))
|
|
builder.command().addAll(fixed)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Add arguments to an existing command, which will be executed.
|
|
*
|
|
* This does not replace commands, it adds to them
|
|
*
|
|
* @param arguments A string array containing the program and/or its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun addArg(arguments: Iterable<String>): Executor {
|
|
val fixed = fixArguments(arguments)
|
|
builder.command().addAll(fixed)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @return the list of arguments assigned to the executable
|
|
*/
|
|
fun getArgs(): List<String> {
|
|
val command = builder.command()
|
|
|
|
return if (command.size <= 1) {
|
|
listOf()
|
|
} else {
|
|
command.subList(1, command.size)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns this process executor's working directory.
|
|
*
|
|
*
|
|
* Subprocesses subsequently started by this object will use this as their working directory.
|
|
*
|
|
* The returned value may be `null` -- this means to use the working directory of the current Java process, usually the
|
|
* directory named by the system property `user.dir` as the working directory of the child process.
|
|
*
|
|
* @return this process executor's working directory
|
|
*/
|
|
fun getWorkingDirectory(): File? {
|
|
return builder.directory()
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets this working directory for the process being executed.
|
|
* The argument may be `null` -- this means to use the
|
|
* working directory of the current Java process, usually the
|
|
* directory named by the system property `user.dir`,
|
|
* as the working directory of the child process.
|
|
*
|
|
* @param directory The new working directory
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun workingDirectory(directory: File?): Executor {
|
|
builder.directory(directory)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets this working directory for the process being executed.
|
|
* The argument may be `null` -- this means to use the
|
|
* working directory of the current Java process, usually the
|
|
* directory named by the system property `user.dir`,
|
|
* as the working directory of the child process.
|
|
*
|
|
* @param directory The new working directory
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun workingDirectory(directory: String?): Executor {
|
|
if (directory != null) {
|
|
builder.directory(File(directory))
|
|
} else {
|
|
builder.directory(directory)
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds additional environment variables for the process being executed.
|
|
*
|
|
* @param env environment variables added to the process being executed.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun environment(env: Map<String, String?>): Executor {
|
|
environment.putAll(env)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a single additional environment variable for the process being executed.
|
|
*
|
|
* @param name name of the environment variable added to the process being executed.
|
|
* @param value value of the environment variable added to the process being executed.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun environment(name: String, value: String?): Executor {
|
|
environment[name] = value
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Copies the current system environment into the executable's environment.
|
|
*
|
|
* Normally the system environment is not used for execution
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun useSystemEnvironment(): Executor {
|
|
environment.putAll(System.getenv())
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds the specified path (or paths) to the command getting executed. This will ALSO enable [executeAsShell], as the only way
|
|
* to add a path for the executed process, is to run as a subshell/fork.
|
|
*/
|
|
fun addPath(vararg pathsToAdd: String): Executor {
|
|
pathsToPrepend.addAll(pathsToAdd)
|
|
executeAsShell = true
|
|
return this
|
|
}
|
|
|
|
|
|
/**
|
|
* It is possible, however unlikely (and never casually noticed), that the
|
|
* PATH environment variable can be "PATH", "Path", or "path" -- all depending
|
|
* on what is set.
|
|
*
|
|
* This will search for all three cases in the following precedence: PATH -> Path -> path
|
|
*
|
|
* @return the system path environment variable
|
|
*/
|
|
fun getSystemPath(): String {
|
|
val systemEnv = System.getenv()
|
|
|
|
return systemEnv["PATH"] ?: systemEnv["Path"] ?: systemEnv["path"] ?: ""
|
|
}
|
|
|
|
/**
|
|
* Sets this process executor's `redirectErrorStream` property.
|
|
*
|
|
*
|
|
* If this property is `true`, then any error output generated by subprocesses will be merged with the standard output.
|
|
* This makes it easier to correlate error messages with the corresponding output.
|
|
* The initial value is `true`.
|
|
*
|
|
* @param redirectErrorStream The new property value
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorStream(redirectErrorStream: Boolean): Executor {
|
|
builder.redirectErrorStream(redirectErrorStream)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Allows any exit value for the process being executed.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun exitValueAny(): Executor {
|
|
allowedExitValues = null
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Allows only `0` as the exit value for the process being executed.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun exitValueNormal(): Executor {
|
|
return exitValues(NORMAL_EXIT_VALUE)
|
|
}
|
|
|
|
/**
|
|
* Sets the allowed exit value for the process being executed.
|
|
*
|
|
* @param exitValue single exit value or `null` if all exit values are allowed.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun exitValue(exitValue: Int): Executor {
|
|
return exitValues(exitValue)
|
|
}
|
|
|
|
/**
|
|
* Sets the allowed exit values for the process being executed.
|
|
*
|
|
* @param exitValues set of exit values or `null` if all exit values are allowed.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun exitValues(vararg exitValues: Int): Executor {
|
|
allowedExitValues = exitValues.toSet()
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the helper for stopping the process in case of timeout or cancellation.
|
|
*
|
|
* By default [DestroyProcessStopper] is used which just invokes [Process.destroy].
|
|
*
|
|
* @param stopper helper for stopping the process (`null` means [NopProcessStopper] - process is not stopped).
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun stopper(stopper: ProcessStopper?): Executor {
|
|
if (stopper == null) {
|
|
this.stopper = NopProcessStopper.INSTANCE
|
|
} else {
|
|
this.stopper = stopper
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @return current stream handler for the process being executed.
|
|
*/
|
|
fun streams(): IOStreamHandler {
|
|
return streams
|
|
}
|
|
|
|
/**
|
|
* Sets a stream handler for the process being executed.
|
|
*
|
|
* This will overwrite any stream redirection that was previously set to use the provided handler.
|
|
*
|
|
* @param streams the stream handler
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun streams(streams: IOStreamHandler): Executor {
|
|
this.streams = streams
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets a timeout for closing standard streams of the process being executed.
|
|
* When this timeout is reached we log a warning but consider that the process has finished.
|
|
* We also flush the streams so that all output read so far is available.
|
|
*
|
|
*
|
|
* This can be used on Windows in case a process exits quickly but closing the streams blocks forever.
|
|
*
|
|
*
|
|
*
|
|
* Closing timeout must fit into the general execution timeout (see [.timeout]).
|
|
* By default there's no closing timeout.
|
|
*
|
|
* @param timeout timeout for closing streams of a process.
|
|
* @param unit the time unit of the timeout
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun closeTimeout(timeout: Long, unit: TimeUnit): Executor {
|
|
closeTimeout = timeout
|
|
closeTimeoutUnit = unit
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Explicitly permit reading the output from this process.
|
|
*
|
|
* If the process is started via [Executor.startAsync], then reading the process output will be **BLOCKING**.
|
|
*
|
|
* If the process is started via [Executor.start], the reading the process output will occur once the process has
|
|
* exited (and it doesn't matter if reading the output is blocking or not).
|
|
*
|
|
* If you set the output stream manually - you can read the output as you choose. This method exists to make it easier to read the
|
|
* output.
|
|
*
|
|
* By default, the process output is discarded
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun enableRead(): Executor {
|
|
this.readOutput = true
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the new process to inherit the current Java process source and destination I/O stream
|
|
*/
|
|
fun inheritIO(): Executor {
|
|
this.inheritIO = true
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the input stream to redirect to the process' input stream.
|
|
*
|
|
* If this method is invoked multiple times each call overwrites the previous.
|
|
*
|
|
* @param input input stream that will be written to the process input stream (`null` means nothing will be written to the process input stream).
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectInput(input: InputStream?): Executor {
|
|
val inputStream = when (input) {
|
|
null -> {
|
|
NopInputStream.INPUT_STREAM
|
|
}
|
|
else -> {
|
|
input
|
|
}
|
|
}
|
|
|
|
// Only set the input stream handler, preserve the same output and error stream handler
|
|
streams = streams.setInputStream(inputStream)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Redirects the process' output stream to given output stream.
|
|
*
|
|
* If this method is invoked multiple times each call overwrites the previous.
|
|
* Use [.redirectOutputAlsoTo] if you want to redirect the output to multiple streams.
|
|
*
|
|
* @param output output stream where the process output is redirected to (`null` means [NopOutputStream] which acts like a `/dev/null`).
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
@JvmOverloads
|
|
fun redirectOutput(output: OutputStream? = null): Executor {
|
|
var outputStream = output
|
|
if (outputStream == null) {
|
|
outputStream = NopOutputStream.OUTPUT_STREAM
|
|
}
|
|
|
|
// Only set the output stream handler, preserve the same error stream handler
|
|
streams = streams.setOutputStream(outputStream)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Redirects the process' error stream to given output stream.
|
|
*
|
|
* If this method is invoked multiple times each call overwrites the previous.
|
|
* Use [.redirectErrorAlsoTo] if you want to redirect the error to multiple streams.
|
|
*
|
|
*
|
|
* Calling this method automatically disables merging the process error stream to its output stream.
|
|
*
|
|
*
|
|
* @param output output stream where the process error is redirected to (`null` means [NopOutputStream] which acts like `/dev/null`).
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectError(output: OutputStream?): Executor {
|
|
var outputStream = output
|
|
if (outputStream == null) {
|
|
outputStream = NopOutputStream.OUTPUT_STREAM
|
|
}
|
|
|
|
// Only set the error stream handler, preserve the same output stream handler
|
|
streams = streams.setErrorStream(outputStream)
|
|
redirectErrorStream(false)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Redirects the process' output stream also to a given output stream.
|
|
*
|
|
* This method can be used to redirect output to multiple streams.
|
|
*
|
|
*
|
|
* @param output the stream to redirect this output to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAlsoTo(output: OutputStream): Executor {
|
|
streams = streams.teeOutputStream(output)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Redirects the process' error stream also to a given output stream.
|
|
*
|
|
* This method can be used to redirect error to multiple streams.
|
|
*
|
|
* Calling this method automatically disables merging the process error stream to its output stream.
|
|
*
|
|
*
|
|
* @param output the output stream to redirect the error stream to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAlsoTo(output: OutputStream): Executor {
|
|
streams = streams.teeErrorStream(output)
|
|
redirectErrorStream(false)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `trace` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsTrace(): Executor {
|
|
return redirectOutput(Slf4jStream.asDebug(LoggerFactory.getLogger(
|
|
CallerLoggerUtil.getName(null, 1))))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `debug` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsDebug(): Executor {
|
|
return redirectOutput(Slf4jStream.asDebug(LoggerFactory.getLogger(
|
|
CallerLoggerUtil.getName(null, 1))))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `info` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsInfo(): Executor {
|
|
return redirectOutput(Slf4jStream.asInfo(LoggerFactory.getLogger(
|
|
CallerLoggerUtil.getName(null, 1))))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `warn` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsWarn(): Executor {
|
|
return redirectOutput(Slf4jStream.asWarn(LoggerFactory.getLogger(
|
|
CallerLoggerUtil.getName(null, 1))))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `error` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsError(): Executor {
|
|
return redirectOutput(Slf4jStream.asDebug(LoggerFactory.getLogger(
|
|
CallerLoggerUtil.getName(null, 1))))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `trace` level.
|
|
*
|
|
* @param log the logger to output the message to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsTrace(log: Logger): Executor {
|
|
return redirectOutput(Slf4jStream.asDebug(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `debug` level.
|
|
*
|
|
* @param log the logger to output the message to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsDebug(log: Logger): Executor {
|
|
return redirectOutput(Slf4jStream.asDebug(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `info` level.
|
|
*
|
|
* @param log the logger to output the message to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsInfo(log: Logger): Executor {
|
|
return redirectOutput(Slf4jStream.asInfo(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `warn` level.
|
|
*
|
|
* @param log the logger to output the message to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsWarn(log: Logger): Executor {
|
|
return redirectOutput(Slf4jStream.asWarn(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' output to a given [Logger] with `error` level.
|
|
*
|
|
* @param log the logger to output the message to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectOutputAsError(log: Logger): Executor {
|
|
return redirectOutput(Slf4jStream.asDebug(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `trace` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsTrace(): Executor {
|
|
return redirectError(Slf4jStream.asTrace(LoggerFactory.getLogger(
|
|
CallerLoggerUtil.getName(null, 1))))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `debug` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsDebug(): Executor {
|
|
return redirectError(Slf4jStream.asDebug(LoggerFactory.getLogger(
|
|
CallerLoggerUtil.getName(null, 1))))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `info` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsInfo(): Executor {
|
|
return redirectError(Slf4jStream.asInfo(LoggerFactory.getLogger(
|
|
CallerLoggerUtil.getName(null, 1))))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `warn` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsWarn(): Executor {
|
|
return redirectError(Slf4jStream.asWarn(LoggerFactory.getLogger(
|
|
CallerLoggerUtil.getName(null, 1))))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `error` level of the calling class logger.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsError(): Executor {
|
|
return redirectError(Slf4jStream.asError(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `trace` level.
|
|
*
|
|
* @param log the logger to process output to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsTrace(log: Logger): Executor {
|
|
return redirectError(Slf4jStream.asTrace(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `debug` level.
|
|
*
|
|
* @param log the logger to process the error to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsDebug(log: Logger): Executor {
|
|
return redirectError(Slf4jStream.asDebug(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `info` level.
|
|
*
|
|
* @param log the logger to process output to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsInfo(log: Logger): Executor {
|
|
return redirectError(Slf4jStream.asInfo(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `warn` level.
|
|
*
|
|
* @param log the logger to process output to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsWarn(log: Logger): Executor {
|
|
return redirectError(Slf4jStream.asWarn(log))
|
|
}
|
|
|
|
/**
|
|
* Logs the process' error to a given [Logger] with `error` level.
|
|
*
|
|
* @param log the logger to process output to
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun redirectErrorAsError(log: Logger): Executor {
|
|
return redirectError(Slf4jStream.asError(log))
|
|
}
|
|
|
|
/**
|
|
* Adds a process destroyer to be notified when the process starts and stops.
|
|
*
|
|
* @param destroyer helper for destroying all processes on certain event such as VM exit (not `null`).
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun addDestroyer(destroyer: ProcessDestroyer): Executor {
|
|
return addListener(DestroyerListenerAdapter(destroyer))
|
|
}
|
|
|
|
/**
|
|
* Sets the process destroyer to be notified when the process starts and stops.
|
|
*
|
|
* This methods always removes any other [ProcessDestroyer] registered. Use [.addDestroyer] to keep the existing ones.
|
|
*
|
|
* @param destroyer helper for destroying all processes on certain event such as VM exit (maybe `null`).
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun destroyer(destroyer: ProcessDestroyer?): Executor {
|
|
removeListeners(DestroyerListenerAdapter::class.java)
|
|
if (destroyer != null) {
|
|
addListener(DestroyerListenerAdapter(destroyer))
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the started process to be destroyed on VM exit (shutdown hooks are executed).
|
|
* If this VM gets killed the started process may not get destroyed.
|
|
*
|
|
* To undo this command call `destroyer(null)`.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun destroyOnExit(): Executor {
|
|
return destroyer(ShutdownHookProcessDestroyer.INSTANCE)
|
|
}
|
|
|
|
/**
|
|
* Unregister all existing process event handlers and register new one.
|
|
*
|
|
* @param listener process event handler to be set (maybe `null`).
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun listener(listener: ProcessListener?): Executor {
|
|
clearListeners()
|
|
|
|
listener?.let { addListener(it) }
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Register new process event handler.
|
|
*
|
|
* @param listener process event handler to be added.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun addListener(listener: ProcessListener): Executor {
|
|
listeners.add(listener)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Unregister existing process event handler.
|
|
*
|
|
* @param listener process event handler to be removed.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun removeListener(listener: ProcessListener): Executor {
|
|
listeners.remove(listener)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Unregister existing process event handlers of given type or its sub-types.
|
|
*
|
|
* @param listenerType process event handler type.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun removeListeners(listenerType: Class<out ProcessListener>): Executor {
|
|
listeners.removeAll(listenerType)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Unregister all existing process event handlers.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun clearListeners(): Executor {
|
|
listeners.clear()
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Check the exit value of given process result. This can be used by unit tests.
|
|
*
|
|
* @param result process result which maybe constructed by a unit test.
|
|
*
|
|
* @throws InvalidExitValueException if the given exit value was rejected.
|
|
*/
|
|
@Throws(InvalidExitValueException::class)
|
|
fun checkExitValue(result: ProcessResult) {
|
|
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.
|
|
*
|
|
* By default, there is only 1 thread.
|
|
*
|
|
* After calling this, there will be 1 thread per I/O stream to be pumped
|
|
*/
|
|
fun highPerformanceIO(): Executor {
|
|
this.highPerformanceIO = true
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
@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!!
|
|
}
|
|
|
|
/**
|
|
* Execute this command on a remote host via SSH.
|
|
*
|
|
* This should be used last, as the only thing possible from here is [start] and [startAsync] variants
|
|
*/
|
|
fun asSshProcess(): SshExecOptions {
|
|
sshExecOptions = SshExecOptions(this)
|
|
return sshExecOptions!!
|
|
}
|
|
|
|
/**
|
|
* Start the sub process, as a shell command (bash/cmd/etc), instead of directly invoking the command as a forked process.
|
|
*
|
|
* This method does not wait until the process exits.
|
|
*
|
|
* The value passed to [.timeout] is ignored. Use [DeferredProcessResult.await] to wait for the process to finish.
|
|
*
|
|
* Invoke [DeferredProcessResult.cancel] to destroy the process.
|
|
*
|
|
* @return DeferredProcessResult representing the exit value of the finished process.
|
|
*
|
|
* @throws IOException an error occurred when process was started.
|
|
*/
|
|
@JvmOverloads
|
|
@Throws(IOException::class)
|
|
fun startAsShellAsync(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): DeferredProcessResult {
|
|
executeAsShell = true
|
|
return startAsync(timeout, timeoutUnit)
|
|
}
|
|
|
|
/**
|
|
* Start the sub process in a new coroutine. The calling thread will continue execution. This method does not wait until the process exits.
|
|
*
|
|
* Calling [SyncProcessResult.output] will result in a blocking read of process output.
|
|
*
|
|
* The value passed to [.timeout] is ignored. Use [DeferredProcessResult.await] to wait for the process to finish.
|
|
*
|
|
* 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(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): DeferredProcessResult {
|
|
return prepareProcess(timeout, timeoutUnit, true)
|
|
}
|
|
|
|
/**
|
|
* The calling thread will immediately execute the sub process as a shell command (bash/cmd/etc).
|
|
*
|
|
* When trying to close the input streams, the calling thread may suspend.
|
|
*
|
|
* Waits until:
|
|
* - the process stops
|
|
* - a timeout occurs and the caller thread gets interrupted. (In this case the process gets destroyed as well.)
|
|
*
|
|
* 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.
|
|
* @throws InterruptedException this thread was interrupted.
|
|
* @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(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult {
|
|
executeAsShell = true
|
|
|
|
@Suppress("BlockingMethodInNonBlockingContext")
|
|
return start(timeout, timeoutUnit)
|
|
}
|
|
|
|
/**
|
|
* The calling thread will immediately execute the sub process as a shell command (bash/cmd/etc).
|
|
*
|
|
* When trying to close the input streams, the calling thread may block.
|
|
*
|
|
* Waits until:
|
|
* - the process stops
|
|
* - a timeout occurs and the caller thread gets interrupted. (In this case the process gets destroyed as well.)
|
|
*
|
|
* 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.
|
|
* @throws InterruptedException this thread was interrupted.
|
|
* @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(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult {
|
|
return runBlocking {
|
|
startAsShell(timeout, timeoutUnit)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* The calling thread will immediately execute the sub process. When trying to close the input streams, the calling thread may suspend.
|
|
*
|
|
* Waits until:
|
|
* - the process stops
|
|
* - a timeout occurs and the caller thread gets interrupted. (In this case the process gets destroyed as well.)
|
|
*
|
|
* 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.
|
|
* @throws InterruptedException this thread was interrupted.
|
|
* @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(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(timeout, timeoutUnit, false).await(timeout, timeoutUnit)
|
|
}
|
|
|
|
/**
|
|
* The calling thread will immediately execute the sub process. When trying to close the input streams, the calling thread may block.
|
|
*
|
|
* Waits until:
|
|
* - the process stops
|
|
* - a timeout occurs and the caller thread gets interrupted. (In this case the process gets destroyed as well.)
|
|
*
|
|
* 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.
|
|
* @throws InterruptedException this thread was interrupted.
|
|
* @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(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult {
|
|
return runBlocking {
|
|
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).
|
|
*/
|
|
@OptIn(ExperimentalCoroutinesApi::class)
|
|
private fun prepareProcess(timeout: Long, timeoutUnit: TimeUnit, asyncProcessStart: Boolean): DeferredProcessResult {
|
|
// establish incompatible options
|
|
check(!(inheritIO && highPerformanceIO)) { "inheritIO & highPerformanceIO cannot be both set at the same time, they cancel each-other out" }
|
|
|
|
|
|
|
|
// Invoke listeners - they can modify this executor
|
|
listeners.beforeStart(this)
|
|
val command = builder.command()
|
|
|
|
if (jvmExecOptions == null) {
|
|
// when running "normally", we must check if our commands has been set, otherwise... whoopsie!
|
|
check(command.isNotEmpty()) { "Command has not been set." }
|
|
}
|
|
|
|
// configure the environment
|
|
val hasPathToPrepend = pathsToPrepend.isNotEmpty()
|
|
val prependPaths = if (hasPathToPrepend) pathsToPrepend.joinToString(separator = File.pathSeparator) else ""
|
|
|
|
if (environment.isNotEmpty() || hasPathToPrepend) {
|
|
// Take care of Windows environments that may contain "Path" OR "PATH" or "path" - both possibly existing, but not necessarily
|
|
val systemEnv = System.getenv()
|
|
val systemUsesAllCapsPath = systemEnv["PATH"] != null
|
|
val systemUsesMixCasePath = systemEnv["Path"] != null
|
|
val systemUsesLowCasePath = systemEnv["path"] != null
|
|
|
|
val correctPathName = when {
|
|
systemUsesAllCapsPath -> "PATH"
|
|
systemUsesMixCasePath -> "Path"
|
|
else -> "path"
|
|
}
|
|
|
|
if (systemUsesAllCapsPath || systemUsesMixCasePath || systemUsesLowCasePath) {
|
|
// we have to make sure we use the same for our locally set env vars!
|
|
val localEnvUsesAllCapsPath = environment.remove("PATH") ?: ""
|
|
val localEnvUsesMixCasePath = environment.remove("Path") ?: ""
|
|
val localEnvUsesLowCasePath = environment.remove("path") ?: ""
|
|
|
|
if (localEnvUsesAllCapsPath.isNotEmpty() || localEnvUsesMixCasePath.isNotEmpty() || localEnvUsesLowCasePath.isNotEmpty()) {
|
|
// we jam these all together, if possible
|
|
var path = ""
|
|
if (localEnvUsesAllCapsPath.isNotEmpty()) {
|
|
path += localEnvUsesAllCapsPath
|
|
}
|
|
|
|
if (localEnvUsesMixCasePath.isNotEmpty()) {
|
|
if (path.isNotEmpty()) {
|
|
path += File.pathSeparator
|
|
}
|
|
path += localEnvUsesMixCasePath
|
|
}
|
|
|
|
if (localEnvUsesLowCasePath.isNotEmpty()) {
|
|
if (path.isNotEmpty()) {
|
|
path += File.pathSeparator
|
|
}
|
|
path += localEnvUsesLowCasePath
|
|
}
|
|
|
|
// we have to prepend our own paths to the environment variables.
|
|
if (hasPathToPrepend) {
|
|
path = prependPaths + File.pathSeparator + path
|
|
}
|
|
|
|
environment[correctPathName] = path
|
|
} else {
|
|
// who knows WHAT is going on. Just slap on the path we want to add
|
|
environment[correctPathName] = prependPaths
|
|
}
|
|
} else if (hasPathToPrepend) {
|
|
// who knows WHAT is going on. Just slap on the path we want to add
|
|
environment[correctPathName] = prependPaths
|
|
}
|
|
|
|
val env = builder.environment()
|
|
environment.forEach { (key, value) ->
|
|
if (value == null) {
|
|
env.remove(key)
|
|
} else {
|
|
env[key] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (jvmExecOptions != null) {
|
|
val jvm = jvmExecOptions!!
|
|
|
|
// this means that we want to launch this as a forked JAVA process, using the same JVM as we were started with
|
|
// the builder commands are seen as options to the JAVA process.
|
|
jvm.configure()
|
|
}
|
|
|
|
if (executeAsShell) {
|
|
// NOTE: Also run when we prepend paths to the executable, as this is the only way for it to work.
|
|
// should we execute the command as a "shell command", or should we fork the process and run it directly?
|
|
|
|
// if we are executing as a "shell", ALL THE PARAMETERS ARE A SINGLE PARAMETER!
|
|
// dump all the parameters, and "start over" -- adding them as a single parameter.
|
|
|
|
val shellCommand = command.joinToString(separator = " ")
|
|
command.clear()
|
|
|
|
|
|
if (IS_OS_WINDOWS) {
|
|
// add our commands to the internal command list
|
|
command.addAll(listOf("cmd", "/c"))
|
|
} else {
|
|
// do a quick invocation of the "echo $SHELL" command, and save that for all future runs of the executor,
|
|
// to calculate what the shell is (and cache it)
|
|
|
|
// it might be in the ENV var, for example: SHELL=/bin/zsh
|
|
if (DEFAULT_SHELL == null) {
|
|
val shell = System.getenv()["SHELL"]
|
|
if (shell != null && File(shell).canExecute()) {
|
|
DEFAULT_SHELL = shell
|
|
}
|
|
}
|
|
|
|
// IF THIS DOES NOT WORK, then parse out the *potential* list of shells below.
|
|
if (DEFAULT_SHELL == null) {
|
|
try {
|
|
// we don't support java 6, so the proper method of scanner + auto-closable via java7 is appropriate.
|
|
val result = Runtime.getRuntime().exec(arrayOf("sh", "-c", "echo \$SHELL")).inputStream.use { inputStream ->
|
|
java.util.Scanner(inputStream).useDelimiter("\\A").use { s ->
|
|
if (s.hasNext()) {
|
|
s.next().trim()
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result != null) {
|
|
if (result.isNotBlank() && File(result).canExecute()) {
|
|
DEFAULT_SHELL = result
|
|
}
|
|
}
|
|
} catch (ignored: IOException) {
|
|
}
|
|
}
|
|
|
|
// fall-back
|
|
if (DEFAULT_SHELL == null) {
|
|
arrayOf("/bin/bash", "/usr/bin/bash",
|
|
"/bin/pfbash", "/usr/bin/pfbash",
|
|
"/bin/csh", "/usr/bin/csh",
|
|
"/bin/pfcsh", "/usr/bin/pfcsh",
|
|
"/bin/jsh", "/usr/bin/jsh",
|
|
"/bin/ksh", "/usr/bin/ksh",
|
|
"/bin/pfksh", "/usr/bin/pfksh",
|
|
"/bin/ksh93", "/usr/bin/ksh93",
|
|
"/bin/pfksh93", "/usr/bin/pfksh93",
|
|
"/bin/pfsh", "/usr/bin/pfsh",
|
|
"/bin/tcsh", "/usr/bin/tcsh",
|
|
"/bin/pftcsh", "/usr/bin/pftcsh",
|
|
"/usr/xpg4/bin/sh", "/usr/xp4/bin/pfsh",
|
|
"/bin/zsh", "/usr/bin/zsh",
|
|
"/bin/pfzsh", "/usr/bin/pfzsh",
|
|
"/bin/sh", "/usr/bin/sh").forEach { shell ->
|
|
|
|
if (File(shell).canExecute()) {
|
|
DEFAULT_SHELL = shell
|
|
return@forEach
|
|
}
|
|
}
|
|
}
|
|
|
|
if (DEFAULT_SHELL == null) {
|
|
throw IllegalStateException("Unable to determine the default shell for the linux/unix environment.")
|
|
}
|
|
|
|
// *nix
|
|
// add our commands to the internal command list
|
|
command.addAll(listOf(DEFAULT_SHELL!!, "-c"))
|
|
}
|
|
|
|
// now add our original command + parameters.
|
|
command.add(shellCommand)
|
|
|
|
|
|
if (IS_OS_MAC && !environment.containsKey("SOFTWARE")) {
|
|
// Enable LANG overrides
|
|
environment["SOFTWARE"] = ""
|
|
}
|
|
|
|
// Make sure all shell calls are the machine language default (UTF8) THIS CAN BE OVERRIDDEN
|
|
if (!environment.containsKey("LANG")) {
|
|
// "export LANG=en_US.UTF-8"
|
|
// the value of "C" makes this the machine language default
|
|
environment["LANG"] = "C"
|
|
}
|
|
}
|
|
|
|
if (inheritIO) {
|
|
// Sets the child process to inherit the current Java process source and destination I/O stream
|
|
builder.inheritIO()
|
|
}
|
|
|
|
|
|
val attributes = attributes // this makes a copy of the attributes!
|
|
val newListeners = listeners.clone()
|
|
|
|
|
|
// are we interested in process output?? THIS MUST BE THE VERY LAST THING!
|
|
// we can read the output IN ADDITION TO having our own output stream for the process.
|
|
if (!readOutput && executeAsShell && streams.out is NopOutputStream) {
|
|
// NOTE: this ONLY works if we are running as a shell!
|
|
|
|
// should we redirect our output to null? we are not interested in the program output
|
|
if (IS_OS_WINDOWS) {
|
|
// >NUL on windows
|
|
command.add("2>&1>")
|
|
command.add("nul")
|
|
|
|
} else {
|
|
// we will "pipe" it to /dev/null on *nix
|
|
command.add(">/dev/null")
|
|
command.add("2>&1")
|
|
}
|
|
}
|
|
|
|
|
|
|
|
val nativeProcess = try {
|
|
val timeoutInfo = if (timeout > 0L) {
|
|
"(timeout: $timeout $timeoutUnit) "
|
|
} else {
|
|
""
|
|
}
|
|
|
|
val executeText = if (executeAsShell) {
|
|
"Executing as shell $timeoutInfo"
|
|
} else {
|
|
"Executing $timeoutInfo"
|
|
}
|
|
|
|
if (sshExecOptions != null) {
|
|
LogHelper.logAtLowestLevel(logger, "$executeText on ${sshExecOptions!!.info()} $executingMessageParams")
|
|
sshExecOptions!!.startProcess(timeout, timeoutUnit, logger)
|
|
} else {
|
|
LogHelper.logAtLowestLevel(logger, "$executeText $executingMessageParams")
|
|
builder.start()
|
|
}
|
|
} catch (e: Exception) {
|
|
val errorMessage = if (sshExecOptions != null) {
|
|
"Could not execute on ${sshExecOptions!!.info()} $executingMessageParams"
|
|
} else {
|
|
"Could not execute $executingMessageParams"
|
|
}
|
|
|
|
throw ProcessInitException.newInstance(errorMessage, e) ?: IOException(errorMessage, e)
|
|
}
|
|
|
|
// we might reassign the streams if they are to be read
|
|
var streams: IOStreamHandler
|
|
|
|
/*
|
|
* Defines how we create the process results (sync, async, no-op) based on what type of startup we have. This
|
|
* unit function saves us from complicated code paths.
|
|
*
|
|
* This is only called once (either when the process is complete, or when there is an error)
|
|
*/
|
|
val createProcessResults: (Long, Int) -> SyncProcessResult
|
|
|
|
// this is ONLY called if there is an exception while waiting for the process to complete.
|
|
val errorMessageHandler: (StringBuilder) -> Unit
|
|
|
|
if (!readOutput) {
|
|
// if we don't read the output, then we don't care about blocking/non-blocking reads
|
|
|
|
streams = this.streams // note: this leaves the ORIGINAL streams alone!
|
|
|
|
createProcessResults = { pid, exitValue ->
|
|
// no byte output
|
|
NopProcessResult(pid, exitValue)
|
|
}
|
|
|
|
errorMessageHandler = { sb ->
|
|
// necessary because we throw an exception if we try to get the output from a NopProcessResult
|
|
DeferredProcessResult.addExceptionMessageSuffix(attributes, sb, "")
|
|
}
|
|
} else {
|
|
streams = this.streams
|
|
|
|
if (asyncProcessStart) {
|
|
// This means we want blocking reads from the output since we can read the output while the process is running
|
|
// make the streams support async reads
|
|
streams = streams.asyncMode() // note: this leaves the ORIGINAL streams alone!
|
|
}
|
|
|
|
// this means we want a SINGLE read from the output, when the process has finished running
|
|
val out = ByteArrayOutputStream()
|
|
|
|
// tee the output, this will only happen if the output stream has previously been set
|
|
streams = streams.teeOutputStream(out) // note: this leaves the ORIGINAL streams alone!
|
|
|
|
createProcessResults = { pid, exitValue ->
|
|
// create output based on the ByteArrayOutputStream. This is read when the process is completed
|
|
SyncProcessResult(pid, exitValue, out.toByteArray())
|
|
}
|
|
|
|
errorMessageHandler = { sb ->
|
|
val results = SyncProcessResult(0, 0, out.toByteArray()) // don't care about the pid or exit value
|
|
DeferredProcessResult.addExceptionMessageSuffix(attributes, sb, results.output.string())
|
|
}
|
|
}
|
|
|
|
// setup and start the stream processing
|
|
val separateErrorStream = !builder.redirectErrorStream()
|
|
streams.start(nativeProcess, separateErrorStream, highPerformanceIO)
|
|
|
|
// Invoke listeners - changing this executor does not affect the started process
|
|
newListeners.afterStart(nativeProcess, this)
|
|
|
|
val logger = logger
|
|
val params = Params(processAttributes = attributes,
|
|
stopper = stopper,
|
|
listener = newListeners,
|
|
streams = streams,
|
|
logger = logger,
|
|
errorMessageHandler = errorMessageHandler,
|
|
closeTimeout = closeTimeout,
|
|
closeTimeoutUnit = closeTimeoutUnit,
|
|
asyncProcessStart = asyncProcessStart)
|
|
|
|
val deferred = DeferredProcessResult(nativeProcess, params, createProcessResults)
|
|
val processPid = deferred.pid
|
|
if (processPid == PidHelper.INVALID) {
|
|
LogHelper.logAtLowestLevel(logger, "Started process")
|
|
} else {
|
|
LogHelper.logAtLowestLevel(logger, "Started process [pid={}]", processPid)
|
|
}
|
|
|
|
deferred.start()
|
|
|
|
return deferred
|
|
}
|
|
}
|