blob: 20ed0e2258b5c4559ea30a31deca3bfbc41672e0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.nativeexecution;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
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 javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.modules.nativeexecution.api.ExecutionEnvironment;
import org.netbeans.modules.nativeexecution.api.HostInfo;
import org.netbeans.modules.nativeexecution.api.NativeProcess;
import org.netbeans.modules.nativeexecution.api.NativeProcess.State;
import org.netbeans.modules.nativeexecution.api.NativeProcessChangeEvent;
import org.netbeans.modules.nativeexecution.api.ProcessInfo;
import org.netbeans.modules.nativeexecution.api.ProcessInfoProvider;
import org.netbeans.modules.nativeexecution.api.ProcessStatusEx;
import org.netbeans.modules.nativeexecution.api.util.ConnectionManager.CancellationException;
import org.netbeans.modules.nativeexecution.api.util.HostInfoUtils;
import org.netbeans.modules.nativeexecution.api.util.Signal;
import org.netbeans.modules.nativeexecution.signals.SignalSupport;
import org.netbeans.modules.nativeexecution.spi.ProcessInfoProviderFactory;
import org.netbeans.modules.nativeexecution.support.Logger;
import org.netbeans.modules.nativeexecution.support.NativeTaskExecutorService;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
public abstract class AbstractNativeProcess extends NativeProcess implements ExProcessInfoProvider {
protected static final java.util.logging.Logger LOG = Logger.getInstance();
private static final Integer PID_TIMEOUT =
Integer.valueOf(System.getProperty(
"dlight.nativeexecutor.pidtimeout", "70")); // NOI18N
private static final Integer SIGKILL_TIMEOUT =
Integer.valueOf(System.getProperty(
"dlight.nativeexecutor.forcekill.timeout", "5")); // NOI18N
/*package*/ static final Boolean FIX_ERASE_KEY_IN_TERMINAL = Boolean.valueOf(System.getProperty("fixEraseKeyInTerminal", "true")); // NOI18N;
protected final NativeProcessInfo info;
protected final HostInfo hostInfo;
private final ConcurrentHashMap<String, String> processInfo = new ConcurrentHashMap<>();
protected long creation_ts = -1;
private final String id;
private final ExecutionEnvironment execEnv;
// Immutable listeners list.
private final Collection<ChangeListener> listeners;
private final Object stateLock;
private volatile State state;
private volatile int pid = 0;
private volatile boolean isInterrupted;
private final AtomicBoolean cancelledFlag = new AtomicBoolean(false);
private Future<ProcessInfoProvider> infoProviderSearchTask;
private volatile Future<Integer> waitTask = null;
private final Object resultLock = new Object();
private Integer result = null;
private InputStream inputStream;
private InputStream errorStream;
private OutputStream outputStream;
public AbstractNativeProcess(NativeProcessInfo info) {
this.info = info;
isInterrupted = false;
state = State.INITIAL;
inputStream = new ByteArrayInputStream(new byte[0]);
errorStream = new ByteArrayInputStream(new byte[0]);
outputStream = new ByteArrayOutputStream();
execEnv = info.getExecutionEnvironment();
String cmd = info.getCommandLineForShell();
if (cmd == null) {
cmd = Arrays.toString(info.getCommand().toArray(new String[0]));
}
id = execEnv.toString() + ' ' + cmd;
stateLock = "StateLock: " + id; // NOI18N
HostInfo hinfo = null;
try {
hinfo = HostInfoUtils.getHostInfo(execEnv);
} catch (CancellationException ex) {
// no logging for cancellation
} catch (InterruptedIOException ex) {
// no logging for interrupting
} catch (IOException ex) {
// log.log(Level.INFO, "Exception while getting host info:", ex); //NOI18N
// Exceptions.printStackTrace(ex);
}
hostInfo = hinfo;
listeners = info.getListenersSnapshot();
}
public final NativeProcess createAndStart() {
try {
if (hostInfo == null) {
throw new IllegalStateException("Unable to create process - no HostInfo available"); // NOI18N
}
setState(State.STARTING);
create();
setState(State.RUNNING);
findInfoProvider();
waitTask = NativeTaskExecutorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int exitCode = -1;
State state = null;
try {
exitCode = waitResult();
state = State.FINISHED;
} catch (InterruptedException ex) {
state = State.CANCELLED;
throw ex;
} catch (Throwable th) {
state = State.ERROR;
Exceptions.printStackTrace(th);
} finally {
setResult(exitCode);
if (cancelledFlag.get()) {
setState(State.CANCELLED);
} else if (state != null) {
setState(state);
}
}
return exitCode;
}
}, "Waiting for " + id); // NOI18N
} catch (Throwable ex) {
setResult(-2);
setState(State.ERROR);
destroy();
LOG.log(Level.FINE, loc("NativeProcess.exceptionOccured.text", ex.getMessage()), ex); // NOI18N
String msg = (ex.getMessage() == null ? ex.toString() : ex.getMessage());
if (info.isRedirectError()) {
inputStream = new ByteArrayInputStream((msg + "\n").getBytes()); // NOI18N
} else {
errorStream = new ByteArrayInputStream((msg + "\n").getBytes()); // NOI18N
}
}
return this;
}
protected String getProcessInfo(String key) {
return processInfo.get(key);
}
protected void addProcessInfo(String info) {
int spos = info.indexOf('=');
if (spos < 0) {
throw new IllegalArgumentException("info must be in format NAME=VALUE - was " + info); // NOI18N
}
processInfo.put(info.substring(0, spos), info.substring(spos + 1));
}
private void setResult(int exitCode) {
synchronized (resultLock) {
result = exitCode;
}
}
protected abstract void create() throws Throwable;
protected final boolean isInterrupted() {
try {
Thread.sleep(0);
} catch (InterruptedException ex) {
isInterrupted = true;
Thread.currentThread().interrupt();
}
isInterrupted |= Thread.currentThread().isInterrupted();
return isInterrupted;
}
@Override
public final ExecutionEnvironment getExecutionEnvironment() {
return execEnv;
}
@Override
public final int getPID() throws IOException {
synchronized (this) {
if (pid == 0) {
if (isInterrupted()) {
destroy();
throw new InterruptedIOException();
} else {
throw new IOException("PID of process '" + id + "' is not received!"); // NOI18N
}
}
return pid;
}
}
/**
* To be implemented by a successor. It must implement the specific
* termination of the underlying system process on this method call. It is
* guaranteed that this method is called only once. Implementation should
* not (but may) wait for the actual termination before returning from the
* call. If destroyImpl() returns and process's waitFor() still not exited
* during specified (by return value) seconds (i.e. process was not actually
* terminated), then a SIGTERM is send to the process.
*
* Default implementation just returns 0. So SIGTERM is send immediately to
* force-terminate the process.
*
* SIGKILL is send if after SIGTERM process is still alive for
* "dlight.nativeexecutor.forcekill.timeout".
*
* @return number of seconds to wait before doing an attempt to
* force-terminate the process with the SIGTERM (and SIGKILL) signal (signal
* is send only if process was not finished by that time).
*/
protected int destroyImpl() {
return 0;
}
/**
* To be implemented by a successor. This method must cause the current
* thread to wait until the underlaying system process is done and return
* it's exit code.
*
* @return exit code of underlaying system process.
* @exception InterruptedException if the current thread is
* {@link Thread#interrupt() interrupted} by another thread
* while it is waiting, then the wait is ended and an
* {@link InterruptedException} is thrown.
*/
protected abstract int waitResult() throws InterruptedException;
@Override
public final State getState() {
synchronized (stateLock) {
return state;
}
}
/**
* Returns human-readable identification of the <tt>AbstractNativeProcess</tt>.
* @return string that identifies the <tt>AbstractNativeProcess</tt>.
*/
@Override
public final String toString() {
return (id == null) ? super.toString() : id.trim();
}
/**
* Terminates the underlying system process. The system process represented
* by this <code>AbstractNativeProcess</code> object is forcibly terminated.
*
* Returning from the call of this method does not mean that the process was
* already terminated.
*
* May block caller thread for significant time
*/
@Override
public final void destroy() {
if (cancelledFlag.getAndSet(true)) {
return;
}
final int timeToWait = destroyImpl();
if (waitTask == null) {
// this could be in a case if exception occured during the process
// creation ...
return;
}
try {
waitTask.get(timeToWait, TimeUnit.SECONDS);
return;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
} catch (ExecutionException ex) {
} catch (TimeoutException ex) {
}
try {
exitValue();
// No exception means successful termination
return;
} catch (IllegalThreadStateException ex) {
}
try {
SignalSupport.signalProcess(execEnv, pid, Signal.SIGTERM);
} catch (UnsupportedOperationException ex) {
// ignore ...
}
try {
waitTask.get(SIGKILL_TIMEOUT, TimeUnit.SECONDS);
return;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
} catch (ExecutionException ex) {
} catch (TimeoutException ex) {
}
try {
exitValue();
// No exception means successful termination
return;
} catch (IllegalThreadStateException ex) {
}
try {
SignalSupport.signalProcess(execEnv, pid, Signal.SIGKILL);
} catch (UnsupportedOperationException ex) {
// ignore ...
}
}
/**
* Causes the current thread to wait, if necessary, until the
* process represented by this <code>AbstractNativeProcess</code> object has
* terminated. This method returns immediately if the subprocess has already
* terminated. If the subprocess has not yet terminated, the calling thread
* will be blocked until the subprocess exits.
*
* @return the exit value of the process. By convention,
* <code>0</code> indicates normal termination.
* @exception InterruptedException if the current thread is
* {@link Thread#interrupt() interrupted} by another thread
* while it is waiting, then the wait is ended and an
* {@link InterruptedException} is thrown.
*/
@Override
public final int waitFor() throws InterruptedException {
int exitStatus = -1;
if (waitTask == null) {
// createAndStart() failed
return exitStatus;
}
try {
exitStatus = waitTask.get();
} catch (ExecutionException ex) {
if (ex.getCause() instanceof InterruptedException) {
throw (InterruptedException) ex.getCause();
} else {
Exceptions.printStackTrace(ex);
}
} finally {
// Will clear interrupted flag (if set) as it is a general
// convension that "any method that exits by throwing an
// InterruptedException clears interrupt status when it does so."
// http://java.sun.com/docs/books/tutorial/essential/concurrency/interrupt.html
//
// This convension is violated in java.lang.Process.waitFor()
// doesn't do this (http://bugs.sun.com/view_bug.do?bug_id=6420270)
//
// But having this Thread.interrupted() here doesn't harm in other
// cases as well.
Thread.interrupted();
}
return exitStatus;
}
/**
* Returns the exit code for the underlying system process.
*
* @return the exit code of the system process represented by this
* <code>AbstractNativeProcess</code> object. By convention, the value
* <code>0</code> indicates normal termination.
* @exception IllegalThreadStateException if the system process
* represented by this <code>Process</code> object has not
* yet terminated.
*/
@Override
public final int exitValue() {
if (waitTask == null || !waitTask.isDone()) {
synchronized (resultLock) {
if (result != null) {
return result.intValue();
}
}
// Process not started/finished yet...
throw new IllegalThreadStateException();
}
try {
return waitTask.get();
} catch (InterruptedException ex) {
// cancelled
return -1;
} catch (ExecutionException ex) {
return -1;
}
}
@Override
public ProcessStatusEx getExitStatusEx() {
exitValue();
return null;
}
protected final void finishing() {
setState(State.FINISHING);
}
private void setState(State state) {
synchronized (stateLock) {
if (this.state == state) {
return;
}
/*
* Process has determinated order of states it can be set to:
* INITIAL ---> STARTING ---> RUNNING ---> FINISHING ---> FINISHED
* |-> CANCELLED |-> CENCELLED |-> CANCELLED
* |-> ERROR |-> ERROR |-> ERROR
*
* CANCELLED, ERROR and FINISHED are terminal states.
*/
if (this.state == State.CANCELLED
|| this.state == State.ERROR
|| this.state == State.FINISHED) {
return;
}
try {
if (isInterrupted()) {
// clear flag.
// will restore in finally block.
Thread.interrupted();
}
if (!isInterrupted()) {
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest(String.format("%s [%d]: State changed: %s -> %s", // NOI18N
this.toString(), this.pid, this.state, state));
}
}
this.state = state;
if (!listeners.isEmpty()) {
final ChangeEvent event = new NativeProcessChangeEvent(this, state, pid);
for (ChangeListener l : listeners) {
l.stateChanged(event);
}
if (this.state == State.CANCELLED
|| this.state == State.ERROR
|| this.state == State.FINISHED) {
listeners.clear();
}
}
} finally {
if (isInterrupted()) {
Thread.currentThread().interrupt();
}
}
}
}
protected final void setPID(final int pid) {
this.pid = pid;
}
// To be called from successors' constructor only...
protected final void readPID(final InputStream is) throws IOException {
int c;
pid = 0;
while (!isInterrupted()) {
c = is.read();
if (c >= '0' && c <= '9') {
pid = pid * 10 + (c - '0');
} else {
break;
}
}
}
@Override
public String getTTY() {
return getProcessInfo("TTY"); // NOI18N
}
@Override
public ProcessInfo getProcessInfo() {
ProcessInfoProvider provider = null;
try {
provider = infoProviderSearchTask.get();
} catch (Throwable ex) {
LOG.finest(ex.getMessage());
}
return provider == null ? new ProcessInfo() {
@Override
public long getCreationTimestamp(TimeUnit unit) {
return unit.convert(creation_ts, TimeUnit.NANOSECONDS);
}
} : provider.getProcessInfo();
}
private static String loc(String key, String... params) {
return NbBundle.getMessage(AbstractNativeProcess.class, key, params);
}
@Override
public final InputStream getErrorStream() {
return errorStream;
}
@Override
public final OutputStream getOutputStream() {
return outputStream;
}
@Override
public final InputStream getInputStream() {
return inputStream;
}
protected final void setErrorStream(InputStream error) {
errorStream = error;
}
protected final void setOutputStream(OutputStream output) {
outputStream = output;
}
protected final void setInputStream(InputStream input) {
inputStream = input;
}
private void findInfoProvider() {
Callable<ProcessInfoProvider> callable = new Callable<ProcessInfoProvider>() {
@Override
public ProcessInfoProvider call() throws Exception {
final Collection<? extends ProcessInfoProviderFactory> factories =
Lookup.getDefault().lookupAll(ProcessInfoProviderFactory.class);
ProcessInfoProvider pip = null;
for (ProcessInfoProviderFactory factory : factories) {
pip = factory.getProvider(execEnv, pid);
if (pip != null) {
break;
}
}
return pip;
}
};
infoProviderSearchTask = NativeTaskExecutorService.submit(callable,
"get info provider for process " + pid); // NOI18N
}
String getCharset() {
Charset charset = info.getCharset();
if (charset != null) {
return charset.name();
}
return null;
}
}