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