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