All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.mike10004.nativehelper.subprocess.ProcessMissionControl Maven / Gradle / Ivy

/*
 * A lot of code taken from Execute class in apache-ant and (c) Apache Software Foundation
 */
package com.github.mike10004.nativehelper.subprocess;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ProcessBuilder.Redirect;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static java.util.Objects.requireNonNull;

class ProcessMissionControl {

    private static final Logger log = LoggerFactory.getLogger(ProcessMissionControl.class);

    private final ListeningExecutorService terminationWaitingService;
    private final Subprocess program;
    private final ProcessTracker processTracker;

    public ProcessMissionControl(Subprocess program, ProcessTracker processTracker, ListeningExecutorService terminationWaitingService) {
        this.program = requireNonNull(program);
        this.processTracker = requireNonNull(processTracker);
        this.terminationWaitingService = requireNonNull(terminationWaitingService);
    }

    public interface Execution {
        Process getProcess();
        FluentFuture> getFuture();
    }

    public  Execution launch(StreamControl streamControl, Function> resultTransform) {
        Process process = execute();
        FluentFuture> future = FluentFuture.from(terminationWaitingService.submit(() -> {
            Integer exitCode = follow(process, streamControl);
            return resultTransform.apply(exitCode);
        }));
        return new Execution() {
            @Override
            public Process getProcess() {
                return process;
            }

            @Override
            public FluentFuture> getFuture() {
                return future;
            }
        };
    }

    private ImmutableList getCommandLine() {
        ImmutableList.Builder cmdline = ImmutableList.builder();
        cmdline.add(program.executable());
        cmdline.addAll(program.arguments());
        return cmdline.build();
    }

    private Process createProcess(List cmdline) {
        ProcessBuilder pb = new ProcessBuilder()
                .command(cmdline)
                .redirectError(Redirect.PIPE)
                .redirectOutput(Redirect.PIPE)
                .redirectInput(Redirect.PIPE)
                .directory(program.workingDirectory());
        Map pbenv = pb.environment();
        pbenv.putAll(program.environment());
        try {
            return pb.start();
        } catch (IOException e) {
            throw new ProcessStartException(e);
        }
    }

    static class ProcessStartException extends ProcessLaunchException {
        public ProcessStartException(IOException cause) {
            super(cause);
        }
    }

    /**
     * Runs a process and returns its exit status.
     *
     * @return the exit status of the subprocess or null if the process did
     * @exception ProcessException The exception is thrown, if launching
     *            of the subprocess failed.
     */
    @VisibleForTesting
    Process execute() {
        File workingDirectory = program.workingDirectory();
        if (!InvalidWorkingDirectoryException.check(workingDirectory)) {
            throw new InvalidWorkingDirectoryException(workingDirectory);
        }
        final Process process = createProcess(getCommandLine());
        processTracker.add(process);
        return process;
    }

    static class InvalidWorkingDirectoryException extends ProcessLaunchException {
        public InvalidWorkingDirectoryException(File workingDirectory) {
            super("specified working directory " + workingDirectory + " is not a directory; is file? " + workingDirectory.isFile());
        }

        static boolean check(@Nullable File workingDirectory) {
            return workingDirectory == null || workingDirectory.isDirectory();
        }
    }

    private static class MaybeNullResource implements java.io.Closeable {
        @Nullable
        public final T resource;

        private MaybeNullResource(@Nullable T closeable) {
            this.resource = closeable;
        }

        @Override
        public void close() throws IOException {
            if (resource != null) {
                resource.close();
            }
        }

        public static  MaybeNullResource of(T stream) {
            return new MaybeNullResource<>(stream);
        }
    }

    @VisibleForTesting
    @Nullable
    Integer follow(Process process, StreamControl outputContext) throws IOException {
        boolean terminated = false;
        @Nullable Integer exitVal;
        OutputStream processStdin = null;
        InputStream processStdout = null, processStderr = null;
        try (MaybeNullResource inResource = MaybeNullResource.of(outputContext.openStdinSource());
            OutputStream stdoutDestination = outputContext.openStdoutSink();
            OutputStream stderrDestination = outputContext.openStderrSink()) {
            StreamConduit conduit = new StreamConduit(stdoutDestination, stderrDestination, inResource.resource);
            processStdin = process.getOutputStream();
            processStdout = process.getInputStream();
            processStderr = process.getErrorStream();
            try (Closeable ignore = conduit.connect(processStdin, processStdout, processStderr)) {
                exitVal = waitFor(process);
                if (exitVal != null) {
                    terminated = true;
                }
            }
        } finally {
            if (!terminated) {
                destroy(process);
            }
            processTracker.remove(process);
            closeStreams(processStdin, processStdout, processStderr);
        }
        if (exitVal == null) {
            throw new IllegalProcessStateException("no way to wait for process; probably interrupted in ProcessMissionControl.waitFor");
        }
        return exitVal;
    }

    private static class IllegalProcessStateException extends IllegalStateException {
        public IllegalProcessStateException(String msg) {
            super(msg);
        }
    }

    private void destroy(Process process) {
        boolean terminatedNaturally = false;
        try {
            terminatedNaturally = process.waitFor(0, TimeUnit.MILLISECONDS);
        } catch(InterruptedException e) {
            log.error("interrupted while waiting with timeout 0", e);
            throw new IllegalStateException("BUG: interrupted exception shouldn't happen because timeout length is zero", e);
        } finally {
            if (!terminatedNaturally && process.isAlive()) {
                process.destroy();
            }
        }

    }

    /**
     * Wait for a given process.
     *
     * @param process the process one wants to wait for.
     */
    @Nullable
    private Integer waitFor(Process process) {
        try {
            return process.waitFor();
        } catch (InterruptedException e) {
            log.info("interrupted in Process.waitFor");
            return null;
        }
    }

    private static void closeStreams(java.io.Closeable...streams) {
        for (java.io.Closeable stream : streams) {
            try {
                stream.close();
            } catch (IOException ignore) {
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy