/* * PS3 Media Server, for streaming any medias to your PS3. * Copyright (C) 2008 A.Brochard * * 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; version 2 * of the License only. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms.io; import com.sun.jna.Platform; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; import net.pms.PMS; import net.pms.encoders.AviDemuxerInputStream; import net.pms.util.ProcessUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ProcessWrapperImpl extends Thread implements ProcessWrapper { private static final Logger LOGGER = LoggerFactory.getLogger(ProcessWrapperImpl.class); /** FONTCONFIG_PATH environment variable name */ private static final String FONTCONFIG_PATH = "FONTCONFIG_PATH"; private Process process; private OutputConsumer stdoutConsumer; private OutputConsumer stderrConsumer; private OutputParams params; private boolean destroyed; private String[] cmdArray; private boolean nullable; private ArrayList<ProcessWrapper> attachedProcesses; private BufferedOutputFile bo = null; private boolean keepStdout; private boolean keepStderr; private static int processCounter = 0; private boolean success; private final boolean useByteArrayStdConsumer; @Override public String toString() { return super.getName(); } public boolean isSuccess() { return success; } public ProcessWrapperImpl(String cmdArray[], OutputParams params) { this(cmdArray, params, false, false); } public ProcessWrapperImpl(String cmdArray[], boolean useByteArrayStdConsumer, OutputParams params) { this(cmdArray, useByteArrayStdConsumer, params, false, false); } public ProcessWrapperImpl(String cmdArray[], OutputParams params, boolean keepOutput) { this(cmdArray, false, params, keepOutput, keepOutput); } public ProcessWrapperImpl( String cmdArray[], boolean useByteArrayStdConsumer, OutputParams params, boolean keepOutput ) { this(cmdArray, useByteArrayStdConsumer, params, keepOutput, keepOutput); } public ProcessWrapperImpl(String cmdArray[], OutputParams params, boolean keepStdout, boolean keepStderr) { this(cmdArray, false, params, keepStdout, keepStderr); } public ProcessWrapperImpl( String cmdArray[], boolean useByteArrayStdConsumer, OutputParams params, boolean keepStdout, boolean keepStderr ) { super(); this.useByteArrayStdConsumer = useByteArrayStdConsumer; // Determine a suitable thread name for this process: // use the command name, but remove its path first. String threadName = cmdArray[0]; if (threadName.indexOf('/') >= 0) { threadName = threadName.substring(threadName.lastIndexOf('/') + 1); } if (threadName.indexOf('\\') >= 0) { threadName = threadName.substring(threadName.lastIndexOf('\\') + 1); } setName(threadName + "-" + getProcessCounter()); File exec = new File(cmdArray[0]); if (exec.isFile()) { cmdArray[0] = exec.getAbsolutePath(); } this.cmdArray = cmdArray; this.params = params; this.keepStdout = keepStdout; this.keepStderr = keepStderr; attachedProcesses = new ArrayList<>(); } private synchronized int getProcessCounter() { return processCounter++; } public void attachProcess(ProcessWrapper process) { attachedProcesses.add(process); } @Override public void run() { ProcessBuilder pb = new ProcessBuilder(cmdArray); try { LOGGER.debug("Starting " + ProcessUtil.dbgWashCmds(cmdArray)); if (params.workDir != null && params.workDir.isDirectory()) { pb.directory(params.workDir); } // Retrieve all environment variables of the process Map<String,String> environment = pb.environment(); // The variable params.env is initialized to null in the OutputParams // constructor and never set to another value in PMS code. Plugins // might use it? if (params.env != null && !params.env.isEmpty()) { // Actual name of system path var is case-sensitive String sysPathKey = Platform.isWindows() ? "Path" : "PATH"; // As is Map String PATH = params.env.containsKey("PATH") ? params.env.get("PATH") : params.env.containsKey("path") ? params.env.get("path") : params.env.containsKey("Path") ? params.env.get("Path") : null; if (PATH != null) { PATH += (File.pathSeparator + environment.get(sysPathKey)); } environment.putAll(params.env); if (PATH != null) { environment.put(sysPathKey, PATH); } } // Fontconfig on Mac OS X may have problems locating fonts. As a result // subtitles may be rendered invisible. Force feed fontconfig the // FONTCONFIG_PATH environment variable to the prepackaged fontconfig // configuration directory that comes with UMS on Mac OS X to make // sure it has sensible defaults. if (Platform.isMac()) { // Do not overwrite the variable if it already exists. if (!environment.containsKey(FONTCONFIG_PATH)) { String pmsWorkingDirectory = new File("").getAbsolutePath(); String fontconfigFontsPath = pmsWorkingDirectory + "/fonts"; LOGGER.trace("Setting FONTCONFIG_PATH to \"" + fontconfigFontsPath + "\""); environment.put(FONTCONFIG_PATH, fontconfigFontsPath); } } // XXX A cleaner way to execute short-running commands (e.g. vlc -version) // is being developed. When that's done, this class can be used solely // for the long-running tasks i.e. transcodes. At that point, we won't need // separate stdout and stderr and can merge them by uncommenting the // following line: // pb.redirectErrorStream(true); process = pb.start(); PMS.get().currentProcesses.add(process); if (stderrConsumer == null) { stderrConsumer = keepStderr ? new OutputTextConsumer(process.getErrorStream(), true) : new OutputTextLogger(process.getErrorStream()); } else { stderrConsumer.setInputStream(process.getErrorStream()); } stderrConsumer.setName(getName() + "-2"); stderrConsumer.start(); stdoutConsumer = null; if (useByteArrayStdConsumer) { stdoutConsumer = new ByteArrayOutputStreamConsumer(process.getInputStream(), params); bo = stdoutConsumer.getBuffer(); } else if (params.input_pipes[0] != null) { LOGGER.debug("Reading pipe: " + params.input_pipes[0].getInputPipe()); bo = params.input_pipes[0].getDirectBuffer(); if (bo == null || params.losslessaudio || params.lossyaudio || params.no_videoencode) { InputStream is = params.input_pipes[0].getInputStream(); if (params.avidemux) { is = new AviDemuxerInputStream(is, params, attachedProcesses); } stdoutConsumer = new OutputBufferConsumer(is, params); bo = stdoutConsumer.getBuffer(); } bo.attachThread(this); new OutputTextLogger(process.getInputStream()).start(); } else if (params.log) { stdoutConsumer = keepStdout ? new OutputTextConsumer(process.getInputStream(), true) : new OutputTextLogger(process.getInputStream()); } else { stdoutConsumer = new OutputBufferConsumer(process.getInputStream(), params); bo = stdoutConsumer.getBuffer(); bo.attachThread(this); } if (stdoutConsumer != null) { stdoutConsumer.setName(getName() + "-1"); stdoutConsumer.start(); } if (params.stdin != null) { params.stdin.push(process.getOutputStream()); } Integer pid = ProcessUtil.getProcessID(process); if (pid != null) { LOGGER.debug("Unix process ID ({}): {}", cmdArray[0], pid); } ProcessUtil.waitFor(process); // Wait up to a second for the stderr consumer thread to finish try { if (stderrConsumer != null) { stderrConsumer.join(1000); } } catch (InterruptedException e) { } // wait up to a second for the stdout consumer thread to finish try { if (stdoutConsumer != null) { stdoutConsumer.join(1000); } } catch (InterruptedException e) { } } catch (IOException e) { LOGGER.error("Error initializing process: ", e.getMessage()); if (LOGGER.isTraceEnabled()) { LOGGER.trace("", e); } stopProcess(); } finally { try { if (bo != null) { bo.close(); } } catch (IOException ioe) { LOGGER.debug("Error closing buffered output file", ioe.getMessage()); } if (!destroyed && !params.noexitcheck) { try { success = true; if (process != null && process.exitValue() != 0) { LOGGER.info("Process {} has a return code of {}! Maybe an error occurred... check the log file", cmdArray[0], process.exitValue()); success = false; } } catch (IllegalThreadStateException itse) { LOGGER.error("Error reading process exit value: {}", itse.getMessage()); LOGGER.trace("", itse); } } if (attachedProcesses != null) { for (ProcessWrapper pw : attachedProcesses) { if (pw != null) { pw.stopProcess(); } } } PMS.get().currentProcesses.remove(process); } } /** * Same as {@link #start()}, merely making the intention explicit in the * method name. * @see #runInSameThread() */ @Override public void runInNewThread() { this.start(); } /** * Same as {@link #run()}, merely making the intention explicit in the * method name. * @see #runInNewThread() */ @Override @SuppressFBWarnings("RU_INVOKE_RUN") public void runInSameThread() { if (!useByteArrayStdConsumer && !params.log) { LOGGER.warn( "ProcessWrapperImpl.runInSameThread() is called without using " + "byte array standard consumer or a text consumer. This can " + "cause this thread to hang and should be avoided!"); } this.run(); } /** * This method is only valid if the constructor was called with * {@code useByteArrayStdConsumer} set to {@code true}. Otherwise returns * {@code null}. * * @return The {@link BufferedOutputByteArrayImpl} or {@code null}. */ public BufferedOutputByteArrayImpl getOutputByteArray() { return bo instanceof BufferedOutputByteArrayImpl ? (BufferedOutputByteArrayImpl) bo : null; } @Override public InputStream getInputStream(long seek) throws IOException { if (bo != null) { return bo.getInputStream(seek); } else if (stdoutConsumer != null && stdoutConsumer.getBuffer() != null) { return stdoutConsumer.getBuffer().getInputStream(seek); } return null; } public List<String> getOtherResults() { if (stdoutConsumer == null) { return null; } try { stdoutConsumer.join(1000); } catch (InterruptedException e) { } return stdoutConsumer.getResults(); } @Override public List<String> getResults() { try { stderrConsumer.join(1000); } catch (InterruptedException e) { } return stderrConsumer.getResults(); } @Override public synchronized void stopProcess() { if (!destroyed) { destroyed = true; if (process != null) { Integer pid = ProcessUtil.getProcessID(process); if (pid != null) { LOGGER.debug("Stopping Unix process " + pid + ": " + this); } else { LOGGER.debug("Stopping process: " + this); } ProcessUtil.destroy(process); } if (attachedProcesses != null) { for (ProcessWrapper pw : attachedProcesses) { if (pw != null) { pw.stopProcess(); } } } if (stdoutConsumer != null && stdoutConsumer.getBuffer() != null) { stdoutConsumer.getBuffer().reset(); } } } @Override public boolean isDestroyed() { return destroyed; } @Override public boolean isReadyToStop() { return nullable; } @Override public void setReadyToStop(boolean nullable) { if (nullable != this.nullable) { LOGGER.trace("Ready to Stop: " + nullable); } this.nullable = nullable; } // TODO: implement setStdoutConsumer() ? public void setStderrConsumer(OutputConsumer consumer) { this.stderrConsumer = consumer; } }