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