Last active
March 4, 2024 02:18
-
-
Save devstar0209/1f0bb5d9eb5f0e7aa92d32a61ff110e4 to your computer and use it in GitHub Desktop.
This file contains 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
///usr/bin/env jbang "$0" "$@" ; exit $? | |
//JAVA 17+ | |
//KOTLIN 1.7.20 | |
//DEPS org.apache.commons:commons-text:1.9 | |
//DEPS org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 | |
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.async | |
import kotlinx.coroutines.runBlocking | |
import kotlinx.coroutines.runInterruptible | |
import kotlinx.coroutines.withContext | |
import kotlinx.coroutines.withTimeout | |
import org.apache.commons.text.StringEscapeUtils | |
import java.nio.charset.StandardCharsets.UTF_8 | |
import java.nio.file.Path | |
import kotlin.time.Duration.Companion.seconds | |
data class CommandResult( | |
val exitCode: Int, | |
val stdout: String, | |
val stderr: String, | |
) | |
/** | |
* Executes a program. This needs to be a valid path on the | |
* file system. | |
* | |
* See [executeShellCommand] for the version that executes | |
* `/bin/sh` commands. | |
*/ | |
suspend fun executeCommand( | |
executable: Path, | |
vararg args: String | |
): CommandResult = | |
// Blocking I/O should use threads designated for I/O | |
withContext(Dispatchers.IO) { | |
val cmdArgs = listOf(executable.toAbsolutePath().toString()) + args | |
val proc = Runtime.getRuntime().exec(cmdArgs.toTypedArray()) | |
try { | |
// Concurrent execution ensures the stream's buffer doesn't | |
// block processing when overflowing | |
val stdout = async { | |
runInterruptible { | |
// That `InputStream.read` doesn't listen to thread interruption | |
// signals; but for future development it doesn't hurt | |
String(proc.inputStream.readAllBytes(), UTF_8) | |
} | |
} | |
val stderr = async { | |
runInterruptible { | |
String(proc.errorStream.readAllBytes(), UTF_8) | |
} | |
} | |
CommandResult( | |
exitCode = runInterruptible { proc.waitFor() }, | |
stdout = stdout.await(), | |
stderr = stderr.await() | |
) | |
} finally { | |
// This interrupts the streams as well, so it terminates | |
// async execution, even if thread interruption for that | |
// InputStream doesn't work | |
proc.destroy() | |
} | |
} | |
/** | |
* Executes shell commands. | |
* | |
* WARN: command arguments need be given explicitly because | |
* they need to be properly escaped. | |
*/ | |
suspend fun executeShellCommand( | |
command: String, | |
vararg args: String | |
): CommandResult = | |
executeCommand( | |
Path.of("/bin/sh"), | |
"-c", | |
(listOf(command) + args) | |
.map(StringEscapeUtils::escapeXSI) | |
.joinToString(" ") | |
) | |
fun main(vararg args: String) = runBlocking { | |
// Dealing with timeouts | |
val r = withTimeout(3.seconds) { | |
executeShellCommand("ls", "-alh") | |
} | |
System.out.print(r.stdout) | |
System.err.print(r.stderr) | |
System.exit(r.exitCode) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment