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