xref: /aosp_15_r20/external/aws-crt-java/src/main/java/software/amazon/awssdk/crt/CRT.java (revision 3c7ae9de214676c52d19f01067dc1a404272dc11)
1 /**
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  * SPDX-License-Identifier: Apache-2.0.
4  */
5 package software.amazon.awssdk.crt;
6 
7 import software.amazon.awssdk.crt.io.ClientBootstrap;
8 import software.amazon.awssdk.crt.io.EventLoopGroup;
9 import software.amazon.awssdk.crt.io.HostResolver;
10 
11 import java.io.BufferedReader;
12 import java.io.File;
13 import java.io.FileOutputStream;
14 import java.io.FilenameFilter;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.InputStreamReader;
18 import java.lang.reflect.InvocationTargetException;
19 import java.util.*;
20 import java.util.regex.Pattern;
21 
22 /**
23  * This class is responsible for loading the aws-crt-jni shared lib for the
24  * current platform out of aws-crt-java.jar. One instance of this class has to
25  * be created somewhere to invoke the static initialization block which will
26  * load the shared lib
27  */
28 public final class CRT {
29     private static final String CRT_ARCH_OVERRIDE_SYSTEM_PROPERTY = "aws.crt.arch";
30     private static final String CRT_ARCH_OVERRIDE_ENVIRONMENT_VARIABLE = "AWS_CRT_ARCH";
31 
32     private static final String CRT_LIB_NAME = "aws-crt-jni";
33     public static final int AWS_CRT_SUCCESS = 0;
34     private static final CrtPlatform s_platform;
35 
36     static {
37         // Scan for and invoke any platform specific initialization
38         s_platform = findPlatformImpl();
jvmInit()39         jvmInit();
40         try {
41             // If the lib is already present/loaded or is in java.library.path, just use it
42             System.loadLibrary(CRT_LIB_NAME);
43         } catch (UnsatisfiedLinkError e) {
44             // otherwise, load from the jar this class is in
45             loadLibraryFromJar();
46         }
47 
48         // Initialize the CRT
49         int memoryTracingLevel = 0;
50         try {
51             memoryTracingLevel = Integer.parseInt(System.getProperty("aws.crt.memory.tracing"));
52         } catch (Exception ex) {
53         }
54         boolean debugWait = System.getProperty("aws.crt.debugwait") != null;
55         boolean strictShutdown = System.getProperty("aws.crt.strictshutdown") != null;
awsCrtInit(memoryTracingLevel, debugWait, strictShutdown)56         awsCrtInit(memoryTracingLevel, debugWait, strictShutdown);
57 
58         Runtime.getRuntime().addShutdownHook(new Thread()
59         {
60             public void run()
61             {
62                 CRT.releaseShutdownRef();
63             }
64         });
65 
66         try {
Log.initLoggingFromSystemProperties()67             Log.initLoggingFromSystemProperties();
68         } catch (IllegalArgumentException e) {
69             ;
70         }
71     }
72 
73     /**
74      * Exception thrown when we can't detect what platform we're running on and thus can't figure out
75      * the native library name/path to load.
76      */
77     public static class UnknownPlatformException extends RuntimeException {
UnknownPlatformException(String message)78         UnknownPlatformException(String message) {
79             super(message);
80         }
81     }
82 
normalize(String value)83     private static String normalize(String value) {
84         if (value == null) {
85             return "";
86         }
87         return value.toLowerCase(Locale.US).replaceAll("[^a-z0-9]+", "");
88     }
89 
90     /**
91      * @return a string describing the detected platform the CRT is executing on
92      */
getOSIdentifier()93     public static String getOSIdentifier() throws UnknownPlatformException {
94         CrtPlatform platform = getPlatformImpl();
95         String name = normalize(platform != null ? platform.getOSIdentifier() : System.getProperty("os.name"));
96 
97         if (name.contains("windows")) {
98             return "windows";
99         } else if (name.contains("linux")) {
100             return "linux";
101         } else if (name.contains("freebsd")) {
102             return "freebsd";
103         } else if (name.contains("macosx")) {
104             return "osx";
105         } else if (name.contains("sun os") || name.contains("sunos") || name.contains("solaris")) {
106             return "solaris";
107         } else if (name.contains("android")){
108             return "android";
109         }
110 
111         throw new UnknownPlatformException("AWS CRT: OS not supported: " + name);
112     }
113 
114     /**
115      * @return a string describing the detected architecture the CRT is executing on
116      */
getArchIdentifier()117     public static String getArchIdentifier() throws UnknownPlatformException {
118         String systemPropertyOverride = System.getProperty(CRT_ARCH_OVERRIDE_SYSTEM_PROPERTY);
119         if (systemPropertyOverride != null && systemPropertyOverride.length() > 0) {
120             return systemPropertyOverride;
121         }
122 
123         String environmentOverride = System.getProperty(CRT_ARCH_OVERRIDE_ENVIRONMENT_VARIABLE);
124         if (environmentOverride != null && environmentOverride.length() > 0) {
125             return environmentOverride;
126         }
127 
128         CrtPlatform platform = getPlatformImpl();
129         String arch = normalize(platform != null ? platform.getArchIdentifier() : System.getProperty("os.arch"));
130         if (arch.matches("^(x8664|amd64|ia32e|em64t|x64|x86_64)$")) {
131             return "x86_64";
132         } else if (arch.matches("^(x8632|x86|i[3-6]86|ia32|x32)$")) {
133             return "x86_32";
134         } else if (arch.startsWith("armeabi")) {
135             if (arch.contains("v7")) {
136                 return "armv7";
137             } else {
138                 return "armv6";
139             }
140         } else if (arch.startsWith("arm64") || arch.startsWith("aarch64")) {
141             return "armv8";
142         } else if (arch.equals("armv7l")) {
143             return "armv7";
144         } else if (arch.startsWith("arm")) {
145             return "armv6";
146         }
147 
148         throw new UnknownPlatformException("AWS CRT: architecture not supported: " + arch);
149     }
150 
151     private static final String NON_LINUX_RUNTIME_TAG = "cruntime";
152     private static final String MUSL_RUNTIME_TAG = "musl";
153     private static final String GLIBC_RUNTIME_TAG = "glibc";
154 
getCRuntime(String osIdentifier)155     public static String getCRuntime(String osIdentifier) {
156         if (!osIdentifier.equals("linux")) {
157             return NON_LINUX_RUNTIME_TAG;
158         }
159 
160         // If system property is set, use that.
161         String systemPropertyOverride = System.getProperty("aws.crt.libc");
162         if (systemPropertyOverride != null) {
163             systemPropertyOverride = systemPropertyOverride.toLowerCase().trim();
164             if (!systemPropertyOverride.isEmpty()) {
165                 return systemPropertyOverride;
166             }
167         }
168 
169         // Be warned, the system might have both musl and glibc on it:
170         // https://github.com/awslabs/aws-crt-java/issues/659
171 
172         // Next, check which one java is using.
173         // Run: ldd /path/to/java
174         // If musl, has a line like: libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f7732ae4000)
175         // If glibc, has a line like: libc.so.6 => /lib64/ld-linux-x86-64.so.2 (0x7f112c894000)
176         Pattern muslWord = Pattern.compile("\\bmusl\\b", Pattern.CASE_INSENSITIVE);
177         Pattern libcWord = Pattern.compile("\\blibc\\b", Pattern.CASE_INSENSITIVE);
178         String javaHome = System.getProperty("java.home");
179         if (javaHome != null) {
180             File javaExecutable = new File(new File(javaHome, "bin"), "java");
181             if (javaExecutable.exists()) {
182                 try {
183                     String[] lddJavaCmd = {"ldd", javaExecutable.toString()};
184                     List<String> lddJavaOutput = runProcess(lddJavaCmd);
185                     for (String line : lddJavaOutput) {
186                         // check if the "libc" line mentions "musl"
187                         if (libcWord.matcher(line).find()) {
188                             if (muslWord.matcher(line).find()) {
189                                 return MUSL_RUNTIME_TAG;
190                             } else {
191                                 return GLIBC_RUNTIME_TAG;
192                             }
193                         }
194                     }
195                     // uncertain, continue to next check
196                 } catch (IOException ex) {
197                     // uncertain, continue to next check
198                 }
199             }
200         }
201 
202         // Next, check whether ldd says it's using musl
203         // Run: ldd --version
204         // If musl, has a line like: musl libc (x86_64)
205         try {
206             String[] lddVersionCmd = {"ldd", "--version"};
207             List<String> lddVersionOutput = runProcess(lddVersionCmd);
208             for (String line : lddVersionOutput) {
209                 // any mention of "musl" is sufficient
210                 if (muslWord.matcher(line).find()) {
211                     return MUSL_RUNTIME_TAG;
212                 }
213             }
214             // uncertain, continue to next check
215 
216         } catch (IOException io) {
217             // uncertain, continue to next check
218         }
219 
220         // Assume it's glibc
221         return GLIBC_RUNTIME_TAG;
222     }
223 
224     // Run process and return lines of output.
225     // Output is stdout and stderr merged together.
226     // The exit code is ignored.
227     // We do it this way because, on some Linux distros (Alpine),
228     // "ldd --version" reports exit code 1 and prints to stderr.
229     // But on most Linux distros it reports exit code 0 and prints to stdout.
runProcess(String[] cmdArray)230     private static List<String> runProcess(String[] cmdArray) throws IOException {
231         java.lang.Process proc = new ProcessBuilder(cmdArray)
232                 .redirectErrorStream(true) // merge stderr into stdout
233                 .start();
234 
235         // confusingly, getInputStream() gets you stdout
236         BufferedReader outputReader = new BufferedReader(new
237                 InputStreamReader(proc.getInputStream()));
238 
239         String line;
240         List<String> output = new ArrayList<String>();
241         while ((line = outputReader.readLine()) != null) {
242             output.add(line);
243         }
244 
245         return output;
246     }
247 
extractAndLoadLibrary(String path)248     private static void extractAndLoadLibrary(String path) {
249         try {
250             // Check java.io.tmpdir
251             String tmpdirPath;
252             File tmpdirFile;
253             try {
254                 tmpdirFile = new File(path).getAbsoluteFile();
255                 tmpdirPath = tmpdirFile.getAbsolutePath();
256                 if (tmpdirFile.exists()) {
257                     if (!tmpdirFile.isDirectory()) {
258                         throw new IOException("not a directory: " + tmpdirPath);
259                     }
260                 } else {
261                     tmpdirFile.mkdirs();
262                 }
263 
264                 if (!tmpdirFile.canRead() || !tmpdirFile.canWrite()) {
265                     throw new IOException("access denied: " + tmpdirPath);
266                 }
267             } catch (Exception ex) {
268                 String msg = "Invalid directory: " + path;
269                 throw new IOException(msg, ex);
270             }
271 
272             String libraryName = System.mapLibraryName(CRT_LIB_NAME);
273 
274             // Prefix the lib we'll extract to disk
275             String tempSharedLibPrefix = "AWSCRT_";
276 
277             File tempSharedLib = File.createTempFile(tempSharedLibPrefix, libraryName, tmpdirFile);
278             if (!tempSharedLib.setExecutable(true, true)) {
279                 throw new CrtRuntimeException("Unable to make shared library executable by owner only");
280             }
281             if (!tempSharedLib.setWritable(true, true)) {
282                 throw new CrtRuntimeException("Unable to make shared library writeable by owner only");
283             }
284             if (!tempSharedLib.setReadable(true, true)) {
285                 throw new CrtRuntimeException("Unable to make shared library readable by owner only");
286             }
287 
288 			// The temp lib file should be deleted when we're done with it.
289 			// Ask Java to try and delete it on exit. We call this immediately
290 			// so that if anything goes wrong writing the file to disk, or
291 			// loading it as a shared lib, it will still get cleaned up.
292 			tempSharedLib.deleteOnExit();
293 
294             // Unfortunately File.deleteOnExit() won't work on Windows, where
295             // files cannot be deleted while they're in use. On Windows, once
296             // our .dll is loaded, it can't be deleted by this process.
297             //
298             // The Windows-only solution to this problem is to scan on startup
299             // for old instances of the .dll and try to delete them. If another
300             // process is still using the .dll, the delete will fail, which is fine.
301             String os = getOSIdentifier();
302             if (os.equals("windows")) {
303                 tryDeleteOldLibrariesFromTempDir(tmpdirFile, tempSharedLibPrefix, libraryName);
304             }
305 
306             // open a stream to read the shared lib contents from this JAR
307             String libResourcePath = "/" + os + "/" + getArchIdentifier() + "/" +  getCRuntime(os) + "/" + libraryName;
308             // Check whether there is a platform specific resource path to use
309             CrtPlatform platform = getPlatformImpl();
310             if (platform != null){
311                 String platformLibResourcePath = platform.getResourcePath(getCRuntime(os), libraryName);
312                 if (platformLibResourcePath != null){
313                     libResourcePath = platformLibResourcePath;
314                 }
315             }
316 
317             try (InputStream in = CRT.class.getResourceAsStream(libResourcePath)) {
318                 if (in == null) {
319                     throw new IOException("Unable to open library in jar for AWS CRT: " + libResourcePath);
320                 }
321 
322                 // Copy from jar stream to temp file
323                 try (FileOutputStream out = new FileOutputStream(tempSharedLib)) {
324                     int read;
325                     byte [] bytes = new byte[1024];
326                     while ((read = in.read(bytes)) != -1){
327                         out.write(bytes, 0, read);
328                     }
329                 }
330             }
331 
332             if (!tempSharedLib.setWritable(false)) {
333                 throw new CrtRuntimeException("Unable to make shared library read-only");
334             }
335 
336             // load the shared lib from the temp path
337             System.load(tempSharedLib.getAbsolutePath());
338         } catch (CrtRuntimeException crtex) {
339             System.err.println("Unable to initialize AWS CRT: " + crtex);
340             crtex.printStackTrace();
341             throw crtex;
342         } catch (UnknownPlatformException upe) {
343             System.err.println("Unable to determine platform for AWS CRT: " + upe);
344             upe.printStackTrace();
345             CrtRuntimeException rex = new CrtRuntimeException("Unable to determine platform for AWS CRT");
346             rex.initCause(upe);
347             throw rex;
348         } catch (Exception ex) {
349             System.err.println("Unable to unpack AWS CRT lib: " + ex);
350             ex.printStackTrace();
351             CrtRuntimeException rex = new CrtRuntimeException("Unable to unpack AWS CRT library");
352             rex.initCause(ex);
353             throw rex;
354         }
355     }
356 
loadLibraryFromJar()357     private static void loadLibraryFromJar() {
358         // By default, just try java.io.tmpdir
359         List<String> pathsToTry = new LinkedList<>();
360         pathsToTry.add(System.getProperty("java.io.tmpdir"));
361 
362         // If aws.crt.lib.dir is specified, try that first
363         String overrideLibDir = System.getProperty("aws.crt.lib.dir");
364         if (overrideLibDir != null) {
365             pathsToTry.add(0, overrideLibDir);
366         }
367 
368         List<Exception> exceptions = new LinkedList<>();
369         for (String path : pathsToTry) {
370             try {
371                 extractAndLoadLibrary(path);
372                 return;
373             } catch (CrtRuntimeException ex) {
374                 exceptions.add(ex);
375             }
376         }
377 
378         // Aggregate the exceptions in order and throw a single failure exception
379         StringBuilder failureMessage = new StringBuilder();
380         exceptions.stream().map(Exception::toString).forEach(failureMessage::append);
381         throw new CrtRuntimeException(failureMessage.toString());
382     }
383 
384     // Try to delete old CRT libraries that were extracted to the temp dir by previous runs.
tryDeleteOldLibrariesFromTempDir(File tmpDir, String libNamePrefix, String libNameSuffix)385     private static void tryDeleteOldLibrariesFromTempDir(File tmpDir, String libNamePrefix, String libNameSuffix) {
386         try {
387             File[] oldLibs = tmpDir.listFiles(new FilenameFilter() {
388                 public boolean accept(File dir, String name) {
389                     return name.startsWith(libNamePrefix) && name.endsWith(libNameSuffix);
390                 }
391             });
392 
393             // Don't delete files that are too new.
394             // We don't want to delete another process's lib in the
395             // millisecond between the file being written to disk,
396             // and the file being loaded as a shared lib.
397             long aFewMomentsAgo = System.currentTimeMillis() - 10_000; // 10sec
398             for (File oldLib : oldLibs) {
399                 try {
400                     if (oldLib.lastModified() < aFewMomentsAgo) {
401                         oldLib.delete();
402                     }
403                 } catch (Exception e) {}
404             }
405         } catch (Exception e) {}
406     }
407 
findPlatformImpl()408     private static CrtPlatform findPlatformImpl() {
409         ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
410         String[] platforms = new String[] {
411                 // Search for OS specific test impl first
412                 String.format("software.amazon.awssdk.crt.test.%s.CrtPlatformImpl", getOSIdentifier()),
413                 // Search for android test impl specifically because getOSIdentifier will return "linux" on android
414                 "software.amazon.awssdk.crt.test.android.CrtPlatformImpl",
415                 "software.amazon.awssdk.crt.android.CrtPlatformImpl",
416                 // Fall back to crt
417                 String.format("software.amazon.awssdk.crt.%s.CrtPlatformImpl", getOSIdentifier()), };
418         for (String platformImpl : platforms) {
419             try {
420                 Class<?> platformClass = classLoader.loadClass(platformImpl);
421                 CrtPlatform instance = (CrtPlatform) platformClass.getDeclaredConstructor().newInstance();
422                 return instance;
423             } catch (ClassNotFoundException ex) {
424                 // IGNORED
425             } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException ex) {
426                 throw new CrtRuntimeException(ex.toString());
427             }
428         }
429         return null;
430     }
431 
getPlatformImpl()432     public static CrtPlatform getPlatformImpl() {
433         return s_platform;
434     }
435 
jvmInit()436     private static void jvmInit() {
437         CrtPlatform platform = getPlatformImpl();
438         if (platform != null) {
439             platform.jvmInit();
440         }
441     }
442 
443     private static int shutdownRefCount = 1;
444 
445     /**
446      * Public API that allows a user to indicate interest in controlling the CRT's time of shutdown.  The
447      * shutdown process works via ref-counting, with a default starting count of 1 which is decremented by a
448      * JVM shutdown hook.  Each external call to `acquireShutdownRef()` requires a corresponding call to
449      * `releaseShutdownRef()` when the caller is ready for the CRT to be shut down.  Once all shutdown references
450      * have been released, the CRT will be shut down.
451      *
452      * If the ref count is not properly driven to zero (and thus leaving the CRT active), the JVM may crash
453      * if unmanaged native code in the CRT is still busy and attempts to call back into the JVM after the JVM cleans
454      * up JNI.
455      */
acquireShutdownRef()456     public static void acquireShutdownRef() {
457         synchronized(CRT.class) {
458             if (shutdownRefCount <= 0) {
459                 throw new CrtRuntimeException("Cannot acquire CRT shutdown when ref count is non-positive");
460             }
461             ++shutdownRefCount;
462         }
463     }
464 
465     /**
466      * Public API to release a shutdown reference that blocks CRT shutdown from proceeding.  Must be called once, and
467      * only once, for each call to `acquireShutdownRef()`.  Once all shutdown references have been released (including
468      * the initial reference that is managed by a JVM shutdown hook), the CRT will begin its shutdown process which
469      * permanently severs all native-JVM interactions.
470      */
releaseShutdownRef()471     public static void releaseShutdownRef() {
472         boolean invoke_native_shutdown = false;
473         synchronized(CRT.class) {
474             if (shutdownRefCount <= 0) {
475                 throw new CrtRuntimeException("Cannot release CRT shutdown when ref count is non-positive");
476             }
477 
478             --shutdownRefCount;
479             if (shutdownRefCount == 0) {
480                 invoke_native_shutdown = true;
481             }
482         }
483 
484         if (invoke_native_shutdown) {
485             onJvmShutdown();
486             ClientBootstrap.closeStaticDefault();
487             EventLoopGroup.closeStaticDefault();
488             HostResolver.closeStaticDefault();
489         }
490     }
491 
492     // Called internally when bootstrapping the CRT, allows native code to do any
493     // static initialization it needs
awsCrtInit(int memoryTracingLevel, boolean debugWait, boolean strictShutdown)494     private static native void awsCrtInit(int memoryTracingLevel, boolean debugWait, boolean strictShutdown)
495             throws CrtRuntimeException;
496 
497     /**
498      * Returns the last error on the current thread.
499      *
500      * @return Last error code recorded in this thread
501      */
awsLastError()502     public static native int awsLastError();
503 
504     /**
505      * Given an integer error code from an internal operation
506      *
507      * @param errorCode An error code returned from an exception or other native
508      *                  function call
509      * @return A user-friendly description of the error
510      */
awsErrorString(int errorCode)511     public static native String awsErrorString(int errorCode);
512 
513     /**
514      * Given an integer error code from an internal operation
515      *
516      * @param errorCode An error code returned from an exception or other native
517      *                  function call
518      * @return A string identifier for the error
519      */
awsErrorName(int errorCode)520     public static native String awsErrorName(int errorCode);
521 
522     /**
523      * @return The number of bytes allocated in native resources. If
524      *         aws.crt.memory.tracing is 1 or 2, this will be a non-zero value.
525      *         Otherwise, no tracing will be done, and the value will always be 0
526      */
nativeMemory()527     public static long nativeMemory() {
528         return awsNativeMemory();
529     }
530 
531     /**
532      * Dump info to logs about all memory currently allocated by native resources.
533      * The following system properties must be set to see a dump:
534      * aws.crt.memory.tracing must be 1 or 2
535      * aws.crt.log.level must be "Trace"
536      */
dumpNativeMemory()537     public static native void dumpNativeMemory();
538 
awsNativeMemory()539     private static native long awsNativeMemory();
540 
testJniException(boolean throwException)541     static void testJniException(boolean throwException) {
542         if (throwException) {
543             throw new RuntimeException("Testing");
544         }
545     }
546 
checkJniExceptionContract(boolean clearException)547     public static void checkJniExceptionContract(boolean clearException) {
548         nativeCheckJniExceptionContract(clearException);
549     }
550 
nativeCheckJniExceptionContract(boolean clearException)551     private static native void nativeCheckJniExceptionContract(boolean clearException);
552 
onJvmShutdown()553     private static native void onJvmShutdown();
554 };
555