Add CLI atExit shutdown handlers
This commit is contained in:
parent
0e73d80707
commit
88ce04102a
@ -6,6 +6,7 @@ The Lyng CLI is the reference command-line tool for the Lyng language. It lets y
|
||||
- Use standard argument passing (`ARGV`) to your scripts.
|
||||
- Resolve local file imports from the executed script's directory tree.
|
||||
- Format Lyng source files via the built-in `fmt` subcommand.
|
||||
- Register synchronous process-exit handlers with `atExit(...)`.
|
||||
|
||||
|
||||
## Building on Linux
|
||||
@ -87,6 +88,43 @@ lyng --version
|
||||
lyng --help
|
||||
```
|
||||
|
||||
### Exit handlers: `atExit(...)`
|
||||
|
||||
The CLI exposes a CLI-only builtin:
|
||||
|
||||
```lyng
|
||||
extern fun atExit(append: Bool=true, handler: ()->Void)
|
||||
```
|
||||
|
||||
Use it to register synchronous cleanup handlers that should run when the CLI process is leaving.
|
||||
|
||||
Semantics:
|
||||
- `append=true` appends the handler to the end of the queue.
|
||||
- `append=false` inserts the handler at the front of the queue.
|
||||
- Handlers run one by one.
|
||||
- Exceptions thrown by a handler are ignored, and the next handler still runs.
|
||||
- Handlers are best-effort and run on:
|
||||
- normal script completion
|
||||
- script failure
|
||||
- script `exit(code)`
|
||||
- process shutdown such as `SIGTERM`
|
||||
|
||||
Non-goals:
|
||||
- `SIGKILL`, hard crashes, and power loss cannot be intercepted.
|
||||
- `atExit` is currently a CLI feature only; it is not part of the general embedding/runtime surface.
|
||||
|
||||
Examples:
|
||||
|
||||
```lyng
|
||||
atExit {
|
||||
println("closing resources")
|
||||
}
|
||||
|
||||
atExit(false) {
|
||||
println("runs first")
|
||||
}
|
||||
```
|
||||
|
||||
### Local imports for file execution
|
||||
|
||||
When you execute a script file, the CLI builds a temporary local import manager rooted at the directory that contains the entry script.
|
||||
|
||||
@ -19,6 +19,8 @@ plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
}
|
||||
|
||||
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "unspecified"
|
||||
|
||||
@ -83,6 +85,9 @@ kotlin {
|
||||
implementation(libs.okio.fakefilesystem)
|
||||
}
|
||||
}
|
||||
val linuxTest by creating {
|
||||
dependsOn(commonTest)
|
||||
}
|
||||
val nativeMain by creating {
|
||||
dependsOn(commonMain)
|
||||
}
|
||||
@ -100,5 +105,16 @@ kotlin {
|
||||
val linuxX64Main by getting {
|
||||
dependsOn(nativeMain)
|
||||
}
|
||||
val linuxX64Test by getting {
|
||||
dependsOn(linuxTest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<KotlinNativeTest>("linuxX64Test") {
|
||||
dependsOn(tasks.named("linkDebugExecutableLinuxX64"))
|
||||
environment(
|
||||
"LYNG_CLI_NATIVE_BIN",
|
||||
layout.buildDirectory.file("bin/linuxX64/debugExecutable/lyng.kexe").get().asFile.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@ import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.LyngVersion
|
||||
@ -36,6 +38,7 @@ import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.ScriptError
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.asFacade
|
||||
import net.sergeych.lyng.io.console.createConsoleModule
|
||||
import net.sergeych.lyng.io.fs.createFs
|
||||
import net.sergeych.lyng.io.http.createHttpModule
|
||||
@ -57,6 +60,14 @@ import okio.Path.Companion.toPath
|
||||
|
||||
expect fun exit(code: Int)
|
||||
|
||||
internal expect class CliPlatformShutdownHooks {
|
||||
fun uninstall()
|
||||
|
||||
companion object {
|
||||
fun install(runtime: CliExecutionRuntime): CliPlatformShutdownHooks
|
||||
}
|
||||
}
|
||||
|
||||
expect class ShellCommandExecutor {
|
||||
fun executeCommand(command: String): CommandResult
|
||||
|
||||
@ -71,6 +82,51 @@ data class CommandResult(
|
||||
val error: String
|
||||
)
|
||||
|
||||
private const val cliBuiltinsDeclarations = """
|
||||
extern fun atExit(append: Bool=true, handler: ()->Void)
|
||||
"""
|
||||
|
||||
private class CliExitRequested(val code: Int) : RuntimeException("CLI exit requested: $code")
|
||||
|
||||
internal class CliExecutionRuntime(
|
||||
private val session: EvalSession,
|
||||
private val rootScope: Scope
|
||||
) {
|
||||
private val shutdownMutex = Mutex()
|
||||
private var shutdownStarted = false
|
||||
private val exitHandlers = mutableListOf<Obj>()
|
||||
|
||||
fun registerAtExit(handler: Obj, append: Boolean) {
|
||||
if (append) {
|
||||
exitHandlers += handler
|
||||
} else {
|
||||
exitHandlers.add(0, handler)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun shutdown() {
|
||||
shutdownMutex.withLock {
|
||||
if (shutdownStarted) return
|
||||
shutdownStarted = true
|
||||
}
|
||||
val handlers = exitHandlers.toList()
|
||||
val facade = rootScope.asFacade()
|
||||
for (handler in handlers) {
|
||||
runCatching {
|
||||
facade.call(handler)
|
||||
}
|
||||
}
|
||||
session.cancelAndJoin()
|
||||
shutdownSystemNetEngine()
|
||||
}
|
||||
|
||||
fun shutdownBlocking() {
|
||||
runBlocking {
|
||||
shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val baseCliImportManagerDefer = globalDefer {
|
||||
val manager = Script.defaultImportManager.copy().apply {
|
||||
installCliModules(this)
|
||||
@ -91,14 +147,76 @@ val baseScopeDefer = globalDefer {
|
||||
baseCliImportManagerDefer.await().copy().apply {
|
||||
invalidateCliModuleCaches()
|
||||
}.newStdScope().apply {
|
||||
installCliDeclarations()
|
||||
installCliBuiltins()
|
||||
addConst("ARGV", ObjList(mutableListOf()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun Scope.installCliBuiltins() {
|
||||
private suspend fun Scope.installCliDeclarations() {
|
||||
eval(Source("<cli-builtins>", cliBuiltinsDeclarations))
|
||||
}
|
||||
|
||||
private fun Scope.installCliBuiltins(runtime: CliExecutionRuntime? = null) {
|
||||
addFn("exit") {
|
||||
exit(requireOnlyArg<ObjInt>().toInt())
|
||||
val code = requireOnlyArg<ObjInt>().toInt()
|
||||
if (runtime == null) {
|
||||
exit(code)
|
||||
}
|
||||
throw CliExitRequested(code)
|
||||
}
|
||||
addFn("atExit") {
|
||||
if (runtime == null) {
|
||||
raiseIllegalState("atExit is only available while running a CLI script")
|
||||
}
|
||||
if (args.list.size > 2) {
|
||||
raiseError("Expected at most 2 positional arguments, got ${args.list.size}")
|
||||
}
|
||||
var append = true
|
||||
var appendSet = false
|
||||
var handler: Obj? = null
|
||||
|
||||
when (args.list.size) {
|
||||
1 -> {
|
||||
val only = args.list[0]
|
||||
if (only.isInstanceOf("Callable")) {
|
||||
handler = only
|
||||
} else {
|
||||
append = only.toBool()
|
||||
appendSet = true
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
append = args.list[0].toBool()
|
||||
appendSet = true
|
||||
handler = args.list[1]
|
||||
}
|
||||
}
|
||||
|
||||
for ((name, value) in args.named) {
|
||||
when (name) {
|
||||
"append" -> {
|
||||
if (appendSet) {
|
||||
raiseIllegalArgument("argument 'append' is already set")
|
||||
}
|
||||
append = value.toBool()
|
||||
appendSet = true
|
||||
}
|
||||
"handler" -> {
|
||||
if (handler != null) {
|
||||
raiseIllegalArgument("argument 'handler' is already set")
|
||||
}
|
||||
handler = value
|
||||
}
|
||||
else -> raiseIllegalArgument("unknown argument '$name'")
|
||||
}
|
||||
}
|
||||
|
||||
val handlerValue = handler ?: raiseError("argument 'handler' is required")
|
||||
if (!handlerValue.isInstanceOf("Callable")) {
|
||||
raiseClassCastError("Expected handler to be callable")
|
||||
}
|
||||
runtime.registerAtExit(handlerValue, append)
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
@ -237,6 +355,7 @@ private fun registerLocalCliModules(manager: ImportManager, modules: List<LocalC
|
||||
|
||||
private suspend fun ImportManager.newCliScope(argv: List<String>): Scope =
|
||||
newStdScope().apply {
|
||||
installCliDeclarations()
|
||||
installCliBuiltins()
|
||||
addConst("ARGV", ObjList(argv.map { ObjString(it) }.toMutableList()))
|
||||
}
|
||||
@ -408,12 +527,22 @@ fun executeFileWithArgs(fileName: String, args: List<String>) {
|
||||
|
||||
suspend fun executeSource(source: Source, initialScope: Scope? = null) {
|
||||
val session = EvalSession(initialScope ?: baseScopeDefer.await())
|
||||
val rootScope = session.getScope()
|
||||
val runtime = CliExecutionRuntime(session, rootScope)
|
||||
rootScope.installCliBuiltins(runtime)
|
||||
val shutdownHooks = CliPlatformShutdownHooks.install(runtime)
|
||||
var requestedExitCode: Int? = null
|
||||
try {
|
||||
try {
|
||||
evalOnCliDispatcher(session, source)
|
||||
} finally {
|
||||
session.cancelAndJoin()
|
||||
shutdownSystemNetEngine()
|
||||
} catch (e: CliExitRequested) {
|
||||
requestedExitCode = e.code
|
||||
}
|
||||
} finally {
|
||||
shutdownHooks.uninstall()
|
||||
runtime.shutdown()
|
||||
}
|
||||
requestedExitCode?.let { exit(it) }
|
||||
}
|
||||
|
||||
internal suspend fun evalOnCliDispatcher(session: EvalSession, source: Source): Obj =
|
||||
|
||||
@ -24,6 +24,34 @@ import kotlin.system.exitProcess
|
||||
@PublishedApi
|
||||
internal var jvmExitImpl: (Int) -> Nothing = { code -> exitProcess(code) }
|
||||
|
||||
internal actual class CliPlatformShutdownHooks private constructor(
|
||||
private val shutdownHook: Thread?
|
||||
) {
|
||||
actual fun uninstall() {
|
||||
val hook = shutdownHook ?: return
|
||||
runCatching {
|
||||
Runtime.getRuntime().removeShutdownHook(hook)
|
||||
}
|
||||
}
|
||||
|
||||
actual companion object {
|
||||
actual fun install(runtime: CliExecutionRuntime): CliPlatformShutdownHooks {
|
||||
val hook = Thread(
|
||||
{
|
||||
runtime.shutdownBlocking()
|
||||
},
|
||||
"lyng-cli-shutdown"
|
||||
)
|
||||
return runCatching {
|
||||
Runtime.getRuntime().addShutdownHook(hook)
|
||||
CliPlatformShutdownHooks(hook)
|
||||
}.getOrElse {
|
||||
CliPlatformShutdownHooks(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual fun exit(code: Int) {
|
||||
jvmExitImpl(code)
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import net.sergeych.jvmExitImpl
|
||||
import net.sergeych.runMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.PrintStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class CliAtExitJvmTest {
|
||||
private val originalOut: PrintStream = System.out
|
||||
private val originalErr: PrintStream = System.err
|
||||
|
||||
private class TestExit(val code: Int) : RuntimeException()
|
||||
|
||||
private data class CliResult(val out: String, val err: String, val exitCode: Int?)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
jvmExitImpl = { code -> throw TestExit(code) }
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
System.setOut(originalOut)
|
||||
System.setErr(originalErr)
|
||||
jvmExitImpl = { code -> kotlin.system.exitProcess(code) }
|
||||
}
|
||||
|
||||
private fun runCli(vararg args: String): CliResult {
|
||||
val outBuf = ByteArrayOutputStream()
|
||||
val errBuf = ByteArrayOutputStream()
|
||||
System.setOut(PrintStream(outBuf, true, Charsets.UTF_8))
|
||||
System.setErr(PrintStream(errBuf, true, Charsets.UTF_8))
|
||||
|
||||
var exitCode: Int? = null
|
||||
try {
|
||||
runMain(arrayOf(*args))
|
||||
} catch (e: TestExit) {
|
||||
exitCode = e.code
|
||||
} finally {
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
}
|
||||
return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode)
|
||||
}
|
||||
|
||||
private fun runScript(scriptText: String): CliResult {
|
||||
val tmp: Path = Files.createTempFile("lyng_atexit_", ".lyng")
|
||||
try {
|
||||
Files.writeString(tmp, scriptText)
|
||||
return runCli(tmp.toString())
|
||||
} finally {
|
||||
Files.deleteIfExists(tmp)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atExitRunsInRequestedOrderAndIgnoresHandlerExceptions() {
|
||||
val result = runScript(
|
||||
"""
|
||||
atExit {
|
||||
println("tail")
|
||||
}
|
||||
atExit(false) {
|
||||
println("head")
|
||||
throw Exception("ignored")
|
||||
}
|
||||
println("body")
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
assertNull(result.err.takeIf { it.isNotBlank() })
|
||||
assertNull(result.exitCode)
|
||||
val lines = result.out
|
||||
.lineSequence()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.toList()
|
||||
assertEquals(listOf("body", "head", "tail"), lines)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atExitRunsBeforeScriptExitTerminatesProcess() {
|
||||
val result = runScript(
|
||||
"""
|
||||
atExit {
|
||||
println("cleanup")
|
||||
}
|
||||
exit(7)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
assertEquals(7, result.exitCode)
|
||||
assertTrue(result.out.lineSequence().any { it.trim() == "cleanup" })
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toPath
|
||||
import platform.posix.O_CREAT
|
||||
import platform.posix.O_TRUNC
|
||||
import platform.posix.O_WRONLY
|
||||
import platform.posix.SIGTERM
|
||||
import platform.posix._exit
|
||||
import platform.posix.close
|
||||
import platform.posix.dup2
|
||||
import platform.posix.execvp
|
||||
import platform.posix.fork
|
||||
import platform.posix.getenv
|
||||
import platform.posix.getpid
|
||||
import platform.posix.kill
|
||||
import platform.posix.open
|
||||
import platform.posix.usleep
|
||||
import platform.posix.waitpid
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
class CliAtExitLinuxNativeTest {
|
||||
@Test
|
||||
fun atExitRunsOnSigtermForNativeCli() {
|
||||
val executable = getenv("LYNG_CLI_NATIVE_BIN")?.toKString()
|
||||
?: error("LYNG_CLI_NATIVE_BIN is not set")
|
||||
val fs = FileSystem.SYSTEM
|
||||
val tempDir = "/tmp/lyng_cli_native_${getpid()}_${kotlin.random.Random.nextInt()}".toPath()
|
||||
val scriptPath = tempDir / "sigterm.lyng"
|
||||
val stdoutPath = tempDir / "stdout.txt"
|
||||
val stderrPath = tempDir / "stderr.txt"
|
||||
|
||||
fs.createDirectories(tempDir)
|
||||
try {
|
||||
fs.write(scriptPath) {
|
||||
writeUtf8(
|
||||
"""
|
||||
atExit {
|
||||
println("cleanup-native")
|
||||
}
|
||||
while(true) {
|
||||
yield()
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
val pid = launchCli(executable, scriptPath, stdoutPath, stderrPath)
|
||||
usleep(300_000u)
|
||||
assertEquals(0, kill(pid, SIGTERM), "failed to send SIGTERM")
|
||||
|
||||
val status = waitForPid(pid)
|
||||
val exitCode = if ((status and 0x7f) == 0) (status shr 8) and 0xff else -1
|
||||
val stdout = readUtf8IfExists(fs, stdoutPath)
|
||||
val stderr = readUtf8IfExists(fs, stderrPath)
|
||||
|
||||
assertEquals(143, exitCode, "unexpected native CLI exit status; stderr=$stderr")
|
||||
assertTrue(stdout.contains("cleanup-native"), "stdout did not contain cleanup marker. stdout=$stdout stderr=$stderr")
|
||||
} finally {
|
||||
fs.deleteRecursively(tempDir, mustExist = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readUtf8IfExists(fs: FileSystem, path: Path): String {
|
||||
return if (fs.exists(path)) {
|
||||
fs.read(path) { readUtf8() }
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun waitForPid(pid: Int): Int = memScoped {
|
||||
val status = alloc<IntVar>()
|
||||
val waited = waitpid(pid, status.ptr, 0)
|
||||
check(waited == pid) { "waitpid failed for $pid" }
|
||||
status.value
|
||||
}
|
||||
|
||||
private fun launchCli(
|
||||
executable: String,
|
||||
scriptPath: Path,
|
||||
stdoutPath: Path,
|
||||
stderrPath: Path
|
||||
): Int = memScoped {
|
||||
val pid = fork()
|
||||
check(pid >= 0) { "fork failed" }
|
||||
if (pid == 0) {
|
||||
val stdoutFd = open(stdoutPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
||||
val stderrFd = open(stderrPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
||||
if (stdoutFd < 0 || stderrFd < 0) {
|
||||
_exit(2)
|
||||
}
|
||||
dup2(stdoutFd, 1)
|
||||
dup2(stderrFd, 2)
|
||||
close(stdoutFd)
|
||||
close(stderrFd)
|
||||
|
||||
val argv = allocArray<CPointerVar<ByteVar>>(3)
|
||||
argv[0] = executable.cstr.ptr
|
||||
argv[1] = scriptPath.toString().cstr.ptr
|
||||
argv[2] = null
|
||||
execvp(executable, argv)
|
||||
_exit(127)
|
||||
}
|
||||
pid
|
||||
}
|
||||
}
|
||||
@ -22,11 +22,40 @@
|
||||
package net.sergeych
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import kotlin.native.concurrent.ThreadLocal
|
||||
import platform.posix.fgets
|
||||
import platform.posix.pclose
|
||||
import platform.posix.popen
|
||||
import platform.posix.signal
|
||||
import platform.posix.atexit
|
||||
import platform.posix.SIGINT
|
||||
import platform.posix.SIGHUP
|
||||
import platform.posix.SIGTERM
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@ThreadLocal
|
||||
private var activeCliRuntime: CliExecutionRuntime? = null
|
||||
|
||||
@ThreadLocal
|
||||
private var nativeCliHooksInstalled: Boolean = false
|
||||
|
||||
private fun installNativeCliHooksOnce() {
|
||||
if (nativeCliHooksInstalled) return
|
||||
nativeCliHooksInstalled = true
|
||||
atexit(staticCFunction(::nativeCliAtExit))
|
||||
signal(SIGTERM, staticCFunction(::nativeCliSignalHandler))
|
||||
signal(SIGINT, staticCFunction(::nativeCliSignalHandler))
|
||||
signal(SIGHUP, staticCFunction(::nativeCliSignalHandler))
|
||||
}
|
||||
|
||||
private fun nativeCliAtExit() {
|
||||
activeCliRuntime?.shutdownBlocking()
|
||||
}
|
||||
|
||||
private fun nativeCliSignalHandler(signal: Int) {
|
||||
exitProcess(128 + signal)
|
||||
}
|
||||
|
||||
actual class ShellCommandExecutor() {
|
||||
actual fun executeCommand(command: String): CommandResult {
|
||||
val outputBuilder = StringBuilder()
|
||||
@ -62,6 +91,24 @@ actual class ShellCommandExecutor() {
|
||||
}
|
||||
}
|
||||
|
||||
internal actual class CliPlatformShutdownHooks private constructor(
|
||||
private val runtime: CliExecutionRuntime
|
||||
) {
|
||||
actual fun uninstall() {
|
||||
if (activeCliRuntime === runtime) {
|
||||
activeCliRuntime = null
|
||||
}
|
||||
}
|
||||
|
||||
actual companion object {
|
||||
actual fun install(runtime: CliExecutionRuntime): CliPlatformShutdownHooks {
|
||||
installNativeCliHooksOnce()
|
||||
activeCliRuntime = runtime
|
||||
return CliPlatformShutdownHooks(runtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual fun exit(code: Int) {
|
||||
exitProcess(code)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user