source: josm/trunk/src/org/openstreetmap/josm/tools/PlatformHookUnixoid.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.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.Utils.getSystemEnv;
6import static org.openstreetmap.josm.tools.Utils.getSystemProperty;
7
8import java.awt.Desktop;
9import java.awt.event.KeyEvent;
10import java.io.BufferedReader;
11import java.io.File;
12import java.io.IOException;
13import java.io.InputStream;
14import java.net.URISyntaxException;
15import java.nio.charset.StandardCharsets;
16import java.nio.file.Files;
17import java.nio.file.Path;
18import java.nio.file.Paths;
19import java.security.KeyStoreException;
20import java.security.NoSuchAlgorithmException;
21import java.security.cert.CertificateException;
22import java.security.cert.CertificateFactory;
23import java.security.cert.X509Certificate;
24import java.util.Arrays;
25import java.util.Collection;
26import java.util.HashSet;
27import java.util.Locale;
28import java.util.Optional;
29import java.util.Set;
30import java.util.concurrent.ExecutionException;
31
32import org.openstreetmap.josm.data.Preferences;
33import org.openstreetmap.josm.io.CertificateAmendment.NativeCertAmend;
34import org.openstreetmap.josm.spi.preferences.Config;
35
36/**
37 * {@code PlatformHook} implementation for Unix systems.
38 * @since 1023
39 */
40public class PlatformHookUnixoid implements PlatformHook {
41
42 private String osDescription;
43
44 @Override
45 public Platform getPlatform() {
46 return Platform.UNIXOID;
47 }
48
49 @Override
50 public void preStartupHook() {
51 // See #12022, #16666 - Disable GNOME ATK Java wrapper as it causes a lot of serious trouble
52 if (isDebianOrUbuntu()) {
53 if (Utils.getJavaVersion() >= 9) {
54 // TODO: find a way to disable ATK wrapper on Java >= 9
55 // We should probably be able to do that by embedding a no-op AccessibilityProvider in our jar
56 // so that it is loaded by ServiceLoader without error
57 // But this require to compile at least one class with Java 9
58 } else {
59 // Java 8 does a simple Class.newInstance() from system classloader
60 Utils.updateSystemProperty("javax.accessibility.assistive_technologies", "java.lang.Object");
61 }
62 }
63 }
64
65 @Override
66 public void startupHook(JavaExpirationCallback javaCallback, WebStartMigrationCallback webStartCallback,
67 SanityCheckCallback sanityCheckCallback) {
68 checkWebStartMigration(webStartCallback);
69 PlatformHook.super.startupHook(javaCallback, webStartCallback, sanityCheckCallback);
70 }
71
72 @Override
73 public void openUrl(String url) throws IOException {
74 for (String program : Config.getPref().getList("browser.unix",
75 Arrays.asList("xdg-open", "#DESKTOP#", "$BROWSER", "gnome-open", "kfmclient openURL", "firefox"))) {
76 try {
77 if ("#DESKTOP#".equals(program)) {
78 Desktop.getDesktop().browse(Utils.urlToURI(url));
79 } else if (program.startsWith("$")) {
80 program = System.getenv().get(program.substring(1));
81 Runtime.getRuntime().exec(new String[]{program, url});
82 } else {
83 Runtime.getRuntime().exec(new String[]{program, url});
84 }
85 return;
86 } catch (IOException | URISyntaxException e) {
87 Logging.warn(e);
88 }
89 }
90 }
91
92 @Override
93 public void initSystemShortcuts() {
94 // CHECKSTYLE.OFF: LineLength
95 // TODO: Insert system shortcuts here. See Windows and especially OSX to see how to.
96 for (int i = KeyEvent.VK_F1; i <= KeyEvent.VK_F12; ++i) {
97 Shortcut.registerSystemShortcut("screen:toogle"+i, tr("reserved"), i, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)
98 .setAutomatic();
99 }
100 Shortcut.registerSystemShortcut("system:reset", tr("reserved"), KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)
101 .setAutomatic();
102 Shortcut.registerSystemShortcut("system:resetX", tr("reserved"), KeyEvent.VK_BACK_SPACE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)
103 .setAutomatic();
104 // CHECKSTYLE.ON: LineLength
105 }
106
107 @Override
108 public String getDefaultStyle() {
109 return "javax.swing.plaf.metal.MetalLookAndFeel";
110 }
111
112 /**
113 * Returns desktop environment based on the environment variable {@code XDG_CURRENT_DESKTOP}.
114 * @return desktop environment.
115 */
116 public Optional<String> getDesktopEnvironment() {
117 return Optional.ofNullable(getSystemEnv("XDG_CURRENT_DESKTOP")).filter(s -> !s.isEmpty());
118 }
119
120 /**
121 * Determines if the distribution is Debian or Ubuntu, or a derivative.
122 * @return {@code true} if the distribution is Debian, Ubuntu or Mint, {@code false} otherwise
123 */
124 public static boolean isDebianOrUbuntu() {
125 try {
126 String dist = Utils.execOutput(Arrays.asList("lsb_release", "-i", "-s"));
127 return "Debian".equalsIgnoreCase(dist) || "Ubuntu".equalsIgnoreCase(dist) || "Mint".equalsIgnoreCase(dist);
128 } catch (IOException | ExecutionException | InterruptedException e) {
129 // lsb_release is not available on all Linux systems, so don't log at warning level
130 Logging.debug(e);
131 return false;
132 }
133 }
134
135 /**
136 * Get the package name including detailed version.
137 * @param packageNames The possible package names (when a package can have different names on different distributions)
138 * @return The package name and package version if it can be identified, null otherwise
139 * @since 7314
140 */
141 public static String getPackageDetails(String... packageNames) {
142 try {
143 // CHECKSTYLE.OFF: SingleSpaceSeparator
144 boolean dpkg = Paths.get("/usr/bin/dpkg-query").toFile().exists();
145 boolean eque = Paths.get("/usr/bin/equery").toFile().exists();
146 boolean rpm = Paths.get("/bin/rpm").toFile().exists();
147 // CHECKSTYLE.ON: SingleSpaceSeparator
148 if (dpkg || rpm || eque) {
149 for (String packageName : packageNames) {
150 String[] args;
151 if (dpkg) {
152 args = new String[] {"dpkg-query", "--show", "--showformat", "${Architecture}-${Version}", packageName};
153 } else if (eque) {
154 args = new String[] {"equery", "-q", "list", "-e", "--format=$fullversion", packageName};
155 } else {
156 args = new String[] {"rpm", "-q", "--qf", "%{arch}-%{version}", packageName};
157 }
158 try {
159 String version = Utils.execOutput(Arrays.asList(args));
160 if (!Utils.isEmpty(version)) {
161 return packageName + ':' + version;
162 }
163 } catch (ExecutionException e) {
164 // Package does not exist, continue
165 Logging.trace(e);
166 }
167 }
168 }
169 } catch (IOException | InterruptedException e) {
170 Logging.warn(e);
171 }
172 return null;
173 }
174
175 /**
176 * Get the Java package name including detailed version.
177 *
178 * Some Java bugs are specific to a certain security update, so in addition
179 * to the Java version, we also need the exact package version.
180 *
181 * @return The package name and package version if it can be identified, null otherwise
182 */
183 public String getJavaPackageDetails() {
184 String home = getSystemProperty("java.home");
185 if (home.contains("java-8-openjdk") || home.contains("java-1.8.0-openjdk")) {
186 return getPackageDetails("openjdk-8-jre", "java-1_8_0-openjdk", "java-1.8.0-openjdk");
187 } else if (home.contains("java-9-openjdk") || home.contains("java-1.9.0-openjdk")) {
188 return getPackageDetails("openjdk-9-jre", "java-1_9_0-openjdk", "java-1.9.0-openjdk", "java-9-openjdk");
189 } else if (home.contains("java-10-openjdk")) {
190 return getPackageDetails("openjdk-10-jre", "java-10-openjdk");
191 } else if (home.contains("java-11-openjdk")) {
192 return getPackageDetails("openjdk-11-jre", "java-11-openjdk");
193 } else if (home.contains("java-17-openjdk")) {
194 return getPackageDetails("openjdk-17-jre", "java-17-openjdk");
195 } else if (home.contains("java-openjdk")) {
196 return getPackageDetails("java-openjdk");
197 } else if (home.contains("icedtea")) {
198 return getPackageDetails("icedtea-bin");
199 } else if (home.contains("oracle")) {
200 return getPackageDetails("oracle-jdk-bin", "oracle-jre-bin");
201 }
202 return null;
203 }
204
205 /**
206 * Get the Web Start package name including detailed version.
207 *
208 * OpenJDK packages are shipped with icedtea-web package,
209 * but its version generally does not match main java package version.
210 *
211 * Simply return {@code null} if there's no separate package for Java WebStart.
212 *
213 * @return The package name and package version if it can be identified, null otherwise
214 */
215 public String getWebStartPackageDetails() {
216 if (isOpenJDK()) {
217 return getPackageDetails("icedtea-netx", "icedtea-web");
218 }
219 return null;
220 }
221
222 /**
223 * Get the Gnome ATK wrapper package name including detailed version.
224 *
225 * Debian and Ubuntu derivatives come with a pre-enabled accessibility software
226 * completely buggy that makes Swing crash in a lot of different ways.
227 *
228 * Simply return {@code null} if it's not found.
229 *
230 * @return The package name and package version if it can be identified, null otherwise
231 */
232 public String getAtkWrapperPackageDetails() {
233 if (isOpenJDK() && isDebianOrUbuntu()) {
234 return getPackageDetails("libatk-wrapper-java");
235 }
236 return null;
237 }
238
239 private String buildOSDescription() {
240 String osName = getSystemProperty("os.name");
241 if ("Linux".equalsIgnoreCase(osName)) {
242 try {
243 // Try lsb_release (only available on LSB-compliant Linux systems,
244 // see https://www.linuxbase.org/lsb-cert/productdir.php?by_prod )
245 String line = exec("lsb_release", "-ds");
246 if (!Utils.isEmpty(line)) {
247 line = line.replaceAll("\"+", "");
248 line = line.replace("NAME=", ""); // strange code for some Gentoo's
249 if (line.startsWith("Linux ")) // e.g. Linux Mint
250 return line;
251 else if (!line.isEmpty())
252 return "Linux " + line;
253 }
254 } catch (IOException e) {
255 Logging.debug(e);
256 // Non LSB-compliant Linux system. List of common fallback release files: http://linuxmafia.com/faq/Admin/release-files.html
257 for (LinuxReleaseInfo info : new LinuxReleaseInfo[]{
258 new LinuxReleaseInfo("/etc/lsb-release", "DISTRIB_DESCRIPTION", "DISTRIB_ID", "DISTRIB_RELEASE"),
259 new LinuxReleaseInfo("/etc/os-release", "PRETTY_NAME", "NAME", "VERSION"),
260 new LinuxReleaseInfo("/etc/arch-release"),
261 new LinuxReleaseInfo("/etc/debian_version", "Debian GNU/Linux "),
262 new LinuxReleaseInfo("/etc/fedora-release"),
263 new LinuxReleaseInfo("/etc/gentoo-release"),
264 new LinuxReleaseInfo("/etc/redhat-release"),
265 new LinuxReleaseInfo("/etc/SuSE-release")
266 }) {
267 String description = info.extractDescription();
268 if (!Utils.isEmpty(description)) {
269 return "Linux " + description;
270 }
271 }
272 }
273 }
274 return osName;
275 }
276
277 @Override
278 public String getOSDescription() {
279 if (osDescription == null) {
280 osDescription = buildOSDescription();
281 }
282 return osDescription;
283 }
284
285 private static class LinuxReleaseInfo {
286 private final String path;
287 private final String descriptionField;
288 private final String idField;
289 private final String releaseField;
290 private final boolean plainText;
291 private final String prefix;
292
293 LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField) {
294 this(path, descriptionField, idField, releaseField, false, null);
295 }
296
297 LinuxReleaseInfo(String path) {
298 this(path, null, null, null, true, null);
299 }
300
301 LinuxReleaseInfo(String path, String prefix) {
302 this(path, null, null, null, true, prefix);
303 }
304
305 private LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField, boolean plainText, String prefix) {
306 this.path = path;
307 this.descriptionField = descriptionField;
308 this.idField = idField;
309 this.releaseField = releaseField;
310 this.plainText = plainText;
311 this.prefix = prefix;
312 }
313
314 @Override
315 public String toString() {
316 return "ReleaseInfo [path=" + path + ", descriptionField=" + descriptionField +
317 ", idField=" + idField + ", releaseField=" + releaseField + ']';
318 }
319
320 /**
321 * Extracts OS detailed information from a Linux release file (/etc/xxx-release)
322 * @return The OS detailed information, or {@code null}
323 */
324 public String extractDescription() {
325 String result = null;
326 if (path != null) {
327 Path p = Paths.get(path);
328 if (p.toFile().exists()) {
329 try (BufferedReader reader = Files.newBufferedReader(p, StandardCharsets.UTF_8)) {
330 String id = null;
331 String release = null;
332 String line;
333 while (result == null && (line = reader.readLine()) != null) {
334 if (line.contains("=")) {
335 String[] tokens = line.split("=", -1);
336 if (tokens.length >= 2) {
337 // Description, if available, contains exactly what we need
338 if (descriptionField != null && descriptionField.equalsIgnoreCase(tokens[0])) {
339 result = Utils.strip(tokens[1]);
340 } else if (idField != null && idField.equalsIgnoreCase(tokens[0])) {
341 id = Utils.strip(tokens[1]);
342 } else if (releaseField != null && releaseField.equalsIgnoreCase(tokens[0])) {
343 release = Utils.strip(tokens[1]);
344 }
345 }
346 } else if (plainText && !line.isEmpty()) {
347 // Files composed of a single line
348 result = Utils.strip(line);
349 }
350 }
351 // If no description has been found, try to rebuild it with "id" + "release" (i.e. "name" + "version")
352 if (result == null && id != null && release != null) {
353 result = id + ' ' + release;
354 }
355 } catch (IOException e) {
356 // Ignore
357 Logging.trace(e);
358 }
359 }
360 }
361 // Append prefix if any
362 if (!Utils.isEmpty(result) && !Utils.isEmpty(prefix)) {
363 result = prefix + result;
364 }
365 if (result != null)
366 result = result.replaceAll("\"+", "");
367 return result;
368 }
369 }
370
371 /**
372 * Get the dot directory <code>~/.josm</code>.
373 * @return the dot directory
374 */
375 private static File getDotDirectory() {
376 String dirName = "." + Preferences.getJOSMDirectoryBaseName().toLowerCase(Locale.ENGLISH);
377 return new File(getSystemProperty("user.home"), dirName);
378 }
379
380 /**
381 * Returns true if the dot directory should be used for storing preferences,
382 * cache and user data.
383 * Currently this is the case, if the dot directory already exists.
384 * @return true if the dot directory should be used
385 */
386 private static boolean useDotDirectory() {
387 return getDotDirectory().exists();
388 }
389
390 @Override
391 public File getDefaultCacheDirectory() {
392 if (useDotDirectory()) {
393 return new File(getDotDirectory(), "cache");
394 } else {
395 String xdgCacheDir = getSystemEnv("XDG_CACHE_HOME");
396 if (!Utils.isEmpty(xdgCacheDir)) {
397 return new File(xdgCacheDir, Preferences.getJOSMDirectoryBaseName());
398 } else {
399 return new File(getSystemProperty("user.home") + File.separator +
400 ".cache" + File.separator + Preferences.getJOSMDirectoryBaseName());
401 }
402 }
403 }
404
405 @Override
406 public File getDefaultPrefDirectory() {
407 if (useDotDirectory()) {
408 return getDotDirectory();
409 } else {
410 String xdgConfigDir = getSystemEnv("XDG_CONFIG_HOME");
411 if (!Utils.isEmpty(xdgConfigDir)) {
412 return new File(xdgConfigDir, Preferences.getJOSMDirectoryBaseName());
413 } else {
414 return new File(getSystemProperty("user.home") + File.separator +
415 ".config" + File.separator + Preferences.getJOSMDirectoryBaseName());
416 }
417 }
418 }
419
420 @Override
421 public File getDefaultUserDataDirectory() {
422 if (useDotDirectory()) {
423 return getDotDirectory();
424 } else {
425 String xdgDataDir = getSystemEnv("XDG_DATA_HOME");
426 if (!Utils.isEmpty(xdgDataDir)) {
427 return new File(xdgDataDir, Preferences.getJOSMDirectoryBaseName());
428 } else {
429 return new File(getSystemProperty("user.home") + File.separator +
430 ".local" + File.separator + "share" + File.separator + Preferences.getJOSMDirectoryBaseName());
431 }
432 }
433 }
434
435 @Override
436 public X509Certificate getX509Certificate(NativeCertAmend certAmend)
437 throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
438 for (String dir : new String[] {"/etc/ssl/certs", "/usr/share/ca-certificates/mozilla"}) {
439 File f = new File(dir, certAmend.getFilename());
440 if (f.exists()) {
441 CertificateFactory fact = CertificateFactory.getInstance("X.509");
442 try (InputStream is = Files.newInputStream(f.toPath())) {
443 return (X509Certificate) fact.generateCertificate(is);
444 }
445 }
446 }
447 return null;
448 }
449
450 @Override
451 public Collection<String> getPossiblePreferenceDirs() {
452 Set<String> locations = new HashSet<>();
453 locations.add("/usr/local/share/josm/");
454 locations.add("/usr/local/lib/josm/");
455 locations.add("/usr/share/josm/");
456 locations.add("/usr/lib/josm/");
457 return locations;
458 }
459}
Note: See TracBrowser for help on using the repository browser.