View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.util.internal;
17  
18  import io.netty.util.internal.logging.InternalLogger;
19  import io.netty.util.internal.logging.InternalLoggerFactory;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.Closeable;
23  import java.io.File;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.lang.reflect.Method;
30  import java.net.URL;
31  import java.security.AccessController;
32  import java.security.PrivilegedAction;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.EnumSet;
36  import java.util.List;
37  import java.util.Set;
38  
39  /**
40   * Helper class to load JNI resources.
41   *
42   */
43  public final class NativeLibraryLoader {
44  
45      private static final InternalLogger logger = InternalLoggerFactory.getInstance(NativeLibraryLoader.class);
46  
47      private static final String NATIVE_RESOURCE_HOME = "META-INF/native/";
48      private static final File WORKDIR;
49      private static final boolean DELETE_NATIVE_LIB_AFTER_LOADING;
50  
51      static {
52          String workdir = SystemPropertyUtil.get("io.netty.native.workdir");
53          if (workdir != null) {
54              File f = new File(workdir);
55              f.mkdirs();
56  
57              try {
58                  f = f.getAbsoluteFile();
59              } catch (Exception ignored) {
60                  // Good to have an absolute path, but it's OK.
61              }
62  
63              WORKDIR = f;
64              logger.debug("-Dio.netty.native.workdir: " + WORKDIR);
65          } else {
66              WORKDIR = PlatformDependent.tmpdir();
67              logger.debug("-Dio.netty.native.workdir: " + WORKDIR + " (io.netty.tmpdir)");
68          }
69  
70          DELETE_NATIVE_LIB_AFTER_LOADING = SystemPropertyUtil.getBoolean(
71                  "io.netty.native.deleteLibAfterLoading", true);
72      }
73  
74      /**
75       * Loads the first available library in the collection with the specified
76       * {@link ClassLoader}.
77       *
78       * @throws IllegalArgumentException
79       *         if none of the given libraries load successfully.
80       */
81      public static void loadFirstAvailable(ClassLoader loader, String... names) {
82          List<Throwable> suppressed = new ArrayList<Throwable>();
83          for (String name : names) {
84              try {
85                  load(name, loader);
86                  return;
87              } catch (Throwable t) {
88                  suppressed.add(t);
89                  logger.debug("Unable to load the library '{}', trying next name...", name, t);
90              }
91          }
92          IllegalArgumentException iae =
93                  new IllegalArgumentException("Failed to load any of the given libraries: " + Arrays.toString(names));
94          ThrowableUtil.addSuppressedAndClear(iae, suppressed);
95          throw iae;
96      }
97  
98      /**
99       * The shading prefix added to this class's full name.
100      *
101      * @throws UnsatisfiedLinkError if the shader used something other than a prefix
102      */
103     private static String calculatePackagePrefix() {
104         String maybeShaded = NativeLibraryLoader.class.getName();
105         // Use ! instead of . to avoid shading utilities from modifying the string
106         String expected = "io!netty!util!internal!NativeLibraryLoader".replace('!', '.');
107         if (!maybeShaded.endsWith(expected)) {
108             throw new UnsatisfiedLinkError(String.format(
109                     "Could not find prefix added to %s to get %s. When shading, only adding a "
110                     + "package prefix is supported", expected, maybeShaded));
111         }
112         return maybeShaded.substring(0, maybeShaded.length() - expected.length());
113     }
114 
115     /**
116      * Load the given library with the specified {@link ClassLoader}
117      */
118     public static void load(String originalName, ClassLoader loader) {
119         // Adjust expected name to support shading of native libraries.
120         String name = calculatePackagePrefix().replace('.', '_') + originalName;
121         List<Throwable> suppressed = new ArrayList<Throwable>();
122         try {
123             // first try to load from java.library.path
124             loadLibrary(loader, name, false);
125             return;
126         } catch (Throwable ex) {
127             suppressed.add(ex);
128             logger.debug(
129                     "{} cannot be loaded from java.libary.path, "
130                     + "now trying export to -Dio.netty.native.workdir: {}", name, WORKDIR, ex);
131         }
132 
133         String libname = System.mapLibraryName(name);
134         String path = NATIVE_RESOURCE_HOME + libname;
135 
136         InputStream in = null;
137         OutputStream out = null;
138         File tmpFile = null;
139         URL url;
140         if (loader == null) {
141             url = ClassLoader.getSystemResource(path);
142         } else {
143             url = loader.getResource(path);
144         }
145         try {
146             if (url == null) {
147                 if (PlatformDependent.isOsx()) {
148                     String fileName = path.endsWith(".jnilib") ? NATIVE_RESOURCE_HOME + "lib" + name + ".dynlib" :
149                             NATIVE_RESOURCE_HOME + "lib" + name + ".jnilib";
150                     if (loader == null) {
151                         url = ClassLoader.getSystemResource(fileName);
152                     } else {
153                         url = loader.getResource(fileName);
154                     }
155                     if (url == null) {
156                         FileNotFoundException fnf = new FileNotFoundException(fileName);
157                         ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
158                         throw fnf;
159                     }
160                 } else {
161                     FileNotFoundException fnf = new FileNotFoundException(path);
162                     ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
163                     throw fnf;
164                 }
165             }
166 
167             int index = libname.lastIndexOf('.');
168             String prefix = libname.substring(0, index);
169             String suffix = libname.substring(index, libname.length());
170 
171             tmpFile = File.createTempFile(prefix, suffix, WORKDIR);
172             in = url.openStream();
173             out = new FileOutputStream(tmpFile);
174 
175             byte[] buffer = new byte[8192];
176             int length;
177             while ((length = in.read(buffer)) > 0) {
178                 out.write(buffer, 0, length);
179             }
180             out.flush();
181 
182             // Close the output stream before loading the unpacked library,
183             // because otherwise Windows will refuse to load it when it's in use by other process.
184             closeQuietly(out);
185             out = null;
186 
187             loadLibrary(loader, tmpFile.getPath(), true);
188         } catch (UnsatisfiedLinkError e) {
189             try {
190                 if (tmpFile != null && tmpFile.isFile() && tmpFile.canRead() &&
191                     !NoexecVolumeDetector.canExecuteExecutable(tmpFile)) {
192                     logger.info("{} exists but cannot be executed even when execute permissions set; " +
193                                 "check volume for \"noexec\" flag; use -Dio.netty.native.workdir=[path] " +
194                                 "to set native working directory separately.",
195                                 tmpFile.getPath());
196                 }
197             } catch (Throwable t) {
198                 suppressed.add(t);
199                 logger.debug("Error checking if {} is on a file store mounted with noexec", tmpFile, t);
200             }
201             // Re-throw to fail the load
202             ThrowableUtil.addSuppressedAndClear(e, suppressed);
203             throw e;
204         } catch (Exception e) {
205             UnsatisfiedLinkError ule = new UnsatisfiedLinkError("could not load a native library: " + name);
206             ule.initCause(e);
207             ThrowableUtil.addSuppressedAndClear(ule, suppressed);
208             throw ule;
209         } finally {
210             closeQuietly(in);
211             closeQuietly(out);
212             // After we load the library it is safe to delete the file.
213             // We delete the file immediately to free up resources as soon as possible,
214             // and if this fails fallback to deleting on JVM exit.
215             if (tmpFile != null && (!DELETE_NATIVE_LIB_AFTER_LOADING || !tmpFile.delete())) {
216                 tmpFile.deleteOnExit();
217             }
218         }
219     }
220 
221     /**
222      * Loading the native library into the specified {@link ClassLoader}.
223      * @param loader - The {@link ClassLoader} where the native library will be loaded into
224      * @param name - The native library path or name
225      * @param absolute - Whether the native library will be loaded by path or by name
226      */
227     private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) {
228         Throwable suppressed = null;
229         try {
230             try {
231                 // Make sure the helper is belong to the target ClassLoader.
232                 final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class);
233                 loadLibraryByHelper(newHelper, name, absolute);
234                 logger.debug("Successfully loaded the library {}", name);
235                 return;
236             } catch (UnsatisfiedLinkError e) { // Should by pass the UnsatisfiedLinkError here!
237                 suppressed = e;
238                 logger.debug("Unable to load the library '{}', trying other loading mechanism.", name, e);
239             } catch (Exception e) {
240                 suppressed = e;
241                 logger.debug("Unable to load the library '{}', trying other loading mechanism.", name, e);
242             }
243             NativeLibraryUtil.loadLibrary(name, absolute);  // Fallback to local helper class.
244             logger.debug("Successfully loaded the library {}", name);
245         } catch (UnsatisfiedLinkError ule) {
246             if (suppressed != null) {
247                 ThrowableUtil.addSuppressed(ule, suppressed);
248             }
249             throw ule;
250         }
251     }
252 
253     private static void loadLibraryByHelper(final Class<?> helper, final String name, final boolean absolute)
254             throws UnsatisfiedLinkError {
255         Object ret = AccessController.doPrivileged(new PrivilegedAction<Object>() {
256             @Override
257             public Object run() {
258                 try {
259                     // Invoke the helper to load the native library, if succeed, then the native
260                     // library belong to the specified ClassLoader.
261                     Method method = helper.getMethod("loadLibrary", String.class, boolean.class);
262                     method.setAccessible(true);
263                     return method.invoke(null, name, absolute);
264                 } catch (Exception e) {
265                     return e;
266                 }
267             }
268         });
269         if (ret instanceof Throwable) {
270             Throwable t = (Throwable) ret;
271             assert !(t instanceof UnsatisfiedLinkError) : t + " should be a wrapper throwable";
272             Throwable cause = t.getCause();
273             if (cause instanceof UnsatisfiedLinkError) {
274                 throw (UnsatisfiedLinkError) cause;
275             }
276             UnsatisfiedLinkError ule = new UnsatisfiedLinkError(t.getMessage());
277             ule.initCause(t);
278             throw ule;
279         }
280     }
281 
282     /**
283      * Try to load the helper {@link Class} into specified {@link ClassLoader}.
284      * @param loader - The {@link ClassLoader} where to load the helper {@link Class}
285      * @param helper - The helper {@link Class}
286      * @return A new helper Class defined in the specified ClassLoader.
287      * @throws ClassNotFoundException Helper class not found or loading failed
288      */
289     private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper)
290             throws ClassNotFoundException {
291         try {
292             return Class.forName(helper.getName(), false, loader);
293         } catch (ClassNotFoundException e1) {
294             if (loader == null) {
295                 // cannot defineClass inside bootstrap class loader
296                 throw e1;
297             }
298             try {
299                 // The helper class is NOT found in target ClassLoader, we have to define the helper class.
300                 final byte[] classBinary = classToByteArray(helper);
301                 return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
302                     @Override
303                     public Class<?> run() {
304                         try {
305                             // Define the helper class in the target ClassLoader,
306                             //  then we can call the helper to load the native library.
307                             Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
308                                     byte[].class, int.class, int.class);
309                             defineClass.setAccessible(true);
310                             return (Class<?>) defineClass.invoke(loader, helper.getName(), classBinary, 0,
311                                     classBinary.length);
312                         } catch (Exception e) {
313                             throw new IllegalStateException("Define class failed!", e);
314                         }
315                     }
316                 });
317             } catch (ClassNotFoundException e2) {
318                 ThrowableUtil.addSuppressed(e2, e1);
319                 throw e2;
320             } catch (RuntimeException e2) {
321                 ThrowableUtil.addSuppressed(e2, e1);
322                 throw e2;
323             } catch (Error e2) {
324                 ThrowableUtil.addSuppressed(e2, e1);
325                 throw e2;
326             }
327         }
328     }
329 
330     /**
331      * Load the helper {@link Class} as a byte array, to be redefined in specified {@link ClassLoader}.
332      * @param clazz - The helper {@link Class} provided by this bundle
333      * @return The binary content of helper {@link Class}.
334      * @throws ClassNotFoundException Helper class not found or loading failed
335      */
336     private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException {
337         String fileName = clazz.getName();
338         int lastDot = fileName.lastIndexOf('.');
339         if (lastDot > 0) {
340             fileName = fileName.substring(lastDot + 1);
341         }
342         URL classUrl = clazz.getResource(fileName + ".class");
343         if (classUrl == null) {
344             throw new ClassNotFoundException(clazz.getName());
345         }
346         byte[] buf = new byte[1024];
347         ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
348         InputStream in = null;
349         try {
350             in = classUrl.openStream();
351             for (int r; (r = in.read(buf)) != -1;) {
352                 out.write(buf, 0, r);
353             }
354             return out.toByteArray();
355         } catch (IOException ex) {
356             throw new ClassNotFoundException(clazz.getName(), ex);
357         } finally {
358             closeQuietly(in);
359             closeQuietly(out);
360         }
361     }
362 
363     private static void closeQuietly(Closeable c) {
364         if (c != null) {
365             try {
366                 c.close();
367             } catch (IOException ignore) {
368                 // ignore
369             }
370         }
371     }
372 
373     private NativeLibraryLoader() {
374         // Utility
375     }
376 
377     private static final class NoexecVolumeDetector {
378 
379         private static boolean canExecuteExecutable(File file) throws IOException {
380             if (PlatformDependent.javaVersion() < 7) {
381                 // Pre-JDK7, the Java API did not directly support POSIX permissions; instead of implementing a custom
382                 // work-around, assume true, which disables the check.
383                 return true;
384             }
385 
386             // If we can already execute, there is nothing to do.
387             if (file.canExecute()) {
388                 return true;
389             }
390 
391             // On volumes, with noexec set, even files with the executable POSIX permissions will fail to execute.
392             // The File#canExecute() method honors this behavior, probaby via parsing the noexec flag when initializing
393             // the UnixFileStore, though the flag is not exposed via a public API.  To find out if library is being
394             // loaded off a volume with noexec, confirm or add executalbe permissions, then check File#canExecute().
395 
396             // Note: We use FQCN to not break when netty is used in java6
397             Set<java.nio.file.attribute.PosixFilePermission> existingFilePermissions =
398                     java.nio.file.Files.getPosixFilePermissions(file.toPath());
399             Set<java.nio.file.attribute.PosixFilePermission> executePermissions =
400                     EnumSet.of(java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE,
401                             java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE,
402                             java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE);
403             if (existingFilePermissions.containsAll(executePermissions)) {
404                 return false;
405             }
406 
407             Set<java.nio.file.attribute.PosixFilePermission> newPermissions = EnumSet.copyOf(existingFilePermissions);
408             newPermissions.addAll(executePermissions);
409             java.nio.file.Files.setPosixFilePermissions(file.toPath(), newPermissions);
410             return file.canExecute();
411         }
412 
413         private NoexecVolumeDetector() {
414             // Utility
415         }
416     }
417 }