DefaultProcessExecutor.java

  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. import javax.annotation.CheckForNull;
  28. import javax.annotation.Nonnull;
  29. import javax.annotation.concurrent.ThreadSafe;
  30. import java.util.ArrayList;
  31. import java.util.Collections;
  32. import java.util.List;
  33. import java.util.Scanner;
  34. import java.util.concurrent.ExecutorService;
  35. import java.util.concurrent.Executors;
  36. import java.util.concurrent.atomic.AtomicBoolean;
  37. import java.util.concurrent.atomic.AtomicInteger;
  38. import java.util.regex.Pattern;
  39. import java.io.File;
  40. import java.io.IOException;
  41. import java.io.InputStream;
  42. import java.io.InputStreamReader;
  43. import java.io.PrintWriter;
  44. import it.tidalwave.util.ProcessExecutor;
  45. import lombok.AccessLevel;
  46. import lombok.Getter;
  47. import lombok.NoArgsConstructor;
  48. import lombok.RequiredArgsConstructor;
  49. import lombok.Setter;
  50. import lombok.extern.slf4j.Slf4j;

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

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

  68.         @Nonnull
  69.         private final InputStream input;

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

  72.         private volatile String latestLine;

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

  74.         private final AtomicBoolean started = new AtomicBoolean();
  75.        
  76.         @CheckForNull @Setter @Getter
  77.         private Listener listener;

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

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

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

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

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

  139.             log.info("{} - started", name);
  140.             executorService.submit(reader);
  141.             executorService.submit(logger);
  142.             return this;
  143.           }

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

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

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

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

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

  187.             // TODO: sync
  188.             if (latestLine != null)
  189.               {
  190.                 strings.add(latestLine);
  191.               }

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

  196.                 if (m.matches())
  197.                   {
  198.                     result.add(m.group(1));
  199.                   }
  200.               }

  201.             return result;
  202.           }

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

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

  228.             return this;
  229.           }

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

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

  251.                 for (;;)
  252.                   {
  253.                     final var c = is.read();

  254.                     if (c < 0)
  255.                       {
  256.                         break;
  257.                       }

  258.                     //                if (c == 10)
  259.                     //                  {
  260.                     //                    continue;
  261.                     //                  }

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

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

  280.                     synchronized (this)
  281.                       {
  282.                         notifyAll();
  283.                       }
  284.                   }

  285.                 log.debug(">>>>>> {} closed", name);
  286.               }
  287.           }
  288.       }

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

  290.     private Process process;

  291.     @Getter
  292.     private ConsoleOutput stdout;

  293.     @Getter
  294.     private ConsoleOutput stderr;

  295.     private PrintWriter stdin;

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

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

  332.     /***********************************************************************************************************************************************************
  333.      * {@inheritDoc}
  334.      **********************************************************************************************************************************************************/
  335.     @Override @Nonnull
  336.     public DefaultProcessExecutor withArgument (@Nonnull final String argument)
  337.       {
  338.         arguments.add(argument);
  339.         return this;
  340.       }

  341.     /***********************************************************************************************************************************************************
  342.      * {@inheritDoc}
  343.      **********************************************************************************************************************************************************/
  344.     @Override @Nonnull
  345.     public DefaultProcessExecutor withArguments (@Nonnull final String ... arguments)
  346.       {
  347.         this.arguments.addAll(List.of(arguments));
  348.         return this;
  349.       }

  350.     /***********************************************************************************************************************************************************
  351.      * {@inheritDoc}
  352.      **********************************************************************************************************************************************************/
  353.     @Override @Nonnull
  354.     public DefaultProcessExecutor start()
  355.       throws IOException
  356.       {
  357.         log.info(">>>> executing {} ...", arguments);

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

  359.         for (final var e : System.getenv().entrySet())
  360.           {
  361.             environment.add(String.format("%s=%s", e.getKey(), e.getValue()));
  362.           }

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

  366.         stdout = new DefaultConsoleOutput("out", process.getInputStream()).start();
  367.         stderr = new DefaultConsoleOutput("err", process.getErrorStream()).start();
  368.         stdin  = new PrintWriter(process.getOutputStream(), true);

  369.         return this;
  370.       }

  371.     /***********************************************************************************************************************************************************
  372.      * {@inheritDoc}
  373.      **********************************************************************************************************************************************************/
  374.     @Override
  375.     public void stop()
  376.       {
  377.         log.info("stop()");
  378.         process.destroy();
  379.         executorService.shutdownNow();
  380.       }

  381.     /***********************************************************************************************************************************************************
  382.      * {@inheritDoc}
  383.      **********************************************************************************************************************************************************/
  384.     @Override @Nonnull
  385.     public DefaultProcessExecutor waitForCompletion()
  386.             throws InterruptedException
  387.       {
  388.         if (process.waitFor() != 0)
  389.           {
  390. //            throw new IOException("Process exited with " + process.exitValue()); FIXME
  391.           }

  392.         return this;
  393.       }

  394.     /***********************************************************************************************************************************************************
  395.      * {@inheritDoc}
  396.      **********************************************************************************************************************************************************/
  397.     @Override @Nonnull
  398.     public DefaultProcessExecutor send (@Nonnull final String string)
  399.       {
  400.         log.debug(">>>> sending '{}'...", string.replaceAll("\n", "<CR>"));
  401.         stdin.print(string);
  402.         stdin.flush();
  403.         return this;
  404.       }

  405.     /***********************************************************************************************************************************************************
  406.      **********************************************************************************************************************************************************/
  407.     private static boolean isWindows()
  408.       {
  409.         return System.getProperty ("os.name").toLowerCase().startsWith("windows");
  410.       }
  411.   }