/*
This file is part of Fantom.
Fantom is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Fantom is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Fantom. If not, see <http://www.gnu.org/licenses/>.
*/
package cz.matfyz.aai.fantom.server;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.log4j.Logger;
import cz.matfyz.aai.fantom.message.ClientType;
import cz.matfyz.aai.fantom.message.Message;
import cz.matfyz.aai.fantom.message.MessageReady;
import cz.matfyz.aai.fantom.message.MessagingClientBase;
/**
* Maintains the client process and provides method for messaging
* with the client.
*/
public class Client extends MessagingClientBase {
/**
* The logger used by this class.
*/
protected Logger logger = Logger.getLogger(Server.LOGGER_NAME);
/**
* The tokenized command line used to start the client process.
*/
private String[] commandLine;
/**
* The name of the client.
*/
private String clientName;
/**
* Caches the full name of the client, as soon as it is known (after initializing
* the communication with the client).
*
* @see #getFullClientName()
*/
private String fullName;
/**
* The process of the client.
*/
private Process clientProcess;
/**
* The type of the client (detective/phantom).
*/
private ClientType clientType;
/**
* Output logger used to redirect standard error output of the
* client process to a file.
*/
private OutputLogger clientErrLogger;
/**
* The name of the current run.
*/
private String runName;
/**
* Specifies whether the messages between the server and the clients
* should be logged to a file.
*/
private boolean logMessages;
/**
* Ends the client process.
* @throws ServerException if there is a problem with ending
* the client process.
*/
public void endProcess() throws ServerException {
if(clientProcess == null)
return;
try {
closeStreams();
int exit = clientProcess.waitFor();
if(exit != 0)
System.err.format("Client '%s' terminated with exit code %d\n", getFullClientName(), exit);
}
catch (IOException e) {
throw new ServerException("Could not close the streams", e);
}
catch (InterruptedException e) {
throw new ServerException("Interrupted", e);
}
}
/**
* Returns (the name of) the file that contains (or should contain) the logs
* from the client. In the current run. The name of the file is determined
* using the name of the current run, and the type of the client.
*
* @return the file object that points to the file that is used to store logs
* from the client in the current run.
* @throws IOException
*/
public File getClientLogFile() throws IOException {
String fileName = String.format("%s-%s.log", runName, getClientType().toString());
return new File(fileName);
}
/**
* Returns the name of the file that contains the logs of messages going between
* the server and the client in current run. The name of the file is determined
* using the name of the run and the type of the client.
*
* @return the file object that points to the file that is used to store message
* logs.
* @throws IOException
*/
public File getClientMessageLogFile() throws IOException {
String fileName = String.format("%s-%s.message.log", runName, getClientType().toString());
return new File(fileName);
}
/**
* Returns the name of the client (as sent by the client in the beginning of the
* client-server communication).
* @return the name of the client.
*/
public String getClientName() {
return this.clientName;
}
/**
* Returns the type of the client.
* @return the type of the client.
*/
public ClientType getClientType() {
return this.clientType;
}
/**
* Returns a full name of the client. This consists of the name of the client and
* the command-line arguments used to run the client.
* @return the full name of the client.
*/
public String getFullClientName() {
if(fullName == null) {
StringBuilder sb = new StringBuilder();
if(clientName != null && !clientName.isEmpty()) {
sb.append(clientName);
sb.append(" : ");
}
for(String clPart : commandLine) {
if(sb.length() > 0)
sb.append(' ');
sb.append(clPart);
}
// Do not store the full name, unless the name reported by the client is
// known - this method gets called both before and after the client reports
// its name.
if(clientName == null || clientName.isEmpty())
return sb.toString();
fullName = sb.toString();
}
return fullName;
}
/**
* Kills the client process and closes the associated streams. This should
* only be used, if the game ends in an unexpected way.
*/
public void killProcess() {
if(clientProcess == null)
return;
clientProcess.destroy();
try {
closeStreams();
}
catch(IOException e) {
}
try {
if(clientErrLogger != null)
clientErrLogger.wait(1000);
}
catch(Exception e) {
}
}
/**
* Starts the client process and reads the name of the client.
*
* @throws ServerException if there is a problem with starting the
* client process, or with communication with the client.
*/
public void startProcess() throws ServerException {
try {
logger.debug("Starting client process");
clientProcess = Runtime.getRuntime().exec(commandLine);
} catch (IOException e) {
throw new ClientException("The client process was not started", this);
}
try {
logger.debug("Redirecting input and output for the clients");
FileOutputStream clientMessageLogger = null;
if(logMessages)
clientMessageLogger = new FileOutputStream(getClientMessageLogFile());
setStreams(clientProcess.getInputStream(), clientProcess.getOutputStream(), clientMessageLogger);
logger.debug("Redirecting standard error output of the client");
clientErrLogger = new OutputLogger(clientProcess.getErrorStream(), getClientLogFile());
clientErrLogger.start();
} catch (IOException e) {
throw new ServerException("Initialization of the communication with the client failed");
}
logger.debug("Waiting for 'ready' from the client");
Message msg = receiveMessage(null);
if(!(msg instanceof MessageReady))
throw new ProtocolException("The 'ready' message was expected", this);
MessageReady msgReady = (MessageReady)msg;
this.clientName = msgReady.getClientName();
logger.debug("Client connected: " + this.clientName);
if(this.clientType != msgReady.getClientType())
throw new ClientException("Expected client type " + this.clientType
+ " does not match the real client type " + msgReady.getClientType(), this);
}
/**
* Initializes a new client.
*
* @param commandLine the tokenized command line used to start the client.
*/
public Client(String[] commandLine, ClientType clientType, String runName, boolean logMessages) {
if(commandLine == null || commandLine.length == 0)
throw new IllegalArgumentException("The command for running the client was not provided");
if(runName == null)
throw new IllegalArgumentException("No run name was specified");
if(clientType == null)
throw new IllegalArgumentException("No client type was specified");
this.commandLine = commandLine;
this.clientType = clientType;
this.runName = runName;
this.logMessages = logMessages;
}
}