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