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