org.prebake.os.PipeFlusher.java Source code

Java tutorial

Introduction

Here is the source code for org.prebake.os.PipeFlusher.java

Source

// Copyright 2010, Mike Samuel
//
// 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 org.prebake.os;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import com.google.common.io.Closeables;

/**
 * Keeps {@link OsProcess external processes'} pipes from getting clogged
 * by shuttling bytes from one process's output stream to another process's
 * input stream.
 *
 * @author Mike Samuel <mikesamuel@gmail.com>
 */
public final class PipeFlusher implements Closeable {
    // TODO: unittests
    private boolean closed = false;
    /** Pipes that might need to be serviced. */
    private final ConcurrentLinkedQueue<Pipe> livePipes = new ConcurrentLinkedQueue<Pipe>();

    private final ScheduledExecutorService execer;

    public PipeFlusher(ScheduledExecutorService execer) {
        assert execer != null;
        this.execer = execer;
    }

    void createPipe(OsProcess from, OsProcess to) throws InterruptedException {
        pushPipe(new Pipe(from, to));
    }

    private void pushPipe(Pipe p) throws InterruptedException {
        synchronized (this) {
            if (closed) {
                throw new InterruptedException();
            }
        }
        livePipes.add(p);
    }

    /**
     * There's no way to check whether an input stream is closed without blocking
     * on it, so periodically (after 10 empty reads), we do a blocking read.
     */
    private void checkClosed(final Pipe p) {
        execer.submit(new Runnable() {
            public void run() {
                try {
                    InputStream in = p.from.getInputStream();
                    int read = in.read();
                    if (read < 0) {
                        dropPipe(p);
                    } else {
                        doHandlePipe(in, read, p.to.getOutputStream(), p);
                    }
                } catch (IOException ex) {
                    dropPipe(p);
                }
            }
        });
    }

    private ScheduledFuture<?> pipeDispatcherFuture;

    public void start() {
        Runnable pipeDispatcher = new Runnable() {
            public void run() {
                boolean closed;
                synchronized (PipeFlusher.this) {
                    closed = PipeFlusher.this.closed;
                }
                if (closed) {
                    for (Pipe p; (p = livePipes.poll()) != null;) {
                        OutputStream out = p.from.getOutputStream();
                        if (out != null) {
                            Closeables.closeQuietly(out);
                        }
                        InputStream in = p.to.getInputStream();
                        if (in != null) {
                            Closeables.closeQuietly(in);
                        }
                    }
                    pipeDispatcherFuture.cancel(false);
                    return;
                }
                for (Pipe p; (p = livePipes.poll()) != null;) {
                    OutputStream out = p.to.getOutputStream();
                    if (out == null) {
                        dropPipe(p);
                        continue;
                    }
                    InputStream in = p.from.getInputStream();
                    try {
                        int n = in.available();
                        if (n > 0) {
                            handlePipe(in, out, p);
                        } else {
                            try {
                                if (p.noneAvailableCount++ >= 10) {
                                    checkClosed(p);
                                } else {
                                    pushPipe(p);
                                }
                            } catch (InterruptedException ex) {
                                dropPipe(p);
                            }
                        }
                    } catch (IOException ex) {
                        dropPipe(p);
                    }
                }
            }
        };
        pipeDispatcherFuture = execer.scheduleWithFixedDelay(pipeDispatcher, 50, 50, TimeUnit.MILLISECONDS);
    }

    private void handlePipe(final InputStream in, final OutputStream out, final Pipe p) {
        execer.submit(new Runnable() {
            public void run() {
                doHandlePipe(in, -1, out, p);
            }
        });
    }

    private void doHandlePipe(final InputStream in, int firstByte, final OutputStream out, final Pipe p) {
        try {
            byte[] buf = getBuffer(in.available());
            try {
                if (firstByte >= 0) {
                    buf[0] = (byte) firstByte;
                    if (in.available() > 0) {
                        int nRead = in.read(buf, 1, buf.length - 1);
                        out.write(buf, 0, nRead >= 0 ? nRead + 1 : 1);
                    }
                }
                while (in.available() > 0) {
                    out.write(buf, 0, in.read(buf));
                }
            } finally {
                releaseBuffer(buf);
            }
            p.noneAvailableCount = 0;
            try {
                pushPipe(p);
            } catch (InterruptedException ex) {
                dropPipe(p);
            }
        } catch (IOException ex) {
            dropPipe(p);
        }
    }

    private static final int BUF_SIZE = 4096;
    private final Queue<byte[]> freeBuffers = new ArrayBlockingQueue<byte[]>(4);

    private byte[] getBuffer(int desiredSize) {
        if (desiredSize >= 1024) {
            byte[] buf = freeBuffers.poll();
            if (buf != null) {
                return buf;
            }
        }
        return new byte[Math.min(desiredSize, BUF_SIZE)];
    }

    private void releaseBuffer(byte[] buffer) {
        if (buffer.length == BUF_SIZE) {
            // OK.  If we don't add it, then there are plenty available.
            intentionallyIgnoreResult(freeBuffers.offer(buffer));
        }
    }

    /** @param result intentionally ignored. */
    private static void intentionallyIgnoreResult(boolean result) {
        // FindBugs requires us to check results in some places.
        // We don't want to turn these checks off in general, but in some cases
        // we are just making a best effort.
    }

    /** Called when a pipe's input has been drained. */
    private void dropPipe(Pipe p) {
        // Don't requeue.  Make sure that the recipient process knows it won't
        // getting any more input.
        Closeables.closeQuietly(p.from.getOutputStream());
        p.to.noMoreInput();
    }

    public void close() {
        synchronized (this) {
            closed = true;
            freeBuffers.clear();
        }
    }

    private static final class Pipe {
        final OsProcess from, to;
        int noneAvailableCount = 0;

        Pipe(OsProcess from, OsProcess to) {
            this.from = from;
            this.to = to;
        }

        @Override
        public String toString() {
            return from.getCommand() + "|" + to.getCommand();
        }
    }
}