├── src └── main │ ├── resources │ └── bin │ │ ├── procrun │ │ ├── prunsrv.exe │ │ └── x64 │ │ │ └── prunsrv.exe │ │ └── jsvc │ │ ├── linux64-brew │ │ └── jsvc │ │ └── macosx-yosemite-brew │ │ └── jsvc │ └── java │ ├── co │ └── paralleluniverse │ │ └── capsule │ │ └── daemon │ │ └── DaemonAdapter.java │ └── DaemonCapsule.java ├── .gitignore └── README.md /src/main/resources/bin/procrun/prunsrv.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puniverse/capsule-daemon/HEAD/src/main/resources/bin/procrun/prunsrv.exe -------------------------------------------------------------------------------- /src/main/resources/bin/jsvc/linux64-brew/jsvc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puniverse/capsule-daemon/HEAD/src/main/resources/bin/jsvc/linux64-brew/jsvc -------------------------------------------------------------------------------- /src/main/resources/bin/procrun/x64/prunsrv.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puniverse/capsule-daemon/HEAD/src/main/resources/bin/procrun/x64/prunsrv.exe -------------------------------------------------------------------------------- /src/main/resources/bin/jsvc/macosx-yosemite-brew/jsvc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puniverse/capsule-daemon/HEAD/src/main/resources/bin/jsvc/macosx-yosemite-brew/jsvc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /dist/ 3 | /target/ 4 | /out/ 5 | _site/ 6 | /docs/javadoc/ 7 | .idea/ 8 | *.iml 9 | /artifacts 10 | /nbproject/private/ 11 | /.nb-gradle 12 | build/ 13 | .gradle/ 14 | .nb-gradle-properties 15 | .bdb/ 16 | -------------------------------------------------------------------------------- /src/main/java/co/paralleluniverse/capsule/daemon/DaemonAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Capsule 3 | * Copyright (c) 2015, Parallel Universe Software Co. and Contributors. All rights reserved. 4 | * 5 | * This program and the accompanying materials are licensed under the terms 6 | * of the Eclipse Public License v1.0, available at 7 | * http://www.eclipse.org/legal/epl-v10.html 8 | */ 9 | package co.paralleluniverse.capsule.daemon; 10 | 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.lang.reflect.Method; 13 | 14 | /** 15 | * @author circlespainter 16 | */ 17 | public class DaemonAdapter { 18 | 19 | public static final String PROP_INIT_CLASS = "capsule.daemon.initClass"; 20 | public static final String PROP_INIT_METHOD = "capsule.daemon.initMethod"; 21 | public static final String PROP_START_CLASS = "capsule.daemon.startClass"; 22 | public static final String PROP_START_METHOD = "capsule.daemon.startMethod"; 23 | public static final String PROP_STOP_CLASS = "capsule.daemon.stopClass"; 24 | public static final String PROP_STOP_METHOD = "capsule.daemon.stopMethod"; 25 | public static final String PROP_DESTROY_CLASS = "capsule.daemon.destroyClass"; 26 | public static final String PROP_DESTROY_METHOD = "capsule.daemon.destroyMethod"; 27 | 28 | private static String[] mainArgs; 29 | 30 | public static void init(String args[]) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException { 31 | mainArgs = (String[]) i(p(PROP_INIT_CLASS), p(PROP_INIT_METHOD), args); 32 | } 33 | 34 | public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException { 35 | i(p(PROP_START_CLASS), p(PROP_START_METHOD), new Object[]{args}, STRING_ARRAY_ARG_TYPES); 36 | } 37 | 38 | public static void start() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException { 39 | main(mainArgs); 40 | } 41 | 42 | public static void stop() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException { 43 | i(p(PROP_STOP_CLASS), p(PROP_STOP_METHOD)); 44 | } 45 | 46 | public static void destroy() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException { 47 | i(p(PROP_DESTROY_CLASS), p(PROP_DESTROY_METHOD)); 48 | } 49 | 50 | private static String p(String s) { 51 | return System.getProperty(s); 52 | } 53 | 54 | private static Object i(String className, String methodName, Object o, Object[] args, Class[] argTypes) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException { 55 | if (className != null && methodName != null) 56 | return m(className, methodName, argTypes).invoke(o, args); 57 | return args; 58 | } 59 | 60 | private static Object i(String className, String methodName) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException { 61 | return i(className, methodName, null, null, null); 62 | } 63 | 64 | private static final Class[] STRING_ARRAY_ARG_TYPES = new Class[]{String[].class}; 65 | 66 | private static Object i(String className, String methodName, String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException { 67 | return i(className, methodName, args, args != null ? STRING_ARRAY_ARG_TYPES : null); 68 | } 69 | 70 | private static Object i(String className, String methodName, Object[] args, Class[] types) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException { 71 | return i(className, methodName, null, args, types); 72 | } 73 | 74 | private static Method m(String className, String methodName, Class[] types) throws ClassNotFoundException, NoSuchMethodException { 75 | return DaemonAdapter.class.getClassLoader().loadClass(className).getMethod(methodName, types); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capsule Daemon 2 | 3 | A [caplet](https://github.com/puniverse/capsule#what-are-caplets) that runs a Java [capsule](https://github.com/puniverse/capsule) as a Unix service through [jsvc](http://commons.apache.org/proper/commons-daemon/jsvc.html) and as a Windows service through [procrun](http://commons.apache.org/proper/commons-daemon/procrun.html). 4 | 5 | **NOTE**: currently `capsule-daemon` [doesn't support `Application-Script` capsules](https://github.com/puniverse/capsule-daemon/issues/11). 6 | 7 | ## Requirements 8 | 9 | In addition to [Capsule's](https://github.com/puniverse/capsule), only if the platform is not Windows, Linux 64 bit nor Mac OS X then [jsvc](http://commons.apache.org/proper/commons-daemon/jsvc.html) must be correctly installed locally. 10 | 11 | ## Usage 12 | 13 | The Gradle-style dependency you need to embed in your Capsule JAR, which you can generate with the tool you prefer (f.e. with plain Maven/Gradle as in [Photon](https://github.com/puniverse/photon) and [`capsule-gui-demo`](https://github.com/puniverse/capsule-gui-demo) or higher-level [Capsule build plugins](https://github.com/puniverse/capsule#build-tool-plugins)), is `co.paralleluniverse:capsule-daemon:0.2.0`. Also include the caplet class in your Capsule manifest, for example: 14 | 15 | ``` gradle 16 | Caplets: MavenCapsule DaemonCapsule 17 | ``` 18 | 19 | `capsule-daemon` can also be run as a wrapper capsule without embedding it: 20 | 21 | ``` bash 22 | $ java -Dcapsule.log=verbose -jar capsule-daemon-0.2.0.jar my-capsule.jar my-capsule-arg1 ... 23 | ``` 24 | 25 | It can be both run against (or embedded in) plain (e.g. "fat") capsules and [Maven-based](https://github.com/puniverse/capsule-maven) ones. 26 | 27 | ## Additional Capsule manifest entries 28 | 29 | The following additional manifest entries and system properties can be used to customize `capsule-daemon`'s behaviour (see the [jsvc docs](http://commons.apache.org/proper/commons-daemon/jsvc.html) and the [procrun docs](http://commons.apache.org/proper/commons-daemon/procrun.html) for further details): 30 | 31 | - Both Unix and Windows: 32 | - Manifest entries: 33 | - `Daemon-Start-Class`: class containing the `start` method (default: app's main). 34 | - `Daemon-Start-Method`: static `String[] -> void` service start method short name run as the specified, if any (default: app's main). 35 | - `Daemon-Stop-Class`: class containing the `stop` method (default: none). 36 | - `Daemon-Stop-Method`: static `String[] -> void` service stop method short name run as the specified, if any (default: none). 37 | - `Daemon-User`: the username under which the service will run. The `capsule.daemon.user` system property can override it. 38 | - `Daemon-Cwd`: working directory of start/stop (default: `/` on Unix). The `capsule.daemon.cwd` system property can override it. 39 | - `Daemon-Stdout-File`: stdout (default: `/dev/null` on Unix, `/service-stdout.YEAR-MONTH-DAY.log` on Windows). The `capsule.daemon.stdoutFile` system property can override it. 40 | - `Daemon-Stderr-File`: stdout (default: `/dev/null` on Unix, `/service-stderr.YEAR-MONTH-DAY.log` on Windows). . The `capsule.daemon.stderrFile` system property can override it. 41 | - `Daemon-PID-File`: PID file (default: `/var/run/.pid` on Unix, `/.pid` on Windows). The `capsule.daemon.pidFile` system property can override it. 42 | - System properties: 43 | - `capsule.daemon.stop`: if `true` or barely present will stop a running service rather than starting one. 44 | - Only Unix: 45 | - System properties: 46 | - `capsule.daemon.checkOnly`: `jsvc` check run, won't start the service. 47 | - `capsule.daemon.debug`: turn on debug `jsvc` logging. 48 | - `capsule.daemon.verbose`: turn on verbose `jsvc` logging. 49 | - `capsule.daemon.jsvc`: specifies the pathname of a system-installed `jsvc` command to be used instead of the one provided by `capsule-daemon`. 50 | - Manifest entries: 51 | - `Init-Class`: class containing the `init` method (default: none). 52 | - `Init-Method`: static `String[] -> String[]` service initialization method, it will be run as `root`; the return value will be passed to the `Start` method (default: none). 53 | - `Destroy-Class`: class containing the `destroy` method (default: none). 54 | - `Destroy-Method`: static `void -> void` cleanup method, it will be run as `root` (default: none). 55 | - `No-Detach`: don't detach from the parent process. The `capsule.daemon.noDetach` system property can override it. 56 | - `Keep-Stdin`: don't redirect the standard input to `/dev/null`. The `capsule.daemon.keepStdin` system property can override it. 57 | - `Wait-Secs`: Wait seconds for service readiness, must be multiple of 10. The `capsule.daemon.waitSecs` system property can override it. 58 | - Only Windows 59 | - `Daemon-Password`: the password of the user under which the service will run (default: none). The `capsule.daemon.password` system property can override it. 60 | - `Daemon-Java-Exec-User`: the password of the user that will execute the final Java process (default: none). The `capsule.daemon.javaExecUser` system property can override it. 61 | - `Daemon-Java-Exec-Password`: the password of the user that will execute the final Java process (default: none). The `capsule.daemon.javaExecPassword` system property can override it. 62 | - `Daemon-Service-Name`: the service internal name (default: app ID). The `capsule.daemon.serviceName` system property can override it. 63 | - `Daemon-Display-Name`: the service display name (default: app ID). The `capsule.daemon.displayName` system property can override it. 64 | - `Daemon-Description`: the service description (default: app ID). The `capsule.daemon.description` system property can override it. 65 | - `Daemon-Startup`: the service startup mode, either `auto` or `manual` (default: `manual`). The `capsule.daemon.startup` system property can override it. 66 | - `Daemon-Type`: the service type, it can be `interactive` (default: none). The `capsule.daemon.type` system property can override it. 67 | - `Daemon-DependsOn`: the list of service dependencies (default: none). The `capsule.daemon.dependsOn` system property can override it. 68 | - `Daemon-Stop-Params`: the list of service stop parameters (default: none). The `capsule.daemon.stopParams` system property can override it. 69 | - `Daemon-Stop-Timeout`: service stop timeout in seconds (default: none). The `capsule.daemon.stopTimeout` system property can override it. 70 | - `Daemon-Log-Path`: the log path (default: `%SystemRoot%\System32\LogFiles\Apache`). The `capsule.daemon.logPath` system property can override it. 71 | - `Daemon-Log-Prefix`: the log prefix (default: app ID). The `capsule.daemon.logPrefix` system property can override it. 72 | - `Daemon-Log-Level`: the log level between `error`, `info`, `warn` and `debug` (default: `info`). The `capsule.daemon.logLevel` system property can override it. 73 | 74 | ## Notes 75 | 76 | * `jsvc` with default settings (due to the default PID file location) and `procrun` in any case (for service installation, uninstallation and upgrade) require resp. `root` and administrative privileges. 77 | * Launch, Java and service execution users must all be able to access the same Capsule's cache directory. You can set it to a commonly accessible location (for example in `/tmp/capsule`) through the `CAPSULE_CACHE_DIR` environment variable. 78 | 79 | ## License 80 | 81 | Copyright (c) 2015, Parallel Universe Software Co. and Contributors. All rights reserved. 82 | 83 | This program and the accompanying materials are licensed under the terms 84 | of the Eclipse Public License v1.0 as published by the Eclipse Foundation. 85 | 86 | http://www.eclipse.org/legal/epl-v10.html 87 | -------------------------------------------------------------------------------- /src/main/java/DaemonCapsule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Capsule 3 | * Copyright (c) 2015-2016, Parallel Universe Software Co. and Contributors. All rights reserved. 4 | * 5 | * This program and the accompanying materials are licensed under the terms 6 | * of the Eclipse Public License v1.0, available at 7 | * http://www.eclipse.org/legal/epl-v10.html 8 | */ 9 | 10 | import co.paralleluniverse.capsule.daemon.DaemonAdapter; 11 | 12 | import java.io.*; 13 | import java.net.URI; 14 | import java.net.URISyntaxException; 15 | import java.net.URL; 16 | import java.nio.charset.Charset; 17 | import java.nio.file.*; 18 | import java.nio.file.attribute.FileTime; 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.jar.JarInputStream; 24 | import java.util.jar.Manifest; 25 | import java.util.regex.Matcher; 26 | import java.util.regex.Pattern; 27 | 28 | /** 29 | * A caplet that will use jsvc (needs to be installed) to launch the application as an Unix daemon. Several configuration options are provided. 30 | * 31 | * @author circlespainter 32 | * @see jsvc 33 | * @see jsvc 34 | */ 35 | public class DaemonCapsule extends Capsule { 36 | 37 | private static final String CONF_FILE = "WindowsServiceCmdline"; 38 | private static final Pattern CAPSULE_PORT_PATTERN = Pattern.compile("-Dcapsule\\.port=\\d+"); 39 | 40 | // 41 | // Common 42 | private static final Map.Entry ATTR_START_CLASS = ATTRIBUTE("Daemon-Start-Class", T_STRING(), null, true, "Class containing the start method (default: app's main)"); 43 | private static final Map.Entry ATTR_START_METHOD = ATTRIBUTE("Daemon-Start-Method", T_STRING(), null, true, "Static 'String[] -> void' service start method short name run as the specified, if any (default: app's main)"); 44 | private static final Map.Entry ATTR_STOP_CLASS = ATTRIBUTE("Daemon-Stop-Class", T_STRING(), null, true, "Class containing the stop method, if any (default: none)"); 45 | private static final Map.Entry ATTR_STOP_METHOD = ATTRIBUTE("Daemon-Stop-Method", T_STRING(), null, true, "Static 'String[] -> void' service stop method short name run as the specified, if any (default: none)"); 46 | private static final String PROP_USER = "capsule.daemon.user"; 47 | private static final Map.Entry ATTR_USER = ATTRIBUTE("Daemon-User", T_STRING(), null, true, "The username under which the service will run"); 48 | private static final String PROP_CWD = "capsule.daemon.cwd"; 49 | private static final Map.Entry ATTR_CWD = ATTRIBUTE("Daemon-Cwd", T_STRING(), null, true, "Working dir (default: / on Unix)"); 50 | private static final String PROP_STDOUT_FILE = "capsule.daemon.stdoutFile"; 51 | private static final Map.Entry ATTR_STDOUT_FILE = ATTRIBUTE("Daemon-Stdout-File", T_STRING(), null, true, "stdout (default: /dev/null on Unix, /service-stdout.YEAR-MONTH-DAY.log on Windows)"); 52 | private static final String PROP_STDERR_FILE = "capsule.daemon.stderrFile"; 53 | private static final Map.Entry ATTR_STDERR_FILE = ATTRIBUTE("Daemon-Stderr-File", T_STRING(), null, true, "stderr (default: /dev/null on Unix, /service-stderr.YEAR-MONTH-DAY.log on Windows))"); 54 | private static final String PROP_PID_FILE = "capsule.daemon.pidFile"; 55 | private static final Map.Entry ATTR_PID_FILE = ATTRIBUTE("Daemon-PID-File", T_STRING(), null, true, "PID file (default: /var/run/.pid on Unix, /.pid on Windows)"); 56 | 57 | private static final String PROP_STOP = "capsule.daemon.stop"; 58 | 59 | // Windows only 60 | private static final String PROP_PASSWORD = "capsule.daemon.password"; 61 | private static final Map.Entry ATTR_PASSWORD = ATTRIBUTE("Daemon-Password", T_STRING(), null, true, "The password of the user under which the service will run (default: none, Windows only)"); 62 | private static final String PROP_JAVA_EXEC_USER = "capsule.daemon.javaExecUser"; 63 | private static final Map.Entry ATTR_JAVA_EXEC_USER = ATTRIBUTE("Daemon-Java-Exec-User", T_STRING(), null, true, "The password of the user that will execute the final Java process (default: none, Windows only)"); 64 | private static final String PROP_JAVA_EXEC_PASSWORD = "capsule.daemon.javaExecPassword"; 65 | private static final Map.Entry ATTR_JAVA_EXEC_PASSWORD = ATTRIBUTE("Daemon-Java-Exec-Password", T_STRING(), null, true, "The password of the user that will execute the final Java process (default: none, Windows only)"); 66 | private static final String PROP_SERVICE_NAME = "capsule.daemon.serviceName"; 67 | private static final Map.Entry ATTR_SERVICE_NAME = ATTRIBUTE("Daemon-Service-Name", T_STRING(), null, true, "The service internal name (default: app ID, Windows only)"); 68 | private static final String PROP_DISPLAY_NAME = "capsule.daemon.displayName"; 69 | private static final Map.Entry ATTR_DISPLAY_NAME = ATTRIBUTE("Daemon-Display-Name", T_STRING(), null, true, "The service display name (default: app ID, Windows only)"); 70 | private static final String PROP_DESCRIPTION = "capsule.daemon.description"; 71 | private static final Map.Entry ATTR_DESCRIPTION = ATTRIBUTE("Daemon-Description", T_STRING(), null, true, "The service description (default: app ID, Windows only)"); 72 | private static final String PROP_STARTUP = "capsule.daemon.startup"; 73 | private static final Map.Entry ATTR_STARTUP = ATTRIBUTE("Daemon-Startup", T_STRING(), null, true, "The service startup mode, either 'auto' or 'manual' (default: manual, Windows only)"); 74 | private static final String PROP_TYPE = "capsule.daemon.type"; 75 | private static final Map.Entry ATTR_TYPE = ATTRIBUTE("Daemon-Type", T_STRING(), null, true, "The service type, it can be 'interactive' (default: none, Windows only)"); 76 | private static final String PROP_DEPENDS_ON = "capsule.daemon.dependsOn"; 77 | private static final Map.Entry> ATTR_DEPENDS_ON = ATTRIBUTE("Daemon-Depends-On", T_LIST(T_STRING()), null, true, "The service dependencies, as a list (default: none, Windows only)"); 78 | private static final String PROP_STOP_PARAMS = "capsule.daemon.stopParams"; 79 | private static final Map.Entry> ATTR_STOP_PARAMS = ATTRIBUTE("Daemon-Stop-Params", T_LIST(T_STRING()), null, true, "The service stop parameters (default: none, Windows only)"); 80 | private static final String PROP_STOP_TIMEOUT = "capsule.daemon.stopTimeout"; 81 | private static final Map.Entry ATTR_STOP_TIMEOUT = ATTRIBUTE("Daemon-Stop-Timeout", T_LONG(), null, true, "Service stop timeout in seconds (default: none, Windows only)"); 82 | private static final String PROP_LOG_PATH = "capsule.daemon.logPath"; 83 | private static final Map.Entry ATTR_LOG_PATH = ATTRIBUTE("Daemon-Log-Path", T_STRING(), null, true, "The log path (default: %SystemRoot%\\System32\\LogFiles\\Apache, Windows only)"); 84 | private static final String PROP_LOG_PREFIX = "capsule.daemon.logPrefix"; 85 | private static final Map.Entry ATTR_LOG_PREFIX = ATTRIBUTE("Daemon-Log-Prefix", T_STRING(), null, true, "The log prefix (default: app ID, Windows only)"); 86 | private static final String PROP_LOG_LEVEL = "capsule.daemon.logLevel"; 87 | private static final Map.Entry ATTR_LOG_LEVEL = ATTRIBUTE("Daemon-Log-Level", T_STRING(), null, true, "The log level between 'error', 'info', 'warn' and 'debug' (default: info, Windows only)"); 88 | 89 | // Unix only 90 | private static final String PROP_CHECK_ONLY = "capsule.daemon.checkOnly"; 91 | private static final String PROP_DEBUG = "capsule.daemon.debug"; 92 | private static final String PROP_VERBOSE = "capsule.daemon.verbose"; 93 | private static final String PROP_JSVC = "capsule.daemon.jsvc"; 94 | 95 | private static final Map.Entry ATTR_INIT_CLASS = ATTRIBUTE("Init-Class", T_STRING(), null, true, "Class containing the init method (default: none, Unix only)"); 96 | private static final Map.Entry ATTR_INIT_METHOD = ATTRIBUTE("Init-Method", T_STRING(), null, true, "Static 'String[] -> String[]' service initialization method short name run as 'root'; the return value will be passed to the 'Start' method (default: none, Unix only)"); 97 | private static final Map.Entry ATTR_DESTROY_CLASS = ATTRIBUTE("Destroy-Class", T_STRING(), null, true, "Class containing the destroy method (default: none, Unix only)"); 98 | private static final Map.Entry ATTR_DESTROY_METHOD = ATTRIBUTE("Destroy-Method", T_STRING(), null, true, "Static service cleanup method short name run as 'root' (default: none, Unix only)"); 99 | private static final String PROP_NO_DETACH = "capsule.daemon.noDetach"; 100 | private static final Map.Entry ATTR_NO_DETACH = ATTRIBUTE("No-Detach", T_BOOL(), false, true, "Don't detach from parent process (default: false, Unix only)"); 101 | private static final String PROP_KEEP_STDIN = "capsule.daemon.keepStdin"; 102 | private static final Map.Entry ATTR_KEEP_STDIN = ATTRIBUTE("Keep-Stdin", T_BOOL(), false, true, "Don't redirect stdin to /dev/null (default: false, Unix only)"); 103 | private static final String PROP_WAIT_SECS = "capsule.daemon.waitSecs"; 104 | private static final Map.Entry ATTR_WAIT_SECS = ATTRIBUTE("Wait-Secs", T_LONG(), null, true, "Wait seconds for service, must be multiple of 10 (default: 10 secs, Unix only)"); 105 | // 106 | 107 | private static Path hostAbsoluteOwnJarFile; 108 | private static Path svcExec; 109 | 110 | private Map env; 111 | private String appClass; 112 | 113 | public DaemonCapsule(Capsule pred) { 114 | super(pred); 115 | } 116 | 117 | @Override 118 | protected Path getJavaExecutable() { 119 | if (svcExec == null) { 120 | if (isUnix()) { 121 | final String systemJsvc = getProperty(PROP_JSVC); 122 | if (systemJsvc != null) 123 | return (svcExec = Paths.get(systemJsvc)); 124 | } 125 | svcExec = setupBinDir().resolve(platformExecPath()).toAbsolutePath().normalize(); 126 | } 127 | return svcExec; 128 | } 129 | 130 | @Override 131 | protected Map buildEnvironmentVariables(Map env) { 132 | this.env = env; 133 | return super.buildEnvironmentVariables(env); 134 | } 135 | 136 | @Override 137 | @SuppressWarnings("unchecked") 138 | protected T attribute(Map.Entry attr) { 139 | if (ATTR_APP_CLASS_PATH == attr && isWrapperCapsule()) { 140 | final List cp = new ArrayList<>(super.attribute(ATTR_APP_CLASS_PATH)); 141 | cp.add(findOwnJarFile().toAbsolutePath().normalize()); 142 | return (T) cp; 143 | } 144 | // if (ATTR_APP_CLASS == attr) 145 | // return (T) DaemonAdapter.class.getName(); 146 | return super.attribute(attr); 147 | } 148 | 149 | @Override 150 | protected final ProcessBuilder prelaunch(List jvmArgs, List args) { 151 | final ProcessBuilder pb = super.prelaunch(jvmArgs, args); 152 | final List svcCmd; 153 | try { 154 | svcCmd = isStop() ? toSvcStop(pb.command()) : toSvc(pb.command()); 155 | } catch (final IOException e) { 156 | throw new RuntimeException(e); 157 | } 158 | return new ProcessBuilder(svcCmd); 159 | } 160 | 161 | private boolean isStop() { 162 | return emptyOrTrue(System.getProperty(PROP_STOP)); 163 | } 164 | 165 | @Override 166 | protected Process postlaunch(Process child) { 167 | return null; // Don't wait for the child: the management of the service application is delegated to 'jsvc'/'procrun' 168 | } 169 | 170 | // 171 | private Path setupBinDir() { 172 | final Path libdir = binDir(); 173 | final String[] ress = new String[]{ 174 | "jsvc/linux64-brew/jsvc", 175 | "jsvc/macosx-yosemite-brew/jsvc", 176 | "procrun/prunsrv.exe", 177 | "procrun/x64/prunsrv.exe", 178 | }; 179 | log(LOG_VERBOSE, "Copying daemon native helpers " + Arrays.toString(ress) + " in " + libdir.toAbsolutePath().normalize().toString()); 180 | if (Files.exists(libdir)) { 181 | try { 182 | delete(libdir); 183 | } catch (IOException e) { 184 | log(LOG_VERBOSE, "WARNING: Could not delete jsvc/procrun executables: " + e.getMessage()); 185 | } 186 | } 187 | try { 188 | addTempFile(Files.createDirectory(libdir)); 189 | } catch (IOException e) { 190 | log(LOG_VERBOSE, "WARNING: Could not create jsvc/procrun executables: " + e.getMessage()); 191 | } 192 | 193 | for (final String filename : ress) { 194 | try { 195 | copy(filename, "bin", libdir); 196 | } catch (IOException e) { 197 | log(LOG_VERBOSE, "WARNING: Could not copy jsvc/procrun executables: " + e.getMessage()); 198 | } 199 | } 200 | return libdir; 201 | } 202 | 203 | private Path binDir() { 204 | return findOwnJarFile().toAbsolutePath().getParent().resolve("bin"); 205 | } 206 | 207 | private static Path copy(String filename, String resourceDir, Path targetDir, OpenOption... opts) throws IOException { 208 | try (final InputStream in = DaemonCapsule.class.getClassLoader().getResourceAsStream(resourceDir + '/' + filename)) { 209 | final Path f = targetDir.resolve(filename); 210 | Files.createDirectories(f.getParent()); 211 | try (final OutputStream out = Files.newOutputStream(f, opts)) { 212 | copy0(in, out); 213 | final Path ret = targetDir.resolve(filename); 214 | //noinspection ResultOfMethodCallIgnored 215 | ret.toFile().setExecutable(true); 216 | log(LOG_VERBOSE, "Successfully copied resource " + resourceDir + "/" + filename + " to " + targetDir.toAbsolutePath().normalize().toString()); 217 | return ret; 218 | } 219 | } 220 | } 221 | 222 | private List toSvcStop(List command) throws IOException { 223 | if (isWindows()) 224 | return stopWindowsCmd(); 225 | else 226 | return setupUnixCmd(command, true); 227 | } 228 | 229 | private List toSvc(List cmd) throws IOException { 230 | if (isWindows()) 231 | return setupWindowsCmd(cmd); 232 | else 233 | return setupUnixCmd(cmd); 234 | } 235 | 236 | private List stopWindowsCmd() throws IOException { 237 | final List ret = new ArrayList<>(); 238 | 239 | ret.add(doubleQuote(binDir().resolve(platformExecPath(true)).toString())); 240 | 241 | ret.add("stop"); 242 | 243 | String svcName = getPropertyOrAttributeString(PROP_SERVICE_NAME, ATTR_SERVICE_NAME); 244 | if (svcName == null) 245 | svcName = getAppId(); 246 | ret.add(doubleQuote(svcName)); 247 | 248 | return ret; 249 | } 250 | 251 | private List setupWindowsCmd(List cmd) throws IOException { 252 | if (svcExec == null) 253 | throw new UnsupportedOperationException("At present `capsule-daemon` only supports Java capsules and not `Application-Script` ones"); 254 | 255 | String svcName = getPropertyOrAttributeString(PROP_SERVICE_NAME, ATTR_SERVICE_NAME); 256 | if (svcName == null) 257 | svcName = getAppId(); 258 | 259 | final List installCmd = new ArrayList<>(); 260 | 261 | installCmd.add(doubleQuote(svcExec.toString())); 262 | installCmd.add("install"); 263 | installCmd.add(doubleQuote(svcName)); 264 | 265 | final List jvmOpts = new ArrayList<>(); 266 | final List appOpts = new ArrayList<>(); 267 | 268 | // TODO Not nicest but redefining ATTR_APP_CLASS seems to break a lot of stuff 269 | final String appClass = parseWindows(cmd, installCmd, jvmOpts, appOpts); 270 | 271 | int i = installCmd.size(); 272 | 273 | installCmd.add(i++, "--JavaHome"); 274 | installCmd.add(i++, doubleQuote(getJavaHome().toAbsolutePath().normalize().toString())); 275 | 276 | // Add attrs 277 | installCmd.add(i++, "--Description"); 278 | final String desc = getPropertyOrAttributeString(PROP_DESCRIPTION, ATTR_DESCRIPTION); 279 | installCmd.add(i++, doubleQuote(desc != null ? desc : getAppId())); 280 | 281 | installCmd.add(i++, "--DisplayName"); 282 | final String dName = getPropertyOrAttributeString(PROP_DISPLAY_NAME, ATTR_DISPLAY_NAME); 283 | installCmd.add(i++, doubleQuote(dName != null ? dName : getAppId())); 284 | 285 | i = addPropertyOrAttributeStringAsOptionDoubleQuote(installCmd, PROP_STARTUP, ATTR_STARTUP, "--Startup", i); 286 | i = addPropertyOrAttributeStringAsOptionDoubleQuote(installCmd, PROP_TYPE, ATTR_TYPE, "--Type", i); 287 | 288 | final List dependsOn = getPropertyOrAttributeStringList(PROP_DEPENDS_ON, ATTR_DEPENDS_ON); 289 | if (dependsOn != null && !dependsOn.isEmpty()) { 290 | installCmd.add(i++, "++DependsOn"); 291 | installCmd.add(i++, doubleQuote(join(dependsOn, ";", "'"))); 292 | } 293 | 294 | // Add env 295 | if (env != null && !env.isEmpty()) { 296 | final ArrayList envL = new ArrayList<>(); 297 | for (final String e : env.keySet()) 298 | envL.add(e + "=" + env.get(e)); 299 | if (!envL.isEmpty()) { 300 | installCmd.add(i++, "++Environment"); 301 | installCmd.add(i++, doubleQuote(join(envL, ";", "'"))); 302 | } 303 | } 304 | 305 | i = addPropertyOrAttributeStringAsOptionDoubleQuote(installCmd, PROP_JAVA_EXEC_USER, ATTR_JAVA_EXEC_USER, "--User", i); 306 | i = addPropertyOrAttributeStringAsOptionDoubleQuote(installCmd, PROP_JAVA_EXEC_PASSWORD, ATTR_JAVA_EXEC_PASSWORD, "--Password", i); 307 | i = addPropertyOrAttributeStringAsOptionDoubleQuote(installCmd, PROP_USER, ATTR_USER, "--ServiceUser", i); 308 | i = addPropertyOrAttributeStringAsOptionDoubleQuote(installCmd, PROP_PASSWORD, ATTR_PASSWORD, "--ServicePassword", i); 309 | 310 | installCmd.add(i++, "--StartMode"); 311 | installCmd.add(i++, "Java"); 312 | 313 | i = addPropertyOrAttributeStringAsOptionDoubleQuote(installCmd, PROP_CWD, ATTR_CWD, "--StartPath", i); 314 | 315 | // Not using DaemonAdapter, not needed for Windows 316 | 317 | installCmd.add(i++, "--StartClass"); 318 | final String startC = getAttribute(ATTR_START_CLASS); 319 | installCmd.add(i++, (startC != null ? startC : appClass)); 320 | 321 | final String startM = getAttribute(ATTR_START_METHOD); 322 | if (startM != null) { 323 | installCmd.add(i++, "--StartMethod"); 324 | installCmd.add(i++, startM); 325 | } 326 | 327 | if (!appOpts.isEmpty()) { 328 | installCmd.add(i++, "++StartParams"); 329 | installCmd.add(i++, doubleQuote(join(appOpts, ";", "'"))); 330 | } 331 | 332 | installCmd.add(i++, "--StopMode"); 333 | installCmd.add(i++, "Java"); 334 | 335 | i = addPropertyOrAttributeStringAsOptionDoubleQuote(installCmd, PROP_CWD, ATTR_CWD, "--StopPath", i); 336 | 337 | final String stopC = getAttribute(ATTR_STOP_CLASS); 338 | if (stopC != null) { 339 | installCmd.add(i++, "--StopClass"); 340 | installCmd.add(i++, stopC); 341 | } 342 | 343 | final String stopM = getAttribute(ATTR_STOP_METHOD); 344 | if (stopM != null) { 345 | installCmd.add(i++, "--StopMethod"); 346 | installCmd.add(i++, stopM); 347 | } 348 | 349 | final List stopParams = getPropertyOrAttributeStringList(PROP_STOP_PARAMS, ATTR_STOP_PARAMS); 350 | if (stopParams != null && !stopParams.isEmpty()) { 351 | installCmd.add(i++, "++StopParams"); 352 | installCmd.add(i++, doubleQuote(join(stopParams, ";", "'"))); 353 | } 354 | 355 | final Long stopTimeout = getPropertyOrAttributeLong(PROP_STOP_TIMEOUT, ATTR_STOP_TIMEOUT); 356 | if (stopTimeout != null) { 357 | installCmd.add(i++, "++StopTimeout"); 358 | installCmd.add(i++, stopTimeout.toString()); 359 | } 360 | 361 | i = addPropertyOrAttributeStringAsOptionDoubleQuote(installCmd, PROP_LOG_PATH, ATTR_LOG_PATH, "--LogPath", i); 362 | 363 | installCmd.add(i++, "--LogPrefix"); 364 | final String logPrefix = getPropertyOrAttributeString(PROP_LOG_PREFIX, ATTR_LOG_PREFIX); 365 | installCmd.add(i++, doubleQuote(logPrefix != null ? logPrefix : getAppId())); 366 | 367 | i = addPropertyOrAttributeStringAsOption(installCmd, PROP_LOG_LEVEL, ATTR_LOG_LEVEL, "--LogLevel", i); 368 | 369 | installCmd.add(i++, "--StdOutput"); 370 | final String stdout = getPropertyOrAttributeString(PROP_STDOUT_FILE, ATTR_STDOUT_FILE); 371 | installCmd.add(i++, doubleQuote(stdout != null ? stdout : "auto")); 372 | 373 | installCmd.add(i++, "--StdError"); 374 | final String stderr = getPropertyOrAttributeString(PROP_STDERR_FILE, ATTR_STDERR_FILE); 375 | installCmd.add(i++, doubleQuote(stderr != null ? stderr : "auto")); 376 | 377 | installCmd.add(i++, "--PidFile"); 378 | final String pid = getPropertyOrAttributeString(PROP_PID_FILE, ATTR_PID_FILE); 379 | installCmd.add(i++, doubleQuote(pid != null ? pid : getAppId() + ".pid")); 380 | 381 | installCmd.add(i++, "++JvmOptions"); 382 | installCmd.add(i, doubleQuote(join(jvmOpts, ";"))); 383 | 384 | final String installCmdline = join(installCmd, " "); 385 | if (isReinstallNeeded(installCmdline)) { 386 | // Write new install cmdline 387 | log(LOG_VERBOSE, "Windows: service " + svcName + " needs re-installation, writing cmdline in " + getCmdlineFile().toString()); 388 | dump(installCmdline, getCmdlineFile()); 389 | 390 | // Remove old service 391 | try { 392 | final ProcessBuilder pb = new ProcessBuilder().command(doubleQuote(svcExec.toString()), "delete", doubleQuote(svcName)); 393 | final Process p = pb.start(); 394 | if (p.waitFor() != 0) 395 | log(LOG_VERBOSE, "Windows: couldn't delete service " + svcName + ".\n\tstderr:\n\t\t" + slurp(p.getErrorStream()) + "\n\tstdout:\n\t\t" + slurp(p.getInputStream())); 396 | else 397 | log(LOG_VERBOSE, "Windows: service " + svcName + " successfully deleted"); 398 | } catch (InterruptedException | IOException ignored) { 399 | log(LOG_VERBOSE, "Windows: couldn't delete service " + svcName + ", exception message: " + ignored.getMessage()); 400 | // Try proceeding anyway 401 | } 402 | 403 | // Re-install 404 | try { 405 | log(LOG_VERBOSE, "Windows: installing service " + svcName + " with command: " + installCmd.toString()); 406 | final Process p = new ProcessBuilder(installCmd).start(); 407 | if (p.waitFor() != 0) 408 | log(LOG_VERBOSE, "Windows: couldn't install install " + svcName + ".\n\tstderr:\n\t\t" + slurp(p.getErrorStream()) + "\n\tstdout:\n\t\t" + slurp(p.getInputStream())); 409 | else 410 | log(LOG_VERBOSE, "Windows: service " + svcName + " successfully installed"); 411 | } catch (InterruptedException | IOException e) { 412 | throw new RuntimeException(e); 413 | } 414 | } 415 | 416 | // Return command for service start 417 | final List ret = new ArrayList<>(); 418 | ret.add(svcExec.toString()); 419 | ret.add("start"); 420 | ret.add(svcName); 421 | return ret; 422 | } 423 | 424 | private boolean isReinstallNeeded(String cmdLine) throws IOException { 425 | // Check if the conf file exists 426 | if (!Files.exists(getCmdlineFile())) { 427 | log(LOG_VERBOSE, "Windows: service install cmdline file " + getCmdlineFile() + " is not present"); 428 | return true; 429 | } 430 | 431 | // Check if the conf content has changed 432 | try (final InputStream is = new FileInputStream(getCmdlineFile().toFile())) { 433 | final String cmdlineFile = slurp(is); 434 | if (!removeCapsulePort(cmdlineFile).equals(removeCapsulePort(cmdLine))) { 435 | log(LOG_VERBOSE, "Windows: service install cmdline file content " + getCmdlineFile() + " has changed"); 436 | return true; 437 | } 438 | } 439 | 440 | // Check if the application is newer 441 | try { 442 | FileTime jarTime = Files.getLastModifiedTime(getJarFile()); 443 | if (isWrapperCapsule()) { 444 | final FileTime wrapperTime = Files.getLastModifiedTime(findOwnJarFile()); 445 | if (wrapperTime.compareTo(jarTime) > 0) 446 | jarTime = wrapperTime; 447 | } 448 | 449 | final FileTime confTime = Files.getLastModifiedTime(getCmdlineFile()); 450 | 451 | final boolean buildNeeded = confTime.compareTo(jarTime) < 0; 452 | if (buildNeeded) 453 | log(LOG_VERBOSE, "Windows: application " + getJarFile() + " has changed"); 454 | return buildNeeded; 455 | } catch (IOException e) { 456 | throw new RuntimeException(e); 457 | } 458 | } 459 | 460 | private Path getDaemonDir() throws IOException { 461 | @SuppressWarnings("deprecation") final Path ret = getCacheDir().resolve("daemon"); 462 | if (!Files.exists(ret)) 463 | Files.createDirectories(ret); 464 | return ret; 465 | } 466 | 467 | private Path getCmdlineFile() throws IOException { 468 | return getDaemonDir().resolve(CONF_FILE); 469 | } 470 | 471 | private int addPropertyOrAttributeStringAsOption(List outCmd, String prop, Map.Entry attr, String opt, int pos) { 472 | final String v = getPropertyOrAttributeString(prop, attr); 473 | if (v != null) { 474 | outCmd.add(pos++, opt); 475 | outCmd.add(pos++, v); 476 | } 477 | return pos; 478 | } 479 | 480 | private int addPropertyOrAttributeStringAsOptionDoubleQuote(List outCmd, String prop, Map.Entry attr, String opt, int pos) { 481 | final String v = getPropertyOrAttributeString(prop, attr); 482 | if (v != null) { 483 | outCmd.add(pos++, opt); 484 | outCmd.add(pos++, doubleQuote(v)); 485 | } 486 | return pos; 487 | } 488 | 489 | private int addAttributeStringAsProperty(List outCmd, Map.Entry inAttr, String outPropKey, int pos) { 490 | final String v = getAttribute(inAttr); 491 | if (v != null) 492 | outCmd.add(pos++, "-D" + outPropKey + "=" + v); 493 | return pos; 494 | } 495 | 496 | private String parseWindows(List cmds, List outCmdOpts, List outJvmOpts, List outAppOpts) { 497 | final List otherJvmOpts = new ArrayList<>(); 498 | boolean addToCmdOpts = false; 499 | for (final String c : cmds.subList(1, cmds.size())) { // Skip actual command 500 | if (addToCmdOpts) { 501 | addToCmdOpts = false; 502 | outCmdOpts.add(doubleQuote(c)); 503 | } else if ("-cp".equals(c) || "-classpath".equals(c)) { 504 | outCmdOpts.add("--Classpath"); 505 | addToCmdOpts = true; 506 | } else if ("-Xmx".equals(c)) 507 | outJvmOpts.add("--JvmMx"); 508 | else if ("-Xms".equals(c)) 509 | outJvmOpts.add("--JvmMs"); 510 | else if ("-Xss".equals(c)) 511 | outJvmOpts.add("--JvmSs"); 512 | else if (c.startsWith("-Djava.library.path=")) { 513 | outCmdOpts.add("--LibraryPath"); 514 | outCmdOpts.add(doubleQuote(c.substring("-Djava.library.path=".length()))); 515 | } else if (c.startsWith("-D") || c.startsWith("-X") 516 | || "-server".equals(c) || "-client".equals(c) || "-d32".equals(c) || "-d64".equals(c) 517 | || "-?".equals(c) || "-help".equals(c) || "-showversion".equals(c) 518 | || "-esa".equals(c) || "-dsa".equals(c) || "-enablesystemassertions".equals(c) || "-disablesystemassertions".equals(c) 519 | || c.startsWith("-agentlib") || c.startsWith("-agentpath") || c.startsWith("-javaagent") 520 | || c.startsWith("-ea") || c.startsWith("-da") || c.startsWith("-enableassertions") || c.startsWith("-disableassertions") 521 | || c.startsWith("-version") || c.startsWith("-verbose:") || c.startsWith("-splash:")) 522 | otherJvmOpts.add(c); 523 | else 524 | outAppOpts.add(doubleQuote(c)); 525 | } 526 | outJvmOpts.add(join(otherJvmOpts, ";", "'")); 527 | 528 | return outAppOpts.remove(outAppOpts.indexOf(doubleQuote(getAppClass()))); 529 | } 530 | 531 | private List setupUnixCmd(List cmd) { 532 | return setupUnixCmd(cmd, false); 533 | } 534 | 535 | private List setupUnixCmd(List cmd, boolean stop) { 536 | final List ret = new ArrayList<>(cmd); 537 | 538 | int i = 1; 539 | ret.add(i++, "-java-home"); 540 | Path javaHome = getJavaHome().toAbsolutePath().normalize(); 541 | if (isMac() && javaHome.toString().endsWith("/Home/jre")) 542 | javaHome = javaHome.getParent(); 543 | ret.add(i++, javaHome.toString()); 544 | 545 | i = addPropertyOrAttributeStringAsOption(ret, PROP_USER, ATTR_USER, "-user", i); 546 | 547 | if (!stop && getPropertyOrAttributeBool(PROP_KEEP_STDIN, ATTR_KEEP_STDIN)) 548 | ret.add(i++, "-keepstdin"); 549 | 550 | if (!stop && getPropertyOrAttributeBool(PROP_NO_DETACH, ATTR_NO_DETACH)) 551 | ret.add(i++, "-nodetach"); 552 | 553 | final String checkOnly = System.getProperty(PROP_CHECK_ONLY); 554 | if (checkOnly != null && !"false".equals(checkOnly)) 555 | ret.add(i++, "-check"); 556 | 557 | final String debug = System.getProperty(PROP_DEBUG); 558 | if (debug != null && !"false".equals(debug)) 559 | ret.add(i++, "-debug"); 560 | 561 | final String verbose = System.getProperty(PROP_VERBOSE); 562 | if (verbose != null) { 563 | if (verbose.length() == 0) 564 | ret.add(i++, "-verbose"); 565 | else 566 | ret.add(i++, "-verbose:" + verbose); 567 | } 568 | 569 | if (!stop) { 570 | i = addPropertyOrAttributeStringAsOption(ret, PROP_CWD, ATTR_CWD, "-cwd", i); 571 | i = addPropertyOrAttributeStringAsOption(ret, PROP_STDOUT_FILE, ATTR_STDOUT_FILE, "-outfile", i); 572 | i = addPropertyOrAttributeStringAsOption(ret, PROP_STDERR_FILE, ATTR_STDERR_FILE, "-errfile", i); 573 | } 574 | 575 | ret.add(i++, "-pidfile"); 576 | final String pid = getPropertyOrAttributeString(PROP_PID_FILE, ATTR_PID_FILE); 577 | ret.add(i++, pid != null ? pid : "/var/run/" + getAppId() + ".pid"); 578 | 579 | if (stop) { 580 | ret.add(i++, "-stop"); 581 | } 582 | 583 | if (!stop) { 584 | final Long wait = getPropertyOrAttributeLong(PROP_WAIT_SECS, ATTR_WAIT_SECS); 585 | if (wait != null) { 586 | ret.add(i++, "-wait"); 587 | ret.add(i++, wait.toString()); 588 | } 589 | } 590 | 591 | i = addAttributeStringAsProperty(ret, ATTR_INIT_CLASS, DaemonAdapter.PROP_INIT_CLASS, i); 592 | i = addAttributeStringAsProperty(ret, ATTR_INIT_METHOD, DaemonAdapter.PROP_INIT_METHOD, i); 593 | 594 | // TODO Not nicest but redefining ATTR_APP_CLASS seems to break a lot of stuff, see https://github.com/puniverse/capsule/issues/82 595 | final String startC = getAttribute(ATTR_START_CLASS); 596 | final int appClassIdx = ret.indexOf(getAppClass()); 597 | final String appClass = ret.remove(appClassIdx); 598 | ret.add(appClassIdx, DaemonAdapter.class.getName()); 599 | ret.add(i++, "-D" + DaemonAdapter.PROP_START_CLASS + "=" + (startC != null ? startC : appClass)); 600 | 601 | final String startM = getAttribute(ATTR_START_METHOD); 602 | ret.add(i++, "-D" + DaemonAdapter.PROP_START_METHOD + "=" + (startM != null ? startM : "main")); 603 | i = addAttributeStringAsProperty(ret, ATTR_STOP_CLASS, DaemonAdapter.PROP_STOP_METHOD, i); 604 | i = addAttributeStringAsProperty(ret, ATTR_DESTROY_CLASS, DaemonAdapter.PROP_DESTROY_CLASS, i); 605 | addAttributeStringAsProperty(ret, ATTR_DESTROY_METHOD, DaemonAdapter.PROP_DESTROY_METHOD, i); 606 | 607 | return ret; 608 | } 609 | 610 | private String getPropertyOrAttributeString(String propName, Map.Entry attr) { 611 | final String propValue = System.getProperty(propName); 612 | if (propValue == null) 613 | return getAttribute(attr); 614 | return propValue; 615 | } 616 | 617 | private List getPropertyOrAttributeStringList(String propName, Map.Entry> attr) { 618 | final String propValue = System.getProperty(propName); 619 | if (propValue == null) 620 | return getAttribute(attr); 621 | return Arrays.asList(propValue.split(";")); 622 | } 623 | 624 | private Boolean getPropertyOrAttributeBool(String propName, Map.Entry attr) { 625 | final String propValue = System.getProperty(propName); 626 | if (propValue == null) 627 | return getAttribute(attr); 628 | try { 629 | return Boolean.parseBoolean(propValue); 630 | } catch (Throwable t) { 631 | return getAttribute(attr); 632 | } 633 | } 634 | 635 | private Long getPropertyOrAttributeLong(String propName, Map.Entry attr) { 636 | final String propValue = System.getProperty(propName); 637 | if (propValue == null) 638 | return getAttribute(attr); 639 | try { 640 | return Long.parseLong(propValue); 641 | } catch (Throwable t) { 642 | return getAttribute(attr); 643 | } 644 | } 645 | 646 | private boolean isLinux64() { 647 | return "linux".equals(getPlatform()) && System.getProperty("os.arch").contains("64"); 648 | } 649 | 650 | private Path platformExecPath() { 651 | return platformExecPath(false); 652 | } 653 | 654 | private Path platformExecPath(boolean stop) { 655 | if (isMac()) 656 | return Paths.get("jsvc", "macosx-yosemite-brew", "jsvc"); 657 | if (isWindows()) { 658 | if (!stop && is64bit()) 659 | return Paths.get("procrun", "x64", "prunsrv.exe"); 660 | else 661 | return Paths.get("procrun", "prunsrv.exe"); 662 | } 663 | if (isLinux64()) { 664 | return Paths.get("jsvc", "linux64-brew", "jsvc"); 665 | } 666 | try (final BufferedReader reader = new BufferedReader(new InputStreamReader(new ProcessBuilder("which", "jsvc").start().getInputStream(), Charset.defaultCharset()))) { 667 | return Paths.get(reader.readLine()); 668 | } catch (IOException e) { 669 | throw new RuntimeException(e); 670 | } 671 | } 672 | 673 | private static boolean is64bit() { 674 | if (System.getProperty("os.name").toLowerCase().contains("windows")) 675 | return (System.getenv("ProgramFiles(x86)") != null); 676 | else 677 | return (System.getProperty("os.arch").contains("64")); 678 | } 679 | 680 | private String getAppClass() { 681 | if (appClass == null) { 682 | if (hasAttribute(ATTR_APP_CLASS)) 683 | appClass = getAttribute(ATTR_APP_CLASS); 684 | else if (hasAttribute(ATTR_APP_ARTIFACT)) { 685 | final List appArtifactPaths = resolve(lookup(getAttribute(ATTR_APP_ARTIFACT))); 686 | if (appArtifactPaths.size() <= 0) 687 | throw new IllegalStateException("Can't figure out the application's main class: resolving 'Application' yields " + appArtifactPaths.size() + " artifacts, need at least one"); 688 | appClass = getMainClass(appArtifactPaths.get(0)); 689 | } else 690 | throw new IllegalStateException("Can't figure out the application's main class: nor 'Application-Class' neither 'Application' have been found"); 691 | } 692 | return appClass; 693 | } 694 | 695 | private static Path findOwnJarFile() { 696 | if (hostAbsoluteOwnJarFile == null) { 697 | final URL url = DaemonCapsule.class.getClassLoader().getResource(DaemonCapsule.class.getName().replace('.', '/') + ".class"); 698 | if (url != null) { 699 | if (!"jar".equals(url.getProtocol())) 700 | throw new IllegalStateException("The Capsule class must be in a JAR file, but was loaded from: " + url); 701 | final String path = url.getPath(); 702 | if (path == null) // || !path.startsWith("file:") 703 | throw new IllegalStateException("The Capsule class must be in a local JAR file, but was loaded from: " + url); 704 | 705 | try { 706 | final URI jarUri = new URI(path.substring(0, path.indexOf('!'))); 707 | hostAbsoluteOwnJarFile = Paths.get(jarUri).toAbsolutePath().normalize(); 708 | } catch (URISyntaxException e) { 709 | throw new AssertionError(e); 710 | } 711 | } else 712 | throw new RuntimeException("Can't locate capsule's own class"); 713 | } 714 | return hostAbsoluteOwnJarFile; 715 | } 716 | 717 | private static String join(List values, String sep) { 718 | return join(values, sep, "", ""); 719 | } 720 | 721 | private static String join(List values, String sep, String prePostfix) { 722 | return join(values, sep, prePostfix, prePostfix); 723 | } 724 | 725 | private static String join(List values, String sep, String prefix, String postfix) { 726 | final ArrayList vals = values != null ? new ArrayList<>(values) : new ArrayList(); 727 | final String v0 = vals.size() > 0 ? (prefix + vals.remove(0) + postfix) : ""; 728 | final StringBuilder sb = new StringBuilder(v0); 729 | for (String v : vals) 730 | sb.append(sep).append(prefix).append(v).append(postfix); 731 | return sb.toString(); 732 | } 733 | 734 | private static String slurp(InputStream in) throws IOException { 735 | final StringBuilder out = new StringBuilder(); 736 | final byte[] b = new byte[4096]; 737 | int n; 738 | while ((n = in.read(b)) != -1) 739 | out.append(new String(b, 0, n)); 740 | 741 | return out.toString(); 742 | } 743 | 744 | private static void dump(String content, Path loc) throws IOException { 745 | try (final PrintWriter out = new PrintWriter(new OutputStreamWriter(Files.newOutputStream(loc), Charset.defaultCharset()))) { 746 | out.print(content); 747 | } 748 | } 749 | 750 | private static String doubleQuote(String s) { 751 | if (s.startsWith("\"") && s.endsWith("\"")) { 752 | if (escaped("\"", s)) 753 | return s; 754 | return s.replace("\"", "\\\""); 755 | } 756 | return "\"" + s.replace("\"", "\\\"") + "\""; 757 | } 758 | 759 | private static boolean escaped(String s, String in) { 760 | int idx = 0; 761 | while ((idx = in.indexOf(s, idx)) != -1) { 762 | if (idx == 0 || in.charAt(idx - 1) != '\\') 763 | return false; 764 | } 765 | return true; 766 | } 767 | 768 | private static String removeCapsulePort(String s) { 769 | final Matcher m = CAPSULE_PORT_PATTERN.matcher(s); 770 | return m.replaceFirst(""); 771 | } 772 | 773 | // 774 | 775 | // 776 | private static final String ATTR_MAIN_CLASS = "Main-Class"; 777 | 778 | private static boolean emptyOrTrue(String value) { 779 | return value != null && (value.isEmpty() || Boolean.parseBoolean(value)); 780 | } 781 | 782 | private static String getMainClass(Path jar) { 783 | return getMainClass(getManifest(jar)); 784 | } 785 | 786 | private static String getMainClass(Manifest manifest) { 787 | if (manifest == null) 788 | return null; 789 | return manifest.getMainAttributes().getValue(ATTR_MAIN_CLASS); 790 | } 791 | 792 | private static Manifest getManifest(Path jar) { 793 | try (JarInputStream jis = openJarInputStream(jar)) { 794 | return jis.getManifest(); 795 | } catch (IOException e) { 796 | throw new RuntimeException("Error reading manifest from " + jar, e); 797 | } 798 | } 799 | 800 | private static JarInputStream openJarInputStream(Path jar) throws IOException { 801 | return new JarInputStream(skipToZipStart(Files.newInputStream(jar), null)); 802 | } 803 | 804 | private static final int[] ZIP_HEADER = new int[]{'P', 'K', 0x03, 0x04}; 805 | 806 | private static InputStream skipToZipStart(InputStream is, OutputStream os) throws IOException { 807 | if (!is.markSupported()) 808 | is = new BufferedInputStream(is); 809 | int state = 0; 810 | for (; ; ) { 811 | if (state == 0) 812 | is.mark(ZIP_HEADER.length); 813 | final int b = is.read(); 814 | if (b < 0) 815 | throw new IllegalArgumentException("Not a JAR/ZIP file"); 816 | if (state >= 0 && b == ZIP_HEADER[state]) { 817 | state++; 818 | if (state == ZIP_HEADER.length) 819 | break; 820 | } else { 821 | state = -1; 822 | if (b == '\n' || b == 0) // start matching on \n and \0 823 | state = 0; 824 | } 825 | if (os != null) 826 | os.write(b); 827 | } 828 | is.reset(); 829 | return is; 830 | } 831 | 832 | private static void copy0(InputStream is, OutputStream out) throws IOException { 833 | final byte[] buffer = new byte[1024]; 834 | for (int bytesRead; (bytesRead = is.read(buffer)) != -1; ) 835 | out.write(buffer, 0, bytesRead); 836 | out.flush(); 837 | } 838 | // 839 | } 840 | --------------------------------------------------------------------------------