source: josm/trunk/src/org/openstreetmap/josm/tools/PlatformHook.java@ 18986

Last change on this file since 18986 was 18986, checked in by taylor.smock, 11 months ago

See #23355: Sanity check JVM arguments on startup

Change the icon for Continue and be a more lenient when comparing expected JVM
arguments against actual JVM arguments.

This additionally updates the Eclipse launch files.

  • Property svn:eol-style set to native
File size: 19.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.GraphicsEnvironment;
7import java.awt.Toolkit;
8import java.awt.event.KeyEvent;
9import java.io.BufferedReader;
10import java.io.File;
11import java.io.IOException;
12import java.io.InputStreamReader;
13import java.lang.management.ManagementFactory;
14import java.nio.charset.StandardCharsets;
15import java.security.KeyStoreException;
16import java.security.NoSuchAlgorithmException;
17import java.security.cert.CertificateException;
18import java.security.cert.X509Certificate;
19import java.text.DateFormat;
20import java.util.ArrayList;
21import java.util.Collection;
22import java.util.Collections;
23import java.util.Date;
24import java.util.List;
25
26import org.openstreetmap.josm.data.projection.datum.NTV2Proj4DirGridShiftFileSource;
27import org.openstreetmap.josm.io.CertificateAmendment.NativeCertAmend;
28import org.openstreetmap.josm.spi.preferences.Config;
29import org.openstreetmap.josm.tools.date.DateUtils;
30
31/**
32 * This interface allows platform (operating system) dependent code
33 * to be bundled into self-contained classes.
34 * @since 1023
35 */
36public interface PlatformHook {
37
38 /**
39 * Visitor to construct a PlatformHook from a given {@link Platform} object.
40 */
41 PlatformVisitor<PlatformHook> CONSTRUCT_FROM_PLATFORM = new PlatformVisitor<PlatformHook>() {
42 @Override
43 public PlatformHook visitUnixoid() {
44 return new PlatformHookUnixoid();
45 }
46
47 @Override
48 public PlatformHook visitWindows() {
49 return new PlatformHookWindows();
50 }
51
52 @Override
53 public PlatformHook visitOsx() {
54 return new PlatformHookOsx();
55 }
56 };
57
58 /**
59 * Get the platform corresponding to this platform hook.
60 * @return the platform corresponding to this platform hook
61 */
62 Platform getPlatform();
63
64 /**
65 * The preStartupHook will be called extremely early. It is
66 * guaranteed to be called before the GUI setup has started.
67 *
68 * Reason: On OSX we need to inform the Swing libraries
69 * that we want to be integrated with the OS before we setup our GUI.
70 */
71 default void preStartupHook() {
72 // Do nothing
73 }
74
75 /**
76 * The afterPrefStartupHook will be called early, but after
77 * the preferences have been loaded and basic processing of
78 * command line arguments is finished.
79 * It is guaranteed to be called before the GUI setup has started.
80 */
81 default void afterPrefStartupHook() {
82 // Do nothing
83 }
84
85 /**
86 * The startupHook will be called early, but after the GUI
87 * setup has started.
88 *
89 * Reason: On OSX we need to register some callbacks with the
90 * OS, so we'll receive events from the system menu.
91 * @param javaCallback Java expiration callback, providing GUI feedback
92 * @param webStartCallback WebStart migration callback, providing GUI feedback
93 * @since 18985
94 */
95 default void startupHook(JavaExpirationCallback javaCallback, WebStartMigrationCallback webStartCallback,
96 SanityCheckCallback sanityCheckCallback) {
97 startupSanityChecks(sanityCheckCallback);
98 }
99
100 /**
101 * The openURL hook will be used to open an URL in the
102 * default web browser.
103 * @param url The URL to open
104 * @throws IOException if any I/O error occurs
105 */
106 void openUrl(String url) throws IOException;
107
108 /**
109 * The initSystemShortcuts hook will be called by the
110 * Shortcut class after the modifier groups have been read
111 * from the config, but before any shortcuts are read from
112 * it or registered from within the application.
113 *
114 * Please note that you are not allowed to register any
115 * shortcuts from this hook, but only "systemCuts"!
116 *
117 * BTW: SystemCuts should be named "system:&lt;whatever&gt;",
118 * and it'd be best if you'd recycle the names already used
119 * by the Windows and OSX hooks. Especially the later has
120 * really many of them.
121 *
122 * You should also register any and all shortcuts that the
123 * operation system handles itself to block JOSM from trying
124 * to use them---as that would just not work. Call setAutomatic
125 * on them to prevent the keyboard preferences from allowing the
126 * user to change them.
127 */
128 void initSystemShortcuts();
129
130 /**
131 * Returns the default LAF to be used on this platform to look almost as a native application.
132 * @return The default native LAF for this platform
133 */
134 String getDefaultStyle();
135
136 /**
137 * Determines if the platform allows full-screen.
138 * @return {@code true} if full screen is allowed, {@code false} otherwise
139 */
140 default boolean canFullscreen() {
141 return !GraphicsEnvironment.isHeadless() &&
142 GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().isFullScreenSupported();
143 }
144
145 /**
146 * Renames a file.
147 * @param from Source file
148 * @param to Target file
149 * @return {@code true} if the file has been renamed, {@code false} otherwise
150 */
151 default boolean rename(File from, File to) {
152 return from.renameTo(to);
153 }
154
155 /**
156 * Returns a detailed OS description (at least family + version).
157 * @return A detailed OS description.
158 * @since 5850
159 */
160 String getOSDescription();
161
162 /**
163 * Returns OS build number.
164 * @return OS build number.
165 * @since 12217
166 */
167 default String getOSBuildNumber() {
168 return "";
169 }
170
171 /**
172 * Returns the {@code X509Certificate} matching the given certificate amendment information.
173 * @param certAmend certificate amendment
174 * @return the {@code X509Certificate} matching the given certificate amendment information, or {@code null}
175 * @throws KeyStoreException in case of error
176 * @throws IOException in case of error
177 * @throws CertificateException in case of error
178 * @throws NoSuchAlgorithmException in case of error
179 * @since 13450
180 */
181 default X509Certificate getX509Certificate(NativeCertAmend certAmend)
182 throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
183 return null;
184 }
185
186 /**
187 * Executes a native command and returns the first line of standard output.
188 * @param command array containing the command to call and its arguments.
189 * @return first stripped line of standard output
190 * @throws IOException if an I/O error occurs
191 * @since 12217
192 */
193 default String exec(String... command) throws IOException {
194 Process p = Runtime.getRuntime().exec(command);
195 try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
196 return Utils.strip(input.readLine());
197 }
198 }
199
200 /**
201 * Returns the platform-dependent default cache directory.
202 * @return the platform-dependent default cache directory
203 * @since 7829
204 */
205 File getDefaultCacheDirectory();
206
207 /**
208 * Returns the platform-dependent default preferences directory.
209 * @return the platform-dependent default preferences directory
210 * @since 7831
211 */
212 File getDefaultPrefDirectory();
213
214 /**
215 * Returns the platform-dependent default user data directory.
216 * @return the platform-dependent default user data directory
217 * @since 7834
218 */
219 File getDefaultUserDataDirectory();
220
221 /**
222 * Returns the list of platform-dependent default datum shifting directories for the PROJ.4 library.
223 * @return the list of platform-dependent default datum shifting directories for the PROJ.4 library
224 * @since 11642
225 */
226 default List<File> getDefaultProj4NadshiftDirectories() {
227 return getPlatform().accept(NTV2Proj4DirGridShiftFileSource.getInstance());
228 }
229
230 /**
231 * Determines if the JVM is OpenJDK-based.
232 * @return {@code true} if {@code java.home} contains "openjdk", {@code false} otherwise
233 * @since 12219
234 */
235 default boolean isOpenJDK() {
236 String javaHome = Utils.getSystemProperty("java.home");
237 return javaHome != null && javaHome.contains("openjdk");
238 }
239
240 /**
241 * Determines if HTML rendering is supported in menu tooltips.
242 * @return {@code true} if HTML rendering is supported in menu tooltips
243 * @since 18116
244 */
245 default boolean isHtmlSupportedInMenuTooltips() {
246 return true;
247 }
248
249 /**
250 * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts.
251 * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but:
252 * <ul>
253 * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended
254 * modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li>
255 * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li>
256 * </ul>
257 * @return extended modifier key used as the appropriate accelerator key for menu shortcuts
258 * @since 12748 (as a replacement to {@code GuiHelper.getMenuShortcutKeyMaskEx()})
259 */
260 default int getMenuShortcutKeyMaskEx() {
261 // To remove when switching to Java 10+, and use Toolkit.getMenuShortcutKeyMaskEx instead
262 return KeyEvent.CTRL_DOWN_MASK;
263 }
264
265 /**
266 * Called when an outdated version of Java is detected at startup.
267 * @since 12270
268 */
269 @FunctionalInterface
270 interface JavaExpirationCallback {
271 /**
272 * Asks user to update its version of Java.
273 * @param updVersion target update version
274 * @param url download URL
275 * @param major true for a migration towards a major version of Java (8:9), false otherwise
276 * @param eolDate the EOL/expiration date
277 */
278 void askUpdateJava(String updVersion, String url, String eolDate, boolean major);
279 }
280
281 /**
282 * Called when Oracle Java WebStart is detected at startup.
283 * @since 17679
284 */
285 @FunctionalInterface
286 interface WebStartMigrationCallback {
287 /**
288 * Asks user to migrate to OpenWebStart.
289 * @param url download URL
290 */
291 void askMigrateWebStart(String url);
292 }
293
294 /**
295 * Inform the user that a sanity check or checks failed
296 */
297 @FunctionalInterface
298 interface SanityCheckCallback {
299 /**
300 * Tells the user that a sanity check failed
301 * @param title The title of the message to show
302 * @param canContinue {@code true} if the failed sanity check(s) will not instantly kill JOSM when the user edits
303 * @param message The message parts to show the user (as a list)
304 */
305 void sanityCheckFailed(String title, boolean canContinue, String... message);
306 }
307
308 /**
309 * Checks if the running version of Java has expired, proposes to user to update it if needed.
310 * @param callback Java expiration callback
311 * @since 12270 (signature)
312 * @since 12219
313 */
314 default void checkExpiredJava(JavaExpirationCallback callback) {
315 Date expiration = Utils.getJavaExpirationDate();
316 if (expiration != null && expiration.before(new Date())) {
317 String latestVersion = Utils.getJavaLatestVersion();
318 String currentVersion = Utils.getSystemProperty("java.version");
319 // #17831 WebStart may be launched with an expired JRE but then launching JOSM with up-to-date JRE
320 if (latestVersion == null || !latestVersion.equalsIgnoreCase(currentVersion)) {
321 callback.askUpdateJava(latestVersion != null ? latestVersion : "latest",
322 Config.getPref().get("java.update.url", getJavaUrl()),
323 DateUtils.getDateFormat(DateFormat.MEDIUM).format(expiration), false);
324 }
325 }
326 }
327
328 /**
329 * Checks if we will soon not be supporting the running version of Java
330 * @param callback Java expiration callback
331 * @since 18580
332 */
333 default void warnSoonToBeUnsupportedJava(JavaExpirationCallback callback) {
334 // Java 17 is our next minimum version, and OpenWebStart should be replacing Oracle WebStart
335 if (Utils.getJavaVersion() < 17 && !Utils.isRunningWebStart()) {
336 String latestVersion = Utils.getJavaLatestVersion();
337 String currentVersion = Utils.getSystemProperty("java.version");
338 // #17831 WebStart may be launched with an expired JRE but then launching JOSM with up-to-date JRE
339 if (latestVersion == null || !latestVersion.equalsIgnoreCase(currentVersion)) {
340 callback.askUpdateJava(latestVersion != null ? latestVersion : "latest",
341 Config.getPref().get("java.update.url", getJavaUrl()),
342 null, Utils.getJavaVersion() < 17);
343 }
344 }
345 }
346
347 /**
348 * Get the Java download URL (really shouldn't be used outside of JOSM startup checks)
349 * @return The download URL to use.
350 * @since 18580
351 */
352 default String getJavaUrl() {
353 StringBuilder defaultDownloadUrl = new StringBuilder("https://www.azul.com/downloads/?version=java-21-lts");
354 if (PlatformManager.isPlatformWindows()) {
355 defaultDownloadUrl.append("&os=windows");
356 } else if (PlatformManager.isPlatformOsx()) {
357 defaultDownloadUrl.append("&os=macos");
358 } // else probably `linux`, but they should be using a package manager.
359 // For available architectures, see
360 // https://github.com/openjdk/jdk/blob/master/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/utilities/PlatformInfo.java#L53
361 String osArch = System.getProperty("os.arch");
362 if (osArch != null) {
363 // See https://learn.microsoft.com/en-us/windows/win32/winprog64/wow64-implementation-details#environment-variables
364 // for PROCESSOR_ARCHITEW6432
365 if ("x86_64".equals(osArch) || "amd64".equals(osArch)
366 || "AMD64".equalsIgnoreCase(System.getenv("PROCESSOR_ARCHITEW6432"))) {
367 defaultDownloadUrl.append("&architecture=x86-64-bit").append("&package=jdk-fx"); // jdk-fx has an installer
368 } else if ("aarch64".equals(osArch)) {
369 defaultDownloadUrl.append("&architecture=arm-64-bit").append("&package=jdk-fx"); // jdk-fx has an installer
370 } else if ("x86".equals(osArch)) {
371 // Honestly, just about everyone should be on x86_64 at this point. But just in case someone
372 // is running JOSM on a 10-year-old computer. They'd probably be better off running a RPi.
373 defaultDownloadUrl.append("&architecture=x86-32-bit").append("&package=jdk"); // jdk has an installer
374 } // else user will have to figure it out themselves.
375 }
376 defaultDownloadUrl.append("#zulu"); // Scrolls to download section
377 return defaultDownloadUrl.toString();
378 }
379
380 /**
381 * Checks if we run Oracle Web Start, proposes to user to migrate to OpenWebStart.
382 * @param callback WebStart migration callback
383 * @since 17679
384 */
385 default void checkWebStartMigration(WebStartMigrationCallback callback) {
386 if (Utils.isRunningJavaWebStart()) {
387 callback.askMigrateWebStart(Config.getPref().get("openwebstart.download.url", "https://openwebstart.com/download/"));
388 }
389 }
390
391 default void startupSanityChecks(SanityCheckCallback sanityCheckCallback) {
392 final String arch = System.getProperty("os.arch");
393 final List<String> messages = new ArrayList<>();
394 final String jvmArch = System.getProperty("sun.arch.data.model");
395 boolean canContinue = true;
396 if (Utils.getJavaVersion() < 11) {
397 canContinue = false;
398 messages.add(tr("You must update Java to Java {0} or later in order to run this version of JOSM", 17));
399 // Reset webstart/java update prompts
400 Config.getPref().put("askUpdateWebStart", null);
401 Config.getPref().put("askUpdateJava" + Utils.getJavaLatestVersion(), null);
402 Config.getPref().put("askUpdateJavalatest", null);
403 }
404 if (!"x86".equals(arch) && "32".equals(jvmArch)) {
405 messages.add(tr("Please use a 64 bit version of Java -- this will avoid out of memory errors"));
406 }
407 // Note: these might be able to be removed with the appropriate module-info.java settings.
408 final String[] expectedJvmArguments = {
409 "--add-exports=java.base/sun.security.action=ALL-UNNAMED",
410 "--add-exports=java.desktop/com.sun.imageio.plugins.jpeg=ALL-UNNAMED",
411 "--add-exports=java.desktop/com.sun.imageio.spi=ALL-UNNAMED"
412 };
413 final List<String> vmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
414 final StringBuilder missingArguments = new StringBuilder();
415 for (String arg : expectedJvmArguments) {
416 if (vmArguments.stream().noneMatch(s -> s.contains(arg))) {
417 if (missingArguments.length() > 0) {
418 missingArguments.append("<br>");
419 }
420 missingArguments.append(arg);
421 }
422 }
423 if (missingArguments.length() > 0) {
424 final String args = missingArguments.toString();
425 messages.add(tr("Missing JVM Arguments:<br>{0}", args));
426 }
427 if (!messages.isEmpty()) {
428 if (canContinue) {
429 sanityCheckCallback.sanityCheckFailed(tr("JOSM may work improperly"), true,
430 messages.toArray(new String[0]));
431 } else {
432 sanityCheckCallback.sanityCheckFailed(tr("JOSM will be unable to work properly and will exit"), false,
433 messages.toArray(new String[0]));
434 }
435 }
436 }
437
438 /**
439 * Called when interfacing with native OS functions. Currently only used with macOS.
440 * The callback must perform all GUI-related tasks associated to an OS request.
441 * The non-GUI, platform-specific tasks, are usually performed by the {@code PlatformHook}.
442 * @since 12695
443 */
444 interface NativeOsCallback {
445 /**
446 * macOS: Called when JOSM is asked to open a list of files.
447 * @param files list of files to open
448 */
449 void openFiles(List<File> files);
450
451 /**
452 * macOS: Invoked when JOSM is asked to quit.
453 * @return {@code true} if JOSM has been closed, {@code false} if the user has cancelled the operation.
454 */
455 boolean handleQuitRequest();
456
457 /**
458 * macOS: Called when JOSM is asked to show it's about dialog.
459 */
460 void handleAbout();
461
462 /**
463 * macOS: Called when JOSM is asked to show it's preferences UI.
464 */
465 void handlePreferences();
466 }
467
468 /**
469 * Registers the native OS callback. Currently only needed for macOS.
470 * @param callback the native OS callback
471 * @since 12695
472 */
473 default void setNativeOsCallback(NativeOsCallback callback) {
474 // To be implemented if needed
475 }
476
477 /**
478 * Resolves a file link to its destination file.
479 * @param file file (link or regular file)
480 * @return destination file in case of a file link, file if regular
481 * @since 13691
482 */
483 default File resolveFileLink(File file) {
484 // Override if needed
485 return file;
486 }
487
488 /**
489 * Returns a set of possible platform specific directories where resources could be stored.
490 * @return A set of possible platform specific directories where resources could be stored.
491 * @since 14144
492 */
493 default Collection<String> getPossiblePreferenceDirs() {
494 return Collections.emptyList();
495 }
496}
Note: See TracBrowser for help on using the repository browser.