Package mudpy :: Module misc
[frames] | no frames]

Source Code for Module mudpy.misc

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