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