hihex.cs.PidDatabase.java Source code

Java tutorial

Introduction

Here is the source code for hihex.cs.PidDatabase.java

Source

/*
 * 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;
        }
    }
}