You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@httpd.apache.org by Ben Laurie <be...@gonzo.ben.algroup.co.uk> on 1996/06/25 21:15:45 UTC

Oh sod it

Regardless of the Listen thing, here's the patches for a proper graceful
restart. If someone could test Listen, it would be a Good Thing.

Now, the downside: it can take a large number of connections before the "old"
servers get used again, so they can serve up old configurations an
unexpectedly long time after the kill. The only neat way I can see around this
involves sending the children a signal which is blocked everywhere except in
the select in the main loop. Do all platforms support blocking? Can I be
bothered to write the code right now? No.

Cheers,

Ben.

Index: http_main.c
===================================================================
RCS file: /export/home/cvs/apache/src/http_main.c,v
retrieving revision 1.40
diff -c -r1.40 http_main.c
*** http_main.c	1996/06/22 13:58:34	1.40
--- http_main.c	1996/06/25 19:51:29
***************
*** 88,93 ****
--- 88,94 ----
  #include "http_core.h"          /* for get_remote_host */
  #include "scoreboard.h"
  #include <setjmp.h>
+ #include <assert.h>
  #ifdef HAVE_SHMGET
  #include <sys/types.h>
  #include <sys/ipc.h>
***************
*** 427,440 ****
   */
  
  #if defined(HAVE_MMAP)
! static short_score *scoreboard_image=NULL;
  
  static void setup_shared_mem(void)
  {
      caddr_t m;
  #if defined(MAP_ANON) || defined(MAP_FILE)
  /* BSD style */
!     m = mmap((caddr_t)0, HARD_SERVER_LIMIT*sizeof(short_score),
  	     PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0);
      if (m == (caddr_t)-1)
      {
--- 428,441 ----
   */
  
  #if defined(HAVE_MMAP)
! static scoreboard *scoreboard_image=NULL;
  
  static void setup_shared_mem(void)
  {
      caddr_t m;
  #if defined(MAP_ANON) || defined(MAP_FILE)
  /* BSD style */
!     m = mmap((caddr_t)0, SCOREBOARD_SIZE,
  	     PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0);
      if (m == (caddr_t)-1)
      {
***************
*** 453,459 ****
  	fprintf(stderr, "httpd: Could not open /dev/zero\n");
  	exit(1);
      }
!     m = mmap((caddr_t)0, HARD_SERVER_LIMIT*sizeof(short_score),
  	     PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
      if (m == (caddr_t)-1)
      {
--- 454,460 ----
  	fprintf(stderr, "httpd: Could not open /dev/zero\n");
  	exit(1);
      }
!     m = mmap((caddr_t)0, SCOREBOARD_SIZE,
  	     PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
      if (m == (caddr_t)-1)
      {
***************
*** 463,486 ****
      }
      close(fd);
  #endif
!     scoreboard_image = (short_score *)m;
  }
  
  #elif defined(HAVE_SHMGET)
! static short_score *scoreboard_image=NULL;
  static key_t shmkey = IPC_PRIVATE;
  static int shmid = -1;
  
  static void setup_shared_mem(void)
  {
-     int score_size = HARD_SERVER_LIMIT*sizeof(short_score);
      char errstr[MAX_STRING_LEN];
      struct shmid_ds shmbuf;
  #ifdef MOVEBREAK
      char *obrk;
  #endif
  
!     if ((shmid = shmget(shmkey, score_size, IPC_CREAT|SHM_R|SHM_W)) == -1)
      {
  	perror("shmget");
  	fprintf(stderr, "httpd: Could not call shmget\n");
--- 464,487 ----
      }
      close(fd);
  #endif
!     scoreboard_image = (scoreboard *)m;
!     scoreboard_image->global.exit_generation=0;
  }
  
  #elif defined(HAVE_SHMGET)
! static scoreboard *scoreboard_image=NULL;
  static key_t shmkey = IPC_PRIVATE;
  static int shmid = -1;
  
  static void setup_shared_mem(void)
  {
      char errstr[MAX_STRING_LEN];
      struct shmid_ds shmbuf;
  #ifdef MOVEBREAK
      char *obrk;
  #endif
  
!     if ((shmid = shmget(shmkey, SCOREBOARD_SIZE, IPC_CREAT|SHM_R|SHM_W)) == -1)
      {
  	perror("shmget");
  	fprintf(stderr, "httpd: Could not call shmget\n");
***************
*** 507,514 ****
      }
  #endif
  
! #define BADSHMAT	((short_score*)(-1))
!     if ((scoreboard_image = (short_score*)shmat(shmid, 0, 0)) == BADSHMAT)
      {
  	perror("shmat");
  	fprintf(stderr, "httpd: Could not call shmat\n");
--- 508,515 ----
      }
  #endif
  
! #define BADSHMAT	((scoreboard *)(-1))
!     if ((scoreboard_image = (scoreboard *)shmat(shmid, 0, 0)) == BADSHMAT)
      {
  	perror("shmat");
  	fprintf(stderr, "httpd: Could not call shmat\n");
***************
*** 554,563 ****
  	fprintf(stderr, "httpd: Could not move break back\n");
      }
  #endif
  }
  
  #else
! static short_score scoreboard_image[HARD_SERVER_LIMIT];
  static int have_scoreboard_fname = 0;
  static int scoreboard_fd;
  
--- 555,566 ----
  	fprintf(stderr, "httpd: Could not move break back\n");
      }
  #endif
+     scoreboard_image->global.exit_generation=0;
  }
  
  #else
! static scoreboard _scoreboard_image;
! static scoreboard *scoreboard_image=&_scoreboard_image;
  static int have_scoreboard_fname = 0;
  static int scoreboard_fd;
  
***************
*** 595,606 ****
  /* Called by parent process */
  void reinit_scoreboard (pool *p)
  {
  #if defined(HAVE_SHMGET) || defined(HAVE_MMAP)
      if (scoreboard_image == NULL)
      {
  	setup_shared_mem();
      }
!     memset(scoreboard_image, 0, HARD_SERVER_LIMIT*sizeof(short_score));
  #else
      scoreboard_fname = server_root_relative (p, scoreboard_fname);
  
--- 598,614 ----
  /* Called by parent process */
  void reinit_scoreboard (pool *p)
  {
+     int exit_gen=0;
+     if(scoreboard_image)
+ 	exit_gen=scoreboard_image->global.exit_generation;
+ 	
  #if defined(HAVE_SHMGET) || defined(HAVE_MMAP)
      if (scoreboard_image == NULL)
      {
  	setup_shared_mem();
      }
!     memset(scoreboard_image, 0, SCOREBOARD_SIZE);
!     scoreboard_image->global.exit_generation=exit_gen;
  #else
      scoreboard_fname = server_root_relative (p, scoreboard_fname);
  
***************
*** 614,622 ****
  	exit (1);
      }
  
!     memset ((char*)scoreboard_image, 0, sizeof(scoreboard_image));
      force_write (scoreboard_fd, (char*)scoreboard_image,
! 		 sizeof(scoreboard_image));
  #endif
  }
  
--- 622,631 ----
  	exit (1);
      }
  
!     memset ((char*)scoreboard_image, 0, sizeof(*scoreboard_image));
!     scoreboard_image->global.exit_generation=exit_gen;
      force_write (scoreboard_fd, (char*)scoreboard_image,
! 		 sizeof(*scoreboard_image));
  #endif
  }
  
***************
*** 659,665 ****
  #if !defined(HAVE_MMAP) && !defined(HAVE_SHMGET)
      lseek (scoreboard_fd, 0L, 0);
      force_read (scoreboard_fd, (char*)scoreboard_image,
! 		sizeof(scoreboard_image));
  #endif
  }
  
--- 668,674 ----
  #if !defined(HAVE_MMAP) && !defined(HAVE_SHMGET)
      lseek (scoreboard_fd, 0L, 0);
      force_read (scoreboard_fd, (char*)scoreboard_image,
! 		sizeof(*scoreboard_image));
  #endif
  }
  
***************
*** 671,677 ****
      if (child_num < 0)
  	return -1;
      
!     memcpy(&new_score_rec,&scoreboard_image[child_num],sizeof new_score_rec);
      new_score_rec.pid = getpid();
      old_status = new_score_rec.status;
      new_score_rec.status = status;
--- 680,686 ----
      if (child_num < 0)
  	return -1;
      
!     memcpy(&new_score_rec,&scoreboard_image->servers[child_num],sizeof new_score_rec);
      new_score_rec.pid = getpid();
      old_status = new_score_rec.status;
      new_score_rec.status = status;
***************
*** 702,708 ****
  #endif
  
  #if defined(HAVE_MMAP) || defined(HAVE_SHMGET)
!     memcpy(&scoreboard_image[child_num], &new_score_rec, sizeof(short_score));
  #else
      lseek (scoreboard_fd, (long)child_num * sizeof(short_score), 0);
      force_write (scoreboard_fd, (char*)&new_score_rec, sizeof(short_score));
--- 711,717 ----
  #endif
  
  #if defined(HAVE_MMAP) || defined(HAVE_SHMGET)
!     memcpy(&scoreboard_image->servers[child_num], &new_score_rec, sizeof new_score_rec);
  #else
      lseek (scoreboard_fd, (long)child_num * sizeof(short_score), 0);
      force_write (scoreboard_fd, (char*)&new_score_rec, sizeof(short_score));
***************
*** 711,722 ****
      return old_status;
  }
  
  int get_child_status (int child_num)
  {
      if (child_num<0 || child_num>=HARD_SERVER_LIMIT)
      	return -1;
      else
! 	return scoreboard_image[child_num].status;
  }
  
  int count_busy_servers ()
--- 720,741 ----
      return old_status;
  }
  
+ void update_scoreboard_global()
+     {
+ #if !defined(HAVE_MMAP) && !defined(HAVE_SHMGET)
+     lseek(scoreboard_fd,
+ 	  (char *)&scoreboard_image->global-(char *)scoreboard_image,0);
+     force_write(scoreboard_fd,(char *)&scoreboard_image->global,
+ 		sizeof scoreboard_image->global);
+ #endif
+     }
+ 
  int get_child_status (int child_num)
  {
      if (child_num<0 || child_num>=HARD_SERVER_LIMIT)
      	return -1;
      else
! 	return scoreboard_image->servers[child_num].status;
  }
  
  int count_busy_servers ()
***************
*** 725,742 ****
      int res = 0;
  
      for (i = 0; i < HARD_SERVER_LIMIT; ++i)
!       if (scoreboard_image[i].status == SERVER_BUSY_READ ||
!               scoreboard_image[i].status == SERVER_BUSY_WRITE ||
!               scoreboard_image[i].status == SERVER_BUSY_KEEPALIVE ||
!               scoreboard_image[i].status == SERVER_BUSY_LOG ||
!               scoreboard_image[i].status == SERVER_BUSY_DNS)
            ++res;
      return res;
  }
  
  short_score get_scoreboard_info(int i)
  {
!     return (scoreboard_image[i]);
  }
  
  #if defined(STATUS)
--- 744,772 ----
      int res = 0;
  
      for (i = 0; i < HARD_SERVER_LIMIT; ++i)
!       if (scoreboard_image->servers[i].status == SERVER_BUSY_READ ||
!               scoreboard_image->servers[i].status == SERVER_BUSY_WRITE ||
!               scoreboard_image->servers[i].status == SERVER_BUSY_KEEPALIVE ||
!               scoreboard_image->servers[i].status == SERVER_BUSY_LOG ||
!               scoreboard_image->servers[i].status == SERVER_BUSY_DNS)
            ++res;
      return res;
  }
  
+ int count_live_servers()
+     {
+     int i;
+     int res = 0;
+ 
+     for (i = 0; i < HARD_SERVER_LIMIT; ++i)
+       if (scoreboard_image->servers[i].status != SERVER_DEAD)
+ 	  ++res;
+     return res;
+     }
+ 
  short_score get_scoreboard_info(int i)
  {
!     return (scoreboard_image->servers[i]);
  }
  
  #if defined(STATUS)
***************
*** 776,783 ****
      int res = 0;
  
      for (i = 0; i < HARD_SERVER_LIMIT; ++i)
! 	if (scoreboard_image[i].status == SERVER_READY
! 	  || scoreboard_image[i].status == SERVER_STARTING)
  	    ++res;
  
      return res;
--- 806,813 ----
      int res = 0;
  
      for (i = 0; i < HARD_SERVER_LIMIT; ++i)
! 	if (scoreboard_image->servers[i].status == SERVER_READY
! 	  || scoreboard_image->servers[i].status == SERVER_STARTING)
  	    ++res;
  
      return res;
***************
*** 788,794 ****
      int i;
  
      for (i = 0; i < HARD_SERVER_LIMIT; ++i)
! 	if (scoreboard_image[i].status == SERVER_DEAD)
  	    return i;
  
      return -1;
--- 818,824 ----
      int i;
  
      for (i = 0; i < HARD_SERVER_LIMIT; ++i)
! 	if (scoreboard_image->servers[i].status == SERVER_DEAD)
  	    return i;
  
      return -1;
***************
*** 799,805 ****
      int i;
  
      for (i = 0; i < HARD_SERVER_LIMIT; ++i)
! 	if (scoreboard_image[i].pid == pid)
  	    return i;
  
      return -1;
--- 829,835 ----
      int i;
  
      for (i = 0; i < HARD_SERVER_LIMIT; ++i)
! 	if (scoreboard_image->servers[i].pid == pid)
  	    return i;
  
      return -1;
***************
*** 812,821 ****
  
      sync_scoreboard_image();
      for (i = 0; i < HARD_SERVER_LIMIT; ++i) {
! 	int pid = scoreboard_image[i].pid;
  
  	if (pid != my_pid && pid != 0)
! 	    waitpid (scoreboard_image[i].pid, &status, 0);
      }
  }
  
--- 842,851 ----
  
      sync_scoreboard_image();
      for (i = 0; i < HARD_SERVER_LIMIT; ++i) {
! 	int pid = scoreboard_image->servers[i].pid;
  
  	if (pid != my_pid && pid != 0)
! 	    waitpid (scoreboard_image->servers[i].pid, &status, 0);
      }
  }
  
***************
*** 984,992 ****
--- 1014,1026 ----
    }
  }
  
+ static int is_graceful;
+ static int generation;
+ 
  void restart() {
      signal (SIGALRM, SIG_IGN);
      alarm (0);
+     is_graceful=0;
  #if defined(NEXT) || defined(USE_LONGJMP)
      longjmp(restart_buffer,1);
  #else
***************
*** 994,999 ****
--- 1028,1045 ----
  #endif
  }
  
+ void graceful_restart()
+     {
+     scoreboard_image->global.exit_generation=generation;
+     is_graceful=1;
+     update_scoreboard_global();
+ #if defined(NEXT) || defined(USE_LONGJMP)
+     longjmp(restart_buffer,1);
+ #else
+     siglongjmp(restart_buffer,1);
+ #endif
+     }
+ 
  void set_signals() {
  #ifndef NO_USE_SIGACTION
      struct sigaction sa;
***************
*** 1006,1011 ****
--- 1052,1058 ----
  #ifdef NO_USE_SIGACTION
      signal(SIGTERM,(void (*)())sig_term);
      signal(SIGHUP,(void (*)())restart);
+     signal(SIGINT,(void (*)())graceful_restart);
  #else
      memset(&sa,0,sizeof sa);
      sa.sa_handler=(void (*)())sig_term;
***************
*** 1014,1019 ****
--- 1061,1069 ----
      sa.sa_handler=(void (*)())restart;
      if(sigaction(SIGHUP,&sa,NULL) < 0)
  	log_unixerr("sigaction(SIGHUP)", NULL, NULL, server_conf);
+     sa.sa_handler=(void (*)())graceful_restart;
+     if(sigaction(SIGINT,&sa,NULL) < 0)
+ 	log_unixerr("sigaction(SIGINT)", NULL, NULL, server_conf);
  #endif
  }
  
***************
*** 1166,1171 ****
--- 1216,1225 ----
  	clear_pool (ptrans);
  	
  	sync_scoreboard_image();
+ 
+ fprintf(stderr,"%d check %d %d\n",getpid(),scoreboard_image->global.exit_generation,generation);
+ 	if(scoreboard_image->global.exit_generation >= generation)
+ 	    exit(0);
  	
  	if ((count_idle_servers() >= daemons_max_free)
  	    || (max_requests_per_child > 0
***************
*** 1250,1255 ****
--- 1304,1314 ----
  #if defined(STATUS)
  	  if (r) increment_counts(child_num,r,0);
  #endif
+ 	  sync_scoreboard_image();
+ 	  if(scoreboard_image->global.exit_generation >= generation)
+ 	      exit(0);
+ fprintf(stderr,"%d check %d %d\n",getpid(),scoreboard_image->global.exit_generation,generation);
+ 
  	}
  #if 0	
  	if (bytes_in_pool (ptrans) > 80000)
***************
*** 1297,1303 ****
          exit(1);
      }
  
!     note_cleanups_for_fd (pconf, s); /* arrange to close on exec or restart */
      
      if((setsockopt(s, SOL_SOCKET,SO_REUSEADDR,(char *)&one,sizeof(one)))
         == -1) {
--- 1356,1362 ----
          exit(1);
      }
  
!     /*    note_cleanups_for_fd (pconf, s); /* arrange to close on exec or restart */
      
      if((setsockopt(s, SOL_SOCKET,SO_REUSEADDR,(char *)&one,sizeof(one)))
         == -1) {
***************
*** 1342,1347 ****
--- 1401,1449 ----
      return s;
  }
  
+ static listen_rec *old_listeners;
+ 
+ static void copy_listeners()
+     {
+     listen_rec *lr;
+ 
+     assert(old_listeners == NULL);
+     for(lr=listeners ; lr ; lr=lr->next)
+ 	{
+ 	listen_rec *nr=malloc(sizeof *nr);
+ 	*nr=*lr;
+ 	nr->next=old_listeners;
+ 	assert(!nr->used);
+ 	old_listeners=nr;
+ 	}
+     }
+ 
+ static int find_listener(listen_rec *lr)
+     {
+     listen_rec *or;
+ 
+     for(or=old_listeners ; or ; or=or->next)
+ 	if(!memcmp(&or->local_addr,&lr->local_addr,sizeof or->local_addr))
+ 	    {
+ 	    or->used=1;
+ 	    return or->fd;
+ 	    }
+     return -1;
+     }
+ 
+ static void close_unused_listeners()
+     {
+     listen_rec *or,*next;
+ 
+     for(or=old_listeners ; or ; or=next)
+ 	{
+ 	next=or->next;
+ 	if(!or->used)
+ 	    close(or->fd);
+ 	free(or);
+ 	}
+     old_listeners=NULL;
+     }
  
  /*****************************************************************
   * Executive routines.
***************
*** 1352,1357 ****
--- 1454,1460 ----
  void standalone_main(int argc, char **argv)
  {
      struct sockaddr_in sa_server;
+     int saved_sd;
  
      standalone = 1;
      sd = listenmaxfd = -1;
***************
*** 1364,1372 ****
      sigsetjmp(restart_buffer,1);
  #endif
  
      signal (SIGHUP, SIG_IGN);	/* Until we're done (re)reading config */
      
!     if(!one_process)
      {
  #ifndef NO_KILLPG
        if (killpg(pgrp,SIGHUP) < 0)    /* Kill 'em off */
--- 1467,1477 ----
      sigsetjmp(restart_buffer,1);
  #endif
  
+     ++generation;
+ 
      signal (SIGHUP, SIG_IGN);	/* Until we're done (re)reading config */
      
!     if(!one_process && !is_graceful)
      {
  #ifndef NO_KILLPG
        if (killpg(pgrp,SIGHUP) < 0)    /* Kill 'em off */
***************
*** 1376,1386 ****
          log_unixerr ("killpg SIGHUP", NULL, NULL, server_conf);
      }
      
!     if (sd != -1 || listenmaxfd != -1) {
  	reclaim_child_processes(); /* Not when just starting up */
  	log_error ("SIGHUP received.  Attempting to restart", server_conf);
      }
      
      restart_time = time(NULL);
      clear_pool (pconf);
      ptrans = make_sub_pool (pconf);
--- 1481,1495 ----
          log_unixerr ("killpg SIGHUP", NULL, NULL, server_conf);
      }
      
!     if(is_graceful)
! 	log_error("SIGINT received.  Doing graceful restart",server_conf);
!     else if (sd != -1 || listenmaxfd != -1) {
  	reclaim_child_processes(); /* Not when just starting up */
  	log_error ("SIGHUP received.  Attempting to restart", server_conf);
      }
      
+     copy_listeners();
+     saved_sd=sd;
      restart_time = time(NULL);
      clear_pool (pconf);
      ptrans = make_sub_pool (pconf);
***************
*** 1395,1406 ****
  
      if (listeners == NULL)
      {
! 	memset((char *) &sa_server, 0, sizeof(sa_server));
! 	sa_server.sin_family=AF_INET;
! 	sa_server.sin_addr=bind_address;
! 	sa_server.sin_port=htons(server_conf->port);
  
! 	sd = make_sock(pconf, &sa_server);
      } else
      {
  	listen_rec *lr;
--- 1504,1520 ----
  
      if (listeners == NULL)
      {
!         if(!is_graceful)
! 	    {
! 	    memset((char *) &sa_server, 0, sizeof(sa_server));
! 	    sa_server.sin_family=AF_INET;
! 	    sa_server.sin_addr=bind_address;
! 	    sa_server.sin_port=htons(server_conf->port);
  
! 	    sd = make_sock(pconf, &sa_server);
! 	    }
! 	else
! 	    sd=saved_sd;
      } else
      {
  	listen_rec *lr;
***************
*** 1410,1419 ****
  	FD_ZERO(&listenfds);
  	for (lr=listeners; lr != NULL; lr=lr->next)
  	{
! 	    fd = make_sock(pconf, &lr->local_addr);
  	    FD_SET(fd, &listenfds);
  	    if (fd > listenmaxfd) listenmaxfd = fd;
  	}
  	sd = -1;
      }
  
--- 1524,1538 ----
  	FD_ZERO(&listenfds);
  	for (lr=listeners; lr != NULL; lr=lr->next)
  	{
! 	    fd=find_listener(lr);
! 	    if(fd < 0)
! 		fd = make_sock(pconf, &lr->local_addr);
! fprintf(stderr,"listening on %d\n",fd);
  	    FD_SET(fd, &listenfds);
  	    if (fd > listenmaxfd) listenmaxfd = fd;
+ 	    lr->fd=fd;
  	}
+ 	close_unused_listeners();
  	sd = -1;
      }
  
***************
*** 1451,1457 ****
--- 1570,1586 ----
  	    (void)update_child_status(child_slot,SERVER_STARTING,
  	     (request_rec*)NULL);
  	    make_child(server_conf, child_slot);
+ 
  	}
+ 
+ 	/*
+ 	if(scoreboard_image->global.please_exit && !count_live_servers())
+ #ifdef NEXT
+ 	    longjmp(restart_buffer,1);
+ #else
+ 	    siglongjmp(restart_buffer,1);
+ #endif
+ 	*/
      }
  
  } /* standalone_main */
Index: httpd.h
===================================================================
RCS file: /export/home/cvs/apache/src/httpd.h,v
retrieving revision 1.34
diff -c -r1.34 httpd.h
*** httpd.h	1996/06/17 21:38:21	1.34
--- httpd.h	1996/06/25 19:51:40
***************
*** 498,503 ****
--- 498,505 ----
  struct listen_rec {
      listen_rec *next;
      struct sockaddr_in local_addr; /* local IP address and port */
+     int fd;
+     int used;	/* Only used during restart */
  /* more stuff here, like which protocol is bound to the port */
  };
  
Index: scoreboard.h
===================================================================
RCS file: /export/home/cvs/apache/src/scoreboard.h,v
retrieving revision 1.12
diff -c -r1.12 scoreboard.h
*** scoreboard.h	1996/06/07 17:39:26	1.12
--- scoreboard.h	1996/06/25 19:51:47
***************
*** 90,94 ****
--- 90,109 ----
  #endif
  } short_score;
  
+ typedef struct
+     {
+     int exit_generation;	/* Set by the main process if a graceful
+ 				   restart is required */
+     } global_score;
+ 
+ typedef struct
+     {
+     short_score servers[HARD_SERVER_LIMIT];
+     global_score global;
+     } scoreboard;
+ 
+ #define SCOREBOARD_SIZE		sizeof(scoreboard)
+ 
  extern void sync_scoreboard_image(void);
  short_score get_scoreboard_info(int x);
+ 

-- 
Ben Laurie                  Phone: +44 (181) 994 6435
Freelance Consultant and    Fax:   +44 (181) 994 6472
Technical Director          Email: ben@algroup.co.uk
A.L. Digital Ltd,           URL: http://www.algroup.co.uk
London, England.