Skip to content

Commit efce1ba

Browse files
authored
Allow configuring log level (#3)
1 parent adf8579 commit efce1ba

File tree

8 files changed

+165
-29
lines changed

8 files changed

+165
-29
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [0.2.0] - unreleased
7+
## [0.3.0] - unreleased
88

9-
## [0.1.0] - 2025-02-??
9+
## [0.2.0] - 2025-07-05
10+
11+
* [PR #3](https://github.com/itsallcode/simple-process/pull/3): Allow configuring log level
12+
13+
## [0.1.0] - 2025-06-028
1014

1115
* [PR #1](https://github.com/itsallcode/simple-process/pull/1): Initial release

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ plugins {
1111
}
1212

1313
group = 'org.itsallcode'
14-
version = '0.1.0'
14+
version = '0.2.0'
1515

1616
repositories {
1717
mavenCentral()
@@ -110,8 +110,8 @@ publishing {
110110
}
111111
}
112112

113-
114113
signing {
114+
required = { gradle.taskGraph.hasTask("publish") }
115115
def signingKey = findProperty("signingKey")
116116
def signingPassword = findProperty("signingPassword")
117117
useInMemoryPgpKeys(signingKey, signingPassword)

src/main/java/module-info.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
* {@link org.itsallcode.process.SimpleProcessBuilder#start()}.
99
*/
1010

11-
module simple.process {
11+
module org.itsallcode.process {
1212
exports org.itsallcode.process;
1313

14-
requires java.logging;
14+
requires transitive java.logging;
1515
}

src/main/java/org/itsallcode/process/ProcessOutputConsumer.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
import java.util.Collections;
55
import java.util.List;
66
import java.util.concurrent.Executor;
7+
import java.util.logging.Level;
78
import java.util.logging.Logger;
89

910
/**
1011
* Consumes stdout and stderr of a process asynchronously.
1112
*/
12-
class ProcessOutputConsumer<T> {
13+
final class ProcessOutputConsumer<T> {
14+
private static final String STD_ERR_NAME = "stdErr";
15+
private static final String STD_OUT_NAME = "stdOut";
1316
private static final Logger LOG = Logger.getLogger(ProcessOutputConsumer.class.getName());
1417
private final Executor executor;
1518
private final Process process;
@@ -18,7 +21,7 @@ class ProcessOutputConsumer<T> {
1821
private final StreamCollector<T> stdOutCollector;
1922
private final StreamCollector<T> stdErrCollector;
2023

21-
ProcessOutputConsumer(final Executor executor, final Process process,
24+
private ProcessOutputConsumer(final Executor executor, final Process process,
2225
final List<Runnable> consumers, final List<StreamCloseWaiter> streamCloseWaiter,
2326
final StreamCollector<T> stdOutCollector, final StreamCollector<T> stdErrCollector) {
2427
this.executor = executor;
@@ -30,15 +33,17 @@ class ProcessOutputConsumer<T> {
3033
}
3134

3235
static <T> ProcessOutputConsumer<T> create(final Executor executor, final Process process,
33-
final Duration streamCloseTimeout, final StreamCollector<T> stdOutCollector,
36+
final Duration streamCloseTimeout, Level logLevel, final StreamCollector<T> stdOutCollector,
3437
final StreamCollector<T> stdErrCollector) {
3538
final long pid = process.pid();
36-
final StreamCloseWaiter stdOutCloseWaiter = new StreamCloseWaiter("stdOut", pid, streamCloseTimeout);
37-
final StreamCloseWaiter stdErrCloseWaiter = new StreamCloseWaiter("stdErr", pid, streamCloseTimeout);
38-
final AsyncStreamConsumer stdOutConsumer = new AsyncStreamConsumer("stdout", pid, process.getInputStream(),
39-
new DelegatingConsumer(List.of(stdOutCloseWaiter, stdOutCollector)));
40-
final AsyncStreamConsumer stdErrConsumer = new AsyncStreamConsumer("stderr", pid, process.getErrorStream(),
41-
new DelegatingConsumer(List.of(stdErrCloseWaiter, stdErrCollector)));
39+
final StreamCloseWaiter stdOutCloseWaiter = new StreamCloseWaiter(STD_OUT_NAME, pid, streamCloseTimeout);
40+
final StreamCloseWaiter stdErrCloseWaiter = new StreamCloseWaiter(STD_ERR_NAME, pid, streamCloseTimeout);
41+
final AsyncStreamConsumer stdOutConsumer = new AsyncStreamConsumer(STD_OUT_NAME, pid, process.getInputStream(),
42+
new DelegatingConsumer(
43+
List.of(stdOutCloseWaiter, stdOutCollector, new StreamLogger(pid, STD_OUT_NAME, logLevel))));
44+
final AsyncStreamConsumer stdErrConsumer = new AsyncStreamConsumer(STD_ERR_NAME, pid, process.getErrorStream(),
45+
new DelegatingConsumer(
46+
List.of(stdErrCloseWaiter, stdErrCollector, new StreamLogger(pid, STD_ERR_NAME, logLevel))));
4247
return new ProcessOutputConsumer<>(executor, process, List.of(stdOutConsumer, stdErrConsumer),
4348
List.of(stdOutCloseWaiter, stdErrCloseWaiter), stdOutCollector, stdErrCollector);
4449
}

src/main/java/org/itsallcode/process/SimpleProcess.java

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ public int waitForTermination() {
3636
private int waitForProcess() {
3737
try {
3838
LOG.finest(() -> "Waiting for process %d (command '%s') to terminate...".formatted(
39-
process.pid(), command));
39+
pid(), command));
4040
return process.waitFor();
4141
} catch (final InterruptedException exception) {
4242
Thread.currentThread().interrupt();
4343
throw new IllegalStateException(
44-
"Interrupted while waiting for process %d (command '%s') to finish".formatted(process.pid(),
44+
"Interrupted while waiting for process %d (command '%s') to finish".formatted(pid(),
4545
command),
4646
exception);
4747
}
@@ -70,7 +70,7 @@ public void waitForTermination(final int expectedExitCode) {
7070
if (exitCode != expectedExitCode) {
7171
throw new IllegalStateException(
7272
"Expected process %d (command '%s') to terminate with exit code %d but was %d"
73-
.formatted(process.pid(), command, expectedExitCode, exitCode));
73+
.formatted(pid(), command, expectedExitCode, exitCode));
7474
}
7575
}
7676

@@ -84,25 +84,25 @@ public void waitForTermination(final int expectedExitCode) {
8484
*/
8585
public void waitForTermination(final Duration timeout) {
8686
waitForProcess(timeout);
87-
LOG.finest(() -> "Process %d (command '%s') terminated with exit code %d".formatted(process.pid(), command,
87+
LOG.finest(() -> "Process %d (command '%s') terminated with exit code %d".formatted(pid(), command,
8888
exitValue()));
8989
consumer.waitForStreamsClosed();
9090
}
9191

9292
private void waitForProcess(final Duration timeout) {
9393
try {
9494
LOG.finest(() -> "Waiting %s for process %d (command '%s') to terminate...".formatted(timeout,
95-
process.pid(), command));
95+
pid(), command));
9696
if (!process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
9797
throw new IllegalStateException(
98-
"Timeout while waiting %s for process %d (command '%s')".formatted(timeout, process.pid(),
98+
"Timeout while waiting %s for process %d (command '%s')".formatted(timeout, pid(),
9999
command));
100100
}
101101
} catch (final InterruptedException exception) {
102102
Thread.currentThread().interrupt();
103103
throw new IllegalStateException(
104104
"Interrupted while waiting %s for process %d (command '%s') to finish".formatted(timeout,
105-
process.pid(), command),
105+
pid(), command),
106106
exception);
107107
}
108108
}
@@ -112,7 +112,7 @@ private void waitForProcess(final Duration timeout) {
112112
*
113113
* @return standard output
114114
*/
115-
T getStdOut() {
115+
public T getStdOut() {
116116
return consumer.getStdOut();
117117
}
118118

@@ -121,7 +121,7 @@ T getStdOut() {
121121
*
122122
* @return standard error
123123
*/
124-
T getStdErr() {
124+
public T getStdErr() {
125125
return consumer.getStdErr();
126126
}
127127

@@ -131,7 +131,7 @@ T getStdErr() {
131131
* @return {@code true} if the process has not yet terminated
132132
* @see Process#isAlive()
133133
*/
134-
boolean isAlive() {
134+
public boolean isAlive() {
135135
return process.isAlive();
136136
}
137137

@@ -141,16 +141,26 @@ boolean isAlive() {
141141
* @return exit value
142142
* @see Process#exitValue()
143143
*/
144-
int exitValue() {
144+
public int exitValue() {
145145
return process.exitValue();
146146
}
147147

148+
/**
149+
* Get the process ID.
150+
*
151+
* @return process ID
152+
* @see Process#pid()
153+
*/
154+
public long pid() {
155+
return process.pid();
156+
}
157+
148158
/**
149159
* Kill the process.
150160
*
151-
* @See Process#destroy()
161+
* @see Process#destroy()
152162
*/
153-
void destroy() {
163+
public void destroy() {
154164
process.destroy();
155165
}
156166

src/main/java/org/itsallcode/process/SimpleProcessBuilder.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public final class SimpleProcessBuilder {
2020
private final ProcessBuilder processBuilder;
2121
private Duration streamCloseTimeout = Duration.ofSeconds(1);
2222
private Executor executor;
23+
private Level streamLogLevel = Level.FINE;
2324

2425
private SimpleProcessBuilder() {
2526
this.processBuilder = new ProcessBuilder();
@@ -59,6 +60,17 @@ public SimpleProcessBuilder command(final List<String> command) {
5960
return this;
6061
}
6162

63+
/**
64+
* Set working directory to the current process's working directory.
65+
*
66+
* @return {@code this} for fluent programming
67+
* @see ProcessBuilder#directory(java.io.File)
68+
*/
69+
public SimpleProcessBuilder currentProcessWorkingDir() {
70+
this.processBuilder.directory(null);
71+
return this;
72+
}
73+
6274
/**
6375
* Set working directory.
6476
*
@@ -96,6 +108,8 @@ public SimpleProcessBuilder setStreamCloseTimeout(final Duration streamCloseTime
96108

97109
/**
98110
* Set a custom executor for asynchronous stream readers.
111+
* <p>
112+
* Default: Create new threads on demand.
99113
*
100114
* @param executor executor
101115
* @return {@code this} for fluent programming
@@ -105,6 +119,19 @@ public SimpleProcessBuilder streamConsumerExecutor(final Executor executor) {
105119
return this;
106120
}
107121

122+
/**
123+
* Log level for the process's stdout and stderr.
124+
* <p>
125+
* Default: {@link Level#FINE}
126+
*
127+
* @param streamLogLevel log level
128+
* @return {@code this} for fluent programming
129+
*/
130+
public SimpleProcessBuilder streamLogLevel(Level streamLogLevel) {
131+
this.streamLogLevel = streamLogLevel;
132+
return this;
133+
}
134+
108135
/**
109136
* Start the new process.
110137
*
@@ -114,7 +141,7 @@ public SimpleProcessBuilder streamConsumerExecutor(final Executor executor) {
114141
public SimpleProcess<String> start() {
115142
final Process process = startProcess();
116143
final ProcessOutputConsumer<String> consumer = ProcessOutputConsumer.create(getExecutor(process), process,
117-
streamCloseTimeout, new StringCollector(), new StringCollector());
144+
streamCloseTimeout, streamLogLevel, new StringCollector(), new StringCollector());
118145
consumer.start();
119146
return new SimpleProcess<>(process, consumer, getCommand());
120147
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.itsallcode.process;
2+
3+
import java.io.IOException;
4+
import java.util.logging.Level;
5+
import java.util.logging.Logger;
6+
7+
class StreamLogger implements ProcessStreamConsumer {
8+
9+
private static final Logger LOG = Logger.getLogger(StreamLogger.class.getName());
10+
private final Level logLevel;
11+
private final long pid;
12+
private final String streamName;
13+
14+
StreamLogger(long pid, String streamName, Level logLevel) {
15+
this.pid = pid;
16+
this.streamName = streamName;
17+
this.logLevel = logLevel;
18+
}
19+
20+
@Override
21+
public void accept(String line) {
22+
LOG.log(logLevel, () -> "%d:%s> %s".formatted(pid, streamName, line));
23+
}
24+
25+
@Override
26+
public void streamFinished() {
27+
// Ignore
28+
}
29+
30+
@Override
31+
public void streamReadingFailed(IOException exception) {
32+
// Ignore
33+
}
34+
}

0 commit comments

Comments
 (0)