/*
* 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.Pair;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.util.regex.Pattern;
/**
* The thread that collects logcat events into files.
*/
public final class LogRecorder implements Runnable {
private final Config mConfig;
private int[] mDebuggedPids = Config.EMPTY_PID_ARRAY;
public LogRecorder(final Config config) {
mConfig = config;
Events.bus.register(this);
}
@Override
public void run() {
final Runtime runtime = Runtime.getRuntime();
try {
// Clean up all previous logcat processes if we are restarted. See http://stackoverflow.com/q/16173387/.
try {
final Process killLogcatProcess = runtime.exec(new String[]{"killall", "-2", "logcat"});
killLogcatProcess.waitFor();
} catch (final IOException e) {
// Ignore if we can't kill existing logcat. It's just minor annoyance.
}
final Process process = runtime.exec(new String[]{"logcat", "-B"});
final LogEntry entry = new LogEntry();
final InputStream stream = process.getInputStream();
while (true) {
entry.read(stream);
if (entry.isSystemRestart()) {
mConfig.refreshPids();
}
final int pid = entry.pid();
if (pid == mConfig.systemServerPid()) {
handleSystemServerLog(entry);
} else if (pid == mConfig.debuggerdPid()) {
handleDebuggerdLog(entry);
}
final int[] writeToPids = mConfig.getFilteredPidsForLog(entry);
if (entry.isJniCrash()) {
mDebuggedPids = writeToPids;
}
for (final int targetPid : writeToPids) {
final Optional<Writer> optWriter = mConfig.splitLogAndGetWriter(targetPid);
if (optWriter.isPresent()) {
final Writer writer = optWriter.get();
writeLogEntry(writer, entry);
if (entry.isAnr()) {
writeAnrTraces(writer, pid);
if (pid != targetPid) {
writeAnrTraces(writer, targetPid);
}
}
}
}
if (writeToPids != Config.EMPTY_PID_ARRAY) {
// Note that getFilteredPidsForLog() returns EMPTY_PID_ARRAY_LIVE_ALLOWED if live logging is
// possible for this entry. Only when the entry is filtered out it will return EMPTY_PID_ARRAY.
Events.bus.post(new Events.LiveEntry(entry));
}
}
} catch (final IOException | InterruptedException e) {
CsLog.e("Encountered exception when recording LogCat", e);
}
}
private void handleSystemServerLog(final LogEntry entry) throws IOException {
final Pair<Integer, String> startProcessInfo = entry.checkStartProcessInfo();
if (startProcessInfo != null) {
createProcess(entry, startProcessInfo.first, startProcessInfo.second);
return;
}
final int endProcessPid = entry.checkEndProcessInfo();
if (endProcessPid != -1) {
deleteProcess(entry, endProcessPid);
}
}
private void handleDebuggerdLog(final LogEntry entry) throws IOException {
if (mDebuggedPids.length == 0) {
return;
}
for (final int pid : mDebuggedPids) {
final Optional<Writer> writer = mConfig.splitLogAndGetWriter(pid);
if (writer.isPresent()) {
writeLogEntry(writer.get(), entry);
}
}
if (entry.isJniCrashLogEnded()) {
mDebuggedPids = Config.EMPTY_PID_ARRAY;
}
}
private void createProcess(final LogEntry entry, final int pid, final String processName) throws IOException {
final Preferences preferences = mConfig.preferences;
final Pattern filter = mConfig.preferences.getFilter();
if (!filter.matcher(processName).find()) {
return;
}
mConfig.logFiles.removeExpired(preferences);
final Optional<Writer> optWriter = mConfig.startRecording(pid, Optional.of(processName), entry.timestamp());
if (optWriter.isPresent()) {
final Writer writer = optWriter.get();
writeLogEntry(writer, entry);
}
}
private void deleteProcess(final LogEntry entry, final int pid) {
mConfig.stopRecording(pid, new Function<Writer, Void>() {
@Override
public Void apply(final Writer writer) {
try {
writeLogEntry(writer, entry);
} catch (final IOException e) {
// Ignore.
}
return null;
}
});
}
private void writeLogEntry(final Writer writer, final LogEntry entry) throws IOException {
mConfig.renderer.writeLogEntry(writer, entry);
}
private void writeAnrTraces(final Writer writer, final int pid) throws IOException {
mConfig.renderer.writeAnrTraces(writer, pid);
}
}