/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package java.lang;
import android.system.ErrnoException;
import android.util.MutableInt;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import libcore.io.IoUtils;
import libcore.io.Libcore;
import static android.system.OsConstants.*;
/**
* Manages child processes.
*/
final class ProcessManager {
/**
* Map from pid to Process. We keep weak references to the Process objects
* and clean up the entries when no more external references are left. The
* process objects themselves don't require much memory, but file
* descriptors (associated with stdin/stdout/stderr in this case) can be
* a scarce resource.
*/
private final Map<Integer, ProcessReference> processReferences
= new HashMap<Integer, ProcessReference>();
/** Keeps track of garbage-collected Processes. */
private final ProcessReferenceQueue referenceQueue = new ProcessReferenceQueue();
private ProcessManager() {
// Spawn a thread to listen for signals from child processes.
Thread reaperThread = new Thread(ProcessManager.class.getName()) {
@Override public void run() {
watchChildren();
}
};
reaperThread.setDaemon(true);
reaperThread.start();
}
/**
* Cleans up after garbage collected processes. Requires the lock on the
* map.
*/
private void cleanUp() {
ProcessReference reference;
while ((reference = referenceQueue.poll()) != null) {
synchronized (processReferences) {
processReferences.remove(reference.processId);
}
}
}
/**
* Loops indefinitely and calls ProcessManager.onExit() when children exit.
*/
private void watchChildren() {
MutableInt status = new MutableInt(-1);
while (true) {
try {
// Wait for children in our process group.
int pid = Libcore.os.waitpid(0, status, 0);
// Work out what onExit wants to hear.
int exitValue;
if (WIFEXITED(status.value)) {
exitValue = WEXITSTATUS(status.value);
} else if (WIFSIGNALED(status.value)) {
exitValue = WTERMSIG(status.value);
} else if (WIFSTOPPED(status.value)) {
exitValue = WSTOPSIG(status.value);
} else {
throw new AssertionError("unexpected status from waitpid: " + status.value);
}
onExit(pid, exitValue);
} catch (ErrnoException errnoException) {
if (errnoException.errno == ECHILD) {
// Expected errno: there are no children to wait for.
// onExit will sleep until it is informed of another child coming to life.
waitForMoreChildren();
continue;
} else {
throw new AssertionError(errnoException);
}
}
}
}
/**
* Called by {@link #watchChildren()} when a child process exits.
*
* @param pid ID of process that exited
* @param exitValue value the process returned upon exit
*/
private void onExit(int pid, int exitValue) {
ProcessReference processReference = null;
synchronized (processReferences) {
cleanUp();
processReference = processReferences.remove(pid);
}
if (processReference != null) {
ProcessImpl process = processReference.get();
if (process != null) {
process.setExitValue(exitValue);
}
}
}
private void waitForMoreChildren() {
synchronized (processReferences) {
if (processReferences.isEmpty()) {
// There are no eligible children; wait for one to be added.
// This wait will return because of the notifyAll call in exec.
try {
processReferences.wait();
} catch (InterruptedException ex) {
// This should never happen.
throw new AssertionError("unexpected interrupt");
}
} else {
/*
* A new child was spawned just before we entered
* the synchronized block. We can just fall through
* without doing anything special and land back in
* the native waitpid().
*/
}
}
}
/**
* Executes a native process. Fills in in, out, and err and returns the
* new process ID upon success.
*/
private static native int exec(String[] command, String[] environment,
String workingDirectory, FileDescriptor in, FileDescriptor out,
FileDescriptor err, boolean redirectErrorStream) throws IOException;
/**
* Executes a process and returns an object representing it.
*/
public Process exec(String[] taintedCommand, String[] taintedEnvironment, File workingDirectory,
boolean redirectErrorStream) throws IOException {
// Make sure we throw the same exceptions as the RI.
if (taintedCommand == null) {
throw new NullPointerException("taintedCommand == null");
}
if (taintedCommand.length == 0) {
throw new IndexOutOfBoundsException("taintedCommand.length == 0");
}
// Handle security and safety by copying mutable inputs and checking them.
String[] command = taintedCommand.clone();
String[] environment = taintedEnvironment != null ? taintedEnvironment.clone() : null;
// Check we're not passing null Strings to the native exec.
for (int i = 0; i < command.length; i++) {
if (command[i] == null) {
throw new NullPointerException("taintedCommand[" + i + "] == null");
}
}
// The environment is allowed to be null or empty, but no element may be null.
if (environment != null) {
for (int i = 0; i < environment.length; i++) {
if (environment[i] == null) {
throw new NullPointerException("taintedEnvironment[" + i + "] == null");
}
}
}
FileDescriptor in = new FileDescriptor();
FileDescriptor out = new FileDescriptor();
FileDescriptor err = new FileDescriptor();
String workingPath = (workingDirectory == null)
? null
: workingDirectory.getPath();
// Ensure onExit() doesn't access the process map before we add our
// entry.
synchronized (processReferences) {
int pid;
try {
pid = exec(command, environment, workingPath, in, out, err, redirectErrorStream);
} catch (IOException e) {
IOException wrapper = new IOException("Error running exec()."
+ " Command: " + Arrays.toString(command)
+ " Working Directory: " + workingDirectory
+ " Environment: " + Arrays.toString(environment));
wrapper.initCause(e);
throw wrapper;
}
ProcessImpl process = new ProcessImpl(pid, in, out, err);
ProcessReference processReference = new ProcessReference(process, referenceQueue);
processReferences.put(pid, processReference);
/*
* This will wake up the child monitor thread in case there
* weren't previously any children to wait on.
*/
processReferences.notifyAll();
return process;
}
}
static class ProcessImpl extends Process {
private final int pid;
private final InputStream errorStream;
/** Reads output from process. */
private final InputStream inputStream;
/** Sends output to process. */
private final OutputStream outputStream;
/** The process's exit value. */
private Integer exitValue = null;
private final Object exitValueMutex = new Object();
ProcessImpl(int pid, FileDescriptor in, FileDescriptor out, FileDescriptor err) {
this.pid = pid;
this.errorStream = new ProcessInputStream(err);
this.inputStream = new ProcessInputStream(in);
this.outputStream = new ProcessOutputStream(out);
}
public void destroy() {
// If the process hasn't already exited, send it SIGKILL.
synchronized (exitValueMutex) {
if (exitValue == null) {
try {
Libcore.os.kill(pid, SIGKILL);
} catch (ErrnoException e) {
System.logI("Failed to destroy process " + pid, e);
}
}
}
// Close any open streams.
IoUtils.closeQuietly(inputStream);
IoUtils.closeQuietly(errorStream);
IoUtils.closeQuietly(outputStream);
}
public int exitValue() {
synchronized (exitValueMutex) {
if (exitValue == null) {
throw new IllegalThreadStateException("Process has not yet terminated: " + pid);
}
return exitValue;
}
}
public InputStream getErrorStream() {
return this.errorStream;
}
public InputStream getInputStream() {
return this.inputStream;
}
public OutputStream getOutputStream() {
return this.outputStream;
}
public int waitFor() throws InterruptedException {
synchronized (exitValueMutex) {
while (exitValue == null) {
exitValueMutex.wait();
}
return exitValue;
}
}
void setExitValue(int exitValue) {
synchronized (exitValueMutex) {
this.exitValue = exitValue;
exitValueMutex.notifyAll();
}
}
@Override
public String toString() {
return "Process[pid=" + pid + "]";
}
}
static class ProcessReference extends WeakReference<ProcessImpl> {
final int processId;
public ProcessReference(ProcessImpl referent, ProcessReferenceQueue referenceQueue) {
super(referent, referenceQueue);
this.processId = referent.pid;
}
}
static class ProcessReferenceQueue extends ReferenceQueue<ProcessImpl> {
@Override
public ProcessReference poll() {
// Why couldn't they get the generics right on ReferenceQueue? :(
Object reference = super.poll();
return (ProcessReference) reference;
}
}
private static final ProcessManager instance = new ProcessManager();
/** Gets the process manager. */
public static ProcessManager getInstance() {
return instance;
}
/** Automatically closes fd when collected. */
private static class ProcessInputStream extends FileInputStream {
private FileDescriptor fd;
private ProcessInputStream(FileDescriptor fd) {
super(fd);
this.fd = fd;
}
@Override
public void close() throws IOException {
try {
super.close();
} finally {
synchronized (this) {
try {
IoUtils.close(fd);
} finally {
fd = null;
}
}
}
}
}
/** Automatically closes fd when collected. */
private static class ProcessOutputStream extends FileOutputStream {
private FileDescriptor fd;
private ProcessOutputStream(FileDescriptor fd) {
super(fd);
this.fd = fd;
}
@Override
public void close() throws IOException {
try {
super.close();
} finally {
synchronized (this) {
try {
IoUtils.close(fd);
} finally {
fd = null;
}
}
}
}
}
}