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