1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.newsclub.net.unix;
19
20 import java.io.Closeable;
21 import java.io.File;
22 import java.io.FileNotFoundException;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.net.URL;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.Objects;
32 import java.util.Properties;
33 import java.util.concurrent.atomic.AtomicBoolean;
34
35 import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
36
37 @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
38 final class NativeLibraryLoader implements Closeable {
39 private static final String PROP_LIBRARY_DISABLE = "org.newsclub.net.unix.library.disable";
40 private static final String PROP_LIBRARY_OVERRIDE = "org.newsclub.net.unix.library.override";
41 private static final String PROP_LIBRARY_OVERRIDE_FORCE =
42 "org.newsclub.net.unix.library.override.force";
43 private static final String PROP_LIBRARY_TMPDIR = "org.newsclub.net.unix.library.tmpdir";
44
45 private static final File TEMP_DIR;
46 private static final String OS_NAME_SIMPLIFIED = lookupArchProperty("os.name", "UnknownOS");
47
48 private static final List<String> ARCHITECTURE_AND_OS = architectureAndOS();
49 private static final String LIBRARY_NAME = "junixsocket-native";
50
51 private static final AtomicBoolean LOADED = new AtomicBoolean(false);
52 private static final boolean IS_ANDROID = checkAndroid();
53
54 static {
55 String dir = System.getProperty(PROP_LIBRARY_TMPDIR, null);
56 TEMP_DIR = (dir == null) ? null : new File(dir);
57 }
58
59 NativeLibraryLoader() {
60 }
61
62
63
64
65
66
67 static File tempDir() {
68 return TEMP_DIR;
69 }
70
71 private List<LibraryCandidate> tryProviderClass(String providerClassname, String artifactName)
72 throws IOException, ClassNotFoundException {
73 Class<?> providerClass = Class.forName(providerClassname);
74
75 String version = getArtifactVersion(providerClass, artifactName);
76 String libraryNameAndVersion = LIBRARY_NAME + "-" + version;
77
78 return findLibraryCandidates(artifactName, libraryNameAndVersion, providerClass);
79 }
80
81 public static String getJunixsocketVersion() throws IOException {
82
83
84 String v = BuildProperties.getBuildProperties().get("git.build.version");
85 if (v != null && !v.startsWith("$")) {
86 return v;
87 }
88
89 return getArtifactVersion(AFSocket.class, "junixsocket-common");
90 }
91
92 private static String getArtifactVersion(Class<?> providerClass, String... artifactNames)
93 throws IOException {
94 for (String artifactName : artifactNames) {
95 Properties p = new Properties();
96 String resource = "/META-INF/maven/com.kohlschutter.junixsocket/" + artifactName
97 + "/pom.properties";
98 try (InputStream in = providerClass.getResourceAsStream(resource)) {
99 if (in == null) {
100 throw new FileNotFoundException("Could not find resource " + resource + " relative to "
101 + providerClass);
102 }
103 p.load(in);
104 String version = p.getProperty("version");
105
106 Objects.requireNonNull(version, "Could not read version from pom.properties");
107 return version;
108 }
109 }
110 throw new IllegalStateException("No artifact names specified");
111 }
112
113 private abstract static class LibraryCandidate implements Closeable {
114 protected final String libraryNameAndVersion;
115
116 protected LibraryCandidate(String libraryNameAndVersion) {
117 this.libraryNameAndVersion = libraryNameAndVersion;
118 }
119
120 @SuppressFBWarnings("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION")
121 abstract String load() throws Exception;
122
123 @Override
124 public abstract void close();
125
126 @Override
127 public String toString() {
128 return super.toString() + "[" + libraryNameAndVersion + "]";
129 }
130 }
131
132 private static final class StandardLibraryCandidate extends LibraryCandidate {
133 StandardLibraryCandidate(String version) {
134 super(version == null ? LIBRARY_NAME : LIBRARY_NAME + "-" + version);
135 }
136
137 @Override
138 @SuppressFBWarnings("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION")
139 String load() throws Exception, LinkageError {
140 if (libraryNameAndVersion != null) {
141 System.loadLibrary(libraryNameAndVersion);
142 return libraryNameAndVersion;
143 }
144 return null;
145 }
146
147 @Override
148 public void close() {
149 }
150
151 @Override
152 public String toString() {
153 return super.toString() + "(standard library path)";
154 }
155 }
156
157 private static final class ClasspathLibraryCandidate extends LibraryCandidate {
158 private final String artifactName;
159 private final URL library;
160 private final String path;
161
162 ClasspathLibraryCandidate(String artifactName, String libraryNameAndVersion, String path,
163 URL library) {
164 super(libraryNameAndVersion);
165 this.artifactName = artifactName;
166 this.path = path;
167 this.library = library;
168 }
169
170 @Override
171 synchronized String load() throws IOException, LinkageError {
172 if (libraryNameAndVersion == null) {
173 return null;
174 }
175
176 File libDir = TEMP_DIR;
177
178 for (int attempt = 0; attempt < 3; attempt++) {
179 File libFile;
180 try {
181 libFile = File.createTempFile("libtmp", System.mapLibraryName(libraryNameAndVersion),
182 libDir);
183 try (InputStream libraryIn = library.openStream();
184 OutputStream out = new FileOutputStream(libFile)) {
185 byte[] buf = new byte[4096];
186 int read;
187 while ((read = libraryIn.read(buf)) >= 0) {
188 out.write(buf, 0, read);
189 }
190 }
191 } catch (IOException e) {
192 throw e;
193 }
194
195 try {
196 System.load(libFile.getAbsolutePath());
197 } catch (UnsatisfiedLinkError e) {
198
199
200
201 switch (attempt) {
202 case 0:
203 libDir = new File(System.getProperty("user.home", "."));
204 break;
205 case 1:
206 libDir = new File(System.getProperty("user.dir", "."));
207 break;
208 default:
209 throw e;
210 }
211
212 continue;
213 } finally {
214 if (!libFile.delete()) {
215 libFile.deleteOnExit();
216 }
217 }
218
219
220 break;
221 }
222 return artifactName + "/" + libraryNameAndVersion;
223 }
224
225 @Override
226 public void close() {
227 }
228
229 @Override
230 public String toString() {
231 return super.toString() + "(" + artifactName + ":" + path + ")";
232 }
233 }
234
235 private synchronized void setLoaded(String library) {
236 setLoaded0(library);
237 }
238
239 @SuppressFBWarnings("THROWS_METHOD_THROWS_RUNTIMEEXCEPTION")
240 private static synchronized void setLoaded0(String library) {
241 if (LOADED.compareAndSet(false, true)) {
242 NativeUnixSocket.setLoaded(true);
243 AFSocket.loadedLibrary = library;
244 try {
245 NativeUnixSocket.init();
246 } catch (RuntimeException e) {
247 throw e;
248 } catch (Exception e) {
249 throw new IllegalStateException(e);
250 }
251 }
252 }
253
254 private Throwable loadLibraryOverride() {
255 String libraryOverride = System.getProperty(PROP_LIBRARY_OVERRIDE, "");
256 String libraryOverrideForce = System.getProperty(PROP_LIBRARY_OVERRIDE_FORCE, "false");
257
258 boolean overrideIsAbsolute;
259 try {
260 if (libraryOverrideForce.length() <= 5) {
261 overrideIsAbsolute = false;
262 } else {
263 overrideIsAbsolute = new File(libraryOverrideForce).isAbsolute();
264 }
265 } catch (Exception e) {
266 overrideIsAbsolute = false;
267 e.printStackTrace();
268 }
269 if (libraryOverride.isEmpty() && overrideIsAbsolute) {
270 libraryOverride = libraryOverrideForce;
271 libraryOverrideForce = "true";
272 }
273
274 if (!libraryOverride.isEmpty()) {
275 try {
276 System.load(libraryOverride);
277 setLoaded(libraryOverride);
278 return null;
279 } catch (Exception | LinkageError e) {
280 if (Boolean.parseBoolean(libraryOverrideForce)) {
281 throw e;
282 }
283 return e;
284 }
285 } else {
286 return new Exception("No library specified with -D" + PROP_LIBRARY_OVERRIDE + "=");
287 }
288 }
289
290 private static Object loadLibrarySyncMonitor() {
291 Object monitor = NativeLibraryLoader.class.getClassLoader();
292 if (monitor == null) {
293
294 return NativeLibraryLoader.class;
295 } else {
296 return monitor;
297 }
298 }
299
300 @SuppressWarnings("null")
301 public synchronized void loadLibrary() {
302 synchronized (loadLibrarySyncMonitor()) {
303 if (LOADED.get()) {
304
305 return;
306 }
307
308 NativeUnixSocket.initPre();
309
310
311
312 if ("provided".equals(System.getProperty(PROP_LIBRARY_OVERRIDE_FORCE, ""))) {
313 setLoaded("provided");
314 return;
315 }
316
317 boolean provided = false;
318 try {
319 NativeUnixSocket.noop();
320 provided = true;
321 } catch (UnsatisfiedLinkError | Exception e) {
322
323 }
324 if (provided) {
325 setLoaded("provided");
326 return;
327 }
328
329 if (Boolean.parseBoolean(System.getProperty(PROP_LIBRARY_DISABLE, "false"))) {
330 throw initCantLoadLibraryError(Collections.singletonList(new UnsupportedOperationException(
331 "junixsocket disabled by System.property " + PROP_LIBRARY_DISABLE)));
332 }
333
334 List<Throwable> suppressedThrowables = new ArrayList<>();
335 Throwable ex = loadLibraryOverride();
336 if (ex == null) {
337 return;
338 }
339 suppressedThrowables.add(ex);
340
341 List<LibraryCandidate> candidates = initLibraryCandidates(suppressedThrowables);
342
343 String loadedLibraryId = null;
344 for (LibraryCandidate candidate : candidates) {
345 try {
346 if ((loadedLibraryId = candidate.load()) != null) {
347 break;
348 }
349 } catch (Exception | LinkageError e) {
350 suppressedThrowables.add(e);
351 }
352 }
353
354 for (LibraryCandidate candidate : candidates) {
355 candidate.close();
356 }
357
358 if (loadedLibraryId == null) {
359 throw initCantLoadLibraryError(suppressedThrowables);
360 }
361
362 setLoaded(loadedLibraryId);
363 }
364 }
365
366 private UnsatisfiedLinkError initCantLoadLibraryError(List<Throwable> suppressedThrowables) {
367 String message = "Could not load native library " + LIBRARY_NAME + " for architecture "
368 + ARCHITECTURE_AND_OS;
369
370 String cp = System.getProperty("java.class.path", "");
371 if (cp.contains("junixsocket-native-custom/target-eclipse") || cp.contains(
372 "junixsocket-native-common/target-eclipse")) {
373 message += "\n\n*** ECLIPSE USERS ***\nIf you're running from within Eclipse, "
374 + "please close the projects \"junixsocket-native-common\" and \"junixsocket-native-custom\"\n";
375 }
376
377 UnsatisfiedLinkError e = new UnsatisfiedLinkError(message);
378 if (suppressedThrowables != null) {
379 for (Throwable suppressed : suppressedThrowables) {
380 e.addSuppressed(suppressed);
381 }
382 }
383 throw e;
384 }
385
386 private List<LibraryCandidate> initLibraryCandidates(List<Throwable> suppressedThrowables) {
387 List<LibraryCandidate> candidates = new ArrayList<>();
388 try {
389 String version = getArtifactVersion(getClass(), "junixsocket-common", "junixsocket-core");
390 if (version != null) {
391 candidates.add(new StandardLibraryCandidate(version));
392 }
393 } catch (Exception e) {
394 suppressedThrowables.add(e);
395 }
396
397 try {
398 candidates.addAll(tryProviderClass("org.newsclub.lib.junixsocket.custom.NarMetadata",
399 "junixsocket-native-custom"));
400 } catch (Exception e) {
401 suppressedThrowables.add(e);
402 }
403 try {
404 candidates.addAll(tryProviderClass("org.newsclub.lib.junixsocket.common.NarMetadata",
405 "junixsocket-native-common"));
406 } catch (Exception e) {
407 suppressedThrowables.add(e);
408 }
409
410 candidates.add(new StandardLibraryCandidate(null));
411
412 return candidates;
413 }
414
415 private static String lookupArchProperty(String key, String defaultVal) {
416 return System.getProperty(key, defaultVal).replaceAll("[ /\\\\'\";:\\$]", "");
417 }
418
419 private static List<String> architectureAndOS() {
420 String arch = lookupArchProperty("os.arch", "UnknownArch");
421
422 List<String> list = new ArrayList<>();
423 if (IS_ANDROID) {
424
425
426 list.add(arch + "-Android");
427 }
428 list.add(arch + "-" + OS_NAME_SIMPLIFIED);
429 if (OS_NAME_SIMPLIFIED.startsWith("Windows") && !"Windows10".equals(OS_NAME_SIMPLIFIED)) {
430 list.add(arch + "-" + "Windows10");
431 }
432
433 if ("MacOSX".equals(OS_NAME_SIMPLIFIED) && "x86_64".equals(arch)) {
434 list.add("aarch64-MacOSX");
435 }
436
437 return list;
438 }
439
440 private static boolean checkAndroid() {
441 String vmName = lookupArchProperty("java.vm.name", "UnknownVM");
442 String vmSpecVendor = lookupArchProperty("java.vm.specification.vendor",
443 "UnknownSpecificationVendor");
444
445 return ("Dalvik".equals(vmName) || vmSpecVendor.contains("Android"));
446 }
447
448 static boolean isAndroid() {
449 return IS_ANDROID;
450 }
451
452 static List<String> getArchitectureAndOS() {
453 return ARCHITECTURE_AND_OS;
454 }
455
456 private static URL validateResourceURL(URL url) {
457 if (url == null) {
458 return null;
459 }
460 try (InputStream unused = url.openStream()) {
461 return url;
462 } catch (IOException e) {
463 return null;
464 }
465 }
466
467 private static String mapLibraryName(String libraryNameAndVersion) {
468 String mappedName = System.mapLibraryName(libraryNameAndVersion);
469 if (mappedName.endsWith(".so")) {
470
471
472 switch (OS_NAME_SIMPLIFIED) {
473 case "AIX":
474 mappedName = mappedName.substring(0, mappedName.length() - 3) + ".a";
475 break;
476 case "OS400":
477 mappedName = mappedName.substring(0, mappedName.length() - 3) + ".srvpgm";
478 break;
479 default:
480 break;
481 }
482 }
483 return mappedName;
484 }
485
486 private List<LibraryCandidate> findLibraryCandidates(String artifactName,
487 String libraryNameAndVersion, Class<?> providerClass) {
488 String mappedName = mapLibraryName(libraryNameAndVersion);
489
490 String[] prefixes = mappedName.startsWith("lib") ? new String[] {""} : new String[] {"", "lib"};
491
492 List<LibraryCandidate> list = new ArrayList<>();
493 for (String archOs : ARCHITECTURE_AND_OS) {
494 for (String compiler : new String[] {"clang", "gcc"
495
496
497
498 }) {
499 for (String prefix : prefixes) {
500 String path = "/lib/" + archOs + "-" + compiler + "/jni/" + prefix + mappedName;
501
502 URL url;
503
504 url = validateResourceURL(providerClass.getResource(path));
505 if (url != null) {
506 list.add(new ClasspathLibraryCandidate(artifactName, libraryNameAndVersion, path, url));
507 }
508
509
510
511
512 String nodepsPath = nodepsPath(path);
513 if (nodepsPath != null) {
514 url = validateResourceURL(providerClass.getResource(nodepsPath));
515 if (url != null) {
516 list.add(new ClasspathLibraryCandidate(artifactName, libraryNameAndVersion,
517 nodepsPath, url));
518 }
519 }
520 }
521 }
522 }
523 return list;
524 }
525
526 private String nodepsPath(String path) {
527 int lastDot = path.lastIndexOf('.');
528 if (lastDot == -1) {
529 return null;
530 } else {
531 return path.substring(0, lastDot) + ".nodeps" + path.substring(lastDot);
532 }
533 }
534
535 @Override
536 public void close() {
537 }
538 }