/******************************************************************************
 JMDIServer.cc

	Base class for handling Multiple Document Interface (MDI) requests.
	It creates a UNIX domain socket so all new invocations of the application
	will be passed to us (via WillBeMDIServer()).

	If the application supports MDI, it must create a derived class and
	construct it with a unique application signature.

	To guarantee that your application signature is unique, you should
	register it with jx-registry@alice.wonderland.caltech.edu
	(This is a free service.  We simply check whether anybody else has
	 already registered the signature.)

	The derived class must implement the following functions:

		CanAcceptMDIReqest
			Return kFalse if the application is busy and cannot accept
			the request (e.g. due to re-entrancy problems).

		HandleMDIRequest
			The arguments to this function are the directory from which the
			MDI request was made and argv[].

	BASE CLASS = NONE

	Copyright © 1997 by John Lindal. All rights reserved.

 ******************************************************************************/

#include <JMDIServer.h>
#include <ace/LSOCK_Acceptor.h>
#include <ace/LSOCK_Connector.h>
#include <ace/LSOCK_Stream.h>
#include <ace/UNIX_Addr.h>
#include <JString.h>
#include <jUNIXUtil.h>
#include <jFileUtil.h>
#include <jDirUtil.h>
#include <stdlib.h>
#include <stdio.h>
#include <jAssert.h>

const JSize kMDIServerQSize    = 1;
const JSize kMaxWaitSeconds    = 5;
const JCharacter kEndOfLine    = '\n';
const JCharacter kEndOfMessage = '\0';

static const JCharacter* kServerReadyMsg = "JMDIServer ready";
static const JCharacter* kServerBusyMsg  = "JMDIServer busy";

/******************************************************************************
 Constructor

 ******************************************************************************/

JMDIServer::JMDIServer
	(
	const JCharacter* signature
	)
{
	const JString socketName = GetMDISocketName(signature);
	ACE_OS::unlink(socketName);

	ACE_UNIX_Addr addr(socketName);
	itsAcceptor = new ACE_LSOCK_Acceptor(addr, 0, PF_UNIX, kMDIServerQSize);
	assert( itsAcceptor != NULL );

	itsSocket = new ACE_LSOCK_Stream;
	assert( itsSocket != NULL );
}

/******************************************************************************
 Destructor

 ******************************************************************************/

JMDIServer::~JMDIServer()
{
	itsSocket->close();
	delete itsSocket;

	itsAcceptor->remove();
	delete itsAcceptor;
}

/******************************************************************************
 WillBeMDIServer (static)

	If the application supports MDI, main() should call this before creating
	an application object.  If this function returns kFalse, main() can
	exit immediately.

 ******************************************************************************/

JBoolean
JMDIServer::WillBeMDIServer
	(
	const JCharacter*	signature,
	const int			argc,
	char*				argv[]
	)
{
	assert( argc >= 1 );

	// If the socket doesn't exist, we will be the server.

	const JString socketName = GetMDISocketName(signature);
	if (!JUNIXSocketExists(socketName))
		{
		if (JNameUsed(socketName))
			{
			cerr << "Unable to initiate MDI because " << socketName << endl;
			cerr << "exists as something else." << endl;
			exit(1);
			}
		return kTrue;
		}

	// open a connection to the existing server

	ACE_UNIX_Addr addr(socketName);
	ACE_LSOCK_Connector connector;
	ACE_LSOCK_Stream socket;
	ACE_Time_Value maxWait(kMaxWaitSeconds);
	if (connector.connect(socket, addr, &maxWait) == -1)
		{
		ACE_OS::unlink(socketName);
		return kTrue;
		}

	JBoolean receivedFinishedFlag = kFalse;

	// wait for "server ok" message

	JString serverStatus;
	const JBoolean serverOK =
		ReceiveLine(socket, &serverStatus, &receivedFinishedFlag);
	if (!serverOK && !JUNIXSocketExists(socketName))		// user deleted dead socket
		{
		socket.close();
		return kTrue;
		}
	else if (!serverOK && ACE_OS::unlink(socketName) == -1)
		{
		cerr << "Unable to transmit MDI request." << endl;
		cerr << socketName << "appears to be dead," << endl;
		cerr << "but an error ocurred while trying to delete it." << endl;
		exit(1);
		}
	else if (!serverOK)
		{
		socket.close();
		return kTrue;
		}

	if (serverStatus == kServerBusyMsg)
		{
		cerr << argv[0] << " is busy, probably because of a blocking window." << endl;
		cerr << "(e.g. a dialog or an error message)" << endl;
		WaitForFinished(socket, receivedFinishedFlag);
		return kFalse;
		}

	assert( serverStatus == kServerReadyMsg );

	// send our message

	const JString dir = JGetCurrentDirectory();
	SendLine(socket, dir);

	for (JIndex i=0; i < (JSize) argc; i++)
		{
		SendLine(socket, argv[i]);
		}

	WaitForFinished(socket, receivedFinishedFlag);
	return kFalse;
}

/******************************************************************************
 HandleCmdLineOptions

	Convenience function to convert the command line options from the
	first invocation of the program into an MDI request.  This allows the
	argument parsing code to be in one place.

 ******************************************************************************/

void
JMDIServer::HandleCmdLineOptions
	(
	const int	argc,
	char*		argv[]
	)
{
	const JString dir = JGetCurrentDirectory();

	JPtrArray<JString> argList;
	for (JIndex i=0; i < (JIndex) argc; i++)
		{
		JString* arg = new JString(argv[i]);
		assert( arg != NULL );
		argList.Append(arg);
		}

	HandleMDIRequest(dir, argList);

	argList.DeleteAll();
}

/******************************************************************************
 CheckForConnections

	By having an existing ACE_LSOCK_Stream, accept() is as fast as possible.

 ******************************************************************************/

void
JMDIServer::CheckForConnections()
{
	ACE_Time_Value dontWait(0,0);
	if (itsAcceptor->accept(*itsSocket, NULL, &dontWait) != -1)
		{
		ProcessMDIMessage();
		}
}

/******************************************************************************
 ProcessMDIMessage (private)

	Unpacks MDI request and calls HandleMDIRequest().
	The socket is closed before returning.

 ******************************************************************************/

void
JMDIServer::ProcessMDIMessage()
{
	JBoolean receivedFinishedFlag = kFalse;

	// tell them our status

	if (CanAcceptMDIRequest())
		{
		SendLine(*itsSocket, kServerReadyMsg);
		}
	else
		{
		SendLine(*itsSocket, kServerBusyMsg);
		WaitForFinished(*itsSocket, receivedFinishedFlag);
		return;
		}

	// receive their message

	JString dir;
	if (!ReceiveLine(*itsSocket, &dir, &receivedFinishedFlag))
		{
		itsSocket->close();
		return;
		}

	JPtrArray<JString> argList;
	JString tempStr;
	while (ReceiveLine(*itsSocket, &tempStr, &receivedFinishedFlag))
		{
		JString* arg = new JString(tempStr);
		assert( arg != NULL );
		argList.Append(arg);
		}

	WaitForFinished(*itsSocket, receivedFinishedFlag);

	if (argList.IsEmpty())
		{
		return;
		}

	// handle the request

	HandleMDIRequest(dir, argList);

	// clean up

	argList.DeleteAll();
}

/******************************************************************************
 GetMDISocketName (static private)

 ******************************************************************************/

JString
JMDIServer::GetMDISocketName
	(
	const JCharacter* signature
	)
{
	const JString tmpName = tmpnam(NULL);
	assert( tmpName.GetFirstCharacter() == '/' );	// require absolute path

	JString path,name;
	JSplitPathAndName(tmpName, &path, &name);

	name  = path;
	name += ".";
	name += signature;
	name += "_MDI_";
	name += JGetUserName();
	return name;
}

/******************************************************************************
 SendLine (static private)

 ******************************************************************************/

void
JMDIServer::SendLine
	(
	ACE_LSOCK_Stream&	socket,
	const JCharacter*	line
	)
{
	socket.send_n(line, strlen(line));

	JCharacter c = kEndOfLine;
	socket.send_n(&c, 1);
}

/******************************************************************************
 ReceiveLine (static private)

	Returns kFalse if the connection times out.

 ******************************************************************************/

JBoolean
JMDIServer::ReceiveLine
	(
	ACE_LSOCK_Stream&	socket,
	JString*			line,
	JBoolean*			receivedFinishedFlag
	)
{
	line->Clear();

	while (!(*receivedFinishedFlag))
		{
		JCharacter c;
		const ACE_Time_Value maxWait(kMaxWaitSeconds);
		const int result = socket.recv(&c, 1, 0, &maxWait);
		if (result == 1 && c == kEndOfLine)
			{
			return kTrue;
			}
		else if (result == 1 && c == kEndOfMessage)
			{
			*receivedFinishedFlag = kTrue;
			return kFalse;
			}
		else if (result == 1)
			{
			line->AppendCharacter(c);
			}
		else if (result == -1)
			{
			return kFalse;
			}
		}

	return kFalse;
}

/******************************************************************************
 WaitForFinished (static private)

	This tosses all unread data.

 ******************************************************************************/

void
JMDIServer::WaitForFinished
	(
	ACE_LSOCK_Stream&	socket,
	const JBoolean		receivedFinishedFlag
	)
{
	// Tell the other end that we are finished.

	JCharacter c = kEndOfMessage;
	socket.send_n(&c, 1);

	// To avoid a broken pipe error, we have to wait until they are also finished.
	// (Note that this also returns if recv() returns -1.)

	if (!receivedFinishedFlag)
		{
		while (socket.recv(&c, 1) == 0 || c != kEndOfMessage)
			{ };
		}

	socket.close();
}
