All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cp.elements.process.ProcessAdapter Maven / Gradle / Ivy

/*
 * Copyright 2016 Author or Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.cp.elements.process;

import static org.cp.elements.io.IOUtils.close;
import static org.cp.elements.lang.RuntimeExceptionsFactory.newUnsupportedOperationException;
import static org.cp.elements.lang.concurrent.SimpleThreadFactory.newThreadFactory;
import static org.cp.elements.process.ProcessContext.newProcessContext;
import static org.cp.elements.process.support.RuntimeProcessExecutor.newRuntimeProcessExecutor;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.cp.elements.io.FileSystemUtils;
import org.cp.elements.lang.Assert;
import org.cp.elements.lang.Constants;
import org.cp.elements.lang.Identifiable;
import org.cp.elements.lang.Initable;
import org.cp.elements.lang.SystemUtils;
import org.cp.elements.lang.ThrowableUtils;
import org.cp.elements.process.event.ProcessStreamListener;
import org.cp.elements.process.util.ProcessUtils;
import org.cp.elements.util.Environment;

/**
 * The {@link ProcessAdapter} class is an Adapter (wrapper) for a Java {@link Process} object.
 *
 * This class provides additional, convenient operations on an instance of {@link Process} that
 * are not directly available in the Java {@link Process} API itself.
 *
 * @author John J. Blum
 * @see java.io.File
 * @see java.lang.Process
 * @see java.util.UUID
 * @see java.util.logging.Logger
 * @see java.util.concurrent.ExecutorService
 * @see java.util.concurrent.Executors
 * @see java.util.concurrent.Future
 * @see org.cp.elements.lang.Identifiable
 * @see org.cp.elements.lang.Initable
 * @see org.cp.elements.process.ProcessContext
 * @see org.cp.elements.process.ProcessExecutor
 * @see org.cp.elements.process.event.ProcessStreamListener
 * @see org.cp.elements.process.util.ProcessUtils
 * @see org.cp.elements.util.Environment
 * @since 1.0.0
 */
@SuppressWarnings("unused")
public class ProcessAdapter implements Identifiable, Initable {

  protected static final boolean DAEMON_THREAD = true;

  protected static final int THREAD_PRIORITY = Thread.NORM_PRIORITY;

  protected static final long DEFAULT_TIMEOUT_MILLISECONDS = TimeUnit.SECONDS.toMillis(30);

  /**
   * Factory method used to construct an instance of {@link ProcessAdapter} initialized with the given {@link Process}.
   *
   * @param process {@link Process} object to adapt/wrap with an instance of {@link ProcessAdapter}.
   * @return a newly constructed {@link ProcessAdapter} adapting/wrapping the given {@link Process}.
   * @throws IllegalArgumentException if {@link Process} is {@literal null}.
   * @see #newProcessAdapter(Process, ProcessContext)
   * @see java.lang.Process
   */
  public static ProcessAdapter newProcessAdapter(Process process) {
    return newProcessAdapter(process, newProcessContext(process).ranBy(SystemUtils.USERNAME)
      .ranIn(FileSystemUtils.WORKING_DIRECTORY).usingEnvironmentVariables());
  }

  /**
   * Factory method used to construct an instance of {@link ProcessAdapter} initialized with the given {@link Process}
   * and corresponding {@link ProcessContext}.
   *
   * @param process {@link Process} object to adapt/wrap with an instance of {@link ProcessAdapter}.
   * @param processContext {@link ProcessContext} object containing contextual information about the environment
   * in which the {@link Process} is running.
   * @return a newly constructed {@link ProcessAdapter} adapting/wrapping the given {@link Process}.
   * @throws IllegalArgumentException if {@link Process} or {@link ProcessContext} is {@literal null}.
   * @see #ProcessAdapter(Process, ProcessContext)
   * @see org.cp.elements.process.ProcessContext
   * @see java.lang.Process
   */
  public static ProcessAdapter newProcessAdapter(Process process, ProcessContext processContext) {
    return new ProcessAdapter(process, processContext);
  }

  private final AtomicBoolean initialized = new AtomicBoolean(false);

  private final CopyOnWriteArraySet listeners = new CopyOnWriteArraySet<>();

  private final Logger logger = Logger.getLogger(getClass().getName());

  private final Process process;

  private final ProcessContext processContext;

  private final ProcessStreamListener compositeProcessStreamListener =
    line -> this.listeners.forEach(listener -> listener.onInput(line));

  private final ThreadGroup threadGroup;

  /**
   * Constructs an instance of {@link ProcessAdapter} initialized with the given {@link Process}
   * and {@link ProcessContext}.
   *
   * @param process {@link Process} object adapted/wrapped by this {@link ProcessAdapter}.
   * @param processContext {@link ProcessContext} object containing contextual information about the environment
   * in which the {@link Process} is running.
   * @throws IllegalArgumentException if {@link Process} or {@link ProcessContext} is {@literal null}.
   * @see org.cp.elements.process.ProcessContext
   * @see java.lang.Process
   */
  public ProcessAdapter(Process process, ProcessContext processContext) {
    Assert.notNull(process, "Process cannot be null");
    Assert.notNull(processContext, "ProcessContext cannot be null");

    this.process = process;
    this.processContext = processContext;
    this.threadGroup = new ThreadGroup(String.format("Process [%s] Thread Group", UUID.randomUUID()));
  }

  /**
   * Initializes the {@link ProcessAdapter} by starting {@link Thread Threads} to process the {@link Process Process's}
   * input and error IO streams.
   *
   * @see org.cp.elements.lang.Initable#init()
   */
  @Override
  public void init() {
    if (!getProcessContext().inheritsIO()) {
      newThread(String.format("Process [%d] Standard Out Reader", safeGetId()),
        newProcessStreamReader(getProcess().getInputStream())).start();

      if (!getProcessContext().isRedirectingErrorStream()) {
        newThread(String.format("Process [%d] Standard Error Reader", safeGetId()),
          newProcessStreamReader(getProcess().getErrorStream())).start();
      }
    }

    initialized.set(true);
  }

  /* (non-Javadoc) */
  protected Runnable newProcessStreamReader(InputStream in) {
    return () -> {
      if (isRunning()) {
        BufferedReader reader = newReader(in);

        try {
          for (String input = reader.readLine(); input != null; input = reader.readLine()) {
            this.compositeProcessStreamListener.onInput(input);
          }
        }
        catch (IOException ignore) {
          // Ignore IO error and just stop reading from the process input stream
          // The IO error occurred most likely because the process was terminated
        }
        finally {
          close(reader);
        }
      }
    };
  }

  /* (non-Javadoc) */
  protected BufferedReader newReader(InputStream in) {
    return new BufferedReader(new InputStreamReader(in));
  }

  /* (non-Javadoc) */
  protected Thread newThread(String name, Runnable task) {
    return newThreadFactory().as(DAEMON_THREAD).in(resolveThreadGroup()).with(THREAD_PRIORITY).newThread(name, task);
  }

  /* (non-Javadoc) */
  protected ThreadGroup resolveThreadGroup() {
    return this.threadGroup;
  }

  /**
   * Returns a reference to the {@link Process} object adapted/wrapped by this {@link ProcessAdapter}.
   *
   * @return a reference to the {@link Process} object adapted/wrapped by this {@link ProcessAdapter}.
   * @see java.lang.Process
   */
  public Process getProcess() {
    return this.process;
  }

  /**
   * Returns the {@link ProcessContext} containing contextual information about the environment and system
   * in which the {@link Process} is running.
   *
   * @return the {@link ProcessContext} containing contextual information about the environment and system
   * in which the {@link Process} is running.
   * @see org.cp.elements.process.ProcessContext
   */
  public ProcessContext getProcessContext() {
    return this.processContext;
  }

  /**
   * Determines whether this {@link Process} is still alive.
   *
   * @return a boolean value indicating whether this {@link Process} is still alive.
   * @see org.cp.elements.process.util.ProcessUtils#isAlive(Process)
   * @see #getProcess()
   */
  public boolean isAlive() {
    return ProcessUtils.isAlive(getProcess());
  }

  /**
   * Determines whether this {@link Process} is still alive.
   *
   * @return a boolean value indicating whether this {@link Process} is still alive.
   * @see #isAlive()
   */
  public boolean isNotAlive() {
    return !isAlive();
  }

  /**
   * Determines whether this {@link ProcessAdapter} has been initialized.
   *
   * @return a boolean value indicating whether this {@link ProcessAdapter} has been initialized yet.
   * @see org.cp.elements.lang.Initable#isInitialized()
   */
  @Override
  public boolean isInitialized() {
    return this.initialized.get();
  }

  /**
   * Determines whether this {@link Process} is still running.
   *
   * @return a boolean value indicating whether this {@link Process} is still running.
   * @see org.cp.elements.process.util.ProcessUtils#isRunning(Process)
   * @see #isNotRunning()
   * @see #getProcess()
   */
  public boolean isRunning() {
    return ProcessUtils.isRunning(getProcess());
  }

  /**
   * Determines whether this {@link Process} is still running.
   *
   * @return a boolean value indicating whether this {@link Process} is still running.
   * @see #isRunning()
   */
  public boolean isNotRunning() {
    return !isRunning();
  }

  /**
   * Returns the command-line used to execute and run this {@link Process}.
   *
   * @return a {@link List} of {@link String Strings} containing the command-line elements used to execute
   * and run this {@link Process}.
   * @see org.cp.elements.process.ProcessContext#getCommandLine()
   * @see #getProcessContext()
   */
  public List getCommandLine() {
    return getProcessContext().getCommandLine();
  }

  /**
   * Returns the file system {@link File directory} in which this {@link Process} is running.
   *
   * @return a {@link File} reference to the file system directory in which this {@link Process} is running.
   * @see org.cp.elements.process.ProcessContext#getDirectory()
   * @see #getProcessContext()
   */
  public File getDirectory() {
    return getProcessContext().getDirectory();
  }

  /**
   * Returns a reference to the {@link Environment} configuration used to execute the {@link Process}.
   *
   * @return a reference to the {@link Environment} configuration used to execute the {@link Process}.
   * @see org.cp.elements.process.ProcessContext#getEnvironment()
   * @see #getProcessContext()
   */
  public Environment getEnvironment() {
    return getProcessContext().getEnvironment();
  }

  /**
   * Returns the identifier of the {@link Process} (PID).
   *
   * @return an integer value containing the {@link Process} ID (PID).
   * @throws PidUnknownException if the {@link Process} ID (PID) cannot be determined.
   * @see org.cp.elements.lang.Identifiable#getId()
   * @see org.cp.elements.process.util.ProcessUtils#findPidFile(File)
   * @see org.cp.elements.process.util.ProcessUtils#readPid(File)
   * @see #getDirectory()
   */
  @Override
  public Integer getId() {
    try {
      return ProcessUtils.readPid(ProcessUtils.findPidFile(getDirectory()));
    }
    catch (Throwable cause) {
      if (cause instanceof PidUnknownException) {
        throw (PidUnknownException) cause;
      }

      throw new PidUnknownException("Process ID (PID) cannot be determined", cause);
    }
  }

  /**
   * Returns the process identifier (PID) of this running {@link Process}.
   *
   * This operation is safe from the unhandled {@link PidUnknownException}, which will be thrown
   * if the {@link Process} has terminated and the PID file was deleted.
   *
   * @return an integer value specifying the ID of the running {@link Process} or {@literal -1}
   * if the process ID cannot be determined.
   * @see org.cp.elements.process.PidUnknownException
   * @see #getId()
   */
  public Integer safeGetId() {
    try {
      return getId();
    }
    catch (PidUnknownException ignore) {
      return -1;
    }
  }

  /**
   * Throws an {@link UnsupportedOperationException} since the identifier of a {@link Process} cannot be set.
   * It is only ever assigned by the host operating system (OS).
   *
   * @param id identifier.
   * @throws UnsupportedOperationException since the ID of a {@link Process} cannot be set.
   * @see org.cp.elements.lang.Identifiable#setId(Comparable)
   */
  @Override
  public final void setId(Integer id) {
    throw newUnsupportedOperationException(Constants.OPERATION_NOT_SUPPORTED);
  }

  /**
   * Returns the configured {@link Logger} to log runtime details.
   *
   * @return the {@link Logger}.
   * @see java.util.logging.Logger
   */
  protected Logger getLogger() {
    return this.logger;
  }

  /**
   * Returns the standard error stream (stderr) of this process.
   *
   * @return an {@link InputStream} connected to the standard error stream (stderr) of this process.
   * @see java.lang.Process#getErrorStream()
   * @see #getProcess()
   */
  public InputStream getStandardErrorStream() {
    return getProcess().getErrorStream();
  }

  /**
   * Returns the standard input stream (stdin) of this process.
   *
   * @return an {@link OutputStream} connecting to the standard input stream (stdin) of this process.
   * @see java.lang.Process#getOutputStream()
   * @see #getProcess()
   */
  public OutputStream getStandardInStream() {
    return getProcess().getOutputStream();
  }

  /**
   * Returns the standard output stream (stdout) of this process.
   *
   * @return an {@link InputStream} connecting to the standard output stream (stdout) of this process.
   * @see java.lang.Process#getInputStream()
   * @see #getProcess()
   */
  public InputStream getStandardOutStream() {
    return getProcess().getInputStream();
  }

  /**
   * Returns the name of the user used to run this {@link Process}.
   *
   * @return a {@link String} containing the name of the user used to run this {@link Process}.
   * @see org.cp.elements.process.ProcessContext#getUsername()
   * @see #getProcessContext()
   */
  public String getUsername() {
    return getProcessContext().getUsername();
  }

  /**
   * Returns the exit value of this terminated {@link Process}.
   *
   * @return the exit value of this terminated {@link Process}.
   * @throws IllegalThreadStateException if the {@link Process} has not yet stopped.
   * @see java.lang.Process#exitValue()
   * @see #getProcess()
   */
  public int exitValue() {
    return getProcess().exitValue();
  }

  /**
   * Returns the exit value of this terminated {@link Process}.  Handles the {@link IllegalThreadStateException}
   * if this {@link Process} has not yet stopped by returning a {@literal -1}.
   *
   * @return the exit value of this terminated {@link Process} or {@literal -1}
   * if this {@link Process} has not yet stopped.
   * @see java.lang.IllegalThreadStateException
   * @see #exitValue()
   */
  public int safeExitValue() {
    try {
      return exitValue();
    }
    catch (IllegalThreadStateException ignore) {
      return -1;
    }
  }

  /**
   * Forcibly terminates this {@link Process} if still running.  Returns the exit value even if this {@link Process}
   * was previously terminated.
   *
   * @return an integer value indicating the exit value of this [forcibly] terminated {@link Process}.
   * @see java.lang.Process#destroyForcibly()
   * @see #safeExitValue()
   * @see #isRunning()
   */
  public synchronized int kill() {
    return (isRunning() ? newProcessAdapter(getProcess().destroyForcibly(), getProcessContext()).waitFor()
      : safeExitValue());
  }

  /**
   * Restarts this {@link Process} by first attempting to stop this {@link Process} if running
   * and then running this {@link Process} by executing this {@link Process Process's}
   * {@link #getCommandLine() command-line} in the given {@link #getDirectory() directory}.
   *
   * @return a reference to this newly started and running {@link ProcessAdapter}.
   * @throws IllegalStateException if this {@link Process} cannot be stopped and restarted.
   * @see #execute(ProcessAdapter, ProcessContext)
   * @see #isNotRunning()
   * @see #isRunning()
   * @see #stopAndWait()
   */
  public synchronized ProcessAdapter restart() {
    if (isRunning()) {
      stopAndWait();
    }

    Assert.state(isNotRunning(), "Process [%d] failed to stop", safeGetId());

    return execute(this, getProcessContext());
  }

  /* (non-Javadoc) */
  protected ProcessAdapter execute(ProcessAdapter processAdapter, ProcessContext processContext) {
    return newProcessExecutor().execute(processAdapter.getDirectory(), processAdapter.getCommandLine());
  }

  /* (non-Javadoc) */
  protected ProcessExecutor newProcessExecutor() {
    return newRuntimeProcessExecutor();
  }

  /**
   * Terminates this {@link Process} if still running.  {@code #stop()} has no effect if this {@link Process}
   * was previously terminated and will just return the exit value.
   *
   * @return an integer value indicating the exit value of this {@link Process} after it has stopped.
   * @see #stop(long, TimeUnit)
   */
  public synchronized int stop() {
    return stop(DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
  }

  /**
   * Attempts to terminate this {@link Process} within the given timeout if still running.
   * {@code #stop(long, TimeUnit)} has no effect if this {@link Process} was previously terminated
   * and will just return the exit value.
   *
   * @param timeout duration to wait for this {@link Process} to stop.
   * @param unit {@link TimeUnit} used in the duration to wait for this {@link Process} to stop.
   * @return an integer value indicating the exit value of this {@link Process} after it has stopped.
   * @see java.util.concurrent.TimeUnit
   * @see java.lang.Process#destroy()
   * @see java.lang.Process#waitFor()
   * @see #isRunning()
   * @see #exitValue()
   * @see #safeExitValue()
   */
  public synchronized int stop(long timeout, TimeUnit unit) {
    if (isRunning()) {
      ExecutorService executorService = Executors.newSingleThreadExecutor(
        newThreadFactory().as(DAEMON_THREAD).in(resolveThreadGroup()).with(THREAD_PRIORITY));

      try {
        Future futureExitValue = executorService.submit(() -> {
          getProcess().destroy();
          return getProcess().waitFor();
        });

        try {
          int exitValue = futureExitValue.get(timeout, unit);
          getLogger().info(String.format("Process [%d] has been stopped", safeGetId()));
          return exitValue;
        }
        catch (ExecutionException e) {
          if (getLogger().isLoggable(Level.FINE)) {
            getLogger().fine(ThrowableUtils.getStackTrace(e));
          }
        }
        catch (InterruptedException e) {
          Thread.currentThread().interrupt();
        }
        catch (TimeoutException e) {
          getLogger().warning(String.format("Process [%1$d] could not be stopped within the given timeout [%2$d ms]",
            safeGetId(), unit.toMillis(timeout)));
        }
      }
      finally {
        executorService.shutdownNow();
      }

      return safeExitValue();
    }
    else {
      return exitValue();
    }
  }

  /**
   * Terminates this {@link Process} and wait indefinitely for this {@link Process} to stop.
   *
   * @return the exit value for this {@link Process} when it stops.
   * @see #stop()
   * @see #waitFor()
   */
  public int stopAndWait() {
    stop();
    return waitFor();
  }

  /**
   * Terminates this {@link Process} and wait until the given timeout for this {@link Process} to stop.
   *
   * @param timeout long value to indicate the number of units in the timeout.
   * @param unit {@link TimeUnit} of the timeout (e.g. seconds).
   * @return the exit value for this {@link Process} when if it stops, or {@literal -1} if this {@link Process}
   * does not stop within the given timeout.
   * @see #stop(long, TimeUnit)
   * @see #waitFor(long, TimeUnit)
   * @see #safeExitValue()
   */
  public int stopAndWait(long timeout, TimeUnit unit) {
    stop(timeout, unit);
    return (waitFor(timeout, unit) ? exitValue() : safeExitValue());
  }

  /**
   * Registers the provided {@link ProcessStreamListener} to listen for the {@link Process Process's}
   * standard out and error stream events.
   *
   * @param listener {@link ProcessStreamListener} to unregister.
   * @return this {@link ProcessAdapter}.
   * @see org.cp.elements.process.event.ProcessStreamListener
   */
  public ProcessAdapter register(ProcessStreamListener listener) {
    this.listeners.add(listener);
    return this;
  }

  /**
   * Registers a {@link Runtime} shutdown hook to stop this {@link Process} when the JVM exits.
   *
   * @return this {@link ProcessAdapter}.
   * @see java.lang.Runtime#addShutdownHook(Thread)
   * @see #newThread(String, Runnable)
   * @see #stop()
   */
  public ProcessAdapter registerShutdownHook() {
    Runtime.getRuntime().addShutdownHook(
      newThread(String.format("Process [%d] Runtime Shutdown Hook", safeGetId()), this::stop));

    return this;
  }

  /**
   * Unregisters the given {@link ProcessStreamListener} from listening for the {@link Process Process's}
   * standard output and error stream events.
   *
   * @param listener {@link ProcessStreamListener} to unregister.
   * @return this {@link ProcessAdapter}.
   * @see org.cp.elements.process.event.ProcessStreamListener
   */
  public ProcessAdapter unregister(ProcessStreamListener listener) {
    this.listeners.remove(listener);
    return this;
  }

  /**
   * Waits indefinitely until this {@link Process} stops.
   *
   * This method handles the {@link InterruptedException} thrown by {@link Process#waitFor()}
   * by resetting the interrupt bit on the current (calling) {@link Thread}.
   *
   * @return an integer value specifying the exit value of the stopped {@link Process}.
   * @see java.lang.Process#waitFor()
   * @see #safeExitValue()
   */
  public int waitFor() {
    try {
      return getProcess().waitFor();
    }
    catch (InterruptedException ignore) {
      Thread.currentThread().interrupt();
      return safeExitValue();
    }
  }

  /**
   * Waits until the specified timeout for this {@link Process} to stop.
   *
   * This method handles the {@link InterruptedException} thrown by {@link Process#waitFor(long, TimeUnit)}
   * by resetting the interrupt bit on the current (calling) {@link Thread}.
   *
   * @param timeout long value to indicate the number of units in the timeout.
   * @param unit {@link TimeUnit} of the timeout (e.g. seconds).
   * @return a boolean value indicating whether the {@link Process} stopped within the given timeout.
   * @see java.lang.Process#waitFor(long, TimeUnit)
   * @see #isNotRunning()
   */
  public boolean waitFor(long timeout, TimeUnit unit) {
    try {
      return getProcess().waitFor(timeout, unit);
    }
    catch (InterruptedException ignore) {
      Thread.currentThread().interrupt();
      return isNotRunning();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy