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