diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/NewRunCommand.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/NewRunCommand.kt new file mode 100644 index 00000000..a0178c68 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/NewRunCommand.kt @@ -0,0 +1,127 @@ +package com.beust.kobalt.misc + +import java.io.BufferedReader +import java.io.File +import java.io.InputStream +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit + +class RunCommandInfo { + lateinit var command: String + var args : List = arrayListOf() + var directory : File = File("") + var env : Map = hashMapOf() + + /** + * Some commands fail but return 0, so the only way to find out if they failed is to look + * at the error stream. However, some commands succeed but output text on the error stream. + * This field is used to specify how errors are caught. + */ + var useErrorStreamAsErrorIndicator : Boolean = true + var useInputStreamAsErrorIndicator : Boolean = false + + var errorCallback: Function1, Unit> = NewRunCommand.DEFAULT_ERROR + var successCallback: Function1, Unit> = NewRunCommand.DEFAULT_SUCCESS + + var isSuccess: (Boolean, List, List) -> Boolean = { + isSuccess: Boolean, + input: List, + error: List -> + var hasErrors = ! isSuccess + if (useErrorStreamAsErrorIndicator && ! hasErrors) { + hasErrors = hasErrors || error.size > 0 + } + if (useInputStreamAsErrorIndicator && ! hasErrors) { + hasErrors = hasErrors || input.size > 0 + } + + ! hasErrors + } +} + +fun runCommand(init: RunCommandInfo.() -> Unit) = NewRunCommand(RunCommandInfo().apply { init() }).invoke() + +open class NewRunCommand(val info: RunCommandInfo) { + + companion object { + val DEFAULT_SUCCESS = { output: List -> } + // val DEFAULT_SUCCESS_VERBOSE = { output: List -> log(2, "Success:\n " + output.joinToString("\n"))} + // val defaultSuccess = DEFAULT_SUCCESS + val DEFAULT_ERROR = { + output: List -> + kotlin.error(output.joinToString("\n ")) + } + } + + // fun useErrorStreamAsErrorIndicator(f: Boolean) : RunCommand { + // useErrorStreamAsErrorIndicator = f + // return this + // } + + fun invoke() : Int { + val allArgs = arrayListOf() + allArgs.add(info.command) + allArgs.addAll(info.args) + + val pb = ProcessBuilder(allArgs) + pb.directory(info.directory) + log(2, "Running command in directory ${info.directory.absolutePath}" + + "\n " + allArgs.joinToString(" ").replace("\\", "/")) + pb.environment().let { pbEnv -> + info.env.forEach { + pbEnv.put(it.key, it.value) + } + } + + val process = pb.start() + + // Run the command and collect the return code and streams + val returnCode = process.waitFor(30, TimeUnit.SECONDS) + val input = if (process.inputStream.available() > 0) fromStream(process.inputStream) + else listOf() + val error = if (process.errorStream.available() > 0) fromStream(process.errorStream) + else listOf() + + // Check to see if the command succeeded + val isSuccess = isSuccess(returnCode, input, error) + + if (isSuccess) { + info.successCallback(input) + } else { + info.errorCallback(error + input) + } + + return if (isSuccess) 0 else 1 + } + + /** + * Subclasses can override this method to do their own error handling, since commands can + * have various ways to signal errors. + */ + open protected fun isSuccess(isSuccess: Boolean, input: List, error: List) : Boolean { + var hasErrors = ! isSuccess + if (info.useErrorStreamAsErrorIndicator && ! hasErrors) { + hasErrors = hasErrors || error.size > 0 + } + if (info.useInputStreamAsErrorIndicator && ! hasErrors) { + hasErrors = hasErrors || input.size > 0 + } + + return ! hasErrors + } + + /** + * Turn the given InputStream into a list of strings. + */ + private fun fromStream(ins: InputStream) : List { + val result = arrayListOf() + val br = BufferedReader(InputStreamReader(ins)) + var line = br.readLine() + + while (line != null) { + result.add(line) + line = br.readLine() + } + return result + } +}