+/*
+ * Copyright (c) 2009, Takeyuki Nagao
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the
+ * following conditions are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer in the documentation and/or other
+ * materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+ * OF SUCH DAMAGE.
+ */
+
+package jp.sourceforge.dvibrowser.dvicore.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jp.sourceforge.dvibrowser.dvicore.DviException;
+
+
+public class CommandShell
+{
+ public static final int RETCODE_SUCCESS = 0;
+
+ private final ScheduledExecutorService exe = Executors.newSingleThreadScheduledExecutor
+ (new DaemonThreadFactory());
+
+ private static final Logger LOGGER = Logger.getLogger(CommandShell.class.getName());
+ private ArrayList<String> commandLine = new ArrayList<String>();
+
+ public void setCommandLine(String ... commandLine)
+ {
+ ArrayList<String> list = new ArrayList<String>();
+ for (String arg : commandLine) {
+ list.add(arg);
+ }
+ this.commandLine = list;
+ }
+
+ public void setCommandLine(Collection<String> cmdLine)
+ {
+ ArrayList<String> list = new ArrayList<String>();
+ list.addAll(cmdLine);
+ this.commandLine = list;
+ }
+
+ public Collection<String> getCommandLine()
+ {
+ return Collections.unmodifiableCollection(commandLine);
+ }
+
+ private ArrayList<String> envs = null;
+ public void setEnvironment(Collection<String> envs)
+ {
+ if (envs == null) {
+ this.envs = null;
+ return;
+ } else {
+ ArrayList<String> list = new ArrayList<String>();
+ list.addAll(envs);
+ this.envs = list;
+ }
+ }
+
+ public void setEnvironment(String ... envs)
+ {
+ ArrayList<String> list = new ArrayList<String>();
+ for (String arg : envs) {
+ list.add(arg);
+ }
+ this.envs = list;
+ }
+
+ public Collection<String> getEnvironment()
+ {
+ if (envs == null) return null;
+ return Collections.unmodifiableCollection(envs);
+ }
+
+ private File dir = null;
+ public void setWorkingDirectory(File dir)
+ {
+ this.dir = dir;
+ }
+
+ public File getWorkingDirectory()
+ {
+ return dir;
+ }
+
+ private TimeUnit timeUnit;
+ private long timeout = 0;
+ public void setTimeout(long timeout, TimeUnit timeUnit)
+ {
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout is negative");
+ }
+ this.timeout = timeout;
+ this.timeUnit = timeUnit;
+ }
+
+ private CommandShellHandler handler = null;
+ public void setHandler(CommandShellHandler handler)
+ {
+ this.handler = handler;
+ }
+
+ public CommandShellHandler getHandler()
+ {
+ return handler;
+ }
+
+ protected void checkVars()
+ {
+ if (commandLine == null)
+ throw new IllegalArgumentException
+ ("commandLine can't be null");
+ if (commandLine.size() < 1)
+ throw new IllegalArgumentException
+ ("commandLine can't be empty");
+ }
+
+ private volatile Process p = null;
+ private Thread stdoutThread = null;
+ private Thread stderrThread = null;
+ private Thread stdinThread = null;
+ private volatile Throwable stdoutThrowable = null;
+ private volatile Throwable stderrThrowable = null;
+ private volatile Throwable stdinThrowable = null;
+
+ public int execute()
+ throws IOException, InterruptedException, DviException
+ {
+ int result = -1;
+
+ p = null;
+ checkVars();
+
+ try {
+ final String commandLineStr = DviUtils.join(" ", commandLine);
+ LOGGER.fine("Running command: " + commandLineStr);
+ p = Runtime.getRuntime().exec(
+ commandLine.toArray(new String[commandLine.size()]),
+ getEnvironmentAsArray(), dir);
+ processStreams();
+ ScheduledFuture<?> future = null;
+ if (timeUnit != null && timeout > 0) {
+ LOGGER.fine("Starting timer: timeout=" + timeout + " timeUnit=" + timeUnit);
+ future = exe.schedule(new Runnable() {
+ public void run() {
+ LOGGER.warning("Command timed out: " + commandLineStr);
+ p.destroy();
+ }
+ }, timeout, timeUnit);
+ }
+ LOGGER.fine("waiting for the process to terminate.");
+ result = p.waitFor();
+ LOGGER.fine("process exit with retcode " + result);
+ if (future != null) {
+ future.cancel(false);
+ }
+ } catch (InterruptedException ex) {
+ if (p != null) {
+ p.destroy();
+ }
+ throw ex;
+ } catch (IOException ex) {
+ if (p != null) {
+ p.destroy();
+ }
+ throw ex;
+ } finally {
+ reapThread(stderrThread, "stderr");
+ stderrThread = null;
+ reapThread(stdoutThread, "stdout");
+ stdoutThread = null;
+ reapThread(stdinThread, "stdin");
+ stdinThread = null;
+
+ if (p != null) {
+ DviUtils.silentClose(p.getErrorStream());
+ DviUtils.silentClose(p.getInputStream());
+ DviUtils.silentClose(p.getOutputStream());
+ }
+ }
+
+ if (stdinThrowable != null) {
+ throw new DviException(stdinThrowable);
+ }
+ if (stdoutThrowable != null) {
+ throw new DviException(stdoutThrowable);
+ }
+ if (stderrThrowable != null) {
+ throw new DviException(stderrThrowable);
+ }
+
+ p = null;
+
+ return result;
+ }
+
+ private String[] getEnvironmentAsArray() {
+ if (envs == null) return null;
+ return envs.toArray(new String[envs.size()]);
+ }
+
+ private void reapThread(Thread thread, String name)
+ {
+ try {
+ LOGGER.fine("waiting for the " + name + " thread");
+ if (thread != null)
+ thread.join();
+ } catch (InterruptedException ex) {
+ DviUtils.logStackTrace(LOGGER, Level.WARNING, ex);
+ }
+ }
+
+ protected void processStreams()
+ throws IOException
+ {
+ if (handler != null) {
+ stdinThread = new Thread(new Runnable() {
+ public void run() {
+ OutputStream os = p.getOutputStream();
+ try {
+ handler.handleStdin(os);
+ } catch (Throwable ex) {
+ DviUtils.logStackTrace(LOGGER, Level.WARNING, ex);
+ stdinThrowable = ex;
+ p.destroy();
+ } finally {
+ DviUtils.silentClose(os);
+ }
+ }
+ });
+ stderrThread = new Thread(new Runnable() {
+ public void run() {
+ InputStream is = p.getErrorStream();
+ try {
+ handler.handleStderr(is);
+ } catch (Throwable ex) {
+ DviUtils.logStackTrace(LOGGER, Level.WARNING, ex);
+ stderrThrowable = ex;
+ p.destroy();
+ } finally {
+ DviUtils.silentClose(is);
+ }
+ }
+ });
+ stdoutThread = new Thread(new Runnable() {
+ public void run() {
+ InputStream is = p.getInputStream();
+ try {
+ handler.handleStdout(is);
+ } catch (Throwable ex) {
+ DviUtils.logStackTrace(LOGGER, Level.WARNING, ex);
+ stdoutThrowable = ex;
+ p.destroy();
+ } finally {
+ DviUtils.silentClose(is);
+ }
+ }
+ });
+
+ stdinThread .start();
+ stderrThread.start();
+ stdoutThread.start();
+ } else {
+ DviUtils.silentClose(p.getOutputStream());
+ DviUtils.silentClose(p.getInputStream());
+ DviUtils.silentClose(p.getErrorStream());
+ }
+ }
+}