import socket, datetime, time, sys, threading, random, subprocess, os, json, signal, traceback, api, world, StringIO, ConfigParser
[docs]class Server:
def __init__(self, args, log, config, wrapper):
self.log = log
self.config = config
self.wrapper = wrapper
self.players = {}
self.status = 0 # 0 is off, 1 is starting, 2 is started, 3 is shutting down
self.start = True
self.sock = False
self.currentSecond = 0
self.backupInterval = 0
self.uuid = {}
self.data = ""
self.backups = []
self.oldServer = []
self.bootTime = time.time()
self.worldName = None
self.protocolVersion = -1 # -1 until proxy mode checks the server's MOTD on boot
self.version = None
self.world = world.World()
# Read server.properties and extract some information out of it
s = StringIO.StringIO() # Stupid StringIO doesn't support __exit__()
config = open("server.properties", "r").read()
s.write("[main]\n" + config)
s.seek(0)
self.properties = ConfigParser.ConfigParser(allow_no_value = True)
self.properties.readfp(s)
self.worldName = self.properties.get("main", "level-name")
#t = threading.Thread(target=self.readServerLog, args=())
# t.daemon = True
# t.start()
[docs] def login(self, user):
try:
if user not in self.players:
# time.sleep(1)
self.players[user] = api.Player(user, self.wrapper)
except:
traceback.print_exc()
[docs] def logout(self, user):
if self.wrapper.proxy:
for client in self.wrapper.proxy.clients:
uuid = self.players[user].uuid
# client.send(0x02, "json|byte", ({"text": "%s left the game BOIIIi (uuid %s)" % (user, uuid), "color":"purple"}, 0))
# print len(client.send(0x38, "varint|varint|uuid", (4, 1, self.players[user].uuid)))
if user in self.players:
del self.players[user]
[docs] def getPlayer(self, username):
""" Returns a player object with the specified name, or False if the user is not logged in/doesn't exist. """
if username in self.players:
return self.players[username]
return False
[docs] def argserver(self, i):
try: return self.line.split(" ")[i]
except: return ""
[docs] def argsAfter(self, i):
try: return " ".join(self.line.split(" ")[i:])
except: return ""
[docs] def getName(self):
return "Minecraft Server"
# -- IRC functions -- #
[docs] def msg(self, message):
if self.config["IRC"]["enabled"]:
self.wrapper.irc.msgQueue.append(message)
[docs] def filterName(self, name):
if self.config["IRC"]["obstruct-nicknames"]:
return "_" + name[1:]
else:
return name
# -- server management -- #
[docs] def announce(self, text):
for channel in self.config["IRC"]["channels"]:
self.send('PRIVMSG %s :%s' % (channel, text))
# self.console()
[docs] def console(self, text):
if not self.config["General"]["pre-1.7-mode"]:
self.run("tellraw @a %s" % json.dumps(text, encoding='utf-8'))
else:
say = ""
print text
for i in text:
if i == "extra":
for x in text[i]:
say += x["text"]
elif i == "text":
say += text[i]
self.run("say %s" % say)
[docs] def run(self, text):
try:
self.proc.stdin.write("%s\n" % text)
except:
pass
[docs] def captureSTDOUT(self):
while not self.wrapper.halt:
try:
data = self.proc.stdout.readline()
if len(data) > 0:
self.data += data
except:
continue
[docs] def captureSTDERR(self):
while not self.wrapper.halt:
try:
data = self.proc.stderr.readline()
if len(data) > 0:
self.data += data
except:
continue
[docs] def stripSpecialIRCChars(self, value):
for i in range(16):
value = value.replace("\x03%d" % i, "")
value = value.replace("\x0f", "")
return value
[docs] def startServer(self):
while not self.wrapper.halt:
if not self.start:
time.sleep(0.1)
continue
self.players = {}
self.status = 1
self.log.info("Starting server...")
self.wrapper.callEvent("server.start", {})
self.proc = subprocess.Popen(self.serverArgs, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
while True:
# timer & backup
if self.currentSecond == int(time.time()): # don't make fun of me for this
pass
else:
self.backupInterval += 1
self.currentSecond = int(time.time())
self.wrapper.callEvent("timer.second", {})
if self.config["General"]["timed-reboot"]:
if time.time() - self.bootTime > self.config["General"]["timed-reboot-seconds"]:
self.stop("Server is conducting a scheduled reboot. The server will be back momentarily!")
self.start = True
self.bootTime = time.time()
if self.backupInterval == self.config["Backups"]["backup-interval"] and self.config["Backups"]["enabled"] and self.wrapper.callEvent("wrapper.backupBegin", {}):
self.backupInterval = 0
if not os.path.exists(self.config["Backups"]["backup-location"]):
os.mkdir(self.config["Backups"]["backup-location"])
if len(self.backups) == 0 and os.path.exists(self.config["Backups"]["backup-location"] + "/backups.json"):
f = open(self.config["Backups"]["backup-location"] + "/backups.json", "r")
try:
self.backups = json.loads(f.read())
except:
self.log.error("NOTE - backups.json was unreadable. This might be due to corruption. This might lead to backups never being deleted.")
for channel in self.config["IRC"]["channels"]:
self.send("PRIVMSG %s :ERROR - backups.json is corrupted. Please contact an administer instantly, this may be critical." % (channel))
self.backups = []
f.close()
else:
if len(os.listdir(self.config["Backups"]["backup-location"])) > 0:
# import old backups from previous versions of Wrapper.py
backupTimestamps = []
for backupNames in os.listdir(self.config["Backups"]["backup-location"]):
try:
backupTimestamps.append(int(backupNames[backupNames.find('-')+1:backupNames.find('.')]))
except:
pass
backupTimestamps.sort()
for backupI in backupTimestamps:
self.backups.append((int(backupI), "backup-%s.tar" % str(backupI)))
if self.config["Backups"]["backup-notification"]:
self.msg("Backing up... prepare for lag")
self.console("\xc2\xa7bBacking up, IRC bridge will freeze and lag may occur")
timestamp = int(time.time())
filename = "backup-%s.tar" % datetime.datetime.fromtimestamp(int(timestamp)).strftime("%Y-%m-%d_%H:%M:%S")
self.run("save-all")
self.run("save-off")
time.sleep(0.5)
if not os.path.exists(str(self.config["Backups"]["backup-location"])):
os.mkdir(self.config["Backups"]["backup-location"])
arguments = ["tar", "cfpv", '%s/%s' % (self.config["Backups"]["backup-location"], filename)]
for file in self.config["Backups"]["backup-folders"]:
if os.path.exists(file):
arguments.append(file)
self.log.error("Backup file '%s' does not exist - will not backup")
statusCode = os.system(" ".join(arguments))
self.run("save-on")
if self.config["Backups"]["backup-notification"]:
self.console("\xc2\xa7aBackup complete!")
self.msg("Backup complete!")
self.wrapper.callEvent("wrapper.backupEnd", {"backupFile": filename, "status": statusCode})
self.backups.append((timestamp, 'backup-%s.tar' % datetime.datetime.fromtimestamp(int(timestamp)).strftime('%Y-%m-%d_%H:%M:%S')))
if len(self.backups) > self.config["Backups"]["backups-keep"]:
self.log.info("Deleting old backups...")
while len(self.backups) > self.config["Backups"]["backups-keep"]:
backup = self.backups[0]
if not self.wrapper.callEvent("wrapper.backupDelete", {"backupFile": filename}): break
try:
os.remove('%s/%s' % (self.config["Backups"]["backup-location"], backup[1]))
except:
print "Failed to delete"
self.log.info("Deleting old backup: %s" % datetime.datetime.fromtimestamp(int(backup[0])).strftime('%Y-%m-%d_%H:%M:%S'))
hink = self.backups[0][1][:]
del self.backups[0]
f = open(self.config["Backups"]["backup-location"] + "/backups.json", "w")
f.write(json.dumps(self.backups))
f.close()
if self.proc.poll() is not None:
self.status = 0
self.msg("Server shutdown")
self.log.info("Server shutdown")
if not self.config["General"]["auto-restart"]:
self.halt = True
sys.exit(0)
break
if len(self.data) > 0:
data = self.data.split("\n")
self.data = ""
else:
data = []
# parse server console output
deathPrefixes = ['fell', 'was', 'drowned', 'blew', 'walked', 'went', 'burned', 'hit', 'tried',
'died', 'got', 'starved', 'suffocated', 'withered']
for line in data:
if len(line) < 1: continue
self.line = line
self.wrapper.callEvent("server.consoleMessage", {"message": line})
print line
if self.argserver(3) is not False:
try:
if not self.config["General"]["pre-1.7-mode"]:
if self.argserver(3)[0] == "<":
name = self.formatForIRC(self.filterName(self.argserver(3)[1:self.argserver(3).find('>')]))
message = self.formatForIRC(" ".join(line.split(' ')[4:]).replace('\x1b', '').replace("\xc2\xfa", ""))
# self.msg("<%s> %s" % (name, message))
player = self.getPlayer(self.stripSpecialIRCChars(name))
self.wrapper.callEvent("player.message", {"player": player, "message": message})
elif self.argserver(3) == "Preparing" and self.argserver(4) == "level":
self.worldName = self.argserver(5).replace('"', "")
elif self.argserver(4) == "logged":
name = self.formatForIRC(self.filterName(self.argserver(3)[0:self.argserver(3).find('[')]))
self.msg("[%s connected]" % name)
self.login(name)
player = self.getPlayer(self.stripSpecialIRCChars(name))
self.wrapper.callEvent("player.join", {"player": player})
elif self.argserver(3)[0] == "[" and self.argserver(3)[-1] == "]":
name = self.argserver(3)[1:-1]
message = self.formatForIRC(" ".join(line.split(' ')[4:]).replace('\x1b', '').replace("\xc2\xfa", ""))
self.wrapper.callEvent("server.say", {"player": name, "message": message})
elif self.argserver(4) == 'lost':
name = self.filterName(self.argserver(3))
self.msg("[%s disconnected]" % (name))
player = self.getPlayer(self.stripSpecialIRCChars(name))
self.wrapper.callEvent("player.logout", {"player": player})
self.logout(name)
elif self.argserver(4) == 'issued': # this kinda doesn't work anymore unless you have a bukkit plugin.
name = self.filterName(self.argserver(3))
command = message = ' '.join(line.split(' ')[7:])
if self.config["IRC"]["forward-commands-to-irc"]:
self.msg("%s issued command: %s" % (name, command))
elif self.argserver(3) == 'Done':
self.status = 2
self.msg("Server started")
self.log.info("Server started")
self.wrapper.callEvent("server.started", {})
self.bootTime = time.time()
elif self.argserver(3) == 'Starting' and self.argserver(4) == "minecraft":
self.version = self.argserver(7)
elif self.argserver(4) in deathPrefixes:
self.msg(' '.join(self.line.split(' ')[3:]))
name = self.filterName(self.argserver(3))
deathMessage = self.config["Death"]["death-kick-messages"][random.randrange(0, len(self.config["Death"]["death-kick-messages"]))]
if self.config["Death"]["kick-on-death"] and name in self.config["Death"]["users-to-kick"]:
server.proc.stdin.write('kick %s %s\n\r' % (name, deathMessage))
player = self.getPlayer(self.stripSpecialIRCChars(name))
self.wrapper.callEvent("player.death", {"player": player, "death": self.argsAfter(4)})
elif self.argserver(3) == "*":
name = self.formatForIRC(self.filterName(self.argserver(4)))
message = message = ' '.join(line.split(' ')[5:])
self.msg("* %s %s" % (name, message))
player = self.getPlayer(self.stripSpecialIRCChars(name))
self.wrapper.callEvent("player.action", {"player": player, "action": message})
elif self.argserver(4) == "has" and self.argserver(8) == "achievement":
name = self.filterName(self.argserver(3))
achievement = ' '.join(line.split(' ')[9:])
self.msg("%s has just earned the achievement %s" % (name, achievement))
self.wrapper.callEvent("player.achievement", {"player": name, "achievement": achievement})
else: # -- FOR 1.6.4 AND PREVIOUSLY ONLY!!!!! --
if self.argserver(3)[0] == '<':
name = self.filterName(self.argserver(3)[1:self.argserver(3).find('>')].replace("\xc2\xfa", ""))
message = ' '.join(line.split(' ')[4:]).replace('\x1b', '').replace("\xc2\xfa", "")
self.msg('<%s> %s' % (name, message))
self.wrapper.callEvent("player.message", {"player": self.stripSpecialIRCChars(name), "message": message})
elif self.argserver(4) == 'logged':
name = self.argserver(3)[0:self.argserver(3).find('[')]
self.msg('[%s connected]' % name)
self.login(name)
self.wrapper.callEvent("player.join", {"player": name})
elif self.argserver(4) == 'lost':
name = self.argserver(3)
reason = self.argserver(6)
self.msg('[%s disconnected] (%s)' % (name, reason))
self.logout(name)
self.wrapper.callEvent("player.logout", {"player": name})
elif self.argserver(3) == "*":
name = self.formatForIRC(self.filterName(self.argserver(4)))
message = message = ' '.join(line.split(' ')[5:])
self.msg("* %s %s" % (name, message))
self.wrapper.callEvent("player.action", {"player": name, "action": message})
elif self.argserver(3) == 'Kicked':
name = self.argserver(6)
self.msg('[%s disconnected] (Kicked :O)' % name)
self.logout(name)
self.wrapper.callEvent("player.logout", {"player": name})
elif self.argserver(3) == 'issued':
name = self.argserver(2)
command = message = ' '.join(line.split(' ')[6:])
if self.config["forwardCommandsToIRC"]:
self.msg('%s issued command: %s' % (name, command))
elif self.argserver(3) == 'Done':
self.status = 2
self.msg('Server started')
elif self.argserver(3) in deathPrefixes:
self.msg(' '.join(self.line.split(' ')[2:]))
name = self.argserver(2)
randThing = self.config["deathKickMessages"][random.randrange(0, len(self.config["deathKickMessages"]))]
if self.config["deathKick"] and name in self.config["deathKickers"]:
self.run("kick %s %s" % (name, randThing))
self.wrapper.callEvent("player.death", {"player": self.stripSpecialIRCChars(name), "death": self.argsAfter(4)})
except:
pass
time.sleep(0.1)
# functions useful for the API
[docs] def stop(self, message="Server going down for maintenance"):
self.start = False
for name in self.players:
self.run("kick %s %s" % (name, message))
time.sleep(0.2)
self.run("stop")