/*******************************************************************************
* 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;
import java.awt.EventQueue;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jubula.autagent.agent.AutAgent;
import org.eclipse.jubula.autagent.i18n.Messages;
import org.eclipse.jubula.autagent.remote.dialogs.ChooseCheckModeDialogBP;
import org.eclipse.jubula.autagent.remote.dialogs.ObservationConsoleBP;
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.AutRegisteredMessage;
import org.eclipse.jubula.communication.internal.message.Message;
import org.eclipse.jubula.communication.internal.message.StartAUTServerStateMessage;
import org.eclipse.jubula.tools.internal.constants.AUTServerExitConstants;
import org.eclipse.jubula.tools.internal.constants.AUTStartResponse;
import org.eclipse.jubula.tools.internal.constants.StringConstants;
import org.eclipse.jubula.tools.internal.exception.CommunicationException;
import org.eclipse.jubula.tools.internal.exception.JBVersionException;
import org.eclipse.jubula.tools.internal.i18n.I18n;
import org.eclipse.jubula.tools.internal.registration.AutIdentifier;
import org.eclipse.jubula.tools.internal.utils.IsAliveThread;
import org.eclipse.jubula.tools.internal.utils.SysoRedirect;
import org.eclipse.osgi.util.NLS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The AutStarter for starting, watching and stopping the AUTServer.
*
* ExitCodes:
* <ul>
* <li>-1: invalid command line options</li>
* <li>0: option -h(elp)</li>
* <li>1: security violation when trying accepting connections</li>
* <li>2: io exception when trying accepting connections</li>
* <li>3: configuration error</li>
* </ul>
*
* @author BREDEX GmbH
* @created 26.07.2004
*
*/
public class AutStarter {
/**
* Possible values for determining how much output should be produced.
*/
public static enum Verbosity {
/** error messages will be printed to the console */
QUIET,
/** normal output */
NORMAL,
/** messages will be shown in dialogs */
VERBOSE
}
/** the logger */
private static Logger log = LoggerFactory.getLogger(AutStarter.class);
/** the instance */
private static AutStarter instance = null;
/** the communicator to use */
private Communicator m_communicator;
/** the communicator to use to communicate with AUTServer*/
private Communicator m_autCommunicator;
/**
* the timeout for killing the autServerVM when the connection to the
* JubulaClient was closed, defaults to 10 seconds
*/
private int m_stopAUTServerTimeout = 10000;
/** the AUT Agent that is used for AUT registration and de-registration */
private AutAgent m_agent;
/** sends messages using the Agent's communicator(s) */
private CommunicationHelper m_messenger;
/** controls the amount and form of output produced */
private Verbosity m_verbosity;
/**
* private constructor
*/
private AutStarter() {
super();
AutAgent agent = new AutAgent();
m_messenger = new CommunicationHelper();
// AUT Registration listener. Sends registration information to
// connected client.
agent.addPropertyChangeListener(
AutAgent.PROP_NAME_AUTS, new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
Communicator clientComm =
AutStarter.getInstance().getCommunicator();
if (clientComm == null
|| clientComm.getConnection() == null) {
// No connection. Do nothing.
return;
}
try {
Object newValue = evt.getNewValue();
if (newValue instanceof AutIdentifier) {
clientComm.send(new AutRegisteredMessage(
(AutIdentifier)newValue, true));
}
Object oldValue = evt.getOldValue();
if (oldValue instanceof AutIdentifier) {
clientComm.send(new AutRegisteredMessage(
(AutIdentifier)oldValue, false));
}
} catch (CommunicationException ce) {
log.error(Messages.RegistationSendingError, ce);
}
}
});
m_agent = agent;
}
/**
* starts watching the given process <br>
* @param process the process representing an AUT, must not be null
* @param isAgentSet true if executable file and agent are set.
* @param autId is the id of AUT
* @throws IllegalArgumentException if the given process is null
* @return false when no more server can watched (it's only one),
* true otherwise
*/
public boolean watchAUT(Process process, boolean isAgentSet,
AutIdentifier autId) throws IllegalArgumentException {
// check parameter
if (process == null) {
throw new IllegalArgumentException(Messages.ProcessMustNotBeNull);
}
// start thread waiting for termination
new AUTWatcher(process, isAgentSet, m_messenger, autId).start();
return true;
}
/**
* Method to get the single instance of this class.
*
* @return the instance of this Singleton
*/
public static AutStarter getInstance() {
if (instance == null) {
instance = new AutStarter();
}
return instance;
}
/**
* @return Returns the communicator.
*/
public synchronized Communicator getCommunicator() {
return m_communicator;
}
/**
* @param communicator
* The communicator to set.
*/
public synchronized void setCommunicator(Communicator communicator) {
m_communicator = communicator;
}
/**
* @return Returns the communicator.
*/
public synchronized Communicator getAutCommunicator() {
return m_autCommunicator;
}
/**
* @param communicator
* The communicator to set.
*/
public synchronized void setAutCommunicator(Communicator communicator) {
m_autCommunicator = communicator;
}
/**
* @return Returns the stopAUTServerTimeout.
*/
public int getStopAUTServerTimeout() {
return m_stopAUTServerTimeout;
}
/**
* sets the timeout used at killing the autServerProcess.
*
* @param stopAUTServerTimeout
* The stopAUTServerTimeout to set, for negative values zero is
* used.
*/
public void setStopAUTServerTimeout(int stopAUTServerTimeout) {
if (stopAUTServerTimeout < 0) {
m_stopAUTServerTimeout = 0;
} else {
m_stopAUTServerTimeout = stopAUTServerTimeout;
}
}
/**
* Start accepting connections. Depending on the value of
* <code>isBlocking</code>, this method will block execution until the
* AUT Agent is shutdown.
*
* @param port The port on which the AUT Agent should listen for incoming
* connections.
* @param killDuplicateAuts Whether AUTs attempting to register with an
* already registered AUT ID should be terminated.
* @param verbosity Controls the amount and form of output produced.
* @param isBlocking Whether the method should block execution until the
* AUT Agent is shutdown.
* @throws IOException
* @throws UnknownHostException
* @throws JBVersionException
*/
public void start(int port, boolean killDuplicateAuts,
Verbosity verbosity, boolean isBlocking)
throws UnknownHostException, IOException, JBVersionException {
m_verbosity = verbosity;
String infoMessage = I18n.getString("AUTAgent.StartErrorText"); //$NON-NLS-1$
Thread clientSocketThread = null;
try {
getAgent().setKillDuplicateAuts(killDuplicateAuts);
infoMessage = I18n.getString("AUTAgent.StartCommErrorText", //$NON-NLS-1$
new Object[] {StringConstants.EMPTY + port});
clientSocketThread = initClientConnectionSocket(port);
initAutConnectionSocket();
if (m_verbosity.compareTo(Verbosity.VERBOSE) >= 0) {
infoMessage = I18n.getString("AUTAgent.StartSuccessText") + //$NON-NLS-1$
getCommunicator().getLocalPort() + StringConstants.DOT;
} else {
infoMessage = StringConstants.EMPTY;
}
} finally {
// print information box to user
if (infoMessage.length() > 0) {
showUserInfo(infoMessage);
}
if (isBlocking && clientSocketThread != null) {
try {
clientSocketThread.join();
} catch (InterruptedException e) {
log.warn(Messages.InterruptedThread, e);
}
}
}
}
/**
* initializes the Socket for the client to connect.
*
* @param port
* int
*
* @return the Thread responsible for accepting connections.
*
* @throws IOException
* error
* @throws JBVersionException
* in case of version error between Client and AutStarter
*/
private Thread initClientConnectionSocket(int port)
throws IOException, JBVersionException {
Map<String, IConnectionInitializer> clientTypeToInitializer =
new HashMap<String, IConnectionInitializer>();
clientTypeToInitializer.putAll(m_agent.getConnectionInitializers());
clientTypeToInitializer.put(
ConnectionState.CLIENT_TYPE_COMMAND_SHUTDOWN,
new IConnectionInitializer() {
public void initConnection(Socket socket,
BufferedReader reader) {
Thread.currentThread().interrupt();
}
});
// create a communicator
setCommunicator(
new Communicator(port, this.getClass().getClassLoader(),
clientTypeToInitializer));
getCommunicator().addCommunicationErrorListener(
new CommunicationListener());
logRunning();
// start listening
logStartListening();
return getCommunicator().run();
}
/**
* initializes the Socket for the AUTServer to connect
*
* @throws IOException
* error
* @throws JBVersionException
* in case of a version error between Client and AutStarter
*/
private void initAutConnectionSocket() throws IOException,
JBVersionException {
// create a communicator on any free port
setAutCommunicator(new Communicator(0, this.getClass()
.getClassLoader()));
getAutCommunicator()
.addCommunicationErrorListener(new CommunicationListener());
getAutCommunicator().run();
}
/**
* @param infoMessage message to show
*/
private void showUserInfo(final String infoMessage) {
if (m_verbosity.compareTo(Verbosity.QUIET) <= 0) {
System.out.println(infoMessage);
} else {
try {
UIManager.setLookAndFeel(UIManager.
getSystemLookAndFeelClassName());
} catch (ClassNotFoundException e) { // NOPMD by al on 3/19/07 1:55 PM
// as this is just for the information box, ignore exceptions.
} catch (InstantiationException e) { // NOPMD by al on 3/19/07 1:55 PM
// as this is just for the information box, ignore exceptions.
} catch (IllegalAccessException e) { // NOPMD by al on 3/19/07 1:55 PM
// as this is just for the information box, ignore exceptions.
} catch (UnsupportedLookAndFeelException e) { // NOPMD by al on 3/19/07 1:55 PM
// as this is just for the information box, ignore exceptions.
}
Thread t = new IsAliveThread() {
public void run() {
try {
sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
EventQueue.invokeLater(new Runnable() {
public void run() {
JOptionPane.getRootFrame().dispose();
}
});
}
};
t.start();
JOptionPane.showMessageDialog(null, infoMessage
+ I18n.getString("AUTAgent.dialogClose"), //$NON-NLS-1$
I18n.getString("AUTAgent.failedStartDialogTitle"), //$NON-NLS-1$
JOptionPane.INFORMATION_MESSAGE);
}
}
/**
* private method which prints a start message (java version)
*/
private void logRunning() {
if (log.isInfoEnabled()) {
log.info(NLS.bind(Messages.RunningVM,
System.getProperty("java.version"))); //$NON-NLS-1$
}
}
/**
* private method which prints a 'listening' message (the port number
* listening to)
*/
private void logStartListening() {
if (log.isInfoEnabled()) {
log.info(NLS.bind(Messages.ListeningToPort,
getCommunicator().getLocalPort()));
}
}
/**
*
* @return The AUT Agent responsible for managing running AUTs.
*/
public AutAgent getAgent() {
return m_agent;
}
/**
* A thread waiting for termination of the autServerVM. Puts the exit code
* into m_autExitValue and handle the termination.
*
* @author BREDEX GmbH
* @created 03.08.2004
*/
private static class AUTWatcher extends IsAliveThread {
/** lock for synchronizing on m_autServerVM */
private final Object m_autServerLock = new Object();
/**
* the started VM the AUTServer running in, it's null, when no AutServer
* was started
*/
private Process m_autProcess;
/** the exit value of the VM the AUTServer is running in */
private int m_autExitValue;
/** used to pick up the 'Unrecognized option' error stream */
private String m_errorMessage;
/** base error message of the process execution */
private String m_errorLog;
/**
* whether the server is expecting the AUT server to stop. Used for
* deciding whether to report the stop as an error.
*/
private boolean m_isExpectingAUTServerStop;
/** whether the AUT was started using the Java agent mechanism */
private boolean m_isAgentSet;
/** sends messages concerning the AUT Server */
private CommunicationHelper m_messenger;
/** Id of AUT */
private AutIdentifier m_autId;
/**
* Constructor
*
* @param autProcess The process in which the AUT and AUT Server
* are running.
* @param isAgentSet Whether the AUT was started using the Java agent
* mechanism.
* @param autId the id of AUT
* @param messenger Sends messages concerning the AUT Server.
*/
public AUTWatcher(Process autProcess, boolean isAgentSet,
CommunicationHelper messenger, AutIdentifier autId) {
super("AUTWatcher"); //$NON-NLS-1$
m_autProcess = autProcess;
m_isAgentSet = isAgentSet;
m_messenger = messenger;
m_autId = autId;
}
/**
* handles the termination of the AUTServer, uses m_autExitValue
*/
private void handleStoppedAUTServer() {
if (log.isInfoEnabled()) {
log.info(NLS.bind(Messages.SendMessageToClient,
m_autExitValue));
}
StartAUTServerStateMessage message = null;
ChooseCheckModeDialogBP.getInstance().closeDialog();
ObservationConsoleBP.getInstance().closeShell();
switch (m_autExitValue) {
case AUTServerExitConstants.EXIT_OK:
log.info(Messages.RegularTermination);
break;
case AUTServerExitConstants.AUT_START_ERROR:
message = new StartAUTServerStateMessage(
AUTStartResponse.ERROR, Messages.AutStartError);
break;
case AUTServerExitConstants.EXIT_INVALID_ARGS:
if (m_isAgentSet && (m_errorMessage != null)) {
message = new StartAUTServerStateMessage(
AUTStartResponse.JDK_INVALID, Messages.InvalidJDK);
} else {
message = createInvalidArgumentMessage();
}
break;
case AUTServerExitConstants.EXIT_INVALID_NUMBER_OF_ARGS:
message = new StartAUTServerStateMessage(AUTStartResponse
.INVALID_ARGUMENTS, Messages.InvalidNumOfArguments);
break;
case AUTServerExitConstants.EXIT_UNKNOWN_ITE_CLIENT:
message = new StartAUTServerStateMessage(AUTStartResponse
.COMMUNICATION, Messages.UnknowIteClient);
break;
case AUTServerExitConstants.EXIT_COMMUNICATION_ERROR:
message = new StartAUTServerStateMessage(AUTStartResponse.
COMMUNICATION, Messages.CommunicationError);
break;
case AUTServerExitConstants
.EXIT_SECURITY_VIOLATION_AWT_EVENT_LISTENER:
case AUTServerExitConstants
.EXIT_SECURITY_VIOLATION_COMMUNICATION:
case AUTServerExitConstants.EXIT_SECURITY_VIOLATION_REFLECTION:
case AUTServerExitConstants.EXIT_SECURITY_VIOLATION_SHUTDOWN:
message = new StartAUTServerStateMessage(AUTStartResponse
.SECURITY, Messages.SecuritiViolation);
break;
case AUTServerExitConstants.EXIT_AUT_NOT_FOUND:
case AUTServerExitConstants.EXIT_MISSING_AGENT_INFO:
// do nothing : AUTServer sent already a message
break;
case AUTServerExitConstants.EXIT_AUT_WRONG_CLASS_VERSION:
message = new StartAUTServerStateMessage(AUTStartResponse.
UNSUPPORTED_CLASS, Messages.UnsupportedClass);
break;
case AUTServerExitConstants.RESTART:
message = m_messenger.handleAutRestart();
break;
case AUTServerExitConstants.AUT_START_ADDRESS_ALREADY_IN_USE:
message = new StartAUTServerStateMessage(AUTStartResponse
.COMMUNICATION, Messages.AddressAlreadyInUse);
break;
default:
message = handleGlobalError();
}
message.setAutId(m_autId);
appendErrorLog(message);
m_messenger.sendStoppedAUTServerMessage(message);
}
/**
* Append the error log to the aut server state message, the
* error log is availabe
* @param message message to hold the error log
*/
private void appendErrorLog(StartAUTServerStateMessage message) {
if (StringUtils.isNotBlank(m_errorLog)) {
StringBuilder builder = new StringBuilder(
message.getDescription());
builder.append(StringConstants.NEWLINE);
builder.append(StringConstants.SPACE);
builder.append(m_errorLog);
message.setDescription(builder.toString());
}
}
/**
* Create an aut server state message with the details of error
* @return message
*/
private StartAUTServerStateMessage createInvalidArgumentMessage() {
return new StartAUTServerStateMessage(AUTStartResponse
.INVALID_ARGUMENTS, Messages.InvalidArguments);
}
/**
* Handle global error
* @return aut server state message
*/
private StartAUTServerStateMessage handleGlobalError() {
String message = NLS.bind(Messages.UnknowExitCode, m_autExitValue);
log.error(message);
return new StartAUTServerStateMessage(
AUTStartResponse.ERROR, message);
}
/**
* {@inheritDoc}
*/
public void run() {
SysoRedirect dn;
try {
// clear the streams of the autServerVM
synchronized (m_autServerLock) {
dn = new SysoRedirect(m_autProcess.getErrorStream(),
Messages.AutsSysError);
dn.start();
new SysoRedirect(m_autProcess.getInputStream(),
Messages.AutsSysOut).start();
}
// don't synchronized, catching NullPointerException which is
// raised if the process has already input terminated
m_autExitValue = m_autProcess.waitFor();
// picking up the 'Unrecognized option' error stream
m_errorMessage = dn.getLine();
m_errorLog = dn.getTruncatedLog();
synchronized (m_autServerLock) {
m_autProcess = null;
}
if (log.isInfoEnabled()) {
log.info(NLS.bind(Messages.VmStopped, m_autExitValue));
}
if (!m_isExpectingAUTServerStop) {
handleStoppedAUTServer();
}
m_isExpectingAUTServerStop = false;
} catch (InterruptedException ie) {
log.info(Messages.ObservingInterrupted, ie);
} catch (NullPointerException npe) {
log.debug(Messages.TerminatedProcess, npe);
}
}
}
/**
* Helper class for sending messages through the AutStarter's
* communicator(s).
*
* @author BREDEX GmbH
* @created Feb 25, 2010
*/
private class CommunicationHelper {
/**
* sends a message via communicator if the AutServer has stopped
* @param message the message to send
*/
public void sendStoppedAUTServerMessage(
StartAUTServerStateMessage message) {
if (message != null) {
try {
getCommunicator().send(message);
} catch (CommunicationException bce) { // NOPMD by al on 3/19/07 1:55 PM
// communication already closed, do nothing
} catch (NullPointerException npe) { // NOPMD by al on 3/19/07 1:56 PM
// communication already closed, do nothing
}
}
}
/**
* Handles the restart of the AUT(Server) while test execution
* @return StartAUTServerStateMessage
*/
public StartAUTServerStateMessage handleAutRestart() {
StartAUTServerStateMessage message = null;
getAutCommunicator().close();
getAutCommunicator().getConnectionManager()
.remove(getAutCommunicator().getConnection());
try {
initAutConnectionSocket();
} catch (JBVersionException e) {
message = new StartAUTServerStateMessage(AUTStartResponse.
COMMUNICATION, Messages.VersionException);
} catch (IOException e) {
message = new StartAUTServerStateMessage(AUTStartResponse
.COMMUNICATION, Messages.IoException);
}
return message;
}
}
/**
* Inner class listening for closing connections. In case of a shutdown the
* communicator is restarted.
*
* @author BREDEX GmbH
* @created 26.07.2004
*/
private class CommunicationListener
implements ICommunicationErrorListener {
/**
* {@inheritDoc}
*/
public void connectingFailed(InetAddress inetAddress, int port) {
log.error(Messages.ConnectionErrorInServer);
}
/**
* {@inheritDoc}
*/
public void connectionGained(InetAddress inetAddress, int port) {
if (log.isInfoEnabled()) {
try {
log.info(NLS.bind(Messages.AcceptedConnectionFrom,
new Object[]{inetAddress.getHostName(), port}));
} catch (SecurityException se) {
log.warn(Messages.SecuritiViolationWhileGettingHostName,
se);
}
}
}
/**
* {@inheritDoc}
*/
public void acceptingFailed(int port) {
log.error(NLS.bind(Messages.AcceptingFailed, port));
}
/**
* {@inheritDoc}
*/
public void sendFailed(Message message) {
log.warn(NLS.bind(Messages.SendingMessageFailed, message));
}
/**
* {@inheritDoc}
*/
public void shutDown() {
log.info(Messages.ConnectionClosed);
}
}
}