Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get process PID, use it to send SIGTERM #120

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/main/scala/scala/sys/process/ProcessWithPid.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package scala.sys.process
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be in the scala.sys.process package so it has access to Process.SimpleProcess, which is package private


import java.lang.{Process => JProcess}
import sbt.{Level, Logger}
import scala.util.control.NonFatal

case class ProcessWithPid(process: Process, pid: Option[Long])

object ProcessWithPid {
private def reflectJProcess(p: Process.SimpleProcess): JProcess = {
val field = p.getClass.getDeclaredField("p")
field.setAccessible(true)
field.get(p).asInstanceOf[JProcess]
}

// Java 9+ has a `Process#pid()` method, but Java 8 and below have a private `Process#pid` field
// We first try to reflect on the method and then fall back to reflecting on the field
private def reflectJProcessPid(p: JProcess): Long =
try {
val method = classOf[JProcess].getMethod("pid")
method.invoke(p) match {
case pid: java.lang.Long => pid
case pid => throw new RuntimeException(s"Expected process PID ($pid) to be a Long, but it was a ${pid.getClass.getName}")
}
Comment on lines +20 to +24
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're content with requiring everyone who develops sbt-revolver to use Java 9+ to do so, then this could be simplified to just p.pid, plus changing the catch block to handle NoSuchMethodError when users are on Java 8.

Otherwise, compiling this on Java 8 fails with an error

/Users/matt/sbt-revolver/src/main/scala/scala/sys/process/ProcessWithPid.scala:21:9: value pid is not a member of Process
      p.pid
        ^

} catch {
case e: NoSuchMethodException =>
val field = p.getClass.getDeclaredField("pid")
field.setAccessible(true)
field.getLong(p)
}

def apply(process: Process, log: Logger): ProcessWithPid =
try {
process match {
case p: Process.SimpleProcess =>
val jp = reflectJProcess(p)
val pid = reflectJProcessPid(jp)
ProcessWithPid(process, Some(pid))

case p =>
throw new RuntimeException(s"Expected app process to be a Process.SimpleProcess but it was a ${p.getClass.getName}")
}
} catch {
case NonFatal(e) =>
log.log(Level.Warn, s"Failed to determine process PID: $e")
ProcessWithPid(process, None)
}
}
6 changes: 3 additions & 3 deletions src/main/scala/spray/revolver/Actions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package spray.revolver
import sbt.Keys._
import sbt.{Fork, ForkOptions, LoggedOutput, Logger, Path, ProjectRef, State, complete}
import java.io.File
import scala.sys.process.Process
import scala.sys.process.ProcessWithPid

object Actions {
import Utilities._
Expand Down Expand Up @@ -129,13 +129,13 @@ object Actions {
def formatAppName(projectName: String, projectColor: String, color: String = "[YELLOW]"): String =
"[RESET]%s%s[RESET]%s" format (projectColor, projectName, color)

def forkRun(config: ForkOptions, mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger, extraJvmArgs: Seq[String]): Process = {
def forkRun(config: ForkOptions, mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger, extraJvmArgs: Seq[String]): ProcessWithPid = {
log.info(options.mkString("Starting " + mainClass + ".main(", ", ", ")"))
val scalaOptions = "-classpath" :: Path.makeString(classpath) :: mainClass :: options.toList
val newOptions = config
.withOutputStrategy(config.outputStrategy getOrElse LoggedOutput(log))
.withRunJVMOptions(config.runJVMOptions ++ extraJvmArgs)

Fork.java.fork(newOptions, scalaOptions)
ProcessWithPid(Fork.java.fork(newOptions, scalaOptions), log)
}
}
27 changes: 21 additions & 6 deletions src/main/scala/spray/revolver/AppProcess.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,38 @@
package spray.revolver

import java.lang.{Runtime => JRuntime}
import java.util.concurrent.TimeUnit
import sbt.{Logger, ProjectRef}

import scala.sys.process.Process
import scala.sys.process.ProcessWithPid

/**
* A token which we put into the SBT state to hold the Process of an application running in the background.
*/
case class AppProcess(projectRef: ProjectRef, consoleColor: String, log: Logger)(process: Process) {
case class AppProcess(projectRef: ProjectRef, consoleColor: String, log: Logger)(process: ProcessWithPid) {
val shutdownHook = createShutdownHook("... killing ...")

private def destroyProcess(): Unit = process.process.destroy()

private def killProcess(pid: Long): Unit = {
val exited = try {
JRuntime.getRuntime.exec(s"kill -15 $pid").waitFor(10, TimeUnit.SECONDS)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open to suggestions on how long this waits

} catch { case e: InterruptedException => true }

if (!exited) destroyProcess()
}

private def stopProcess(): Int = {
process.pid.fold(destroyProcess())(killProcess)
process.process.exitValue()
}

def createShutdownHook(msg: => String) =
new Thread(new Runnable {
def run() {
if (isRunning) {
log.info(msg)
process.destroy()
stopProcess()
}
}
})
Expand All @@ -42,7 +58,7 @@ case class AppProcess(projectRef: ProjectRef, consoleColor: String, log: Logger)
val watchThread = {
val thread = new Thread(new Runnable {
def run() {
val code = process.exitValue()
val code = process.process.exitValue()
finishState = Some(code)
log.info("... finished with exit code %d" format code)
unregisterShutdownHook()
Expand All @@ -58,8 +74,7 @@ case class AppProcess(projectRef: ProjectRef, consoleColor: String, log: Logger)

def stop() {
unregisterShutdownHook()
process.destroy()
process.exitValue()
stopProcess()
}

def registerShutdownHook() {
Expand Down