The source code for this chapter comprises five files, not including some of the common library routines we've used in earlier chapters:
ipp.h
Header file containing IPP definitions
print.h
Header containing common constants, data structure definitions, and utility routine declarations
util.c
Utility routines used by the two programs
print.c
The C source file for the command used to print a file
printd.c
The C source file for the printer spooling daemon
We will study each file in the order listed.We start with the ipp.h header file.1 #ifndef _IPP_H 2 #define _IPP_H 3 /* 4 * Defines parts of the IPP protocol between the scheduler 5 * and the printer. Based on RFC2911 and RFC2910. 6 */ 7 /* 8 * Status code classes. 9 */ 10 #define STATCLASS_OK(x) ((x) >= 0x0000 && (x) <= 0x00ff) 11 #define STATCLASS_INFO(x) ((x) >= 0x0100 && (x) <= 0x01ff) 12 #define STATCLASS_REDIR(x) ((x) >= 0x0200 && (x) <= 0x02ff) 13 #define STATCLASS_CLIERR(x)((x) >= 0x0400 && (x) <= 0x04ff) 14 #define STATCLASS_SRVERR(x)((x) >= 0x0500 && (x) <= 0x05ff) 15 /* 16 * Status codes. 17 */ 18 #define STAT_OK 0x0000 /* success */ 19 #define STAT_OK_ATTRIGN 0x0001 /* OK; some attrs ignored */ 20 #define STAT_OK_ATTRCON 0x0002 /* OK; some attrs conflicted */ 21 #define STAT_CLI_BADREQ 0x0400 /* invalid client request */ 22 #define STAT_CLI_FORBID 0x0401 /* request is forbidden */ 23 #define STAT_CLI_NOAUTH 0x0402 /* authentication required */ 24 #define STAT_CLI_NOPERM 0x0403 /* client not authorized */ 25 #define STAT_CLI_NOTPOS 0x0404 /* request not possible */ 26 #define STAT_CLI_TIMOUT 0x0405 /* client too slow */ 27 #define STAT_CLI_NOTFND 0x0406 /* no object found for URI */ 28 #define STAT_CLI_OBJGONE 0x0407 /* object no longer available */ 29 #define STAT_CLI_TOOBIG 0x0408 /* requested entity too big */ 30 #define STAT_CLI_TOOLNG 0x0409 /* attribute value too large */ 31 #define STAT_CLI_BADFMT 0x040a /* unsupported doc format */ 32 #define STAT_CLI_NOTSUP 0x040b /* attributes not supported */ 33 #define STAT_CLI_NOSCHM 0x040c /* URI scheme not supported */ 34 #define STAT_CLI_NOCHAR 0x040d /* charset not supported */ 35 #define STAT_CLI_ATTRCON 0x040e /* attributes conflicted */ 36 #define STAT_CLI_NOCOMP 0x040f /* compression not supported */ 37 #define STAT_CLI_COMPERR 0x0410 /* data can't be decompressed */ 38 #define STAT_CLI_FMTERR 0x0411 /* document format error */ 39 #define STAT_CLI_ACCERR 0x0412 /* error accessing data */
[114]
We start the ipp.h header with the standard #ifdef to prevent errors when it is included twice in the same file. Then we define the classes of IPP status codes (see Section 13 in RFC 2911).
[1539]
We define specific status codes based on RFC 2911. We don't use these codes in the program shown here; their use is left as an exercise (See Exercise 21.1).
We continue to define status codes. The ones in the range 0x500 to 0x5ff are server error codes. All codes are described in Sections 13.1.1 through 13.1.5 in RFC 2911.
[5068]
We define the various operation IDs next. There is one ID for each task defined by IPP (see Section 4.4.15 in RFC 2911). In our example, we will use only the print-job operation.
[6976]
The attribute tags delimit the attribute groups in the IPP request and response messages. The tag values are defined in Section 3.5.1 of RFC 2910.
The value tags indicate the format of individual attributes and parameters. They are defined in Section 3.5.2 of RFC 2910.
[100113]
We define the structure of an IPP header. Request messages start with the same header as response messages, except that the operation ID in the request is replaced by a status code in the response.
We end the header file with a #endif to match the #ifdef at the start of the file.
We include all header files that an application might need if it included this header. This makes it easy for applications to include print.h without having to track down all the header dependencies.
[1317]
We define the files and directories for the implementation. Copies of the files to be printed will be stored in the directory /var/spool/printer/data; control information for each request will be stored in the directory /var/spool/printer/reqs. The file containing the next job number is /var/spool/printer/jobno.
[1830]
Next, we define limits and constants. FILEPERM is the permissions used when creating copies of files submitted to be printed. The permissions are restrictive because we don't want ordinary users to be able to read one another's files while they are waiting to be printed. IPP is defined to use port 631. The QLEN is the backlog parameter we pass to listen (see Section 16.4 for details).
31 #ifndef ETIME 32 #define ETIME ETIMEDOUT 33 #endif 34 extern int getaddrlist(const char *, const char *, 35 struct addrinfo **); 36 extern char *get_printserver(void); 37 extern struct addrinfo *get_printaddr(void); 38 extern ssize_t tread(int, void *, size_t, unsigned int); 39 extern ssize_t treadn(int, void *, size_t, unsigned int); 40 extern int connect_retry(int, const struct sockaddr *, socklen_t); 41 extern int initserver(int, struct sockaddr *, socklen_t, int); 42 /* 43 * Structure describing a print request. 44 */ 45 struct printreq { 46 long size; /* size in bytes */ 47 long flags; /* see below */ 48 char usernm[USERNM_MAX]; /* user's name */ 49 char jobnm[JOBNM_MAX]; /* job's name */ 50 }; 51 /* 52 * Request flags. 53 */ 54 #define PR_TEXT 0x01 /* treat file as plain text */ 55 /* 56 * The response from the spooling daemon to the print command. 57 */ 58 struct printresp { 59 long retcode; /* 0=success, !0=error code */ 60 long jobid; /* job ID */ 61 char msg[MSGLEN_MAX]; /* error message */ 62 }; 63 #endif /* _PRINT_H */
The printreq and printresp structures define the protocol between the print command and the printer spooling daemon. The print command sends the printreq structure defining the user name, job name, and file size to the printer spooling daemon. The spooling daemon responds with a printresp structure consisting of a return code, a job ID, and an error message if the request failed.
The next file we will look at is util.c, the file containing utility routines.1 #include "apue.h" 2 #include "print.h" 3 #include <ctype.h> 4 #include <sys/select.h> 5 #define MAXCFGLINE 512 6 #define MAXKWLEN 16 7 #define MAXFMTLEN 16 8 /* 9 * Get the address list for the given host and service and 10 * return through ailistpp. Returns 0 on success or an error 11 * code on failure. Note that we do not set errno if we 12 * encounter an error. 13 * 14 * LOCKING: none. 15 */ 16 int 17 getaddrlist(const char *host, const char *service, 18 struct addrinfo **ailistpp) 19 { 20 int err; 21 struct addrinfo hint; 22 hint.ai_flags = AI_CANONNAME; 23 hint.ai_family = AF_INET; 24 hint.ai_socktype = SOCK_STREAM; 25 hint.ai_protocol = 0; 26 hint.ai_addrlen = 0; 27 hint.ai_canonname = NULL; 28 hint.ai_addr = NULL; 29 hint.ai_next = NULL; 30 err = getaddrinfo(host, service, &hint, ailistpp); 31 return(err); 32 }
[17]
We first define the limits needed by the functions in this file. MAXCFGLINE is the maximum size of a line in the printer configuration file, MAXKWLEN is the maximum size of a keyword in the configuration file, and MAXFMTLEN is the maximum size of the format string we pass to sscanf.
[832]
The first function is getaddrlist. It is a wrapper for getaddrinfo (Section 16.3.3), since we always call getaddrinfo with the same hint structure. Note that we need no mutex locking in this function. The LOCKING comment at the beginning of each function is intended only for documenting multithreaded locking. This comment lists the assumptions, if any, that are made regarding the locking, tells which locks the function might acquire or release, and tells which locks must be held to call the function.
33 /* 34 * Given a keyword, scan the configuration file for a match 35 * and return the string value corresponding to the keyword. 36 * 37 * LOCKING: none. 38 */ 39 static char * 40 scan_configfile(char *keyword) 41 { 42 int n, match; 43 FILE *fp; 44 char keybuf[MAXKWLEN], pattern[MAXFMTLEN]; 45 char line[MAXCFGLINE]; 46 static char valbuf[MAXCFGLINE]; 47 if ((fp = fopen(CONFIG_FILE, "r")) == NULL) 48 log_sys("can't open %s", CONFIG_FILE); 49 sprintf(pattern, "%%%ds %%%ds", MAXKWLEN-1, MAXCFGLINE-1); 50 match = 0; 51 while (fgets(line, MAXCFGLINE, fp) != NULL) { 52 n = sscanf(line, pattern, keybuf, valbuf); 53 if (n == 2 && strcmp(keyword, keybuf) == 0) { 54 match = 1; 55 break; 56 } 57 } 58 fclose(fp); 59 if (match != 0) 60 return(valbuf); 61 else 62 return(NULL); 63 }
[3346]
The scan_configfile function searches through the printer configuration file for the specified keyword.
[4763]
We open the configuration file for reading and build the format string corresponding to the search pattern. The notation %%%ds builds a format specifier that limits the string size so we don't overrun the buffers used to store the strings on the stack. We read the file one line at a time and scan for two strings separated by white space; if we find them, we compare the first string with the keyword. If we find a match or we reach the end of the file, the loop ends and we close the file. If the keyword matches, we return a pointer to the buffer containing the string after the keyword; otherwise, we return NULL.
The string returned is stored in a static buffer (valbuf), which can be overwritten on successive calls. Thus, scan_configfile can't be called by a multithreaded application unless we take care to avoid calling it from multiple threads at the same time.
64 /* 65 * Return the host name running the print server or NULL on error. 66 * 67 * LOCKING: none. 68 */ 69 char * 70 get_printserver(void) 71 { 72 return(scan_configfile("printserver")); 73 } 74 /* 75 * Return the address of the network printer or NULL on error. 76 * 77 * LOCKING: none. 78 */ 79 struct addrinfo * 80 get_printaddr(void) 81 { 82 int err; 83 char *p; 84 struct addrinfo *ailist; 85 if ((p = scan_configfile("printer")) != NULL) { 86 if ((err = getaddrlist(p, "ipp", &ailist)) != 0) { 87 log_msg("no address information for %s", p); 88 return(NULL); 89 } 90 return(ailist); 91 } 92 log_msg("no printer address specified"); 93 return(NULL); 94 }
[6473]
The get_printserver function is simply a wrapper function that calls scan_configfile to find the name of the computer system where the printer spooling daemon is running.
[7494]
We use the get_printaddr function to get the address of the network printer. It is similar to the previous function except that when we find the name of the printer in the configuration file, we use the name to find the corresponding network address.
Both get_printserver and get_printaddr call scan_configfile. If it can't open the printer configuration file, scan_configfile calls log_sys to print an error message and exit. Although get_printserver is meant to be called from a client command and get_printaddr is meant to be called from the daemon, having both call log_sys is OK, because we can arrange for the log functions to print to the standard error instead of to the log file by setting a global variable.
95 /* 96 * "Timed" read - timout specifies the # of seconds to wait before 97 * giving up (5th argument to select controls how long to wait for 98 * data to be readable). Returns # of bytes read or -1 on error. 99 * 100 * LOCKING: none. 101 */ 102 ssize_t 103 tread(int fd, void *buf, size_t nbytes, unsigned int timout) 104 { 105 int nfds; 106 fd_set readfds; 107 struct timeval tv; 108 tv.tv_sec = timout; 109 tv.tv_usec = 0; 110 FD_ZERO(&readfds); 111 FD_SET(fd, &readfds); 112 nfds = select(fd+1, &readfds, NULL, NULL, &tv); 113 if (nfds <= 0) { 114 if (nfds == 0) 115 errno = ETIME; 116 return(-1); 117 } 118 return(read(fd, buf, nbytes)); 119 }
[95107]
We provide a function called TRead to read a specified number of bytes, but block for at most timout seconds before giving up. This function is useful when reading from a socket or a pipe. If we don't receive data before the specified time limit, we return 1 with errno set to ETIME. If data is available within the time limit, we return at most nbytes bytes of data, but we can return less than requested if all the data doesn't arrive in time.
We will use TRead to prevent denial-of-service attacks on the printer spooling daemon. A malicious user might repeatedly try to connect to the daemon without sending it data, just to prevent other users from being able to submit print jobs. By giving up after a reasonable amount of time, we prevent this from happening. The tricky part is selecting a suitable timeout value that is large enough to prevent premature failures when the system is under load and tasks are taking longer to complete. If we choose a value too large, however, we might enable denial-of-service attacks by allowing the daemon to consume too many resources to process the pending requests.
[108119]
We use select to wait for the specified file descriptor to be readable. If the time limit expires before data is available to be read, select returns 0, so we set errno to ETIME in this case. If select fails or times out, we return 1. Otherwise, we return whatever data is available.
120 /* 121 * "Timed" read - timout specifies the number of seconds to wait 122 * per read call before giving up, but read exactly nbytes bytes. 123 * Returns number of bytes read or -1 on error. 124 * 125 * LOCKING: none. 126 */ 127 ssize_t 128 treadn(int fd, void *buf, size_t nbytes, unsigned int timout) 129 { 130 size_t nleft; 131 ssize_t nread; 132 nleft = nbytes; 133 while (nleft > 0) { 134 if ((nread = tread(fd, buf, nleft, timout)) < 0) { 135 if (nleft == nbytes) 136 return(-1); /* error, return -1 */ 137 else 138 break; /* error, return amount read so far */ 139 } else if (nread == 0) { 140 break; /* EOF */ 141 } 142 nleft -= nread; 143 buf += nread; 144 } 145 return(nbytes - nleft); /* return >= 0 */ 146 }
Section 14.8, but with the addition of the timeout parameter.
To read exactly nbytes bytes, we have to be prepared to make multiple calls to read. The difficult part is trying to apply a single timeout value to multiple calls to read. We don't want to use an alarm, because signals can be messy to deal with in multithreaded applications. We can't rely on the system updating the timeval structure on return from select to indicate the amount of time left, because many platforms do not support this (Section 14.5.1). Thus, we compromise and define the timeout value in this case to apply to an individual read call. Instead of limiting the total amount of time we wait, it limits the amount of time we'll wait in every iteration of the loop. The maximum time we can wait is bounded by (nbytes x timout ) seconds (worst case, we'll receive only 1 byte at a time).
We use nleft to record the number of bytes remaining to be read. If TRead fails and we have received data in a previous iteration, we break out of the while loop and return the number of bytes read; otherwise, we return 1.
The command used to submit a print job is shown next. The C source file is print.c.1 /* 2 * The client command for printing documents. Opens the file 3 * and sends it to the printer spooling daemon. Usage: 4 * print [-t] filename 5 */ 6 #include "apue.h" 7 #include "print.h" 8 #include <fcntl.h> 9 #include <pwd.h> 10 /* 11 * Needed for logging funtions. 12 */ 13 int log_to_stderr = 1; 14 void submit_file(int, int, const char *, size_t, int); 15 int 16 main(int argc, char *argv[]) 17 { 18 int fd, sockfd, err, text, c; 19 struct stat sbuf; 20 char *host; 21 struct addrinfo *ailist, *aip; 22 err = 0; 23 text = 0; 24 while ((c = getopt(argc, argv, "t")) != -1) { 25 switch (c) { 26 case 't': 27 text = 1; 28 break; 29 case '?': 30 err = 1; 31 break; 32 } 33 }
[114]
We need to define an integer called log_to_stderr to be able to use the log functions in our library. If set to a nonzero value, error messages will be sent to the standard error stream instead of to a log file. Although we don't use any logging functions in print.c, we do link util.o with print.o to build the executable print command, and util.c contains functions for both user commands and daemons.
[1533]
We support one option, -t, to force the file to be printed as text (instead of as a PostScript program, for example). We use the getopt(3) function to process the command options.
34 if (err || (optind != argc - 1)) 35 err_quit("usage: print [-t] filename"); 36 if ((fd = open(argv[optind], O_RDONLY)) < 0) 37 err_sys("print: can't open %s", argv[1]); 38 if (fstat(fd, &sbuf) < 0) 39 err_sys("print: can't stat %s", argv[1]); 40 if (!S_ISREG(sbuf.st_mode)) 41 err_quit("print: %s must be a regular file\n", argv[1]); 42 /* 43 * Get the hostname of the host acting as the print server. 44 */ 45 if ((host = get_printserver()) == NULL) 46 err_quit("print: no print server defined"); 47 if ((err = getaddrlist(host, "print", &ailist)) != 0) 48 err_quit("print: getaddrinfo error: %s", gai_strerror(err)); 49 for (aip = ailist; aip != NULL; aip = aip->ai_next) { 50 if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { 51 err = errno; 52 } else if (connect_retry(sockfd, aip->ai_addr, 53 aip->ai_addrlen) < 0) { 54 err = errno;
Section 16.3.4) and that our daemon will have to run with superuser privileges to allow it to bind to a reserved port.
[4954]
We try to connect to the daemon using one address at a time from the list returned by getaddrinfo. We will try to send the file to the daemon using the first address to which we can connect.
If we can make a connection, we call submit_file to transmit the file to the printer spooling daemon. If we can't connect to any of the addresses, we print an error message and exit. We use err_ret and exit instead of making a single call to err_sys to avoid a compiler warning, because the last line in main wouldn't be a return statement or a call to exit.
[6487]
submit_file sends a print request to the daemon and reads the response.First, we build the printreq request header. We use geteuid to get the caller's effective user ID and pass this to getpwuid to look for the user in the system's password file. We copy the user's name to the request header or use the string unknown if we can't identify the user. We store the size of the file to be printed in the header after converting it to network byte order. Then we do the same with the PR_TEXT flag if the file is to be printed as plaintext.
88 if ((len = strlen(fname)) >= JOBNM_MAX) { 89 /* 90 * Truncate the filename (+-5 accounts for the leading 91 * four characters and the terminating null). 92 */ 93 strcpy(req.jobnm, "... "); 94 strncat(req.jobnm, &fname[len-JOBNM_MAX+5], JOBNM_MAX-5); 95 } else { 96 strcpy(req.jobnm, fname); 97 } 98 /* 99 * Send the header to the server. 100 */ 101 nw = writen(sockfd, &req, sizeof(struct printreq)); 102 if (nw != sizeof(struct printreq)) { 103 if (nw < 0) 104 err_sys("can't write to print server"); 105 else 106 err_quit("short write (%d/%d) to print server", 107 nw, sizeof(struct printreq)); 108 } 109 /* 110 * Now send the file. 111 */ 112 while ((nr = read(fd, buf, IOBUFSZ)) != 0) { 113 nw = writen(sockfd, buf, nr); 114 if (nw != nr) { 115 if (nw < 0) 116 err_sys("can't write to print server"); 117 else 118 err_quit("short write (%d/%d) to print server", 119 nw, nr); 120 } 121 }
[88108]
We set the job name to the name of the file being printed. If the name is longer than will fit in the message, we truncate the beginning portion of the name and prepend an ellipsis to indicate that there were more characters than would fit in the field. Then we send the request header to the daemon using writen. If the write fails or if we transmit less than we expect, we print an error message and exit.
[109121]
After sending the header to the daemon, we send the file to be printed. We read the file IOBUFSZ bytes at a time and use writen to send the data to the daemon. As with the header, if the write fails or we write less than we expect, we print an error message and exit.
Chapter 19.It is important that all commands on a system follow the same conventions, because this makes them easier to use. If someone is familiar with the way command-line options are formed with one command, it would create more chances for mistakes if another command followed different conventions.This problem is sometimes visible when dealing with white space on the command line. Some commands require that an option be separated from its argument by white space, but other commands require the argument to follow immediate after its option, without any intervening spaces. Without a consistent set of rules to follow, users either have to memorize the syntax of all commands or resort to a trial-and-error process when invoking them.The Single UNIX Specification includes a set of conventions and guidelines that promote consistent command-line syntax. They include such suggestions as "Restrict each command-line option to a single alphanumeric character" and "All options should be preceded by a - character."Luckily, the getopt function exists to help command developers process command-line options in a consistent manner.
[View full width]#include <fcntl.h> int getopt(int argc , const * const argv[] , const char *options ); extern int optind, opterr, optopt; extern char *optarg;
Returns: the next option character, or 1 when all options have been processed
The argc and argv arguments are the same ones passed to the main function of the program. The options argument is a string containing the option characters supported by the command. If an option character is followed by a colon, then the option takes an argument. Otherwise, the option exists by itself. For example, if the usage statement for a command wascommand [-i] [-u username] [-z] filename we would pass "iu:z" as the options string to getopt.The normal use of getopt is in a loop that terminates when getopt returns 1. During each iteration of the loop, getopt will return the next option processed. It is up to the application to sort out any conflict in options, however; getopt simply parses the options and enforces a standard format.When it encounters an invalid option, getopt returns a question mark instead of the character. If an option's argument is missing, getopt will also return a question mark, but if the first character in the options string is a colon, getopt returns a colon instead. The special pattern -- will cause getopt to stop processing options and return 1. This allows users to provide command arguments that start with a minus sign but aren't options. For example, if you have a file named -bar, you can't remove it by typingrm -bar because rm will try to interpret -bar as options. The way to remove the file is to typerm -- -bar The getopt function supports four external variables.
optarg
If an option takes an argument, getopt sets optarg to point to the option's argument string when an option is processed.
opterr
If an option error is encountered, getopt will print an error message by default. To disable this behavior, applications can set opterr to 0.
optind
The index in the argv array of the next string to be processed. It starts out at 1 and is incremented for each argument processed by getopt.
optopt
If an error is encountered during options processing, getopt will set optopt to point to the option string that caused the error.
The last file we will look at is the C source file for the printer spooling daemon.1 /* 2 * Print server daemon. 3 */ 4 #include "apue.h" 5 #include "print.h" 6 #include "ipp.h" 7 #include <fcntl.h> 8 #include <dirent.h> 9 #include <ctype.h> 10 #include <pwd.h> 11 #include <pthread.h> 12 #include <strings.h> 13 #include <sys/select.h> 14 #include <sys/uio.h> 15 /* 16 * These are for the HTTP response from the printer. 17 */ 18 #define HTTP_INFO(x) ((x) >= 100 && (x) <= 199) 19 #define HTTP_SUCCESS(x) ((x) >= 200 && (x) <= 299) 20 /* 21 * Describes a print job. 22 */ 23 struct job { 24 struct job *next; /* next in list */ 25 struct job *prev; /* previous in list */ 26 long jobid; /* job ID */ 27 struct printreq req; /* copy of print request */ 28 }; 29 /* 30 * Describes a thread processing a client request. 31 */ 32 struct worker_thread { 33 struct worker_thread *next; /* next in list */ 34 struct worker_thread *prev; /* previous in list */ 35 pthread_t tid; /* thread ID */ 36 int sockfd; /* socket */ 37 };
[119]
The printer spooling daemon includes the IPP header file that we saw earlier, because the daemon needs to communicate with the printer using this protocol. The HTTP_INFO and HTTP_SUCCESS macros define the status of the HTTP request (recall that IPP is built on top of HTTP).
[2037]
The job and worker_thread structures are used by the spooling daemon to keep track of print jobs and threads accepting print requests, respectively.
Our logging functions require that we define the log_to_stderr variable and set it to 0 to force log messages to be sent to the system log instead of to the standard error. In print.c, we defined log_to_stderr and set it to 1, even though we don't use the log functions in the user command. We could have avoided this by splitting the utility functions into two separate files: one for the server and one for the client commands.
[4248]
We use the global variable printer to hold the network address of the printer.We store the host name of the printer in printer_name. The configlock mutex protects access to the reread variable, which is used to indicate that the daemon needs to reread the configuration file, presumably because an administrator changed the printer or its network address.
[4954]
Next, we define the thread-related variables. We use workers as the head of a doubly-linked list of threads that are receiving files from clients. This list is protected by the mutex workerlock. The signal mask used by the threads is held in the variable mask.
[5559]
For the list of pending jobs, we define jobhead to be the start of the list and jobtail to be the tail of the list. This list is also doubly linked, but we need to add jobs to the end of the list, so we need to remember a pointer to the list tail. With the list of worker threads, the order doesn't matter, so we can add them to the head of the list and don't need to remember the tail pointer. jobfd is the file descriptor for the job file.
nextjob is the ID of the next print job to be received. The joblock mutex protects the linked list of jobs, as well as the condition represented by the jobwait condition variable.
[6381]
We declare the function prototypes for the remaining functions in this file. Doing this up front allows us to place the functions in the file without worrying about the order in which each is called.
[8297]
The main function for the printer spooling daemon has two tasks to perform: initialize the daemon and then process connect requests from clients.
Figure 13.1 to become a daemon. After this point, we can't print error messages to standard error; we need to log them instead.
[101112]
We arrange to ignore SIGPIPE. We will be writing to socket file descriptors, and we don't want a write error to trigger SIGPIPE, because the default action is to kill the process. Next, we set the signal mask of the thread to include SIGHUP and SIGTERM. All threads we create will inherit this signal mask. We'll use SIGHUP to tell the daemon to reread the configuration file and SIGTERM to tell the daemon to clean up and exit gracefully. We call init_request to initialize the job requests and ensure that only one copy of the daemon is running, and we call init_printer to initialize the printer information (we'll see both of these functions shortly).
[113121]
If the platform defines the _SC_HOST_NAME_MAX symbol, we call sysconf to get the maximum size of a host name. If sysconf fails or the limit is undefined, we use HOST_NAME_MAX as a best guess. Sometimes, this is defined for us by the platform, but if it isn't, we chose our own value in print.h. We allocate memory to hold the host name and call gethostname to retrieve it.
122 if ((err = getaddrlist(host, "print", &ailist)) != 0) { 123 log_quit("getaddrinfo error: %s", gai_strerror(err)); 124 exit(1); 125 } 126 FD_ZERO(&rendezvous); 127 maxfd = -1; 128 for (aip = ailist; aip != NULL; aip = aip->ai_next) { 129 if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, 130 aip->ai_addrlen, QLEN)) >= 0) { 131 FD_SET(sockfd, &rendezvous); 132 if (sockfd > maxfd) 133 maxfd = sockfd; 134 } 135 } 136 if (maxfd == -1) 137 log_quit("service not enabled"); 138 pwdp = getpwnam("lp"); 139 if (pwdp == NULL) 140 log_sys("can't find user lp"); 141 if (pwdp->pw_uid == 0) 142 log_quit("user lp is privileged"); 143 if (setuid(pwdp->pw_uid) < 0) 144 log_sys("can't change IDs to user lp");
Figure 16.20) to allocate and initialize a socket. If initserver succeeds, we add the file descriptor to the fd_set; if it is greater than the maximum, we set maxfd equal to the socket file descriptor.
[136137]
If maxfd is still 1 after stepping through the list of addrinfo structures, we can't enable the printer spooling service, so we log a message and exit.
[138144]
Our daemon needs superuser privileges to bind a socket to a reserved port number. Now that this is done, we can lower its privileges by changing its user ID to the one associated with user lp (recall the security discussion in Section 21.4). We want to follow the principles of least privilege to avoid exposing the system to any potential vulnerabilities in the daemon. We call getpwnam to find the password entry associated with user lp. If no such user account exists, or if it exists with the same user ID as the superuser, we log a message and exit. Otherwise, we call setuid to change both the real and effective user IDs to the user ID for lp. To avoid exposing our system, we choose to provide no service at all if we can't reduce our privileges.
145 pthread_create(&tid, NULL, printer_thread, NULL); 146 pthread_create(&tid, NULL, signal_thread, NULL); 147 build_qonstart(); 148 log_msg("daemon initialized"); 149 for (;;) { 150 rset = rendezvous; 151 if (select(maxfd+1, &rset, NULL, NULL, NULL) < 0) 152 log_sys("select failed"); 153 for (i = 0; i <= maxfd; i++) { 154 if (FD_ISSET(i, &rset)) { 155 /* 156 * Accept the connection and handle 157 * the request. 158 */ 159 sockfd = accept(i, NULL, NULL); 160 if (sockfd < 0) 161 log_ret("accept failed"); 162 pthread_create(&tid, NULL, client_thread, 163 (void *)sockfd); 164 } 165 } 166 } 167 exit(1); 168 }
[145148]
We call pthread_create twice to create one thread to handle signals and one thread to communicate with the printer. (By restricting printer communication to one thread, we can simplify the locking of the printer-related data structures.) Then we call build_qonstart to search the directories in /var/spool/printer for any pending jobs. For each job that we find on disk, we will create a structure to let the printer thread know that it should send the file to the printer. At this point, we are done setting up the daemon, so we log a message to indicate that the daemon has initialized successfully.
[149168]
We copy the rendezvous fd_set structure to rset and call select to wait for one of the file descriptors to become readable. We have to copy rendezvous, because select will modify the fd_set structure that we pass to it to include only those file descriptors that satisfy the event. Since the sockets have been initialized for use by a server, a readable file descriptor means that a connect request is pending. After select returns, we check rset for a readable file descriptor. If we find one, we call accept to accept the connection. If this fails, we log a message and continue checking for more readable file descriptors. Otherwise, we create a thread to handle the client connection. The main thread loops, farming requests out to other threads for processing, and should never reach the exit statement.
169 /* 170 * Initialize the job ID file. Use a record lock to prevent 171 * more than one printer daemon from running at a time. 172 * 173 * LOCKING: none, except for record-lock on job ID file. 174 */ 175 void 176 init_request(void) 177 { 178 int n; 179 char name[FILENMSZ]; 180 sprintf(name, "%s/%s", SPOOLDIR, JOBFILE); 181 jobfd = open(name, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR); 182 if (write_lock(jobfd, 0, SEEK_SET, 0) < 0) 183 log_quit("daemon already running"); 184 /* 185 * Reuse the name buffer for the job counter. 186 */ 187 if ((n = read(jobfd, name, FILENMSZ)) < 0) 188 log_sys("can't read job file"); 189 if (n == 0) 190 nextjob = 1; 191 else 192 nextjob = atol(name); 193 }
The job file contains an ASCII integer string representing the next job number. If the file was just created and therefore is empty, we set nextjob to 1. Otherwise, we use atol to convert the string to an integer and use this as the next job number. We leave jobfd open to the job file so that we can update the job number as jobs are created. We can't close the file, because this would release the write lock that we've placed on it.
On a system where a long integer is 64 bits wide, we need a buffer at least 21 bytes in size to fit a string representing the largest possible long integer. We are safe reusing the filename buffer, because FILENMSZ is defined to be 64 in print.h.
The init_printer function is used to set the printer name and address. We get the printer address by calling get_printaddr (from util.c). If this fails, we log a message and exit. We can't do this by calling log_sys, because get_printaddr can fail without setting errno. When it fails and does set errno, however, get_printaddr logs its own error message. We set the printer name to the ai_canonname field in the addrinfo structure. If this field is null, we set the printer name to a default value of printer. Note that we log the name of the printer we are using to aid administrators in diagnosing problems with the spooling system.
[212225]
The update_jobno function is used to write the next job number to the job file, /var/spool/printer/jobno. First, we seek to the beginning of the file. Then we convert the integer job number into a string and write it to the file. If the write fails, we log an error message and exit.
226 /* 227 * Get the next job number. 228 * 229 * LOCKING: acquires and releases joblock. 230 */ 231 long 232 get_newjobno(void) 233 { 234 long jobid; 235 pthread_mutex_lock(&joblock); 236 jobid = nextjob++; 237 if (nextjob <= 0) 238 nextjob = 1; 239 pthread_mutex_unlock(&joblock); 240 return(jobid); 241 } 242 /* 243 * Add a new job to the list of pending jobs. Then signal 244 * the printer thread that a job is pending. 245 * 246 * LOCKING: acquires and releases joblock. 247 */ 248 void 249 add_job(struct printreq *reqp, long jobid) 250 { 251 struct job *jp; 252 if ((jp = malloc(sizeof(struct job))) == NULL) 253 log_sys("malloc failed"); 254 memcpy(&jp->req, reqp, sizeof(struct printreq));
Figure 11.9 to see what could happen if we don't serialize the threads in this case.)
[242254]
The add_job function is used to add a new print request to the end of the list of pending print jobs. We start by allocating space for the job structure. If this fails, we log a message and exit. At this point, the print request is stored safely on disk; when the printer spooling daemon is restarted, it will pick the request up. After we allocate memory for the new job, we copy the request structure from the client into the job structure. Recall from print.h that a job structure consists of a pair of list pointers, a job ID, and a copy of the printreq structure sent to us by the client print command.
We save the job ID and lock the joblock mutex to gain exclusive access to the linked list of print jobs. We are about to add the new job structure to the end of the list. We set the new structure's previous pointer to the last job on the list. If the list is empty, we set jobhead to point to the new structure. Otherwise, we set the next pointer in the last entry on the list to point to the new structure. Then we set jobtail to point to the new structure. We unlock the mutex and signal the printer thread that another job is available.
[267284]
The replace_job function is used to insert a job at the head of the pending job list. We acquire the joblock mutex, set the previous pointer in the job structure to null, and set the next pointer in the job structure to point to the head of the list. If the list is empty, we set jobtail to point to the job structure we are replacing. Otherwise, we set the previous pointer in the first job structure on the list to point to the job structure we are replacing. Then we set the jobhead pointer to the job structure we are replacing. Finally, we release the joblock mutex.
285 /* 286 * Remove a job from the list of pending jobs. 287 * 288 * LOCKING: caller must hold joblock. 289 */ 290 void 291 remove_job(struct job *target) 292 { 293 if (target->next != NULL) 294 target->next->prev = target->prev; 295 else 296 jobtail = target->prev; 297 if (target->prev != NULL) 298 target->prev->next = target->next; 299 else 300 jobhead = target->next; 301 } 302 /* 303 * Check the spool directory for pending jobs on start-up. 304 * 305 * LOCKING: none. 306 */ 307 void 308 build_qonstart(void) 309 { 310 int fd, err, nr; 311 long jobid; 312 DIR *dirp; 313 struct dirent *entp; 314 struct printreq req; 315 char dname[FILENMSZ], fname[FILENMSZ]; 316 sprintf(dname, "%s/%s", SPOOLDIR, REQDIR); 317 if ((dirp = opendir(dname)) == NULL) 318 return;
[285301]
remove_job removes a job from the list of pending jobs given a pointer to the job to be removed. The caller must already hold the joblock mutex. If the next pointer is non-null, we set the next entry's previous pointer to the target's previous pointer. Otherwise, the entry is the last one on the list, so we set jobtail to the target's previous pointer. If the target's previous pointer is non-null, we set the previous entry's next pointer equal to the target's next pointer. Otherwise, this is the first entry in the list, so we set jobhead to point to the next entry in the list after the target.
[302318]
When the daemon starts, it calls build_qonstart to build an in-memory list of print jobs from the disk files stored in /var/spool/printer/reqs. If we can't open the directory, no print jobs are pending, so we return.
We read each entry in the directory, one at a time. We skip the entries for dot and dot-dot.
[326346]
For each entry, we create the full pathname of the file and open it for reading. If the open call fails, we just skip the file. Otherwise, we read the printreq structure stored in it. If we don't read the entire structure, we close the file, log a message, and unlink the file. Then we create the full pathname of the corresponding data file and unlink it, too.
[347352]
If we were able to read a complete printreq structure, we convert the filename into a job ID (the name of the file is its job ID), log a message, and then add the request to the list of pending print jobs. When we are done reading the directory, readdir will return NULL, and we close the directory and return.
Section 11.5 for a discussion of thread cleanup handlers). The cleanup handler is client_cleanup, which we will see later. It takes a single argument: our thread ID. Then we call add_worker to create a worker_thread structure and add it to the list of active client threads.
[372385]
At this point, we are done with the thread's initialization tasks, so we read the request header from the client. If the client sends less than we expect or we encounter an error, we respond with a message indicating the reason for the error and call pthread_exit to terminate the thread.
386 req.size = ntohl(req.size); 387 req.flags = ntohl(req.flags); 388 /* 389 * Create the data file. 390 */ 391 jobid = get_newjobno(); 392 sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jobid); 393 if ((fd = creat(name, FILEPERM)) < 0) { 394 res.jobid = 0; 395 if (n < 0) 396 res.retcode = htonl(errno); 397 else 398 res.retcode = htonl(EIO); 399 log_msg("client_thread: can't create %s: %s", name, 400 strerror(res.retcode)); 401 strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX); 402 writen(sockfd, &res, sizeof(struct printresp)); 403 pthread_exit((void *)1); 404 } 405 /* 406 * Read the file and store it in the spool directory. 407 */ 408 first = 1; 409 while ((nr = tread(sockfd, buf, IOBUFSZ, 20)) > 0) { 410 if (first) { 411 first = 0; 412 if (strncmp(buf, "%!PS", 4) != 0) 413 req.flags |= PR_TEXT; 414 }
We write the data that we read from the client to the data file. If write fails, we log an error message, close the file descriptor for the data file, send an error message back to the client, delete the data file, and terminate the thread by calling pthread_exit. Note that we do not explicitly close the socket file descriptor. This is done for us by our thread cleanup handler as part of the processing that occurs when we call pthread_exit.
When we receive all the data to be printed, we close the file descriptor for the data file.
[431449]
Next, we create a file, /var/spool/printer/reqs/jobid , to remember the print request. If this fails, we log an error message, send an error response to the client, remove the data file, and terminate the thread.
We write the printreq structure to the control file. On error, we log a message, close the descriptor for the control file, send a failure response back to the client, remove the data and control files, and terminate the thread.
[467474]
We close the file descriptor for the control file and send a message containing the job ID and a successful status (retcode set to 0) back to the client.
[475482]
We call add_job to add the received job to the list of pending print jobs and call pthread_cleanup_pop to complete the cleanup processing. The thread terminates when we return.
Note that before the thread exits, we must close any file descriptors we no longer need. Unlike process termination, file descriptors are not closed automatically when a thread ends if other threads exist in the process. If we didn't close unneeded file descriptors, we'd eventually run out of resources.
add_worker adds a worker_thread structure to the list of active threads. We allocate memory for the structure, initialize it, lock the workerlock mutex, add the structure to the head of the list, and unlock the mutex.
[507520]
The kill_workers function walks the list of worker threads and cancels each one. We hold the workerlock mutex while we walk the list. Recall that pthread_cancel merely schedules a thread for cancellation; actual cancellation happens when each thread reaches the next cancellation point.
The client_cleanup function is the thread cleanup handler for the worker threads that communicate with client commands. This function is called when the thread calls pthread_exit, calls pthread_cleanup_pop with a nonzero argument, or responds to a cancellation request. The argument is the thread ID of the thread terminating.
We lock the workerlock mutex and search the list of worker threads until we find a matching thread ID. When we find a match, we remove the worker thread structure from the list and stop the search.
[544549]
We unlock the workerlock mutex, close the socket file descriptor used by the thread to communicate with the client, and free the memory backing the worker_thread structure.
Since we try to acquire the workerlock mutex, if a thread reaches a cancellation point while the kill_workers function is still walking the list, we will have to wait until kill_workers releases the mutex before we can proceed.
550 /* 551 * Deal with signals. 552 * 553 * LOCKING: acquires and releases configlock. 554 */ 555 void * 556 signal_thread(void *arg) 557 { 558 int err, signo; 559 for (;;) { 560 err = sigwait(&mask, &signo); 561 if (err != 0) 562 log_quit("sigwait failed: %s", strerror(err)); 563 switch (signo) { 564 case SIGHUP: 565 /* 566 * Schedule to re-read the configuration file. 567 */ 568 pthread_mutex_lock(&configlock); 569 reread = 1; 570 pthread_mutex_unlock(&configlock); 571 break; 572 case SIGTERM: 573 kill_workers(); 574 log_msg("terminate with signal %s", strsignal(signo)); 575 exit(0); 576 default: 577 kill_workers(); 578 log_quit("unexpected signal %d", signo); 579 } 580 } 581 }
[550563]
The signal_thread function is run by the thread that is responsible for handling signals. In the main function, we initialized the signal mask to include SIGHUP and SIGTERM. Here, we call sigwait to wait for one of these signals to occur. If sigwait fails, we log an error message and exit.
[564571]
If we receive SIGHUP, we acquire the configlock mutex, set the reread variable to 1, and release the mutex. This tells the printer daemon to reread the configuration file on the next iteration in its processing loop.
[572575]
If we receive SIGTERM, we call kill_workers to kill all the worker threads, log a message, and call exit to terminate the process.
[576581]
If we receive a signal we are not expecting, we kill the worker threads and call log_quit to log a message and exit.
Figure 21.3 that the format of an attribute is a 1-byte tag describing the type of the attribute, followed by the length of the attribute name stored in binary as a 2-byte integer, followed by the name, the size of the attribute value, and finally the value itself.
IPP makes no attempt to control the alignment of the binary integers embedded in the header. Some processor architectures, such as the SPARC, can't load an integer from an arbitrary address. This means that we can't store the integers in the header by casting a pointer to an int16_t to the address in the header where the integer is to be stored. Instead, we need to copy the integer 1 byte at a time. This is why we define the union containing a 16-bit integer and 2 bytes.
[595608]
We store the tag in the header and convert the length of the attribute name to network byte order. We copy the length 1 byte at a time to the header. Then we copy the attribute name. We repeat this process for the attribute value and return the address in the header where the next part of the header should begin.
609 /* 610 * Single thread to communicate with the printer. 611 * 612 * LOCKING: acquires and releases joblock and configlock. 613 */ 614 void * 615 printer_thread(void *arg) 616 { 617 struct job *jp; 618 int hlen, ilen, sockfd, fd, nr, nw; 619 char *icp, *hcp; 620 struct ipp_hdr *hp; 621 struct stat sbuf; 622 struct iovec iov[2]; 623 char name[FILENMSZ]; 624 char hbuf[HBUFSZ]; 625 char ibuf[IBUFSZ]; 626 char buf[IOBUFSZ]; 627 char str[64]; 628 for (;;) { 629 /* 630 * Get a job to print. 631 */ 632 pthread_mutex_lock(&joblock); 633 while (jobhead == NULL) { 634 log_msg("printer_thread: waiting..."); 635 pthread_cond_wait(&jobwait, &joblock); 636 } 637 remove_job(jp = jobhead); 638 log_msg("printer_thread: picked up job %ld", jp->jobid); 639 pthread_mutex_unlock(&joblock); 640 update_jobno();
[609627]
The printer_thread function is run by the thread that communicates with the network printer. We'll use icp and ibuf to build the IPP header. We'll use hcp and hbuf to build the HTTP header. We need to build the headers in separate buffers. The HTTP header includes a length field in ASCII, and we won't know how much space to reserve for it until we assemble the IPP header. We'll use writev to write these two headers in one call.
[628640]
The printer thread runs in an infinite loop that waits for jobs to transmit to the printer. We use the joblock mutex to protect the list of jobs. If a job is not pending, we use pthread_cond_wait to wait for one to arrive. When a job is ready, we remove it from the list by calling remove_job. We still hold the mutex at this point, so we release it and call update_jobno to write the next job number to /var/spool/printer/jobno.
If we can't open the data file, we log a message, free the job structure, and continue. After opening the file, we call fstat to find the size of the file. If this fails, we log a message, clean up, and continue.
Figure 21.4 lists the required and optional attributes for print-job requests. The first three are required. We specify the character set to be UTF-8, which the printer must support. We specify the language as en-us, which represents U.S. English. Another required attribute is the printer Universal Resource Identifier (URI). We set it to http://printer_name :631. (We really should ask the printer for a list of supported URIs and select one from that list, but that would complicate this example without adding much value.)
The requesting-user-name attribute is recommended, but not required. The job-name attribute is optional. Recall that the print command sends the name of the file being printed as the job name, which can help users distinguish among multiple pending jobs. The last attribute we supply is the document-format. If we omit it, the printer will assume that the file conforms to the printer's default format. For a PostScript printer, this is probably PostScript, but some printers can autosense the format and choose between PostScript and text or PostScript and PCL (HP's Printer Command Language). If the PR_TEXT flag is set, we specify the document format as text/plain. Otherwise, we set it to application/postscript. Then we delimit the end of the attributes portion of the header with an end-of-attributes tag and calculate the size of the IPP header.
[713728]
Now that we know the IPP header size, we can set up the HTTP header. We set the Context-Length to the size in bytes of the IPP header plus the size of the file to be printed. The Content-Type is application/ipp. We mark the end of the HTTP header with a carriage return and a line feed.
729 /* 730 * Write the headers first. Then send the file. 731 */ 732 iov[0].iov_base = hbuf; 733 iov[0].iov_len = hlen; 734 iov[1].iov_base = ibuf; 735 iov[1].iov_len = ilen; 736 if ((nw = writev(sockfd, iov, 2)) != hlen + ilen) { 737 log_ret("can't write to printer"); 738 goto defer; 739 } 740 while ((nr = read(fd, buf, IOBUFSZ)) > 0) { 741 if ((nw = write(sockfd, buf, nr)) != nr) { 742 if (nw < 0) 743 log_ret("can't write to printer"); 744 else 745 log_msg("short write (%d/%d) to printer", nw, nr); 746 goto defer; 747 } 748 } 749 if (nr < 0) { 750 log_ret("can't read %s", name); 751 goto defer; 752 } 753 /* 754 * Read the response from the printer. 755 */ 756 if (printer_status(sockfd, jp)) { 757 unlink(name); 758 sprintf(name, "%s/%s/%ld", SPOOLDIR, REQDIR, jp->jobid); 759 unlink(name); 760 free(jp); 761 jp = NULL; 762 }
[729739]
We set the first element of the iovec array to refer to the HTTP header and the second element to refer to the IPP header. Then we use writev to send both headers to the printer. If the write fails, we log a message and jump to defer, where we will clean up and delay before trying again.
[740752]
Next, we send the data file to the printer. We read the data file in IOBUFSZ chunks and write it to the socket connected to the printer. If either read or write fails, we log a message and jump to defer.
[753762]
After sending the entire file to be printed, we call printer_status to receive the printer's response to our print request. If printer_status succeeds, it returns a positive value, and we delete the data and control files. Then we free the job structure, set its pointer to NULL, and fall through to the defer label.
763 defer: 764 close(fd); 765 if (sockfd >= 0) 766 close(sockfd); 767 if (jp != NULL) { 768 replace_job(jp); 769 sleep(60); 770 } 771 } 772 } 773 /* 774 * Read data from the printer, possibly increasing the buffer. 775 * Returns offset of end of data in buffer or -1 on failure. 776 * 777 * LOCKING: none. 778 */ 779 ssize_t 780 readmore(int sockfd, char **bpp, int off, int *bszp) 781 { 782 ssize_t nr; 783 char *bp = *bpp; 784 int bsz = *bszp; 785 if (off >= bsz) { 786 bsz += IOBUFSZ; 787 if ((bp = realloc(*bpp, bsz)) == NULL) 788 log_sys("readmore: can't allocate bigger read buffer"); 789 *bszp = bsz; 790 *bpp = bp; 791 } 792 if ((nr = tread(sockfd, &bp[off], bsz-off, 1)) > 0) 793 return(off+nr); 794 else 795 return(-1); 796 }
[763772]
At the defer label, we close the file descriptor for the open data file. If the socket descriptor is valid, we close it. On error, we place the job back on the head of the pending job list and delay for 1 minute. On success, jp is NULL, so we simply go back to the top of the loop to get the next job to print.
[773796]
The readmore function is used to read part of the response message from the printer. If we're at the end of the buffer, we reallocate a bigger buffer and return the new starting buffer address and buffer size through the bpp and bszp parameters, respectively. In either case, we read as much as the buffer will hold, starting at the end of the data already in the buffer. We return the new offset in the buffer corresponding to the end of the data read. If the read fails or the timeout expires, we return 1.
797 /* 798 * Read and parse the response from the printer. Return 1 799 * if the request was successful, and 0 otherwise. 800 * 801 * LOCKING: none. 802 */ 803 int 804 printer_status(int sockfd, struct job *jp) 805 { 806 int i, success, code, len, found, bufsz; 807 long jobid; 808 ssize_t nr; 809 char *statcode, *reason, *cp, *contentlen; 810 struct ipp_hdr *hp; 811 char *bp; 812 /* 813 * Read the HTTP header followed by the IPP response header. 814 * They can be returned in multiple read attempts. Use the 815 * Content-Length specifier to determine how much to read. 816 */ 817 success = 0; 818 bufsz = IOBUFSZ; 819 if ((bp = malloc(IOBUFSZ)) == NULL) 820 log_sys("printer_status: can't allocate read buffer"); 821 while ((nr = tread(sockfd, bp, IOBUFSZ, 5)) > 0) { 822 /* 823 * Find the status. Response starts with "HTTP/x.y" 824 * so we can skip the first 8 characters. 825 */ 826 cp = bp + 8; 827 while (isspace((int)*cp)) 828 cp++; 829 statcode = cp; 830 while (isdigit((int)*cp)) 831 cp++; 832 if (cp == statcode) { /* Bad format; log it and move on */ 833 log_msg(bp);
[797811]
The printer_status function reads the printer's response to a print-job request. We don't know how the printer will respond; it might send a response in multiple messages, send the complete response in one message, or include intermediate acknowledgements, such as HTTP 100 Continue messages. We need to handle all these possibilities.
[812833]
We allocate a buffer and read from the printer, expecting a response to be available within about 5 seconds. We skip the HTTP/1.1 and any white space that starts the message. The numeric status code should follow. If it doesn't, we log the contents of the message.
834 } else { 835 *cp++ = '\0'; 836 reason = cp; 837 while (*cp != '\r' && *cp != '\n') 838 cp++; 839 *cp = '\0'; 840 code = atoi(statcode); 841 if (HTTP_INFO(code)) 842 continue; 843 if (!HTTP_SUCCESS(code)) { /* probable error: log it */ 844 bp[nr] = '\0'; 845 log_msg("error: %s", reason); 846 break; 847 } 848 /* 849 * The HTTP request was okay, but we still 850 * need to check the IPP status. First 851 * search for the Content-Length specifier. 852 */ 853 i = cp - bp; 854 for (;;) { 855 while (*cp != 'C' && *cp != 'c' && i < nr) { 856 cp++; 857 i++; 858 } 859 if (i >= nr && /* get more header */ 860 ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0)) 861 goto out; 862 cp = &bp[i];
[834839]
If we have found a numeric status code in the response, we convert the first nondigit character to a null byte. The reason string (a text message) should follow. We search for the terminating carriage return or line feed, also terminating the text string with a null byte.
[840847]
We convert the code to an integer. If this is an informational message only, we ignore it and continue the loop so we end up reading more. We expect to see either a success message or an error message. If we get an error message, we log the error and break out of the loop.
[848862]
If the HTTP request was successful, we need to check the IPP status. We search through the message until we find the Content-Length attribute, so we look for a C or c. HTTP header keywords are case-insensitive, so we need to check both lowercase and uppercase characters.
If we run out of buffer space, we read some more. Since readmore calls realloc, which might change the address of the buffer, we need to reset cp to point to the correct place in the buffer.
863 if (strncasecmp(cp, "Content-Length:", 15) == 0) { 864 cp += 15; 865 while (isspace((int)*cp)) 866 cp++; 867 contentlen = cp; 868 while (isdigit((int)*cp)) 869 cp++; 870 *cp++ = '\0'; 871 i = cp - bp; 872 len = atoi(contentlen); 873 break; 874 } else { 875 cp++; 876 i++; 877 } 878 } 879 if (i >= nr && /* get more header */ 880 ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0)) 881 goto out; 882 cp = &bp[i]; 883 found = 0; 884 while (!found) { /* look for end of HTTP header */ 885 while (i < nr - 2) { 886 if (*cp == '\n' && *(cp + 1) == '\r' && 887 *(cp + 2) == '\n') { 888 found = 1; 889 cp += 3; 890 i += 3; 891 break; 892 } 893 cp++; 894 i++; 895 } 896 if (i >= nr && /* get more header */ 897 ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0)) 898 goto out; 899 cp = &bp[i]; 900 }
[863882]
If we find the Content-Length attribute string, we search for its value. We convert this numeric string into an integer, break out of the for loop, and read more from the printer if we've exhausted the contents of the buffer. If we reach the end of the buffer without finding the Content-Length attribute, we continue in the loop and read some more from the printer.
[883900]
Once we get the length of the message as specified by the Content-Length attribute, we search for the end of the HTTP header (a blank line). If we find it, we set the found flag and skip past the blank line in the message.
901 if (nr - i < len && /* get more header */ 902 ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0)) 903 goto out; 904 cp = &bp[i]; 905 hp = (struct ipp_hdr *)cp; 906 i = ntohs(hp->status); 907 jobid = ntohl(hp->request_id); 908 if (jobid != jp->jobid) { 909 /* 910 * Different jobs. Ignore it. 911 */ 912 log_msg("jobid %ld status code %d", jobid, i); 913 break; 914 } 915 if (STATCLASS_OK(i)) 916 success = 1; 917 break; 918 } 919 } 920 out: 921 free(bp); 922 if (nr < 0) { 923 log_msg("jobid %ld: error reading printer response: %s", 924 jobid, strerror(errno)); 925 } 926 return(success); 927 }
[901904]
We continue searching for the end of the HTTP header. If we run out of space in the buffer, we read more. When we find the end of the HTTP header, we calculate the number of bytes that the HTTP header consumed. If the amount we've read minus the size of the HTTP header is not equal to the amount of data in the IPP message (the value we calculated from the content length), then we read some more.
[905927]
We get the status and job ID from the IPP header in the message. Both are stored as integers in network byte order, so we need to convert them to the host byte order by calling ntohs and ntohl. If the job IDs don't match, then this is not our response, so we log a message and break out of the outer while loop. If the IPP status indicates success, then we save the return value and break out of the loop. We return 1 if the print request was successful and 0 if it failed.
This concludes our look at the extended example in this chapter. The programs in this chapter were tested with a Xerox Phaser 860 network-attached PostScript printer. Unfortunately, this printer doesn't recognize the text/plain document format, but it does support the ability to autosense between plaintext and PostScript. Therefore, with this printer, we can print PostScript files and text files, but we cannot print the source to a PostScript program as plaintext unless we use some other utility, such as a2ps(1) to encapsulate the PostScript program.