Skip to content

Instantly share code, notes, and snippets.

@iseki0
Last active March 27, 2023 15:00
Show Gist options
  • Save iseki0/bd4681e0c002c82cb61af162be546963 to your computer and use it in GitHub Desktop.
Save iseki0/bd4681e0c002c82cb61af162be546963 to your computer and use it in GitHub Desktop.
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
}
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