Back end data storage separation.
[mudpy.git] / lib / mudpy / misc.py
1 # -*- coding: utf-8 -*-
2 u"""Miscellaneous functions for the mudpy engine."""
3
4 # Copyright (c) 2004-2010 Jeremy Stanley <fungi@yuggoth.org>. Permission
5 # to use, copy, modify, and distribute this software is granted under
6 # terms provided in the LICENSE file distributed with this software.
7
8 class Element:
9    u"""An element of the universe."""
10    def __init__(self, key, universe, filename=None):
11       u"""Set up a new element."""
12       import data, os.path
13
14       # keep track of our key name
15       self.key = key
16
17       # keep track of what universe it's loading into
18       self.universe = universe
19
20       # clone attributes if this is replacing another element
21       if self.key in self.universe.contents:
22          old_element = self.universe.contents[self.key]
23          for attribute in vars(old_element).keys():
24             exec(u"self." + attribute + u" = old_element." + attribute)
25          if self.owner: self.owner.avatar = self
26
27       # i guess this is a new element then
28       else:
29
30          # not owned by a user by default (used for avatars)
31          self.owner = None
32
33          # no contents in here by default
34          self.contents = {}
35
36          # parse out appropriate category and subkey names, add to list
37          if self.key.find(u":") > 0:
38             self.category, self.subkey = self.key.split(u":", 1)
39          else:
40             self.category = u"other"
41             self.subkey = self.key
42          if not self.category in self.universe.categories:
43             self.category = u"other"
44             self.subkey = self.key
45
46          # get an appropriate filename for the origin
47          if not filename:
48             filename = self.universe.default_origins[self.category]
49          if not os.path.isabs(filename):
50             filename = os.path.abspath(filename)
51
52          # add the file if it doesn't exist yet
53          if not filename in self.universe.files:
54             data.DataFile(filename, self.universe)
55
56       # record or reset a pointer to the origin file
57       self.origin = self.universe.files[filename]
58
59       # add a data section to the origin if necessary
60       if not self.origin.data.has_section(self.key):
61          self.origin.data.add_section(self.key)
62
63       # add or replace this element in the universe
64       self.universe.contents[self.key] = self
65       self.universe.categories[self.category][self.subkey] = self
66
67    def reload(self):
68       u"""Create a new element and replace this one."""
69       new_element = Element(self.key, self.universe, self.origin.filename)
70       del(self)
71    def destroy(self):
72       u"""Remove an element from the universe and destroy it."""
73       self.origin.data.remove_section(self.key)
74       del self.universe.categories[self.category][self.subkey]
75       del self.universe.contents[self.key]
76       del self
77    def facets(self):
78       u"""Return a list of non-inherited facets for this element."""
79       if self.key in self.origin.data.sections():
80          return self.origin.data.options(self.key)
81       else: return []
82    def has_facet(self, facet):
83       u"""Return whether the non-inherited facet exists."""
84       return facet in self.facets()
85    def remove_facet(self, facet):
86       u"""Remove a facet from the element."""
87       if self.has_facet(facet):
88          self.origin.data.remove_option(self.key, facet)
89          self.origin.modified = True
90    def ancestry(self):
91       u"""Return a list of the element's inheritance lineage."""
92       if self.has_facet(u"inherit"):
93          ancestry = self.getlist(u"inherit")
94          for parent in ancestry[:]:
95             ancestors = self.universe.contents[parent].ancestry()
96             for ancestor in ancestors:
97                if ancestor not in ancestry: ancestry.append(ancestor)
98          return ancestry
99       else: return []
100    def get(self, facet, default=None):
101       u"""Retrieve values."""
102       if default is None: default = u""
103       if self.origin.data.has_option(self.key, facet):
104          raw_data = self.origin.data.get(self.key, facet)
105          if raw_data.startswith(u"u\"") or raw_data.startswith(u"u'"):
106             raw_data = raw_data[1:]
107          raw_data.strip(u"\"'")
108          if type(raw_data) == str: return unicode(raw_data, "utf-8")
109          else: return raw_data
110       elif self.has_facet(u"inherit"):
111          for ancestor in self.ancestry():
112             if self.universe.contents[ancestor].has_facet(facet):
113                return self.universe.contents[ancestor].get(facet)
114       else: return default
115    def getboolean(self, facet, default=None):
116       u"""Retrieve values as boolean type."""
117       if default is None: default=False
118       if self.origin.data.has_option(self.key, facet):
119          return self.origin.data.getboolean(self.key, facet)
120       elif self.has_facet(u"inherit"):
121          for ancestor in self.ancestry():
122             if self.universe.contents[ancestor].has_facet(facet):
123                return self.universe.contents[ancestor].getboolean(facet)
124       else: return default
125    def getint(self, facet, default=None):
126       u"""Return values as int/long type."""
127       if default is None: default = 0
128       if self.origin.data.has_option(self.key, facet):
129          return self.origin.data.getint(self.key, facet)
130       elif self.has_facet(u"inherit"):
131          for ancestor in self.ancestry():
132             if self.universe.contents[ancestor].has_facet(facet):
133                return self.universe.contents[ancestor].getint(facet)
134       else: return default
135    def getfloat(self, facet, default=None):
136       u"""Return values as float type."""
137       if default is None: default = 0.0
138       if self.origin.data.has_option(self.key, facet):
139          return self.origin.data.getfloat(self.key, facet)
140       elif self.has_facet(u"inherit"):
141          for ancestor in self.ancestry():
142             if self.universe.contents[ancestor].has_facet(facet):
143                return self.universe.contents[ancestor].getfloat(facet)
144       else: return default
145    def getlist(self, facet, default=None):
146       u"""Return values as list type."""
147       import data
148       if default is None: default = []
149       value = self.get(facet)
150       if value: return data.makelist(value)
151       else: return default
152    def getdict(self, facet, default=None):
153       u"""Return values as dict type."""
154       import data
155       if default is None: default = {}
156       value = self.get(facet)
157       if value: return data.makedict(value)
158       else: return default
159    def set(self, facet, value):
160       u"""Set values."""
161       if not self.has_facet(facet) or not self.get(facet) == value:
162          if type(value) is long: value = unicode(value)
163          elif not type(value) is unicode: value = repr(value)
164          self.origin.data.set(self.key, facet, value)
165          self.origin.modified = True
166    def append(self, facet, value):
167       u"""Append value tp a list."""
168       if type(value) is long: value = unicode(value)
169       elif not type(value) is unicode: value = repr(value)
170       newlist = self.getlist(facet)
171       newlist.append(value)
172       self.set(facet, newlist)
173
174    def new_event(self, action, when=None):
175       u"""Create, attach and enqueue an event element."""
176
177       # if when isn't specified, that means now
178       if not when: when = self.universe.get_time()
179
180       # events are elements themselves
181       event = Element(u"event:" + self.key + u":" + counter)
182
183    def send(
184       self,
185       message,
186       eol=u"$(eol)",
187       raw=False,
188       flush=False,
189       add_prompt=True,
190       just_prompt=False,
191       add_terminator=False,
192       prepend_padding=True
193    ):
194       u"""Convenience method to pass messages to an owner."""
195       if self.owner:
196          self.owner.send(
197             message,
198             eol,
199             raw,
200             flush,
201             add_prompt,
202             just_prompt,
203             add_terminator,
204             prepend_padding
205          )
206
207    def can_run(self, command):
208       u"""Check if the user can run this command object."""
209
210       # has to be in the commands category
211       if command not in self.universe.categories[u"command"].values():
212          result = False
213
214       # avatars of administrators can run any command
215       elif self.owner and self.owner.account.getboolean(u"administrator"):
216          result = True
217
218       # everyone can run non-administrative commands
219       elif not command.getboolean(u"administrative"):
220          result = True
221
222       # otherwise the command cannot be run by this actor
223       else:
224          result = False
225
226       # pass back the result
227       return result
228
229    def update_location(self):
230       u"""Make sure the location's contents contain this element."""
231       location = self.get(u"location")
232       if location in self.universe.contents:
233          self.universe.contents[location].contents[self.key] = self
234    def clean_contents(self):
235       u"""Make sure the element's contents aren't bogus."""
236       for element in self.contents.values():
237          if element.get(u"location") != self.key:
238             del self.contents[element.key]
239    def go_to(self, location):
240       u"""Relocate the element to a specific location."""
241       current = self.get(u"location")
242       if current and self.key in self.universe.contents[current].contents:
243          del universe.contents[current].contents[self.key]
244       if location in self.universe.contents: self.set(u"location", location)
245       self.universe.contents[location].contents[self.key] = self
246       self.look_at(location)
247    def go_home(self):
248       u"""Relocate the element to its default location."""
249       self.go_to(self.get(u"default_location"))
250       self.echo_to_location(
251          u"You suddenly realize that " + self.get(u"name") + u" is here."
252       )
253    def move_direction(self, direction):
254       u"""Relocate the element in a specified direction."""
255       self.echo_to_location(
256          self.get(
257             u"name"
258          ) + u" exits " + self.universe.categories[
259             u"internal"
260          ][
261             u"directions"
262          ].getdict(
263             direction
264          )[
265             u"exit"
266          ] + u"."
267       )
268       self.send(
269          u"You exit " + self.universe.categories[
270             u"internal"
271          ][
272             u"directions"
273          ].getdict(
274             direction
275          )[
276             u"exit"
277          ] + u".",
278          add_prompt=False
279       )
280       self.go_to(
281          self.universe.contents[self.get(u"location")].link_neighbor(direction)
282       )
283       self.echo_to_location(
284          self.get(
285             u"name"
286          ) + u" arrives from " + self.universe.categories[
287             u"internal"
288          ][
289             u"directions"
290          ].getdict(
291             direction
292          )[
293             u"enter"
294          ] + u"."
295       )
296    def look_at(self, key):
297       u"""Show an element to another element."""
298       if self.owner:
299          element = self.universe.contents[key]
300          message = u""
301          name = element.get(u"name")
302          if name: message += u"$(cyn)" + name + u"$(nrm)$(eol)"
303          description = element.get(u"description")
304          if description: message += description + u"$(eol)"
305          portal_list = element.portals().keys()
306          if portal_list:
307             portal_list.sort()
308             message += u"$(cyn)[ Exits: " + u", ".join(
309                portal_list
310             ) + u" ]$(nrm)$(eol)"
311          for element in self.universe.contents[
312             self.get(u"location")
313          ].contents.values():
314             if element.getboolean(u"is_actor") and element is not self:
315                message += u"$(yel)" + element.get(
316                   u"name"
317                ) + u" is here.$(nrm)$(eol)"
318             elif element is not self:
319                message += u"$(grn)" + element.get(
320                   u"impression"
321                ) + u"$(nrm)$(eol)"
322          self.send(message)
323    def portals(self):
324       u"""Map the portal directions for a room to neighbors."""
325       import re
326       portals = {}
327       if re.match(u"""^location:-?\d+,-?\d+,-?\d+$""", self.key):
328          coordinates = [(int(x)) for x in self.key.split(u":")[1].split(u",")]
329          directions = self.universe.categories[u"internal"][u"directions"]
330          offsets = dict(
331             [
332                (
333                   x, directions.getdict(x)[u"vector"]
334                ) for x in directions.facets()
335             ]
336          )
337          for portal in self.getlist(u"gridlinks"):
338             adjacent = map(lambda c,o: c+o, coordinates, offsets[portal])
339             neighbor = u"location:" + u",".join(
340                [(unicode(x)) for x in adjacent]
341             )
342             if neighbor in self.universe.contents: portals[portal] = neighbor
343       for facet in self.facets():
344          if facet.startswith(u"link_"):
345             neighbor = self.get(facet)
346             if neighbor in self.universe.contents:
347                portal = facet.split(u"_")[1]
348                portals[portal] = neighbor
349       return portals
350    def link_neighbor(self, direction):
351       u"""Return the element linked in a given direction."""
352       portals = self.portals()
353       if direction in portals: return portals[direction]
354    def echo_to_location(self, message):
355       u"""Show a message to other elements in the current location."""
356       for element in self.universe.contents[
357          self.get(u"location")
358       ].contents.values():
359          if element is not self: element.send(message)
360
361 class Universe:
362    u"""The universe."""
363
364    def __init__(self, filename=u"", load=False):
365       u"""Initialize the universe."""
366       import os, os.path
367       self.categories = {}
368       self.contents = {}
369       self.default_origins = {}
370       self.loglines = []
371       self.pending_events_long = {}
372       self.pending_events_short = {}
373       self.private_files = []
374       self.reload_flag = False
375       self.startdir = os.getcwd()
376       self.terminate_flag = False
377       self.userlist = []
378       if not filename:
379          possible_filenames = [
380             u".mudpyrc",
381             u".mudpy/mudpyrc",
382             u".mudpy/mudpy.conf",
383             u"mudpy.conf",
384             u"etc/mudpy.conf",
385             u"/usr/local/mudpy/mudpy.conf",
386             u"/usr/local/mudpy/etc/mudpy.conf",
387             u"/etc/mudpy/mudpy.conf",
388             u"/etc/mudpy.conf"
389             ]
390          for filename in possible_filenames:
391             if os.access(filename, os.R_OK): break
392       if not os.path.isabs(filename):
393          filename = os.path.join(self.startdir, filename)
394       self.filename = filename
395       if load: self.load()
396
397    def load(self):
398       u"""Load universe data from persistent storage."""
399       import data
400
401       # the files dict must exist and filename needs to be read-only
402       if not hasattr(
403          self, u"files"
404       ) or not (
405          self.filename in self.files and self.files[
406             self.filename
407          ].is_writeable()
408       ):
409
410          # clear out all read-only files
411          if hasattr(self, u"files"):
412             for data_filename in self.files.keys():
413                if not self.files[data_filename].is_writeable():
414                   del self.files[data_filename]
415
416          # start loading from the initial file
417          data.DataFile(self.filename, self)
418
419       # make a list of inactive avatars
420       inactive_avatars = []
421       for account in self.categories[u"account"].values():
422          inactive_avatars += [
423             (self.contents[x]) for x in account.getlist(u"avatars")
424          ]
425       for user in self.userlist:
426          if user.avatar in inactive_avatars:
427             inactive_avatars.remove(user.avatar)
428
429       # go through all elements to clear out inactive avatar locations
430       for element in self.contents.values():
431          location = element.get(u"location")
432          if element in inactive_avatars and location:
433             if location in self.contents and element.key in self.contents[
434                location
435             ].contents:
436                del self.contents[location].contents[element.key]
437             element.set(u"default_location", location)
438             element.remove_facet(u"location")
439
440       # another pass to straighten out all the element contents
441       for element in self.contents.values():
442          element.update_location()
443          element.clean_contents()
444
445    def new(self):
446       u"""Create a new, empty Universe (the Big Bang)."""
447       new_universe = Universe()
448       for attribute in vars(self).keys():
449          exec(u"new_universe." + attribute + u" = self." + attribute)
450       new_universe.reload_flag = False
451       del self
452       return new_universe
453
454    def save(self):
455       u"""Save the universe to persistent storage."""
456       for key in self.files: self.files[key].save()
457
458    def initialize_server_socket(self):
459       u"""Create and open the listening socket."""
460       import socket
461
462       # need to know the local address and port number for the listener
463       host = self.categories[u"internal"][u"network"].get(u"host")
464       port = self.categories[u"internal"][u"network"].getint(u"port")
465
466       # if no host was specified, bind to all local addresses (preferring ipv6)
467       if not host:
468          if socket.has_ipv6: host = u"::"
469          else: host = u"0.0.0.0"
470
471       # figure out if this is ipv4 or v6
472       family = socket.getaddrinfo(host, port)[0][0]
473       if family is socket.AF_INET6 and not socket.has_ipv6:
474          log(u"No support for IPv6 address %s (use IPv4 instead)." % host)
475
476       # create a new stream-type socket object
477       self.listening_socket = socket.socket(family, socket.SOCK_STREAM)
478
479       # set the socket options to allow existing open ones to be
480       # reused (fixes a bug where the server can't bind for a minute
481       # when restarting on linux systems)
482       self.listening_socket.setsockopt(
483          socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
484       )
485
486       # bind the socket to to our desired server ipa and port
487       self.listening_socket.bind((host, port))
488
489       # disable blocking so we can proceed whether or not we can
490       # send/receive
491       self.listening_socket.setblocking(0)
492
493       # start listening on the socket
494       self.listening_socket.listen(1)
495
496       # note that we're now ready for user connections
497       log(
498          u"Listening for Telnet connections on: " + host + u":" + unicode(port)
499       )
500
501    def get_time(self):
502       u"""Convenience method to get the elapsed time counter."""
503       return self.categories[u"internal"][u"counters"].getint(u"elapsed")
504
505 class User:
506    u"""This is a connected user."""
507
508    def __init__(self):
509       u"""Default values for the in-memory user variables."""
510       import telnet
511       self.account = None
512       self.address = u""
513       self.authenticated = False
514       self.avatar = None
515       self.columns = 79
516       self.connection = None
517       self.error = u""
518       self.input_queue = []
519       self.last_address = u""
520       self.last_input = universe.get_time()
521       self.menu_choices = {}
522       self.menu_seen = False
523       self.negotiation_pause = 0
524       self.output_queue = []
525       self.partial_input = ""
526       self.password_tries = 0
527       self.state = u"initial"
528       self.telopts = {}
529
530    def quit(self):
531       u"""Log, close the connection and remove."""
532       if self.account: name = self.account.get(u"name")
533       else: name = u""
534       if name: message = u"User " + name
535       else: message = u"An unnamed user"
536       message += u" logged out."
537       log(message, 2)
538       self.deactivate_avatar()
539       self.connection.close()
540       self.remove()
541
542    def check_idle(self):
543       u"""Warn or disconnect idle users as appropriate."""
544       idletime = universe.get_time() - self.last_input
545       linkdead_dict = universe.categories[u"internal"][u"time"].getdict(
546          u"linkdead"
547       )
548       if self.state in linkdead_dict: linkdead_state = self.state
549       else: linkdead_state = u"default"
550       if idletime > linkdead_dict[linkdead_state]:
551          self.send(
552             u"$(eol)$(red)You've done nothing for far too long... goodbye!" \
553                + u"$(nrm)$(eol)",
554             flush=True,
555             add_prompt=False
556          )
557          logline = u"Disconnecting "
558          if self.account and self.account.get(u"name"):
559             logline += self.account.get(u"name")
560          else:
561             logline += u"an unknown user"
562          logline += u" after idling too long in the " + self.state + u" state."
563          log(logline, 2)
564          self.state = u"disconnecting"
565          self.menu_seen = False
566       idle_dict = universe.categories[u"internal"][u"time"].getdict(u"idle")
567       if self.state in idle_dict: idle_state = self.state
568       else: idle_state = u"default"
569       if idletime == idle_dict[idle_state]:
570          self.send(
571             u"$(eol)$(red)If you continue to be unproductive, " \
572                + u"you'll be shown the door...$(nrm)$(eol)"
573          )
574
575    def reload(self):
576       u"""Save, load a new user and relocate the connection."""
577
578       # get out of the list
579       self.remove()
580
581       # create a new user object
582       new_user = User()
583
584       # set everything equivalent
585       for attribute in vars(self).keys():
586          exec(u"new_user." + attribute + u" = self." + attribute)
587
588       # the avatar needs a new owner
589       if new_user.avatar: new_user.avatar.owner = new_user
590
591       # add it to the list
592       universe.userlist.append(new_user)
593
594       # get rid of the old user object
595       del(self)
596
597    def replace_old_connections(self):
598       u"""Disconnect active users with the same name."""
599
600       # the default return value
601       return_value = False
602
603       # iterate over each user in the list
604       for old_user in universe.userlist:
605
606          # the name is the same but it's not us
607          if hasattr(
608             old_user, u"account"
609          ) and old_user.account and old_user.account.get(
610             u"name"
611          ) == self.account.get(
612             u"name"
613          ) and old_user is not self:
614
615             # make a note of it
616             log(
617                u"User " + self.account.get(
618                   u"name"
619                ) + u" reconnected--closing old connection to " \
620                   + old_user.address + u".",
621                2
622             )
623             old_user.send(
624                u"$(eol)$(red)New connection from " + self.address \
625                   + u". Terminating old connection...$(nrm)$(eol)", \
626                flush=True,
627                add_prompt=False
628             )
629
630             # close the old connection
631             old_user.connection.close()
632
633             # replace the old connection with this one
634             old_user.send(
635                u"$(eol)$(red)Taking over old connection from " \
636                   + old_user.address + u".$(nrm)"
637             )
638             old_user.connection = self.connection
639             old_user.last_address = old_user.address
640             old_user.address = self.address
641
642             # take this one out of the list and delete
643             self.remove()
644             del(self)
645             return_value = True
646             break
647
648       # true if an old connection was replaced, false if not
649       return return_value
650
651    def authenticate(self):
652       u"""Flag the user as authenticated and disconnect duplicates."""
653       if not self.state is u"authenticated":
654          log(u"User " + self.account.get(u"name") + u" logged in.", 2)
655          self.authenticated = True
656          if self.account.subkey in universe.categories[
657             u"internal"
658          ][
659             u"limits"
660          ].getlist(
661             u"default_admins"
662          ):
663             self.account.set(u"administrator", u"True")
664
665    def show_menu(self):
666       u"""Send the user their current menu."""
667       if not self.menu_seen:
668          self.menu_choices = get_menu_choices(self)
669          self.send(
670             get_menu(self.state, self.error, self.menu_choices),
671             u"",
672             add_terminator=True
673          )
674          self.menu_seen = True
675          self.error = False
676          self.adjust_echoing()
677
678    def adjust_echoing(self):
679       u"""Adjust echoing to match state menu requirements."""
680       import telnet
681       if telnet.is_enabled(self, telnet.TELOPT_ECHO, telnet.US):
682          if menu_echo_on(self.state):
683             telnet.disable(self, telnet.TELOPT_ECHO, telnet.US)
684       elif not menu_echo_on(self.state):
685          telnet.enable(self, telnet.TELOPT_ECHO, telnet.US)
686
687    def remove(self):
688       u"""Remove a user from the list of connected users."""
689       universe.userlist.remove(self)
690
691    def send(
692       self,
693       output,
694       eol=u"$(eol)",
695       raw=False,
696       flush=False,
697       add_prompt=True,
698       just_prompt=False,
699       add_terminator=False,
700       prepend_padding=True
701    ):
702       u"""Send arbitrary text to a connected user."""
703       import telnet
704
705       # unless raw mode is on, clean it up all nice and pretty
706       if not raw:
707
708          # strip extra $(eol) off if present
709          while output.startswith(u"$(eol)"): output = output[6:]
710          while output.endswith(u"$(eol)"): output = output[:-6]
711          extra_lines = output.find(u"$(eol)$(eol)$(eol)")
712          while extra_lines > -1:
713             output = output[:extra_lines] + output[extra_lines+6:]
714             extra_lines = output.find(u"$(eol)$(eol)$(eol)")
715
716          # start with a newline, append the message, then end
717          # with the optional eol string passed to this function
718          # and the ansi escape to return to normal text
719          if not just_prompt and prepend_padding:
720             if not self.output_queue \
721                or not self.output_queue[-1].endswith("\r\n"):
722                output = u"$(eol)" + output
723             elif not self.output_queue[-1].endswith(
724                "\r\n\x1b[0m\r\n"
725             ) and not self.output_queue[-1].endswith(
726                "\r\n\r\n"
727             ):
728                output = u"$(eol)" + output
729          output += eol + unichr(27) + u"[0m"
730
731          # tack on a prompt if active
732          if self.state == u"active":
733             if not just_prompt: output += u"$(eol)"
734             if add_prompt:
735                output += u"> "
736                mode = self.avatar.get(u"mode")
737                if mode: output += u"(" + mode + u") "
738
739          # find and replace macros in the output
740          output = replace_macros(self, output)
741
742          # wrap the text at the client's width (min 40, 0 disables)
743          if self.columns:
744             if self.columns < 40: wrap = 40
745             else: wrap = self.columns
746             output = wrap_ansi_text(output, wrap)
747
748          # if supported by the client, encode it utf-8
749          if telnet.is_enabled(self, telnet.TELOPT_BINARY, telnet.US):
750             encoded_output = output.encode(u"utf-8")
751
752          # otherwise just send ascii
753          else: encoded_output = output.encode(u"ascii", u"replace")
754
755          # end with a terminator if requested
756          if add_prompt or add_terminator:
757             if telnet.is_enabled(self, telnet.TELOPT_EOR, telnet.US):
758                encoded_output += telnet.telnet_proto(telnet.IAC, telnet.EOR)
759             elif not telnet.is_enabled(self, telnet.TELOPT_SGA, telnet.US):
760                encoded_output += telnet.telnet_proto(telnet.IAC, telnet.GA)
761
762          # and tack it onto the queue
763          self.output_queue.append(encoded_output)
764
765          # if this is urgent, flush all pending output
766          if flush: self.flush()
767
768       # just dump raw bytes as requested
769       else:
770          self.output_queue.append(output)
771          self.flush()
772
773    def pulse(self):
774       u"""All the things to do to the user per increment."""
775
776       # if the world is terminating, disconnect
777       if universe.terminate_flag:
778          self.state = u"disconnecting"
779          self.menu_seen = False
780
781       # check for an idle connection and act appropriately
782       else: self.check_idle()
783
784       # if output is paused, decrement the counter
785       if self.state == u"initial":
786          if self.negotiation_pause: self.negotiation_pause -= 1
787          else: self.state = u"entering_account_name"
788
789       # show the user a menu as needed
790       elif not self.state == u"active": self.show_menu()
791
792       # flush any pending output in the queue
793       self.flush()
794
795       # disconnect users with the appropriate state
796       if self.state == u"disconnecting": self.quit()
797
798       # check for input and add it to the queue
799       self.enqueue_input()
800
801       # there is input waiting in the queue
802       if self.input_queue:
803          handle_user_input(self)
804
805    def flush(self):
806       u"""Try to send the last item in the queue and remove it."""
807       if self.output_queue:
808          try:
809             self.connection.send(self.output_queue[0])
810             del self.output_queue[0]
811          except:
812             if self.account and self.account.get(u"name"):
813                account = self.account.get(u"name")
814             else: account = u"an unknown user"
815             log(
816                u"Sending to %s raised an exception (broken pipe?)." % account,
817                7
818             )
819             pass
820
821
822    def enqueue_input(self):
823       u"""Process and enqueue any new input."""
824       import telnet, unicodedata
825
826       # check for some input
827       try:
828          raw_input = self.connection.recv(1024)
829       except:
830          raw_input = ""
831
832       # we got something
833       if raw_input:
834
835          # tack this on to any previous partial
836          self.partial_input += raw_input
837
838          # reply to and remove any IAC negotiation codes
839          telnet.negotiate_telnet_options(self)
840
841          # separate multiple input lines
842          new_input_lines = self.partial_input.split("\n")
843
844          # if input doesn't end in a newline, replace the
845          # held partial input with the last line of it
846          if not self.partial_input.endswith("\n"):
847             self.partial_input = new_input_lines.pop()
848
849          # otherwise, chop off the extra null input and reset
850          # the held partial input
851          else:
852             new_input_lines.pop()
853             self.partial_input = ""
854
855          # iterate over the remaining lines
856          for line in new_input_lines:
857
858             # strip off extra whitespace
859             line = line.strip()
860
861             # make sure it's valid unicode (probably no longer needed)
862             try: unicode(line, u"utf-8")
863             except UnicodeDecodeError:
864                logline = u"Non-unicode data from "
865                if self.account and self.account.get(u"name"):
866                   logline += self.account.get(u"name") + u": "
867                else: logline += u"unknown user: "
868                logline += repr(line)
869                log(logline, 4)
870                line = ""
871
872             # log non-printable characters remaining
873             if telnet.is_enabled(self, telnet.TELOPT_BINARY, telnet.HIM):
874                asciiline = filter(lambda x: " " <= x <= "~", line)
875                if line != asciiline:
876                   logline = u"Non-ASCII characters from "
877                   if self.account and self.account.get(u"name"):
878                      logline += self.account.get(u"name") + u": "
879                   else: logline += u"unknown user: "
880                   logline += repr(line)
881                   log(logline, 4)
882                   line = asciiline
883
884             # put on the end of the queue
885             self.input_queue.append(
886                unicodedata.normalize( u"NFKC", unicode(line, u"utf-8") )
887             )
888
889    def new_avatar(self):
890       u"""Instantiate a new, unconfigured avatar for this user."""
891       counter = 0
892       while u"avatar:" + self.account.get(u"name") + u":" + unicode(
893          counter
894       ) in universe.categories[u"actor"].keys():
895          counter += 1
896       self.avatar = Element(
897          u"actor:avatar:" + self.account.get(u"name") + u":" + unicode(
898             counter
899          ),
900          universe
901       )
902       self.avatar.append(u"inherit", u"archetype:avatar")
903       self.account.append(u"avatars", self.avatar.key)
904
905    def delete_avatar(self, avatar):
906       u"""Remove an avatar from the world and from the user's list."""
907       if self.avatar is universe.contents[avatar]: self.avatar = None
908       universe.contents[avatar].destroy()
909       avatars = self.account.getlist(u"avatars")
910       avatars.remove(avatar)
911       self.account.set(u"avatars", avatars)
912
913    def activate_avatar_by_index(self, index):
914       u"""Enter the world with a particular indexed avatar."""
915       self.avatar = universe.contents[self.account.getlist(u"avatars")[index]]
916       self.avatar.owner = self
917       self.state = u"active"
918       self.avatar.go_home()
919
920    def deactivate_avatar(self):
921       u"""Have the active avatar leave the world."""
922       if self.avatar:
923          current = self.avatar.get(u"location")
924          if current:
925             self.avatar.set(u"default_location", current)
926             self.avatar.echo_to_location(
927                u"You suddenly wonder where " + self.avatar.get(
928                   u"name"
929                ) + u" went."
930             )
931             del universe.contents[current].contents[self.avatar.key]
932             self.avatar.remove_facet(u"location")
933          self.avatar.owner = None
934          self.avatar = None
935
936    def destroy(self):
937       u"""Destroy the user and associated avatars."""
938       for avatar in self.account.getlist(u"avatars"):
939          self.delete_avatar(avatar)
940       self.account.destroy()
941
942    def list_avatar_names(self):
943       u"""List names of assigned avatars."""
944       return [
945          universe.contents[avatar].get(
946             u"name"
947          ) for avatar in self.account.getlist(u"avatars")
948       ]
949
950 def broadcast(message, add_prompt=True):
951    u"""Send a message to all connected users."""
952    for each_user in universe.userlist:
953       each_user.send(u"$(eol)" + message, add_prompt=add_prompt)
954
955 def log(message, level=0):
956    u"""Log a message."""
957    import codecs, os.path, syslog, time
958
959    # a couple references we need
960    file_name = universe.categories[u"internal"][u"logging"].get(u"file")
961    max_log_lines = universe.categories[u"internal"][u"logging"].getint(
962       u"max_log_lines"
963    )
964    syslog_name = universe.categories[u"internal"][u"logging"].get(u"syslog")
965    timestamp = time.asctime()[4:19]
966
967    # turn the message into a list of lines
968    lines = filter(
969       lambda x: x!=u"", [ (x.rstrip()) for x in message.split(u"\n") ]
970    )
971
972    # send the timestamp and line to a file
973    if file_name:
974       if not os.path.isabs(file_name):
975          file_name = os.path.join(universe.startdir, file_name)
976       file_descriptor = codecs.open(file_name, u"a", u"utf-8")
977       for line in lines: file_descriptor.write(timestamp + u" " + line + u"\n")
978       file_descriptor.flush()
979       file_descriptor.close()
980
981    # send the timestamp and line to standard output
982    if universe.categories[u"internal"][u"logging"].getboolean(u"stdout"):
983       for line in lines: print(timestamp + u" " + line)
984
985    # send the line to the system log
986    if syslog_name:
987       syslog.openlog(
988          syslog_name.encode("utf-8"),
989          syslog.LOG_PID,
990          syslog.LOG_INFO | syslog.LOG_DAEMON
991       )
992       for line in lines: syslog.syslog(line)
993       syslog.closelog()
994
995    # display to connected administrators
996    for user in universe.userlist:
997       if user.state == u"active" and user.account.getboolean(
998          u"administrator"
999       ) and user.account.getint(u"loglevel") <= level:
1000          # iterate over every line in the message
1001          full_message = u""
1002          for line in lines:
1003             full_message += u"$(bld)$(red)" + timestamp + u" " + line.replace(
1004                u"$(", u"$_("
1005             ) + u"$(nrm)$(eol)"
1006          user.send(full_message, flush=True)
1007
1008    # add to the recent log list
1009    for line in lines:
1010       while 0 < len(universe.loglines) >= max_log_lines:
1011          del universe.loglines[0]
1012       universe.loglines.append((level, timestamp + u" " + line))
1013
1014 def get_loglines(level, start, stop):
1015    u"""Return a specific range of loglines filtered by level."""
1016
1017    # filter the log lines
1018    loglines = filter(lambda x: x[0]>=level, universe.loglines)
1019
1020    # we need these in several places
1021    total_count = unicode(len(universe.loglines))
1022    filtered_count = len(loglines)
1023
1024    # don't proceed if there are no lines
1025    if filtered_count:
1026
1027       # can't start before the begining or at the end
1028       if start > filtered_count: start = filtered_count
1029       if start < 1: start = 1
1030
1031       # can't stop before we start
1032       if stop > start: stop = start
1033       elif stop < 1: stop = 1
1034
1035       # some preamble
1036       message = u"There are " + unicode(total_count)
1037       message += u" log lines in memory and " + unicode(filtered_count)
1038       message += u" at or above level " + unicode(level) + u"."
1039       message += u" The matching lines from " + unicode(stop) + u" to "
1040       message += unicode(start) + u" are:$(eol)$(eol)"
1041
1042       # add the text from the selected lines
1043       if stop > 1: range_lines = loglines[-start:-(stop-1)]
1044       else: range_lines = loglines[-start:]
1045       for line in range_lines:
1046          message += u"   (" + unicode(line[0]) + u") " + line[1].replace(
1047             u"$(", u"$_("
1048          ) + u"$(eol)"
1049
1050    # there were no lines
1051    else:
1052       message = u"None of the " + unicode(total_count)
1053       message += u" lines in memory matches your request."
1054
1055    # pass it back
1056    return message
1057
1058 def glyph_columns(character):
1059    u"""Convenience function to return the column width of a glyph."""
1060    import unicodedata
1061    if unicodedata.east_asian_width(character) in u"FW": return 2
1062    else: return 1
1063
1064 def wrap_ansi_text(text, width):
1065    u"""Wrap text with arbitrary width while ignoring ANSI colors."""
1066    import unicodedata
1067
1068    # the current position in the entire text string, including all
1069    # characters, printable or otherwise
1070    absolute_position = 0
1071
1072    # the current text position relative to the begining of the line,
1073    # ignoring color escape sequences
1074    relative_position = 0
1075
1076    # the absolute position of the most recent whitespace character
1077    last_whitespace = 0
1078
1079    # whether the current character is part of a color escape sequence
1080    escape = False
1081
1082    # normalize any potentially composited unicode before we count it
1083    text = unicodedata.normalize(u"NFKC", text)
1084
1085    # iterate over each character from the begining of the text
1086    for each_character in text:
1087
1088       # the current character is the escape character
1089       if each_character == u"\x1b" and not escape:
1090          escape = True
1091
1092       # the current character is within an escape sequence
1093       elif escape:
1094
1095          # the current character is m, which terminates the
1096          # escape sequence
1097          if each_character == u"m":
1098             escape = False
1099
1100       # the current character is a newline, so reset the relative
1101       # position (start a new line)
1102       elif each_character == u"\n":
1103          relative_position = 0
1104          last_whitespace = absolute_position
1105
1106       # the current character meets the requested maximum line width,
1107       # so we need to backtrack and find a space at which to wrap;
1108       # special care is taken to avoid an off-by-one in case the
1109       # current character is a double-width glyph
1110       elif each_character != u"\r" and (
1111          relative_position >= width or (
1112             relative_position >= width -1 and glyph_columns(
1113                each_character
1114             ) == 2
1115          )
1116       ):
1117
1118          # it's always possible we landed on whitespace
1119          if unicodedata.category(each_character) in (u"Cc",u"Zs"):
1120             last_whitespace = absolute_position
1121
1122          # insert an eol in place of the space
1123          text = text[:last_whitespace] + u"\r\n" + text[last_whitespace + 1:]
1124
1125          # increase the absolute position because an eol is two
1126          # characters but the space it replaced was only one
1127          absolute_position += 1
1128
1129          # now we're at the begining of a new line, plus the
1130          # number of characters wrapped from the previous line
1131          relative_position = 0
1132          for remaining_characters in text[last_whitespace:absolute_position]:
1133             relative_position += glyph_columns(remaining_characters)
1134
1135       # as long as the character is not a carriage return and the
1136       # other above conditions haven't been met, count it as a
1137       # printable character
1138       elif each_character != u"\r":
1139          relative_position += glyph_columns(each_character)
1140          if unicodedata.category(each_character) in (u"Cc",u"Zs"):
1141             last_whitespace = absolute_position
1142
1143       # increase the absolute position for every character
1144       absolute_position += 1
1145
1146    # return the newly-wrapped text
1147    return text
1148
1149 def weighted_choice(data):
1150    u"""Takes a dict weighted by value and returns a random key."""
1151    import random
1152
1153    # this will hold our expanded list of keys from the data
1154    expanded = []
1155
1156    # create the expanded list of keys
1157    for key in data.keys():
1158       for count in range(data[key]):
1159          expanded.append(key)
1160
1161    # return one at random
1162    return random.choice(expanded)
1163
1164 def random_name():
1165    u"""Returns a random character name."""
1166    import random
1167
1168    # the vowels and consonants needed to create romaji syllables
1169    vowels = [
1170       u"a",
1171       u"i",
1172       u"u",
1173       u"e",
1174       u"o"
1175    ]
1176    consonants = [
1177       u"'",
1178       u"k",
1179       u"z",
1180       u"s",
1181       u"sh",
1182       u"z",
1183       u"j",
1184       u"t",
1185       u"ch",
1186       u"ts",
1187       u"d",
1188       u"n",
1189       u"h",
1190       u"f",
1191       u"m",
1192       u"y",
1193       u"r",
1194       u"w"
1195    ]
1196
1197    # this dict will hold our weighted list of syllables
1198    syllables = {}
1199
1200    # generate the list with an even weighting
1201    for consonant in consonants:
1202       for vowel in vowels:
1203          syllables[consonant + vowel] = 1
1204
1205    # we'll build the name into this string
1206    name = u""
1207
1208    # create a name of random length from the syllables
1209    for syllable in range(random.randrange(2, 6)):
1210       name += weighted_choice(syllables)
1211
1212    # strip any leading quotemark, capitalize and return the name
1213    return name.strip(u"'").capitalize()
1214
1215 def replace_macros(user, text, is_input=False):
1216    u"""Replaces macros in text output."""
1217    import codecs, data, os.path
1218
1219    # third person pronouns
1220    pronouns = {
1221       u"female": { u"obj": u"her", u"pos": u"hers", u"sub": u"she" },
1222       u"male": { u"obj": u"him", u"pos": u"his", u"sub": u"he" },
1223       u"neuter": { u"obj": u"it", u"pos": u"its", u"sub": u"it" }
1224       }
1225
1226    # a dict of replacement macros
1227    macros = {
1228       u"eol": u"\r\n",
1229       u"bld": unichr(27) + u"[1m",
1230       u"nrm": unichr(27) + u"[0m",
1231       u"blk": unichr(27) + u"[30m",
1232       u"blu": unichr(27) + u"[34m",
1233       u"cyn": unichr(27) + u"[36m",
1234       u"grn": unichr(27) + u"[32m",
1235       u"mgt": unichr(27) + u"[35m",
1236       u"red": unichr(27) + u"[31m",
1237       u"yel": unichr(27) + u"[33m",
1238       }
1239
1240    # add dynamic macros where possible
1241    if user.account:
1242       account_name = user.account.get(u"name")
1243       if account_name:
1244          macros[u"account"] = account_name
1245    if user.avatar:
1246       avatar_gender = user.avatar.get(u"gender")
1247       if avatar_gender:
1248          macros[u"tpop"] = pronouns[avatar_gender][u"obj"]
1249          macros[u"tppp"] = pronouns[avatar_gender][u"pos"]
1250          macros[u"tpsp"] = pronouns[avatar_gender][u"sub"]
1251
1252    # loop until broken
1253    while True:
1254
1255       # find and replace per the macros dict
1256       macro_start = text.find(u"$(")
1257       if macro_start == -1: break
1258       macro_end = text.find(u")", macro_start) + 1
1259       macro = text[macro_start+2:macro_end-1]
1260       if macro in macros.keys():
1261          replacement = macros[macro]
1262
1263       # this is how we handle local file inclusion (dangerous!)
1264       elif macro.startswith(u"inc:"):
1265          incfile = data.find_file(macro[4:], universe=universe)
1266          if os.path.exists(incfile):
1267             incfd = codecs.open(incfile, u"r", u"utf-8")
1268             replacement = u""
1269             for line in incfd:
1270                if line.endswith(u"\n") and not line.endswith(u"\r\n"):
1271                   line = line.replace(u"\n", u"\r\n")
1272                replacement += line
1273             # lose the trailing eol
1274             replacement = replacement[:-2]
1275          else:
1276             replacement = u""
1277             log(u"Couldn't read included " + incfile + u" file.", 6)
1278
1279       # if we get here, log and replace it with null
1280       else:
1281          replacement = u""
1282          if not is_input:
1283             log(u"Unexpected replacement macro " + macro + u" encountered.", 6)
1284
1285       # and now we act on the replacement
1286       text = text.replace(u"$(" + macro + u")", replacement)
1287
1288    # replace the look-like-a-macro sequence
1289    text = text.replace(u"$_(", u"$(")
1290
1291    return text
1292
1293 def escape_macros(text):
1294    u"""Escapes replacement macros in text."""
1295    return text.replace(u"$(", u"$_(")
1296
1297 def first_word(text, separator=u" "):
1298    u"""Returns a tuple of the first word and the rest."""
1299    if text:
1300       if text.find(separator) > 0: return text.split(separator, 1)
1301       else: return text, u""
1302    else: return u"", u""
1303
1304 def on_pulse():
1305    u"""The things which should happen on each pulse, aside from reloads."""
1306    import time
1307
1308    # open the listening socket if it hasn't been already
1309    if not hasattr(universe, u"listening_socket"):
1310       universe.initialize_server_socket()
1311
1312    # assign a user if a new connection is waiting
1313    user = check_for_connection(universe.listening_socket)
1314    if user: universe.userlist.append(user)
1315
1316    # iterate over the connected users
1317    for user in universe.userlist: user.pulse()
1318
1319    # add an element for counters if it doesn't exist
1320    if not u"counters" in universe.categories[u"internal"]:
1321       universe.categories[u"internal"][u"counters"] = Element(
1322          u"internal:counters", universe
1323       )
1324
1325    # update the log every now and then
1326    if not universe.categories[u"internal"][u"counters"].getint(u"mark"):
1327       log(unicode(len(universe.userlist)) + u" connection(s)")
1328       universe.categories[u"internal"][u"counters"].set(
1329          u"mark", universe.categories[u"internal"][u"time"].getint(
1330             u"frequency_log"
1331          )
1332       )
1333    else:
1334       universe.categories[u"internal"][u"counters"].set(
1335          u"mark", universe.categories[u"internal"][u"counters"].getint(
1336             u"mark"
1337          ) - 1
1338       )
1339
1340    # periodically save everything
1341    if not universe.categories[u"internal"][u"counters"].getint(u"save"):
1342       universe.save()
1343       universe.categories[u"internal"][u"counters"].set(
1344          u"save", universe.categories[u"internal"][u"time"].getint(
1345             u"frequency_save"
1346          )
1347       )
1348    else:
1349       universe.categories[u"internal"][u"counters"].set(
1350          u"save", universe.categories[u"internal"][u"counters"].getint(
1351             u"save"
1352          ) - 1
1353       )
1354
1355    # pause for a configurable amount of time (decimal seconds)
1356    time.sleep(universe.categories[u"internal"][u"time"].getfloat(u"increment"))
1357
1358    # increase the elapsed increment counter
1359    universe.categories[u"internal"][u"counters"].set(
1360       u"elapsed", universe.categories[u"internal"][u"counters"].getint(
1361          u"elapsed"
1362       ) + 1
1363    )
1364
1365 def reload_data():
1366    u"""Reload all relevant objects."""
1367    for user in universe.userlist[:]: user.reload()
1368    for element in universe.contents.values():
1369       if element.origin.is_writeable(): element.reload()
1370    universe.load()
1371
1372 def check_for_connection(listening_socket):
1373    u"""Check for a waiting connection and return a new user object."""
1374    import telnet
1375
1376    # try to accept a new connection
1377    try:
1378       connection, address = listening_socket.accept()
1379    except:
1380       return None
1381
1382    # note that we got one
1383    log(u"Connection from " + address[0], 2)
1384
1385    # disable blocking so we can proceed whether or not we can send/receive
1386    connection.setblocking(0)
1387
1388    # create a new user object
1389    user = User()
1390
1391    # associate this connection with it
1392    user.connection = connection
1393
1394    # set the user's ipa from the connection's ipa
1395    user.address = address[0]
1396
1397    # let the client know we WILL EOR (RFC 885)
1398    telnet.enable(user, telnet.TELOPT_EOR, telnet.US)
1399    user.negotiation_pause = 2
1400
1401    # return the new user object
1402    return user
1403
1404 def get_menu(state, error=None, choices=None):
1405    u"""Show the correct menu text to a user."""
1406
1407    # make sure we don't reuse a mutable sequence by default
1408    if choices is None: choices = {}
1409
1410    # get the description or error text
1411    message = get_menu_description(state, error)
1412
1413    # get menu choices for the current state
1414    message += get_formatted_menu_choices(state, choices)
1415
1416    # try to get a prompt, if it was defined
1417    message += get_menu_prompt(state)
1418
1419    # throw in the default choice, if it exists
1420    message += get_formatted_default_menu_choice(state)
1421
1422    # display a message indicating if echo is off
1423    message += get_echo_message(state)
1424
1425    # return the assembly of various strings defined above
1426    return message
1427
1428 def menu_echo_on(state):
1429    u"""True if echo is on, false if it is off."""
1430    return universe.categories[u"menu"][state].getboolean(u"echo", True)
1431
1432 def get_echo_message(state):
1433    u"""Return a message indicating that echo is off."""
1434    if menu_echo_on(state): return u""
1435    else: return u"(won't echo) "
1436
1437 def get_default_menu_choice(state):
1438    u"""Return the default choice for a menu."""
1439    return universe.categories[u"menu"][state].get(u"default")
1440
1441 def get_formatted_default_menu_choice(state):
1442    u"""Default menu choice foratted for inclusion in a prompt string."""
1443    default_choice = get_default_menu_choice(state)
1444    if default_choice: return u"[$(red)" + default_choice + u"$(nrm)] "
1445    else: return u""
1446
1447 def get_menu_description(state, error):
1448    u"""Get the description or error text."""
1449
1450    # an error condition was raised by the handler
1451    if error:
1452
1453       # try to get an error message matching the condition
1454       # and current state
1455       description = universe.categories[u"menu"][state].get(u"error_" + error)
1456       if not description: description = u"That is not a valid choice..."
1457       description = u"$(red)" + description + u"$(nrm)"
1458
1459    # there was no error condition
1460    else:
1461
1462       # try to get a menu description for the current state
1463       description = universe.categories[u"menu"][state].get(u"description")
1464
1465    # return the description or error message
1466    if description: description += u"$(eol)$(eol)"
1467    return description
1468
1469 def get_menu_prompt(state):
1470    u"""Try to get a prompt, if it was defined."""
1471    prompt = universe.categories[u"menu"][state].get(u"prompt")
1472    if prompt: prompt += u" "
1473    return prompt
1474
1475 def get_menu_choices(user):
1476    u"""Return a dict of choice:meaning."""
1477    menu = universe.categories[u"menu"][user.state]
1478    create_choices = menu.get(u"create")
1479    if create_choices: choices = eval(create_choices)
1480    else: choices = {}
1481    ignores = []
1482    options = {}
1483    creates = {}
1484    for facet in menu.facets():
1485       if facet.startswith(u"demand_") and not eval(
1486          universe.categories[u"menu"][user.state].get(facet)
1487       ):
1488          ignores.append(facet.split(u"_", 2)[1])
1489       elif facet.startswith(u"create_"):
1490          creates[facet] = facet.split(u"_", 2)[1]
1491       elif facet.startswith(u"choice_"):
1492          options[facet] = facet.split(u"_", 2)[1]
1493    for facet in creates.keys():
1494       if not creates[facet] in ignores:
1495          choices[creates[facet]] = eval(menu.get(facet))
1496    for facet in options.keys():
1497       if not options[facet] in ignores:
1498          choices[options[facet]] = menu.get(facet)
1499    return choices
1500
1501 def get_formatted_menu_choices(state, choices):
1502    u"""Returns a formatted string of menu choices."""
1503    choice_output = u""
1504    choice_keys = choices.keys()
1505    choice_keys.sort()
1506    for choice in choice_keys:
1507       choice_output += u"   [$(red)" + choice + u"$(nrm)]  " + choices[
1508          choice
1509       ] + u"$(eol)"
1510    if choice_output: choice_output += u"$(eol)"
1511    return choice_output
1512
1513 def get_menu_branches(state):
1514    u"""Return a dict of choice:branch."""
1515    branches = {}
1516    for facet in universe.categories[u"menu"][state].facets():
1517       if facet.startswith(u"branch_"):
1518          branches[
1519             facet.split(u"_", 2)[1]
1520          ] = universe.categories[u"menu"][state].get(facet)
1521    return branches
1522
1523 def get_default_branch(state):
1524    u"""Return the default branch."""
1525    return universe.categories[u"menu"][state].get(u"branch")
1526
1527 def get_choice_branch(user, choice):
1528    u"""Returns the new state matching the given choice."""
1529    branches = get_menu_branches(user.state)
1530    if choice in branches.keys(): return branches[choice]
1531    elif choice in user.menu_choices.keys():
1532       return get_default_branch(user.state)
1533    else: return u""
1534
1535 def get_menu_actions(state):
1536    u"""Return a dict of choice:branch."""
1537    actions = {}
1538    for facet in universe.categories[u"menu"][state].facets():
1539       if facet.startswith(u"action_"):
1540          actions[
1541             facet.split(u"_", 2)[1]
1542          ] = universe.categories[u"menu"][state].get(facet)
1543    return actions
1544
1545 def get_default_action(state):
1546    u"""Return the default action."""
1547    return universe.categories[u"menu"][state].get(u"action")
1548
1549 def get_choice_action(user, choice):
1550    u"""Run any indicated script for the given choice."""
1551    actions = get_menu_actions(user.state)
1552    if choice in actions.keys(): return actions[choice]
1553    elif choice in user.menu_choices.keys():
1554       return get_default_action(user.state)
1555    else: return u""
1556
1557 def handle_user_input(user):
1558    u"""The main handler, branches to a state-specific handler."""
1559    import telnet
1560
1561    # if the user's client echo is off, send a blank line for aesthetics
1562    if telnet.is_enabled(user, telnet.TELOPT_ECHO, telnet.US):
1563       user.send(u"", add_prompt=False, prepend_padding=False)
1564
1565    # check to make sure the state is expected, then call that handler
1566    if u"handler_" + user.state in globals():
1567       exec(u"handler_" + user.state + u"(user)")
1568    else:
1569       generic_menu_handler(user)
1570
1571    # since we got input, flag that the menu/prompt needs to be redisplayed
1572    user.menu_seen = False
1573
1574    # update the last_input timestamp while we're at it
1575    user.last_input = universe.get_time()
1576
1577 def generic_menu_handler(user):
1578    u"""A generic menu choice handler."""
1579
1580    # get a lower-case representation of the next line of input
1581    if user.input_queue:
1582       choice = user.input_queue.pop(0)
1583       if choice: choice = choice.lower()
1584    else: choice = u""
1585    if not choice: choice = get_default_menu_choice(user.state)
1586    if choice in user.menu_choices:
1587       exec(get_choice_action(user, choice))
1588       new_state = get_choice_branch(user, choice)
1589       if new_state: user.state = new_state
1590    else: user.error = u"default"
1591
1592 def handler_entering_account_name(user):
1593    u"""Handle the login account name."""
1594
1595    # get the next waiting line of input
1596    input_data = user.input_queue.pop(0)
1597
1598    # did the user enter anything?
1599    if input_data:
1600       
1601       # keep only the first word and convert to lower-case
1602       name = input_data.lower()
1603
1604       # fail if there are non-alphanumeric characters
1605       if name != filter(
1606          lambda x: x>=u"0" and x<=u"9" or x>=u"a" and x<=u"z", name
1607       ):
1608          user.error = u"bad_name"
1609
1610       # if that account exists, time to request a password
1611       elif name in universe.categories[u"account"]:
1612          user.account = universe.categories[u"account"][name]
1613          user.state = u"checking_password"
1614
1615       # otherwise, this could be a brand new user
1616       else:
1617          user.account = Element(u"account:" + name, universe)
1618          user.account.set(u"name", name)
1619          log(u"New user: " + name, 2)
1620          user.state = u"checking_new_account_name"
1621
1622    # if the user entered nothing for a name, then buhbye
1623    else:
1624       user.state = u"disconnecting"
1625
1626 def handler_checking_password(user):
1627    u"""Handle the login account password."""
1628    import password
1629
1630    # get the next waiting line of input
1631    input_data = user.input_queue.pop(0)
1632
1633    # does the hashed input equal the stored hash?
1634    if password.verify( input_data, user.account.get(u"passhash") ):
1635
1636       # if so, set the username and load from cold storage
1637       if not user.replace_old_connections():
1638          user.authenticate()
1639          user.state = u"main_utility"
1640
1641    # if at first your hashes don't match, try, try again
1642    elif user.password_tries < universe.categories[
1643       u"internal"
1644    ][
1645       u"limits"
1646    ].getint(
1647       u"password_tries"
1648    ) - 1:
1649       user.password_tries += 1
1650       user.error = u"incorrect"
1651
1652    # we've exceeded the maximum number of password failures, so disconnect
1653    else:
1654       user.send(
1655          u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1656       )
1657       user.state = u"disconnecting"
1658
1659 def handler_entering_new_password(user):
1660    u"""Handle a new password entry."""
1661    import password
1662
1663    # get the next waiting line of input
1664    input_data = user.input_queue.pop(0)
1665
1666    # make sure the password is strong--at least one upper, one lower and
1667    # one digit, seven or more characters in length
1668    if len(input_data) > 6 and len(
1669       filter( lambda x: x>=u"0" and x<=u"9", input_data )
1670    ) and len(
1671       filter( lambda x: x>=u"A" and x<=u"Z", input_data )
1672    ) and len(
1673       filter( lambda x: x>=u"a" and x<=u"z", input_data )
1674    ):
1675
1676       # hash and store it, then move on to verification
1677       user.account.set( u"passhash", password.create(input_data) )
1678       user.state = u"verifying_new_password"
1679
1680    # the password was weak, try again if you haven't tried too many times
1681    elif user.password_tries < universe.categories[
1682       u"internal"
1683    ][
1684       u"limits"
1685    ].getint(
1686       u"password_tries"
1687    ) - 1:
1688       user.password_tries += 1
1689       user.error = u"weak"
1690
1691    # too many tries, so adios
1692    else:
1693       user.send(
1694          u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1695       )
1696       user.account.destroy()
1697       user.state = u"disconnecting"
1698
1699 def handler_verifying_new_password(user):
1700    u"""Handle the re-entered new password for verification."""
1701    import password
1702
1703    # get the next waiting line of input
1704    input_data = user.input_queue.pop(0)
1705
1706    # hash the input and match it to storage
1707    if password.verify( input_data, user.account.get(u"passhash") ):
1708       user.authenticate()
1709
1710       # the hashes matched, so go active
1711       if not user.replace_old_connections(): user.state = u"main_utility"
1712
1713    # go back to entering the new password as long as you haven't tried
1714    # too many times
1715    elif user.password_tries < universe.categories[
1716       u"internal"
1717    ][
1718       u"limits"
1719    ].getint(
1720       u"password_tries"
1721    ) - 1:
1722       user.password_tries += 1
1723       user.error = u"differs"
1724       user.state = u"entering_new_password"
1725
1726    # otherwise, sayonara
1727    else:
1728       user.send(
1729          u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1730       )
1731       user.account.destroy()
1732       user.state = u"disconnecting"
1733
1734 def handler_active(user):
1735    u"""Handle input for active users."""
1736
1737    # get the next waiting line of input
1738    input_data = user.input_queue.pop(0)
1739
1740    # is there input?
1741    if input_data:
1742
1743       # split out the command and parameters
1744       actor = user.avatar
1745       mode = actor.get(u"mode")
1746       if mode and input_data.startswith(u"!"):
1747          command_name, parameters = first_word(input_data[1:])
1748       elif mode == u"chat":
1749          command_name = u"say"
1750          parameters = input_data
1751       else:
1752          command_name, parameters = first_word(input_data)
1753
1754       # lowercase the command
1755       command_name = command_name.lower()
1756
1757       # the command matches a command word for which we have data
1758       if command_name in universe.categories[u"command"]:
1759          command = universe.categories[u"command"][command_name]
1760       else: command = None
1761
1762       # if it's allowed, do it
1763       if actor.can_run(command): exec(command.get(u"action"))
1764
1765       # otherwise, give an error
1766       elif command_name: command_error(actor, input_data)
1767
1768    # if no input, just idle back with a prompt
1769    else: user.send(u"", just_prompt=True)
1770    
1771 def command_halt(actor, parameters):
1772    u"""Halt the world."""
1773    if actor.owner:
1774
1775       # see if there's a message or use a generic one
1776       if parameters: message = u"Halting: " + parameters
1777       else:
1778          message = u"User " + actor.owner.account.get(
1779             u"name"
1780          ) + u" halted the world."
1781
1782       # let everyone know
1783       broadcast(message, add_prompt=False)
1784       log(message, 8)
1785
1786       # set a flag to terminate the world
1787       universe.terminate_flag = True
1788
1789 def command_reload(actor):
1790    u"""Reload all code modules, configs and data."""
1791    if actor.owner:
1792
1793       # let the user know and log
1794       actor.send(u"Reloading all code modules, configs and data.")
1795       log(
1796          u"User " + actor.owner.account.get(u"name") + u" reloaded the world.",
1797          8
1798       )
1799
1800       # set a flag to reload
1801       universe.reload_flag = True
1802
1803 def command_quit(actor):
1804    u"""Leave the world and go back to the main menu."""
1805    if actor.owner:
1806       actor.owner.state = u"main_utility"
1807       actor.owner.deactivate_avatar()
1808
1809 def command_help(actor, parameters):
1810    u"""List available commands and provide help for commands."""
1811
1812    # did the user ask for help on a specific command word?
1813    if parameters and actor.owner:
1814
1815       # is the command word one for which we have data?
1816       if parameters in universe.categories[u"command"]:
1817          command = universe.categories[u"command"][parameters]
1818       else: command = None
1819
1820       # only for allowed commands
1821       if actor.can_run(command):
1822
1823          # add a description if provided
1824          description = command.get(u"description")
1825          if not description:
1826             description = u"(no short description provided)"
1827          if command.getboolean(u"administrative"): output = u"$(red)"
1828          else: output = u"$(grn)"
1829          output += parameters + u"$(nrm) - " + description + u"$(eol)$(eol)"
1830
1831          # add the help text if provided
1832          help_text = command.get(u"help")
1833          if not help_text:
1834             help_text = u"No help is provided for this command."
1835          output += help_text
1836
1837          # list related commands
1838          see_also = command.getlist(u"see_also")
1839          if see_also:
1840             really_see_also = u""
1841             for item in see_also:
1842                if item in universe.categories[u"command"]:
1843                   command = universe.categories[u"command"][item]
1844                   if actor.can_run(command):
1845                      if really_see_also:
1846                         really_see_also += u", "
1847                      if command.getboolean(u"administrative"):
1848                         really_see_also += u"$(red)"
1849                      else:
1850                         really_see_also += u"$(grn)"
1851                      really_see_also += item + u"$(nrm)"
1852             if really_see_also:
1853                output += u"$(eol)$(eol)See also: " + really_see_also
1854
1855       # no data for the requested command word
1856       else:
1857          output = u"That is not an available command."
1858
1859    # no specific command word was indicated
1860    else:
1861
1862       # give a sorted list of commands with descriptions if provided
1863       output = u"These are the commands available to you:$(eol)$(eol)"
1864       sorted_commands = universe.categories[u"command"].keys()
1865       sorted_commands.sort()
1866       for item in sorted_commands:
1867          command = universe.categories[u"command"][item]
1868          if actor.can_run(command):
1869             description = command.get(u"description")
1870             if not description:
1871                description = u"(no short description provided)"
1872             if command.getboolean(u"administrative"): output += u"   $(red)"
1873             else: output += u"   $(grn)"
1874             output += item + u"$(nrm) - " + description + u"$(eol)"
1875       output += u"$(eol)Enter \"help COMMAND\" for help on a command " \
1876          + u"named \"COMMAND\"."
1877
1878    # send the accumulated output to the user
1879    actor.send(output)
1880
1881 def command_move(actor, parameters):
1882    u"""Move the avatar in a given direction."""
1883    if parameters in universe.contents[actor.get(u"location")].portals():
1884       actor.move_direction(parameters)
1885    else: actor.send(u"You cannot go that way.")
1886
1887 def command_look(actor, parameters):
1888    u"""Look around."""
1889    if parameters: actor.send(u"You can't look at or in anything yet.")
1890    else: actor.look_at(actor.get(u"location"))
1891
1892 def command_say(actor, parameters):
1893    u"""Speak to others in the same room."""
1894    import unicodedata
1895
1896    # check for replacement macros and escape them
1897    parameters = escape_macros(parameters)
1898
1899    # if the message is wrapped in quotes, remove them and leave contents intact
1900    if parameters.startswith(u"\"") and parameters.endswith(u"\""):
1901       message = parameters[1:-1]
1902       literal = True
1903
1904    # otherwise, get rid of stray quote marks on the ends of the message
1905    else:
1906       message = parameters.strip(u"\"'`")
1907       literal = False
1908
1909    # the user entered a message
1910    if message:
1911
1912       # match the punctuation used, if any, to an action
1913       actions = universe.categories[u"internal"][u"language"].getdict(
1914          u"actions"
1915       )
1916       default_punctuation = universe.categories[u"internal"][u"language"].get(
1917          u"default_punctuation"
1918       )
1919       action = u""
1920       for mark in actions.keys():
1921          if not literal and message.endswith(mark):
1922             action = actions[mark]
1923             break
1924
1925       # add punctuation if needed
1926       if not action:
1927          action = actions[default_punctuation]
1928          if message and not (
1929             literal or unicodedata.category(message[-1]) == u"Po"
1930          ):
1931             message += default_punctuation
1932
1933       # failsafe checks to avoid unwanted reformatting and null strings
1934       if message and not literal:
1935
1936          # decapitalize the first letter to improve matching
1937          message = message[0].lower() + message[1:]
1938
1939          # iterate over all words in message, replacing typos
1940          typos = universe.categories[u"internal"][u"language"].getdict(
1941             u"typos"
1942          )
1943          words = message.split()
1944          for index in range(len(words)):
1945             word = words[index]
1946             while unicodedata.category(word[0]) == u"Po":
1947                word = word[1:]
1948             while unicodedata.category(word[-1]) == u"Po":
1949                word = word[:-1]
1950             if word in typos.keys():
1951                words[index] = words[index].replace(word, typos[word])
1952          message = u" ".join(words)
1953
1954          # capitalize the first letter
1955          message = message[0].upper() + message[1:]
1956
1957    # tell the room
1958    if message:
1959       actor.echo_to_location(
1960          actor.get(u"name") + u" " + action + u"s, \"" + message + u"\""
1961       )
1962       actor.send(u"You " + action + u", \"" + message + u"\"")
1963
1964    # there was no message
1965    else:
1966       actor.send(u"What do you want to say?")
1967
1968 def command_chat(actor):
1969    u"""Toggle chat mode."""
1970    mode = actor.get(u"mode")
1971    if not mode:
1972       actor.set(u"mode", u"chat")
1973       actor.send(u"Entering chat mode (use $(grn)!chat$(nrm) to exit).")
1974    elif mode == u"chat":
1975       actor.remove_facet(u"mode")
1976       actor.send(u"Exiting chat mode.")
1977    else: actor.send(u"Sorry, but you're already busy with something else!")
1978
1979 def command_show(actor, parameters):
1980    u"""Show program data."""
1981    import re
1982    message = u""
1983    arguments = parameters.split()
1984    if not parameters: message = u"What do you want to show?"
1985    elif arguments[0] == u"time":
1986       message = universe.categories[u"internal"][u"counters"].get(
1987          u"elapsed"
1988       ) + u" increments elapsed since the world was created."
1989    elif arguments[0] == u"categories":
1990       message = u"These are the element categories:$(eol)"
1991       categories = universe.categories.keys()
1992       categories.sort()
1993       for category in categories:
1994          message += u"$(eol)   $(grn)" + category + u"$(nrm)"
1995    elif arguments[0] == u"files":
1996       message = u"These are the current files containing the universe:$(eol)"
1997       filenames = universe.files.keys()
1998       filenames.sort()
1999       for filename in filenames:
2000          if universe.files[filename].is_writeable(): status = u"rw"
2001          else: status = u"ro"
2002          message += u"$(eol)   $(red)(" + status + u") $(grn)" + filename \
2003             + u"$(nrm)"
2004    elif arguments[0] == u"category":
2005       if len(arguments) != 2: message = u"You must specify one category."
2006       elif arguments[1] in universe.categories:
2007          message = u"These are the elements in the \"" + arguments[1] \
2008             + u"\" category:$(eol)"
2009          elements = [
2010             (
2011                universe.categories[arguments[1]][x].key
2012             ) for x in universe.categories[arguments[1]].keys()
2013          ]
2014          elements.sort()
2015          for element in elements:
2016             message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2017       else: message = u"Category \"" + arguments[1] + u"\" does not exist."
2018    elif arguments[0] == u"file":
2019       if len(arguments) != 2: message = u"You must specify one file."
2020       elif arguments[1] in universe.files:
2021          message = u"These are the elements in the \"" + arguments[1] \
2022             + u"\" file:$(eol)"
2023          elements = universe.files[arguments[1]].data.sections()
2024          elements.sort()
2025          for element in elements:
2026             message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2027       else: message = u"Category \"" + arguments[1] + u"\" does not exist."
2028    elif arguments[0] == u"element":
2029       if len(arguments) != 2: message = u"You must specify one element."
2030       elif arguments[1] in universe.contents:
2031          element = universe.contents[arguments[1]]
2032          message = u"These are the properties of the \"" + arguments[1] \
2033             + u"\" element (in \"" + element.origin.filename + u"\"):$(eol)"
2034          facets = element.facets()
2035          facets.sort()
2036          for facet in facets:
2037             message += u"$(eol)   $(grn)" + facet + u": $(red)" \
2038                + escape_macros(element.get(facet)) + u"$(nrm)"
2039       else: message = u"Element \"" + arguments[1] + u"\" does not exist."
2040    elif arguments[0] == u"result":
2041       if len(arguments) < 2: message = u"You need to specify an expression."
2042       else:
2043          try:
2044             message = repr(eval(u" ".join(arguments[1:])))
2045          except:
2046             message = u"Your expression raised an exception!"
2047    elif arguments[0] == u"log":
2048       if len(arguments) == 4:
2049          if re.match(u"^\d+$", arguments[3]) and int(arguments[3]) >= 0:
2050             stop = int(arguments[3])
2051          else: stop = -1
2052       else: stop = 0
2053       if len(arguments) >= 3:
2054          if re.match(u"^\d+$", arguments[2]) and int(arguments[2]) > 0:
2055             start = int(arguments[2])
2056          else: start = -1
2057       else: start = 10
2058       if len(arguments) >= 2:
2059          if re.match(u"^\d+$", arguments[1]) and 0 <= int(arguments[1]) <= 9:
2060             level = int(arguments[1])
2061          else: level = -1
2062       elif 0 <= actor.owner.account.getint(u"loglevel") <= 9:
2063          level = actor.owner.account.getint(u"loglevel")
2064       else: level = 1
2065       if level > -1 and start > -1 and stop > -1:
2066          message = get_loglines(level, start, stop)
2067       else:
2068          message = u"When specified, level must be 0-9 (default 1), " \
2069             + u"start and stop must be >=1 (default 10 and 1)."
2070    else: message = u"I don't know what \"" + parameters + u"\" is."
2071    actor.send(message)
2072
2073 def command_create(actor, parameters):
2074    u"""Create an element if it does not exist."""
2075    if not parameters:
2076       message = u"You must at least specify an element to create."
2077    elif not actor.owner: message = u""
2078    else:
2079       arguments = parameters.split()
2080       if len(arguments) == 1: arguments.append(u"")
2081       if len(arguments) == 2:
2082          element, filename = arguments
2083          if element in universe.contents:
2084             message = u"The \"" + element + u"\" element already exists."
2085          else:
2086             message = u"You create \"" + element + u"\" within the universe."
2087             logline = actor.owner.account.get(
2088                u"name"
2089             ) + u" created an element: " + element
2090             if filename:
2091                logline += u" in file " + filename
2092                if filename not in universe.files:
2093                   message += u" Warning: \"" + filename \
2094                      + u"\" is not yet included in any other file and will " \
2095                      + u"not be read on startup unless this is remedied."
2096             Element(element, universe, filename)
2097             log(logline, 6)
2098       elif len(arguments) > 2:
2099          message = u"You can only specify an element and a filename."
2100    actor.send(message)
2101
2102 def command_destroy(actor, parameters):
2103    u"""Destroy an element if it exists."""
2104    if actor.owner:
2105       if not parameters: message = u"You must specify an element to destroy."
2106       else:
2107          if parameters not in universe.contents:
2108             message = u"The \"" + parameters + u"\" element does not exist."
2109          else:
2110             universe.contents[parameters].destroy()
2111             message = u"You destroy \"" + parameters \
2112                + u"\" within the universe."
2113             log(
2114                actor.owner.account.get(
2115                   u"name"
2116                ) + u" destroyed an element: " + parameters,
2117                6
2118             )
2119       actor.send(message)
2120
2121 def command_set(actor, parameters):
2122    u"""Set a facet of an element."""
2123    if not parameters:
2124       message = u"You must specify an element, a facet and a value."
2125    else:
2126       arguments = parameters.split(u" ", 2)
2127       if len(arguments) == 1:
2128          message = u"What facet of element \"" + arguments[0] \
2129             + u"\" would you like to set?"
2130       elif len(arguments) == 2:
2131          message = u"What value would you like to set for the \"" \
2132             + arguments[1] + u"\" facet of the \"" + arguments[0] \
2133             + u"\" element?"
2134       else:
2135          element, facet, value = arguments
2136          if element not in universe.contents:
2137             message = u"The \"" + element + u"\" element does not exist."
2138          else:
2139             universe.contents[element].set(facet, value)
2140             message = u"You have successfully (re)set the \"" + facet \
2141                + u"\" facet of element \"" + element \
2142                + u"\". Try \"show element " + element + u"\" for verification."
2143    actor.send(message)
2144
2145 def command_delete(actor, parameters):
2146    u"""Delete a facet from an element."""
2147    if not parameters: message = u"You must specify an element and a facet."
2148    else:
2149       arguments = parameters.split(u" ")
2150       if len(arguments) == 1:
2151          message = u"What facet of element \"" + arguments[0] \
2152          + u"\" would you like to delete?"
2153       elif len(arguments) != 2:
2154          message = u"You may only specify an element and a facet."
2155       else:
2156          element, facet = arguments
2157          if element not in universe.contents:
2158             message = u"The \"" + element + u"\" element does not exist."
2159          elif facet not in universe.contents[element].facets():
2160             message = u"The \"" + element + u"\" element has no \"" + facet \
2161                + u"\" facet."
2162          else:
2163             universe.contents[element].remove_facet(facet)
2164             message = u"You have successfully deleted the \"" + facet \
2165                + u"\" facet of element \"" + element \
2166                + u"\". Try \"show element " + element + u"\" for verification."
2167    actor.send(message)
2168
2169 def command_error(actor, input_data):
2170    u"""Generic error for an unrecognized command word."""
2171    import random
2172
2173    # 90% of the time use a generic error
2174    if random.randrange(10):
2175       message = u"I'm not sure what \"" + input_data + u"\" means..."
2176
2177    # 10% of the time use the classic diku error
2178    else:
2179       message = u"Arglebargle, glop-glyf!?!"
2180
2181    # send the error message
2182    actor.send(message)
2183
2184 def daemonize(universe):
2185    u"""Fork and disassociate from everything."""
2186    import codecs, ctypes, ctypes.util, os, os.path, sys
2187
2188    # only if this is what we're configured to do
2189    if universe.contents[u"internal:process"].getboolean(u"daemon"):
2190
2191       # if possible, we want to rename the process to the same as the script
2192       # (these will need to be byte type during 2to3 migration)
2193       new_argv = "\0".join(sys.argv) + "\0"
2194       new_short_argv0 = os.path.basename(sys.argv[0]) + "\0"
2195
2196       # attempt the linux way first
2197       try:
2198          argv_array = ctypes.POINTER(ctypes.c_char_p)
2199          ctypes.pythonapi.Py_GetArgcArgv.argtypes = (
2200             ctypes.POINTER(ctypes.c_int),
2201             ctypes.POINTER(argv_array)
2202          )
2203          argc = argv_array()
2204          ctypes.pythonapi.Py_GetArgcArgv(
2205             ctypes.c_int(0),
2206             ctypes.pointer(argc)
2207          )
2208          old_argv0_size = len(argc.contents.value)
2209          ctypes.memset( argc.contents, 0, len(new_argv)+old_argv0_size )
2210          ctypes.memmove( argc.contents, new_argv, len(new_argv) )
2211          ctypes.CDLL( ctypes.util.find_library(u"c") ).prctl(
2212             15,
2213             new_short_argv0,
2214             0,
2215             0,
2216             0
2217          )
2218
2219       except:
2220
2221          # since that failed, maybe it's bsd?
2222          try:
2223
2224             # much simpler, since bsd has a libc function call for this
2225             ctypes.CDLL( ctypes.util.find_library(u"c") ).setproctitle(
2226                new_argv
2227             )
2228
2229          except:
2230
2231             # that didn't work either, so just log that we couldn't
2232             log(u"Failed to rename the interpreter process (cosmetic).")
2233
2234       # log before we start forking around, so the terminal gets the message
2235       log(u"Disassociating from the controlling terminal.")
2236
2237       # fork off and die, so we free up the controlling terminal
2238       if os.fork(): os._exit(0)
2239
2240       # switch to a new process group
2241       os.setsid()
2242
2243       # fork some more, this time to free us from the old process group
2244       if os.fork(): os._exit(0)
2245
2246       # reset the working directory so we don't needlessly tie up mounts
2247       os.chdir(u"/")
2248
2249       # clear the file creation mask so we can bend it to our will later
2250       os.umask(0)
2251
2252       # redirect stdin/stdout/stderr and close off their former descriptors
2253       for stdpipe in range(3): os.close(stdpipe)
2254       sys.stdin = codecs.open(u"/dev/null", u"r", u"utf-8")
2255       sys.__stdin__ = codecs.open(u"/dev/null", u"r", u"utf-8")
2256       sys.stdout = codecs.open(u"/dev/null", u"w", u"utf-8")
2257       sys.stderr = codecs.open(u"/dev/null", u"w", u"utf-8")
2258       sys.__stdout__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2259       sys.__stderr__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2260
2261 def create_pidfile(universe):
2262    u"""Write a file containing the current process ID."""
2263    import codecs, os, os.path
2264    pid = unicode(os.getpid())
2265    log(u"Process ID: " + pid)
2266    file_name = universe.contents[u"internal:process"].get(u"pidfile")
2267    if file_name:
2268       if not os.path.isabs(file_name):
2269          file_name = os.path.join(universe.startdir, file_name)
2270       file_descriptor = codecs.open(file_name, u"w", u"utf-8")
2271       file_descriptor.write(pid + u"\n")
2272       file_descriptor.flush()
2273       file_descriptor.close()
2274
2275 def remove_pidfile(universe):
2276    u"""Remove the file containing the current process ID."""
2277    import os, os.path
2278    file_name = universe.contents[u"internal:process"].get(u"pidfile")
2279    if file_name:
2280       if not os.path.isabs(file_name):
2281          file_name = os.path.join(universe.startdir, file_name)
2282       if os.access(file_name, os.W_OK): os.remove(file_name)
2283
2284 def excepthook(excepttype, value, tracebackdata):
2285    u"""Handle uncaught exceptions."""
2286    import traceback
2287
2288    # assemble the list of errors into a single string
2289    message = u"".join(
2290       traceback.format_exception(excepttype, value, tracebackdata)
2291    )
2292
2293    # try to log it, if possible
2294    try: log(message, 9)
2295    except: pass
2296
2297    # try to write it to stderr, if possible
2298    try: sys.stderr.write(message)
2299    except: pass
2300
2301 def sighook(what, where):
2302    u"""Handle external signals."""
2303    import signal
2304
2305    # a generic message
2306    message = u"Caught signal: "
2307
2308    # for a hangup signal
2309    if what == signal.SIGHUP:
2310       message += u"hangup (reloading)"
2311       universe.reload_flag = True
2312
2313    # for a terminate signal
2314    elif what == signal.SIGTERM:
2315       message += u"terminate (halting)"
2316       universe.terminate_flag = True
2317
2318    # catchall for unexpected signals
2319    else: message += unicode(what) + u" (unhandled)"
2320
2321    # log what happened
2322    log(message, 8)
2323
2324 def override_excepthook():
2325    u"""Redefine sys.excepthook with our own."""
2326    import sys
2327    sys.excepthook = excepthook
2328
2329 def assign_sighook():
2330    u"""Assign a customized handler for some signals."""
2331    import signal
2332    signal.signal(signal.SIGHUP, sighook)
2333    signal.signal(signal.SIGTERM, sighook)
2334
2335 def setup():
2336    """This contains functions to be performed when starting the engine."""
2337    import sys
2338
2339    # see if a configuration file was specified
2340    if len(sys.argv) > 1: conffile = sys.argv[1]
2341    else: conffile = u""
2342
2343    # the big bang
2344    global universe
2345    universe = Universe(conffile, True)
2346
2347    # log an initial message
2348    log(u"Started mudpy with command line: " + u" ".join(sys.argv))
2349
2350    # fork and disassociate
2351    daemonize(universe)
2352
2353    # override the default exception handler so we get logging first thing
2354    override_excepthook()
2355
2356    # set up custom signal handlers
2357    assign_sighook()
2358
2359    # make the pidfile
2360    create_pidfile(universe)
2361
2362    # pass the initialized universe back
2363    return universe
2364
2365 def finish():
2366    """This contains functions to be performed when shutting down the engine."""
2367
2368    # the loop has terminated, so save persistent data
2369    universe.save()
2370
2371    # log a final message
2372    log(u"Shutting down now.")
2373
2374    # get rid of the pidfile
2375    remove_pidfile(universe)