]> gitweb.pimeys.fr Git - bots/parrot.git/blob - parrot.py
caffouillage de regex : on pouvait pas quoter une place sans mettre le quoter
[bots/parrot.git] / parrot.py
1 #!/usr/bin/python
2 # -*- encoding: utf-8 -*-
3
4 """ Un bot IRC qui enregistre et ressort des citations """
5
6 import threading
7 import random
8 import time
9 import json
10 import re
11 import os
12 import signal
13 import sys
14
15 # Oui, j'ai recodé ma version d'irclib pour pouvoir rattrapper les SIGHUP
16 sys.path.insert(0, "/home/vincent/scripts/python-myirclib")
17 import irclib
18 import ircbot
19
20 from commands import getstatusoutput as ex
21
22 #: Config du bot
23 import config
24 #: Module définissant les erreurs
25 import errors
26 #: Module de gestion des quotes
27 import quotes
28
29 # Je veux pouvoir éditer ce que crée ce bot
30 os.umask(002)
31
32 def get_config_logfile(serveur):
33 """Renvoie le nom du fichier de log en fonction du ``serveur`` et de la config."""
34 serveurs = {"irc.crans.org" : "crans"}
35 return config.logfile_template % (serveurs[serveur],)
36
37 def log(serveur, channel, auteur=None, message=None):
38 """Enregistre une ligne de log."""
39 if auteur == message == None:
40 # alors c'est que c'est pas un channel mais juste une ligne de log
41 chain = u"%s %s" % (time.strftime("%F %T"), channel)
42 else:
43 chain = u"%s [%s:%s] %s" % (time.strftime("%F %T"), channel, auteur, message)
44 f = open(get_config_logfile(serveur), "a")
45 f.write((chain + u"\n").encode("utf-8"))
46 f.close()
47 if config.debug_stdout:
48 print chain.encode("utf-8")
49
50 def ignore_event(serv, ev):
51 """Retourne ``True`` si il faut ignorer cet évènement."""
52 for (blackmask, exceptlist) in config.blacklisted_masks:
53 usermask = ev.source()
54 blackit = bool(irclib.mask_matches(usermask, blackmask))
55 exceptit = any([bool(irclib.mask_matches(usermask, exceptmask)) for exceptmask in exceptlist])
56 if exceptit: # Il est exempté
57 return False
58 else:
59 if blackit: # Il n'est pas exempté et matche la blacklist
60 return True
61
62
63 def bot_unicode(chain):
64 """Essaye de décoder ``chain`` en UTF-8.
65 Lève une py:class:`errors.UnicodeBotError` en cas d'échec."""
66 try:
67 return chain.decode("utf8")
68 except UnicodeDecodeError as exc:
69 raise errors.UnicodeBotError
70
71
72 class Parrot(ircbot.SingleServerIRCBot):
73 """Classe principale : définition du bot."""
74 def __init__(self, serveur, debug=False):
75 temporary_pseudo = config.irc_pseudo + str(random.randrange(10000,100000))
76 ircbot.SingleServerIRCBot.__init__(self, [(serveur, 6667)],
77 temporary_pseudo, "Parrot, le bot irc. [Codé par 20-100]", 10)
78 self.debug = debug
79 self.serveur = serveur
80 self.overops = config.overops
81 self.ops = self.overops + config.ops
82 self.report_bugs_to = config.report_bugs_to
83 self.chanlist = config.chanlist
84 self.stay_channels = config.stay_channels
85 self.quiet_channels = config.quiet_channels
86 self.last_perdu = 0
87
88 self.quotedb = quotes.QuoteDB()
89 self.reload_quotes()
90
91 # Pour agir à la réception des whois 307 = registered nick
92 self.ircobj.add_global_handler("307", self.on_registered_nick)
93
94
95 ### Utilitaires
96 def _getnick(self):
97 """Récuère le nick effectif du bot sur le serveur."""
98 return self.serv.get_nickname()
99 nick = property(_getnick)
100
101 def give_me_my_pseudo(self, serv):
102 """Récupère le pseudo auprès de NickServ."""
103 serv.privmsg("NickServ", "GHOST %s %s" % (config.irc_pseudo, config.irc_password))
104 serv.privmsg("NickServ", "RELEASE %s %s" % (config.irc_pseudo, config.irc_password))
105 time.sleep(0.3)
106 serv.nick(config.irc_pseudo)
107
108 def pourmoi(self, serv, message):
109 """Renvoie (False, lemessage) ou (True, le message amputé de "pseudo: ")"""
110 pseudo = self.nick
111 pseudo = pseudo.decode("utf-8")
112 size = len(pseudo)
113 if message[:size] == pseudo and len(message) > size and message[size] == ":":
114 return (True, message[size+1:].lstrip(" "))
115 else:
116 return (False, message)
117
118 ### Exécution d'actions
119 def quitter(self, chan, leave_message=None):
120 """Quitter un channel avec un message customisable."""
121 if leave_message == None:
122 leave_message = random.choice(config.leave_messages)
123 self.serv.part(chan, message=leave_message.encode("utf8"))
124
125 def mourir(self):
126 """Se déconnecter du serveur IRC avec un message customisable."""
127 quit_message = random.choice(config.quit_messages)
128 self.die(msg=quit_message.encode("utf8"))
129
130 def reload_quotes(self):
131 """ Recharge la base de données des quotes et recompile la regexp de quote """
132 self.quotedb.load()
133 self.quote_pattern = re.compile(config.quote_regexp, flags=re.UNICODE)
134
135 def execute_reload(self, auteur=None):
136 """Recharge la config."""
137 reload(config)
138 self.reload_quotes()
139 if auteur in [None, "SIGHUP"]:
140 towrite = "Config reloaded" + " (SIGHUP received)" * (auteur == "SIGHUP")
141 for to in config.report_bugs_to:
142 self.serv.privmsg(to, towrite)
143 log(self.serveur, towrite)
144 return True, None
145 else:
146 return True, u"Config reloaded"
147
148 def crash(self, who="nobody", chan="nowhere"):
149 """Fait crasher le bot."""
150 where = "en privé" if chan == "priv" else "sur le chan %s" % chan
151 raise errors.CrashError((u"Crash demandé par %s %s" % (who, where)).encode("utf-8"))
152
153 ACTIONS = {
154 "reload" : execute_reload,
155 }
156
157 def execute_something(self, something, params, place=None, auteur=None):
158 """Exécute une action et répond son résultat à ``auteur``
159 sur un chan ou en privé en fonction de ``place``"""
160 action = self.ACTIONS[something]
161 success, message = action(self, **params)
162 if message:
163 if irclib.is_channel(place):
164 message = "%s: %s" % (auteur, message.encode("utf-8"))
165 self.serv.privmsg(place, message)
166 log(self.serveur, place, auteur, something + "%r" % params + ("[successful]" if success else "[failed]"))
167
168
169 def acknowledge(self, asked_by, asked_where, message):
170 """Répond quelque chose au demandeur d'une action.
171 ``asked_where=None`` signifie en privé."""
172 if asked_where is None:
173 self.serv.privmsg(asked_by, message)
174 else:
175 self.serv.privmsg(asked_where, "%s: %s" % (asked_by, message))
176
177 def dump(self, asked_by, asked_where=None):
178 """Dumpe les quotes. ``asked_where=None`` signifie en privé."""
179 quotes.dump(self.quotedb)
180 self.acknowledge(asked_by, asked_where, "Quotes dumpées")
181
182 def restore(self, asked_by, asked_where=None):
183 """Restaure les quotes à partir du dump. ``asked_where=None`` signifie en privé."""
184 self.quotedb = quotes.restore()
185 self.acknowledge(asked_by, asked_where, "Quotes restaurées à partir du dump (pas de backup effectué).")
186 many = self.quotedb.get_clash_authors()
187 if many:
188 self.acknowledge(asked_by, asked_where, "Auteurs de casse différente : %s" % (many))
189
190 ### Surcharge des events du Bot
191 def on_welcome(self, serv, ev):
192 """À l'arrivée sur le serveur."""
193 self.serv = serv # ça serv ira :)
194 self.give_me_my_pseudo(serv)
195 serv.privmsg("NickServ", "IDENTIFY %s" % (config.irc_password))
196 log(self.serveur, "Connected")
197 if self.debug:
198 self.chanlist = ["#bot"]
199 for c in self.chanlist:
200 log(self.serveur, "JOIN %s" % (c))
201 serv.join(c)
202
203 def on_privmsg(self, serv, ev):
204 """À la réception d'un message en privé."""
205 if ignore_event(serv, ev):
206 return
207 message = ev.arguments()[0]
208 auteur = irclib.nm_to_n(ev.source())
209 try:
210 message = bot_unicode(message)
211 except errors.UnicodeBotError:
212 if config.utf8_trigger:
213 serv.privmsg(auteur, random.choice(config.utf8_fail_answers).encode("utf8"))
214 return
215 message = message.split()
216 cmd = message[0].lower()
217 notunderstood = False
218 if cmd == u"help":
219 op,overop=auteur in self.ops, auteur in self.overops
220 if len(message)==1:
221 helpmsg = config.helpmsg_default
222 if op:
223 helpmsg += config.helpmsg_ops
224 if overop:
225 helpmsg += config.helpmsg_overops
226 else:
227 helpmsgs = config.helpdico.get(message[1].lower(), ["Commande inconnue.", None, None])
228 helpmsg = helpmsgs[0]
229 if op and helpmsgs[1]:
230 if helpmsg:
231 helpmsg += "\n" + helpmsgs[1]
232 else:
233 helpmsg = helpmsgs[1]
234 if overop and helpmsgs[2]:
235 if helpmsg:
236 helpmsg += "\n" + helpmsgs[2]
237 else:
238 helpmsg = helpmsgs[2]
239 for ligne in helpmsg.split("\n"):
240 serv.privmsg(auteur, ligne.encode("utf-8"))
241 elif cmd == u"join":
242 if auteur in self.ops:
243 if len(message) > 1:
244 if message[1] in self.chanlist:
245 serv.privmsg(auteur, "Je suis déjà sur %s" % (message[1]))
246 else:
247 serv.join(message[1])
248 self.chanlist.append(message[1])
249 serv.privmsg(auteur, "Channels : " + " ".join(self.chanlist))
250 log(self.serveur, "priv", auteur, " ".join(message))
251 else:
252 serv.privmsg(auteur, "Channels : " + " ".join(self.chanlist))
253 else:
254 notunderstood = True
255 elif cmd == u"leave":
256 if auteur in self.ops and len(message) > 1:
257 if message[1] in self.chanlist:
258 if not (message[1] in self.stay_channels) or auteur in self.overops:
259 self.quitter(message[1].encode("utf-8"), " ".join(message[2:]))
260 self.chanlist.remove(message[1])
261 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
262 else:
263 serv.privmsg(auteur, "Non, je reste !")
264 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
265 else:
266 serv.privmsg(auteur, "Je ne suis pas sur %s" % (message[1]))
267 else:
268 notunderstood = True
269 elif cmd == u"stay":
270 if auteur in self.overops:
271 if len(message) > 1:
272 if message[1] in self.stay_channels:
273 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
274 serv.privmsg(auteur, "Je stay déjà sur %s." % (message[1]))
275 else:
276 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
277 self.stay_channels.append(message[1])
278 serv.privmsg(auteur, "Stay channels : " + " ".join(self.stay_channels))
279 else:
280 serv.privmsg(auteur, "Stay channels : " + " ".join(self.stay_channels))
281 else:
282 notunderstood = True
283 elif cmd == u"nostay":
284 if auteur in self.overops:
285 if len(message) > 1:
286 if message[1] in self.stay_channels:
287 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
288 self.stay_channels.remove(message[1])
289 serv.privmsg(auteur, "Stay channels : " + " ".join(self.stay_channels))
290 else:
291 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
292 serv.privmsg(auteur, "Je ne stay pas sur %s." % (message[1]))
293 else:
294 notunderstood = True
295 elif cmd == u"die":
296 if auteur in self.overops:
297 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
298 self.mourir()
299 else:
300 notunderstood = True
301 elif cmd == u"crash":
302 if auteur in self.overops:
303 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
304 self.crash(auteur, "priv")
305 else:
306 notunderstood = True
307 elif cmd == u"reload":
308 if auteur in self.ops:
309 self.execute_something("reload", {"auteur" : auteur}, place=auteur, auteur=auteur)
310 else:
311 notunderstood = True
312 elif cmd == u"quiet":
313 if auteur in self.ops:
314 if len(message) > 1:
315 if message[1] in self.quiet_channels:
316 serv.privmsg(auteur, "Je me la ferme déjà sur %s" % (message[1]))
317 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
318 else:
319 self.quiet_channels.append(message[1])
320 serv.privmsg(auteur, "Quiet channels : " + " ".join(self.quiet_channels))
321 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
322 else:
323 serv.privmsg(auteur, "Quiet channels : " + " ".join(self.quiet_channels))
324 else:
325 notunderstood = True
326 elif cmd == u"noquiet":
327 if auteur in self.ops:
328 if len(message) > 1:
329 if message[1] in self.quiet_channels:
330 self.quiet_channels.remove(message[1])
331 serv.privmsg(auteur, "Quiet channels : " + " ".join(self.quiet_channels))
332 log(self.serveur, "priv", auteur, " ".join(message) + "[successful]")
333 else:
334 serv.privmsg(auteur, "Je ne me la ferme pas sur %s." % (message[1]))
335 log(self.serveur, "priv", auteur, " ".join(message) + "[failed]")
336 else:
337 notunderstood = True
338 elif cmd == u"say":
339 if auteur in self.overops and len(message) > 2:
340 serv.privmsg(message[1].encode("utf-8"), (u" ".join(message[2:])).encode("utf-8"))
341 log(self.serveur, "priv", auteur, " ".join(message))
342 elif len(message) <= 2:
343 serv.privmsg(auteur, "Syntaxe : SAY <channel> <message>")
344 else:
345 notunderstood = True
346 elif cmd == u"do":
347 if auteur in self.overops and len(message) > 2:
348 serv.action(message[1], " ".join(message[2:]))
349 log(self.serveur, "priv", auteur, " ".join(message))
350 elif len(message) <= 2:
351 serv.privmsg(auteur, "Syntaxe : DO <channel> <action>")
352 else:
353 notunderstood = True
354 elif cmd == u"kick":
355 if auteur in self.overops and len(message) > 2:
356 serv.kick(message[1].encode("utf-8"), message[2].encode("utf-8"), " ".join(message[3:]).encode("utf-8"))
357 log(self.serveur, "priv", auteur, " ".join(message))
358 elif len(message) <= 2:
359 serv.privmsg(auteur, "Syntaxe : KICK <channel> <pseudo> [<raison>]")
360 else:
361 notunderstood = True
362 elif cmd == u"ops":
363 if auteur in self.overops:
364 serv.privmsg(auteur, " ".join(self.ops))
365 else:
366 notunderstood = True
367 elif cmd == u"overops":
368 if auteur in self.overops:
369 serv.privmsg(auteur, " ".join(self.overops))
370 else:
371 notunderstood = True
372 elif cmd == u"dump" and auteur in self.ops:
373 self.dump(asked_by=auteur)
374 elif cmd == u"restore" and auteur in self.overops:
375 self.restore(asked_by=auteur)
376 else:
377 notunderstood = True
378 if notunderstood:
379 serv.privmsg(auteur, "Je n'ai pas compris. Essayez HELP…")
380
381 def on_pubmsg(self, serv, ev):
382 """À la réception d'un message sur un channel."""
383 if ignore_event(serv, ev):
384 return
385 auteur = irclib.nm_to_n(ev.source())
386 canal = ev.target()
387 message = ev.arguments()[0]
388 try:
389 message = bot_unicode(message)
390 except errors.UnicodeBotError:
391 if config.utf8_trigger and not canal in self.quiet_channels:
392 serv.privmsg(canal, (u"%s: %s"% ( auteur, random.choice(config.utf8_fail_answers))).encode("utf8"))
393 return
394 pour_moi, message = self.pourmoi(serv, message)
395 if pour_moi and message.split()!=[]:
396 cmd = message.split()[0].lower()
397 try:
398 args = " ".join(message.split()[1:])
399 except:
400 args = ""
401 if cmd in [u"meurs", u"die", u"crève"]:
402 if auteur in self.overops:
403 log(self.serveur, canal, auteur, message + "[successful]")
404 self.mourir()
405 else:
406 serv.privmsg(canal,(u"%s: %s"%(auteur, random.choice(config.quit_fail_messages))).encode("utf8"))
407 log(self.serveur, canal, auteur, message + "[failed]")
408 elif cmd == u"reload":
409 if auteur in self.ops:
410 self.execute_something("reload", {"auteur" : auteur}, place=canal, auteur=auteur)
411 elif cmd == u"crash":
412 if auteur in self.overops:
413 self.crash(auteur, canal)
414 elif cmd in [u"part", u"leave", u"dégage", u"va-t-en"]:
415 if auteur in self.ops and (not (canal in self.stay_channels)
416 or auteur in self.overops):
417 self.quitter(canal)
418 log(self.serveur, canal, auteur, message + "[successful]")
419 if canal in self.chanlist:
420 self.chanlist.remove(canal)
421 else:
422 serv.privmsg(canal,(u"%s: %s" % (auteur, random.choice(config.leave_fail_messages))).encode("utf8"))
423 log(self.serveur, canal, auteur, message + "[failed]")
424
425 elif cmd in [u"ping"] and not canal in self.quiet_channels:
426 serv.privmsg(canal, "%s: pong" % (auteur))
427 elif cmd in [u"dump"]:
428 self.dump(asked_by=auteur, asked_where=canal)
429 elif cmd in [u"restore"] and auteur in self.overops:
430 self.restore(asked_by=auteur, asked_where=canal)
431 elif cmd in [u"display"]:
432 self.serv.privmsg(canal, "%s: %s" % (auteur, config.quote_display_url.encode("utf-8")))
433 else:
434 # Vu que ce bot est prévu pour parser des quotes il va falloir bosser ici
435 match = self.quote_pattern.match(message)
436 if match:
437 d = match.groupdict()
438 # On n'autorise pas les gens à déclarer le quoter
439 d["quoter"] = auteur.decode("utf-8")
440 if self.quotedb.store(**d):
441 serv.privmsg(canal, (u"%s: Ce sera retenu, répété, amplifié" % (auteur,)).encode("utf-8"))
442 self.quotedb.save()
443 else:
444 serv.privmsg(canal, (u"%s: Je le savais déjà." % (auteur,)).encode("utf-8"))
445 if message.startswith(u"!quote"):
446 if message.strip() == u"!quote":
447 q = self.quotedb.random()
448 serv.privmsg(canal, str(q))
449 elif message.startswith("!quote "):
450 author = message[7:].strip()
451 try:
452 q = self.quotedb.randomfrom(author)
453 except IndexError:
454 serv.privmsg(canal, (u"Pas de quote de %s en mémoire." % author).encode("utf-8"))
455 return
456 serv.privmsg(canal, str(q))
457 elif message.startswith(u"!author") or message.startswith(u"!from"):
458 words = message.split()
459 cmd = words[0].lstrip("!")
460 regexp = any([cmd.endswith(suffix) for suffix in config.regex_suffixes])
461 search = u" ".join(words[1:])
462 authors = self.quotedb.search_authors(search, regexp)
463 if not authors:
464 serv.privmsg(canal, "%s: Pas d'auteur correspondant à la recherche." % (auteur,))
465 return
466 if cmd.startswith("author"):
467 if len(authors) > config.search_max_authors:
468 authors = authors[:config.search_max_authors+1] + ["+%s" % (len(authors) - config.search_max_authors)]
469 serv.privmsg(canal, "%s: %s" % (auteur, (u", ".join(authors)).encode("utf-8")))
470 elif cmd.startswith("from"):
471 quotes = sum([self.quotedb.quotesfrom(a) for a in authors], [])
472 q = random.choice(quotes)
473 serv.privmsg(canal, str(q))
474 elif message.startswith(u"!search"):
475 words = message.split()
476 cmd = words[0].lstrip("!")
477 regexp = cmd in ["search" + suffix for suffix in config.regex_suffixes]
478 search = u" ".join(words[1:])
479 quotes = self.quotedb.search(inquote=search, regexp=regexp)
480 if quotes:
481 q = random.choice(quotes)
482 serv.privmsg(canal, str(q))
483 else:
484 serv.privmsg(canal, "%s: Pas de quotes correspondant à la recherche." % (auteur,))
485
486 def on_action(self, serv, ev):
487 """À la réception d'une action."""
488 if ignore_event(serv, ev):
489 return
490 action = ev.arguments()[0]
491 auteur = irclib.nm_to_n(ev.source())
492 channel = ev.target()
493 try:
494 action = bot_unicode(action)
495 except errors.UnicodeBotError:
496 if config.utf8_trigger and not channel in self.quiet_channels:
497 serv.privmsg(channel, (u"%s: %s"%(auteur,random.choice(config.utf8_fail_answers))).encode("utf8"))
498 return
499 mypseudo = self.nick
500
501 def on_kick(self, serv, ev):
502 """À la réception d'une action."""
503 auteur = irclib.nm_to_n(ev.source())
504 channel = ev.target()
505 victime = ev.arguments()[0]
506 raison = ev.arguments()[1]
507 if victime == self.nick:
508 log(self.serveur, "%s kické de %s par %s (raison : %s)" % (victime, channel, auteur, raison))
509 time.sleep(2)
510 serv.join(channel)
511
512 def on_registered_nick(self, serv, ev):
513 """À la réception d'un résultat de whois."""
514 pseudo, regis = ev.arguments()
515 if regis == 'is a registered nick':
516 print "%s est enregistré \o/" % pseudo
517
518 ### .fork trick
519 def start_as_daemon(self, outfile):
520 sys.stderr = Logger(outfile)
521 self.start()
522
523
524 class Logger(object):
525 """Pour écrire ailleurs que sur stdout"""
526 def __init__(self, filename="parrot.full.log"):
527 self.filename = filename
528
529 def write(self, message):
530 f = open(self.filename, "a")
531 f.write(message)
532 f.close()
533
534 def main():
535 """Exécution principale : lecture des paramètres et lancement du bot."""
536 if len(sys.argv) == 1:
537 print "Usage : parrot.py <serveur> [--debug] [--no-output] [--daemon [--pidfile]] [--outfile]"
538 print " --outfile sans --no-output ni --daemon n'a aucun effet"
539 exit(1)
540 serveur = sys.argv[1]
541 if "--daemon" in sys.argv:
542 thisfile = os.path.realpath(__file__)
543 thisdirectory = thisfile.rsplit("/", 1)[0]
544 os.chdir(thisdirectory)
545 daemon = True
546 else:
547 daemon = False
548 if "debug" in sys.argv or "--debug" in sys.argv:
549 debug = True
550 else:
551 debug = False
552 if "--quiet" in sys.argv:
553 config.debug_stdout = False
554 serveurs = {
555 "irc" : "irc.crans.org",
556 "crans" : "irc.crans.org",
557 "irc.crans.org" : "irc.crans.org"}
558 if "--no-output" in sys.argv or "--daemon" in sys.argv:
559 outfile = "/var/log/bots/parrot.full.log"
560 for arg in sys.argv:
561 arg = arg.split("=")
562 if arg[0].strip('-') in ["out", "outfile", "logfile"]:
563 outfile = arg[1]
564 sys.stdout = Logger(outfile)
565 try:
566 serveur = serveurs[serveur]
567 except KeyError:
568 print "Server Unknown : %s" % (serveur)
569 exit(404)
570 parrot = Parrot(serveur,debug)
571 # Si on reçoit un SIGHUP, on reload la config
572 def sighup_handler(signum, frame):
573 parrot.execute_reload(auteur="SIGHUP")
574 signal.signal(signal.SIGHUP, sighup_handler)
575 # Daemonization
576 if daemon:
577 child_pid = os.fork()
578 if child_pid == 0:
579 os.setsid()
580 parrot.start_as_daemon(outfile)
581 else:
582 # on enregistre le pid de parrot
583 pidfile = "/var/run/bots/parrot.pid"
584 for arg in sys.argv:
585 arg = arg.split("=")
586 if arg[0].strip('-') in ["pidfile"]:
587 pidfile = arg[1]
588 f = open(pidfile, "w")
589 f.write("%s\n" % child_pid)
590 f.close()
591 else:
592 parrot.start()
593
594 if __name__ == "__main__":
595 main()