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

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

Fix #23355: Sanity check JVM arguments on startup

See #17858: JOSM will no longer continue running if the user is on an unsupported
Java version (for this commit, older than Java 11; message indicates Java 17).
This does update the link for Azul from Java 17 to Java 21 as well.

In order to (hopefully) reduce confusion, the webstart and Java update nags will
also be reset in the event that JOSM will exit due to old Java versions. This is
mostly so that users will get the messages to update to OpenWebstart or the
appropriate Java link for their platform and architecture.

Additionally, this will (hopefully) reduce the number of tickets we have to close
due to missing JVM arguments by informing users of the missing arguments at startup.

  • 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.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.