/*
* CatSaver
* Copyright (C) 2015 HiHex Ltd.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package hihex.cs;
import android.util.JsonWriter;
import android.util.Log;
import android.util.Pair;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Bytes;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An entry reported by logcat.
*/
public final class LogEntry {
private static final byte[] SYSTEM_RESTART_PAYLOAD = "\4SystemServer\0Entered the Android system server!".getBytes(Charsets.ISO_8859_1);
private static final byte[] DEBUGGERD_RESTART_PAYLOAD_PREFIX = "\4\0debuggerd: ".getBytes(Charsets.ISO_8859_1);
private static final byte[] TOMBSTONE_PAYLOAD_PREFIX = "\4DEBUG\0\nTombstone written to: /data/tombstones/tombstone_".getBytes(Charsets.ISO_8859_1);
private static final byte[] CODE_AROUND_PC_PREFIX = "\4DEBUG\0\ncode around pc:".getBytes(Charsets.ISO_8859_1);
private static final byte[] START_PROCESS_PAYLOAD_PREFIX = "\4ActivityManager\0Start proc ".getBytes(Charsets.ISO_8859_1);
private static final Pattern START_PROCESS_PATTERN = Pattern.compile("^Start proc (\\S+)[^:]*: pid=([0-9]+)");
private static final Pattern START_PROCESS_PATTERN_LOLLIPOP_MR1 = Pattern.compile("^Start proc ([0-9]+):([^/]+)");
private static final byte[] FORCE_STOP_PROCESS_PAYLOAD_PREFIX = "\4ActivityManager\0Killing ".getBytes(Charsets.ISO_8859_1);
private static final Pattern FORCE_STOP_PROCESS_PATTERN = Pattern.compile("^Killing (?:proc )?([0-9]+):");
private static final byte[] KILL_PROCESS_PAYLOAD_PREFIX = "\4ActivityManager\0Process ".getBytes(Charsets.ISO_8859_1);
private static final Pattern KILL_PROCESS_PATTERN = Pattern.compile("^Process \\S+ \\(pid ([0-9]+)\\) has died\\.?$");
private static final Pattern JNI_SIGNAL_PATTERN = Pattern.compile("^Fatal signal (?:[0-9]+) \\([0-9A-Z?]+\\)");
private static final byte[] ANR_PAYLOAD_PREFIX_DALVIKVM = "\4dalvikvm\0Wrote stack traces to '/data/anr/traces.txt'".getBytes(Charsets.ISO_8859_1);
private static final byte[] ANR_PAYLOAD_PREFIX_ZYGOTE = "\4zygote\0Wrote stack traces to '/data/anr/traces.txt'".getBytes(Charsets.ISO_8859_1);
private static final byte[] ANR_PAYLOAD_PREFIX_ART = "\4art\0Wrote stack traces to '/data/anr/traces.txt'".getBytes(Charsets.ISO_8859_1);
private final byte[] mSharedArray;
private final ByteBuffer mSharedBuffer;
private int mPid;
private int mTid;
private int mSec;
private int mNSec;
private int mPayloadLength;
private int mTagSeparator;
private Optional<String> mTag = Optional.absent();
private Optional<String> mMessage = Optional.absent();
private String mPackageName = "[unknown package]";
private String mThreadName = "[unknown thread]";
public LogEntry() {
mSharedArray = new byte[5120];
mSharedBuffer = ByteBuffer.wrap(mSharedArray).order(ByteOrder.LITTLE_ENDIAN);
}
public LogEntry(final LogEntry entry) {
mSharedArray = new byte[] {entry.mSharedArray[0]};
mSharedBuffer = null;
mTag = Optional.of(entry.tag());
mMessage = Optional.of(entry.message());
mPid = entry.mPid;
mTid = entry.mTid;
mSec = entry.mSec;
mNSec = entry.mNSec;
mPayloadLength = entry.mPayloadLength;
mTagSeparator = entry.mTagSeparator;
mPackageName = entry.mPackageName;
mThreadName = entry.mThreadName;
}
/**
* Replace the current entry with the content of the input stream.
*/
public void read(final InputStream stream) throws IOException {
ByteStreams.readFully(stream, mSharedArray, 0, 4);
mPayloadLength = mSharedBuffer.getShort(0);
int headerLength = mSharedBuffer.getShort(2);
if (headerLength != 24) {
// FIXME In logger_entry(_v1) the __pad can be filled with garbage. We don't know if we are targeting v1 or
// not. Maybe do an actual ioctl() check in the future.
headerLength = 20;
}
ByteStreams.readFully(stream, mSharedArray, 0, headerLength - 4);
mPid = mSharedBuffer.getInt(0);
mTid = mSharedBuffer.getInt(4);
mSec = mSharedBuffer.getInt(8);
mNSec = mSharedBuffer.getInt(12);
ByteStreams.readFully(stream, mSharedArray, 0, mPayloadLength);
mTagSeparator = Bytes.indexOf(mSharedArray, (byte) 0);
mTag = Optional.absent();
mMessage = Optional.absent();
}
public int pid() {
return mPid;
}
public int tid() {
return mTid;
}
public Date timestamp() {
return new Date(mSec * 1000L + mNSec / 1_000_000L);
}
public int logLevel() {
return mSharedArray[0];
}
public static char logLevelChar(final int logLevel) {
switch (logLevel) {
case Log.ASSERT:
return 'F';
case Log.ERROR:
return 'E';
case Log.WARN:
return 'W';
case Log.INFO:
return 'I';
case Log.DEBUG:
return 'D';
case Log.VERBOSE:
return 'V';
default:
return '?';
}
}
public char logLevelChar() {
return logLevelChar(logLevel());
}
public String tag() {
if (!mTag.isPresent()) {
mTag = Optional.of(new String(mSharedArray, 1, mTagSeparator - 1, Charsets.UTF_8));
}
return mTag.get();
}
public String message() {
if (!mMessage.isPresent()) {
final int messageLength = mPayloadLength - mTagSeparator - 2;
mMessage = Optional.of(new String(mSharedArray, mTagSeparator + 1, messageLength, Charsets.UTF_8));
}
return mMessage.get();
}
private boolean payloadStartsWith(final byte[] prefix) {
final int length = prefix.length;
if (mPayloadLength < length) {
return false;
}
for (int i = 0; i < length; ++i) {
if (mSharedArray[i] != prefix[i]) {
return false;
}
}
return true;
}
/**
* Check whether this log indicates the system_server has restarted.
*/
public boolean isSystemRestart() {
return (mPayloadLength == SYSTEM_RESTART_PAYLOAD.length && payloadStartsWith(SYSTEM_RESTART_PAYLOAD))
|| (payloadStartsWith(DEBUGGERD_RESTART_PAYLOAD_PREFIX));
}
/**
* Checks whether this log indicates a process has started. If true, returns the information about this new process.
*
* @return The pair of the new PID and process name if it is a start-process entry, null otherwise.
*/
public Pair<Integer, String> checkStartProcessInfo() {
if (payloadStartsWith(START_PROCESS_PAYLOAD_PREFIX)) {
final String message = message();
final Matcher matcher = START_PROCESS_PATTERN.matcher(message);
if (matcher.find()) {
final String processName = matcher.group(1);
final String pidString = matcher.group(2);
final Integer pid = Integer.decode(pidString);
return Pair.create(pid, processName);
}
final Matcher matcherLollipop = START_PROCESS_PATTERN_LOLLIPOP_MR1.matcher(message);
if (matcherLollipop.find()) {
final String pidString = matcherLollipop.group(1);
final String processName = matcherLollipop.group(2);
final Integer pid = Integer.decode(pidString);
return Pair.create(pid, processName);
}
}
return null;
}
/**
* Checks whether this log indicates a process has ended. If true, returns the pid of the dying process.
*
* @return The PID of the process being killed if it is an end-process entry, -1 otherwise.
*/
public int checkEndProcessInfo() {
final Pattern pattern;
if (payloadStartsWith(FORCE_STOP_PROCESS_PAYLOAD_PREFIX)) {
pattern = FORCE_STOP_PROCESS_PATTERN;
} else if (payloadStartsWith(KILL_PROCESS_PAYLOAD_PREFIX)) {
pattern = KILL_PROCESS_PATTERN;
} else {
return -1;
}
final Matcher matcher = pattern.matcher(message());
if (matcher.find()) {
final String pidString = matcher.group(1);
return Integer.parseInt(pidString);
} else {
return -1;
}
}
public boolean isJniCrash() {
if (logLevel() != Log.ASSERT || !"libc".equals(tag())) {
return false;
}
final Matcher matcher = JNI_SIGNAL_PATTERN.matcher(message());
return matcher.find();
}
public boolean isAnr() {
return payloadStartsWith(ANR_PAYLOAD_PREFIX_DALVIKVM) ||
payloadStartsWith(ANR_PAYLOAD_PREFIX_ZYGOTE) ||
payloadStartsWith(ANR_PAYLOAD_PREFIX_ART);
}
public boolean isJniCrashLogEnded() {
return payloadStartsWith(TOMBSTONE_PAYLOAD_PREFIX) || payloadStartsWith(CODE_AROUND_PC_PREFIX);
}
public void writeJSON(final Writer writer) throws IOException {
final JsonWriter json = new JsonWriter(writer);
json.beginObject();
json.name("pid").value(mPid);
json.name("tid").value(mTid);
json.name("time").value(mSec * 1000L + mNSec / 1_000_000L);
json.name("level").value(String.valueOf(logLevelChar()));
json.name("tag").value(tag());
json.name("msg").value(message());
json.name("process").value(getProcessName());
json.name("thread").value(getThreadName());
json.endObject();
}
public void populateProcessName(final PidDatabase database) {
final int pid = pid();
mPackageName = database.getProcessName(pid);
final Optional<PidEntry> entry = database.getEntry(pid);
if (entry.isPresent()) {
mThreadName = entry.get().getThreadName(tid());
} else {
mThreadName = "TID:" + tid();
}
}
public String getProcessName() {
return mPackageName;
}
public String getThreadName() {
return mThreadName;
}
}