Skip to content

Commit

Permalink
Display command output asynchronously.
Browse files Browse the repository at this point in the history
Replaced fail() function with failOnExit().
  • Loading branch information
ethauvin committed Apr 5, 2024
1 parent 8b80ca1 commit e7d3060
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 170 deletions.
25 changes: 5 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/bld/java/rife/bld/extension/ExecOperationBuild.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
27 changes: 0 additions & 27 deletions src/main/java/rife/bld/extension/ExecFail.java

This file was deleted.

79 changes: 17 additions & 62 deletions src/main/java/rife/bld/extension/ExecOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@

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;
Expand All @@ -36,7 +37,7 @@
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_;
Expand Down Expand Up @@ -80,8 +81,6 @@ public void execute() throws Exception {
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());
Expand All @@ -91,6 +90,7 @@ public void execute() throws Exception {

if (workDir.isDirectory()) {
var pb = new ProcessBuilder();
pb.inheritIO();
pb.command(args_);
pb.directory(workDir);

Expand All @@ -100,63 +100,28 @@ public void execute() throws Exception {

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;
}

Expand All @@ -171,16 +136,6 @@ public ExecOperation fromProject(BaseProject project) {
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.
*
Expand All @@ -193,7 +148,7 @@ public ExecOperation timeout(int timeout) {
}

/**
* Configures the working directory.
* Configures the work directory.
*
* @param dir the directory path
* @return this operation instance
Expand Down
72 changes: 12 additions & 60 deletions src/test/java/rife/bld/extension/ExecOperationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@
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;
Expand All @@ -31,18 +29,6 @@

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 {
Expand All @@ -52,22 +38,11 @@ void testCat() throws Exception {
.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(() ->
Expand All @@ -78,54 +53,32 @@ void testException() {
}

@Test
void testExit() {
void testExitStatus() {
assertThatCode(() ->
new ExecOperation()
.fromProject(new BaseProject())
.command("tail", FOO)
.fail(ExecFail.EXIT)
.execute()).message().startsWith("EXIT ");
}

@Test
void testNone() {
assertThatCode(() ->
new ExecOperation()
.fromProject(new WebProject())
.command("cat", FOO)
.fail(ExecFail.NONE)
.execute()).doesNotThrowAnyException();
}

@Test
void testOutput() {
assertThatCode(() ->
new ExecOperation()
.fromProject(new WebProject())
.command("echo")
.fail(ExecFail.OUTPUT)
.execute()
).message().isEqualTo("STDOUT -> ");
.command(List.of("cat", FOO))
.execute()).message().contains("exit status");
}

@Test
void testStdErr() {
void testFailOnExit() {
assertThatCode(() ->
new ExecOperation()
.fromProject(new BaseProject())
.command("logger", "-s", HELLO)
.fail(ExecFail.STDERR)
.execute()).message().startsWith("STDERR -> ").endsWith(HELLO);
.command(List.of("cat", FOO))
.failOnExit(false)
.execute()).doesNotThrowAnyException();
}

@Test
void testStdOut() {
void testTimeout() {
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
Expand All @@ -135,7 +88,6 @@ void testWorkDir() {
.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);
}
}

0 comments on commit e7d3060

Please sign in to comment.