Skip to content

Package: DefaultProcessExecutor

DefaultProcessExecutor

nameinstructionbranchcomplexitylinemethod
DefaultProcessExecutor(String)
M: 0 C: 21
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 5
100%
M: 0 C: 1
100%
findPathFor(String)
M: 0 C: 44
100%
M: 0 C: 4
100%
M: 0 C: 3
100%
M: 0 C: 6
100%
M: 0 C: 1
100%
log(String, DefaultProcessExecutor.DefaultConsoleOutput)
M: 18 C: 0
0%
M: 2 C: 0
0%
M: 2 C: 0
0%
M: 4 C: 0
0%
M: 1 C: 0
0%
send(String)
M: 10 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
start()
M: 0 C: 73
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 11
100%
M: 0 C: 1
100%
static {...}
M: 0 C: 4
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
waitForCompletion()
M: 0 C: 80
100%
M: 0 C: 4
100%
M: 0 C: 3
100%
M: 0 C: 14
100%
M: 0 C: 1
100%
withArgument(String)
M: 7 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
withArguments(String[])
M: 0 C: 8
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 2
100%
M: 0 C: 1
100%
withWorkingDirectory(Path)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 2
100%
M: 0 C: 1
100%

Coverage

1: /*
2: * *************************************************************************************************************************************************************
3: *
4: * NorthernWind - lightweight CMS
5: * http://tidalwave.it/projects/northernwind
6: *
7: * Copyright (C) 2011 - 2025 Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *************************************************************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12: * You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * 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
17: * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
18: *
19: * *************************************************************************************************************************************************************
20: *
21: * git clone https://bitbucket.org/tidalwave/northernwind-src
22: * git clone https://github.com/tidalwave-it/northernwind-src
23: *
24: * *************************************************************************************************************************************************************
25: */
26: package it.tidalwave.util.impl;
27:
28: import javax.annotation.Nonnull;
29: import javax.annotation.concurrent.NotThreadSafe;
30: import java.util.ArrayList;
31: import java.util.Collections;
32: import java.util.List;
33: import java.util.Scanner;
34: import java.util.concurrent.Executors;
35: import java.util.regex.Pattern;
36: import java.io.BufferedReader;
37: import java.io.File;
38: import java.io.IOException;
39: import java.io.InputStream;
40: import java.io.InputStreamReader;
41: import java.io.PrintWriter;
42: import java.nio.file.Path;
43: import it.tidalwave.util.ProcessExecutor;
44: import it.tidalwave.util.ProcessExecutorException;
45: import lombok.AccessLevel;
46: import lombok.Getter;
47: import lombok.RequiredArgsConstructor;
48: import lombok.extern.slf4j.Slf4j;
49:
50: /***************************************************************************************************************************************************************
51: *
52: * A helper class for launching an external process and handling its output.
53: *
54: * @author Fabrizio Giudici
55: *
56: **************************************************************************************************************************************************************/
57: @NotThreadSafe @Slf4j
58: public final class DefaultProcessExecutor implements ProcessExecutor
59: {
60: private static final String PROCESS_EXITED_WITH = "Process exited with ";
61:
62: /***********************************************************************************************************************************************************
63: *
64: **********************************************************************************************************************************************************/
65: @RequiredArgsConstructor(access = AccessLevel.PACKAGE)
66: public class DefaultConsoleOutput implements ConsoleOutput
67: {
68: @Nonnull
69: private final String name;
70:
71: @Nonnull
72: private final InputStream input;
73:
74: @Getter
75: private final List<String> content = Collections.synchronizedList(new ArrayList<>());
76:
77: private volatile boolean completed;
78:
79: /** The consumer for output. */
80: private final Runnable consoleConsumer = () ->
81: {
82: try
83: {
84: read();
85: }
86: catch (IOException e)
87: {
88: log.warn("while reading from process console", e);
89: }
90:
91: synchronized (DefaultConsoleOutput.this)
92: {
93: completed = true;
94: DefaultConsoleOutput.this.notifyAll();
95: }
96: };
97:
98: /***************************************************************************************************************
99: *
100: * {@inheritDoc}
101: *
102: **************************************************************************************************************/
103: @Override @Nonnull
104: public ConsoleOutput start()
105: {
106: Executors.newSingleThreadExecutor().submit(consoleConsumer);
107: return this;
108: }
109:
110: /***************************************************************************************************************
111: *
112: * {@inheritDoc}
113: *
114: **************************************************************************************************************/
115: @Override @Nonnull
116: public synchronized ConsoleOutput waitForCompleted ()
117: throws InterruptedException
118: {
119: while (!completed)
120: {
121: wait();
122: }
123:
124: return this;
125: }
126:
127: /***************************************************************************************************************
128: *
129: * {@inheritDoc}
130: *
131: **************************************************************************************************************/
132: @Override @Nonnull @SuppressWarnings({"squid:S2095", "IOResourceOpenedButNotSafelyClosed"})
133: public Scanner filteredAndSplitBy (@Nonnull final String filterRegexp, @Nonnull final String delimiterRegexp)
134: {
135: return new Scanner(filteredBy(filterRegexp).get(0)).useDelimiter(Pattern.compile(delimiterRegexp));
136: }
137:
138: /***************************************************************************************************************
139: *
140: * {@inheritDoc}
141: *
142: **************************************************************************************************************/
143: @Override @Nonnull
144: public List<String> filteredBy (@Nonnull final String filterRegexp)
145: {
146: final var p = Pattern.compile(filterRegexp);
147: final List<String> result = new ArrayList<>();
148:
149: for (final var s : new ArrayList<>(content))
150: {
151: final var m = p.matcher(s);
152:
153: if (m.matches())
154: {
155: result.add(m.group(1));
156: }
157: }
158:
159: return result;
160: }
161:
162: /***************************************************************************************************************
163: *
164: * {@inheritDoc}
165: *
166: **************************************************************************************************************/
167: @Override @Nonnull
168: public ConsoleOutput waitFor (@Nonnull final String regexp)
169: throws InterruptedException, IOException
170: {
171: log.debug("waitFor({})", regexp);
172:
173: while (filteredBy(regexp).isEmpty())
174: {
175: try
176: {
177: final var exitValue = process.exitValue();
178: throw new IOException(PROCESS_EXITED_WITH + exitValue);
179: }
180: catch (IllegalThreadStateException e) // ok, process not terminated yet
181: {
182: synchronized (this)
183: {
184: wait(50); // FIXME: polls because it doesn't get notified
185: }
186: }
187: }
188:
189: return this;
190: }
191:
192: /***************************************************************************************************************
193: *
194: * {@inheritDoc}
195: *
196: **************************************************************************************************************/
197: @Override
198: public void clear()
199: {
200: content.clear();
201: }
202:
203: /***************************************************************************************************************
204: *
205: * {@inheritDoc}
206: *
207: **************************************************************************************************************/
208: @Override
209: public void read()
210: throws IOException
211: {
212: try (final var br = new BufferedReader(new InputStreamReader(input)))
213: {
214: for (; ; )
215: {
216: final var s = br.readLine();
217:
218: if (s == null)
219: {
220: break;
221: }
222:
223: log.trace(">>>>>>>> {}: {}", name, s);
224: content.add(s);
225:
226: synchronized (this)
227: {
228: notifyAll();
229: }
230: }
231: }
232: }
233: }
234:
235: /** The arguments to pass to the external process. */
236: private final List<String> arguments = new ArrayList<>();
237:
238: /** The working directory for the external process. */
239: private Path workingDirectory = new File(".").toPath();
240:
241: /** The external process. */
242: private Process process;
243:
244: /** The processor of stdout. */
245: @Getter
246: private ConsoleOutput stdout;
247:
248: /** The processor of stderr. */
249: @Getter
250: private ConsoleOutput stderr;
251:
252: /** The writer to feed the process' stdin. */
253: private PrintWriter stdin;
254:
255: /***********************************************************************************************************************************************************
256: *
257: **********************************************************************************************************************************************************/
258: public DefaultProcessExecutor (@Nonnull final String executable)
259: throws IOException
260: {
261: arguments.add(DefaultProcessExecutor.findPathFor(executable));
262: }
263:
264: /***********************************************************************************************************************************************************
265: * {@inheritDoc}
266: **********************************************************************************************************************************************************/
267: @Override @Nonnull
268: public ProcessExecutor withArgument (@Nonnull final String argument)
269: {
270: arguments.add(argument);
271: return this;
272: }
273:
274: /***********************************************************************************************************************************************************
275: * {@inheritDoc}
276: **********************************************************************************************************************************************************/
277: @Override @Nonnull
278: public ProcessExecutor withArguments (@Nonnull final String... arguments)
279: {
280: this.arguments.addAll(List.of(arguments));
281: return this;
282: }
283:
284: /***********************************************************************************************************************************************************
285: * {@inheritDoc}
286: **********************************************************************************************************************************************************/
287: @Override @Nonnull
288: public ProcessExecutor withWorkingDirectory (@Nonnull final Path workingDirectory)
289: {
290: this.workingDirectory = workingDirectory;
291: return this;
292: }
293:
294: /***********************************************************************************************************************************************************
295: * {@inheritDoc}
296: **********************************************************************************************************************************************************/
297: @Override @Nonnull
298: public ProcessExecutor start()
299: throws IOException
300: {
301: log.debug(">>>> executing: {}", String.join(" ", arguments));
302:
303: final List<String> environment = new ArrayList<>();
304:
305: // for (final Entry<String, String> e : System.getenv().entrySet())
306: // {
307: // environment.add(String.format("%s=%s", e.getKey(), e.getValue()));
308: // }
309:
310: log.debug(">>>> working directory: {}", workingDirectory.toFile().getCanonicalPath());
311: log.debug(">>>> environment: {}", environment);
312: process = Runtime.getRuntime().exec(arguments.toArray(new String[0]),
313: environment.toArray(new String[0]),
314: workingDirectory.toFile());
315:
316: stdout = new DefaultConsoleOutput("STDOUT", process.getInputStream()).start();
317: stderr = new DefaultConsoleOutput("STDERR", process.getErrorStream()).start();
318: stdin = new PrintWriter(process.getOutputStream(), true);
319:
320: return this;
321: }
322:
323: /***********************************************************************************************************************************************************
324: * {@inheritDoc}
325: **********************************************************************************************************************************************************/
326: @Override @Nonnull
327: public ProcessExecutor waitForCompletion()
328: throws IOException, InterruptedException
329: {
330:• if (process.waitFor() != 0)
331: {
332: final List<String> environment = new ArrayList<>();
333:
334:• for (final var e : System.getenv().entrySet())
335: {
336: environment.add(String.format("%s=%s, ", e.getKey(), e.getValue()));
337: }
338:
339: log.warn(PROCESS_EXITED_WITH + process.exitValue());
340: log.debug(">>>> executed: {}", arguments);
341: log.debug(">>>> working directory: {}", workingDirectory.toFile().getCanonicalPath());
342: log.debug(">>>> environment: {}", environment);
343: // log("STDOUT", stdout);
344: // log("STDERR", stderr);
345: throw new ProcessExecutorException(PROCESS_EXITED_WITH + process.exitValue(),
346: process.exitValue(),
347: stdout.waitForCompleted().getContent(),
348: stderr.waitForCompleted().getContent());
349: }
350:
351: return this;
352: }
353:
354: /***********************************************************************************************************************************************************
355: * {@inheritDoc}
356: **********************************************************************************************************************************************************/
357: @Override @Nonnull
358: public ProcessExecutor send (@Nonnull final String string)
359: {
360: log.info(">>>> sending '{}'...", string);
361: stdin.println(string);
362: return this;
363: }
364:
365: /***********************************************************************************************************************************************************
366: * Scans the {@code PATH} for finding the absolute path of the given executable.
367: *
368: * @param executable the executable to search for
369: * @return the absolute path
370: * @throws IOException if the executable can't be found
371: **********************************************************************************************************************************************************/
372: @Nonnull
373: private static String findPathFor (@Nonnull final String executable)
374: throws IOException
375: {
376: final var pathEnv = System.getenv("PATH") + File.pathSeparator + "/usr/local/bin";
377:
378:• for (final var path : pathEnv.split(File.pathSeparator))
379: {
380: final var file = new File(new File(path), executable);
381:
382:• if (file.canExecute())
383: {
384: return file.getAbsolutePath();
385: }
386: }
387:
388: throw new IOException("Can't find " + executable + " in PATH");
389: }
390:
391: /***********************************************************************************************************************************************************
392: * Logs a whole console output.
393: *
394: * @param prefix a log prefix
395: * @param consoleOutput the output
396: **********************************************************************************************************************************************************/
397: private static void log (@Nonnull final String prefix, @Nonnull final DefaultConsoleOutput consoleOutput)
398: {
399:• for (final var line : consoleOutput.getContent())
400: {
401: log.error("{}: {}", prefix, line);
402: }
403: }
404: }
405: