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.Arrays;
  33. import java.util.Collections;
  34. import java.util.List;
  35. import java.util.Map.Entry;
  36. import java.util.Scanner;
  37. import java.util.concurrent.ExecutorService;
  38. import java.util.concurrent.Executors;
  39. import java.util.concurrent.atomic.AtomicBoolean;
  40. import java.util.concurrent.atomic.AtomicInteger;
  41. import java.util.regex.Matcher;
  42. import java.util.regex.Pattern;
  43. import java.io.File;
  44. import java.io.IOException;
  45. import java.io.InputStream;
  46. import java.io.InputStreamReader;
  47. import java.io.PrintWriter;
  48. import it.tidalwave.util.ProcessExecutor;
  49. import lombok.AccessLevel;
  50. import lombok.Getter;
  51. import lombok.NoArgsConstructor;
  52. import lombok.RequiredArgsConstructor;
  53. import lombok.Setter;
  54. import lombok.extern.slf4j.Slf4j;

  55. /***********************************************************************************************************************
  56.  *
  57.  * @author  Fabrizio Giudici
  58.  * @since   1.39
  59.  *
  60.  **********************************************************************************************************************/
  61. @ThreadSafe @NoArgsConstructor(access=AccessLevel.PRIVATE) @Slf4j
  62. public class DefaultProcessExecutor implements ProcessExecutor
  63.   {
  64.     private final ExecutorService executorService = Executors.newFixedThreadPool(10);

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

  74.         @Nonnull
  75.         private final InputStream input;

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

  78.         private volatile String latestLine;

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

  80.         private final AtomicBoolean started = new AtomicBoolean();
  81.        
  82.         @CheckForNull @Setter @Getter
  83.         private Listener listener;

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

  103.         /***************************************************************************************************************
  104.          *
  105.          *
  106.          ***************************************************************************************************************/
  107.         private final Runnable logger = new Runnable()
  108.           {
  109.             @Override
  110.             public void run()
  111.               {
  112.                 int l = 0;

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

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

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

  145.             log.info("{} - started", name);
  146.             executorService.submit(reader);
  147.             executorService.submit(logger);
  148.             return this;
  149.           }

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

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

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

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

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

  193.             // TODO: sync
  194.             if (latestLine != null)
  195.               {
  196.                 strings.add(latestLine);
  197.               }

  198.             for (final String s : strings)
  199.               {
  200. //                log.trace(">>>>>>>> matching '{}' with '{}'...", s, filter);
  201.                 final Matcher m = pattern.matcher(s);

  202.                 if (m.matches())
  203.                   {
  204.                     result.add(m.group(1));
  205.                   }
  206.               }

  207.             return result;
  208.           }

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

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

  234.             return this;
  235.           }

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

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

  257.                 for (;;)
  258.                   {
  259.                     final int c = is.read();

  260.                     if (c < 0)
  261.                       {
  262.                         break;
  263.                       }

  264.                     //                if (c == 10)
  265.                     //                  {
  266.                     //                    continue;
  267.                     //                  }

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

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

  286.                     synchronized (this)
  287.                       {
  288.                         notifyAll();
  289.                       }
  290.                   }

  291.                 log.debug(">>>>>> {} closed", name);
  292.               }
  293.           }
  294.       }

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

  296.     private Process process;

  297.     @Getter
  298.     private ConsoleOutput stdout;

  299.     @Getter
  300.     private ConsoleOutput stderr;

  301.     private PrintWriter stdin;

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

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

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

  351.     /*******************************************************************************************************************
  352.      *
  353.      * {@inheritDoc}
  354.      *
  355.      ******************************************************************************************************************/
  356.     @Override @Nonnull
  357.     public DefaultProcessExecutor withArguments (@Nonnull final String ... arguments)
  358.       {
  359.         this.arguments.addAll(Arrays.asList(arguments));
  360.         return this;
  361.       }

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

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

  373.         for (final Entry<String, String> e : System.getenv().entrySet())
  374.           {
  375.             environment.add(String.format("%s=%s", e.getKey(), e.getValue()));
  376.           }

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

  380.         stdout = new DefaultConsoleOutput("out", process.getInputStream()).start();
  381.         stderr = new DefaultConsoleOutput("err", process.getErrorStream()).start();
  382.         stdin  = new PrintWriter(process.getOutputStream(), true);

  383.         return this;
  384.       }

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

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

  410.         return this;
  411.       }

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

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