OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 80 | Rev 177 | Go to most recent revision | 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
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
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;
18
import org.openconcerto.utils.ProcessStreams;
19
import org.openconcerto.utils.ProcessStreams.Action;
20
import org.openconcerto.utils.PropertiesUtils;
21
 
22
import java.io.File;
23
import java.io.IOException;
24
import java.lang.management.ManagementFactory;
25
import java.util.ArrayList;
26
import java.util.Arrays;
27
import java.util.Collections;
28
import java.util.List;
29
import java.util.Map;
30
import java.util.Properties;
31
import java.util.regex.Matcher;
32
import java.util.regex.Pattern;
33
 
34
/**
35
 * A class meant to be used as the main class of a jar and which launch another instance of the Java
36
 * VM.
37
 *
38
 * @author Sylvain
39
 * @see #launch(String, List)
40
 */
41
public abstract class VMLauncher {
42
 
43
    /**
44
     * Boolean system property, if set to <code>true</code> then {@link #restart(Class, List)} will
45
     * simply return <code>null</code>. Useful e.g. when using IDE launch configuration (to debug).
46
     */
47
    static public final String NO_RESTART = "vm.noRestart";
48
 
49
    private static NativeLauncherFinder getNativeAppLauncher() {
50
        final OSFamily os = OSFamily.getInstance();
51
        final NativeLauncherFinder l;
52
        if (os.equals(OSFamily.Windows)) {
53
            l = new WinLauncherFinder();
54
        } else if (os.equals(OSFamily.Mac)) {
55
            l = new MacLauncherFinder();
56
        } else {
57
            l = UnknownLauncherFinder;
58
        }
59
        return l;
60
    }
61
 
62
    private static List<String> getNativeCommand(List<String> args) {
63
        final NativeLauncherFinder l = getNativeAppLauncher();
64
        return l.getAppPath() == null ? null : l.getCommand(args);
65
    }
66
 
67
    /**
68
     * Allow to find out if the running VM was launched using a native application.
69
     *
70
     * @author Sylvain
71
     */
72
    private static abstract class NativeLauncherFinder {
73
        private final String cp, firstItem;
74
 
75
        public NativeLauncherFinder() {
76
            this.cp = ManagementFactory.getRuntimeMXBean().getClassPath();
77
            final int sepIndex = this.cp.indexOf(File.pathSeparatorChar);
78
            this.firstItem = sepIndex < 0 ? this.cp : this.cp.substring(0, sepIndex);
79
        }
80
 
81
        public final String getClassPath() {
82
            return this.cp;
83
        }
84
 
85
        public final String getFirstItem() {
86
            return this.firstItem;
87
        }
88
 
89
        /**
90
         * The path to the native application if any.
91
         *
92
         * @return the path, <code>null</code> if no native application could be found.
93
         */
94
        public abstract String getAppPath();
95
 
96
        /**
97
         * The command to launch this application with the passed arguments.
98
         *
99
         * @param args the program arguments.
100
         * @return the command.
101
         */
102
        public abstract List<String> getCommand(final List<String> args);
103
    }
104
 
105
    private static class MacLauncherFinder extends NativeLauncherFinder {
106
        private static final String APP_EXT = ".app";
107
        private static final Pattern MAC_PATTERN = Pattern.compile(Pattern.quote(APP_EXT) + "/Contents/Resources(/Java)?/[^/]+\\.jar$");
108
 
109
        @Override
110
        public String getAppPath() {
111
            final Matcher matcher = MAC_PATTERN.matcher(this.getFirstItem());
112
            if (matcher.matches()) {
113
                final String appPath = getFirstItem().substring(0, matcher.start() + APP_EXT.length());
114
                final File contentsDir = new File(appPath, "Contents");
115
                final List<String> bundleContent = Arrays.asList(contentsDir.list());
116
                if (bundleContent.contains("Info.plist") && bundleContent.contains("PkgInfo") && new File(contentsDir, "MacOS").isDirectory())
117
                    return appPath;
118
            }
119
            return null;
120
        }
121
 
122
        @Override
123
        public List<String> getCommand(List<String> args) {
124
            final List<String> command = new ArrayList<String>(4 + args.size());
125
            command.add("open");
126
            // since we restarting we need to launch a new instance of us
127
            command.add("-n");
128
            command.add(getAppPath());
129
            command.add("--args");
130
            command.addAll(args);
131
            return command;
132
        }
133
    }
134
 
135
    private static class WinLauncherFinder extends NativeLauncherFinder {
136
        @Override
137
        public String getAppPath() {
138
            // launch4j
139
            if (this.getFirstItem().endsWith(".exe"))
140
                return getFirstItem();
141
            else
142
                return null;
143
        }
144
 
145
        @Override
146
        public List<String> getCommand(List<String> args) {
147
            final List<String> command = new ArrayList<String>(4 + args.size());
148
            command.add(getAppPath());
149
            command.addAll(args);
150
            return command;
151
        }
152
    }
153
 
154
    private static final NativeLauncherFinder UnknownLauncherFinder = new NativeLauncherFinder() {
155
        @Override
156
        public String getAppPath() {
157
            return null;
158
        }
159
 
160
        @Override
161
        public List<String> getCommand(List<String> args) {
162
            throw new UnsupportedOperationException();
163
        }
164
    };
165
 
166
    public static final Process restart(final Class<?> mainClass, final String... args) throws IOException {
167
        return restart(mainClass, Arrays.asList(args));
168
    }
169
 
170
    public static final Process restart(final Action action, final Class<?> mainClass, final String... args) throws IOException {
171
        return restart(action, mainClass, Arrays.asList(args));
172
    }
173
 
174
    /**
175
     * Restart the VM. If this VM was launched using a native application (e.g. .exe or .app) then
176
     * this will be executed. Else the <code>mainClass</code> will be used.
177
     *
178
     * @param mainClass the main() to use (if no native application was found).
179
     * @param args the program arguments to pass.
180
     * @return the new process, <code>null</code> if the program wasn't started.
181
     * @throws IOException if the VM couldn't be launched.
182
     * @see #NO_RESTART
183
     */
184
    public static final Process restart(final Class<?> mainClass, final List<String> args) throws IOException {
185
        return restart(Action.CLOSE, mainClass, args);
186
    }
187
 
188
    public static final Process restart(final Action action, final Class<?> mainClass, final List<String> args) throws IOException {
189
        if (Boolean.getBoolean(NO_RESTART))
190
            return null;
191
        final File wd = FileUtils.getWD();
192
        final List<String> command = getNativeCommand(args);
193
        if (command != null) {
194
            return ProcessStreams.handle(new ProcessBuilder(command).directory(wd).start(), action);
195
        } else {
196
            try {
197
                mainClass.getMethod("main", String[].class);
198
            } catch (NoSuchMethodException e) {
199
                throw new IllegalArgumentException(mainClass + " doesn't containt a main()", e);
200
            }
201
            return new VMLauncher() {
202
                @Override
203
                protected File getWD() {
204
                    return wd;
205
                }
206
 
207
                @Override
208
                protected File getPropFile(String mainClass) {
209
                    return null;
210
                }
211
 
212
                @Override
213
                protected Action getStreamAction() {
214
                    return action;
215
                }
216
            }.launch(mainClass.getName(), args);
217
        }
218
    }
219
 
220
    public static final String ENV_VMARGS = "JAVA_VMARGS";
221
    public static final String PROPS_VMARGS = "VMARGS";
222
    public static final String ENV_PROGARGS = "JAVA_PROGARGS";
223
 
224
    // handle DOS, Mac and Unix newlines
225
    private static final Pattern NL = Pattern.compile("\\p{Cntrl}+");
226
 
227
    private File wd;
228
 
229
    public VMLauncher() {
230
        this.wd = null;
231
    }
232
 
233
    public final File getLauncherWD() {
234
        if (this.wd == null) {
235
            final NativeLauncherFinder nativeAppLauncher = getNativeAppLauncher();
236
            final String appPath = nativeAppLauncher.getAppPath();
237
            if (appPath != null)
238
                this.wd = new File(appPath).getAbsoluteFile().getParentFile();
239
            // when launched with -jar there's only one item
240
            else if (nativeAppLauncher.getFirstItem().equals(nativeAppLauncher.getClassPath()) && new File(nativeAppLauncher.getFirstItem()).isFile())
241
                this.wd = new File(nativeAppLauncher.getFirstItem()).getParentFile();
242
            // support launch in an IDE
243
            else
244
                this.wd = FileUtils.getWD();
245
        }
246
        return this.wd;
247
    }
248
 
249
    private final List<String> split(String res) {
250
        res = res.trim();
251
        if (res.length() == 0) {
252
            return Collections.emptyList();
253
        } else {
254
            return Arrays.asList(NL.split(res));
255
        }
256
    }
257
 
258
    private final List<String> getProp(final File propFile, final String propName) {
259
        return this.getProp(this.getProps(propFile), propName);
260
    }
261
 
262
    private final Properties getProps(final File propFile) {
263
        if (propFile != null && propFile.canRead()) {
264
            try {
265
                return PropertiesUtils.createFromFile(propFile);
266
            } catch (IOException e) {
267
                e.printStackTrace();
268
            }
269
        }
270
        return new Properties();
271
    }
272
 
273
    private final List<String> getProp(final Properties props, final String propName) {
274
        String res = "";
275
        if (props != null) {
276
            res = props.getProperty(propName, res);
277
        }
278
        return split(res);
279
    }
280
 
281
    protected final Process launch(final String mainClass) throws IOException {
282
        return this.launch(mainClass, Collections.<String> emptyList());
283
    }
284
 
285
    /**
286
     * Launch a new Java VM. This method will try to launch {@link #getJavaBinary() java} from the
287
     * same installation, if that fails it will use the system path for binaries. VM arguments can
288
     * be specified with :
289
     * <ol>
290
     * <li>the {@value #ENV_VMARGS} environment variable</li>
291
     * <li>the {@value #PROPS_VMARGS} property in {@link #getPropFile(String)}</li>
292
     * <li>the {@link #getVMArguments()} method</li>
293
     * </ol>
294
     * Program arguments :
295
     * <ol>
296
     * <li>the <code>progParams</code> parameter</li>
297
     * <li>the {@value #ENV_PROGARGS} environment variable</li>
298
     * </ol>
299
     *
300
     * @param mainClass the main class.
301
     * @param progParams the program arguments for <code>mainClass</code>.
302
     * @return the new Process.
303
     * @throws IOException if the process couldn't be started.
304
     */
305
    protected final Process launch(final String mainClass, final List<String> progParams) throws IOException {
306
        final boolean debug = Boolean.getBoolean("launcher.debug");
307
        final String javaBinary = getJavaBinary();
308
        final File sameJava = new File(System.getProperty("java.home"), "bin/" + javaBinary);
309
        final String java = sameJava.canExecute() ? sameJava.getAbsolutePath() : javaBinary;
310
        final File propFile = this.getPropFile(mainClass);
311
        final Properties props = this.getProps(propFile);
312
        if (debug)
313
            System.err.println("propFile : " + propFile);
314
 
315
        final List<String> command = new ArrayList<String>();
316
        command.add(java);
317
 
318
        if (this.enableRemoteDebug(props)) {
93 ilm 319
            command.add(RemoteDebugArgs.getArgs(props.getProperty("remoteDebugAddr")));
80 ilm 320
        }
321
        command.addAll(this.getVMArguments());
322
 
323
        // for java the last specified property wins
324
        if (propFile != null) {
325
            final List<String> appProps = this.getProp(props, PROPS_VMARGS);
326
            command.addAll(appProps);
327
            final File userFile = new File(System.getProperty("user.home"), ".java/ilm/" + propFile.getName());
328
            final List<String> userProps = this.getProp(userFile, PROPS_VMARGS);
329
            command.addAll(userProps);
330
            if (debug) {
331
                System.err.println("appProps : " + appProps);
332
                System.err.println("userProps ( from " + userFile + ") : " + userProps);
333
            }
334
        }
335
        final String envVMArgs = System.getenv(ENV_VMARGS);
336
        if (envVMArgs != null)
337
            command.addAll(split(envVMArgs));
338
 
339
        command.add("-cp");
340
        command.add(getClassPath());
341
        command.add(mainClass);
342
        final String envProgArgs = System.getenv(ENV_PROGARGS);
343
        if (envProgArgs != null)
344
            command.addAll(split(envProgArgs));
345
        command.addAll(progParams);
346
 
347
        // inherit environment so that the next launch() can access the same variables
348
        final ProcessBuilder procBuilder = new ProcessBuilder(command).directory(getWD());
349
        this.modifyEnv(procBuilder.environment());
350
        if (debug) {
351
            System.err.println("Command line : " + procBuilder.command());
352
            System.err.println("Dir : " + procBuilder.directory());
353
            System.err.println("Std out and err :");
354
        }
355
 
356
        final Process res = procBuilder.start();
357
        ProcessStreams.handle(res, debug ? Action.REDIRECT : this.getStreamAction());
358
 
359
        return res;
360
    }
361
 
362
    protected void modifyEnv(Map<String, String> environment) {
363
    }
364
 
365
    protected Action getStreamAction() {
366
        return Action.CLOSE;
367
    }
368
 
369
    protected boolean enableRemoteDebug(Properties props) {
370
        final String prop = props.getProperty("remoteDebug");
371
        return prop == null ? remoteDebugDefault() : Boolean.parseBoolean(prop);
372
    }
373
 
374
    protected boolean remoteDebugDefault() {
375
        return false;
376
    }
377
 
378
    /**
379
     * The program to launch. This implementation returns <code>javaw</code> for Windows and
380
     * <code>java</code> for other OS.
381
     *
382
     * @return the name of the binary.
383
     */
384
    protected String getJavaBinary() {
385
        return OSFamily.getInstance() == OSFamily.Windows ? "javaw" : "java";
386
    }
387
 
388
    protected List<String> getVMArguments() {
389
        return Arrays.asList("-Dfile.encoding=UTF-8", "-Xms100M", "-Xmx256M");
390
    }
391
 
392
    // by default in the same jar
393
    protected String getClassPath() {
394
        return ManagementFactory.getRuntimeMXBean().getClassPath();
395
    }
396
 
397
    // by default in the same directory
398
    protected File getWD() {
399
        return this.getLauncherWD();
400
    }
401
 
402
    protected File getPropFile(final String mainClass) {
403
        final String className = mainClass.substring(mainClass.lastIndexOf('.') + 1);
404
        return new File(getWD(), className + ".properties");
405
    }
406
}