net.dankito.utils.process.CommandExecutor.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-utils Show documentation
Show all versions of java-utils Show documentation
Some basic utils needed in many projects
package net.dankito.utils.process
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import org.slf4j.LoggerFactory
import java.io.InputStream
open class CommandExecutor : ICommandExecutor {
companion object {
private val log = LoggerFactory.getLogger(CommandExecutor::class.java)
}
/**
* Do not call this for commands that have a large standard or error output!
*
* This method does not read process' standard and error output on extra threads like [executeCommand] does.
*
* Standard and error output are being read on same thread after process ended. Therefore if standard and error
* InputStream reader's buffer is not large enough, the call to Process.waitFor() will hang forever.
*
* So it has slightly better performance than [executeCommand] has as it doesn't create two new threads but at the
* cost that app may hangs forever.
*/
override fun executeCommandWithLittleOutput(vararg arguments: String): ExecuteCommandResult {
return executeCommandWithLittleOutput(CommandConfig(arguments.asList()))
}
/**
* Do not call this for commands that have a large standard or error output!
*
* This method does not read process' standard and error output on extra threads like [executeCommand] does.
*
* Standard and error output are being read on same thread after process ended. Therefore if standard and error
* InputStream reader's buffer is not large enough, the call to Process.waitFor() will hang forever.
*
* So it has slightly better performance than [executeCommand] has as it doesn't create two new threads but at the
* cost that app may hangs forever.
*/
override fun executeCommandWithLittleOutput(config: CommandConfig): ExecuteCommandResult {
try {
val process = createProcessFromCommandConfig(config)
val exitCode = process.waitFor()
val outputLines = process.inputStream.bufferedReader().lineSequence().toList()
val errorLines = process.errorStream.bufferedReader().lineSequence().toList()
return ExecuteCommandResult(exitCode, outputLines, errorLines)
} catch (e: Exception) {
return parseExceptionToExecuteCommandResult(config, e)
}
}
override fun executeCommand(config: CommandConfig): ExecuteCommandResult {
try {
val process = createProcessFromCommandConfig(config)
val outputStreamReader = AsyncStreamReader(process.inputStream)
val errorStreamReader = AsyncStreamReader(process.errorStream)
outputStreamReader.start()
errorStreamReader.start()
val exitCode = process.waitFor()
val outputLines = outputStreamReader.lines
val errorsLines = errorStreamReader.lines
return ExecuteCommandResult(exitCode, outputLines, errorsLines)
} catch (e: Exception) {
return parseExceptionToExecuteCommandResult(config, e)
}
}
override suspend fun executeCommandSuspendable(config: CommandConfig, scope: CoroutineScope): ExecuteCommandResult {
try {
val process = createProcessFromCommandConfig(config)
val outputStream = scope.async(Dispatchers.IO) { readStream(process.inputStream) }
val errorStream = scope.async(Dispatchers.IO) { readStream(process.errorStream) }
while (process.isAlive) {
delay(25)
}
val exitCode = process.waitFor()
return ExecuteCommandResult(exitCode, outputStream.await(), errorStream.await())
} catch (e: Exception) {
return parseExceptionToExecuteCommandResult(config, e)
}
}
protected open fun readStream(inputStream: InputStream): List {
val lines = mutableListOf()
try {
inputStream.bufferedReader().use { reader ->
reader.forEachLine {
lines.add(it)
}
}
} catch (e: Exception) {
log.error("Error occurred while reading stream", e)
}
return lines
}
protected open fun createProcessFromCommandConfig(config: CommandConfig): Process {
log.debug("Executing command '$config' ...")
val processBuilder = ProcessBuilder(config.commandArgs)
config.workingDir?.let { workingDir ->
processBuilder.directory(workingDir)
}
config.environmentVariables.forEach { environmentVariable ->
processBuilder.environment().put(environmentVariable.key, environmentVariable.value)
}
return processBuilder.start()
}
protected open fun parseExceptionToExecuteCommandResult(config: CommandConfig, e: Exception): ExecuteCommandResult {
if (config.logErrors) {
log.error("Could not execute command '$config'", e)
}
var exitCode = -1
e.message?.let { exceptionMessage ->
if (exceptionMessage.contains("error=")) {
val startIndex = exceptionMessage.indexOf("error=") + "error=".length
var endIndex = startIndex + 1
for (i in startIndex + 1..exceptionMessage.length - 1) {
if (exceptionMessage[i].isDigit() == false) {
endIndex = i
break
}
}
exitCode = exceptionMessage.substring(startIndex, endIndex).toInt()
}
}
return ExecuteCommandResult(exitCode, listOf(), listOf(e.localizedMessage))
}
}