OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 93 | Rev 180 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 package org.openconcerto.utils.prog;

import org.openconcerto.utils.FileUtils;
import org.openconcerto.utils.OSFamily;
import org.openconcerto.utils.ProcessStreams;
import org.openconcerto.utils.PropertiesUtils;

import java.io.File;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A class meant to be used as the main class of a jar and which launch another instance of the Java
 * VM.
 * 
 * @author Sylvain
 * @see #launch(String, List)
 */
public abstract class VMLauncher {

    /**
     * Boolean system property, if set to <code>true</code> then {@link #restart(Class, List)} will
     * simply return <code>null</code>. Useful e.g. when using IDE launch configuration (to debug).
     */
    static public final String NO_RESTART = "vm.noRestart";

    private static NativeLauncherFinder getNativeAppLauncher() {
        final OSFamily os = OSFamily.getInstance();
        final NativeLauncherFinder l;
        if (os.equals(OSFamily.Windows)) {
            l = new WinLauncherFinder();
        } else if (os.equals(OSFamily.Mac)) {
            l = new MacLauncherFinder();
        } else {
            l = UnknownLauncherFinder;
        }
        return l;
    }

    private static List<String> getNativeCommand(List<String> args) {
        final NativeLauncherFinder l = getNativeAppLauncher();
        return l.getAppPath() == null ? null : l.getCommand(args);
    }

    /**
     * Allow to find out if the running VM was launched using a native application.
     * 
     * @author Sylvain
     */
    private static abstract class NativeLauncherFinder {
        private final String cp, firstItem;

        public NativeLauncherFinder() {
            this.cp = ManagementFactory.getRuntimeMXBean().getClassPath();
            final int sepIndex = this.cp.indexOf(File.pathSeparatorChar);
            this.firstItem = sepIndex < 0 ? this.cp : this.cp.substring(0, sepIndex);
        }

        public final String getClassPath() {
            return this.cp;
        }

        public final String getFirstItem() {
            return this.firstItem;
        }

        /**
         * The path to the native application if any.
         * 
         * @return the path, <code>null</code> if no native application could be found.
         */
        public abstract String getAppPath();

        /**
         * The command to launch this application with the passed arguments.
         * 
         * @param args the program arguments.
         * @return the command.
         */
        public abstract List<String> getCommand(final List<String> args);
    }

    private static class MacLauncherFinder extends NativeLauncherFinder {
        private static final String APP_EXT = ".app";
        private static final Pattern MAC_PATTERN = Pattern.compile(Pattern.quote(APP_EXT) + "/Contents/Resources(/Java)?/[^/]+\\.jar$");

        @Override
        public String getAppPath() {
            final Matcher matcher = MAC_PATTERN.matcher(this.getFirstItem());
            if (matcher.matches()) {
                final String appPath = getFirstItem().substring(0, matcher.start() + APP_EXT.length());
                final File contentsDir = new File(appPath, "Contents");
                final List<String> bundleContent = Arrays.asList(contentsDir.list());
                if (bundleContent.contains("Info.plist") && bundleContent.contains("PkgInfo") && new File(contentsDir, "MacOS").isDirectory())
                    return appPath;
            }
            return null;
        }

        @Override
        public List<String> getCommand(List<String> args) {
            final List<String> command = new ArrayList<String>(4 + args.size());
            command.add("open");
            // since we restarting we need to launch a new instance of us
            command.add("-n");
            command.add(getAppPath());
            command.add("--args");
            command.addAll(args);
            return command;
        }
    }

    private static class WinLauncherFinder extends NativeLauncherFinder {
        @Override
        public String getAppPath() {
            // launch4j
            if (this.getFirstItem().endsWith(".exe"))
                return getFirstItem();
            else
                return null;
        }

        @Override
        public List<String> getCommand(List<String> args) {
            final List<String> command = new ArrayList<String>(4 + args.size());
            command.add(getAppPath());
            command.addAll(args);
            return command;
        }
    }

    private static final NativeLauncherFinder UnknownLauncherFinder = new NativeLauncherFinder() {
        @Override
        public String getAppPath() {
            return null;
        }

        @Override
        public List<String> getCommand(List<String> args) {
            throw new UnsupportedOperationException();
        }
    };

    public static final Process restart(final Class<?> mainClass, final String... args) throws IOException {
        return restart(mainClass, Arrays.asList(args));
    }

    public static final Process restart(final Redirect action, final Class<?> mainClass, final String... args) throws IOException {
        return restart(action, mainClass, Arrays.asList(args));
    }

    /**
     * Restart the VM. If this VM was launched using a native application (e.g. .exe or .app) then
     * this will be executed. Else the <code>mainClass</code> will be used.
     * 
     * @param mainClass the main() to use (if no native application was found).
     * @param args the program arguments to pass.
     * @return the new process, <code>null</code> if the program wasn't started.
     * @throws IOException if the VM couldn't be launched.
     * @see #NO_RESTART
     */
    public static final Process restart(final Class<?> mainClass, final List<String> args) throws IOException {
        return restart(ProcessStreams.DISCARD, mainClass, args);
    }

    public static final Process restart(final Redirect action, final Class<?> mainClass, final List<String> args) throws IOException {
        if (Boolean.getBoolean(NO_RESTART))
            return null;
        final File wd = FileUtils.getWD();
        final List<String> command = getNativeCommand(args);
        if (command != null) {
            return new ProcessBuilder(command).directory(wd).redirectErrorStream(true).redirectOutput(action).start();
        } else {
            try {
                mainClass.getMethod("main", String[].class);
            } catch (NoSuchMethodException e) {
                throw new IllegalArgumentException(mainClass + " doesn't containt a main()", e);
            }
            return new VMLauncher() {
                @Override
                protected File getWD() {
                    return wd;
                }

                @Override
                protected File getPropFile(String mainClass) {
                    return null;
                }

                @Override
                protected Redirect getStreamRedirect() {
                    return action;
                }
            }.launch(mainClass.getName(), args);
        }
    }

    public static final String ENV_VMARGS = "JAVA_VMARGS";
    public static final String PROPS_VMARGS = "VMARGS";
    public static final String ENV_PROGARGS = "JAVA_PROGARGS";

    // handle DOS, Mac and Unix newlines
    private static final Pattern NL = Pattern.compile("\\p{Cntrl}+");

    private File wd;

    public VMLauncher() {
        this.wd = null;
    }

    public final File getLauncherWD() {
        if (this.wd == null) {
            final NativeLauncherFinder nativeAppLauncher = getNativeAppLauncher();
            final String appPath = nativeAppLauncher.getAppPath();
            if (appPath != null)
                this.wd = new File(appPath).getAbsoluteFile().getParentFile();
            // when launched with -jar there's only one item
            else if (nativeAppLauncher.getFirstItem().equals(nativeAppLauncher.getClassPath()) && new File(nativeAppLauncher.getFirstItem()).isFile())
                this.wd = new File(nativeAppLauncher.getFirstItem()).getParentFile();
            // support launch in an IDE
            else
                this.wd = FileUtils.getWD();
        }
        return this.wd;
    }

    private final List<String> split(String res) {
        res = res.trim();
        if (res.length() == 0) {
            return Collections.emptyList();
        } else {
            return Arrays.asList(NL.split(res));
        }
    }

    private final List<String> getProp(final File propFile, final String propName) {
        return this.getProp(this.getProps(propFile), propName);
    }

    private final Properties getProps(final File propFile) {
        if (propFile != null && propFile.canRead()) {
            try {
                return PropertiesUtils.createFromFile(propFile);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return new Properties();
    }

    private final List<String> getProp(final Properties props, final String propName) {
        String res = "";
        if (props != null) {
            res = props.getProperty(propName, res);
        }
        return split(res);
    }

    public final Process launch(final String mainClass) throws IOException {
        return this.launch(mainClass, Collections.<String> emptyList());
    }

    /**
     * Launch a new Java VM. This method will try to launch {@link #getJavaBinary() java} from the
     * same installation, if that fails it will use the system path for binaries. VM arguments can
     * be specified with :
     * <ol>
     * <li>the {@value #ENV_VMARGS} environment variable</li>
     * <li>the {@value #PROPS_VMARGS} property in {@link #getPropFile(String)}</li>
     * <li>the {@link #getVMArguments()} method</li>
     * </ol>
     * Program arguments :
     * <ol>
     * <li>the <code>progParams</code> parameter</li>
     * <li>the {@value #ENV_PROGARGS} environment variable</li>
     * </ol>
     * 
     * @param mainClass the main class.
     * @param progParams the program arguments for <code>mainClass</code>.
     * @return the new Process.
     * @throws IOException if the process couldn't be started.
     */
    public final Process launch(final String mainClass, final List<String> progParams) throws IOException {
        final boolean debug = Boolean.getBoolean("launcher.debug");
        final String javaBinary = getJavaBinary();
        final File sameJava = new File(System.getProperty("java.home"), "bin/" + javaBinary);
        final String java = sameJava.canExecute() ? sameJava.getAbsolutePath() : javaBinary;
        final File propFile = this.getPropFile(mainClass);
        final Properties props = this.getProps(propFile);
        if (debug)
            System.err.println("propFile : " + propFile);

        final List<String> command = new ArrayList<String>();
        command.add(java);

        if (this.enableRemoteDebug(props)) {
            command.add(RemoteDebugArgs.getArgs(props.getProperty("remoteDebugAddr")));
        }
        command.addAll(this.getVMArguments());

        // for java the last specified property wins
        if (propFile != null) {
            final List<String> appProps = this.getProp(props, PROPS_VMARGS);
            command.addAll(appProps);
            final File userFile = new File(System.getProperty("user.home"), ".java/ilm/" + propFile.getName());
            final List<String> userProps = this.getProp(userFile, PROPS_VMARGS);
            command.addAll(userProps);
            if (debug) {
                System.err.println("appProps : " + appProps);
                System.err.println("userProps ( from " + userFile + ") : " + userProps);
            }
        }
        final String envVMArgs = System.getenv(ENV_VMARGS);
        if (envVMArgs != null)
            command.addAll(split(envVMArgs));

        command.add("-cp");
        command.add(getClassPath());
        command.add(mainClass);
        final String envProgArgs = System.getenv(ENV_PROGARGS);
        if (envProgArgs != null)
            command.addAll(split(envProgArgs));
        command.addAll(progParams);

        // inherit environment so that the next launch() can access the same variables
        final ProcessBuilder procBuilder = new ProcessBuilder(command).directory(getWD());
        this.modifyEnv(procBuilder.environment());
        if (debug) {
            System.err.println("Command line : " + procBuilder.command());
            System.err.println("Dir : " + procBuilder.directory());
            System.err.println("Std out and err :");
        }

        procBuilder.redirectErrorStream(true).redirectOutput(debug ? Redirect.INHERIT : this.getStreamRedirect());
        return procBuilder.start();
    }

    protected void modifyEnv(Map<String, String> environment) {
    }

    protected Redirect getStreamRedirect() {
        return ProcessStreams.DISCARD;
    }

    protected boolean enableRemoteDebug(Properties props) {
        final String prop = props.getProperty("remoteDebug");
        return prop == null ? remoteDebugDefault() : Boolean.parseBoolean(prop);
    }

    protected boolean remoteDebugDefault() {
        return false;
    }

    /**
     * The program to launch. This implementation returns <code>javaw</code> for Windows and
     * <code>java</code> for other OS.
     * 
     * @return the name of the binary.
     */
    protected String getJavaBinary() {
        return OSFamily.getInstance() == OSFamily.Windows ? "javaw" : "java";
    }

    protected List<String> getVMArguments() {
        return Arrays.asList("-Dfile.encoding=UTF-8", "-Xms100M", "-Xmx256M");
    }

    // by default in the same jar
    protected String getClassPath() {
        return ManagementFactory.getRuntimeMXBean().getClassPath();
    }

    // by default in the same directory
    protected File getWD() {
        return this.getLauncherWD();
    }

    protected File getPropFile(final String mainClass) {
        final String className = mainClass.substring(mainClass.lastIndexOf('.') + 1);
        return new File(getWD(), className + ".properties");
    }
}