/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly, CloudBees, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package hudson;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Proc.LocalProc;
import hudson.Proc.ProcWithJenkins23271Patch;
import hudson.model.Computer;
import hudson.model.Node;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.remoting.Channel;
import hudson.remoting.Pipe;
import hudson.remoting.RemoteInputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
import hudson.util.ArgumentListBuilder;
import hudson.util.ProcessTree;
import hudson.util.QuotedStringTokenizer;
import hudson.util.StreamCopyThread;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.agents.ControllerToAgentCallable;
import jenkins.model.Jenkins;
import jenkins.security.MasterToSlaveCallable;
import jenkins.tasks.filters.EnvVarsFilterLocalRule;
import jenkins.tasks.filters.EnvVarsFilterRuleWrapper;
import jenkins.tasks.filters.EnvVarsFilterableBuilder;
import jenkins.util.MemoryReductionUtil;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
 * Starts a process.
 *
 * <p>
 * This hides the difference between running programs locally vs remotely.
 *
 *
 * <h2>'env' parameter</h2>
 * <p>
 * To allow important environment variables to be copied over to the remote machine,
 * the 'env' parameter shouldn't contain default inherited environment variables
 * (which often contains machine-specific information, like PATH, TIMEZONE, etc.)
 *
 * <p>
 * {@link Launcher} is responsible for inheriting environment variables.
 *
 *
 * @author Kohsuke Kawaguchi
 * @see FilePath#createLauncher(TaskListener)
 */
public abstract class Launcher {

    @NonNull
    protected final TaskListener listener;

    @CheckForNull
    protected final VirtualChannel channel;

    @Restricted(Beta.class)
    protected EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper;

    protected Launcher(@NonNull TaskListener listener, @CheckForNull VirtualChannel channel) {
        this.listener = listener;
        this.channel = channel;
    }

    /**
     * Constructor for a decorator.
     * @param launcher Launcher to be decorated
     */
    protected Launcher(@NonNull Launcher launcher) {
        this(launcher.listener, launcher.channel);
    }

    /**
     * Build the environment filter rules that will be applied on the environment variables
     * @param run The run that requested the command interpretation, could be <code>null</code> if outside of a run context.
     * @param builder The builder that asked to run this command
     *
     * @since 2.246
     */
    @Restricted(Beta.class)
    public void prepareFilterRules(@CheckForNull Run<?, ?> run, @NonNull EnvVarsFilterableBuilder builder) {
        List<EnvVarsFilterLocalRule> specificRuleList = builder.buildEnvVarsFilterRules();
        EnvVarsFilterRuleWrapper ruleWrapper = EnvVarsFilterRuleWrapper.createRuleWrapper(run, builder, this, specificRuleList);
        this.setEnvVarsFilterRuleWrapper(ruleWrapper);
    }

    @Restricted(Beta.class)
    protected void setEnvVarsFilterRuleWrapper(EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper) {
        this.envVarsFilterRuleWrapper = envVarsFilterRuleWrapper;
    }

    /**
     * Gets the channel that can be used to run a program remotely.
     *
     * @return
     *      {@code null} if the target node is not configured to support this.
     *      this is a transitional measure.
     *      Note that a launcher for the built-in node is always non-null.
     */
    @CheckForNull
    public VirtualChannel getChannel() {
        return channel;
    }

    /**
     * Gets the {@link TaskListener} that this launcher uses to
     * report the commands that it's executing.
     *
     * @return Task listener
     */
    @NonNull
    public TaskListener getListener() {
        return listener;
    }

    /**
     * If this {@link Launcher} is encapsulating an execution on a specific {@link Computer},
     * return it.
     *
     * <p>
     * Because of the way internal Hudson abstractions are set up (that is, {@link Launcher} only
     * needs a {@link VirtualChannel} to do its job and isn't really required that the channel
     * comes from an existing {@link Computer}), this method may not always the right {@link Computer} instance.
     *
     * @return
     *      {@code null} if this launcher is not created from a {@link Computer} object.
     * @deprecated since 2008-11-16.
     *      See the javadoc for why this is inherently unreliable. If you are trying to
     *      figure out the current {@link Computer} from within a build, use
     *      {@link FilePath#toComputer()} or {@link Computer#currentComputer()}.
     */
    @Deprecated
    @CheckForNull
    public Computer getComputer() {
        for (Computer c : Jenkins.get().getComputers())
            if (c.getChannel() == channel)
                return c;
        return null;
    }

    /**
     * Builder pattern for configuring a process to launch.
     * @since 1.311
     */
    public final class ProcStarter {
        protected List<String> commands;
        @CheckForNull
        protected boolean[] masks;
        private boolean quiet;
        @CheckForNull
        protected FilePath pwd;
        @CheckForNull
        protected OutputStream stdout = OutputStream.nullOutputStream(), stderr;
        @CheckForNull
        private TaskListener stdoutListener;
        @CheckForNull
        protected InputStream stdin = NULL_INPUT_STREAM;
        @CheckForNull
        protected String[] envs = null;
        /**
         * Represent the build step, either from legacy build process or from pipeline one
         */
        @CheckForNull
        @Restricted(Beta.class)
        protected EnvVarsFilterableBuilder envVarsFilterableBuilder = null;

        /**
         * True to reverse the I/O direction.
         *
         * For example, if {@link #reverseStdout}==true, then we expose
         * {@link InputStream} from {@link Proc} and expect the client to read from it,
         * whereas normally we take {@link OutputStream} via {@link #stdout(OutputStream)}
         * and feed stdout into that output.
         *
         * @since 1.399
         */
        protected boolean reverseStdin, reverseStdout, reverseStderr;

        /**
         * Passes a white-space separated single-string command (like "cat abc def") and parse them
         * as a command argument. This method also handles quotes.
         */
        public ProcStarter cmdAsSingleString(String s) {
            return cmds(QuotedStringTokenizer.tokenize(s));
        }

        public ProcStarter cmds(String... args) {
            return cmds(Arrays.asList(args));
        }

        public ProcStarter cmds(File program, String... args) {
            commands = new ArrayList<>(args.length + 1);
            commands.add(program.getPath());
            commands.addAll(Arrays.asList(args));
            return this;
        }

        public ProcStarter cmds(List<String> args) {
            commands = new ArrayList<>(args);
            return this;
        }

        public ProcStarter cmds(ArgumentListBuilder args) {
            commands = args.toList();
            masks = args.toMaskArray();
            return this;
        }

        public List<String> cmds() {
            return commands;
        }

        /**
         * Hide parts of the command line from being printed to the log.
         * @param masks true for each position in {@link #cmds(String[])} which should be masked, false to print
         * @return this
         * @see ArgumentListBuilder#add(String, boolean)
         * @see #maskedPrintCommandLine(List, boolean[], FilePath)
         */
        public ProcStarter masks(@CheckForNull boolean... masks) {
            this.masks = masks;
            return this;
        }

        @CheckForNull
        public boolean[] masks() {
            return masks;
        }

        /**
         * Allows {@link #maskedPrintCommandLine(List, boolean[], FilePath)} to be suppressed from {@link hudson.Launcher.LocalLauncher#launch(hudson.Launcher.ProcStarter)}.
         * Useful when the actual command being printed is noisy and unreadable and the caller would rather print diagnostic information in a customized way.
         * @param quiet to suppress printing the command line when starting the process; false to keep default behavior of printing
         * @return {@code this}
         * @since 1.576
         */
        public ProcStarter quiet(boolean quiet) {
            this.quiet = quiet;
            return this;
        }

        /**
         * @since 1.576
         */
        public boolean quiet() {
            return quiet;
        }

        /**
         * Sets the current directory.
         *
         * @param workDir Work directory to be used.
         *                If {@code null}, the default/current directory will be used by the process starter
         * @return {@code this}
         */
        public ProcStarter pwd(@CheckForNull FilePath workDir) {
            this.pwd = workDir;
            return this;
        }

        public ProcStarter pwd(@NonNull File workDir) {
            return pwd(new FilePath(workDir));
        }

        public ProcStarter pwd(@NonNull String workDir) {
            return pwd(new File(workDir));
        }

        @CheckForNull
        public FilePath pwd() {
            return pwd;
        }

        /**
         * Sets STDOUT destination.
         *
         * @param out Output stream.
         *            Use {@code null} to send STDOUT to {@code /dev/null}.
         * @return {@code this}
         */
        public ProcStarter stdout(@CheckForNull OutputStream out) {
            this.stdout = out;
            stdoutListener = null;
            return this;
        }

        /**
         * Sends the stdout to the given {@link TaskListener}.
         *
         * @param out Task listener (must be safely remotable)
         * @return {@code this}
         */
        public ProcStarter stdout(@NonNull TaskListener out) {
            stdout = out.getLogger();
            stdoutListener = out;
            return this;
        }

        /**
         * Gets current STDOUT destination.
         *
         * @return STDOUT output stream. {@code null} if STDOUT is suppressed or undefined.
         */
        @CheckForNull
        public OutputStream stdout() {
            return stdout;
        }

        /**
         * Controls where the stderr of the process goes.
         * By default, it's bundled into stdout.
         */
        public ProcStarter stderr(@CheckForNull OutputStream err) {
            this.stderr =  err;
            return this;
        }

        /**
         * Gets current STDERR destination.
         *
         * @return STDERR output stream. {@code null} if suppressed or undefined.
         */
        @CheckForNull
        public OutputStream stderr() {
            return stderr;
        }

        /**
         * Controls where the stdin of the process comes from.
         * By default, {@code /dev/null}.
         *
         * @return {@code this}
         */
        @NonNull
        public ProcStarter stdin(@CheckForNull InputStream in) {
            this.stdin = in;
            return this;
        }

        /**
         * Gets current STDIN destination.
         *
         * @return STDIN output stream. {@code null} if suppressed or undefined.
         */
        @CheckForNull
        public InputStream stdin() {
            return stdin;
        }

        /**
         * Sets the environment variable overrides.
         *
         * <p>
         * In addition to what the current process
         * is inherited (if this is going to be launched from a agent agent, that
         * becomes the "current" process), these variables will be also set.
         *
         * @param overrides Environment variables to be overridden
         * @return {@code this}
         */
        public ProcStarter envs(@NonNull Map<String, String> overrides) {
            this.envs = Util.mapToEnv(overrides);
            return this;
        }

        /**
         * @param overrides
         *      List of "VAR=VALUE". See {@link #envs(Map)} for the semantics.
         *
         * @return {@code this}
         */
        public ProcStarter envs(@CheckForNull String... overrides) {
            if (overrides != null) {
                for (String override : overrides) {
                    if (override.indexOf('=') == -1) {
                        throw new IllegalArgumentException(override);
                    }
                }
            }
            this.envs = overrides;
            return this;
        }

        /**
         * Gets a list of environment variables to be set.
         * Returns an empty array if envs field has not been initialized.
         *
         * @return If initialized, returns a copy of internal envs array. Otherwise - a new empty array.
         */
        @NonNull
        public String[] envs() {
            return envs != null ? envs.clone() : MemoryReductionUtil.EMPTY_STRING_ARRAY;
        }

        /**
         * Indicates that the caller will pump {@code stdout} from the child process
         * via {@link Proc#getStdout()} (whereas by default you call {@link #stdout(OutputStream)}
         * and let Jenkins pump stdout into your {@link OutputStream} of choosing.
         *
         * <p>
         * When this method is called, {@link Proc#getStdout()} will read the combined output
         * of {@code stdout/stderr} from the child process, unless {@link #readStderr()} is called
         * separately, which lets the caller read those two streams separately.
         *
         * @return {@code this}
         * @since 1.399
         */
        public ProcStarter readStdout() {
            reverseStdout = true;
            stdout = stderr = null;
            return this;
        }

        /**
         * In addition to the effect of {@link #readStdout()}, indicate that the caller will pump {@code stderr}
         * from the child process separately from {@code stdout}. The stderr will be readable from
         * {@link Proc#getStderr()} while {@link Proc#getStdout()} reads from stdout.
         *
         * @return {@code this}
         * @since 1.399
         */
        public ProcStarter readStderr() {
            reverseStdout = true;
            reverseStderr = true;
            return this;
        }

        /**
         * Indicates that the caller will directly write to the child process {@link #stdin()} via {@link Proc#getStdin()}.
         * (Whereas by default you call {@link #stdin(InputStream)}
         * and let Jenkins pump your {@link InputStream} of choosing to stdin.)
         *
         * @return {@code this}
         * @since 1.399
         */
        public ProcStarter writeStdin() {
            reverseStdin = true;
            stdin = null;
            return this;
        }

        /**
         * Specify the build step that want to run the command to enable the environment filters
         * @return {@code this}
         * @since 2.246
         */
        @Restricted(Beta.class)
        public ProcStarter buildStep(EnvVarsFilterableBuilder envVarsFilterableBuilder) {
            this.envVarsFilterableBuilder = envVarsFilterableBuilder;
            return this;
        }

        /**
         * @return if set, returns the build step that wants to run the command
         * @since 2.246
         */
        @Restricted(Beta.class)
        public @CheckForNull
        EnvVarsFilterableBuilder buildStep() {
            return envVarsFilterableBuilder;
        }

        /**
         * Starts the new process as configured.
         */
        public Proc start() throws IOException {
            return launch(this);
        }

        /**
         * Starts the process and waits for its completion.
         * @return Return code of the invoked process
         * @throws IOException Operation error (e.g. remote call failure)
         * @throws InterruptedException The process has been interrupted
         */
        public int join() throws IOException, InterruptedException {
            // The logging around procHolderForJoin prevents the preliminary object deallocation we saw in JENKINS-23271
            final Proc procHolderForJoin = start();
            LOGGER.log(Level.FINER, "Started the process {0}", procHolderForJoin);

            if (procHolderForJoin instanceof ProcWithJenkins23271Patch) {
                return procHolderForJoin.join();
            } else {
                // Fallback to the internal handling logic
                if (!(procHolderForJoin instanceof LocalProc)) {
                    // We consider that the process may be at risk of JENKINS-23271
                    LOGGER.log(Level.FINE, "Process {0} of type {1} is neither {2} nor instance of {3}. "
                            + "If this process operates with Jenkins agents via remote invocation, you may get into JENKINS-23271",
                            new Object[] {procHolderForJoin, procHolderForJoin.getClass(), LocalProc.class, ProcWithJenkins23271Patch.class});
                }
                try {
                    final int returnCode = procHolderForJoin.join();
                    if (LOGGER.isLoggable(Level.FINER)) {
                        LOGGER.log(Level.FINER, "Process {0} has finished with the return code {1}", new Object[]{procHolderForJoin, returnCode});
                    }
                    return returnCode;
                } finally {
                    if (procHolderForJoin.isAlive()) { // Should never happen but this forces Proc to not be removed and early GC by escape analysis
                        LOGGER.log(Level.WARNING, "Process {0} has not finished after the join() method completion", procHolderForJoin);
                    }
                }
            }
        }

        /**
         * Copies a {@link ProcStarter}.
         */
        @NonNull
        public ProcStarter copy() {
            ProcStarter rhs = new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs).quiet(quiet).buildStep(envVarsFilterableBuilder);
            rhs.stdoutListener = stdoutListener;
            rhs.reverseStdin  = this.reverseStdin;
            rhs.reverseStderr = this.reverseStderr;
            rhs.reverseStdout = this.reverseStdout;
            return rhs;
        }
    }

    /**
     * Launches a process by using a {@linkplain ProcStarter builder-pattern} to configure
     * the parameters.
     */
    @NonNull
    public final ProcStarter launch() {
        return new ProcStarter();
    }

    /**
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String cmd, Map<String, String> env, OutputStream out, FilePath workDir) throws IOException {
        return launch(cmd, Util.mapToEnv(env), out, workDir);
    }

    /**
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String[] cmd, Map<String, String> env, OutputStream out, FilePath workDir) throws IOException {
        return launch(cmd, Util.mapToEnv(env), out, workDir);
    }

    /**
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String[] cmd, Map<String, String> env, InputStream in, OutputStream out) throws IOException {
        return launch(cmd, Util.mapToEnv(env), in, out);
    }

    /**
     * Launch a command with optional censoring of arguments from the listener (Note: <strong>The censored portions will
     * remain visible through /proc, pargs, process explorer, etc. i.e. people logged in on the same machine</strong>
     * This version of the launch command just ensures that it is not visible from a build log which is exposed via the
     * web)
     *
     * @param cmd     The command and all it's arguments.
     * @param mask    Which of the command and arguments should be masked from the listener
     * @param env     Environment variable overrides.
     * @param out     stdout and stderr of the process will be sent to this stream. the stream won't be closed.
     * @param workDir null if the working directory could be anything.
     * @return The process of the command.
     * @throws IOException When there are IO problems.
     *
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String[] cmd, boolean[] mask, Map<String, String> env, OutputStream out, FilePath workDir) throws IOException {
        return launch(cmd, mask, Util.mapToEnv(env), out, workDir);
    }

    /**
     * Launch a command with optional censoring of arguments from the listener (Note: <strong>The censored portions will
     * remain visible through /proc, pargs, process explorer, etc. i.e. people logged in on the same machine</strong>
     * This version of the launch command just ensures that it is not visible from a build log which is exposed via the
     * web)
     *
     * @param cmd     The command and all it's arguments.
     * @param mask    Which of the command and arguments should be masked from the listener
     * @param env     Environment variable overrides.
     * @param in      null if there's no input.
     * @param out     stdout and stderr of the process will be sent to this stream. the stream won't be closed.
     * @return The process of the command.
     * @throws IOException When there are IO problems.
     *
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String[] cmd, boolean[] mask, Map<String, String> env, InputStream in, OutputStream out) throws IOException {
        return launch(cmd, mask, Util.mapToEnv(env), in, out);
    }

    /**
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String cmd, String[] env, OutputStream out, FilePath workDir) throws IOException {
        return launch(Util.tokenize(cmd), env, out, workDir);
    }

    /**
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String[] cmd, String[] env, OutputStream out, FilePath workDir) throws IOException {
        return launch(cmd, env, null, out, workDir);
    }

    /**
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String[] cmd, String[] env, InputStream in, OutputStream out) throws IOException {
        return launch(cmd, env, in, out, null);
    }

    /**
     * Launch a command with optional censoring of arguments from the listener (Note: <strong>The censored portions will
     * remain visible through /proc, pargs, process explorer, etc. i.e. people logged in on the same machine</strong>
     * This version of the launch command just ensures that it is not visible from a build log which is exposed via the
     * web)
     *
     * @param cmd     The command and all it's arguments.
     * @param mask    Which of the command and arguments should be masked from the listener
     * @param env     Environment variable overrides.
     * @param out     stdout and stderr of the process will be sent to this stream. the stream won't be closed.
     * @param workDir null if the working directory could be anything.
     * @return The process of the command.
     * @throws IOException When there are IO problems.
     *
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String[] cmd, boolean[] mask, String[] env, OutputStream out, FilePath workDir) throws IOException {
        return launch(cmd, mask, env, null, out, workDir);
    }

    /**
     * Launch a command with optional censoring of arguments from the listener (Note: <strong>The censored portions will
     * remain visible through /proc, pargs, process explorer, etc. i.e. people logged in on the same machine</strong>
     * This version of the launch command just ensures that it is not visible from a build log which is exposed via the
     * web)
     *
     * @param cmd     The command and all it's arguments.
     * @param mask    Which of the command and arguments should be masked from the listener
     * @param env     Environment variable overrides.
     * @param in      null if there's no input.
     * @param out     stdout and stderr of the process will be sent to this stream. the stream won't be closed.
     * @return The process of the command.
     * @throws IOException When there are IO problems.
     *
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public final Proc launch(String[] cmd, boolean[] mask, String[] env, InputStream in, OutputStream out) throws IOException {
        return launch(cmd, mask, env, in, out, null);
    }

    /**
     * @param env
     *      Environment variable overrides.
     * @param in
     *      null if there's no input.
     * @param workDir
     *      null if the working directory could be anything.
     * @param out
     *      stdout and stderr of the process will be sent to this stream.
     *      the stream won't be closed.
     *
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public Proc launch(String[] cmd, String[] env, InputStream in, OutputStream out, FilePath workDir) throws IOException {
        return launch(launch().cmds(cmd).envs(env).stdin(in).stdout(out).pwd(workDir));
    }

    /**
     * Launch a command with optional censoring of arguments from the listener (Note: <strong>The censored portions will
     * remain visible through /proc, pargs, process explorer, etc. i.e. people logged in on the same machine</strong>
     * This version of the launch command just ensures that it is not visible from a build log which is exposed via the
     * web)
     *
     * @param cmd     The command and all it's arguments.
     * @param mask    Which of the command and arguments should be masked from the listener
     * @param env     Environment variable overrides.
     * @param in      null if there's no input.
     * @param out     stdout and stderr of the process will be sent to this stream. the stream won't be closed.
     * @param workDir null if the working directory could be anything.
     * @return The process of the command.
     * @throws IOException When there are IO problems.
     *
     * @deprecated as of 1.311
     *      Use {@link #launch()} and its associated builder pattern
     */
    @Deprecated
    public Proc launch(String[] cmd, boolean[] mask, String[] env, InputStream in, OutputStream out, FilePath workDir) throws IOException {
        return launch(launch().cmds(cmd).masks(mask).envs(env).stdin(in).stdout(out).pwd(workDir));
    }

    /**
     * Primarily invoked from {@link ProcStarter#start()} to start a process with a specific launcher.
     */
    public abstract Proc launch(@NonNull ProcStarter starter) throws IOException;

    /**
     * Launches a specified process and connects its input/output to a {@link Channel}, then
     * return it.
     *
     * <p>
     * When the returned channel is terminated, the process will be killed.
     *
     * @param cmd
     *      The commands.
     * @param out
     *      Where the stderr from the launched process will be sent.
     * @param workDir
     *      The working directory of the new process, or {@code null} to inherit
     *      from the current process
     * @param envVars
     *      Environment variable overrides. In addition to what the current process
     *      is inherited (if this is going to be launched from an agent, that
     *      becomes the "current" process), these variables will be also set.
     */
    public abstract Channel launchChannel(@NonNull String[] cmd, @NonNull OutputStream out,
            @CheckForNull FilePath workDir, @NonNull Map<String, String> envVars) throws IOException, InterruptedException;

    /**
     * Returns true if this {@link Launcher} is going to launch on Unix.
     */
    public boolean isUnix() {
        return File.pathSeparatorChar == ':';
    }

    /**
     * Calls {@link ProcessTree#killAll(Map)} to kill processes.
     */
    public abstract void kill(Map<String, String> modelEnvVars) throws IOException, InterruptedException;

    /**
     * Prints out the command line to the listener so that users know what we are doing.
     */
    protected final void printCommandLine(@NonNull String[] cmd, @CheckForNull FilePath workDir) {
        StringBuilder buf = new StringBuilder();
        if (workDir != null) {
            buf.append('[');
            if (showFullPath)
                buf.append(workDir.getRemote());
            else
                buf.append(workDir.getRemote().replaceFirst("^.+[/\\\\]", ""));
            buf.append("] ");
        }
        buf.append('$');
        for (String c : cmd) {
            buf.append(' ');
            if (c.indexOf(' ') >= 0) {
                if (c.indexOf('"') >= 0)
                    buf.append('\'').append(c).append('\'');
                else
                    buf.append('"').append(c).append('"');
            } else
                buf.append(c);
        }
        listener.getLogger().println(buf);
        listener.getLogger().flush();
    }

    /**
     * Prints out the command line to the listener with some portions masked to prevent sensitive information from being
     * recorded on the listener.
     *
     * @param cmd     The commands
     * @param mask    An array of booleans which control whether a cmd element should be masked ({@code true}) or
     *                remain unmasked ({@code false}).
     * @param workDir The work dir.
     */
    protected final void maskedPrintCommandLine(@NonNull List<String> cmd, @CheckForNull boolean[] mask, @CheckForNull FilePath workDir) {
        if (mask == null) {
            printCommandLine(cmd.toArray(new String[0]), workDir);
            return;
        }

        assert mask.length == cmd.size();
        final String[] masked = new String[cmd.size()];
        for (int i = 0; i < cmd.size(); i++) {
            if (mask[i]) {
                masked[i] = "********";
            } else {
                masked[i] = cmd.get(i);
            }
        }
        printCommandLine(masked, workDir);
    }

    protected final void maskedPrintCommandLine(@NonNull String[] cmd, @NonNull boolean[] mask, @CheckForNull FilePath workDir) {
        maskedPrintCommandLine(Arrays.asList(cmd), mask, workDir);
    }

    /**
     * Returns a decorated {@link Launcher} for the given node.
     *
     * @param node Node for which this launcher is created.
     * @return Decorated instance of the Launcher.
     */
    @NonNull
    public final Launcher decorateFor(@NonNull Node node) {
        Launcher l = this;
        for (LauncherDecorator d : LauncherDecorator.all())
            l = d.decorate(l, node);
        return l;
    }

    /**
     * Returns a decorated {@link Launcher} that puts the given set of arguments as a prefix to any commands
     * that it invokes.
     *
     * @param prefix Prefixes to be appended
     * @since 1.299
     */
    @NonNull
    public final Launcher decorateByPrefix(final String... prefix) {
        final Launcher outer = this;
        return new Launcher(outer) {
            @Override
            public boolean isUnix() {
                return outer.isUnix();
            }

            @Override
            public Proc launch(ProcStarter starter) throws IOException {
                starter.commands.addAll(0, Arrays.asList(prefix));
                boolean[] masks = starter.masks;
                if (masks != null) {
                    starter.masks = prefix(masks);
                }
                return outer.launch(starter);
            }

            @Override
            public Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map<String, String> envVars) throws IOException, InterruptedException {
                return outer.launchChannel(prefix(cmd), out, workDir, envVars);
            }

            @Override
            public void kill(Map<String, String> modelEnvVars) throws IOException, InterruptedException {
                outer.kill(modelEnvVars);
            }

            private String[] prefix(@NonNull String[] args) {
                String[] newArgs = new String[args.length + prefix.length];
                System.arraycopy(prefix, 0, newArgs, 0, prefix.length);
                System.arraycopy(args, 0, newArgs, prefix.length, args.length);
                return newArgs;
            }

            private boolean[] prefix(@NonNull boolean[] args) {
                boolean[] newArgs = new boolean[args.length + prefix.length];
                System.arraycopy(args, 0, newArgs, prefix.length, args.length);
                return newArgs;
            }
        };
    }

    /**
     * Returns a decorated {@link Launcher} that automatically adds the specified environment
     * variables.
     *
     * Those that are specified in {@link ProcStarter#envs(String...)} will take precedence over
     * what's specified here.
     *
     * @since 1.489
     */
    @NonNull
    public final Launcher decorateByEnv(@NonNull EnvVars _env) {
        final EnvVars env = new EnvVars(_env);
        final Launcher outer = this;
        return new Launcher(outer) {
            @Override
            public boolean isUnix() {
                return outer.isUnix();
            }

            @Override
            public Proc launch(ProcStarter starter) throws IOException {
                EnvVars e = new EnvVars(env);
                if (starter.envs != null) {
                    for (String env : starter.envs) {
                        e.addLine(env);
                    }
                }
                starter.envs = Util.mapToEnv(e);
                return outer.launch(starter);
            }

            @Override
            public Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map<String, String> envVars) throws IOException, InterruptedException {
                EnvVars e = new EnvVars(env);
                e.putAll(envVars);
                return outer.launchChannel(cmd, out, workDir, e);
            }

            @Override
            public void kill(Map<String, String> modelEnvVars) throws IOException, InterruptedException {
                outer.kill(modelEnvVars);
            }
        };
    }

    /**
     * {@link Launcher} that launches process locally.
     */
    public static class LocalLauncher extends Launcher {
        public LocalLauncher(@NonNull TaskListener listener) {
            this(listener, FilePath.localChannel);
        }

        public LocalLauncher(TaskListener listener, VirtualChannel channel) {
            super(listener, channel);
        }

        @Override
        public Proc launch(ProcStarter ps) throws IOException {
            if (!ps.quiet) {
                maskedPrintCommandLine(ps.commands, ps.masks, ps.pwd);
            }

            EnvVars jobEnv = inherit(ps.envs);

            if (envVarsFilterRuleWrapper != null) {
                envVarsFilterRuleWrapper.filter(jobEnv, this, listener);
                // reset the rules to prevent build step without rules configuration to re-use those
                envVarsFilterRuleWrapper = null;
            }

            // replace variables in command line
            String[] jobCmd = new String[ps.commands.size()];
            for (int idx = 0; idx < jobCmd.length; idx++)
                jobCmd[idx] = jobEnv.expand(ps.commands.get(idx));

            return new LocalProc(jobCmd, Util.mapToEnv(jobEnv),
                    ps.reverseStdin ? LocalProc.SELFPUMP_INPUT : ps.stdin,
                    ps.reverseStdout ? LocalProc.SELFPUMP_OUTPUT : ps.stdout,
                    ps.reverseStderr ? LocalProc.SELFPUMP_OUTPUT : ps.stderr,
                    toFile(ps.pwd));
        }

        private File toFile(FilePath f) {
            return f == null ? null : new File(f.getRemote());
        }

        @Override
        @SuppressFBWarnings(value = "COMMAND_INJECTION", justification = "TODO needs triage")
        public Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map<String, String> envVars) throws IOException {
            printCommandLine(cmd, workDir);

            ProcessBuilder pb = new ProcessBuilder(cmd);
            pb.directory(toFile(workDir));
            if (envVars != null) pb.environment().putAll(envVars);

            return launchChannel(out, pb);
        }

        @Override
        public void kill(Map<String, String> modelEnvVars) throws InterruptedException {
            ProcessTree.get().killAll(modelEnvVars);
        }

        /**
         * @param out
         *      Where the stderr from the launched process will be sent.
         */
        public Channel launchChannel(OutputStream out, ProcessBuilder pb) throws IOException {
            final EnvVars cookie = EnvVars.createCookie();
            pb.environment().putAll(cookie);

            final Process proc = pb.start();

            final Thread t2 = new StreamCopyThread(pb.command() + ": stderr copier", proc.getErrorStream(), out);
            t2.start();

            return new Channel("locally launched channel on " + pb.command(),
                Computer.threadPoolForRemoting, proc.getInputStream(), proc.getOutputStream(), out) {

                /**
                 * Kill the process when the channel is severed.
                 */
                @Override
                public synchronized void terminate(IOException e) {
                    super.terminate(e);
                    ProcessTree pt = ProcessTree.get();
                    try {
                        pt.killAll(proc, cookie);
                    } catch (InterruptedException x) {
                        LOGGER.log(Level.INFO, "Interrupted", x);
                    }
                }

                @Override
                public synchronized void close() throws IOException {
                    super.close();
                    // wait for all the output from the process to be picked up
                    try {
                        t2.join();
                    } catch (InterruptedException e) {
                        // process the interrupt later
                        Thread.currentThread().interrupt();
                    }
                }
            };
        }
    }

    @Restricted(NoExternalUse.class)
    public static class DummyLauncher extends Launcher {

        public DummyLauncher(@NonNull TaskListener listener) {
            super(listener, null);
        }

        @Override
        public Proc launch(ProcStarter starter) throws IOException {
            throw new IOException("Can not call launch on a dummy launcher.");
        }

        @Override
        public Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map<String, String> envVars) throws IOException, InterruptedException {
            throw new IOException("Can not call launchChannel on a dummy launcher.");
        }

        @Override
        public void kill(Map<String, String> modelEnvVars) throws IOException, InterruptedException {
            // Kill method should do nothing.
        }
    }


    /**
     * Launches processes remotely by using the given channel.
     */
    public static class RemoteLauncher extends Launcher {
        private final boolean isUnix;

        public RemoteLauncher(@NonNull TaskListener listener, @NonNull VirtualChannel channel, boolean isUnix) {
            super(listener, channel);
            this.isUnix = isUnix;
        }

        @Override
        @NonNull
        public VirtualChannel getChannel() {
            VirtualChannel vc = super.getChannel();
            if (vc == null) {
                throw new IllegalStateException("RemoteLauncher has been initialized with Null channel. It should not happen");
            }
            return super.getChannel();
        }

        @Override
        public Proc launch(ProcStarter ps) throws IOException {
            final OutputStream out = ps.stdout == null || ps.stdoutListener != null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stdout));
            final OutputStream err = ps.stderr == null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stderr));
            final InputStream in = ps.stdin == null || ps.stdin == NULL_INPUT_STREAM ? null : new RemoteInputStream(ps.stdin, false);

            final FilePath psPwd = ps.pwd;
            final String workDir = psPwd == null ? null : psPwd.getRemote();

            try {
                RemoteLaunchCallable remote = new RemoteLaunchCallable(
                        ps.commands,
                        ps.masks,
                        ps.envs,
                        in,
                        ps.reverseStdin,
                        out,
                        ps.reverseStdout,
                        err,
                        ps.reverseStderr,
                        ps.quiet,
                        workDir,
                        listener,
                        ps.stdoutListener,
                        envVarsFilterRuleWrapper);
                // reset the rules to prevent build step without rules configuration to re-use those
                envVarsFilterRuleWrapper = null;
                return new ProcImpl(getChannel().call(remote));
            } catch (InterruptedException e) {
                throw (IOException) new InterruptedIOException().initCause(e);
            }
        }

        @Override
        public Channel launchChannel(String[] cmd, OutputStream err, FilePath _workDir, Map<String, String> envOverrides) throws IOException, InterruptedException {
            printCommandLine(cmd, _workDir);

            Pipe out = Pipe.createRemoteToLocal();
            final String workDir = _workDir == null ? null : _workDir.getRemote();

            OutputStream os = getChannel().call(new RemoteChannelLaunchCallable(cmd, out, err, workDir, envOverrides));

            return new Channel("remotely launched channel on " + channel,
                Computer.threadPoolForRemoting, out.getIn(), new BufferedOutputStream(os));
        }

        @Override
        public boolean isUnix() {
            return isUnix;
        }

        @Override
        public void kill(final Map<String, String> modelEnvVars) throws IOException, InterruptedException {
            getChannel().call(new KillTask(modelEnvVars));
        }

        @Override
        public String toString() {
            return "RemoteLauncher[" + getChannel() + "]";
        }

        private static final class KillTask extends MasterToSlaveCallable<Void, RuntimeException> {
            private final Map<String, String> modelEnvVars;

            KillTask(Map<String, String> modelEnvVars) {
                this.modelEnvVars = modelEnvVars;
            }

            @Override
            public Void call() throws RuntimeException {
                try {
                    ProcessTree.get().killAll(modelEnvVars);
                } catch (InterruptedException e) {
                    // we are asked to terminate early by the caller, so no need to do anything
                }
                return null;
            }

            private static final long serialVersionUID = 1L;
        }

        public static final class ProcImpl extends Proc implements ProcWithJenkins23271Patch {
            private final RemoteProcess process;
            private final IOTriplet io;

            public ProcImpl(RemoteProcess process) {
                this.process = process;
                this.io = process.getIOtriplet();
            }

            @Override
            public void kill() throws IOException, InterruptedException {
                try {
                    process.kill();
                } finally {
                    if (this.isAlive()) { // Should never happen but this forces Proc to not be removed and early GC by escape analysis
                        LOGGER.log(Level.WARNING, "Process {0} has not really finished after the kill() method execution", this);
                    }
                }
            }

            @Override
            public int join() throws IOException, InterruptedException {
                try {
                    final int returnCode = process.join();
                    if (LOGGER.isLoggable(Level.FINER)) {
                        LOGGER.log(Level.FINER, "Process {0} has finished with the return code {1}", new Object[]{this, returnCode});
                    }
                    return returnCode;
                } finally {
                    if (this.isAlive()) { // Should never happen but this forces Proc to not be removed and early GC by escape analysis
                        LOGGER.log(Level.WARNING, "Process {0} has not really finished after the join() method completion", this);
                    }
                }
            }

            @Override
            public boolean isAlive() throws IOException, InterruptedException {
                return process.isAlive();
            }

            @Override
            public InputStream getStdout() {
                return io.stdout;
            }

            @Override
            public InputStream getStderr() {
                return io.stderr;
            }

            @Override
            public OutputStream getStdin() {
                return io.stdin;
            }
        }
    }

    /**
     * A launcher which delegates to a provided inner launcher.
     * Allows subclasses to only implement methods they want to override.
     * Originally, this launcher has been implemented in
     * <a href="https://plugins.jenkins.io/custom-tools-plugin">
     * Custom Tools Plugin</a>.
     *
     * @author rcampbell
     * @author Oleg Nenashev, Synopsys Inc.
     * @since 1.568
     */
    public static class DecoratedLauncher extends Launcher {

        private final Launcher inner;

        public DecoratedLauncher(@NonNull Launcher inner) {
            super(inner);
            this.inner = inner;
        }

        @Override
        public Proc launch(ProcStarter starter) throws IOException {
            return inner.launch(starter);
        }

        @Override
        public Channel launchChannel(String[] cmd, OutputStream out,
                FilePath workDir, Map<String, String> envVars) throws IOException,
                InterruptedException {
            return inner.launchChannel(cmd, out, workDir, envVars);
        }

        @Override
        public void kill(Map<String, String> modelEnvVars) throws IOException,
                InterruptedException {
            inner.kill(modelEnvVars);
        }

        @Override
        public boolean isUnix() {
            return inner.isUnix();
        }

        @Override
        public Proc launch(String[] cmd, boolean[] mask, String[] env, InputStream in, OutputStream out, FilePath workDir) throws IOException {
            return inner.launch(cmd, mask, env, in, out, workDir);
        }

        @Override
        public Computer getComputer() {
            return inner.getComputer();
        }

        @Override
        public TaskListener getListener() {
            return inner.getListener();
        }

        @Override
        public String toString() {
            return super.toString() + "; decorates " + inner.toString();
        }

        @Override
        public VirtualChannel getChannel() {
            return inner.getChannel();
        }

        @Override
        public Proc launch(String[] cmd, String[] env, InputStream in, OutputStream out, FilePath workDir) throws IOException {
            return inner.launch(cmd, env, in, out, workDir);
        }

        /**
         * Gets nested launcher.
         * @return Inner launcher
         */
        @NonNull
        public Launcher getInner() {
            return inner;
        }
    }

    public static class IOTriplet implements Serializable {
        @CheckForNull
        InputStream stdout, stderr;
        @CheckForNull
        OutputStream stdin;
        private static final long serialVersionUID = 1L;
    }
    /**
     * Remoting interface of a remote process
     */

    public interface RemoteProcess {
        int join() throws InterruptedException, IOException;

        void kill() throws IOException, InterruptedException;

        boolean isAlive() throws IOException, InterruptedException;

        @NonNull
        IOTriplet getIOtriplet();
    }

    private record RemoteLaunchCallable(@NonNull List<String> cmd, @CheckForNull boolean[] masks, @CheckForNull String[] env,
                @CheckForNull InputStream in, boolean reverseStdin,
                @CheckForNull OutputStream out, boolean reverseStdout,
                @CheckForNull OutputStream err, boolean reverseStderr,
                boolean quiet, @CheckForNull String workDir,
                @NonNull TaskListener listener, @CheckForNull TaskListener stdoutListener,
                @CheckForNull EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper) implements ControllerToAgentCallable<RemoteProcess, IOException> {
        @Override
        public RemoteProcess call() throws IOException {
            final Channel channel = getOpenChannelOrFail();
            LocalLauncher localLauncher = new LocalLauncher(listener);
            localLauncher.setEnvVarsFilterRuleWrapper(envVarsFilterRuleWrapper);

            Launcher.ProcStarter ps = localLauncher.launch();
            ps.cmds(cmd).masks(masks).envs(env).stdin(in).stderr(err).quiet(quiet);
            if (stdoutListener != null) {
                ps.stdout(stdoutListener.getLogger());
            } else {
                ps.stdout(out);
            }
            if (workDir != null)   ps.pwd(workDir);
            if (reverseStdin)   ps.writeStdin();
            if (reverseStdout)  ps.readStdout();
            if (reverseStderr)  ps.readStderr();

            final Proc p = ps.start();

            return channel.export(RemoteProcess.class, new RemoteProcess() {
                @Override
                public int join() throws InterruptedException, IOException {
                    try {
                        return p.join();
                    } finally {
                        // make sure I/O is delivered to the remote before we return
                        Channel taskChannel = null;
                        try {
                            // Sync IO will fail automatically if the channel is being closed, no need to use getOpenChannelOrFail()
                            taskChannel = Channel.currentOrFail();
                            taskChannel.syncIO();
                        } catch (Throwable t) {
                            // this includes a failure to sync, agent.jar too old, etc
                            LOGGER.log(Level.INFO, "Failed to synchronize IO streams on the channel " + taskChannel, t);
                        }
                    }
                }

                @Override
                public void kill() throws IOException, InterruptedException {
                    p.kill();
                }

                @Override
                public boolean isAlive() throws IOException, InterruptedException {
                    return p.isAlive();
                }

                @Override
                public IOTriplet getIOtriplet() {
                    IOTriplet r = new IOTriplet();
                    if (reverseStdout)  r.stdout = new RemoteInputStream(p.getStdout());
                    if (reverseStderr)  r.stderr = new RemoteInputStream(p.getStderr());
                    if (reverseStdin)   r.stdin  = new RemoteOutputStream(p.getStdin());
                    return r;
                }
            });
        }
    }

    private static class RemoteChannelLaunchCallable extends MasterToSlaveCallable<OutputStream, IOException> {
        @NonNull
        private final String[] cmd;
        @NonNull
        private final Pipe out;
        @CheckForNull
        private final String workDir;
        @NonNull
        private final OutputStream err;
        @NonNull
        private final Map<String, String> envOverrides;

        RemoteChannelLaunchCallable(@NonNull String[] cmd, @NonNull Pipe out, @NonNull OutputStream err,
                @CheckForNull String workDir, @NonNull Map<String, String> envOverrides) {
            this.cmd = cmd;
            this.out = out;
            this.err = new RemoteOutputStream(err);
            this.workDir = workDir;
            this.envOverrides = envOverrides;
        }

        @SuppressFBWarnings(value = "COMMAND_INJECTION", justification = "TODO needs triage")
        private Process launchProcess() throws IOException {
            return Runtime.getRuntime()
                    .exec(cmd, Util.mapToEnv(inherit(envOverrides)), workDir == null ? null : new File(workDir));
        }

        @Override
        public OutputStream call() throws IOException {
            Process p = launchProcess();

            List<String> cmdLines = Arrays.asList(cmd);
            new StreamCopyThread("stdin copier for remote agent on " + cmdLines,
                p.getInputStream(), out.getOut()).start();
            new StreamCopyThread("stderr copier for remote agent on " + cmdLines,
                p.getErrorStream(), err).start();

            // TODO: don't we need to join?

            return new RemoteOutputStream(p.getOutputStream());
        }

        private static final long serialVersionUID = 1L;
    }

    /**
     * Expands the list of environment variables by inheriting current env variables.
     */
    private static EnvVars inherit(@CheckForNull String[] env) {
        // convert String[] to Map first
        EnvVars m = new EnvVars();
        if (env != null) {
            for (String e : env) {
                int index = e.indexOf('=');
                m.put(e.substring(0, index), e.substring(index + 1));
            }
        }
        // then do the inheritance
        return inherit(m);
    }

    /**
     * Expands the list of environment variables by inheriting current env variables.
     */
    private static EnvVars inherit(@NonNull Map<String, String> overrides) {
        EnvVars m = new EnvVars(EnvVars.masterEnvVars);
        m.overrideExpandingAll(overrides);
        return m;
    }

    /**
     * Debug option to display full current path instead of just the last token.
     */
    @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for debugging")
    public static boolean showFullPath = false;

    private static final InputStream NULL_INPUT_STREAM = InputStream.nullInputStream();

    private static final Logger LOGGER = Logger.getLogger(Launcher.class.getName());
}
