001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2025-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.common.exec;
019
020import static java.util.Objects.requireNonNull;
021
022import com.google.common.base.Supplier;
023import com.google.common.collect.ImmutableList;
024
025import dev.enola.common.linereader.ExecutionContext;
026
027import java.nio.file.Path;
028
029/**
030 * ProcessRequest describes a request for the Operating System to run a program from an image in an
031 * executable file.
032 *
033 * <p>This is not a {@link Process}, but what is needed to create one process (or several).
034 *
035 * <p>{@link ProcessBuilder} is similar to this. But that's tied to the JVM's builtin process
036 * launcher - while this may use other means to execute the command. This intentionally has no
037 * start() method, because it is passed to a {@link ProcessLauncher} to actually run it.
038 *
039 * @param directory the working directory for the process
040 * @param command the command to execute
041 * @param ctx the {@link ExecutionContext}
042 * @param mergeErrIntoOut whether to merge the error stream into the output stream
043 */
044public record ProcessRequest(
045        Path directory,
046        ImmutableList<String> command,
047        Supplier<ExecutionContext> ctx,
048        boolean mergeErrIntoOut) {
049
050    // TODO ExecutionContext: Flip InputStream/OutputStream? To avoid unnecessary pumping!
051
052    // TODO Use a "Handler", with ByteBuffer; like
053    // https://brettwooldridge.github.io/NuProcess/apidocs/com/zaxxer/nuprocess/NuProcessHandler.html
054
055    // TODO Integration with common.io.Resource; https://docs.enola.dev/use/fetch/#exec
056
057    public ProcessRequest {
058        requireNonNull(directory, "directory");
059        requireNonNull(command, "command");
060        requireNonNull(ctx, "ctx");
061        if (command.isEmpty()) throw new IllegalArgumentException("command is empty");
062    }
063
064    public static class Builder {
065        private Path directory;
066        private final ImmutableList.Builder<String> command = new ImmutableList.Builder<>();
067        private Supplier<ExecutionContext> ctx;
068        private boolean mergeErrIntoOut = false;
069
070        public Builder directory(Path directory) {
071            if (this.directory != null) throw new IllegalStateException("directory already set");
072            this.directory = requireNonNull(directory, "directory");
073            return this;
074        }
075
076        public Builder command(String... command) {
077            this.command.add(command);
078            return this;
079        }
080
081        public Builder executionContext(Supplier<ExecutionContext> ctx) {
082            if (this.ctx != null) throw new IllegalStateException("ctx already set");
083            this.ctx = requireNonNull(ctx, "ctx");
084            return this;
085        }
086
087        public Builder mergeErrIntoOut(boolean mergeErrIntoOut) {
088            this.mergeErrIntoOut = mergeErrIntoOut;
089            return this;
090        }
091
092        public Builder mergeErrIntoOut() {
093            return mergeErrIntoOut(true);
094        }
095
096        public ProcessRequest build() {
097            return new ProcessRequest(directory, command.build(), ctx, mergeErrIntoOut);
098        }
099    }
100}