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