From 3ce81b20aa859b11ba1bbc8bbecb249e69f4d4f3 Mon Sep 17 00:00:00 2001 From: Florian Friemel Date: Fri, 21 Dec 2018 14:04:12 +0000 Subject: [PATCH] [CORDA-2311] Create a subclass of CordaCaplet for the web server. (#4448) * Create a subclass of CordaCaplet for the web server to put the cordapp JARs on its class path. * Revert "[CORDA-2303]: Fix issue with corda-webserver not looking for plugins in cordapp jars (#4390)" This reverts commit bad7b9b18778e075f10c382169fd57d2e48b54d5. * Revert "Create a subclass of CordaCaplet for the web server to put the cordapp JARs on its class path." This reverts commit 12f14275c00997f470f6097f0d332b6960268194. * Create seperate webserver caplet --- webserver/build.gradle | 4 + .../src/main/java/CordaWebserverCaplet.java | 228 ++++++++++++++++++ .../corda/webserver/internal/NodeWebServer.kt | 24 +- webserver/webcapsule/build.gradle | 6 +- 4 files changed, 237 insertions(+), 25 deletions(-) create mode 100644 webserver/src/main/java/CordaWebserverCaplet.java diff --git a/webserver/build.gradle b/webserver/build.gradle index eef01e3530..204fbe94dc 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -55,6 +55,10 @@ dependencies { // For rendering the index page. compile "org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.3" + // Capsule is a library for building independently executable fat JARs. + // We only need this dependency to compile our Caplet against. + compileOnly "co.paralleluniverse:capsule:$capsule_version" + integrationTestCompile project(':node-driver') testCompile "junit:junit:$junit_version" } diff --git a/webserver/src/main/java/CordaWebserverCaplet.java b/webserver/src/main/java/CordaWebserverCaplet.java new file mode 100644 index 0000000000..b79e473077 --- /dev/null +++ b/webserver/src/main/java/CordaWebserverCaplet.java @@ -0,0 +1,228 @@ +// Due to Capsule being in the default package, which cannot be imported, this caplet +// must also be in the default package. When using Kotlin there are a whole host of exceptions +// trying to construct this from Capsule, so it is written in Java. + +import com.typesafe.config.*; +import sun.misc.Signal; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +public class CordaWebserverCaplet extends Capsule { + + private Config nodeConfig = null; + private String baseDir = null; + + protected CordaWebserverCaplet(Capsule pred) { + super(pred); + } + + private Config parseConfigFile(List args) { + this.baseDir = getBaseDirectory(args); + String config = getOption(args, "--config-file"); + File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config); + try { + ConfigParseOptions parseOptions = ConfigParseOptions.defaults().setAllowMissing(false); + Config defaultConfig = ConfigFactory.parseResources("reference.conf", parseOptions); + Config baseDirectoryConfig = ConfigFactory.parseMap(Collections.singletonMap("baseDirectory", baseDir)); + Config nodeConfig = ConfigFactory.parseFile(configFile, parseOptions); + return baseDirectoryConfig.withFallback(nodeConfig).withFallback(defaultConfig).resolve(); + } catch (ConfigException e) { + log(LOG_DEBUG, e); + return ConfigFactory.empty(); + } + } + + File getConfigFile(List args, String baseDir) { + String config = getOptionMultiple(args, Arrays.asList("--config-file", "-f")); + return (config == null || config.equals("")) ? new File(baseDir, "node.conf") : new File(config); + } + + String getBaseDirectory(List args) { + String baseDir = getOptionMultiple(args, Arrays.asList("--base-directory", "-b")); + return Paths.get((baseDir == null) ? "." : baseDir).toAbsolutePath().normalize().toString(); + } + + private String getOptionMultiple(List args, List possibleOptions) { + String result = null; + for(String option: possibleOptions) { + result = getOption(args, option); + if (result != null) break; + } + return result; + } + + private String getOption(List args, String option) { + final String lowerCaseOption = option.toLowerCase(); + int index = 0; + for (String arg : args) { + if (arg.toLowerCase().equals(lowerCaseOption)) { + if (index < args.size() - 1 && !args.get(index + 1).startsWith("-")) { + return args.get(index + 1); + } else { + return null; + } + } + + if (arg.toLowerCase().startsWith(lowerCaseOption)) { + if (arg.length() > option.length() && arg.substring(option.length(), option.length() + 1).equals("=")) { + return arg.substring(option.length() + 1); + } else { + return null; + } + } + index++; + } + return null; + } + + @Override + protected ProcessBuilder prelaunch(List jvmArgs, List args) { + checkJavaVersion(); + nodeConfig = parseConfigFile(args); + return super.prelaunch(jvmArgs, args); + } + + // Add working directory variable to capsules string replacement variables. + @Override + protected String getVarValue(String var) { + if (var.equals("baseDirectory")) { + return baseDir; + } else { + return super.getVarValue(var); + } + } + + /** + * Overriding the Caplet classpath generation via the intended interface in Capsule. + */ + @Override + @SuppressWarnings("unchecked") + protected T attribute(Map.Entry attr) { + // Equality is used here because Capsule never instantiates these attributes but instead reuses the ones + // defined as public static final fields on the Capsule class, therefore referential equality is safe. + if (ATTR_APP_CLASS_PATH == attr) { + T cp = super.attribute(attr); + + File cordappsDir = new File(baseDir, "cordapps"); + // Create cordapps directory if it doesn't exist. + if (!checkIfCordappDirExists(cordappsDir)) { + // If it fails, just return the existing class path. The main Corda jar will detect the error and fail gracefully. + return cp; + } + augmentClasspath((List) cp, cordappsDir); + try { + List jarDirs = nodeConfig.getStringList("jarDirs"); + log(LOG_VERBOSE, "Configured JAR directories = " + jarDirs); + for (String jarDir : jarDirs) { + augmentClasspath((List) cp, new File(jarDir)); + } + } catch (ConfigException.Missing e) { + // Ignore since it's ok to be Missing. Other errors would be unexpected. + } catch (ConfigException e) { + log(LOG_QUIET, e); + } + return cp; + } else if (ATTR_JVM_ARGS == attr) { + // Read JVM args from the config if specified, else leave alone. + List jvmArgs = new ArrayList<>((List) super.attribute(attr)); + try { + List configJvmArgs = nodeConfig.getStringList("custom.jvmArgs"); + jvmArgs.clear(); + jvmArgs.addAll(configJvmArgs); + log(LOG_VERBOSE, "Configured JVM args = " + jvmArgs); + } catch (ConfigException.Missing e) { + // Ignore since it's ok to be Missing. Other errors would be unexpected. + } catch (ConfigException e) { + log(LOG_QUIET, e); + } + return (T) jvmArgs; + } else if (ATTR_SYSTEM_PROPERTIES == attr) { + // Add system properties, if specified, from the config. + Map systemProps = new LinkedHashMap<>((Map) super.attribute(attr)); + try { + Config overrideSystemProps = nodeConfig.getConfig("systemProperties"); + log(LOG_VERBOSE, "Configured system properties = " + overrideSystemProps); + for (Map.Entry entry : overrideSystemProps.entrySet()) { + systemProps.put(entry.getKey(), entry.getValue().unwrapped().toString()); + } + } catch (ConfigException.Missing e) { + // Ignore since it's ok to be Missing. Other errors would be unexpected. + } catch (ConfigException e) { + log(LOG_QUIET, e); + } + return (T) systemProps; + } else return super.attribute(attr); + } + + private void augmentClasspath(List classpath, File dir) { + try { + if (dir.exists()) { + // The following might return null if the directory is not there (we check this already) or if an I/O error occurs. + for (File file : dir.listFiles()) { + addToClasspath(classpath, file); + } + } else { + log(LOG_VERBOSE, "Directory to add in Classpath was not found " + dir.getAbsolutePath()); + } + } catch (SecurityException | NullPointerException e) { + log(LOG_QUIET, e); + } + } + + private static void checkJavaVersion() { + String version = System.getProperty("java.version"); + if (version == null || !version.startsWith("1.8")) { + System.err.printf("Error: Unsupported Java version %s; currently only version 1.8 is supported.\n", version); + System.exit(1); + } + } + + private Boolean checkIfCordappDirExists(File dir) { + try { + if (!dir.mkdir() && !dir.exists()) { // It is unlikely to enter this if-branch, but just in case. + logOnFailedCordappDir(); + return false; + } + } + catch (SecurityException | NullPointerException e) { + logOnFailedCordappDir(); + return false; + } + return true; + } + + private void logOnFailedCordappDir() { + log(LOG_VERBOSE, "Cordapps dir could not be created"); + } + + private void addToClasspath(List classpath, File file) { + try { + if (file.canRead()) { + if (file.isFile() && isJAR(file)) { + classpath.add(file.toPath().toAbsolutePath()); + } else if (file.isDirectory()) { // Search in nested folders as well. TODO: check for circular symlinks. + augmentClasspath(classpath, file); + } + } else { + log(LOG_VERBOSE, "File or directory to add in Classpath could not be read " + file.getAbsolutePath()); + } + } catch (SecurityException | NullPointerException e) { + log(LOG_QUIET, e); + } + } + + @Override + protected void liftoff() { + super.liftoff(); + Signal.handle(new Signal("INT"), signal -> { + // Disable Ctrl-C for this process, so the child process can handle it in the shell instead. + }); + } + + private Boolean isJAR(File file) { + return file.getName().toLowerCase().endsWith(".jar"); + } +} diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 9dd793e90f..9ca4e03546 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -5,10 +5,7 @@ import io.netty.channel.unix.Errors import net.corda.client.jackson.JacksonSupport import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.RPCException -import net.corda.core.internal.div import net.corda.core.internal.errors.AddressBindingException -import net.corda.core.internal.exists -import net.corda.core.internal.list import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.contextLogger import net.corda.webserver.WebServerConfig @@ -30,13 +27,9 @@ import java.io.IOException import java.io.Writer import java.lang.reflect.InvocationTargetException import java.net.BindException -import java.net.URL -import java.net.URLClassLoader import java.nio.file.NoSuchFileException -import java.nio.file.Path import java.util.* import javax.servlet.http.HttpServletRequest -import kotlin.streams.toList class NodeWebServer(val config: WebServerConfig) { private companion object { @@ -211,22 +204,9 @@ class NodeWebServer(val config: WebServerConfig) { return connection.proxy } - - private fun jarUrlsInDirectory(directory: Path): List { - return if (!directory.exists()) { - emptyList() - } else { - directory.list { paths -> - // `toFile()` can't be used here... - paths.filter { it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList() - } - } - } - - /** Fetch WebServerPluginRegistry classes registered in META-INF/services/net.corda.webserver.services.WebServerPluginRegistry files that exist in the classpath or in cordapps */ + /** Fetch WebServerPluginRegistry classes registered in META-INF/services/net.corda.webserver.services.WebServerPluginRegistry files that exist in the classpath */ val pluginRegistries: List by lazy { - val urls = jarUrlsInDirectory(config.baseDirectory / "cordapps").toTypedArray() - ServiceLoader.load(WebServerPluginRegistry::class.java, URLClassLoader(urls, javaClass.classLoader)).toList() + ServiceLoader.load(WebServerPluginRegistry::class.java).toList() } /** Used for useful info that we always want to show, even when not logging to the console */ diff --git a/webserver/webcapsule/build.gradle b/webserver/webcapsule/build.gradle index 9d63c537a3..eae2c99121 100644 --- a/webserver/webcapsule/build.gradle +++ b/webserver/webcapsule/build.gradle @@ -38,8 +38,8 @@ task buildWebserverJar(type: FatCapsule, dependsOn: project(':node').tasks.jar) applicationSource = files( project(':webserver').configurations.runtimeClasspath, project(':webserver').tasks.jar, - project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet.class', - project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet$1.class', + project(':webserver').sourceSets.main.java.outputDir.toString() + '/CordaWebserverCaplet.class', + project(':webserver').sourceSets.main.java.outputDir.toString() + '/CordaWebserverCaplet$1.class', project(':node').buildDir.toString() + '/resources/main/reference.conf', "$rootDir/config/dev/log4j2.xml", project(':node:capsule').projectDir.toString() + '/NOTICE' // Copy CDDL notice @@ -52,7 +52,7 @@ task buildWebserverJar(type: FatCapsule, dependsOn: project(':node').tasks.jar) systemProperties['visualvm.display.name'] = 'Corda Webserver' minJavaVersion = '1.8.0' minUpdateVersion['1.8'] = java8_minUpdateVersion - caplets = ['CordaCaplet'] + caplets = ['CordaWebserverCaplet'] // JVM configuration: // - Constrain to small heap sizes to ease development on low end devices.