chiark / gitweb /
fixes
[inn-innduct.git] / innfeed / innlistener.c
1 /*  $Id: innlistener.c 6716 2004-05-16 20:26:56Z rra $
2 **
3 **  The implementation of the innfeed InnListener class.
4 **
5 **  Written by James Brister <brister@vix.com>
6 */
7
8 #include "innfeed.h"
9 #include "config.h"
10 #include "clibrary.h"
11
12 #include <assert.h>
13 #include <ctype.h>
14 #include <errno.h>
15 #include <fcntl.h>
16 #include <syslog.h>
17 #include <time.h>
18
19 #include "inn/messages.h"
20 #include "libinn.h"
21
22 #include "article.h"
23 #include "buffer.h"
24 #include "configfile.h"
25 #include "endpoint.h"
26 #include "host.h"
27 #include "innlistener.h"
28 #include "nntp.h"
29 #include "tape.h"
30
31 #define LISTENER_INPUT_BUFFER (1024 * 8) /* byte size of the input buffer */
32 #define EOF_SLEEP_TIME 1        /* seconds to sleep when EOF on InputFile */
33
34 struct innlistener_s 
35 {
36     EndPoint myep ;
37
38     Host *myHosts ;
39     size_t hostLen ;
40     Buffer inputBuffer ;
41     bool dummyListener ;
42     bool dynamicPeers ;
43     TimeoutId inputEOFSleepId ;
44
45     InnListener next ;
46 };
47
48 static unsigned int listenerCount = 0 ;
49 static InnListener listenerList = NULL ;
50
51 InnListener mainListener ;
52
53 static FILE *droppedFp = NULL ;
54 static long droppedCount = 0 ;
55 static int droppedFileCount = 0 ;
56 static char *dropArtFile = NULL ;
57 static bool fastExit = false ;
58
59 extern const char *pidFile ;
60 extern const char *InputFile ;
61 extern bool RollInputFile ;
62 extern bool genHtml ;
63
64
65 static void giveArticleToPeer (InnListener lis,
66                                Article article, const char *peerName) ;
67 static void newArticleCommand (EndPoint ep, IoStatus i,
68                                Buffer *buffs, void *data) ;
69 static void wakeUp (TimeoutId id, void *data) ;
70 static void writeCheckPoint (int offsetAdjust) ;
71 static void dropArticle (const char *peer, Article article) ;
72 static void listenerCleanup (void) ;
73
74 static bool inited = false ;
75
76
77 void listenerLogStatus (FILE *fp)
78 {
79   fprintf (fp,"%sListener Status:%s\n",
80            genHtml ? "<B>" : "", genHtml ? "</B>" : "") ;
81   fprintf (fp,"    Dropped article file: %s\n",dropArtFile) ;
82   fprintf (fp,"   Dropped article count: %ld\n",(long) droppedCount) ;
83   fprintf (fp,"\n") ;
84 }
85
86 InnListener newListener (EndPoint endp, bool isDummy, bool dynamicPeers)
87 {
88   InnListener l = xcalloc (1, sizeof(struct innlistener_s)) ;
89   Buffer *readArray ;
90
91   if (!inited)
92     {
93       inited = true ;
94       atexit (listenerCleanup) ;
95     }
96   
97   l->myep = endp ;
98
99   l->hostLen = MAX_HOSTS ;
100   l->myHosts = xcalloc (l->hostLen, sizeof(Host)) ;
101
102   l->inputBuffer = newBuffer (LISTENER_INPUT_BUFFER) ;
103   l->dummyListener = isDummy ;
104   l->dynamicPeers = dynamicPeers ;
105
106   addPointerFreedOnExit ((char *)bufferBase(l->inputBuffer)) ;
107   addPointerFreedOnExit ((char *)l->myHosts) ;
108   addPointerFreedOnExit ((char *)l) ;
109
110   readArray = makeBufferArray (bufferTakeRef (l->inputBuffer), NULL) ;
111   prepareRead (endp,readArray,newArticleCommand,l,1) ;
112
113   l->next = listenerList ;
114   listenerList = l ;
115
116   listenerCount++ ;
117
118   return l ;
119 }
120
121 void gPrintListenerInfo (FILE *fp, unsigned int indentAmt)
122 {
123   InnListener p ;
124   char indent [INDENT_BUFFER_SIZE] ;
125   unsigned int i ;
126   
127   for (i = 0 ; i < MIN(INDENT_BUFFER_SIZE - 1,indentAmt) ; i++)
128     indent [i] = ' ' ;
129   indent [i] = '\0' ;
130
131   fprintf (fp,"%sGlobal InnListener list : %p (count %d) {\n",
132            indent,(void *) listenerList,listenerCount) ;
133   for (p = listenerList ; p != NULL ; p = p->next)
134     printListenerInfo (p,fp,indentAmt + INDENT_INCR) ;
135   fprintf (fp,"%s}\n",indent) ;
136 }
137
138
139
140 void printListenerInfo (InnListener listener, FILE *fp, unsigned int indentAmt)
141 {
142   char indent [INDENT_BUFFER_SIZE] ;
143   unsigned int i ;
144   
145   for (i = 0 ; i < MIN(INDENT_BUFFER_SIZE - 1,indentAmt) ; i++)
146     indent [i] = ' ' ;
147   indent [i] = '\0' ;
148
149   fprintf (fp,"%sInnListener : %p {\n",indent,(void *) listener) ;
150   fprintf (fp,"%s    endpoint : %p\n", indent,(void *) listener->myep) ;
151   fprintf (fp,"%s    dummy-listener : %s\n",indent,
152            boolToString (listener->dummyListener)) ;
153   fprintf (fp,"%s    dynamicPeers : %s\n",indent,
154            boolToString (listener->dynamicPeers)) ;
155
156   fprintf (fp,"%s    input-buffer {\n",indent) ;
157   printBufferInfo (listener->inputBuffer,fp,indentAmt + INDENT_INCR) ;
158   fprintf (fp,"%s    }\n",indent) ;
159
160   fprintf (fp,"%s    hosts {\n",indent) ;
161   for (i = 0 ; i < listener->hostLen ; i++)
162     {
163 #if 0
164       if (listener->myHosts [i] != NULL)
165         printHostInfo (listener->myHosts [i],fp,indentAmt + INDENT_INCR) ;
166 #else
167       fprintf (fp,"%s        %p\n",indent,(void *) listener->myHosts[i]) ;
168 #endif
169     }
170   
171   fprintf (fp,"%s    }\n",indent) ;
172   
173   fprintf (fp,"%s}\n",indent) ;
174 }
175
176   /* Unlink the pidFile if and only if it is our pidFile.
177      There is still a racecondition here but very small. */
178 static void unlinkPidFile (void)
179 {
180   FILE *fp;
181   char buf[32];
182
183   if ((fp = fopen(pidFile, "r")) == NULL)
184     return;
185
186   if (fgets(buf, 32, fp) != NULL && atoi(buf) == getpid())
187     unlink(pidFile);
188   fclose(fp);
189 }
190
191   /* Close down all hosts on this listener. When they're all gone the
192      Listener will be deleted. */
193 void shutDown (InnListener l)
194 {
195   unsigned int i ;
196   unsigned int count ;
197
198   d_printf (1,"Shutting down the listener\n") ;
199
200   /* When shutting down the mainListener, stop writing to the
201      StatusFile and remove our pidFile. */
202   if (l == mainListener)
203     {
204       /*hostCleanup (); should do this but .. */
205       hostSetStatusFile ("/dev/null");
206       unlinkPidFile();
207     }
208
209   closeDroppedArticleFile () ;
210   
211   if (l->myep != NULL)
212     {
213       if (l->inputEOFSleepId != 0)
214         removeTimeout (l->inputEOFSleepId) ;
215       l->inputEOFSleepId = 0 ;
216       delEndPoint (l->myep) ;
217     }
218   l->myep = NULL ;
219   
220   for (i = 0, count = 0 ; i < l->hostLen ; i++)
221     if (l->myHosts [i] != NULL) 
222       {
223         hostClose (l->myHosts[i]) ;
224         count++ ;
225       }
226
227   if (count == 0 || fastExit)
228     {
229       time_t now = theTime () ;
230       char dateString [30] ;
231
232       gHostStats();
233       strlcpy (dateString,ctime (&now),sizeof (dateString)) ;
234       dateString [24] = '\0' ;
235
236       if (fastExit)
237         notice ("ME finishing (quickly) at %s", dateString) ;
238       else
239         notice ("ME finishing at %s", dateString) ;
240
241       unlinkPidFile();
242       exit (0) ;
243     }
244 }
245
246
247 bool listenerAddPeer (InnListener listener, Host hostObj)
248 {
249   unsigned int i ;
250
251   d_printf (1,"Adding peer: %s\n", hostPeerName (hostObj)) ;
252   
253   for (i = 0 ; i < listener->hostLen ; i++)
254     {
255       if (listener->myHosts [i] == NULL)
256         {
257           listener->myHosts [i] = hostObj ;
258
259           return true ;
260         }
261     }
262
263   return false ;
264 }
265
266
267 /* return true if this listener doesn't ever generate articles. */
268 bool listenerIsDummy (InnListener listener)
269 {
270   return listener->dummyListener ;
271 }
272
273 /* Called by the Host when it (the Host) is about to delete itself. */
274 unsigned int listenerHostGone (InnListener listener, Host host)
275 {
276   unsigned int i ;
277   unsigned int someThere = 0 ;
278
279   d_printf (1,"Host is gone: %s\n", hostPeerName (host)) ;
280   
281   for (i = 0 ; i < listener->hostLen ; i++)
282     if (listener->myHosts [i] == host)
283       listener->myHosts [i] = NULL ;
284     else if (listener->myHosts [i] != NULL)
285       someThere++ ;
286
287   return someThere ;
288 }
289
290
291 /* called by the Host when it has nothing to do. */
292 void listenerHostIsIdle (InnListener listener, Host host)
293 {
294   ASSERT (listener != NULL) ;
295   ASSERT (host != NULL) ;
296
297   d_printf (1,"Host is idle: %s\n", hostPeerName (host)) ;
298   
299   if (!listener->dummyListener)
300     return ;
301
302   /* if this listener is a dummy (i.e. not generating articles cause we're
303      just dealing with backlog files) then forget about the host and when
304      last one is gone we exit. */
305
306   hostClose (host) ;
307 }
308
309
310 void openInputFile (void)
311 {
312   int fd, i, mainFd ;
313   off_t offset ;
314   char buf [32], *p ;
315
316   ASSERT (InputFile && *InputFile) ;
317
318   fd = open(InputFile, O_RDWR) ;
319   if (fd < 0)
320     die ("open %s: %s\n", InputFile, strerror(errno)) ;
321
322   mainFd = getMainEndPointFd() ;
323   if (fd != mainFd)
324     {
325       if (dup2(fd, mainFd) < 0)
326         die ("dup2 %d %d: %s\n", fd, mainFd, strerror(errno)) ;
327       close (fd);
328     }
329
330   i = read(mainFd, buf, sizeof (buf)) ;
331   if (i < 0)
332     die ("read %s: %s\n", InputFile, strerror(errno)) ;
333   else if (i > 0)
334     {
335       p = buf;
336       buf [ sizeof(buf) - 1 ] = '\0';
337       offset = (off_t) strtol (p, &p, 10) ;
338       if (offset > 0 && *p == '\n')
339         lseek (mainFd, offset, SEEK_SET) ;
340       else
341         lseek (mainFd, 0, SEEK_SET) ;
342     }
343   syslog(LOG_NOTICE, "ME opened %s", InputFile);
344 }
345
346
347 int listenerConfigLoadCbk (void *data UNUSED)
348 {
349   int bval ;
350
351   if (getBool (topScope,"fast-exit",&bval,NO_INHERIT))
352     fastExit = (bval ? true : false) ;
353
354   return 1 ;
355 }
356
357 /**********************************************************************/
358 /**                     STATIC PRIVATE FUNCTIONS                     **/
359 /**********************************************************************/
360
361
362 /* EndPoint callback function for when the InnListener's fd is ready for
363    reading. */
364 static void newArticleCommand (EndPoint ep, IoStatus i,
365                                Buffer *buffs, void *data)
366 {
367   InnListener lis = (InnListener) data ;
368   char *msgid, *msgidEnd ;
369   char *fileName, *fileNameEnd ;
370   char *peer, *peerEnd ;
371   char *cmd, *endc ;
372   char *bbase = bufferBase (buffs [0]) ;
373   size_t blen = bufferDataSize (buffs [0]) ;
374   Buffer *readArray ;
375   static int checkPointCounter ;
376   char *s;
377
378   ASSERT (ep == lis->myep) ;
379
380   bufferAddNullByte  (buffs [0]) ;
381   
382   if (i == IoEOF)
383     {
384       if ( lis == mainListener && InputFile != NULL )
385         {
386           if ( RollInputFile )
387             {
388               syslog(LOG_NOTICE, "ME reached EOF in %s", InputFile);
389               openInputFile () ;
390               RollInputFile = false ;
391               readArray = makeBufferArray (bufferTakeRef (buffs [0]),NULL) ;
392               prepareRead (ep, readArray, newArticleCommand, data, 1) ;
393             }
394           else
395             {
396               lis->inputEOFSleepId =
397                 prepareSleep (wakeUp, EOF_SLEEP_TIME, data) ;
398
399             }
400         }
401       else 
402         {
403           d_printf (1,"Got EOF on listener\n") ;
404           notice ("ME source lost . Exiting");
405           shutDown (lis) ;
406         }
407     }
408   else if (i == IoFailed)
409     {
410       errno = endPointErrno (ep) ;
411 #if HAVE_SOCKETPAIR
412       if (errno != ECONNABORTED)
413 #endif
414       syswarn ("ME source read error, exiting") ;
415       d_printf (1,"Got IO Error on listener\n") ;
416       shutDown (lis) ;
417     }
418   else if (strchr (bbase, '\n') == NULL) /* partial read */
419     {
420       /* check for input corrupted by NULs - if they
421          precede the newline, we never get out of here */
422       if (strlen(bbase) < blen)
423         {
424           warn ("ME source format bad, exiting: %s", bbase) ;
425           shutDown (lis) ;
426
427           return ;
428         }
429       if (blen == bufferSize(buffs [0])) {
430         if (!expandBuffer (buffs [0], BUFFER_EXPAND_AMOUNT)) {
431           warn ("ME error expanding input buffer") ;
432           shutDown (lis) ;
433           return ;
434         }
435       }
436       readArray = makeBufferArray (bufferTakeRef (buffs [0]),NULL) ;
437       if (!prepareRead (ep, readArray, newArticleCommand, data, 1)) {
438         warn ("ME error prepare read failed") ;
439         freeBufferArray (readArray) ;
440         shutDown (lis) ;
441         return ;
442       }
443     }
444   else
445     {
446       /* now iterate over each full command we got on the input. */
447       cmd = bbase ;
448       while ((cmd < (bbase + blen)) && ((endc = strchr (cmd,'\n')) != NULL))
449         {
450           Article article ;
451           char *next = endc + 1;
452
453           if (*next == '\r')
454             next++ ;
455
456           endc-- ;
457           if (*endc != '\r')
458             endc++ ;
459
460           *endc = '\0' ;
461
462           /* check for input corrupted by NULs - if they are preceded
463              by newline, we may skip a large chunk without noticing */
464           if (*next == '\0' && next < bbase + blen)
465             {
466               warn ("ME source format bad, exiting: %s", cmd) ;
467               shutDown (lis) ;
468
469               return ;
470             }
471           
472           d_printf (2,"INN Command: %s\n", cmd) ;
473
474           /* pick out the leading string (the filename) */
475           if ((fileName = findNonBlankString (cmd,&fileNameEnd)) == NULL)
476             {
477               warn ("ME source format bad, exiting: %s", cmd) ;
478               shutDown (lis) ;
479
480               return ;
481             }
482           
483           *fileNameEnd = '\0' ; /* for the benefit of newArticle() */
484
485           /* now pick out the next string (the message id) */
486           if ((msgid = findNonBlankString (fileNameEnd + 1,&msgidEnd)) == NULL)
487             {
488               *fileNameEnd = ' ' ; /* to make syslog work properly */
489               warn ("ME source format bad, exiting: %s", cmd) ;
490               shutDown (lis) ;
491
492               return ;
493             }
494
495           *msgidEnd = '\0' ;    /* for the benefit of newArticle() */
496           
497           /* now create an article object and give it all the peers on the
498              rest of the command line. Will return null if file is missing. */
499           article = newArticle (fileName, msgid) ;
500           *fileNameEnd = ' ' ;
501
502           /* Check the message ID length */
503           if (strlen(msgid) > NNTP_MSGID_MAXLEN) {
504             warn ("ME message id exceeds limit of %d octets: %s",
505                   NNTP_MSGID_MAXLEN, msgid) ;
506             *(msgidEnd+1) = '\0' ;
507           }
508           *msgidEnd = ' ' ;
509
510           /* Check if message ID starts with < and ends with > */
511           if (*msgid != '<' || *(msgidEnd-1) != '>') {
512             warn ("ME source format bad, exiting: %s", cmd) ;
513             *(msgidEnd+1) = '\0';
514           }
515
516           /* now get all the peernames off the rest of the command lines */
517           peerEnd = msgidEnd ;
518           do 
519             {
520               *peerEnd = ' ' ;
521
522               /* pick out the next peer name */
523               if ((peer = findNonBlankString (peerEnd + 1,&peerEnd))==NULL)
524                 break ;     /* even no peer names is OK. */ /* XXX REALLY? */
525
526               *peerEnd = '\0' ;
527               
528               /* See if this is a valid peername */
529               for(s = peer; *s; s++)
530                 if (!CTYPE(isalnum, *s) && *s != '.' && *s != '-' && *s != '_')
531                   break;
532               if (*s != 0) {
533                   warn ("ME invalid peername %s", peer) ;
534                   continue;
535               }
536               if (article != NULL)
537                 giveArticleToPeer (lis,article,peer) ;
538             }
539           while (peerEnd < endc) ;
540
541           delArticle (article) ;
542           
543           cmd = next ;
544
545           /* write a checkpoint marker if we've done another large chunk */
546           if (InputFile && *InputFile && ++checkPointCounter == 1000)
547             {
548               /* adjust the seek pointer value by the current location
549                  within the input buffer */
550               writeCheckPoint (blen - (cmd - bbase)) ;
551               checkPointCounter = 0 ;
552             }
553
554         }
555
556       if (*cmd != '\0')         /* partial command left in buffer */
557         {
558           Buffer *bArr ;
559           unsigned int leftAmt = blen - (cmd - bbase) ;
560
561           ASSERT (cmd != bbase) ;
562           /* first we shift whats left in the buffer down to the bottom */
563           memmove (bbase,cmd,leftAmt) ;
564           bufferSetDataSize (buffs [0],leftAmt) ;
565       
566           bArr = makeBufferArray (bufferTakeRef (buffs [0]),NULL) ;
567       
568           if ( !prepareRead (lis->myep, bArr, newArticleCommand, lis, 1) )
569             {
570               warn ("ME error prepare read failed") ;
571
572               freeBufferArray (bArr) ;
573               
574               shutDown (lis) ;
575
576               return ;
577             }
578         }
579       else if ( !readIsPending (lis->myep) ) 
580         {                       /* XXX read should never be pending here */
581           Buffer *bArr = makeBufferArray (bufferTakeRef (buffs [0]),NULL) ;
582       
583           bufferSetDataSize (buffs [0],0) ;
584       
585           if ( !prepareRead (lis->myep, bArr, newArticleCommand, lis, 1) )
586             {
587               warn ("ME error prepare read failed") ;
588
589               shutDown (lis) ;
590
591               return ;
592             }
593         }
594     }
595
596   freeBufferArray (buffs) ;
597 }
598
599 /* EndPoint callback function for when the sleep due to 
600    having reached EOF on InputFile is done. */
601 static void wakeUp (TimeoutId id, void *data)
602 {
603   InnListener lis = (InnListener) data ;
604   Buffer *readArray ;
605
606   ASSERT (id == lis->inputEOFSleepId) ;
607
608   lis->inputEOFSleepId = 0 ;
609   readArray = makeBufferArray (bufferTakeRef (lis->inputBuffer), NULL) ;
610   prepareRead (lis->myep,readArray,newArticleCommand,lis,1) ;
611 }
612
613
614 /* Find the Host object for the peer and hand off a reference to the
615    article for it to transmit. */
616 static void giveArticleToPeer (InnListener lis,
617                                Article article, const char *peerName)
618 {
619   unsigned int i ;
620
621   for (i = 0 ; i < lis->hostLen ; i++)
622     if (lis->myHosts[i] != NULL)
623       if (strcmp (peerName,hostPeerName (lis->myHosts [i])) == 0)
624         {
625           d_printf (1,"Giving article to peer: %s\n", peerName) ;
626           hostSendArticle (lis->myHosts [i],artTakeRef (article)) ;
627           break ;
628         }
629
630   if (i == lis->hostLen)
631     {
632       d_printf (1,"Failed to give article to peer: -%s-\n", peerName) ;
633       
634       if (lis->dynamicPeers)
635         {
636           Host newHostObj;
637
638           d_printf (1, "Adding peer dynamically\n") ;
639           
640           newHostObj = newDefaultHost (lis, peerName);
641
642           if (newHostObj == NULL)
643             {
644               /* Most likely we couldn't get the lock, i.e. the
645                  peer is blocked.
646                */
647               dropArticle (peerName,article) ;
648             }
649           else if ( !listenerAddPeer (lis, newHostObj) )
650             {
651               /* XXX need to remember we've gone over the limit and not try
652                  to add any more. */
653               warn ("ME internal too many hosts. (max is %lu)",
654                     (unsigned long) lis->hostLen) ;
655               dropArticle (peerName,article) ;
656             }
657           else
658             {
659               d_printf (1,"Giving article to peer: %s\n", peerName) ;
660               hostSendArticle (newHostObj,artTakeRef (article)) ;
661             }
662         }
663       else
664         {
665           dropArticle (peerName,article) ;
666         }
667     }
668 }
669
670
671 static void writeCheckPoint (int offsetAdjust)
672 {
673   char offsetString[16], *writePointer ;
674   off_t offset ;
675   int writeBytes, writeReturn, mainFd ;
676               
677   mainFd = getMainEndPointFd() ;
678   offset = lseek (mainFd, 0, SEEK_CUR) ;
679   if (offset < 0)
680     syslog (LOG_ERR, "ME tell(mainFd): %m") ;
681   else
682     {
683       snprintf (offsetString, sizeof(offsetString), "%ld\n",
684                       (long)(offset - offsetAdjust) ) ;
685       if ( lseek (mainFd, 0, SEEK_SET) != 0 )
686         syslog (LOG_ERR, "ME seek(mainFd, 0, 0): %m") ;
687       else
688         {
689           writeBytes = strlen (offsetString) ;
690           writePointer = offsetString ;
691           do
692             { 
693               writeReturn = write (mainFd, writePointer, writeBytes) ;
694               if (writeReturn < 0)
695                 {
696                   syslog (LOG_ERR,"ME write input checkpoint: %m") ;
697                   break ;
698                 }
699               writePointer += writeReturn ;
700               writeBytes -= writeReturn ;
701             } while (writeBytes) ;
702           if ( lseek (mainFd, offset, SEEK_SET) != offset )
703             die ("ME seek(mainFd, %ld, SEEK_SET): %s\n", (long)offset,
704                  strerror(errno) ) ;
705         }
706     }
707 }
708
709
710 void openDroppedArticleFile (void) 
711 {
712   pid_t myPid = getpid () ;
713   const char *tapeDir = getTapeDirectory() ;
714   size_t len;
715
716   if (dropArtFile != NULL)
717     free (dropArtFile) ;
718
719   len = pathMax(tapeDir) + 1;
720   dropArtFile = xmalloc(len);
721   snprintf (dropArtFile,len,"%s/innfeed-dropped.%c%06d",
722             tapeDir, droppedFileCount + 'A', (int) myPid) ;
723
724   if ((droppedFp = fopen (dropArtFile,"w")) == NULL)
725     {
726       syswarn ("ME cant open %s: loosing articles", dropArtFile) ;
727
728       free (dropArtFile) ;
729       dropArtFile = NULL ;
730       
731       if ((droppedFp = fopen ("/dev/null","w")) == NULL)
732         {
733           die ("ME error opening /dev/null") ;
734         }
735     }
736
737   
738 }
739
740 void closeDroppedArticleFile (void)
741 {
742   off_t pos ;
743
744   if (droppedFp == NULL)
745     return ;
746
747   fflush (droppedFp) ;
748   pos = ftello (droppedFp) ;
749
750   fclose (droppedFp) ;
751   droppedFp = NULL ;
752
753   if (pos == 0 && dropArtFile != NULL)
754     unlink (dropArtFile) ;
755   else if (pos != 0 && dropArtFile == NULL)
756     warn ("ME lost %ld articles", droppedCount) ;
757   else if (pos != 0)
758     notice ("ME dropped %ld articles", droppedCount) ;
759      
760   droppedFileCount = (droppedFileCount + 1) % 26 ;
761   droppedCount = 0 ;
762 }
763
764 static void dropArticle (const char *peerName, Article article)
765 {
766   static bool logged = false ;
767
768   if (!logged)
769     {
770       warn ("ME dropping articles into %s", dropArtFile) ;
771       logged = true ;
772     }
773   
774   droppedCount++ ;
775   fprintf (droppedFp,"%s %s %s\n",artFileName (article),
776            artMsgId (article), peerName) ;
777 }
778
779
780 static void listenerCleanup (void)
781 {
782   free (dropArtFile) ;
783   dropArtFile = NULL ;
784 }