Display command output asynchronously.

Replaced fail() function with failOnExit().
This commit is contained in:
Erik C. Thauvin 2024-04-04 18:15:00 -07:00
parent 8b80ca1bc0
commit e7d3060649
Signed by: erik
GPG key ID: 776702A6A2DA330E
5 changed files with 35 additions and 170 deletions

View file

@ -21,10 +21,9 @@ public void startServer() throws Exception {
} }
``` ```
### Failure Modes ## Exit Status
Use the `fail` function to specify whether data returned to the standard streams and/or an abnormal exit value Use the `failOnExit` function to specify whether a command non-zero exit status constitutes a failure.
constitute a failure.
```java ```java
@BuildCommand @BuildCommand
@ -38,28 +37,14 @@ public void startServer() throws Exception {
new ExecOperation() new ExecOperation()
.fromProject(this) .fromProject(this)
.command(cmds) .command(cmds)
.fail(ExecFail.STDERR) .failOneExit(false)
.execute(); .execute();
} }
``` ```
The following predefined values are available: ## Work Directory
| Name | Failure When | You can also specify the work directory:
|:------------------|:-----------------------------------------------------------------|
| `ExecFail.EXIT` | Exit value > 0 |
| `ExecFail.NORMAL` | Exit value > 0 or any data to the standard error stream (stderr) |
| `ExecFail.OUTPUT` | Any data to the standard output stream (stdout) or stderr. |
| `ExecFail.STDERR` | Any data to stderr. |
| `ExecFail.STDOUT` | Any data to stdout. |
| `ExecFail.ALL` | Any of the conditions above. |
| `ExecFail.NONE` | Never fails. |
`ExecFail.NORMAL` is the default value.
## Working Directory
You can also specify the working directory:
```java ```java
@BuildCommand @BuildCommand

View file

@ -35,7 +35,7 @@ public class ExecOperationBuild extends Project {
public ExecOperationBuild() { public ExecOperationBuild() {
pkg = "rife.bld.extension"; pkg = "rife.bld.extension";
name = "ExecOperation"; name = "ExecOperation";
version = version(0, 9, 3); version = version(1, 0, 0);
javaRelease = 17; javaRelease = 17;

View file

@ -1,27 +0,0 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package rife.bld.extension;
/**
* The failure modes enumeration.
*
* @author <a href="https://erik.thauvin.net/">Erik C. Thauvin</a>
* @since 1.0
*/
public enum ExecFail {
ALL, EXIT, NONE, NORMAL, OUTPUT, STDERR, STDOUT
}

View file

@ -21,8 +21,9 @@ import rife.bld.operations.AbstractOperation;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.util.ArrayList;
import java.util.*; import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -36,7 +37,7 @@ import java.util.logging.Logger;
public class ExecOperation extends AbstractOperation<ExecOperation> { public class ExecOperation extends AbstractOperation<ExecOperation> {
private static final Logger LOGGER = Logger.getLogger(ExecOperation.class.getName()); private static final Logger LOGGER = Logger.getLogger(ExecOperation.class.getName());
private final List<String> args_ = new ArrayList<>(); private final List<String> args_ = new ArrayList<>();
private final Set<ExecFail> fail_ = new HashSet<>(); private boolean failOnExit_ = true;
private BaseProject project_; private BaseProject project_;
private int timeout = 30; private int timeout = 30;
private String workDir_; private String workDir_;
@ -80,8 +81,6 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
LOGGER.severe("A project must be specified."); LOGGER.severe("A project must be specified.");
} }
var errorMessage = new StringBuilder(27);
final File workDir; final File workDir;
if (workDir_ == null || workDir_.isBlank()) { if (workDir_ == null || workDir_.isBlank()) {
workDir = new File(project_.workDirectory().getAbsolutePath()); workDir = new File(project_.workDirectory().getAbsolutePath());
@ -91,6 +90,7 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
if (workDir.isDirectory()) { if (workDir.isDirectory()) {
var pb = new ProcessBuilder(); var pb = new ProcessBuilder();
pb.inheritIO();
pb.command(args_); pb.command(args_);
pb.directory(workDir); pb.directory(workDir);
@ -100,63 +100,28 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
var proc = pb.start(); var proc = pb.start();
var err = proc.waitFor(timeout, TimeUnit.SECONDS); var err = proc.waitFor(timeout, TimeUnit.SECONDS);
var stdout = readStream(proc.getInputStream());
var stderr = readStream(proc.getErrorStream());
if (!err) { if (!err) {
errorMessage.append("TIMEOUT"); proc.destroy();
} else if (!fail_.contains(ExecFail.NONE)) { throw new IOException("The command timed out.");
var all = fail_.contains(ExecFail.ALL); } else if (proc.exitValue() != 0 && failOnExit_) {
var output = fail_.contains(ExecFail.OUTPUT); throw new IOException("The command exit status is: " + proc.exitValue());
if ((all || fail_.contains(ExecFail.EXIT) || fail_.contains(ExecFail.NORMAL)) && proc.exitValue() > 0) {
errorMessage.append("EXIT ").append(proc.exitValue());
if (!stderr.isEmpty()) {
errorMessage.append(", STDERR -> ").append(stderr.get(0));
} else if (!stdout.isEmpty()) {
errorMessage.append(", STDOUT -> ").append(stdout.get(0));
}
} else if ((all || output || fail_.contains(ExecFail.STDERR) || fail_.contains(ExecFail.NORMAL))
&& !stderr.isEmpty()) {
errorMessage.append("STDERR -> ").append(stderr.get(0));
} else if ((all || output || fail_.contains(ExecFail.STDOUT)) && !stdout.isEmpty()) {
errorMessage.append("STDOUT -> ").append(stdout.get(0));
}
}
if (LOGGER.isLoggable(Level.INFO) && errorMessage.isEmpty() && !stdout.isEmpty()) {
for (var l : stdout) {
LOGGER.info(l);
}
} }
} else { } else {
errorMessage.append("Invalid working directory: ").append(workDir.getCanonicalPath()); throw new IOException("Invalid work directory: " + workDir);
}
if (!errorMessage.isEmpty()) {
throw new IOException(errorMessage.toString());
} }
} }
/** /**
* Configure the failure mode. * Configures whether the operation should fail if the command exit status is not 0.
* <p> * <p>
* The failure modes are: * Default is {@code TRUE}
* <ul>
* <li>{@link ExecFail#EXIT}<p>Exit value > 0</p></li>
* <li>{@link ExecFail#NORMAL}<p>Exit value > 0 or any data to the standard error stream (stderr)</p></li>
* <li>{@link ExecFail#OUTPUT}<p>Any data to the standard output stream (stdout) or stderr</p></li>
* <li>{@link ExecFail#STDERR}<p>Any data to stderr</p></li>
* <li>{@link ExecFail#STDOUT}<p>Any data to stdout</p></li>
* <li>{@link ExecFail#ALL}<p>Any of the conditions above</p></li>
* <li>{@link ExecFail#NONE}<p>Never fails</p></li>
* </ul>
* *
* @param fail one or more failure modes * @param failOnExit The fail on exit toggle
* @return this operation instance * @return this operation instance.
* @see ExecFail
*/ */
public ExecOperation fail(ExecFail... fail) { public ExecOperation failOnExit(boolean failOnExit) {
fail_.addAll(Set.of(fail)); failOnExit_ = failOnExit;
return this; return this;
} }
@ -171,16 +136,6 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
return this; return this;
} }
private List<String> readStream(InputStream stream) {
var lines = new ArrayList<String>();
try (var scanner = new Scanner(stream)) {
while (scanner.hasNextLine()) {
lines.add(scanner.nextLine());
}
}
return lines;
}
/** /**
* Configure the command timeout. * Configure the command timeout.
* *
@ -193,7 +148,7 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
} }
/** /**
* Configures the working directory. * Configures the work directory.
* *
* @param dir the directory path * @param dir the directory path
* @return this operation instance * @return this operation instance

View file

@ -19,10 +19,8 @@ package rife.bld.extension;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import rife.bld.BaseProject; import rife.bld.BaseProject;
import rife.bld.Project; import rife.bld.Project;
import rife.bld.WebProject;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -31,18 +29,6 @@ import static org.assertj.core.api.Assertions.assertThatCode;
class ExecOperationTest { class ExecOperationTest {
private static final String FOO = "foo"; private static final String FOO = "foo";
private static final String HELLO = "Hello";
@Test
void testAll() {
assertThatCode(() ->
new ExecOperation()
.fromProject(new Project())
.command("date")
.fail(ExecFail.ALL)
.execute()
).isInstanceOf(IOException.class);
}
@Test @Test
void testCat() throws Exception { void testCat() throws Exception {
@ -52,22 +38,11 @@ class ExecOperationTest {
.fromProject(new Project()) .fromProject(new Project())
.timeout(10) .timeout(10)
.command("touch", tmpFile.getName()) .command("touch", tmpFile.getName())
.fail(ExecFail.NORMAL)
.execute(); .execute();
assertThat(tmpFile).exists(); assertThat(tmpFile).exists();
} }
@Test
void testCommandList() {
assertThatCode(() ->
new ExecOperation()
.fromProject(new BaseProject())
.command(List.of("logger", "-s", HELLO))
.fail(ExecFail.STDERR)
.execute()).message().startsWith("STDERR -> ").endsWith(HELLO);
}
@Test @Test
void testException() { void testException() {
assertThatCode(() -> assertThatCode(() ->
@ -78,54 +53,32 @@ class ExecOperationTest {
} }
@Test @Test
void testExit() { void testExitStatus() {
assertThatCode(() -> assertThatCode(() ->
new ExecOperation() new ExecOperation()
.fromProject(new BaseProject()) .fromProject(new BaseProject())
.command("tail", FOO) .command(List.of("cat", FOO))
.fail(ExecFail.EXIT) .execute()).message().contains("exit status");
.execute()).message().startsWith("EXIT ");
} }
@Test @Test
void testNone() { void testFailOnExit() {
assertThatCode(() -> assertThatCode(() ->
new ExecOperation() new ExecOperation()
.fromProject(new WebProject()) .fromProject(new BaseProject())
.command("cat", FOO) .command(List.of("cat", FOO))
.fail(ExecFail.NONE) .failOnExit(false)
.execute()).doesNotThrowAnyException(); .execute()).doesNotThrowAnyException();
} }
@Test @Test
void testOutput() { void testTimeout() {
assertThatCode(() ->
new ExecOperation()
.fromProject(new WebProject())
.command("echo")
.fail(ExecFail.OUTPUT)
.execute()
).message().isEqualTo("STDOUT -> ");
}
@Test
void testStdErr() {
assertThatCode(() -> assertThatCode(() ->
new ExecOperation() new ExecOperation()
.fromProject(new BaseProject()) .fromProject(new BaseProject())
.command("logger", "-s", HELLO) .timeout(5)
.fail(ExecFail.STDERR) .command(List.of("sleep", "10"))
.execute()).message().startsWith("STDERR -> ").endsWith(HELLO); .execute()).message().contains("timed out");
}
@Test
void testStdOut() {
assertThatCode(() ->
new ExecOperation()
.fromProject(new BaseProject())
.command("echo", HELLO)
.fail(ExecFail.STDOUT)
.execute()).message().isEqualTo("STDOUT -> Hello");
} }
@Test @Test
@ -135,7 +88,6 @@ class ExecOperationTest {
.fromProject(new BaseProject()) .fromProject(new BaseProject())
.command("echo") .command("echo")
.workDir(FOO) .workDir(FOO)
.fail(ExecFail.NORMAL) .execute()).message().startsWith("Invalid work directory: ").endsWith(FOO);
.execute()).message().startsWith("Invalid working directory: ").endsWith(FOO);
} }
} }