I am trying to spawn one JVM process from another and make them communicate via RMI. I managed to make it work from IDE, but for some reason when I try to run the code from sbt
it fails with:
java.rmi.ServerError: Error occurred in server thread; nested exception is:
java.lang.NoClassDefFoundError: scala/Option
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:417) ~[na:1.8.0_60]
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:268) ~[na:1.8.0_60]
My problem is figuring out what changes between running it from IDE and SBT.
First I am trying to create registry with random port number to avoid failure due to used port:
@tailrec
def getRegister(attemptsLeft: Integer = 10): (Registry, Integer) = {
val possiblePorts = (1024 to 65536)
val randomPort = possiblePorts(scala.util.Random.nextInt(possiblePorts.size))
Try (LocateRegistry createRegistry randomPort) match {
case Success(registry) => (registry, randomPort)
case Failure(ex) => if (attemptsLeft <= 0) throw ex
else getRegister(attemptsLeft - 1)
}
}
I used LocateRegistry.createRegistry
because it should solve problems with starting and ending RMI process and passing current classpath into it.
When I start child process I copy class path of the parent process - main class is contained within the same project so I can simply copy JVM arguments used to run parent process to make sure that it will have access to the same libraries.
Then child process used following code:
Try {
val server = ... // class which will do the job
val stub = UnicastRemoteObject.exportObject(server, 0).asInstanceOf[Server]
val registry = LocateRegistry getRegistry remotePort
registry.bind(serverName, stub) // throws in SBT, succeeds in IDE
} match {
case Success(_) => logger debug "Remote ready"
case Failure(ex) => logger error("Remote failed", ex)
System exit -1
}
What did I missed? Using LocateRegistry.createRegistry
should copy the class path of the parent process (which uses Option
in several places already, it has to have access to the class), child process has access this class as well (I checked to be sure). Yet for some reason when I run the code from under the sbt
LocateRegistry.createRegistry
fails to pass scala.Option
location to the classpath.
I managed to make it work setting up java.rmi.server.codebase
System property.
I am not sure what actually broke and I would be glad if someone actually explained it. My wild guess it that when I run LocateRegistry getRegistry remotePort
it makes use of "java.class.path"
which is kind of unreliable.
When I start application from IDE it passes all deps directly to JVM - all JARs used appear in java.class.path
. On the other hand when I start it from SBT all I get is /usr/share/sbt-launcher-packaging/bin/sbt-launch.jar
.
I didn't noticed the issue because I don't rely on this property while populating class path argument for child JVM. Instead I used something like:
lazy val javaHome = System getProperty "java.home"
lazy val classPath = System getProperty "java.class.path"
private lazy val jarClassPathPattern = "jar:(file:)?([^!]+)!.+".r
private lazy val fileClassPathPattern = "file:(.+).class".r
def classPathFor[T](clazz: Class[T]): List[String] = {
val pathToClass = getPathToClassFor(clazz)
val propClassPath = classPath split File.pathSeparator toSet
val loaderClassPath = clazz.getClassLoader.asInstanceOf[URLClassLoader].getURLs.map(_.getFile).toSet
val jarClassPath = jarClassPathPattern.findFirstMatchIn(pathToClass) map { matcher =>
val jarDir = Paths get (matcher group 2) getParent()
s"${jarDir}/*"
} toSet
val fileClassPath = fileClassPathPattern.findFirstMatchIn(pathToClass) map { matcher =>
val suffix = "/" + clazz.getName
val fullPath = matcher group 1
fullPath substring (0, fullPath.length - suffix.length)
} toSet
(propClassPath ++ loaderClassPath ++ jarClassPath ++ fileClassPath ++ Set(".")).toList
}
def getPathToClassFor[T](clazz: Class[T]) = {
val url = clazz getResource s"${clazz.getSimpleName}.class"
Try (URLDecoder decode (url.toString, "UTF-8")) match {
case Success(classFilePath) => classFilePath
case Failure(_) => throw new IllegalStateException("")
}
}
After reusing those additional JARs in java.rmi.server.codebase
everything started to work reliably:
def configureRMIFor[T](clazz: Class[T]): Unit = {
val classPath = classPathFor(clazz)
val codebase = if (classPath isEmpty) ""
else classPath map (new File(_).getAbsoluteFile.toURI.toURL.toString) reduce (_ + " " + _)
logger trace s"Set java.rmi.server.codebase to: $codebase"
System setProperty ("java.rmi.server.codebase", codebase)
}
Still, I it would be nice if someone more knowledgeable would come and explain it what exactly made the difference.