Skip to content

Commit

Permalink
Fix destroyOnExit default forwarding, make destroy recursive by def…
Browse files Browse the repository at this point in the history
…ault (#359)

* Previously a bunch of `bincompat` forwarders were incorrectly setting
`destroyOnExit = false` when the default is `true`, this makes them
consistently pass `destroyOnExit = false` when an explicit value is not
provided
* `os.proc.call` was forgetting to forward `shutdownGracePeriod` and
`destroyOnExit` to the underlying `os.proc.spawn`
* `SubProcess#destroy` was not destroying descendent processes. While
this has always been the default, it has always been confusing, and
often resulted in leaking orphan processes. This PR makes it recursively
destroy child processes by default, unless passed `recursive = false`.
`destroyOnExit` and on timeout now is always recursive

Dropping Scala-Native support for now due to:

```
os.native[2.12.17].test.nativeLink scala.scalanative.linker.LinkingException: Unreachable symbols found after classloading run. It can happen when using dependencies not cross-compiled for Scala Native or not yet ported JDK definitions.
````
Will open an issue to figure it out asynchronously
  • Loading branch information
lihaoyi authored Feb 18, 2025
1 parent 3aea8be commit ff52a8b
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 8
java-version: 11

- run: ./mill -i -k __.mimaReportBinaryIssues

Expand Down
4 changes: 2 additions & 2 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,14 @@ object os extends Module {
object nohometest extends ScalaTests with OsLibTestModule
}

object native extends Cross[OsNativeModule](scalaVersions)
/*object native extends Cross[OsNativeModule](scalaVersions)
trait OsNativeModule extends OsModule with ScalaNativeModule {
def scalaNativeVersion = "0.5.2"
object test extends ScalaNativeTests with OsLibTestModule {
def nativeLinkStubs = true
}
object nohometest extends ScalaNativeTests with OsLibTestModule
}
}*/

object watch extends Module {
object jvm extends Cross[WatchJvmModule](scalaVersions)
Expand Down
12 changes: 7 additions & 5 deletions os/src/ProcessOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ object call {
check = check,
propagateEnv = propagateEnv,
shutdownGracePeriod = timeoutGracePeriod,
destroyOnExit = false
destroyOnExit = true
)
}
}
Expand Down Expand Up @@ -130,7 +130,7 @@ object spawn {
mergeErrIntoOut = mergeErrIntoOut,
propagateEnv = propagateEnv,
shutdownGracePeriod = 100,
destroyOnExit = false
destroyOnExit = true
)
}
}
Expand Down Expand Up @@ -219,7 +219,9 @@ case class proc(command: Shellable*) {
chunks.add(Right(new geny.Bytes(java.util.Arrays.copyOf(buf, n))))
),
mergeErrIntoOut,
propagateEnv
propagateEnv,
shutdownGracePeriod = shutdownGracePeriod,
destroyOnExit = destroyOnExit
)

sub.join(timeout, shutdownGracePeriod)
Expand Down Expand Up @@ -277,7 +279,7 @@ case class proc(command: Shellable*) {
check,
propagateEnv,
timeoutGracePeriod,
destroyOnExit = false
destroyOnExit = true
)

/**
Expand Down Expand Up @@ -374,7 +376,7 @@ case class proc(command: Shellable*) {
mergeErrIntoOut = mergeErrIntoOut,
propagateEnv = propagateEnv,
shutdownGracePeriod = 100,
destroyOnExit = false
destroyOnExit = true
)

/**
Expand Down
42 changes: 29 additions & 13 deletions os/src/SubProcess.scala
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ class SubProcess(
*/
def destroy(): Unit = destroy(shutdownGracePeriod = this.shutdownGracePeriod, async = false)

def destroy(
shutdownGracePeriod: Long,
async: Boolean
): Unit = destroy(shutdownGracePeriod, async, recursive = true)

/**
* Destroys the subprocess, via the underlying JVM APIs, with configurable levels of
* aggressiveness:
Expand All @@ -155,26 +160,37 @@ class SubProcess(
* that was used to spawned the process, but can be set to 0
* (i.e. force exit immediately) or -1 (i.e. never force exit)
* or anything in between. Typically defaults to 100 milliseconds.
* @param recursive whether or not to also destroy this process's own child processes and
* descendents. Each parent process is destroyed before its children, to
* ensure that when we are destroying the child processes no other children
* can be spawned concurrently
*/
def destroy(
shutdownGracePeriod: Long = this.shutdownGracePeriod,
async: Boolean = false
async: Boolean = false,
recursive: Boolean = true
): Unit = {
wrapped.destroy()
if (!async) {
val now = System.currentTimeMillis()

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

if (wrapped.isAlive) {
println("wrapped.destroyForcibly()")
wrapped.destroyForcibly()
def destroy0(p: ProcessHandle) = {
p.destroy()
if (!async) {
val now = System.currentTimeMillis()

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

if (p.isAlive) p.destroyForcibly()
}
}
def rec(p: ProcessHandle): Unit = {
destroy0(p)
p.children().forEach(c => rec(c))
}
if (recursive) rec(wrapped.toHandle)
else destroy0(wrapped.toHandle)
}

@deprecated("Use destroy(shutdownGracePeriod = 0)")
Expand Down
2 changes: 1 addition & 1 deletion os/test/src-jvm/SpawningSubprocessesNewTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ object SpawningSubprocessesNewTests extends TestSuite {
}
}

test("destroyNoGrace") {
test("destroyNoGrace") - retry(3) {
if (Unix()) {
val temp = os.temp()
val subprocess = os.spawn((sys.env("TEST_SPAWN_EXIT_HOOK_ASSEMBLY"), temp))
Expand Down
2 changes: 1 addition & 1 deletion os/test/src/ZipOpTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ object ZipOpTests extends TestSuite {
wd / "unzipAllExceptExcludingCertainFiles/one.txt"
)

assert(paths == expected)
assert(paths.toSet == expected.toSet)
}

test("zipList") - prep { wd =>
Expand Down

0 comments on commit ff52a8b

Please sign in to comment.