17 |
ilm |
1 |
/*
|
|
|
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
|
|
|
3 |
*
|
|
|
4 |
* Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
|
|
|
5 |
*
|
|
|
6 |
* The contents of this file are subject to the terms of the GNU General Public License Version 3
|
|
|
7 |
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
|
|
|
8 |
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
|
|
|
9 |
* language governing permissions and limitations under the License.
|
|
|
10 |
*
|
|
|
11 |
* When distributing the software, include this License Header Notice in each file.
|
|
|
12 |
*/
|
|
|
13 |
|
|
|
14 |
package org.openconcerto.utils;
|
|
|
15 |
|
80 |
ilm |
16 |
import org.openconcerto.utils.OSFamily.Unix;
|
|
|
17 |
|
17 |
ilm |
18 |
import java.io.ByteArrayOutputStream;
|
|
|
19 |
import java.io.File;
|
|
|
20 |
import java.io.IOException;
|
142 |
ilm |
21 |
import java.io.InputStream;
|
17 |
ilm |
22 |
import java.lang.reflect.Method;
|
93 |
ilm |
23 |
import java.nio.charset.Charset;
|
180 |
ilm |
24 |
import java.util.concurrent.FutureTask;
|
|
|
25 |
import java.util.concurrent.RunnableFuture;
|
142 |
ilm |
26 |
import java.util.logging.Level;
|
17 |
ilm |
27 |
import java.util.regex.Matcher;
|
|
|
28 |
import java.util.regex.Pattern;
|
|
|
29 |
|
180 |
ilm |
30 |
import javax.swing.SwingUtilities;
|
17 |
ilm |
31 |
import javax.swing.filechooser.FileSystemView;
|
142 |
ilm |
32 |
import javax.xml.parsers.DocumentBuilder;
|
|
|
33 |
import javax.xml.parsers.DocumentBuilderFactory;
|
17 |
ilm |
34 |
|
142 |
ilm |
35 |
import org.w3c.dom.Element;
|
|
|
36 |
import org.w3c.dom.NodeList;
|
|
|
37 |
|
180 |
ilm |
38 |
import net.jcip.annotations.GuardedBy;
|
|
|
39 |
import net.jcip.annotations.ThreadSafe;
|
|
|
40 |
|
17 |
ilm |
41 |
/**
|
|
|
42 |
* A desktop environment like Gnome or MacOS.
|
|
|
43 |
*
|
|
|
44 |
* @author Sylvain CUAZ
|
|
|
45 |
* @see #getDE()
|
|
|
46 |
*/
|
180 |
ilm |
47 |
@ThreadSafe
|
17 |
ilm |
48 |
public abstract class DesktopEnvironment {
|
|
|
49 |
|
|
|
50 |
static public final class Gnome extends DesktopEnvironment {
|
|
|
51 |
|
142 |
ilm |
52 |
static private final String getTextContent(final Element parentElem, final String childName) {
|
|
|
53 |
final NodeList children = parentElem.getElementsByTagName(childName);
|
|
|
54 |
if (children.getLength() != 1)
|
|
|
55 |
throw new IllegalStateException("Not one child " + childName + " in " + parentElem);
|
|
|
56 |
return children.item(0).getTextContent();
|
|
|
57 |
}
|
|
|
58 |
|
|
|
59 |
private final String name;
|
|
|
60 |
|
|
|
61 |
public Gnome(final String name) {
|
|
|
62 |
this.name = name;
|
|
|
63 |
}
|
|
|
64 |
|
17 |
ilm |
65 |
@Override
|
|
|
66 |
protected String findVersion() {
|
|
|
67 |
try {
|
142 |
ilm |
68 |
if (this.name.equals("gnome")) {
|
|
|
69 |
// e.g. GNOME gnome-about 2.24.1
|
|
|
70 |
final String line = cmdSubstitution(Runtime.getRuntime().exec(new String[] { "gnome-about", "--version" }));
|
|
|
71 |
final String[] words = line.split(" ");
|
|
|
72 |
return words[words.length - 1];
|
|
|
73 |
} else if (this.name.equals("gnome3")) {
|
|
|
74 |
final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
|
|
75 |
// <gnome-version>
|
|
|
76 |
// <platform>3</platform>
|
|
|
77 |
// <minor>18</minor>
|
|
|
78 |
// <micro>2</micro>
|
|
|
79 |
// </gnome-version>
|
|
|
80 |
final Element root = builder.parse(new File("/usr/share/gnome/gnome-version.xml")).getDocumentElement();
|
|
|
81 |
return getTextContent(root, "platform") + '.' + getTextContent(root, "minor") + '.' + getTextContent(root, "micro");
|
|
|
82 |
} else {
|
|
|
83 |
throw new IllegalStateException("unknown name : " + this.name);
|
|
|
84 |
}
|
|
|
85 |
} catch (Exception e) {
|
|
|
86 |
e.printStackTrace();
|
|
|
87 |
return null;
|
|
|
88 |
}
|
|
|
89 |
}
|
|
|
90 |
}
|
|
|
91 |
|
|
|
92 |
static public final class MATE extends DesktopEnvironment {
|
|
|
93 |
|
|
|
94 |
@Override
|
|
|
95 |
protected String findVersion() {
|
|
|
96 |
try {
|
17 |
ilm |
97 |
// e.g. GNOME gnome-about 2.24.1
|
142 |
ilm |
98 |
final String line = cmdSubstitution(Runtime.getRuntime().exec(new String[] { "mate-about", "--version" }));
|
17 |
ilm |
99 |
final String[] words = line.split(" ");
|
|
|
100 |
return words[words.length - 1];
|
|
|
101 |
} catch (Exception e) {
|
|
|
102 |
e.printStackTrace();
|
|
|
103 |
return null;
|
|
|
104 |
}
|
|
|
105 |
}
|
|
|
106 |
}
|
|
|
107 |
|
|
|
108 |
static public final class KDE extends DesktopEnvironment {
|
|
|
109 |
|
|
|
110 |
private static final Pattern versionPattern = Pattern.compile("^KDE: (.*)$", Pattern.MULTILINE);
|
|
|
111 |
|
|
|
112 |
@Override
|
|
|
113 |
protected String findVersion() {
|
|
|
114 |
try {
|
|
|
115 |
// Qt: 3.3.8b
|
|
|
116 |
// KDE: 3.5.10
|
|
|
117 |
// kde-config: 1.0
|
|
|
118 |
final String line = cmdSubstitution(Runtime.getRuntime().exec(new String[] { "kde-config", "--version" }));
|
|
|
119 |
final Matcher matcher = versionPattern.matcher(line);
|
|
|
120 |
matcher.find();
|
|
|
121 |
return matcher.group(1);
|
|
|
122 |
} catch (Exception e) {
|
|
|
123 |
e.printStackTrace();
|
|
|
124 |
return null;
|
|
|
125 |
}
|
|
|
126 |
}
|
|
|
127 |
}
|
|
|
128 |
|
|
|
129 |
static public final class XFCE extends DesktopEnvironment {
|
142 |
ilm |
130 |
// xfce4-about 4.11.1 (Xfce 4.10)
|
|
|
131 |
// Copyright (c) 2008-2011
|
|
|
132 |
private static final Pattern versionPattern = Pattern.compile("^xfce4-about.+\\(\\p{Alnum}+\\p{Blank}+(.+)\\)$", Pattern.MULTILINE);
|
17 |
ilm |
133 |
|
|
|
134 |
@Override
|
|
|
135 |
protected String findVersion() {
|
142 |
ilm |
136 |
try {
|
|
|
137 |
final String line = cmdSubstitution(Runtime.getRuntime().exec(new String[] { "xfce4-about", "--version" }));
|
|
|
138 |
final Matcher matcher = versionPattern.matcher(line);
|
|
|
139 |
matcher.find();
|
|
|
140 |
return matcher.group(1);
|
|
|
141 |
} catch (Exception e) {
|
|
|
142 |
e.printStackTrace();
|
|
|
143 |
return null;
|
|
|
144 |
}
|
17 |
ilm |
145 |
}
|
|
|
146 |
}
|
|
|
147 |
|
|
|
148 |
static public final class Unknown extends DesktopEnvironment {
|
|
|
149 |
@Override
|
|
|
150 |
protected String findVersion() {
|
|
|
151 |
return "";
|
|
|
152 |
}
|
|
|
153 |
}
|
|
|
154 |
|
|
|
155 |
static private class DEisOS extends DesktopEnvironment {
|
|
|
156 |
@Override
|
|
|
157 |
protected String findVersion() {
|
|
|
158 |
return System.getProperty("os.version");
|
|
|
159 |
}
|
|
|
160 |
}
|
|
|
161 |
|
|
|
162 |
static public final class Windows extends DEisOS {
|
28 |
ilm |
163 |
static private boolean needsQuoting(String s) {
|
|
|
164 |
final int len = s.length();
|
|
|
165 |
if (len == 0) // empty string have to be quoted
|
|
|
166 |
return true;
|
|
|
167 |
for (int i = 0; i < len; i++) {
|
|
|
168 |
switch (s.charAt(i)) {
|
93 |
ilm |
169 |
case ' ':
|
|
|
170 |
case '\t':
|
|
|
171 |
case '\\':
|
|
|
172 |
case '"':
|
|
|
173 |
return true;
|
28 |
ilm |
174 |
}
|
|
|
175 |
}
|
|
|
176 |
return false;
|
|
|
177 |
}
|
|
|
178 |
|
61 |
ilm |
179 |
// on Windows program themselves are required to parse the command line, thus a lot of them
|
|
|
180 |
// do it differently, see http://www.autohotkey.net/~deleyd/parameters/parameters.htm
|
|
|
181 |
|
|
|
182 |
static private final Pattern quotePatrn = Pattern.compile("([\\\\]*)\"");
|
|
|
183 |
static private final Pattern endSlashPatrn = Pattern.compile("([\\\\]*)\\z");
|
|
|
184 |
|
28 |
ilm |
185 |
// see http://bugs.sun.com/view_bug.do?bug_id=6468220
|
61 |
ilm |
186 |
// e.g. find.exe, choice.exe
|
|
|
187 |
public String quoteParamForMsftC(String s) {
|
28 |
ilm |
188 |
if (!needsQuoting(s))
|
|
|
189 |
return s;
|
61 |
ilm |
190 |
if (s.length() > 0) {
|
|
|
191 |
// replace '(\*)"' by '$1$1\"', e.g. '\quote " \"' by '\quote \" \\\"'
|
|
|
192 |
// $1 needed so that the backslash we add isn't escaped itself by a preceding
|
|
|
193 |
// backslash
|
|
|
194 |
s = quotePatrn.matcher(s).replaceAll("$1$1\\\\\"");
|
|
|
195 |
// replace '(\*)\z' by '$1$1', e.g. 'foo\' by 'foo\\'
|
|
|
196 |
// needed to not escape closing quote
|
|
|
197 |
s = endSlashPatrn.matcher(s).replaceAll("$1$1");
|
|
|
198 |
}
|
|
|
199 |
return '"' + s + '"';
|
|
|
200 |
}
|
|
|
201 |
|
|
|
202 |
// e.g. bash.exe
|
|
|
203 |
public String quoteParamForGCC(String s) {
|
28 |
ilm |
204 |
return StringUtils.doubleQuote(s);
|
|
|
205 |
}
|
61 |
ilm |
206 |
|
|
|
207 |
public String quoteParamForScript(final String s) {
|
|
|
208 |
if (s.indexOf('"') >= 0)
|
|
|
209 |
throw new IllegalArgumentException("Can not pass a double quote as part of a parameter");
|
|
|
210 |
return '"' + s + '"';
|
|
|
211 |
}
|
|
|
212 |
|
|
|
213 |
@Override
|
|
|
214 |
public String quoteParamForExec(final String s) {
|
|
|
215 |
return quoteParamForMsftC(s);
|
|
|
216 |
}
|
17 |
ilm |
217 |
}
|
|
|
218 |
|
|
|
219 |
static public final class Mac extends DEisOS {
|
|
|
220 |
|
|
|
221 |
// From CarbonCore/Folders.h
|
180 |
ilm |
222 |
public static final String kDocumentsDirectory = "docs";
|
|
|
223 |
public static final String kPreferencesDirectory = "pref";
|
17 |
ilm |
224 |
private static Class<?> FileManagerClass;
|
|
|
225 |
private static Short kUserDomain;
|
|
|
226 |
private static Method OSTypeToInt;
|
|
|
227 |
|
|
|
228 |
private static Class<?> getFileManagerClass() {
|
|
|
229 |
if (FileManagerClass == null) {
|
|
|
230 |
try {
|
|
|
231 |
FileManagerClass = Class.forName("com.apple.eio.FileManager");
|
|
|
232 |
OSTypeToInt = FileManagerClass.getMethod("OSTypeToInt", String.class);
|
|
|
233 |
kUserDomain = (Short) FileManagerClass.getField("kUserDomain").get(null);
|
|
|
234 |
} catch (RuntimeException e) {
|
|
|
235 |
throw e;
|
|
|
236 |
} catch (Exception e) {
|
|
|
237 |
throw new IllegalStateException(e);
|
|
|
238 |
}
|
|
|
239 |
}
|
|
|
240 |
return FileManagerClass;
|
|
|
241 |
}
|
|
|
242 |
|
|
|
243 |
@Override
|
|
|
244 |
public File getDocumentsFolder() {
|
180 |
ilm |
245 |
return new File(System.getProperty("user.home"), "Documents/");
|
17 |
ilm |
246 |
}
|
|
|
247 |
|
180 |
ilm |
248 |
// There was a warning until JRE v16, now "--add-exports
|
|
|
249 |
// java.desktop/com.apple.eio=ALL-UNNAMED" is needed. Further this needs the EDT which is
|
|
|
250 |
// undesirable just to get some paths.
|
|
|
251 |
// https://bugs.openjdk.java.net/browse/JDK-8187981
|
|
|
252 |
@Deprecated
|
17 |
ilm |
253 |
public File getFolder(String type) {
|
|
|
254 |
try {
|
|
|
255 |
final Method findFolder = getFileManagerClass().getMethod("findFolder", Short.TYPE, Integer.TYPE);
|
180 |
ilm |
256 |
final RunnableFuture<String> f = new FutureTask<>(() -> (String) findFolder.invoke(null, kUserDomain, OSTypeToInt.invoke(null, type)));
|
|
|
257 |
// EDT needed at least for JRE v14-16 on macOS 10.15, otherwise the VM crashes :
|
|
|
258 |
// Problematic frame: __NSAssertMainEventQueueIsCurrentEventQueue_block_invoke or
|
|
|
259 |
// "The current event queue and the main event queue are not the same. This is
|
|
|
260 |
// probably because _TSGetMainThread was called for the first time off the main
|
|
|
261 |
// thread."
|
|
|
262 |
if (SwingUtilities.isEventDispatchThread())
|
|
|
263 |
f.run();
|
|
|
264 |
else
|
|
|
265 |
SwingUtilities.invokeLater(f);
|
|
|
266 |
final String path = f.get();
|
17 |
ilm |
267 |
return new File(path);
|
|
|
268 |
} catch (RuntimeException e) {
|
|
|
269 |
throw e;
|
|
|
270 |
} catch (Exception e) {
|
|
|
271 |
throw new IllegalStateException(e);
|
|
|
272 |
}
|
|
|
273 |
}
|
|
|
274 |
|
61 |
ilm |
275 |
public File getAppDir(final String bundleID) throws IOException {
|
|
|
276 |
// we used to ask for the URL of the application file but since 10.7 it returns a
|
|
|
277 |
// file reference URL like "file:///.file/id=6723689.35865"
|
142 |
ilm |
278 |
final ProcessBuilder processBuilder = new ProcessBuilder("osascript", "-e",
|
|
|
279 |
"tell application id \"com.apple.Finder\" to POSIX path of (application file id \"" + bundleID + "\" as string)");
|
61 |
ilm |
280 |
// if not found prints nothing to out and a cryptic error to the standard error stream
|
|
|
281 |
final String dir = cmdSubstitution(processBuilder.start()).trim();
|
|
|
282 |
return dir.length() == 0 ? null : new File(dir);
|
|
|
283 |
}
|
17 |
ilm |
284 |
}
|
|
|
285 |
|
|
|
286 |
/**
|
|
|
287 |
* Execute the passed command and test its return code.
|
|
|
288 |
*
|
|
|
289 |
* @param command the command to {@link Runtime#exec(String[]) execute}.
|
|
|
290 |
* @return <code>false</code> if the {@link Process#waitFor() return code} is not 0 or an
|
|
|
291 |
* exception is thrown.
|
|
|
292 |
* @throws RTInterruptedException if this is interrupted while waiting.
|
|
|
293 |
*/
|
|
|
294 |
public static final boolean test(final String... command) throws RTInterruptedException {
|
|
|
295 |
try {
|
|
|
296 |
return Runtime.getRuntime().exec(command).waitFor() == 0;
|
|
|
297 |
} catch (InterruptedException e) {
|
|
|
298 |
throw new RTInterruptedException(e);
|
|
|
299 |
} catch (IOException e) {
|
|
|
300 |
Log.get().finer(e.getLocalizedMessage());
|
|
|
301 |
return false;
|
|
|
302 |
}
|
|
|
303 |
}
|
|
|
304 |
|
|
|
305 |
public final static String cmdSubstitution(Process p) throws IOException {
|
93 |
ilm |
306 |
return cmdSubstitution(p, null);
|
|
|
307 |
}
|
|
|
308 |
|
|
|
309 |
public final static String cmdSubstitution(final Process p, final Charset encoding) throws IOException {
|
17 |
ilm |
310 |
final ByteArrayOutputStream out = new ByteArrayOutputStream(100 * 1024);
|
144 |
ilm |
311 |
// some programs won't write anything until they read everything (e.g. powershell.exe)
|
|
|
312 |
p.getOutputStream().close();
|
93 |
ilm |
313 |
p.getErrorStream().close();
|
17 |
ilm |
314 |
StreamUtils.copy(p.getInputStream(), out);
|
93 |
ilm |
315 |
p.getInputStream().close();
|
|
|
316 |
return encoding == null ? out.toString() : out.toString(encoding.name());
|
17 |
ilm |
317 |
}
|
|
|
318 |
|
142 |
ilm |
319 |
private static final String detectXDG() {
|
|
|
320 |
String res = null;
|
|
|
321 |
InputStream scriptIns = null;
|
|
|
322 |
try {
|
|
|
323 |
scriptIns = DesktopEnvironment.class.getResourceAsStream("DesktopEnvironmentXDG.sh");
|
|
|
324 |
final Process ps = new ProcessBuilder("sh").start();
|
|
|
325 |
ps.getErrorStream().close();
|
|
|
326 |
StreamUtils.copy(scriptIns, ps.getOutputStream());
|
|
|
327 |
ps.getOutputStream().close();
|
|
|
328 |
res = FileUtils.readUTF8(ps.getInputStream()).trim();
|
|
|
329 |
ps.getInputStream().close();
|
|
|
330 |
if (ps.waitFor() != 0)
|
|
|
331 |
throw new IllegalStateException("Not OK : " + ps.exitValue());
|
|
|
332 |
} catch (Exception e) {
|
|
|
333 |
Log.get().fine(e.getLocalizedMessage());
|
|
|
334 |
} finally {
|
|
|
335 |
if (scriptIns != null) {
|
|
|
336 |
try {
|
|
|
337 |
scriptIns.close();
|
|
|
338 |
} catch (IOException e) {
|
|
|
339 |
e.printStackTrace();
|
|
|
340 |
}
|
|
|
341 |
}
|
|
|
342 |
}
|
|
|
343 |
return res;
|
|
|
344 |
}
|
|
|
345 |
|
17 |
ilm |
346 |
private static final DesktopEnvironment detectDE() {
|
80 |
ilm |
347 |
final OSFamily os = OSFamily.getInstance();
|
|
|
348 |
if (os == OSFamily.Windows) {
|
17 |
ilm |
349 |
return new Windows();
|
80 |
ilm |
350 |
} else if (os == OSFamily.Mac) {
|
17 |
ilm |
351 |
return new Mac();
|
80 |
ilm |
352 |
} else if (os instanceof Unix) {
|
142 |
ilm |
353 |
final String de = detectXDG();
|
|
|
354 |
if (de.equals("xfce"))
|
|
|
355 |
return new XFCE();
|
|
|
356 |
else if (de.equals("mate"))
|
|
|
357 |
return new MATE();
|
|
|
358 |
else if (de.equals("kde"))
|
17 |
ilm |
359 |
return new KDE();
|
142 |
ilm |
360 |
else if (de.startsWith("gnome"))
|
|
|
361 |
return new Gnome(de);
|
17 |
ilm |
362 |
}
|
|
|
363 |
return new Unknown();
|
|
|
364 |
}
|
|
|
365 |
|
180 |
ilm |
366 |
@GuardedBy("DesktopEnvironment.class")
|
17 |
ilm |
367 |
private static DesktopEnvironment DE = null;
|
|
|
368 |
|
180 |
ilm |
369 |
public synchronized static final DesktopEnvironment getDE() {
|
17 |
ilm |
370 |
if (DE == null) {
|
|
|
371 |
DE = detectDE();
|
|
|
372 |
}
|
|
|
373 |
return DE;
|
|
|
374 |
}
|
|
|
375 |
|
180 |
ilm |
376 |
public synchronized static final void resetDE() {
|
17 |
ilm |
377 |
DE = null;
|
|
|
378 |
}
|
|
|
379 |
|
180 |
ilm |
380 |
@GuardedBy("this")
|
17 |
ilm |
381 |
private String version;
|
|
|
382 |
|
|
|
383 |
private DesktopEnvironment() {
|
|
|
384 |
this.version = null;
|
|
|
385 |
}
|
|
|
386 |
|
|
|
387 |
protected abstract String findVersion();
|
|
|
388 |
|
180 |
ilm |
389 |
public synchronized final String getVersion() {
|
17 |
ilm |
390 |
if (this.version == null)
|
|
|
391 |
this.version = this.findVersion();
|
|
|
392 |
return this.version;
|
|
|
393 |
}
|
|
|
394 |
|
142 |
ilm |
395 |
// where to write user-visible files (e.g. spreadsheets, exports)
|
17 |
ilm |
396 |
public File getDocumentsFolder() {
|
|
|
397 |
return FileSystemView.getFileSystemView().getDefaultDirectory();
|
|
|
398 |
}
|
|
|
399 |
|
28 |
ilm |
400 |
// on some systems arguments are not passed correctly by ProcessBuilder
|
|
|
401 |
public String quoteParamForExec(String s) {
|
|
|
402 |
return s;
|
|
|
403 |
}
|
|
|
404 |
|
17 |
ilm |
405 |
@Override
|
|
|
406 |
public String toString() {
|
|
|
407 |
return "DesktopEnvironment " + this.getClass().getSimpleName();
|
|
|
408 |
}
|
|
|
409 |
|
|
|
410 |
public static void main(String[] args) {
|
142 |
ilm |
411 |
// removes default
|
|
|
412 |
LogUtils.rmRootHandlers();
|
|
|
413 |
// add console handler
|
|
|
414 |
LogUtils.setUpConsoleHandler();
|
|
|
415 |
Log.get().setLevel(Level.FINE);
|
|
|
416 |
final DesktopEnvironment de = getDE();
|
|
|
417 |
System.out.println(de + " version " + de.getVersion());
|
17 |
ilm |
418 |
}
|
|
|
419 |
}
|