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