Last active
March 27, 2023 15:00
-
-
Save iseki0/bd4681e0c002c82cb61af162be546963 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.async | |
import kotlinx.coroutines.launch | |
import kotlinx.coroutines.runBlocking | |
import kotlinx.coroutines.slf4j.MDCContext | |
import java.io.File | |
import java.io.InputStream | |
typealias StdoutHandler<R> = (input: InputStream) -> R | |
typealias StderrHandler<R> = (input: InputStream) -> R | |
class CommandNonZeroExitException(override val message: String) : RuntimeException() | |
class Cmdline<StdoutType, StderrType> private constructor( | |
private val d: D, | |
private val stdoutHandler: StdoutHandler<StdoutType>? = null, | |
private val stderrHandler: StderrHandler<StderrType>? = null, | |
) { | |
private data class D( | |
val command: List<String>, | |
val environments: List<Pair<String, String?>> = emptyList(), | |
val workingDirectory: File? = null, | |
val ignoreNonZeroExitCode: Boolean = false, | |
) | |
private fun copyD(d: D) = Cmdline(d = d, stdoutHandler, stderrHandler) | |
fun withEnvironment(vararg envs: Pair<String, String?>) = copyD(d.copy(environments = d.environments + envs)) | |
fun withWorkingDirectory(dir: File) = copyD(d.copy(workingDirectory = dir)) | |
fun <T> withStdoutHandler(handler: StdoutHandler<T>) = Cmdline(d = d, handler, stderrHandler) | |
fun <T> withStderrHandler(handler: StderrHandler<T>) = Cmdline(d = d, stdoutHandler, handler) | |
fun ignoreNonZeroExitCode(ignore: Boolean = true) = copyD(d.copy(ignoreNonZeroExitCode = true)) | |
companion object { | |
operator fun invoke(cmdArray: List<String>) = Cmdline<Nothing, Nothing>(D(cmdArray)) | |
} | |
fun execute(): Result<StdoutType, StderrType> { | |
val pb = ProcessBuilder(d.command) | |
val pbEnv by lazy { pb.environment() } | |
d.environments.forEach { (k, v) -> if (v == null) pbEnv.remove(k) else pbEnv[k] = v } | |
d.workingDirectory?.also(pb::directory) | |
if (stdoutHandler == null) pb.redirectOutput(ProcessBuilder.Redirect.DISCARD) | |
val process = checkNotNull(pb.start()) | |
return runBlocking(MDCContext() + Dispatchers.IO) { | |
// todo: stdin | |
runCatching { process.outputStream.close() } | |
val errorRecorder = LineRecorder() | |
// stderr | |
val processStderr = checkNotNull(process.errorStream) | |
val stderrValue = if (stderrHandler != null) { | |
// todo: handler exception handling | |
async { stderrHandler.invoke(processStderr) } | |
.apply { invokeOnCompletion { if (it != null) process.destroyForcibly() } } | |
} else { | |
launch { processStderr.reader().copyTo(errorRecorder) } | |
.apply { invokeOnCompletion { if (it != null) process.destroyForcibly() } } | |
null | |
} | |
val processStdout = checkNotNull(process.inputStream) | |
// stdout | |
val stdoutValue = stdoutHandler?.let { | |
async { it(processStdout) }.apply { invokeOnCompletion { if (it != null) process.destroyForcibly() } } | |
} | |
// waiting | |
val exitCode = process.waitFor() | |
if (exitCode != 0 && !d.ignoreNonZeroExitCode) { | |
throw CommandNonZeroExitException("command exit with non-zero code($exitCode), stderr: $errorRecorder") | |
} | |
Result( | |
exitCode = exitCode, | |
stderrSnapshot = errorRecorder.toString(), | |
stdoutValue = stdoutValue?.await() ?: null as StdoutType, | |
stderrValue = stderrValue?.await() ?: null as StderrType, | |
) | |
} | |
} | |
class Result<StdoutType, StderrType>( | |
val exitCode: Int, | |
val stdoutValue: StdoutType, | |
val stderrValue: StderrType, | |
val stderrSnapshot: String, | |
) | |
} | |
fun List<String>.asCmdline() = Cmdline(this) | |
fun main() { | |
val result = listOf("ls", "-l").asCmdline() | |
.ignoreNonZeroExitCode() | |
.withStdoutHandler { it.reader().readText() } | |
.execute() | |
println(result.stdoutValue) | |
val a = result.stderrValue // kotlin.KotlinNothingValueExceptionkotlin.KotlinNothingValueException | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.Writer | |
class LineRecorder( | |
private val lineLength: Int = 120, | |
private val topLines: Int = 20, | |
private val bottomLines: Int = 30 | |
) : Writer() { | |
init { | |
check(lineLength > 0) | |
check(topLines > 0) | |
check(bottomLines > 0) | |
} | |
private val topLineList = ArrayList<String>() | |
private val bottomLineList = ArrayDeque<String>() | |
override fun close() { | |
commitLine() | |
} | |
override fun flush() {} | |
private val buffer = StringBuilder() | |
private fun commitToBuf(cbuf: CharArray, from: Int, to: Int) { | |
val free = lineLength - buffer.length | |
@Suppress("NAME_SHADOWING") val to = minOf(free + from, to) | |
buffer.appendRange(cbuf, from, from + to) | |
} | |
private fun commitLine() { | |
val line = buffer.toString().trim() | |
buffer.clear() | |
if (line.isBlank()) return | |
if (topLineList.size < topLines) { | |
topLineList += line | |
return | |
} | |
bottomLineList += line | |
if (bottomLineList.size >= bottomLines) bottomLineList.removeFirst() | |
} | |
override fun write(cbuf: CharArray, off: Int, len: Int) { | |
val validIndices = off until off + len | |
var p = off | |
var q = off | |
while (p in validIndices && q in validIndices) { | |
if (cbuf[q] != '\n') { | |
q++ | |
continue | |
} | |
commitToBuf(cbuf, p, q) | |
commitLine() | |
p = q + 1 | |
q = p | |
} | |
if (p in validIndices) { | |
commitToBuf(cbuf, p, off + len) | |
} | |
} | |
override fun toString(): String = | |
topLineList.joinToString(separator = "\n") + | |
if (bottomLineList.isEmpty()) "" else "\n...\n" + bottomLineList.joinToString(separator = "\n") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment