blended.launcher.jvmrunner.JvmLauncher.scala Maven / Gradle / Ivy
package blended.launcher.jvmrunner
import java.io.{File, FileInputStream}
import java.util.Properties
import blended.launcher.internal.{ARM, Logger}
import blended.updater.config.OverlayConfigCompanion
import scala.collection.JavaConverters._
import scala.util.Try
import scala.util.control.NonFatal
object JvmLauncher {
private[this] lazy val log = Logger[JvmLauncher.type]
private[this] lazy val launcher = new JvmLauncher()
def main(args: Array[String]): Unit = {
try {
val exitVal = launcher.run(args)
sys.exit(exitVal)
} catch {
case NonFatal(e) => sys.exit(1)
}
}
}
/**
* A small Java wrapper rresponsiblefor controlling the actual Container JVM.
*/
class JvmLauncher() {
private[this] lazy val log = Logger[JvmLauncher]
private[this] var runningProcess: Option[RunningProcess] = None
private[this] val shutdownHook = new Thread("jvm-launcher-shutdown-hook") {
override def run(): Unit = {
log.info("Caught shutdown. Stopping process")
runningProcess foreach { p =>
p.stop()
}
}
}
Runtime.getRuntime.addShutdownHook(shutdownHook)
def run(args: Array[String]): Int = {
val config = checkConfig(parse(args)).get
log.debug("JvmLauncherConfig = " + config)
config.action match {
// Try to start the inner JVM
case Some("start") =>
log.debug("Request: start process")
runningProcess match {
case Some(_) =>
log.debug("Process already running")
sys.error("Already started")
case None =>
var retVal = 1
do {
// If the container JVM terminated with exit code 2, we will restart
// the container JVM.
if (retVal == 2) {
config.restartDelaySec match {
// In some cases we need a cool down period before we restart the container
// JVM. In that case we will wait for the specified number of seconds.
case Some(delay) =>
log.debug("Waiting " + delay + " seconds before restarting the container.")
try {
Thread.sleep(delay * 1000)
} catch {
case e: InterruptedException =>
log.debug("Delay interrupted!")
}
case _ =>
}
log.debug("About to restart the container process.")
} else {
log.debug("About to start process")
}
// Starting the container requires 2 steps:
// First, we will start the container with the parameter --write-system-properties.
// This will cause that the container runtime evaluates the current profile and it's
// overlays. In this mode the container will dump the determined System Properties
// into a file and terminate.
//
// We will then start the container with the calculated system properties.
//
// This gives us the opportunity to handle properties configuring the JVM itself with
// the blended overlay mechanism.
log.info("-" * 80)
log.info("Starting container in write properties mode")
log.info("-" * 80)
val sysProps: Map[String, String] = {
val sysPropsFile = File.createTempFile("jvmlauncher", ".properties")
val p = startJava(
classpath = config.classpath,
jvmOpts = config.jvmOpts.toArray,
arguments = config.otherArgs.toArray ++ Array("--write-system-properties", sysPropsFile.getAbsolutePath()),
interactive = false,
errorsIntoOutput = false,
directory = new File(".").getAbsoluteFile()
)
val retVal = p.waitFor
if (retVal != 0) {
log.error(s"The launcher-process to retrieve the JVM properties exited with an unexpected return code: ${retVal}. We try to read the properties file anyway!")
}
val props = new Properties()
Try {
ARM.using(new FileInputStream(sysPropsFile)) { inStream =>
props.load(inStream)
}
}.recover {
case e: Throwable => log.error("Could not read properties file", e)
}
sysPropsFile.delete()
props.asScala.toList.toMap
}
// Now we can extract the JVM memory settings if given
val xmsOpt = sysProps.collect { case (OverlayConfigCompanion.Properties.JVM_USE_MEM, x) => s"-Xms${x}" }
val xmxOpt = sysProps.collect { case (OverlayConfigCompanion.Properties.JVM_MAX_MEM, x) => s"-Xmx${x}" }
val ssOpt = sysProps.collect { case (OverlayConfigCompanion.Properties.JVM_STACK_SIZE, x) => s"-Xss${x}" }
log.info("-" * 80)
log.info("Starting blended container instance")
log.info("-" * 80)
val p = startJava(
classpath = config.classpath,
jvmOpts = (config.jvmOpts ++ xmsOpt ++ xmxOpt ++ ssOpt ++ sysProps.map { case (k, v) => s"-D${k}=${v}" }).toArray,
arguments = config.otherArgs.toArray,
interactive = config.interactive,
errorsIntoOutput = false,
directory = new File(".").getAbsoluteFile()
)
log.debug("Process started: " + p)
runningProcess = Option(p)
retVal = p.waitFor
log.info("-" * 80)
log.info(s"Blended container instance terminated with exit code [$retVal]")
log.info("-" * 80)
runningProcess = None
} while (retVal == 2)
retVal
}
// try to stop the inner JVM
case Some("stop") =>
log.debug("Request: stop process")
runningProcess match {
case None =>
log.debug("No process running")
sys.error("Not started")
case Some(p) =>
p.stop()
}
// All other commands are considered to be errors
case a @ _ =>
sys.error(s"Not a valid action : [$a]")
}
}
case class Config(
classpath: Seq[File] = Seq(),
otherArgs: Seq[String] = Seq(),
action: Option[String] = None,
jvmOpts: Seq[String] = Seq(),
interactive: Boolean = true,
restartDelaySec: Option[Int] = None) {
private[this] lazy val prettyPrint : String =
s"""${getClass().getSimpleName()}(
|classpath=
|${classpath.mkString(" ", "\n ", "")},
|action=${action},
|otherArgs=
|${otherArgs.mkString(" ", "\n ", "")}
|)""".stripMargin
override def toString(): String = prettyPrint
}
def parse(args: Seq[String], initialConfig: Config = Config()): Config = {
args match {
case Seq() =>
initialConfig
case Seq("--", rest @ _*) =>
initialConfig.copy(otherArgs = rest)
case Seq("start", rest @ _*) if initialConfig.action.isEmpty =>
parse(rest, initialConfig.copy(action = Option("start")))
case Seq("stop", rest @ _*) if initialConfig.action.isEmpty =>
parse(rest, initialConfig.copy(action = Option("stop")))
case Seq(cp, rest @ _*) if initialConfig.classpath.isEmpty && cp.startsWith("-cp=") =>
// Also support ":" on non-windows platform
val cps = cp.substring("-cp=".length).split("[;]").toSeq.map(_.trim()).filter(!_.isEmpty).map(new File(_))
parse(rest, initialConfig.copy(classpath = cps))
case Seq(delay, rest @ _*) if initialConfig.restartDelaySec.isEmpty && delay.startsWith("-restartDelay=") =>
val delaySec = delay.substring("-restartDelay=".length).toInt
parse(rest, initialConfig.copy(restartDelaySec = Option(delaySec)))
case Seq(jvmOpt, rest @ _*) if jvmOpt.startsWith("-jvmOpt=") =>
val opt = jvmOpt.substring("-jvmOpt=".length).trim()
parse(rest, initialConfig.copy(jvmOpts = initialConfig.jvmOpts ++ Seq(opt).filter(!_.isEmpty)))
case Seq(interactive, rest @_*) if interactive.startsWith("-interactive") =>
val iAct = interactive.substring("-interactive=".length).toBoolean
parse(rest, initialConfig.copy(interactive = iAct))
case _ =>
sys.error("Cannot parse arguments: " + args)
}
}
def checkConfig(config: Config): Try[Config] = Try {
if (config.action.isEmpty) sys.error("Missing arguments for action: start|stop")
if (config.classpath.isEmpty) Console.err.println("Warning: No classpath given")
config
}
def startJava(classpath: Seq[File],
jvmOpts: Array[String],
arguments: Array[String],
interactive: Boolean = false,
errorsIntoOutput: Boolean = true,
directory: File = new File(".")
): RunningProcess = {
log.debug("About to run Java process")
// lookup java by JAVA_HOME env variable
val javaHome = System.getenv("JAVA_HOME")
val java =
if (javaHome != null) s"${
javaHome
}/bin/java"
else "java"
log.debug("Using java executable: " + java)
val cpArgs = classpath match {
case null | Seq() => Array[String]()
case cp => Array("-cp", pathAsArg(classpath))
}
log.debug("Using classpath args: " + cpArgs.mkString(" "))
val propArgs = System.getProperties.asScala.map(p => s"-D${
p._1
}=${
p._2
}").toArray[String]
log.debug("Using property args: " + propArgs.mkString(" "))
val command = Array(java) ++ cpArgs ++ jvmOpts ++ propArgs ++ arguments
val pb = new ProcessBuilder(command: _*)
log.debug("Run command: " + command.mkString(" "))
pb.environment().putAll(sys.env.asJava)
pb.directory(directory)
// if (!env.isEmpty) env.foreach { case (k, v) => pb.environment().put(k, v) }
val p = pb.start
new RunningProcess(p, errorsIntoOutput, interactive)
}
/**
* Converts a Seq of files into a string containing the absolute file paths concatenated with the platform specific path separator (":" on Unix, ";" on Windows).
*/
def pathAsArg(paths: Seq[File]): String = paths.map(p => p.getPath).mkString(File.pathSeparator)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy