diff options
15 files changed, 5573 insertions, 0 deletions
diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java b/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java new file mode 100644 index 000000000000..fdb0fc538fdf --- /dev/null +++ b/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 android.service.watchdog; + +import static android.os.Parcelable.Creator; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.crashrecovery.flags.Flags; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * A service to provide packages supporting explicit health checks and route checks to these + * packages on behalf of the package watchdog. + * + * <p>To extend this class, you must declare the service in your manifest file with the + * {@link android.Manifest.permission#BIND_EXPLICIT_HEALTH_CHECK_SERVICE} permission, + * and include an intent filter with the {@link #SERVICE_INTERFACE} action. In adddition, + * your implementation must live in + * {@link PackageManager#getServicesSystemSharedLibraryPackageName()}. + * For example:</p> + * <pre> + * <service android:name=".FooExplicitHealthCheckService" + * android:exported="true" + * android:priority="100" + * android:permission="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"> + * <intent-filter> + * <action android:name="android.service.watchdog.ExplicitHealthCheckService" /> + * </intent-filter> + * </service> + * </pre> + * @hide + */ +@SystemApi +public abstract class ExplicitHealthCheckService extends Service { + + private static final String TAG = "ExplicitHealthCheckService"; + + /** + * {@link Bundle} key for a {@link List} of {@link PackageConfig} value. + * + * {@hide} + */ + public static final String EXTRA_SUPPORTED_PACKAGES = + "android.service.watchdog.extra.supported_packages"; + + /** + * {@link Bundle} key for a {@link List} of {@link String} value. + * + * {@hide} + */ + public static final String EXTRA_REQUESTED_PACKAGES = + "android.service.watchdog.extra.requested_packages"; + + /** + * {@link Bundle} key for a {@link String} value. + */ + @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) + public static final String EXTRA_HEALTH_CHECK_PASSED_PACKAGE = + "android.service.watchdog.extra.HEALTH_CHECK_PASSED_PACKAGE"; + + /** + * The Intent action that a service must respond to. Add it to the intent filter of the service + * in its manifest. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.service.watchdog.ExplicitHealthCheckService"; + + /** + * The permission that a service must require to ensure that only Android system can bind to it. + * If this permission is not enforced in the AndroidManifest of the service, the system will + * skip that service. + */ + public static final String BIND_PERMISSION = + "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"; + + private final ExplicitHealthCheckServiceWrapper mWrapper = + new ExplicitHealthCheckServiceWrapper(); + + /** + * Called when the system requests an explicit health check for {@code packageName}. + * + * <p> When {@code packageName} passes the check, implementors should call + * {@link #notifyHealthCheckPassed} to inform the system. + * + * <p> It could take many hours before a {@code packageName} passes a check and implementors + * should never drop requests unless {@link onCancel} is called or the service dies. + * + * <p> Requests should not be queued and additional calls while expecting a result for + * {@code packageName} should have no effect. + */ + public abstract void onRequestHealthCheck(@NonNull String packageName); + + /** + * Called when the system cancels the explicit health check request for {@code packageName}. + * Should do nothing if there are is no active request for {@code packageName}. + */ + public abstract void onCancelHealthCheck(@NonNull String packageName); + + /** + * Called when the system requests for all the packages supporting explicit health checks. The + * system may request an explicit health check for any of these packages with + * {@link #onRequestHealthCheck}. + * + * @return all packages supporting explicit health checks + */ + @NonNull public abstract List<PackageConfig> onGetSupportedPackages(); + + /** + * Called when the system requests for all the packages that it has currently requested + * an explicit health check for. + * + * @return all packages expecting an explicit health check result + */ + @NonNull public abstract List<String> onGetRequestedPackages(); + + private final Handler mHandler = Handler.createAsync(Looper.getMainLooper()); + @Nullable private Consumer<Bundle> mHealthCheckResultCallback; + @Nullable private Executor mCallbackExecutor; + + @Override + @NonNull + public final IBinder onBind(@NonNull Intent intent) { + return mWrapper; + } + + /** + * Sets a callback to be invoked when an explicit health check passes for a package. + * <p> + * The callback will receive a {@link Bundle} containing the package name that passed the + * health check, identified by the key {@link #EXTRA_HEALTH_CHECK_PASSED_PACKAGE}. + * <p> + * <b>Note:</b> This API is primarily intended for testing purposes. Calling this outside of a + * test environment will override the default callback mechanism used to notify the system + * about health check results. Use with caution in production code. + * + * @param executor The executor on which the callback should be invoked. If {@code null}, the + * callback will be executed on the main thread. + * @param callback A callback that receives a {@link Bundle} containing the package name that + * passed the health check. + */ + @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) + public final void setHealthCheckPassedCallback(@CallbackExecutor @Nullable Executor executor, + @Nullable Consumer<Bundle> callback) { + mCallbackExecutor = executor; + mHealthCheckResultCallback = callback; + } + + private void executeCallback(@NonNull String packageName) { + if (mHealthCheckResultCallback != null) { + Objects.requireNonNull(packageName, + "Package passing explicit health check must be non-null"); + Bundle bundle = new Bundle(); + bundle.putString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE, packageName); + mHealthCheckResultCallback.accept(bundle); + } else { + Log.wtf(TAG, "System missed explicit health check result for " + packageName); + } + } + + /** + * Implementors should call this to notify the system when explicit health check passes + * for {@code packageName}; + */ + public final void notifyHealthCheckPassed(@NonNull String packageName) { + if (mCallbackExecutor != null) { + mCallbackExecutor.execute(() -> executeCallback(packageName)); + } else { + mHandler.post(() -> executeCallback(packageName)); + } + } + + /** + * A PackageConfig contains a package supporting explicit health checks and the + * timeout in {@link System#uptimeMillis} across reboots after which health + * check requests from clients are failed. + * + * @hide + */ + @SystemApi + public static final class PackageConfig implements Parcelable { + private static final long DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(1); + + private final String mPackageName; + private final long mHealthCheckTimeoutMillis; + + /** + * Creates a new instance. + * + * @param packageName the package name + * @param durationMillis the duration in milliseconds, must be greater than or + * equal to 0. If it is 0, it will use a system default value. + */ + public PackageConfig(@NonNull String packageName, long healthCheckTimeoutMillis) { + mPackageName = Preconditions.checkNotNull(packageName); + if (healthCheckTimeoutMillis == 0) { + mHealthCheckTimeoutMillis = DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS; + } else { + mHealthCheckTimeoutMillis = Preconditions.checkArgumentNonnegative( + healthCheckTimeoutMillis); + } + } + + private PackageConfig(Parcel parcel) { + mPackageName = parcel.readString(); + mHealthCheckTimeoutMillis = parcel.readLong(); + } + + /** + * Gets the package name. + * + * @return the package name + */ + public @NonNull String getPackageName() { + return mPackageName; + } + + /** + * Gets the timeout in milliseconds to evaluate an explicit health check result after a + * request. + * + * @return the duration in {@link System#uptimeMillis} across reboots + */ + public long getHealthCheckTimeoutMillis() { + return mHealthCheckTimeoutMillis; + } + + @NonNull + @Override + public String toString() { + return "PackageConfig{" + mPackageName + ", " + mHealthCheckTimeoutMillis + "}"; + } + + @Override + public boolean equals(@Nullable Object other) { + if (other == this) { + return true; + } + if (!(other instanceof PackageConfig)) { + return false; + } + + PackageConfig otherInfo = (PackageConfig) other; + return Objects.equals(otherInfo.getHealthCheckTimeoutMillis(), + mHealthCheckTimeoutMillis) + && Objects.equals(otherInfo.getPackageName(), mPackageName); + } + + @Override + public int hashCode() { + return Objects.hash(mPackageName, mHealthCheckTimeoutMillis); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@SuppressLint({"MissingNullability"}) Parcel parcel, int flags) { + parcel.writeString(mPackageName); + parcel.writeLong(mHealthCheckTimeoutMillis); + } + + public static final @NonNull Creator<PackageConfig> CREATOR = new Creator<PackageConfig>() { + @Override + public PackageConfig createFromParcel(Parcel source) { + return new PackageConfig(source); + } + + @Override + public PackageConfig[] newArray(int size) { + return new PackageConfig[size]; + } + }; + } + + + private class ExplicitHealthCheckServiceWrapper extends IExplicitHealthCheckService.Stub { + @Override + public void setCallback(RemoteCallback callback) throws RemoteException { + mHandler.post(() -> mHealthCheckResultCallback = callback::sendResult); + } + + @Override + public void request(String packageName) throws RemoteException { + mHandler.post(() -> ExplicitHealthCheckService.this.onRequestHealthCheck(packageName)); + } + + @Override + public void cancel(String packageName) throws RemoteException { + mHandler.post(() -> ExplicitHealthCheckService.this.onCancelHealthCheck(packageName)); + } + + @Override + public void getSupportedPackages(RemoteCallback callback) throws RemoteException { + mHandler.post(() -> { + List<PackageConfig> packages = + ExplicitHealthCheckService.this.onGetSupportedPackages(); + Objects.requireNonNull(packages, "Supported package list must be non-null"); + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, new ArrayList<>(packages)); + callback.sendResult(bundle); + }); + } + + @Override + public void getRequestedPackages(RemoteCallback callback) throws RemoteException { + mHandler.post(() -> { + List<String> packages = + ExplicitHealthCheckService.this.onGetRequestedPackages(); + Objects.requireNonNull(packages, "Requested package list must be non-null"); + Bundle bundle = new Bundle(); + bundle.putStringArrayList(EXTRA_REQUESTED_PACKAGES, new ArrayList<>(packages)); + callback.sendResult(bundle); + }); + } + } +} diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl b/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl new file mode 100644 index 000000000000..90965092ac2b --- /dev/null +++ b/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 android.service.watchdog; + +import android.os.RemoteCallback; + +/** + * @hide + */ +@PermissionManuallyEnforced +oneway interface IExplicitHealthCheckService +{ + void setCallback(in @nullable RemoteCallback callback); + void request(String packageName); + void cancel(String packageName); + void getSupportedPackages(in RemoteCallback callback); + void getRequestedPackages(in RemoteCallback callback); +} diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS b/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS new file mode 100644 index 000000000000..1c045e10c0ec --- /dev/null +++ b/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS @@ -0,0 +1,3 @@ +narayan@google.com +nandana@google.com +olilan@google.com diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl b/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl new file mode 100644 index 000000000000..013158676f79 --- /dev/null +++ b/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 android.service.watchdog; + +/** + * @hide + */ +parcelable PackageConfig; diff --git a/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java b/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java new file mode 100644 index 000000000000..da9a13961f79 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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.android.server; + +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE; +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES; +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES; +import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; + +import android.Manifest; +import android.annotation.MainThread; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.IBinder; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.watchdog.ExplicitHealthCheckService; +import android.service.watchdog.IExplicitHealthCheckService; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +// TODO(b/120598832): Add tests +/** + * Controls the connections with {@link ExplicitHealthCheckService}. + */ +class ExplicitHealthCheckController { + private static final String TAG = "ExplicitHealthCheckController"; + private final Object mLock = new Object(); + private final Context mContext; + + // Called everytime a package passes the health check, so the watchdog is notified of the + // passing check. In practice, should never be null after it has been #setEnabled. + // To prevent deadlocks between the controller and watchdog threads, we have + // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. + // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. + @GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer; + // Called everytime after a successful #syncRequest call, so the watchdog can receive packages + // supporting health checks and update its internal state. In practice, should never be null + // after it has been #setEnabled. + // To prevent deadlocks between the controller and watchdog threads, we have + // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. + // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. + @GuardedBy("mLock") @Nullable private Consumer<List<PackageConfig>> mSupportedConsumer; + // Called everytime we need to notify the watchdog to sync requests between itself and the + // health check service. In practice, should never be null after it has been #setEnabled. + // To prevent deadlocks between the controller and watchdog threads, we have + // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. + // It's easier to just NOT hold #mLock when calling into watchdog code on this runnable. + @GuardedBy("mLock") @Nullable private Runnable mNotifySyncRunnable; + // Actual binder object to the explicit health check service. + @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService; + // Connection to the explicit health check service, necessary to unbind. + // We should only try to bind if mConnection is null, non-null indicates we + // are connected or at least connecting. + @GuardedBy("mLock") @Nullable private ServiceConnection mConnection; + // Bind state of the explicit health check service. + @GuardedBy("mLock") private boolean mEnabled; + + ExplicitHealthCheckController(Context context) { + mContext = context; + } + + /** Enables or disables explicit health checks. */ + public void setEnabled(boolean enabled) { + synchronized (mLock) { + Slog.i(TAG, "Explicit health checks " + (enabled ? "enabled." : "disabled.")); + mEnabled = enabled; + } + } + + /** + * Sets callbacks to listen to important events from the controller. + * + * <p> Should be called once at initialization before any other calls to the controller to + * ensure a happens-before relationship of the set parameters and visibility on other threads. + */ + public void setCallbacks(Consumer<String> passedConsumer, + Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) { + synchronized (mLock) { + if (mPassedConsumer != null || mSupportedConsumer != null + || mNotifySyncRunnable != null) { + Slog.wtf(TAG, "Resetting health check controller callbacks"); + } + + mPassedConsumer = Objects.requireNonNull(passedConsumer); + mSupportedConsumer = Objects.requireNonNull(supportedConsumer); + mNotifySyncRunnable = Objects.requireNonNull(notifySyncRunnable); + } + } + + /** + * Calls the health check service to request or cancel packages based on + * {@code newRequestedPackages}. + * + * <p> Supported packages in {@code newRequestedPackages} that have not been previously + * requested will be requested while supported packages not in {@code newRequestedPackages} + * but were previously requested will be cancelled. + * + * <p> This handles binding and unbinding to the health check service as required. + * + * <p> Note, calling this may modify {@code newRequestedPackages}. + * + * <p> Note, this method is not thread safe, all calls should be serialized. + */ + public void syncRequests(Set<String> newRequestedPackages) { + boolean enabled; + synchronized (mLock) { + enabled = mEnabled; + } + + if (!enabled) { + Slog.i(TAG, "Health checks disabled, no supported packages"); + // Call outside lock + mSupportedConsumer.accept(Collections.emptyList()); + return; + } + + getSupportedPackages(supportedPackageConfigs -> { + // Notify the watchdog without lock held + mSupportedConsumer.accept(supportedPackageConfigs); + getRequestedPackages(previousRequestedPackages -> { + synchronized (mLock) { + // Hold lock so requests and cancellations are sent atomically. + // It is important we don't mix requests from multiple threads. + + Set<String> supportedPackages = new ArraySet<>(); + for (PackageConfig config : supportedPackageConfigs) { + supportedPackages.add(config.getPackageName()); + } + // Note, this may modify newRequestedPackages + newRequestedPackages.retainAll(supportedPackages); + + // Cancel packages no longer requested + actOnDifference(previousRequestedPackages, + newRequestedPackages, p -> cancel(p)); + // Request packages not yet requested + actOnDifference(newRequestedPackages, + previousRequestedPackages, p -> request(p)); + + if (newRequestedPackages.isEmpty()) { + Slog.i(TAG, "No more health check requests, unbinding..."); + unbindService(); + return; + } + } + }); + }); + } + + private void actOnDifference(Collection<String> collection1, Collection<String> collection2, + Consumer<String> action) { + Iterator<String> iterator = collection1.iterator(); + while (iterator.hasNext()) { + String packageName = iterator.next(); + if (!collection2.contains(packageName)) { + action.accept(packageName); + } + } + } + + /** + * Requests an explicit health check for {@code packageName}. + * After this request, the callback registered on {@link #setCallbacks} can receive explicit + * health check passed results. + */ + private void request(String packageName) { + synchronized (mLock) { + if (!prepareServiceLocked("request health check for " + packageName)) { + return; + } + + Slog.i(TAG, "Requesting health check for package " + packageName); + try { + mRemoteService.request(packageName); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to request health check for package " + packageName, e); + } + } + } + + /** + * Cancels all explicit health checks for {@code packageName}. + * After this request, the callback registered on {@link #setCallbacks} can no longer receive + * explicit health check passed results. + */ + private void cancel(String packageName) { + synchronized (mLock) { + if (!prepareServiceLocked("cancel health check for " + packageName)) { + return; + } + + Slog.i(TAG, "Cancelling health check for package " + packageName); + try { + mRemoteService.cancel(packageName); + } catch (RemoteException e) { + // Do nothing, if the service is down, when it comes up, we will sync requests, + // if there's some other error, retrying wouldn't fix anyways. + Slog.w(TAG, "Failed to cancel health check for package " + packageName, e); + } + } + } + + /** + * Returns the packages that we can request explicit health checks for. + * The packages will be returned to the {@code consumer}. + */ + private void getSupportedPackages(Consumer<List<PackageConfig>> consumer) { + synchronized (mLock) { + if (!prepareServiceLocked("get health check supported packages")) { + return; + } + + Slog.d(TAG, "Getting health check supported packages"); + try { + mRemoteService.getSupportedPackages(new RemoteCallback(result -> { + List<PackageConfig> packages = + result.getParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, android.service.watchdog.ExplicitHealthCheckService.PackageConfig.class); + Slog.i(TAG, "Explicit health check supported packages " + packages); + consumer.accept(packages); + })); + } catch (RemoteException e) { + // Request failed, treat as if all observed packages are supported, if any packages + // expire during this period, we may incorrectly treat it as failing health checks + // even if we don't support health checks for the package. + Slog.w(TAG, "Failed to get health check supported packages", e); + } + } + } + + /** + * Returns the packages for which health checks are currently in progress. + * The packages will be returned to the {@code consumer}. + */ + private void getRequestedPackages(Consumer<List<String>> consumer) { + synchronized (mLock) { + if (!prepareServiceLocked("get health check requested packages")) { + return; + } + + Slog.d(TAG, "Getting health check requested packages"); + try { + mRemoteService.getRequestedPackages(new RemoteCallback(result -> { + List<String> packages = result.getStringArrayList(EXTRA_REQUESTED_PACKAGES); + Slog.i(TAG, "Explicit health check requested packages " + packages); + consumer.accept(packages); + })); + } catch (RemoteException e) { + // Request failed, treat as if we haven't requested any packages, if any packages + // were actually requested, they will not be cancelled now. May be cancelled later + Slog.w(TAG, "Failed to get health check requested packages", e); + } + } + } + + /** + * Binds to the explicit health check service if the controller is enabled and + * not already bound. + */ + private void bindService() { + synchronized (mLock) { + if (!mEnabled || mConnection != null || mRemoteService != null) { + if (!mEnabled) { + Slog.i(TAG, "Not binding to service, service disabled"); + } else if (mRemoteService != null) { + Slog.i(TAG, "Not binding to service, service already connected"); + } else { + Slog.i(TAG, "Not binding to service, service already connecting"); + } + return; + } + ComponentName component = getServiceComponentNameLocked(); + if (component == null) { + Slog.wtf(TAG, "Explicit health check service not found"); + return; + } + + Intent intent = new Intent(); + intent.setComponent(component); + mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Slog.i(TAG, "Explicit health check service is connected " + name); + initState(service); + } + + @Override + @MainThread + public void onServiceDisconnected(ComponentName name) { + // Service crashed or process was killed, #onServiceConnected will be called. + // Don't need to re-bind. + Slog.i(TAG, "Explicit health check service is disconnected " + name); + synchronized (mLock) { + mRemoteService = null; + } + } + + @Override + public void onBindingDied(ComponentName name) { + // Application hosting service probably got updated + // Need to re-bind. + Slog.i(TAG, "Explicit health check service binding is dead. Rebind: " + name); + unbindService(); + bindService(); + } + + @Override + public void onNullBinding(ComponentName name) { + // Should never happen. Service returned null from #onBind. + Slog.wtf(TAG, "Explicit health check service binding is null?? " + name); + } + }; + + mContext.bindServiceAsUser(intent, mConnection, + Context.BIND_AUTO_CREATE, UserHandle.SYSTEM); + Slog.i(TAG, "Explicit health check service is bound"); + } + } + + /** Unbinds the explicit health check service. */ + private void unbindService() { + synchronized (mLock) { + if (mRemoteService != null) { + mContext.unbindService(mConnection); + mRemoteService = null; + mConnection = null; + } + Slog.i(TAG, "Explicit health check service is unbound"); + } + } + + @GuardedBy("mLock") + @Nullable + private ServiceInfo getServiceInfoLocked() { + final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE); + final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA + | PackageManager.MATCH_SYSTEM_ONLY); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + Slog.w(TAG, "No valid components found."); + return null; + } + return resolveInfo.serviceInfo; + } + + @GuardedBy("mLock") + @Nullable + private ComponentName getServiceComponentNameLocked() { + final ServiceInfo serviceInfo = getServiceInfoLocked(); + if (serviceInfo == null) { + return null; + } + + final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); + if (!Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE + .equals(serviceInfo.permission)) { + Slog.w(TAG, name.flattenToShortString() + " does not require permission " + + Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE); + return null; + } + return name; + } + + private void initState(IBinder service) { + synchronized (mLock) { + if (!mEnabled) { + Slog.w(TAG, "Attempting to connect disabled service?? Unbinding..."); + // Very unlikely, but we disabled the service after binding but before we connected + unbindService(); + return; + } + mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service); + try { + mRemoteService.setCallback(new RemoteCallback(result -> { + String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE); + if (!TextUtils.isEmpty(packageName)) { + if (mPassedConsumer == null) { + Slog.wtf(TAG, "Health check passed for package " + packageName + + "but no consumer registered."); + } else { + // Call without lock held + mPassedConsumer.accept(packageName); + } + } else { + Slog.wtf(TAG, "Empty package passed explicit health check?"); + } + })); + Slog.i(TAG, "Service initialized, syncing requests"); + } catch (RemoteException e) { + Slog.wtf(TAG, "Could not setCallback on explicit health check service"); + } + } + // Calling outside lock + mNotifySyncRunnable.run(); + } + + /** + * Prepares the health check service to receive requests. + * + * @return {@code true} if it is ready and we can proceed with a request, + * {@code false} otherwise. If it is not ready, and the service is enabled, + * we will bind and the request should be automatically attempted later. + */ + @GuardedBy("mLock") + private boolean prepareServiceLocked(String action) { + if (mRemoteService != null && mEnabled) { + return true; + } + Slog.i(TAG, "Service not ready to " + action + + (mEnabled ? ". Binding..." : ". Disabled")); + if (mEnabled) { + bindService(); + } + return false; + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java new file mode 100644 index 000000000000..e4f07f9fc213 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java @@ -0,0 +1,2253 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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.android.server; + +import static android.content.Intent.ACTION_REBOOT; +import static android.content.Intent.ACTION_SHUTDOWN; +import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; +import static android.util.Xml.Encoding.UTF_8; + +import static com.android.server.crashrecovery.CrashRecoveryUtils.dumpCrashRecoveryEvents; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.os.SystemProperties; +import android.provider.DeviceConfig; +import android.sysprop.CrashRecoveryProperties; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.EventLog; +import android.util.IndentingPrintWriter; +import android.util.LongArrayQueue; +import android.util.Slog; +import android.util.Xml; +import android.util.XmlUtils; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FastXmlSerializer; +import com.android.modules.utils.BackgroundThread; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * Monitors the health of packages on the system and notifies interested observers when packages + * fail. On failure, the registered observer with the least user impacting mitigation will + * be notified. + * @hide + */ +@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) +public class PackageWatchdog { + private static final String TAG = "PackageWatchdog"; + + static final String PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS = + "watchdog_trigger_failure_duration_millis"; + static final String PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT = + "watchdog_trigger_failure_count"; + static final String PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED = + "watchdog_explicit_health_check_enabled"; + + // TODO: make the following values configurable via DeviceConfig + private static final long NATIVE_CRASH_POLLING_INTERVAL_MILLIS = + TimeUnit.SECONDS.toMillis(30); + private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10; + + + /** Reason for package failure could not be determined. */ + public static final int FAILURE_REASON_UNKNOWN = 0; + + /** The package had a native crash. */ + public static final int FAILURE_REASON_NATIVE_CRASH = 1; + + /** The package failed an explicit health check. */ + public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2; + + /** The app crashed. */ + public static final int FAILURE_REASON_APP_CRASH = 3; + + /** The app was not responding. */ + public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4; + + /** The device was boot looping. */ + public static final int FAILURE_REASON_BOOT_LOOP = 5; + + /** @hide */ + @IntDef(prefix = { "FAILURE_REASON_" }, value = { + FAILURE_REASON_UNKNOWN, + FAILURE_REASON_NATIVE_CRASH, + FAILURE_REASON_EXPLICIT_HEALTH_CHECK, + FAILURE_REASON_APP_CRASH, + FAILURE_REASON_APP_NOT_RESPONDING, + FAILURE_REASON_BOOT_LOOP + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FailureReasons {} + + // Duration to count package failures before it resets to 0 + @VisibleForTesting + static final int DEFAULT_TRIGGER_FAILURE_DURATION_MS = + (int) TimeUnit.MINUTES.toMillis(1); + // Number of package failures within the duration above before we notify observers + @VisibleForTesting + static final int DEFAULT_TRIGGER_FAILURE_COUNT = 5; + @VisibleForTesting + static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2); + // Sliding window for tracking how many mitigation calls were made for a package. + @VisibleForTesting + static final long DEFAULT_DEESCALATION_WINDOW_MS = TimeUnit.HOURS.toMillis(1); + // Whether explicit health checks are enabled or not + private static final boolean DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED = true; + + @VisibleForTesting + static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5; + + static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10); + + // Time needed to apply mitigation + private static final String MITIGATION_WINDOW_MS = + "persist.device_config.configuration.mitigation_window_ms"; + @VisibleForTesting + static final long DEFAULT_MITIGATION_WINDOW_MS = TimeUnit.SECONDS.toMillis(5); + + // Threshold level at which or above user might experience significant disruption. + private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD = + "persist.device_config.configuration.major_user_impact_level_threshold"; + private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; + + // Comma separated list of all packages exempt from user impact level threshold. If a package + // in the list is crash looping, all the mitigations including factory reset will be performed. + private static final String PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD = + "persist.device_config.configuration.packages_exempt_from_impact_level_threshold"; + + // Comma separated list of default packages exempt from user impact level threshold. + private static final String DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD = + "com.android.systemui"; + + private long mNumberOfNativeCrashPollsRemaining; + + private static final int DB_VERSION = 1; + private static final String TAG_PACKAGE_WATCHDOG = "package-watchdog"; + private static final String TAG_PACKAGE = "package"; + private static final String TAG_OBSERVER = "observer"; + private static final String ATTR_VERSION = "version"; + private static final String ATTR_NAME = "name"; + private static final String ATTR_DURATION = "duration"; + private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration"; + private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check"; + private static final String ATTR_MITIGATION_CALLS = "mitigation-calls"; + private static final String ATTR_MITIGATION_COUNT = "mitigation-count"; + + // A file containing information about the current mitigation count in the case of a boot loop. + // This allows boot loop information to persist in the case of an fs-checkpoint being + // aborted. + private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt"; + + /** + * EventLog tags used when logging into the event log. Note the values must be sync with + * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct + * name translation. + */ + private static final int LOG_TAG_RESCUE_NOTE = 2900; + + private static final Object sPackageWatchdogLock = new Object(); + @GuardedBy("sPackageWatchdogLock") + private static PackageWatchdog sPackageWatchdog; + + private static final Object sLock = new Object(); + // System server context + private final Context mContext; + // Handler to run short running tasks + private final Handler mShortTaskHandler; + // Handler for processing IO and long running tasks + private final Handler mLongTaskHandler; + // Contains (observer-name -> observer-handle) that have ever been registered from + // previous boots. Observers with all packages expired are periodically pruned. + // It is saved to disk on system shutdown and repouplated on startup so it survives reboots. + @GuardedBy("sLock") + private final ArrayMap<String, ObserverInternal> mAllObservers = new ArrayMap<>(); + // File containing the XML data of monitored packages /data/system/package-watchdog.xml + private final AtomicFile mPolicyFile; + private final ExplicitHealthCheckController mHealthCheckController; + private final Runnable mSyncRequests = this::syncRequests; + private final Runnable mSyncStateWithScheduledReason = this::syncStateWithScheduledReason; + private final Runnable mSaveToFile = this::saveToFile; + private final SystemClock mSystemClock; + private final BootThreshold mBootThreshold; + private final DeviceConfig.OnPropertiesChangedListener + mOnPropertyChangedListener = this::onPropertyChanged; + + private final Set<String> mPackagesExemptFromImpactLevelThreshold = new ArraySet<>(); + + // The set of packages that have been synced with the ExplicitHealthCheckController + @GuardedBy("sLock") + private Set<String> mRequestedHealthCheckPackages = new ArraySet<>(); + @GuardedBy("sLock") + private boolean mIsPackagesReady; + // Flag to control whether explicit health checks are supported or not + @GuardedBy("sLock") + private boolean mIsHealthCheckEnabled = DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED; + @GuardedBy("sLock") + private int mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS; + @GuardedBy("sLock") + private int mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT; + // SystemClock#uptimeMillis when we last executed #syncState + // 0 if no prune is scheduled. + @GuardedBy("sLock") + private long mUptimeAtLastStateSync; + // If true, sync explicit health check packages with the ExplicitHealthCheckController. + @GuardedBy("sLock") + private boolean mSyncRequired = false; + + @GuardedBy("sLock") + private long mLastMitigation = -1000000; + + @FunctionalInterface + @VisibleForTesting + interface SystemClock { + long uptimeMillis(); + } + + private PackageWatchdog(Context context) { + // Needs to be constructed inline + this(context, new AtomicFile( + new File(new File(Environment.getDataDirectory(), "system"), + "package-watchdog.xml")), + new Handler(Looper.myLooper()), BackgroundThread.getHandler(), + new ExplicitHealthCheckController(context), + android.os.SystemClock::uptimeMillis); + } + + /** + * Creates a PackageWatchdog that allows injecting dependencies. + */ + @VisibleForTesting + PackageWatchdog(Context context, AtomicFile policyFile, Handler shortTaskHandler, + Handler longTaskHandler, ExplicitHealthCheckController controller, + SystemClock clock) { + mContext = context; + mPolicyFile = policyFile; + mShortTaskHandler = shortTaskHandler; + mLongTaskHandler = longTaskHandler; + mHealthCheckController = controller; + mSystemClock = clock; + mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS; + mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS); + + loadFromFile(); + sPackageWatchdog = this; + } + + /** + * Creates or gets singleton instance of PackageWatchdog. + * + * @param context The system server context. + */ + public static @NonNull PackageWatchdog getInstance(@NonNull Context context) { + synchronized (sPackageWatchdogLock) { + if (sPackageWatchdog == null) { + new PackageWatchdog(context); + } + return sPackageWatchdog; + } + } + + /** + * Called during boot to notify when packages are ready on the device so we can start + * binding. + * @hide + */ + public void onPackagesReady() { + synchronized (sLock) { + mIsPackagesReady = true; + mHealthCheckController.setCallbacks(packageName -> onHealthCheckPassed(packageName), + packages -> onSupportedPackages(packages), + this::onSyncRequestNotified); + setPropertyChangedListenerLocked(); + updateConfigs(); + } + } + + /** + * Registers {@code observer} to listen for package failures. Add a new ObserverInternal for + * this observer if it does not already exist. + * For executing mitigations observers will receive callback on the given executor. + * + * <p>Observers are expected to call this on boot. It does not specify any packages but + * it will resume observing any packages requested from a previous boot. + * + * @param observer instance of {@link PackageHealthObserver} for observing package failures + * and boot loops. + * @param executor Executor for the thread on which observers would receive callbacks + */ + public void registerHealthObserver(@NonNull @CallbackExecutor Executor executor, + @NonNull PackageHealthObserver observer) { + synchronized (sLock) { + ObserverInternal internalObserver = mAllObservers.get(observer.getUniqueIdentifier()); + if (internalObserver != null) { + internalObserver.registeredObserver = observer; + internalObserver.observerExecutor = executor; + } else { + internalObserver = new ObserverInternal(observer.getUniqueIdentifier(), + new ArrayList<>()); + internalObserver.registeredObserver = observer; + internalObserver.observerExecutor = executor; + mAllObservers.put(observer.getUniqueIdentifier(), internalObserver); + syncState("added new observer"); + } + } + } + + /** + * Starts observing the health of the {@code packages} for {@code observer}. + * Note: Observer needs to be registered with {@link #registerHealthObserver} before calling + * this API. + * + * <p>If monitoring a package supporting explicit health check, at the end of the monitoring + * duration if {@link #onHealthCheckPassed} was never called, + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} will be called as if the + * package failed. + * + * <p>If {@code observer} is already monitoring a package in {@code packageNames}, + * the monitoring window of that package will be reset to {@code durationMs} and the health + * check state will be reset to a default. + * + * <p>The {@code observer} must be registered with {@link #registerHealthObserver} before + * calling this method. + * + * @param packageNames The list of packages to check. If this is empty, the call will be a + * no-op. + * + * @param timeoutMs The timeout after which Explicit Health Checks would not run. If this is + * less than 1, a default monitoring duration 2 days will be used. + * + * @throws IllegalStateException if the observer was not previously registered + */ + public void startExplicitHealthCheck(@NonNull List<String> packageNames, long timeoutMs, + @NonNull PackageHealthObserver observer) { + synchronized (sLock) { + if (!mAllObservers.containsKey(observer.getUniqueIdentifier())) { + Slog.wtf(TAG, "No observer found, need to register the observer: " + + observer.getUniqueIdentifier()); + throw new IllegalStateException("Observer not registered"); + } + } + if (packageNames.isEmpty()) { + Slog.wtf(TAG, "No packages to observe, " + observer.getUniqueIdentifier()); + return; + } + if (timeoutMs < 1) { + Slog.wtf(TAG, "Invalid duration " + timeoutMs + "ms for observer " + + observer.getUniqueIdentifier() + ". Not observing packages " + packageNames); + timeoutMs = DEFAULT_OBSERVING_DURATION_MS; + } + + List<MonitoredPackage> packages = new ArrayList<>(); + for (int i = 0; i < packageNames.size(); i++) { + // Health checks not available yet so health check state will start INACTIVE + MonitoredPackage pkg = newMonitoredPackage(packageNames.get(i), timeoutMs, false); + if (pkg != null) { + packages.add(pkg); + } else { + Slog.w(TAG, "Failed to create MonitoredPackage for pkg=" + packageNames.get(i)); + } + } + + if (packages.isEmpty()) { + return; + } + + // Sync before we add the new packages to the observers. This will #pruneObservers, + // causing any elapsed time to be deducted from all existing packages before we add new + // packages. This maintains the invariant that the elapsed time for ALL (new and existing) + // packages is the same. + mLongTaskHandler.post(() -> { + syncState("observing new packages"); + + synchronized (sLock) { + ObserverInternal oldObserver = mAllObservers.get(observer.getUniqueIdentifier()); + if (oldObserver == null) { + Slog.d(TAG, observer.getUniqueIdentifier() + " started monitoring health " + + "of packages " + packageNames); + mAllObservers.put(observer.getUniqueIdentifier(), + new ObserverInternal(observer.getUniqueIdentifier(), packages)); + } else { + Slog.d(TAG, observer.getUniqueIdentifier() + " added the following " + + "packages to monitor " + packageNames); + oldObserver.updatePackagesLocked(packages); + } + } + + // Sync after we add the new packages to the observers. We may have received packges + // requiring an earlier schedule than we are currently scheduled for. + syncState("updated observers"); + }); + + } + + /** + * Unregisters {@code observer} from listening to package failure. + * Additionally, this stops observing any packages that may have previously been observed + * even from a previous boot. + */ + public void unregisterHealthObserver(@NonNull PackageHealthObserver observer) { + mLongTaskHandler.post(() -> { + synchronized (sLock) { + mAllObservers.remove(observer.getUniqueIdentifier()); + } + syncState("unregistering observer: " + observer.getUniqueIdentifier()); + }); + } + + /** + * Called when a process fails due to a crash, ANR or explicit health check. + * + * <p>For each package contained in the process, one registered observer with the least user + * impact will be notified for mitigation. + * + * <p>This method could be called frequently if there is a severe problem on the device. + */ + public void notifyPackageFailure(@NonNull List<VersionedPackage> packages, + @FailureReasons int failureReason) { + if (packages == null) { + Slog.w(TAG, "Could not resolve a list of failing packages"); + return; + } + synchronized (sLock) { + final long now = mSystemClock.uptimeMillis(); + if (Flags.recoverabilityDetection()) { + if (now >= mLastMitigation + && (now - mLastMitigation) < getMitigationWindowMs()) { + Slog.i(TAG, "Skipping notifyPackageFailure mitigation"); + return; + } + } + } + mLongTaskHandler.post(() -> { + synchronized (sLock) { + if (mAllObservers.isEmpty()) { + return; + } + boolean requiresImmediateAction = (failureReason == FAILURE_REASON_NATIVE_CRASH + || failureReason == FAILURE_REASON_EXPLICIT_HEALTH_CHECK); + if (requiresImmediateAction) { + handleFailureImmediately(packages, failureReason); + } else { + for (int pIndex = 0; pIndex < packages.size(); pIndex++) { + VersionedPackage versionedPackage = packages.get(pIndex); + // Observer that will receive failure for versionedPackage + ObserverInternal currentObserverToNotify = null; + int currentObserverImpact = Integer.MAX_VALUE; + MonitoredPackage currentMonitoredPackage = null; + + // Find observer with least user impact + for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { + ObserverInternal observer = mAllObservers.valueAt(oIndex); + PackageHealthObserver registeredObserver = observer.registeredObserver; + if (registeredObserver != null + && observer.notifyPackageFailureLocked( + versionedPackage.getPackageName())) { + MonitoredPackage p = observer.getMonitoredPackage( + versionedPackage.getPackageName()); + int mitigationCount = 1; + if (p != null) { + mitigationCount = p.getMitigationCountLocked() + 1; + } + int impact = registeredObserver.onHealthCheckFailed( + versionedPackage, failureReason, mitigationCount); + if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 + && impact < currentObserverImpact) { + currentObserverToNotify = observer; + currentObserverImpact = impact; + currentMonitoredPackage = p; + } + } + } + + // Execute action with least user impact + if (currentObserverToNotify != null) { + int mitigationCount; + if (currentMonitoredPackage != null) { + currentMonitoredPackage.noteMitigationCallLocked(); + mitigationCount = + currentMonitoredPackage.getMitigationCountLocked(); + } else { + mitigationCount = 1; + } + if (Flags.recoverabilityDetection()) { + maybeExecute(currentObserverToNotify, versionedPackage, + failureReason, currentObserverImpact, mitigationCount); + } else { + PackageHealthObserver registeredObserver = + currentObserverToNotify.registeredObserver; + currentObserverToNotify.observerExecutor.execute(() -> + registeredObserver.onExecuteHealthCheckMitigation( + versionedPackage, failureReason, mitigationCount)); + } + } + } + } + } + }); + } + + /** + * For native crashes or explicit health check failures, call directly into each observer to + * mitigate the error without going through failure threshold logic. + */ + @GuardedBy("sLock") + private void handleFailureImmediately(List<VersionedPackage> packages, + @FailureReasons int failureReason) { + VersionedPackage failingPackage = packages.size() > 0 ? packages.get(0) : null; + ObserverInternal currentObserverToNotify = null; + int currentObserverImpact = Integer.MAX_VALUE; + for (ObserverInternal observer: mAllObservers.values()) { + PackageHealthObserver registeredObserver = observer.registeredObserver; + if (registeredObserver != null) { + int impact = registeredObserver.onHealthCheckFailed( + failingPackage, failureReason, 1); + if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 + && impact < currentObserverImpact) { + currentObserverToNotify = observer; + currentObserverImpact = impact; + } + } + } + if (currentObserverToNotify != null) { + if (Flags.recoverabilityDetection()) { + maybeExecute(currentObserverToNotify, failingPackage, failureReason, + currentObserverImpact, /*mitigationCount=*/ 1); + } else { + PackageHealthObserver registeredObserver = + currentObserverToNotify.registeredObserver; + currentObserverToNotify.observerExecutor.execute(() -> + registeredObserver.onExecuteHealthCheckMitigation(failingPackage, + failureReason, 1)); + + } + } + } + + private void maybeExecute(ObserverInternal currentObserverToNotify, + VersionedPackage versionedPackage, + @FailureReasons int failureReason, + int currentObserverImpact, + int mitigationCount) { + if (allowMitigations(currentObserverImpact, versionedPackage)) { + PackageHealthObserver registeredObserver; + synchronized (sLock) { + mLastMitigation = mSystemClock.uptimeMillis(); + registeredObserver = currentObserverToNotify.registeredObserver; + } + currentObserverToNotify.observerExecutor.execute(() -> + registeredObserver.onExecuteHealthCheckMitigation(versionedPackage, + failureReason, mitigationCount)); + } + } + + private boolean allowMitigations(int currentObserverImpact, + VersionedPackage versionedPackage) { + return currentObserverImpact < getUserImpactLevelLimit() + || getPackagesExemptFromImpactLevelThreshold().contains( + versionedPackage.getPackageName()); + } + + private long getMitigationWindowMs() { + return SystemProperties.getLong(MITIGATION_WINDOW_MS, DEFAULT_MITIGATION_WINDOW_MS); + } + + + /** + * Called when the system server boots. If the system server is detected to be in a boot loop, + * query each observer and perform the mitigation action with the lowest user impact. + * + * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots + * are not counted in bootloop. + * @hide + */ + @SuppressWarnings("GuardedBy") + public void noteBoot() { + synchronized (sLock) { + // if boot count has reached threshold, start mitigation. + // We wait until threshold number of restarts only for the first time. Perform + // mitigations for every restart after that. + boolean mitigate = mBootThreshold.incrementAndTest(); + if (mitigate) { + if (!Flags.recoverabilityDetection()) { + mBootThreshold.reset(); + } + int mitigationCount = mBootThreshold.getMitigationCount() + 1; + ObserverInternal currentObserverToNotify = null; + int currentObserverImpact = Integer.MAX_VALUE; + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + PackageHealthObserver registeredObserver = observer.registeredObserver; + if (registeredObserver != null) { + int impact = Flags.recoverabilityDetection() + ? registeredObserver.onBootLoop( + observer.getBootMitigationCount() + 1) + : registeredObserver.onBootLoop(mitigationCount); + if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 + && impact < currentObserverImpact) { + currentObserverToNotify = observer; + currentObserverImpact = impact; + } + } + } + + if (currentObserverToNotify != null) { + PackageHealthObserver registeredObserver = + currentObserverToNotify.registeredObserver; + if (Flags.recoverabilityDetection()) { + int currentObserverMitigationCount = + currentObserverToNotify.getBootMitigationCount() + 1; + currentObserverToNotify.setBootMitigationCount( + currentObserverMitigationCount); + saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); + currentObserverToNotify.observerExecutor + .execute(() -> registeredObserver.onExecuteBootLoopMitigation( + currentObserverMitigationCount)); + } else { + mBootThreshold.setMitigationCount(mitigationCount); + mBootThreshold.saveMitigationCountToMetadata(); + currentObserverToNotify.observerExecutor + .execute(() -> registeredObserver.onExecuteBootLoopMitigation( + mitigationCount)); + + } + } + } + } + } + + // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also + // avoid holding lock? + // This currently adds about 7ms extra to shutdown thread + /** @hide Writes the package information to file during shutdown. */ + public void writeNow() { + synchronized (sLock) { + // Must only run synchronous tasks as this runs on the ShutdownThread and no other + // thread is guaranteed to run during shutdown. + if (!mAllObservers.isEmpty()) { + mLongTaskHandler.removeCallbacks(mSaveToFile); + pruneObserversLocked(); + saveToFile(); + Slog.i(TAG, "Last write to update package durations"); + } + } + } + + /** + * Enables or disables explicit health checks. + * <p> If explicit health checks are enabled, the health check service is started. + * <p> If explicit health checks are disabled, pending explicit health check requests are + * passed and the health check service is stopped. + */ + private void setExplicitHealthCheckEnabled(boolean enabled) { + synchronized (sLock) { + mIsHealthCheckEnabled = enabled; + mHealthCheckController.setEnabled(enabled); + mSyncRequired = true; + // Prune to update internal state whenever health check is enabled/disabled + syncState("health check state " + (enabled ? "enabled" : "disabled")); + } + } + + /** + * This method should be only called on mShortTaskHandler, since it modifies + * {@link #mNumberOfNativeCrashPollsRemaining}. + */ + private void checkAndMitigateNativeCrashes() { + mNumberOfNativeCrashPollsRemaining--; + // Check if native watchdog reported a crash + if ("1".equals(SystemProperties.get("sys.init.updatable_crashing"))) { + // We rollback all available low impact rollbacks when crash is unattributable + notifyPackageFailure(Collections.EMPTY_LIST, FAILURE_REASON_NATIVE_CRASH); + // we stop polling after an attempt to execute rollback, regardless of whether the + // attempt succeeds or not + } else { + if (mNumberOfNativeCrashPollsRemaining > 0) { + mShortTaskHandler.postDelayed(() -> checkAndMitigateNativeCrashes(), + NATIVE_CRASH_POLLING_INTERVAL_MILLIS); + } + } + } + + /** + * Since this method can eventually trigger a rollback, it should be called + * only once boot has completed {@code onBootCompleted} and not earlier, because the install + * session must be entirely completed before we try to rollback. + * @hide + */ + public void scheduleCheckAndMitigateNativeCrashes() { + Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check " + + "and mitigate native crashes"); + mShortTaskHandler.post(()->checkAndMitigateNativeCrashes()); + } + + private int getUserImpactLevelLimit() { + return SystemProperties.getInt(MAJOR_USER_IMPACT_LEVEL_THRESHOLD, + DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD); + } + + private Set<String> getPackagesExemptFromImpactLevelThreshold() { + if (mPackagesExemptFromImpactLevelThreshold.isEmpty()) { + String packageNames = SystemProperties.get(PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD, + DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD); + return Set.of(packageNames.split("\\s*,\\s*")); + } + return mPackagesExemptFromImpactLevelThreshold; + } + + /** + * Indicates that a mitigation was successfully triggered or executed during + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} or + * {@link PackageHealthObserver#onExecuteBootLoopMitigation}. + */ + public static final int MITIGATION_RESULT_SUCCESS = + ObserverMitigationResult.MITIGATION_RESULT_SUCCESS; + + /** + * Indicates that a mitigation executed during + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} or + * {@link PackageHealthObserver#onExecuteBootLoopMitigation} was skipped. + */ + public static final int MITIGATION_RESULT_SKIPPED = + ObserverMitigationResult.MITIGATION_RESULT_SKIPPED; + + + /** + * Possible return values of the for mitigations executed during + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} and + * {@link PackageHealthObserver#onExecuteBootLoopMitigation}. + * @hide + */ + @Retention(SOURCE) + @IntDef(prefix = "MITIGATION_RESULT_", value = { + ObserverMitigationResult.MITIGATION_RESULT_SUCCESS, + ObserverMitigationResult.MITIGATION_RESULT_SKIPPED, + }) + public @interface ObserverMitigationResult { + int MITIGATION_RESULT_SUCCESS = 1; + int MITIGATION_RESULT_SKIPPED = 2; + } + + /** + * The minimum value that can be returned by any observer. + * It represents that no mitigations were available. + */ + public static final int USER_IMPACT_THRESHOLD_NONE = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + + /** + * The mitigation impact beyond which the user will start noticing the mitigations. + */ + public static final int USER_IMPACT_THRESHOLD_MEDIUM = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20; + + /** + * The mitigation impact beyond which the user impact is severely high. + */ + public static final int USER_IMPACT_THRESHOLD_HIGH = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; + + /** + * Possible severity values of the user impact of a + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation}. + * @hide + */ + @Retention(SOURCE) + @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_10, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_40, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_50, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_70, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_75, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_80, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_90, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_100}) + public @interface PackageHealthObserverImpact { + /** No action to take. */ + int USER_IMPACT_LEVEL_0 = 0; + /* Action has low user impact, user of a device will barely notice. */ + int USER_IMPACT_LEVEL_10 = 10; + /* Actions having medium user impact, user of a device will likely notice. */ + int USER_IMPACT_LEVEL_20 = 20; + int USER_IMPACT_LEVEL_30 = 30; + int USER_IMPACT_LEVEL_40 = 40; + int USER_IMPACT_LEVEL_50 = 50; + int USER_IMPACT_LEVEL_70 = 70; + /* Action has high user impact, a last resort, user of a device will be very frustrated. */ + int USER_IMPACT_LEVEL_71 = 71; + int USER_IMPACT_LEVEL_75 = 75; + int USER_IMPACT_LEVEL_80 = 80; + int USER_IMPACT_LEVEL_90 = 90; + int USER_IMPACT_LEVEL_100 = 100; + } + + /** Register instances of this interface to receive notifications on package failure. */ + @SuppressLint({"CallbackName"}) + public interface PackageHealthObserver { + /** + * Called when health check fails for the {@code versionedPackage}. + * Note: if the returned user impact is higher than {@link #USER_IMPACT_THRESHOLD_HIGH}, + * then {@link #onExecuteHealthCheckMitigation} would be called only in severe device + * conditions like boot-loop or network failure. + * + * @param versionedPackage the package that is failing. This may be null if a native + * service is crashing. + * @param failureReason the type of failure that is occurring. + * @param mitigationCount the number of times mitigation has been called for this package + * (including this time). + * + * @return any value greater than {@link #USER_IMPACT_THRESHOLD_NONE} to express + * the impact of mitigation on the user in {@link #onExecuteHealthCheckMitigation}. + * Returning {@link #USER_IMPACT_THRESHOLD_NONE} would indicate no mitigations available. + */ + @PackageHealthObserverImpact int onHealthCheckFailed( + @Nullable VersionedPackage versionedPackage, + @FailureReasons int failureReason, + int mitigationCount); + + /** + * This would be called after {@link #onHealthCheckFailed}. + * This is called only if current observer returned least impact mitigation for failed + * health check. + * + * @param versionedPackage the package that is failing. This may be null if a native + * service is crashing. + * @param failureReason the type of failure that is occurring. + * @param mitigationCount the number of times mitigation has been called for this package + * (including this time). + * @return {@link #MITIGATION_RESULT_SUCCESS} if the mitigation was successful, + * or {@link #MITIGATION_RESULT_SKIPPED} if the mitigation was skipped. + */ + @ObserverMitigationResult int onExecuteHealthCheckMitigation( + @Nullable VersionedPackage versionedPackage, + @FailureReasons int failureReason, int mitigationCount); + + + /** + * Called when the system server has booted several times within a window of time, defined + * by {@link #mBootThreshold} + * + * @param mitigationCount the number of times mitigation has been attempted for this + * boot loop (including this time). + * + * @return any value greater than {@link #USER_IMPACT_THRESHOLD_NONE} to express + * the impact of mitigation on the user in {@link #onExecuteBootLoopMitigation}. + * Returning {@link #USER_IMPACT_THRESHOLD_NONE} would indicate no mitigations available. + */ + default @PackageHealthObserverImpact int onBootLoop(int mitigationCount) { + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + + /** + * This would be called after {@link #onBootLoop}. + * This is called only if current observer returned least impact mitigation for fixing + * boot loop. + * + * @param mitigationCount the number of times mitigation has been attempted for this + * boot loop (including this time). + * + * @return {@link #MITIGATION_RESULT_SUCCESS} if the mitigation was successful, + * or {@link #MITIGATION_RESULT_SKIPPED} if the mitigation was skipped. + */ + default @ObserverMitigationResult int onExecuteBootLoopMitigation(int mitigationCount) { + return ObserverMitigationResult.MITIGATION_RESULT_SKIPPED; + } + + // TODO(b/120598832): Ensure uniqueness? + /** + * Identifier for the observer, should not change across device updates otherwise the + * watchdog may drop observing packages with the old name. + */ + @NonNull String getUniqueIdentifier(); + + /** + * An observer will not be pruned if this is set, even if the observer is not explicitly + * monitoring any packages. + */ + default boolean isPersistent() { + return false; + } + + /** + * Returns {@code true} if this observer wishes to observe the given package, {@code false} + * otherwise. + * Any failing package can be passed on to the observer. Currently the packages that have + * ANRs and perform {@link android.service.watchdog.ExplicitHealthCheckService} are being + * passed to observers in these API. + * + * <p> A persistent observer may choose to start observing certain failing packages, even if + * it has not explicitly asked to watch the package with {@link #startExplicitHealthCheck}. + */ + default boolean mayObservePackage(@NonNull String packageName) { + return false; + } + } + + @VisibleForTesting + long getTriggerFailureCount() { + synchronized (sLock) { + return mTriggerFailureCount; + } + } + + @VisibleForTesting + long getTriggerFailureDurationMs() { + synchronized (sLock) { + return mTriggerFailureDurationMs; + } + } + + /** + * Serializes and syncs health check requests with the {@link ExplicitHealthCheckController}. + */ + private void syncRequestsAsync() { + mShortTaskHandler.removeCallbacks(mSyncRequests); + mShortTaskHandler.post(mSyncRequests); + } + + /** + * Syncs health check requests with the {@link ExplicitHealthCheckController}. + * Calls to this must be serialized. + * + * @see #syncRequestsAsync + */ + private void syncRequests() { + boolean syncRequired = false; + synchronized (sLock) { + if (mIsPackagesReady) { + Set<String> packages = getPackagesPendingHealthChecksLocked(); + if (mSyncRequired || !packages.equals(mRequestedHealthCheckPackages) + || packages.isEmpty()) { + syncRequired = true; + mRequestedHealthCheckPackages = packages; + } + } // else, we will sync requests when packages become ready + } + + // Call outside lock to avoid holding lock when calling into the controller. + if (syncRequired) { + Slog.i(TAG, "Syncing health check requests for packages: " + + mRequestedHealthCheckPackages); + mHealthCheckController.syncRequests(mRequestedHealthCheckPackages); + mSyncRequired = false; + } + } + + /** + * Updates the observers monitoring {@code packageName} that explicit health check has passed. + * + * <p> This update is strictly for registered observers at the time of the call + * Observers that register after this signal will have no knowledge of prior signals and will + * effectively behave as if the explicit health check hasn't passed for {@code packageName}. + * + * <p> {@code packageName} can still be considered failed if reported by + * {@link #notifyPackageFailureLocked} before the package expires. + * + * <p> Triggered by components outside the system server when they are fully functional after an + * update. + */ + private void onHealthCheckPassed(String packageName) { + Slog.i(TAG, "Health check passed for package: " + packageName); + boolean isStateChanged = false; + + synchronized (sLock) { + for (int observerIdx = 0; observerIdx < mAllObservers.size(); observerIdx++) { + ObserverInternal observer = mAllObservers.valueAt(observerIdx); + MonitoredPackage monitoredPackage = observer.getMonitoredPackage(packageName); + + if (monitoredPackage != null) { + int oldState = monitoredPackage.getHealthCheckStateLocked(); + int newState = monitoredPackage.tryPassHealthCheckLocked(); + isStateChanged |= oldState != newState; + } + } + } + + if (isStateChanged) { + syncState("health check passed for " + packageName); + } + } + + private void onSupportedPackages(List<PackageConfig> supportedPackages) { + boolean isStateChanged = false; + + Map<String, Long> supportedPackageTimeouts = new ArrayMap<>(); + Iterator<PackageConfig> it = supportedPackages.iterator(); + while (it.hasNext()) { + PackageConfig info = it.next(); + supportedPackageTimeouts.put(info.getPackageName(), info.getHealthCheckTimeoutMillis()); + } + + synchronized (sLock) { + Slog.d(TAG, "Received supported packages " + supportedPackages); + Iterator<ObserverInternal> oit = mAllObservers.values().iterator(); + while (oit.hasNext()) { + Iterator<MonitoredPackage> pit = oit.next().getMonitoredPackages() + .values().iterator(); + while (pit.hasNext()) { + MonitoredPackage monitoredPackage = pit.next(); + String packageName = monitoredPackage.getName(); + int oldState = monitoredPackage.getHealthCheckStateLocked(); + int newState; + + if (supportedPackageTimeouts.containsKey(packageName)) { + // Supported packages become ACTIVE if currently INACTIVE + newState = monitoredPackage.setHealthCheckActiveLocked( + supportedPackageTimeouts.get(packageName)); + } else { + // Unsupported packages are marked as PASSED unless already FAILED + newState = monitoredPackage.tryPassHealthCheckLocked(); + } + isStateChanged |= oldState != newState; + } + } + } + + if (isStateChanged) { + syncState("updated health check supported packages " + supportedPackages); + } + } + + private void onSyncRequestNotified() { + synchronized (sLock) { + mSyncRequired = true; + syncRequestsAsync(); + } + } + + @GuardedBy("sLock") + private Set<String> getPackagesPendingHealthChecksLocked() { + Set<String> packages = new ArraySet<>(); + Iterator<ObserverInternal> oit = mAllObservers.values().iterator(); + while (oit.hasNext()) { + ObserverInternal observer = oit.next(); + Iterator<MonitoredPackage> pit = + observer.getMonitoredPackages().values().iterator(); + while (pit.hasNext()) { + MonitoredPackage monitoredPackage = pit.next(); + String packageName = monitoredPackage.getName(); + if (monitoredPackage.isPendingHealthChecksLocked()) { + packages.add(packageName); + } + } + } + return packages; + } + + /** + * Syncs the state of the observers. + * + * <p> Prunes all observers, saves new state to disk, syncs health check requests with the + * health check service and schedules the next state sync. + */ + private void syncState(String reason) { + synchronized (sLock) { + Slog.i(TAG, "Syncing state, reason: " + reason); + pruneObserversLocked(); + + saveToFileAsync(); + syncRequestsAsync(); + + // Done syncing state, schedule the next state sync + scheduleNextSyncStateLocked(); + } + } + + private void syncStateWithScheduledReason() { + syncState("scheduled"); + } + + @GuardedBy("sLock") + private void scheduleNextSyncStateLocked() { + long durationMs = getNextStateSyncMillisLocked(); + mShortTaskHandler.removeCallbacks(mSyncStateWithScheduledReason); + if (durationMs == Long.MAX_VALUE) { + Slog.i(TAG, "Cancelling state sync, nothing to sync"); + mUptimeAtLastStateSync = 0; + } else { + mUptimeAtLastStateSync = mSystemClock.uptimeMillis(); + mShortTaskHandler.postDelayed(mSyncStateWithScheduledReason, durationMs); + } + } + + /** + * Returns the next duration in millis to sync the watchdog state. + * + * @returns Long#MAX_VALUE if there are no observed packages. + */ + @GuardedBy("sLock") + private long getNextStateSyncMillisLocked() { + long shortestDurationMs = Long.MAX_VALUE; + for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { + ArrayMap<String, MonitoredPackage> packages = mAllObservers.valueAt(oIndex) + .getMonitoredPackages(); + for (int pIndex = 0; pIndex < packages.size(); pIndex++) { + MonitoredPackage mp = packages.valueAt(pIndex); + long duration = mp.getShortestScheduleDurationMsLocked(); + if (duration < shortestDurationMs) { + shortestDurationMs = duration; + } + } + } + return shortestDurationMs; + } + + /** + * Removes {@code elapsedMs} milliseconds from all durations on monitored packages + * and updates other internal state. + */ + @GuardedBy("sLock") + private void pruneObserversLocked() { + long elapsedMs = mUptimeAtLastStateSync == 0 + ? 0 : mSystemClock.uptimeMillis() - mUptimeAtLastStateSync; + if (elapsedMs <= 0) { + Slog.i(TAG, "Not pruning observers, elapsed time: " + elapsedMs + "ms"); + return; + } + + Iterator<ObserverInternal> it = mAllObservers.values().iterator(); + while (it.hasNext()) { + ObserverInternal observer = it.next(); + Set<MonitoredPackage> failedPackages = + observer.prunePackagesLocked(elapsedMs); + if (!failedPackages.isEmpty()) { + onHealthCheckFailed(observer, failedPackages); + } + if (observer.getMonitoredPackages().isEmpty() && (observer.registeredObserver == null + || !observer.registeredObserver.isPersistent())) { + Slog.i(TAG, "Discarding observer " + observer.name + ". All packages expired"); + it.remove(); + } + } + } + + private void onHealthCheckFailed(ObserverInternal observer, + Set<MonitoredPackage> failedPackages) { + mLongTaskHandler.post(() -> { + synchronized (sLock) { + PackageHealthObserver registeredObserver = observer.registeredObserver; + if (registeredObserver != null) { + Iterator<MonitoredPackage> it = failedPackages.iterator(); + while (it.hasNext()) { + VersionedPackage versionedPkg = getVersionedPackage(it.next().getName()); + if (versionedPkg != null) { + Slog.i(TAG, + "Explicit health check failed for package " + versionedPkg); + observer.observerExecutor.execute(() -> + registeredObserver.onExecuteHealthCheckMitigation(versionedPkg, + PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK, + 1)); + } + } + } + } + }); + } + + /** + * Gets PackageInfo for the given package. Matches any user and apex. + * + * @throws PackageManager.NameNotFoundException if no such package is installed. + */ + private PackageInfo getPackageInfo(String packageName) + throws PackageManager.NameNotFoundException { + PackageManager pm = mContext.getPackageManager(); + try { + // The MATCH_ANY_USER flag doesn't mix well with the MATCH_APEX + // flag, so make two separate attempts to get the package info. + // We don't need both flags at the same time because we assume + // apex files are always installed for all users. + return pm.getPackageInfo(packageName, PackageManager.MATCH_ANY_USER); + } catch (PackageManager.NameNotFoundException e) { + return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); + } + } + + @Nullable + private VersionedPackage getVersionedPackage(String packageName) { + final PackageManager pm = mContext.getPackageManager(); + if (pm == null || TextUtils.isEmpty(packageName)) { + return null; + } + try { + final long versionCode = getPackageInfo(packageName).getLongVersionCode(); + return new VersionedPackage(packageName, versionCode); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + /** + * Loads mAllObservers from file. + * + * <p>Note that this is <b>not</b> thread safe and should only called be called + * from the constructor. + */ + private void loadFromFile() { + InputStream infile = null; + mAllObservers.clear(); + try { + infile = mPolicyFile.openRead(); + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(infile, UTF_8.name()); + XmlUtils.beginDocument(parser, TAG_PACKAGE_WATCHDOG); + int outerDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + ObserverInternal observer = ObserverInternal.read(parser, this); + if (observer != null) { + mAllObservers.put(observer.name, observer); + } + } + } catch (FileNotFoundException e) { + // Nothing to monitor + } catch (Exception e) { + Slog.wtf(TAG, "Unable to read monitored packages, deleting file", e); + mPolicyFile.delete(); + } finally { + IoUtils.closeQuietly(infile); + } + } + + private void onPropertyChanged(DeviceConfig.Properties properties) { + try { + updateConfigs(); + } catch (Exception ignore) { + Slog.w(TAG, "Failed to reload device config changes"); + } + } + + /** Adds a {@link DeviceConfig#OnPropertiesChangedListener}. */ + private void setPropertyChangedListenerLocked() { + DeviceConfig.addOnPropertiesChangedListener( + DeviceConfig.NAMESPACE_ROLLBACK, + mContext.getMainExecutor(), + mOnPropertyChangedListener); + } + + @VisibleForTesting + void removePropertyChangedListener() { + DeviceConfig.removeOnPropertiesChangedListener(mOnPropertyChangedListener); + } + + /** + * Health check is enabled or disabled after reading the flags + * from DeviceConfig. + */ + @VisibleForTesting + void updateConfigs() { + synchronized (sLock) { + mTriggerFailureCount = DeviceConfig.getInt( + DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + DEFAULT_TRIGGER_FAILURE_COUNT); + if (mTriggerFailureCount <= 0) { + mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT; + } + + mTriggerFailureDurationMs = DeviceConfig.getInt( + DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS, + DEFAULT_TRIGGER_FAILURE_DURATION_MS); + if (mTriggerFailureDurationMs <= 0) { + mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS; + } + + setExplicitHealthCheckEnabled(DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED, + DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED)); + } + } + + /** + * Persists mAllObservers to file. Threshold information is ignored. + */ + private boolean saveToFile() { + Slog.i(TAG, "Saving observer state to file"); + synchronized (sLock) { + FileOutputStream stream; + try { + stream = mPolicyFile.startWrite(); + } catch (IOException e) { + Slog.w(TAG, "Cannot update monitored packages", e); + return false; + } + + try { + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(stream, UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_PACKAGE_WATCHDOG); + out.attribute(null, ATTR_VERSION, Integer.toString(DB_VERSION)); + for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { + mAllObservers.valueAt(oIndex).writeLocked(out); + } + out.endTag(null, TAG_PACKAGE_WATCHDOG); + out.endDocument(); + mPolicyFile.finishWrite(stream); + return true; + } catch (IOException e) { + Slog.w(TAG, "Failed to save monitored packages, restoring backup", e); + mPolicyFile.failWrite(stream); + return false; + } + } + } + + private void saveToFileAsync() { + if (!mLongTaskHandler.hasCallbacks(mSaveToFile)) { + mLongTaskHandler.post(mSaveToFile); + } + } + + /** @hide Convert a {@code LongArrayQueue} to a String of comma-separated values. */ + public static String longArrayQueueToString(LongArrayQueue queue) { + if (queue.size() > 0) { + StringBuilder sb = new StringBuilder(); + sb.append(queue.get(0)); + for (int i = 1; i < queue.size(); i++) { + sb.append(","); + sb.append(queue.get(i)); + } + return sb.toString(); + } + return ""; + } + + /** @hide Parse a comma-separated String of longs into a LongArrayQueue. */ + public static LongArrayQueue parseLongArrayQueue(String commaSeparatedValues) { + LongArrayQueue result = new LongArrayQueue(); + if (!TextUtils.isEmpty(commaSeparatedValues)) { + String[] values = commaSeparatedValues.split(","); + for (String value : values) { + result.addLast(Long.parseLong(value)); + } + } + return result; + } + + + /** Dump status of every observer in mAllObservers. */ + public void dump(@NonNull PrintWriter pw) { + if (Flags.synchronousRebootInRescueParty() && isRecoveryTriggeredReboot()) { + dumpInternal(pw); + } else { + synchronized (sLock) { + dumpInternal(pw); + } + } + } + + /** + * Check if we're currently attempting to reboot during mitigation. This method must return + * true if triggered reboot early during a boot loop, since the device will not be fully booted + * at this time. + */ + public static boolean isRecoveryTriggeredReboot() { + return isFactoryResetPropertySet() || isRebootPropertySet(); + } + + private static boolean isFactoryResetPropertySet() { + return CrashRecoveryProperties.attemptingFactoryReset().orElse(false); + } + + private static boolean isRebootPropertySet() { + return CrashRecoveryProperties.attemptingReboot().orElse(false); + } + + private void dumpInternal(@NonNull PrintWriter pw) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + ipw.println("Package Watchdog status"); + ipw.increaseIndent(); + synchronized (sLock) { + for (String observerName : mAllObservers.keySet()) { + ipw.println("Observer name: " + observerName); + ipw.increaseIndent(); + ObserverInternal observerInternal = mAllObservers.get(observerName); + observerInternal.dump(ipw); + ipw.decreaseIndent(); + } + } + ipw.decreaseIndent(); + dumpCrashRecoveryEvents(ipw); + } + + @VisibleForTesting + @GuardedBy("sLock") + void registerObserverInternal(ObserverInternal observerInternal) { + mAllObservers.put(observerInternal.name, observerInternal); + } + + /** + * Represents an observer monitoring a set of packages along with the failure thresholds for + * each package. + * + * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing + * instances of this class. + */ + static class ObserverInternal { + public final String name; + @GuardedBy("sLock") + private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>(); + @Nullable + @GuardedBy("sLock") + public PackageHealthObserver registeredObserver; + public Executor observerExecutor; + private int mMitigationCount; + + ObserverInternal(String name, List<MonitoredPackage> packages) { + this(name, packages, /*mitigationCount=*/ 0); + } + + ObserverInternal(String name, List<MonitoredPackage> packages, int mitigationCount) { + this.name = name; + updatePackagesLocked(packages); + this.mMitigationCount = mitigationCount; + } + + /** + * Writes important {@link MonitoredPackage} details for this observer to file. + * Does not persist any package failure thresholds. + */ + @GuardedBy("sLock") + public boolean writeLocked(XmlSerializer out) { + try { + out.startTag(null, TAG_OBSERVER); + out.attribute(null, ATTR_NAME, name); + if (Flags.recoverabilityDetection()) { + out.attribute(null, ATTR_MITIGATION_COUNT, Integer.toString(mMitigationCount)); + } + for (int i = 0; i < mPackages.size(); i++) { + MonitoredPackage p = mPackages.valueAt(i); + p.writeLocked(out); + } + out.endTag(null, TAG_OBSERVER); + return true; + } catch (IOException e) { + Slog.w(TAG, "Cannot save observer", e); + return false; + } + } + + public int getBootMitigationCount() { + return mMitigationCount; + } + + public void setBootMitigationCount(int mitigationCount) { + mMitigationCount = mitigationCount; + } + + @GuardedBy("sLock") + public void updatePackagesLocked(List<MonitoredPackage> packages) { + for (int pIndex = 0; pIndex < packages.size(); pIndex++) { + MonitoredPackage p = packages.get(pIndex); + MonitoredPackage existingPackage = getMonitoredPackage(p.getName()); + if (existingPackage != null) { + existingPackage.updateHealthCheckDuration(p.mDurationMs); + } else { + putMonitoredPackage(p); + } + } + } + + /** + * Reduces the monitoring durations of all packages observed by this observer by + * {@code elapsedMs}. If any duration is less than 0, the package is removed from + * observation. If any health check duration is less than 0, the health check result + * is evaluated. + * + * @return a {@link Set} of packages that were removed from the observer without explicit + * health check passing, or an empty list if no package expired for which an explicit health + * check was still pending + */ + @GuardedBy("sLock") + private Set<MonitoredPackage> prunePackagesLocked(long elapsedMs) { + Set<MonitoredPackage> failedPackages = new ArraySet<>(); + Iterator<MonitoredPackage> it = mPackages.values().iterator(); + while (it.hasNext()) { + MonitoredPackage p = it.next(); + int oldState = p.getHealthCheckStateLocked(); + int newState = p.handleElapsedTimeLocked(elapsedMs); + if (oldState != HealthCheckState.FAILED + && newState == HealthCheckState.FAILED) { + Slog.i(TAG, "Package " + p.getName() + " failed health check"); + failedPackages.add(p); + } + if (p.isExpiredLocked()) { + it.remove(); + } + } + return failedPackages; + } + + /** + * Increments failure counts of {@code packageName}. + * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise + * @hide + */ + @GuardedBy("sLock") + public boolean notifyPackageFailureLocked(String packageName) { + if (getMonitoredPackage(packageName) == null && registeredObserver.isPersistent() + && registeredObserver.mayObservePackage(packageName)) { + putMonitoredPackage(sPackageWatchdog.newMonitoredPackage( + packageName, DEFAULT_OBSERVING_DURATION_MS, false)); + } + MonitoredPackage p = getMonitoredPackage(packageName); + if (p != null) { + return p.onFailureLocked(); + } + return false; + } + + /** + * Returns the map of packages monitored by this observer. + * + * @return a mapping of package names to {@link MonitoredPackage} objects. + */ + @GuardedBy("sLock") + public ArrayMap<String, MonitoredPackage> getMonitoredPackages() { + return mPackages; + } + + /** + * Returns the {@link MonitoredPackage} associated with a given package name if the + * package is being monitored by this observer. + * + * @param packageName: the name of the package. + * @return the {@link MonitoredPackage} object associated with the package name if one + * exists, {@code null} otherwise. + */ + @GuardedBy("sLock") + @Nullable + public MonitoredPackage getMonitoredPackage(String packageName) { + return mPackages.get(packageName); + } + + /** + * Associates a {@link MonitoredPackage} with the observer. + * + * @param p: the {@link MonitoredPackage} to store. + */ + @GuardedBy("sLock") + public void putMonitoredPackage(MonitoredPackage p) { + mPackages.put(p.getName(), p); + } + + /** + * Returns one ObserverInternal from the {@code parser} and advances its state. + * + * <p>Note that this method is <b>not</b> thread safe. It should only be called from + * #loadFromFile which in turn is only called on construction of the + * singleton PackageWatchdog. + **/ + public static ObserverInternal read(XmlPullParser parser, PackageWatchdog watchdog) { + String observerName = null; + int observerMitigationCount = 0; + if (TAG_OBSERVER.equals(parser.getName())) { + observerName = parser.getAttributeValue(null, ATTR_NAME); + if (TextUtils.isEmpty(observerName)) { + Slog.wtf(TAG, "Unable to read observer name"); + return null; + } + } + List<MonitoredPackage> packages = new ArrayList<>(); + int innerDepth = parser.getDepth(); + try { + if (Flags.recoverabilityDetection()) { + try { + observerMitigationCount = Integer.parseInt( + parser.getAttributeValue(null, ATTR_MITIGATION_COUNT)); + } catch (Exception e) { + Slog.i( + TAG, + "ObserverInternal mitigation count was not present."); + } + } + while (XmlUtils.nextElementWithin(parser, innerDepth)) { + if (TAG_PACKAGE.equals(parser.getName())) { + try { + MonitoredPackage pkg = watchdog.parseMonitoredPackage(parser); + if (pkg != null) { + packages.add(pkg); + } + } catch (NumberFormatException e) { + Slog.wtf(TAG, "Skipping package for observer " + observerName, e); + continue; + } + } + } + } catch (XmlPullParserException | IOException e) { + Slog.wtf(TAG, "Unable to read observer " + observerName, e); + return null; + } + if (packages.isEmpty()) { + return null; + } + return new ObserverInternal(observerName, packages, observerMitigationCount); + } + + /** Dumps information about this observer and the packages it watches. */ + public void dump(IndentingPrintWriter pw) { + boolean isPersistent = registeredObserver != null && registeredObserver.isPersistent(); + pw.println("Persistent: " + isPersistent); + for (String packageName : mPackages.keySet()) { + MonitoredPackage p = getMonitoredPackage(packageName); + pw.println(packageName + ": "); + pw.increaseIndent(); + pw.println("# Failures: " + p.mFailureHistory.size()); + pw.println("Monitoring duration remaining: " + p.mDurationMs + "ms"); + pw.println("Explicit health check duration: " + p.mHealthCheckDurationMs + "ms"); + pw.println("Health check state: " + p.toString(p.mHealthCheckState)); + pw.decreaseIndent(); + } + } + } + + /** @hide */ + @Retention(SOURCE) + @IntDef(value = { + HealthCheckState.ACTIVE, + HealthCheckState.INACTIVE, + HealthCheckState.PASSED, + HealthCheckState.FAILED}) + public @interface HealthCheckState { + // The package has not passed health check but has requested a health check + int ACTIVE = 0; + // The package has not passed health check and has not requested a health check + int INACTIVE = 1; + // The package has passed health check + int PASSED = 2; + // The package has failed health check + int FAILED = 3; + } + + MonitoredPackage newMonitoredPackage( + String name, long durationMs, boolean hasPassedHealthCheck) { + return newMonitoredPackage(name, durationMs, Long.MAX_VALUE, hasPassedHealthCheck, + new LongArrayQueue()); + } + + MonitoredPackage newMonitoredPackage(String name, long durationMs, long healthCheckDurationMs, + boolean hasPassedHealthCheck, LongArrayQueue mitigationCalls) { + return new MonitoredPackage(name, durationMs, healthCheckDurationMs, + hasPassedHealthCheck, mitigationCalls); + } + + MonitoredPackage parseMonitoredPackage(XmlPullParser parser) + throws XmlPullParserException { + String packageName = parser.getAttributeValue(null, ATTR_NAME); + long duration = Long.parseLong(parser.getAttributeValue(null, ATTR_DURATION)); + long healthCheckDuration = Long.parseLong(parser.getAttributeValue(null, + ATTR_EXPLICIT_HEALTH_CHECK_DURATION)); + boolean hasPassedHealthCheck = Boolean.parseBoolean(parser.getAttributeValue(null, + ATTR_PASSED_HEALTH_CHECK)); + LongArrayQueue mitigationCalls = parseLongArrayQueue( + parser.getAttributeValue(null, ATTR_MITIGATION_CALLS)); + return newMonitoredPackage(packageName, + duration, healthCheckDuration, hasPassedHealthCheck, mitigationCalls); + } + + /** + * Represents a package and its health check state along with the time + * it should be monitored for. + * + * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing + * instances of this class. + */ + class MonitoredPackage { + private final String mPackageName; + // Times when package failures happen sorted in ascending order + @GuardedBy("sLock") + private final LongArrayQueue mFailureHistory = new LongArrayQueue(); + // Times when an observer was called to mitigate this package's failure. Sorted in + // ascending order. + @GuardedBy("sLock") + private final LongArrayQueue mMitigationCalls; + // One of STATE_[ACTIVE|INACTIVE|PASSED|FAILED]. Updated on construction and after + // methods that could change the health check state: handleElapsedTimeLocked and + // tryPassHealthCheckLocked + private int mHealthCheckState = HealthCheckState.INACTIVE; + // Whether an explicit health check has passed. + // This value in addition with mHealthCheckDurationMs determines the health check state + // of the package, see #getHealthCheckStateLocked + @GuardedBy("sLock") + private boolean mHasPassedHealthCheck; + // System uptime duration to monitor package. + @GuardedBy("sLock") + private long mDurationMs; + // System uptime duration to check the result of an explicit health check + // Initially, MAX_VALUE until we get a value from the health check service + // and request health checks. + // This value in addition with mHasPassedHealthCheck determines the health check state + // of the package, see #getHealthCheckStateLocked + @GuardedBy("sLock") + private long mHealthCheckDurationMs = Long.MAX_VALUE; + + MonitoredPackage(String packageName, long durationMs, + long healthCheckDurationMs, boolean hasPassedHealthCheck, + LongArrayQueue mitigationCalls) { + mPackageName = packageName; + mDurationMs = durationMs; + mHealthCheckDurationMs = healthCheckDurationMs; + mHasPassedHealthCheck = hasPassedHealthCheck; + mMitigationCalls = mitigationCalls; + updateHealthCheckStateLocked(); + } + + /** Writes the salient fields to disk using {@code out}. + * @hide + */ + @GuardedBy("sLock") + public void writeLocked(XmlSerializer out) throws IOException { + out.startTag(null, TAG_PACKAGE); + out.attribute(null, ATTR_NAME, getName()); + out.attribute(null, ATTR_DURATION, Long.toString(mDurationMs)); + out.attribute(null, ATTR_EXPLICIT_HEALTH_CHECK_DURATION, + Long.toString(mHealthCheckDurationMs)); + out.attribute(null, ATTR_PASSED_HEALTH_CHECK, Boolean.toString(mHasPassedHealthCheck)); + LongArrayQueue normalizedCalls = normalizeMitigationCalls(); + out.attribute(null, ATTR_MITIGATION_CALLS, longArrayQueueToString(normalizedCalls)); + out.endTag(null, TAG_PACKAGE); + } + + /** + * Increment package failures or resets failure count depending on the last package failure. + * + * @return {@code true} if failure count exceeds a threshold, {@code false} otherwise + */ + @GuardedBy("sLock") + public boolean onFailureLocked() { + // Sliding window algorithm: find out if there exists a window containing failures >= + // mTriggerFailureCount. + final long now = mSystemClock.uptimeMillis(); + mFailureHistory.addLast(now); + while (now - mFailureHistory.peekFirst() > mTriggerFailureDurationMs) { + // Prune values falling out of the window + mFailureHistory.removeFirst(); + } + boolean failed = mFailureHistory.size() >= mTriggerFailureCount; + if (failed) { + mFailureHistory.clear(); + } + return failed; + } + + /** + * Notes the timestamp of a mitigation call into the observer. + */ + @GuardedBy("sLock") + public void noteMitigationCallLocked() { + mMitigationCalls.addLast(mSystemClock.uptimeMillis()); + } + + /** + * Prunes any mitigation calls outside of the de-escalation window, and returns the + * number of calls that are in the window afterwards. + * + * @return the number of mitigation calls made in the de-escalation window. + */ + @GuardedBy("sLock") + public int getMitigationCountLocked() { + try { + final long now = mSystemClock.uptimeMillis(); + while (now - mMitigationCalls.peekFirst() > DEFAULT_DEESCALATION_WINDOW_MS) { + mMitigationCalls.removeFirst(); + } + } catch (NoSuchElementException ignore) { + } + + return mMitigationCalls.size(); + } + + /** + * Before writing to disk, make the mitigation call timestamps relative to the current + * system uptime. This is because they need to be relative to the uptime which will reset + * at the next boot. + * + * @return a LongArrayQueue of the mitigation calls relative to the current system uptime. + */ + @GuardedBy("sLock") + public LongArrayQueue normalizeMitigationCalls() { + LongArrayQueue normalized = new LongArrayQueue(); + final long now = mSystemClock.uptimeMillis(); + for (int i = 0; i < mMitigationCalls.size(); i++) { + normalized.addLast(mMitigationCalls.get(i) - now); + } + return normalized; + } + + /** + * Sets the initial health check duration. + * + * @return the new health check state + */ + @GuardedBy("sLock") + public int setHealthCheckActiveLocked(long initialHealthCheckDurationMs) { + if (initialHealthCheckDurationMs <= 0) { + Slog.wtf(TAG, "Cannot set non-positive health check duration " + + initialHealthCheckDurationMs + "ms for package " + getName() + + ". Using total duration " + mDurationMs + "ms instead"); + initialHealthCheckDurationMs = mDurationMs; + } + if (mHealthCheckState == HealthCheckState.INACTIVE) { + // Transitions to ACTIVE + mHealthCheckDurationMs = initialHealthCheckDurationMs; + } + return updateHealthCheckStateLocked(); + } + + /** + * Updates the monitoring durations of the package. + * + * @return the new health check state + */ + @GuardedBy("sLock") + public int handleElapsedTimeLocked(long elapsedMs) { + if (elapsedMs <= 0) { + Slog.w(TAG, "Cannot handle non-positive elapsed time for package " + getName()); + return mHealthCheckState; + } + // Transitions to FAILED if now <= 0 and health check not passed + mDurationMs -= elapsedMs; + if (mHealthCheckState == HealthCheckState.ACTIVE) { + // We only update health check durations if we have #setHealthCheckActiveLocked + // This ensures we don't leave the INACTIVE state for an unexpected elapsed time + // Transitions to FAILED if now <= 0 and health check not passed + mHealthCheckDurationMs -= elapsedMs; + } + return updateHealthCheckStateLocked(); + } + + /** Explicitly update the monitoring duration of the package. */ + @GuardedBy("sLock") + public void updateHealthCheckDuration(long newDurationMs) { + mDurationMs = newDurationMs; + } + + /** + * Marks the health check as passed and transitions to {@link HealthCheckState.PASSED} + * if not yet {@link HealthCheckState.FAILED}. + * + * @return the new {@link HealthCheckState health check state} + */ + @GuardedBy("sLock") + @HealthCheckState + public int tryPassHealthCheckLocked() { + if (mHealthCheckState != HealthCheckState.FAILED) { + // FAILED is a final state so only pass if we haven't failed + // Transition to PASSED + mHasPassedHealthCheck = true; + } + return updateHealthCheckStateLocked(); + } + + /** Returns the monitored package name. */ + private String getName() { + return mPackageName; + } + + /** + * Returns the current {@link HealthCheckState health check state}. + */ + @GuardedBy("sLock") + @HealthCheckState + public int getHealthCheckStateLocked() { + return mHealthCheckState; + } + + /** + * Returns the shortest duration before the package should be scheduled for a prune. + * + * @return the duration or {@link Long#MAX_VALUE} if the package should not be scheduled + */ + @GuardedBy("sLock") + public long getShortestScheduleDurationMsLocked() { + // Consider health check duration only if #isPendingHealthChecksLocked is true + return Math.min(toPositive(mDurationMs), + isPendingHealthChecksLocked() + ? toPositive(mHealthCheckDurationMs) : Long.MAX_VALUE); + } + + /** + * Returns {@code true} if the total duration left to monitor the package is less than or + * equal to 0 {@code false} otherwise. + */ + @GuardedBy("sLock") + public boolean isExpiredLocked() { + return mDurationMs <= 0; + } + + /** + * Returns {@code true} if the package, {@link #getName} is expecting health check results + * {@code false} otherwise. + */ + @GuardedBy("sLock") + public boolean isPendingHealthChecksLocked() { + return mHealthCheckState == HealthCheckState.ACTIVE + || mHealthCheckState == HealthCheckState.INACTIVE; + } + + /** + * Updates the health check state based on {@link #mHasPassedHealthCheck} + * and {@link #mHealthCheckDurationMs}. + * + * @return the new {@link HealthCheckState health check state} + */ + @GuardedBy("sLock") + @HealthCheckState + private int updateHealthCheckStateLocked() { + int oldState = mHealthCheckState; + if (mHasPassedHealthCheck) { + // Set final state first to avoid ambiguity + mHealthCheckState = HealthCheckState.PASSED; + } else if (mHealthCheckDurationMs <= 0 || mDurationMs <= 0) { + // Set final state first to avoid ambiguity + mHealthCheckState = HealthCheckState.FAILED; + } else if (mHealthCheckDurationMs == Long.MAX_VALUE) { + mHealthCheckState = HealthCheckState.INACTIVE; + } else { + mHealthCheckState = HealthCheckState.ACTIVE; + } + + if (oldState != mHealthCheckState) { + Slog.i(TAG, "Updated health check state for package " + getName() + ": " + + toString(oldState) + " -> " + toString(mHealthCheckState)); + } + return mHealthCheckState; + } + + /** Returns a {@link String} representation of the current health check state. */ + private String toString(@HealthCheckState int state) { + switch (state) { + case HealthCheckState.ACTIVE: + return "ACTIVE"; + case HealthCheckState.INACTIVE: + return "INACTIVE"; + case HealthCheckState.PASSED: + return "PASSED"; + case HealthCheckState.FAILED: + return "FAILED"; + default: + return "UNKNOWN"; + } + } + + /** Returns {@code value} if it is greater than 0 or {@link Long#MAX_VALUE} otherwise. */ + private long toPositive(long value) { + return value > 0 ? value : Long.MAX_VALUE; + } + + /** Compares the equality of this object with another {@link MonitoredPackage}. */ + @VisibleForTesting + boolean isEqualTo(MonitoredPackage pkg) { + return (getName().equals(pkg.getName())) + && mDurationMs == pkg.mDurationMs + && mHasPassedHealthCheck == pkg.mHasPassedHealthCheck + && mHealthCheckDurationMs == pkg.mHealthCheckDurationMs + && (mMitigationCalls.toString()).equals(pkg.mMitigationCalls.toString()); + } + } + + @GuardedBy("sLock") + @SuppressWarnings("GuardedBy") + void saveAllObserversBootMitigationCountToMetadata(String filePath) { + HashMap<String, Integer> bootMitigationCounts = new HashMap<>(); + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + bootMitigationCounts.put(observer.name, observer.getBootMitigationCount()); + } + + FileOutputStream fileStream = null; + ObjectOutputStream objectStream = null; + try { + fileStream = new FileOutputStream(new File(filePath)); + objectStream = new ObjectOutputStream(fileStream); + objectStream.writeObject(bootMitigationCounts); + objectStream.flush(); + } catch (Exception e) { + Slog.i(TAG, "Could not save observers metadata to file: " + e); + return; + } finally { + IoUtils.closeQuietly(objectStream); + IoUtils.closeQuietly(fileStream); + } + } + + /** + * Handles the thresholding logic for system server boots. + */ + class BootThreshold { + + private final int mBootTriggerCount; + private final long mTriggerWindow; + + BootThreshold(int bootTriggerCount, long triggerWindow) { + this.mBootTriggerCount = bootTriggerCount; + this.mTriggerWindow = triggerWindow; + } + + public void reset() { + setStart(0); + setCount(0); + } + + protected int getCount() { + return CrashRecoveryProperties.rescueBootCount().orElse(0); + } + + protected void setCount(int count) { + CrashRecoveryProperties.rescueBootCount(count); + } + + public long getStart() { + return CrashRecoveryProperties.rescueBootStart().orElse(0L); + } + + public int getMitigationCount() { + return CrashRecoveryProperties.bootMitigationCount().orElse(0); + } + + public void setStart(long start) { + CrashRecoveryProperties.rescueBootStart(getStartTime(start)); + } + + public void setMitigationStart(long start) { + CrashRecoveryProperties.bootMitigationStart(getStartTime(start)); + } + + public long getMitigationStart() { + return CrashRecoveryProperties.bootMitigationStart().orElse(0L); + } + + public void setMitigationCount(int count) { + CrashRecoveryProperties.bootMitigationCount(count); + } + + private static long constrain(long amount, long low, long high) { + return amount < low ? low : (amount > high ? high : amount); + } + + public long getStartTime(long start) { + final long now = mSystemClock.uptimeMillis(); + return constrain(start, 0, now); + } + + public void saveMitigationCountToMetadata() { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(METADATA_FILE))) { + writer.write(String.valueOf(getMitigationCount())); + } catch (Exception e) { + Slog.e(TAG, "Could not save metadata to file: " + e); + } + } + + public void readMitigationCountFromMetadataIfNecessary() { + File bootPropsFile = new File(METADATA_FILE); + if (bootPropsFile.exists()) { + try (BufferedReader reader = new BufferedReader(new FileReader(METADATA_FILE))) { + String mitigationCount = reader.readLine(); + setMitigationCount(Integer.parseInt(mitigationCount)); + bootPropsFile.delete(); + } catch (Exception e) { + Slog.i(TAG, "Could not read metadata file: " + e); + } + } + } + + + /** Increments the boot counter, and returns whether the device is bootlooping. */ + @GuardedBy("sLock") + public boolean incrementAndTest() { + if (Flags.recoverabilityDetection()) { + readAllObserversBootMitigationCountIfNecessary(METADATA_FILE); + } else { + readMitigationCountFromMetadataIfNecessary(); + } + + final long now = mSystemClock.uptimeMillis(); + if (now - getStart() < 0) { + Slog.e(TAG, "Window was less than zero. Resetting start to current time."); + setStart(now); + setMitigationStart(now); + } + if (now - getMitigationStart() > DEFAULT_DEESCALATION_WINDOW_MS) { + setMitigationStart(now); + if (Flags.recoverabilityDetection()) { + resetAllObserversBootMitigationCount(); + } else { + setMitigationCount(0); + } + } + final long window = now - getStart(); + if (window >= mTriggerWindow) { + setCount(1); + setStart(now); + return false; + } else { + int count = getCount() + 1; + setCount(count); + EventLog.writeEvent(LOG_TAG_RESCUE_NOTE, Process.ROOT_UID, count, window); + if (Flags.recoverabilityDetection()) { + // After a reboot (e.g. by WARM_REBOOT or mainline rollback) we apply + // mitigations without waiting for DEFAULT_BOOT_LOOP_TRIGGER_COUNT. + return (count >= mBootTriggerCount) + || (performedMitigationsDuringWindow() && count > 1); + } + return count >= mBootTriggerCount; + } + } + + @GuardedBy("sLock") + private boolean performedMitigationsDuringWindow() { + for (ObserverInternal observerInternal: mAllObservers.values()) { + if (observerInternal.getBootMitigationCount() > 0) { + return true; + } + } + return false; + } + + @GuardedBy("sLock") + private void resetAllObserversBootMitigationCount() { + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + observer.setBootMitigationCount(0); + } + saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); + } + + @GuardedBy("sLock") + @SuppressWarnings("GuardedBy") + void readAllObserversBootMitigationCountIfNecessary(String filePath) { + File metadataFile = new File(filePath); + if (metadataFile.exists()) { + FileInputStream fileStream = null; + ObjectInputStream objectStream = null; + HashMap<String, Integer> bootMitigationCounts = null; + try { + fileStream = new FileInputStream(metadataFile); + objectStream = new ObjectInputStream(fileStream); + bootMitigationCounts = + (HashMap<String, Integer>) objectStream.readObject(); + } catch (Exception e) { + Slog.i(TAG, "Could not read observer metadata file: " + e); + return; + } finally { + IoUtils.closeQuietly(objectStream); + IoUtils.closeQuietly(fileStream); + } + + if (bootMitigationCounts == null || bootMitigationCounts.isEmpty()) { + Slog.i(TAG, "No observer in metadata file"); + return; + } + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + if (bootMitigationCounts.containsKey(observer.name)) { + observer.setBootMitigationCount( + bootMitigationCounts.get(observer.name)); + } + } + } + } + } + + /** + * Register broadcast receiver for shutdown. + * We would save the observer state to persist across boots. + * + * @hide + */ + public void registerShutdownBroadcastReceiver() { + BroadcastReceiver shutdownEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Only write if intent is relevant to device reboot or shutdown. + String intentAction = intent.getAction(); + if (ACTION_REBOOT.equals(intentAction) + || ACTION_SHUTDOWN.equals(intentAction)) { + writeNow(); + } + } + }; + + // Setup receiver for device reboots or shutdowns. + IntentFilter filter = new IntentFilter(ACTION_REBOOT); + filter.addAction(ACTION_SHUTDOWN); + mContext.registerReceiverForAllUsers(shutdownEventReceiver, filter, null, + /* run on main thread */ null); + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java new file mode 100644 index 000000000000..846da194b3c3 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java @@ -0,0 +1,861 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.server; + +import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SKIPPED; +import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS; +import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; +import android.os.Build; +import android.os.PowerManager; +import android.os.RecoverySystem; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.provider.Settings; +import android.sysprop.CrashRecoveryProperties; +import android.text.TextUtils; +import android.util.EventLog; +import android.util.FileUtils; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.PackageWatchdog.FailureReasons; +import com.android.server.PackageWatchdog.PackageHealthObserver; +import com.android.server.PackageWatchdog.PackageHealthObserverImpact; +import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; + +import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Utilities to help rescue the system from crash loops. Callers are expected to + * report boot events and persistent app crashes, and if they happen frequently + * enough this class will slowly escalate through several rescue operations + * before finally rebooting and prompting the user if they want to wipe data as + * a last resort. + * + * @hide + */ +public class RescueParty { + @VisibleForTesting + static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue"; + @VisibleForTesting + static final int LEVEL_NONE = 0; + @VisibleForTesting + static final int LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 1; + @VisibleForTesting + static final int LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 2; + @VisibleForTesting + static final int LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 3; + @VisibleForTesting + static final int LEVEL_WARM_REBOOT = 4; + @VisibleForTesting + static final int LEVEL_FACTORY_RESET = 5; + @VisibleForTesting + static final int RESCUE_LEVEL_NONE = 0; + @VisibleForTesting + static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1; + @VisibleForTesting + static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2; + @VisibleForTesting + static final int RESCUE_LEVEL_WARM_REBOOT = 3; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6; + @VisibleForTesting + static final int RESCUE_LEVEL_FACTORY_RESET = 7; + + @IntDef(prefix = { "RESCUE_LEVEL_" }, value = { + RESCUE_LEVEL_NONE, + RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET, + RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET, + RESCUE_LEVEL_WARM_REBOOT, + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS, + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES, + RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS, + RESCUE_LEVEL_FACTORY_RESET + }) + @Retention(RetentionPolicy.SOURCE) + @interface RescueLevels {} + + @VisibleForTesting + static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit"; + @VisibleForTesting + static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1; + @VisibleForTesting + static final String TAG = "RescueParty"; + @VisibleForTesting + static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2); + @VisibleForTesting + static final int DEVICE_CONFIG_RESET_MODE = Settings.RESET_MODE_TRUSTED_DEFAULTS; + // The DeviceConfig namespace containing all RescueParty switches. + @VisibleForTesting + static final String NAMESPACE_CONFIGURATION = "configuration"; + @VisibleForTesting + static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG = + "namespace_to_package_mapping"; + @VisibleForTesting + static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 1440; + + private static final String NAME = "rescue-party-observer"; + + private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue"; + private static final String PROP_VIRTUAL_DEVICE = "ro.hardware.virtual_device"; + private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG = + "persist.device_config.configuration.disable_rescue_party"; + private static final String PROP_DISABLE_FACTORY_RESET_FLAG = + "persist.device_config.configuration.disable_rescue_party_factory_reset"; + private static final String PROP_THROTTLE_DURATION_MIN_FLAG = + "persist.device_config.configuration.rescue_party_throttle_duration_min"; + + private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT + | ApplicationInfo.FLAG_SYSTEM; + + /** + * EventLog tags used when logging into the event log. Note the values must be sync with + * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct + * name translation. + */ + private static final int LOG_TAG_RESCUE_SUCCESS = 2902; + private static final int LOG_TAG_RESCUE_FAILURE = 2903; + + /** Register the Rescue Party observer as a Package Watchdog health observer */ + public static void registerHealthObserver(Context context) { + PackageWatchdog.getInstance(context).registerHealthObserver( + context.getMainExecutor(), RescuePartyObserver.getInstance(context)); + } + + private static boolean isDisabled() { + // Check if we're explicitly enabled for testing + if (SystemProperties.getBoolean(PROP_ENABLE_RESCUE, false)) { + return false; + } + + // We're disabled if the DeviceConfig disable flag is set to true. + // This is in case that an emergency rollback of the feature is needed. + if (SystemProperties.getBoolean(PROP_DEVICE_CONFIG_DISABLE_FLAG, false)) { + Slog.v(TAG, "Disabled because of DeviceConfig flag"); + return true; + } + + // We're disabled on all engineering devices + if (Build.TYPE.equals("eng")) { + Slog.v(TAG, "Disabled because of eng build"); + return true; + } + + // We're disabled on userdebug devices connected over USB, since that's + // a decent signal that someone is actively trying to debug the device, + // or that it's in a lab environment. + if (Build.TYPE.equals("userdebug") && isUsbActive()) { + Slog.v(TAG, "Disabled because of active USB connection"); + return true; + } + + // One last-ditch check + if (SystemProperties.getBoolean(PROP_DISABLE_RESCUE, false)) { + Slog.v(TAG, "Disabled because of manual property"); + return true; + } + + return false; + } + + /** + * Check if we're currently attempting to reboot for a factory reset. This method must + * return true if RescueParty tries to reboot early during a boot loop, since the device + * will not be fully booted at this time. + */ + public static boolean isRecoveryTriggeredReboot() { + return isFactoryResetPropertySet() || isRebootPropertySet(); + } + + static boolean isFactoryResetPropertySet() { + return CrashRecoveryProperties.attemptingFactoryReset().orElse(false); + } + + static boolean isRebootPropertySet() { + return CrashRecoveryProperties.attemptingReboot().orElse(false); + } + + protected static long getLastFactoryResetTimeMs() { + return CrashRecoveryProperties.lastFactoryResetTimeMs().orElse(0L); + } + + protected static int getMaxRescueLevelAttempted() { + return CrashRecoveryProperties.maxRescueLevelAttempted().orElse(LEVEL_NONE); + } + + protected static void setFactoryResetProperty(boolean value) { + CrashRecoveryProperties.attemptingFactoryReset(value); + } + protected static void setRebootProperty(boolean value) { + CrashRecoveryProperties.attemptingReboot(value); + } + + protected static void setLastFactoryResetTimeMs(long value) { + CrashRecoveryProperties.lastFactoryResetTimeMs(value); + } + + protected static void setMaxRescueLevelAttempted(int level) { + CrashRecoveryProperties.maxRescueLevelAttempted(level); + } + + @VisibleForTesting + static long getElapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + private static int getMaxRescueLevel(boolean mayPerformReboot) { + if (Flags.recoverabilityDetection()) { + if (!mayPerformReboot + || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT, + DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT); + } + return RESCUE_LEVEL_FACTORY_RESET; + } else { + if (!mayPerformReboot + || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; + } + return LEVEL_FACTORY_RESET; + } + } + + private static int getMaxRescueLevel() { + if (!SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return Level.factoryReset(); + } + return Level.reboot(); + } + + /** + * Get the rescue level to perform if this is the n-th attempt at mitigating failure. + * + * @param mitigationCount: the mitigation attempt number (1 = first attempt etc.) + * @param mayPerformReboot: whether or not a reboot and factory reset may be performed + * for the given failure. + * @return the rescue level for the n-th mitigation attempt. + */ + private static int getRescueLevel(int mitigationCount, boolean mayPerformReboot) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + if (mitigationCount == 1) { + return LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS; + } else if (mitigationCount == 2) { + return LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES; + } else if (mitigationCount == 3) { + return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; + } else if (mitigationCount == 4) { + return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_WARM_REBOOT); + } else if (mitigationCount >= 5) { + return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_FACTORY_RESET); + } else { + Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount); + return LEVEL_NONE; + } + } else { + if (mitigationCount == 1) { + return Level.reboot(); + } else if (mitigationCount >= 2) { + return Math.min(getMaxRescueLevel(), Level.factoryReset()); + } else { + Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount); + return LEVEL_NONE; + } + } + } + + /** + * Get the rescue level to perform if this is the n-th attempt at mitigating failure. + * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and + * all device config reset). Behaves as if one mitigation attempt was already done. + * + * @param mitigationCount the mitigation attempt number (1 = first attempt etc.). + * @param mayPerformReboot whether or not a reboot and factory reset may be performed + * for the given failure. + * @param failedPackage in case of bootloop this is null. + * @return the rescue level for the n-th mitigation attempt. + */ + private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot, + @Nullable VersionedPackage failedPackage) { + // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed + // package. + if (failedPackage == null && mitigationCount > 0) { + mitigationCount += 1; + } + if (mitigationCount == 1) { + return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET; + } else if (mitigationCount == 2) { + return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET; + } else if (mitigationCount == 3) { + return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT); + } else if (mitigationCount == 4) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS); + } else if (mitigationCount == 5) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES); + } else if (mitigationCount == 6) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS); + } else if (mitigationCount >= 7) { + return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET); + } else { + return RESCUE_LEVEL_NONE; + } + } + + /** + * Get the rescue level to perform if this is the n-th attempt at mitigating failure. + * + * @param mitigationCount the mitigation attempt number (1 = first attempt etc.). + * @return the rescue level for the n-th mitigation attempt. + */ + private static @RescueLevels int getRescueLevel(int mitigationCount) { + if (mitigationCount == 1) { + return Level.reboot(); + } else if (mitigationCount >= 2) { + return Math.min(getMaxRescueLevel(), Level.factoryReset()); + } else { + return Level.none(); + } + } + + private static void executeRescueLevel(Context context, @Nullable String failedPackage, + int level) { + Slog.w(TAG, "Attempting rescue level " + levelToString(level)); + try { + executeRescueLevelInternal(context, level, failedPackage); + EventLog.writeEvent(LOG_TAG_RESCUE_SUCCESS, level); + String successMsg = "Finished rescue level " + levelToString(level); + if (!TextUtils.isEmpty(failedPackage)) { + successMsg += " for package " + failedPackage; + } + logCrashRecoveryEvent(Log.DEBUG, successMsg); + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } + + private static void executeRescueLevelInternal(Context context, int level, @Nullable + String failedPackage) throws Exception { + if (Flags.recoverabilityDetection()) { + executeRescueLevelInternalNew(context, level, failedPackage); + } else { + executeRescueLevelInternalOld(context, level, failedPackage); + } + } + + private static void executeRescueLevelInternalOld(Context context, int level, @Nullable + String failedPackage) throws Exception { + CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, + level, levelToString(level)); + // Try our best to reset all settings possible, and once finished + // rethrow any exception that we encountered + Exception res = null; + switch (level) { + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + break; + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + break; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + break; + case LEVEL_WARM_REBOOT: + executeWarmReboot(context, level, failedPackage); + break; + case LEVEL_FACTORY_RESET: + // Before the completion of Reboot, if any crash happens then PackageWatchdog + // escalates to next level i.e. factory reset, as they happen in separate threads. + // Adding a check to prevent factory reset to execute before above reboot completes. + // Note: this reboot property is not persistent resets after reboot is completed. + if (isRebootPropertySet()) { + return; + } + executeFactoryReset(context, level, failedPackage); + break; + } + + if (res != null) { + throw res; + } + } + + private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level, + @Nullable String failedPackage) throws Exception { + CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, + level, levelToString(level)); + switch (level) { + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + break; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + break; + case RESCUE_LEVEL_WARM_REBOOT: + executeWarmReboot(context, level, failedPackage); + break; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + // do nothing + break; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + // do nothing + break; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + // do nothing + break; + case RESCUE_LEVEL_FACTORY_RESET: + // Before the completion of Reboot, if any crash happens then PackageWatchdog + // escalates to next level i.e. factory reset, as they happen in separate threads. + // Adding a check to prevent factory reset to execute before above reboot completes. + // Note: this reboot property is not persistent resets after reboot is completed. + if (isRebootPropertySet()) { + return; + } + executeFactoryReset(context, level, failedPackage); + break; + } + } + + private static void executeWarmReboot(Context context, int level, + @Nullable String failedPackage) { + if (Flags.deprecateFlagsAndSettingsResets()) { + if (shouldThrottleReboot()) { + return; + } + } + + // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog + // when device shutting down. + setRebootProperty(true); + + if (Flags.synchronousRebootInRescueParty()) { + try { + PowerManager pm = context.getSystemService(PowerManager.class); + if (pm != null) { + pm.reboot(TAG); + } + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } else { + Runnable runnable = () -> { + try { + PowerManager pm = context.getSystemService(PowerManager.class); + if (pm != null) { + pm.reboot(TAG); + } + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + } + + private static void executeFactoryReset(Context context, int level, + @Nullable String failedPackage) { + if (Flags.deprecateFlagsAndSettingsResets()) { + if (shouldThrottleReboot()) { + return; + } + } + setFactoryResetProperty(true); + long now = System.currentTimeMillis(); + setLastFactoryResetTimeMs(now); + + if (Flags.synchronousRebootInRescueParty()) { + try { + RecoverySystem.rebootPromptAndWipeUserData(context, TAG + "," + failedPackage); + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } else { + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + RecoverySystem.rebootPromptAndWipeUserData(context, + TAG + "," + failedPackage); + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + } + + + private static String getCompleteMessage(Throwable t) { + final StringBuilder builder = new StringBuilder(); + builder.append(t.getMessage()); + while ((t = t.getCause()) != null) { + builder.append(": ").append(t.getMessage()); + } + return builder.toString(); + } + + private static void logRescueException(int level, @Nullable String failedPackageName, + Throwable t) { + final String msg = getCompleteMessage(t); + EventLog.writeEvent(LOG_TAG_RESCUE_FAILURE, level, msg); + String failureMsg = "Failed rescue level " + levelToString(level); + if (!TextUtils.isEmpty(failedPackageName)) { + failureMsg += " for package " + failedPackageName; + } + logCrashRecoveryEvent(Log.ERROR, failureMsg + ": " + msg); + } + + private static int mapRescueLevelToUserImpact(int rescueLevel) { + if (Flags.recoverabilityDetection()) { + switch (rescueLevel) { + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_40; + case RESCUE_LEVEL_WARM_REBOOT: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80; + case RESCUE_LEVEL_FACTORY_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; + default: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + } else { + switch (rescueLevel) { + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + case LEVEL_WARM_REBOOT: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + case LEVEL_FACTORY_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; + default: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + } + } + + /** + * Handle mitigation action for package failures. This observer will be register to Package + * Watchdog and will receive calls about package failures. This observer is persistent so it + * may choose to mitigate failures for packages it has not explicitly asked to observe. + */ + public static class RescuePartyObserver implements PackageHealthObserver { + + private final Context mContext; + private final Map<String, Set<String>> mCallingPackageNamespaceSetMap = new HashMap<>(); + private final Map<String, Set<String>> mNamespaceCallingPackageSetMap = new HashMap<>(); + + @GuardedBy("RescuePartyObserver.class") + static RescuePartyObserver sRescuePartyObserver; + + private RescuePartyObserver(Context context) { + mContext = context; + } + + /** Creates or gets singleton instance of RescueParty. */ + public static RescuePartyObserver getInstance(Context context) { + synchronized (RescuePartyObserver.class) { + if (sRescuePartyObserver == null) { + sRescuePartyObserver = new RescuePartyObserver(context); + } + return sRescuePartyObserver; + } + } + + /** Gets singleton instance. It returns null if the instance is not created yet.*/ + @Nullable + public static RescuePartyObserver getInstanceIfCreated() { + synchronized (RescuePartyObserver.class) { + return sRescuePartyObserver; + } + } + + @VisibleForTesting + static void reset() { + synchronized (RescuePartyObserver.class) { + sRescuePartyObserver = null; + } + } + + @Override + public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage, + @FailureReasons int failureReason, int mitigationCount) { + int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH + || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) { + if (Flags.recoverabilityDetection()) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage), failedPackage)); + } else { + impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount)); + } + } else { + impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage))); + } + } + + Slog.i(TAG, "Checking available remediations for health check failure." + + " failedPackage: " + + (failedPackage == null ? null : failedPackage.getPackageName()) + + " failureReason: " + failureReason + + " available impact: " + impact); + return impact; + } + + @Override + public int onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage, + @FailureReasons int failureReason, int mitigationCount) { + if (isDisabled()) { + return MITIGATION_RESULT_SKIPPED; + } + Slog.i(TAG, "Executing remediation." + + " failedPackage: " + + (failedPackage == null ? null : failedPackage.getPackageName()) + + " failureReason: " + failureReason + + " mitigationCount: " + mitigationCount); + if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH + || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) { + final int level; + if (Flags.recoverabilityDetection()) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage), + failedPackage); + } else { + level = getRescueLevel(mitigationCount); + } + } else { + level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage)); + } + executeRescueLevel(mContext, + failedPackage == null ? null : failedPackage.getPackageName(), level); + return MITIGATION_RESULT_SUCCESS; + } else { + return MITIGATION_RESULT_SKIPPED; + } + } + + @Override + public boolean isPersistent() { + return true; + } + + @Override + public boolean mayObservePackage(String packageName) { + PackageManager pm = mContext.getPackageManager(); + try { + // A package is a module if this is non-null + if (pm.getModuleInfo(packageName, 0) != null) { + return true; + } + } catch (PackageManager.NameNotFoundException | IllegalStateException ignore) { + } + + return isPersistentSystemApp(packageName); + } + + @Override + public int onBootLoop(int mitigationCount) { + if (isDisabled()) { + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + if (Flags.recoverabilityDetection()) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + true, /*failedPackage=*/ null)); + } else { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount)); + } + } else { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true)); + } + } + + @Override + public int onExecuteBootLoopMitigation(int mitigationCount) { + if (isDisabled()) { + return MITIGATION_RESULT_SKIPPED; + } + boolean mayPerformReboot = !shouldThrottleReboot(); + final int level; + if (Flags.recoverabilityDetection()) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + level = getRescueLevel(mitigationCount, mayPerformReboot, + /*failedPackage=*/ null); + } else { + level = getRescueLevel(mitigationCount); + } + } else { + level = getRescueLevel(mitigationCount, mayPerformReboot); + } + executeRescueLevel(mContext, /*failedPackage=*/ null, level); + return MITIGATION_RESULT_SUCCESS; + } + + @Override + public String getUniqueIdentifier() { + return NAME; + } + + /** + * Returns {@code true} if the failing package is non-null and performing a reboot or + * prompting a factory reset is an acceptable mitigation strategy for the package's + * failure, {@code false} otherwise. + */ + private boolean mayPerformReboot(@Nullable VersionedPackage failingPackage) { + if (failingPackage == null) { + return false; + } + if (shouldThrottleReboot()) { + return false; + } + + return isPersistentSystemApp(failingPackage.getPackageName()); + } + + private boolean isPersistentSystemApp(@NonNull String packageName) { + PackageManager pm = mContext.getPackageManager(); + try { + ApplicationInfo info = pm.getApplicationInfo(packageName, 0); + return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private synchronized Set<String> getCallingPackagesSet(String namespace) { + return mNamespaceCallingPackageSetMap.get(namespace); + } + } + + /** + * Returns {@code true} if Rescue Party is allowed to attempt a reboot or factory reset. + * Will return {@code false} if a factory reset was already offered recently. + */ + private static boolean shouldThrottleReboot() { + Long lastResetTime = getLastFactoryResetTimeMs(); + long now = System.currentTimeMillis(); + long throttleDurationMin = SystemProperties.getLong(PROP_THROTTLE_DURATION_MIN_FLAG, + DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN); + return now < lastResetTime + TimeUnit.MINUTES.toMillis(throttleDurationMin); + } + + /** + * Hacky test to check if the device has an active USB connection, which is + * a good proxy for someone doing local development work. + */ + private static boolean isUsbActive() { + if (SystemProperties.getBoolean(PROP_VIRTUAL_DEVICE, false)) { + Slog.v(TAG, "Assuming virtual device is connected over USB"); + return true; + } + try { + final String state = FileUtils + .readTextFile(new File("/sys/class/android_usb/android0/state"), 128, ""); + return "CONFIGURED".equals(state.trim()); + } catch (Throwable t) { + Slog.w(TAG, "Failed to determine if device was on USB", t); + return false; + } + } + + private static class Level { + static int none() { + return Flags.recoverabilityDetection() ? RESCUE_LEVEL_NONE : LEVEL_NONE; + } + + static int reboot() { + return Flags.recoverabilityDetection() ? RESCUE_LEVEL_WARM_REBOOT : LEVEL_WARM_REBOOT; + } + + static int factoryReset() { + return Flags.recoverabilityDetection() + ? RESCUE_LEVEL_FACTORY_RESET + : LEVEL_FACTORY_RESET; + } + } + + private static String levelToString(int level) { + if (Flags.recoverabilityDetection()) { + switch (level) { + case RESCUE_LEVEL_NONE: + return "NONE"; + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + return "SCOPED_DEVICE_CONFIG_RESET"; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + return "ALL_DEVICE_CONFIG_RESET"; + case RESCUE_LEVEL_WARM_REBOOT: + return "WARM_REBOOT"; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return "RESET_SETTINGS_UNTRUSTED_CHANGES"; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return "RESET_SETTINGS_TRUSTED_DEFAULTS"; + case RESCUE_LEVEL_FACTORY_RESET: + return "FACTORY_RESET"; + default: + return Integer.toString(level); + } + } else { + switch (level) { + case LEVEL_NONE: + return "NONE"; + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return "RESET_SETTINGS_UNTRUSTED_CHANGES"; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return "RESET_SETTINGS_TRUSTED_DEFAULTS"; + case LEVEL_WARM_REBOOT: + return "WARM_REBOOT"; + case LEVEL_FACTORY_RESET: + return "FACTORY_RESET"; + default: + return Integer.toString(level); + } + } + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java new file mode 100644 index 000000000000..8a81aaa1e636 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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.android.server.crashrecovery; + +import android.content.Context; + +import com.android.server.PackageWatchdog; +import com.android.server.RescueParty; +import com.android.server.SystemService; + + +/** This class encapsulate the lifecycle methods of CrashRecovery module. + * + * @hide + */ +public class CrashRecoveryModule { + private static final String TAG = "CrashRecoveryModule"; + + /** Lifecycle definition for CrashRecovery module. */ + public static class Lifecycle extends SystemService { + private Context mSystemContext; + private PackageWatchdog mPackageWatchdog; + + public Lifecycle(Context context) { + super(context); + mSystemContext = context; + mPackageWatchdog = PackageWatchdog.getInstance(context); + } + + @Override + public void onStart() { + RescueParty.registerHealthObserver(mSystemContext); + mPackageWatchdog.registerShutdownBroadcastReceiver(); + mPackageWatchdog.noteBoot(); + } + + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + mPackageWatchdog.onPackagesReady(); + } + } + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java new file mode 100644 index 000000000000..2e2a93776f9d --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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.android.server.crashrecovery; + +import android.os.Environment; +import android.util.IndentingPrintWriter; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * Class containing helper methods for the CrashRecoveryModule. + * + * @hide + */ +public class CrashRecoveryUtils { + private static final String TAG = "CrashRecoveryUtils"; + private static final long MAX_CRITICAL_INFO_DUMP_SIZE = 1000 * 1000; // ~1MB + private static final Object sFileLock = new Object(); + + /** Persist recovery related events in crashrecovery events file.**/ + public static void logCrashRecoveryEvent(int priority, String msg) { + Log.println(priority, TAG, msg); + try { + File fname = getCrashRecoveryEventsFile(); + synchronized (sFileLock) { + FileOutputStream out = new FileOutputStream(fname, true); + PrintWriter pw = new PrintWriter(out); + String dateString = LocalDateTime.now(ZoneId.systemDefault()).toString(); + pw.println(dateString + ": " + msg); + pw.close(); + } + } catch (IOException e) { + Log.e(TAG, "Unable to log CrashRecoveryEvents " + e.getMessage()); + } + } + + /** Dump recovery related events from crashrecovery events file.**/ + public static void dumpCrashRecoveryEvents(IndentingPrintWriter pw) { + pw.println("CrashRecovery Events: "); + pw.increaseIndent(); + final File file = getCrashRecoveryEventsFile(); + final long skipSize = file.length() - MAX_CRITICAL_INFO_DUMP_SIZE; + synchronized (sFileLock) { + try (BufferedReader in = new BufferedReader(new FileReader(file))) { + if (skipSize > 0) { + in.skip(skipSize); + } + String line; + while ((line = in.readLine()) != null) { + pw.println(line); + } + } catch (IOException e) { + Log.e(TAG, "Unable to dump CrashRecoveryEvents " + e.getMessage()); + } + } + pw.decreaseIndent(); + } + + private static File getCrashRecoveryEventsFile() { + File systemDir = new File(Environment.getDataDirectory(), "system"); + return new File(systemDir, "crashrecovery-events.txt"); + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java new file mode 100644 index 000000000000..4978df491c62 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java @@ -0,0 +1,785 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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.android.server.rollback; + +import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SKIPPED; +import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS; +import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; + +import android.annotation.AnyThread; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.annotation.WorkerThread; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.content.rollback.PackageRollbackInfo; +import android.content.rollback.RollbackInfo; +import android.content.rollback.RollbackManager; +import android.crashrecovery.flags.Flags; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.PowerManager; +import android.os.SystemProperties; +import android.sysprop.CrashRecoveryProperties; +import android.util.ArraySet; +import android.util.FileUtils; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; +import com.android.server.PackageWatchdog; +import com.android.server.PackageWatchdog.FailureReasons; +import com.android.server.PackageWatchdog.PackageHealthObserver; +import com.android.server.PackageWatchdog.PackageHealthObserverImpact; +import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * {@link PackageHealthObserver} for {@link RollbackManagerService}. + * This class monitors crashes and triggers RollbackManager rollback accordingly. + * It also monitors native crashes for some short while after boot. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) +@SuppressLint({"CallbackName"}) +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) +public final class RollbackPackageHealthObserver implements PackageHealthObserver { + private static final String TAG = "RollbackPackageHealthObserver"; + private static final String NAME = "rollback-observer"; + private static final String CLASS_NAME = RollbackPackageHealthObserver.class.getName(); + + private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT + | ApplicationInfo.FLAG_SYSTEM; + + private static final String PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG = + "persist.device_config.configuration.disable_high_impact_rollback"; + + private final Context mContext; + private final Handler mHandler; + private final File mLastStagedRollbackIdsFile; + private final File mTwoPhaseRollbackEnabledFile; + // Staged rollback ids that have been committed but their session is not yet ready + private final Set<Integer> mPendingStagedRollbackIds = new ArraySet<>(); + // True if needing to roll back only rebootless apexes when native crash happens + private boolean mTwoPhaseRollbackEnabled; + + @VisibleForTesting + public RollbackPackageHealthObserver(@NonNull Context context) { + mContext = context; + HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver"); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper()); + File dataDir = new File(Environment.getDataDirectory(), "rollback-observer"); + dataDir.mkdirs(); + mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids"); + mTwoPhaseRollbackEnabledFile = new File(dataDir, "two-phase-rollback-enabled"); + PackageWatchdog.getInstance(mContext).registerHealthObserver(context.getMainExecutor(), + this); + + if (SystemProperties.getBoolean("sys.boot_completed", false)) { + // Load the value from the file if system server has crashed and restarted + mTwoPhaseRollbackEnabled = readBoolean(mTwoPhaseRollbackEnabledFile); + } else { + // Disable two-phase rollback for a normal reboot. We assume the rebootless apex + // installed before reboot is stable if native crash didn't happen. + mTwoPhaseRollbackEnabled = false; + writeBoolean(mTwoPhaseRollbackEnabledFile, false); + } + } + + @Override + public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage, + @FailureReasons int failureReason, int mitigationCount) { + int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + if (Flags.recoverabilityDetection()) { + List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); + List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel( + availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW); + if (!lowImpactRollbacks.isEmpty()) { + if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { + // For native crashes, we will directly roll back any available rollbacks at low + // impact level + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; + } else if (getRollbackForPackage(failedPackage, lowImpactRollbacks) != null) { + // Rollback is available for crashing low impact package + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; + } else { + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; + } + } + } else { + boolean anyRollbackAvailable = !mContext.getSystemService(RollbackManager.class) + .getAvailableRollbacks().isEmpty(); + + if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH + && anyRollbackAvailable) { + // For native crashes, we will directly roll back any available rollbacks + // Note: For non-native crashes the rollback-all step has higher impact + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; + } else if (getAvailableRollback(failedPackage) != null) { + // Rollback is available, we may get a callback into #onExecuteHealthCheckMitigation + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; + } else if (anyRollbackAvailable) { + // If any rollbacks are available, we will commit them + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; + } + } + + Slog.i(TAG, "Checking available remediations for health check failure." + + " failedPackage: " + + (failedPackage == null ? null : failedPackage.getPackageName()) + + " failureReason: " + failureReason + + " available impact: " + impact); + return impact; + } + + @Override + public int onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage, + @FailureReasons int rollbackReason, int mitigationCount) { + Slog.i(TAG, "Executing remediation." + + " failedPackage: " + + (failedPackage == null ? null : failedPackage.getPackageName()) + + " rollbackReason: " + rollbackReason + + " mitigationCount: " + mitigationCount); + if (Flags.recoverabilityDetection()) { + List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); + if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { + mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason)); + return MITIGATION_RESULT_SUCCESS; + } + + List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel( + availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW); + RollbackInfo rollback = getRollbackForPackage(failedPackage, lowImpactRollbacks); + if (rollback != null) { + mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason)); + } else if (!lowImpactRollbacks.isEmpty()) { + // Apply all available low impact rollbacks. + mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason)); + } + } else { + if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { + mHandler.post(() -> rollbackAll(rollbackReason)); + return MITIGATION_RESULT_SUCCESS; + } + + RollbackInfo rollback = getAvailableRollback(failedPackage); + if (rollback != null) { + mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason)); + } else { + mHandler.post(() -> rollbackAll(rollbackReason)); + } + } + + // Assume rollbacks executed successfully + return MITIGATION_RESULT_SUCCESS; + } + + @Override + public int onBootLoop(int mitigationCount) { + int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + if (Flags.recoverabilityDetection()) { + List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); + if (!availableRollbacks.isEmpty()) { + impact = getUserImpactBasedOnRollbackImpactLevel(availableRollbacks); + } + } + return impact; + } + + @Override + public int onExecuteBootLoopMitigation(int mitigationCount) { + if (Flags.recoverabilityDetection()) { + List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); + + triggerLeastImpactLevelRollback(availableRollbacks, + PackageWatchdog.FAILURE_REASON_BOOT_LOOP); + return MITIGATION_RESULT_SUCCESS; + } + return MITIGATION_RESULT_SKIPPED; + } + + @Override + @NonNull + public String getUniqueIdentifier() { + return NAME; + } + + @Override + public boolean isPersistent() { + return true; + } + + @Override + public boolean mayObservePackage(@NonNull String packageName) { + if (getAvailableRollbacks().isEmpty()) { + return false; + } + return isPersistentSystemApp(packageName); + } + + private List<RollbackInfo> getAvailableRollbacks() { + return mContext.getSystemService(RollbackManager.class).getAvailableRollbacks(); + } + + private boolean isPersistentSystemApp(@NonNull String packageName) { + PackageManager pm = mContext.getPackageManager(); + try { + ApplicationInfo info = pm.getApplicationInfo(packageName, 0); + return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private void assertInWorkerThread() { + Preconditions.checkState(mHandler.getLooper().isCurrentThread()); + } + + @AnyThread + @NonNull + public void notifyRollbackAvailable(@NonNull RollbackInfo rollback) { + mHandler.post(() -> { + // Enable two-phase rollback when a rebootless apex rollback is made available. + // We assume the rebootless apex is stable and is less likely to be the cause + // if native crash doesn't happen before reboot. So we will clear the flag and disable + // two-phase rollback after reboot. + if (isRebootlessApex(rollback)) { + mTwoPhaseRollbackEnabled = true; + writeBoolean(mTwoPhaseRollbackEnabledFile, true); + } + }); + } + + private static boolean isRebootlessApex(RollbackInfo rollback) { + if (!rollback.isStaged()) { + for (PackageRollbackInfo info : rollback.getPackages()) { + if (info.isApex()) { + return true; + } + } + } + return false; + } + + /** Verifies the rollback state after a reboot and schedules polling for sometime after reboot + * to check for native crashes and mitigate them if needed. + */ + @AnyThread + public void onBootCompletedAsync() { + mHandler.post(()->onBootCompleted()); + } + + @WorkerThread + private void onBootCompleted() { + assertInWorkerThread(); + + RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); + if (!rollbackManager.getAvailableRollbacks().isEmpty()) { + // TODO(gavincorkery): Call into Package Watchdog from outside the observer + PackageWatchdog.getInstance(mContext).scheduleCheckAndMitigateNativeCrashes(); + } + + SparseArray<String> rollbackIds = popLastStagedRollbackIds(); + for (int i = 0; i < rollbackIds.size(); i++) { + WatchdogRollbackLogger.logRollbackStatusOnBoot(mContext, + rollbackIds.keyAt(i), rollbackIds.valueAt(i), + rollbackManager.getRecentlyCommittedRollbacks()); + } + } + + @AnyThread + private RollbackInfo getAvailableRollback(VersionedPackage failedPackage) { + RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); + for (RollbackInfo rollback : rollbackManager.getAvailableRollbacks()) { + for (PackageRollbackInfo packageRollback : rollback.getPackages()) { + if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) { + return rollback; + } + // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have + // to rely on complicated reasoning as below + + // Due to b/147666157, for apk in apex, we do not know the version we are rolling + // back from. But if a package X is embedded in apex A exclusively (not embedded in + // any other apex), which is not guaranteed, then it is sufficient to check only + // package names here, as the version of failedPackage and the PackageRollbackInfo + // can't be different. If failedPackage has a higher version, then it must have + // been updated somehow. There are two ways: it was updated by an update of apex A + // or updated directly as apk. In both cases, this rollback would have gotten + // expired when onPackageReplaced() was called. Since the rollback exists, it has + // same version as failedPackage. + if (packageRollback.isApkInApex() + && packageRollback.getVersionRolledBackFrom().getPackageName() + .equals(failedPackage.getPackageName())) { + return rollback; + } + } + } + return null; + } + + @AnyThread + private RollbackInfo getRollbackForPackage(@Nullable VersionedPackage failedPackage, + List<RollbackInfo> availableRollbacks) { + if (failedPackage == null) { + return null; + } + + for (RollbackInfo rollback : availableRollbacks) { + for (PackageRollbackInfo packageRollback : rollback.getPackages()) { + if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) { + return rollback; + } + // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have + // to rely on complicated reasoning as below + + // Due to b/147666157, for apk in apex, we do not know the version we are rolling + // back from. But if a package X is embedded in apex A exclusively (not embedded in + // any other apex), which is not guaranteed, then it is sufficient to check only + // package names here, as the version of failedPackage and the PackageRollbackInfo + // can't be different. If failedPackage has a higher version, then it must have + // been updated somehow. There are two ways: it was updated by an update of apex A + // or updated directly as apk. In both cases, this rollback would have gotten + // expired when onPackageReplaced() was called. Since the rollback exists, it has + // same version as failedPackage. + if (packageRollback.isApkInApex() + && packageRollback.getVersionRolledBackFrom().getPackageName() + .equals(failedPackage.getPackageName())) { + return rollback; + } + } + } + return null; + } + + /** + * Returns {@code true} if staged session associated with {@code rollbackId} was marked + * as handled, {@code false} if already handled. + */ + @WorkerThread + private boolean markStagedSessionHandled(int rollbackId) { + assertInWorkerThread(); + return mPendingStagedRollbackIds.remove(rollbackId); + } + + /** + * Returns {@code true} if all pending staged rollback sessions were marked as handled, + * {@code false} if there is any left. + */ + @WorkerThread + private boolean isPendingStagedSessionsEmpty() { + assertInWorkerThread(); + return mPendingStagedRollbackIds.isEmpty(); + } + + private static boolean readBoolean(File file) { + try (FileInputStream fis = new FileInputStream(file)) { + return fis.read() == 1; + } catch (IOException ignore) { + return false; + } + } + + private static void writeBoolean(File file, boolean value) { + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(value ? 1 : 0); + fos.flush(); + FileUtils.sync(fos); + } catch (IOException ignore) { + } + } + + @WorkerThread + private void saveStagedRollbackId(int stagedRollbackId, @Nullable VersionedPackage logPackage) { + assertInWorkerThread(); + writeStagedRollbackId(mLastStagedRollbackIdsFile, stagedRollbackId, logPackage); + } + + static void writeStagedRollbackId(File file, int stagedRollbackId, + @Nullable VersionedPackage logPackage) { + try { + FileOutputStream fos = new FileOutputStream(file, true); + PrintWriter pw = new PrintWriter(fos); + String logPackageName = logPackage != null ? logPackage.getPackageName() : ""; + pw.append(String.valueOf(stagedRollbackId)).append(",").append(logPackageName); + pw.println(); + pw.flush(); + FileUtils.sync(fos); + pw.close(); + } catch (IOException e) { + Slog.e(TAG, "Failed to save last staged rollback id", e); + file.delete(); + } + } + + @WorkerThread + private SparseArray<String> popLastStagedRollbackIds() { + assertInWorkerThread(); + try { + return readStagedRollbackIds(mLastStagedRollbackIdsFile); + } finally { + mLastStagedRollbackIdsFile.delete(); + } + } + + static SparseArray<String> readStagedRollbackIds(File file) { + SparseArray<String> result = new SparseArray<>(); + try { + String line; + BufferedReader reader = new BufferedReader(new FileReader(file)); + while ((line = reader.readLine()) != null) { + // Each line is of the format: "id,logging_package" + String[] values = line.trim().split(","); + String rollbackId = values[0]; + String logPackageName = ""; + if (values.length > 1) { + logPackageName = values[1]; + } + result.put(Integer.parseInt(rollbackId), logPackageName); + } + } catch (Exception ignore) { + return new SparseArray<>(); + } + return result; + } + + + /** + * Returns true if the package name is the name of a module. + */ + @AnyThread + private boolean isModule(String packageName) { + // Check if the package is listed among the system modules or is an + // APK inside an updatable APEX. + try { + PackageManager pm = mContext.getPackageManager(); + final PackageInfo pkg = pm.getPackageInfo(packageName, 0 /* flags */); + String apexPackageName = pkg.getApexPackageName(); + if (apexPackageName != null) { + packageName = apexPackageName; + } + + return pm.getModuleInfo(packageName, 0 /* flags */) != null; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + /** + * Rolls back the session that owns {@code failedPackage} + * + * @param rollback {@code rollbackInfo} of the {@code failedPackage} + * @param failedPackage the package that needs to be rolled back + */ + @WorkerThread + private void rollbackPackage(RollbackInfo rollback, VersionedPackage failedPackage, + @FailureReasons int rollbackReason) { + assertInWorkerThread(); + String failedPackageName = (failedPackage == null ? null : failedPackage.getPackageName()); + + Slog.i(TAG, "Rolling back package. RollbackId: " + rollback.getRollbackId() + + " failedPackage: " + failedPackageName + + " rollbackReason: " + rollbackReason); + logCrashRecoveryEvent(Log.DEBUG, String.format("Rolling back %s. Reason: %s", + failedPackageName, rollbackReason)); + final RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); + int reasonToLog = WatchdogRollbackLogger.mapFailureReasonToMetric(rollbackReason); + final String failedPackageToLog; + if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { + failedPackageToLog = SystemProperties.get( + "sys.init.updatable_crashing_process_name", ""); + } else { + failedPackageToLog = failedPackage.getPackageName(); + } + VersionedPackage logPackageTemp = null; + if (isModule(failedPackage.getPackageName())) { + logPackageTemp = WatchdogRollbackLogger.getLogPackage(mContext, failedPackage); + } + + final VersionedPackage logPackage = logPackageTemp; + WatchdogRollbackLogger.logEvent(logPackage, + CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE, + reasonToLog, failedPackageToLog); + + Consumer<Intent> onResult = result -> { + assertInWorkerThread(); + int status = result.getIntExtra(RollbackManager.EXTRA_STATUS, + RollbackManager.STATUS_FAILURE); + if (status == RollbackManager.STATUS_SUCCESS) { + if (rollback.isStaged()) { + int rollbackId = rollback.getRollbackId(); + saveStagedRollbackId(rollbackId, logPackage); + WatchdogRollbackLogger.logEvent(logPackage, + CrashRecoveryStatsLog + .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED, + reasonToLog, failedPackageToLog); + + } else { + WatchdogRollbackLogger.logEvent(logPackage, + CrashRecoveryStatsLog + .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS, + reasonToLog, failedPackageToLog); + } + } else { + WatchdogRollbackLogger.logEvent(logPackage, + CrashRecoveryStatsLog + .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE, + reasonToLog, failedPackageToLog); + } + if (rollback.isStaged()) { + markStagedSessionHandled(rollback.getRollbackId()); + // Wait for all pending staged sessions to get handled before rebooting. + if (isPendingStagedSessionsEmpty()) { + CrashRecoveryProperties.attemptingReboot(true); + mContext.getSystemService(PowerManager.class).reboot("Rollback staged install"); + } + } + }; + + // Define a BroadcastReceiver to handle the result + BroadcastReceiver rollbackReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent result) { + mHandler.post(() -> onResult.accept(result)); + } + }; + + String intentActionName = CLASS_NAME + rollback.getRollbackId(); + // Register the BroadcastReceiver + mContext.registerReceiver(rollbackReceiver, + new IntentFilter(intentActionName), + Context.RECEIVER_NOT_EXPORTED); + + Intent intentReceiver = new Intent(intentActionName); + intentReceiver.putExtra("rollbackId", rollback.getRollbackId()); + intentReceiver.setPackage(mContext.getPackageName()); + intentReceiver.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + + PendingIntent rollbackPendingIntent = PendingIntent.getBroadcast(mContext, + rollback.getRollbackId(), + intentReceiver, + PendingIntent.FLAG_MUTABLE); + + rollbackManager.commitRollback(rollback.getRollbackId(), + Collections.singletonList(failedPackage), + rollbackPendingIntent.getIntentSender()); + } + + /** + * Two-phase rollback: + * 1. roll back rebootless apexes first + * 2. roll back all remaining rollbacks if native crash doesn't stop after (1) is done + * + * This approach gives us a better chance to correctly attribute native crash to rebootless + * apex update without rolling back Mainline updates which might contains critical security + * fixes. + */ + @WorkerThread + private boolean useTwoPhaseRollback(List<RollbackInfo> rollbacks) { + assertInWorkerThread(); + if (!mTwoPhaseRollbackEnabled) { + return false; + } + + Slog.i(TAG, "Rolling back all rebootless APEX rollbacks"); + boolean found = false; + for (RollbackInfo rollback : rollbacks) { + if (isRebootlessApex(rollback)) { + VersionedPackage firstRollback = + rollback.getPackages().get(0).getVersionRolledBackFrom(); + rollbackPackage(rollback, firstRollback, + PackageWatchdog.FAILURE_REASON_NATIVE_CRASH); + found = true; + } + } + return found; + } + + /** + * Rollback the package that has minimum rollback impact level. + * @param availableRollbacks all available rollbacks + * @param rollbackReason reason to rollback + */ + private void triggerLeastImpactLevelRollback(List<RollbackInfo> availableRollbacks, + @FailureReasons int rollbackReason) { + int minRollbackImpactLevel = getMinRollbackImpactLevel(availableRollbacks); + + if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_LOW) { + // Apply all available low impact rollbacks. + mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason)); + } else if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH) { + // Check disable_high_impact_rollback device config before performing rollback + if (SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) { + return; + } + // Rollback one package at a time. If that doesn't resolve the issue, rollback + // next with same impact level. + mHandler.post(() -> rollbackHighImpact(availableRollbacks, rollbackReason)); + } + } + + /** + * sort the available high impact rollbacks by first package name to have a deterministic order. + * Apply the first available rollback. + * @param availableRollbacks all available rollbacks + * @param rollbackReason reason to rollback + */ + @WorkerThread + private void rollbackHighImpact(List<RollbackInfo> availableRollbacks, + @FailureReasons int rollbackReason) { + assertInWorkerThread(); + List<RollbackInfo> highImpactRollbacks = + getRollbacksAvailableForImpactLevel( + availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_HIGH); + + // sort rollbacks based on package name of the first package. This is to have a + // deterministic order of rollbacks. + List<RollbackInfo> sortedHighImpactRollbacks = highImpactRollbacks.stream().sorted( + Comparator.comparing(a -> a.getPackages().get(0).getPackageName())).toList(); + VersionedPackage firstRollback = + sortedHighImpactRollbacks + .get(0) + .getPackages() + .get(0) + .getVersionRolledBackFrom(); + Slog.i(TAG, "Rolling back high impact rollback for package: " + + firstRollback.getPackageName()); + rollbackPackage(sortedHighImpactRollbacks.get(0), firstRollback, rollbackReason); + } + + @WorkerThread + private void rollbackAll(@FailureReasons int rollbackReason) { + assertInWorkerThread(); + RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); + List<RollbackInfo> rollbacks = rollbackManager.getAvailableRollbacks(); + if (useTwoPhaseRollback(rollbacks)) { + return; + } + + Slog.i(TAG, "Rolling back all available rollbacks"); + // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all + // pending staged rollbacks are handled. + for (RollbackInfo rollback : rollbacks) { + if (rollback.isStaged()) { + mPendingStagedRollbackIds.add(rollback.getRollbackId()); + } + } + + for (RollbackInfo rollback : rollbacks) { + VersionedPackage firstRollback = + rollback.getPackages().get(0).getVersionRolledBackFrom(); + rollbackPackage(rollback, firstRollback, rollbackReason); + } + } + + /** + * Rollback all available low impact rollbacks + * @param availableRollbacks all available rollbacks + * @param rollbackReason reason to rollbacks + */ + @WorkerThread + private void rollbackAllLowImpact( + List<RollbackInfo> availableRollbacks, @FailureReasons int rollbackReason) { + assertInWorkerThread(); + + List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel( + availableRollbacks, + PackageManager.ROLLBACK_USER_IMPACT_LOW); + if (useTwoPhaseRollback(lowImpactRollbacks)) { + return; + } + + Slog.i(TAG, "Rolling back all available low impact rollbacks"); + logCrashRecoveryEvent(Log.DEBUG, "Rolling back all available. Reason: " + rollbackReason); + // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all + // pending staged rollbacks are handled. + for (RollbackInfo rollback : lowImpactRollbacks) { + if (rollback.isStaged()) { + mPendingStagedRollbackIds.add(rollback.getRollbackId()); + } + } + + for (RollbackInfo rollback : lowImpactRollbacks) { + VersionedPackage firstRollback = + rollback.getPackages().get(0).getVersionRolledBackFrom(); + rollbackPackage(rollback, firstRollback, rollbackReason); + } + } + + private List<RollbackInfo> getRollbacksAvailableForImpactLevel( + List<RollbackInfo> availableRollbacks, int impactLevel) { + return availableRollbacks.stream() + .filter(rollbackInfo -> rollbackInfo.getRollbackImpactLevel() == impactLevel) + .toList(); + } + + private int getMinRollbackImpactLevel(List<RollbackInfo> availableRollbacks) { + return availableRollbacks.stream() + .mapToInt(RollbackInfo::getRollbackImpactLevel) + .min() + .orElse(-1); + } + + private int getUserImpactBasedOnRollbackImpactLevel(List<RollbackInfo> availableRollbacks) { + int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + int minImpact = getMinRollbackImpactLevel(availableRollbacks); + switch (minImpact) { + case PackageManager.ROLLBACK_USER_IMPACT_LOW: + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; + break; + case PackageManager.ROLLBACK_USER_IMPACT_HIGH: + if (!SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) { + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_90; + } + break; + default: + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + return impact; + } + + @VisibleForTesting + Handler getHandler() { + return mHandler; + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java new file mode 100644 index 000000000000..9cfed02f9355 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * 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.android.server.rollback; + +import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.content.rollback.PackageRollbackInfo; +import android.content.rollback.RollbackInfo; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.PackageWatchdog; +import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; + +import java.util.List; + +/** + * This class handles the logic for logging Watchdog-triggered rollback events. + * @hide + */ +public final class WatchdogRollbackLogger { + private static final String TAG = "WatchdogRollbackLogger"; + + private static final String LOGGING_PARENT_KEY = "android.content.pm.LOGGING_PARENT"; + + private WatchdogRollbackLogger() { + } + + @Nullable + private static String getLoggingParentName(Context context, @NonNull String packageName) { + PackageManager packageManager = context.getPackageManager(); + try { + int flags = PackageManager.MATCH_APEX | PackageManager.GET_META_DATA; + ApplicationInfo ai = packageManager.getPackageInfo(packageName, flags).applicationInfo; + if (ai.metaData == null) { + return null; + } + return ai.metaData.getString(LOGGING_PARENT_KEY); + } catch (Exception e) { + Slog.w(TAG, "Unable to discover logging parent package: " + packageName, e); + return null; + } + } + + /** + * Returns the logging parent of a given package if it exists, {@code null} otherwise. + * + * The logging parent is defined by the {@code android.content.pm.LOGGING_PARENT} field in the + * metadata of a package's AndroidManifest.xml. + */ + @VisibleForTesting + @Nullable + static VersionedPackage getLogPackage(Context context, + @NonNull VersionedPackage failingPackage) { + String logPackageName; + VersionedPackage loggingParent; + logPackageName = getLoggingParentName(context, failingPackage.getPackageName()); + if (logPackageName == null) { + return null; + } + try { + loggingParent = new VersionedPackage(logPackageName, context.getPackageManager() + .getPackageInfo(logPackageName, 0 /* flags */).getLongVersionCode()); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + return loggingParent; + } + + static void logRollbackStatusOnBoot(Context context, int rollbackId, String logPackageName, + List<RollbackInfo> recentlyCommittedRollbacks) { + PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); + + RollbackInfo rollback = null; + for (RollbackInfo info : recentlyCommittedRollbacks) { + if (rollbackId == info.getRollbackId()) { + rollback = info; + break; + } + } + + if (rollback == null) { + Slog.e(TAG, "rollback info not found for last staged rollback: " + rollbackId); + return; + } + + // Use the version of the logging parent that was installed before + // we rolled back for logging purposes. + VersionedPackage oldLoggingPackage = null; + if (!TextUtils.isEmpty(logPackageName)) { + for (PackageRollbackInfo packageRollback : rollback.getPackages()) { + if (logPackageName.equals(packageRollback.getPackageName())) { + oldLoggingPackage = packageRollback.getVersionRolledBackFrom(); + break; + } + } + } + + int sessionId = rollback.getCommittedSessionId(); + PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId); + if (sessionInfo == null) { + Slog.e(TAG, "On boot completed, could not load session id " + sessionId); + return; + } + + if (sessionInfo.isStagedSessionApplied()) { + logEvent(oldLoggingPackage, + WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS, + WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN, ""); + } else if (sessionInfo.isStagedSessionFailed()) { + logEvent(oldLoggingPackage, + WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE, + WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN, ""); + } + } + + /** + * Log a Watchdog rollback event to statsd. + * + * @param logPackage the package to associate the rollback with. + * @param type the state of the rollback. + * @param rollbackReason the reason Watchdog triggered a rollback, if known. + * @param failingPackageName the failing package or process which triggered the rollback. + */ + public static void logEvent(@Nullable VersionedPackage logPackage, int type, + int rollbackReason, @NonNull String failingPackageName) { + String logMsg = "Watchdog event occurred with type: " + rollbackTypeToString(type) + + " logPackage: " + logPackage + + " rollbackReason: " + rollbackReasonToString(rollbackReason) + + " failedPackageName: " + failingPackageName; + Slog.i(TAG, logMsg); + if (logPackage != null) { + CrashRecoveryStatsLog.write( + CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED, + type, + logPackage.getPackageName(), + logPackage.getVersionCode(), + rollbackReason, + failingPackageName, + new byte[]{}); + } else { + // In the case that the log package is null, still log an empty string as an + // indication that retrieving the logging parent failed. + CrashRecoveryStatsLog.write( + CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED, + type, + "", + 0, + rollbackReason, + failingPackageName, + new byte[]{}); + } + + logTestProperties(logMsg); + } + + /** + * Writes properties which will be used by rollback tests to check if particular rollback + * events have occurred. + */ + private static void logTestProperties(String logMsg) { + // This property should be on only during the tests + if (!SystemProperties.getBoolean("persist.sys.rollbacktest.enabled", false)) { + return; + } + logCrashRecoveryEvent(Log.DEBUG, logMsg); + } + + @VisibleForTesting + static int mapFailureReasonToMetric(@PackageWatchdog.FailureReasons int failureReason) { + switch (failureReason) { + case PackageWatchdog.FAILURE_REASON_NATIVE_CRASH: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH; + case PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK; + case PackageWatchdog.FAILURE_REASON_APP_CRASH: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH; + case PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING; + case PackageWatchdog.FAILURE_REASON_BOOT_LOOP: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING; + default: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN; + } + } + + private static String rollbackTypeToString(int type) { + switch (type) { + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE: + return "ROLLBACK_INITIATE"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS: + return "ROLLBACK_SUCCESS"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE: + return "ROLLBACK_FAILURE"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED: + return "ROLLBACK_BOOT_TRIGGERED"; + default: + return "UNKNOWN"; + } + } + + private static String rollbackReasonToString(int reason) { + switch (reason) { + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH: + return "REASON_NATIVE_CRASH"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK: + return "REASON_EXPLICIT_HEALTH_CHECK"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH: + return "REASON_APP_CRASH"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING: + return "REASON_APP_NOT_RESPONDING"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT: + return "REASON_NATIVE_CRASH_DURING_BOOT"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING: + return "REASON_BOOT_LOOP"; + default: + return "UNKNOWN"; + } + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java new file mode 100644 index 000000000000..29ff7cced897 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 android.util; + +import android.annotation.Nullable; + +/** + * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java + * + * @hide + */ +public class ArrayUtils { + private ArrayUtils() { /* cannot be instantiated */ } + + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable int[] array) { + return array == null || array.length == 0; + } + + /** + * True if the byte array is null or has length 0. + */ + public static boolean isEmpty(@Nullable byte[] array) { + return array == null || array.length == 0; + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java new file mode 100644 index 000000000000..d60a9b9847ca --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 android.util; + +import android.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Bits and pieces copied from hidden API of android.os.FileUtils. + * + * @hide + */ +public class FileUtils { + /** + * Read a text file into a String, optionally limiting the length. + * + * @param file to read (will not seek, so things like /proc files are OK) + * @param max length (positive for head, negative of tail, 0 for no limit) + * @param ellipsis to add of the file was truncated (can be null) + * @return the contents of the file, possibly truncated + * @throws IOException if something goes wrong reading the file + * @hide + */ + public static @Nullable String readTextFile(@Nullable File file, @Nullable int max, + @Nullable String ellipsis) throws IOException { + InputStream input = new FileInputStream(file); + // wrapping a BufferedInputStream around it because when reading /proc with unbuffered + // input stream, bytes read not equal to buffer size is not necessarily the correct + // indication for EOF; but it is true for BufferedInputStream due to its implementation. + BufferedInputStream bis = new BufferedInputStream(input); + try { + long size = file.length(); + if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes + if (size > 0 && (max == 0 || size < max)) max = (int) size; + byte[] data = new byte[max + 1]; + int length = bis.read(data); + if (length <= 0) return ""; + if (length <= max) return new String(data, 0, length); + if (ellipsis == null) return new String(data, 0, max); + return new String(data, 0, max) + ellipsis; + } else if (max < 0) { // "tail" mode: keep the last N + int len; + boolean rolled = false; + byte[] last = null; + byte[] data = null; + do { + if (last != null) rolled = true; + byte[] tmp = last; + last = data; + data = tmp; + if (data == null) data = new byte[-max]; + len = bis.read(data); + } while (len == data.length); + + if (last == null && len <= 0) return ""; + if (last == null) return new String(data, 0, len); + if (len > 0) { + rolled = true; + System.arraycopy(last, len, last, 0, last.length - len); + System.arraycopy(data, 0, last, last.length - len, len); + } + if (ellipsis == null || !rolled) return new String(last); + return ellipsis + new String(last); + } else { // "cat" mode: size unknown, read it all in streaming fashion + ByteArrayOutputStream contents = new ByteArrayOutputStream(); + int len; + byte[] data = new byte[1024]; + do { + len = bis.read(data); + if (len > 0) contents.write(data, 0, len); + } while (len == data.length); + return contents.toString(); + } + } finally { + bis.close(); + input.close(); + } + } + + /** + * Perform an fsync on the given FileOutputStream. The stream at this + * point must be flushed but not yet closed. + * + * @hide + */ + public static boolean sync(FileOutputStream stream) { + try { + if (stream != null) { + stream.getFD().sync(); + } + return true; + } catch (IOException e) { + } + return false; + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java b/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java new file mode 100644 index 000000000000..9a24ada8b69a --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 android.util; + +import libcore.util.EmptyArray; + +import java.util.NoSuchElementException; + +/** + * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java + * + * @hide + */ +public class LongArrayQueue { + + private long[] mValues; + private int mSize; + private int mHead; + private int mTail; + + private long[] newUnpaddedLongArray(int num) { + return new long[num]; + } + /** + * Initializes a queue with the given starting capacity. + * + * @param initialCapacity the capacity. + */ + public LongArrayQueue(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.LONG; + } else { + mValues = newUnpaddedLongArray(initialCapacity); + } + mSize = 0; + mHead = mTail = 0; + } + + /** + * Initializes a queue with default starting capacity. + */ + public LongArrayQueue() { + this(16); + } + + /** @hide */ + public static int growSize(int currentSize) { + return currentSize <= 4 ? 8 : currentSize * 2; + } + + private void grow() { + if (mSize < mValues.length) { + throw new IllegalStateException("Queue not full yet!"); + } + final int newSize = growSize(mSize); + final long[] newArray = newUnpaddedLongArray(newSize); + final int r = mValues.length - mHead; // Number of elements on and to the right of head. + System.arraycopy(mValues, mHead, newArray, 0, r); + System.arraycopy(mValues, 0, newArray, r, mHead); + mValues = newArray; + mHead = 0; + mTail = mSize; + } + + /** + * Returns the number of elements in the queue. + */ + public int size() { + return mSize; + } + + /** + * Removes all elements from this queue. + */ + public void clear() { + mSize = 0; + mHead = mTail = 0; + } + + /** + * Adds a value to the tail of the queue. + * + * @param value the value to be added. + */ + public void addLast(long value) { + if (mSize == mValues.length) { + grow(); + } + mValues[mTail] = value; + mTail = (mTail + 1) % mValues.length; + mSize++; + } + + /** + * Removes an element from the head of the queue. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long removeFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final long ret = mValues[mHead]; + mHead = (mHead + 1) % mValues.length; + mSize--; + return ret; + } + + /** + * Returns the element at the given position from the head of the queue, where 0 represents the + * head of the queue. + * + * @param position the position from the head of the queue. + * @return the element found at the given position. + * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or + * {@code position} >= {@link #size()} + */ + public long get(int position) { + if (position < 0 || position >= mSize) { + throw new IndexOutOfBoundsException("Index " + position + + " not valid for a queue of size " + mSize); + } + final int index = (mHead + position) % mValues.length; + return mValues[index]; + } + + /** + * Returns the element at the head of the queue, without removing it. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty + */ + public long peekFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + return mValues[mHead]; + } + + /** + * Returns the element at the tail of the queue. + * + * @return the element at the tail of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long peekLast() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1; + return mValues[index]; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + if (mSize <= 0) { + return "{}"; + } + + final StringBuilder buffer = new StringBuilder(mSize * 64); + buffer.append('{'); + buffer.append(get(0)); + for (int i = 1; i < mSize; i++) { + buffer.append(", "); + buffer.append(get(i)); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java new file mode 100644 index 000000000000..488b531c2b8a --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 android.util; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * Bits and pieces copied from hidden API of + * frameworks/base/core/java/com/android/internal/util/XmlUtils.java + * + * @hide + */ +public class XmlUtils { + + /** @hide */ + public static final void beginDocument(XmlPullParser parser, String firstElementName) + throws XmlPullParserException, IOException { + int type; + while ((type = parser.next()) != parser.START_TAG + && type != parser.END_DOCUMENT) { + // Do nothing + } + + if (type != parser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + if (!parser.getName().equals(firstElementName)) { + throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + + ", expected " + firstElementName); + } + } + + /** @hide */ + public static boolean nextElementWithin(XmlPullParser parser, int outerDepth) + throws IOException, XmlPullParserException { + for (;;) { + int type = parser.next(); + if (type == XmlPullParser.END_DOCUMENT + || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) { + return false; + } + if (type == XmlPullParser.START_TAG + && parser.getDepth() == outerDepth + 1) { + return true; + } + } + } +} |