Skip to content

Commit 504eda8

Browse files
committed
Delegate ExecOperation to ProcessExecutor, add outputConsumer support
1 parent fa7c376 commit 504eda8

4 files changed

Lines changed: 205 additions & 138 deletions

File tree

src/bld/java/rife/bld/extension/ExecOperationBuild.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ public ExecOperationBuild() {
4141
downloadSources = true;
4242
autoDownloadPurge = true;
4343

44-
repositories = List.of(MAVEN_CENTRAL, CENTRAL_SNAPSHOTS, RIFE2_RELEASES);
44+
repositories = List.of(MAVEN_CENTRAL, CENTRAL_SNAPSHOTS, RIFE2_RELEASES, RIFE2_SNAPSHOTS);
4545

4646
var junit = version(6, 0, 3);
4747
scope(compile)
4848
.include(dependency("com.uwyn.rife2", "bld-extensions-tools",
49-
version(1, 2, 0)))
49+
version(1, 3, 0, "SNAPSHOT")))
5050
.include(dependency("com.uwyn.rife2", "bld",
5151
version(2, 3, 1, "SNAPSHOT")));
5252
scope(provided)

src/main/java/rife/bld/extension/ExecOperation.java

Lines changed: 52 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@
2121
import rife.bld.BaseProject;
2222
import rife.bld.extension.tools.IOTools;
2323
import rife.bld.extension.tools.ObjectTools;
24+
import rife.bld.extension.tools.ProcessExecutor;
2425
import rife.bld.extension.tools.SystemTools;
2526
import rife.bld.operations.AbstractOperation;
2627
import rife.bld.operations.exceptions.ExitStatusException;
2728

28-
import java.io.*;
29-
import java.nio.charset.StandardCharsets;
29+
import java.io.File;
30+
import java.io.IOException;
3031
import java.nio.file.Path;
3132
import java.util.*;
32-
import java.util.concurrent.TimeUnit;
33+
import java.util.function.Consumer;
3334
import java.util.logging.Level;
3435
import java.util.logging.Logger;
3536

@@ -39,42 +40,55 @@
3940
* @author <a href="https://erik.thauvin.net/">Erik C. Thauvin</a>
4041
* @since 1.0
4142
*/
42-
@SuppressWarnings("PMD.DoNotUseThreads")
4343
@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "intentional and documented")
4444
public class ExecOperation extends AbstractOperation<ExecOperation> {
4545

4646
private static final String COMMAND_NOT_VALID = "command values must not be null or empty";
4747
private static final Logger logger = Logger.getLogger(ExecOperation.class.getName());
48+
private static final Consumer<String> DEFAULT_OUTPUT_CONSUMER = logger::info;
4849
private final List<String> args_ = new ArrayList<>();
4950
private final Map<String, String> env_ = new HashMap<>();
5051
private boolean failOnExit_ = true;
5152
private boolean inheritIO_ = true;
52-
private int timeout_ = 30;
53+
@NonNull
54+
private Consumer<String> outputConsumer_ = DEFAULT_OUTPUT_CONSUMER;
55+
private int timeout_ = ProcessExecutor.DEFAULT_TIMEOUT_SECONDS;
5356
private File workDir_;
5457

5558
@Override
56-
@SuppressWarnings({"PMD.CloseResource", "PMD.PreserveStackTrace"})
59+
@SuppressWarnings({"PMD.PreserveStackTrace"})
5760
@SuppressFBWarnings("LEST_LOST_EXCEPTION_STACK_TRACE")
5861
public void execute() throws Exception {
5962
validatePreconditions();
6063
logExecutionStart();
6164

62-
var pb = createProcessBuilder();
63-
Process proc = null;
64-
Thread outputThread = null;
65-
6665
try {
67-
proc = pb.start();
68-
outputThread = startOutputReader(proc);
69-
waitForCompletion(proc);
70-
handleExitCode(proc);
71-
} catch (IOException e) {
66+
var executor = new ProcessExecutor()
67+
.command(args_)
68+
.workDir(workDir_)
69+
.timeout(timeout_)
70+
.inheritIO(inheritIO_);
71+
72+
if (!env_.isEmpty()) {
73+
executor.env(env_);
74+
}
75+
76+
if (!inheritIO_) {
77+
executor.outputConsumer(outputConsumer_);
78+
}
79+
80+
var result = executor.execute();
81+
handleExitCode(result.exitCode());
82+
83+
if (result.timedOut() && logger.isLoggable(Level.SEVERE) && !silent()) {
84+
logger.severe("The command timed out after " + timeout_ + " seconds.");
85+
throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
86+
}
87+
} catch (IOException | InterruptedException e) {
7288
if (logger.isLoggable(Level.SEVERE) && !silent()) {
7389
logger.log(Level.SEVERE, "Failed to execute command.", e);
7490
}
7591
throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
76-
} finally {
77-
cleanup(proc, outputThread);
7892
}
7993
}
8094

@@ -457,8 +471,8 @@ public ExecOperation onUnix(@NonNull Collection<String> args) {
457471
* <pre>{@code
458472
* new ExecOperation()
459473
* .fromProject(this)
460-
* .isWindows("cmd", "/c", "build.bat")
461-
* .isUnix("./build.sh")
474+
* .onWindows("cmd", "/c", "build.bat")
475+
* .onUnix("./build.sh")
462476
* .execute();
463477
* }</pre>
464478
* <p>
@@ -498,6 +512,21 @@ public ExecOperation onWindows(@NonNull Collection<String> args) {
498512
return this;
499513
}
500514

515+
/**
516+
* Sets a consumer to receive output lines when not inheriting I/O.
517+
* <p>
518+
* Only called when {@link #isInheritIO()} is {@code false}. Default logs at INFO level.
519+
*
520+
* @param outputConsumer the output consumer, must not be null
521+
* @return this operation instance
522+
* @throws NullPointerException if outputConsumer is null
523+
*/
524+
public ExecOperation outputConsumer(@NonNull Consumer<String> outputConsumer) {
525+
Objects.requireNonNull(outputConsumer, "outputConsumer must not be null");
526+
outputConsumer_ = outputConsumer;
527+
return this;
528+
}
529+
501530
/**
502531
* Configure the command timeout.
503532
*
@@ -570,54 +599,7 @@ public File workDir() {
570599
return workDir_;
571600
}
572601

573-
private void cleanup(Process proc, Thread outputThread) {
574-
if (proc != null) {
575-
if (proc.isAlive()) {
576-
proc.destroyForcibly();
577-
}
578-
closeQuietly(proc.getInputStream());
579-
closeQuietly(proc.getErrorStream());
580-
closeQuietly(proc.getOutputStream());
581-
}
582-
if (outputThread != null) {
583-
outputThread.interrupt();
584-
try {
585-
outputThread.join(1000);
586-
} catch (InterruptedException ignored) {
587-
Thread.currentThread().interrupt();
588-
}
589-
}
590-
}
591-
592-
private void closeQuietly(Closeable closeable) {
593-
try {
594-
closeable.close();
595-
} catch (IOException ignored) {
596-
}
597-
}
598-
599-
@SuppressFBWarnings("COMMAND_INJECTION")
600-
private ProcessBuilder createProcessBuilder() {
601-
var pb = new ProcessBuilder();
602-
pb.command(args_);
603-
pb.directory(workDir_);
604-
605-
if (!env_.isEmpty()) {
606-
pb.environment().putAll(env_);
607-
}
608-
609-
if (inheritIO_) {
610-
pb.inheritIO();
611-
} else {
612-
pb.redirectErrorStream(true);
613-
var devNull = SystemTools.isWindows() ? "NUL" : "/dev/null";
614-
pb.redirectInput(ProcessBuilder.Redirect.from(new File(devNull)));
615-
}
616-
return pb;
617-
}
618-
619-
private void handleExitCode(Process proc) throws ExitStatusException {
620-
int exitCode = proc.exitValue();
602+
private void handleExitCode(int exitCode) throws ExitStatusException {
621603
if (exitCode != 0 && failOnExit_) {
622604
if (logger.isLoggable(Level.SEVERE) && !silent()) {
623605
logger.log(Level.SEVERE, "The command exit value/status is: " + exitCode);
@@ -628,45 +610,14 @@ private void handleExitCode(Process proc) throws ExitStatusException {
628610

629611
private void logExecutionStart() {
630612
if (logger.isLoggable(Level.INFO) && !silent()) {
631-
logger.log(Level.INFO, "Working directory: " + workDir_.getAbsolutePath());
613+
logger.log(Level.INFO, "Working directory: " + workDir_.getAbsolutePath());
632614
if (!env_.isEmpty()) {
633615
logger.log(Level.INFO, "Environment: " + env_);
634616
}
635617
logger.info(String.join(" ", args_));
636618
}
637619
}
638620

639-
@SuppressFBWarnings("CRLF_INJECTION_LOGS")
640-
private void readProcessOutput(Process proc) {
641-
final var logInfo = logger.isLoggable(Level.INFO) && !silent();
642-
final var logSevere = logger.isLoggable(Level.SEVERE) && !silent();
643-
644-
try (var reader = new BufferedReader(
645-
new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8))) {
646-
String line;
647-
while (!Thread.currentThread().isInterrupted() && (line = reader.readLine()) != null) {
648-
if (logInfo) {
649-
logger.info(line);
650-
}
651-
}
652-
} catch (IOException e) {
653-
if (logSevere && proc.isAlive() && !Thread.currentThread().isInterrupted()) {
654-
logger.log(Level.SEVERE, "Failed to read command output.", e);
655-
}
656-
}
657-
}
658-
659-
private Thread startOutputReader(Process proc) {
660-
if (inheritIO_) {
661-
return null;
662-
}
663-
664-
var thread = new Thread(() -> readProcessOutput(proc));
665-
thread.setDaemon(true);
666-
thread.start();
667-
return thread;
668-
}
669-
670621
private void validatePreconditions() throws ExitStatusException {
671622
final var logSevere = logger.isLoggable(Level.SEVERE) && !silent();
672623

@@ -682,29 +633,9 @@ private void validatePreconditions() throws ExitStatusException {
682633
}
683634
throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
684635
}
685-
}
686-
687-
@SuppressWarnings("PMD.PreserveStackTrace")
688-
@SuppressFBWarnings("LEST_LOST_EXCEPTION_STACK_TRACE")
689-
private void waitForCompletion(Process proc) throws ExitStatusException {
690-
final var logSevere = logger.isLoggable(Level.SEVERE) && !silent();
691-
692-
try {
693-
if (!proc.waitFor(timeout_, TimeUnit.SECONDS)) {
694-
proc.destroyForcibly();
695-
if (!proc.waitFor(5, TimeUnit.SECONDS) && proc.isAlive()) {
696-
if (logSevere) {
697-
logger.severe("Process could not be killed after timeout.");
698-
}
699-
} else if (logSevere) {
700-
logger.severe("The command timed out after " + timeout_ + " seconds.");
701-
}
702-
throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
703-
}
704-
} catch (InterruptedException e) {
705-
Thread.currentThread().interrupt();
636+
if (inheritIO_ && outputConsumer_ != DEFAULT_OUTPUT_CONSUMER) {
706637
if (logSevere) {
707-
logger.log(Level.SEVERE, "The command was interrupted.", e);
638+
logger.severe("Cannot use custom outputConsumer with inheritIO(true).");
708639
}
709640
throw new ExitStatusException(ExitStatusException.EXIT_FAILURE);
710641
}

src/main/java/rife/bld/extension/package-info.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
1817
/**
1918
* Provides a <a href="https://rife2.com/">bld</a> extension to perform command line execution.
2019
*

0 commit comments

Comments
 (0)