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