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}