Automatically restart on crash
[people/stefanha/gpxebot.git] / gpxebot.py
1 #!/usr/bin/env python
2 # Copyright (C) 2008 Stefan Hajnoczi <stefanha@gmail.com>.
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 2 of the
7 # License, or any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17 import select
18 import socket
19 import sys
20 import time
21 import os
22
23 import config
24 import cmds
25 import utils
26
27 NO_ARGS = -1
28
29 handlers = {}
30
31 def autojoin():
32     del handlers['376']
33
34     # Receive '+' or '-' indicator with every message showing whether the
35     # sender is identified to nick services.  Only identified users may be bot
36     # operators.
37     cmd('CAP REQ :identify-msg')
38
39     return do_response(cmds.readrc_command())
40
41 def ping(_, arg):
42     cmd('PONG %s' % arg)
43     return False
44
45 def do_response(response):
46     # it should be in the format (command, args) 
47     # or [(command, args), (command, args)...]
48     # args is a tuple
49     if not response:
50         return False
51     if isinstance(response, list):
52         return any(do_response(r) for r in response)
53     (command, args) = response
54     if command == 'PRIVMSG':
55         pmsg(*args)
56     elif command == 'CMD':
57         cmd(*args)
58     return command == 'RESTART'
59
60 def privmsg(_, target, msg):
61     # Establish identify of sender from CAPAB IDENTIFY-MSG info
62     is_identified = msg.startswith('+')
63     if msg.startswith('-') or msg.startswith('+'):
64         msg = msg[1:]
65     who = cmds.Who(mask, is_identified)
66
67     utils.do_log(cmds.logs, target, who.nick, msg)
68
69     words = msg.split()
70     if not words:
71         return False
72     if target.startswith('#'):
73         replyto = target
74         if not config.NICK in words[0]:
75             return False
76         words.pop(0)
77     elif target == config.NICK:
78         replyto = who.nick
79
80     return do_response(cmds.do_command(who, target, replyto, words))
81
82 def add_handler(command, handler, nargs):
83     handlers[command] = (handler, nargs)
84
85 def cmd(msg):
86     utils.dbg('WRITE ' + msg)
87     sock.sendall('%s\r\n' % msg)
88
89 def pmsg(target, msg):
90     if target:
91         utils.do_log(cmds.logs, target, config.NICK, msg)
92         cmd('PRIVMSG %s :%s' % (target, msg))
93
94 def dispatch(args):
95     command = args[0]
96     if command in handlers:
97         h = handlers[command]
98         if h[1] == NO_ARGS:
99             return h[0]()
100         elif len(args) == h[1]:
101             return h[0](*args)
102     return False
103
104 def parse(line):
105     if line[0] == ':':
106         mask, line = line[1:].split(None, 1)
107     else:
108         mask = None
109
110     args = []
111     while line and line[0] != ':':
112         fields = line.split(None, 1)
113         if len(fields) == 1:
114             fields.append(None)
115         arg, line = fields
116         args.append(arg)
117     if line:
118         args.append(line[1:])
119
120     return mask, args
121
122 add_handler('376', autojoin, NO_ARGS)
123 add_handler('PING', ping, 2)
124 add_handler('PRIVMSG', privmsg, 3)
125
126 try:
127     if len(sys.argv) == 3 and sys.argv[1] == '--socket-fd':
128         # Restart with existing IRC session
129         fd = int(sys.argv[2])
130         sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
131         os.close(fd)
132         autojoin()
133     else:
134         # Connect to server and start new IRC session
135         sock = socket.socket()
136         sock.connect((config.HOST, config.PORT))
137         cmd('NICK %s' % config.NICK)
138         cmd('USER %s none none :%s' % (config.IDENT, config.REALNAME))
139 except Exception, e:
140     # Log top-level exceptions and restart
141     try:
142         sock.close()
143     except Exception:
144         pass
145     sys.stderr.write(str(e) + '\n')
146     time.sleep(10) # prevent spinning on repeated failure
147     os.execl(sys.argv[0], sys.argv[0])
148
149 # Try to create command FIFO
150 try:
151     os.mkfifo(config.FIFO)
152 except OSError:
153     pass
154
155 # Opening a fifo for reading with no writers normally blocks, but opening
156 # read/write will not block on Linux.
157 cmdfifo = open(config.FIFO, 'r+')
158
159 restart = False
160 sockbuf = ''
161 try:
162     while not restart:
163         rlist, _, xlist = select.select([sock, cmdfifo], [], [sock])
164
165         if sock in rlist or sock in xlist:
166             r = sock.recv(4096)
167             if not r:
168                 break
169             sockbuf += r
170
171             while '\r\n' in sockbuf:
172                 line, sockbuf = sockbuf.split('\r\n', 1)
173                 if not line:
174                     continue
175                 utils.dbg('READ ' + line)
176                 mask, args = parse(line)
177                 restart = dispatch(args)
178
179         if cmdfifo in rlist:
180             words = cmdfifo.readline().strip().split()
181             restart = do_response(cmds.do_command(cmds.Who(config.NICK, True), config.NICK, config.NICK, words)) or restart
182 except Exception, e:
183     # Log top-level exceptions and restart
184     cmdfifo.close()
185     sock.close()
186     sys.stderr.write(str(e) + '\n')
187     time.sleep(10) # prevent spinning on repeated failure
188     os.execl(sys.argv[0], sys.argv[0])
189
190 cmdfifo.close()
191
192 # Restart with existing IRC session
193 fd = sock.fileno()
194 os.execl(sys.argv[0], sys.argv[0], '--socket-fd', str(fd))