split utils/campaigns_client.py into a re-usable module plus a script

This commit is contained in:
Elias Pschernig 2007-06-02 15:35:20 +00:00
parent ea4287e5d9
commit d9c12066ea
3 changed files with 426 additions and 419 deletions

View file

@ -145,7 +145,10 @@ ACLOCAL_AMFLAGS = -I m4
EXTRA_DIST = config/config.rpath config/mkinstalldirs config/py-compile
if PYTHON
pkgpython_PYTHON = data/tools/wesnoth/wmltools.py data/tools/wesnoth/wmldata.py data/tools/wesnoth/wmlparser.py \
pkgpython_PYTHON = data/tools/wesnoth/wmltools.py \
data/tools/wesnoth/wmldata.py \
data/tools/wesnoth/wmlparser.py \
data/tools/wesnoth/campaignserver_client.py \
data/tools/wesnoth/__init__.py
endif

View file

@ -0,0 +1,409 @@
import socket, struct, glob, sys, shutil, threading
import wesnoth.wmldata as wmldata
class CampaignClient:
# First port listed will bw used as default.
portmap = (("15003", "1.3.x"), ("15004", "1.2.x"))
def __init__(self, address = None):
"""
Return a new connection to the campaign server at the given address.
"""
self.length = 0 # length of last processed packet
self.counter = 0 # position inside above packet
self.words = {} # dictionary for WML decoder
self.wordcount = 4 # codewords in above dictionary
self.codes = {} # dictionary for WML encoder
self.codescount = 4 # codewords in above dictionary
if address != None:
s = address.split(":")
if len(s) == 2:
self.host, self.port = s
else:
self.host = s[0]
self.port = self.portmap[0][0]
self.port = int(self.port)
self.canceled = False
self.error = False
addr = socket.getaddrinfo(self.host, self.port, socket.AF_INET,
socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
sys.stderr.write("Opening socket to %s" % address)
bfwv = dict(self.portmap).get(str(self.port))
if bfwv:
sys.stderr.write(" for " + bfwv + "\n")
else:
sys.stderr.write("\n")
self.sock = socket.socket(addr[0], addr[1], addr[2])
self.sock.connect(addr[4])
self.sock.send(struct.pack("!l", 0))
try:
connection_num = self.sock.recv(4)
except socket.error:
connection_num = struct.pack("!l", -1)
self.error = True
sys.stderr.write("Connected as %d.\n" % struct.unpack(
"!l", connection_num))
def async_cancel(self):
"""
Call from another thread to cancel any current network operation.
"""
self.canceled = True
def __del__(self):
if self.canceled:
sys.stderr.write("Canceled socket.\n")
elif self.error:
sys.stderr.write("Unexpected disconnection.\n")
else:
sys.stderr.write("Closing socket.\n")
try:
self.sock.shutdown(2)
except socket.error:
pass # Well, what can we do?
def send_packet(self, packet):
"""
Send binary data to the server.
"""
packet = struct.pack("!l", len(packet)) + packet
l = len(packet)
self.length = l
s = 0
while s < l and not self.canceled:
s += self.sock.send(packet[s:])
self.counter = s
def read_packet(self):
"""
Read binary data from the server.
"""
l = struct.unpack("!l", self.sock.recv(4))[0]
self.length = l
packet = ""
while len(packet) < l and not self.canceled:
packet += self.sock.recv(l - len(packet))
self.counter = len(packet)
if self.canceled: return None
return packet
def decode_WML(self, data):
"""
Given a block of binary data, decode it as binary WML and return it
as a WML object.
The binary WML format is a byte stream. The format is:
0 3 <name> means a new element <name> is opened
0 4..255 means the same as 0 3 but with a coded name
1 means the current element is closed
2 <name> means, add a new code for <name> to the dictionary
3 <name> <data> means, a new attribute <name> is added with <data>
4..255 <data> means, the same as above but with a coded name
For example if we got:
[message]text=blah[/message]
[message]text=bleh[/message]
It would look like:
0 2 message 4 2 text 5 blah 1 0 4 5 bleh 1
This would be the same:
0 3 message 3 text blah 1 0 3 message 3 text bleh 1
"""
WML = wmldata.DataSub("campaign_server")
pos = [0]
tag = [WML]
open_element = False
def done():
return pos[0] >= len(data)
def next():
c = data[pos[0]]
pos[0] += 1
return c
def literal():
s = pos[0]
e = data.find("\00", s)
pack = data[s:e]
pack = pack.replace("\01\01", "\00")
pack = pack.replace("\01\02", "\01")
pos[0] = e + 1
return pack
while not done():
code = ord(next())
if code == 0: # open element (name code follows)
open_element = True
elif code == 1: # close current element
tag.pop()
elif code == 2: # add code
self.words[self.wordcount] = literal()
self.wordcount += 1
else:
if code == 3: word = literal() # literal word
else: word = self.words[code] # code
if open_element: # we handle opening an element
element = wmldata.DataSub(word)
tag[-1].insert(element) # add it to the current one
tag.append(element) # put to our stack to keep track
elif word == "contents": # detect any binary attributes
binary = wmldata.DataBinary(word, literal())
tag[-1].insert(binary)
else: # others are text attributes
text = wmldata.DataText(word, literal())
tag[-1].insert(text)
open_element = False
return WML
def encode_WML(self, data):
"""
Given a WML object, encode it into binary WML and return it as
a python string.
"""
packet = str("")
def literal(data):
if not data: return str("")
data = data.replace("\01", "\01\02")
data = data.replace("\00", "\01\01")
return str(data)
def encode(name):
if name in self.codes:
return self.codes[name]
what = literal(name)
# Gah.. why didn't nobody tell me about the 20 characters limit?
# Without adhering to this (hardcoded in the C++ code) limit, the
# campaign server simply will crash if we talk to it.
if len(what) <= 20 and self.codescount < 256:
data = "\02" + what + "\00" + chr(self.codescount)
self.codes[name] = chr(self.codescount)
self.codescount += 1
return data
return "\03" + what + "\00"
if isinstance(data, wmldata.DataSub):
packet += "\00" + encode(data.name)
for item in data.data:
encoded = self.encode_WML(item)
packet += encoded
packet += "\01"
elif isinstance(data, wmldata.DataText):
packet += encode(data.name)
packet += literal(data.data) + "\00"
elif isinstance(data, wmldata.DataBinary):
packet += encode(data.name)
packet += literal(data.data) + "\00"
return packet
def list_campaigns(self):
"""
Returns a WML object containing all available info from the server.
"""
if self.error: return None
request = wmldata.DataSub("request_campaign_list")
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def validate_campaign(self, name, passphrase):
"""
Validates python scripts in the named campaign.
"""
request = wmldata.DataSub("validate_scripts")
request.set_text_val("name", name)
request.set_text_val("master_password", passphrase)
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def delete_campaign(self, name, passphrase):
"""
Deletes the named campaign on the server.
"""
request = wmldata.DataSub("delete")
request.set_text_val("name", name)
request.set_text_val("passphrase", passphrase)
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def change_passphrase(self, name, old, new):
"""
Changes the passphrase of a campaign on the server.
"""
request = wmldata.DataSub("change_passphrase")
request.set_text_val("name", name)
request.set_text_val("passphrase", old)
request.set_text_val("new_passphrase", new)
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def get_campaign_raw(self, name):
"""
Downloads the named campaign and returns it as a raw binary WML packet.
"""
request = wmldata.DataSub("request_campaign")
request.insert(wmldata.DataText("name", name))
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
if self.canceled: return None
return packet
def get_campaign(self, name):
"""
Downloads the named campaign and returns it as a WML object.
"""
packet = self.get_campaign_raw(name)
if packet: return self.decode_WML(packet)
return None
def put_campaign(self, title, name, author, passphrase, description,
version, icon, cfgfile, directory):
"""
Uploads a campaign to the server. The title, name, author, passphrase,
description, version and icon parameters are what would normally be
found in a .pbl file.
The cfgfile is the name of the main .cfg file of the campaign.
The directory is the name of the campaign's directory.
"""
request = wmldata.DataSub("upload")
request.set_text_val("title", title)
request.set_text_val("name", name)
request.set_text_val("author", author)
request.set_text_val("passphrase", passphrase)
request.set_text_val("description", description)
request.set_text_val("version", version)
request.set_text_val("icon", icon)
data = wmldata.DataSub("data")
def put_file(name, f):
data = wmldata.DataSub("file")
data.set_text_val("name", name)
data.insert(wmldata.DataBinary("contents", f.read()))
return data
def put_dir(name, path):
data = wmldata.DataSub("dir")
data.set_text_val("name", name)
for fn in glob.glob(path + "/*"):
if os.path.isdir(fn):
sub = put_dir(os.path.basename(fn), fn)
else:
sub = put_file(os.path.basename(fn), file(fn))
data.insert(sub)
return data
data.insert(put_file(name + ".cfg", file(cfgfile)))
data.insert(put_dir(name, directory))
request.insert(data)
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def get_campaign_async(self, name, raw = False):
"""
This is like get_campaign, but returns immediately, doing server
communications in a background thread.
"""
class MyThread(threading.Thread):
def run(self):
if raw:
data = self.cs.get_campaign_raw(self.name)
else:
data = self.cs.get_campaign(self.name)
self.data = data
self.event.set()
def cancel(self):
self.data = None
self.event.set()
self.cs.async_cancel()
mythread = MyThread()
mythread.cs = self
mythread.name = name
mythread.event = threading.Event()
mythread.start()
return mythread
def put_campaign_async(self, *args):
"""
This is like put_campaign, but returns immediately, doing server
communications in a background thread.
"""
class MyThread(threading.Thread):
def run(self):
data = self.cs.put_campaign(*self.args)
self.data = data
self.event.set()
def cancel(self):
self.data = None
self.event.set()
self.cs.async_cancel()
mythread = MyThread()
mythread.cs = self
mythread.name = args[1]
mythread.args = args
mythread.event = threading.Event()
mythread.start()
return mythread
def unpackdir(self, data, path, i = 0, verbose = False):
"""
Call this to unpack a campaign contained in a WML object to the
filesystem. The data parameter is the WML object, path is the path under
which it will be placed.
"""
try:
os.mkdir(path)
except:
pass
for f in data.get_all("file"):
name = f.get_text_val("name", "?")
contents = f.get_binary_val("contents")
if contents:
if verbose:
print i * " " + name + " (" +\
str(len(contents)) + ")"
file(path + "/" + name, "wb").write(contents)
for dir in data.get_all("dir"):
name = dir.get_text_val("name", "?")
shutil.rmtree(path + "/" + name, True)
os.mkdir(path + "/" + name)
if verbose:
print i * " " + name
self.unpackdir(dir, path + "/" + name, i + 2, verbose)

View file

@ -1,418 +1,11 @@
#!/usr/bin/env python
# encoding: utf8
import socket, struct, sys, glob, os.path, shutil, threading, re
import sys, os.path, re
# in case the wesnoth python package has not been installed
sys.path.append("data/tools")
import wesnoth.wmldata
# First port listed will bw used as default.
portmap = (("15003", "1.3.x"), ("15004", "1.2.x"))
class CampaignBrowser:
def __init__(self, address = None):
"""
Return a new connection to the campaign server at the given address.
"""
self.length = 0 # length of last processed packet
self.counter = 0 # position inside above packet
self.words = {} # dictionary for WML decoder
self.wordcount = 4 # codewords in above dictionary
self.codes = {} # dictionary for WML encoder
self.codescount = 4 # codewords in above dictionary
if address != None:
s = address.split(":")
if len(s) == 2:
self.host, self.port = s
else:
self.host = s[0]
self.port = portmap[0][0]
self.port = int(self.port)
self.canceled = False
self.error = False
addr = socket.getaddrinfo(self.host, self.port, socket.AF_INET,
socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
sys.stderr.write("Opening socket to %s" % address)
bfwv = dict(portmap).get(str(self.port))
if bfwv:
sys.stderr.write(" for " + bfwv + "\n")
else:
sys.stderr.write("\n")
self.sock = socket.socket(addr[0], addr[1], addr[2])
self.sock.connect(addr[4])
self.sock.send(struct.pack("!l", 0))
try:
connection_num = self.sock.recv(4)
except socket.error:
connection_num = struct.pack("!l", -1)
self.error = True
sys.stderr.write("Connected as %d.\n" % struct.unpack(
"!l", connection_num))
def async_cancel(self):
"""
Call from another thread to cancel any current network operation.
"""
self.canceled = True
def __del__(self):
if self.canceled:
sys.stderr.write("Canceled socket.\n")
elif self.error:
sys.stderr.write("Unexpected disconnection.\n")
else:
sys.stderr.write("Closing socket.\n")
try:
self.sock.shutdown(2)
except socket.error:
pass # Well, what can we do?
def send_packet(self, packet):
"""
Send binary data to the server.
"""
packet = struct.pack("!l", len(packet)) + packet
l = len(packet)
self.length = l
s = 0
while s < l and not self.canceled:
s += self.sock.send(packet[s:])
self.counter = s
def read_packet(self):
"""
Read binary data from the server.
"""
l = struct.unpack("!l", self.sock.recv(4))[0]
self.length = l
packet = ""
while len(packet) < l and not self.canceled:
packet += self.sock.recv(l - len(packet))
self.counter = len(packet)
if self.canceled: return None
return packet
def decode_WML(self, data):
"""
Given a block of binary data, decode it as binary WML and return it
as a WML object.
The binary WML format is a byte stream. The format is:
0 3 <name> means a new element <name> is opened
0 4..255 means the same as 0 3 but with a coded name
1 means the current element is closed
2 <name> means, add a new code for <name> to the dictionary
3 <name> <data> means, a new attribute <name> is added with <data>
4..255 <data> means, the same as above but with a coded name
For example if we got:
[message]text=blah[/message]
[message]text=bleh[/message]
It would look like:
0 2 message 4 2 text 5 blah 1 0 4 5 bleh 1
This would be the same:
0 3 message 3 text blah 1 0 3 message 3 text bleh 1
"""
WML = wmldata.DataSub("campaign_server")
pos = [0]
tag = [WML]
open_element = False
def done():
return pos[0] >= len(data)
def next():
c = data[pos[0]]
pos[0] += 1
return c
def literal():
s = pos[0]
e = data.find("\00", s)
pack = data[s:e]
pack = pack.replace("\01\01", "\00")
pack = pack.replace("\01\02", "\01")
pos[0] = e + 1
return pack
while not done():
code = ord(next())
if code == 0: # open element (name code follows)
open_element = True
elif code == 1: # close current element
tag.pop()
elif code == 2: # add code
self.words[self.wordcount] = literal()
self.wordcount += 1
else:
if code == 3: word = literal() # literal word
else: word = self.words[code] # code
if open_element: # we handle opening an element
element = wmldata.DataSub(word)
tag[-1].insert(element) # add it to the current one
tag.append(element) # put to our stack to keep track
elif word == "contents": # detect any binary attributes
binary = wmldata.DataBinary(word, literal())
tag[-1].insert(binary)
else: # others are text attributes
text = wmldata.DataText(word, literal())
tag[-1].insert(text)
open_element = False
return WML
def encode_WML(self, data):
"""
Given a WML object, encode it into binary WML and return it as
a python string.
"""
packet = str("")
def literal(data):
if not data: return str("")
data = data.replace("\01", "\01\02")
data = data.replace("\00", "\01\01")
return str(data)
def encode(name):
if name in self.codes:
return self.codes[name]
what = literal(name)
# Gah.. why didn't nobody tell me about the 20 characters limit?
# Without adhering to this (hardcoded in the C++ code) limit, the
# campaign server simply will crash if we talk to it.
if len(what) <= 20 and self.codescount < 256:
data = "\02" + what + "\00" + chr(self.codescount)
self.codes[name] = chr(self.codescount)
self.codescount += 1
return data
return "\03" + what + "\00"
if isinstance(data, wmldata.DataSub):
packet += "\00" + encode(data.name)
for item in data.data:
encoded = self.encode_WML(item)
packet += encoded
packet += "\01"
elif isinstance(data, wmldata.DataText):
packet += encode(data.name)
packet += literal(data.data) + "\00"
elif isinstance(data, wmldata.DataBinary):
packet += encode(data.name)
packet += literal(data.data) + "\00"
return packet
def list_campaigns(self):
"""
Returns a WML object containing all available info from the server.
"""
if self.error: return None
request = wmldata.DataSub("request_campaign_list")
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def validate_campaign(self, name, passphrase):
"""
Validates python scripts in the named campaign.
"""
request = wmldata.DataSub("validate_scripts")
request.set_text_val("name", name)
request.set_text_val("master_password", passphrase)
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def delete_campaign(self, name, passphrase):
"""
Deletes the named campaign on the server.
"""
request = wmldata.DataSub("delete")
request.set_text_val("name", name)
request.set_text_val("passphrase", passphrase)
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def change_passphrase(self, name, old, new):
"""
Changes the passphrase of a campaign on the server.
"""
request = wmldata.DataSub("change_passphrase")
request.set_text_val("name", name)
request.set_text_val("passphrase", old)
request.set_text_val("new_passphrase", new)
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def get_campaign_raw(self, name):
"""
Downloads the named campaign and returns it as a raw binary WML packet.
"""
request = wmldata.DataSub("request_campaign")
request.insert(wmldata.DataText("name", name))
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
if self.canceled: return None
return packet
def get_campaign(self, name):
"""
Downloads the named campaign and returns it as a WML object.
"""
packet = self.get_campaign_raw(name)
if packet: return self.decode_WML(packet)
return None
def put_campaign(self, title, name, author, passphrase, description,
version, icon, cfgfile, directory):
"""
Uploads a campaign to the server. The title, name, author, passphrase,
description, version and icon parameters are what would normally be
found in a .pbl file.
The cfgfile is the name of the main .cfg file of the campaign.
The directory is the name of the campaign's directory.
"""
request = wmldata.DataSub("upload")
request.set_text_val("title", title)
request.set_text_val("name", name)
request.set_text_val("author", author)
request.set_text_val("passphrase", passphrase)
request.set_text_val("description", description)
request.set_text_val("version", version)
request.set_text_val("icon", icon)
data = wmldata.DataSub("data")
def put_file(name, f):
data = wmldata.DataSub("file")
data.set_text_val("name", name)
data.insert(wmldata.DataBinary("contents", f.read()))
return data
def put_dir(name, path):
data = wmldata.DataSub("dir")
data.set_text_val("name", name)
for fn in glob.glob(path + "/*"):
if os.path.isdir(fn):
sub = put_dir(os.path.basename(fn), fn)
else:
sub = put_file(os.path.basename(fn), file(fn))
data.insert(sub)
return data
data.insert(put_file(name + ".cfg", file(cfgfile)))
data.insert(put_dir(name, directory))
request.insert(data)
packet = self.encode_WML(request)
self.send_packet(packet);
packet = self.read_packet()
data = self.decode_WML(packet)
return data
def get_campaign_async(self, name, raw = False):
"""
This is like get_campaign, but returns immediately, doing server
communications in a background thread.
"""
class MyThread(threading.Thread):
def run(self):
if raw:
data = self.cs.get_campaign_raw(self.name)
else:
data = self.cs.get_campaign(self.name)
self.data = data
self.event.set()
def cancel(self):
self.data = None
self.event.set()
self.cs.async_cancel()
mythread = MyThread()
mythread.cs = self
mythread.name = name
mythread.event = threading.Event()
mythread.start()
return mythread
def put_campaign_async(self, *args):
"""
This is like put_campaign, but returns immediately, doing server
communications in a background thread.
"""
class MyThread(threading.Thread):
def run(self):
data = self.cs.put_campaign(*self.args)
self.data = data
self.event.set()
def cancel(self):
self.data = None
self.event.set()
self.cs.async_cancel()
mythread = MyThread()
mythread.cs = self
mythread.name = args[1]
mythread.args = args
mythread.event = threading.Event()
mythread.start()
return mythread
def unpackdir(self, data, path, i = 0, verbose = False):
"""
Call this to unpack a campaign contained in a WML object to the
filesystem. The data parameter is the WML object, path is the path under
which it will be placed.
"""
try:
os.mkdir(path)
except:
pass
for f in data.get_all("file"):
name = f.get_text_val("name", "?")
contents = f.get_binary_val("contents")
if contents:
if verbose:
print i * " " + name + " (" +\
str(len(contents)) + ")"
file(path + "/" + name, "wb").write(contents)
for dir in data.get_all("dir"):
name = dir.get_text_val("name", "?")
shutil.rmtree(path + "/" + name, True)
os.mkdir(path + "/" + name)
if verbose:
print i * " " + name
self.unpackdir(dir, path + "/" + name, i + 2, verbose)
import wesnoth.wmldata as wmldata
from wesnoth.campaignserver_client import CampaignClient
if __name__ == "__main__":
import optparse, subprocess
@ -423,8 +16,10 @@ if __name__ == "__main__":
optionparser = optparse.OptionParser()
optionparser.add_option("-a", "--address", help = "specify server address",
default = "campaigns.wesnoth.org")
optionparser.add_option("-p", "--port", help = "specify server port or bfW version (%s)" % " or ".join(map(lambda x: x[1], portmap)),
default = portmap[0][0])
optionparser.add_option("-p", "--port",
help = "specify server port or bfW version (%s)" % " or ".join(
map(lambda x: x[1], CampaignClient.portmap)),
default = CampaignClient.portmap[0][0])
optionparser.add_option("-l", "--list", help = "list available campaigns",
action = "store_true",)
optionparser.add_option("-w", "--wml",
@ -469,7 +64,7 @@ if __name__ == "__main__":
port = options.port
if "." in options.port:
for (portnum, version) in portmap:
for (portnum, version) in CampaignClient.portmap:
if options.port == version:
port = portnum
break
@ -482,7 +77,7 @@ if __name__ == "__main__":
address += ":" + str(port)
if options.list:
cs = CampaignBrowser(address)
cs = CampaignClient(address)
data = cs.list_campaigns()
if data:
campaigns = data.get_or_create_sub("campaigns")
@ -501,7 +96,7 @@ if __name__ == "__main__":
else:
sys.stderr.write("Could not connect.\n")
elif options.download:
cs = CampaignBrowser(address)
cs = CampaignClient(address)
if re.escape(options.download).replace("\\_", "_") == options.download:
fetchlist = [options.download]
else:
@ -529,17 +124,17 @@ if __name__ == "__main__":
for message in mythread.data.find_all("message", "error"):
print message.get_text_val("message")
elif options.remove:
cs = CampaignBrowser(address)
cs = CampaignClient(address)
data = cs.delete_campaign(options.remove, options.password)
for message in data.find_all("message", "error"):
print message.get_text_val("message")
elif options.change_passphrase:
cs = CampaignBrowser(address)
cs = CampaignClient(address)
data = cs.change_passphrase(*options.change_passphrase)
for message in data.find_all("message", "error"):
print message.get_text_val("message")
elif options.upload:
cs = CampaignBrowser(address)
cs = CampaignClient(address)
pbl = wmldata.read_file(options.upload, "PBL")
name = os.path.basename(options.upload)
name = os.path.splitext(name)[0]