001/******************************************************************************* 002 * Copyright (c) 2024, 2026, Olivier Ayache. All rights reserved. 003 * 004 * This file is part of AVPKit. 005 * 006 * AVPKit is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU Lesser General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * AVPKit is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with AVPKit. If not, see <http://www.gnu.org/licenses/>. 018 *******************************************************************************/ 019package com.avpkit.ferry; 020 021import java.io.File; 022import java.io.FileOutputStream; 023import java.io.FilenameFilter; 024import java.io.IOException; 025import java.io.InputStream; 026import java.net.URI; 027import java.net.URISyntaxException; 028import java.net.URL; 029import java.util.Enumeration; 030import java.util.HashMap; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.Map; 034import java.util.Properties; 035import java.util.jar.Manifest; 036 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040public class JNILibrary implements Comparable<JNILibrary> 041{ 042 private static final Logger log = LoggerFactory 043 .getLogger(JNILibrary.class); 044 045 private static final String XUGGLE_TEMP_EXTENSION = ".avpkit"; 046 047 private static final Map<String, List<JNIManifest>> mManifestLists = new HashMap<String, List<JNIManifest>>(); 048 private static final Map<String, JNILibrary> mAttemptedLibraries = new HashMap<String, JNILibrary>(); 049 050 final static private Object mLock = new Object(); 051 052 private static List<JNIManifest> getNativeManifests(String appName) 053 { 054 final List<JNIManifest> cached = mManifestLists.get(appName); 055 if (cached != null) 056 return cached; 057 058 // well, it's not cached. let's do this the hard way. 059 final List<JNIManifest> retval = new LinkedList<JNIManifest>(); 060 final ClassLoader loader = JNILibrary.class.getClassLoader(); 061 062 // First properties 063 try { 064 final Enumeration<URL> properties = loader.getResources("com/avpkit/ferry/native-contents.properties"); 065 while (properties.hasMoreElements()) { 066 final URL url = properties.nextElement(); 067 log.trace("Examining properties: {}", url); 068 final InputStream stream = url.openStream(); 069 if (stream != null) { 070 final Properties props = new Properties(); 071 props.load(stream); 072 final JNIManifest manifest = JNIManifest.create(url, appName, props); 073 if (manifest != null) { 074 log.trace("found manifest: {}; url: {}", manifest, url); 075 retval.add(manifest); 076 } 077 stream.close(); 078 } 079 } 080 } catch (IOException e) { 081 log.debug("could not open properties: {}", e); 082 } 083 084 // now manifests 085 try { 086 final Enumeration<URL> manifests = loader.getResources("META-INF/MANIFEST.MF"); 087 while (manifests.hasMoreElements()) { 088 final URL url = manifests.nextElement(); 089 log.trace("Examining manifest: {}", url); 090 final InputStream stream = url.openStream(); 091 if (stream != null) { 092 final Manifest jarManifest = new Manifest(stream); 093 if (jarManifest != null) { 094 final JNIManifest manifest = JNIManifest.create(url, appName, jarManifest); 095 if (manifest != null) { 096 log.trace("found manifest: {}; url: {}", manifest, url); 097 retval.add(manifest); 098 } 099 } 100 } 101 } 102 } catch (IOException e) { 103 log.debug("could not open manifest: {}", e); 104 } 105 106 // add to cache 107 mManifestLists.put(appName, retval); 108 return retval; 109 } 110 111 private final String mName; 112 private final Long mVersion; 113 114 private boolean mLoadAttempted; 115 private boolean mLoadSuccessful; 116 117 // a static initializer 118 static { 119 deleteTemporaryFiles(); 120 } 121 122 public JNILibrary(String name, Long version) 123 { 124 if (name == null || name.length() <= 0) 125 throw new IllegalArgumentException("need a valid name"); 126 mName = name; 127 mVersion = version; 128 mLoadAttempted = false; 129 mLoadSuccessful = false; 130 } 131 public String getName() { return mName; } 132 public Long getVersion() { return mVersion; } 133 134 public boolean isLoadAttempted() { return mLoadAttempted; } 135 public boolean isLoadSuccessful() { return mLoadSuccessful; } 136 137 @Override 138 public String toString() { return super.toString()+"[ name=" + mName + "; version=" + mVersion + "; ]"; } 139 140 /** 141 * Load the given library into the given application. 142 * 143 * This method first searches in the classpath for native libraries 144 * that are bundled in there, and only if no matches are found, 145 * will it search the run-time paths of each OS. 146 * 147 * @param appname the name of the application. This should match what shows 148 * up in jar manifests or avpkit native property files. 149 * @param library the library object 150 * @throws UnsatisfiedLinkError if library cannot be loaded. 151 */ 152 @SuppressWarnings("deprecation") 153 public static void load(String appname, JNILibrary library) { 154 // we force ALL work on all libraries to be synchronized 155 synchronized(mLock) { 156 deleteTemporaryFiles(); 157 try { 158 library.load(appname); 159 } catch (UnsatisfiedLinkError e) { 160 // failed; faill back to old way 161 JNILibraryLoader.loadLibrary(library.getName(), 162 library.getVersion()); 163 } 164 } 165 } 166 private void load(String appName) throws UnsatisfiedLinkError, SecurityException 167 { 168 if (mLoadAttempted) { 169 if (mLoadSuccessful) 170 return; 171 else 172 throw new UnsatisfiedLinkError("already attempted and failed to load library: " + getName()); 173 } 174 mLoadAttempted = true; 175 // finally attempt to load ourselves 176 loadFromClasspath(appName); 177 mLoadSuccessful = false; 178 } 179 180 private void loadFromClasspath(String appName) 181 { 182 final JNILibrary priorAttempt = mAttemptedLibraries.get(getName()); 183 if (priorAttempt != null) { 184 if (priorAttempt.mLoadSuccessful) 185 return; 186 else 187 throw new UnsatisfiedLinkError("previously attempted to load library and it failed: " + priorAttempt.getName()); 188 } 189 190 // from the manifests build a list of candidate library names to try 191 final List<String> libraryURLs = generateCandidateLibraryURLs(appName, getName()); 192 193 // finally go through each one until we get a load 194 for(String url : libraryURLs) { 195 if (unpackLibrary(url)) { 196 return; 197 } 198 } 199 // if we get all the way hwere, we did NOT succeed 200 throw new UnsatisfiedLinkError("could not load library: " + getName()); 201 202 } 203 private void doJNILoad(String url) 204 { 205 try { 206 log.trace( 207 "Attempt: library load of library: {}; url: {}", 208 new Object[] 209 { 210 getName(), url 211 }); 212 213 System.load(url); 214 log.trace( 215 "Success: library load of library: {}; url: {}", 216 new Object[] 217 { 218 getName(), url 219 }); 220 221 } catch (UnsatisfiedLinkError e) { 222 log.warn( 223 "Failure: library load of library: {}; url: {}; error: {}", 224 new Object[] 225 { 226 getName(), url, e 227 }); 228 throw e; 229 } catch (SecurityException e) { 230 log.warn( 231 "Failure: library load of library: {}; url: {}; error: {}", 232 new Object[] 233 { 234 getName(), url, e 235 }); 236 throw e; 237 } 238 } 239 240 /** Looks for a URL in a classpath, and if found, unpacks it */ 241 private boolean unpackLibrary(String path) 242 { 243 final URL url = JNILibrary.class.getResource(path); 244 log.trace("path: {}; url: {}", path, url); 245 if (url == null) 246 return false; 247 248 boolean unpacked = false; 249 File lib; 250 if (url.getProtocol().toLowerCase().equals("file")) { 251 // it SHOULD already exist on the disk. let's look for it. 252 try { 253 lib = new File(new URI(url.toString())); 254 } catch (URISyntaxException e) { 255 lib = new File(url.getPath()); 256 } 257 if (!lib.exists()) { 258 log.error("Unpacked library not unpacked correctedly; url: {}", url); 259 return false; 260 } 261 } else { 262 // sucktastic -- we cannot in a JVM load a shared library 263 // directly from a JAR, so we need to unpack to a temp 264 // directory and load from there. 265 InputStream stream = JNILibrary.class.getResourceAsStream(path); 266 if (stream == null) { 267 log.error("could not get stream for resource: {}", path); 268 return false; 269 } 270 FileOutputStream out=null; 271 try { 272 File dir = getTmpDir(); 273 // did you know windows REQUIRES .dll. Sigh. 274 lib = ITempFileCreator.Builder.getDefault().createTempFile("avpkit", JNIEnv.getEnv().getOSFamily() == JNIEnv.OSFamily.WINDOWS ? ".dll" : null, dir); 275 lib.deleteOnExit(); 276 out = new FileOutputStream(lib); 277 int bytesRead = 0; 278 final byte[] buffer = new byte[2048]; 279 while ((bytesRead = stream.read(buffer, 0, buffer.length)) > 0) { 280 out.write(buffer, 0, bytesRead); 281 } 282 unpacked = true; 283 } catch (IOException e) { 284 log.error("could not create temp file: {}", e); 285 return false; 286 } finally { 287 try { stream.close(); } catch (IOException e) {} 288 if (out != null) 289 try { out.close() ; } catch (IOException e) {} 290 } 291 } 292 try { 293 doJNILoad(lib.getAbsolutePath()); 294 } finally { 295 if (unpacked) { 296 // Well let's try to clean up after ourselves since 297 // we had ot unpack. 298 deleteUnpackedFile(lib.getAbsolutePath()); 299 } 300 } 301 return true; 302 } 303 private void deleteUnpackedFile(String absolutePath) 304 { 305 final File file = new File(absolutePath); 306 if (file.delete()) 307 return; 308 // sigh -- we could not delete it. so we put a marker 309 // file along side and delete it the next time the library starts 310 // up. 311 final String markerName = file.getName()+XUGGLE_TEMP_EXTENSION; 312 try { 313 File marker = new File(file.getParentFile(), markerName); 314 marker.createNewFile(); 315 } catch (IOException e) { 316 log.error("could not create marker file: {}; error: {}", markerName, e); 317 // and swallow it. 318 } 319 } 320 private static File getTmpDir() { 321 File tmpdir = new File(System.getProperty("java.io.tmpdir")); 322 File avpkitdir = new File(tmpdir, "avpkit"); 323 avpkitdir.mkdirs(); 324 return avpkitdir.exists() ? avpkitdir : tmpdir; 325 } 326 /** 327 * Finds all ".avpkit" temp files in the temp directory and 328 * nukes them. 329 */ 330 private static void deleteTemporaryFiles() 331 { 332 final File dir = getTmpDir(); 333 final FilenameFilter filter = new FilenameFilter() { 334 public boolean accept(File dir, String name) 335 { 336 return name.endsWith(XUGGLE_TEMP_EXTENSION); 337 } 338 }; 339 final File markers[] = dir.listFiles(filter); 340 for(File marker: markers) 341 { 342 final String markerName = marker.getName(); 343 final String libName = markerName.substring(0, markerName.length() - XUGGLE_TEMP_EXTENSION.length()); 344 final File lib = new File(marker.getParentFile(), libName); 345 if (!lib.exists() || lib.delete()) 346 marker.delete(); 347 } 348 } 349 private List<String> generateCandidateLibraryURLs(String appName, 350 String libname) 351 { 352 final List<String> retval = new LinkedList<String>(); 353 final List<JNIManifest> manifests = getNativeManifests(appName); 354 355 // for each manifest, generate URLs 356 for (final JNIManifest manifest : manifests) { 357 generateLibnames(retval, manifest.getPath(), libname); 358 } 359 // and finally we also test the top of the classpath in the event 360 // that this is an applet or Web-Start app. 361 generateLibnames(retval, "/", libname); 362 363 return retval; 364 } 365 366 private static JNIEnv.OSFamily mOSFamily = JNIEnv.getEnv().getOSFamily(); 367 // These methods are at package visibility to allow testing only 368 // They are not meant for use 369 void setOSFamily(JNIEnv.OSFamily os) { mOSFamily = os; } 370 JNIEnv.OSFamily getOSFamily() { return mOSFamily; } 371 372 private void generateLibnames(List<String> list, 373 String path, 374 String libname) 375 { 376 final String[] prefixes; 377 final String[] suffixes; 378 switch (getOSFamily()) 379 { 380 case UNKNOWN: 381 case LINUX: 382 prefixes = new String[] 383 { 384 "lib", "" 385 }; 386 suffixes = new String[] 387 { 388 ".so",".so."+com.avpkit.core.Version.MAJOR_VERSION 389 }; 390 break; 391 case WINDOWS: 392 prefixes = new String[] 393 { 394 "lib", "", "cyg" 395 }; 396 suffixes = new String[] 397 { 398 "-"+com.avpkit.core.Version.MAJOR_VERSION+".dll", ".dll" 399 }; 400 break; 401 case MAC: 402 prefixes = new String[] 403 { 404 "lib", "" 405 }; 406 suffixes = new String[] 407 { 408 ".dylib" 409 }; 410 break; 411 default: 412 // really no cases should get here 413 prefixes = null; 414 suffixes = null; 415 break; 416 } 417 // can assume URL separators 418 final String dirSeparator = "/"; 419 if (path.length() > 0 && !path.endsWith(dirSeparator)) 420 path = path + dirSeparator; 421 for (String suffix : suffixes) 422 for (String prefix : prefixes) 423 list.add(path + prefix + libname + suffix); 424 } 425 426 public int compareTo(JNILibrary o) 427 { 428 if (o == null) 429 return -1; 430 431 int retval = mName.compareTo(o.mName); 432 if (retval == 0) { 433 if (mVersion == null) { 434 if (o.mVersion != null) { 435 retval = 1; 436 } 437 } else { 438 if (o.mVersion == null) 439 retval = -1; 440 else 441 retval = mVersion.compareTo(o.mVersion); 442 } 443 } 444 return retval; 445 } 446}