/*******************************************************************************
* Copyright (c) 2004, 2010 BREDEX GmbH.
* 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:
* BREDEX GmbH - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.jubula.autagent.agent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jubula.autagent.commands.AbstractStartToolkitAut;
import org.eclipse.jubula.autagent.commands.IStartAut;
import org.eclipse.jubula.autagent.i18n.Messages;
import org.eclipse.jubula.communication.internal.Communicator;
import org.eclipse.jubula.communication.internal.IConnectionInitializer;
import org.eclipse.jubula.communication.internal.connection.ConnectionState;
import org.eclipse.jubula.communication.internal.listener.ICommunicationErrorListener;
import org.eclipse.jubula.communication.internal.message.ConnectToAutResponseMessage;
import org.eclipse.jubula.communication.internal.message.ConnectToClientMessage;
import org.eclipse.jubula.communication.internal.message.Message;
import org.eclipse.jubula.communication.internal.message.PrepareForShutdownMessage;
import org.eclipse.jubula.communication.internal.message.StartAUTServerMessage;
import org.eclipse.jubula.tools.AUTIdentifier;
import org.eclipse.jubula.tools.internal.constants.AutConfigConstants;
import org.eclipse.jubula.tools.internal.constants.CommandConstants;
import org.eclipse.jubula.tools.internal.constants.EnvConstants;
import org.eclipse.jubula.tools.internal.exception.CommunicationException;
import org.eclipse.jubula.tools.internal.exception.JBVersionException;
import org.eclipse.jubula.tools.internal.registration.AutIdentifier;
import org.eclipse.jubula.tools.internal.utils.IsAliveThread;
import org.eclipse.jubula.tools.internal.utils.StringParsing;
import org.eclipse.jubula.tools.internal.utils.TimeUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A server that allows AUTs to be registered with and accessed from a
* centralized location.
*
* @author BREDEX GmbH
* @created Dec 1, 2009
*/
public class AutAgent {
/**
* the default value to wait after a proper AUT termination (== de-registration)
*/
public static final int AUT_POST_DEREGISTRATION_DELAY_DEFAULT = 2000;
/**
* Name of the variable to override the AUTs proper AUT de-registration delay
*/
public static final String AUT_POST_DEREGISTRATION_DELAY_VAR = "TEST_AUT_POST_DEREGISTRATION_DELAY"; //$NON-NLS-1$
/** property name for collection of registered AUTs */
public static final String PROP_NAME_AUTS = "auts"; //$NON-NLS-1$
/** property name for AUT ID mode */
public static final String PROP_KILL_DUPLICATE_AUTS = "killDuplicateAuts"; //$NON-NLS-1$
/** the log */
private static final Logger LOG = LoggerFactory.getLogger(AutAgent.class);
/**
* Initializes the connection to a registering AUT.
*
* @author BREDEX GmbH
* @created Mar 26, 2010
*/
private class AutRegistrationInitializer
implements IConnectionInitializer {
/**
* {@inheritDoc}
*/
public void initConnection(final Socket socket,
final BufferedReader reader) {
new IsAliveThread("Register AUT") { //$NON-NLS-1$
/**
* {@inheritDoc}
*/
@SuppressWarnings("synthetic-access")
public void run() {
try {
String autInfoLine = reader.readLine();
if (autInfoLine != null
&& autInfoLine.length() > 0) {
final AutIdentifier autId =
AutIdentifier.decode(autInfoLine);
final Communicator autCommunicator =
new Communicator(0,
this.getClass().getClassLoader());
autCommunicator.addCommunicationErrorListener(
new AutCommunicationErrorListener(
autId, autCommunicator));
autCommunicator.run();
PrintStream printStream =
new PrintStream(socket.getOutputStream());
printStream.println(EnvConstants.LOCALHOST_FQDN);
printStream.println(autCommunicator.getLocalPort());
printStream.flush();
} else {
LOG.debug("AUT did not send information and so will not be registered."); //$NON-NLS-1$
}
} catch (IOException ioe) {
// Error occurred while constructing the stream
// or reading AUT information. The AUT was not
// successfully registered.
// Let thread execution continue in order to
// close the connection.
LOG.error("Error occurred while establishing communication with AUT.", ioe); //$NON-NLS-1$
} catch (SecurityException se) {
LOG.error("Error occurred while establishing communication with AUT.", se); //$NON-NLS-1$
} catch (JBVersionException gdve) {
LOG.error("Error occurred while establishing communication with AUT.", gdve); //$NON-NLS-1$
}
// Try to cleanup
try {
socket.close();
} catch (IOException e) {
// Socket could not be closed.
// Do nothing.
}
}
} .start();
}
}
/**
* Initializes the connection to an instance of autrun.
*
* @author BREDEX GmbH
* @created Mar 26, 2010
*/
private class AutRunConnectionInitializer
implements IConnectionInitializer {
/**
* {@inheritDoc}
*/
public void initConnection(final Socket socket,
final BufferedReader reader) {
new IsAliveThread("Register autrun") { //$NON-NLS-1$
/**
* {@inheritDoc}
*/
@SuppressWarnings("synthetic-access")
public void run() {
try {
String autID = reader.readLine();
String toolkit = reader.readLine();
if (autID != null
&& autID.length() > 0
&& toolkit != null
&& toolkit.length() > 0) {
AutIdentifier autId = new AutIdentifier(autID);
m_autIdToRestartHandler.put(autId,
new RestartAutAutRun(
autId, socket, reader, toolkit));
}
} catch (IOException ioe) {
// Error occurred while constructing the stream
// or reading autrun information. autrun was not
// successfully registered.
// Let thread execution continue in order to
// close the connection.
LOG.error("Error occurred while establishing communication with autrun.", ioe); //$NON-NLS-1$
// Try to cleanup
try {
socket.close();
} catch (IOException e) {
// Socket could not be closed.
// Do nothing.
}
} catch (SecurityException se) {
LOG.error("Error occurred while establishing communication with autrun.", se); //$NON-NLS-1$
// Try to cleanup
try {
socket.close();
} catch (IOException e) {
// Socket could not be closed.
// Do nothing.
}
}
}
} .start();
}
}
/**
* Error listener for communication with the AUT Server. Handles
* registration / de-registration when the connection is gained / lost.
*
* @author BREDEX GmbH
* @created Mar 22, 2010
*/
private class AutCommunicationErrorListener
implements ICommunicationErrorListener {
/**
* the ID of the Running AUT with which communication is being
* established
*/
private AutIdentifier m_autId;
/**
* the communicator being listened to
*/
private Communicator m_autCommunicator;
/**
* Constructor
*
* @param autId The ID of the Running AUT with which communication is
* being established.
* @param autCommunicator The communicator being listened to.
*/
public AutCommunicationErrorListener(
AutIdentifier autId, Communicator autCommunicator) {
m_autId = autId;
m_autCommunicator = autCommunicator;
}
/**
*
* {@inheritDoc}
*/
@SuppressWarnings("synthetic-access")
public void shutDown() {
removeAut(m_autId);
}
/**
*
* {@inheritDoc}
*/
public void sendFailed(Message message) {
// Do nothing
}
/**
*
* {@inheritDoc}
*/
@SuppressWarnings("synthetic-access")
public void connectionGained(InetAddress inetAddress, int port) {
boolean registeredAutSetWasModified =
addAut(m_autId, m_autCommunicator);
while (!registeredAutSetWasModified && !m_killDuplicateAuts) {
m_autId = new AutIdentifier(StringParsing.incrementSequence(
m_autId.getExecutableName()));
registeredAutSetWasModified =
addAut(m_autId, m_autCommunicator);
}
if (!registeredAutSetWasModified && m_killDuplicateAuts) {
try {
m_autCommunicator.send(new PrepareForShutdownMessage());
} catch (CommunicationException e) {
LOG.info(e.getLocalizedMessage(), e);
// As a result of not being able to send the message,
// the AUT will end with a different exit code. This
// may result in an unnecessary error dialog.
}
m_autCommunicator.clearListeners();
m_autCommunicator.close();
}
}
/**
*
* {@inheritDoc}
*/
public void connectingFailed(InetAddress inetAddress, int port) {
// Do nothing
}
/**
*
* {@inheritDoc}
*/
public void acceptingFailed(int port) {
// Do nothing
}
}
/** the socket that accepts incoming connections */
private ServerSocket m_serverSocket;
/** property change support */
private PropertyChangeSupport m_propertyChangeSupport;
/**
* maps registered AUTs to their corresponding communicator
*/
private Map<AutIdentifier, Communicator> m_auts =
new HashMap<AutIdentifier, Communicator>();
/** whether the AUT Agent is running */
private boolean m_isRunning = true;
/**
* flag indicating whether AUTs that attempt to register with an AUT ID
* that is already registered should be shutdown
*/
private boolean m_killDuplicateAuts = true;
/**
* mapping from AUT ID to information about how to start the
* corresponding AUT
*/
private Map<AutIdentifier, IRestartAutHandler> m_autIdToRestartHandler =
new HashMap<AutIdentifier, IRestartAutHandler>();
/** mapping from client type to connection initializer */
private Map<String, IConnectionInitializer> m_connectionInitializers =
new HashMap<String, IConnectionInitializer>();
/** Cache for {@link AutIdentifier} to toolkit */
private Map<AutIdentifier, String> m_autToolkits =
new LinkedHashMap<AutIdentifier, String>(5) {
protected boolean removeEldestEntry(
Map.Entry<AutIdentifier, String> eldest) {
return size() > 10; // caching only 10 autIdentifiers
}
};
/**
* Constructor
*
* Creates an agent that can be used as part of a server. This agent will
* not open its own server socket.
*
*/
public AutAgent() {
m_propertyChangeSupport = new PropertyChangeSupport(this);
initConnectionInitializers();
}
/**
* Constructor
*
* Starts the constructed agent on the given port.
*
* @param port The port number on which to start the agent. A port number
* of <code>0</code> starts the agent on an available port.
*
* @throws IOException if an error occurs while initializing the
* agent's server socket.
*/
public AutAgent(int port) throws IOException {
m_propertyChangeSupport = new PropertyChangeSupport(this);
m_serverSocket = new ServerSocket(port);
initConnectionInitializers();
}
/**
* Initializes the connection initializer map.
*/
@SuppressWarnings("synthetic-access")
private void initConnectionInitializers() {
m_connectionInitializers.put(ConnectionState.CLIENT_TYPE_AUT,
new AutRegistrationInitializer());
m_connectionInitializers.put(ConnectionState.CLIENT_TYPE_AUTRUN,
new AutRunConnectionInitializer());
}
/**
*
* @return the port number on which the agent is running.
*/
public int getPort() {
return m_serverSocket.getLocalPort();
}
/**
* Waits for and delegates incoming connections. This method does not return
* until the agent is shut down.
*
* @throws IOException
*/
public void waitForConnections() throws IOException {
while (m_isRunning) {
try {
final Socket socket = m_serverSocket.accept();
m_connectionInitializers.get(ConnectionState.CLIENT_TYPE_AUT)
.initConnection(socket, new BufferedReader(
new InputStreamReader(socket.getInputStream())));
} catch (SocketException se) {
// Server is shutting down
// Do nothing. The loop will end on the next iteration.
}
}
}
/**
*
* @return a copy of the collection of registered AUTs. Changes made to the
* returned copy do not write through to the internal collection
* maintained by the agent.
*/
public Set<AutIdentifier> getAuts() {
synchronized (m_auts) {
return new HashSet<AutIdentifier>(m_auts.keySet());
}
}
/**
* Adds the given AUT to the collection of registered AUTs.
*
* @param autId The ID of the AUT to register.
* @param autCommunicator The communicator to use for the registered
* AUT.
* @return <code>true</code> if the set of Registered AUTs was changed as
* a result of this call (i.e. the AUT was not already registered).
* Otherwise <code>false</code>.
*/
private boolean addAut(AutIdentifier autId, Communicator autCommunicator) {
boolean wasSetChanged = false;
synchronized (m_auts) {
if (m_isRunning) {
wasSetChanged = !m_auts.containsKey(autId);
if (wasSetChanged) {
m_auts.put(autId, autCommunicator);
}
}
}
if (wasSetChanged) {
m_propertyChangeSupport.firePropertyChange(
PROP_NAME_AUTS, null, autId);
}
return wasSetChanged;
}
/**
* Removes the given AUT from the collection of registered AUTs.
*
* @param autId
* The ID of the AUT to de-register.
*/
private void removeAut(AutIdentifier autId) {
boolean wasSetChanged = false;
synchronized (m_auts) {
if (m_isRunning) {
m_autIdToRestartHandler.remove(autId);
Communicator autCommunicator = m_auts.remove(autId);
if (autCommunicator != null) {
autCommunicator.prepareForConnectionProblems();
try {
autCommunicator.send(
new PrepareForShutdownMessage());
} catch (CommunicationException e) {
LOG.info(e.getLocalizedMessage(), e);
// As a result of not being able to send the message,
// the AUT will end with a different exit code. This
// may result in an unnecessary error dialog.
}
autCommunicator.clearListeners();
autCommunicator.close();
}
wasSetChanged = autCommunicator != null;
}
}
if (wasSetChanged) {
m_propertyChangeSupport.firePropertyChange(
PROP_NAME_AUTS, autId, null);
}
}
/**
* Add a PropertyChangeListener for a specific property. The listener
* will be invoked only when a call on firePropertyChange names that
* specific property.
*
* @param propertyName The name of the property to listen on.
* @param listener The PropertyChangeListener to be added
*/
public void addPropertyChangeListener(
String propertyName, PropertyChangeListener listener) {
m_propertyChangeSupport.addPropertyChangeListener(
propertyName, listener);
}
/**
* Add a PropertyChangeListener to the listener list.
* The listener is registered for all properties.
* The same listener object may be added more than once, and will be called
* as many times as it is added.
* If <code>listener</code> is null, no exception is thrown and no action
* is taken.
*
* @param listener The PropertyChangeListener to be added
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
m_propertyChangeSupport.addPropertyChangeListener(listener);
}
/**
* Remove a PropertyChangeListener for a specific property.
*
* @param propertyName The name of the property that was listened on.
* @param listener The PropertyChangeListener to be removed
*/
public void removePropertyChangeListener(
String propertyName, PropertyChangeListener listener) {
m_propertyChangeSupport.removePropertyChangeListener(
propertyName, listener);
}
/**
* Shuts down the agent.
*/
public void shutdown() {
synchronized (m_auts) {
m_isRunning = false;
try {
m_serverSocket.close();
} catch (IOException e) {
// Unable to close server socket.
// Do nothing.
}
m_auts = Collections.emptyMap();
}
}
/**
*
* @param args program arguments
*/
public static void main(String[] args) throws IOException {
int port = 0;
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException nfe) {
System.err.println("Port argument '" //$NON-NLS-1$
+ args[0]
+ "' is not an integer. A different port will be used."); //$NON-NLS-1$
}
}
AutAgent agent = new AutAgent(port);
System.out.println("Agent started on port: " + agent.getPort()); //$NON-NLS-1$
// AUT Registration listener. Prints registration changes to the
// console.
agent.addPropertyChangeListener(
PROP_NAME_AUTS, new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
Object newValue = evt.getNewValue();
if (newValue instanceof AutIdentifier) {
System.out.println("Registered AUT: " + ((AutIdentifier)newValue).getExecutableName()); //$NON-NLS-1$
}
Object oldValue = evt.getOldValue();
if (oldValue instanceof AutIdentifier) {
System.out.println("Deregistered AUT: " + ((AutIdentifier)oldValue).getExecutableName()); //$NON-NLS-1$
}
}
});
agent.waitForConnections();
}
/**
* Stops the given AUT.
*
* @param autId
* The ID of the Running AUT to stop.
* @param timeout
* indicates whether the AUT should be forced to quit (timeout ==
* 0) or whether the AUT should terminate by itself (timeout > 0)
*/
public void stopAut(AutIdentifier autId, int timeout) {
boolean force = timeout == 0;
if (!force) {
long startTime = System.currentTimeMillis();
boolean timedOut = false;
while (m_auts.containsKey(autId) && !timedOut) {
timedOut = (startTime + timeout) < System.currentTimeMillis();
TimeUtil.delay(250);
}
if (!timedOut) {
// The AUT has just unregistered itself - which must not be exactly
// the same as terminated - therefore we wait for another moment
// of time
TimeUtil.delayDefaultOrExternalTime(
AUT_POST_DEREGISTRATION_DELAY_DEFAULT,
AUT_POST_DEREGISTRATION_DELAY_VAR);
} else {
force = true;
}
}
synchronized (m_auts) {
Communicator autCommunicator = m_auts.get(autId);
if (autCommunicator != null) {
if (force) {
removeAut(autId);
}
}
}
}
/**
* Restarts the AUT with the given ID.
*
* @param autId
* The ID of the Running AUT to restart.
* @param timeout
* indicates whether the AUT should be forced to quit (timeout ==
* 0) or whether the AUT should terminate by itself (timeout > 0)
*/
public void restartAut(AutIdentifier autId, int timeout) {
// cache the start method
final IRestartAutHandler message = m_autIdToRestartHandler.get(autId);
message.restartAut(this, timeout);
}
/**
* Assigns the given startup information to the corresponding AUT ID. This
* information will be used when restarting an AUT.
*
* @param message The startup information.
*/
public void setStartAutMessage(StartAUTServerMessage message) {
Map<String, String> autConfig = message.getAutConfiguration();
String autExecName = autConfig.get(AutConfigConstants.AUT_ID);
AutIdentifier autId = new AutIdentifier(autExecName);
m_autToolkits.put(autId, message.getAutToolKit());
synchronized (m_auts) {
if (!m_auts.containsKey(autId)) {
m_autIdToRestartHandler.put(
autId, new RestartAutConfiguration(autId, message));
}
}
}
/**
* Sends a request to a Running AUT. The AUT should then connect to the
* Client using the provided connection information.
*
* @param autId The ID of the AUT to which the message should be sent.
* @param clientHostName The host name to which the AUT should connect.
* @param clientPort The port number to which the AUT should connect.
* @return a response that indicates whether the message was successfully
* sent. This response does <b>not</b> indicate whether the message
* was successfully received and processed.
*/
public ConnectToAutResponseMessage sendConnectToClientMessage(
AutIdentifier autId, String clientHostName, int clientPort) {
synchronized (m_auts) {
Communicator autSocket = m_auts.get(autId);
if (autSocket == null) {
LOG.error(Messages.AutConnectionError);
return new ConnectToAutResponseMessage(
Messages.AutConnectionError);
}
try {
Map<String, String> fragmentMap = new HashMap<>();
//Create fragment classpath for on demand fragment loading
synchronized (m_autIdToRestartHandler) {
//Determine toolkit of the AUT
String startClass = m_autIdToRestartHandler.get(autId)
.getAUTStartClass();
Class autServerClass = Class.forName(startClass);
AbstractStartToolkitAut autStarter =
(AbstractStartToolkitAut) autServerClass
.newInstance();
String rcBundleID = autStarter.getRcBundleId();
//Only for Java Toolkits
if (CommandConstants.RC_JAVAFX_BUNDLE_ID.equals(rcBundleID)
|| CommandConstants.RC_SWING_BUNDLE_ID.equals(
rcBundleID)
|| CommandConstants.RC_SWT_BUNDLE_ID.equals(
rcBundleID)) {
List<Bundle> fragments = new ArrayList<Bundle>();
fragments = AbstractStartToolkitAut
.getFragmentsForBundleId(rcBundleID);
for (Bundle bundle : fragments) {
StringBuilder pathBuilder = new StringBuilder();
for (String entry : AbstractStartToolkitAut
.getPathforBundle(bundle)) {
pathBuilder.append(entry).append(
IStartAut.PATH_SEPARATOR);
}
if (pathBuilder.length() > 0) {
fragmentMap.put(pathBuilder.substring(
0,
pathBuilder.lastIndexOf
(IStartAut.PATH_SEPARATOR)),
bundle.getHeaders()
.get(Constants.BUNDLE_NAME));
}
}
}
}
autSocket.send(new ConnectToClientMessage(clientHostName,
clientPort, fragmentMap));
} catch (CommunicationException | ClassNotFoundException
| InstantiationException | IllegalAccessException ce) {
LOG.error(ce.getLocalizedMessage(), ce);
return new ConnectToAutResponseMessage(
ce.getLocalizedMessage());
}
return new ConnectToAutResponseMessage(null);
}
}
/**
*
* @return a copy of the connection initializer map used by the receiver.
*/
public Map<String, IConnectionInitializer> getConnectionInitializers() {
return new HashMap<String, IConnectionInitializer>(
m_connectionInitializers);
}
/**
* Configures how the Agent will handle attempts to register an AUT with
* an ID that is already registered.
*
* @param killDuplicateAuts <code>true</code> if the Agent should shutdown
* duplicate AUTs. <code>false</code> if duplicate
* AUTs should be allowed to continue running
* (they will not, however, be registered).
*/
public void setKillDuplicateAuts(boolean killDuplicateAuts) {
boolean oldValue = m_killDuplicateAuts;
m_killDuplicateAuts = killDuplicateAuts;
m_propertyChangeSupport.firePropertyChange(
PROP_KILL_DUPLICATE_AUTS, oldValue, m_killDuplicateAuts);
}
/**
*
* @return <code>true</code> if the Agent should shutdown
* duplicate AUTs. <code>false</code> if duplicate AUTs should be
* allowed to continue running (they will not, however,
* be registered).
*/
public boolean isKillDuplicateAuts() {
return m_killDuplicateAuts;
}
/**
*
* @param id the {@link AUTIdentifier}
* @return the {@link Communicator} from the AUTagent to the AUT
*/
public Communicator getAutCommunicator(AUTIdentifier id) {
return m_auts.get(id);
}
/**
* This method is using a cache which is saving the {@link AutIdentifier} to ToolkitName
* @param connectedAutId the {@link AutIdentifier to check}
* @return the ToolkitName if it is in the Cache
*/
public String getToolkitForAutID(AutIdentifier connectedAutId) {
String toolkit = m_autToolkits.get(connectedAutId);
if (StringUtils.isBlank(toolkit)) {
return getToolkitFromRestartHandler(connectedAutId);
}
return toolkit;
}
/**
*
* @param autIdentifier the {@link AutIdentifier} from which you want to know the Toolkit
* @return the toolkit string or <code>null</code>
*/
private String getToolkitFromRestartHandler(AutIdentifier autIdentifier) {
IRestartAutHandler iRestartAutHandler = m_autIdToRestartHandler
.get(autIdentifier);
if (iRestartAutHandler != null) {
String startCommand = iRestartAutHandler
.getAUTStartClass();
if (StringUtils.containsIgnoreCase(startCommand, "swing")) { //$NON-NLS-1$
return CommandConstants.SWING_TOOLKIT;
} else if (StringUtils.containsIgnoreCase(startCommand, "swt")) { //$NON-NLS-1$
return CommandConstants.SWT_TOOLKIT;
} else if (StringUtils.containsIgnoreCase(startCommand, "rcp")) { //$NON-NLS-1$
return CommandConstants.RCP_TOOLKIT;
} else if (StringUtils.containsIgnoreCase(startCommand, "javafx")) { //$NON-NLS-1$
return CommandConstants.JAVAFX_TOOLKIT;
} else if (StringUtils.containsIgnoreCase(startCommand, "html")) { //$NON-NLS-1$
return CommandConstants.HTML_TOOLKIT;
}
}
return null;
}
}