/*
* 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.text.TextUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* A database of {@link PidEntry}. Each entry consists of a running process ID (pid), process name, and the filename and
* writer of the corresponding process.
*/
public final class PidDatabase {
private static final File PROC_FILE = new File("/proc");
private static final Pattern UNSAFE_SHELL_CHARACTERS = Pattern.compile("[^0-9a-zA-Z_@%+=:,./-]");
private ArrayList<PidEntry> mEntries = new ArrayList<>();
/**
* Finds the process name for a process ID.
*
* @param pidString The process ID, as a string (e.g. "1234").
* @return The process name. If the process is already dead, a dummy representation will be returned.
*/
private static String getProcessName(final String pidString) {
try {
final File file = new File("/proc/" + pidString + "/cmdline");
final String processName = Files.toString(file, Charsets.UTF_8).trim();
if (!processName.isEmpty()) {
final String[] arguments = processName.split("\0");
final StringBuilder builder = new StringBuilder();
boolean isFirst = true;
for (final String argument : arguments) {
if (isFirst) {
isFirst = false;
} else {
builder.append(' ');
}
if (TextUtils.isEmpty(argument)) {
builder.append("''");
} else if (!UNSAFE_SHELL_CHARACTERS.matcher(argument).find()) {
builder.append(argument);
} else {
builder.append('\'');
builder.append(argument.replace("'", "'\"'\"'"));
builder.append('\'');
}
}
return builder.toString();
}
} catch (IOException e) {
// Ignore.
}
try {
final File file = new File("/proc/" + pidString + "/comm");
final String processName = Files.toString(file, Charsets.UTF_8).trim();
if (!processName.isEmpty()) {
return processName;
}
} catch (IOException e) {
// Ignore.
}
return "PID:" + pidString;
}
/**
* Refreshes the list of running processes. If any processes are recorded but is dead after this call, the
* corresponding log files will be closed.
*/
public synchronized void refresh() {
// Dump all running processes from /proc
final HashMap<Integer, String> pidToProcessNames = new HashMap<>();
for (final String pidString : PROC_FILE.list()) {
if (!TextUtils.isDigitsOnly(pidString)) {
continue;
}
final int pid = Integer.parseInt(pidString);
final String processName = getProcessName(pidString);
pidToProcessNames.put(pid, processName);
}
// Record all entries that are dead.
final ArrayList<PidEntry> removableEntries = new ArrayList<>();
for (final PidEntry entry : mEntries) {
final Object isExistingProcess = pidToProcessNames.remove(entry.pid);
if (isExistingProcess == null) {
removableEntries.add(entry);
entry.close();
}
}
mEntries.removeAll(removableEntries);
// Do the update.
for (final Map.Entry<Integer, String> input : pidToProcessNames.entrySet()) {
mEntries.add(new PidEntry(input.getKey(), input.getValue()));
}
}
/**
* Find the pid corresponding to the filename, if still recording. Returns -1 if not recording.
*/
public synchronized int findPid(final String filename) {
final PidEntry entry = findEntry(filename).orNull();
return (entry != null) ? entry.pid : -1;
}
/**
* Find the process entry corresponding to the filename, if still recording.
*
* @param filename The name of the log file.
* @return The process entry.
*/
public synchronized Optional<PidEntry> findEntry(final String filename) {
for (final PidEntry entry : mEntries) {
if (entry.path.isPresent()) {
final String entryFilename = entry.path.get().getName();
if (filename.equals(entryFilename)) {
return Optional.of(entry);
}
}
}
return Optional.absent();
}
/**
* Obtains the list of running processes. Each item of the list is a map that can be supplied to a Chunk template
* for rendering.
*/
public synchronized List<HashMap<String, String>> runningProcesses() {
final ArrayList<PidEntry> entries = mEntries;
Collections.sort(entries, new Comparator<PidEntry>() {
@Override
public int compare(final PidEntry lhs, final PidEntry rhs) {
final File aFile = new File("/proc/" + lhs.pid + "/comm");
final File bFile = new File("/proc/" + rhs.pid + "/comm");
final long aTime = aFile.lastModified();
final long bTime = bFile.lastModified();
int res = Longs.compare(bTime, aTime);
if (res == 0) {
res = Ints.compare(rhs.pid, lhs.pid);
}
return res;
}
});
return Lists.transform(entries, new Function<PidEntry, HashMap<String, String>>() {
@Override
public HashMap<String, String> apply(final PidEntry input) {
final HashMap<String, String> summary = new HashMap<>(3);
summary.put("pid", String.valueOf(input.pid));
summary.put("name", input.processName);
if (input.writer.isPresent()) {
summary.put("recording", "true");
}
return summary;
}
});
}
/**
* Finds the process ID whose name is exactly the same as the given input.
*
* @param processName The exact name of the process
* @return The corresponding process ID, or -1 if not found.
*/
public synchronized int findPidForExactProcessName(final String processName) {
for (final PidEntry entry : mEntries) {
if (processName.equals(entry.processName)) {
return entry.pid;
}
}
return -1;
}
private int getEntryIndex(final int pid) {
int i = 0;
for (final PidEntry entry : mEntries) {
if (entry.pid == pid) {
return i;
}
++i;
}
return -1;
}
public synchronized Optional<PidEntry> getEntry(final int pid) {
final int index = getEntryIndex(pid);
if (index >= 0) {
return Optional.of(mEntries.get(index));
} else {
return Optional.absent();
}
}
public synchronized String getProcessName(final int pid) {
final int index = getEntryIndex(pid);
if (index >= 0) {
return mEntries.get(index).processName;
} else {
return getProcessName(String.valueOf(pid));
}
}
/**
* Start recording logs for the specified pid.
*/
public synchronized void startRecording(final int pid,
final Optional<String> processName,
final LogFiles logFiles,
final Date timestamp,
final Function<PidEntry, ?> initialize) throws IOException {
int index = getEntryIndex(pid);
final PidEntry oldEntry;
if (index >= 0) {
oldEntry = mEntries.get(index);
} else if (processName.isPresent()) {
oldEntry = new PidEntry(pid, processName.get());
} else {
return;
}
final PidEntry newEntry = oldEntry.open(logFiles, timestamp);
if (newEntry != oldEntry) {
try {
initialize.apply(newEntry);
} finally {
if (index >= 0) {
mEntries.set(index, newEntry);
} else {
mEntries.add(newEntry);
}
}
}
}
public synchronized void stopRecording(final int pid, final Function<Writer, ?> cleanup) {
final int index = getEntryIndex(pid);
if (index < 0) {
return;
}
final PidEntry oldEntry = mEntries.get(index);
if (oldEntry.writer.isPresent()) {
cleanup.apply(oldEntry.writer.get());
}
final PidEntry newEntry = oldEntry.close();
if (newEntry != oldEntry) {
mEntries.set(index, newEntry);
}
}
public synchronized int countRecordingEntries() {
int count = 0;
for (final PidEntry entry : mEntries) {
if (entry.writer.isPresent()) {
count += 1;
}
}
return count;
}
public synchronized Set<String> listRecordingProcessNames() {
final HashSet<String> result = new HashSet<>();
for (final PidEntry entry : mEntries) {
if (entry.writer.isPresent()) {
result.add(entry.processName);
}
}
return result;
}
public synchronized PidEntry splitEntry(final int pid, final LogFiles logFiles) {
final int index = getEntryIndex(pid);
final PidEntry oldEntry = mEntries.get(index);
try {
final PidEntry newEntry = mEntries.get(index).split(logFiles);
mEntries.set(index, newEntry);
return newEntry;
} catch (final IOException e) {
return oldEntry;
}
}
}