001/*-------------------------------------------------------------------------- 002 * Copyright 2007 Taro L. Saito 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 *--------------------------------------------------------------------------*/ 016package org.fusesource.jansi.internal; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.FileOutputStream; 021import java.io.FilenameFilter; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.net.URL; 026import java.util.Arrays; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Properties; 030import java.util.Random; 031 032import org.fusesource.jansi.AnsiConsole; 033 034/** 035 * Set the system properties, org.jansi.lib.path, org.jansi.lib.name, 036 * appropriately so that jansi can find *.dll, *.jnilib and 037 * *.so files, according to the current OS (win, linux, mac). 038 * <p> 039 * The library files are automatically extracted from this project's package 040 * (JAR). 041 * <p> 042 * usage: call {@link #initialize()} before using Jansi. 043 */ 044public class JansiLoader { 045 046 private static boolean loaded = false; 047 private static String nativeLibraryPath; 048 private static String nativeLibrarySourceUrl; 049 050 /** 051 * Loads Jansi native library. 052 * 053 * @return True if jansi native library is successfully loaded; false 054 * otherwise. 055 */ 056 public static synchronized boolean initialize() { 057 // only cleanup before the first extract 058 if (!loaded) { 059 cleanup(); 060 } 061 try { 062 loadJansiNativeLibrary(); 063 } catch (Exception e) { 064 if (!Boolean.parseBoolean(System.getProperty(AnsiConsole.JANSI_GRACEFUL, "true"))) { 065 throw new RuntimeException("Unable to load jansi native library. You may want set the `jansi.graceful` system property to true to be able to use Jansi on your platform", e); 066 } 067 } 068 return loaded; 069 } 070 071 public static String getNativeLibraryPath() { 072 return nativeLibraryPath; 073 } 074 075 public static String getNativeLibrarySourceUrl() { 076 return nativeLibrarySourceUrl; 077 } 078 079 private static File getTempDir() { 080 return new File(System.getProperty("jansi.tmpdir", System.getProperty("java.io.tmpdir"))); 081 } 082 083 /** 084 * Deleted old native libraries e.g. on Windows the DLL file is not removed 085 * on VM-Exit (bug #80) 086 */ 087 static void cleanup() { 088 String tempFolder = getTempDir().getAbsolutePath(); 089 File dir = new File(tempFolder); 090 091 File[] nativeLibFiles = dir.listFiles(new FilenameFilter() { 092 private final String searchPattern = "jansi-" + getVersion(); 093 094 public boolean accept(File dir, String name) { 095 return name.startsWith(searchPattern) && !name.endsWith(".lck"); 096 } 097 }); 098 if (nativeLibFiles != null) { 099 for (File nativeLibFile : nativeLibFiles) { 100 File lckFile = new File(nativeLibFile.getAbsolutePath() + ".lck"); 101 if (!lckFile.exists()) { 102 try { 103 nativeLibFile.delete(); 104 } catch (SecurityException e) { 105 System.err.println("Failed to delete old native lib" + e.getMessage()); 106 } 107 } 108 } 109 } 110 } 111 112 private static int readNBytes(InputStream in, byte[] b) throws IOException { 113 int n = 0; 114 int len = b.length; 115 while (n < len) { 116 int count = in.read(b, n, len - n); 117 if (count <= 0) 118 break; 119 n += count; 120 } 121 return n; 122 } 123 124 private static String contentsEquals(InputStream in1, InputStream in2) throws IOException { 125 byte[] buffer1 = new byte[8192]; 126 byte[] buffer2 = new byte[8192]; 127 int numRead1; 128 int numRead2; 129 while (true) { 130 numRead1 = readNBytes(in1, buffer1); 131 numRead2 = readNBytes(in2, buffer2); 132 if (numRead1 > 0) { 133 if (numRead2 <= 0) { 134 return "EOF on second stream but not first"; 135 } 136 if (numRead2 != numRead1) { 137 return "Read size different (" + numRead1 + " vs " + numRead2 + ")"; 138 } 139 // Otherwise same number of bytes read 140 if (!Arrays.equals(buffer1, buffer2)) { 141 return "Content differs"; 142 } 143 // Otherwise same bytes read, so continue ... 144 } else { 145 // Nothing more in stream 1 ... 146 if (numRead2 > 0) { 147 return "EOF on first stream but not second"; 148 } else { 149 return null; 150 } 151 } 152 } 153 } 154 155 /** 156 * Extracts and loads the specified library file to the target folder 157 * 158 * @param libFolderForCurrentOS Library path. 159 * @param libraryFileName Library name. 160 * @param targetFolder Target folder. 161 * @return 162 */ 163 private static boolean extractAndLoadLibraryFile(String libFolderForCurrentOS, String libraryFileName, 164 String targetFolder) { 165 String nativeLibraryFilePath = libFolderForCurrentOS + "/" + libraryFileName; 166 // Include architecture name in temporary filename in order to avoid conflicts 167 // when multiple JVMs with different architectures running at the same time 168 String uuid = randomUUID(); 169 String extractedLibFileName = String.format("jansi-%s-%s-%s", getVersion(), uuid, libraryFileName); 170 String extractedLckFileName = extractedLibFileName + ".lck"; 171 172 File extractedLibFile = new File(targetFolder, extractedLibFileName); 173 File extractedLckFile = new File(targetFolder, extractedLckFileName); 174 175 try { 176 // Extract a native library file into the target directory 177 try (InputStream in = JansiLoader.class.getResourceAsStream(nativeLibraryFilePath)) { 178 if (!extractedLckFile.exists()) { 179 new FileOutputStream(extractedLckFile).close(); 180 } 181 try (OutputStream out = new FileOutputStream(extractedLibFile)) { 182 copy(in, out); 183 } 184 } finally { 185 // Delete the extracted lib file on JVM exit. 186 extractedLibFile.deleteOnExit(); 187 extractedLckFile.deleteOnExit(); 188 } 189 190 // Set executable (x) flag to enable Java to load the native library 191 extractedLibFile.setReadable(true); 192 extractedLibFile.setWritable(true); 193 extractedLibFile.setExecutable(true); 194 195 // Check whether the contents are properly copied from the resource folder 196 try (InputStream nativeIn = JansiLoader.class.getResourceAsStream(nativeLibraryFilePath)) { 197 try (InputStream extractedLibIn = new FileInputStream(extractedLibFile)) { 198 String eq = contentsEquals(nativeIn, extractedLibIn); 199 if (eq != null) { 200 throw new RuntimeException(String.format("Failed to write a native library file at %s because %s", extractedLibFile, eq)); 201 } 202 } 203 } 204 205 // Load library 206 if (loadNativeLibrary(extractedLibFile)) { 207 nativeLibrarySourceUrl = JansiLoader.class.getResource(nativeLibraryFilePath).toExternalForm(); 208 return true; 209 } 210 } catch (IOException e) { 211 System.err.println(e.getMessage()); 212 } 213 return false; 214 } 215 216 private static String randomUUID() { 217 return Long.toHexString(new Random().nextLong()); 218 } 219 220 private static void copy(InputStream in, OutputStream out) throws IOException { 221 byte[] buf = new byte[8192]; 222 int n; 223 while ((n = in.read(buf)) > 0) { 224 out.write(buf, 0, n); 225 } 226 } 227 228 /** 229 * Loads native library using the given path and name of the library. 230 * 231 * @param libPath Path of the native library. 232 * @return True for successfully loading; false otherwise. 233 */ 234 private static boolean loadNativeLibrary(File libPath) { 235 if (libPath.exists()) { 236 try { 237 String path = libPath.getAbsolutePath(); 238 System.load(path); 239 nativeLibraryPath = path; 240 return true; 241 } catch (UnsatisfiedLinkError e) { 242 if (!libPath.canExecute()) { 243 // NOTE: this can be tested using something like: 244 // docker run --rm --tmpfs /tmp -v $PWD:/jansi openjdk:11 java -jar /jansi/target/jansi-xxx-SNAPSHOT.jar 245 System.err.printf("Failed to load native library:%s. The native library file at %s is not executable, " 246 + "make sure that the directory is mounted on a partition without the noexec flag, or set the " 247 + "jansi.tmpdir system property to point to a proper location. osinfo: %s%n", 248 libPath.getName(), libPath, OSInfo.getNativeLibFolderPathForCurrentOS()); 249 } else { 250 System.err.printf("Failed to load native library:%s. osinfo: %s%n", 251 libPath.getName(), OSInfo.getNativeLibFolderPathForCurrentOS()); 252 } 253 System.err.println(e); 254 return false; 255 } 256 257 } else { 258 return false; 259 } 260 } 261 262 /** 263 * Loads jansi library using given path and name of the library. 264 * 265 * @throws 266 */ 267 private static void loadJansiNativeLibrary() throws Exception { 268 if (loaded) { 269 return; 270 } 271 272 List<String> triedPaths = new LinkedList<String>(); 273 274 // Try loading library from library.jansi.path library path */ 275 String jansiNativeLibraryPath = System.getProperty("library.jansi.path"); 276 String jansiNativeLibraryName = System.getProperty("library.jansi.name"); 277 if (jansiNativeLibraryName == null) { 278 jansiNativeLibraryName = System.mapLibraryName("jansi"); 279 assert jansiNativeLibraryName != null; 280 if (jansiNativeLibraryName.endsWith(".dylib")) { 281 jansiNativeLibraryName = jansiNativeLibraryName.replace(".dylib", ".jnilib"); 282 } 283 } 284 285 if (jansiNativeLibraryPath != null) { 286 String withOs = jansiNativeLibraryPath + "/" + OSInfo.getNativeLibFolderPathForCurrentOS(); 287 if (loadNativeLibrary(new File(withOs, jansiNativeLibraryName))) { 288 loaded = true; 289 return; 290 } else { 291 triedPaths.add(withOs); 292 } 293 294 if (loadNativeLibrary(new File(jansiNativeLibraryPath, jansiNativeLibraryName))) { 295 loaded = true; 296 return; 297 } else { 298 triedPaths.add(jansiNativeLibraryPath); 299 } 300 } 301 302 // Load the os-dependent library from the jar file 303 String packagePath = JansiLoader.class.getPackage().getName().replace('.', '/'); 304 jansiNativeLibraryPath = String.format("/%s/native/%s", packagePath, OSInfo.getNativeLibFolderPathForCurrentOS()); 305 boolean hasNativeLib = hasResource(jansiNativeLibraryPath + "/" + jansiNativeLibraryName); 306 307 308 if (hasNativeLib) { 309 // temporary library folder 310 String tempFolder = getTempDir().getAbsolutePath(); 311 // Try extracting the library from jar 312 if (extractAndLoadLibraryFile(jansiNativeLibraryPath, jansiNativeLibraryName, tempFolder)) { 313 loaded = true; 314 return; 315 } else { 316 triedPaths.add(jansiNativeLibraryPath); 317 } 318 } 319 320 // As a last resort try from java.library.path 321 String javaLibraryPath = System.getProperty("java.library.path", ""); 322 for (String ldPath : javaLibraryPath.split(File.pathSeparator)) { 323 if (ldPath.isEmpty()) { 324 continue; 325 } 326 if (loadNativeLibrary(new File(ldPath, jansiNativeLibraryName))) { 327 loaded = true; 328 return; 329 } else { 330 triedPaths.add(ldPath); 331 } 332 } 333 334 throw new Exception(String.format("No native library found for os.name=%s, os.arch=%s, paths=[%s]", 335 OSInfo.getOSName(), OSInfo.getArchName(), join(triedPaths, File.pathSeparator))); 336 } 337 338 private static boolean hasResource(String path) { 339 return JansiLoader.class.getResource(path) != null; 340 } 341 342 343 /** 344 * @return The major version of the jansi library. 345 */ 346 public static int getMajorVersion() { 347 String[] c = getVersion().split("\\."); 348 return (c.length > 0) ? Integer.parseInt(c[0]) : 1; 349 } 350 351 /** 352 * @return The minor version of the jansi library. 353 */ 354 public static int getMinorVersion() { 355 String[] c = getVersion().split("\\."); 356 return (c.length > 1) ? Integer.parseInt(c[1]) : 0; 357 } 358 359 /** 360 * @return The version of the jansi library. 361 */ 362 public static String getVersion() { 363 364 URL versionFile = JansiLoader.class.getResource("/org/fusesource/jansi/jansi.properties"); 365 366 String version = "unknown"; 367 try { 368 if (versionFile != null) { 369 Properties versionData = new Properties(); 370 versionData.load(versionFile.openStream()); 371 version = versionData.getProperty("version", version); 372 version = version.trim().replaceAll("[^0-9.]", ""); 373 } 374 } catch (IOException e) { 375 System.err.println(e); 376 } 377 return version; 378 } 379 380 private static String join(List<String> list, String separator) { 381 StringBuilder sb = new StringBuilder(); 382 boolean first = true; 383 for (String item : list) { 384 if (first) 385 first = false; 386 else 387 sb.append(separator); 388 389 sb.append(item); 390 } 391 return sb.toString(); 392 } 393 394}