View Javadoc
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   }