11.3 Modules

We provide four modules to demonstrate the kind of functionality you could implement using this server implementation. Implementing your own server module is as simple as defining a module_generate function to return the appropriate HTML text.

11.3.1 Show Wall-Clock Time

The time.so module (see Listing 11.6) generates a simple page containing the server's local wall-clock time. This module's module_generate calls gettimeofday to obtain the current time (see Section 8.7, "gettimeofday: Wall-Clock Time," in Chapter 8, "Linux System Calls") and uses localtime and strftime to generate a text representation of it. This representation is embedded in the HTML template page_template.

Listing 11.6 (time.c) Server Module to Show Wall-Clock Time
#include <assert.h> 
#include <stdio.h> 
#include <sys/time.h> 
#include <time.h> 
 
#include "server.h" 
 
/* A template for the HTML page this module generates.  */ 
 
static char* page_template = 
  "<html>\n" 
  " <head>\n" 
  "  <meta http-equiv=\"refresh\" content=\"5\">\n" 
  " </head>\n" 
  " <body>\n" 
  "  <p>>The current time is %s.</p>\n" 
  " </body>\n" 
  "</html>\n"; 
 
void module_generate (int fd) 
{
  struct timeval tv; 
  struct tm* ptm; 
  char time_string[40]; 
  FILE* fp; 
 
  /* Obtain the time of day, and convert it to a tm struct.  */ 
  gettimeofday (&tv, NULL); 
  ptm = localtime (&tv.tv_sec); 
  /* Format the date and time, down to a single second.  */ 
  strftime (time_string, sizeof (time_string), "%H:%M:%S", ptm); 
 
  /* Create a stream corresponding to the client socket file 
     descriptor.  */ 
  fp = fdopen (fd, "w"); 
  assert (fp != NULL); 
  /* Generate the HTML output.  */ 
  fprintf (fp, page_template, time_string); 
  /* All done; flush the stream.  */ 
  fflush (fp); 
} 

This module uses standard C library I/O routines for convenience. The fdopen call generates a stream pointer (FILE*) corresponding to the client socket file descriptor (see Section B.4, "Relation to Standard C Library I/O Functions," in Appendix B, "Low-Level I/O"). The module writes to it using fprintf and flushes it using fflush to prevent the loss of buffered data when the socket is closed.

The HTML page returned by the time.so module includes a <meta> element in the page header that instructs clients to reload the page every 5 seconds. This way the client displays the current time.

11.3.2 Show the GNU/Linux Distribution

The issue.so module (see Listing 11.7) displays information about the GNU/Linux distribution running on the server. This information is traditionally stored in the file /etc/issue. This module sends the contents of this file, wrapped in a <pre> element of an HTML page.

Listing 11.7 (issue.c) Server Module to Display GNU/Linux Distribution Information
#include <fcntl.h> 
#include <string.h> 
#include <sys/sendfile.h> 
#include <sys/stat.h> 
#include <sys/types.h> 
#include <unistd.h> 
 
#include "server.h" 
 
/* HTML source for the start of the page we generate.  */ 
 
static char* page_start = 
  "<html>\n" 
  "<body>\n" 
  "<pre>\n"; 
 
/* HTML source for the end of the page we generate.  */ 
 
static char* page_end = 
  "  </pre>\n" 
  " </body>\n" 
  "</html>\n"; 
 
/* HTML source for the page indicating there was a problem opening 
   /proc/issue.  */ 
 
static char* error_page = 
  "<html>\n" 
  " <body>\n" 
  "  <p>Error: Could not open /proc/issue.</p>\n" 
  " </body>\n" 
  "</html>\n"; 
 
/* HTML source indicating an error.  */ 
 
static char* error_message = "Error reading /proc/issue."; 
 
void module_generate (int fd) 
{
  int input_fd; 
  struct stat file_info; 
  int rval; 
 
  /* Open /etc/issue.  */ 
  input_fd = open ("/etc/issue", O_RDONLY); 
  if (input_fd == -1) 
    system_error ("open"); 
  /* Obtain file information about it.  */ 
  rval = fstat (input_fd, &file_info); 
 
  if (rval == -1) 
    /* Either we couldn't open the file or we couldn't read from it.  */ 
    write (fd, error_page, strlen (error_page)); 
  else {
    int rval; 
    off_t offset = 0; 
 
    /* Write the start of the page.  */ 
    write (fd, page_start, strlen (page_start)); 
    /* Copy from /proc/issue to the client socket.  */ 
    rval = sendfile (fd, input_fd, &offset, file_info.st_size); 
    if (rval == -1) 
      /* Something went wrong sending the contents of /proc/issue. 
         Write an error message.  */ 
      write (fd, error_message, strlen (error_message)); 
      /* End the page.  */ 
      write (fd, page_end, strlen (page_end)); 
    } 
 
    close (input_fd); 
} 

The module first tries to open /etc/issue. If that file can't be opened, the module sends an error page to the client. Otherwise, the module sends the start of the HTML page, contained in page_start. Then it sends the contents of /etc/issue using sendfile (see Section 8.12, "sendfile: Fast Data Transfers," in Chapter 8). Finally, it sends the end of the HTML page, contained in page_end.

You can easily adapt this module to send the contents of another file. If the file contains a complete HTML page, simply omit the code that sends the contents of page_start and page_end. You could also adapt the main server implementation to serve static files, in the manner of a traditional Web server. Using sendfile provides an extra degree of efficiency.

11.3.3 Show Free Disk Space

The diskfree.so module (see Listing 11.8) generates a page displaying information about free disk space on the file systems mounted on the server computer. This generated information is simply the output of invoking the df -h command. Like issue.so, this module wraps the output in a <pre> element of an HTML page.

Listing 11.8 (diskfree.c) Server Module to Display Information About Free Disk Space
#include <stdlib.h> 
#include <string.h> 
#include <sys/types.h> 
#include <sys/wait.h> 
#include <unistd.h> 
 
#include "server.h" 
 
/* HTML source for the start of the page we generate.  */ 
 
static char* page_start = 
  "<html>\n" 
  " <body>\n" 
   "  <pre>\n"; 
 
   /* HTML source for the end of the page we generate.  */ 
 
   static char* page_end = 
     "  </pre>\n" 
     " </body>\n" 
     "</html>\n"; 
 
  void module_generate (int fd) 
  {
     pid_t child_pid; 
     int rval; 
 
     /* Write the start of the page.  */ 
     write (fd, page_start, strlen (page_start)); 
     /* Fork a child process.  */ 
     child_pid = fork (); 
     if (child_pid == 0) {
       /* This is the child process.  */ 
       /* Set up an argument list for the invocation of df.  */ 
       char* argv[] == { "/bin/df", "-h", NULL }; 
 
       /* Duplicate stdout and stderr to send data to the client socket.  */ 
       rval = dup2 (fd, STDOUT_FILENO); 
       if (rval == -1) 
         system_error ("dup2"); 
       rval = dup2 (fd, STDERR_FILENO); 
       if (rval == -1) 
         system_error ("dup2"); 
       /* Run df to show the free space on mounted file systems.  */ 
       execv (argv[0], argv); 
       /* A call to execv does not return unless an error occurred.  */ 
       system_error ("execv"); 
  } 
  else if (child_pid > 0) {
       /* This is the parent process. Wait for the child process to 
          finish.  */ 
       rval = waitpid (child_pid, NULL, 0); 
       if (rval == -1) 
         system_error ("waitpid"); 
  } 
  else 
       /* The call to fork failed.  */ 
       system_error ("fork"); 
  /* Write the end of the page.  */ 
  write (fd, page_end, strlen (page_end)); 
} 

While issue.so sends the contents of a file using sendfile, this module must invoke a command and redirect its output to the client. To do this, the module follows these steps:

1.       First, the module creates a child process using fork (see Section 3.2.2, "Using fork and exec," in Chapter 3)

2.       The child process copies the client socket file descriptor to file descriptors STDOUT_FILENO and STDERR_FILENO, which correspond to standard output and standard error (see Section 2.1.4, "Standard I/O," in Chapter 2). The file descriptors are copied using the dup2 call (see Section 5.4.3, "Redirecting the Standard Input," in Chapter 5). All further output from the process to either of these streams is sent to the client socket.

3.       The child process invokes the df command with the -h option by calling execv (see Section 3.2.2, "Using fork and exec," in Chapter 3)

4.       The parent process waits for the child process to exit by calling waitpid (see Section 3.4.2, "The wait System Calls," in Chapter 3).

You could easily adapt this module to invoke a different command and redirect its output to the client.

11.3.4 Summarize Running Processes

The processes.so module (see Listing 11.9) is a more extensive server module implementation. It generates a page containing a table that summarizes the processes currently running on the server system. Each process is represented by a row in the table that lists the PID, the executable program name, the owning user and group names, and the resident set size.

Listing 11.9 (processes.c) Server Module to Summarize Processes
#include <assert.h> 
#include <dirent.h> 
#include <fcntl.h> 
#include <grp.h> 
#include <pwd.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <sys/stat.h> 
#include <sys/types.h> 
#include <sys/uio.h> 
#include <unistd.h> 
 
#include "server.h" 
 
/* Set *UID and *GID to the owning user ID and group ID, respectively, 
   of process PID. Return 0 on success, nonzero on failure.  */ 
static int get_uid_gid (pid_t pid, uid_t* uid, gid_t* gid) 
{
   char dir_name[64]; 
   struct stat dir_info; 
   int rval; 
 
   /* Generate the name of the process's directory in /proc.  */ 
   snprintf (dir_name, sizeof (dir_name), "/proc/%d", (int) pid); 
   /* Obtain information about the directory.  */ 
   rval = stat (dir_name, &dir_info); 
   if (rval != 0) 
     /* Couldn't find it; perhaps this process no longer exists.  */ 
     return 1; 
   /* Make sure it's a directory; anything else is unexpected.  */ 
   assert (S_ISDIR (dir_info.st_mode)); 
 
   /* Extract the IDs we want.  */ 
   *uid = dir_info.st_uid; 
   *gid = dir_info.st_gid; 
   return 0; 
} 
 
/* Return the name of user UID. The return value is a buffer that the 
   caller must allocate with free. UID must be a valid user ID.  */ 
 
static char* get_user_name (uid_t uid) 
{
   struct passwd* entry; 
 
   entry = getpwuid (uid); 
   if (entry == NULL) 
     system_error ("getpwuid"); 
   return xstrdup (entry->pw_name); 
} 
 
/* Return the name of group GID. The return value is a buffer that the 
   caller must allocate with free. GID must be a valid group ID.  */ 
 
static char* get_group_name (gid_t gid) 
{
   struct group* entry; 
 
   entry = getgrgid (gid); 
   if (entry == NULL) 
     system_error ("getgrgid"); 
   return xstrdup (entry->gr_name); 
} 
/* Return the name of the program running in process PID, or NULL on 
   error.  The return value is a newly allocated buffer which the caller 
   must deallocate with free.  */ 
 
static char* get_program_name (pid_t pid) 
{
   char file_name[64]; 
   char status_info[256]; 
   int fd; 
   int rval; 
   char* open_paren; 
   char* close_paren; 
   char* result; 
 
   /* Generate the name of the "stat" file in the process's /proc 
      directory, and open it.  */ 
   snprintf (file_name, sizeof (file_name), "/proc/%d/stat", (int) pid); 
   fd = open (file_name, O_RDONLY); 
   if (fd == -1) 
     /* Couldn't open the stat file for this process.  Perhaps the 
        process no longer exists.  */ 
     return NULL; 
   /* Read the contents.  */ 
   rval = read (fd, status_info, sizeof (status_info) - 1); 
   close (fd); 
   if (rval <= 0) 
     /* Couldn't read, for some reason; bail.  */ 
     return NULL; 
   /* NUL-terminate the file contents.  */ 
   status_info[rval] = '\0'; 
 
   /* The program name is the second element of the file contents and is 
      surrounded by parentheses. Find the positions of the parentheses 
      in the file contents.  */ 
   open_paren = strchr (status_info, '('); 
   close_paren = strchr (status_info, ')'); 
   if (open_paren == NULL 
       || close_paren == NULL 
       || close_paren < open_paren) 
      /* Couldn't find them; bail.  */ 
      return NULL; 
   /* Allocate memory for the result.  */ 
   result = (char*) xmalloc (close_paren - open_paren); 
   /* Copy the program name into the result.  */ 
   strncpy (result, open_paren + 1, close_paren - open_paren - 1); 
   /* strncpy doesn't NUL-terminate the result, so do it here.  */ 
   result[close_paren - open_paren - 1] == '\0'; 
   /* All done.  */ 
   return result; 
} 
/* Return the resident set size (RSS), in kilobytes, of process PID. 
   Return -1 on failure.  */ 
 
static int get_rss (pid_t pid) 
{
   char file_name[64]; 
   int fd; 
   char mem_info[128]; 
   int rval; 
   int rss; 
 
   /* Generate the name of the process's "statm" entry in its //proc 
      directory.  */ 
   snprintf (file_name, sizeof (file_name), "/proc/%d/statm", (int) pid); 
   /* Open it.  */ 
   fd = open (file_name, O_RDONLY); 
   if (fd == -1) 
     /* Couldn't open it; perhaps this process no longer exists.  */ 
     return -1; 
   /* Read the file's contents.  */ 
   rval = read (fd, mem_info, sizeof (mem_info) - 1); 
   close (fd); 
   if (rval <= 0) 
     /* Couldn't read the contents; bail.  */ 
     return -1; 
   /* NUL-terminate the contents.  */ 
   mem_info[rval] = '\0'; 
   /* Extract the RSS. It's the second item.  */ 
   rval = sscanf (mem_info, "%*d %d", &rss); 
   if (rval != 1) 
     /* The contents of statm are formatted in a way we don't understand.  */ 
     return -1; 
 
   /* The values in statm are in units of the system's page size. 
      Convert the RSS to kilobytes.  */ 
   return rss * getpagesize () / 1024; 
} 
 
/* Generate an HTML table row for process PID. The return value is a 
   pointer to a buffer that the caller must deallocate with free, or 
   NULL if an error occurs.  */ 
 
static char* format_process_info (pid_t pid) 
{
   int rval; 
   uid_t uid; 
   gid_t gid; 
   char* user_name; 
   char* group_name; 
   int rss; 
   char* program_name; 
   size_t result_length; 
   char* result; 
 
   /* Obtain the process's user and group IDs.  */ 
   rval = get_uid_gid (pid, &uid, &gid); 
   if (rval != 0) 
     return NULL; 
   /* Obtain the process's RSS.  */ 
   rss = get_rss (pid); 
   if (rss == -1) 
     return NULL; 
   /* Obtain the process's program name.  */ 
   program_name = get_program_name (pid); 
   if (program_name == NULL) 
     return NULL; 
   /* Convert user and group IDs to corresponding names.  */ 
   user_name = get_user_name (uid); 
   group_name = get_group_name (gid); 
 
   /* Compute the length of the string we'll need to hold the result, and 
      allocate memory to hold it.  */ 
   result_length = strlen (program_name) 
     + strlen (user_name) + strlen (group_name) + 128; 
   result = (char*) xmalloc (result_length); 
   /* Format the result.  */ 
   snprintf (result, result_length, 
             "<tr><td align=\"right\">%d</td><td><tt>%s</tt> </td><td>%s</td>" 
             "<td>%s</td><td align=\"right\">%d</td></tr>\n", 
             (int) pid, program_name, user_name, group_name, rss); 
   /* Clean up.  */ 
   free (program_name); 
   free (user_name); 
   free (group_name); 
   /* All done.  */ 
   return result; 
} 
 
/* HTML source for the start of the process listing page.  */ 
 
static char* page_start = 
   "<html>\n" 
   " <body>\n" 
   "  <table cellpadding=\"4\" cellspacing=\"0\" border=\"1\">\n" 
   "   <thead>\n" 
   "    <tr>\n" 
   "    <th>PID</th>\n" 
   "    <th>Program</th>\n" 
   "    <th>User</th>\n" 
   "    <th>Group</th>\n" 
   "     <th>RSS&nbsp;(KB)</th>\n" 
   "    </tr>\n" 
   "   </thead>\n" 
   "   <tbody>\n"; 
 
/* HTML source for the end of the process listing page.  */ 
 
static char* page_end = 
   "   </tbody>\n" 
   "  </table>\n" 
   " </body>\n" 
   "</html>\n"; 
 
void module_generate (int fd) 
{
   size_t i; 
   DIR* proc_listing; 
 
   /* Set up an iovec array. We'll fill this with buffers that'll be 
      part of our output, growing it dynamically as necessary.  */ 
 
   /* The number of elements in the array that we've used.  */ 
   size_t vec_length = 0; 
   /* The allocated size of the array.  */ 
   size_t vec_size = 16; 
   /* The array of iovcec elements.  */ 
   struct iovec* vec = 
     (struct iovec*) xmalloc (vec_size * sizeof (struct iovec)); 
 
   /* The first buffer is the HTML source for the start of the page.  */ 
   vec[vec_length].iov_base = page_start; 
   vec[vec_length].iov_len = strlen (page_start); 
   ++vec_length; 
 
   /* Start a directory listing for /proc.  */ 
   proc_listing = opendir ("/proc"); 
   if (proc_listing == NULL) 
     system_error ("opendir"); 
 
   /* Loop over directory entries in /proc.  */ 
   while (1) {
     struct dirent* proc_entry; 
     const char* name; 
     pid_t pid; 
     char* process_info; 
 
     /* Get the next entry in /proc.  */ 
     proc_entry = readdir (proc_listing); 
     if (proc_entry == NULL) 
       /* We've hit the end of the listing.  */ 
       break; 
       /* If this entry is not composed purely of digits, it's not a 
          process directory, so skip it.  */ 
       name = proc_entry->d_name; 
       if (strspn (name, "0123456789") != strlen (name)) 
         continue; 
       /* The name of the entry is the process ID.  */ 
       pid = (pid_t) atoi (name); 
       /* Generate HTML for a table row describing this process.  */ 
       process_info = format_process_info (pid); 
       if (process_info == NULL) 
         /* Something went wrong. The process may have vanished while we 
            were looking at it. Use a placeholder row instead.  */ 
        process_info = "<tr><td colspan=\"5\">ERROR</td></tr>"; 
 
       /* Make sure the iovec array is long enough to hold this buffer 
          (plus one more because we'll add an extra element when we're done 
          listing processes). If not, grow it to twice its current size.  */ 
       if (vec_length == vec_size - 1) {
         vec_size *= 2; 
         vec = xrealloc (vec, vec_size * sizeof (struct iovec)); 
       } 
       /* Store this buffer as the next element of the array.  */ 
       vec[vec_length].iov_base = process_info; 
       vec[vec_length].iov_len = strlen (process_info); 
       ++vec_length; 
   } 
 
   /* End the directory listing operation.  */ 
   closedir (proc_listing); 
 
   /* Add one last buffer with HTML that ends the page.  */ 
   vec[vec_length].iov_base = page_end; 
   vec[vec_length].iov_len = strlen (page_end); 
   ++vec_length; 
 
   /* Output the entire page to the client file descriptor all at once.  */ 
   writev (fd, vec, vec_length); 
 
   /* Deallocate the buffers we created. The first and last are static 
      and should not be deallocated.  */ 
   for (i = 1; i < vec_length - 1; ++i) 
      free (vec[i].iov_base); 
   /* Deallocate the iovec array. */ 
   free (vec); 
} 

Gathering process data and formatting it as an HTML table is broken down into several simpler operations:

·         get_uid_gid extracts the IDs of the owning user and group of a process. To do this, the function invokes stat (see Section B.2, "stat," in Appendix B) on the process's subdirectory in /proc (see Section 7.2, "Process Entries," in Chapter 7). The user and group that own this directory are identical to the process's owning user and group.

·         get_user_name returns the username corresponding to a UID. This function simply calls the C library function getpwuid, which consults the system's /etc/passwd file and returns a copy of the result. get_group_name returns the group name corresponding to a GID. It uses the getgrgid call.

·         get_program_name returns the name of the program running in a specified process. This information is extracted from the stat entry in the process's directory under /proc (see Section 7.2, "Process Entries," in Chapter 7). We use this entry rather than examining the exe symbolic link (see Section 7.2.4, "Process Executable," in Chapter 7) or cmdline entry (see Section 7.2.2, "Process Argument List," in Chapter 7) because the latter two are inaccessible if the process running the server isn't owned by the same user as the process being examined. Also, reading from stat doesn't force Linux to page the process under examination back into memory, if it happens to be swapped out.

·         get_rss returns the resident set size of a process. This information is available as the second element in the contents of the process's statm entry (see Section 7.2.6, "Process Memory Statistics," in Chapter 7) in its /proc subdirectory.

·         format_process_info generates a string containing HTML elements for a single table row, representing a single process. After calling the functions listed previously to obtain this information, it allocates a buffer and generates HTML using snprintf.

·         module_generate generates the entire HTML page, including the table. The output consists of one string containing the start of the page and the table (in page_start), one string for each table row (generated by format_process_info), and one string containing the end of the table and the page (in page_end).

module_generate determines the PIDs of the processes running on the system by examining the contents of /proc. It obtains a listing of this directory using opendir and readdir (see Section B.6, "Reading Directory Contents," in Appendix B). It scans the contents, looking for entries whose names are composed entirely of digits; these are taken to be process entries.

Potentially a large number of strings must be written to the client socket—one each for the page start and end, plus one for each process. If we were to write each string to the client socket file descriptor with a separate call to write, this would generate unnecessary network traffic because each string may be sent in a separate network packet.

To optimize packing of data into packets, we use a single call to writev instead (see Section B.3, "Vector Reads and Writes," in Appendix B). To do this, we must construct an array of struct iovec objects, vec. However, because we do not know the number of processes beforehand, we must start with a small array and expand it as new processes are added. The variable vec_length contains the number of elements of vec that are used, while vec_size contains the allocated size of vec. When vec_length is about to exceed vec_size, we expand vec to twice its size by calling xrealloc. When we're done with the vector write, we must deallocate all of the dynamically allocated strings pointed to by vec, and then vec itself.