changeset 127:3c71e28c8eef noffle

[svn] Release-1-0 mergedocs/NOTES
author bears
date Tue, 25 Jul 2000 13:14:54 +0100
parents 7c7a7c96d35b
children 8897b7e3b108
files ChangeLog NEWS docs/NOTES docs/noffle.1 src/client.c src/client.h src/database.c src/database.h src/fetch.c src/group.c src/group.h src/lock.c src/lock.h src/noffle.c src/protocol.c src/server.c src/util.c
diffstat 17 files changed, 843 insertions(+), 445 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Tue Jul 25 13:12:50 2000 +0100
+++ b/ChangeLog	Tue Jul 25 13:14:54 2000 +0100
@@ -2,6 +2,44 @@
 NOFFLE ChangeLog
 -------------------------------------------------------------------------------
 
+Mon 24 Jul 20:30:05 BST 2000 Jim Hague <jim.hague@acm.org>
+
+ * src/database.h,src/database.c,src/server.c: Redo XHDR and
+   XPAT to handle 'XHDR <msgid>' and 'XPAT <msgid>'. Common up
+   code shared by XHDR and XPAT.
+ * src/server.c,packages/redhat/noffle.spec: Remove pipe through
+   sort(1) in printGroups() and dependency on textutil(RedHat)/
+   textutils(SuSE) packages in RPM. The RPM now works on RedHat
+   and derivatives and SuSE.
+ * src/server.c: Release lock while collecting POST article and while
+   reporting XOVER x-y results.
+ * src/server.c: Update group 'last accessed' timestamp when XOVER read.
+   This for smooth working with 'trn' and other newsreaders that only
+   read the XOVER unless an article read. I have newsgroups I want to scan
+   but only infrequently read an article - I don't want them getting
+   unsubscribed.
+ * src/server.c: Change server read timeout to 2 secs, release the lock
+   if we'v held it for 2 secs so we don't starve anybody else, and
+   if reading a command when we don't hold the lock don't use timeout.
+ * src/util.c: Fix Utl_newsDate and Utl_parseNewsDateto work properly
+   in all timezones.
+ * src/lock.h,src/lock.c,src/noffle.c: Add 'fetch' lock to main lock,
+   add Lock_gotLock() so we can test the main lock state (currently used
+   only in ASSERTs). Fetch lock can be obtained WAIT or NOWAIT - latter
+   fails immediately if lock not available.
+ * src/client.c,src/fetch.c,src/noffle.c: Change lock handling during a fetch.
+   Begin by obtaining 'fetch' lock - if busy, exit fetch immediately with
+   error message. Then during fetch release the lock except when processing
+   received data; in cases when incoming data is multi-line, collect all
+   data first and then process. The aim is to ensure the lock isn't held
+   with a network operation is in progress. I may carry on and extend this
+   to all server operations as well (so a slow client can't hog the lock).
+	
+Sat Jun 22 2000 Markus Enzenberger <markus.enzenberger@t-online.de>
+
+ * doc/NOTES: Removed section about GNUS hanging. It was caused
+   by the "select" bug in Noffle.
+
 Sat Jul 22 2000 Markus Enzenberger <markus.enzenberger@t-online.de> 
 
  * request.c: Applied patch from M.Nalis:
@@ -16,13 +54,48 @@
 
 Wed Jul 19 2000 Markus Enzenberger <markus.enzenberger@t-online.de>
 
- * Do not acquire global lock for prniting help
+ * Do not acquire global lock for printing help
+
+Mon Jul 17 11:19:06 BST 2000 Jim Hague <jim.hague@acm.org>
+
+ * src/client.h,src/client.c,src/noffle.c,docs/noffle.1: Remove
+   '--query times' option. Creation time should always be time of
+   creation on local server.
+ * src/group.h,src/group.c: Remove Grp_setCreated().
+	
+Fri Jul 14 15:20:14 BST 2000 Jim Hague <jim.hague@acm.org>
+
+ * src/client.c,src/fetch.c,src/noffle.c: The groupinfo.lastupdate file
+   was being used to (a) indicate the time of the last addition to group
+   information, and (b) the last access to a server. With more than one
+   server specified, (a) meant the groupinfo.lastupdate file was updated
+   when new groups on the server (or a new fetch of the server group list)
+   were processed. When server 2 came to use the file for (b) (to determine
+   the time to specify when sending the NEWGROUPS command), the time was
+   already set by server 1 and so server 2 would not see new groups. Fix
+   this by creating lastupdate.<server> files for use (b).
+ * src/client.c,src/group.c: Set initial group creation time to the current
+   time. Previously it was set to 0, so new groups were not reported to a
+   NEWGROUPS request unless 'noffle --query times' was run - even then,
+   local groups would never be reported. Setting the creation time to the
+   creation time on the local server makes things work properly. NB - the
+   group creation time should always be reported as the creation time on
+   the server; must fix this and remove '--query times'. This change will
+   require 'noffle --query groups' to be re-run to create the server
+   lastupdate.<server> files.
 
 Fri Jul 14 2000 Matija Nalis <mnalis-sf@voyager.hr>
 
  * Added counter for --fetch so one can see how much it is until the
    end of the transfer.
 
+Mon Jul 03 12:05:50 BST 2000 Jim Hague <jim.hague@acm.org>
+
+ * src/database.h,src/database.c,src/server.c: Fix 'XHDR <msgId>' and
+   add 'XPAT <msgId>'. 'XHDR <msgId>' previously only worked if
+   msgId was a message in the current group. My brain was really
+   in neutral when I did that.
+
 Fri Jun 30 2000 Markus Enzenberger <markus.enzenberger@t-online.de>
 
   * src/server.c: Leave online mode, if the connection to a remote server
@@ -55,6 +128,12 @@
  * src/protocol.c: Fix bug in Prt_genMsgId that caused duplicate
    message IDs to be generated for posts in the same second.
 
+Sat Jun 22 2000 Markus Enzenberger <markus.enzenberger@t-online.de>
+
+ * src/server.c, src/protocol.c: Fixed a critical bug. "select" cannot
+   be used with buffered stdio. This caused Noffle to hang with some
+   readers (like tin).
+	
 Mon Jun 19 22:43:38 BST 2000 Jim Hague <jim.hague@acm.org>
 
  * src/util.c, src/database.c: Fix header line reading bug.
--- a/NEWS	Tue Jul 25 13:12:50 2000 +0100
+++ b/NEWS	Tue Jul 25 13:14:54 2000 +0100
@@ -5,6 +5,11 @@
 Current development version:
 ----------------------------
 
+ * Fix bug with time of last server access for setups with multiple servers.
+   You must re-run 'noffle --query groups'.
+ * Remove '--query times'.
+ * Fix date printing bug. Now works in all(?) timezones.
+ * Fix bug with 'XHDR <msgId>'. Add 'XPAT <msgId>'.
  * Allow post to local moderated group if 'Approved:' header present.
  * Added '--post' command line option.
  * Add 'path-header' and Path: addition to posted articles.
@@ -12,6 +17,7 @@
    add new 'auto-subscribe-mode <group pattern> <mode>' option.
  * Observe Expires: and Supersedes: headers.
  * Add getgroups and omitgroups options.
+ * Fixed a bug that caused noffle to hang with some readers (like tin).
 
 1.0pre6
 -------
--- a/docs/NOTES	Tue Jul 25 13:12:50 2000 +0100
+++ b/docs/NOTES	Tue Jul 25 13:14:54 2000 +0100
@@ -176,16 +176,6 @@
 1.8 Emacs Gnus
 --------------
 
-Some versions of Gnus freeze up when retrieving active groups. Since
-NOFFLE's log files in DEBUG mode show nothing unusual, I believe that
-this is a bug in Gnus. Sometimes it helps to remove all ".newsrc" and
-similar files on ones home directory and restarting Gnus.
-
-[ Alternatively, set option Gnus/Gnus Start/Gnus Init File from the ]
-[ default ~/.newsrc to, say, ~/.gnus-newsrc. The hang I experienced ]
-[ seems to be due to problems reading an existing .newsrc, nothing  ]
-[ to do with NOFFLE at all.                                         ]
-
 Here is a proposal for changing some key-bindings.
 
    ;; Customising Gnus for use with the NOFFLE news server
--- a/docs/noffle.1	Tue Jul 25 13:12:50 2000 +0100
+++ b/docs/noffle.1	Tue Jul 25 13:14:54 2000 +0100
@@ -1,5 +1,5 @@
 .TH noffle 1
-.\" $Id: noffle.1 165 2000-06-25 18:42:10Z bears $
+.\" $Id: noffle.1 183 2000-07-25 12:14:54Z bears $
 .SH NAME
 noffle \- Usenet package optimized for dialup connections.
 
@@ -51,7 +51,7 @@
 \-p | \-\-post
 .br
 .B noffle
-\-q | \-\-query groups|desc|times
+\-q | \-\-query groups|desc
 .br
 .B noffle
 \-R | \-\-requested
@@ -233,7 +233,7 @@
 have an 'Approved:' header to be posted.
 
 .TP
-.B \-q, \-\-query groups|desc|times
+.B \-q, \-\-query groups|desc
 Query information about all groups from the remote server and merge it to
 the
 .B groupinfo
@@ -246,8 +246,6 @@
 (resets all local article counters),
 .B desc
 retrieves all newsgroup descriptions,
-.B times
-retrieves the creation times of the newsgroups.
 
 .TP
 .B \-r, \-\-server
--- a/src/client.c	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/client.c	Tue Jul 25 13:14:54 2000 +0100
@@ -1,7 +1,7 @@
 /*
   client.c
 
-  $Id: client.c 177 2000-07-22 06:21:11Z enz $
+  $Id: client.c 183 2000-07-25 12:14:54Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -25,6 +25,7 @@
 #include "dynamicstring.h"
 #include "group.h"
 #include "itemlist.h"
+#include "lock.h"
 #include "log.h"
 #include "over.h"
 #include "protocol.h"
@@ -269,6 +270,29 @@
     return ( r >= 0 );
 }
 
+static DynStr *
+collectTxt( void )
+{
+    DynStr *res;
+    Str line;
+    Bool err;
+
+    res = new_DynStr(2048);
+    if ( res == NULL )
+	return NULL;
+
+    while ( getTxtLn( line, &err ) && ! err )
+	DynStr_appLn( res, line );
+
+    if ( err )
+    {
+	del_DynStr( res );
+	return NULL;
+    }
+    else
+	return res;
+}
+
 Bool
 Client_connect( const char *serv )
 {
@@ -280,6 +304,7 @@
     Str host, s;
     struct sockaddr_in sIn;
 
+    ASSERT( client.in == NULL && client.out == NULL );
     client.auth = FALSE;
     Utl_cpyStr( s, serv );
     pStart = Utl_stripWhiteSpace( s );
@@ -328,6 +353,7 @@
 		if ( client.out != NULL )
 		    fclose( client.out );
                 close( sock );
+		client.in = client.out = NULL;
                 break;
             }
             stat = getStat();
@@ -352,6 +378,7 @@
 	    fclose( client.in );
 	    fclose( client.out );
 	    close( sock );
+	    client.in = client.out = NULL;
         }
     }
     return FALSE;
@@ -418,14 +445,19 @@
 }
 
 static void
-processGrps( Bool noServerPattern )
+processGrps( const char *lines, Bool noServerPattern )
 {
     char postAllow;
-    Bool err;
     int first, last;
     Str grp, line, file;
-    
-    while ( getTxtLn( line, &err ) && ! err )
+    Bool groupupdate;
+
+    ASSERT( ! Lock_gotLock() );
+    if ( ! Lock_openDatabases() )
+	return;
+
+    groupupdate = FALSE;
+    while ( ( lines = Utl_getLn( line, lines) ) != NULL )
     {
         if ( sscanf( line, "%s %d %d %c",
                      grp, &last, &first, &postAllow ) != 4 )
@@ -454,6 +486,7 @@
                 Grp_setFirstLast( grp, 1, 0 );
             Grp_setServ( grp, client.serv );
 	    Grp_setPostAllow( grp, postAllow );
+	    groupupdate = TRUE;
         }
         else
         {
@@ -464,18 +497,24 @@
                 Grp_setServ( grp, client.serv );
                 Grp_setRmtNext( grp, first );
 		Grp_setPostAllow( grp, postAllow );
+		groupupdate = TRUE;
             }
             else
                 Log_dbg( "Group %s is already fetched from %s",
-                           grp, Grp_server( grp ) );
-            
+			 grp, Grp_server( grp ) );            
         }
     }
-    if ( ! err )
+
+    snprintf( file, MAXCHAR, "%s/lastupdate.%s",
+	      Cfg_spoolDir(), client.serv );
+    Utl_stamp( file );
+    if ( groupupdate )
     {
-        snprintf( file, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
-        Utl_stamp( file );
+	snprintf( file, MAXCHAR, "%s/groupinfo.lastupdate",
+		  Cfg_spoolDir() );
+	Utl_stamp( file );
     }
+    Lock_closeDatabases();
 }
 
 void
@@ -493,6 +532,7 @@
 {
     Str cmd;
     int stat;
+    DynStr *response;
 
     Utl_cpyStr( cmd, "LIST ACTIVE" );
     if ( pattern[ 0 ] != '\0' )
@@ -517,7 +557,13 @@
 	Log_err( "%s failed: %s", cmd, client.lastStat );
 	return FALSE;
     }
-    processGrps( *noServerPattern );
+
+    response = collectTxt();
+    if ( response == NULL )
+	return FALSE;
+    
+    processGrps( DynStr_str( response ), *noServerPattern );
+    del_DynStr( response );
     return TRUE;
 }
 
@@ -551,10 +597,12 @@
 static Bool
 doGetDsc( const char *pattern, Bool *noServerPattern )
 {
-    Bool err;
     Str name, line, dsc, cmd;
     int stat;
+    DynStr *response;
+    const char *lines;
 
+    ASSERT( ! Lock_gotLock() );
     Utl_cpyStr( cmd, "LIST NEWSGROUPS" );
     if ( pattern[ 0 ] != '\0' )
     {
@@ -578,7 +626,16 @@
         Log_err( "%s failed: %s", cmd, client.lastStat );
         return FALSE;
     }
-    while ( getTxtLn( line, &err ) && ! err )
+
+    response = collectTxt();
+    if ( response == NULL )
+	return FALSE;
+    
+    if ( ! Lock_openDatabases() )
+	return FALSE;
+    
+    lines = DynStr_str( response );
+    while ( ( lines = Utl_getLn( line, lines) ) != NULL )
     {
         if ( sscanf( line, "%s", name ) != 1 )
         {
@@ -594,6 +651,8 @@
             Grp_setDsc( name, dsc );
         }
     }
+    Lock_closeDatabases();
+    del_DynStr( response );
     return TRUE;
 }
 
@@ -624,87 +683,12 @@
     return res;
 }
 
-static Bool
-doGetCreationTimes( const char *pattern, Bool *noServerPattern )
-{
-    Bool err;
-    Str name, line, cmd;
-    time_t t;
-    int stat;
-
-    Utl_cpyStr( cmd, "LIST ACTIVE.TIMES" );
-    if ( pattern[ 0 ] != '\0' )
-    {
-	Utl_catStr( cmd, " " );
-	Utl_catStr( cmd, pattern );
-    }
-
-    *noServerPattern = FALSE;
-    if ( ! putCmd( cmd ) )
-        return FALSE;
-    stat = getStat();
-    if ( pattern[ 0 ] != '\0' && stat != STAT_GRPS_FOLLOW )
-    {
-	*noServerPattern= TRUE;
-	if ( ! putCmd( "LIST ACTIVE.TIMES" ) )
-	    return FALSE;
-	stat = getStat();
-    }    
-    if ( stat != STAT_GRPS_FOLLOW )
-    {
-        Log_err( "%s failed: %s", cmd, client.lastStat );
-        return FALSE;
-    }
-    while ( getTxtLn( line, &err ) && ! err )
-    {
-        if ( sscanf( line, "%s %ld", name, &t ) != 2 )
-        {
-            Log_err( "Unknown reply to LIST ACTIVE.TIMES: %s", line );
-            continue;
-        }
-	if ( *noServerPattern && ! isGetGroup( name ) )
-	    continue;
-        if ( Grp_exists( name ) )
-        {
-            Log_inf( "Creation time of %s: %ld", name, t );
-            Grp_setCreated( name, t );
-        }
-    }
-    return TRUE;
-}
-
-Bool
-Client_getCreationTimes( void )
-{
-    GroupEnum *ge;
-    const char *pattern;
-    Bool doneOne, noServerPattern, res;
-
-    Log_inf( "Querying group creation times" );
-
-    doneOne = FALSE;
-    res = TRUE;
-    ge = new_GetGrEn( client.serv );
-    while ( res && ( pattern = GrEn_next( ge ) ) != NULL )
-    {
-	res = doGetCreationTimes( pattern, &noServerPattern );
-	doneOne = TRUE;
-	if ( noServerPattern )
-	    break;
-    }
-
-    if ( ! doneOne )
-	res = doGetCreationTimes( "", &noServerPattern );
-
-    del_GrEn( ge );
-    return res;
-}
-
 Bool
 Client_getNewgrps( const time_t *lastTime )
 {
     Str s;
     const char *p;
+    DynStr *response;
 
     ASSERT( *lastTime > 0 );
     strftime( s, MAXCHAR, "%Y%m%d %H%M00", gmtime( lastTime ) );
@@ -721,7 +705,13 @@
         Log_err( "NEWGROUPS command failed: %s", client.lastStat );
         return FALSE;
     }
-    processGrps( TRUE );
+
+    response = collectTxt();
+    if ( response == NULL )
+	return FALSE;
+    
+    processGrps( DynStr_str( response ), TRUE );
+    del_DynStr( response );
     return TRUE;
 }
 
@@ -816,6 +806,7 @@
     double threadFollowTime, secPerDay, maxTime, timeSinceLastAccess;
     ItemList *itl;
 
+    ASSERT( Lock_gotLock() );
     Log_dbg( "Checking references '%s' for thread mode", ref );
     result = FALSE;
     itl = new_Itl( ref, " \t" );
@@ -862,6 +853,7 @@
     const char *msgId, *p, *xref;
     int n;
 
+    ASSERT( Lock_gotLock() );
     msgId = Ov_msgId( ov );
     if ( Pseudo_isGeneralInfo( msgId ) )
         Log_dbg( "Skipping general info '%s'", msgId );
@@ -900,15 +892,17 @@
 }
 
 Bool
-Client_getOver( int rmtFirst, int rmtLast, FetchMode mode )
+Client_getOver( const char *grp, int rmtFirst, int rmtLast, FetchMode mode )
 {
-    Bool err;
-    size_t bytes, lines;
+    size_t nbytes, nlines;
     int rmtNumb, oldLast, cntMarked;
     Over *ov;
     Str line, subj, from, date, msgId, ref;
+    DynStr *response;
+    const char *lines;
 
-    ASSERT( strcmp( client.grp, "" ) != 0 );
+    ASSERT( ! Lock_gotLock() );
+    ASSERT( strcmp( grp, "" ) != 0 );
     if ( ! putCmd( "XOVER %lu-%lu", rmtFirst, rmtLast ) )
         return FALSE;
     if ( getStat() != STAT_OVERS_FOLLOW )
@@ -917,18 +911,27 @@
         return FALSE;
     }
     Log_dbg( "Requesting overview for remote %lu-%lu", rmtFirst, rmtLast );
+
+    response = collectTxt();
+    if ( response == NULL )
+	return FALSE;
+
+    if ( ! Lock_openDatabases() )
+	return FALSE;
+    Cont_read( grp );
     oldLast = Cont_last();
     cntMarked = 0;
-    while ( getTxtLn( line, &err ) && ! err )
+    lines = DynStr_str( response );
+    while ( ( lines = Utl_getLn( line, lines) ) != NULL )
     {
         if ( ! parseOvLn( line, &rmtNumb, subj, from, date, msgId, ref,
-                          &bytes, &lines ) )
+                          &nbytes, &nlines ) )
             Log_err( "Bad overview line: %s", line );
 	else if ( Cont_find( msgId ) >= 0 )
 	    Log_inf( "Already have '%s'", msgId );
         else
         {
-            ov = new_Over( subj, from, date, msgId, ref, bytes, lines );
+            ov = new_Over( subj, from, date, msgId, ref, nbytes, nlines );
             Cont_app( ov );
             prepareEntry( ov );
             if ( mode == FULL || ( mode == THREAD && needsMark( ref ) ) )
@@ -942,7 +945,11 @@
     if ( oldLast != Cont_last() )
         Log_inf( "Added %s %lu-%lu", client.grp, oldLast + 1, Cont_last() );
     Log_inf( "%u articles marked for download in %s", cntMarked, client.grp  );
-    return err;
+    Cont_write();
+    Grp_setFirstLast( grp, Cont_first(), Cont_last() );
+    Lock_closeDatabases();
+    del_DynStr( response );
+    return TRUE;
 }
 
 static void
@@ -950,10 +957,14 @@
 {
     int status;
 
+    ASSERT( ! Lock_gotLock() );
     Log_err( "Retrieving of %s failed: %s", msgId, reason );
+    if ( ! Lock_openDatabases() )
+	return;
     status = Db_status( msgId );
     Pseudo_retrievingFailed( msgId, reason );
     Db_setStatus( msgId, status | DB_RETRIEVING_FAILED );
+    Lock_closeDatabases();
 }
 
 static Bool
@@ -961,17 +972,24 @@
 {
     Bool err;
     DynStr *s = NULL;
-    Str line;
 
+    ASSERT( ! Lock_gotLock() );
     Log_inf( "[%d/%d] Retrieving %s", artcnt, artmax, msgId );
-    s = new_DynStr( 5000 );
-    while ( getTxtLn( line, &err ) && ! err )
-        DynStr_appLn( s, line );
-    if ( ! err )
+    err = TRUE;
+
+    s = collectTxt();
+    if ( s != NULL )
     {
 	const char *txt;
-
+	
 	txt = DynStr_str( s );
+	if ( ! Lock_openDatabases() )
+	{
+	    del_DynStr( s );
+	    retrievingFailed( msgId, "Can't open message base" );
+	    return FALSE;
+	}
+	
         err = ! Db_storeArt( msgId, txt );
 	if ( ! err )
 	{
@@ -990,16 +1008,18 @@
 		del_Itl( ids );
 	    }
 	}
+	Lock_closeDatabases();
+	del_DynStr( s );
     }
     else
         retrievingFailed( msgId, "Connection broke down" );
-    del_DynStr( s );
     return ! err;
 }
 
 void
 Client_retrieveArt( const char *msgId )
 {
+    ASSERT( Lock_gotLock() );
     if ( ! Db_contains( msgId ) )
     {
         Log_err( "Article '%s' not prepared in database. Skipping.", msgId );
@@ -1010,6 +1030,8 @@
         Log_inf( "Article '%s' already retrieved. Skipping.", msgId );
         return;
     }
+
+    Lock_closeDatabases();
     if ( ! putCmd( "ARTICLE %s", msgId ) )
         retrievingFailed( msgId, "Connection broke down" );
     else if ( getStat() != STAT_ART_FOLLOWS )
@@ -1025,6 +1047,7 @@
     DynStr *s;
     const char *p;
     
+    ASSERT( Lock_gotLock() );
     Log_inf( "Retrieving article list" );
     s = new_DynStr( (int)strlen( list ) );
     p = list;
@@ -1042,8 +1065,11 @@
         }
         else
             DynStr_appLn( s, msgId );
+
+    Lock_closeDatabases();
     fflush( client.out );
     Log_dbg( "[S FLUSH]" );
+    
     p = DynStr_str( s );
     while ( ( p = Utl_getLn( msgId, p ) ) )
     {
@@ -1053,6 +1079,7 @@
             break;
     }
     del_DynStr( s );
+    Lock_openDatabases();
 }
 
 Bool
@@ -1061,6 +1088,7 @@
     unsigned int stat;
     int estimatedNumb, first, last;
 
+    ASSERT( Lock_gotLock() );
     if ( ! Grp_exists( name ) )
         return FALSE;
     if ( ! putCmd( "GROUP %s", name ) )
@@ -1082,6 +1110,7 @@
 void
 Client_rmtFirstLast( int *first, int *last )
 {
+    ASSERT( Lock_gotLock() );
     *first = client.rmtFirst;
     *last = client.rmtLast;
 }
--- a/src/client.h	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/client.h	Tue Jul 25 13:14:54 2000 +0100
@@ -3,7 +3,7 @@
 
   Noffle acting as client to other NNTP-servers
 
-  $Id: client.h 174 2000-07-19 07:02:45Z enz $
+  $Id: client.h 183 2000-07-25 12:14:54Z bears $
 */
 
 #ifndef CLIENT_H
@@ -42,9 +42,6 @@
 Client_getDsc( void );
 
 Bool
-Client_getCreationTimes( void );
-
-Bool
 Client_getNewgrps( const time_t *lastTime );
 
 /*
@@ -60,7 +57,7 @@
   or THREAD mode, store IDs in request database.
 */
 Bool
-Client_getOver( int rmtFirst, int rmtLast, FetchMode mode );
+Client_getOver( const char *grp, int rmtFirst, int rmtLast, FetchMode mode );
 
 /*
   Retrieve full article text and store it into database.
--- a/src/database.c	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/database.c	Tue Jul 25 13:14:54 2000 +0100
@@ -1,7 +1,7 @@
 /*
   database.c
 
-  $Id: database.c 149 2000-06-19 21:56:12Z bears $
+  $Id: database.c 183 2000-07-25 12:14:54Z bears $
 
   Uses GNU gdbm library. Using Berkeley db (included in libc6) was
   cumbersome. It is based on Berkeley db 1.85, which has severe bugs
@@ -515,6 +515,15 @@
     return db.date;
 }
 
+Over *
+Db_over( const char *msgId )
+{
+    if ( ! loadArt( msgId ) )
+	return NULL;
+    return new_Over( db.subj, db.from, db.date, msgId,
+		     db.ref, db.bytes, db.lines );
+}
+
 Bool
 Db_contains( const char *msgId )
 {
--- a/src/database.h	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/database.h	Tue Jul 25 13:14:54 2000 +0100
@@ -3,7 +3,7 @@
 
   Article database.
 
-  $Id: database.h 67 2000-05-12 17:19:38Z enz $
+  $Id: database.h 183 2000-07-25 12:14:54Z bears $
 */
 
 #ifndef DB_H
@@ -90,6 +90,10 @@
 const char *
 Db_xref( const char *msgId );
 
+/* Overview - need to del_Over result when finished with */
+Over *
+Db_over( const char *msgId );
+
 Bool
 Db_contains( const char *msgId );
 
--- a/src/fetch.c	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/fetch.c	Tue Jul 25 13:14:54 2000 +0100
@@ -1,7 +1,7 @@
 /*
   fetch.c
 
-  $Id: fetch.c 174 2000-07-19 07:02:45Z enz $
+  $Id: fetch.c 183 2000-07-25 12:14:54Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -31,6 +31,7 @@
 #include "fetchlist.h"
 #include "request.h"
 #include "group.h"
+#include "lock.h"
 #include "log.h"
 #include "outgoing.h"
 #include "protocol.h"
@@ -38,6 +39,8 @@
 #include "util.h"
 #include "portable.h"
 
+#define	MAX_ARTICLE_CMDS_QUEUED		20
+
 struct Fetch
 {
     Bool ready;
@@ -63,7 +66,8 @@
     Str file;
 
     ASSERT( fetch.ready );
-    snprintf( file, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
+    snprintf( file, MAXCHAR, "%s/lastupdate.%s",
+	      Cfg_spoolDir(), fetch.serv );
     if ( ! Utl_getStamp( &t, file ) )
     {
         Log_err( "Cannot read %s. Please run noffle --query groups", file );
@@ -71,21 +75,22 @@
     }
     Log_inf( "Updating groupinfo" );
     Client_getNewgrps( &t );
-    Utl_stamp( file );
 }
 
-void
-Fetch_getNewArts( const char *name, FetchMode mode )
+/* Databases open on entry, closed on exit. */
+static void
+fetchNewArts( const char *name, FetchMode mode )
 {
     int next, first, last;
 
     if ( ! Client_changeToGrp( name ) )
     {
         Log_err( "Could not change to group %s", name );
+	Lock_closeDatabases();
         return;
     }
+    Client_rmtFirstLast( &first, &last );
     Cont_read( name );
-    Client_rmtFirstLast( &first, &last );
     next = Grp_rmtNext( name );
     if ( next == GRP_RMT_NEXT_NOT_SUBSCRIBED )
 	next = first;
@@ -94,6 +99,7 @@
         Log_inf( "No new articles in %s", name );
         Cont_write();
         Grp_setFirstLast( name, Cont_first(), Cont_last() );
+	Lock_closeDatabases();
         return;
     }
     if ( first == 0 && last == 0 )
@@ -101,6 +107,7 @@
         Log_inf( "No articles in %s", name );
         Cont_write();
         Grp_setFirstLast( name, Cont_first(), Cont_last() );
+	Lock_closeDatabases();
         return;
     }
     if ( next > last + 1 )
@@ -124,9 +131,19 @@
     }
     Log_inf( "Getting remote overviews %lu-%lu for group %s",
              first, last, name );
-    Client_getOver( first, last, mode );
-    Cont_write();
-    Grp_setFirstLast( name, Cont_first(), Cont_last() );
+    Lock_closeDatabases();
+    Client_getOver( name, first, last, mode );
+}
+
+void
+Fetch_getNewArts( const char *name, FetchMode mode )
+{
+    if ( ! Lock_openDatabases() )
+    {
+        Log_err( "Could not open message base" );
+        return;
+    }
+    fetchNewArts( name, mode );
 }
 
 void
@@ -134,17 +151,43 @@
 {
     FetchMode mode;
     int i, size;
-    const char* name;
+    const char *name;
 
     ASSERT( fetch.ready );
+    if ( ! Lock_openDatabases() )
+    {
+        Log_err( "Could not open message base" );
+        return;
+    }
     Fetchlist_read();
     size = Fetchlist_size();
     for ( i = 0; i < size; ++i )
     {
         Fetchlist_element( &name, &mode, i );
         if ( strcmp( Grp_server( name ), fetch.serv ) == 0 )
-            Fetch_getNewArts( name, mode );
+	{
+            fetchNewArts( name, mode );
+	    if ( ! Lock_openDatabases() )
+	    {
+		Log_err( "Could not open message base" );
+		return;
+	    }
+	}
     }
+    Lock_closeDatabases();
+}
+
+static void
+fetchMessageList( const char *list, int *artcnt, int artmax )
+{
+    const char *p;
+    Str msgId;
+
+    ASSERT( Lock_gotLock() );
+    Client_retrieveArtList( list, artcnt, artmax );
+    p = list;
+    while ( ( p = Utl_getLn( msgId, p ) ) )
+        Req_remove( fetch.serv, msgId );
 }
 
 void
@@ -152,33 +195,60 @@
 {
     Str msgId;
     DynStr *list;
+    DynStr *fetchList;
     const char *p;
     int count = 0, artcnt = 0, artmax = 0;
 
     ASSERT( fetch.ready );
     Log_dbg( "Retrieving articles marked for download" );
     list = new_DynStr( 10000 );
-    if ( Req_first( fetch.serv, msgId ) ) do { artmax++; } while ( Req_next( msgId ) );
-    Log_inf( "%d TOTAL messages to download", artmax);
+    fetchList = new_DynStr( 1000 );
+    if ( list == NULL || fetchList == NULL )
+    {
+	if ( list != NULL )
+	    del_DynStr( list );
+        Log_err( "Out of memory in Fetch_get_Req_");
+	return;
+    }
+
+    /*
+     * Get all waiting message IDs for this server. We copy into a master
+     * list as the requests file will be closed and re-opened during the
+     * fetch and the position therein will be lost.
+     */
+    if ( ! Lock_openDatabases() )
+    {
+        Log_err( "Could not open message base" );
+        return;
+    }
+
     if ( Req_first( fetch.serv, msgId ) )
+    {
         do
         {
             DynStr_appLn( list, msgId );
-            if ( ++count % 20 == 0 ) /* Send max. 20 ARTICLE cmds at once */
-            {
-                p = DynStr_str( list );
-                Client_retrieveArtList( p, &artcnt, artmax );
-                while ( ( p = Utl_getLn( msgId, p ) ) )
-                    Req_remove( fetch.serv, msgId );
-                DynStr_clear( list );
-            }
+	    artmax++;
         }
         while ( Req_next( msgId ) );
+	Log_inf( "%d TOTAL messages to download", artmax);
+    }
+
+    /* Retrieve in groups of up to size MAX_ARTICLE_CMDS_QUEUED. */
     p = DynStr_str( list );
-    Client_retrieveArtList( p, &artcnt, artmax );
-    while ( ( p = Utl_getLn( msgId, p ) ) )
-        Req_remove( fetch.serv, msgId );
+    while ( ( p = Utl_getLn( msgId, p ) ) != NULL )
+    {
+	DynStr_appLn( fetchList, msgId );
+	if ( ++count % MAX_ARTICLE_CMDS_QUEUED == 0 )
+	{
+	    fetchMessageList( DynStr_str( fetchList ), &artcnt, artmax );
+	    DynStr_clear( fetchList );
+	}
+    }
+    fetchMessageList( DynStr_str( fetchList ), &artcnt, artmax );
+
+    del_DynStr( fetchList );
     del_DynStr( list );
+    Lock_closeDatabases();
 }
 
 static void
@@ -256,6 +326,7 @@
 Bool
 Fetch_init( const char *serv )
 {
+    Lock_closeDatabases();
     if ( ! connectToServ( serv ) )
         return FALSE;
     Utl_cpyStr( fetch.serv, serv );
@@ -269,4 +340,5 @@
     Client_disconnect();
     fetch.ready = FALSE;
     Log_inf( "Fetch from '%s' finished", fetch.serv );
+    Lock_openDatabases();
 }
--- a/src/group.c	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/group.c	Tue Jul 25 13:14:54 2000 +0100
@@ -7,7 +7,7 @@
   loadGrp() and saveGrp(). This is done transparently. Access to the groups
   database is done by group name, by the functions defined in group.h.        
 
-  $Id: group.c 135 2000-06-05 08:57:05Z bears $
+  $Id: group.c 183 2000-07-25 12:14:54Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -178,7 +178,7 @@
     grp.entry.first = 1;
     grp.entry.last = 0;
     grp.entry.rmtNext = GRP_RMT_NEXT_NOT_SUBSCRIBED;
-    grp.entry.created = 0;
+    grp.entry.created = time( NULL );
     grp.entry.lastAccess = 0;
     grp.postAllow = 'y';
     saveGrp();
@@ -295,16 +295,6 @@
 }
 
 void
-Grp_setCreated( const char *name, time_t value )
-{
-    if ( loadGrp( name ) )
-    {
-        grp.entry.created = value;
-        saveGrp();
-    }
-}
-
-void
 Grp_setRmtNext( const char *name, int value )
 {
     if ( loadGrp( name ) )
--- a/src/group.h	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/group.h	Tue Jul 25 13:14:54 2000 +0100
@@ -3,7 +3,7 @@
 
   Groups database
 
-  $Id: group.h 135 2000-06-05 08:57:05Z bears $
+  $Id: group.h 183 2000-07-25 12:14:54Z bears $
 */
 
 #ifndef GRP_H
@@ -100,9 +100,6 @@
 Grp_setServ( const char *name, const char *value );
 
 void
-Grp_setCreated( const char *name, time_t value );
-
-void
 Grp_setRmtNext( const char *name, int value );
 
 void
--- a/src/lock.c	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/lock.c	Tue Jul 25 13:14:54 2000 +0100
@@ -1,7 +1,7 @@
 /*
   lock.c
 
-  $Id: lock.c 60 2000-05-09 22:28:38Z uh1763 $
+  $Id: lock.c 183 2000-07-25 12:14:54Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -37,62 +37,79 @@
 
 struct Lock
 {
+    const char *name;
     int lockFd;
     Str lockFile;
-} lock = { -1, "" };
+};
 
+struct Lock globalLock = { "global", -1, "" };
+struct Lock fetchLock = { "fetch", -1, "" };
 
-#ifdef DEBUG
+/* Check the global lock held. */
 static Bool
-testLock( void )
+gotLock( struct Lock *lock )
 {
-    return ( lock.lockFd != -1 );    
+    return ( lock->lockFd != -1 );    
 }
-#endif
 
 static Bool
-waitLock( void )
+waitLock( struct Lock *lock, enum LockRequestWait wait )
 {
     int fd;
     struct flock l;
 
-    ASSERT( ! testLock() );
-    Log_dbg( "Waiting for lock ..." );
-    snprintf( lock.lockFile, MAXCHAR, "%s/lock/global", Cfg_spoolDir() );
-    if ( ( fd = open( lock.lockFile, O_WRONLY | O_CREAT, 0644 ) ) < 0 )
+    ASSERT( ! gotLock( lock ) );
+    Log_dbg( "Waiting for lock %s ...", lock->name );
+    if ( lock->lockFile[ 0 ] == '\0' )
+	snprintf( lock->lockFile, MAXCHAR, "%s/lock/%s",
+		  Cfg_spoolDir(), lock->name );
+    if ( ( fd = open( lock->lockFile, O_WRONLY | O_CREAT, 0644 ) ) < 0 )
     {
-        Log_err( "Cannot open %s (%s)", lock.lockFile, strerror( errno ) );
+        Log_err( "Cannot open %s (%s)", lock->lockFile, strerror( errno ) );
         return FALSE;
     }
     l.l_type = F_WRLCK;
     l.l_start = 0;
     l.l_whence = SEEK_SET;
     l.l_len = 0;
-    if ( fcntl( fd, F_SETLKW, &l ) < 0 )
+    if ( wait == LOCK_WAIT )
     {
-        Log_err( "Cannot lock %s: %s", lock.lockFile, strerror( errno ) );
-        return FALSE;
+	if ( fcntl( fd, F_SETLKW, &l ) < 0 )
+	{
+	    Log_err( "Cannot lock %s: %s", lock->lockFile, strerror( errno ) );
+	    close( lock->lockFd );
+	    return FALSE;
+	}
     }
-    lock.lockFd = fd;
+    else
+    {
+	if ( fcntl( fd, F_SETLK, &l ) < 0 )
+	{
+	    close( lock->lockFd );
+	    return FALSE;
+	}
+    }
+	
+    lock->lockFd = fd;
     Log_dbg( "Lock successful" );
     return TRUE;
 }
 
 static void
-releaseLock( void )
+releaseLock( struct Lock *lock )
 {
     struct flock l;
 
-    ASSERT( testLock() );    
+    ASSERT( gotLock( lock ) );    
     l.l_type = F_UNLCK;
     l.l_start = 0;
     l.l_whence = SEEK_SET;
     l.l_len = 0;
-    if ( fcntl( lock.lockFd, F_SETLK, &l ) < 0 )
-        Log_err( "Cannot release %s: %s", lock.lockFile,
+    if ( fcntl( lock->lockFd, F_SETLK, &l ) < 0 )
+        Log_err( "Cannot release %s: %s", lock->lockFile,
                  strerror( errno ) );
-    close( lock.lockFd );
-    lock.lockFd = -1;
+    close( lock->lockFd );
+    lock->lockFd = -1;
     Log_dbg( "Releasing lock" );
 }
 
@@ -101,34 +118,34 @@
 Bool
 Lock_openDatabases( void )
 {
-  if ( ! waitLock() )
+    if ( ! waitLock( &globalLock, LOCK_WAIT ) )
     {
-      Log_err( "Could not get write lock" );
-      return FALSE;
+	Log_err( "Could not get write lock" );
+	return FALSE;
     }
-  if ( ! Db_open() )
+    if ( ! Db_open() )
     {
-      Log_err( "Could not open database" );
-      releaseLock();
-      return FALSE;
+	Log_err( "Could not open database" );
+	releaseLock( &globalLock );
+	return FALSE;
     }
-  if ( ! Grp_open() )
+    if ( ! Grp_open() )
     {
-      Log_err( "Could not open groupinfo" );
-      Db_close();
-      releaseLock();
-      return FALSE;
+	Log_err( "Could not open groupinfo" );
+	Db_close();
+	releaseLock( &globalLock );
+	return FALSE;
     }
-  if ( ! Req_open() )
+    if ( ! Req_open() )
     {
-      Log_err( "Could not initialize request database" );
-      Grp_close();
-      Db_close();
-      releaseLock();
-      return FALSE;
+	Log_err( "Could not initialize request database" );
+	Grp_close();
+	Db_close();
+	releaseLock( &globalLock );
+	return FALSE;
     }
 
-  return TRUE;
+    return TRUE;
 }
 
 
@@ -136,8 +153,37 @@
 void
 Lock_closeDatabases( void )
 {
-  Grp_close();
-  Db_close();
-  Req_close();
-  releaseLock();
+    Grp_close();
+    Db_close();
+    Req_close();
+    releaseLock( &globalLock );
+}
+
+/* Check the global lock held. */
+Bool
+Lock_gotLock( void )
+{
+    return gotLock( &globalLock );
 }
+
+/* Get fetch lock. */
+Bool
+Lock_getFetchLock( enum LockRequestWait wait )
+{
+    return waitLock( &fetchLock, wait );
+}
+
+/* Release fetch lock. */
+void
+Lock_releaseFetchLock( void )
+{
+    releaseLock( &fetchLock );
+}
+
+/* Check the fetch lock held. */
+Bool
+Lock_fetchLock( void )
+{
+    return gotLock( &fetchLock );
+}
+
--- a/src/lock.h	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/lock.h	Tue Jul 25 13:14:54 2000 +0100
@@ -5,7 +5,7 @@
   articla database, groups database, outgoing articles database, requests
   database. Handles global lock.
 
-  $Id: lock.h 51 2000-05-05 23:49:38Z uh1763 $
+  $Id: lock.h 183 2000-07-25 12:14:54Z bears $
 */
 
 #ifndef LOCK_H
@@ -17,6 +17,8 @@
 
 #include "common.h"
 
+enum LockRequestWait { LOCK_WAIT, LOCK_NOWAIT };
+
 /* Open all databases and set global lock. */
 Bool
 Lock_openDatabases( void );
@@ -25,4 +27,20 @@
 void
 Lock_closeDatabases( void );
 
+/* Check the global lock held. */
+Bool
+Lock_gotLock( void );
+
+/* Get fetch lock. */
+Bool
+Lock_getFetchLock( enum LockRequestWait wait );
+
+/* Release fetch lock. */
+void
+Lock_releaseFetchLock( void );
+
+/* Check the fetch lock held. */
+Bool
+Lock_fetchLock( void );
+
 #endif
--- a/src/noffle.c	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/noffle.c	Tue Jul 25 13:14:54 2000 +0100
@@ -10,7 +10,7 @@
   received for some seconds (to allow multiple clients connect at the same
   time).
 
-  $Id: noffle.c 176 2000-07-19 19:47:41Z enz $
+  $Id: noffle.c 183 2000-07-25 12:14:54Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -156,6 +156,12 @@
 {
     Str serv;
 
+    if ( ! Lock_getFetchLock( LOCK_NOWAIT ) )
+    {
+	Log_err( "Another 'noffle --fetch' is in progress" );
+	return;
+    }
+    
     Cfg_beginServEnum();
     while ( Cfg_nextServ( serv ) )
         if ( Fetch_init( serv ) )
@@ -174,6 +180,8 @@
 
             Fetch_close();
         }
+
+    Lock_releaseFetchLock();
 }
 
 static Bool
@@ -215,8 +223,6 @@
                 Client_getGrps();
             if ( noffle.queryDsc )
                 Client_getDsc();
-            if ( noffle.queryTimes )
-                Client_getCreationTimes();
             Fetch_close();
         }
 }
@@ -306,6 +312,9 @@
         Grp_create( name );
         Grp_setLocal( name );
 	printf( "New local group '%s' created.\n", name );
+	
+	snprintf( grp, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
+	Utl_stamp( grp );
     }
 }
 
@@ -532,7 +541,6 @@
       " -p | --post                      Post article on stdin\n"
       " -q | --query groups              Get group list from server\n"
       " -q | --query desc                Get group descriptions from server\n"
-      " -q | --query times               Get group creation times from server\n"
       " -r | --server                    Run as server on stdin/stdout\n"
       " -R | --requested                 List articles marked for download\n"
       " -s | --subscribe-over <grp>      Add group to fetch list (overview)\n"
@@ -772,8 +780,6 @@
                 noffle.queryGrps = TRUE;
             else if ( strcmp( optarg, "desc" ) == 0 )
                 noffle.queryDsc = TRUE;
-            else if ( strcmp( optarg, "times" ) == 0 )
-                noffle.queryTimes = TRUE;
             else
             {
                 fprintf( stderr, "Unknown argument -q %s\n", optarg );
--- a/src/protocol.c	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/protocol.c	Tue Jul 25 13:14:54 2000 +0100
@@ -1,7 +1,7 @@
 /*
   protocol.c
 
-  $Id: protocol.c 165 2000-06-25 18:42:10Z bears $
+  $Id: protocol.c 183 2000-07-25 12:14:54Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -32,7 +32,7 @@
     */
     if ( ! fgets( line, MAXCHAR, f ) )
     {
-        Log_dbg( "Prt_getLine failed" );
+	Log_dbg( "Prt_getLn failed" );
         return FALSE;
     }
     len = strlen( line );
--- a/src/server.c	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/server.c	Tue Jul 25 13:14:54 2000 +0100
@@ -1,7 +1,7 @@
 /*
   server.c
 
-  $Id: server.c 167 2000-06-30 07:10:45Z enz $
+  $Id: server.c 183 2000-07-25 12:14:54Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -52,9 +52,11 @@
 struct
 {
     Bool running;
+    time_t lastServerOpen;
     int artPtr;
     Str grp; /* selected group, "" if none */
-} server = { FALSE, 0, "" };
+    Bool readAlarmFlag;
+} server = { FALSE, 0L, 0, "", FALSE };
 
 typedef struct Cmd
 {
@@ -87,6 +89,9 @@
 static Bool notImplemented( char *arg, const Cmd *cmd );
 static void putStat( unsigned int stat, const char *fmt, ... );
 
+static void closeServer( void );
+static Bool initServer( void );
+
 Cmd commands[] =
 {
     { "article", "ARTICLE [msg-id|n]", &doArt },
@@ -107,8 +112,8 @@
     { "quit", "QUIT", &doQuit },
     { "slave", "SLAVE (ignored)", &doSlave },
     { "stat", "STAT [msg-id|n]", &doStat },
-    { "xhdr", "XHDR over-field [m[-[n]]]", &doXhdr },
-    { "xpat", "XPAT over-field m[-[n]] pat", &doXpat },
+    { "xhdr", "XHDR over-field [msg-id|m[-[n]]]", &doXhdr },
+    { "xpat", "XPAT over-field msg-id|m[-[n]] pat", &doXpat },
     { "xover", "XOVER [m[-[n]]]", &doXOver }
 };
 
@@ -202,10 +207,66 @@
     putStat( STAT_SYNTAX_ERR, "Syntax error. Usage: %s", cmd->syntax );
 }
 
-static Bool
-getLn( Str line )
+static void
+readAlarm( int sig )
+{
+    UNUSED( sig );
+
+    server.readAlarmFlag = TRUE;
+    return;
+}
+
+static sig_t
+installSignalHandler( int sig, sig_t handler )
 {
-    return Prt_getLn( line, stdin );
+    struct sigaction act, oldAct;
+
+    act.sa_handler = handler;
+    sigemptyset( &act.sa_mask );
+    act.sa_flags = 0;
+    if ( sig != SIGALRM )
+        act.sa_flags |= SA_RESTART;
+    if ( sigaction( sig, &act, &oldAct ) < 0 )
+        return SIG_ERR;
+    return oldAct.sa_handler;
+}
+
+/*
+ * Returns: < 0 on error, 0 on timeout, > 0 on success.
+ * If timeout is zero, wait indefinitely.
+ */
+static int
+waitCmdLn( Str line, int timeoutSeconds )
+{
+    sig_t oldHandler;
+    int r;
+
+    ASSERT( timeoutSeconds >= 0 );
+
+    /* Special case - no timeout. */
+    if ( timeoutSeconds == 0 )
+    {
+	r = Prt_getLn( line, stdin );
+	return ( r ) ? 1 : -1;
+    }
+
+    server.readAlarmFlag = FALSE;
+    oldHandler = installSignalHandler( SIGALRM, readAlarm );
+    if ( oldHandler == SIG_ERR )
+    {
+        Log_err( "client.c:connectWithTimeout: signal failed." );
+        return -1;
+    }
+    if ( alarm( ( unsigned int ) timeoutSeconds ) != 0 )
+        Log_err( "server.c:waitCmdLn: Alarm was already set." );
+    r = Prt_getLn( line, stdin );
+    alarm( 0 );
+    installSignalHandler( SIGALRM, oldHandler );
+    if ( server.readAlarmFlag )
+        return 0;
+    else if ( r )
+        return 1;
+    return -1;
 }
 
 static Bool
@@ -636,9 +697,6 @@
 {
     Str line;
     const char *g;
-    FILE *f;
-    sig_t lastHandler;
-    int ret;
 
     putStat( STAT_GRPS_FOLLOW, "Groups" );
     fflush( stdout );
@@ -651,42 +709,15 @@
     }                    
     else
     {
-        lastHandler = signal( SIGPIPE, SIG_IGN );
-        f = popen( "sort", "w" );
-        if ( f == NULL )
-        {
-            Log_err( "Cannot open pipe to 'sort'" );
-            if ( Grp_firstGrp( &g ) )
-                do
-                    if ( Wld_match( g, pat ) )
-                    {
-                        (*printProc)( line, g );
-                        if ( ! Prt_putTxtLn( line, stdout ) )
-                            Log_err( "Writing to stdout failed." );
-                    }
-                while ( Grp_nextGrp( &g ) );
-        }
-        else
-        {
-            if ( Grp_firstGrp( &g ) )
-                do
-                    if ( Wld_match( g, pat ) )
-                    {
-                        (*printProc)( line, g );
-                        if ( ! Prt_putTxtLn( line, f ) )
-                        {
-                            Log_err( "Writing to 'sort' pipe failed." );
-                            break;
-                        }                    
-                    }
-                while ( Grp_nextGrp( &g ) );
-            ret = pclose( f );
-            if ( ret != EXIT_SUCCESS )
-                Log_err( "sort command returned %d", ret );
-            fflush( stdout );
-            Log_dbg( "[S FLUSH]" );
-            signal( SIGPIPE, lastHandler );
-        }
+	if ( Grp_firstGrp( &g ) )
+	    do
+		if ( Wld_match( g, pat ) )
+		{
+		    (*printProc)( line, g );
+		    if ( ! Prt_putTxtLn( line, stdout ) )
+			Log_err( "Writing to stdout failed." );
+		}
+	    while ( Grp_nextGrp( &g ) );
     }
     putEndOfTxt();
 }
@@ -813,6 +844,14 @@
     else
     {
         changeToGrp( arg );
+
+	/*
+	 * The output may take some time, so release the lock
+	 * while outputting (all the required data is in RAM
+	 * at this point).
+	 */
+	closeServer();
+	
         first = Cont_first();
         last = Cont_last();
         putStat( STAT_GRP_SELECTED, "Article list" );
@@ -980,10 +1019,24 @@
     fflush( stdout );
     Log_dbg( "[S FLUSH]" );
     s = new_DynStr( 10000 );
+	
+    
+    /*
+     * The article may take some time coming in, so release the
+     * lock while collecting it.
+     */
+    closeServer();
+    
     err = FALSE;
     while ( ! err && getTxtLn( line, &err ) )
 	DynStr_appLn( s, line );
 
+    if ( ! initServer() )
+    {
+	del_DynStr( s );
+	return FALSE;
+    }
+
     if ( ! err
 	 && Post_open( DynStr_str( s ) )
 	 && Post_post() )
@@ -1033,46 +1086,104 @@
             ++(*numb);
 }
 
+enum XhdrType { SUBJ, FROM, DATE, MSG_ID, REF, BYTES, LINES, XREF, UNKNOWN };
+
+static enum XhdrType
+whatXhdrField( const char * fieldname )
+{
+    Str name;
+
+    Utl_cpyStr( name, fieldname );
+    Utl_toLower( name );
+    if ( strcmp( name, "subject" ) == 0 )
+        return SUBJ;
+    else if ( strcmp( name, "from" ) == 0 )
+        return FROM;
+    else if ( strcmp( name, "date" ) == 0 )
+        return DATE;
+    else if ( strcmp( name, "message-id" ) == 0 )
+        return MSG_ID;
+    else if ( strcmp( name, "references" ) == 0 )
+        return REF;
+    else if ( strcmp( name, "bytes" ) == 0 )
+        return BYTES;
+    else if ( strcmp( name, "lines" ) == 0 )
+        return LINES;
+    else if ( strcmp( name, "xref" ) == 0 )
+        return XREF;
+    else
+	return UNKNOWN;
+}
+
+static void
+getXhdrField( enum XhdrType what, const Over * ov, Str res )
+{
+    const char * msgId;
+    Str host;
+    
+    switch ( what )
+    {
+    case SUBJ:
+	Utl_cpyStr( res, Ov_subj( ov ) );
+	break;
+    case FROM:
+	Utl_cpyStr( res, Ov_from( ov ) );
+	break;
+    case DATE:
+	Utl_cpyStr( res, Ov_date( ov ) );
+	break;
+    case MSG_ID:
+	Utl_cpyStr( res, Ov_msgId( ov ) );
+	break;
+    case REF:
+	Utl_cpyStr( res, Ov_ref( ov ) );
+	break;
+    case BYTES:
+	snprintf( res, MAXCHAR, "%d", Ov_bytes( ov ) );
+	break;
+    case LINES:
+	snprintf( res, MAXCHAR, "%d", Ov_lines( ov ) );
+	break;
+    case XREF:
+	msgId = Ov_msgId( ov );
+	/*
+	 * Gen info messages don't have an Xref header. When INN is asked
+	 * for a header that doesn't exist in a message, it reports the
+	 * header value as '(none)', so do the same.
+	 */
+	if ( Pseudo_isGeneralInfo( msgId ) )
+	    Utl_cpyStr( res, "none" );
+	else
+	{
+	    gethostname( host, MAXCHAR );
+	    snprintf( res, MAXCHAR, "%s %s", host, Db_xref( msgId ) );
+	}
+	break;
+    default:
+	ASSERT( FALSE );
+    }
+}
+
 /*
   Note this only handles a subset of headers. But they are all
   the headers any newsreader should need to work properly.
-
+ 
   That last sentence will at some stage be proved wrong.
  */
 static Bool
 doXhdr( char *arg, const Cmd *cmd )
 {
-    int first, last, i, n, numb;
-    enum { SUBJ, FROM, DATE, MSG_ID, REF, BYTES, LINES, XREF } what;
-    const char *p, *msgId;
-    const Over *ov;
+    enum XhdrType what;
+    const char *p;
     Str whatStr;
 
-    if ( ! testGrpSelected() )
-        return TRUE;
     if ( sscanf( arg, "%s", whatStr ) != 1 )
     {
         putSyntax( cmd );
         return TRUE;
     }
-    Utl_toLower( whatStr );
-    if ( strcmp( whatStr, "subject" ) == 0 )
-        what = SUBJ;
-    else if ( strcmp( whatStr, "from" ) == 0 )
-        what = FROM;
-    else if ( strcmp( whatStr, "date" ) == 0 )
-        what = DATE;
-    else if ( strcmp( whatStr, "message-id" ) == 0 )
-        what = MSG_ID;
-    else if ( strcmp( whatStr, "references" ) == 0 )
-        what = REF;
-    else if ( strcmp( whatStr, "bytes" ) == 0 )
-        what = BYTES;
-    else if ( strcmp( whatStr, "lines" ) == 0 )
-        what = LINES;
-    else if ( strcmp( whatStr, "xref" ) == 0 )
-        what = XREF;
-    else
+    what = whatXhdrField( whatStr );
+    if ( what == UNKNOWN )
     {
         putStat( STAT_HEAD_FOLLOWS, "Unknown header (empty list follows)" );
         putEndOfTxt();
@@ -1081,131 +1192,112 @@
     p = Utl_restOfLn( arg, 1 );
     if ( p[ 0 ] == '<' )
     {
-	first = last = Cont_find( p );
-	if ( first < 0 )
-	    numb = 0;
+	Over * ov;
+	Str field;
+	
+	/* Argument is message ID */
+	ov = Db_over( p );
+	if ( ov == NULL )
+	{
+	    putStat( STAT_NO_SUCH_ID, "No such article" );
+	    return TRUE;
+	}
+        putStat( STAT_HEAD_FOLLOWS, "%s header %s", whatStr, p ) ;
+	getXhdrField( what, ov, field );
+	putTxtLn( "%s %s", p, field );
+	del_Over( ov );
     }
     else
+    {
+	const Over * ov;
+	int first, last, i, n, numb;
+	Str field;
+	
+	/* Argument is article no. or range */
+	if ( ! testGrpSelected() )
+	    return TRUE;
 	parseRange( p, &first, &last, &numb );
-    if ( numb == 0 )
-        putStat( STAT_NO_ART_SELECTED, "No articles selected" );
-    else
-    {
+	if ( numb == 0 )
+	{
+	    putStat( STAT_NO_ART_SELECTED, "No articles selected" );
+	    return TRUE;
+	}
         putStat( STAT_HEAD_FOLLOWS, "%s header %lu-%lu",
-                 whatStr, first, last ) ;
+		 whatStr, first, last ) ;
         for ( i = first; i <= last; ++i )
             if ( ( ov = Cont_get( i ) ) )
             {
                 n = Ov_numb( ov );
-                switch ( what )
-                {
-                case SUBJ:
-                    putTxtLn( "%lu %s", n, Ov_subj( ov ) );
-                    break;
-                case FROM:
-                    putTxtLn( "%lu %s", n, Ov_from( ov ) );
-                    break;
-                case DATE:
-                    putTxtLn( "%lu %s", n, Ov_date( ov ) );
-                    break;
-                case MSG_ID:
-                    putTxtLn( "%lu %s", n, Ov_msgId( ov ) );
-                    break;
-                case REF:
-                    putTxtLn( "%lu %s", n, Ov_ref( ov ) );
-                    break;
-                case BYTES:
-                    putTxtLn( "%lu %d", n, Ov_bytes( ov ) );
-                    break;
-                case LINES:
-                    putTxtLn( "%lu %d", n, Ov_lines( ov ) );
-                    break;
-		case XREF:
-		    msgId = Ov_msgId( ov );
-		    if ( Pseudo_isGeneralInfo( msgId ) )
-			putTxtLn( "%lu %s:%lu", n, server.grp, n );
-		    else
-			putTxtLn( "%lu %s", n, Db_xref( msgId ) );
-		    break;
-                default:
-                    ASSERT( FALSE );
-                }
+		getXhdrField( what, ov, field );
+		putTxtLn( "%lu %s", n, field );
             }
-        putEndOfTxt();
     }
+    putEndOfTxt();
     return TRUE;
 }
 
 static Bool
 doXpat( char *arg, const Cmd *cmd )
 {
-    int first, last, i, n;
-    enum { SUBJ, FROM, DATE, MSG_ID, REF } what;
-    const Over *ov;
-    Str whatStr, pat;
+    enum XhdrType what;
+    Str whatStr, articles, pat;
 
-    if ( ! testGrpSelected() )
-        return TRUE;
-    if ( sscanf( arg, "%s %d-%d %s", whatStr, &first, &last, pat ) != 4 )
+    if ( sscanf( arg, "%s %s %s", whatStr, articles, pat ) != 3 )
     {
-        if ( sscanf( arg, "%s %d- %s", whatStr, &first, pat ) == 3 )
-            last = Cont_last();
-        else if ( sscanf( arg, "%s %d %s", whatStr, &first, pat ) == 3 )
-            last = first;
-        else
-        {
-            putSyntax( cmd );
-            return TRUE;
-        }
+	putSyntax( cmd );
+	return TRUE;
     }
-    Utl_toLower( whatStr );
-    if ( strcmp( whatStr, "subject" ) == 0 )
-        what = SUBJ;
-    else if ( strcmp( whatStr, "from" ) == 0 )
-        what = FROM;
-    else if ( strcmp( whatStr, "date" ) == 0 )
-        what = DATE;
-    else if ( strcmp( whatStr, "message-id" ) == 0 )
-        what = MSG_ID;
-    else if ( strcmp( whatStr, "references" ) == 0 )
-        what = REF;
-    else
+    what = whatXhdrField( whatStr );
+    if ( what == UNKNOWN )
     {
-        putStat( STAT_HEAD_FOLLOWS, "invalid header (empty list follows)" );
+        putStat( STAT_HEAD_FOLLOWS, "Unknown header (empty list follows)" );
         putEndOfTxt();
         return TRUE;
     }
-    putStat( STAT_HEAD_FOLLOWS, "header" ) ;
-    for ( i = first; i <= last; ++i )
-        if ( ( ov = Cont_get( i ) ) )
-        {
-            n = Ov_numb( ov );
-            switch ( what )
+    if ( articles[ 0 ] == '<' )
+    {
+	Over * ov;
+	Str field;
+	
+	/* Argument is message ID */
+	ov = Db_over( articles );
+	if ( ov == NULL )
+	{
+	    putStat( STAT_NO_SUCH_ID, "No such article" );
+	    return TRUE;
+	}
+        putStat( STAT_HEAD_FOLLOWS, "%s header %s", whatStr, articles ) ;
+	getXhdrField( what, ov, field );
+	if ( Wld_match( field, pat ) )
+	    putTxtLn( "%s %s", articles, field );
+	del_Over( ov );
+    }
+    else
+    {
+	const Over * ov;
+	Str field;
+	int first, last, i, n, numb;
+
+	/* Argument is article no. or range */
+	if ( ! testGrpSelected() )
+	    return TRUE;
+	parseRange( articles, &first, &last, &numb );
+	if ( numb == 0 )
+	{
+	    putStat( STAT_NO_ART_SELECTED, "No articles selected" );
+	    return TRUE;
+	}
+        putStat( STAT_HEAD_FOLLOWS, "%s header %lu-%lu",
+		 whatStr, first, last ) ;
+        for ( i = first; i <= last; ++i )
+            if ( ( ov = Cont_get( i ) ) )
             {
-            case SUBJ:
-                if ( Wld_match( Ov_subj( ov ), pat ) )
-                     putTxtLn( "%lu %s", n, Ov_subj( ov ) );
-                break;
-            case FROM:
-                if ( Wld_match( Ov_from( ov ), pat ) )
-                    putTxtLn( "%lu %s", n, Ov_from( ov ) );
-                break;
-            case DATE:
-                if ( Wld_match( Ov_date( ov ), pat ) )
-                    putTxtLn( "%lu %s", n, Ov_date( ov ) );
-                break;
-            case MSG_ID:
-                if ( Wld_match( Ov_msgId( ov ), pat ) )
-                    putTxtLn( "%lu %s", n, Ov_msgId( ov ) );
-                break;
-            case REF:
-                if ( Wld_match( Ov_ref( ov ), pat ) )
-                    putTxtLn( "%lu %s", n, Ov_ref( ov ) );
-                break;
-            default:
-                ASSERT( FALSE );
+                n = Ov_numb( ov );
+		getXhdrField( what, ov, field );
+		if ( Wld_match( field, pat ) )
+		    putTxtLn( "%lu %s", n, field );
             }
-        }
+    }
     putEndOfTxt();
     return TRUE;
 }
@@ -1258,6 +1350,15 @@
     
     if ( ! testGrpSelected() )
         return TRUE;
+    Grp_setLastAccess( server.grp, time( NULL ) );
+
+    /*
+     * All the info we require is now in RAM, and we may generate
+     * lots of output (consider XOVER 2-3999), so release the lock
+     * while responding.
+     */
+    closeServer();
+    
     parseRange( arg, &first, &last, &n );
     if ( n == 0 )
 	first = last = server.artPtr;
@@ -1332,6 +1433,7 @@
     if ( ! Lock_openDatabases() )
       return FALSE;
     server.running = TRUE;
+    server.lastServerOpen = time( NULL );
     return TRUE;
 }
 
@@ -1349,45 +1451,50 @@
     Bool done;
     int r;
     Str line;
-    struct timeval timeOut;
-    fd_set readSet;
 
     putWelcome();
     done = FALSE;
     while ( ! done )
     {
-        FD_ZERO( &readSet );
-        FD_SET( STDIN_FILENO, &readSet );
-        /* Never hold lock more than 5 seconds (empirically good value,
-           avoids to close/open databases, if clients sends several
-           commands, but releases the lock often enough, for allowing
-           multiple persons to read news at the same time) */
-        timeOut.tv_sec = 5;
-        timeOut.tv_usec = 0;
-        r = select( STDIN_FILENO + 1, &readSet, NULL, NULL, &timeOut );
+	/*
+	 * If we've had the lock for more than 2 seconds,
+	 * force it to be released. Otherwise, if we have
+	 * the lock but have had it for < 2 secs, 
+	 * wait for a command line for a maximum
+	 * of 2 seconds. This is all an attempt at striking a
+	 * balance between efficient processing of commands
+	 * and hogging the lock.
+	 */
+	if ( server.running )
+	{
+	    time_t now;
+
+	    now = time( NULL );
+	    if ( difftime( now, server.lastServerOpen ) > 2.0 )
+		closeServer();
+	}
+        r = waitCmdLn( line, ( server.running ) ? 2 : 0 );
         if ( r < 0 )
+        {
+            Log_inf( "Client disconnected. Terminating." );
             done = TRUE;
+        }
         else if ( r == 0 )
-        {
-            if ( server.running )
-                closeServer();
-        }
-        else /* ( r > 0 ) */
-        {
-            if ( ! server.running )
-            {
+	{
+	    if ( server.running )
+		closeServer();
+	}
+	else /* ( r > 0 ) */
+	{
+	    if ( ! server.running )
+	    {
                 if ( ! initServer() )
                 {
                     putFatal( "Cannot init server" );
                     done = TRUE;
                 }
             }
-            if ( ! getLn( line ) )
-            {
-                Log_inf( "Client disconnected. Terminating." );
-                done = TRUE;
-            }
-            else if ( ! parseAndExecute( line ) )
+            if ( ! parseAndExecute( line ) )
                 done = TRUE;
         }
     }
--- a/src/util.c	Tue Jul 25 13:12:50 2000 +0100
+++ b/src/util.c	Tue Jul 25 13:14:54 2000 +0100
@@ -1,7 +1,7 @@
 /*
   util.c
 
-  $Id: util.c 149 2000-06-19 21:56:12Z bears $
+  $Id: util.c 183 2000-07-25 12:14:54Z bears $
 */
 
 #if HAVE_CONFIG_H
@@ -265,31 +265,75 @@
 static const char *MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
 			     "Aug", "Sep", "Oct", "Nov", "Dec", NULL };
 
+/*
+ * Calculate the difference between local time and GMT. This is INN's
+ * 'always-works' method. It assumes the time differences is < 24hrs.
+ * Sounds reasonable to me. It also assumes it can ignore seconds.
+ * Returns GMT - localtime minutes. It will also trash the localtime/
+ * gmtime/etc. static buffer.
+ */
+static int
+tzDiff( void )
+{
+    time_t now;
+    struct tm local, gmt, *tm;
+    static time_t nextCalc = 0;
+    static int res = 0;
+
+    now = time( NULL );
+    if ( now < nextCalc )
+	return res;
+    
+    tm = localtime( &now );
+    if ( tm == NULL )
+	return 0;
+    local = *tm;
+    tm = gmtime( &now );
+    if ( tm == NULL )
+	return 0;
+    gmt = *tm;
+
+    res = gmt.tm_yday - local.tm_yday;
+    if ( res < -1 )
+	res = -1;		/* Year rollover? */
+    else if ( res > 1 )
+	res = 1;
+
+    res *= 24;
+    res += gmt.tm_hour - local.tm_hour;
+    res *= 60;
+    res += gmt.tm_min - local.tm_min;
+
+    /* Need to recalc at start of next hour */
+    nextCalc = now + ( 60 - local.tm_sec ) + 60 * ( 59 - local.tm_min );
+    
+    return res;
+}
+
 void
 Utl_newsDate( time_t t, Str res )
 {    
-    struct tm local, localAsGmt;
-    time_t tlocalAsGmt;
-    int tzdiff, hoffset, moffset;
+    struct tm *local;
+    long tzdiff, hoffset, moffset;
+
+    tzdiff = - tzDiff();
 
-    local = *localtime( &t );
-    memset( &localAsGmt, 0, sizeof( localAsGmt ) );
-    localAsGmt.tm_sec = local.tm_sec;
-    localAsGmt.tm_min = local.tm_min;
-    localAsGmt.tm_hour = local.tm_hour;
-    localAsGmt.tm_mday = local.tm_mday;
-    localAsGmt.tm_mon = local.tm_mon;
-    localAsGmt.tm_year = local.tm_year;
-    tlocalAsGmt = mktime( &localAsGmt );
-    tzdiff = (int) ( ( (long) difftime( tlocalAsGmt, t ) ) / 60 );
+    local = localtime( &t );
+    if ( local == NULL )
+    {
+	Utl_cpyStr( res, "** localtime failure **" );
+	return;
+    }
+    
     hoffset = tzdiff / 60;
     moffset = tzdiff % 60;
-    if ( moffset < 0 ) moffset = -moffset;
+    if ( moffset < 0 )
+	moffset = - moffset;
 
-    sprintf( res, "%s, %d %s %4d %02d:%02d:%02d %+03d%02d",
-	     DOTW[local.tm_wday], local.tm_mday,
-	     MON[local.tm_mon], local.tm_year + 1900,
-	     local.tm_hour, local.tm_min, local.tm_sec,
+    sprintf( res, "%s, %d %s %4d %02d:%02d:%02d %+03ld%02ld",
+	     DOTW[local->tm_wday], local->tm_mday,
+	     MON[local->tm_mon], local->tm_year + 1900,
+	     local->tm_hour, local->tm_min, local->tm_sec,
 	     hoffset, moffset );
 }
 
@@ -297,12 +341,13 @@
 Utl_parseNewsDate( const char *s )
 {
     struct tm tm;
-    int wday, tzoffset, hoffset, moffset;
+    int wday, offset, tzoffset;
     char *p;
     time_t res;
 
     memset( &tm, 0, sizeof( tm ) );
     wday = -1;
+    tm.tm_isdst = -1;
     
     s = nextNonWhiteSpace( s );
 
@@ -380,8 +425,9 @@
 	s += 2;
     else
     {
-	tzoffset = (int) strtol( s, &p, 10 );
+	offset = (int) strtol( s, &p, 10 );
 	s = p;
+	tzoffset = ( offset / 100 ) * 60 + ( offset % 100 );
     }
 
     /* Check for following junk */
@@ -395,9 +441,12 @@
     if ( wday >= 0 && wday != tm.tm_wday )
 	return (time_t) -1;
 
-    moffset = tzoffset % 100;
-    hoffset = tzoffset / 100;
-    res = res - ( ( hoffset * 60 + moffset ) * 60 );
+    /* Remove local time diff from res to give time as if GMT */
+    res -= tzDiff() * 60;
+
+    /* And now adjust for tzoffset */
+    res -= tzoffset * 60;
+    
     return res;
 }
 
@@ -429,7 +478,9 @@
 
     for ( ; ; )
     {
-	printf( "\nEnter date:  " );
+	t = time( NULL );
+	Utl_newsDate( t, line );
+	printf( "\n(%s) Enter date:  ", line );
 	(void) fflush( stdout );
 	if ( gets( line ) == NULL || line[0] == '\0' )
 	    break;
@@ -440,8 +491,7 @@
 	else
 	{
 	    Utl_newsDate( t, line );
-	    printf( "Utl_newsDate -> '%s'\nctime() -> '%s'\n",
-		    line, ctime( &t ) );
+	    printf( "Utl_newsDate -> '%s'\n", line );
 	}
     }