1/*
2  Copyright (C) 2000-2005 SKYRIX Software AG
3  Copyright (C) 2009 Inverse inc.
4
5  This file is part of SOPE.
6
7  SOPE is free software; you can redistribute it and/or modify it under
8  the terms of the GNU Lesser General Public License as published by the
9  Free Software Foundation; either version 2, or (at your option) any
10  later version.
11
12  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
13  WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
15  License for more details.
16
17  You should have received a copy of the GNU Lesser General Public
18  License along with SOPE; see the file COPYING.  If not, write to the
19  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
20  02111-1307, USA.
21*/
22
23#import <Foundation/NSAutoreleasePool.h>
24#import <Foundation/NSCalendarDate.h>
25#import <Foundation/NSData.h>
26#import <Foundation/NSException.h>
27#import <Foundation/NSFileHandle.h>
28#import <Foundation/NSProcessInfo.h>
29#import <Foundation/NSRunLoop.h>
30#import <Foundation/NSTimer.h>
31#import <Foundation/NSThread.h>
32#import <Foundation/NSUserDefaults.h>
33#import <Foundation/NSValue.h>
34
35#import <NGObjWeb/WOAdaptor.h>
36#import <NGObjWeb/WOApplication.h>
37#import <NGExtensions/NSObject+Logs.h>
38#import <NGStreams/NGActiveSocket.h>
39#import <NGStreams/NGCTextStream.h>
40#import <NGStreams/NGInternetSocketAddress.h>
41#import <NGStreams/NGNetUtilities.h>
42#import <NGStreams/NGPassiveSocket.h>
43
44/* for signal handlers */
45static volatile int pendingSIGHUP = 0;
46static volatile BOOL shouldTerminate = NO;
47
48void handle_SIGINTTERM(int signum)
49{
50  shouldTerminate = YES;
51}
52
53void handle_SIGHUP(int signum)
54{
55  pendingSIGHUP++;
56}
57
58extern void handle_SIGPIPE(int signum);
59
60#if defined(__CYGWIN32__) || defined(__MINGW32__)
61
62int WOWatchDogApplicationMain
63(NSString *appName, int argc, const char *argv[])
64{
65  /* no watchdog support on Win* */
66  return WOApplicationMain(appName, argc, argv);
67}
68
69#else
70
71#include <sys/wait.h>
72#include <sys/types.h>
73#include <unistd.h>
74#include <time.h>
75#include <string.h>
76
77#define OUT_OF_CHILD_SLEEPTIME 250 /* ~0.25ms */
78/* We sleep longer than that so the log interval is not very accurate.
79 * Should be good enough to avoid spamming the logs */
80#define OUT_OF_CHILD_LOG_INTERVAL (1000000 / OUT_OF_CHILD_SLEEPTIME) /* 1 sec */
81
82static NSTimeInterval respawnDelay; /* seconds */
83static const char *pidFile = NULL;
84NSInteger watchDogRequestTimeout;
85
86typedef enum {
87  WOChildStatusDown = 0,
88  WOChildStatusSpawning,
89  WOChildStatusReady,
90  WOChildStatusBusy,
91  WOChildStatusExcessive,
92  WOChildStatusTerminating,
93  WOChildStatusMax
94} WOChildStatus;
95
96@class WOWatchDog;
97
98@interface WOWatchDogChild : NSObject <RunLoopEvents>
99{
100  pid_t pid;
101  int counter;
102  NGActiveSocket *controlSocket;
103  WOChildStatus status;
104  NSTimer *killTimer;
105  NSUInteger killTimerIteration;
106  WOWatchDog *watchDog;
107  NSCalendarDate *lastSpawn;
108  BOOL loggedNotRespawn;
109}
110
111- (void) setWatchDog: (WOWatchDog *) newWatchDog;
112
113- (void) setPid: (int) newPid;
114- (int) pid;
115
116- (void) revokeKillTimer;
117
118- (void) handleProcessStatus: (int) status;
119
120- (void) setControlSocket: (NGActiveSocket *) newSocket;
121- (NGActiveSocket *) controlSocket;
122
123- (void) setStatus: (WOChildStatus) newStatus;
124- (WOChildStatus) status;
125
126- (void) setLastSpawn: (NSCalendarDate *) newLastSpawn;
127- (NSCalendarDate *) lastSpawn;
128- (NSCalendarDate *) nextSpawn;
129- (void) logNotRespawn;
130
131- (BOOL) readMessage;
132
133- (void) notify;
134- (void) terminate;
135
136@end
137
138@interface WOWatchDog : NSObject <RunLoopEvents>
139{
140  NSString *appName;
141  int argc;
142  const char **argv;
143
144  pid_t pid;
145  NSTimer *loopTimer;
146  BOOL terminate;
147  BOOL willTerminate;
148
149  NGPassiveSocket *listeningSocket;
150
151  int numberOfChildren;
152  NSMutableArray *children;
153  NSMutableArray *readyChildren;
154  NSMutableArray *downChildren;
155
156  long outOfChildSleepCount;
157}
158
159+ (id) sharedWatchDog;
160
161- (pid_t) pid;
162
163- (void) declareChildReady: (WOWatchDogChild *) readyChild;
164- (void) declareChildDown: (WOWatchDogChild *) readyChild;
165
166- (int) run: (NSString *) appName
167       argc: (int) argc
168       argv: (const char **) argv;
169
170@end
171
172@implementation WOWatchDogChild
173
174+ (void) initialize
175{
176  watchDogRequestTimeout = [[NSUserDefaults standardUserDefaults]
177                             integerForKey: @"WOWatchDogRequestTimeout"];
178  if (watchDogRequestTimeout > 0)
179    [self logWithFormat: @"watchdog request timeout set to %d minutes",
180          watchDogRequestTimeout];
181  else
182    [self warnWithFormat: @"watchdog request timeout not set"];
183}
184
185+ (WOWatchDogChild *) watchDogChild
186{
187  WOWatchDogChild *newChild;
188
189  newChild = [self new];
190  [newChild autorelease];
191
192  return newChild;
193}
194
195- (id) init
196{
197  if ((self = [super init]))
198    {
199      pid = -1;
200      controlSocket = nil;
201      status = WOChildStatusDown;
202      killTimer = nil;
203      killTimerIteration = 0;
204      counter = 0;
205      lastSpawn = nil;
206      loggedNotRespawn = NO;
207    }
208
209  return self;
210}
211
212- (void) dealloc
213{
214  // [self logWithFormat: @"-dealloc (pid: %d)", pid];
215  [killTimer invalidate];
216  [self setControlSocket: nil];
217  [lastSpawn release];
218  [super dealloc];
219}
220
221- (NSString *) description
222{
223  return [NSString stringWithFormat: @"WOWatchDogChild (pid: %d)", pid];
224}
225
226- (void) setWatchDog: (WOWatchDog *) newWatchDog
227{
228  watchDog = newWatchDog;
229}
230
231- (void) setPid: (int) newPid
232{
233  pid = newPid;
234}
235
236- (int) pid
237{
238  return pid;
239}
240
241- (void) revokeKillTimer
242{
243  [killTimer invalidate];
244  killTimer = nil;
245}
246
247- (void) handleProcessStatus: (int) processStatus
248{
249  int code;
250
251  code = WEXITSTATUS (processStatus);
252  if (code == 0)
253    [self logWithFormat: @"child %d exited", pid];
254  else
255    [self logWithFormat: @"child %d exited with code %i", pid, code];
256  if (WIFSIGNALED (processStatus))
257    [self logWithFormat: @" (terminated due to signal %i%@)",
258          WTERMSIG (processStatus),
259          WCOREDUMP (processStatus) ? @", coredump" : @""];
260  if (WIFSTOPPED (processStatus))
261    [self logWithFormat: @" (stopped due to signal %i)",
262          WSTOPSIG (processStatus)];
263  [self setStatus: WOChildStatusDown];
264  [self setControlSocket: nil];
265  [self revokeKillTimer];
266}
267
268- (void) setControlSocket: (NGActiveSocket *) newSocket
269{
270  NSRunLoop *runLoop;
271
272  runLoop = [NSRunLoop currentRunLoop];
273  if (controlSocket)
274    [runLoop removeEvent: (void *) ((long) [controlSocket fileDescriptor])
275                    type: ET_RDESC
276                 forMode: NSDefaultRunLoopMode
277                     all: YES];
278  [controlSocket close];
279  ASSIGN (controlSocket, newSocket);
280  if (controlSocket)
281    [runLoop addEvent: (void *) ((long) [controlSocket fileDescriptor])
282                 type: ET_RDESC
283              watcher: self
284              forMode: NSDefaultRunLoopMode];
285}
286
287- (NGActiveSocket *) controlSocket
288{
289  return controlSocket;
290}
291
292- (void) setStatus: (WOChildStatus) newStatus
293{
294  status = newStatus;
295}
296
297- (WOChildStatus) status
298{
299  return status;
300}
301
302- (void) setLastSpawn: (NSCalendarDate *) newLastSpawn
303{
304  ASSIGN (lastSpawn, newLastSpawn);
305  loggedNotRespawn = NO;
306}
307
308- (NSCalendarDate *) lastSpawn
309{
310  return lastSpawn;
311}
312
313- (NSCalendarDate *) nextSpawn
314{
315  return [lastSpawn addYear: 0 month: 0 day: 0
316                       hour: 0 minute: 0
317                     second: respawnDelay];
318}
319
320- (void) logNotRespawn
321{
322  if (!loggedNotRespawn)
323    {
324      [self logWithFormat:
325              @"avoiding to respawn child before %@", [self nextSpawn]];
326      loggedNotRespawn = YES;
327    }
328}
329
330- (void) _safetyBeltIteration: (NSTimer *) aKillTimer
331{
332  if ([watchDog pid] == getpid ()) {
333    killTimerIteration++;
334    if (killTimerIteration < watchDogRequestTimeout) {
335      [self warnWithFormat:
336              @"pid %d has been hanging in the same request for %d minutes",
337            pid, killTimerIteration];
338    }
339    else {
340      if (status != WOChildStatusDown) {
341        [self warnWithFormat: @"safety belt -- sending KILL signal to pid %d",
342              pid];
343        kill (pid, SIGKILL);
344        [self revokeKillTimer];
345      }
346    }
347  }
348  else {
349    [self errorWithFormat:
350        @"messy processes: safety belt iteration occurring on child: %d",
351          pid];
352    [self revokeKillTimer];
353  }
354}
355
356- (void) _kill
357{
358  if (status != WOChildStatusDown) {
359    [self logWithFormat: @"sending terminate signal to pid %d", pid];
360    status = WOChildStatusTerminating;
361    kill (pid, SIGTERM);
362    [self revokeKillTimer];
363  }
364}
365
366- (BOOL) readMessage
367{
368  WOChildMessage message;
369  BOOL rc;
370  NSException *e;
371
372  if ([controlSocket readBytes: &message
373                         count: sizeof (WOChildMessage)] == NGStreamError) {
374    rc = NO;
375    [self errorWithFormat: @"FAILURE receiving status for child %d", pid];
376    [self errorWithFormat: @"  socket: %@", controlSocket];
377    e = [controlSocket lastException];
378    if (e)
379      [self errorWithFormat: @"  exception: %@", e];
380    [self _kill];
381  }
382  else {
383    rc = YES;
384    [self revokeKillTimer];
385    if (message == WOChildMessageAccept) {
386      status = WOChildStatusBusy;
387      if (watchDogRequestTimeout > 0) {
388        /* We schedule an X minutes grace period while the child is processing
389           the request. This enables long requests to complete while providing
390           a safety belt for children gone rogue. */
391        killTimer
392          = [NSTimer scheduledTimerWithTimeInterval: 60
393                                             target: self
394                                           selector: @selector (_safetyBeltIteration:)
395                                           userInfo: nil
396                                            repeats: YES];
397        killTimerIteration = 0;
398      }
399    }
400    else if (message == WOChildMessageReady) {
401      status = WOChildStatusReady;
402      [watchDog declareChildReady: self];
403    }
404  }
405
406  return rc;
407}
408
409- (void) notify
410{
411  WOChildMessage message;
412
413  counter++;
414  message = WOChildMessageAccept;
415  if ([controlSocket writeBytes: &message
416                          count: sizeof (WOChildMessage)] == NGStreamError
417      || ![self readMessage]) {
418    [self errorWithFormat: @"FAILURE notifying child %d", pid];
419    [self _kill];
420  }
421}
422
423- (void) terminate
424{
425  if (status == WOChildStatusDown) {
426    [self logWithFormat: @"child is already down"];
427  } else {
428    [self _kill];
429  }
430}
431
432- (void) receivedEvent: (void*)data
433                  type: (RunLoopEventType)type
434                 extra: (void*)extra
435               forMode: (NSString*)mode
436{
437  if ([controlSocket isAlive])
438    [self readMessage];
439  else {
440    /* This happens when a socket has been closed by the child but the child
441       has not terminated yet. */
442    [[NSRunLoop currentRunLoop] removeEvent: data
443                                       type: ET_RDESC
444                                    forMode: NSDefaultRunLoopMode
445                                        all: YES];
446    [self setControlSocket: nil];
447  }
448}
449
450@end
451
452@implementation WOWatchDog
453
454+ (id) sharedWatchDog
455{
456  static WOWatchDog *sharedWatchDog = nil;
457
458  if (!sharedWatchDog)
459    sharedWatchDog = [self new];
460
461  return sharedWatchDog;
462}
463
464- (id) init
465{
466  if ((self = [super init]))
467    {
468      listeningSocket = nil;
469      terminate = NO;
470      willTerminate = NO;
471      shouldTerminate = NO;
472      pendingSIGHUP = 0;
473
474      outOfChildSleepCount = 0;
475      numberOfChildren = 0;
476      children = [[NSMutableArray alloc] initWithCapacity: 10];
477      readyChildren = [[NSMutableArray alloc] initWithCapacity: 10];
478      downChildren = [[NSMutableArray alloc] initWithCapacity: 10];
479    }
480
481  return self;
482}
483
484- (void) _releaseListeningSocket
485{
486  if (listeningSocket) {
487    [[NSRunLoop currentRunLoop] removeEvent: (void *) ((long) [listeningSocket fileDescriptor])
488                                       type: ET_RDESC
489                                    forMode: NSDefaultRunLoopMode
490                                        all: YES];
491    [listeningSocket close];
492    [listeningSocket release];
493    listeningSocket = nil;
494  }
495}
496
497- (void) dealloc
498{
499  [self _releaseListeningSocket];
500  [appName release];
501  [children release];
502  [super dealloc];
503}
504
505- (pid_t) pid
506{
507  return pid;
508}
509
510- (void) _runChildWithControlSocket: (NGActiveSocket *) controlSocket
511{
512  WOApplication *app;
513  extern char **environ;
514
515  [NSProcessInfo initializeWithArguments: (char **) argv
516                                   count: argc
517                             environment: environ];
518  NGInitTextStdio();
519  app = [NSClassFromString(appName) new];
520  [app autorelease];
521  [app setListeningSocket: listeningSocket];
522  [app setControlSocket: controlSocket];
523  [app run];
524}
525
526- (void) receivedEvent: (void*)data
527                  type: (RunLoopEventType)type
528                 extra: (void*)extra
529               forMode: (NSString*)mode
530{
531  int nextId;
532  WOWatchDogChild *child;
533  NSUInteger max;
534
535  max = [readyChildren count];
536  if (max > 0) {
537    outOfChildSleepCount = 0;
538    nextId = max - 1;
539    child = [readyChildren objectAtIndex: nextId];
540    [readyChildren removeObjectAtIndex: nextId];
541    [child notify];
542  }
543  else {
544    /* we're out of ready children, sleep a bit to avoid hogging the CPU */
545    usleep(OUT_OF_CHILD_SLEEPTIME);
546    if (outOfChildSleepCount % OUT_OF_CHILD_LOG_INTERVAL == 0) {
547      [self errorWithFormat: @"No child available to handle incoming request!"];
548    }
549    outOfChildSleepCount++;
550  }
551}
552
553- (void) _cleanupSignalAndEventHandlers
554{
555  NSRunLoop *runLoop;
556
557  signal(SIGHUP, SIG_DFL);
558  signal(SIGINT, SIG_DFL);
559  signal(SIGTERM, SIG_DFL);
560  signal(SIGPIPE, SIG_DFL);
561
562  [loopTimer invalidate];
563  loopTimer = nil;
564  runLoop = [NSRunLoop currentRunLoop];
565  [runLoop removeEvent: (void *) ((long) [listeningSocket fileDescriptor])
566                  type: ET_RDESC
567               forMode: NSDefaultRunLoopMode
568                   all: YES];
569}
570
571- (BOOL) _spawnChild: (WOWatchDogChild *) child
572{
573  NGActiveSocket *pair[2];
574  BOOL isChild;
575  int childPid;
576  extern char **environ;
577
578  isChild = NO;
579
580  if ([NGActiveSocket socketPair: pair]) {
581    childPid = fork ();
582    if (childPid == 0) {
583      setsid ();
584      isChild = YES;
585      [self _cleanupSignalAndEventHandlers];
586
587      [child retain];
588      [pair[0] retain];
589
590      [children makeObjectsPerformSelector: @selector (revokeKillTimer)];
591      [children release];
592      children = nil;
593      [readyChildren release];
594      readyChildren = nil;
595      [downChildren release];
596      downChildren = nil;
597
598      [[NSAutoreleasePool currentPool] emptyPool];
599
600      [self _runChildWithControlSocket: pair[0]];
601
602      [pair[0] autorelease];
603      [child autorelease];
604    } else if (childPid > 0) {
605      [self logWithFormat: @"child spawned with pid %d", childPid];
606      [child setPid: childPid];
607      [child setStatus: WOChildStatusSpawning];
608      [pair[1] setReceiveTimeout: 5.0];
609      [child setControlSocket: pair[1]];
610      [child setLastSpawn: [NSCalendarDate date]];
611    } else {
612      perror ("fork");
613    }
614  }
615
616  return isChild;
617}
618
619- (void) _ensureNumberOfChildren
620{
621  int currentNumber, delta, count, min, max;
622  WOWatchDogChild *child;
623
624  currentNumber = [children count];
625  if (currentNumber < numberOfChildren) {
626      delta = numberOfChildren - currentNumber;
627      for (count = 0; count < delta; count++) {
628        child = [WOWatchDogChild watchDogChild];
629        [child setWatchDog: self];
630        [children addObject: child];
631        [downChildren addObject: child];
632      }
633      [self logWithFormat: @"preparing %d children", delta];
634  }
635  else if (currentNumber > numberOfChildren) {
636    delta = currentNumber - numberOfChildren;
637    max = [downChildren count];
638    if (max > delta)
639      min = max - delta;
640    else
641      min = 0;
642    for (count = max - 1; count >= min; count--) {
643      child = [downChildren objectAtIndex: count];
644      [downChildren removeObjectAtIndex: count];
645      [children removeObject: child];
646      delta--;
647      [self logWithFormat: @"%d processes purged from pool", delta];
648    }
649
650    max = [readyChildren count];
651    if (max > delta)
652      max -= delta;
653    for (count = max - 1; count > -1; count--) {
654      child = [readyChildren objectAtIndex: count];
655      [readyChildren removeObjectAtIndex: count];
656      [child terminate];
657      [child setStatus: WOChildStatusExcessive];
658      delta--;
659    }
660    [self logWithFormat: @"%d processes left to terminate", delta];
661  }
662}
663
664- (void) _noop
665{
666}
667
668- (BOOL) _ensureChildren
669{
670  int count, max;
671  WOWatchDogChild *child;
672  BOOL isChild, delayed;
673  NSCalendarDate *now, *nextSpawn;
674
675  isChild = NO;
676
677  if (!willTerminate) {
678    [self _ensureNumberOfChildren];
679    max = [downChildren count];
680    for (count = max - 1; !isChild && count > -1; count--) {
681      delayed = NO;
682      child = [downChildren objectAtIndex: count];
683
684      if ([child status] == WOChildStatusExcessive)
685        [children removeObject: child];
686      else {
687        now = [NSCalendarDate date];
688        nextSpawn = [child nextSpawn];
689        if ([nextSpawn earlierDate: now] == nextSpawn)
690          isChild = [self _spawnChild: child];
691        else {
692          delayed = YES;
693          [child logNotRespawn];
694        }
695      }
696      if (!(delayed || isChild))
697        [downChildren removeObjectAtIndex: count];
698    }
699  }
700
701  return isChild;
702}
703
704/* SOPE on GNUstep does not need to parse the argument line, since the
705   arguments will be put in the NSArgumentDomain. I don't know about
706   libFoundation but OSX is supposed to act the same way. */
707- (NGInternetSocketAddress *) _listeningAddress
708{
709  NGInternetSocketAddress *listeningAddress;
710  NSUserDefaults *ud;
711  id port, allow;
712  static BOOL warnedAboutAllow = NO;
713
714  listeningAddress = nil;
715
716  ud = [NSUserDefaults standardUserDefaults];
717  port = [ud objectForKey:@"p"];
718  if (!port) {
719    port = [ud objectForKey:@"WOPort"];
720    if (!port)
721      port = @"auto";
722  }
723  allow = [ud objectForKey:@"WOHttpAllowHost"];
724  if ([allow count] > 0 && !warnedAboutAllow) {
725    [self warnWithFormat: @"'WOHttpAllowHost' is ignored in watchdog mode,"
726          @" use a real firewall instead"];
727    warnedAboutAllow = YES;
728  }
729
730  if ([port isKindOfClass: [NSString class]]) {
731    if ([port isEqualToString: @"auto"]) {
732      listeningAddress
733        = [[NGInternetSocketAddress alloc] initWithPort:0 onHost:@"127.0.0.1"];
734      [listeningAddress autorelease];
735    } else if ([port rangeOfString: @":"].location == NSNotFound) {
736      if (allow)
737        listeningAddress =
738          [NGInternetSocketAddress wildcardAddressWithPort:[port intValue]];
739      else
740        port = [NSString stringWithFormat: @"127.0.0.1:%d", [port intValue]];
741    }
742  }
743  else {
744    if (allow)
745      listeningAddress =
746        [NGInternetSocketAddress wildcardAddressWithPort:[port intValue]];
747    else {
748      port = [NSString stringWithFormat: @"127.0.0.1:%@", port];
749    }
750  }
751
752  if (!listeningAddress)
753    listeningAddress = (NGInternetSocketAddress *) NGSocketAddressFromString(port);
754
755  return listeningAddress;
756}
757
758- (BOOL) _prepareListeningSocket
759{
760  NGInternetSocketAddress *addr;
761  NSString *address;
762  BOOL rc;
763  int backlog;
764
765  addr = [self _listeningAddress];
766  NS_DURING {
767    [listeningSocket release];
768    listeningSocket = [[NGPassiveSocket alloc] initWithDomain: [addr domain]];
769    [listeningSocket bindToAddress: addr];
770    backlog = [[NSUserDefaults standardUserDefaults]
771                integerForKey: @"WOListenQueueSize"];
772    if (!backlog)
773      backlog = 5;
774    [listeningSocket listenWithBacklog: backlog];
775    address = [addr address];
776    if (!address)
777      address = @"*";
778    [self logWithFormat: @"listening on %@:%d", address, [addr port]];
779    [[NSRunLoop currentRunLoop] addEvent: (void *) ((long) [listeningSocket fileDescriptor])
780                                    type: ET_RDESC
781                                 watcher: self
782                                 forMode: NSDefaultRunLoopMode];
783    rc = YES;
784  }
785  NS_HANDLER {
786    rc = NO;
787  }
788  NS_ENDHANDLER;
789
790  return rc;
791}
792
793- (WOWatchDogChild *) _childWithPID: (pid_t) childPid
794{
795  WOWatchDogChild *currentChild, *child;
796  int count;
797
798  child = nil;
799  for (count = 0; !child && count < numberOfChildren; count++) {
800    currentChild = [children objectAtIndex: count];
801    if ([currentChild pid] == childPid)
802      child = currentChild;
803  }
804
805  return child;
806}
807
808- (void) _setupSignals
809{
810  signal(SIGHUP, handle_SIGHUP);
811  signal(SIGINT, handle_SIGINTTERM);
812  signal(SIGTERM, handle_SIGINTTERM);
813  signal(SIGPIPE, handle_SIGPIPE);
814}
815
816- (void) declareChildReady: (WOWatchDogChild *) readyChild
817{
818  if (![readyChildren containsObject: readyChild])
819    [readyChildren addObject: readyChild];
820}
821
822- (void) declareChildDown: (WOWatchDogChild *) downChild
823{
824  if (![downChildren containsObject: downChild])
825    [downChildren addObject: downChild];
826}
827
828- (void) _ensureWorkersCount
829{
830  int newNumberOfChildren;
831  NSUserDefaults *ud;
832
833  ud = [NSUserDefaults standardUserDefaults];
834  [ud synchronize];
835  newNumberOfChildren = [ud integerForKey: @"WOHttpAdaptorForkCount"];
836  if (newNumberOfChildren)
837    [self logWithFormat: @"user default 'WOHttpAdaptorForkCount' has been"
838          " replaced with 'WOWorkersCount'"];
839  else
840    newNumberOfChildren = [ud integerForKey: @"WOWorkersCount"];
841  if (newNumberOfChildren < 1)
842    newNumberOfChildren = 1;
843  numberOfChildren = newNumberOfChildren;
844}
845
846- (void) _handlePostTerminationSignal
847{
848  WOWatchDogChild *child;
849  int count;
850
851  [self logWithFormat: @"Terminating with SIGINT or SIGTERM"];
852  [self _releaseListeningSocket];
853  for (count = 0; count < numberOfChildren; count++) {
854    child = [children objectAtIndex: count];
855    if ([child status] != WOChildStatusDown
856        && [child status] != WOChildStatusTerminating)
857      [child terminate];
858  }
859
860  if ([downChildren count] == numberOfChildren) {
861    [self logWithFormat: @"all children exited. We now terminate."];
862    terminate = YES;
863  }
864  else
865    willTerminate = YES;
866}
867
868- (void) _checkProcessesStatus
869{
870  int status;
871  pid_t childPid;
872  WOWatchDogChild *child;
873
874  while ((childPid = waitpid (-1, &status, WNOHANG)) > 0) {
875    child = [self _childWithPID: childPid];
876    [child handleProcessStatus: status];
877    [self declareChildDown: child];
878    if (willTerminate && [downChildren count] == numberOfChildren) {
879      [self logWithFormat: @"all children exited. We now terminate."];
880      terminate = YES;
881    }
882  }
883}
884
885- (int) run: (NSString *) newAppName
886       argc: (int) newArgC argv: (const char **) newArgV
887{
888  NSAutoreleasePool *pool;
889  NSRunLoop *runLoop;
890  NSDate *limitDate;
891  BOOL listening;
892  int retries;
893
894  willTerminate = NO;
895
896  ASSIGN (appName, newAppName);
897
898  argc = newArgC;
899  argv = newArgV;
900
901  listening = NO;
902  retries = 0;
903  while (!listening && retries < 5) {
904    listening = [self _prepareListeningSocket];
905    retries++;
906    if (!listening) {
907      [self warnWithFormat: @"listening socket: attempt %d failed", retries];
908      [NSThread sleepForTimeInterval: 1.0];
909    }
910  }
911  if (listening) {
912    pid = getpid ();
913    [self logWithFormat: @"watchdog process pid: %d", pid];
914    [self _setupSignals];
915    [self _ensureWorkersCount];
916
917    // NSLog (@"ready to process requests");
918    runLoop = [NSRunLoop currentRunLoop];
919
920    /* This timer ensures the looping of the runloop at reasonable intervals
921       for correct processing of signal handlers. */
922    loopTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5
923                                                 target: self
924                                               selector: @selector (_noop)
925                                               userInfo: nil
926                                                repeats: YES];
927    terminate = NO;
928    while (!terminate) {
929      pool = [NSAutoreleasePool new];
930
931      while (pendingSIGHUP) {
932        [self logWithFormat: @"received SIGHUP"];
933        [self _ensureWorkersCount];
934        pendingSIGHUP--;
935      }
936
937      if (shouldTerminate && pidFile)
938        unlink(pidFile);
939
940      // [self logWithFormat: @"watchdog loop"];
941      NS_DURING {
942        terminate = [self _ensureChildren];
943        if (!terminate) {
944          limitDate = [runLoop limitDateForMode:NSDefaultRunLoopMode];
945          [runLoop runMode: NSDefaultRunLoopMode beforeDate: limitDate];
946        }
947      }
948      NS_HANDLER {
949        terminate = YES;
950        [self errorWithFormat:
951                @"an exception occured in runloop %@", localException];
952      }
953      NS_ENDHANDLER;
954
955      if (!terminate) {
956        if (shouldTerminate)
957          [self _handlePostTerminationSignal];
958        [self _checkProcessesStatus];
959      }
960
961      [pool release];
962    }
963
964    [self _cleanupSignalAndEventHandlers];
965  }
966  else
967    [self errorWithFormat: @"unable to listen on specified port,"
968          @" check that no other process is already using it"];
969
970  return 0;
971}
972
973@end
974
975static BOOL _writePid(NSString *nsPidFile) {
976  NSString *pid;
977  BOOL rc;
978
979  pid = [NSString stringWithFormat: @"%d", getpid()];
980  rc = [pid writeToFile: nsPidFile atomically: NO];
981
982  return rc;
983}
984
985int WOWatchDogApplicationMain
986(NSString *appName, int argc, const char *argv[])
987{
988  NSAutoreleasePool *pool;
989  NSUserDefaults *ud;
990  NSString *logFile, *nsPidFile;
991  int rc;
992  pid_t childPid;
993  NSProcessInfo *processInfo;
994  Class WOAppClass;
995
996  pool = [[NSAutoreleasePool alloc] init];
997
998#if LIB_FOUNDATION_LIBRARY || defined(GS_PASS_ARGUMENTS)
999  {
1000    extern char **environ;
1001    [NSProcessInfo initializeWithArguments:(void*)argv count:argc
1002                   environment:(void*)environ];
1003  }
1004#endif
1005
1006  /* This invocation forces the class initialization of WOCoreApplication,
1007     which causes the NSUserDefaults to be initialized as well with
1008     Defaults.plist. */
1009  WOAppClass = [NSClassFromString (appName) class];
1010
1011  ud = [NSUserDefaults standardUserDefaults];
1012  processInfo = [NSProcessInfo processInfo];
1013
1014  logFile = [ud objectForKey: @"WOLogFile"];
1015  if (!logFile)
1016    logFile = [NSString stringWithFormat: @"/var/log/%@/%@.log",
1017                        [processInfo processName],
1018                        [processInfo processName]];
1019  if (![logFile isEqualToString: @"-"]) {
1020    freopen([logFile cString], "a", stdout);
1021    freopen([logFile cString], "a", stderr);
1022  }
1023  if ([ud boolForKey: @"WONoDetach"])
1024    childPid = 0;
1025  else
1026    childPid = fork();
1027
1028  if (childPid) {
1029    rc = 0;
1030  }
1031  else {
1032    nsPidFile = [ud objectForKey: @"WOPidFile"];
1033    if (!nsPidFile)
1034      nsPidFile = [NSString stringWithFormat: @"/var/run/%@/%@.pid",
1035                            [processInfo processName],
1036                            [processInfo processName]];
1037    pidFile = [nsPidFile UTF8String];
1038    if (_writePid(nsPidFile)) {
1039      respawnDelay = [ud integerForKey: @"WORespawnDelay"];
1040      if (!respawnDelay)
1041        respawnDelay = 5;
1042
1043      if ([WOAppClass respondsToSelector: @selector (applicationWillStart)])
1044        [WOAppClass applicationWillStart];
1045
1046      /* default is to use the watch dog! */
1047      if ([ud objectForKey:@"WOUseWatchDog"] != nil
1048          && ![ud boolForKey:@"WOUseWatchDog"])
1049        rc = WOApplicationMain(appName, argc, argv);
1050      else
1051        rc = [[WOWatchDog sharedWatchDog] run: appName argc: argc argv: argv];
1052    }
1053    else {
1054      [ud errorWithFormat: @"unable to open pid file: %s", pidFile];
1055      rc = -1;
1056    }
1057  }
1058
1059  [pool release];
1060
1061  return rc;
1062}
1063#endif
1064
1065/* main function which initializes server defaults (usually in /etc) */
1066
1067@interface NSUserDefaults(ServerDefaults)
1068+ (id)hackInServerDefaults:(NSUserDefaults *)_ud
1069         withAppDomainPath:(NSString *)_appDomainPath
1070          globalDomainPath:(NSString *)_globalDomainPath;
1071@end
1072
1073int WOWatchDogApplicationMainWithServerDefaults
1074(NSString *appName, int argc, const char *argv[],
1075 NSString *globalDomainPath, NSString *appDomainPath)
1076{
1077  NSAutoreleasePool *pool;
1078  Class defClass;
1079
1080  pool = [[NSAutoreleasePool alloc] init];
1081#if LIB_FOUNDATION_LIBRARY || defined(GS_PASS_ARGUMENTS)
1082  {
1083    extern char **environ;
1084    [NSProcessInfo initializeWithArguments:(void*)argv count:argc
1085                               environment:(void*)environ];
1086  }
1087#endif
1088
1089  if ((defClass = NSClassFromString(@"WOServerDefaults")) != nil) {
1090    NSUserDefaults *ud;
1091
1092    ud = [NSUserDefaults standardUserDefaults];
1093    [defClass hackInServerDefaults:ud
1094              withAppDomainPath:appDomainPath
1095              globalDomainPath:globalDomainPath];
1096
1097#if 0
1098    if (((sd == nil) || (sd == ud)) && (appDomainPath != nil)) {
1099      NSLog(@"Note: not using server defaults: '%@' "
1100            @"(not supported on this Foundation)", appDomainPath);
1101    }
1102#endif
1103  }
1104
1105  [pool release];
1106
1107  return WOWatchDogApplicationMain(appName, argc, argv);
1108}
1109