/*
 * 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 java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
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.ErrnoException;
import libcore.io.IoUtils;
import libcore.io.Libcore;
import libcore.util.MutableInt;
import static libcore.io.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;
                    }
                }
            }
        }
    }
}
