|
| 1 | +#!/usr/bin/env python |
| 2 | +# |
| 3 | +# Install this script as `/usr/bin/lp', renaming the old lp |
| 4 | +# command. Make sure printserver.py is running on your CUPS |
| 5 | +# server and ready to accept data. |
| 6 | +# |
| 7 | +# You need Python (1.3.x) on your SCO box to run this. |
| 8 | +# |
| 9 | +# |
| 10 | +# This spooler uses the following very simple network protocol: |
| 11 | +# First, a handshake string is sent. This is a fixed-size message |
| 12 | +# of the form: |
| 13 | +# |
| 14 | +# SPOOL queuename job-bytes |
| 15 | +# |
| 16 | +# "SPOOL" is a literal five byte string. |
| 17 | +# |
| 18 | +# queuename is a string exactly thirty bytes long that encodes the |
| 19 | +# name of the print queue, with a null byte terminating the name. |
| 20 | +# The remainder of the string is padding and is ignored. |
| 21 | +# |
| 22 | +# job-bytes is a string exactly 10 bytes long. It is a number in ASCII |
| 23 | +# representation, padded by spaces with no sign, that indicates the size |
| 24 | +# of the file that will be sent by the client. |
| 25 | +# |
| 26 | +# Total message size is 45 bytes. There is no terminator, and no |
| 27 | +# separator between fields. |
| 28 | +# |
| 29 | +# |
| 30 | +# |
| 31 | +# The server replies to this message with another fixed size message, |
| 32 | +# simply the five-byte string: |
| 33 | +# |
| 34 | +# READY |
| 35 | +# |
| 36 | +# without any terminator. |
| 37 | +# |
| 38 | +# |
| 39 | +# |
| 40 | +# The client proceeds to send the print job data. When it's sent the last |
| 41 | +# byte it waits for a reply from the server. |
| 42 | +# |
| 43 | +# |
| 44 | +# The server reads client data until the last byte is received. It spools |
| 45 | +# it, then informs the client of the result with a simple fixed-length |
| 46 | +# 7 byte message, either: |
| 47 | +# |
| 48 | +# SUCCESS |
| 49 | +# |
| 50 | +# or |
| 51 | +# |
| 52 | +# FAILURE |
| 53 | +# |
| 54 | +# (unterminated strings). No additional error info is provided; that'll be |
| 55 | +# logged server side. |
| 56 | +# |
| 57 | +# After sending status, the server closes its socket. The client closes |
| 58 | +# its socket when it receives the ack message. |
| 59 | +# |
| 60 | + |
| 61 | +# |
| 62 | +import os |
| 63 | +import sys |
| 64 | +import socket |
| 65 | +import threading |
| 66 | +import tempfile |
| 67 | +import syslog |
| 68 | + |
| 69 | +printer_map = { |
| 70 | + 'hp' : 'iprint', |
| 71 | + 'P1' : 'iprint', |
| 72 | + 'P2' : 'accounts' |
| 73 | +} |
| 74 | + |
| 75 | +listen_host = "10.0.0.10" |
| 76 | +spoolport = 6668 |
| 77 | + |
| 78 | +class ClientThread(threading.Thread): |
| 79 | + |
| 80 | + def __init__(self, sock_info): |
| 81 | + threading.Thread.__init__(self) |
| 82 | + (self.sock, self.client_address) = sock_info |
| 83 | + |
| 84 | + def _log(self, prio, msg): |
| 85 | + syslog.syslog(syslog.LOG_DAEMON|prio, "exprintserver (%s:%s): " % (self.client_address) + msg) |
| 86 | + |
| 87 | + def run(self): |
| 88 | + try: |
| 89 | + self._log(syslog.LOG_DEBUG, "connect") |
| 90 | + # A 30-second timeout is set on socket I/O operations |
| 91 | + # so that protocol issues or unexpected remote end death |
| 92 | + # don't cause our threads to hang around forever. |
| 93 | + self.sock.settimeout(30) |
| 94 | + self.expect_handshake() |
| 95 | + self.send_handshake_reply() |
| 96 | + self.read_job_data() |
| 97 | + if self._should_ignore_job(): |
| 98 | + status_ok = True |
| 99 | + else: |
| 100 | + status_ok = self.spool_job() |
| 101 | + self.send_job_ack(status_ok) |
| 102 | + self.shutdown_connection() |
| 103 | + self._log(syslog.LOG_DEBUG, "disconnect") |
| 104 | + except Exception, e: |
| 105 | + self._log(syslog.LOG_ERR, "exception during client communication: " + str(e)) |
| 106 | + |
| 107 | + def _recv_fixed_msg(self,length_bytes): |
| 108 | + bytes_remaining = length_bytes |
| 109 | + msg = "" |
| 110 | + while bytes_remaining > 0: |
| 111 | + frag = self.sock.recv(bytes_remaining) |
| 112 | + if (frag == ""): |
| 113 | + raise Exception("Unexpected EOF in fixed msg recv") |
| 114 | + bytes_remaining = bytes_remaining - len(frag) |
| 115 | + msg = msg + frag |
| 116 | + return msg |
| 117 | + |
| 118 | + def _send_fixed_msg(self,msg): |
| 119 | + totalsent = 0 |
| 120 | + msglen = len(msg) |
| 121 | + while totalsent < msglen: |
| 122 | + sent = self.sock.send(msg[totalsent:]) |
| 123 | + if sent == 0: |
| 124 | + raise Exception("Unexpected EOF in fixed msg send") |
| 125 | + totalsent = totalsent + sent |
| 126 | + |
| 127 | + def expect_handshake(self): |
| 128 | + msg = self._recv_fixed_msg(45) |
| 129 | + if not msg.startswith("SPOOL"): |
| 130 | + raise Exception("Invalid handshake") |
| 131 | + self.queuename = msg[5:35].strip() |
| 132 | + jobsize_str = msg[35:45].strip() |
| 133 | + self.jobsize = int(jobsize_str) |
| 134 | + if self.jobsize == 0: |
| 135 | + raise Exception("Zero sized job proposed") |
| 136 | + |
| 137 | + def send_handshake_reply(self): |
| 138 | + self._send_fixed_msg("READY") |
| 139 | + |
| 140 | + def read_job_data(self): |
| 141 | + # spool the fucker into RAM for now |
| 142 | + jobdata = self._recv_fixed_msg(self.jobsize) |
| 143 | + self.f = tempfile.NamedTemporaryFile() |
| 144 | + self.f.write(jobdata) |
| 145 | + self.jobfilename = self.f.name |
| 146 | + self.f.flush() |
| 147 | + |
| 148 | + def _should_ignore_job(self): |
| 149 | + # Return true if this job should be discarded unprinted. |
| 150 | + # this lets us filter jobs from the system based on their contents. |
| 151 | + self.f.seek(0) |
| 152 | + if self.f.read().find("Cannot get TTY") != -1: |
| 153 | + # Ignore "Cannot get TTY" error messages instead of printing them. |
| 154 | + self._log(syslog.LOG_INFO, "ignoring job '%s' to '%s' - Cannot get TTY error" % (self.jobfilename, self.queuename)) |
| 155 | + return True |
| 156 | + return False |
| 157 | + |
| 158 | + def spool_job(self): |
| 159 | + """Note: this function may be skipped by the server, so it must not be involved |
| 160 | + in any I/O with the client lest the client get confused if it's skipped.""" |
| 161 | + print "before", self.queuename |
| 162 | + self.queuename = printer_map.get(self.queuename, self.queuename) |
| 163 | + print "after", self.queuename |
| 164 | + self._log(syslog.LOG_INFO, "spooling job '%s' to '%s'" % (self.jobfilename, self.queuename)) |
| 165 | + lpcmd = "lp '-d%s' '%s' >/dev/null 2>/dev/null" % (self.queuename, self.jobfilename) |
| 166 | + print lpcmd |
| 167 | + ret = os.system(lpcmd) |
| 168 | + return ret >> 8 == 0 |
| 169 | + |
| 170 | + def send_job_ack(self,ok): |
| 171 | + if ok: |
| 172 | + msg = "SUCCESS" |
| 173 | + else: |
| 174 | + msg = "FAILURE" |
| 175 | + self._send_fixed_msg(msg) |
| 176 | + self.sock.shutdown(1) |
| 177 | + |
| 178 | + def shutdown_connection(self): |
| 179 | + self.sock.shutdown(0) |
| 180 | + self.sock.close() |
| 181 | + |
| 182 | +def main(): |
| 183 | + try: |
| 184 | + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 185 | + s.bind((listen_host, spoolport)) |
| 186 | + s.listen(5) |
| 187 | + syslog.syslog(syslog.LOG_DAEMON|syslog.LOG_INFO, "exprintserver: startup complete") |
| 188 | + except Exception, e: |
| 189 | + syslog.syslog(syslog.LOG_DAEMON|syslog.LOG_ERR, "exprintserver: startup failed: " + str(e)) |
| 190 | + sys.exit(1) |
| 191 | + try: |
| 192 | + while 1: |
| 193 | + try: |
| 194 | + ct = ClientThread(s.accept()) |
| 195 | + ct.start() |
| 196 | + except socket.timeout: |
| 197 | + syslog.syslog(syslog.LOG_DAEMON|syslog.LOG_ERR, "exprintserver: unexpected timeout on listening socket") |
| 198 | + except KeyboardInterrupt: |
| 199 | + return |
| 200 | + except Exception, e: |
| 201 | + syslog.syslog(syslog.LOG_DAEMON|syslog.LOG_ERR, "exprintserver: unexpected exception during client thread setup: " + str(e)) |
| 202 | + |
| 203 | +def close_stdio(): |
| 204 | + #sys.stdout.close() |
| 205 | + #sys.stdin.close() |
| 206 | + #sys.stderr.close() |
| 207 | + #os.close(0) |
| 208 | + #os.close(1) |
| 209 | + #os.close(2) |
| 210 | + pass |
| 211 | + |
| 212 | +if __name__ == '__main__': |
| 213 | + close_stdio() |
| 214 | + main() |
| 215 | + |
0 commit comments