实战-Android 系统异常日志上报(DropboxManager)

需求
将系统异常日志上报到服务器便于开发统计分析问题。

实现
在DropBoxManagerService.java 中add 添加一个文件时,进行拦截处理,上传到服务器。

diff --git a/services/core/java/com/android/server/DropBoxManagerService.java b/services/core/java/com/android/server/DropBoxManagerService.java
index a44cb72..eeccd80 100644
--- a/services/core/java/com/android/server/DropBoxManagerService.java
+++ b/services/core/java/com/android/server/DropBoxManagerService.java
@@ -18,6 +18,7 @@ package com.android.server;

 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -32,9 +33,11 @@ import android.os.Handler;
 import android.os.Message;
 import android.os.StatFs;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.format.Time;
+import android.text.TextUtils;
 import android.util.Slog;

 import com.android.internal.os.IDropBoxManagerService;
@@ -49,7 +52,11 @@ import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.zip.GZIPOutputStream;
@@ -69,9 +76,22 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {

     // mHandler 'what' value.
     private static final int MSG_SEND_BROADCAST = 1;
+    private static final int MSG_UPDATE_DATABASE = 2;

     private static final boolean PROFILE_DUMP = false;

+    private static final String DROPBOX = "dropbox";
+    private static final int DROPBOX_DISABLED = 0;
+    private static final String mUploadUrl = "xxx";
+    private static final Uri CONTENT_URI = Uri.parse("content://xxxxx");
+    private final static String UNKNOWN_MAC_ADDRESS = "00:00:00:00:00:00";
+    private static final List<String> ENABLED_TAGS = Arrays.asList(
+            "system_server_crash",
+            "data_app_crash",
+            "system_server_watchdog");
+    private static final int EARLIEST_SUPPORTED_TIME = 2007;
+    private static final int LAST_MODIFIED = 7 * 24 * 60 * 60 * 1000;
+
     // TODO: This implementation currently uses one file per entry, which is
     // inefficient for smallish entries -- consider using a single queue file
     // per tag (or even globally) instead.
@@ -103,8 +123,9 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (intent != null && Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+            if (intent != null && Intent.ACTION_FUNTV_2_BOOT_COMPLETED.equals(intent.getAction())) {
                 mBooted = true;
+                uploadLogFile();
                 return;
             }

@@ -143,7 +164,7 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {

         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
-        filter.addAction(Intent.ACTION_BOOT_COMPLETED);
+        filter.addAction(Intent.ACTION_COMPLETED);
         context.registerReceiver(mReceiver, filter);

         mContentResolver.registerContentObserver(
@@ -161,6 +182,14 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
                 if (msg.what == MSG_SEND_BROADCAST) {
                     mContext.sendBroadcastAsUser((Intent)msg.obj, UserHandle.OWNER,
                             android.Manifest.permission.READ_LOGS);
+                } else if (msg.what == MSG_UPDATE_DATABASE) {
+                    try {
+                        String path = (String)msg.obj;
+                        ContentValues cv = new ContentValues();
+                        cv.put("path", path);
+                        cv.put("url", mUploadUrl);
+                        mContext.getContentResolver().insert(CONTENT_URI, cv);
+                    } catch (Exception e){}
                 }
             }
         };
@@ -215,6 +244,10 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
                 flags = flags | DropBoxManager.IS_GZIPPED;
             }

+            byte[] header = getFileHeader().getBytes();
+            output.write(header, 0, header.length);
+            output.flush();
+
             do {
                 output.write(buffer, 0, read);

@@ -242,10 +275,13 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
                 }
             } while (read > 0);

+            if (temp != null)
+                temp.setReadable(true, false);
+
             long time = createEntry(temp, tag, flags);
             temp = null;

-            final Intent dropboxIntent = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
+            /*final Intent dropboxIntent = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
             dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag);
             dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time);
             if (!mBooted) {
@@ -255,7 +291,7 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
             // the caller may be holding the WindowManagerService lock but sendBroadcast requires a
             // lock in ActivityManagerService. ActivityManagerService has been caught holding that
             // very lock while waiting for the WindowManagerService lock.
-            mHandler.sendMessage(mHandler.obtainMessage(MSG_SEND_BROADCAST, dropboxIntent));
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_SEND_BROADCAST, dropboxIntent));*/
         } catch (IOException e) {
             Slog.e(TAG, "Can't write: " + tag, e);
         } finally {
@@ -265,6 +301,42 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
         }
     }

+    private String getFileHeader() {
+        return "Version:" + getRomVersion()
+                + "\n"
+                + "ChipType:" + getChipType()
+                + "\n"
+                + "Mac:" + getMac()
+                + "\n"
+                + "Time:" + new Date().toString()
+                + "\n\n";
+    }
+
+    private void uploadLogFile() {
+        try {
+            if (mStatFs == null) init();
+            Calendar calendar = Calendar.getInstance();
+            File[] files = mDropBoxDir.listFiles();
+            for (File file : files) {
+                calendar.setTimeInMillis(file.lastModified());
+                if (calendar.get(Calendar.YEAR) == EARLIEST_SUPPORTED_TIME) continue;
+                if (System.currentTimeMillis() < file.lastModified() + LAST_MODIFIED) {
+                    updateDatabase(file.getAbsolutePath());
+                    long oldTime = System.currentTimeMillis() - LAST_MODIFIED;
+                    file.setLastModified(oldTime > 0 ? oldTime : 0);
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void updateDatabase(String path) {
+        Message message = mHandler.obtainMessage(MSG_UPDATE_DATABASE);
+        message.obj = path;
+        mHandler.sendMessage(message);
+    }
+
     public boolean isTagEnabled(String tag) {
         final long token = Binder.clearCallingIdentity();
         try {
@@ -501,7 +573,7 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
             this.tag = tag;
             this.timestampMillis = timestampMillis;
             this.flags = flags;
-            this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis +
+            this.file = new File(dir, getFilePrefix() + "@" + Uri.encode(tag) + "@" + timestampMillis +
                     ((flags & DropBoxManager.IS_TEXT) != 0 ? ".txt" : ".dat") +
                     ((flags & DropBoxManager.IS_GZIPPED) != 0 ? ".gz" : ""));

@@ -522,7 +594,7 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
             this.tag = tag;
             this.timestampMillis = timestampMillis;
             this.flags = DropBoxManager.IS_EMPTY;
-            this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis + ".lost");
+            this.file = new File(dir, getFilePrefix() + "@" + Uri.encode(tag) + "@" + timestampMillis + ".lost");
             this.blocks = 0;
             new FileOutputStream(this.file).close();
         }
@@ -537,6 +609,7 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
             this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize);

             String name = file.getName();
+            int prefixIndex = name.indexOf('@');
             int at = name.lastIndexOf('@');
             if (at < 0) {
                 this.tag = null;
@@ -546,7 +619,7 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
             }

             int flags = 0;
-            this.tag = Uri.decode(name.substring(0, at));
+            this.tag = Uri.decode(name.substring(prefixIndex + 1, at));
             if (name.endsWith(".gz")) {
                 flags |= DropBoxManager.IS_GZIPPED;
                 name = name.substring(0, name.length() - 3);
@@ -592,6 +665,7 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
             if (!mDropBoxDir.isDirectory() && !mDropBoxDir.mkdirs()) {
                 throw new IOException("Can't mkdir: " + mDropBoxDir);
             }
+            mDropBoxDir.setExecutable(true, false);
             try {
                 mStatFs = new StatFs(mDropBoxDir.getPath());
                 mBlockSize = mStatFs.getBlockSize();
@@ -676,6 +750,8 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
                     tagFiles.blocks -= late.blocks;
                 }
                 if ((late.flags & DropBoxManager.IS_EMPTY) == 0) {
+                    if (late.timestampMillis > (t + 60 * 1000))
+                        continue;
                     enrollEntry(new EntryFile(
                             late.file, mDropBoxDir, late.tag, t++, late.flags, mBlockSize));
                 } else {
@@ -704,13 +780,22 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
         int maxFiles = Settings.Global.getInt(mContentResolver,
                 Settings.Global.DROPBOX_MAX_FILES, DEFAULT_MAX_FILES);
         long cutoffMillis = System.currentTimeMillis() - ageSeconds * 1000;
+        int renameCounts = 0;
         while (!mAllFiles.contents.isEmpty()) {
             EntryFile entry = mAllFiles.contents.first();
             if (entry.timestampMillis > cutoffMillis && mAllFiles.contents.size() < maxFiles) break;

+            Calendar calendar = Calendar.getInstance();
+            calendar.setTimeInMillis(entry.timestampMillis);
+            int year = calendar.get(Calendar.YEAR);
             FileList tag = mFilesByTag.get(entry.tag);
             if (tag != null && tag.contents.remove(entry)) tag.blocks -= entry.blocks;
             if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
+            if (year == EARLIEST_SUPPORTED_TIME && renameCounts < 5) {
+                rename(entry);
+                renameCounts++;
+                continue;
+            }
             if (entry.file != null) entry.file.delete();
         }

@@ -783,4 +868,42 @@ public final class DropBoxManagerService extends IDropBoxManagerService.Stub {

         return mCachedQuotaBlocks * mBlockSize;
     }
+
+    private void rename(EntryFile entry) {
+        int currentYear = Calendar.getInstance().get(Calendar.YEAR);
+        if (currentYear == EARLIEST_SUPPORTED_TIME) return;
+        String tag = entry.tag;
+        int flags = entry.flags;
+        long currentTimeMillis = System.currentTimeMillis();
+        entry.file.setLastModified(currentTimeMillis);
+        File empty = new File(mDropBoxDir, getFilePrefix() + "@" + Uri.encode(tag) + "@" + currentTimeMillis +
+                ((flags & DropBoxManager.IS_TEXT) != 0 ? ".txt" : ".dat") +
+                ((flags & DropBoxManager.IS_GZIPPED) != 0 ? ".gz" : ""));
+        entry.file.renameTo(empty);
+    }
+
+    private static String getFilePrefix() {
+        return getMac() + "_" + getChipType() + "_" + getRomVersion();
+    }
+
+    private static String getMac() {
+        String mac = UNKNOWN_MAC_ADDRESS;
+        try {
+            mac = SystemProperties.get("ro.boot.mac");
+            if (!TextUtils.isEmpty(mac)) {
+                mac = mac.replaceAll(":", "").toLowerCase();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return mac;
+    }
+
+    private static String getChipType() {
+        return SystemProperties.get("persist.sys.chiptype", "unknown");
+    }
+
+    private static String getRomVersion() {
+        return android.os.Build.VERSION.INCREMENTAL;
+    }
 }

DropBoxManager
DropBoxManager 是 Android 用来持续化存储系统数据的机制, 主要用于记录 Android 运行过程中, 内核, 系统进程, 用户进程等出现严重问题时的 log, 可以认为这是一个可持续存储的系统级别的 logcat.

console:/data/system/dropbox # ls -l
total 848
-rw------- 1 system system   584 2021-12-20 11:29 SYSTEM_AUDIT@1639626755142.txt
-rw------- 1 system system   584 2021-12-16 11:52 SYSTEM_BOOT@1639626761116.txt
-rw------- 1 system system  1176 2021-12-20 11:29 SYSTEM_LAST_KMSG@1639626755141.txt
-rw------- 1 system system 15870 2021-12-20 11:58 SYSTEM_TOMBSTONE@1639626755154.txt.gz
-rw------- 1 system system  2792 2021-12-20 11:22 system_app_strictmode@1639626755091.txt
-rw------- 1 system system   960 2021-12-20 11:26 system_server_strictmode@1639626755092.txt
-rw------- 1 system system  1625 2021-12-16 11:52 system_server_strictmode@1639626761115.txt
-rw------- 1 system system  1281 2021-12-20 11:29 system_server_wtf@1639626755114.txt
-rw------- 1 system system  1174 2021-12-16 11:52 system_server_wtf@1639626761105.txt
console:/data/system/dropbox # 

DropBox文件分类及生成
1)crash
Java层遇到未被 catch 的例外时, ActivityManagerService 会记录一次 crash到 DropBoxManager中, 并弹出 Force Close对话框提示用户

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
void handleApplicationCrashInner(String eventType, ProcessRecord r, String 
      ...
      addErrorToDropBox(eventType, r, processName, null, null, null, null, null, crashInfo);
      crashApplication(r, crashInfo);
      ...
 }

2)anr
当应用程序的主线程(UI线程)长时间未能得到响应时, ActivityManagerService 会记录一次 anr到 DropBoxManager中,并弹出 Application Not Responding对话框提示用户.

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
final void appNotResponding(ProcessRecord app, ActivityRecord activity, ActivityRecord parent, boolean aboveSystem, final String annotation) { 
      ... 
      addErrorToDropBox("anr", app, app.processName, activity, parent, annotation, cpuInfo, tracesFile, null);
      ...
}

3)wtf
应用程序可以在代码中用来主动报告一个不应当发生的情况. 依赖于系统设置, 这个函数会通过 ActivityManagerService 增加一个 wtf 记录到 DropBoxManager中, 并/或终止当前应用程序进程.

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
ProcessRecord handleApplicationWtfInner() {
        ...
        addErrorToDropBox("wtf", r, processName, null, null, tag, null, null, crashInfo);
		...
    }

4)strict_mode
在比正常模式检测得更严格, 通常用来监测不应当在主线程执行的网络, 文件等操作. 任何 StrictMode 违例都会被 ActivityManagerService 在 DropBoxManager 中记录为一次 strict_mode违例。

public void handleApplicationStrictModeViolation(
            IBinder app,
            int violationMask,
            StrictMode.ViolationInfo info) {
       	...
       	logStrictModeViolationToDropBox(r, info);
    }

5)lowmem
在内存不足的时候, Android 会终止后台应用程序来释放内存, 但如果没有后台应用程序可被释放时,ActivityManagerService 就会在 DropBoxManager 中记录一次 lowmem.

void reportMemUsage(ArrayList<ProcessMemInfo> memInfos) {
	...
	addErrorToDropBox("lowmem", null, "system_server", null,
                null, tag.toString(), dropBuilder.toString(), null, null);
	...
}

6)watchdog
WatchDog 监测到系统进程(system_server)出现问题, 会增加一条 watchdog记录到 DropBoxManager 中, 并终止系统进程的执行.

frameworks/base/services/core/java/com/android/server/Watchdog.java
Thread dropboxThread = new Thread("watchdogWriteToDropbox") {
 public void run() {
     mActivity.addErrorToDropBox(
             "watchdog", null, "system_server", null, null,
             name, null, stack, null);
 }
};
dropboxThread.start();

7)netstats_error
8)BATTERY_DISCHARGE_INFO
9)SYSTEM_TOMBSTONE
10)System Serve 启动完成后的检测
包括:
A.每次开机都会增加一条 SYSTEM_BOOT 记录.
B. System Server 重启
C. Kernel Panic
D.系统恢复

private void logBootEvents(Context ctx) throws IOException {
    
    if (recovery != null && db != null) {
        db.addText("SYSTEM_RECOVERY_LOG", headers + recovery);//系统恢复
    }

    if (SystemProperties.getLong("ro.runtime.firstboot", 0) == 0) {
        String now = Long.toString(System.currentTimeMillis());
        SystemProperties.set("ro.runtime.firstboot", now);
        if (db != null) db.addText("SYSTEM_BOOT", headers);//SYSTEM_BOOT 

        // Negative sizes mean to take the *tail* of the file (see FileUtils.readTextFile())
        //
        addFileToDropBox(db, prefs, headers, "/proc/last_kmsg",
                -LOG_SIZE, "SYSTEM_LAST_KMSG");
        addFileToDropBox(db, prefs, headers, "/cache/recovery/log",
                -LOG_SIZE, "SYSTEM_RECOVERY_LOG");
        addFileToDropBox(db, prefs, headers, "/data/dontpanic/apanic_console",
                -LOG_SIZE, "APANIC_CONSOLE");
        addFileToDropBox(db, prefs, headers, "/data/dontpanic/apanic_threads",
                -LOG_SIZE, "APANIC_THREADS");
    } else {
        if (db != null) db.addText("SYSTEM_RESTART", headers);
    }

    // Scan existing tombstones (in case any new ones appeared)
    File[] tombstoneFiles = TOMBSTONE_DIR.listFiles();
    for (int i = 0; tombstoneFiles != null && i < tombstoneFiles.length; i++) {
        addFileToDropBox(db, prefs, headers, tombstoneFiles[i].getPath(),
                LOG_SIZE, "SYSTEM_TOMBSTONE");
    }

    // Start watching for new tombstone files; will record them as they occur.
    // This gets registered with the singleton file observer thread.
    sTombstoneObserver = new FileObserver(TOMBSTONE_DIR.getPath(), FileObserver.CLOSE_WRITE) {
        @Override
        public void onEvent(int event, String path) {
            try {
                String filename = new File(TOMBSTONE_DIR, path).getPath();
                addFileToDropBox(db, prefs, headers, filename, LOG_SIZE, "SYSTEM_TOMBSTONE");
            } catch (IOException e) {
                Slog.e(TAG, "Can't log tombstone", e);
            }
        }
    };

    sTombstoneObserver.startWatching();
}
上一篇:如何使用jwt生成token


下一篇:深入探析 Rational AppScan Standard Edition 新特性之 Glass Box 扫描