#! /usr/bin/env python # # Server base for PyWrite # Copyright (C) 2002 Michael Urman # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # from linux_getpeereid import getpeereid import pywconf import os, sys, pwd, socket, select, time class PyWriteServer: """PywriteServer is the base server class, providing a default server interface which acts like receiving a write, but stores logs in `logs_subdir` (defaults to ~/.pywrite/logs/)""" def __init__(self): """Initialize the server. Attempt to allocate the socket. If it exists, attempt to verify it is alive. If not, wipe and attempt to allocate the socket.""" # ensure pywrite_subdir exists and is a directory if not pywconf.checkdirs(): sys.exit(-1) self.address = pywconf.socket() serv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: serv.bind(self.address) except socket.error, bind_error: # socket exists; may be in use. Try to ascertain try: serv.connect(self.address) except socket.error, connect_error: # connection failed; probably stale print>>sys.stderr, "Found stale socket at %s. Removing." % self.address # unlink presumed-stale socket and try again os.unlink(self.address) try: serv.bind(self.address) except socket.error, new_error: print>>sys.stderr, "Couldn't create server socket. Errors received:" print>>sys.stderr, " - first attempt:", bind_error print>>sys.stderr, " - check for stale:", connect_error print>>sys.stderr, " - second attempt:", new_error raise new_error else: # connection to existing socket succeeded; drop connection serv.shutdown(2) serv.close() print>>sys.stderr, "PyWrite server already running on %s. Exiting." % self.address sys.exit(1) # serv should now be a connected server socket os.chmod(self.address, 0777) serv.listen(3) self.server_socket = serv self.clients = [] self.fd_to_sock = {self.server_socket.fileno() : self.server_socket} def __del__(self): """__del__(self) -> None Attempts some bare-minimum cleanup: - close all sockets - remove server socket""" try: del self.clients finally: try: self.server_socket.shutdown(2) self.server_socket.close() finally: if os.path.exists(self.address): os.unlink(self.address) def run(self): """run(self) -> None Run the server, servicing everything.""" try: while 1: # wait for something to happen all = self.fd_to_sock.keys() rlist, wlist, xlist = select.select(all, [], all) for ready in rlist: sock = self.fd_to_sock[ready] if sock == self.server_socket: # a ready server socket implies a connecting client self.new_client() else: # a ready client implies a message message = self.get_message(sock) if message is not None: self.dispatch(sock, message) # remove on EOF if message.type == ClientMessage.EOF: del self.fd_to_sock[sock.socket.fileno()] del self.clients[self.clients.index(sock)] except Exception, err: try: for client in self.clients: self.dispatch(client, ClientMessage(ClientMessage.EOF, None)) finally: raise err def new_client(self): """newclient(self) -> None Accepts and initializes a new client connecting on self.server_socket""" try: client, addr = self.server_socket.accept() clientsock = ClientSock(client) self.clients.append(clientsock) self.fd_to_sock[clientsock.socket.fileno()] = clientsock except socket.error, err: print>>sys.stderr, "Error adding client:", err def get_message(self, sock): """get_message(self, sock) -> ClientMessage Return a ClientMessage, or None if the message should be ignored.""" return sock.get_message() def dispatch(self, sock, message): """dispatch(self, sock, message) -> None Dispatch a message by writing it to the screen. If the remote user has already sent a message, don't redisplay the "Message From ..." string.""" if message.type == ClientMessage.EOF: if sock.status == ClientSock.WRITING: print "EOF (%s)" % sock.login print >> sock.file, '+', "EOF (%s)" % sock.login sock.file.close() sock.status = ClientSock.CLOSED elif message.type == ClientMessage.TEXT: # beep and say "message from" if not already writing if sock.status != ClientSock.WRITING: sock.status = ClientSock.WRITING timestr = time.ctime(time.time()) sock.filename = os.path.join(pywconf.logdir(), sock.login) sock.file = open(sock.filename, 'a') print "\n\a\tMessage from %s at %s..." % (sock.login, timestr) print >> sock.file, '+', "\tMessage from %s at %s..." % (sock.login, timestr) # show and log message text = message.safetext() print text, print >> sock.file, '+', text, sock.file.flush() class ClientSock: """ClientSock abstracts the user on the remote side of the socket""" CLOSED = 0 WRITING = 1 NONE = 2 def __init__(self, sock): """ClientSock(sock) -> ClientSock Initialize and return a ClientSock for the passed socket""" self.socket = sock self.status = ClientSock.NONE self.uid, self.gid = getpeereid(sock) pw = pwd.getpwuid(self.uid) self.login = pw[0] self.name = pw[4] def __del__(self): """__del__(self) -> None Attempt to close socket on deletion""" if self.status != ClientSock.CLOSED: self.socket.shutdown(2) self.socket.close() def get_message(self): """get_message(self) -> ClientMessage Return a ClientMessage corresponding to socket.readline()""" try: line = self.socket.recv(1024) except socket.error, err: # assume closure caused error print>>sys.stderr, "Error reading closed client %s"%self.login, err line = '' if line == '': return ClientMessage(ClientMessage.EOF, None) else: return ClientMessage(ClientMessage.TEXT, line) class ClientMessage: """Client Message""" TEXT = 2 EOF = 3 def __init__(self, type, text): """ClientMessage(type, text) -> ClientMessage Return an initialzed ClientMessage""" self.type = type self.text = text def safetext(self): """safetext(self) -> string Return a sanitized version of the text, safe to print to stdout""" lines = [] for line in self.text.splitlines(): safetext = [] for char in self.text: if char <= chr(32): safetext.append('^' + chr(ord(char)+ord('A'))) elif chr(128+32) >= char >= chr(128): safetext.append('M-^' + chr(ord(char)-128+ord('A'))) elif char > chr(128+32): safetext.append('M-' + chr(ord(char)-128)) else: safetext.append(char) lines.append(''.join(safetext)) return '\n'.join(lines) + '\n'