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