Better native library loading
This commit is contained in:
parent
0605d57a6e
commit
1c6e54b38f
@ -2,7 +2,7 @@ package it.tdlight.example;
|
|||||||
|
|
||||||
import it.tdlight.Init;
|
import it.tdlight.Init;
|
||||||
import it.tdlight.TelegramClient;
|
import it.tdlight.TelegramClient;
|
||||||
import it.tdlight.utils.CantLoadLibrary;
|
import it.tdlight.util.CantLoadLibrary;
|
||||||
import it.tdlight.jni.TdApi;
|
import it.tdlight.jni.TdApi;
|
||||||
import it.tdlight.ClientFactory;
|
import it.tdlight.ClientFactory;
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import it.tdlight.Init;
|
|||||||
import it.tdlight.jni.TdApi.AuthorizationState;
|
import it.tdlight.jni.TdApi.AuthorizationState;
|
||||||
import it.tdlight.jni.TdApi.Chat;
|
import it.tdlight.jni.TdApi.Chat;
|
||||||
import it.tdlight.jni.TdApi.MessageContent;
|
import it.tdlight.jni.TdApi.MessageContent;
|
||||||
import it.tdlight.utils.CantLoadLibrary;
|
import it.tdlight.util.CantLoadLibrary;
|
||||||
import it.tdlight.jni.TdApi;
|
import it.tdlight.jni.TdApi;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package it.tdlight.utils;
|
package it.tdlight.util;
|
||||||
|
|
||||||
public final class LibraryVersion {
|
public final class LibraryVersion {
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
package it.tdlight;
|
package it.tdlight;
|
||||||
|
|
||||||
import it.tdlight.utils.IntSwapper;
|
import it.tdlight.util.IntSwapper;
|
||||||
|
|
||||||
class ArrayUtil {
|
class ArrayUtil {
|
||||||
|
|
||||||
|
@ -2,9 +2,9 @@ package it.tdlight;
|
|||||||
|
|
||||||
import it.tdlight.jni.TdApi;
|
import it.tdlight.jni.TdApi;
|
||||||
import it.tdlight.jni.TdApi.Object;
|
import it.tdlight.jni.TdApi.Object;
|
||||||
import it.tdlight.utils.CantLoadLibrary;
|
import it.tdlight.util.UnsupportedNativeLibraryException;
|
||||||
import it.tdlight.utils.CleanSupport;
|
import it.tdlight.util.CleanSupport;
|
||||||
import it.tdlight.utils.CleanSupport.CleanableSupport;
|
import it.tdlight.util.CleanSupport.CleanableSupport;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -52,8 +52,8 @@ public class ClientFactory implements AutoCloseable {
|
|||||||
|
|
||||||
public ClientFactory() {
|
public ClientFactory() {
|
||||||
try {
|
try {
|
||||||
Init.start();
|
Init.init();
|
||||||
} catch (CantLoadLibrary e) {
|
} catch (UnsupportedNativeLibraryException e) {
|
||||||
throw new RuntimeException("Can't load the client factory because TDLight can't be loaded", e);
|
throw new RuntimeException("Can't load the client factory because TDLight can't be loaded", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,7 +69,7 @@ public class ClientFactory implements AutoCloseable {
|
|||||||
public void startIfNeeded() {
|
public void startIfNeeded() {
|
||||||
if (state.shouldStartNow()) {
|
if (state.shouldStartNow()) {
|
||||||
try {
|
try {
|
||||||
Init.start();
|
Init.init();
|
||||||
responseReceiver.start();
|
responseReceiver.start();
|
||||||
this.cleanable = CleanSupport.register(responseReceiver, () -> {
|
this.cleanable = CleanSupport.register(responseReceiver, () -> {
|
||||||
try {
|
try {
|
||||||
|
@ -23,7 +23,6 @@ import java.util.HashMap;
|
|||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identify the class by using the Constructor.
|
* Identify the class by using the Constructor.
|
||||||
@ -37,7 +36,7 @@ public final class ConstructorDetector {
|
|||||||
private static void tryInit() {
|
private static void tryInit() {
|
||||||
// Call this to load static methods and prevent errors during startup!
|
// Call this to load static methods and prevent errors during startup!
|
||||||
try {
|
try {
|
||||||
Init.start();
|
Init.init();
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
throwable.printStackTrace();
|
throwable.printStackTrace();
|
||||||
}
|
}
|
||||||
@ -58,7 +57,7 @@ public final class ConstructorDetector {
|
|||||||
/**
|
/**
|
||||||
* Identify the class.
|
* Identify the class.
|
||||||
*
|
*
|
||||||
* @param CONSTRUCTOR CONSTRUCTOR of the Tdlib API.
|
* @param CONSTRUCTOR CONSTRUCTOR of the TDLight API.
|
||||||
* @return The class related to CONSTRUCTOR.
|
* @return The class related to CONSTRUCTOR.
|
||||||
*/
|
*/
|
||||||
public static Class getClass(int CONSTRUCTOR) {
|
public static Class getClass(int CONSTRUCTOR) {
|
||||||
@ -69,7 +68,7 @@ public final class ConstructorDetector {
|
|||||||
/**
|
/**
|
||||||
* Identify the class.
|
* Identify the class.
|
||||||
*
|
*
|
||||||
* @param clazz class of the TDLib API.
|
* @param clazz class of the TDLight API.
|
||||||
* @return The CONSTRUCTOR.
|
* @return The CONSTRUCTOR.
|
||||||
*/
|
*/
|
||||||
public static int getConstructor(Class<? extends TdApi.Object> clazz) {
|
public static int getConstructor(Class<? extends TdApi.Object> clazz) {
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
package it.tdlight;
|
package it.tdlight;
|
||||||
|
|
||||||
import it.tdlight.utils.CantLoadLibrary;
|
import it.tdlight.util.UnsupportedNativeLibraryException;
|
||||||
import it.tdlight.utils.LoadLibrary;
|
import it.tdlight.util.Native;
|
||||||
import it.tdlight.jni.TdApi.LogStreamEmpty;
|
import it.tdlight.jni.TdApi.LogStreamEmpty;
|
||||||
import it.tdlight.jni.TdApi.SetLogStream;
|
import it.tdlight.jni.TdApi.SetLogStream;
|
||||||
import it.tdlight.jni.TdApi.SetLogVerbosityLevel;
|
import it.tdlight.jni.TdApi.SetLogVerbosityLevel;
|
||||||
@ -26,7 +26,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init class to successfully initialize Tdlib
|
* Initialize TDLight
|
||||||
*/
|
*/
|
||||||
public final class Init {
|
public final class Init {
|
||||||
|
|
||||||
@ -35,15 +35,16 @@ public final class Init {
|
|||||||
private static volatile boolean started = false;
|
private static volatile boolean started = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Tdlib
|
* Initialize TDLight.
|
||||||
|
* This method is idempotent.
|
||||||
*
|
*
|
||||||
* @throws CantLoadLibrary An exception that is thrown when the LoadLibrary class fails to load the library.
|
* @throws UnsupportedNativeLibraryException An exception that is thrown when the LoadLibrary class fails to load the library.
|
||||||
*/
|
*/
|
||||||
public static void start() throws CantLoadLibrary {
|
public static void init() throws UnsupportedNativeLibraryException {
|
||||||
if (!started) {
|
if (!started) {
|
||||||
synchronized (Init.class) {
|
synchronized (Init.class) {
|
||||||
if (!started) {
|
if (!started) {
|
||||||
LoadLibrary.load("tdjni");
|
Native.loadNativesInternal();
|
||||||
ConstructorDetector.init();
|
ConstructorDetector.init();
|
||||||
try {
|
try {
|
||||||
NativeClientAccess.execute(new SetLogVerbosityLevel(3));
|
NativeClientAccess.execute(new SetLogVerbosityLevel(3));
|
||||||
|
@ -15,7 +15,7 @@ public final class Log {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
Init.start();
|
Init.init();
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
throwable.printStackTrace();
|
throwable.printStackTrace();
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
|
@ -3,8 +3,8 @@ package it.tdlight;
|
|||||||
import static java.util.stream.Collectors.toList;
|
import static java.util.stream.Collectors.toList;
|
||||||
import static java.util.stream.Collectors.toSet;
|
import static java.util.stream.Collectors.toSet;
|
||||||
|
|
||||||
import it.tdlight.utils.IntSwapper;
|
import it.tdlight.util.IntSwapper;
|
||||||
import it.tdlight.utils.SpinWaitSupport;
|
import it.tdlight.util.SpinWaitSupport;
|
||||||
import it.tdlight.jni.TdApi;
|
import it.tdlight.jni.TdApi;
|
||||||
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -16,7 +16,6 @@ import java.util.Set;
|
|||||||
import java.util.StringJoiner;
|
import java.util.StringJoiner;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
abstract class ResponseReceiver extends Thread implements AutoCloseable {
|
abstract class ResponseReceiver extends Thread implements AutoCloseable {
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
package it.tdlight.client;
|
package it.tdlight.client;
|
||||||
|
|
||||||
import it.tdlight.utils.ScannerUtils;
|
import it.tdlight.util.ScannerUtils;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
public final class ConsoleInteractiveAuthenticationData implements AuthenticationSupplier<AuthenticationData> {
|
public final class ConsoleInteractiveAuthenticationData implements AuthenticationSupplier<AuthenticationData> {
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package it.tdlight.client;
|
package it.tdlight.client;
|
||||||
|
|
||||||
import it.tdlight.utils.ScannerUtils;
|
import it.tdlight.util.ScannerUtils;
|
||||||
import it.tdlight.jni.TdApi.TermsOfService;
|
import it.tdlight.jni.TdApi.TermsOfService;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
@ -6,7 +6,7 @@ import it.tdlight.Init;
|
|||||||
import it.tdlight.ResultHandler;
|
import it.tdlight.ResultHandler;
|
||||||
import it.tdlight.TelegramClient;
|
import it.tdlight.TelegramClient;
|
||||||
import it.tdlight.jni.TdApi.Update;
|
import it.tdlight.jni.TdApi.Update;
|
||||||
import it.tdlight.utils.CantLoadLibrary;
|
import it.tdlight.util.UnsupportedNativeLibraryException;
|
||||||
import it.tdlight.jni.TdApi;
|
import it.tdlight.jni.TdApi;
|
||||||
import it.tdlight.jni.TdApi.ChatListArchive;
|
import it.tdlight.jni.TdApi.ChatListArchive;
|
||||||
import it.tdlight.jni.TdApi.ChatListMain;
|
import it.tdlight.jni.TdApi.ChatListMain;
|
||||||
@ -21,9 +21,7 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -34,8 +32,8 @@ public final class SimpleTelegramClient implements Authenticable, MutableTelegra
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
Init.start();
|
Init.init();
|
||||||
} catch (CantLoadLibrary e) {
|
} catch (UnsupportedNativeLibraryException e) {
|
||||||
throw new RuntimeException("Can't load native libraries", e);
|
throw new RuntimeException("Can't load native libraries", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package it.tdlight.client;
|
package it.tdlight.client;
|
||||||
|
|
||||||
import it.tdlight.utils.LibraryVersion;
|
import it.tdlight.util.LibraryVersion;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package it.tdlight.utils;
|
package it.tdlight.util;
|
||||||
|
|
||||||
public class CleanSupport {
|
public class CleanSupport {
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package it.tdlight.utils;
|
package it.tdlight.util;
|
||||||
|
|
||||||
public final class IntSwapper {
|
public final class IntSwapper {
|
||||||
|
|
166
tdlight-java/src/main/java/it/tdlight/util/Native.java
Normal file
166
tdlight-java/src/main/java/it/tdlight/util/Native.java
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018. Ernesto Castellotti <erny.castell@gmail.com>
|
||||||
|
* This file is part of JTdlib.
|
||||||
|
*
|
||||||
|
* JTdlib is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License.
|
||||||
|
*
|
||||||
|
* JTdlib is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with JTdlib. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package it.tdlight.util;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class to load the libraries needed to run Tdlib
|
||||||
|
*/
|
||||||
|
public final class Native {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal util
|
||||||
|
*/
|
||||||
|
public static void loadNativesInternal() throws UnsupportedNativeLibraryException {
|
||||||
|
loadLibrary("tdlight");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(Native.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a native library
|
||||||
|
* @param libraryName Library name
|
||||||
|
* @throws UnsupportedNativeLibraryException The library can't be loaded
|
||||||
|
*/
|
||||||
|
private static void loadLibrary(String libraryName) throws UnsupportedNativeLibraryException {
|
||||||
|
ClassLoader cl = Native.class.getClassLoader();
|
||||||
|
String staticLibName = libraryName;
|
||||||
|
List<String> sharedLibNames = getNormalizedArchitectures().map(suffix -> staticLibName + "." + suffix).collect(Collectors.toList());
|
||||||
|
if (sharedLibNames.isEmpty()) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
NativeLibraryLoader.loadFirstAvailable(cl, sharedLibNames.toArray(new String[0]));
|
||||||
|
} catch (IllegalArgumentException | UnsatisfiedLinkError e1) {
|
||||||
|
try {
|
||||||
|
NativeLibraryLoader.load(staticLibName, cl);
|
||||||
|
logger.debug("Failed to load {}", String.join(", ", sharedLibNames), e1);
|
||||||
|
} catch (UnsatisfiedLinkError e2) {
|
||||||
|
e1.addSuppressed(e2);
|
||||||
|
throw new UnsupportedNativeLibraryException(e1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<String> getNormalizedArchitectures() {
|
||||||
|
String os = getOs();
|
||||||
|
String arch = getCpuArch();
|
||||||
|
if (os.equals("unknown") || arch.equals("unknown")) {
|
||||||
|
return getAllNormalizedArchitectures();
|
||||||
|
}
|
||||||
|
return getNormalizedArchitectures(os, arch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<String> getAllNormalizedArchitectures() {
|
||||||
|
Set<String> all = new LinkedHashSet<>();
|
||||||
|
for (String os : new String[]{"windows"}) {
|
||||||
|
for (String arch : new String[]{"arm64", "amd64", "armhf", "i386", "s390x", "ppc64le"}) {
|
||||||
|
getNormalizedArchitectures(os, arch).forEach(all::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<String> getNormalizedArchitectures(String os, String arch) {
|
||||||
|
switch (os) {
|
||||||
|
case "linux": {
|
||||||
|
return Stream.of("linux-" + arch + "-ssl1", "linux-" + arch + "-ssl3");
|
||||||
|
}
|
||||||
|
case "windows": {
|
||||||
|
return Stream.of("windows-" + arch);
|
||||||
|
}
|
||||||
|
case "osx": {
|
||||||
|
return Stream.of("osx-" + arch);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getCpuArch() {
|
||||||
|
String architecture = System.getProperty("os.arch").trim();
|
||||||
|
switch (architecture) {
|
||||||
|
case "amd64":
|
||||||
|
case "x86_64":
|
||||||
|
return "amd64";
|
||||||
|
case "i386":
|
||||||
|
case "x86":
|
||||||
|
case "386":
|
||||||
|
case "i686":
|
||||||
|
case "686":
|
||||||
|
return "i386";
|
||||||
|
case "armv6":
|
||||||
|
case "arm":
|
||||||
|
case "armhf":
|
||||||
|
case "aarch32":
|
||||||
|
case "armv7":
|
||||||
|
case "armv7l":
|
||||||
|
return "armhf";
|
||||||
|
case "arm64":
|
||||||
|
case "aarch64":
|
||||||
|
case "armv8":
|
||||||
|
case "armv8l":
|
||||||
|
return "arm64";
|
||||||
|
case "s390x":
|
||||||
|
return "s390x";
|
||||||
|
case "powerpc":
|
||||||
|
case "powerpc64":
|
||||||
|
case "powerpc64le":
|
||||||
|
case "powerpc64el":
|
||||||
|
case "ppc":
|
||||||
|
case "ppc64":
|
||||||
|
case "ppc64le":
|
||||||
|
case "ppc64el":
|
||||||
|
if (ByteOrder
|
||||||
|
.nativeOrder()
|
||||||
|
.equals(ByteOrder.LITTLE_ENDIAN)) // Java always returns ppc64 for all 64-bit powerpc but
|
||||||
|
{
|
||||||
|
return "ppc64le"; // powerpc64le (our target) is very different, it uses this condition to accurately identify the architecture
|
||||||
|
} else {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getOs() {
|
||||||
|
String os = System.getProperty("os.name").toLowerCase().trim();
|
||||||
|
if (os.contains("linux")) {
|
||||||
|
return "linux";
|
||||||
|
}
|
||||||
|
if (os.contains("windows")) {
|
||||||
|
return "windows";
|
||||||
|
}
|
||||||
|
if (os.contains("mac")) {
|
||||||
|
return "osx";
|
||||||
|
}
|
||||||
|
if (os.contains("darwin")) {
|
||||||
|
return "osx";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,598 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 The Netty Project
|
||||||
|
*
|
||||||
|
* The Netty Project licenses this file to you under the Apache License,
|
||||||
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package it.tdlight.util;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to load JNI resources.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public final class NativeLibraryLoader {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(NativeLibraryLoader.class);
|
||||||
|
|
||||||
|
private static final String NATIVE_RESOURCE_HOME = "META-INF/tdlight-native/";
|
||||||
|
private static final Path WORKDIR;
|
||||||
|
private static final boolean DELETE_NATIVE_LIB_AFTER_LOADING;
|
||||||
|
private static final boolean TRY_TO_PATCH_SHADED_ID;
|
||||||
|
private static final boolean DETECT_NATIVE_LIBRARY_DUPLICATES;
|
||||||
|
|
||||||
|
// Just use a-Z and numbers as valid ID bytes.
|
||||||
|
private static final byte[] UNIQUE_ID_BYTES =
|
||||||
|
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
|
||||||
|
static {
|
||||||
|
String workdir = System.getProperty("it.tdlight.native.workdir");
|
||||||
|
if (workdir != null) {
|
||||||
|
Path f = Paths.get(workdir);
|
||||||
|
try {
|
||||||
|
if (Files.notExists(f)) {
|
||||||
|
f = Files.createDirectories(f);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
f = f.toAbsolutePath();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Good to have an absolute path, but it's OK.
|
||||||
|
}
|
||||||
|
|
||||||
|
WORKDIR = f;
|
||||||
|
logger.debug("-Dit.tdlight.native.workdir: " + WORKDIR);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
WORKDIR = Files.createTempDirectory("tdlight-java-natives");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
logger.debug("-Dit.tdlight.native.workdir: " + WORKDIR + " (it.tdlight.tmpdir)");
|
||||||
|
}
|
||||||
|
|
||||||
|
DELETE_NATIVE_LIB_AFTER_LOADING = getBoolean(
|
||||||
|
"it.tdlight.native.deleteLibAfterLoading", true);
|
||||||
|
logger.debug("-Dit.tdlight.native.deleteLibAfterLoading: {}", DELETE_NATIVE_LIB_AFTER_LOADING);
|
||||||
|
|
||||||
|
TRY_TO_PATCH_SHADED_ID = getBoolean(
|
||||||
|
"it.tdlight.native.tryPatchShadedId", true);
|
||||||
|
logger.debug("-Dit.tdlight.native.tryPatchShadedId: {}", TRY_TO_PATCH_SHADED_ID);
|
||||||
|
|
||||||
|
DETECT_NATIVE_LIBRARY_DUPLICATES = getBoolean(
|
||||||
|
"it.tdlight.native.detectNativeLibraryDuplicates", true);
|
||||||
|
logger.debug("-Dit.tdlight.native.detectNativeLibraryDuplicates: {}", DETECT_NATIVE_LIBRARY_DUPLICATES);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean getBoolean(String prop, boolean defaultValue) {
|
||||||
|
String value = System.getProperty(prop);
|
||||||
|
return value == null ? defaultValue : Boolean.parseBoolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the first available library in the collection with the specified
|
||||||
|
* {@link ClassLoader}.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException
|
||||||
|
* if none of the given libraries load successfully.
|
||||||
|
*/
|
||||||
|
public static void loadFirstAvailable(ClassLoader loader, String... names) {
|
||||||
|
List<Throwable> suppressed = new ArrayList<Throwable>();
|
||||||
|
for (String name : names) {
|
||||||
|
try {
|
||||||
|
load(name, loader);
|
||||||
|
logger.debug("Loaded library with name '{}'", name);
|
||||||
|
return;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
suppressed.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IllegalArgumentException iae =
|
||||||
|
new IllegalArgumentException("Failed to load any of the given libraries: " + Arrays.toString(names));
|
||||||
|
addSuppressedAndClear(iae, suppressed);
|
||||||
|
throw iae;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the mangled shading prefix added to this class's full name.
|
||||||
|
*
|
||||||
|
* <p>This method mangles the package name as follows, so we can unmangle it back later:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code _} to {@code _1}</li>
|
||||||
|
* <li>{@code .} to {@code _}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Note that we don't mangle non-ASCII characters here because it's extremely unlikely to have
|
||||||
|
* a non-ASCII character in a package name. For more information, see:
|
||||||
|
* <ul>
|
||||||
|
* <li><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html">JNI
|
||||||
|
* specification</a></li>
|
||||||
|
* <li>{@code parsePackagePrefix()} in {@code netty_jni_util.c}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @throws UnsatisfiedLinkError if the shader used something other than a prefix
|
||||||
|
*/
|
||||||
|
private static String calculateMangledPackagePrefix() {
|
||||||
|
String maybeShaded = NativeLibraryLoader.class.getName();
|
||||||
|
// Use ! instead of . to avoid shading utilities from modifying the string
|
||||||
|
String expected = "it!tdlight!util!internal!NativeLibraryLoader".replace('!', '.');
|
||||||
|
if (!maybeShaded.endsWith(expected)) {
|
||||||
|
throw new UnsatisfiedLinkError(String.format(
|
||||||
|
"Could not find prefix added to %s to get %s. When shading, only adding a "
|
||||||
|
+ "package prefix is supported", expected, maybeShaded));
|
||||||
|
}
|
||||||
|
return maybeShaded.substring(0, maybeShaded.length() - expected.length())
|
||||||
|
.replace("_", "_1")
|
||||||
|
.replace('.', '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the given library with the specified {@link ClassLoader}
|
||||||
|
*/
|
||||||
|
public static void load(String originalName, ClassLoader loader) {
|
||||||
|
String mangledPackagePrefix = calculateMangledPackagePrefix();
|
||||||
|
String name = mangledPackagePrefix + originalName;
|
||||||
|
List<Throwable> suppressed = new ArrayList<Throwable>();
|
||||||
|
try {
|
||||||
|
// first try to load from java.library.path
|
||||||
|
loadLibrary(loader, name, false);
|
||||||
|
return;
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
suppressed.add(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
String libname = System.mapLibraryName(name);
|
||||||
|
String path = NATIVE_RESOURCE_HOME + libname;
|
||||||
|
|
||||||
|
InputStream in = null;
|
||||||
|
OutputStream out = null;
|
||||||
|
Path tmpFile = null;
|
||||||
|
URL url = getResource(path, loader);
|
||||||
|
try {
|
||||||
|
if (url == null) {
|
||||||
|
if (isOsx()) {
|
||||||
|
String fileName = path.endsWith(".jnilib") ? NATIVE_RESOURCE_HOME + "lib" + name + ".dylib" :
|
||||||
|
NATIVE_RESOURCE_HOME + "lib" + name + ".jnilib";
|
||||||
|
url = getResource(fileName, loader);
|
||||||
|
if (url == null) {
|
||||||
|
FileNotFoundException fnf = new FileNotFoundException(fileName);
|
||||||
|
addSuppressedAndClear(fnf, suppressed);
|
||||||
|
throw fnf;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FileNotFoundException fnf = new FileNotFoundException(path);
|
||||||
|
addSuppressedAndClear(fnf, suppressed);
|
||||||
|
throw fnf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = libname.lastIndexOf('.');
|
||||||
|
String prefix = libname.substring(0, index);
|
||||||
|
String suffix = libname.substring(index);
|
||||||
|
|
||||||
|
tmpFile = createTempFile(prefix, suffix, WORKDIR);
|
||||||
|
in = url.openStream();
|
||||||
|
out = Files.newOutputStream(tmpFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||||
|
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int length;
|
||||||
|
while ((length = in.read(buffer)) > 0) {
|
||||||
|
out.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
if (shouldShadedLibraryIdBePatched(mangledPackagePrefix)) {
|
||||||
|
// Let's try to patch the id and re-sign it. This is a best-effort and might fail if a
|
||||||
|
// SecurityManager is setup or the right executables are not installed :/
|
||||||
|
tryPatchShadedLibraryIdAndSign(tmpFile, originalName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the output stream before loading the unpacked library,
|
||||||
|
// because otherwise Windows will refuse to load it when it's in use by other process.
|
||||||
|
closeQuietly(out);
|
||||||
|
out = null;
|
||||||
|
|
||||||
|
loadLibrary(loader, tmpFile.toString(), true);
|
||||||
|
} catch (UnsatisfiedLinkError e) {
|
||||||
|
try {
|
||||||
|
if (tmpFile != null && Files.isRegularFile(tmpFile) && Files.isReadable(tmpFile) &&
|
||||||
|
!NoexecVolumeDetector.canExecuteExecutable(tmpFile)) {
|
||||||
|
// Pass "it.tdlight.native.workdir" as an argument to allow shading tools to see
|
||||||
|
// the string. Since this is printed out to users to tell them what to do next,
|
||||||
|
// we want the value to be correct even when shading.
|
||||||
|
logger.info("{} exists but cannot be executed even when execute permissions set; " +
|
||||||
|
"check volume for \"noexec\" flag; use -D{}=[path] " +
|
||||||
|
"to set native working directory separately.",
|
||||||
|
tmpFile, "it.tdlight.native.workdir");
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
suppressed.add(t);
|
||||||
|
logger.debug("Error checking if {} is on a file store mounted with noexec", tmpFile, t);
|
||||||
|
}
|
||||||
|
// Re-throw to fail the load
|
||||||
|
addSuppressedAndClear(e, suppressed);
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
UnsatisfiedLinkError ule = new UnsatisfiedLinkError("could not load a native library: " + name);
|
||||||
|
ule.initCause(e);
|
||||||
|
addSuppressedAndClear(ule, suppressed);
|
||||||
|
throw ule;
|
||||||
|
} finally {
|
||||||
|
closeQuietly(in);
|
||||||
|
closeQuietly(out);
|
||||||
|
// After we load the library it is safe to delete the file.
|
||||||
|
// We delete the file immediately to free up resources as soon as possible,
|
||||||
|
// and if this fails fallback to deleting on JVM exit.
|
||||||
|
try {
|
||||||
|
if (tmpFile != null && (!DELETE_NATIVE_LIB_AFTER_LOADING || !Files.deleteIfExists(tmpFile))) {
|
||||||
|
tmpFile.toFile().deleteOnExit();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOsx() {
|
||||||
|
return Native.getOs().equals("osx");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path createTempFile(String prefix, String suffix, Path workdir) {
|
||||||
|
try {
|
||||||
|
return Files.createTempFile(workdir, prefix, suffix);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addSuppressedAndClear(Throwable ex, List<Throwable> suppressed) {
|
||||||
|
suppressed.forEach(ex::addSuppressed);
|
||||||
|
suppressed.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static URL getResource(String path, ClassLoader loader) {
|
||||||
|
final Enumeration<URL> urls;
|
||||||
|
try {
|
||||||
|
if (loader == null) {
|
||||||
|
urls = ClassLoader.getSystemResources(path);
|
||||||
|
} else {
|
||||||
|
urls = loader.getResources(path);
|
||||||
|
}
|
||||||
|
} catch (IOException iox) {
|
||||||
|
throw new RuntimeException("An error occurred while getting the resources for " + path, iox);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<URL> urlsList = Collections.list(urls);
|
||||||
|
int size = urlsList.size();
|
||||||
|
switch (size) {
|
||||||
|
case 0:
|
||||||
|
return null;
|
||||||
|
case 1:
|
||||||
|
return urlsList.get(0);
|
||||||
|
default:
|
||||||
|
if (DETECT_NATIVE_LIBRARY_DUPLICATES) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
// We found more than 1 resource with the same name. Let's check if the content of the file is
|
||||||
|
// the same as in this case it will not have any bad effect.
|
||||||
|
URL url = urlsList.get(0);
|
||||||
|
byte[] digest = digest(md, url);
|
||||||
|
boolean allSame = true;
|
||||||
|
if (digest != null) {
|
||||||
|
for (int i = 1; i < size; i++) {
|
||||||
|
byte[] digest2 = digest(md, urlsList.get(i));
|
||||||
|
if (digest2 == null || !Arrays.equals(digest, digest2)) {
|
||||||
|
allSame = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allSame = false;
|
||||||
|
}
|
||||||
|
if (allSame) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
logger.debug("Don't support SHA-256, can't check if resources have same content.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Multiple resources found for '" + path + "' with different content: " + urlsList);
|
||||||
|
} else {
|
||||||
|
logger.warn("Multiple resources found for '" + path + "' with different content: " +
|
||||||
|
urlsList + ". Please fix your dependency graph.");
|
||||||
|
return urlsList.get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] digest(MessageDigest digest, URL url) {
|
||||||
|
InputStream in = null;
|
||||||
|
try {
|
||||||
|
in = url.openStream();
|
||||||
|
byte[] bytes = new byte[8192];
|
||||||
|
int i;
|
||||||
|
while ((i = in.read(bytes)) != -1) {
|
||||||
|
digest.update(bytes, 0, i);
|
||||||
|
}
|
||||||
|
return digest.digest();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.debug("Can't read resource.", e);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
closeQuietly(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tryPatchShadedLibraryIdAndSign(Path libraryFile, String originalName) {
|
||||||
|
if (Files.notExists(Paths.get("/Library/Developer/CommandLineTools"))) {
|
||||||
|
logger.debug("Can't patch shaded library id as CommandLineTools are not installed." +
|
||||||
|
" Consider installing CommandLineTools with 'xcode-select --install'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String newId = new String(generateUniqueId(originalName.length()), StandardCharsets.UTF_8);
|
||||||
|
if (!tryExec("install_name_tool -id " + newId + " " + libraryFile.toAbsolutePath())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryExec("codesign -s - " + libraryFile.toAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean tryExec(String cmd) {
|
||||||
|
try {
|
||||||
|
int exitValue = Runtime.getRuntime().exec(cmd).waitFor();
|
||||||
|
if (exitValue != 0) {
|
||||||
|
logger.debug("Execution of '{}' failed: {}", cmd, exitValue);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
logger.debug("Execution of '{}' succeed: {}", cmd, exitValue);
|
||||||
|
return true;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.info("Execution of '{}' failed.", cmd, e);
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
logger.error("Execution of '{}' failed.", cmd, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldShadedLibraryIdBePatched(String packagePrefix) {
|
||||||
|
return TRY_TO_PATCH_SHADED_ID && isOsx() && !packagePrefix.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateUniqueId(int length) {
|
||||||
|
byte[] idBytes = new byte[length];
|
||||||
|
for (int i = 0; i < idBytes.length; i++) {
|
||||||
|
// We should only use bytes as replacement that are in our UNIQUE_ID_BYTES array.
|
||||||
|
idBytes[i] = UNIQUE_ID_BYTES[ThreadLocalRandom.current()
|
||||||
|
.nextInt(UNIQUE_ID_BYTES.length)];
|
||||||
|
}
|
||||||
|
return idBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading the native library into the specified {@link ClassLoader}.
|
||||||
|
* @param loader - The {@link ClassLoader} where the native library will be loaded into
|
||||||
|
* @param name - The native library path or name
|
||||||
|
* @param absolute - Whether the native library will be loaded by path or by name
|
||||||
|
*/
|
||||||
|
private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) {
|
||||||
|
Throwable suppressed = null;
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
// Make sure the helper belongs to the target ClassLoader.
|
||||||
|
final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class);
|
||||||
|
loadLibraryByHelper(newHelper, name, absolute);
|
||||||
|
logger.debug("Successfully loaded the library {}", name);
|
||||||
|
return;
|
||||||
|
} catch (UnsatisfiedLinkError e) { // Should by pass the UnsatisfiedLinkError here!
|
||||||
|
suppressed = e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
suppressed = e;
|
||||||
|
}
|
||||||
|
NativeLibraryUtil.loadLibrary(name, absolute); // Fallback to local helper class.
|
||||||
|
logger.debug("Successfully loaded the library {}", name);
|
||||||
|
} catch (NoSuchMethodError nsme) {
|
||||||
|
if (suppressed != null) {
|
||||||
|
nsme.addSuppressed(suppressed);
|
||||||
|
}
|
||||||
|
rethrowWithMoreDetailsIfPossible(name, nsme);
|
||||||
|
} catch (UnsatisfiedLinkError ule) {
|
||||||
|
if (suppressed != null) {
|
||||||
|
ule.addSuppressed(suppressed);
|
||||||
|
}
|
||||||
|
throw ule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void rethrowWithMoreDetailsIfPossible(String name, NoSuchMethodError error) {
|
||||||
|
throw new LinkageError(
|
||||||
|
"Possible multiple incompatible native libraries on the classpath for '" + name + "'?", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void loadLibraryByHelper(final Class<?> helper, final String name, final boolean absolute)
|
||||||
|
throws UnsatisfiedLinkError {
|
||||||
|
|
||||||
|
Object ret;
|
||||||
|
try {
|
||||||
|
// Invoke the helper to load the native library, if succeed, then the native
|
||||||
|
// library belong to the specified ClassLoader.
|
||||||
|
Method method = helper.getMethod("loadLibrary", String.class, boolean.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
ret = method.invoke(null, name, absolute);
|
||||||
|
} catch (Exception e) {
|
||||||
|
ret = e;
|
||||||
|
}
|
||||||
|
if (ret instanceof Throwable) {
|
||||||
|
Throwable t = (Throwable) ret;
|
||||||
|
assert !(t instanceof UnsatisfiedLinkError) : t + " should be a wrapper throwable";
|
||||||
|
Throwable cause = t.getCause();
|
||||||
|
if (cause instanceof UnsatisfiedLinkError) {
|
||||||
|
throw (UnsatisfiedLinkError) cause;
|
||||||
|
}
|
||||||
|
UnsatisfiedLinkError ule = new UnsatisfiedLinkError(t.getMessage());
|
||||||
|
ule.initCause(t);
|
||||||
|
throw ule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to load the helper {@link Class} into specified {@link ClassLoader}.
|
||||||
|
* @param loader - The {@link ClassLoader} where to load the helper {@link Class}
|
||||||
|
* @param helper - The helper {@link Class}
|
||||||
|
* @return A new helper Class defined in the specified ClassLoader.
|
||||||
|
* @throws ClassNotFoundException Helper class not found or loading failed
|
||||||
|
*/
|
||||||
|
private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper)
|
||||||
|
throws ClassNotFoundException {
|
||||||
|
try {
|
||||||
|
return Class.forName(helper.getName(), false, loader);
|
||||||
|
} catch (ClassNotFoundException e1) {
|
||||||
|
if (loader == null) {
|
||||||
|
// cannot defineClass inside bootstrap class loader
|
||||||
|
throw e1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// The helper class is NOT found in target ClassLoader, we have to define the helper class.
|
||||||
|
final byte[] classBinary = classToByteArray(helper);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Define the helper class in the target ClassLoader,
|
||||||
|
// then we can call the helper to load the native library.
|
||||||
|
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
|
||||||
|
byte[].class, int.class, int.class);
|
||||||
|
defineClass.setAccessible(true);
|
||||||
|
return (Class<?>) defineClass.invoke(loader, helper.getName(), classBinary, 0,
|
||||||
|
classBinary.length);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Define class failed!", e);
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException | RuntimeException | Error e2) {
|
||||||
|
e2.addSuppressed(e1);
|
||||||
|
throw e2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the helper {@link Class} as a byte array, to be redefined in specified {@link ClassLoader}.
|
||||||
|
* @param clazz - The helper {@link Class} provided by this bundle
|
||||||
|
* @return The binary content of helper {@link Class}.
|
||||||
|
* @throws ClassNotFoundException Helper class not found or loading failed
|
||||||
|
*/
|
||||||
|
private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException {
|
||||||
|
String fileName = clazz.getName();
|
||||||
|
int lastDot = fileName.lastIndexOf('.');
|
||||||
|
if (lastDot > 0) {
|
||||||
|
fileName = fileName.substring(lastDot + 1);
|
||||||
|
}
|
||||||
|
URL classUrl = clazz.getResource(fileName + ".class");
|
||||||
|
if (classUrl == null) {
|
||||||
|
throw new ClassNotFoundException(clazz.getName());
|
||||||
|
}
|
||||||
|
byte[] buf = new byte[1024];
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
|
||||||
|
InputStream in = null;
|
||||||
|
try {
|
||||||
|
in = classUrl.openStream();
|
||||||
|
for (int r; (r = in.read(buf)) != -1;) {
|
||||||
|
out.write(buf, 0, r);
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new ClassNotFoundException(clazz.getName(), ex);
|
||||||
|
} finally {
|
||||||
|
closeQuietly(in);
|
||||||
|
closeQuietly(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void closeQuietly(Closeable c) {
|
||||||
|
if (c != null) {
|
||||||
|
try {
|
||||||
|
c.close();
|
||||||
|
} catch (IOException ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeLibraryLoader() {
|
||||||
|
// Utility
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class NoexecVolumeDetector {
|
||||||
|
|
||||||
|
private static boolean canExecuteExecutable(Path file) throws IOException {
|
||||||
|
// If we can already execute, there is nothing to do.
|
||||||
|
if (Files.isExecutable(file)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On volumes, with noexec set, even files with the executable POSIX permissions will fail to execute.
|
||||||
|
// The File#canExecute() method honors this behavior, probaby via parsing the noexec flag when initializing
|
||||||
|
// the UnixFileStore, though the flag is not exposed via a public API. To find out if library is being
|
||||||
|
// loaded off a volume with noexec, confirm or add executalbe permissions, then check File#canExecute().
|
||||||
|
|
||||||
|
// Note: We use FQCN to not break when netty is used in java6
|
||||||
|
Set<java.nio.file.attribute.PosixFilePermission> existingFilePermissions =
|
||||||
|
java.nio.file.Files.getPosixFilePermissions(file);
|
||||||
|
Set<java.nio.file.attribute.PosixFilePermission> executePermissions =
|
||||||
|
EnumSet.of(java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE,
|
||||||
|
java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE,
|
||||||
|
java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE);
|
||||||
|
if (existingFilePermissions.containsAll(executePermissions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<java.nio.file.attribute.PosixFilePermission> newPermissions = EnumSet.copyOf(existingFilePermissions);
|
||||||
|
newPermissions.addAll(executePermissions);
|
||||||
|
java.nio.file.Files.setPosixFilePermissions(file, newPermissions);
|
||||||
|
return Files.isExecutable(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NoexecVolumeDetector() {
|
||||||
|
// Utility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package it.tdlight.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Utility to Call the {@link System#load(String)} or {@link System#loadLibrary(String)}.
|
||||||
|
* Because the {@link System#load(String)} and {@link System#loadLibrary(String)} are both
|
||||||
|
* CallerSensitive, it will load the native library into its caller's {@link ClassLoader}.
|
||||||
|
* In OSGi environment, we need this helper to delegate the calling to {@link System#load(String)}
|
||||||
|
* and it should be as simple as possible. It will be injected into the native library's
|
||||||
|
* ClassLoader when it is undefined. And therefore, when the defined new helper is invoked,
|
||||||
|
* the native library would be loaded into the native library's ClassLoader, not the
|
||||||
|
* caller's ClassLoader.
|
||||||
|
*/
|
||||||
|
final class NativeLibraryUtil {
|
||||||
|
/**
|
||||||
|
* Delegate the calling to {@link System#load(String)} or {@link System#loadLibrary(String)}.
|
||||||
|
* @param libName - The native library path or name
|
||||||
|
* @param absolute - Whether the native library will be loaded by path or by name
|
||||||
|
*/
|
||||||
|
public static void loadLibrary(String libName, boolean absolute) {
|
||||||
|
if (absolute) {
|
||||||
|
System.load(libName);
|
||||||
|
} else {
|
||||||
|
System.loadLibrary(libName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeLibraryUtil() {
|
||||||
|
// Utility
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package it.tdlight.utils;
|
package it.tdlight.util;
|
||||||
|
|
||||||
import java.io.Console;
|
import java.io.Console;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
@ -1,4 +1,4 @@
|
|||||||
package it.tdlight.utils;
|
package it.tdlight.util;
|
||||||
|
|
||||||
import java.util.concurrent.locks.LockSupport;
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package it.tdlight.utils;
|
package it.tdlight.util;
|
||||||
|
|
||||||
|
|
||||||
import reactor.blockhound.BlockHound.Builder;
|
import reactor.blockhound.BlockHound.Builder;
|
@ -15,29 +15,29 @@
|
|||||||
* along with JTdlib. If not, see <http://www.gnu.org/licenses/>.
|
* along with JTdlib. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package it.tdlight.utils;
|
package it.tdlight.util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An exception that is thrown when the LoadLibrary class fails to load the library.
|
* An exception that is thrown when a native library can't be loaded.
|
||||||
*/
|
*/
|
||||||
public final class CantLoadLibrary extends Exception {
|
public final class UnsupportedNativeLibraryException extends Exception {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new CantLoadLibrary exception.
|
* Creates a new UnsupportedNativeLibraryException.
|
||||||
*/
|
*/
|
||||||
CantLoadLibrary() {
|
UnsupportedNativeLibraryException() {
|
||||||
super("Init failed when loading TDLib native libraries, execution can't continue");
|
super("Failed to load TDLight native libraries");
|
||||||
}
|
}
|
||||||
|
|
||||||
public CantLoadLibrary(String message) {
|
public UnsupportedNativeLibraryException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CantLoadLibrary(String message, Exception cause) {
|
public UnsupportedNativeLibraryException(String message, Exception cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CantLoadLibrary(Throwable cause) {
|
public UnsupportedNativeLibraryException(Throwable cause) {
|
||||||
super(cause);
|
super("Failed to load TDLight native libraries", cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,239 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2018. Ernesto Castellotti <erny.castell@gmail.com>
|
|
||||||
* This file is part of JTdlib.
|
|
||||||
*
|
|
||||||
* JTdlib is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License.
|
|
||||||
*
|
|
||||||
* JTdlib is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public License
|
|
||||||
* along with JTdlib. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package it.tdlight.utils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The class to load the libraries needed to run Tdlib
|
|
||||||
*/
|
|
||||||
public final class LoadLibrary {
|
|
||||||
|
|
||||||
private static final Set<String> LIBRARY_LOADED = new ConcurrentHashMap<String, Boolean>().keySet(true);
|
|
||||||
private static final String LIBS_VERSION =
|
|
||||||
LibraryVersion.IMPLEMENTATION_NAME + "-" + LibraryVersion.VERSION + "-" + LibraryVersion.NATIVES_VERSION;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a library installed in the system (priority choice) or a library included in the jar.
|
|
||||||
*
|
|
||||||
* @param libname The name of the library.
|
|
||||||
* @throws CantLoadLibrary An exception that is thrown when the LoadLibrary class fails to load the library.
|
|
||||||
*/
|
|
||||||
public static void load(String libname) throws CantLoadLibrary {
|
|
||||||
if (libname == null || libname.trim().isEmpty()) {
|
|
||||||
throw new IllegalArgumentException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LIBRARY_LOADED.contains(libname)) return;
|
|
||||||
synchronized (LoadLibrary.class) {
|
|
||||||
if (LIBRARY_LOADED.contains(libname)) return;
|
|
||||||
|
|
||||||
String libraryCachePathString = System.getProperty("it.tdlight.libraryCachePath");
|
|
||||||
Path libraryCachePath = libraryCachePathString != null ? Paths.get(libraryCachePathString) : null;
|
|
||||||
loadLibrary(libname, libraryCachePath);
|
|
||||||
LIBRARY_LOADED.add(libname);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a native library
|
|
||||||
* @param libraryName Library name
|
|
||||||
* @param libraryCachePath optional, path in which the library will be extracted
|
|
||||||
* @throws CantLoadLibrary The library can't be loaded
|
|
||||||
*/
|
|
||||||
private static void loadLibrary(String libraryName, Path libraryCachePath) throws CantLoadLibrary {
|
|
||||||
if (libraryCachePath == null) {
|
|
||||||
libraryCachePath = Paths.get(System.getProperty("user.home")).resolve(".cache").resolve("tdlight-jni-cache");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
loadJarLibrary(libraryName, libraryCachePath);
|
|
||||||
} catch (CantLoadLibrary | UnsatisfiedLinkError e) {
|
|
||||||
if (loadSysLibrary(libraryName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new CantLoadLibrary(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean loadSysLibrary(String libname) {
|
|
||||||
try {
|
|
||||||
System.loadLibrary(libname);
|
|
||||||
} catch (UnsatisfiedLinkError e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void loadJarLibrary(String libraryName, Path libraryCachePath) throws CantLoadLibrary {
|
|
||||||
Path tempPath;
|
|
||||||
try {
|
|
||||||
tempPath = libraryCachePath.resolve("version-" + LIBS_VERSION).resolve(libraryName);
|
|
||||||
if (Files.notExists(tempPath)) {
|
|
||||||
tempPath = Files.createDirectories(tempPath);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new CantLoadLibrary("Can't create temporary files", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClassLoader classForResource = LoadLibrary.class.getClassLoader();
|
|
||||||
List<String> normalizedArchs = getNormalizedArchitectures().collect(Collectors.toList());
|
|
||||||
Exception lastEx = null;
|
|
||||||
loadAny: for (String normalizedArch : normalizedArchs) {
|
|
||||||
Path tempFile = tempPath.resolve(libraryName + "." + normalizedArch);
|
|
||||||
InputStream libInputStream;
|
|
||||||
try {
|
|
||||||
libInputStream = Objects.requireNonNull(classForResource.getResourceAsStream("META-INF/tdlight-jni/lib" + libraryName + "." + normalizedArch));
|
|
||||||
if (Files.notExists(tempFile)) {
|
|
||||||
try {
|
|
||||||
Files.copy(libInputStream, tempFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new CantLoadLibrary("Can't copy native libraries into temporary files", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
libInputStream.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new CantLoadLibrary("Can't load the native libraries", e);
|
|
||||||
}
|
|
||||||
System.load(tempFile.toAbsolutePath().toString());
|
|
||||||
lastEx = null;
|
|
||||||
break loadAny;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
lastEx = new CantLoadLibrary(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lastEx != null) {
|
|
||||||
throw new CantLoadLibrary("Native libraries for platforms "
|
|
||||||
+ String.join(", ", normalizedArchs) + " not found!", lastEx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Stream<String> getNormalizedArchitectures() {
|
|
||||||
String os = getOs();
|
|
||||||
String arch = getCpuArch();
|
|
||||||
if (os.equals("unknown") || arch.equals("unknown")) {
|
|
||||||
return getAllNormalizedArchitectures();
|
|
||||||
}
|
|
||||||
return getNormalizedArchitectures(os, arch);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Stream<String> getAllNormalizedArchitectures() {
|
|
||||||
Set<String> all = new LinkedHashSet<>();
|
|
||||||
for (String os : new String[]{"windows"}) {
|
|
||||||
for (String arch : new String[]{"arm64", "amd64", "armhf", "i386", "s390x", "ppc64le"}) {
|
|
||||||
getNormalizedArchitectures(os, arch).forEach(all::add);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return all.stream();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Stream<String> getNormalizedArchitectures(String os, String arch) {
|
|
||||||
switch (os) {
|
|
||||||
case "linux": {
|
|
||||||
return Stream.of("linux-" + arch + "-ssl1.so", "linux-" + arch + "-ssl3.so");
|
|
||||||
}
|
|
||||||
case "windows": {
|
|
||||||
return Stream.of("windows-" + arch + ".dll");
|
|
||||||
}
|
|
||||||
case "osx": {
|
|
||||||
return Stream.of("osx-" + arch + ".dylib");
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getCpuArch() {
|
|
||||||
String architecture = System.getProperty("os.arch").trim();
|
|
||||||
switch (architecture) {
|
|
||||||
case "amd64":
|
|
||||||
case "x86_64":
|
|
||||||
return "amd64";
|
|
||||||
case "i386":
|
|
||||||
case "x86":
|
|
||||||
case "386":
|
|
||||||
case "i686":
|
|
||||||
case "686":
|
|
||||||
return "i386";
|
|
||||||
case "armv6":
|
|
||||||
case "arm":
|
|
||||||
case "armhf":
|
|
||||||
case "aarch32":
|
|
||||||
case "armv7":
|
|
||||||
case "armv7l":
|
|
||||||
return "armhf";
|
|
||||||
case "arm64":
|
|
||||||
case "aarch64":
|
|
||||||
case "armv8":
|
|
||||||
case "armv8l":
|
|
||||||
return "arm64";
|
|
||||||
case "s390x":
|
|
||||||
return "s390x";
|
|
||||||
case "powerpc":
|
|
||||||
case "powerpc64":
|
|
||||||
case "powerpc64le":
|
|
||||||
case "powerpc64el":
|
|
||||||
case "ppc":
|
|
||||||
case "ppc64":
|
|
||||||
case "ppc64le":
|
|
||||||
case "ppc64el":
|
|
||||||
if (ByteOrder
|
|
||||||
.nativeOrder()
|
|
||||||
.equals(ByteOrder.LITTLE_ENDIAN)) // Java always returns ppc64 for all 64-bit powerpc but
|
|
||||||
{
|
|
||||||
return "ppc64le"; // powerpc64le (our target) is very different, it uses this condition to accurately identify the architecture
|
|
||||||
} else {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getOs() {
|
|
||||||
String os = System.getProperty("os.name").toLowerCase().trim();
|
|
||||||
if (os.contains("linux")) {
|
|
||||||
return "linux";
|
|
||||||
}
|
|
||||||
if (os.contains("windows")) {
|
|
||||||
return "windows";
|
|
||||||
}
|
|
||||||
if (os.contains("mac")) {
|
|
||||||
return "osx";
|
|
||||||
}
|
|
||||||
if (os.contains("darwin")) {
|
|
||||||
return "osx";
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,6 +6,6 @@ module tdlight.java {
|
|||||||
requires static reactor.blockhound;
|
requires static reactor.blockhound;
|
||||||
exports it.tdlight.tdnative;
|
exports it.tdlight.tdnative;
|
||||||
exports it.tdlight;
|
exports it.tdlight;
|
||||||
exports it.tdlight.utils;
|
exports it.tdlight.util;
|
||||||
exports it.tdlight.client;
|
exports it.tdlight.client;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package it.tdlight.utils;
|
package it.tdlight.util;
|
||||||
|
|
||||||
import java.lang.ref.Cleaner;
|
import java.lang.ref.Cleaner;
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package it.tdlight.utils;
|
package it.tdlight.util;
|
||||||
|
|
||||||
public class SpinWaitSupport {
|
public class SpinWaitSupport {
|
||||||
|
|
@ -1 +1 @@
|
|||||||
it.tdlight.utils.TDLightBlockHoundIntegration
|
it.tdlight.util.TDLightBlockHoundIntegration
|
||||||
|
Loading…
Reference in New Issue
Block a user