From ad4dcc14a52b4a137b55d9a0f3fa9acfb6c6e441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Gregorczyk?= Date: Thu, 25 Jan 2024 07:21:57 -0800 Subject: [PATCH] introduce Observer interface Summary: This diff adds `Observer` interface, and `ObserverHolder`, a static holder for an array of `Observer`s that dispatches calls to all of the `Obsever`s in the array. SoLoader uses `ObserverHolder` to notify all `Observer`s of library loading progress. Reviewed By: adicatana, danjin250 Differential Revision: D48040740 fbshipit-source-id: 8963a48749670504d71f894436dab6e9728cd4d8 --- java/com/facebook/soloader/BUCK | 1 + java/com/facebook/soloader/NativeDeps.java | 13 +- java/com/facebook/soloader/SoLoader.java | 135 ++++++++++++----- .../facebook/soloader/observer/Observer.java | 43 ++++++ .../soloader/observer/ObserverHolder.java | 136 ++++++++++++++++++ 5 files changed, 291 insertions(+), 37 deletions(-) create mode 100644 java/com/facebook/soloader/observer/Observer.java create mode 100644 java/com/facebook/soloader/observer/ObserverHolder.java diff --git a/java/com/facebook/soloader/BUCK b/java/com/facebook/soloader/BUCK index 55cf140..658ab8f 100644 --- a/java/com/facebook/soloader/BUCK +++ b/java/com/facebook/soloader/BUCK @@ -19,6 +19,7 @@ fb_core_android_library( LOADER_SRCS = glob( [ "*.java", + "observer/*.java", "recovery/*.java", ], exclude = [ diff --git a/java/com/facebook/soloader/NativeDeps.java b/java/com/facebook/soloader/NativeDeps.java index 34199f3..71ad7e0 100644 --- a/java/com/facebook/soloader/NativeDeps.java +++ b/java/com/facebook/soloader/NativeDeps.java @@ -16,8 +16,10 @@ package com.facebook.soloader; +import android.annotation.SuppressLint; import android.content.Context; import android.os.StrictMode; +import com.facebook.soloader.observer.ObserverHolder; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; @@ -66,10 +68,13 @@ public static void loadDependencies( } } + @SuppressLint({"CatchGeneralException", "EmptyCatchBlock"}) public static String[] getDependencies(String soName, ElfByteChannel bc) throws IOException { + @Nullable Throwable failure = null; if (SoLoader.SYSTRACE_LIBRARY_LOADING) { Api18TraceUtils.beginTraceSection("soloader.NativeDeps.getDependencies[", soName, "]"); } + ObserverHolder.onGetDependenciesStart(); try { String[] deps = awaitGetDepsFromPrecomputedDeps(soName); if (deps != null) { @@ -77,8 +82,14 @@ public static String[] getDependencies(String soName, ElfByteChannel bc) throws } return MinElf.extract_DT_NEEDED(bc); } catch (MinElf.ElfError err) { - throw SoLoaderULErrorFactory.create(soName, err); + UnsatisfiedLinkError ule = SoLoaderULErrorFactory.create(soName, err); + failure = ule; + throw ule; + } catch (Error | RuntimeException t) { + failure = t; + throw t; } finally { + ObserverHolder.onGetDependenciesEnd(failure); if (SoLoader.SYSTRACE_LIBRARY_LOADING) { Api18TraceUtils.endSection(); } diff --git a/java/com/facebook/soloader/SoLoader.java b/java/com/facebook/soloader/SoLoader.java index b3b858c..0038392 100644 --- a/java/com/facebook/soloader/SoLoader.java +++ b/java/com/facebook/soloader/SoLoader.java @@ -26,6 +26,7 @@ import android.text.TextUtils; import com.facebook.soloader.nativeloader.NativeLoader; import com.facebook.soloader.nativeloader.SystemDelegate; +import com.facebook.soloader.observer.ObserverHolder; import com.facebook.soloader.recovery.DefaultRecoveryStrategyFactory; import com.facebook.soloader.recovery.RecoveryStrategy; import com.facebook.soloader.recovery.RecoveryStrategyFactory; @@ -616,6 +617,7 @@ static class TestOnlyUtils { sSoFileLoader = null; sApplicationContext = null; sRecoveryStrategyFactory = null; + ObserverHolder.resetObserversForTestsOnly(); } setSoSources(null); } @@ -771,12 +773,24 @@ public static boolean loadLibrary(String shortName, int loadFlags) throws Unsati return true; } - String mergedLibName = MergedSoMapping.mapLibName(shortName); - - String soName = mergedLibName != null ? mergedLibName : shortName; + return loadLibraryOnAndroid(shortName, loadFlags); + } - return loadLibraryBySoName( - System.mapLibraryName(soName), shortName, mergedLibName, loadFlags, null); + @SuppressLint({"CatchGeneralException", "EmptyCatchBlock"}) + private static boolean loadLibraryOnAndroid(String shortName, int loadFlags) { + @Nullable Throwable failure = null; + ObserverHolder.onLoadLibraryStart(shortName, loadFlags); + try { + String mergedLibName = MergedSoMapping.mapLibName(shortName); + String soName = mergedLibName != null ? mergedLibName : shortName; + return loadLibraryBySoName( + System.mapLibraryName(soName), shortName, mergedLibName, loadFlags, null); + } catch (Throwable t) { + failure = t; + throw t; + } finally { + ObserverHolder.onLoadLibraryEnd(failure); + } } private static @Nullable Boolean loadLibraryOnNonAndroid(String shortName) { @@ -821,10 +835,20 @@ public static boolean loadLibrary(String shortName, int loadFlags) throws Unsati * @param loadFlags * @param oldPolicy */ + @SuppressLint({"CatchGeneralException", "EmptyCatchBlock"}) /* package */ static void loadDependency( String soName, int loadFlags, StrictMode.ThreadPolicy oldPolicy) { - loadLibraryBySoNameImpl( - soName, null, null, loadFlags | SoSource.LOAD_FLAG_ALLOW_IMPLICIT_PROVISION, oldPolicy); + @Nullable Throwable failure = null; + ObserverHolder.onLoadDependencyStart(soName, loadFlags); + try { + loadLibraryBySoNameImpl( + soName, null, null, loadFlags | SoSource.LOAD_FLAG_ALLOW_IMPLICIT_PROVISION, oldPolicy); + } catch (Throwable t) { + failure = t; + throw t; + } finally { + ObserverHolder.onLoadDependencyEnd(failure); + } } private static boolean loadLibraryBySoName( @@ -838,37 +862,60 @@ private static boolean loadLibraryBySoName( try { return loadLibraryBySoNameImpl(soName, shortName, mergedLibName, loadFlags, oldPolicy); } catch (UnsatisfiedLinkError e) { - LogUtil.w(TAG, "Starting recovery for " + soName, e); - sSoSourcesLock.writeLock().lock(); - try { - if (recovery == null) { - recovery = getRecoveryStrategy(); - } - if (recovery != null && recovery.recover(e, sSoSources)) { - sSoSourcesVersion.getAndIncrement(); - LogUtil.w(TAG, "Attempting to load library again"); - continue; - } - } catch (NoBaseApkException noBaseApkException) { - // If we failed during recovery, we only want to throw the recovery exception for the case - // when the base APK path does not exist, everything else should preserve the initial - // error. - LogUtil.e(TAG, "Base APK not found during recovery", noBaseApkException); - throw noBaseApkException; - } catch (Exception recoveryException) { - LogUtil.e( - TAG, - "Got an exception during recovery, will throw the initial error instead", - recoveryException); + recovery = recover(soName, e, recovery); + } + } + } + + @SuppressLint("CatchGeneralException") + private static RecoveryStrategy recover( + String soName, UnsatisfiedLinkError e, @Nullable RecoveryStrategy recovery) { + LogUtil.w(TAG, "Starting recovery for " + soName, e); + sSoSourcesLock.writeLock().lock(); + try { + if (recovery == null) { + recovery = getRecoveryStrategy(); + if (recovery == null) { + LogUtil.w(TAG, "No recovery strategy"); throw e; - } finally { - sSoSourcesLock.writeLock().unlock(); } - - // No recovery mechanism worked, throwing initial error - LogUtil.w(TAG, "Failed to recover"); - throw e; } + if (recoverLocked(e, recovery)) { + sSoSourcesVersion.getAndIncrement(); + return recovery; + } + } catch (NoBaseApkException noBaseApkException) { + // If we failed during recovery, we only want to throw the recovery exception for the case + // when the base APK path does not exist, everything else should preserve the initial + // error. + LogUtil.e(TAG, "Base APK not found during recovery", noBaseApkException); + throw noBaseApkException; + } catch (Exception recoveryException) { + LogUtil.e( + TAG, + "Got an exception during recovery, will throw the initial error instead", + recoveryException); + throw e; + } finally { + sSoSourcesLock.writeLock().unlock(); + } + + // No recovery mechanism worked, throwing initial error + LogUtil.w(TAG, "Failed to recover"); + throw e; + } + + @SuppressLint({"CatchGeneralException", "EmptyCatchBlock"}) + private static boolean recoverLocked(UnsatisfiedLinkError e, RecoveryStrategy recovery) { + @Nullable Throwable failure = null; + ObserverHolder.onRecoveryStart(recovery); + try { + return recovery.recover(e, sSoSources); + } catch (Throwable t) { + failure = t; + throw t; + } finally { + ObserverHolder.onRecoveryEnd(failure); } } @@ -1057,7 +1104,7 @@ private static void doLoadLibraryBySoName( sSoSourcesLock.readLock().lock(); try { for (SoSource source : sSoSources) { - if (source.loadLibrary(soName, loadFlags, oldPolicy) != SoSource.LOAD_RESULT_NOT_FOUND) { + if (loadLibraryFromSoSource(source, soName, loadFlags, oldPolicy)) { return; } } @@ -1083,6 +1130,22 @@ private static void doLoadLibraryBySoName( } } + @SuppressLint({"CatchGeneralException", "EmptyCatchBlock", "MissingSoLoaderLibrary"}) + private static boolean loadLibraryFromSoSource( + SoSource source, String name, int loadFlags, StrictMode.ThreadPolicy oldPolicy) + throws IOException { + @Nullable Throwable failure = null; + ObserverHolder.onSoSourceLoadLibraryStart(source); + try { + return source.loadLibrary(name, loadFlags, oldPolicy) != SoSource.LOAD_RESULT_NOT_FOUND; + } catch (Throwable t) { + failure = t; + throw t; + } finally { + ObserverHolder.onSoSourceLoadLibraryEnd(failure); + } + } + /* package */ static File unpackLibraryBySoName(String soName) throws IOException { sSoSourcesLock.readLock().lock(); try { diff --git a/java/com/facebook/soloader/observer/Observer.java b/java/com/facebook/soloader/observer/Observer.java new file mode 100644 index 0000000..2368743 --- /dev/null +++ b/java/com/facebook/soloader/observer/Observer.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * Licensed 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 + * + * http://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 com.facebook.soloader.observer; + +import com.facebook.soloader.SoSource; +import com.facebook.soloader.recovery.RecoveryStrategy; +import javax.annotation.Nullable; + +public interface Observer { + void onLoadLibraryStart(String library, int flags); + + void onLoadLibraryEnd(@Nullable Throwable t); + + void onLoadDependencyStart(String library, int flags); + + void onLoadDependencyEnd(@Nullable Throwable t); + + void onSoSourceLoadLibraryStart(SoSource source); + + void onSoSourceLoadLibraryEnd(@Nullable Throwable t); + + void onRecoveryStart(RecoveryStrategy recovery); + + void onRecoveryEnd(@Nullable Throwable t); + + void onGetDependenciesStart(); + + void onGetDependenciesEnd(@Nullable Throwable t); +} diff --git a/java/com/facebook/soloader/observer/ObserverHolder.java b/java/com/facebook/soloader/observer/ObserverHolder.java new file mode 100644 index 0000000..7bb0e0d --- /dev/null +++ b/java/com/facebook/soloader/observer/ObserverHolder.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * Licensed 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 + * + * http://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 com.facebook.soloader.observer; + +import com.facebook.soloader.SoSource; +import com.facebook.soloader.recovery.RecoveryStrategy; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; + +public class ObserverHolder { + private static final AtomicReference sObservers = new AtomicReference(); + + public static void resetObserversForTestsOnly() { + sObservers.set(null); + } + + public static void addObserver(Observer newObserver) { + Observer[] oldObservers = null; + Observer[] newObservers = null; + + do { + oldObservers = sObservers.get(); + if (oldObservers == null) { + newObservers = new Observer[] {newObserver}; + } else { + newObservers = new Observer[oldObservers.length + 1]; + System.arraycopy(oldObservers, 0, newObservers, 0, oldObservers.length); + newObservers[oldObservers.length] = newObserver; + } + } while (!sObservers.compareAndSet(oldObservers, newObservers)); + } + + public static void onLoadLibraryStart(String library, int flags) { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onLoadLibraryStart(library, flags); + } + } + } + + public static void onLoadLibraryEnd(@Nullable Throwable t) { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onLoadLibraryEnd(t); + } + } + } + + public static void onLoadDependencyStart(String library, int flags) { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onLoadDependencyStart(library, flags); + } + } + } + + public static void onLoadDependencyEnd(@Nullable Throwable t) { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onLoadDependencyEnd(t); + } + } + } + + public static void onSoSourceLoadLibraryStart(SoSource source) { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onSoSourceLoadLibraryStart(source); + } + } + } + + public static void onSoSourceLoadLibraryEnd(@Nullable Throwable t) { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onSoSourceLoadLibraryEnd(t); + } + } + } + + public static void onRecoveryStart(RecoveryStrategy recovery) { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onRecoveryStart(recovery); + } + } + } + + public static void onRecoveryEnd(@Nullable Throwable t) { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onRecoveryEnd(t); + } + } + } + + public static void onGetDependenciesStart() { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onGetDependenciesStart(); + } + } + } + + public static void onGetDependenciesEnd(@Nullable Throwable t) { + Observer[] observers = sObservers.get(); + if (observers != null) { + for (Observer observer : observers) { + observer.onGetDependenciesEnd(t); + } + } + } +}