
/** Enterprise Control Configuration and Logging

   Copyright (C) 2012 Free Software Foundation, Inc.

   Written by: Richard Frith-Macdonald <rfm@gnu.org>
   Date: Febrary 2010
   Originally developed from 1996 to 2012 by Brainstorm, and donated to
   the FSF.

   This file is part of the GNUstep project.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 3 of the License, or (at your option) any later version.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, write to the Free
   Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02111 USA.

   */

#import <Foundation/Foundation.h>

#import "EcProcess.h"
#import "EcAlarm.h"
#import "EcClientI.h"
#import "EcHost.h"
#import "NSFileHandle+Printf.h"

#import "config.h"

#define	DLY	300.0

static const NSTimeInterval   day = 24.0 * 60.0 * 60.0;

static int	tStatus = 0;

static NSTimeInterval	pingDelay = 240.0;

static int	comp_len = 0;

static int	comp(NSString *s0, NSString *s1)
{
  if ([s0 length] > [s1 length])
    {
      comp_len = -1;
      return -1;
    }
  if ([s1 compare: s0
	  options: NSCaseInsensitiveSearch|NSLiteralSearch
	    range: NSMakeRange(0, [s0 length])] == NSOrderedSame)
    {
      comp_len = [s0 length];
      if (comp_len == (int)[s1 length])
	{
	  return 0;
	}
      else
	{
	  return 1;
	}
    }
  else
    {
      comp_len = -1;
      return -1;
    }
}

static NSString*	cmdWord(NSArray* a, unsigned int pos)
{
  if (a != nil && [a count] > pos)
    {
      return [a objectAtIndex: pos];
    }
  else
    {
      return @"";
    }
}

/* Special configuration options are:
 *
 * CompressLogsAfter
 *   A positive integer number of days after which logs should be compressed
 *   defaults to 14.
 *
 * DeleteLogsAfter
 *   A positive integer number of days after which logs should be deleted.
 *   Constrained to be at least as large as CompressLogsAfter.
 *   Defaults to 1000, but logs may still be deleted as if this were set
 *   to CompressLogsAfter if NodesFree or SpaceFree is reached.
 *
 * Environment
 *   A dictionary setting the default environment for launched processes.
 *
 * Launch
 *   A dictionary describing the processes which the server is responsible
 *   for launching.
 *
 * NodesFree
 *   A string giving a percentage of the total nodes on the disk below
 *   which an alert should be raised. Defaults to 10.
 *   Minimum 2, Maximum 90.
 *
 * SpaceFree
 *   A string giving a percentage of the total space on the disk below
 *   which an alert should be raised. Defaults to 10.
 *   Minimum 2, Maximum 90.
 *
 */
@interface	EcCommand : EcProcess <Command>
{
  NSString		*host;
  id<Control>		control;
  NSMutableArray	*clients;
  NSTimer		*timer;
  NSString		*logname;
  NSMutableDictionary	*config;
  NSDictionary		*launchInfo;
  NSArray               *launchOrder;
  NSDictionary		*environment;
  NSMutableDictionary	*launches;
  NSMutableDictionary   *launching;
  unsigned		pingPosition;
  NSTimer		*terminating;
  NSDate		*lastUnanswered;
  unsigned		fwdSequence;
  unsigned		revSequence;
  float			nodesFree;
  float			spaceFree;
  NSTimeInterval        debUncompressed;
  NSTimeInterval        debUndeleted;
  NSTimeInterval        logUncompressed;
  NSTimeInterval        logUndeleted;
  BOOL                  sweeping;
}
- (NSFileHandle*) openLog: (NSString*)lname;
- (void) cmdGnip: (id <CmdPing>)from
	sequence: (unsigned)num
	   extra: (NSData*)data;
- (void) cmdPing: (id <CmdPing>)from
	sequence: (unsigned)num
	   extra: (NSData*)data;
- (oneway void) cmdQuit: (NSInteger)sig;
- (void) command: (NSData*)dat
	      to: (NSString*)t
	    from: (NSString*)f;
- (NSData *) configurationFor: (NSString *)name;
- (BOOL) connection: (NSConnection*)ancestor
  shouldMakeNewConnection: (NSConnection*)newConn;
- (id) connectionBecameInvalid: (NSNotification*)notification;
- (NSArray*) findAll: (NSArray*)a
      byAbbreviation: (NSString*)s;
- (EcClientI*) findIn: (NSArray*)a
       byAbbreviation: (NSString*)s;
- (EcClientI*) findIn: (NSArray*)a
               byName: (NSString*)s;
- (EcClientI*) findIn: (NSArray*)a
             byObject: (id)s;
- (void) information: (NSString*)inf
		from: (NSString*)s
		type: (EcLogType)t;
- (void) information: (NSString*)inf
		from: (NSString*)s
		  to: (NSString*)d
		type: (EcLogType)t;
- (void) killAll;
- (void) launch;
- (void) logMessage: (NSString*)msg
	       type: (EcLogType)t
	        for: (id<CmdClient>)o;
- (void) logMessage: (NSString*)msg
	       type: (EcLogType)t
	       name: (NSString*)c;
- (NSString*) makeSpace;
- (void) newConfig: (NSMutableDictionary*)newConfig;
- (void) pingControl;
- (void) quitAll;
- (void) requestConfigFor: (id<CmdConfig>)c;
- (NSData*) registerClient: (id<CmdClient>)c
		      name: (NSString*)n;
- (NSData*) registerClient: (id<CmdClient>)c
		      name: (NSString*)n
		 transient: (BOOL)t;
- (void) reply: (NSString*) msg to: (NSString*)n from: (NSString*)c;
- (NSArray*) restartAll;
- (void) terminate;
- (void) timedOut: (NSTimer*)t;
- (void) unregisterByObject: (id)obj;
- (void) unregisterByName: (NSString*)n;
- (void) update;
- (void) updateConfig: (NSData*)data;
@end

@implementation	EcCommand

- (oneway void) alarm: (in bycopy EcAlarm*)alarm
{
  NS_DURING
    {
      [control alarm: alarm];
    }
  NS_HANDLER
    {
      NSLog(@"Exception sending alarm to Control: %@", localException);
    }
  NS_ENDHANDLER
}

- (oneway void) domanage: (in bycopy NSString*)managedObject
{
  NS_DURING
    {
      [control domanage: managedObject];
    }
  NS_HANDLER
    {
      NSLog(@"Exception sending domanage: to Control: %@", localException);
    }
  NS_ENDHANDLER
}

- (oneway void) unmanage: (in bycopy NSString*)managedObject
{
  NS_DURING
    {
      [control unmanage: managedObject];
    }
  NS_HANDLER
    {
      NSLog(@"Exception sending unmanage: to Control: %@", localException);
    }
  NS_ENDHANDLER
}

- (NSFileHandle*) openLog: (NSString*)lname
{
  NSFileManager	*mgr = [NSFileManager defaultManager];
  NSFileHandle	*lf;

  if ([mgr isWritableFileAtPath: lname] == NO)
    {
      if ([mgr createFileAtPath: lname
		       contents: nil
		     attributes: nil] == NO)
	{
	  NSLog(@"Log file '%@' is not writable and can't be created", lname);
	  return nil;
	}
    }

  lf = [NSFileHandle fileHandleForUpdatingAtPath: lname];
  if (lf == nil)
    {
      NSLog(@"Unable to log to %@", lname);
      return nil;
    }
  [lf seekToEndOfFile];
  return lf;
}

- (void) newConfig: (NSMutableDictionary*)newConfig
{
  NSString	*diskCache;
  NSData	*data;

  diskCache = [[self cmdDataDirectory]
    stringByAppendingPathComponent: @"CommandConfig.cache"];

  if (NO == [newConfig isKindOfClass: [NSMutableDictionary class]]
    || 0 == [newConfig count])
    {
      /* If we are called with a nil argument, we must obtain the config
       * from local disk cache (if available).
       */
      if (nil != (data = [NSData dataWithContentsOfFile: diskCache]))
	{
	  newConfig = [NSPropertyListSerialization
	    propertyListWithData: data
	    options: NSPropertyListMutableContainers
	    format: 0
	    error: 0];
	}
      if (NO == [newConfig isKindOfClass: [NSMutableDictionary class]]
	|| 0 == [newConfig count])
	{
	  return;
	}
    }
  else
    {
      data = nil;
    }

  if (nil == config || [config isEqual: newConfig] == NO)
    {
      NSDictionary	*d;
      NSArray		*a;
      unsigned		i;

      ASSIGN(config, newConfig);

      d = [config objectForKey: [self cmdName]];
      DESTROY(launchInfo);
      DESTROY(launchOrder);
      DESTROY(environment);
      if ([d isKindOfClass: [NSDictionary class]] == YES)
	{
          id                    o;
          NSMutableDictionary   *m;
	  NSString              *k;
          NSString              *err = nil;

          m = [[d mutableCopy] autorelease];
          d = m;
          NS_DURING
            [self cmdUpdate: m];
          NS_HANDLER
            NSLog(@"Problem before updating config (in cmdUpdate:) %@",
              localException);
            err = @"the -cmdUpdate: method raised an exception";
          NS_ENDHANDLER
          if (nil == err)
            {
              NS_DURING
                err = [self cmdUpdated];
              NS_HANDLER
                NSLog(@"Problem after updating config (in cmdUpdated) %@",
                  localException);
                err = @"the -cmdUpdated method raised an exception";
              NS_ENDHANDLER
            }
          if ([err length] > 0)
            {
              EcAlarm       *a;

              /* Truncate additional text to fit if necessary.
               */
              err = [err stringByTrimmingSpaces];
              if ([err length] > 255)
                {
                  err = [err substringToIndex: 255];
                  while (255 < strlen([err UTF8String]))
                    {
                      err = [err substringToIndex: [err length] - 1];
                    }
                }
              a = [EcAlarm alarmForManagedObject: nil
                at: nil
                withEventType: EcAlarmEventTypeProcessingError
                probableCause: EcAlarmConfigurationOrCustomizationError
                specificProblem: @"configuration error"
                perceivedSeverity: EcAlarmSeverityMajor
                proposedRepairAction:
                _(@"Correct config or software (check log for details).")
                additionalText: err];
              [self alarm: a];
            }
          else
            {
              EcAlarm       *a;

              a = [EcAlarm alarmForManagedObject: nil
                at: nil
                withEventType: EcAlarmEventTypeProcessingError
                probableCause: EcAlarmConfigurationOrCustomizationError
                specificProblem: @"configuration error"
                perceivedSeverity: EcAlarmSeverityCleared
                proposedRepairAction: nil
                additionalText: nil];
              [self alarm: a];
            }

	  launchInfo = [d objectForKey: @"Launch"];
	  if ([launchInfo isKindOfClass: [NSDictionary class]] == NO)
	    {
	      NSLog(@"No 'Launch' information in latest config update");
	      launchInfo = nil;
	    }
	  else
	    {
	      NSEnumerator	*e = [launchInfo keyEnumerator];

	      while ((k = [e nextObject]) != nil)
		{
		  NSDictionary	*d = [launchInfo objectForKey: k];
		  id		o;

		  if ([d isKindOfClass: [NSDictionary class]] == NO)
		    {
		      NSLog(@"bad 'Launch' information for %@", k);
		      launchInfo = nil;
		      break;
		    }
		  o = [d objectForKey: @"Auto"];
		  if (o != nil && [o isKindOfClass: [NSString class]] == NO)
		    {
		      NSLog(@"bad 'Launch' Auto for %@", k);
		      launchInfo = nil;
		      break;
		    }
		  o = [d objectForKey: @"Disabled"];
		  if (o != nil && [o isKindOfClass: [NSString class]] == NO)
		    {
		      NSLog(@"bad 'Launch' Disabled for %@", k);
		      launchInfo = nil;
		      break;
		    }
		  o = [d objectForKey: @"Args"];
		  if (o != nil && [o isKindOfClass: [NSArray class]] == NO)
		    {
		      NSLog(@"bad 'Launch' Args for %@", k);
		      launchInfo = nil;
		      break;
		    }
		  o = [d objectForKey: @"Home"];
		  if (o != nil && [o isKindOfClass: [NSString class]] == NO)
		    {
		      NSLog(@"bad 'Launch' Home for %@", k);
		      launchInfo = nil;
		      break;
		    }
		  o = [d objectForKey: @"Prog"];
		  if (o == nil || [o isKindOfClass: [NSString class]] == NO)
		    {
		      NSLog(@"bad 'Launch' Prog for %@", k);
		      launchInfo = nil;
		      break;
		    }
		  o = [d objectForKey: @"AddE"];
		  if (o != nil && [o isKindOfClass: [NSDictionary class]] == NO)
		    {
		      NSLog(@"bad 'Launch' AddE for %@", k);
		      launchInfo = nil;
		      break;
		    }
		  o = [d objectForKey: @"SetE"];
		  if (o != nil && [o isKindOfClass: [NSDictionary class]] == NO)
		    {
		      NSLog(@"bad 'Launch' SetE for %@", k);
		      launchInfo = nil;
		      break;
		    }
		}
	    }
	  RETAIN(launchInfo);

          o = [d objectForKey: @"LaunchOrder"];
          if (NO == [o isKindOfClass: [NSArray class]])
            {
              if (nil != o)
                {
                  NSLog(@"bad 'LaunchOrder' config (not an array) ignored");
                }
              /* The default launch order is alphabetical by server name.
               */
              o = [[launchInfo allKeys] sortedArrayUsingSelector:
                @selector(compare:)];
              launchOrder = RETAIN(o);
            }
          else
            {
              NSMutableArray    *m;
              NSEnumerator      *e;
              NSString          *k;
              NSUInteger        c;

              m = AUTORELEASE([o mutableCopy]);
              c = [m count];
              while (c-- > 0)
                {
                  o = [m objectAtIndex: c];
                  if (NO == [o isKindOfClass: [NSString class]])
                    {
                      NSLog(@"bad 'LaunchOrder' item ('%@' at %u) ignored"
                        @" (not a server name)", o, (unsigned)c);
                      [m removeObjectAtIndex: c];
                    }
                  else if ([m indexOfObject: o] != c)
                    {
                      NSLog(@"bad 'LaunchOrder' item ('%@' at %u) ignored"
                        @" (repeat of earlier item)", o, (unsigned)c);
                      [m removeObjectAtIndex: c];
                    }
                  else if (nil == [launchInfo objectForKey: o])
                    {
                      NSLog(@"bad 'LaunchOrder' item ('%@' at %u) ignored"
                        @" (not in 'Launch' dictionary)", o, (unsigned)c);
                      [m removeObjectAtIndex: c];
                    }
                }
              /* Any missing servers are launched after others
               * they are in lexicographic order.
               */
              o = [[launchInfo allKeys] sortedArrayUsingSelector:
                @selector(compare:)];
              e = [o objectEnumerator];
              while (nil != (k = [e nextObject]))
                {
                  if (NO == [m containsObject: k])
                    {
                      [m addObject: k];
                    }
                }
              launchOrder = [m copy];
            }

	  environment = [d objectForKey: @"Environment"];
	  if ([environment isKindOfClass: [NSDictionary class]] == NO)
	    {
	      NSLog(@"No 'Environment' information in latest config update");
	      environment = nil;
	    }
	  RETAIN(environment);

	  k = [d objectForKey: @"NodesFree"];
	  if (YES == [k isKindOfClass: [NSString class]])
	    {
	      nodesFree = [k floatValue];
	      nodesFree /= 100.0;
	    }
	  else
	    {
	      nodesFree = 0.0;
	    }
	  if (nodesFree < 0.02 || nodesFree > 0.9)
	    {
	      NSLog(@"bad or missing minimum disk 'NodesFree' ... using 10%%");
	      nodesFree = 0.1;
	    }
	  k = [d objectForKey: @"SpaceFree"];
	  if (YES == [k isKindOfClass: [NSString class]])
	    {
	      spaceFree = [k floatValue];
	      spaceFree /= 100.0;
	    }
	  else
	    {
	      spaceFree = 0.0;
	    }
	  if (spaceFree < 0.02 || spaceFree > 0.9)
	    {
	      NSLog(@"bad or missing minimum disk 'SpaceFree' ... using 10%%");
	      spaceFree = 0.1;
	    }
	}
      else
	{
	  NSLog(@"No '%@' information in latest config update", [self cmdName]);
	}

      a = [NSArray arrayWithArray: clients];
      i = [a count];
      while (i-- > 0)
	{ 
	  EcClientI	*c = [a objectAtIndex: i];

	  if ([clients indexOfObjectIdenticalTo: c] != NSNotFound)
	    {
	      NS_DURING
		{
		  NSData	*d = [self configurationFor: [c name]];

		  if (nil != d)
		    {
		      [c setConfig: d];
		      [[c obj] updateConfig: d];
		    }
		}
	      NS_HANDLER
		{
		  NSLog(@"Setting config for client: %@", localException);
		}
	      NS_ENDHANDLER
	    }
	}
      if (nil == data)
	{
	  /* Need to update on-disk cache
	   */
	  data = [NSPropertyListSerialization
	    dataFromPropertyList: newConfig
	    format: NSPropertyListBinaryFormat_v1_0
	    errorDescription: 0];
	  [data writeToFile: diskCache atomically: YES];
	}
    }
}

- (void) pingControl
{
  if (control == nil)
    {
      return;
    }
  if (fwdSequence == revSequence)
    {
      lastUnanswered = RETAIN([NSDate date]);
      NS_DURING
	{
	  [control cmdPing: self sequence: ++fwdSequence extra: nil];
	}
      NS_HANDLER
	{
	  NSLog(@"Ping to control server - %@", localException);
	}
      NS_ENDHANDLER
    }
  else
    {
      NSLog(@"Ping to control server when one is already in progress.");
    }
}

- (void) cmdGnip: (id <CmdPing>)from
	sequence: (unsigned)num
	   extra: (NSData*)data
{
  if (from == control)
    {
      if (num != revSequence + 1 && revSequence != 0)
	{
	  NSLog(@"Gnip from control server seq: %u when expecting %u",
	    num, revSequence);
	  if (num == 0)
	    {
	      fwdSequence = 0;	// Reset
	    }
	}
      revSequence = num;
      if (revSequence == fwdSequence)
	{
	  DESTROY(lastUnanswered);
	}
    }
  else
    {
      EcClientI	*r;

      /* See if we have a fitting client - and update records.
       */
      r = [self findIn: clients byObject: (id)from];
      if (r != nil)
	{
          NSString	*managedObject;
          NSString      *n;
          EcAlarm	*a;

	  [r gnip: num];
          n = [r name];

          /* After the first ping response from a client we assume
           * that client has completed startup and is running OK.
           * We can therefore clear any loss of client alarm, any
           * alarm for being unable to register, and launch failure
           * or fatal configuration alarms.
           */
          managedObject = EcMakeManagedObject(host, n, nil);
          a = [EcAlarm alarmForManagedObject: managedObject
            at: nil
            withEventType: EcAlarmEventTypeProcessingError
            probableCause: EcAlarmSoftwareProgramAbnormallyTerminated
            specificProblem: @"Process availability"
            perceivedSeverity: EcAlarmSeverityCleared
            proposedRepairAction: nil
            additionalText: nil];
          [self alarm: a];
          a = [EcAlarm alarmForManagedObject: managedObject
            at: nil
            withEventType: EcAlarmEventTypeProcessingError
            probableCause: EcAlarmSoftwareProgramAbnormallyTerminated
            specificProblem: @"Unable to register"
            perceivedSeverity: EcAlarmSeverityCleared
            proposedRepairAction: nil
            additionalText: nil];
          [self alarm: a];
          [self clearConfigurationFor: managedObject
                      specificProblem: @"Process launch"
                       additionalText: @"Process is now running"];
          [self clearConfigurationFor: managedObject
                      specificProblem: @"Fatal configuration error"
                       additionalText: @"Process is now running"];
	}
    }
}

- (BOOL) cmdIsClient
{
  return NO;	// Not a client of the Command server.
}

- (void) cmdPing: (id <CmdPing>)from
	sequence: (unsigned)num
	   extra: (NSData*)data
{
  /* Send back a response to let the other party know we are alive.
   */
  [from cmdGnip: self sequence: num extra: nil];
}

- (oneway void) cmdQuit: (NSInteger)sig
{
  if (sig == tStatus && control != nil)
    {
      NS_DURING
	{
	  [control unregister: self];
	}
      NS_HANDLER
	{
	  NSLog(@"Exception unregistering from Control: %@", localException);
	}
      NS_ENDHANDLER
    }
  exit(sig);
}

- (void) command: (NSData*)dat
	      to: (NSString*)t
	    from: (NSString*)f
{
  NSMutableArray	*cmd = [NSPropertyListSerialization
    propertyListWithData: dat
    options: NSPropertyListMutableContainers
    format: 0
    error: 0];

  if (cmd == nil || [cmd count] == 0)
    {
      [self information: cmdLogFormat(LT_ERROR, @"bad command array")
		   from: nil
		     to: f
		   type: LT_ERROR];
    }
  else if (t == nil)
    {
      NSString	*m = @"";
      NSString	*wd = cmdWord(cmd, 0);

      if ([wd length] == 0)
	{
	  /* Quietly ignore.	*/
	}
      else if (comp(wd, @"archive") >= 0)
	{
	  NSCalendarDate	*when = [NSCalendarDate date];
	  NSString		*sub;
	  int			yy, mm, dd;

	  yy = [when yearOfCommonEra];
	  mm = [when monthOfYear];
	  dd = [when dayOfMonth];
	  
	  sub = [NSString stringWithFormat: @"%04d-%02d-%02d", yy, mm, dd];
	  m = [NSString stringWithFormat: @"\n%@\n\n", [self cmdArchive: sub]];
	}
      else if (comp(wd, @"help") >= 0)
	{
	  wd = cmdWord(cmd, 1);
	  if ([wd length] == 0)
	    {
	      m = @"Commands are -\n"
	      @"Help\tArchive\tControl\tLaunch\tList\tMemory\t"
              @"Quit\tRestart\tTell\n\n"
	      @"Type 'help' followed by a command word for details.\n"
	      @"A command line consists of a sequence of words, "
	      @"the first of which is the command to be executed. "
	      @"A word can be a simple sequence of non-space characters, "
	      @"or it can be a 'quoted string'. "
	      @"Simple words are converted to lower case before "
	      @"matching them against commands and their parameters. "
	      @"Text in a 'quoted string' is NOT converted to lower case "
	      @"but a '\\' character is treated in a special manner -\n"
	      @"  \\b is replaced by a backspace\n"
	      @"  \\f is replaced by a formfeed\n"
	      @"  \\n is replaced by a linefeed\n"
	      @"  \\r is replaced by a carriage-return\n"
	      @"  \\t is replaced by a tab\n"
	      @"  \\0 followed by up to 3 octal digits is replaced"
	      @" by the octal value\n"
	      @"  \\x followed by up to 2 hex digits is replaced"
	      @" by the hex value\n"
	      @"  \\ followed by any other character is replaced by"
	      @" the second character.\n"
	      @"  This permits use of quotes and backslashes inside"
	      @" a quoted string.\n";
	    }
	  else
	    {
	      if (comp(wd, @"Archive") >= 0)
		{
		  m = @"Archive\nArchives the log file. The archived log "
		      @"file is stored in a subdirectory whose name is of "
		      @"the form YYYYMMDDhhmmss being the date and time at "
		      @"which the archive was created.\n";
		}
	      else if (comp(wd, @"Control") >= 0)
		{
		  m = @"Control ...\nPasses the command to the Control "
		      @"process.  You may disconnect from this host by "
		      @"typing 'control host'\n";
		}
	      else if (comp(wd, @"Launch") >= 0)
		{
		  m = @"Launch <name>\nAdds the named program to the list "
		      @"of programs to be launched as soon as possible.\n"
		      @"Launch all\nAdds all unlaunched programs which do "
		      @"not have autolaunch disabled.\n";
		}
	      else if (comp(wd, @"List") >= 0)
		{
		  m = @"List\nLists all the connected clients.\n"
		      @"List launches\nLists the programs we can launch.\n";
		}
	      else if (comp(wd, @"Memory") >= 0)
		{
		  m = @"Memory\nDisplays recent memory allocation stats.\n"
		      @"Memory all\nDisplays all memory allocation stats.\n";
		}
	      else if (comp(wd, @"Quit") >= 0)
		{
		  m = @"Quit 'name'\n"
		      @"Shuts down the named client process(es).\n"
		      @"Quit all\n"
		      @"Shuts down all client processes.\n"
		      @"Quit self\n"
		      @"Shuts down the Command server for this host.\n";
		}
	      else if (comp(wd, @"Restart") >= 0)
		{
		  m = @"Restart 'name'\n"
		      @"Shuts down and starts the named client process(es).\n"
		      @"Restart all\n"
		      @"Shuts down and starts all client processes.\n"
		      @"Restart self\n"
		      @"Shuts down and starts Command server for this host.\n";
		}
	      else if (comp(wd, @"Tell") >= 0)
		{
		  m = @"Tell 'name' 'command'\n"
		      @"Sends the command to the named client(s).\n"
		      @"You may use 'tell all ...' to send to all clients.\n";
		}
	    }
	}
      else if (comp(wd, @"launch") >= 0)
	{
	  if ([cmd count] > 1)
	    {
	      if (launchInfo != nil)
		{
		  NSEnumerator	*enumerator;
		  NSString	*key;
		  NSString	*nam = [cmd objectAtIndex: 1];
		  BOOL		found = NO;

		  enumerator = [launchOrder objectEnumerator];
                  if ([nam caseInsensitiveCompare: @"all"] == NSOrderedSame)
                    {
                      NSMutableArray  *names = [NSMutableArray array];

                      while ((key = [enumerator nextObject]) != nil)
                        {
                          EcClientI	*r;
                          NSDictionary  *inf;

                          inf = [launchInfo objectForKey: key];
			  if ([[inf objectForKey: @"Auto"] boolValue]==NO)
                            {
                              continue;
                            }
                          r = [self findIn: clients byName: key];
                          if (nil != r)
                            {
                              continue;
                            }
                          found = YES;
                          [launches setObject: [NSDate distantPast]
                                       forKey: key];
                          [names addObject: key];
                        }
                      if (YES == found)
                        {
                          [names sortUsingSelector: @selector(compare:)];
                          m = [NSString stringWithFormat:
                            @"Ok - I will launch %@ when I get a chance.\n",
                            names];
                        }
                    }
                  else
                    {
                      while ((key = [enumerator nextObject]) != nil)
                        {
                          if (comp(nam, key) >= 0)
                            {
                              EcClientI	*r;

                              found = YES;
                              r = [self findIn: clients byName: key];
                              if (r == nil)
                                {
                                  [launches setObject: [NSDate distantPast]
                                               forKey: key];
                                  m = @"Ok - I will launch that program "
                                      @"when I get a chance.\n";
                                }
                              else
                                {
                                  m = @"That program is already running\n";
                                }
                            }
                        }
                    }
		  if (found == NO)
		    {
		      m = @"I don't know how to launch that program.\n";
		    }
		}
	      else
		{
		  m = @"There are no programs we can launch.\n";
		}
	    }
	  else
	    {
	      m = @"I need the name of a program to launch.\n";
	    }
	}
      else if (comp(wd, @"list") >= 0)
	{
	  wd = cmdWord(cmd, 1);
	  if ([wd length] == 0 || comp(wd, @"clients") >= 0)
	    {
	      if ([clients count] == 0)
		{
		  m = @"No clients currently connected.\n";
		}
	      else
		{
		  unsigned	i;

		  m = @"Current client processes -\n";
		  for (i = 0; i < [clients count]; i++)
		    {
		      EcClientI*	c = [clients objectAtIndex: i];

		      m = [NSString stringWithFormat: 
			  @"%@%2d.   %-32.32s\n", m, i, [[c name] cString]];
		    }
		}
	    }
	  else if (comp(wd, @"launches") >= 0)
	    {
	      if (launchInfo != nil)
		{
		  NSEnumerator	*enumerator;
		  NSString	*key;
		  NSDate	*date;
		  NSDate	*now = [NSDate date];

		  m = @"Programs we can launch -\n";
		  enumerator = [[[launchInfo allKeys] sortedArrayUsingSelector:
		    @selector(compare:)] objectEnumerator];
		  while ((key = [enumerator nextObject]) != nil)
		    {
		      EcClientI	*r;
		      NSDictionary	*inf = [launchInfo objectForKey: key];

		      m = [m stringByAppendingFormat: @"  %-32.32s ",
			[key cString]];
		      r = [self findIn: clients byName: key];
		      if (r == nil)
			{
			  if ([[inf objectForKey: @"Disabled"] boolValue]==YES)
			    {
			      m = [m stringByAppendingString: 
				@"disabled in config\n"];
			    }
			  else if ([[inf objectForKey: @"Auto"] boolValue]==NO)
			    {
			      date = [launches objectForKey: key];
			      if (nil == date
				|| [NSDate distantFuture] == date)
				{
				  m = [m stringByAppendingString: 
				    @"may be launched manually\n"];
				}
			      else if ([now timeIntervalSinceDate: date] > DLY)
				{
				  m = [m stringByAppendingString: 
				    @"ready to autolaunch now\n"];
				}
			      else
				{
				  m = [m stringByAppendingString: 
				    @"autolaunch in a few minutes\n"];
				}
			    }
			  else
			    {
			      date = [launches objectForKey: key];
			      if (date == nil)
				{
				  date = now;
				  [launches setObject: date forKey: key];
				}
			      if ([NSDate distantFuture] == date)
				{
				  m = [m stringByAppendingString: 
				    @"manually suspended\n"];
				}
			      else
				{
				  if ([now timeIntervalSinceDate: date] > DLY)
				    {
				      m = [m stringByAppendingString: 
					@"ready to autolaunch now\n"];
				    }
				  else
				    {
				      m = [m stringByAppendingString: 
					@"autolaunch in a few minutes\n"];
				    }
				}
			    }
			}
		      else
			{
			  m = [m stringByAppendingString: @"running\n"];
			}
		    }
		  if ([launchInfo count] == 0)
		    {
		      m = [m stringByAppendingString: @"nothing\n"];
		    }
		}
	      else
		{
		  m = @"There are no programs we can launch.\n";
		}
	    }
	}
      else if (comp(wd, @"memory") >= 0)
	{
	  if (GSDebugAllocationActive(YES) == NO)
	    {
	      m = @"Memory statistics were not being gathered.\n"
		  @"Statistics Will start from NOW.\n";
	    }
	  else
	    {
	      const char*	list;

	      wd = cmdWord(cmd, 1);
	      if ([wd length] > 0 && comp(wd, @"all") >= 0)
		{
		  list = GSDebugAllocationList(NO);
		}
	      else
		{
		  list = GSDebugAllocationList(YES);
		}
	      m = [NSString stringWithCString: list];
	    }
	}
      else if (comp(wd, @"quit") >= 0)
	{
	  wd = cmdWord(cmd, 1);
	  if ([wd length] > 0)
	    {
	      if (comp(wd, @"self") == 0)
		{
		  if (terminating == nil)
		    {
		      NS_DURING
			{
			  [control unregister: self];
			}
		      NS_HANDLER
			{
			  NSLog(@"Exception unregistering from Control: %@",
			    localException);
			}
		      NS_ENDHANDLER
		      exit(0);
		    }
		  else
		    {
		      m = @"Already terminating!\n";
		    }
		}
	      else if (comp(wd, @"all") == 0)
		{
		  [self quitAll];

		  if ([clients count] == 0)
		    {
		      m = @"All clients have been shut down.\n";
		    }
		  else if ([clients count] == 1)
		    {
		      m = @"One client did not shut down.\n";
		    }
		  else
		    {
		      m = @"Some clients did not shut down.\n";
		    }
		}
	      else
		{
		  NSArray	*a = [self findAll: clients byAbbreviation: wd];
		  unsigned	i;
		  BOOL		found = NO;

		  for (i = 0; i < [a count]; i++)
		    {
		      EcClientI	*c = [a objectAtIndex: i];

		      NS_DURING
			{
			  [launches setObject: [NSDate distantFuture]
				       forKey: [c name]];
			  m = [m stringByAppendingFormat: 
			    @"Sent 'quit' to '%@'\n", [c name]];
			  m = [m stringByAppendingString:
			    @"  Please wait for this to be 'removed' before "
			    @"proceeding.\n"];
                          [c setTerminating: YES];
			  [[c obj] cmdQuit: 0];
			  found = YES;
			}
		      NS_HANDLER
			{
			  NSLog(@"Caught exception: %@", localException);
			}
		      NS_ENDHANDLER
		    } 
		  if (launchInfo != nil)
		    {
		      NSEnumerator	*enumerator;
		      NSString		*key;

		      enumerator = [launchOrder objectEnumerator];
		      while ((key = [enumerator nextObject]) != nil)
			{
			  if (comp(wd, key) >= 0)
			    {
			      NSDate	*when = [launches objectForKey: key];

			      found = YES;
			      [launches setObject: [NSDate distantFuture]
					   forKey: key];
			      if (when != [NSDate distantFuture])
				{
				  m = [m stringByAppendingFormat:
				    @"Suspended %@\n", key];
				}
			    }
			}
		    }
		  if (NO == found)
		    {
		      m = [NSString stringWithFormat: 
			@"Nothing to shut down as '%@'\n", wd];
		    }
		}
	    }
	  else
	    {
	      m = @"Quit what?.\n";
	    }
	}
      else if (comp(wd, @"restart") >= 0)
	{
	  wd = cmdWord(cmd, 1);
	  if ([wd length] > 0)
	    {
	      if (comp(wd, @"self") == 0)
		{
		  if (terminating == nil)
		    {
		      NS_DURING
			{
                          [self information: @"Re-starting Command server\n"
                                       from: t
                                         to: f
                                       type: LT_AUDIT];
			  [control unregister: self];
			}
		      NS_HANDLER
			{
			  NSLog(@"Exception unregistering from Control: %@",
			    localException);
			}
		      NS_ENDHANDLER
		      exit(1);          // Watcher should restart us
		    }
		  else
		    {
		      m = @"Already terminating!\n";
		    }
		}
	      else if (comp(wd, @"all") == 0)
		{
		  NSArray       *a;

                  a = [self restartAll];
		  if ([a count] == 0)
		    {
		      m = @"All clients have been shut down for restart.\n";
		    }
		  else if ([a count] == 1)
		    {
		      m = @"One client did not shut down for restart.\n";
		    }
		  else
		    {
		      m = @"Some clients did not shut down for restart.\n";
		    }
		}
	      else
		{
		  NSArray	*a = [self findAll: clients byAbbreviation: wd];
		  unsigned	i;
		  BOOL		found = NO;
                  NSDate        *when;

                  when = [NSDate dateWithTimeIntervalSinceNow: 30.0 - DLY];
		  for (i = 0; i < [a count]; i++)
		    {
		      EcClientI	*c = [a objectAtIndex: i];

		      NS_DURING
			{
			  [launches setObject: when forKey: [c name]];
			  m = [m stringByAppendingFormat: 
			    @"  The process '%@' should restart shortly.\n",
			    [c name]];
                          [c setTerminating: YES];
			  [[c obj] cmdQuit: 0];
			  found = YES;
			}
		      NS_HANDLER
			{
			  NSLog(@"Caught exception: %@", localException);
			}
		      NS_ENDHANDLER
		    } 
		  if (NO == found)
		    {
		      m = [NSString stringWithFormat: 
			@"Nothing to restart as '%@'\n", wd];
		    }
		}
	    }
	  else
	    {
	      m = @"Restart what?.\n";
	    }
	}
      else if (comp(wd, @"tell") >= 0)
	{
	  wd = cmdWord(cmd, 1);
	  if ([wd length] > 0)
	    {
	      NSString	*dest = AUTORELEASE(RETAIN(wd));

	      [cmd removeObjectAtIndex: 0];
	      [cmd removeObjectAtIndex: 0];
	      if (comp(dest, @"all") == 0)
		{
		  unsigned	i;
		  NSArray	*a = [[NSArray alloc] initWithArray: clients];

		  for (i = 0; i < [a count]; i++)
		    {
		      EcClientI*	c = [a objectAtIndex: i];

		      if ([clients indexOfObjectIdenticalTo: c]!=NSNotFound)
			{
			  NS_DURING
			    {
			      NSData	*dat = [NSPropertyListSerialization
				dataFromPropertyList: cmd
				format: NSPropertyListBinaryFormat_v1_0
				errorDescription: 0];
			      [[c obj] cmdMesgData: dat from: f];
			      m = @"Sent message.\n";
			    }
			  NS_HANDLER
			    {
			      NSLog(@"Caught exception: %@", localException);
			    }
			  NS_ENDHANDLER
			}
		    }
		}
	      else
		{
		  NSArray	*a;

		  a = [self findAll: clients byAbbreviation: dest];
		  if ([a count] == 0)
		    {
		      m = [NSString stringWithFormat: 
			@"No such client as '%@'\n", dest];
		    }
		  else
		    {
		      unsigned	i;

		      m = nil;

		      for (i = 0; i < [a count]; i++)
			{
			  EcClientI	*c = [a objectAtIndex: i];

			  NS_DURING
			    {
			      NSData	*dat = [NSPropertyListSerialization
				dataFromPropertyList: cmd
				format: NSPropertyListBinaryFormat_v1_0
				errorDescription: 0];

			      [[c obj] cmdMesgData: dat from: f];
			      if (m == nil)
				{
				  m = [NSString stringWithFormat:
				    @"Sent message to %@", [c name]];
				}
			      else
				{
				  m = [m stringByAppendingFormat:
				    @", %@", [c name]];
				}
			    }
			  NS_HANDLER
			    {
			      NSLog(@"Caught exception: %@", localException);
			      if (m == nil)
				{
				  m = @"Failed to send message!";
				}
			      else
				{
				  m = [m stringByAppendingFormat:
				    @", failed to send to %@", [c name]];
				}
			    }
			  NS_ENDHANDLER
			}
		      if (m != nil)
			m = [m stringByAppendingString: @"\n"];
		    }
		}
	    }
	  else
	    {
	      m = @"Tell where?.\n";
	    }
	}
      else
	{
	  m = [NSString stringWithFormat: @"Unknown command - '%@'\n", wd];
	}

      [self information: m from: t to: f type: LT_AUDIT];
    }
  else
    {
      EcClientI	*client = [self findIn: clients byName: t];

      if (client)
	{
	  NS_DURING
	    {
	      NSData	*dat = [NSPropertyListSerialization
		dataFromPropertyList: cmd
		format: NSPropertyListBinaryFormat_v1_0
		errorDescription: 0];
	      [[client obj] cmdMesgData: dat from: f];
	    }
	  NS_HANDLER
	    {
	      NSLog(@"Caught exception: %@", localException);
	    }
	  NS_ENDHANDLER
	}
      else
	{
	  NSString	*m;

	  m = [NSString stringWithFormat:
            @"command to unregistered client '%@'", t];
	  [self information: cmdLogFormat(LT_ERROR, m)
		       from: nil
			 to: f
		       type: LT_ERROR];
	}
    }
}

- (NSData *) configurationFor: (NSString *)name
{
  NSMutableDictionary *dict;
  NSString	*base;
  NSRange	r;
  id		o;
  
  if (nil == config || 0 == [name length])
    {
      return nil;	// Not available
    }

  r = [name rangeOfString: @"-"
		  options: NSBackwardsSearch | NSLiteralSearch];
  if (r.length > 0)
    {
      base = [name substringToIndex: r.location];
    }
  else
    {
      base = nil;
    }

  dict = [NSMutableDictionary dictionaryWithCapacity: 2];
  o = [config objectForKey: @"*"];
  if (o != nil)
    {
      [dict setObject: o forKey: @"*"];
    }

  o = [config objectForKey: name];		// Lookup config
  if (base != nil)
    {
      if (nil == o)
	{
	  /* No instance specific config found for server,
	   * try using the base server name without instance ID.
	   */
	  o = [config objectForKey: base];
	}
      else
	{
	  id	tmp;

	  /* We found instance specific configuration for the server,
	   * so we merge by taking values from generic server config
	   * (if any) and overwriting them with instance specific values.
	   */
	  tmp = [config objectForKey: base];
	  if ([tmp isKindOfClass: [NSDictionary class]]
	    && [o isKindOfClass: [NSDictionary class]])
	    {
	      tmp = [[tmp mutableCopy] autorelease];
	      [tmp addEntriesFromDictionary: o];
	      o = tmp;
	    }
	}
    }
  if (o != nil)
    {
      [dict setObject: o forKey: name];
    }

  o = [config objectForKey: @"Operators"];
  if (o != nil)
    {
      [dict setObject: o forKey: @"Operators"];
    }

  return [NSPropertyListSerialization
    dataFromPropertyList: dict
    format: NSPropertyListBinaryFormat_v1_0
    errorDescription: 0];
}

- (BOOL) connection: (NSConnection*)ancestor
  shouldMakeNewConnection: (NSConnection*)newConn
{
  [[NSNotificationCenter defaultCenter]
    addObserver: self
       selector: @selector(connectionBecameInvalid:)
	   name: NSConnectionDidDieNotification
         object: (id)newConn];
  [newConn setDelegate: self];
  return YES;
}

- (id) connectionBecameInvalid: (NSNotification*)notification
{
  id conn = [notification object];

  [[NSNotificationCenter defaultCenter]
    removeObserver: self
	      name: NSConnectionDidDieNotification
	    object: conn];
  if ([conn isKindOfClass: [NSConnection class]])
    {
      NSMutableArray	*c;
      NSMutableString	*l = [NSMutableString stringWithCapacity: 20];
      NSMutableString	*e = [NSMutableString stringWithCapacity: 20];
      NSMutableString	*m = [NSMutableString stringWithCapacity: 20];
      BOOL		lostClients = NO;
      NSUInteger	i;

      if (control && [(NSDistantObject*)control connectionForProxy] == conn)
	{
	  [[self cmdLogFile: logname]
	    puts: @"Lost connection to control server.\n"];
	  DESTROY(control);
	}

      /* Now remove the clients from the active list.
       */
      c = AUTORELEASE([clients mutableCopy]);
      i = [c count];
      while (i-- > 0)
	{
	  EcClientI     *o = [c objectAtIndex: i];

	  if ([(id)[o obj] connectionForProxy] == conn)
	    {
	      NSString	*name = [o name];
	      NSString	*s;

	      lostClients = YES;
              [o setUnregistered: YES];
	      [clients removeObjectIdenticalTo: o];
	      if (i <= pingPosition && pingPosition > 0)
		{
		  pingPosition--;
		}
	      if ([o transient] == NO)
		{
		  EcAlarm	*a;

		  s = EcMakeManagedObject(host, name, nil);
		  a = [EcAlarm alarmForManagedObject: s
		    at: nil
		    withEventType: EcAlarmEventTypeProcessingError
		    probableCause: EcAlarmSoftwareProgramAbnormallyTerminated
		    specificProblem: @"Process availability"
		    perceivedSeverity: EcAlarmSeverityCritical
		    proposedRepairAction: @"Check system status"
		    additionalText: @"removed (lost) server"];
		  [self alarm: a];
		}
	      else
		{
		  s = [NSString stringWithFormat: cmdLogFormat(LT_DEBUG,
		    @"removed (lost) server - '%@' on %@"), name, host];
		  [l appendString: s];
		}
	    }
	}

      [c removeAllObjects];

      if ([l length] > 0)
	{
	  [[self cmdLogFile: logname] puts: l];
	}
      if ([m length] > 0)
	{
	  [self information: m from: nil to: nil type: LT_ALERT];
	}
      if ([e length] > 0)
	{
	  [self information: e from: nil to: nil type: LT_ERROR];
	}
      if (lostClients)
	{
	  [self update];
	}
    }
  else
    {
      [self error: "non-Connection sent invalidation"];
    }
  return self;
}

- (void) dealloc
{
  [self cmdLogEnd: logname];
  if (timer != nil)
    {
      [timer invalidate];
    }
  RELEASE(launching);
  RELEASE(launches);
  DESTROY(control);
  RELEASE(host);
  RELEASE(clients);
  RELEASE(launchInfo);
  RELEASE(launchOrder);
  RELEASE(environment);
  RELEASE(lastUnanswered);
  [super dealloc];
}

- (NSArray*) findAll: (NSArray*)a
      byAbbreviation: (NSString*)s
{
  NSMutableArray	*r = [NSMutableArray arrayWithCapacity: 4];
  int			i;

  /*
   *	Special case - a numeric value is used as an index into the array.
   */
  if (isdigit(*[s cString]))
    {
      i = [s intValue];
      if (i >= 0 && i < (int)[a count])
	{
	  [r addObject: [a objectAtIndex: i]];
	}
    }
  else
    {
      EcClientI	*o;

      for (i = 0; i < (int)[a count]; i++)
	{
	  o = (EcClientI*)[a objectAtIndex: i];
	  if (comp(s, [o name]) == 0 || comp_len == (int)[s length])
	    {
	      [r addObject: o];
	    }
	}
    }
  return r;
}

- (EcClientI*) findIn: (NSArray*)a
       byAbbreviation: (NSString*)s
{
  EcClientI	*o;
  int		i;
  int		best_pos = -1;
  int		best_len = 0;

  /*
   *	Special case - a numeric value is used as an index into the array.
   */
  if (isdigit(*[s cString]))
    {
      i = [s intValue];
      if (i >= 0 && i < (int)[a count])
	{
	  return (EcClientI*)[a objectAtIndex: i];
	}
    }

  for (i = 0; i < (int)[a count]; i++)
    {
      o = (EcClientI*)[a objectAtIndex: i];
      if (comp(s, [o name]) == 0)
	{
	  return o;
	}
      if (comp_len > best_len)
	{
	  best_len = comp_len;
	  best_pos = i;
	}
    }
  if (best_pos >= 0)
    {
      return (EcClientI*)[a objectAtIndex: best_pos];
    }
  return nil;
}

- (EcClientI*) findIn: (NSArray*)a
		byName: (NSString*)s
{
  EcClientI	*o;
  int		i;

  for (i = 0; i < (int)[a count]; i++)
    {
      o = (EcClientI*)[a objectAtIndex: i];

      if (comp([o name], s) == 0)
	{
	  return o;
	}
    }
  return nil;
}

- (EcClientI*) findIn: (NSArray*)a
             byObject: (id)s
{
  EcClientI	*o;
  int		i;

  for (i = 0; i < (int)[a count]; i++)
    {
      o = (EcClientI*)[a objectAtIndex: i];

      if ([o obj] == s)
	{
	  return o;
	}
    }
  return nil;
}

- (void) flush
{
  /*
   * Flush logs to disk ... dummy method as we don't cache them at present.
   */
}

- (void) information: (NSString*)inf
		from: (NSString*)s
		type: (EcLogType)t
{
  [self information: inf from: s to: nil type: t];
}

- (void) information: (NSString*)inf
		from: (NSString*)s
		  to: (NSString*)d
		type: (EcLogType)t
{
  if (t != LT_DEBUG && inf != nil && [inf length] > 0)
    {
      if (control == nil)
	{
	  [self timedOut: nil];
	}
      if (control == nil)
	{
	  NSLog(@"Information (from:%@ to:%@ type:%d) with no Control -\n%@",
	    s, d, t, inf);
	}
      else
	{
	  NS_DURING
	    {
	      [control information: inf type: t to: d from: s];
	    }
	  NS_HANDLER
	    {
	      NSLog(@"Sending %@ from %@ to %@ type %x exception: %@",
		inf, s, d, t, localException);
	    }
	  NS_ENDHANDLER
	}
    }
}

- (id) initWithDefaults: (NSDictionary*)defs
{
  ecSetLogsSubdirectory(@"Logs");
  if (nil != (self = [super initWithDefaults: defs]))
    {
      debUncompressed = 0.0;
      debUndeleted = 0.0;
      logUncompressed = 0.0;
      logUndeleted = 0.0;
      nodesFree = 0.1;
      spaceFree = 0.1;

      logname = [[self cmdName] stringByAppendingPathExtension: @"log"];
      RETAIN(logname);
      if ([self cmdLogFile: logname] == nil)
	{
	  exit(0);
	}
      host = RETAIN([[NSHost currentHost] wellKnownName]);
      clients = [[NSMutableArray alloc] initWithCapacity: 10];
      launches = [[NSMutableDictionary alloc] initWithCapacity: 10];
      launching = [[NSMutableDictionary alloc] initWithCapacity: 10];

      timer = [NSTimer scheduledTimerWithTimeInterval: 5.0
					       target: self
					     selector: @selector(timedOut:)
					     userInfo: nil
					      repeats: YES];
      [self timedOut: nil];
    }
  return self;
}

- (void) killAll
{
#ifndef __MINGW__
  NSUInteger    i = [clients count];

  if (i > 0)
    {
      while (i-- > 0)
	{
	  EcClientI	*c;

	  c = [clients objectAtIndex: i];
	  if (nil != c)
	    {
              int       p = [c processIdentifier];

	      if (p > 0)
		{
		  kill(p, SIGKILL);
		}
	    }
	}
    }
#endif
}

- (BOOL) launch: (NSString*)name
{
  EcClientI	*r = [self findIn: clients byName: name];

  if (nil == r)
    {
      NSDictionary	*taskInfo = [launchInfo objectForKey: name];
      NSString          *m;

      if (nil == taskInfo)
        {
          m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT,
            @"unrecognized name to launch %@"), name];
          [self information: m from: nil to: nil type: LT_AUDIT];
          return NO;
        }
      else
        {
          NSDictionary	*env = environment;
          NSArray	*args = [taskInfo objectForKey: @"Args"];
          NSString	*home = [taskInfo objectForKey: @"Home"];
          NSString	*prog = [taskInfo objectForKey: @"Prog"];
          NSDictionary	*addE = [taskInfo objectForKey: @"AddE"];
          NSDictionary	*setE = [taskInfo objectForKey: @"SetE"];
          NSString      *failed = nil;
          NSTask        *task = nil;
          NSString	*m;

          /* As a convenience, the 'Home' option sets the -HomeDirectory
           * for the process.
           */
          if ([home length] > 0)
            {
              NSMutableArray    *a = [[args mutableCopy] autorelease];

              if (nil == a)
                {
                  a = [NSMutableArray arrayWithCapacity: 2];
                }
              [a addObject: @"-HomeDirectory"];
              [a addObject: home];
              args = a;
            }

          /* Record time of launch start and the fact that this is launching.
           */
          [launches setObject: [NSDate date] forKey: name];
          if (nil != [launching objectForKey: name])
            {
              NSString  *managedObject;
              EcAlarm	*a;

              /* We are re-attempting a launch of a program which never
               * contacted us and registered with us ... raise an alarm.
               */
              managedObject = EcMakeManagedObject(host, name, nil);
              a = [EcAlarm alarmForManagedObject: managedObject
                at: nil
                withEventType: EcAlarmEventTypeProcessingError
                probableCause: EcAlarmSoftwareProgramAbnormallyTerminated
                specificProblem: @"Process availability"
                perceivedSeverity: EcAlarmSeverityCritical
                proposedRepairAction: @"Check system status"
                additionalText: @"failed to register after launch"];
              [self alarm: a];
            }
          task = [NSTask new];
          [launching setObject: task forKey: name];
          RELEASE(task);

          if (prog != nil && [prog length] > 0)
            {
              NSFileHandle	*hdl;

              if (setE != nil)
                {
                  env = setE;
                }
              else if (env == nil)
                {
                  env = [[NSProcessInfo processInfo] environment];
                }
              if (addE != nil)
                {
                  NSMutableDictionary	*e = [env mutableCopy];

                  [e addEntriesFromDictionary: addE];
                  env = AUTORELEASE(e);
                }
              [task setEnvironment: env];
              hdl = [NSFileHandle fileHandleWithNullDevice];
              NS_DURING
                {
                  [task setLaunchPath: prog];

                  if ([task validatedLaunchPath] == nil)
                    {
                      failed = @"failed to launch (not executable)";
                      m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT,
                        @"failed to launch (not executable) %@"), name];
                      [self information: m from: nil to: nil type: LT_AUDIT];
                      prog = nil;
                    }
                  if (prog != nil)
                    {
                      NSString  *s;

                      s = [taskInfo objectForKey: @"KeepStandardInput"];
                      if (NO == [s respondsToSelector: @selector(boolValue)]
                        || NO == [s boolValue])
                        {
                          [task setStandardInput: hdl];
                        }
                      s = [taskInfo objectForKey: @"KeepStandardOutput"];
                      if (NO == [s respondsToSelector: @selector(boolValue)]
                        || NO == [s boolValue])
                        {
                          [task setStandardOutput: hdl];
                        }
                      s = [taskInfo objectForKey: @"KeepStandardError"];
                      if (NO == [s respondsToSelector: @selector(boolValue)]
                        || NO == [s boolValue])
                        {
                          [task setStandardError: hdl];
                        }
                      if (home != nil && [home length] > 0)
                        {
                          [task setCurrentDirectoryPath: home];
                        }
                      if (args != nil)
                        {
                          [task setArguments: args];
                        }
                      [task launch];
                      [[self cmdLogFile: logname]
                        printf: @"%@ launched %@\n", [NSDate date], prog];
                    }
                }
              NS_HANDLER
                {
                  failed = @"failed to launch";
                  m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT,
                    @"failed to launch (%@) %@"), localException, name];
                  [self information: m from: nil to: nil type: LT_AUDIT];
                }
              NS_ENDHANDLER
            }
          else
            {
              failed = @"bad program name to launch";
              m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT,
                @"bad program name to launch %@"), name];
              [self information: m from: nil to: nil type: LT_AUDIT];
            }
          if (nil != failed)
            {
              NSString      *managedObject;

              managedObject = EcMakeManagedObject(host, name, nil);
              [self alarmConfigurationFor: managedObject
                specificProblem: @"Process launch"
                additionalText: failed
                critical: NO];
              return NO;
            }
        }
    }
  return YES;
}

- (void) launch
{
  if (launchInfo != nil)
    {
      NSEnumerator	*enumerator;
      NSString		*key;
      NSDate		*date;
      NSString		*firstKey = nil;
      NSDate		*firstDate = nil;
      NSDate		*now = [NSDate date];

      enumerator = [launchOrder objectEnumerator];
      while ((key = [enumerator nextObject]) != nil)
	{
	  EcClientI	*r = [self findIn: clients byName: key];

	  if (r == nil)
	    {
	      NSDictionary	*taskInfo = [launchInfo objectForKey: key];
	      BOOL		disabled;
	      BOOL		autoLaunch;

	      autoLaunch = [[taskInfo objectForKey: @"Auto"] boolValue];
	      disabled = [[taskInfo objectForKey: @"Disabled"] boolValue];

	      if (disabled == NO)
		{
		  date = [launches objectForKey: key];
		  if (nil == date)
		    {
		      if (autoLaunch == YES)
			{
			  NSDate		*start;
			  NSTimeInterval	offset = -(DLY - 5.0);

			  /* If there is no launch date, we set launch
			   * dates so that we can try this in 5 seconds.
			   */
		          start = [NSDate dateWithTimeIntervalSinceNow: offset];
			  [launches setObject: start forKey: key];
			  date = start;
			}
		    }
		  if (date != nil)
		    {
		      if (firstDate == nil
			|| [date earlierDate: firstDate] == date)
			{
			  firstDate = date;
			  firstKey = key;
			}
		    }
		}
	    }
	}

      key = firstKey;
      date = firstDate;

      if (date != nil && [now timeIntervalSinceDate: date] > DLY)
	{
          [self launch: key];
	}
    }
}

- (void) logMessage: (NSString*)msg
	       type: (EcLogType)t
		for: (id<CmdClient>)o
{
  EcClientI*		r = [self findIn: clients byObject: o];
  NSString		*c;

  if (r == nil)
    {
      c = @"unregistered client";
    }
  else
    {
      c = [r name];
    }
  [self logMessage: msg
	      type: t
	      name: c];
}

- (void) logMessage: (NSString*)msg
	       type: (EcLogType)t
	       name: (NSString*)c
{
  NSString		*m;
  
  switch (t)
    {
      case LT_DEBUG: 
	  m = msg;
	  break;

      case LT_WARNING: 
	  m = msg;
	  break;

      case LT_ERROR: 
	  m = msg;
	  break;

      case LT_AUDIT: 
	  m = msg;
	  break;

      case LT_ALERT: 
	  m = msg;
	  break;

      default: 
	  m = [NSString stringWithFormat: @"%@: Message of unknown type - %@",
		      c, msg];
	  break;
    }

  [[self cmdLogFile: logname] puts: m];
  [self information: m from: c to: nil type: t];
}

- (void) quitAll
{
  /*
   * Suspend any task that might potentially be started.
   */
  if (launchInfo != nil)
    {
      NSEnumerator	*enumerator;
      NSString	*key;

      enumerator = [launchOrder objectEnumerator];
      while ((key = [enumerator nextObject]) != nil)
	{
	  [launches setObject: [NSDate distantFuture] forKey: key];
	}
    }

  if ([clients count] > 0)
    {
      NSUInteger	i;
      NSUInteger	j;
      NSMutableArray	*a;

      /* Now we tell all connected clients to quit.
       */
      i = [clients count];
      a = [NSMutableArray arrayWithCapacity: i];
      while (i-- > 0)
	{
	  [a addObject: [[clients objectAtIndex: i] name]];
	}
      for (i = 0; i < [a count]; i++)
	{
	  EcClientI	*c;
	  NSString	*n;

	  n = [a objectAtIndex: i];
	  c = [self findIn: clients byName: n];
	  if (nil != c)
	    {
	      NS_DURING
		{
		  [launches setObject: [NSDate distantFuture] forKey: n];
                  [c setTerminating: YES];
		  [[c obj] cmdQuit: 0];
		}
	      NS_HANDLER
		{
		  NSLog(@"Caught exception: %@", localException);
		}
	      NS_ENDHANDLER
	    }
	}

      /* Give the clients a short time to quit, and re-send
       * the instruction to any which haven't budged.
       */
      for (j = 0; j < 15; j++)
	{
	  NSDate	*next = [NSDate dateWithTimeIntervalSinceNow: 2.0];

	  while ([clients count] > 0 && [next timeIntervalSinceNow] > 0.0)
	    {
	      [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
				       beforeDate: next];
	    }
	  for (i = 0; i < [a count] && [clients count] > 0; i++)
	    {
	      EcClientI	*c;
	      NSString	*n;

	      n = [a objectAtIndex: i];
	      c = [self findIn: clients byName: n];
	      if (nil != c)
		{
		  NS_DURING
		    {
		      [launches setObject: [NSDate distantFuture] forKey: n];
                      [c setTerminating: YES];
		      [[c obj] cmdQuit: 0];
		    }
		  NS_HANDLER
		    {
		      NSLog(@"Caught exception: %@", localException);
		    }
		  NS_ENDHANDLER
		}
	    }
	}
    }
}

/*
 * Handle a request for re-config from a client.
 */
- (void) requestConfigFor: (id<CmdConfig>)c
{
  EcClientI	*info = [self findIn: clients byObject: (id<CmdClient>)c];
  NSData	*conf = [info config];

  if (nil != conf)
    {
      NS_DURING
	{
	  [[info obj] updateConfig: conf];
	}
      NS_HANDLER
	{
	  NSLog(@"Sending config to client: %@", localException);
	}
      NS_ENDHANDLER
    }
}

- (NSData*) registerClient: (id<CmdClient>)c
		      name: (NSString*)n
{
  return [self registerClient: c name: n transient: NO];
}

- (NSData*) registerClient: (id<CmdClient>)c
		      name: (NSString*)n
		 transient: (BOOL)t
{
  NSMutableDictionary	*dict;
  EcClientI		*obj;
  EcClientI		*old;
  NSString		*m;

  [(NSDistantObject*)c setProtocolForProxy: @protocol(CmdClient)];

  if (nil == config)
    {
      m = [NSString stringWithFormat: 
	@"%@ back-off new server with name '%@' on %@\n",
	[NSDate date], n, host];
      [[self cmdLogFile: logname] puts: m];
      [self information: m from: nil to: nil type: LT_AUDIT];
      dict = [NSMutableDictionary dictionaryWithCapacity: 1];
      [dict setObject: @"configuration data not yet available."
	       forKey: @"back-off"];
      return [NSPropertyListSerialization
	dataFromPropertyList: dict
	format: NSPropertyListBinaryFormat_v1_0
	errorDescription: 0];
    }

  /*
   *	Create a new reference for this client.
   */
  obj = [[EcClientI alloc] initFor: c name: n with: self];

  if ((old = [self findIn: clients byName: n]) == nil)
    {
      NSData	*d;

      [clients addObject: obj];
      RELEASE(obj);
      [clients sortUsingSelector: @selector(compare:)];

      [obj setProcessIdentifier: [c processIdentifier]];

      /* This client has launched ... remove it from the set of launching
       * clients and the set of launchable clients.
       */
      [launching removeObjectForKey: n];
      [launches removeObjectForKey: n];

      /* If this client is in the list of clients launched automatically
       * add its launch timestamp so it will be restarted quickly if it
       * gets shut down.
       */
      if ([[[launchInfo objectForKey: n] objectForKey: @"Auto"] boolValue])
        {
          [launches setObject: [NSDate date] forKey: n];
        }

      m = [NSString stringWithFormat: 
	@"%@ registered new server with name '%@' on %@\n",
	[NSDate date], n, host];
      [[self cmdLogFile: logname] puts: m];
      if (t == YES)
	{
	  [obj setTransient: YES];
	}
      else
	{
	  [obj setTransient: NO];
	  [self information: m from: nil to: nil type: LT_AUDIT];
	}
      [self update];
      d = [self configurationFor: n];
      if (nil != d)
	{
	  [obj setConfig: d];
	}
      return [obj config];
    }
  else
    {
      RELEASE(obj);
      m = [NSString stringWithFormat: 
	@"%@ rejected new server with name '%@' on %@\n",
	[NSDate date], n, host];
      [[self cmdLogFile: logname] puts: m];
      [self information: m from: nil to: nil type: LT_AUDIT];
      dict = [NSMutableDictionary dictionaryWithCapacity: 1];
      [dict setObject: @"client with that name already registered."
	       forKey: @"rejected"];
      return [NSPropertyListSerialization
	dataFromPropertyList: dict
	format: NSPropertyListBinaryFormat_v1_0
	errorDescription: 0];
    }
}

- (void) reply: (NSString*) msg to: (NSString*)n from: (NSString*)c
{
  if (control == nil)
    {
      [self timedOut: nil];
    }
  if (control != nil)
    {
      NS_DURING
	{
	  [control reply: msg to: n from: c];
	}
      NS_HANDLER
	{
	  NSLog(@"reply: %@ to: %@ from: %@ - %@", msg, n, c, localException);
	}
      NS_ENDHANDLER
    }
  else
    {
    }
}

- (NSArray*) restartAll
{
  NSMutableArray	*a = nil;

  /* Quit tasks, but don't suspend them.
   */
  if ([clients count] > 0)
    {
      unsigned	i;
      unsigned	j;
      NSDate    *when;


      /* We tell all connected clients to quit ...
       * clients are allowed 30 seconds to terminate
       * (though we give them 35 to allow for delays).
       */
      a = [[clients mutableCopy] autorelease];
      i = [a count];
      when = [NSDate dateWithTimeIntervalSinceNow: 35.0 - DLY];
      while (i-- > 0)
	{
	  EcClientI	*c = [a objectAtIndex: i];

	  if ([clients indexOfObjectIdenticalTo: c] == NSNotFound)
            {
              [a removeObjectAtIndex: i];
            }
          else
	    {
	      NS_DURING
		{
		  [launches setObject: when forKey: [c name]];
                  [c setTerminating: YES];
		  [[c obj] cmdQuit: 0];
		}
	      NS_HANDLER
		{
		  NSLog(@"Caught exception: %@", localException);
		}
	      NS_ENDHANDLER
	    }
	}

      /* Give the clients a short time to quit, and re-send
       * the instruction to any which haven't budged.
       */
      for (j = 0; j < 15; j++)
	{
	  NSDate	*next = [NSDate dateWithTimeIntervalSinceNow: 2.0];

	  while ([a count] > 0 && [next timeIntervalSinceNow] > 0.0)
	    {
	      [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
				       beforeDate: next];
	    }
	  i = [a count];
          when = [NSDate dateWithTimeIntervalSinceNow: 30.0 - DLY];
          while (i-- > 0)
	    {
              EcClientI	*c = [a objectAtIndex: i];

              if ([clients indexOfObjectIdenticalTo: c] == NSNotFound)
                {
                  [a removeObjectAtIndex: i];
                }
              else
		{
		  NS_DURING
		    {
                      [launches setObject: when forKey: [c name]];
                      [c setTerminating: YES];
                      [[c obj] cmdQuit: 0];
		    }
		  NS_HANDLER
		    {
		      NSLog(@"Caught exception: %@", localException);
		    }
		  NS_ENDHANDLER
		}
	    }
	}
    }
  return a;
}

- (NSString*) makeSpace
{
  NSInteger             deleteAfter;
  NSTimeInterval        latestDeleteAt;
  NSTimeInterval        now;
  NSTimeInterval        ti;
  NSFileManager         *mgr;
  NSCalendarDate        *when;
  NSString		*logs;
  NSString		*file;
  NSString              *gone;
  NSAutoreleasePool	*arp;

  gone = nil;
  arp = [NSAutoreleasePool new];
  when = [NSCalendarDate date];
  now = [when timeIntervalSinceReferenceDate];

  logs = [[self ecUserDirectory] stringByAppendingPathComponent: @"DebugLogs"];

  /* When trying to make space, we can delete up to the point when we
   * would start compressing but no further ... we don't want to delete
   * all logs!
   */
  deleteAfter = [[self cmdDefaults] integerForKey: @"CompressLogsAfter"];
  if (deleteAfter < 1)
    {
      deleteAfter = 14;
    }

  mgr = [NSFileManager defaultManager];

  if (0.0 == debUndeleted)
    {
      debUndeleted = now - 365.0 * day;
    }
  ti = debUndeleted;
  latestDeleteAt = now - day * deleteAfter;
  while (nil == gone && ti < latestDeleteAt)
    {
      when = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate: ti];
      file = [[logs stringByAppendingPathComponent:
        [when descriptionWithCalendarFormat: @"%Y-%m-%d"]]
        stringByStandardizingPath];
      if ([mgr fileExistsAtPath: file])
        {
          [mgr removeFileAtPath: file handler: nil];
          gone = [when descriptionWithCalendarFormat: @"%Y-%m-%d"];
        }
      ti += day;
    }
  debUndeleted = ti;
  RETAIN(gone);
  DESTROY(arp);
  return AUTORELEASE(gone);
}

- (void) _sweep: (BOOL)deb at: (NSCalendarDate*)when
{
  NSInteger             compressAfter;
  NSInteger             deleteAfter;
  NSTimeInterval        uncompressed;
  NSTimeInterval        undeleted;
  NSTimeInterval        latestCompressAt;
  NSTimeInterval        latestDeleteAt;
  NSTimeInterval        now;
  NSTimeInterval        ti;
  NSFileManager         *mgr;
  NSString		*dir;
  NSString		*file;
  NSAutoreleasePool	*arp;

  arp = [NSAutoreleasePool new];
  now = [when timeIntervalSinceReferenceDate];

  /* get number of days after which to do log compression/deletion.
   */
  compressAfter = [[self cmdDefaults] integerForKey: @"CompressLogsAfter"];
  if (compressAfter < 1)
    {
      compressAfter = 14;
    }
  deleteAfter = [[self cmdDefaults] integerForKey: @"DeleteLogsAfter"];
  if (deleteAfter < 1)
    {
      deleteAfter = 1000;
    }
  if (deleteAfter < compressAfter)
    {
      deleteAfter = compressAfter;
    }

  mgr = [[NSFileManager new] autorelease];

  dir = [self ecUserDirectory];
  if (YES == deb)
    {
      dir = [dir stringByAppendingPathComponent: @"DebugLogs"];
      uncompressed = debUncompressed;
      undeleted = debUndeleted;
    }
  else
    {
      dir = [dir stringByAppendingPathComponent: @"Logs"];
      uncompressed = logUncompressed;
      undeleted = logUndeleted;
    }
  if (0.0 == undeleted)
    {
      undeleted = now - 365.0 * day;
    }
  ti = undeleted;
  latestDeleteAt = now - day * deleteAfter;
  while (ti < latestDeleteAt)
    {
      NSAutoreleasePool *pool = [NSAutoreleasePool new];

      when = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate: ti];
      file = [[dir stringByAppendingPathComponent:
        [when descriptionWithCalendarFormat: @"%Y-%m-%d"]]
        stringByStandardizingPath];
      if ([mgr fileExistsAtPath: file])
        {
          [mgr removeFileAtPath: file handler: nil];
        }
      ti += day;
      [pool release];
    }
  if (YES == deb) debUndeleted = ti;
  else logUndeleted = ti;

  if (uncompressed < undeleted)
    {
      uncompressed = undeleted;
    }
  ti = uncompressed;
  latestCompressAt = now - day * compressAfter;
  while (ti < latestCompressAt)
    {
      NSAutoreleasePool         *pool = [NSAutoreleasePool new];
      NSDirectoryEnumerator	*enumerator;
      BOOL	                isDirectory;
      NSString                  *base;

      when = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate: ti];
      base = [[dir stringByAppendingPathComponent:
        [when descriptionWithCalendarFormat: @"%Y-%m-%d"]]
        stringByStandardizingPath];
      if ([mgr fileExistsAtPath: base isDirectory: &isDirectory] == NO
        || NO == isDirectory)
        {
          ti += day;
          [pool release];
          continue;     // No log directory for this date.
        }

      enumerator = [mgr enumeratorAtPath: base];
      while ((file = [enumerator nextObject]) != nil)
        {
          NSString	        *src;
          NSString	        *dst;
          NSFileHandle          *sh;
          NSFileHandle          *dh;
          NSDictionary          *a;
          NSData                *d;

          if (YES == [[file pathExtension] isEqualToString: @"gz"])
            {
              continue; // Already compressed
            }
          a = [enumerator fileAttributes];
          if (NSFileTypeRegular != [a fileType])
            {
              continue;	// Not a regular file ... can't compress
            }

          src = [base stringByAppendingPathComponent: file];

          if ([a fileSize] == 0)
            {
              [mgr removeFileAtPath: src handler: nil];
              continue; // Nothing to compress
            }

          dst = [src stringByAppendingPathExtension: @"gz"];
          if ([mgr fileExistsAtPath: dst isDirectory: &isDirectory] == YES)
            {
              [mgr removeFileAtPath: dst handler: nil];
            }

          [mgr createFileAtPath: dst contents: nil attributes: nil];
          dh = [NSFileHandle fileHandleForWritingAtPath: dst];
          if (NO == [dh useCompression])
            {
              [dh closeFile];
              [mgr removeFileAtPath: dst handler: nil];
              [self cmdError: @"Unable to compress %@ to %@", src, dst];
              continue;
            }

          sh = nil;
          NS_DURING
            {
              NSAutoreleasePool *inner;

              sh = [NSFileHandle fileHandleForReadingAtPath: src];
              inner = [NSAutoreleasePool new];
              while ([(d = [sh readDataOfLength: 1000000]) length] > 0)
                {
                  [dh writeData: d];
                  [inner release];
                  inner = [NSAutoreleasePool new];
                }
              [inner release];
              [sh closeFile];
              [dh closeFile];
              [mgr removeFileAtPath: src handler: nil];
            }
          NS_HANDLER
            {
              [mgr removeFileAtPath: dst handler: nil];
              [sh closeFile];
              [dh closeFile];
            }
          NS_ENDHANDLER
        }
      ti += day;
      [pool release];
    }
  if (YES == deb) debUncompressed = ti;
  else logUncompressed = ti;

  DESTROY(arp);
}

/* Perform this one in another thread.
 * The sweep operation may compress really large logfiles and could be
 * very slow, so it's performed in a separate thread to avoid blocking
 * normal operations.
 */
- (void) sweep: (NSCalendarDate*)when
{
  if (nil == when)
    {
      when = [NSDate date];
    }
  [self _sweep: YES at: when];
  [self _sweep: NO at: when];
  sweeping = NO;
}

- (void) ecNewHour: (NSCalendarDate*)when
{
  if (sweeping == YES)
    {
      NSLog(@"Argh - nested sweep attempt");
      return;
    }
  sweeping = YES;
  [NSThread detachNewThreadSelector: @selector(sweep:)
                           toTarget: self
                         withObject: when];
}


/*
 * Tell all our clients to quit, and wait for them to do so.
 * If called while already terminating ... force immediate shutdown.
 */
- (void) terminate: (NSTimer*)t
{
  if (nil == terminating)
    {
      [self information: @"Handling shutdown."
		   from: nil
		     to: nil
		   type: LT_AUDIT];
    }

  if (nil == terminating)
    {
      terminating = [NSTimer scheduledTimerWithTimeInterval: 10.0
	target: self selector: @selector(terminate:)
	userInfo: [NSDate new]
	repeats: YES];
    }

  [self quitAll];

  if (t != nil)
    {
      NSDate	*when = (NSDate*)[t userInfo];

      if ([when timeIntervalSinceNow] < -30.0)
	{
	  [[self cmdLogFile: logname]
	    puts: @"Final shutdown.\n"];
	  [terminating invalidate];
	  terminating = nil;
          [self killAll];
	  [self cmdQuit: tStatus];
	}
    }
}

- (void) terminate
{
  [self terminate: nil];
}

- (void) timedOut: (NSTimer*)t
{
  static BOOL	inTimeout = NO;
  NSDate	*now = [t fireDate];

  if (now == nil)
    {
      now = [NSDate date];
    }

  [[self cmdLogFile: logname] synchronizeFile];
  if (inTimeout == NO)
    {
      static unsigned	pingControlCount = 0;
      NSFileManager	*mgr;
      NSDictionary	*a;
      NSString          *s;
      float		f;
      unsigned		count;
      BOOL		lost = NO;

      inTimeout = YES;
      if (control == nil)
	{
	  NSUserDefaults	*defs;
	  NSString		*ctlName;
	  NSString		*ctlHost;
	  id			c;

	  defs = [self cmdDefaults];
	  ctlName = [defs stringForKey: @"ControlName"];
	  if (ctlName == nil)
	    {
	      ctlName = @"Control";
	    }
	  if (nil != (ctlHost = [NSHost controlWellKnownName]))
            {
              /* Map to operating system host name.
               */
              ctlHost = [[NSHost hostWithWellKnownName: ctlHost] name];
            }
	  if (nil == ctlHost)
	    {
	      ctlHost = @"*";
	    }

	  NS_DURING
	    {
	      NSLog(@"Connecting to %@ on %@", ctlName, ctlHost);
	      control = (id<Control>)[NSConnection
		  rootProxyForConnectionWithRegisteredName: ctlName
						      host: ctlHost
	       usingNameServer: [NSSocketPortNameServer sharedInstance] ];
	    }
	  NS_HANDLER
	    {
	      NSLog(@"Connecting to control server: %@", localException);
	      control = nil;
	    }
	  NS_ENDHANDLER
	  c = control;
	  if (RETAIN(c) != nil)
	    {
	      /* Re-initialise control server ping */
	      DESTROY(lastUnanswered);
	      fwdSequence = 0;
	      revSequence = 0;

	      [(NSDistantObject*)c setProtocolForProxy: @protocol(Control)];
	      c = [(NSDistantObject*)c connectionForProxy];
	      [c setDelegate: self];
	      [[NSNotificationCenter defaultCenter]
		addObserver: self
		   selector: @selector(connectionBecameInvalid:)
		       name: NSConnectionDidDieNotification
		     object: c];
	      NS_DURING
		{
		  NSData		*dat;

		  dat = [control registerCommand: self
					    name: host];
		  if (nil == dat)
		    {
		      // Control server not yet ready.
		      DESTROY(control);
		    }
		  else
		    {
		      NSMutableDictionary	*conf;

		      conf = [NSPropertyListSerialization
			propertyListWithData: dat
			options: NSPropertyListMutableContainers
			format: 0
			error: 0];
		      if ([conf objectForKey: @"rejected"] == nil)
			{
			  [self updateConfig: dat];
			}
		      else
			{
			  [[self cmdLogFile: logname]
			    printf: @"registration attempt rejected - %@\n",
			      [conf objectForKey: @"rejected"]];
			  DESTROY(control);
			}
		    }
		}
	      NS_HANDLER
		{
		  NSLog(@"Registering with control server: %@", localException);
		  DESTROY(control);
		}
	      NS_ENDHANDLER
	      if (control != nil)
		{
		  [self update];
		}
	    }
	}
      [self launch];

      count = [clients count];
      while (count-- > 0)
	{
	  EcClientI	*r = [clients objectAtIndex: count];
	  NSDate	*d = [r lastUnanswered];
	  
	  if (d != nil && [d timeIntervalSinceDate: now] < -pingDelay)
	    {
	      NSString	*m;

	      m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT,
		@"Client '%@' failed to respond for over %d seconds"),
		[r name], (int)pingDelay];
	      [[[[r obj] connectionForProxy] sendPort] invalidate];
	      [self information: m from: nil to: nil type: LT_AUDIT];
	      lost = YES;
	    }
	}
      if (control != nil && lastUnanswered != nil
	&& [lastUnanswered timeIntervalSinceDate: now] < -pingDelay)
	{
	  NSString	*m;

	  m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT,
	    @"Control server failed to respond for over %d seconds"),
	    (int)pingDelay];
	  [[(NSDistantObject*)control connectionForProxy] invalidate];
	  [self information: m from: nil to: nil type: LT_AUDIT];
	  lost = YES;
	}
      if (lost == YES)
	{
	  [self update];
	}
      /*
       * We ping each client in turn.  If there are fewer than 4 clients,
       * we skip timeouts so that clients get pinged no more frequently
       * than one per 4 timeouts.
       */
      count = [clients count];
      pingPosition++;
      if (pingPosition >= 4 && pingPosition >= count)
	{
	  pingPosition = 0;
	}
      if (pingPosition < count)
	{
	  [[clients objectAtIndex: pingPosition] ping];
	}
      // Ping the control server too - once every four times.
      pingControlCount++;
      if (pingControlCount >= 4)
	{
	  pingControlCount = 0;
	}
      if (pingControlCount == 0)
	{
	  [self pingControl];
	}

      /* See if the filesystem containing our logging directory has enough
       * space.
       */
      mgr = [NSFileManager defaultManager];

      s = [[self ecUserDirectory] stringByAppendingPathComponent: @"DebugLogs"];
      a = [mgr fileSystemAttributesAtPath: s];
      f = [[a objectForKey: NSFileSystemFreeSize] floatValue] 
        / [[a objectForKey: NSFileSystemSize] floatValue];
      if (f <= spaceFree)
	{
	  static NSDate	*last = nil;

	  if (nil == last || [last timeIntervalSinceNow] < -DLY)
	    {
	      NSString	*m;

	      m = [self makeSpace];
	      ASSIGN(last, [NSDate date]);
              if ([m length] == 0)
                {
                  m = [NSString stringWithFormat: cmdLogFormat(LT_ALERT,
                    @"Debug disk debug space at %02.1f percent"), f * 100.0];
                }
              else
                {
                  m = [NSString stringWithFormat: cmdLogFormat(LT_ALERT,
                    @"Debug disk space at %02.1f percent"
                    @" - deleted debug logs from %@ to make space"),
                    f * 100.0, m];
                }
	      [self information: m from: nil to: nil type: LT_ALERT];
	    }
	}
      f = [[a objectForKey: NSFileSystemFreeNodes] floatValue] 
        / [[a objectForKey: NSFileSystemNodes] floatValue];
      if (f <= nodesFree)
	{
	  static NSDate	*last = nil;

	  if (nil == last || [last timeIntervalSinceNow] < -DLY)
	    {
	      NSString	*m;

	      m = [self makeSpace];
	      ASSIGN(last, [NSDate date]);
              if ([m length] == 0)
                {
                  m = [NSString stringWithFormat: cmdLogFormat(LT_ALERT,
                    @"Debug disk nodes at %02.1f percent"), f * 100.0];
                }
              else
                {
                  m = [NSString stringWithFormat: cmdLogFormat(LT_ALERT,
                    @"Debug disk nodes at %02.1f percent"
                    @" - deleted debug logs from %@ to make space"),
                    f * 100.0, m];
                }
	      [self information: m from: nil to: nil type: LT_ALERT];
	    }
	}

      s = [[self ecUserDirectory] stringByAppendingPathComponent: @"Logs"];
      a = [mgr fileSystemAttributesAtPath: s];
      f = [[a objectForKey: NSFileSystemFreeSize] floatValue] 
        / [[a objectForKey: NSFileSystemSize] floatValue];
      if (f <= spaceFree)
	{
	  static NSDate	*last = nil;

	  if (nil == last || [last timeIntervalSinceNow] < -DLY)
	    {
	      NSString	*m;

	      ASSIGN(last, [NSDate date]);
              m = [NSString stringWithFormat: cmdLogFormat(LT_ALERT,
                @"Disk space at %02.1f percent"), f * 100.0];
	      [self information: m from: nil to: nil type: LT_ALERT];
	    }
        }
      f = [[a objectForKey: NSFileSystemFreeNodes] floatValue] 
        / [[a objectForKey: NSFileSystemNodes] floatValue];
      if (f <= nodesFree)
	{
	  static NSDate	*last = nil;

	  if (nil == last || [last timeIntervalSinceNow] < -DLY)
	    {
	      NSString	*m;

	      ASSIGN(last, [NSDate date]);
              m = [NSString stringWithFormat: cmdLogFormat(LT_ALERT,
                @"Disk nodes at %02.1f percent"), f * 100.0];
	      [self information: m from: nil to: nil type: LT_ALERT];
	    }
	}
    }
  inTimeout = NO;
}

- (void) unregisterByObject: (id)obj
{
  EcClientI	*o = [self findIn: clients byObject: obj];

  if (o != nil)
    {
      NSString	        *m;
      NSUInteger	i;
      BOOL	        transient = [o transient];
      NSString	        *name = [[[o name] retain] autorelease];

      m = [NSString stringWithFormat: 
	@"\n%@ removed (unregistered) server -\n  '%@' on %@\n",
	[NSDate date], name, host];
      [[self cmdLogFile: logname] puts: m];
      [o setUnregistered: YES];
      i = [clients indexOfObjectIdenticalTo: o];
      if (i != NSNotFound)
	{
	  [clients removeObjectAtIndex: i];
	  if (i <= pingPosition && pingPosition > 0)
	    {
	      pingPosition--;
	    }
	}
      if (transient == NO)
	{
	  [self information: m from: nil to: nil type: LT_AUDIT];
	}
      [self update];
    }
}

- (void) unregisterByName: (NSString*)n
{
  EcClientI	*o = [self findIn: clients byName: n];

  if (o)
    {
      NSString	        *m;
      NSUInteger       	i;
      BOOL	        transient = [o transient];
      NSString	        *name = [[[o name] retain] autorelease];

      m = [NSString stringWithFormat: 
	@"\n%@ removed (unregistered) server -\n  '%@' on %@\n",
	[NSDate date], name, host];
      [[self cmdLogFile: logname] puts: m];
      [o setUnregistered: YES];
      i = [clients indexOfObjectIdenticalTo: o];
      if (i != NSNotFound)
	{
	  [clients removeObjectAtIndex: i];
	  if (i <= pingPosition && pingPosition > 0)
	    {
	      pingPosition--;
	    }
	}
      if (transient == NO)
	{
	  [self information: m from: nil to: nil type: LT_AUDIT];
	}
      [self update];
    }
}

- (void) update
{
  if (control == nil)
    {
      [self timedOut: nil];
    }
  if (control)
    {
      NS_DURING
	{
	  NSMutableArray	*a;
	  int			i;

	  a = [NSMutableArray arrayWithCapacity: [clients count]];
	  for (i = 0; i < (int)[clients count]; i++)
	    {
	      EcClientI	*c;

	      c = [clients objectAtIndex: i];
	      [a addObject: [c name]];
	    }
	  [control servers: [NSPropertyListSerialization
	    dataFromPropertyList: a
	    format: NSPropertyListBinaryFormat_v1_0
	    errorDescription: 0] on: self];
	}
      NS_HANDLER
	{
	  NSLog(@"Exception sending servers to Control: %@", localException);
	}
      NS_ENDHANDLER
    }
  if (terminating != nil && [clients count] == 0)
    {
      [self information: @"Final shutdown."
		   from: nil
		     to: nil
		   type: LT_AUDIT];
      [terminating invalidate];
      terminating = nil;
      [self cmdQuit: tStatus];
    }
}

- (void) updateConfig: (NSData*)data
{
  NSMutableDictionary	*info;
  NSMutableDictionary	*dict;
  NSMutableDictionary	*newConfig;
  NSDictionary		*operators;
  NSEnumerator		*enumerator;
  NSString		*key;

  /* Ignore invalid/empty configuration
   */
  if (nil == data)
    {
      return;
    }
  info = [NSPropertyListSerialization
    propertyListWithData: data
    options: NSPropertyListMutableContainers
    format: 0
    error: 0];
  if (NO == [info isKindOfClass: [NSMutableDictionary class]]
    || 0 == [info count])
    {
      return;
    }

  newConfig = [NSMutableDictionary dictionaryWithCapacity: 32];
  /*
   *	Put all values for this host in the config dictionary.
   */
  dict = [info objectForKey: host];
  if (dict)
    {
      [newConfig addEntriesFromDictionary: dict];
    }
  /*
   *	Add any default values to the config dictionary where we don't have
   *	host specific values.
   */
  dict = [info objectForKey: @"*"];
  if (dict)
    {
      enumerator = [dict keyEnumerator];
      while ((key = [enumerator nextObject]) != nil)
	{
	  NSMutableDictionary	*partial = [newConfig objectForKey: key];
	  NSMutableDictionary	*general = [dict objectForKey: key];
	  NSString		*app = key;

	  if (partial == nil)
	    {
	      /*
	       *	No host-specific info for this application -
	       *	Use the general stuff for the application.
	       */
	      [newConfig setObject: general forKey: key];
	    }
	  else
	    {
	      NSEnumerator	*another = [general keyEnumerator];

	      /*
	       *	Merge in any values for this application which
	       *	exist in the general stuff, but not in the host
	       *	specific area.
	       */
	      while ((key = [another nextObject]) != nil)
		{
		  if ([partial objectForKey: key] == nil)
		    {
		      id	obj = [general objectForKey: key];

		      [partial setObject: obj forKey: key];
		    }
		  else
		    {
		      [[self cmdLogFile: logname]
			printf: @"General config for %@/%@ overridden by"
			@" host-specific version\n", app, key];
		    }
		}
	    }
	}
    }

  /*
   * Add the list of operators to the config.
   */
  operators = [info objectForKey: @"Operators"];
  if (operators != nil)
    {
      [newConfig setObject: operators forKey: @"Operators"];
    }

  /* Finally, replace old config with new if they differ.
   */
  [self newConfig: newConfig];
}

@end

