OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 180 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
80 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
185 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
80 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.utils.prog;
15
 
16
import org.openconcerto.utils.FileUtils;
17
import org.openconcerto.utils.OSFamily;
180 ilm 18
import org.openconcerto.utils.OSFamily.Unix;
80 ilm 19
import org.openconcerto.utils.ProcessStreams;
20
import org.openconcerto.utils.PropertiesUtils;
21
 
22
import java.io.File;
23
import java.io.IOException;
177 ilm 24
import java.lang.ProcessBuilder.Redirect;
80 ilm 25
import java.lang.management.ManagementFactory;
26
import java.util.ArrayList;
27
import java.util.Arrays;
28
import java.util.Collections;
29
import java.util.List;
30
import java.util.Map;
31
import java.util.Properties;
32
import java.util.regex.Matcher;
33
import java.util.regex.Pattern;
34
 
35
/**
36
 * A class meant to be used as the main class of a jar and which launch another instance of the Java
37
 * VM.
38
 *
39
 * @author Sylvain
40
 * @see #launch(String, List)
41
 */
42
public abstract class VMLauncher {
43
 
180 ilm 44
    private static final String PROPERTIES_EXT = ".properties";
80 ilm 45
    /**
46
     * Boolean system property, if set to <code>true</code> then {@link #restart(Class, List)} will
47
     * simply return <code>null</code>. Useful e.g. when using IDE launch configuration (to debug).
48
     */
49
    static public final String NO_RESTART = "vm.noRestart";
50
 
180 ilm 51
    // Explicitly passed to jpackage
52
    static public final String APPDIR_SYSPROP = "jpackage.app.dir";
53
 
54
    // Automatically set by the jpackage launcher (could be set explicitly using --java-options
55
    // '-Djpackage.app-path=$APPDIR/../../bin/launcher' if jpackage ever changes that)
56
    static public final String APP_EXE_SYSPROP = "jpackage.app-path";
57
 
58
    // The path to the app directory with the jar
59
    public static final File getJPackageAppDir() {
60
        final String appPath = System.getProperty(APPDIR_SYSPROP, "");
61
        return appPath.isEmpty() ? null : new File(appPath);
62
    }
63
 
64
    // The path to the executable
65
    private static final String getJPackageAppPath() {
66
        final String appPath = System.getProperty(APP_EXE_SYSPROP, "");
67
        return appPath.isEmpty() ? null : appPath;
68
    }
69
 
70
    private static final void addJPackageSystemPropertyArgument(final List<String> args, final String propName) {
71
        final String arg = getJPackageSystemPropertyArg(propName);
72
        if (arg != null)
73
            args.add(arg);
74
    }
75
 
76
    private static final String getJPackageSystemPropertyArg(final String propName) {
77
        final String val = System.getProperty(propName);
78
        if (val == null)
79
            return null;
80
        return "-D" + propName + "=" + val;
81
    }
82
 
80 ilm 83
    private static NativeLauncherFinder getNativeAppLauncher() {
84
        final OSFamily os = OSFamily.getInstance();
85
        final NativeLauncherFinder l;
86
        if (os.equals(OSFamily.Windows)) {
87
            l = new WinLauncherFinder();
88
        } else if (os.equals(OSFamily.Mac)) {
89
            l = new MacLauncherFinder();
180 ilm 90
        } else if (os instanceof Unix) {
91
            l = new UnixLauncherFinder();
80 ilm 92
        } else {
93
            l = UnknownLauncherFinder;
94
        }
95
        return l;
96
    }
97
 
98
    private static List<String> getNativeCommand(List<String> args) {
99
        final NativeLauncherFinder l = getNativeAppLauncher();
100
        return l.getAppPath() == null ? null : l.getCommand(args);
101
    }
102
 
103
    /**
104
     * Allow to find out if the running VM was launched using a native application.
105
     *
106
     * @author Sylvain
107
     */
108
    private static abstract class NativeLauncherFinder {
109
        private final String cp, firstItem;
110
 
111
        public NativeLauncherFinder() {
112
            this.cp = ManagementFactory.getRuntimeMXBean().getClassPath();
113
            final int sepIndex = this.cp.indexOf(File.pathSeparatorChar);
114
            this.firstItem = sepIndex < 0 ? this.cp : this.cp.substring(0, sepIndex);
115
        }
116
 
117
        public final String getClassPath() {
118
            return this.cp;
119
        }
120
 
121
        public final String getFirstItem() {
122
            return this.firstItem;
123
        }
124
 
125
        /**
126
         * The path to the native application if any.
127
         *
128
         * @return the path, <code>null</code> if no native application could be found.
129
         */
130
        public abstract String getAppPath();
131
 
132
        /**
133
         * The command to launch this application with the passed arguments.
134
         *
135
         * @param args the program arguments.
136
         * @return the command.
180 ilm 137
         * @throws UnsupportedOperationException if {@link #getAppPath()} returns <code>null</code>.
80 ilm 138
         */
180 ilm 139
        public final List<String> getCommand(final List<String> args) throws UnsupportedOperationException {
140
            final String appPath = this.getAppPath();
141
            if (appPath == null)
142
                throw new UnsupportedOperationException();
143
 
144
            return getCommand(appPath, args);
145
        }
146
 
147
        protected List<String> getCommand(final String appPath, final List<String> args) {
148
            final List<String> command = new ArrayList<String>(4 + args.size());
149
            command.add(appPath);
150
            command.addAll(args);
151
            return command;
152
        }
80 ilm 153
    }
154
 
155
    private static class MacLauncherFinder extends NativeLauncherFinder {
156
        private static final String APP_EXT = ".app";
180 ilm 157
        // jpackage uses "Contents/app"
158
        private static final Pattern MAC_PATTERN = Pattern.compile(Pattern.quote(APP_EXT) + "/Contents/(Resources(/Java)?|app)/[^/]+\\.jar$");
80 ilm 159
 
160
        @Override
161
        public String getAppPath() {
162
            final Matcher matcher = MAC_PATTERN.matcher(this.getFirstItem());
163
            if (matcher.matches()) {
164
                final String appPath = getFirstItem().substring(0, matcher.start() + APP_EXT.length());
165
                final File contentsDir = new File(appPath, "Contents");
180 ilm 166
                if (new File(contentsDir, "Info.plist").isFile() && new File(contentsDir, "MacOS").isDirectory())
80 ilm 167
                    return appPath;
168
            }
169
            return null;
170
        }
171
 
172
        @Override
180 ilm 173
        protected List<String> getCommand(String appPath, List<String> args) {
80 ilm 174
            final List<String> command = new ArrayList<String>(4 + args.size());
175
            command.add("open");
176
            // since we restarting we need to launch a new instance of us
177
            command.add("-n");
180 ilm 178
            command.add(appPath);
80 ilm 179
            command.add("--args");
180
            command.addAll(args);
181
            return command;
182
        }
183
    }
184
 
185
    private static class WinLauncherFinder extends NativeLauncherFinder {
186
        @Override
187
        public String getAppPath() {
188
            // launch4j
189
            if (this.getFirstItem().endsWith(".exe"))
190
                return getFirstItem();
191
            else
192
                return null;
193
        }
180 ilm 194
    }
80 ilm 195
 
180 ilm 196
    private static class UnixLauncherFinder extends NativeLauncherFinder {
197
 
198
        private final String jpackageApp;
199
 
200
        public UnixLauncherFinder() {
201
            this.jpackageApp = getJPackageAppPath();
202
        }
203
 
80 ilm 204
        @Override
180 ilm 205
        public String getAppPath() {
206
            return this.jpackageApp;
80 ilm 207
        }
208
    }
209
 
210
    private static final NativeLauncherFinder UnknownLauncherFinder = new NativeLauncherFinder() {
211
        @Override
212
        public String getAppPath() {
213
            return null;
214
        }
215
    };
216
 
217
    public static final Process restart(final Class<?> mainClass, final String... args) throws IOException {
218
        return restart(mainClass, Arrays.asList(args));
219
    }
220
 
177 ilm 221
    public static final Process restart(final Redirect action, final Class<?> mainClass, final String... args) throws IOException {
80 ilm 222
        return restart(action, mainClass, Arrays.asList(args));
223
    }
224
 
225
    /**
226
     * Restart the VM. If this VM was launched using a native application (e.g. .exe or .app) then
227
     * this will be executed. Else the <code>mainClass</code> will be used.
228
     *
229
     * @param mainClass the main() to use (if no native application was found).
230
     * @param args the program arguments to pass.
231
     * @return the new process, <code>null</code> if the program wasn't started.
232
     * @throws IOException if the VM couldn't be launched.
233
     * @see #NO_RESTART
234
     */
235
    public static final Process restart(final Class<?> mainClass, final List<String> args) throws IOException {
177 ilm 236
        return restart(ProcessStreams.DISCARD, mainClass, args);
80 ilm 237
    }
238
 
177 ilm 239
    public static final Process restart(final Redirect action, final Class<?> mainClass, final List<String> args) throws IOException {
80 ilm 240
        if (Boolean.getBoolean(NO_RESTART))
241
            return null;
242
        final File wd = FileUtils.getWD();
243
        final List<String> command = getNativeCommand(args);
244
        if (command != null) {
177 ilm 245
            return new ProcessBuilder(command).directory(wd).redirectErrorStream(true).redirectOutput(action).start();
80 ilm 246
        } else {
247
            try {
248
                mainClass.getMethod("main", String[].class);
249
            } catch (NoSuchMethodException e) {
250
                throw new IllegalArgumentException(mainClass + " doesn't containt a main()", e);
251
            }
252
            return new VMLauncher() {
253
                @Override
254
                protected File getWD() {
255
                    return wd;
256
                }
257
 
258
                @Override
259
                protected File getPropFile(String mainClass) {
260
                    return null;
261
                }
262
 
263
                @Override
177 ilm 264
                protected Redirect getStreamRedirect() {
80 ilm 265
                    return action;
266
                }
267
            }.launch(mainClass.getName(), args);
268
        }
269
    }
270
 
271
    public static final String ENV_VMARGS = "JAVA_VMARGS";
272
    public static final String PROPS_VMARGS = "VMARGS";
273
    public static final String ENV_PROGARGS = "JAVA_PROGARGS";
274
 
180 ilm 275
    // Don't split on spaces to avoid dealing with quotes or escapes : vmArgs=-Dfoo bar\t-DotherProp
276
    // Handle DOS, Mac and Unix newlines (and tabs).
80 ilm 277
    private static final Pattern NL = Pattern.compile("\\p{Cntrl}+");
278
 
279
    private File wd;
280
 
281
    public VMLauncher() {
282
        this.wd = null;
283
    }
284
 
285
    public final File getLauncherWD() {
286
        if (this.wd == null) {
180 ilm 287
            final File appDir = getJPackageAppDir();
288
            if (appDir == null) {
289
                final NativeLauncherFinder nativeAppLauncher = getNativeAppLauncher();
290
                final String appPath = nativeAppLauncher.getAppPath();
291
                if (appPath != null)
292
                    this.wd = new File(appPath).getAbsoluteFile().getParentFile();
293
                // when launched with -jar there's only one item
294
                else if (nativeAppLauncher.getFirstItem().equals(nativeAppLauncher.getClassPath()) && new File(nativeAppLauncher.getFirstItem()).isFile())
295
                    this.wd = new File(nativeAppLauncher.getFirstItem()).getParentFile();
296
                // support launch in an IDE
297
                else
298
                    this.wd = FileUtils.getWD();
299
            } else {
300
                this.wd = appDir;
301
            }
80 ilm 302
        }
303
        return this.wd;
304
    }
305
 
306
    private final List<String> split(String res) {
307
        res = res.trim();
308
        if (res.length() == 0) {
309
            return Collections.emptyList();
310
        } else {
311
            return Arrays.asList(NL.split(res));
312
        }
313
    }
314
 
315
    private final List<String> getProp(final File propFile, final String propName) {
316
        return this.getProp(this.getProps(propFile), propName);
317
    }
318
 
319
    private final Properties getProps(final File propFile) {
320
        if (propFile != null && propFile.canRead()) {
321
            try {
322
                return PropertiesUtils.createFromFile(propFile);
323
            } catch (IOException e) {
324
                e.printStackTrace();
325
            }
326
        }
327
        return new Properties();
328
    }
329
 
330
    private final List<String> getProp(final Properties props, final String propName) {
331
        String res = "";
332
        if (props != null) {
333
            res = props.getProperty(propName, res);
334
        }
335
        return split(res);
336
    }
337
 
177 ilm 338
    public final Process launch(final String mainClass) throws IOException {
80 ilm 339
        return this.launch(mainClass, Collections.<String> emptyList());
340
    }
341
 
342
    /**
343
     * Launch a new Java VM. This method will try to launch {@link #getJavaBinary() java} from the
344
     * same installation, if that fails it will use the system path for binaries. VM arguments can
345
     * be specified with :
346
     * <ol>
347
     * <li>the {@value #ENV_VMARGS} environment variable</li>
348
     * <li>the {@value #PROPS_VMARGS} property in {@link #getPropFile(String)}</li>
349
     * <li>the {@link #getVMArguments()} method</li>
350
     * </ol>
351
     * Program arguments :
352
     * <ol>
353
     * <li>the <code>progParams</code> parameter</li>
354
     * <li>the {@value #ENV_PROGARGS} environment variable</li>
355
     * </ol>
356
     *
357
     * @param mainClass the main class.
358
     * @param progParams the program arguments for <code>mainClass</code>.
359
     * @return the new Process.
360
     * @throws IOException if the process couldn't be started.
361
     */
177 ilm 362
    public final Process launch(final String mainClass, final List<String> progParams) throws IOException {
80 ilm 363
        final boolean debug = Boolean.getBoolean("launcher.debug");
364
        final String javaBinary = getJavaBinary();
365
        final File sameJava = new File(System.getProperty("java.home"), "bin/" + javaBinary);
180 ilm 366
        // allow to know what binary (and thus java.home) was tested
367
        if (debug)
368
            System.err.println("sameJava : " + sameJava);
80 ilm 369
        final String java = sameJava.canExecute() ? sameJava.getAbsolutePath() : javaBinary;
370
        final File propFile = this.getPropFile(mainClass);
371
        final Properties props = this.getProps(propFile);
372
        if (debug)
373
            System.err.println("propFile : " + propFile);
374
 
375
        final List<String> command = new ArrayList<String>();
376
        command.add(java);
377
 
378
        if (this.enableRemoteDebug(props)) {
93 ilm 379
            command.add(RemoteDebugArgs.getArgs(props.getProperty("remoteDebugAddr")));
80 ilm 380
        }
381
        command.addAll(this.getVMArguments());
382
 
180 ilm 383
        // For java the last specified property wins. MAYBE concat properties whose names start with
384
        // PROPS_VMARGS, that way we could override just one of many e.g. VMARGS.garbageCollector.
80 ilm 385
        if (propFile != null) {
386
            final List<String> appProps = this.getProp(props, PROPS_VMARGS);
387
            command.addAll(appProps);
388
            if (debug) {
180 ilm 389
                System.err.println("VM arguments from " + propFile + " : " + appProps);
80 ilm 390
            }
180 ilm 391
            // Don't use Properties(defaults) constructor since we want to combine values.
392
            final File localFile = FileUtils.prependSuffix(propFile, "-local", PROPERTIES_EXT);
393
            final File userFile = new File(System.getProperty("user.home"), ".java/ilm/" + propFile.getName());
394
            for (final File f : Arrays.asList(localFile, userFile)) {
395
                final List<String> moreProps = this.getProp(f, PROPS_VMARGS);
396
                command.addAll(moreProps);
397
                if (debug) {
398
                    System.err.println("VM arguments from " + f + " : " + moreProps);
399
                }
400
            }
80 ilm 401
        }
402
        final String envVMArgs = System.getenv(ENV_VMARGS);
403
        if (envVMArgs != null)
404
            command.addAll(split(envVMArgs));
180 ilm 405
        // launched app may also need this context
406
        addJPackageSystemPropertyArgument(command, APPDIR_SYSPROP);
407
        addJPackageSystemPropertyArgument(command, APP_EXE_SYSPROP);
80 ilm 408
 
409
        command.add("-cp");
410
        command.add(getClassPath());
411
        command.add(mainClass);
412
        final String envProgArgs = System.getenv(ENV_PROGARGS);
413
        if (envProgArgs != null)
414
            command.addAll(split(envProgArgs));
415
        command.addAll(progParams);
416
 
417
        // inherit environment so that the next launch() can access the same variables
418
        final ProcessBuilder procBuilder = new ProcessBuilder(command).directory(getWD());
419
        this.modifyEnv(procBuilder.environment());
420
        if (debug) {
421
            System.err.println("Command line : " + procBuilder.command());
422
            System.err.println("Dir : " + procBuilder.directory());
423
            System.err.println("Std out and err :");
424
        }
425
 
177 ilm 426
        procBuilder.redirectErrorStream(true).redirectOutput(debug ? Redirect.INHERIT : this.getStreamRedirect());
427
        return procBuilder.start();
80 ilm 428
    }
429
 
430
    protected void modifyEnv(Map<String, String> environment) {
431
    }
432
 
177 ilm 433
    protected Redirect getStreamRedirect() {
434
        return ProcessStreams.DISCARD;
80 ilm 435
    }
436
 
437
    protected boolean enableRemoteDebug(Properties props) {
438
        final String prop = props.getProperty("remoteDebug");
439
        return prop == null ? remoteDebugDefault() : Boolean.parseBoolean(prop);
440
    }
441
 
442
    protected boolean remoteDebugDefault() {
443
        return false;
444
    }
445
 
446
    /**
447
     * The program to launch. This implementation returns <code>javaw</code> for Windows and
448
     * <code>java</code> for other OS.
449
     *
450
     * @return the name of the binary.
451
     */
452
    protected String getJavaBinary() {
453
        return OSFamily.getInstance() == OSFamily.Windows ? "javaw" : "java";
454
    }
455
 
456
    protected List<String> getVMArguments() {
457
        return Arrays.asList("-Dfile.encoding=UTF-8", "-Xms100M", "-Xmx256M");
458
    }
459
 
460
    // by default in the same jar
461
    protected String getClassPath() {
462
        return ManagementFactory.getRuntimeMXBean().getClassPath();
463
    }
464
 
465
    // by default in the same directory
466
    protected File getWD() {
467
        return this.getLauncherWD();
468
    }
469
 
470
    protected File getPropFile(final String mainClass) {
471
        final String className = mainClass.substring(mainClass.lastIndexOf('.') + 1);
180 ilm 472
        return new File(getWD(), className + PROPERTIES_EXT);
80 ilm 473
    }
474
}