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}