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

Commit 51aa7435 authored by Ned Burns's avatar Ned Burns
Browse files

Dump LogBuffers to a file before crashing

For now, only do this in NotifCollection. Some day we may expand this to
being a global exception handler, but for now, clients must be explicit
when dumping.

Test: atest, manual
Bug: 151317347,112656837
Change-Id: I22ff91e8f8347e85b1bf5e874d0bb5e29ced5a75
parent 08403647
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -79,7 +79,8 @@ import javax.inject.Inject
 */
class DumpHandler @Inject constructor(
    private val context: Context,
    private val dumpManager: DumpManager
    private val dumpManager: DumpManager,
    private val logBufferEulogizer: LogBufferEulogizer
) {
    /**
     * Dump the diagnostics! Behavior can be controlled via [args].
@@ -125,6 +126,7 @@ class DumpHandler @Inject constructor(

    private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) {
        dumpManager.dumpBuffers(pw, args.tailLength)
        logBufferEulogizer.readEulogyIfPresent(pw)
    }

    private fun dumpDumpables(fw: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
+150 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.dump

import android.content.Context
import android.util.Log
import com.android.systemui.log.LogBuffer
import com.android.systemui.util.io.Files
import com.android.systemui.util.time.SystemClock
import java.io.IOException
import java.io.PrintWriter
import java.io.UncheckedIOException
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardOpenOption.CREATE
import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING
import java.nio.file.attribute.BasicFileAttributes
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Dumps all [LogBuffer]s to a file
 *
 * Intended for emergencies, i.e. we're about to crash. This file can then be read at a later date
 * (usually in a bug report).
 */
@Singleton
class LogBufferEulogizer(
    private val dumpManager: DumpManager,
    private val systemClock: SystemClock,
    private val files: Files,
    private val logPath: Path,
    private val minWriteGap: Long,
    private val maxLogAgeToDump: Long
) {
    @Inject constructor(
        context: Context,
        dumpManager: DumpManager,
        systemClock: SystemClock,
        files: Files
    ) : this(
        dumpManager,
        systemClock,
        files,
        Paths.get(context.filesDir.toPath().toString(), "log_buffers.txt"),
        MIN_WRITE_GAP,
        MAX_AGE_TO_DUMP
    )

    /**
     * Dumps all active log buffers to a file
     *
     * The file will be prefaced by the [reason], which will then be returned (presumably so it can
     * be thrown).
     */
    fun <T : Exception> record(reason: T): T {
        val start = systemClock.uptimeMillis()
        var duration = 0L

        Log.i(TAG, "Performing emergency dump of log buffers")

        val millisSinceLastWrite = getMillisSinceLastWrite(logPath)
        if (millisSinceLastWrite < minWriteGap) {
            Log.w(TAG, "Cannot dump logs, last write was only $millisSinceLastWrite ms ago")
            return reason
        }

        try {
            val writer = files.newBufferedWriter(logPath, CREATE, TRUNCATE_EXISTING)
            writer.use { out ->
                val pw = PrintWriter(out)

                pw.println(DATE_FORMAT.format(systemClock.currentTimeMillis()))
                pw.println()
                pw.println("Dump triggered by exception:")
                reason.printStackTrace(pw)
                dumpManager.dumpBuffers(pw, 0)
                duration = systemClock.uptimeMillis() - start
                pw.println()
                pw.println("Buffer eulogy took ${duration}ms")
            }
        } catch (e: Exception) {
            Log.e(TAG, "Exception while attempting to dump buffers, bailing", e)
        }

        Log.i(TAG, "Buffer eulogy took ${duration}ms")

        return reason
    }

    /**
     * If a eulogy file is present, writes its contents to [pw].
     */
    fun readEulogyIfPresent(pw: PrintWriter) {
        try {
            val millisSinceLastWrite = getMillisSinceLastWrite(logPath)
            if (millisSinceLastWrite > maxLogAgeToDump) {
                Log.i(TAG, "Not eulogizing buffers; they are " +
                        TimeUnit.HOURS.convert(millisSinceLastWrite, TimeUnit.MILLISECONDS) +
                        " hours old")
                return
            }

            files.lines(logPath).use { s ->
                pw.println()
                pw.println()
                pw.println("=============== BUFFERS FROM MOST RECENT CRASH ===============")
                s.forEach { line ->
                    pw.println(line)
                }
            }
        } catch (e: IOException) {
            // File doesn't exist, okay
        } catch (e: UncheckedIOException) {
            Log.e(TAG, "UncheckedIOException while dumping the core", e)
        }
    }

    private fun getMillisSinceLastWrite(path: Path): Long {
        val stats = try {
            files.readAttributes(path, BasicFileAttributes::class.java)
        } catch (e: IOException) {
            // File doesn't exist
            null
        }
        return systemClock.currentTimeMillis() - (stats?.lastModifiedTime()?.toMillis() ?: 0)
    }
}

private const val TAG = "BufferEulogizer"
private val MIN_WRITE_GAP = TimeUnit.MINUTES.toMillis(5)
private val MAX_AGE_TO_DUMP = TimeUnit.HOURS.toMillis(48)
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
 No newline at end of file
+1 −1
Original line number Diff line number Diff line
@@ -209,4 +209,4 @@ class LogBuffer(
}

private const val TAG = "LogBuffer"
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.S", Locale.US)
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
+22 −11
Original line number Diff line number Diff line
@@ -61,6 +61,7 @@ import androidx.annotation.NonNull;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dumpable;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.LogBufferEulogizer;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent;
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
@@ -126,6 +127,7 @@ public class NotifCollection implements Dumpable {
    private final IStatusBarService mStatusBarService;
    private final FeatureFlags mFeatureFlags;
    private final NotifCollectionLogger mLogger;
    private final LogBufferEulogizer mEulogizer;

    private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
    private final Collection<NotificationEntry> mReadOnlyNotificationSet =
@@ -146,10 +148,12 @@ public class NotifCollection implements Dumpable {
            IStatusBarService statusBarService,
            DumpManager dumpManager,
            FeatureFlags featureFlags,
            NotifCollectionLogger logger) {
            NotifCollectionLogger logger,
            LogBufferEulogizer logBufferEulogizer) {
        Assert.isMainThread();
        mStatusBarService = statusBarService;
        mLogger = logger;
        mEulogizer = logBufferEulogizer;
        dumpManager.registerDumpable(TAG, this);
        mFeatureFlags = featureFlags;
    }
@@ -223,7 +227,8 @@ public class NotifCollection implements Dumpable {

            requireNonNull(stats);
            if (entry != mNotificationSet.get(entry.getKey())) {
                throw new IllegalStateException("Invalid entry: " + entry.getKey());
                throw mEulogizer.record(
                        new IllegalStateException("Invalid entry: " + entry.getKey()));
            }

            if (entry.getDismissState() == DISMISSED) {
@@ -367,8 +372,11 @@ public class NotifCollection implements Dumpable {

        final NotificationEntry entry = mNotificationSet.get(sbn.getKey());
        if (entry == null) {
            throw new IllegalStateException("No notification to remove with key " + sbn.getKey());
            throw mEulogizer.record(
                    new IllegalStateException("No notification to remove with key "
                            + sbn.getKey()));
        }

        entry.mCancellationReason = reason;
        tryRemoveNotification(entry);
        applyRanking(rankingMap);
@@ -426,12 +434,15 @@ public class NotifCollection implements Dumpable {
     */
    private boolean tryRemoveNotification(NotificationEntry entry) {
        if (mNotificationSet.get(entry.getKey()) != entry) {
            throw new IllegalStateException("No notification to remove with key " + entry.getKey());
            throw mEulogizer.record(
                    new IllegalStateException("No notification to remove with key "
                            + entry.getKey()));
        }

        if (!isCanceled(entry)) {
            throw new IllegalStateException("Cannot remove notification " + entry.getKey()
                        + ": has not been marked for removal");
            throw mEulogizer.record(
                    new IllegalStateException("Cannot remove notification " + entry.getKey()
                            + ": has not been marked for removal"));
        }

        if (isDismissedByUser(entry)) {
@@ -501,11 +512,11 @@ public class NotifCollection implements Dumpable {
        checkForReentrantCall();

        if (!entry.mLifetimeExtenders.remove(extender)) {
            throw new IllegalStateException(
            throw mEulogizer.record(new IllegalStateException(
                    String.format(
                            "Cannot end lifetime extension for extender \"%s\" (%s)",
                            extender.getName(),
                            extender));
                            extender)));
        }

        mLogger.logLifetimeExtensionEnded(
@@ -581,11 +592,11 @@ public class NotifCollection implements Dumpable {
        checkForReentrantCall();

        if (!entry.mDismissInterceptors.remove(interceptor)) {
            throw new IllegalStateException(
            throw mEulogizer.record(new IllegalStateException(
                    String.format(
                            "Cannot end dismiss interceptor for interceptor \"%s\" (%s)",
                            interceptor.getName(),
                            interceptor));
                            interceptor)));
        }

        if (!isDismissIntercepted(entry)) {
@@ -608,7 +619,7 @@ public class NotifCollection implements Dumpable {

    private void checkForReentrantCall() {
        if (mAmDispatchingToOtherCode) {
            throw new IllegalStateException("Reentrant call detected");
            throw mEulogizer.record(new IllegalStateException("Reentrant call detected"));
        }
    }

+58 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.util.io;

import androidx.annotation.NonNull;

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.stream.Stream;

import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * Wrapper around {@link java.nio.file.Files} that can be mocked in tests.
 */
@Singleton
public class Files {
    @Inject
    public Files() { }

    /** See {@link java.nio.file.Files#newBufferedWriter} */
    public BufferedWriter newBufferedWriter(Path path, OpenOption... options) throws IOException {
        return java.nio.file.Files.newBufferedWriter(path, StandardCharsets.UTF_8, options);
    }

    /** See {@link java.nio.file.Files#lines} */
    public Stream<String> lines(Path path) throws IOException {
        return java.nio.file.Files.lines(path);
    }

    /** See {@link java.nio.file.Files#readAttributes} */
    public <A extends BasicFileAttributes> A readAttributes(
            @NonNull Path path,
            @NonNull Class<A> type,
            @NonNull LinkOption... options) throws IOException {
        return java.nio.file.Files.readAttributes(path, type, options);
    }
}
Loading