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

io.github.mike10004.debianmaven.SubprocessProcessRunner Maven / Gradle / Ivy

Go to download

This plugin helps building DEB packages from Maven projects. The packages can be used in DEB-based operating systems such as Debian and Ubuntu. The plugin uses external Debian tools to do the actual packaging.

The newest version!
package io.github.mike10004.debianmaven;

import io.github.mike10004.subprocess.ProcessMonitor;
import io.github.mike10004.subprocess.ProcessResult;
import io.github.mike10004.subprocess.ScopedProcessTracker;
import io.github.mike10004.subprocess.StreamContent;
import io.github.mike10004.subprocess.StreamContext;
import io.github.mike10004.subprocess.StreamControl;
import io.github.mike10004.subprocess.Subprocess;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;

import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;

class SubprocessProcessRunner implements ProcessRunner {

    private final Supplier logGetter;
    private final Charset processOutputEncoding;
    private final Duration processTimeout;
    private final BiConsumer processStdoutConsumer;
    private final BiConsumer processStderrConsumer;

    public SubprocessProcessRunner(Supplier logGetter) {
        this(logGetter, Charset.defaultCharset(), getDefaultProcessTimeout());
    }

    public SubprocessProcessRunner(Supplier logGetter, Charset processOutputEncoding, Duration processTimeout) {
        this(logGetter, processOutputEncoding, processTimeout, defaultProcessLineConsumer(), defaultProcessLineConsumer());
    }

    public SubprocessProcessRunner(Supplier logGetter, Charset processOutputEncoding, Duration processTimeout, BiConsumer processStdoutConsumer, BiConsumer processStderrConsumer) {
        this.logGetter = requireNonNull(logGetter);
        this.processOutputEncoding = requireNonNull(processOutputEncoding);
        this.processTimeout = requireNonNull(processTimeout);
        this.processStdoutConsumer = requireNonNull(processStdoutConsumer);
        this.processStderrConsumer = requireNonNull(processStderrConsumer);
    }

    public static Duration getDefaultProcessTimeout() {
        return Duration.ofMinutes(15);
    }

    private static class FixedControlStreamContext implements StreamContext {

        private final StreamControl streamControl;

        private FixedControlStreamContext(StreamControl streamControl) {
            this.streamControl = streamControl;
        }

        @Override
        public StreamControl produceControl() throws IOException {
            return streamControl;
        }

        @Override
        public final StreamContent transform(int exitCode, StreamControl context) {
            return StreamContent.absent();
        }

    }

    private static class PipePair {
        public final PipedOutputStream output;
        public final PipedInputStream input;
        public PipePair() throws IOException {
            input = new PipedInputStream();
            output = new PipedOutputStream(input);
        }
    }

    private static class TailingStreamControl implements StreamControl {

        private final PipePair stdoutPipe;
        private final PipePair stderrPipe;

        public TailingStreamControl() throws IOException {
            stdoutPipe = new PipePair();
            stderrPipe = new PipePair();
        }

        @Override
        public OutputStream openStdoutSink() throws IOException {
            return stdoutPipe.output;
        }

        @Override
        public OutputStream openStderrSink() {
            return stderrPipe.output;
        }

        @Nullable
        @Override
        public InputStream openStdinSource() {
            return null;
        }

        public InputStream getStdoutPipe() {
            return stdoutPipe.input;
        }

        public InputStream getStderrPipe() {
            return stderrPipe.input;
        }

    }

    private static BiConsumer defaultProcessLineConsumer() {
        return Log::info;
    }

    @Override
    public void runProcess(String[] cmd, Map env, NonzeroProcessExitAction nonzeroExitAction) throws IOException, MojoExecutionException {
        Function drinkerCreator = new Function() {
            @Override
            public ProcessStreamDrinker apply(TailingStreamControl tailingStreamControl) {
                return new ProcessTextDrinker(tailingStreamControl::getStdoutPipe, line -> processStdoutConsumer.accept(logGetter.get(), line));
            }
        };
        doRunProcess(cmd, env, nonzeroExitAction, drinkerCreator);
    }

    @Override
    public byte[] runProcessWithOutput(String[] cmd, Map env, NonzeroProcessExitAction nonzeroExitAction) throws IOException, MojoExecutionException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
        Function drinkerCreator = new Function() {
            @Override
            public ProcessStreamDrinker apply(TailingStreamControl tailingStreamControl) {
                return new ProcessByteDrinker(tailingStreamControl::getStdoutPipe, buffer);
            }
        };
        doRunProcess(cmd, env, nonzeroExitAction, drinkerCreator);
        return buffer.toByteArray();
    }

    private void doRunProcess(String[] cmd, Map env, NonzeroProcessExitAction nonzeroExitAction, Function drinkerCreator) throws IOException, MojoExecutionException {
        if (cmd.length < 1) {
            throw new IllegalArgumentException("command must have at least one element (executable)");
        }
        String executable = cmd[0];
        List executableArgs = Arrays.asList(cmd).subList(1, cmd.length);
        TailingStreamControl tailingStreamControl = new TailingStreamControl();
        FixedControlStreamContext ctx = new FixedControlStreamContext(tailingStreamControl);
        ProcessResult result;
        try (ScopedProcessTracker processTracker = new ScopedProcessTracker()) {
            ProcessMonitor monitor = Subprocess.running(executable)
                    .args(executableArgs)
                    .env(env)
                    .build()
                    .launcher(processTracker)
                    .output(ctx)
                    .launch();
            Thread stdoutThread = new Thread(() -> {
                ProcessStreamDrinker d = drinkerCreator.apply(tailingStreamControl);
                d.drink();
            });
            Thread stderrThread = new Thread(() -> {
                ProcessStreamDrinker d = new ProcessTextDrinker(tailingStreamControl::getStderrPipe, line -> processStderrConsumer.accept(logGetter.get(), line));
                d.drink();
            });
            stdoutThread.start();
            stderrThread.start();
            doWithTimeout(stdoutThread::join, false);
            doWithTimeout(stderrThread::join, false);
            result = doWithTimeoutAndReturn(t -> {
                return monitor.await(t, TimeUnit.MILLISECONDS);
            }, true, null);
        }
        int exitCode = result.exitCode();
        if (exitCode != 0) {
            logGetter.get().warn("process exit code: " + exitCode);
            nonzeroExitAction.perform(exitCode, cmd);
        }
    }

    private interface BlockingAction {
        T perform(long millis) throws TimeoutException, InterruptedException;
    }

    private interface BlockingRunnable {
        void perform(long millis) throws TimeoutException, InterruptedException;
    }

    @SuppressWarnings({"SameParameterValue", "RedundantSuppression"})
    private void doWithTimeout(BlockingRunnable action, boolean throwOnPrematureTermination) throws MojoExecutionException {
        //noinspection RedundantCast,Convert2Diamond
        doWithTimeoutAndReturn(new BlockingAction() {

            @Override
            public Void perform(long millis) throws TimeoutException, InterruptedException {
                action.perform(millis);
                return null;
            }
        }, throwOnPrematureTermination, (Void) null);
    }

    private  T doWithTimeoutAndReturn(BlockingAction action, boolean throwOnPrematureTermination, T valueIfNoThrow) throws MojoExecutionException {
        try {
            return action.perform(processTimeout.toMillis());
        } catch (InterruptedException | TimeoutException e) {
            if (throwOnPrematureTermination) {
                throw new MojoExecutionException("process execution timed out or was interrupted", e);
            } else {
                logGetter.get().warn("terminated early from " + action);
                return valueIfNoThrow;
            }
        }
    }

    private interface IOSupplier {
        T openStream() throws IOException;
    }

    private final class ProcessTextDrinker implements ProcessStreamDrinker {

        private final IOSupplier inputProvider;
        private final Consumer lineConsumer;

        public ProcessTextDrinker(IOSupplier inputProvider, Consumer lineConsumer) {
            this.inputProvider = inputProvider;
            this.lineConsumer = lineConsumer;
        }

        public void drink() {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputProvider.openStream(), processOutputEncoding))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    lineConsumer.accept(line);
                }
            } catch (IOException e) {
                logGetter.get().warn(e);
            }
        }

    }

    private interface ProcessStreamDrinker {
        void drink();
    }

    private final class ProcessByteDrinker implements ProcessStreamDrinker {

        private final IOSupplier inputProvider;
        private final OutputStream sink;

        public ProcessByteDrinker(IOSupplier inputProvider, OutputStream sink) {
            this.inputProvider = inputProvider;
            this.sink = sink;
        }

        public void drink() {
            try (InputStream inputStream = inputProvider.openStream()) {
                inputStream.transferTo(sink);
            } catch (IOException e) {
                logGetter.get().warn(e);
            }
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy