net.pms.io.ThreadedProcessWrapper.java Source code

Java tutorial

Introduction

Here is the source code for net.pms.io.ThreadedProcessWrapper.java

Source

/*
 * Digital Media Server, for streaming digital media to DLNA compatible devices
 * based on www.ps3mediaserver.org and www.universalmediaserver.com.
 * Copyright (C) 2016 Digital Media Server developers.
 *
 * This program is a 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 static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.pms.service.Services;

/**
 * A non-blocking {@link Process} wrapper that runs the process in a new thread,
 * consumes all output and returns the result as a {@link Future}
 * {@link ProcessWrapperResult}.
 *
 * @param <C> the {@link ProcessWrapperConsumer} to use.
 * @param <R> the {@link ProcessWrapperResult} to use.
 * @param <T> the process output type.
 *
 * @author Nadahar
 */
public class ThreadedProcessWrapper<C extends ProcessWrapperConsumer<R, T>, R extends ProcessWrapperResult<T>, T> {

    private static final Logger LOGGER = LoggerFactory.getLogger(ThreadedProcessWrapper.class);

    private static final AtomicInteger PROCESS_COUNTER = new AtomicInteger(1);

    /** The {@link ProcessWrapperConsumer} instance */
    protected final C consumer;

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is returned as a {@link Future}
     * {@link ListProcessWrapperResult}.
     *
     * @param timeout the process timeout in {@code timeUnit}.
     * @param timeUnit the {@link TimeUnit} for {@code timeout}.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @param command the command(s) used to create the {@link Process}.
     * @return The process result as a {@link Future}
     *         {@link ListProcessWrapperResult}.
     */
    @Nonnull
    public static Future<ListProcessWrapperResult> runProcessListOutput(long timeout, @Nonnull TimeUnit timeUnit,
            long terminateTimeoutMS, @Nonnull String... command) {
        return new ThreadedProcessWrapper<ListProcessWrapperConsumer, ListProcessWrapperResult, List<String>>(
                new ListProcessWrapperConsumer()).runProcess(timeUnit.toMillis(timeout), terminateTimeoutMS,
                        command);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is returned as a {@link Future}
     * {@link ListProcessWrapperResult}.
     *
     * @param timeoutMS the process timeout in milliseconds.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @param command the command(s) used to create the {@link Process}.
     * @return The process result as a {@link Future}
     *         {@link ListProcessWrapperResult}.
     */
    @Nonnull
    public static Future<ListProcessWrapperResult> runProcessListOutput(long timeoutMS, long terminateTimeoutMS,
            @Nonnull String... command) {
        return new ThreadedProcessWrapper<ListProcessWrapperConsumer, ListProcessWrapperResult, List<String>>(
                new ListProcessWrapperConsumer()).runProcess(timeoutMS, terminateTimeoutMS, command);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is returned as a {@link Future}
     * {@link ListProcessWrapperResult}.
     *
     * @param command the command(s) used to create the {@link Process}.
     * @param timeout the process timeout in {@code timeUnit}.
     * @param timeUnit the {@link TimeUnit} for {@code timeout}.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @return The process result as a {@link Future}
     *         {@link ListProcessWrapperResult}.
     */
    @Nonnull
    public static Future<ListProcessWrapperResult> runProcessListOutput(@Nonnull List<String> command, long timeout,
            @Nonnull TimeUnit timeUnit, long terminateTimeoutMS) {
        return new ThreadedProcessWrapper<ListProcessWrapperConsumer, ListProcessWrapperResult, List<String>>(
                new ListProcessWrapperConsumer()).runProcess(command, timeUnit.toMillis(timeout),
                        terminateTimeoutMS);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is returned as a {@link Future}
     * {@link ListProcessWrapperResult}.
     *
     * @param command the command(s) used to create the {@link Process}.
     * @param timeoutMS the process timeout in milliseconds.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @return The process result as a {@link Future}
     *         {@link ListProcessWrapperResult}.
     */
    @Nonnull
    public static Future<ListProcessWrapperResult> runProcessListOutput(@Nonnull List<String> command,
            long timeoutMS, long terminateTimeoutMS) {
        return new ThreadedProcessWrapper<ListProcessWrapperConsumer, ListProcessWrapperResult, List<String>>(
                new ListProcessWrapperConsumer()).runProcess(command, timeoutMS, terminateTimeoutMS);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is returned as a {@link Future}
     * {@link ByteProcessWrapperResult}.
     *
     * @param timeout the process timeout in {@code timeUnit}.
     * @param timeUnit the {@link TimeUnit} for {@code timeout}.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @param command the command(s) used to create the {@link Process}.
     * @return The process result as a {@link Future}
     *         {@link ByteProcessWrapperResult}.
     */
    @Nonnull
    public static Future<ByteProcessWrapperResult> runProcessByteOutput(long timeout, @Nonnull TimeUnit timeUnit,
            long terminateTimeoutMS, @Nonnull String... command) {
        return new ThreadedProcessWrapper<ByteProcessWrapperConsumer, ByteProcessWrapperResult, byte[]>(
                new ByteProcessWrapperConsumer()).runProcess(timeUnit.toMillis(timeout), terminateTimeoutMS,
                        command);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is returned as a {@link Future}
     * {@link ByteProcessWrapperResult}.
     *
     * @param timeoutMS the process timeout in milliseconds.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @param command the command(s) used to create the {@link Process}.
     * @return The process result as a {@link Future}
     *         {@link ByteProcessWrapperResult}.
     */
    @Nonnull
    public static Future<ByteProcessWrapperResult> runProcessByteOutput(long timeoutMS, long terminateTimeoutMS,
            @Nonnull String... command) {
        return new ThreadedProcessWrapper<ByteProcessWrapperConsumer, ByteProcessWrapperResult, byte[]>(
                new ByteProcessWrapperConsumer()).runProcess(timeoutMS, terminateTimeoutMS, command);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is returned as a {@link Future}
     * {@link ByteProcessWrapperResult}.
     *
     * @param command the command(s) used to create the {@link Process}.
     * @param timeout the process timeout in {@code timeUnit}.
     * @param timeUnit the {@link TimeUnit} for {@code timeout}.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @return The process result as a {@link Future}
     *         {@link ByteProcessWrapperResult}.
     */
    @Nonnull
    public static Future<ByteProcessWrapperResult> runProcessByteOutput(@Nonnull List<String> command, long timeout,
            @Nonnull TimeUnit timeUnit, long terminateTimeoutMS) {
        return new ThreadedProcessWrapper<ByteProcessWrapperConsumer, ByteProcessWrapperResult, byte[]>(
                new ByteProcessWrapperConsumer()).runProcess(command, timeUnit.toMillis(timeout),
                        terminateTimeoutMS);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is returned as a {@link Future}
     * {@link ByteProcessWrapperResult}.
     *
     * @param command the command(s) used to create the {@link Process}.
     * @param timeoutMS the process timeout in milliseconds.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @return The process result as a {@link Future}
     *         {@link ByteProcessWrapperResult}.
     */
    @Nonnull
    public static Future<ByteProcessWrapperResult> runProcessByteOutput(@Nonnull List<String> command,
            long timeoutMS, long terminateTimeoutMS) {
        return new ThreadedProcessWrapper<ByteProcessWrapperConsumer, ByteProcessWrapperResult, byte[]>(
                new ByteProcessWrapperConsumer()).runProcess(command, timeoutMS, terminateTimeoutMS);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is discarded.
     *
     * @param timeout the process timeout in {@code timeUnit}.
     * @param timeUnit the {@link TimeUnit} for {@code timeout}.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @param command the command(s) used to create the {@link Process}.
     */
    @Nonnull
    public static void runProcessNullOutput(long timeout, @Nonnull TimeUnit timeUnit, long terminateTimeoutMS,
            @Nonnull String... command) {
        new ThreadedProcessWrapper<NullProcessWrapperConsumer, NullProcessWrapperResult, Void>(
                new NullProcessWrapperConsumer()).runProcess(timeUnit.toMillis(timeout), terminateTimeoutMS,
                        command);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is discarded.
     *
     * @param timeoutMS the process timeout in milliseconds.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     * @param command the command(s) used to create the {@link Process}.
     */
    @Nonnull
    public static void runProcessNullOutput(long timeoutMS, long terminateTimeoutMS, @Nonnull String... command) {
        new ThreadedProcessWrapper<NullProcessWrapperConsumer, NullProcessWrapperResult, Void>(
                new NullProcessWrapperConsumer()).runProcess(timeoutMS, terminateTimeoutMS, command);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is discarded.
     *
     * @param command the command(s) used to create the {@link Process}.
     * @param timeout the process timeout in {@code timeUnit}.
     * @param timeUnit the {@link TimeUnit} for {@code timeout}.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     */
    @Nonnull
    public static void runProcessNullOutput(@Nonnull List<String> command, long timeout, @Nonnull TimeUnit timeUnit,
            long terminateTimeoutMS) {
        new ThreadedProcessWrapper<NullProcessWrapperConsumer, NullProcessWrapperResult, Void>(
                new NullProcessWrapperConsumer()).runProcess(command, timeUnit.toMillis(timeout),
                        terminateTimeoutMS);
    }

    /**
     * Creates and runs a new {@link Process} in a new thread using the
     * specified commands. The process result is discarded.
     *
     * @param command the command(s) used to create the {@link Process}.
     * @param timeoutMS the process timeout in milliseconds.
     * @param terminateTimeoutMS the timeout in milliseconds for each
     *            termination attempt before a new attempt is made.
     */
    @Nonnull
    public static void runProcessNullOutput(@Nonnull List<String> command, long timeoutMS,
            long terminateTimeoutMS) {
        new ThreadedProcessWrapper<NullProcessWrapperConsumer, NullProcessWrapperResult, Void>(
                new NullProcessWrapperConsumer()).runProcess(command, timeoutMS, terminateTimeoutMS);
    }

    /**
     * Creates a new instance instance that will use the specified
     * {@link ProcessWrapperConsumer} instance. In most cases it's simpler to
     * use one of the static factory methods.
     *
     * @param consumer the {@link ProcessWrapperConsumer} instance of type
     *            {@code C} to use.
     */
    public ThreadedProcessWrapper(C consumer) {
        this.consumer = consumer;
    }

    /**
     * Creates a new instance instance that will create and use a new instance
     * the specified {@link ProcessWrapperConsumer} class. In most cases it's
     * simpler to use one of the static factory methods.
     *
     * @param consumer the {@link ProcessWrapperConsumer} class of type
     *            {@code C} to create.
     * @throws InstantiationException If an error occurs during creating of the
     *             new {@link ProcessWrapperConsumer} instance.
     * @throws IllegalAccessException If an access error occurs during creating
     *             of the new {@link ProcessWrapperConsumer} instance.
     */
    public ThreadedProcessWrapper(Class<C> consumer) throws InstantiationException, IllegalAccessException {
        this.consumer = consumer.newInstance();
    }

    /**
     * Runs a process with the given command array.
     *
     * @param timeout the process timeout in {@code timeUnit} after which the
     *            process is terminated. Use zero for no timeout, but be aware
     *            of the <a href=
     *            "https://web.archive.org/web/20121201070147/http://kylecartmell.com/?p=9"
     *            >pitfalls</a>
     * @param timeUnit the {@link TimeUnit} for {@code timeout}.
     * @param terminateTimeoutMS the timeout in milliseconds to wait for each
     *            termination attempt.
     * @param command the {@link String}s used to build the command line.
     * @return The {@link ProcessWrapperResult} from running the process.
     * @throws IllegalArgumentException If {@code command} is {@code null} or
     *             empty.
     */
    @Nonnull
    public Future<R> runProcess(long timeout, @Nonnull TimeUnit timeUnit, long terminateTimeoutMS,
            @Nonnull String... command) {
        return runProcess(Arrays.asList(command), timeUnit.toMillis(timeout), terminateTimeoutMS);
    }

    /**
     * Runs a process with the given command array.
     *
     * @param timeoutMS the process timeout in milliseconds after which the
     *            process is terminated. Use zero for no timeout, but be aware
     *            of the <a href=
     *            "https://web.archive.org/web/20121201070147/http://kylecartmell.com/?p=9"
     *            >pitfalls</a>
     * @param terminateTimeoutMS the timeout in milliseconds to wait for each
     *            termination attempt.
     * @param command the {@link String}s used to build the command line.
     * @return The {@link ProcessWrapperResult} from running the process.
     * @throws IllegalArgumentException If {@code command} is {@code null} or
     *             empty.
     */
    @Nonnull
    public Future<R> runProcess(long timeoutMS, long terminateTimeoutMS, @Nonnull String... command) {
        return runProcess(Arrays.asList(command), timeoutMS, terminateTimeoutMS);
    }

    /**
     * Runs a process with the given command {@link List}.
     *
     * @param command an array of {@link String} used to build the command line.
     * @param timeout the process timeout in {@code timeUnit} after which the
     *            process is terminated. Use zero for no timeout, but be aware
     *            of the <a href=
     *            "https://web.archive.org/web/20121201070147/http://kylecartmell.com/?p=9"
     *            >pitfalls</a>
     * @param timeUnit the {@link TimeUnit} for {@code timeout}.
     * @param terminateTimeoutMS the timeout in milliseconds to wait for each
     *            termination attempt.
     * @return The {@link ProcessWrapperResult} from running the process.
     * @throws IllegalArgumentException If {@code command} is {@code null} or
     *             empty.
     */
    @Nonnull
    public Future<R> runProcess(@Nonnull List<String> command, long timeout, @Nonnull TimeUnit timeUnit,
            long terminateTimeoutMS) {
        return runProcess(command, timeUnit.toMillis(timeout), terminateTimeoutMS);
    }

    /**
     * Runs a process with the given command {@link List}.
     *
     * @param command an array of {@link String} used to build the command line.
     * @param timeoutMS the process timeout in milliseconds after which the
     *            process is terminated. Use zero for no timeout, but be aware
     *            of the <a href=
     *            "https://web.archive.org/web/20121201070147/http://kylecartmell.com/?p=9"
     *            >pitfalls</a>
     * @param terminateTimeoutMS the timeout in milliseconds to wait for each
     *            termination attempt.
     * @return The {@link ProcessWrapperResult} from running the process.
     * @throws IllegalArgumentException If {@code command} is {@code null} or
     *             empty.
     */
    @Nonnull
    public Future<R> runProcess(@Nonnull final List<String> command, final long timeoutMS,
            final long terminateTimeoutMS) {
        if (command == null || command.isEmpty()) {
            throw new IllegalArgumentException("command can't be null or empty");
        }
        final String executableName;
        if (isNotBlank(command.get(0))) {
            Path executable = Paths.get(command.get(0)).getFileName();
            if (executable != null) {
                executableName = executable.toString();
            } else {
                executableName = command.get(0);
            }
        } else {
            executableName = command.get(0);
        }
        final int threadId = PROCESS_COUNTER.getAndIncrement();

        Callable<R> callable = new Callable<R>() {

            @Override
            public R call() throws InterruptedException {
                boolean manageProcess = timeoutMS > 0;
                ProcessBuilder processBuilder = new ProcessBuilder(command);
                processBuilder.redirectErrorStream(true);
                if (LOGGER.isTraceEnabled()) {
                    //XXX: Replace with String.join() in Java 8
                    LOGGER.trace("Executing \"{}\"", StringUtils.join(command, " "));
                }
                Process process;
                try {
                    process = processBuilder.start();
                } catch (IOException e) {
                    LOGGER.debug("IOException when trying to start \"{}\" process: {}", executableName,
                            e.getMessage());
                    LOGGER.trace("", e);
                    return consumer.createResult(null, Integer.MIN_VALUE, e);
                }
                Future<T> output = consumer.consume(process.getInputStream(),
                        "TPW \"" + executableName + "\" consumer " + threadId);
                if (manageProcess) {
                    Services.processManager().addProcess(process, executableName, timeoutMS, terminateTimeoutMS);
                }
                int exitCode = Integer.MIN_VALUE;
                boolean interrupted = false;
                boolean shutdown = false;
                do {
                    interrupted = false;
                    try {
                        exitCode = process.waitFor();
                    } catch (InterruptedException e) {
                        interrupted = Thread.interrupted();
                        if (!shutdown) {
                            if (manageProcess) {
                                Services.processManager().shutdownProcess(process, executableName);
                                manageProcess = false;
                            } else {
                                Services.processManager().addProcess(process, executableName, 0,
                                        terminateTimeoutMS);
                            }
                            shutdown = true;
                        }
                    }
                } while (interrupted);
                if (manageProcess) {
                    Services.processManager().removeProcess(process, executableName);
                }
                try {
                    return consumer.createResult(output.get(), exitCode, null);
                } catch (ExecutionException e) {
                    Throwable cause = e.getCause() != null ? e.getCause() : e;
                    LOGGER.error("ExecutionException in \"{}\" consumer, no output will be returned: {}",
                            executableName, cause.getMessage());
                    LOGGER.trace("", e);
                    return consumer.createResult(null, exitCode, cause);
                }
            }
        };
        FutureTask<R> result = new FutureTask<R>(callable);
        Thread runner = new Thread(result, "TPW \"" + executableName + "\" " + threadId);
        runner.start();
        return result;
    }
}