Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ad8b3d36 authored by Hawkwood's avatar Hawkwood
Browse files

Persist Plugin failures for 24 hours

Persisting plugin failures for a period of time will prevent crash loops
from manifesting. Instead even if the current SystemUI process dies do
to an error, when it is restarted it will check for a recent crash and
revert to the default clock temporarially when it finds one.

Bug: 438515243
Test: Reproduced earlier crashloop
Flag: com.android.systemui.scene_container
Change-Id: Ib2112d0b0bbafc61d99417358278dd22f9760fe0
parent e45b09dc
Loading
Loading
Loading
Loading
+169 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.systemui.plugins.processor

import java.io.BufferedWriter
import java.io.Writer

/** [JavaFileWriter] has a collection of utility functions for generating java files. */
class JavaFileWriter(val writer: TabbedWriter) : TabbedWriter by writer {
    fun pkg(pkg: String) {
        line("package $pkg;")
        line()
    }

    fun imports(vararg importGroups: List<String>) {
        var written = mutableSetOf<String>()
        for (importGroup in importGroups) {
            if (importGroup.size > 0) {
                for (importTarget in importGroup) {
                    if (written.add(importTarget)) {
                        line("import $importTarget;")
                    }
                }
                line()
            }
        }
    }

    fun cls(
        className: String,
        isFinal: Boolean = false,
        visibility: String = "public",
        baseType: String? = null,
        interfaces: List<String> = listOf(),
        contents: JavaClassWriter.() -> Unit = {},
    ) {
        val writer = JavaClassWriter(this.writer, className)
        braceBlock(
            buildString {
                append(visibility)
                if (isFinal) {
                    append(" final")
                }

                append(" class ")
                append(className)

                if (baseType != null) {
                    append(" extends ")
                    append(baseType)
                }

                if (interfaces.size > 0) {
                    append(" implements ")
                    var isFirst = true
                    for (interfaceType in interfaces) {
                        if (!isFirst) append(", ")
                        append(interfaceType)
                        isFirst = false
                    }
                }
            }
        ) {
            writer.contents()
        }
    }

    companion object {
        fun writeTo(writer: Writer, write: JavaFileWriter.() -> Unit) {
            BufferedWriter(writer).use { bufferedWriter ->
                JavaFileWriter(TabbedWriterImpl(bufferedWriter)).write()
            }
        }
    }
}

/** [JavaClassWriter] has a collection of utility functions for generating java classes. */
class JavaClassWriter(val writer: TabbedWriter, val className: String) : TabbedWriter by writer {
    fun constructor(
        visibility: String = "public",
        args: JavaMethodWriter.() -> Unit = {},
        contents: JavaMethodWriter.() -> Unit = {},
    ) {
        method(
            methodName = "$className",
            returnType = "",
            visibility = visibility,
            args = args,
            contents = contents,
        )
    }

    fun method(
        methodName: String,
        visibility: String = "public",
        returnType: String = "void",
        isStatic: Boolean = false,
        args: JavaMethodWriter.() -> Unit = {},
        contents: JavaMethodWriter.() -> Unit = {},
    ) {
        val writer =
            JavaMethodWriter(
                this.writer,
                className = className,
                methodName = methodName,
                returnType = returnType,
                isStatic = isStatic,
            )

        parenBlock(
            buildString {
                append(visibility)
                append(" ")

                if (isStatic) {
                    append(" static ")
                }

                if (!returnType.isNullOrEmpty()) {
                    append(returnType)
                    append(" ")
                }

                append(methodName)
            }
        ) {
            writer.args()
        }

        braceBlock { writer.contents() }
        line()
    }
}

/** [JavaMethodWriter] has a collection of utility functions for generating java functions. */
class JavaMethodWriter(
    val writer: TabbedWriter,
    val className: String,
    val methodName: String,
    val returnType: String,
    val isStatic: Boolean,
) : TabbedWriter by writer {
    val isVoid: Boolean = returnType == "void"

    var isFirstArg = true
    var callArgs = StringBuilder()

    fun arg(name: String, type: String) {
        if (!isFirstArg) {
            completeLine(",")
            callArgs.append(", ")
        }

        startLine("$type $name")
        callArgs.append(name)
        isFirstArg = false
    }
}
+209 −229

File changed.

Preview size limit exceeded, changes collapsed.

+30 −22
Original line number Diff line number Diff line
@@ -14,27 +14,41 @@
package com.android.systemui.plugins.processor

import java.io.BufferedWriter
import java.io.Writer

interface TabbedWriter {
    val tabCount: Int

    fun line()

    fun line(str: String)

    fun completeLine(str: String)

    fun startLine(str: String)

    fun appendLine(str: String)

    fun braceBlock(str: String = "", write: TabbedWriter.() -> Unit)

    fun parenBlock(str: String = "", write: TabbedWriter.() -> Unit)
}

/**
 * [TabbedWriter] is a convience class which tracks and writes correctly tabbed lines for generating
 * source files. These files don't need to be correctly tabbed as they're ephemeral and not part of
 * the source tree, but correct tabbing makes debugging much easier when the build fails.
 * [TabbedWriter] is a convenience class which tracks and writes correctly tabbed lines of text for
 * generating source files. These files don't need to be correctly tabbed as they're ephemeral and
 * not part of the source tree, but correct tabbing makes debugging much easier.
 */
class TabbedWriter(writer: Writer) : AutoCloseable {
    private val target = BufferedWriter(writer)
class TabbedWriterImpl(private val target: BufferedWriter) : TabbedWriter {
    private var isInProgress = false
    var tabCount: Int = 0
    override var tabCount: Int = 0
        private set

    override fun close() = target.close()

    fun line() {
    override fun line() {
        target.newLine()
        isInProgress = false
    }

    fun line(str: String) {
    override fun line(str: String) {
        if (isInProgress) {
            target.newLine()
        }
@@ -45,7 +59,7 @@ class TabbedWriter(writer: Writer) : AutoCloseable {
        isInProgress = false
    }

    fun completeLine(str: String) {
    override fun completeLine(str: String) {
        if (!isInProgress) {
            target.newLine()
            target.append("    ".repeat(tabCount))
@@ -56,7 +70,7 @@ class TabbedWriter(writer: Writer) : AutoCloseable {
        isInProgress = false
    }

    fun startLine(str: String) {
    override fun startLine(str: String) {
        if (isInProgress) {
            target.newLine()
        }
@@ -66,7 +80,7 @@ class TabbedWriter(writer: Writer) : AutoCloseable {
        isInProgress = true
    }

    fun appendLine(str: String) {
    override fun appendLine(str: String) {
        if (!isInProgress) {
            target.append("    ".repeat(tabCount))
        }
@@ -75,11 +89,11 @@ class TabbedWriter(writer: Writer) : AutoCloseable {
        isInProgress = true
    }

    fun braceBlock(str: String = "", write: TabbedWriter.() -> Unit) {
    override fun braceBlock(str: String, write: TabbedWriter.() -> Unit) {
        block(str, " {", "}", true, write)
    }

    fun parenBlock(str: String = "", write: TabbedWriter.() -> Unit) {
    override fun parenBlock(str: String, write: TabbedWriter.() -> Unit) {
        block(str, "(", ")", false, write)
    }

@@ -103,10 +117,4 @@ class TabbedWriter(writer: Writer) : AutoCloseable {
            startLine(end)
        }
    }

    companion object {
        fun writeTo(writer: Writer, write: TabbedWriter.() -> Unit) {
            TabbedWriter(writer).use { it.write() }
        }
    }
}
+50 −8
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.shared.plugins;
import android.app.LoadedApk;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -58,6 +59,14 @@ public class PluginInstance<T extends Plugin>
        implements PluginLifecycleManager, ProtectedPluginListener {
    private static final String TAG = "PluginInstance";

    private static final String FAIL_FILE_FMT = "PluginFailure_%s";
    private static final String FAIL_TIME = "FailureTime";
    private static final String FAIL_MESSAGE = "ErrorMessage";
    private static final String FAIL_STACK_FMT = "Stack[%d]";

    private static final int FAIL_MAX_STACK = 20;
    private static final long FAIL_TIMEOUT_MILLIS = 24 * 60 * 60 * 1000;

    private final Context mHostContext;
    private final PluginListener<T> mListener;
    private final ComponentName mComponentName;
@@ -75,20 +84,16 @@ public class PluginInstance<T extends Plugin>
            Context hostContext,
            PluginListener<T> listener,
            ComponentName componentName,
            PluginFactory<T> pluginFactory,
            @Nullable T plugin) {
            PluginFactory<T> pluginFactory) {
        mHostContext = hostContext;
        mListener = listener;
        mComponentName = componentName;
        mPluginFactory = pluginFactory;
        mPlugin = plugin;
        mTag = String.format("%s[%s]@%h", TAG, mComponentName.getShortClassName(), hashCode());
        mLogger = new Logger(mListener.getLogBuffer() != null ? mListener.getLogBuffer() :
                new LogcatOnlyMessageBuffer(LogLevel.WARNING), mTag);

        if (mPlugin != null) {
            mPluginContext = mPluginFactory.createPluginContext();
        }
        loadFailure();
    }

    @Override
@@ -105,13 +110,50 @@ public class PluginInstance<T extends Plugin>
    public synchronized boolean onFail(String className, String methodName, Throwable failure) {
        PluginActionManager.logFmt(mLogger, LogLevel.ERROR,
                "Failure from %s. Disabling Plugin.", mPlugin.toString(), null);

        storeFailure(failure);
        mHasError = true;
        unloadPlugin();
        mListener.onPluginDetached(this);
        return true;
    }

    /** Persists failure to avoid boot looping if process recovery fails */
    private synchronized void storeFailure(Throwable failure) {
        SharedPreferences.Editor sharedPrefs = mHostContext.getSharedPreferences(
                String.format(FAIL_FILE_FMT, mComponentName.getShortClassName()),
                Context.MODE_PRIVATE).edit();

        sharedPrefs.clear();
        sharedPrefs.putLong(FAIL_TIME, System.currentTimeMillis());
        sharedPrefs.putString(FAIL_MESSAGE, failure.getMessage());
        for (int i = 0; i < failure.getStackTrace().length && i < FAIL_MAX_STACK; i++) {
            String methodSignature = failure.getStackTrace()[i].toString();
            sharedPrefs.putString(String.format(FAIL_STACK_FMT, i), methodSignature);
        }
        sharedPrefs.commit();
    }

    /** Loads a persisted failure if it's still within the timeout. */
    private synchronized void loadFailure() {
        SharedPreferences sharedPrefs = mHostContext.getSharedPreferences(
                String.format(FAIL_FILE_FMT, mComponentName.getShortClassName()),
                Context.MODE_PRIVATE);

        // TODO(b/438515243): Disable in eng builds
        // TODO(b/438515243): Check apk checksums for differences (systemui & plugin)
        // If the failure occurred too long ago, we ignore it to check if it's still happening.
        if (sharedPrefs.getLong(FAIL_TIME, 0) < System.currentTimeMillis() - FAIL_TIMEOUT_MILLIS) {
            return;
        }

        // Log previous the failure so that it appears in debugging data
        PluginActionManager.logFmt(mLogger, LogLevel.ERROR,
                "Disabling Plugin: %s", mPlugin.toString(), null);
        PluginActionManager.logFmt(mLogger, LogLevel.ERROR,
                "Persisted Failure: %s", sharedPrefs.getString(FAIL_MESSAGE, "Unknown"), null);
        mHasError = true;
    }

    /** Alerts listener and plugin that the plugin has been created. */
    public synchronized void onCreate() {
        if (mHasError) {
@@ -307,7 +349,7 @@ public class PluginInstance<T extends Plugin>
            PluginFactory<T> pluginFactory = new PluginFactory<T>(
                    hostContext, mInstanceFactory, pluginAppInfo, componentName,
                    mVersionChecker, pluginClass, mBaseClassLoader);
            return new PluginInstance<T>(hostContext, listener, componentName, pluginFactory, null);
            return new PluginInstance<T>(hostContext, listener, componentName, pluginFactory);
        }

        private boolean isPluginPackagePrivileged(String packageName) {