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
constitute a failure.
Use the `failOnExit` function to specify whether a command non-zero exit status constitutes a failure.
```java
@BuildCommand
@ -38,28 +37,14 @@ public void startServer() throws Exception {
new ExecOperation()
.fromProject(this)
.command(cmds)
.fail(ExecFail.STDERR)
.failOneExit(false)
.execute();
}
```
The following predefined values are available:
## Work Directory
| Name | Failure When |
|:------------------|:-----------------------------------------------------------------|
| `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:
You can also specify the work directory:
```java
@BuildCommand

View file

@ -35,7 +35,7 @@ public class ExecOperationBuild extends Project {
public ExecOperationBuild() {
pkg = "rife.bld.extension";
name = "ExecOperation";
version = version(0, 9, 3);
version = version(1, 0, 0);
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.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -36,7 +37,7 @@ import java.util.logging.Logger;
public class ExecOperation extends AbstractOperation<ExecOperation> {
private static final Logger LOGGER = Logger.getLogger(ExecOperation.class.getName());
private final List<String> args_ = new ArrayList<>();
private final Set<ExecFail> fail_ = new HashSet<>();
private boolean failOnExit_ = true;
private BaseProject project_;
private int timeout = 30;
private String workDir_;
@ -80,8 +81,6 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
LOGGER.severe("A project must be specified.");
}
var errorMessage = new StringBuilder(27);
final File workDir;
if (workDir_ == null || workDir_.isBlank()) {
workDir = new File(project_.workDirectory().getAbsolutePath());
@ -91,6 +90,7 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
if (workDir.isDirectory()) {
var pb = new ProcessBuilder();
pb.inheritIO();
pb.command(args_);
pb.directory(workDir);
@ -100,63 +100,28 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
var proc = pb.start();
var err = proc.waitFor(timeout, TimeUnit.SECONDS);
var stdout = readStream(proc.getInputStream());
var stderr = readStream(proc.getErrorStream());
if (!err) {
errorMessage.append("TIMEOUT");
} else if (!fail_.contains(ExecFail.NONE)) {
var all = fail_.contains(ExecFail.ALL);
var output = fail_.contains(ExecFail.OUTPUT);
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);
}
proc.destroy();
throw new IOException("The command timed out.");
} else if (proc.exitValue() != 0 && failOnExit_) {
throw new IOException("The command exit status is: " + proc.exitValue());
}
} else {
errorMessage.append("Invalid working directory: ").append(workDir.getCanonicalPath());
}
if (!errorMessage.isEmpty()) {
throw new IOException(errorMessage.toString());
throw new IOException("Invalid work directory: " + workDir);
}
}
/**
* Configure the failure mode.
* Configures whether the operation should fail if the command exit status is not 0.
* <p>
* The failure modes are:
* <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>
* Default is {@code TRUE}
*
* @param fail one or more failure modes
* @return this operation instance
* @see ExecFail
* @param failOnExit The fail on exit toggle
* @return this operation instance.
*/
public ExecOperation fail(ExecFail... fail) {
fail_.addAll(Set.of(fail));
public ExecOperation failOnExit(boolean failOnExit) {
failOnExit_ = failOnExit;
return this;
}
@ -171,16 +136,6 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
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.
*
@ -193,7 +148,7 @@ public class ExecOperation extends AbstractOperation<ExecOperation> {
}
/**
* Configures the working directory.
* Configures the work directory.
*
* @param dir the directory path
* @return this operation instance

View file

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