Skip to content

Method: latestLineMatches(String)

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