source: josm/trunk/src/org/openstreetmap/josm/tools/PlatformHookUnixoid.java@ 13648

Last change on this file since 13648 was 13647, checked in by Don-vip, 6 years ago

see #16204 - Allow to start and close JOSM in WebStart sandbox mode (where every external access is denied). This was very useful to reproduce some very tricky bugs that occured in real life but were almost impossible to diagnose.

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