Repositories / gitweb2.git

gitweb2.git

Clone (read-only): git clone http://git.guha-anderson.com/git/gitweb2.git

Branch

Isolate POSIX signal wiring in TerminateSignals.kt

Remove fork-based startup; keep SIGINT/SIGTERM graceful shutdown via
withTerminateSignalsHandled, using a stored Kotlin callback instead
of a global EmbeddedServer reference.

Hats off to the implementer for keeping Main.kt free of cinterop.

Made-with: Cursor
Author
Arjun Guha <a.guha@northeastern.edu>
Date
2026-04-19 17:27:10 -0400
Commit
aa6d291cd4c91011818fd560ce79fafef03b5604
src/main/kotlin/Main.kt
index 40801bd..0caf366 100644
--- a/src/main/kotlin/Main.kt
+++ b/src/main/kotlin/Main.kt
@@ -1,5 +1,3 @@
-@file:OptIn(ExperimentalForeignApi::class)
-
 package gitweb
 
 import io.ktor.http.ContentType
@@ -15,13 +13,6 @@ import io.ktor.server.routing.get
 import io.ktor.server.routing.routing
 import io.ktor.utils.io.charsets.Charsets
 import io.matthewnelson.kmp.process.Process as KmpProcess
-import kotlinx.cinterop.ExperimentalForeignApi
-import kotlinx.cinterop.IntVar
-import kotlinx.cinterop.alloc
-import kotlinx.cinterop.memScoped
-import kotlinx.cinterop.ptr
-import kotlinx.cinterop.staticCFunction
-import kotlinx.cinterop.value
 import kotlinx.git.Blob
 import kotlinx.git.Branch
 import kotlinx.git.BranchType
@@ -31,14 +22,6 @@ import kotlinx.git.Repository
 import kotlinx.git.TreeEntryKind
 import okio.FileSystem
 import okio.Path.Companion.toPath
-import platform.posix.SIGINT
-import platform.posix.SIGKILL
-import platform.posix.SIGTERM
-import platform.posix._exit
-import platform.posix.fork
-import platform.posix.kill
-import platform.posix.signal
-import platform.posix.waitpid
 import kotlin.system.exitProcess
 
 private const val DefaultHost = "127.0.0.1"
@@ -48,7 +31,7 @@ private const val DefaultPygmentsCommand = "pygmentize"
 
 fun runGitWeb2(args: Array<String>) {
     val config = parseArgs(args) ?: return
-    runServerProcess(config)
+    GitWebServer(config.root, config.host, config.port, config.pygmentsCommand).start()
 }
 
 private data class Config(val root: String, val host: String, val port: Int, val pygmentsCommand: String)
@@ -83,7 +66,9 @@ internal class GitWebServer(
             }
         }
         try {
-            server.start(wait = true)
+            withTerminateSignalsHandled(onTerminate = { server.stop(500, 1000) }) {
+                server.start(wait = true)
+            }
         } finally {
             server.stop()
             Git.shutdown()
@@ -372,34 +357,6 @@ private fun readCommandOutput(command: String, input: String? = null): String? {
     return output.stdout.replace("\r\n", "\n").replace('\r', '\n')
 }
 
-private var childPidForSignal: Int = 0
-
-private val parentSignalHandler = staticCFunction<Int, Unit> { signal ->
-    val childPid = childPidForSignal
-    if (childPid > 0) {
-        kill(childPid, SIGKILL)
-    }
-    _exit(128 + signal)
-}
-
-private fun runServerProcess(config: Config) {
-    val childPid = fork()
-    when {
-        childPid < 0 -> GitWebServer(config.root, config.host, config.port, config.pygmentsCommand).start()
-        childPid == 0 -> GitWebServer(config.root, config.host, config.port, config.pygmentsCommand).start()
-        else -> waitForServerProcess(childPid)
-    }
-}
-
-private fun waitForServerProcess(childPid: Int): Nothing = memScoped {
-    childPidForSignal = childPid
-    signal(SIGINT, parentSignalHandler)
-    signal(SIGTERM, parentSignalHandler)
-    val status = alloc<IntVar>()
-    waitpid(childPid, status.ptr, 0)
-    exitProcess(0)
-}
-
 private fun discoverRepos(root: String): List<RepoInfo> {
     val fileSystem = FileSystem.SYSTEM
     val found = mutableListOf<RepoInfo>()
src/main/kotlin/TerminateSignals.kt
new file mode 100644
index 0000000..fef4e38
--- /dev/null
+++ b/src/main/kotlin/TerminateSignals.kt
@@ -0,0 +1,39 @@
+@file:OptIn(ExperimentalForeignApi::class)
+
+package gitweb
+
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.staticCFunction
+import platform.posix.SIG_DFL
+import platform.posix.SIGINT
+import platform.posix.SIGTERM
+import platform.posix.signal
+import kotlin.system.exitProcess
+
+/**
+ * Runs [block] with SIGINT and SIGTERM wired to [onTerminate]. Restores default handlers and
+ * clears state afterward. All POSIX / [staticCFunction] details stay in this file.
+ */
+internal inline fun withTerminateSignalsHandled(crossinline onTerminate: () -> Unit, block: () -> Unit) {
+    terminateAction = { onTerminate() }
+    signal(SIGINT, terminateSignalHandler)
+    signal(SIGTERM, terminateSignalHandler)
+    try {
+        block()
+    } finally {
+        signal(SIGINT, SIG_DFL)
+        signal(SIGTERM, SIG_DFL)
+        terminateAction = null
+    }
+}
+
+private var terminateAction: (() -> Unit)? = null
+
+private val terminateSignalHandler = staticCFunction<Int, Unit> { _ ->
+    val action = terminateAction
+    if (action != null) {
+        action()
+    } else {
+        exitProcess(0)
+    }
+}