/***************************************************************************
*                                                                          *
* SktSocket.m                                                              *
* Copyright 1992 by Nik A Gervae                                           *
*                                                                          *
* One of a set of three Objective-C classes (SktSocketManager, SktSocket,  *
* and SktSocketUser) which implement a convenient interface to Berkeley    *
* stream sockets under NeXTSTEP(r).  See the accompanying class            *
* specifications (files with a .rtf or .spec suffix) for further           *
* information.                                                             *
*                                                                          *
* NeXTSTEP is a registered trademark of NeXT Computer, Inc.                *
*                                                                          *
****************************************************************************
*                                                                          *
* LICENSE                                                                  *
*                                                                          *
* This program 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.                                            *
*                                                                          *
* The program and this makefile are distributed in the hope that it will   *
* be useful, but are provided "AS IS" AND WITHOUT ANY WARRANTY; without    *
* any express or implied warranty of MERCHANTABILITY or FITNESS FOR A      *
* PARTICULAR PURPOSE. See the GNU General Public License for more details. *
* Any use or distribution of the program and documentation must include    *
* appropriate copyrights to acknowledge Nik A. Gervae and the Free         *
* Software Foundation, Inc.                                                *
*                                                                          *
* You should have received a copy of the GNU General Public License        *
* along with this program; if not, write to the Free Software              *
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.                *
*                                                                          *
****************************************************************************
*                                                                          *
* VERSION HISTORY                                                          *
*                                                                          *
* Version numbers are simply dates in the form YYYYMMDD.  These represent  *
* the date that version was finished.  Only significantly changed versions *
* are reported here, or those versions requiring explanation of changes.   *
* There may be many interim stages between dated versions.                 *
*                                                                          *
* DateVersion Primary Author  Notes                                        *
* ----------- --------------- -------------------------------------------- *
* 19920327    Nik A Gervae    First released version                       *
* 19920723    Nik A Gervae    Actually released                            *
*                                                                          *
***************************************************************************/

#import <errno.h>
#import <limits.h>
#import <stdio.h>
#import <string.h>

#import <sys/types.h>
#import <sys/socket.h>
#import <sys/time.h>
#import <netinet/in.h>
#import <netdb.h>
#import <fcntl.h>

#import "SktSocket.h"


typedef struct {
  @defs(SktSocketUser)
} sktsocketuser;

/*
 * See +initialize below for info on these.
 */
typedef id (*qOutputFunc)(id self, SEL _cmd, const char *output,
                                 long int length, ...);
static qOutputFunc   outputToSocket;

/***************************************************************************
*                                                                          *
* SktReportSocketMemError()                                                *
*                                                                          *
* Report appropriately based on whether the process is a server (has an    *
* SktSocketManager), or a client.                                          *
*                                                                          *
***************************************************************************/
void SktReportSocketMemError(id self, char *message) {

  id manager;

  manager = [self manager];

  if (manager) {
    [manager log:message, [self name], [self socketFd]];
  }
  else if (stderr) {
    fprintf(stderr, message, [self name], [self socketFd]);
  }
  return;
}

/***************************************************************************
*                                                                          *
* These are the constant strings used.  Feel free to translate them into   *
* your favorite language.  Do be sure to keep all the % directives in      *
* place, or change the code that accesses these strings.                   *
*                                                                          *
***************************************************************************/
#define STR_ErrorNoFd       "ERROR (%s): "
#define STR_ErrorWithFd     "ERROR (%s %d): "

#define STR_CantAccept      STR_ErrorNoFd "accept() failed.\n"
#define STR_CantSetFNDELAY  STR_ErrorWithFd "fcntl() can\'t set no delay.\n"
#define STR_CantSetOpts     STR_ErrorWithFd "unable to set sender\'s " \
                             "options.\n"
#define STR_NewConnect      "(%s) New connection from %s [%s] " \
                             "(socketFd %d)\n"
#define STR_UnknownParens   "(unknown)"
#define STR_Unknown         "unknown"
#define STR_CantOpenSocket  STR_ErrorNoFd "could not open socket\n"
#define STR_UnknownHost     STR_ErrorNoFd "%s -- unknown host %s\n"
#define STR_CantConnectToServer STR_ErrorWithFd \
                                 "could not connect to server\n"
#define STR_ReadError       STR_ErrorWithFd "read()\n"
#define STR_NoMoreInput     "(%s %d) readInput -- no more input\n"
#define STR_WriteError      STR_ErrorWithFd "write()\n"

#define STR_ReallocFatalError  STR_ErrorWithFd "zone realloc failed.\n"
#define STR_MallocFatalError   STR_ErrorWithFd "zone malloc failed.\n"



@implementation SktSocket

/***************************************************************************
*                                                                          *
* +initialize                                                              *
*                                                                          *
* This method caches the function pointers for the queueOutput:ofLength:   *
* method.  Since these methods are covered by other methods, we want to    *
* avoid a message send each time a cover is used.  This way, we only incur *
* the cost of a function call. Hopefully that's faster. :-)                *
*                                                                          *
***************************************************************************/
+ initialize
{
  static BOOL initted = NO;

 /*
  * Only do this if we're initializing the class itself.  Having a
  * subclass do it only resets the static variables, which is a waste.
  */
  if (NO == initted) {
    outputToSocket = (qOutputFunc)
      [[SktSocket class] instanceMethodFor:@selector(queueOutput:ofLength:)];
    initted = YES;
  }
  return self;
}

/***************************************************************************
*                                                                          *
* -init                                                                    *
*                                                                          *
***************************************************************************/
- init
{
  return [self notImplemented:_cmd];
}

/***************************************************************************
*                                                                          *
* -_init                                                                   *
*                                                                          *
* Don't ever send this yourself.  It simply provides common functionality  *
* for the other three init... methods.                                     *
*                                                                          *
* ERROR CONDITION: If the allocation of the output queue fails, nil is     *
* returned.                                                                *
*                                                                          *
***************************************************************************/
- _init
{
  [super init];

  socketFd    = -1;   // A Clearly invalid value, for now.
  manager     = nil;
  user        = nil;
  hostaddress = NULL;
  hostname    = NULL;

  zone        = [self zone];

  outputQueue = (char *)NXZoneMalloc(zone, OUTQSIZE);
  if (!outputQueue) {
    SktReportSocketMemError(self, STR_MallocFatalError);
    return [self free];
  }
  queueLength = 0;

  return self;
}

/***************************************************************************
*                                                                          *
* -initOnFd:withManager:options:                                           *
*                                                                          *
* THIS IS ONE OF THREE DESIGNATED INITIALIZERS FOR THIS CLASS.  DO NOT     * 
* EVER USE -init AS A DESIGNATED INITIALIZER.                              *
*                                                                          *
* This is the designated initializer for SktSocket objects in the server   *
* program.  It initializes the socket to accept a connection on a file     *
* descriptor, and records the SktSocketManager for further communication.  *
*                                                                          *
* ERROR CONDITIONS: The following conditions cause a return of nil:        *
*                                                                          *
* - No manager provided.                                                   *
* - The socket can't be created/accepted.                                  *
* - setSocketOptions: returns nil.                                         *
* - The FNDELAY option can't be set.                                       *
*                                                                          *
***************************************************************************/
- initOnFd:(int)serviceSocketFd withManager:(SktSocketManager *)aManager 
{
  struct sockaddr_in  peername;
  struct hostent     *peerinfo;
  int                 i;

  [self _init];

 /*
  * Do this right away!  (We can't log if there's no manager.)
  */
  if (aManager) manager = aManager;
  else return [self free];

 /*
  * Accept the connection to get the file desctiptor.
  */
  i = sizeof(peername);
  socketFd = accept(serviceSocketFd, (struct sockaddr *) &peername, &i);

  if (0 > socketFd) {
    [manager log:STR_CantAccept, [[self class] name]];
    return [self free];
  }

 /*
  * Set options on the socket.  We have to do this
  * AFTER the accept(), or accept() will return -1.
  */
  if (![self setSocketOptions:socketFd]) {
    [manager log:STR_CantSetOpts, [self name], socketFd];
    return [self free];
  }

 /*
  * Get the address of the connected host.  If the string duplication
  * fails, report it, but don't return nil.  (This may need changing.)
  */
  hostaddress = zoneStrdup(zone, (char *)inet_ntoa(peername.sin_addr));
  if (!hostaddress) {
    SktReportSocketMemError(self, STR_MallocFatalError);
  }

 /*
  * Now muck around to get the name.  Copy the name of the connected
  * host.  If the string duplication fails, report it, but don't
  * return nil.  (This may need changing.)
  */
/// I think there's a bug in here (call to gethostbyaddr()) somewhere.
  peerinfo = gethostbyaddr((char *)&peername.sin_addr,
                           sizeof(peername.sin_addr), AF_INET);

  if (!peerinfo) hostname = NULL;
  else hostname = zoneStrdup(zone, peerinfo->h_name);
  if (!hostname) {
    SktReportSocketMemError(self, STR_MallocFatalError);
  }

 /*
  * Log the connection.
  */
  if (manager && [manager doesLog]) {
    [manager log:STR_NewConnect, [[self class] name],
                 (hostname == NULL ? STR_UnknownParens : hostname),
                 (hostaddress == NULL ? STR_Unknown : hostaddress),
                 socketFd];
  }
  return self;

} /*initOnFd:withManager:options:*/

/***************************************************************************
*                                                                          *
* -initOnHostname:andPort:options:                                         *
*                                                                          *
* THIS IS ONE OF THREE DESIGNATED INITIALIZERS FOR THIS CLASS.  DO NOT     * 
* EVER USE -init AS A DESIGNATED INITIALIZER.                              *
*                                                                          *
* This is the designated initializer for SktSocket objects in a client     *
* program.  It initializes the socket to request a connection to a host by *
* name and port.                                                           *
*                                                                          *
* ERROR CONDITIONS: The following conditions cause a return of nil:        *
*                                                                          *
* - No hostname provided, or host info can't be accessed.                  *
* - The socket can't be created/connected.                                 *
* - setSocketOptions: returns nil.                                         *
* - The FNDELAY option can't be set.                                       *
*                                                                          *
***************************************************************************/
- initOnHostname:(char *)hostName andPort:(int)port
{
  struct sockaddr_in server;
  struct hostent *host;

  if (!hostName) return [self free];

  [self _init];

 /*
  * Get a new file descriptor.
  */
  socketFd = socket(AF_INET, SOCK_STREAM, 0);
  if (0 > socketFd) {
    if (stderr) fprintf(stderr, STR_CantOpenSocket, [[self class] name]);
    return [self free];
  }

 /*
  * Try to get the host from the name.
  */
  server.sin_family = AF_INET;
  host = gethostbyname(hostName);
  if (!host) {
    if (stderr) fprintf(stderr, STR_UnknownHost, [[self class] name],
                        hostName);
    return [self free];
  }

 /*
  * Now try to connect.
  */
  memcpy(&server.sin_addr, host->h_addr, host->h_length);
  server.sin_port = htons(port);
  if (0 > connect(socketFd, (struct sockaddr *)&server, sizeof(server))) {
    if (stderr) fprintf(stderr, STR_CantConnectToServer,
                       [[self class] name], socketFd);
    return [self free];
  }

 /*
  * Set options on the socket.  We have to do this
  * AFTER the connect(), or connect() will return -1.
  */
  if (![self setSocketOptions:socketFd]) {
    if (stderr) fprintf(stderr, STR_CantSetOpts, [self name], socketFd);
    return [self free];
  }


 /*
  * Save the host name and address.  If a string duplication fails,
  * report it, but don't return nil.  (This may need changing.)
  */
  hostname = zoneStrdup(zone, hostName);
  if (!hostname) SktReportSocketMemError(self, STR_MallocFatalError);

  hostaddress = zoneStrdup(zone, (char *)inet_ntoa(host->h_addr));
  if (!hostaddress) SktReportSocketMemError(self, STR_MallocFatalError);

  return self;

} /*initOnHostname:andPort:options:*/

/***************************************************************************
*                                                                          *
* -initOnAddress:andPort:options:                                          *
*                                                                          *
* THIS IS ONE OF THREE DESIGNATED INITIALIZERS FOR THIS CLASS.  DO NOT     * 
* EVER USE -init AS A DESIGNATED INITIALIZER.                              *
*                                                                          *
* This is the designated initializer for SktSocket objects in a client     *
* program.  It initializes the socket to request a connection to a host by *
* internet address (NULL-terminated string in dot notation and port.       *
*                                                                          *
* ERROR CONDITIONS: The following conditions cause a return of nil:        *
*                                                                          *
* - No host address provided, or host info can't be accessed.              *
* - The socket can't be created/connected.                                 *
* - setSocketOptions: returns nil.                                         *
* - The FNDELAY option can't be set.                                       *
*                                                                          *
***************************************************************************/
- initOnAddress:(char *)hostAddress andPort:(int)port
{
  struct sockaddr_in  server;
  long int            hostaddr;
  struct hostent     *host;

  [self _init];

 /*
  * Get a new file descriptor.
  */
  socketFd = socket(AF_INET, SOCK_STREAM, 0);
  if (0 > socketFd) {
    if (stderr) fprintf(stderr, STR_CantOpenSocket, [[self class] name]);
    [self free];
  }

 /*
  * Get host info based on the address.
  */
  hostaddr = inet_addr(hostAddress);
  host = gethostbyaddr((char *)&hostaddr, sizeof(hostaddr), AF_INET);
  if (!host && stderr) {
    if (stderr) fprintf(stderr, STR_UnknownHost, [[self class] name],
                        hostAddress);
    return [self free];
  }

 /*
  * Now try to connect.
  */
  server.sin_family = AF_INET;
  memcpy(&server.sin_addr, host->h_addr, host->h_length);
  server.sin_port = htons(port);
  if (0 > connect(socketFd, (struct sockaddr *)&server, sizeof(server))) {
    if (stderr) fprintf(stderr, STR_CantConnectToServer,
                        [[self class] name]);
    return [self free];
  }

 /*
  * Set options on the socket.  We have to do this
  * AFTER the connect(), or connect() will return -1.
  */
  if (![self setSocketOptions:socketFd]) {
    if (stderr) fprintf(stderr, STR_CantSetOpts, [self name], socketFd);
    return [self free];
  }

 /*
  * Save the host name and address.  If a string duplication fails,
  * report it, but don't return nil.  (This may need changing.)
  */
  hostname = zoneStrdup(zone, host->h_name);
  if (!hostname) SktReportSocketMemError(self, STR_MallocFatalError);

  hostaddress = zoneStrdup(zone, hostAddress);
  if (!hostaddress) SktReportSocketMemError(self, STR_MallocFatalError);

  return self;

} /*initOnAddress:andPort:options:*/

/***************************************************************************
*                                                                          *
* -setSocketOptions:                                                       *
*                                                                          *
* This method tries to set the FNDELAY option on the socket so it can      *
* handle asynchronous writes.  It may be overriden in subclasses to set    *
* other socket/file descriptor options via setsockopt/fcntl.               *
*                                                                          *
* If this method returns nil, then any initialization is aborted and the   *
* init... method will return nil.                                          *
*                                                                          *
***************************************************************************/
- setSocketOptions:(int)fd
{
  if (-1 == fcntl(fd, F_SETFL, FNDELAY)) {
    if (manager) [manager log:STR_CantSetFNDELAY, [[self class] name], fd];
    if (stderr) fprintf(stderr, STR_CantSetFNDELAY, [[self class] name], fd);
    return nil;
  }

  return self;
}

/***************************************************************************
*                                                                          *
* -close                                                                   *
*                                                                          *
* Properly closes the socket by notifying the SktSocketManager if there is *
* one (which will send it a -free message), otherwise just freeing.        *
*                                                                          *
***************************************************************************/
- close
{
  if (manager) [manager closeSocket:self];
  else return [self free];

  return nil;  // Just in case;
}

/***************************************************************************
*                                                                          *
* -free                                                                    *
*                                                                          *
* Really closes the socket.  Does all the ususal stuff too.  Does not free *
* the user; it is responsible for noticing its socket is gone, and taking  *
* appropriate action.                                                      *
*                                                                          *
***************************************************************************/
- free
{
  if (hostaddress) NXZoneFree(zone, hostaddress);
  if (hostname)    NXZoneFree(zone, hostname);
  if (outputQueue) NXZoneFree(zone, outputQueue);

  close(socketFd);
  if (user && self == [user socket]) [user setSocket:nil];
  return [super free];
}

/***************************************************************************
*                                                                          *
* -setUser:                                                                *
*                                                                          *
* Sets the user object used to that supplied.  This method also mucks with *
* the old and new users, to properly remove self as the old user's socket  *
* without causing an infinite loop.  This is doable with methods, but the  *
* classes are so tightly bound, it's not really worth the code bulk or     *
* cost of a message send.                                                  *
*                                                                          *
***************************************************************************/
- setUser:(SktSocketUser *)aUser
{
  SktSocketUser *oldUser;

  oldUser = user;
  user = aUser;

 /*
  * We need to muck with pointers because messaging would cause an
  * infinite loop.
  */
  if (oldUser) ((sktsocketuser *)oldUser)->socket = nil;
  if (user) ((sktsocketuser *)user)->socket = self;
  return oldUser;
}

/***************************************************************************
*                                                                          *
* -user                                                                    *
*                                                                          *
* Returns the user (an instance of SktSocketUser or a subclass) associated *
* with this SktSocket.                                                     *
*                                                                          *
***************************************************************************/
- (SktSocketUser *)user
{
  return user;
}

/***************************************************************************
*                                                                          *
* -socketFd                                                                *
*                                                                          *
* Returns the file descriptor for the socket.                              *
*                                                                          *
***************************************************************************/
- (int) socketFd
{
  return socketFd;
}

/***************************************************************************
*                                                                          *
* -manager                                                                 *
*                                                                          *
* If the SktSocket has a manager (which it does if it's on the server),    *
* this returns it.                                                         *
*                                                                          *
***************************************************************************/
- (struct SktSocketManager *)manager
{
  return manager;
}

/***************************************************************************
*                                                                          *
* -hostaddress                                                             *
*                                                                          *
* Returns the address of the host the SktSocket's process ison, in dot     *
* notation.                                                                *
*                                                                          *
***************************************************************************/
- (const char *)hostaddress
{
  return hostaddress;
}

/***************************************************************************
*                                                                          *
* -hostname                                                                *
*                                                                          *
* Returns the name of the host the SktSocket's process is on.              *
*                                                                          *
***************************************************************************/
- (const char *)hostname
{
  return hostname;
}

/***************************************************************************
*                                                                          *
* -readInput                                                               *
*                                                                          *
* Performs a read(2) on the socketFd, and has the user, if any, queue the  *
* data read as input.  If there is no user, or an error other than         *
* EWOULDBLOCK occurs, or if the socketFd is at EOF, return nil.            *
*                                                                          *
* All the log messages from this method are sent only to the manager; that *
* only, only when the SktSocket is in a server process.                    *
*                                                                          *
***************************************************************************/
- readInput
{
  char   buf[BUFSIZ];
  int    bytesread = 0;

  if (![self user]) return nil;

  if (0 < (bytesread = read(socketFd, buf, BUFSIZ))) {
    [[self user] queueInput:(const char *)buf ofLength:bytesread];
    return self;
  }
  else if (0 > bytesread) {
    if (EWOULDBLOCK == errno) return self;
    else {      
      if (manager) [manager log:STR_ReadError,
                    [[self class] name], socketFd];
      else if (stderr) fprintf(stderr, STR_ReadError,
                    [[self class] name], socketFd);
      return nil;
    }
  }
  else  {
    if (manager && [manager doesLog]) {
      [manager log:STR_NoMoreInput, [[self class] name], socketFd];
    }
    return nil;
  }
}

/***************************************************************************
*                                                                          *
* -queueOutput:ofLength:                                                   *
*                                                                          *
* Adds output to the output queue and adjusts the length.                  *
*                                                                          *
* ERROR CONDITION: If the reallocation fails, a message is logged, and nil *
* is returned.  The output queue won't exist if this method returns nil,   *
* so you should consider a nil return as a fatal condition and either free *
* the object, or exit the program.                                         *
*                                                                          *
***************************************************************************/
- queueOutput:(const char *)output ofLength:(long int)length
{
  unsigned int fullLength;

 /*
  * I hope nobody is this stupid.
  */
  if (0 >= length) return self;

 /*
  * Find out how much text we're dealing with, check if we need more space,
  * and smash 'em together.  Also update the new queue length.
  */
  fullLength = length + queueLength;

  if (malloc_size(outputQueue) <= fullLength) {
    outputQueue = (char *)NXZoneRealloc(zone, outputQueue, fullLength);
    if (!outputQueue) {
      SktReportSocketMemError(self, STR_ReallocFatalError);
      queueLength = 0;
      return nil;
    }
  }
  memcpy(outputQueue+queueLength, output, length);

  queueLength = fullLength;

  return self;
}

/***************************************************************************
*                                                                          *
* -queueOutputString:                                                      *
*                                                                          *
* A convenience for queueing NULL-terminated strings.                      *
*                                                                          *
***************************************************************************/
- queueOutputString:(const char *)aString
{
  return (*outputToSocket)(self, @selector(queueOutput:ofLength:),
                           aString, strlen(aString));
}

/***************************************************************************
*                                                                          *
* -flushOutput                                                             *
*                                                                          *
* Flushes the output queue to the socketFd.  Doesn't try to write after an *
* EWOULDBLOCK error occurs.                                                *
*                                                                          *
* All the log messages from this method are sent only to the manager; that *
* only, only when the SktSocket is in a server process.                    *
*                                                                          *
* ERROR CONDITION: If the reallocation fails, a message is logged, and nil *
* is returned.  The output queue won't exist if this method returns nil,   *
* so you should consider a nil return as a fatal condition and either free *
* the object, or exit the program.                                         *
*                                                                          *
***************************************************************************/
- flushOutput
{
  long int   writeLength;  // how much to write
  long int   written;      // how much written so far
  int        writeResult;  // how much written this write()
  extern int errno;        // in case Something Bad happens

  writeLength = queueLength;
  written = 0;

 /*
  * Big loop until all output is writte, an EWOULDBLOCK occurs, or
  * an other error occurs.
  */
  while (written < writeLength) {

   /*
    * Be careful with the args to write here.  It expects int's,
    * not long int's or differences thereof.
    */
    writeResult = write(socketFd, (outputQueue + written),
                        INT_MAX <= (writeLength - written) ?
                        INT_MAX : (writeLength - written));

   /*
    * If we get an EWOULDBLOCK, don't hang around trying to write; we'll
    * have another chance later.  If the error is something else, log it.
    */
    if (0 > writeResult) {

      if (EWOULDBLOCK != errno) {
        if (manager)
          [manager log:STR_WriteError, [[self class] name], socketFd];
        else if (stderr)
          fprintf(stderr, STR_WriteError, [[self class] name], socketFd);

        break;
      }
    }

    written += writeResult;

  } /*while(written < writeLength)*/


 /*
  * Shrink the queue if it's empty and really big, or just pull it back.
  */
  if (written >= writeLength) {
    if (malloc_size(outputQueue) > OUTQSIZE) {
      outputQueue = (char *)NXZoneRealloc(zone, outputQueue, OUTQSIZE);
      if (!outputQueue) {
        SktReportSocketMemError(self, STR_ReallocFatalError);
        queueLength = 0;
        return nil;
      }
    }
  }
  else {
    memmove(outputQueue, outputQueue+written, writeLength-written+1);
  }

 /*
  * Update the queue length.
  */
  queueLength = writeLength - written;

  return self;

} /*flushOutput*/

/***************************************************************************
*                                                                          *
* -purgeOutput                                                             *
*                                                                          *
* Marks the queue as empty, and shrinks it if needed.                      *
*                                                                          *
***************************************************************************/
- purgeOutput
{
  queueLength = 0;

  if (malloc_size(outputQueue) > OUTQSIZE) {

    outputQueue = (char *)NXZoneRealloc(zone, outputQueue, OUTQSIZE);

    if (!outputQueue) {
      SktReportSocketMemError(self, STR_ReallocFatalError);
      queueLength = 0;
      return nil;
    }
  }

  return self;
}

@end /*implementation SktSocket*/

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