copyright 2001, D. Glenn Arthur Jr.
[What's new at this site] Last updated 2005-11-24:.

[ About Donations ]
(Back to source code index)

Multifinger.c -- Combine 'finger' Data From Multiple Hosts

At my old ISP, there were four different machines you might wind up on when you telnetted in, with automatic load-balancing between them. Now this was back in the days when the 'finger' command was more useful and more often used. Apparently the remote finger daemon is shut off, most places, for security reasons, but back then if your friends had accounts on Unix machines, it was common to 'finger' them to see whether they were logged on, and to check their .plan files to see what they were up to, if they were the sort to put scheduling info there, or what hilarity was included if they were the sort to insert silly stuff in their .plan.

So if you hadn't heard from someone in a while, you might 'finger' them to see whether they'd at least been well enough to log in recently. Or if you were trying to decide whether to phone or send email to make dinner plans, you'd check to see whether they were logged in right at that moment. And yeah, it got used in somewhat stalkerish ways as well, of course.

Now, it'd be less useful even if it weren't turned off for privacy reasons or network-security reasons, because most people don't log on to a named computer in a known location when they're "on". They use a dynamically addressed personal computer, so you wouldn't know where to direct 'finger' to look, and more often than not they're on a Windows box, not a Unix system. But back then it was a useful tool for many of us, and for those of us at this particular ISP the fact that 'finger' would give login status / last login times for only one of four machines one of our friends might be logged in on kind of undercut its usefulness.

So I wrote this program to collect the 'finger' information from all four hosts at that ISP, present only a single copy of the "who is this person" info and their .plan file, and show all four last-login / currently-logged-in / how-long-idle lines. Convenient, if a mite slow.

I hadn't gotten a handle on socket programming, so I did this all at the "execute the shell command and scrape the output" level, not by talking directly to the finger daemons on each machine. And this was my first ever attempt to use pipes to tie two processes together.

I haven't improved the comments to make myself look better on the web. This is how the file looked the last time I was still using it, back in 1998. Admittedly, I did make the source accessible to other users at that ISP at the time, so the comments weren't written entirely for myself, but a lot of my documentation motivation was the knowledge that I would have forgotten a lot of details by the next time I needed to maintain it.

Today, the program isn't terribly useful (though I just noticed that I do still have a copy in $HOME/bin on my Linux machines at home -- I'd forgotten it was there), but I figured I'd share what I think a commented C program looks like. I've seen a lot of code out there that doesn't really have enough information in the comments to count.

And hey, maybe some newbie will find a fork/pipe example useful (I would have!), or someone will want to borrow the wordwrap() function (or is there a standard library call for that these days?).

multifinger.c

( Download/view this code without the HTML markup)

/* multifinger.c, 3 February, 1997, D. Glenn Arthur Jr.  */

#include <stdio.h>

#define VERSION "version 0.6T (for Linux at houseoftea.org), 19970203.1615, uses /usr/bin/finger, skips access3"

/*====================================================================*/
/*                                                                    */
/* multifinger.c             Gather info from 'finger' responses from */
/*                                                 multiple machines. */
/*                                                                    */
/* This program fingers a user at access.digex.net.  It displays all  */
/* the usual stuff that 'finger' displays, except that it collects    */
/* the "Last login", "On since", and "Never logged in" lines from all */
/* four access machines (access1.digex.net, access2.digex.net,        */
/* access3.digex.net, and digex4.digex.net).                          */
/*                                                                    */
/* The program has three parts:                                       */
/*                                                                    */
/* 1) a parent which forks and waits for its child to start babbling, */
/*    then decides what parts of the result to display,               */
/* 2) a child which redirects its output to a pipe back to the parent */
/*    then forks itself once for each access machine, and             */
/* 3) a grandchild which execs 'finger' to get the info from one      */
/*    machine.                                                        */
/*                                                                    */
/* Flaws:  This program is slow.  It has to run 'finger' four times   */
/* over the local network.  It also reads and discards three copies   */
/* of the target user's .plan file.  Another problem is that it does  */
/* not detect "connection refused" or timeouts on any of the 'finger' */
/* commands it executes.                                              */
/*                                                                    */
/* Improvements:  Allow multiple arguments on the command line to     */
/* finger more than one user at once.  Allow the nomal options to the */
/* standard 'finger' command to have the same effects here.  Handle   */
/* refused connections, timeouts, and the like.  Move functions that  */
/* ought to be library routines to their own modules.  Set up a table */
/* of which domains/hosts are set up with multiple host machines like */
/* Digex is, so that fingering a user at such a site automagically    */
/* does the right thing.  (Table should be user-configurable.)  WRITE */
/* A 'MAN' PAGE.                                                      */
/*                                                                    */
/* Bugs:  If there is a match on the "real name" field as well as on  */
/* username, remote 'finger' doesn't honor the -m option, so data for */
/* users other than the one specified come back, thus confusing this  */
/* program.  Also, if a user is logged on more than once on one host  */
/* (i.e. using 'screen'), the program is similarly confused.          */
/*                                                                    */
/* Version 0.1  DGA  19950406  Announced on digex.general             */
/* Modified     DGA  19950407  Added comments, internal documentation */
/* Modified     DGA  19950408  Added wordwrap() for help message      */
/* Modified     DGA  19950411  Reorganized, split into functions.     */
/* Modified     DGA  19950417  Changed NUMHOSTS to 5.                 */
/* Version 0.4a DGA  19950711  Temp. "IGNORETHREE" fix.               */
/* Modified     DGA  19951214  Added email address to help message    */
/* Modified     DGA  19951218  Made /usr/ucb/finger explicit.         */
/* Modified     DGA  19961011  Removed "IGNORETHREE".                 */
/* Modified     DGA  19970902  Reinstalled "IGNORETHREE".             */
/* Modified     DGA  19970902  Added some clues for 'ps' to chew on   */
/* Version 0.6T DGA  19980203  Uses /usr/bin/finger for Teahouse vrsn */
/*                                                                    */
/*====================================================================*/


/* Glenn's standard debugging macro */

#define DEBUG    if ( debug ) printf
#define debug    0

/* Subscripts for the argument of pipe() */

#define W_END     1
#define R_END     0

/* The standard files */

#define STDIN     0
#define STDOUT    1
#define STDERR    2


/*====================================================================*/
/* CONFIGURATION CONSTANTS                                            */
/*====================================================================*/

#define BUFFLEN    80
#define NUMHOSTS    5

#define HELP_MESSAGE "\
This program takes one argument, a username, and fingers that user \
at access.digex.net, but unlike the normal 'finger' command it gathers \
the login time ('On since' or 'Last login' times) from _each_ of the \
five access machines.  \
\n********\n\
Notes:  Will not yet correctly handle multiple hits in /etc/passwd \
i.e. situations where the username matches more than one user's \
'realname' field.\
\n\nTemporarily ignoring access3, 'cause it seems to have been dead \
a while and messes things up.\
\n\nEmail bug reports, suggestions, patches, praise to \
glenn@access.digex.net\
\n"
 

/* GLOBAL VARIABLES */

char argzero[80];               /* Keeps argv[0] in a globally        */
                                /* accessible location                */

char childname[6] = "child";

/*====================================================================*/
/*                                                                    */
/* fdgets()                         Like gets() but reads from a pipe */
/*                                                                    */
/* Takes three arguments:  the file descriptor of the "read end" of   */
/* pipe you wanna read, the buff to stuff, and the maximum number of  */
/* bytes to transfer.  Works much like gets() -- copies bytes from    */
/* the pipe to the buffer until it's got N bytes or it hits a newline */
/* or end of file happens.  Returns the number of bytes transferred,  */
/* or -1 to signal end of file (closed pipe).  Ought to work on       */
/* ordinary files as well, but I haven't tried that.                  */
/*                                                                    */
/* Improvements:  This function really ought to read into a buffer    */
/* instead of doing all those single-character read() calls, for the  */
/* sake of efficiency.  Even if the OS buffers read() for us, it's    */
/* still that many more function calls.                               */
/*                                                                    */
/* Created   DGA  19950404                                            */
/* Commented DGA  19950411                                            */
/*                                                                    */
/*====================================================================*/

int    fdgets ( fd , s , length )
int    fd ;                     /* File descriptor from which to read */
char    *s;                     /* Buffer into which to put read data */
int    length;                  /* Maximum number of bytes to read    */

    {
    int     count;              /* Holds # bytes read by call to      */
                                /* read()                             */
    int     sofar;              /* Running count of # bytes read by   */
                                /* this invocation of this function   */
    char    c[2];               /* Temporary buffer to hold single    */
                                /* characters read from fd            */

    /* Read a byte at a time from (fd) until a termination condition  */
    /* (newline, EOF, or max # bytes read) happens.  I guess this     */
    /* really ought to be a single for() statement with no body, just */
    /* to be manly ... Let's see, that'd be:                          */
    /*     "for ( sofar = 0 ; (count = read(fd,c,1) != 0) &&          */
    /*     (c[0] != '\n') && (sofar < length-1) ; s[sofar++]          */
    /*     = c[0] );"                                                 */
    /* but I think I split it up for debugging purposes or something. */

    sofar = 0;
    while ( ( count = read(fd, c, 1) != 0 ) 
         && ( c[0] != '\n' ) && ( sofar < length-1 ) )
        {
        s[sofar++] = c[0];
        }

    /* At this point, (sofar) bytes have been read, so s[sofar-1] is  */
    /* the last byte read.  Stick a null on the end no matter what    */
    /* made us stop.                                                  */

    s[sofar] = '\0';

    /* Figure out why we stopped, and return the EOF indicator (-1)   */
    /* or the number of bytes read, as appropriate.                   */

    if ( count == 0 )               /* Last 1-byte read failed.       */
        return ( -1 );              /* Signal EOF to caller.          */
    else                            /* Note EOF yet.                  */
        return ( sofar );           /* Return # bytes actually read.  */
    }

/*===== end of fdgets() ==============================================*/

/*====================================================================*/
/*                                                                    */
/* wordwrap()                              displays text word-wrapped */
/*                                                                    */
/* Takes a loooong string and wraps it to the specified number of     */
/* columns, breaking at whitespace.  Prepends a prefix to each line   */
/* of output.  I should sit down and write a more versatile set of    */
/* related functions.                                                 */
/*                                                                    */
/* Created  19950408  DGA                                             */
/*                                                                    */
/*====================================================================*/


void wordwrap ( longstring , columns , prefix )
char *longstring;               /* Input text                          */
int  columns;                   /* Number of columns to wrap to        */
char *prefix;                   /* String to prepend to output lines   */

    {
    int left, right, lf;
    char linebuff[256];

    left = 0;

    while ( left + columns < strlen(longstring) )
        {

    /* Start at the right margin and work backwards looking for a space */

        for ( right = left + columns ; longstring[right] != ' ' ; right-- );

    /* Copy what's between our left & right markers to (linebuff)       */

        strncpy ( linebuff , &longstring[left] , right-left );

    /* Check for linefeeds -- we need to handle them specially          */

        for ( lf = 0 ; (lf < columns) && (linebuff[lf] != '\n') ; lf++ );

    /* No line feeds -- terminate (linebuff) and adjust the left marker */

        if ( lf < columns )
            {
            linebuff[lf] = '\0';
            left = left + lf + 1;
            }

    /* Found a line feed -- replace it with a null in (linebuff) and    */
    /* move the left marker to the character after it in the input      */
    /* buffer.  The next pass through the loop will pick up with the    */
    /* character after the linefeed being the start of a complete line. */

        else
            {
            linebuff[right-left] = '\0';
            left = right + 1;
            }

    /* Whatever we've got in (linebuff) now gets printed, with (prefix) */
    /* prepended first.                                                 */

        printf ( "%s%s\n" , prefix , linebuff );
        }
 
    /* When we get out of the while() loop, print out whatever's left */
    /* in the input string.                                           */   

    printf ( "%s%s\n" , prefix , &longstring[left] );
    }

/*===== end of wordwrap() ============================================*/

/*====================================================================*/
/*                                                                    */
/* grandchild()                  Exec's 'finger' after having had its */
/*                                       output redirected to a pipe. */
/*                                                                    */
/* This function is called (NUMHOSTS) times by child().  Before this, */
/* child() has redirected stdout to a pipe back to main().  All this  */
/* function does is use sprintf() to combine its arguments into a     */
/* command string and call execlp() to invoke 'finger' on one host.   */
/*                                                                    */
/* Created  19950411  DGA  Split this off into a separate function.   */
/*                                                                    */
/*====================================================================*/

int grandchild ( user , hostsuffix )
char *user;
int  hostsuffix;

    {
    char argbuff[80];

#ifdef IGNORETHREE
    /* This section forces the program to ignore access3.  As this    */
    /* section is being added, access3 has been unresponsive for a    */
    /* while and I'm not ready to rewrite things so that a dead host  */
    /* doesn't screw things up at the moment.                         */
    /* 11 OCTOBER 1996 -- access3 seems to be back up!                */
    /* 25 MARCH 1997   -- access2 is hosed, so this code is being     */
    /*                    reactivated, modified to refer to access2.  */
    /* 2 SEPTEMBER 1997 - access3 has been dead a while, so ...       */

    if (hostsuffix == 3)
        {
        printf ( "Last login:  (access3 not checked).\n" );
	exit(-1);
        }
#endif

    /* printf ( "This is grandchild number %d.\n" , hostsuffix ); */
    sprintf( argbuff, "%s@access%d.digex.net", user, hostsuffix );
    execlp ( "/usr/bin/finger" , "((mfinger))" , argbuff , NULL );

    /* Note that execlp(), if successful,  _does_not_return_, so this */
    /* point should be an implied exit().  Just in case execlp() does */
    /* return, here's an error message.  Might as well re-use argbuff */
    /* for this, since it's there.                                    */

    sprintf ( argbuff , "%s (child process):  attempt to exec 'finger' failed.\n" , argzero );
    write ( STDERR , argbuff , strlen(argbuff) );
    exit(-1);
    }

/*====================================================================*/
/*                                                                    */
/* child()              Redirect stdout to previously opened pipe and */
/*                    fork() (NUMHOSTS) copies of grandchild() to run */
/*                                             'finger' on each host. */
/*                                                                    */
/* This function redirects its stdout into the pipe opened in main(), */
/* then forks and waits (NUMHOSTS) times to run a copy of             */
/* grandchild() on each host.                                         */
/*                                                                    */
/* Created   19950411  DGA  Split off into separate function.         */
/*                                                                    */
/*====================================================================*/

int child( username , pipe_end , pname )
char *username;
int  pipe_end;
char *pname;

    {
    int  suffix;
    int  grandchild_pid;

	strncpy ( pname , "(mfinger)" , strlen(pname) );

#ifdef DEBUG
	sleep(20);	/* Give time to look up PID for debugger */
        DEBUG ( "This is the child.\n" );
#endif

    /* Redirect stdout so that grandchildren's output gets sent back  */
    /* to main().                                                     */

        if ( dup2 ( pipe_end , STDOUT ) != STDOUT )
            {
            printf ( "%s:  problem redirecting output.\n" , argzero );
            exit(-1);
            }
        
    /* For each host, fork a copy of grandchild() to run 'finger' on  */
    /* it.                                                            */

        for ( suffix = 1 ; suffix <= NUMHOSTS ; suffix++ )
            {
            if ( ( grandchild_pid = fork () ) == 0 )
                grandchild ( username , suffix );

    /* Note that grandchild() _does_not_return_, so this point is an   */
    /* implied exit() if the fork succeeded and this is the grandchild */
    /* fork process.                                                   */

    /* If this is not the grandchild process (fork() had a nonzero     */
    /* return), either fork() failed ...                               */

            else
                {
                if ( grandchild_pid == -1 )
                    {
                    printf ( "%s:  fork() problem.\n" , argzero );
                    exit(-1);
                    }

    /* ... or it succeeded and this is not the grandchild, so we need  */
    /* to wait for the grandchild to terminate before forking another, */
    /* so that the output from the grandchildren isn't interleaved.    */

                else
                    wait ( NULL );
                }
            }

    /* In any case, we've done all that this child of main() should do, */
    /* so let's terminate the process.                                  */

        exit(0);
    }

/*==== End of child() ==================================================*/

/*====================================================================*/
/*                                                                    */
/* main()            'multifinger' -- Finger a user on all four Digex */
/*                         "access" hosts at once, showing last-login */
/*                            times for each host and the rest of the */
/*                                         'finger' output only once. */
/*                                                                    */
/* Apart from checking the command line for -v or -h or the wrong     */
/* number of arguments, what this does is:                            */
/*  1)  Create a pipe.                                                */
/*  2)  Fork -- the child will redirect it's stdout and fork again.   */
/*  3)  Wait for output from child and selectively display it.        */
/* For details on #3, see comments inside final while() loop.         */
/*                                                                    */
/* Created  DGA  19950406                                             */
/* Modified DGA  19950411  Split off child() and grandchild()         */
/*                                                                    */
/*====================================================================*/

int    main ( argc , argv )
int    argc;
char   **argv;

    {
    char    username[10];    /* Holds copy of username to try to finger */
    int     child_pid;        /* Holds return value of fork()          */
    int     state, responses; /* Used to determine what parts of the   */
                              /* output from the grandchild processes  */
                              /* to display, and what to throw away.   */
    char    buffer[81];       /* Holds a line of output from granchild */
    int     apipe[2];         /* Pipe through which grandchildren's    */
                              /* output comes back.                    */

    /* Check the command line.   This section is fairly intuitive (and */
    /* rather brute-force.)                                            */

    if ( argc != 2 )
        {
        printf ( "usage:  %s username\n" , argv[0] );
        printf ( " -or-   %s -h        (for more info)\n" , argv[0] );
        printf ( " -or-   %s -v        (for version)\n" , argv[0] );
        exit(0);
        }

    if ( strncmp ( argv[1] , "-v" , 2 ) == 0 )
        {
        printf ( "%s:  %s\n" , argv[0] , VERSION );
        exit(1);
        }

    if ( strncmp ( argv[1] , "-h" , 2 ) == 0 )
        {
        wordwrap ( HELP_MESSAGE , 67-strlen(argv[0]) , sprintf ( buffer , "%s:    " , argv[0]) );
        exit(1);
        }

    DEBUG ( "DEBUG -- Got past checking command line.\n" );

    /* Set things up -- set globals, copy things where they need to be, */
    /* create the pipe. */

    strncpy ( argzero  , argv[0] , 80 );
    strncpy ( username , argv[1] ,  9 );

    if ( pipe ( apipe ) != 0 )
        {
        printf ( "%s: could not open pipe.\n" , argv[0] );
        exit(-1);
        }

    /* Call fork().  If fork() returns zero, do child(), else continue */
    /* with main().   */

    if ( ( child_pid = fork () ) == 0 )
        child ( username , apipe[W_END] , argv[0] );

    else
        {

    /* fork() returned nonzero, so this is the parent process.  First, */
    /* make sure there really _is_ a child process ...                 */

        if ( child_pid == -1 )
            {
            /* If fork() returned -1, that means it failed. */

            printf ( "%s:  fork() failed.\n" , argv[0] );
            exit(-1);
            }
        
        DEBUG ( "This is the parent.  The child is %d.\n" , child_pid );
        DEBUG ( "debug -- about to hit while() loop in parent.\n" );

    /* Close the parent's copy of the writing end of the pipe, so that */
    /* when the child process terminates (closing its copy of that end */
    /* of the pipe), we'll get an end-of-file.                         */

        close ( apipe[W_END] );

    /* Now all that's left is to read the responses we get through the */
    /* pipe and decide which parts to display.  So what follows is a   */
    /* while() loop to read until the pipe closes, with the body being */
    /* a state machine that handles each of the three phases of the    */
    /* output -- printing everything that comes before the last login  */
    /* time, printing the login times, and printing everything that    */
    /* comes after the last login time.                                */

        state = 1;
        while ( fdgets ( apipe[R_END] , buffer , BUFFLEN ) != -1 )
            {
            switch ( state )
                {

    /* State One -- We're looking at the output of the first 'finger'  */
    /* command (access1).  We've not yet gotten to the Last login / On */
    /* since / Never logged in.  Until we get that, just display what  */
    /* comes back from the grandchildren.  When we _do_ get that line, */
    /* prefix it with the name of the machine (access1), display it,   */
    /* and go to state two.                                            */

                case 1:    /* DEBUG ( "debug:  state=1  " ); */
                    if ((strncmp(buffer,"On since",8)==0)||
                        (strncmp(buffer,"Never lo",8)==0)||
                        (strncmp(buffer,"Last log",8)==0))
                        {
                        state = 2;
                        responses = 1;
                        }
                    if ( state == 1 )
                        puts ( buffer );
                    else
                        printf ( "access%d:  %s\n" , responses , buffer );
                    break;

    /* State Two -- We've already started displaying the login times.  */
    /* We're currently looking at the responses from 'finger' at host  */
    /* two, three, or four, and we're looking for that login time,     */
    /* displaying that and ignoring all else.  Each time we get one,   */
    /* increment (responses), the counter of how many hosts we've      */
    /* gotten our answers from.  Once (responses) gets to NUMHOSTS, we */
    /* know we've gotten all the login times we're going to get, so we */
    /* can go on to state three.                                       */

                case 2:    /* DEBUG ( "debug:  state=2  " ); */
                    if ((strncmp(buffer,"On since",8)==0)||
                        (strncmp(buffer,"Never lo",8)==0)||
                        (strncmp(buffer,"Last log",8)==0))
                        {
                        responses++;
                        printf ( "access%d:  %s\n" , responses, buffer );
                        if ( responses == NUMHOSTS )
                            state = 3;
                        }
                    break;

    /* State Two -- We're looking at the output of the first 'finger'  */
    /* State Three -- We've printed the top section (from the first    */
    /* host), all the login times (from all four hosts), and now we're */
    /* ready to print .project and .plan, so since we're getting the   */
    /* response from the last host, we just display everything we get  */
    /* from here until EOF on the pipe.                                */

                case 3:    /* DEBUG ( "debug:  state=3  "); */
                    puts ( buffer );
                    break;
                }
            }
        DEBUG ( "debug -- past final while() loop in parent.\n" );
        }

    }  /* END OF main() */

( Download/view this code without the HTML markup)

(Back to source code index)
(* email D. Glenn Arthur Jr. * Map Of My Web Pages * my main page * about me * musings and observations * me the musician * writings * events * humour *)
[ About Donations ]