1 /*
2 * *************************************************************************************************************************************************************
3 *
4 * TheseFoolishThings: Miscellaneous utilities
5 * http://tidalwave.it/projects/thesefoolishthings
6 *
7 * Copyright (C) 2009 - 2025 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.concurrent.ThreadSafe;
29 import jakarta.annotation.Nonnull;
30 import jakarta.annotation.Nullable;
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 @Nullable @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 }