001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.event.ActionEvent;
008import java.awt.event.KeyEvent;
009import java.io.File;
010import java.io.IOException;
011import java.lang.management.ManagementFactory;
012import java.util.ArrayList;
013import java.util.Arrays;
014import java.util.Collection;
015import java.util.List;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
019import org.openstreetmap.josm.gui.io.SaveLayersDialog;
020import org.openstreetmap.josm.tools.ImageProvider;
021import org.openstreetmap.josm.tools.Shortcut;
022
023/**
024 * Restarts JOSM as it was launched. Comes from "restart" plugin, originally written by Upliner.
025 * <br><br>
026 * Mechanisms have been improved based on #8561 discussions and
027 * <a href="http://lewisleo.blogspot.jp/2012/08/programmatically-restart-java.html">this article</a>.
028 * @since 5857
029 */
030public class RestartAction extends JosmAction {
031
032    // AppleScript to restart OS X package
033    private static final String RESTART_APPLE_SCRIPT =
034              "tell application \"System Events\"\n"
035            + "repeat until not (exists process \"JOSM\")\n"
036            + "delay 0.2\n"
037            + "end repeat\n"
038            + "end tell\n"
039            + "tell application \"JOSM\" to activate";
040
041    /**
042     * Constructs a new {@code RestartAction}.
043     */
044    public RestartAction() {
045        super(tr("Restart"), "restart", tr("Restart the application."),
046                Shortcut.registerShortcut("file:restart", tr("File: {0}", tr("Restart")), KeyEvent.VK_J, Shortcut.ALT_CTRL_SHIFT), false);
047        putValue("help", ht("/Action/Restart"));
048        putValue("toolbar", "action/restart");
049        if (Main.toolbar != null) {
050            Main.toolbar.register(this);
051        }
052        setEnabled(isRestartSupported());
053    }
054
055    @Override
056    public void actionPerformed(ActionEvent e) {
057        // If JOSM has been started with property 'josm.restart=true' this means
058        // it is executed by a start script that can handle restart.
059        // Request for restart is indicated by exit code 9.
060        String scriptRestart = System.getProperty("josm.restart");
061        if ("true".equals(scriptRestart)) {
062            Main.exitJosm(true, 9, SaveLayersDialog.Reason.RESTART);
063        }
064
065        try {
066            restartJOSM();
067        } catch (IOException ex) {
068            Main.error(ex);
069        }
070    }
071
072    /**
073     * Determines if restarting the application should be possible on this platform.
074     * @return {@code true} if the mandatory system property {@code sun.java.command} is defined, {@code false} otherwise.
075     * @since 5951
076     */
077    public static boolean isRestartSupported() {
078        return System.getProperty("sun.java.command") != null;
079    }
080
081    /**
082     * Restarts the current Java application.
083     * @throws IOException in case of any I/O error
084     */
085    public static void restartJOSM() throws IOException {
086        if (isRestartSupported() && !Main.exitJosm(false, 0, SaveLayersDialog.Reason.RESTART)) return;
087        final List<String> cmd;
088        // special handling for OSX .app package
089        if (Main.isPlatformOsx() && System.getProperty("java.library.path").contains("/JOSM.app/Contents/MacOS")) {
090            cmd = getAppleCommands();
091        } else {
092            cmd = getCommands();
093        }
094        Main.info("Restart "+cmd);
095        if (Main.isDebugEnabled() && Main.pref.getBoolean("restart.debug.simulation")) {
096            Main.debug("Restart cancelled to get debug info");
097            return;
098        }
099        // execute the command in a shutdown hook, to be sure that all the
100        // resources have been disposed before restarting the application
101        Runtime.getRuntime().addShutdownHook(new Thread("josm-restarter") {
102            @Override
103            public void run() {
104                try {
105                    Runtime.getRuntime().exec(cmd.toArray(new String[cmd.size()]));
106                } catch (IOException e) {
107                    Main.error(e);
108                }
109            }
110        });
111        // exit
112        System.exit(0);
113    }
114
115    private static List<String> getAppleCommands() {
116        final List<String> cmd = new ArrayList<>();
117        cmd.add("/usr/bin/osascript");
118        for (String line : RESTART_APPLE_SCRIPT.split("\n")) {
119            cmd.add("-e");
120            cmd.add(line);
121        }
122        return cmd;
123    }
124
125    private static List<String> getCommands() throws IOException {
126        final List<String> cmd = new ArrayList<>();
127        // java binary
128        cmd.add(getJavaRuntime());
129        // vm arguments
130        addVMArguments(cmd);
131        // Determine webstart JNLP file. Use jnlpx.origFilenameArg instead of jnlp.application.href,
132        // because only this one is present when run from j2plauncher.exe (see #10795)
133        final String jnlp = System.getProperty("jnlpx.origFilenameArg");
134        // program main and program arguments (be careful a sun property. might not be supported by all JVM)
135        final String javaCommand = System.getProperty("sun.java.command");
136        String[] mainCommand = javaCommand.split(" ");
137        if (javaCommand.endsWith(".jnlp") && jnlp == null) {
138            // see #11751 - jnlp on Linux
139            if (Main.isDebugEnabled()) {
140                Main.debug("Detected jnlp without jnlpx.origFilenameArg property set");
141            }
142            cmd.addAll(Arrays.asList(mainCommand));
143        } else {
144            // look for a .jar in all chunks to support paths with spaces (fix #9077)
145            StringBuilder sb = new StringBuilder(mainCommand[0]);
146            for (int i = 1; i < mainCommand.length && !mainCommand[i-1].endsWith(".jar"); i++) {
147                sb.append(' ').append(mainCommand[i]);
148            }
149            String jarPath = sb.toString();
150            // program main is a jar
151            if (jarPath.endsWith(".jar")) {
152                // if it's a jar, add -jar mainJar
153                cmd.add("-jar");
154                cmd.add(new File(jarPath).getPath());
155            } else {
156                // else it's a .class, add the classpath and mainClass
157                cmd.add("-cp");
158                cmd.add('"' + System.getProperty("java.class.path") + '"');
159                cmd.add(mainCommand[0]);
160            }
161            // add JNLP file.
162            if (jnlp != null) {
163                cmd.add(jnlp);
164            }
165        }
166        // finally add program arguments
167        cmd.addAll(Main.getCommandLineArgs());
168        return cmd;
169    }
170
171    private static String getJavaRuntime() throws IOException {
172        final String java = System.getProperty("java.home") + File.separator + "bin" + File.separator +
173                (Main.isPlatformWindows() ? "java.exe" : "java");
174        if (!new File(java).isFile()) {
175            throw new IOException("Unable to find suitable java runtime at "+java);
176        }
177        return java;
178    }
179
180    private static void addVMArguments(Collection<String> cmd) {
181        List<String> arguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
182        if (Main.isDebugEnabled()) {
183            Main.debug("VM arguments: "+arguments);
184        }
185        for (String arg : arguments) {
186            // When run from jp2launcher.exe, jnlpx.remove is true, while it is not when run from javaws
187            // Always set it to false to avoid error caused by a missing jnlp file on the second restart
188            arg = arg.replace("-Djnlpx.remove=true", "-Djnlpx.remove=false");
189            // if it's the agent argument : we ignore it otherwise the
190            // address of the old application and the new one will be in conflict
191            if (!arg.contains("-agentlib")) {
192                cmd.add(arg);
193            }
194        }
195    }
196
197    /**
198     * Returns a new {@code ButtonSpec} instance that performs this action.
199     * @return A new {@code ButtonSpec} instance that performs this action.
200     */
201    public static ButtonSpec getRestartButtonSpec() {
202        return new ButtonSpec(
203                tr("Restart"),
204                ImageProvider.get("restart"),
205                tr("Restart the application."),
206                ht("/Action/Restart"),
207                isRestartSupported()
208        );
209    }
210
211    /**
212     * Returns a new {@code ButtonSpec} instance that do not perform this action.
213     * @return A new {@code ButtonSpec} instance that do not perform this action.
214     */
215    public static ButtonSpec getCancelButtonSpec() {
216        return new ButtonSpec(
217                tr("Cancel"),
218                ImageProvider.get("cancel"),
219                tr("Click to restart later."),
220                null /* no specific help context */
221        );
222    }
223
224    /**
225     * Returns default {@code ButtonSpec} instances for this action (Restart/Cancel).
226     * @return Default {@code ButtonSpec} instances for this action.
227     * @see #getRestartButtonSpec
228     * @see #getCancelButtonSpec
229     */
230    public static ButtonSpec[] getButtonSpecs() {
231        return new ButtonSpec[] {
232                getRestartButtonSpec(),
233                getCancelButtonSpec()
234        };
235    }
236}