Skip to content

Commit

Permalink
feature: Add java.lang.Runtime.{add,remove}ShutdownHook (#3764)
Browse files Browse the repository at this point in the history
* Add support for `Runtime.{add,remove}ShutdownHook`, remove existing javalib specific `runtime.Shutdown`
  • Loading branch information
WojciechMazur committed Feb 15, 2024
1 parent 9b3e812 commit 4f3e946
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 51 deletions.
1 change: 0 additions & 1 deletion javalib/src/main/scala/java/io/File.scala
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,6 @@ object File {
val resolved = resolveLink(part, resolveAbsolute = true)
// overlap can lead to undefined behaviour
if (resolved != part) strcpy(part, resolved)
strcpy(part, resolved)
strcat(part, path + i + `1U`)

if (strncmp(resolved, path, i + `1U`) == 0) {
Expand Down
88 changes: 83 additions & 5 deletions javalib/src/main/scala/java/lang/Runtime.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package java.lang

import java.io.File
import java.util.{Set => juSet}
import java.util.Comparator
import scala.scalanative.libc.stdlib
import scala.scalanative.posix.unistd._
import scala.scalanative.windows.SysInfoApi._
Expand All @@ -9,6 +11,78 @@ import scala.scalanative.unsafe._
import scala.scalanative.meta.LinktimeInfo.isWindows

class Runtime private () {
import Runtime._
@volatile private var shutdownStarted = false
private lazy val hooks: juSet[Thread] = new java.util.HashSet()

lazy val setupAtExitHandler = {
stdlib.atexit(() => Runtime.getRuntime().runHooks())
}
private def ensureCanModify(hook: Thread): Unit = if (shutdownStarted) {
throw new IllegalStateException(
s"Shutdown sequence started, cannot add/remove hook $hook"
)
}

def addShutdownHook(thread: Thread): Unit = hooks.synchronized {
ensureCanModify(thread)
hooks.add(thread)
setupAtExitHandler
}

def removeShutdownHook(thread: Thread): Boolean = hooks.synchronized {
ensureCanModify(thread)
hooks.remove(thread)
}

private def runHooksConcurrent() = {
val hooks = this.hooks
.toArray()
.asInstanceOf[Array[Thread]]
.sorted(Ordering.by[Thread, Int](-_.getPriority()))
hooks.foreach { t =>
t.setUncaughtExceptionHandler(ShutdownHookUncoughExceptionHandler)
}
// JDK specifies that hooks might run in any order.
// However, for Scala Native it might be beneficial to support partial ordering
// E.g. Zone/MemoryPool shutdownHook cleaning pools should be run after DeleteOnExit using `toCString`
// Group the hooks by priority starting with the ones with highest priority
val limit = hooks.size
var idx = 0
while (idx < limit) {
val groupStart = idx
val groupPriority = hooks(groupStart).getPriority()
while (idx < limit && hooks(idx).getPriority() == groupPriority) {
hooks(idx).start()
idx += 1
}
for (i <- groupStart until limit) {
hooks(i).join()
}
}
}
private def runHooksSequential() = {
this.hooks
.toArray()
.asInstanceOf[Array[Thread]]
.sorted(Ordering.by[Thread, Int](-_.getPriority()))
.foreach { t =>
try t.run()
catch {
case ex: Throwable =>
ShutdownHookUncoughExceptionHandler.uncaughtException(t, ex)
}
}
}
private def runHooks() = {
import scala.scalanative.meta.LinktimeInfo.isMultithreadingEnabled
hooks.synchronized {
shutdownStarted = true
if (isMultithreadingEnabled) runHooksConcurrent()
else runHooksSequential()
}
}

import Runtime.ProcessBuilderOps
def availableProcessors(): Int = {
val available = if (isWindows) {
Expand All @@ -22,8 +96,6 @@ class Runtime private () {
def exit(status: Int): Unit = stdlib.exit(status)
def gc(): Unit = System.gc()

// def addShutdownHook(thread: java.lang.Thread): Unit = ???

def exec(cmdarray: Array[String]): Process =
new ProcessBuilder(cmdarray).start()
def exec(cmdarray: Array[String], envp: Array[String]): Process =
Expand All @@ -36,10 +108,16 @@ class Runtime private () {
exec(Array(cmd), envp, dir)
}

object Runtime {
private val currentRuntime = new Runtime()
private object ShutdownHookUncoughExceptionHandler
extends Thread.UncaughtExceptionHandler {
def uncaughtException(t: Thread, e: Throwable): Unit = {
System.err.println(s"Shutdown hook $t failed, reason: $e")
t.getThreadGroup().uncaughtException(t, e)
}
}

def getRuntime(): Runtime = currentRuntime
object Runtime extends Runtime() {
def getRuntime(): Runtime = this

private implicit class ProcessBuilderOps(val pb: ProcessBuilder)
extends AnyVal {
Expand Down
2 changes: 1 addition & 1 deletion javalib/src/main/scala/java/lang/Thread.scala
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ class Thread private[lang] (

def start(): Unit = synchronized {
if (!isMultithreadingEnabled) UnsupportedFeature.threads()
if (!isDaemon()) JoinNonDaemonThreads.registerExitHook
if (isVirtual())
throw new UnsupportedOperationException(
"VirtualThreads are not yet supported"
Expand Down Expand Up @@ -505,7 +506,6 @@ object Thread {
override protected val tid: scala.Long = 0L
inheritableThreadLocals = new ThreadLocal.Values()
platformCtx.nativeThread = nativeCompanion.create(this, 0L)
if (isMultithreadingEnabled) JoinNonDaemonThreads.registerExitHook()
}

@alwaysinline private[lang] def nativeCompanion: NativeThread.Companion =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import scala.scalanative.unsafe.{Zone, toCString}

object DeleteOnExit {
private val toDeleteSet: mutable.Set[String] = mutable.Set.empty
private val toDelete: mutable.ArrayBuffer[String] =
mutable.ArrayBuffer.empty
Shutdown.addHook(() =>
toDelete.foreach { f =>
Zone.acquire { implicit z => libc.remove(toCString(f)) }
}
)
def addFile(name: String) = toDelete.synchronized {
if (toDeleteSet.add(name)) toDelete += name

lazy val setupShutdownHook = Runtime.getRuntime().addShutdownHook {
val t = new Thread(() => {
Zone.acquire { implicit z =>
toDeleteSet.foreach(f => libc.remove(toCString(f)))
}
})
t.setName("shutdown-hook:delete-on-exit")
t
}
def addFile(name: String) = toDeleteSet.synchronized {
toDeleteSet.add(name)
setupShutdownHook
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,27 @@ package scala.scalanative.runtime
import NativeThread.Registry

object JoinNonDaemonThreads {
def registerExitHook(): Unit = Shutdown.addHook { () =>
def pollDaemonThreads = Registry.aliveThreads.iterator
.map(_.thread)
.filter { thread =>
!thread.isDaemon() && thread.isAlive()
}
lazy val registerExitHook =
try
Runtime.getRuntime().addShutdownHook {
val t = new Thread(() => {
def pollDaemonThreads = Registry.aliveThreads.iterator
.map(_.thread)
.filter { thread =>
thread != Thread.currentThread() && !thread.isDaemon() && thread
.isAlive()
}

Registry.onMainThreadTermination()
Iterator
.continually(pollDaemonThreads)
.takeWhile(_.hasNext)
.flatten
.foreach(_.join())
}
Registry.onMainThreadTermination()
Iterator
.continually(pollDaemonThreads)
.takeWhile(_.hasNext)
.flatten
.foreach(_.join())
})
t.setPriority(Thread.MIN_PRIORITY)
t.setName("shutdown-hook:join-non-deamon-threads")
t
}
catch { case ex: IllegalStateException => () } // shutdown started
}
19 changes: 0 additions & 19 deletions javalib/src/main/scala/scala/scalanative/runtime/Shutdown.scala

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,15 @@ object MemoryPool {

lazy val defaultMemoryPool: MemoryPool = {
// Release allocated chunks satisfy AdressSanitizer
if (asanEnabled) libc.atexit { () =>
defaultMemoryPool.freeChunks()
}
if (asanEnabled)
try
Runtime.getRuntime().addShutdownHook {
val t = new Thread(() => defaultMemoryPool.freeChunks())
t.setPriority(Thread.MIN_PRIORITY)
t.setName("shutdown-hook:memory-pool-cleanup")
t
}
catch { case ex: IllegalStateException => () } // shutdown already started
new MemoryPool()
}

Expand Down

0 comments on commit 4f3e946

Please sign in to comment.