Skip to content

Commit

Permalink
Port killTree and killTreeForcibly to ZIO 2.0 branch (#209)
Browse files Browse the repository at this point in the history
* Add `killTree` and `killTreeForcibly`

* Fix yaml indentation

* Fix tests

* Fix test script permissions

* Fix flaky test

* Try different implementation for killTree

* Fix tests

* Switch to another implementation of killTree
  • Loading branch information
reibitto authored Dec 16, 2021
1 parent 39168e5 commit 6c1165f
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 7 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
fetch-depth: 0
- name: Setup Scala and Java
uses: olafurpg/setup-scala@v13
with:
java-version: [email protected]
- name: Cache scala dependencies
uses: coursier/cache-action@v6
- name: Lint code
Expand All @@ -36,6 +38,8 @@ jobs:
uses: actions/[email protected]
- name: Setup Scala and Java
uses: olafurpg/setup-scala@v13
with:
java-version: [email protected]
- name: Cache scala dependencies
uses: coursier/cache-action@v6
- name: Check Document Generation
Expand All @@ -47,7 +51,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java: ['adopt@1.8', 'adopt@1.11']
java: ['[email protected]']
scala: ['2.11.12', '2.12.15', '2.13.7', '3.1.0']
steps:
- name: Checkout current branch
Expand Down Expand Up @@ -80,7 +84,9 @@ jobs:
uses: actions/[email protected]
with:
fetch-depth: 0
- name: Setup Scala and Java
- name: Setup Scala and Java
with:
java-version: [email protected]
uses: olafurpg/setup-scala@v13
- name: Cache scala dependencies
uses: coursier/cache-action@v6
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: olafurpg/setup-scala@v13
with:
java-version: [email protected]
- run: sbt docs/docusaurusPublishGhpages
env:
GIT_DEPLOY_KEY: ${{ secrets.GIT_DEPLOY_KEY }}
6 changes: 4 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ logo :=
| | |
| |_|
|
| ${version.value}
|
| ${version.value}
|
| Scala ${scalaVersion.value}
|
|""".stripMargin
logoColor := scala.Console.RED
usefulTasks := Seq(
Expand Down
3 changes: 2 additions & 1 deletion project/BuildHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ object BuildHelper {
"-Ywarn-numeric-widen",
"-Ywarn-value-discard",
"-Xlint:_,-type-parameter-shadow",
"-Xsource:2.13"
"-Xsource:2.13",
"-target:jvm-1.8"
)

private def extraOptions(scalaVersion: String) =
Expand Down
43 changes: 43 additions & 0 deletions src/main/scala/zio/process/Process.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import zio.ZIO.{ attemptBlockingCancelable, attemptBlockingInterrupt }
import zio.{ ExitCode, UIO, ZIO }

import java.lang.{ Process => JProcess }
import scala.jdk.CollectionConverters._

final case class Process(private val process: JProcess) {

Expand Down Expand Up @@ -71,6 +72,48 @@ final case class Process(private val process: JProcess) {
()
}

/**
* Kills the entire process tree and will wait until completed. Equivalent to SIGTERM on Unix platforms.
*
* Note: This method requires JDK 9+
*/
def killTree: ZIO[Any, CommandError, Unit] =
execute { process =>
val descendants = process.descendants().iterator().asScala.toSeq
descendants.foreach(_.destroy())

process.destroy()
process.waitFor()

descendants.foreach { p =>
if (p.isAlive) {
p.onExit().get // `ProcessHandle` doesn't have waitFor
()
}
}
}

/**
* Kills the entire process tree and will wait until completed. Equivalent to SIGKILL on Unix platforms.
*
* Note: This method requires JDK 9+
*/
def killTreeForcibly: ZIO[Any, CommandError, Unit] =
execute { process =>
val descendants = process.descendants().iterator().asScala.toSeq
descendants.foreach(_.destroyForcibly())

process.destroyForcibly()
process.waitFor()

descendants.foreach { p =>
if (p.isAlive) {
p.onExit().get // `ProcessHandle` doesn't have waitFor
()
}
}
}

/**
* Return the exit code of this process if it is zero. If non-zero, it will fail with `CommandError.NonZeroErrorCode`.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/test/bash/kill-test/sample-child.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
echo $$
sleep 30
echo -n "end: "
echo $$
7 changes: 7 additions & 0 deletions src/test/bash/kill-test/sample-parent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash
echo $$
./sample-child.sh &
./sample-child.sh &
sleep 30
echo -n "end: "
echo $$
50 changes: 49 additions & 1 deletion src/test/scala/zio/process/CommandSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import zio.{ durationInt, Chunk, ExitCode, ZIO }

import java.io.File
import java.nio.charset.StandardCharsets
import java.util.Optional

// TODO: Add aspects for different OSes? scala.util.Properties.isWin, etc. Also try to make this as OS agnostic as possible in the first place
object CommandSpec extends ZIOProcessBaseSpec {
Expand Down Expand Up @@ -134,6 +135,53 @@ object CommandSpec extends ZIOProcessBaseSpec {
for {
exit <- Command("ls").workingDirectory(new File("/some/bad/path")).lines.exit
} yield assert(exit)(fails(isSubtype[CommandError.WorkingDirectoryMissing](anything)))
}
},
test("kill only kills parent process") {
for {
process <- Command("./sample-parent.sh").workingDirectory(new File("src/test/bash/kill-test")).run
pids <- process.stdout.stream
.via(ZPipeline.utf8Decode)
.via(ZPipeline.splitLines)
.take(3)
.runCollect
.map(_.map(_.toInt))
_ <- process.kill
pidsAlive = pids.map { pid =>
toScalaOption(ProcessHandle.of(pid.toLong)).exists(_.isAlive)
}
} yield assertTrue(pidsAlive == Chunk(false, true, true))
} @@ TestAspect.nonFlaky(25),
test("killTree also kills child processes") {
for {
process <- Command("./sample-parent.sh").workingDirectory(new File("src/test/bash/kill-test")).run
pids <- process.stdout.stream
.via(ZPipeline.utf8Decode)
.via(ZPipeline.splitLines)
.take(3)
.runCollect
.map(_.map(_.toInt))
_ <- process.killTree
pidsAlive = pids.map { pid =>
toScalaOption(ProcessHandle.of(pid.toLong)).exists(_.isAlive)
}
} yield assertTrue(pidsAlive == Chunk(false, false, false))
} @@ TestAspect.nonFlaky(25),
test("killTreeForcibly also kills child processes") {
for {
process <- Command("./sample-parent.sh").workingDirectory(new File("src/test/bash/kill-test")).run
pids <- process.stdout.stream
.via(ZPipeline.utf8Decode)
.via(ZPipeline.splitLines)
.take(3)
.runCollect
.map(_.map(_.toInt))
_ <- process.killTree
pidsAlive = pids.map { pid =>
toScalaOption(ProcessHandle.of(pid.toLong)).exists(_.isAlive)
}
} yield assertTrue(pidsAlive == Chunk(false, false, false))
} @@ TestAspect.nonFlaky(25)
)

private def toScalaOption[A](o: Optional[A]): Option[A] = if (o.isPresent) Some(o.get) else None
}
2 changes: 1 addition & 1 deletion src/test/scala/zio/process/ZIOProcessBaseSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import zio.durationInt
import zio.test._

trait ZIOProcessBaseSpec extends DefaultRunnableSpec {
override def aspects = List(TestAspect.timeout(5.seconds))
override def aspects = List(TestAspect.timeout(30.seconds))
}

0 comments on commit 6c1165f

Please sign in to comment.