/*******************************************************************************
* Copyright (c) 2012 - 2016 Wind River Systems, Inc. and others. All rights reserved.
* This program and the accompanying materials are made available under the terms
* of the Eclipse Public License v1.0 which accompanies this distribution, and is
* available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Wind River Systems - initial API and implementation
*******************************************************************************/
package org.eclipse.tcf.te.tcf.core.va;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
import org.eclipse.tcf.protocol.IChannel;
import org.eclipse.tcf.protocol.IPeer;
import org.eclipse.tcf.protocol.JSON;
import org.eclipse.tcf.protocol.Protocol;
import org.eclipse.tcf.te.runtime.callback.Callback;
import org.eclipse.tcf.te.runtime.interfaces.IDisposable;
import org.eclipse.tcf.te.runtime.interfaces.ISharedConstants;
import org.eclipse.tcf.te.runtime.interfaces.callback.ICallback;
import org.eclipse.tcf.te.runtime.processes.ProcessOutputReaderThread;
import org.eclipse.tcf.te.runtime.utils.net.IPAddressUtil;
import org.eclipse.tcf.te.tcf.core.activator.CoreBundleActivator;
import org.eclipse.tcf.te.tcf.core.interfaces.tracing.ITraceIds;
import org.eclipse.tcf.te.tcf.core.nls.Messages;
import org.eclipse.tcf.te.tcf.core.peers.Peer;
/**
* Abstract external value add implementation.
*/
public abstract class AbstractExternalValueAdd extends AbstractValueAdd {
// The per peer id value add entry map
/* default */ final Map<String, ValueAddEntry> entries = new HashMap<String, ValueAddEntry>();
/**
* Class representing a value add entry
*/
protected class ValueAddEntry implements IDisposable {
public Process process;
public IPeer peer;
public ProcessOutputReaderThread outputReader;
public ProcessOutputReaderThread errorReader;
/* (non-Javadoc)
* @see org.eclipse.tcf.te.runtime.interfaces.IDisposable#dispose()
*/
@Override
public void dispose() {
if (process != null) {
// Invoke the hook to dispose the value-add process before destroying the process
if (!disposeProcess(process)) process.destroy();
process = null;
}
if (outputReader != null) { outputReader.interrupt(); outputReader = null; }
if (errorReader != null) { errorReader.interrupt(); errorReader = null; }
}
}
/**
* Called from {@link #dispose()} to allow to customize the shutdown of
* the external value-add process. If the method returns with <code>false</code>,
* {@link #dispose()} will invoke {@link Process#destroy()} on the passed
* in process object.
* <p>
* The default implementation will do nothing.
*
* @param process The external value-add process to dispose. Must not be <code>null</code>.
* @return <code>True</code> if the external value-add process got successfully disposed, <code>false</code> otherwise.
*/
protected boolean disposeProcess(Process process) {
Assert.isNotNull(process);
return false;
}
/* (non-Javadoc)
* @see org.eclipse.tcf.te.tcf.core.va.interfaces.IValueAdd#getPeer(java.lang.String)
*/
@Override
public IPeer getPeer(String id) {
Assert.isTrue(Protocol.isDispatchThread(), "Illegal Thread Access"); //$NON-NLS-1$
Assert.isNotNull(id);
IPeer peer = null;
ValueAddEntry entry = entries.get(id);
if (entry != null) {
peer = entry.peer;
}
return peer;
}
/* (non-Javadoc)
* @see org.eclipse.tcf.te.tcf.core.va.interfaces.IValueAdd#isAlive(java.lang.String, org.eclipse.tcf.te.runtime.interfaces.callback.ICallback)
*/
@Override
public void isAlive(final String id, final ICallback done) {
isAlive(id, done, true);
}
public void isAlive(final String id, final ICallback done, boolean testResponsive) {
Assert.isTrue(Protocol.isDispatchThread(), "Illegal Thread Access"); //$NON-NLS-1$
Assert.isNotNull(id);
Assert.isNotNull(done);
// Assume that the value-add is not alive
done.setResult(Boolean.FALSE);
// Query the associated entry
ValueAddEntry entry = entries.get(id);
// If no entry is available yet, but a debug peer id
// is set, create a corresponding entry for it
if (entry == null && getDebugPeerId() != null) {
String[] attrs = getDebugPeerId().split(":"); //$NON-NLS-1$
if (attrs.length == 3) {
Map<String, String> props = new HashMap<String, String>();
props.put(IPeer.ATTR_ID, getDebugPeerId());
props.put(IPeer.ATTR_TRANSPORT_NAME, attrs[0]);
if (attrs[1].length() > 0) {
props.put(IPeer.ATTR_IP_HOST, attrs[1]);
} else {
props.put(IPeer.ATTR_IP_HOST, IPAddressUtil.getInstance().getIPv4LoopbackAddress());
}
props.put(IPeer.ATTR_IP_PORT, attrs[2]);
entry = new ValueAddEntry();
entry.peer = new Peer(props);
entries.put(id, entry);
}
}
if (entry != null) {
// Check if the process is still alive or has auto-exited already
boolean exited = false;
if (entry.process != null) {
Assert.isNotNull(entry.peer);
try {
entry.process.exitValue();
exited = true;
} catch (IllegalThreadStateException e) {
/* ignored on purpose */
}
}
// If the process is still running, try to open a channel
if (!exited) {
if (testResponsive) {
final ValueAddEntry finEntry = entry;
final IChannel channel = entry.peer.openChannel();
channel.addChannelListener(new IChannel.IChannelListener() {
@Override
public void onChannelOpened() {
// Remove ourself as channel listener
channel.removeChannelListener(this);
// Close the channel, it is not longer needed
channel.close();
// Invoke the callback
done.setResult(Boolean.TRUE);
done.done(AbstractExternalValueAdd.this, Status.OK_STATUS);
}
@Override
public void onChannelClosed(Throwable error) {
// Remove ourself as channel listener
channel.removeChannelListener(this);
// External value-add is not longer alive, clean up
entries.remove(id);
finEntry.dispose();
// Invoke the callback
done.done(AbstractExternalValueAdd.this, Status.OK_STATUS);
}
@Override
public void congestionLevel(int level) {
}
});
} else {
done.setResult(Boolean.TRUE);
done.done(AbstractExternalValueAdd.this, Status.OK_STATUS);
}
} else {
done.done(AbstractExternalValueAdd.this, Status.OK_STATUS);
}
} else {
done.done(AbstractExternalValueAdd.this, Status.OK_STATUS);
}
}
/* (non-Javadoc)
* @see org.eclipse.tcf.te.tcf.core.va.interfaces.IValueAdd#launch(java.lang.String, org.eclipse.tcf.te.runtime.interfaces.callback.ICallback)
*/
@SuppressWarnings("unchecked")
@Override
public void launch(String id, ICallback done) {
Assert.isTrue(Protocol.isDispatchThread(), "Illegal Thread Access"); //$NON-NLS-1$
Assert.isNotNull(id);
Assert.isNotNull(done);
ValueAddException error = null;
// Get the location of the executable image
IPath path = getLocation();
if (path != null && path.toFile().canRead()) {
ValueAddLauncher launcher = createLauncher(id, path);
try {
launcher.launch();
} catch (ValueAddException e) {
error = e;
}
// Prepare the value-add entry
ValueAddEntry entry = new ValueAddEntry();
if (error == null) {
if (CoreBundleActivator.getTraceHandler().isSlotEnabled(0, ITraceIds.TRACE_CHANNEL_MANAGER)) {
CoreBundleActivator.getTraceHandler().trace(NLS.bind(Messages.AbstractExternalValueAdd_start_at, ISharedConstants.TIME_FORMAT.format(new Date(System.currentTimeMillis())), id),
0, ITraceIds.TRACE_CHANNEL_MANAGER, IStatus.INFO, this);
}
// Get the external process
Process process = launcher.getProcess();
try {
// Check if the process exited right after the launch
int exitCode = process.exitValue();
// Died -> Construct the error
error = onProcessDied(launcher, exitCode);
if (CoreBundleActivator.getTraceHandler().isSlotEnabled(0, ITraceIds.TRACE_CHANNEL_MANAGER)) {
CoreBundleActivator.getTraceHandler().trace(NLS.bind(Messages.AbstractExternalValueAdd_died_at, new Object[] { ISharedConstants.TIME_FORMAT.format(new Date(System.currentTimeMillis())), Integer.valueOf(exitCode), id }),
0, ITraceIds.TRACE_CHANNEL_MANAGER, IStatus.INFO, this);
}
} catch (IllegalThreadStateException e) {
// Still running -> Associate the process with the entry
entry.process = process;
// Associate the reader threads with the entry
entry.outputReader = launcher.getOutputReader();
entry.errorReader = launcher.getErrorReader();
if (CoreBundleActivator.getTraceHandler().isSlotEnabled(0, ITraceIds.TRACE_CHANNEL_MANAGER)) {
CoreBundleActivator.getTraceHandler().trace(NLS.bind(Messages.AbstractExternalValueAdd_running_at, ISharedConstants.TIME_FORMAT.format(new Date(System.currentTimeMillis())), id),
0, ITraceIds.TRACE_CHANNEL_MANAGER, IStatus.INFO, this);
}
}
}
String output = null;
if (error == null) {
// The agent is started with "-S" to write out the peer attributes in JSON format.
long timeout = getWaitForValueAddOutputTimeout();
int counter = Long.valueOf(Math.max(timeout, 200) / 200).intValue();
if (CoreBundleActivator.getTraceHandler().isSlotEnabled(0, ITraceIds.TRACE_CHANNEL_MANAGER)) {
CoreBundleActivator.getTraceHandler().trace(NLS.bind(Messages.AbstractExternalValueAdd_start_waiting_at, new Object[] { ISharedConstants.TIME_FORMAT.format(new Date(System.currentTimeMillis())), Long.valueOf(timeout), Integer.valueOf(counter), id }),
0, ITraceIds.TRACE_CHANNEL_MANAGER, IStatus.INFO, this);
}
while (counter > 0 && output == null) {
try {
// Check if the process is still alive or died in the meanwhile
int exitCode = entry.process.exitValue();
// Died -> Construct the error
error = onProcessDied(launcher, exitCode);
if (CoreBundleActivator.getTraceHandler().isSlotEnabled(0, ITraceIds.TRACE_CHANNEL_MANAGER)) {
CoreBundleActivator.getTraceHandler().trace(NLS.bind(Messages.AbstractExternalValueAdd_died_at, new Object[] { ISharedConstants.TIME_FORMAT.format(new Date(System.currentTimeMillis())), Integer.valueOf(exitCode), id }),
0, ITraceIds.TRACE_CHANNEL_MANAGER, IStatus.INFO, this);
}
} catch (IllegalThreadStateException e) { /* ignored on purpose */ }
if (error != null) break;
// Try to read in the output
output = launcher.getOutputReader().getOutput();
if ("".equals(output) || output.indexOf("Server-Properties:") == -1) { //$NON-NLS-1$ //$NON-NLS-2$
output = null;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
/* ignored on purpose */
}
}
counter--;
}
if (output == null && error == null) {
String stderr = !"".equals(launcher.getErrorReader().getOutput()) ? NLS.bind(Messages.AbstractExternalValueAdd_error_output, getLabel(), formatErrorOutput(launcher.getErrorReader().getOutput())) : ""; //$NON-NLS-1$ //$NON-NLS-2$
error = new ValueAddException(new IOException(NLS.bind(Messages.AbstractExternalValueAdd_error_failedToReadOutput, getLabel(), stderr)));
}
}
if (CoreBundleActivator.getTraceHandler().isSlotEnabled(0, ITraceIds.TRACE_CHANNEL_MANAGER)) {
CoreBundleActivator.getTraceHandler().trace(NLS.bind(Messages.AbstractExternalValueAdd_stop_waiting_at, new Object[] { ISharedConstants.TIME_FORMAT.format(new Date(System.currentTimeMillis())), error, id }),
0, ITraceIds.TRACE_CHANNEL_MANAGER, IStatus.INFO, this);
}
Map<String, String> attrs = null;
if (error == null) {
if (CoreBundleActivator.getTraceHandler().isSlotEnabled(0, ITraceIds.TRACE_CHANNEL_MANAGER)) {
CoreBundleActivator.getTraceHandler().trace(NLS.bind(Messages.AbstractExternalValueAdd_output, output, id),
0, ITraceIds.TRACE_CHANNEL_MANAGER, IStatus.INFO, this);
}
// Find the "Server-Properties: ..." string within the output
int start = output.indexOf("Server-Properties:"); //$NON-NLS-1$
if (start != -1 && start > 0) {
output = output.substring(start);
}
// Strip away "Server-Properties:"
output = output.replace("Server-Properties:", " "); //$NON-NLS-1$ //$NON-NLS-2$
output = output.trim();
// Expectation is that the value-add is printing the server properties as single line.
// If we have still a newline in the string, ignore everything after it
if (output.indexOf('\n') != -1) {
output = output.substring(0, output.indexOf('\n'));
output = output.trim();
}
// Read into an object
Object object = null;
try {
object = JSON.parseOne(output.getBytes("UTF-8")); //$NON-NLS-1$
attrs = new HashMap<String, String>((Map<String, String>)object);
} catch (IOException e) {
error = new ValueAddException(e);
}
}
if (error == null) {
// Construct the peer id from peer attributes
// The expected peer id is "<transport>:<canonical IP>:<port>"
String transport = attrs.get(IPeer.ATTR_TRANSPORT_NAME);
String port = attrs.get(IPeer.ATTR_IP_PORT);
String ip = IPAddressUtil.getInstance().getIPv4LoopbackAddress();
if (transport != null && ip != null && port != null) {
String peerId = transport + ":" + ip + ":" + port; //$NON-NLS-1$ //$NON-NLS-2$
attrs.put(IPeer.ATTR_ID, peerId);
attrs.put(IPeer.ATTR_IP_HOST, ip);
entry.peer = new Peer(attrs);
} else {
error = new ValueAddException(new IOException(NLS.bind(Messages.AbstractExternalValueAdd_error_invalidPeerAttributes, getLabel())));
}
}
if (error == null) {
Assert.isNotNull(entry.process);
Assert.isNotNull(entry.peer);
entries.put(id, entry);
}
// Stop the buffering of the output reader
if (launcher.getOutputReader() != null) {
launcher.getOutputReader().setBuffering(false);
}
// Stop the buffering of the error reader
if (launcher.getErrorReader() != null) {
launcher.getErrorReader().setBuffering(false);
}
// On error, dispose the entry
if (error != null) entry.dispose();
} else {
error = new ValueAddException(new FileNotFoundException(NLS.bind(Messages.AbstractExternalValueAdd_error_invalidLocation, getLabel(), (path != null ? path.toOSString() : "n/a")))); //$NON-NLS-1$
}
IStatus status = Status.OK_STATUS;
if (error != null) {
status = new Status(IStatus.ERROR, CoreBundleActivator.getUniqueIdentifier(), error.getLocalizedMessage(), error);
}
done.done(AbstractExternalValueAdd.this, status);
}
/**
* Returns the absolute path to the value-add executable image.
*
* @return The absolute path or <code>null</code> if not found.
*/
protected abstract IPath getLocation();
/**
* Called if the value add process dies while launching.
*
* @param launcher The value add launcher. Must not be <code>null</code>.
* @param exitCode The process exit code.
*
* @return The error to report.
*/
protected ValueAddException onProcessDied(ValueAddLauncher launcher, int exitCode) {
Assert.isNotNull(launcher);
// Read the error output if there is any
String output = launcher.getErrorReader() != null ? launcher.getErrorReader().getOutput() : null;
String cause = output != null && !"".equals(output) ? NLS.bind(Messages.AbstractExternalValueAdd_error_cause, formatErrorOutput(output)) : null; //$NON-NLS-1$
// Create the exception
String message = NLS.bind(Messages.AbstractExternalValueAdd_error_processDied, getLabel(), Integer.valueOf(exitCode));
return new ValueAddException(new IOException(cause != null ? message + cause : message));
}
/**
* Formats the error output to possible beautify it for the user.
* <p>
* The default implementation returns the passed in output unmodified.
*
* @param output The output. Must not be <code>null</code>.
* @return The beautified output.
*/
protected String formatErrorOutput(String output) {
Assert.isNotNull(output);
return output;
}
/**
* Returns the timeout to wait from the value-add output to appear.
* <p>
* The timeout is in milliseconds and ideally should be <code><n> * 200</code>.
*
* @return The timeout to wait for the value-add output to appear in milliseconds.
*/
protected long getWaitForValueAddOutputTimeout() {
return 10 * 200;
}
/**
* Create a new value-add launcher instance.
*
* @param id The target peer id. Must not be <code>null</code>.
* @param path The absolute path to the value-add executable image. Must not be <code>null</code>.
*
* @return The value-add launcher instance.
*/
protected ValueAddLauncher createLauncher(String id, IPath path) {
Assert.isTrue(Protocol.isDispatchThread(), "Illegal Thread Access"); //$NON-NLS-1$
Assert.isNotNull(id);
Assert.isNotNull(path);
return new ValueAddLauncher(id, path, getLabel() != null ? getLabel() : getId());
}
/* (non-Javadoc)
* @see org.eclipse.tcf.te.tcf.core.va.interfaces.IValueAdd#shutdown(java.lang.String, org.eclipse.tcf.te.runtime.interfaces.callback.ICallback)
*/
@Override
public void shutdown(final String id, final ICallback done) {
Assert.isTrue(Protocol.isDispatchThread(), "Illegal Thread Access"); //$NON-NLS-1$
Assert.isNotNull(id);
Assert.isNotNull(done);
final ValueAddEntry entry = entries.get(id);
if (entry != null) {
isAlive(id, new Callback() {
@Override
protected void internalDone(Object caller, IStatus status) {
boolean alive = ((Boolean)getResult()).booleanValue();
if (alive) {
entries.remove(id);
entry.dispose();
}
done.done(AbstractExternalValueAdd.this, Status.OK_STATUS);
}
}, false);
} else {
done.done(AbstractExternalValueAdd.this, Status.OK_STATUS);
}
}
/* (non-Javadoc)
* @see org.eclipse.tcf.te.tcf.core.va.interfaces.IValueAdd#shutdownAll(org.eclipse.tcf.te.runtime.interfaces.callback.ICallback)
*/
@Override
public void shutdownAll(ICallback done) {
Assert.isTrue(Protocol.isDispatchThread(), "Illegal Thread Access"); //$NON-NLS-1$
Assert.isNotNull(done);
// On shutdown all, we don't care about the alive state of the value-add.
// We force the value-add to shutdown if not yet gone by destroying the process.
for (Entry<String, ValueAddEntry> entry : entries.entrySet()) {
ValueAddEntry value = entry.getValue();
value.dispose();
}
// Clear all entries from the list
entries.clear();
done.done(AbstractExternalValueAdd.this, Status.OK_STATUS);
}
}