Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
lihaoyi committed Oct 21, 2024
1 parent 053ebdc commit c07cb17
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 30 deletions.
6 changes: 6 additions & 0 deletions os/src/ProcessOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ object call {
destroyOnExit = destroyOnExit
)
}

// Bincompat Forwarder
def apply(
cmd: Shellable,
env: Map[String, String],
Expand Down Expand Up @@ -104,6 +106,8 @@ object spawn {
destroyOnExit = destroyOnExit
)
}

// Bincompat Forwarder
def apply(
cmd: Shellable,
// Make sure `cwd` only comes after `env`, so `os.spawn("foo", path)` is a compile error
Expand Down Expand Up @@ -250,6 +254,7 @@ case class proc(command: Shellable*) {
shutdownGracePeriod = 100
)

// Bincompat Forwarder
private[os] def call(
cwd: Path,
env: Map[String, String],
Expand Down Expand Up @@ -351,6 +356,7 @@ case class proc(command: Shellable*) {
proc
}

// Bincompat Forwarder
def spawn(
cwd: Path,
env: Map[String, String],
Expand Down
16 changes: 10 additions & 6 deletions os/src/SubProcess.scala
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,7 @@ class SubProcess(
/**
* Attempt to destroy the subprocess (gently), via the underlying JVM APIs
*/
@deprecated("Use destroy(shutdownGracePeriod = Long.MaxValue)")
def destroy(): Unit = destroy(shutdownGracePeriod = Long.MaxValue)
def destroy(): Unit = destroy(shutdownGracePeriod = this.shutdownGracePeriod, async = false)

/**
* Destroys the subprocess, via the underlying JVM APIs, with configurable levels of
Expand All @@ -154,8 +153,8 @@ class SubProcess(
* @param shutdownGracePeriod use this to override the default wait time for the subprocess
* to gracefully exit before destroying it forcibly. Defaults to the `shutdownGracePeriod`
* that was used to spawned the process, but can be set to 0
* (i.e. force exit immediately) or Long.MaxValue (i.e. never force exit)
* or anything in between. Typically defaults to 100 milliseconds
* (i.e. force exit immediately) or -1 (i.e. never force exit)
* or anything in between. Typically defaults to 100 milliseconds.
*/
def destroy(
shutdownGracePeriod: Long = this.shutdownGracePeriod,
Expand All @@ -165,11 +164,16 @@ class SubProcess(
if (!async) {
val now = System.currentTimeMillis()

while (wrapped.isAlive && System.currentTimeMillis() - now < shutdownGracePeriod) {
while (
wrapped.isAlive && (shutdownGracePeriod == -1 || System.currentTimeMillis() - now < shutdownGracePeriod)
) {
Thread.sleep(1)
}

if (wrapped.isAlive) wrapped.destroyForcibly()
if (wrapped.isAlive) {
println("wrapped.destroyForcibly()")
wrapped.destroyForcibly()
}
}
}

Expand Down
77 changes: 54 additions & 23 deletions os/test/src-jvm/SpawningSubprocessesNewTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -200,31 +200,62 @@ object SpawningSubprocessesNewTests extends TestSuite {
}

test("spawnExitHook") {
if (Unix()) {
val temp = os.temp()
val lock0 = tryLock(temp)
// file starts off not locked so can be taken and released
assert(lock0 != null)
lock0.release()

val subprocess = os.spawn((sys.env("TEST_SPAWN_EXIT_HOOK_ASSEMBLY"), temp))
waitForLockTaken(temp)

subprocess.destroy()
// after calling destroy on the subprocess, the transitive subprocess
// should be killed by the exit hook, so the lock can now be taken
val lock = tryLock(temp)
assert(lock != null)
test("destroyDefaultGrace") {
if (Unix()) {
val temp = os.temp()
val lock0 = tryLock(temp)
// file starts off not locked so can be taken and released
assert(lock0 != null)
lock0.release()

val subprocess = os.spawn((sys.env("TEST_SPAWN_EXIT_HOOK_ASSEMBLY"), temp))
waitForLockTaken(temp)

subprocess.destroy()
// after calling destroy on the subprocess, the transitive subprocess
// should be killed by the exit hook, so the lock can now be taken
val lock = tryLock(temp)
assert(lock != null)
lock.release()
}
}

val temp2 = os.temp()
val subprocess2 = os.spawn((sys.env("TEST_SPAWN_EXIT_HOOK_ASSEMBLY"), temp2))
waitForLockTaken(temp2)
test("destroyNoGrace") {
if (Unix()) {
val temp = os.temp()
val subprocess = os.spawn((sys.env("TEST_SPAWN_EXIT_HOOK_ASSEMBLY"), temp))
waitForLockTaken(temp)

subprocess.destroy(shutdownGracePeriod = 0)
// this should fail since the subprocess is shut down forcibly without grace period
// so there is no time for any exit hooks to run to shut down the transitive subprocess
val lock = tryLock(temp)
assert(lock == null)
}
}

subprocess2.destroy(shutdownGracePeriod = 0)
// this should fail since the subprocess is shut down forcibly without grace period
// so there is no time for any exit hooks to run to shut down the transitive subprocess
val lock2 = tryLock(temp2)
assert(lock2 == null)
test("infiniteGrace") {
if (Unix()) {
val temp = os.temp()
val lock0 = tryLock(temp)
// file starts off not locked so can be taken and released
assert(lock0 != null)
lock0.release()

// Force the subprocess exit to stall for 500ms
val subprocess = os.spawn((sys.env("TEST_SPAWN_EXIT_HOOK_ASSEMBLY"), temp, 500))
waitForLockTaken(temp)

val start = System.currentTimeMillis()
subprocess.destroy(shutdownGracePeriod = -1)
val end = System.currentTimeMillis()
// Because we set the shutdownGracePeriod to -1, it takes more than 500ms to shutdown,
// even though the default shutdown grace period is 100. But the sub-sub-process will
// have been shut down by the time the sub-process exits, so the lock is available
assert(end - start > 500)
val lock = tryLock(temp)
assert(lock != null)
}
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion os/test/testSpawnExitHook/src/TestSpawnExitHook.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ package test.os

object TestSpawnExitHook {
def main(args: Array[String]): Unit = {
os.spawn((sys.env("TEST_SPAWN_EXIT_HOOK_ASSEMBLY2"), args(0)), destroyOnExit = true)
Runtime.getRuntime.addShutdownHook(
new Thread(() => {
for (shutdownDelay <- args.lift(1)) Thread.sleep(shutdownDelay.toLong)
System.err.println("Shutdown Hook")
})
)
val cmd = (sys.env("TEST_SPAWN_EXIT_HOOK_ASSEMBLY2"), args(0))
os.spawn(cmd = cmd, destroyOnExit = true)
Thread.sleep(99999)
}
}

0 comments on commit c07cb17

Please sign in to comment.