Skip to content

Package: DefaultProcessExecutor$DefaultConsoleOutput

DefaultProcessExecutor$DefaultConsoleOutput

nameinstructionbranchcomplexitylinemethod
DefaultProcessExecutor.DefaultConsoleOutput(DefaultProcessExecutor, String, InputStream)
M: 52 C: 0
0%
M: 4 C: 0
0%
M: 3 C: 0
0%
M: 7 C: 0
0%
M: 1 C: 0
0%
clear()
M: 7 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
filteredAndSplitBy(String, String)
M: 15 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
filteredBy(String)
M: 47 C: 0
0%
M: 6 C: 0
0%
M: 4 C: 0
0%
M: 11 C: 0
0%
M: 1 C: 0
0%
getContent()
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%
getListener()
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%
latestLineMatches(String)
M: 38 C: 0
0%
M: 6 C: 0
0%
M: 4 C: 0
0%
M: 7 C: 0
0%
M: 1 C: 0
0%
read()
M: 98 C: 0
0%
M: 10 C: 0
0%
M: 6 C: 0
0%
M: 23 C: 0
0%
M: 1 C: 0
0%
setListener(ProcessExecutor.ConsoleOutput.Listener)
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
start()
M: 31 C: 0
0%
M: 2 C: 0
0%
M: 2 C: 0
0%
M: 6 C: 0
0%
M: 1 C: 0
0%
waitFor(String)
M: 42 C: 0
0%
M: 2 C: 0
0%
M: 2 C: 0
0%
M: 10 C: 0
0%
M: 1 C: 0
0%

Coverage

1: /*
2: * #%L
3: * *********************************************************************************************************************
4: *
5: * These Foolish Things - Miscellaneous utilities
6: * http://thesefoolishthings.tidalwave.it - git clone git@bitbucket.org:tidalwave/thesefoolishthings-src.git
7: * %%
8: * Copyright (C) 2009 - 2018 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: * $Id$
24: *
25: * *********************************************************************************************************************
26: * #L%
27: */
28: package it.tidalwave.util.spi;
29:
30: import javax.annotation.CheckForNull;
31: import javax.annotation.Nonnull;
32: import javax.annotation.concurrent.ThreadSafe;
33: import java.util.ArrayList;
34: import java.util.Arrays;
35: import java.util.Collections;
36: import java.util.List;
37: import java.util.Map.Entry;
38: import java.util.Scanner;
39: import java.util.concurrent.Executors;
40: import java.util.concurrent.ExecutorService;
41: import java.util.concurrent.atomic.AtomicBoolean;
42: import java.util.regex.Matcher;
43: import java.util.regex.Pattern;
44: import java.io.File;
45: import java.io.IOException;
46: import java.io.InputStream;
47: import java.io.InputStreamReader;
48: import java.io.PrintWriter;
49: import it.tidalwave.util.ProcessExecutor;
50: import lombok.AccessLevel;
51: import lombok.Cleanup;
52: import lombok.Getter;
53: import lombok.NoArgsConstructor;
54: import lombok.RequiredArgsConstructor;
55: import lombok.Setter;
56: import lombok.extern.slf4j.Slf4j;
57:
58: /***********************************************************************************************************************
59: *
60: * @author Fabrizio Giudici
61: * @version $Id$
62: * @since 1.39
63: *
64: **********************************************************************************************************************/
65: @ThreadSafe @NoArgsConstructor(access=AccessLevel.PRIVATE) @Slf4j
66: public class DefaultProcessExecutor implements ProcessExecutor
67: {
68: private final ExecutorService executorService = Executors.newFixedThreadPool(10);
69:
70: /*******************************************************************************************************************
71: *
72: *
73: ******************************************************************************************************************/
74:• @RequiredArgsConstructor(access=AccessLevel.PACKAGE)
75: public class DefaultConsoleOutput implements ConsoleOutput
76: {
77: @Nonnull
78: private final String name;
79:
80: @Nonnull
81: private final InputStream input;
82:
83: @Getter
84: private final List<String> content = Collections.synchronizedList(new ArrayList<String>());
85:
86: private volatile String latestLine;
87:
88: private volatile int li = 0;
89:
90: private final AtomicBoolean started = new AtomicBoolean();
91:
92: @CheckForNull @Setter @Getter
93: private Listener listener;
94:
95: /***************************************************************************************************************
96: *
97: *
98: ***************************************************************************************************************/
99: private final Runnable reader = new Runnable()
100: {
101: @Override
102: public void run()
103: {
104: try
105: {
106: read();
107: }
108: catch (IOException e)
109: {
110: log.warn("while reading from " + name, e);
111: }
112: }
113: };
114:
115: /***************************************************************************************************************
116: *
117: *
118: ***************************************************************************************************************/
119: private final Runnable logger = new Runnable()
120: {
121: @Override
122: public void run()
123: {
124: int l = 0;
125:
126: for (;;)
127: {
128: try
129: {
130: if ((l != li) && (latestLine != null))
131: {
132: log.trace(">>>>>>>> {} {}", name, latestLine);
133: }
134:
135: l = li;
136: Thread.sleep(500);
137: }
138: catch (InterruptedException e)
139: {
140: return;
141: }
142: catch (Throwable e)
143: {
144: return;
145: }
146: }
147: }
148: };
149:
150: /***************************************************************************************************************
151: *
152: * Should not be used by the programmer.
153: *
154: ***************************************************************************************************************/
155: @Nonnull
156: public ConsoleOutput start()
157: {
158:• if (started.getAndSet(true))
159: {
160: throw new IllegalStateException("Already started");
161: }
162:
163: log.info("{} - started", name);
164: executorService.submit(reader);
165: executorService.submit(logger);
166: return this;
167: }
168:
169: /***************************************************************************************************************
170: *
171: * {@inheritDoc}
172: *
173: ***************************************************************************************************************/
174: @Override
175: public boolean latestLineMatches (final @Nonnull String regexp)
176: {
177: String s = null;
178:
179:• if (latestLine != null)
180: {
181: s = latestLine;
182: }
183:• else if (!content.isEmpty())
184: {
185: s = content.get(content.size() - 1);
186: }
187:
188: log.trace(">>>> testing '{}' for '{}'", s, regexp);
189:• return (s == null) ? false : Pattern.compile(regexp).matcher(s).matches();
190: // FIXME: sync
191: }
192:
193: /***************************************************************************************************************
194: *
195: * {@inheritDoc}
196: *
197: ***************************************************************************************************************/
198: @Override @Nonnull
199: public Scanner filteredAndSplitBy (final @Nonnull String filterRegexp, final @Nonnull String delimiterRegexp)
200: {
201: final String string = filteredBy(filterRegexp).get(0);
202: return new Scanner(string).useDelimiter(Pattern.compile(delimiterRegexp));
203: }
204:
205: /***************************************************************************************************************
206: *
207: * {@inheritDoc}
208: *
209: ***************************************************************************************************************/
210: @Override @Nonnull
211: public List<String> filteredBy (final @Nonnull String regexp)
212: {
213: final Pattern pattern = Pattern.compile(regexp);
214: final List<String> result = new ArrayList<>();
215: final ArrayList<String> strings = new ArrayList<>(content);
216:
217: // TODO: sync
218:• if (latestLine != null)
219: {
220: strings.add(latestLine);
221: }
222:
223:• for (final String s : strings)
224: {
225: // log.trace(">>>>>>>> matching '{}' with '{}'...", s, filter);
226: final Matcher m = pattern.matcher(s);
227:
228:• if (m.matches())
229: {
230: result.add(m.group(1));
231: }
232: }
233:
234: return result;
235: }
236:
237: /***************************************************************************************************************
238: *
239: * {@inheritDoc}
240: *
241: ***************************************************************************************************************/
242: @Override @Nonnull
243: public ConsoleOutput waitFor (final @Nonnull String regexp)
244: throws InterruptedException, IOException
245: {
246: log.debug("{} - waitFor({})", name, regexp);
247:
248:• while (filteredBy(regexp).isEmpty())
249: {
250: try
251: {
252: final int exitValue = process.exitValue();
253: throw new IOException("Process exited with " + exitValue);
254: }
255: catch (IllegalThreadStateException e) // ok, process not terminated yet
256: {
257: synchronized (this)
258: {
259: wait(50); // FIXME: polls because it doesn't get notified
260: }
261: }
262: }
263:
264: return this;
265: }
266:
267: /***************************************************************************************************************
268: *
269: * {@inheritDoc}
270: *
271: ***************************************************************************************************************/
272: @Override
273: public void clear()
274: {
275: content.clear();
276: latestLine = null;
277: }
278:
279: /***************************************************************************************************************
280: *
281: *
282: ***************************************************************************************************************/
283: private void read()
284: throws IOException
285: {
286:• final @Cleanup InputStreamReader is = new InputStreamReader(input);
287: StringBuilder l = new StringBuilder();
288:
289: for (;;)
290: {
291: int c = is.read();
292:
293:• if (c < 0)
294: {
295: break;
296: }
297:
298: // if (c == 10)
299: // {
300: // continue;
301: // }
302:
303:• if ((c == 13) || (c == 10))
304: {
305: latestLine = l.toString();
306: li++;
307: content.add(latestLine);
308: l = new StringBuilder();
309: log.trace(">>>>>>>> {} {}", name, latestLine);
310:
311:• if (listener != null)
312: {
313: listener.onReceived(latestLine);
314: }
315: }
316: else
317: {
318: l.append((char)c);
319: latestLine = l.toString();
320: li++;
321: }
322:
323: synchronized (this)
324: {
325: notifyAll();
326: }
327: }
328:
329: log.debug(">>>>>> {} closed", name);
330: is.close();
331: }
332: }
333:
334: private final List<String> arguments = new ArrayList<>();
335:
336: private Process process;
337:
338: @Getter
339: private ConsoleOutput stdout;
340:
341: @Getter
342: private ConsoleOutput stderr;
343:
344: private PrintWriter stdin;
345:
346: /*******************************************************************************************************************
347: *
348: * Factory method for associating an executable. It returns an intermediate executor that must be configured and
349: * later started. Under Windows, the '.exe' suffix is automatically appended to the name of the executable.
350: *
351: * @see #start()
352: *
353: * @param executable the executable (with the full path)
354: * @return the executor
355: *
356: ******************************************************************************************************************/
357: @Nonnull
358: public static DefaultProcessExecutor forExecutable (final @Nonnull String executable)
359: {
360: final DefaultProcessExecutor executor = new DefaultProcessExecutor();
361: executor.arguments.add(new File(executable + (isWindows() ? ".exe" : "")).getAbsolutePath());
362: return executor;
363: }
364:
365: // /*******************************************************************************************************************
366: // *
367: // *
368: // ******************************************************************************************************************/
369: // @Nonnull
370: // private static String findPath (final @Nonnull String executable)
371: // throws NotFoundException
372: // {
373: // for (final String path : System.getenv("PATH").split(File.pathSeparator))
374: // {
375: // final File file = new File(new File(path), executable);
376: //
377: // if (file.canExecute())
378: // {
379: // return file.getAbsolutePath();
380: // }
381: // }
382: //
383: // throw new NotFoundException("Can't find " + executable + " in PATH");
384: // }
385:
386: /*******************************************************************************************************************
387: *
388: * {@inheritDoc}
389: *
390: ******************************************************************************************************************/
391: @Override @Nonnull
392: public DefaultProcessExecutor withArgument (final @Nonnull String argument)
393: {
394: arguments.add(argument);
395: return this;
396: }
397:
398: /*******************************************************************************************************************
399: *
400: * {@inheritDoc}
401: *
402: ******************************************************************************************************************/
403: @Override @Nonnull
404: public DefaultProcessExecutor withArguments (final @Nonnull String ... arguments)
405: {
406: this.arguments.addAll(Arrays.asList(arguments));
407: return this;
408: }
409:
410: /*******************************************************************************************************************
411: *
412: * {@inheritDoc}
413: *
414: ******************************************************************************************************************/
415: @Override @Nonnull
416: public DefaultProcessExecutor start()
417: throws IOException
418: {
419: log.info(">>>> executing {} ...", arguments);
420:
421: final List<String> environment = new ArrayList<>();
422:
423: for (final Entry<String, String> e : System.getenv().entrySet())
424: {
425: environment.add(String.format("%s=%s", e.getKey(), e.getValue()));
426: }
427:
428: log.info(">>>> environment: {}", environment);
429: process = Runtime.getRuntime().exec(arguments.toArray(new String[0]),
430: environment.toArray(new String[0]));
431:
432: stdout = new DefaultConsoleOutput("out", process.getInputStream()).start();
433: stderr = new DefaultConsoleOutput("err", process.getErrorStream()).start();
434: stdin = new PrintWriter(process.getOutputStream(), true);
435:
436: return this;
437: }
438:
439: /*******************************************************************************************************************
440: *
441: * {@inheritDoc}
442: *
443: ******************************************************************************************************************/
444: @Override
445: public void stop()
446: {
447: log.info("stop()");
448: process.destroy();
449: executorService.shutdownNow();
450: }
451:
452: /*******************************************************************************************************************
453: *
454: * {@inheritDoc}
455: *
456: ******************************************************************************************************************/
457: @Override @Nonnull
458: public DefaultProcessExecutor waitForCompletion()
459: throws IOException, InterruptedException
460: {
461: if (process.waitFor() != 0)
462: {
463: // throw new IOException("Process exited with " + process.exitValue()); FIXME
464: }
465:
466: return this;
467: }
468:
469: /*******************************************************************************************************************
470: *
471: * {@inheritDoc}
472: *
473: ******************************************************************************************************************/
474: @Override @Nonnull
475: public DefaultProcessExecutor send (final @Nonnull String string)
476: throws IOException
477: {
478: log.debug(">>>> sending '{}'...", string.replaceAll("\n", "<CR>"));
479: stdin.print(string);
480: stdin.flush();
481: return this;
482: }
483:
484: /*******************************************************************************************************************
485: *
486: *
487: ******************************************************************************************************************/
488: private static boolean isWindows()
489: {
490: return System.getProperty ("os.name").toLowerCase().startsWith("windows");
491: }
492: }