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

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

Go to download

Java Simplified. Extensions and Useful Constructs for the Java Platform. Codeprimate Elements (a.k.a. cp-elements) is a Java library and micro-framework used to simplify the development of software applications written in Java. Elements packages several APIs into one library in order to address various application concerns and aspects of software design and development collectively and conveniently. Elements is a highly simple, yet robust and proven library built on solid OO principles, software design patterns and best practices to effectively solve common and reoccurring problems in software development.

There is a newer version: 2.0.0-M1
Show newest version
/*
 * 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 - 2024 Weber Informatics LLC | Privacy Policy