DefaultProcessExecutor.java

  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. 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.  * @author  Fabrizio Giudici
  55.  * @since   1.39
  56.  *
  57.  **********************************************************************************************************************/
  58. @ThreadSafe @NoArgsConstructor(access=AccessLevel.PRIVATE) @Slf4j
  59. public class DefaultProcessExecutor implements ProcessExecutor
  60.   {
  61.     private final ExecutorService executorService = Executors.newFixedThreadPool(10);

  62.     /*******************************************************************************************************************
  63.      *
  64.      *
  65.      ******************************************************************************************************************/
  66.     @RequiredArgsConstructor(access=AccessLevel.PACKAGE)
  67.     public class DefaultConsoleOutput implements ConsoleOutput
  68.       {
  69.         @Nonnull
  70.         private final String name;

  71.         @Nonnull
  72.         private final InputStream input;

  73.         @Getter
  74.         private final List<String> content = Collections.synchronizedList(new ArrayList<>());

  75.         private volatile String latestLine;

  76.         private final AtomicInteger li = new AtomicInteger(0);

  77.         private final AtomicBoolean started = new AtomicBoolean();
  78.        
  79.         @CheckForNull @Setter @Getter
  80.         private Listener listener;

  81.         /***************************************************************************************************************
  82.          *
  83.          *
  84.          ***************************************************************************************************************/
  85.         private final Runnable reader = new Runnable()
  86.           {
  87.             @Override
  88.             public void run()
  89.               {
  90.                 try
  91.                   {
  92.                     read();
  93.                   }
  94.                 catch (IOException e)
  95.                   {
  96.                     log.warn("while reading from " + name, e);
  97.                   }
  98.               }
  99.           };

  100.         /***************************************************************************************************************
  101.          *
  102.          *
  103.          ***************************************************************************************************************/
  104.         private final Runnable logger = new Runnable()
  105.           {
  106.             @Override
  107.             public void run()
  108.               {
  109.                 var l = 0;

  110.                 for (;;)
  111.                   {
  112.                     try
  113.                       {
  114.                         if ((l != li.get()) && (latestLine != null))
  115.                           {
  116.                             log.trace(">>>>>>>> {} {}", name, latestLine);
  117.                           }

  118.                         l = li.get();
  119.                         Thread.sleep(500);
  120.                       }
  121.                     catch (Throwable e)
  122.                       {
  123.                         return;
  124.                       }
  125.                   }
  126.               }
  127.           };

  128.         /***************************************************************************************************************
  129.          *
  130.          * Should not be used by the programmer.
  131.          *
  132.          * @return -
  133.          *
  134.          ***************************************************************************************************************/
  135.         @Nonnull
  136.         public ConsoleOutput start()
  137.           {
  138.             if (started.getAndSet(true))
  139.               {
  140.                 throw new IllegalStateException("Already started");
  141.               }

  142.             log.info("{} - started", name);
  143.             executorService.submit(reader);
  144.             executorService.submit(logger);
  145.             return this;
  146.           }

  147.         /***************************************************************************************************************
  148.          *
  149.          * {@inheritDoc}
  150.          *
  151.          ***************************************************************************************************************/
  152.         @Override
  153.         public boolean latestLineMatches (@Nonnull final String regexp)
  154.           {
  155.             String s = null;

  156.             if (latestLine != null)
  157.               {
  158.                 s = latestLine;
  159.               }
  160.             else if (!content.isEmpty())
  161.               {
  162.                 s = content.get(content.size() - 1);
  163.               }

  164.             log.trace(">>>> testing '{}' for '{}'", s, regexp);
  165.             return (s != null) && Pattern.compile(regexp).matcher(s).matches();
  166.             // FIXME: sync
  167.           }

  168.         /***************************************************************************************************************
  169.          *
  170.          * {@inheritDoc}
  171.          *
  172.          ***************************************************************************************************************/
  173.         @Override @Nonnull
  174.         public Scanner filteredAndSplitBy (@Nonnull final String filterRegexp, @Nonnull final String delimiterRegexp)
  175.           {
  176.             final var string = filteredBy(filterRegexp).get(0);
  177.             return new Scanner(string).useDelimiter(Pattern.compile(delimiterRegexp));
  178.           }

  179.         /***************************************************************************************************************
  180.          *
  181.          * {@inheritDoc}
  182.          *
  183.          ***************************************************************************************************************/
  184.         @Override @Nonnull
  185.         public List<String> filteredBy (@Nonnull final String regexp)
  186.           {
  187.             final var pattern = Pattern.compile(regexp);
  188.             final List<String> result = new ArrayList<>();
  189.             final var strings = new ArrayList<>(content);

  190.             // TODO: sync
  191.             if (latestLine != null)
  192.               {
  193.                 strings.add(latestLine);
  194.               }

  195.             for (final var s : strings)
  196.               {
  197. //                log.trace(">>>>>>>> matching '{}' with '{}'...", s, filter);
  198.                 final var m = pattern.matcher(s);

  199.                 if (m.matches())
  200.                   {
  201.                     result.add(m.group(1));
  202.                   }
  203.               }

  204.             return result;
  205.           }

  206.         /***************************************************************************************************************
  207.          *
  208.          * {@inheritDoc}
  209.          *
  210.          ***************************************************************************************************************/
  211.         @Override @Nonnull
  212.         public ConsoleOutput waitFor (@Nonnull final String regexp)
  213.           throws InterruptedException, IOException
  214.           {
  215.             log.debug("{} - waitFor({})", name, regexp);

  216.             while (filteredBy(regexp).isEmpty())
  217.               {
  218.                 try
  219.                   {
  220.                     final var exitValue = process.exitValue();
  221.                     throw new IOException("Process exited with " + exitValue);
  222.                   }
  223.                 catch (IllegalThreadStateException e) // ok, process not terminated yet
  224.                   {
  225.                     synchronized (this)
  226.                       {
  227.                         wait(50); // FIXME: polls because it doesn't get notified
  228.                       }
  229.                   }
  230.               }

  231.             return this;
  232.           }

  233.         /***************************************************************************************************************
  234.          *
  235.          * {@inheritDoc}
  236.          *
  237.          ***************************************************************************************************************/
  238.         @Override
  239.         public void clear()
  240.           {
  241.             content.clear();
  242.             latestLine = null;
  243.           }

  244.         /***************************************************************************************************************
  245.          *
  246.          *
  247.          ***************************************************************************************************************/
  248.         private void read()
  249.           throws IOException
  250.           {
  251.             try (final var is = new InputStreamReader(input))
  252.               {
  253.                 var l = new StringBuilder();

  254.                 for (;;)
  255.                   {
  256.                     final var c = is.read();

  257.                     if (c < 0)
  258.                       {
  259.                         break;
  260.                       }

  261.                     //                if (c == 10)
  262.                     //                  {
  263.                     //                    continue;
  264.                     //                  }

  265.                     if ((c == 13) || (c == 10))
  266.                       {
  267.                         latestLine = l.toString();
  268.                         li.incrementAndGet();
  269.                         content.add(latestLine);
  270.                         l = new StringBuilder();
  271.                         log.trace(">>>>>>>> {} {}", name, latestLine);

  272.                         if (listener != null)
  273.                           {
  274.                             listener.onReceived(latestLine);
  275.                           }
  276.                       }
  277.                     else
  278.                       {
  279.                         l.append((char)c);
  280.                         latestLine = l.toString();
  281.                         li.incrementAndGet();
  282.                       }

  283.                     synchronized (this)
  284.                       {
  285.                         notifyAll();
  286.                       }
  287.                   }

  288.                 log.debug(">>>>>> {} closed", name);
  289.               }
  290.           }
  291.       }

  292.     private final List<String> arguments = new ArrayList<>();

  293.     private Process process;

  294.     @Getter
  295.     private ConsoleOutput stdout;

  296.     @Getter
  297.     private ConsoleOutput stderr;

  298.     private PrintWriter stdin;

  299.     /*******************************************************************************************************************
  300.      *
  301.      * Factory method for associating an executable. It returns an intermediate executor that must be configured and
  302.      * later started. Under Windows, the '.exe' suffix is automatically appended to the name of the executable.
  303.      *
  304.      * @see #start()
  305.      *
  306.      * @param       executable      the executable (with the full path)
  307.      * @return                      the executor
  308.      *
  309.      ******************************************************************************************************************/
  310.     @Nonnull
  311.     public static DefaultProcessExecutor forExecutable (@Nonnull final String executable)
  312.       {
  313.         final var executor = new DefaultProcessExecutor();
  314.         executor.arguments.add(new File(executable + (isWindows() ? ".exe" : "")).getAbsolutePath());
  315.         return executor;
  316.       }

  317. //    /*******************************************************************************************************************
  318. //     *
  319. //     *
  320. //     ******************************************************************************************************************/
  321. //    @Nonnull
  322. //    private static String findPath (final @Nonnull String executable)
  323. //      throws NotFoundException
  324. //      {
  325. //        for (final String path : System.getenv("PATH").split(File.pathSeparator))
  326. //          {
  327. //            final File file = new File(new File(path), executable);
  328. //
  329. //            if (file.canExecute())
  330. //              {
  331. //                return file.getAbsolutePath();
  332. //              }
  333. //          }
  334. //
  335. //        throw new NotFoundException("Can't find " + executable + " in PATH");
  336. //      }

  337.     /*******************************************************************************************************************
  338.      *
  339.      * {@inheritDoc}
  340.      *
  341.      ******************************************************************************************************************/
  342.     @Override @Nonnull
  343.     public DefaultProcessExecutor withArgument (@Nonnull final String argument)
  344.       {
  345.         arguments.add(argument);
  346.         return this;
  347.       }

  348.     /*******************************************************************************************************************
  349.      *
  350.      * {@inheritDoc}
  351.      *
  352.      ******************************************************************************************************************/
  353.     @Override @Nonnull
  354.     public DefaultProcessExecutor withArguments (@Nonnull final String ... arguments)
  355.       {
  356.         this.arguments.addAll(List.of(arguments));
  357.         return this;
  358.       }

  359.     /*******************************************************************************************************************
  360.      *
  361.      * {@inheritDoc}
  362.      *
  363.      ******************************************************************************************************************/
  364.     @Override @Nonnull
  365.     public DefaultProcessExecutor start()
  366.       throws IOException
  367.       {
  368.         log.info(">>>> executing {} ...", arguments);

  369.         final List<String> environment = new ArrayList<>();

  370.         for (final var e : System.getenv().entrySet())
  371.           {
  372.             environment.add(String.format("%s=%s", e.getKey(), e.getValue()));
  373.           }

  374.         log.info(">>>> environment: {}", environment);
  375.         process = Runtime.getRuntime().exec(arguments.toArray(new String[0]),
  376.                                             environment.toArray(new String[0]));

  377.         stdout = new DefaultConsoleOutput("out", process.getInputStream()).start();
  378.         stderr = new DefaultConsoleOutput("err", process.getErrorStream()).start();
  379.         stdin  = new PrintWriter(process.getOutputStream(), true);

  380.         return this;
  381.       }

  382.     /*******************************************************************************************************************
  383.      *
  384.      * {@inheritDoc}
  385.      *
  386.      ******************************************************************************************************************/
  387.     @Override
  388.     public void stop()
  389.       {
  390.         log.info("stop()");
  391.         process.destroy();
  392.         executorService.shutdownNow();
  393.       }

  394.     /*******************************************************************************************************************
  395.      *
  396.      * {@inheritDoc}
  397.      *
  398.      ******************************************************************************************************************/
  399.     @Override @Nonnull
  400.     public DefaultProcessExecutor waitForCompletion()
  401.             throws InterruptedException
  402.       {
  403.         if (process.waitFor() != 0)
  404.           {
  405. //            throw new IOException("Process exited with " + process.exitValue()); FIXME
  406.           }

  407.         return this;
  408.       }

  409.     /*******************************************************************************************************************
  410.      *
  411.      * {@inheritDoc}
  412.      *
  413.      ******************************************************************************************************************/
  414.     @Override @Nonnull
  415.     public DefaultProcessExecutor send (@Nonnull final String string)
  416.       {
  417.         log.debug(">>>> sending '{}'...", string.replaceAll("\n", "<CR>"));
  418.         stdin.print(string);
  419.         stdin.flush();
  420.         return this;
  421.       }

  422.     /*******************************************************************************************************************
  423.      *
  424.      *
  425.      ******************************************************************************************************************/
  426.     private static boolean isWindows()
  427.       {
  428.         return System.getProperty ("os.name").toLowerCase().startsWith("windows");
  429.       }
  430.   }