from socket import * from select import select from struct import pack, unpack import zlib, os, random, struct, hashlib, sys from time import time, ctime from common.msgstruct import * from errno import EWOULDBLOCK SERVER_TIMEOUT = 7200 # 2 hours without any connection or port activity def protofilepath(filename): dirpath = filename path = [] while dirpath: dirpath, component = os.path.split(dirpath) assert component, "invalid file path %r" % (filename,) path.insert(0, component) path.insert(0, game.FnBasePath) return '/'.join(path) class Icon: count = 0 def __init__(self, bitmap, code, x,y,w,h, alpha=255): self.w = w self.h = h self.origin = (bitmap, x, y) self.code = code if alpha == 255: self.msgdef = message(MSG_DEF_ICON, bitmap.code, code, x,y,w,h) else: self.msgdef = message(MSG_DEF_ICON, bitmap.code, code, x,y,w,h, alpha) framemsgappend(self.msgdef) def getimage(self): from . import pixmap bitmap, x, y = self.origin image = pixmap.decodepixmap(bitmap.read()) return pixmap.cropimage(image, (x, y, self.w, self.h)) def getorigin(self): bitmap, x, y = self.origin return bitmap, (x, y, self.w, self.h) class DataChunk: def __init__(self): for c in clients: if c.initialized == 2: self.defall(c) if recording and game: self.defall(recording) def read(self, slice=None): f = open(self.filename, "rb") data = f.read() f.close() if slice: start, length = slice data = data[start:start+length] return data def defall(self, client): if client.proto == 1 or not self.filename: # protocol 1 try: msgdef = self.msgdef except AttributeError: data = zlib.compress(self.read()) msgdef = self.msgdef = self.getmsgdef(data) else: # protocol >= 2 try: msgdef = self.sendmsgdef except AttributeError: fileid = len(filereaders) filereaders[fileid] = self.read data = self.read() msgdef = self.sendmsgdef = (self.getmd5def(fileid, data) + self.getmsgdef(fileid)) client.msgl.append(msgdef) def getmd5def(self, fileid, data, offset=0): md5 = hashlib.md5() md5.update(data) checksum = md5.digest() return message(MSG_MD5_FILE, fileid, protofilepath(self.filename).encode(), offset, len(data), checksum) class Bitmap(DataChunk): def __init__(self, code, filename, colorkey=None): self.code = code self.filename = filename self.icons = {} self.colorkey = colorkey DataChunk.__init__(self) def geticon(self, x,y,w,h, alpha=255): rect = (x,y,w,h) try: return self.icons[rect] except: ico = Icon(self, Icon.count, x,y,w,h, alpha) Icon.count += 1 self.icons[rect] = ico return ico def geticonlist(self, w, h, count): return list(map(lambda i, fn=self.geticon, w=w, h=h: fn(i*w, 0, w, h), list(range(count)))) def getmsgdef(self, data): if self.colorkey is not None: return message(MSG_DEF_BITMAP, self.code, data, self.colorkey) else: return message(MSG_DEF_BITMAP, self.code, data) def defall(self, client): DataChunk.defall(self, client) for i in list(self.icons.values()): client.msgl.append(i.msgdef) class MemoryBitmap(Bitmap): def __init__(self, code, data, colorkey=None): self.data = data Bitmap.__init__(self, code, None, colorkey) def read(self, slice=None): data = self.data if slice: start, length = slice data = data[start:start+length] return data class Sample(DataChunk): def __init__(self, code, filename, freqfactor=1): self.code = code self.filename = filename self.freqfactor = freqfactor DataChunk.__init__(self) def defall(self, client): if client.has_sound > 0: DataChunk.defall(self, client) def getmsgdef(self, data): return message(MSG_DEF_SAMPLE, self.code, data) def read(self, slice=None): f = open(self.filename, "rb") data = f.read() f.close() if self.freqfactor != 1: freq, = unpack(" 1.0: lvolume = 1.0 if rvolume < 0.0: rvolume = 0.0 elif rvolume > 1.0: rvolume = 1.0 message = pack("!hBBh", self.code, int(lvolume*255.0), int(rvolume*255.0), -1) if singleclient is None: clist = clients[:] else: clist = [singleclient] for c in clist: if c.has_sound: c.sounds.setdefault(message, 4) class Music(DataChunk): def __init__(self, filename, filerate=44100): self.filename = filename self.filerate = filerate self.f = open(filename, 'rb') self.f.seek(0, 2) filesize = self.f.tell() self.endpos = max(self.filerate, filesize - self.filerate) self.fileid = len(filereaders) filereaders[self.fileid] = self.read self.md5msgs = {} DataChunk.__init__(self) def read(self, start, length): self.f.seek(start) return self.f.read(length) def msgblock(self, position, limited=1): blocksize = self.filerate if limited and position+blocksize > self.endpos: blocksize = self.endpos-position if blocksize <= 0: return '' #self.f.seek(position) #return message(MSG_DEF_MUSIC, self.code, position, self.f.read(blocksize)) try: msg = self.md5msgs[position] except KeyError: data = self.read(position, blocksize) checksum = hashlib.md5(data).digest() msg = message(MSG_MD5_FILE, self.fileid, protofilepath(self.filename).encode(), position, blocksize, checksum) self.md5msgs[position] = msg return msg def clientsend(self, clientpos): msg = self.msgblock(clientpos) #print 'clientsend:', self.code, len(msg), clientpos if msg: return [msg], clientpos + self.filerate else: return [], None def initialsend(self, c): return [self.msgblock(0), self.msgblock(self.endpos, 0)], self.filerate def defall(self, client): pass def clearsprites(): sprites_by_n.clear() sprites[:] = [b''] def compactsprites(insert_new=None, insert_before=None): global sprites, sprites_by_n if insert_before is not None: if insert_new.alive: insert_before = insert_before.alive else: insert_before = None newsprites = [b''] newd = {} l = list(sprites_by_n.items()) l.sort() for n, s in l: if n == insert_before: prevn = insert_new.alive newn = insert_new.alive = len(newsprites) newsprites.append(sprites[prevn]) newd[newn] = insert_new l.remove((prevn, insert_new)) newn = s.alive = len(newsprites) newsprites.append(sprites[n]) newd[newn] = s sprites = newsprites sprites_by_n = newd class Sprite: ## try: ## import psyco.classes ## except ImportError: ## pass ## else: ## __slots__ = ['x', 'y', 'ico', 'alive'] ## __metaclass__ = psyco.classes.psymetaclass def __init__(self, ico, x,y): self.x = x self.y = y self.ico = ico self.alive = len(sprites) if (-ico.w < x < game.width and -ico.h < y < game.height): sprites.append(pack("!hhh", x, y, ico.code)) else: sprites.append(b'') # starts off-screen sprites_by_n[self.alive] = self def move(self, x,y, ico=None): self.x = x self.y = y if ico is not None: self.ico = ico sprites[self.alive] = pack("!hhh", x, y, self.ico.code) def setdisplaypos(self, x, y): # special use only (self.x,y are not updated) s = sprites[self.alive] if len(s) == 6: sprites[self.alive] = pack("!hh", x, y) + s[4:] def setdisplayicon(self, ico): # special use only (self.ico is not updated) s = sprites[self.alive] if len(s) == 6: sprites[self.alive] = s[:4] + pack("!h", ico.code) #sizeof_displaypos = struct.calcsize("!hh") def getdisplaypos(self): # special use only (normally, read self.x,y,ico directly) s = sprites[self.alive] if self.alive and len(s) == 6: return unpack("!hh", s[:4]) else: return None, None def step(self, dx,dy): x = self.x = self.x + dx y = self.y = self.y + dy sprites[self.alive] = pack("!hhh", x, y, self.ico.code) def seticon(self, ico): self.ico = ico sprites[self.alive] = pack("!hhh", self.x, self.y, ico.code) def hide(self): sprites[self.alive] = b'' def kill(self): if self.alive: del sprites_by_n[self.alive] sprites[self.alive] = b'' self.alive = 0 def prefix(self, n, m=0): pass #sprites[self.alive] = pack("!hhh", n, m, 32767) + sprites[self.alive] def to_front(self): if self.alive and self.alive < len(sprites)-1: self._force_to_front() def _force_to_front(self): info = sprites[self.alive] sprites[self.alive] = b'' del sprites_by_n[self.alive] self.alive = len(sprites) sprites_by_n[self.alive] = self sprites.append(info) def to_back(self, limit=None): assert self is not limit if limit: n1 = limit.alive + 1 else: n1 = 1 if self.alive > n1: if n1 in sprites_by_n: keys = list(sprites_by_n.keys()) keys.remove(self.alive) keys.sort() keys = keys[keys.index(n1):] reinsert = [sprites_by_n[n] for n in keys] for s1 in reinsert: s1._force_to_front() assert n1 not in sprites_by_n info = sprites[self.alive] sprites[self.alive] = b'' del sprites_by_n[self.alive] self.alive = n1 sprites_by_n[n1] = self sprites[n1] = info def __repr__(self): if self.alive: return "" % (self.alive, self.x, self.y) else: return "" class Player: standardplayericon = None def playerjoin(self): pass def playerleaves(self): pass def _playerleaves(self): if self.isplaying(): self._client.killplayer(self) del self._client self.playerleaves() def isplaying(self): return hasattr(self, "_client") class Client: SEND_BOUND_PER_FRAME = 0x6000 # bytes KEEP_ALIVE = 2.2 # seconds def __init__(self, socket, addr): socket.setblocking(0) self.socket = socket self.addr = addr self.udpsocket = None self.udpsockcounter = 0 self.initialdata = MSG_WELCOME self.initialized = 0 self.msgl = [message(MSG_PING)] self.buf = b"" self.players = { } self.sounds = None self.has_sound = 0 self.has_music = 0 self.musicpos = { } self.proto = 1 self.dyncompress = None addsocket('CLIENT', self.socket, self.input_handler) clients.append(self) self.log('connected') self.send_buffer(self.initialdata) def opengame(self, game): if self.initialized == 0: self.initialdata += game.FnDesc.encode() + b'\n' self.initialized = 1 if self.initialized == 1: if game.broadcast_port: self.initialdata += message(MSG_BROADCAST_PORT, game.broadcast_port) game.trigger_broadcast() self.initialdata += game.deffieldmsg() else: self.msgl.append(game.deffieldmsg()) self.activity = self.last_ping = time() self.force_ping_delay = 0.6 for c in clients: for id in list(c.players.keys()): self.msgl.append(message(MSG_PLAYER_JOIN, id, c is self)) def emit(self, udpdata, broadcast_extras): if self.initialdata: self.send_buffer(self.initialdata) elif self.initialized == 2: buffer = b''.join(self.msgl) if buffer: self.send_buffer(buffer) if self.udpsocket is not None: if self.sounds: if broadcast_extras is None or self not in broadcast_clients: udpdata = ''.join(list(self.sounds.keys()) + [udpdata]) else: broadcast_extras.update(self.sounds) for key, value in list(self.sounds.items()): if value: self.sounds[key] = value-1 else: del self.sounds[key] if broadcast_extras is None or self not in broadcast_clients: if self.dyncompress is not None: udpdatas = self.dynamic_compress(udpdata) else: udpdatas = [udpdata] for udpdata in udpdatas: try: self.udpsockcounter += self.udpsocket.send(udpdata) except error as e: print('ignored:', str(e), file=sys.stderr) pass # ignore UDP send errors (buffer full, etc.) if self.has_music > 1 and NOW >= self.musicstreamer: self.musicstreamer += 0.99 self.sendmusicdata() if not self.msgl: if abs(NOW - self.activity) <= self.KEEP_ALIVE: if abs(NOW - self.last_ping) <= self.force_ping_delay: return if self.udpsockcounter < 1024: return self.force_ping_delay += 0.2 self.msgl.append(message(MSG_PING, self.udpsockcounter>>10)) self.last_ping = NOW def setup_dyncompress(self): def dyncompress(): # See comments in pclient.Playfield.dynamic_decompress(). threads = [] for t in range(3): co = zlib.compressobj(6) threads.append( (bytes(0x88 + t, t), co) ) frame = 0 globalsync = 0 while 1: # write three normal packets, one on each thread for t in range(3): head, co = threads.pop(0) yield head + chr(frame), co threads.append((chr(ord(head[0]) & 0x87) + chr(frame), co)) yield None, None frame = (frame + 1) & 0xFF # sync frame, write two packets (on two threads) # and restart compression at the current frame for these threads head, co = threads.pop(0) yield head + chr(frame), co co1 = zlib.compressobj(6) co2 = zlib.compressobj(6) globalsync += 1 if globalsync == 4: # next on this thread will be a global sync packet nextframe = (frame + 2) & 0xFF globalsync = 0 else: # next of this thread will be a local sync packet yield None, co1 nextframe = frame threads.append((chr(ord(head[0]) | 8) + chr(nextframe), co1)) # 2nd packet of the current frame head, co = threads.pop(0) yield head + chr(frame), co yield None, co2 threads.append((chr(ord(head[0]) | 8) + chr(frame), co2)) yield None, None frame = (frame + 1) & 0xFF self.dyncompress = dyncompress() def dynamic_compress(self, framedata): result = [] for head, co in self.dyncompress: if not co: return result data = [head, co.compress(framedata), co.flush(zlib.Z_SYNC_FLUSH)] if head: result.append(''.join(data)) def send_can_mix(self): return not self.msgl and self.socket is not None def send_buffer(self, buffer): try: count = self.socket.send(buffer[:self.SEND_BOUND_PER_FRAME]) except error as e: if e.args[0] != EWOULDBLOCK: self.msgl = [] self.initialdata = "" self.disconnect(e, 'emit') return else: #g = open('log', 'ab'); g.write(buffer[:count]); g.close() buffer = buffer[count:] self.activity = NOW if self.initialdata: self.initialdata = buffer elif buffer: self.msgl = [buffer] else: self.msgl = [] def receive(self, data): #print "receive:", `data` try: data = self.buf + data while data: values, data = decodemessage(data) if not values: break # incomplete message fn = self.MESSAGES.get(values[0]) if fn: fn(self, *values[1:]) else: print("unknown message from", self.addr, ":", values) self.buf = data except struct.error: import traceback traceback.print_exc() self.socket.send(b'\n\n

Protocol Error

\n') hs = findsocket('HTTP') if hs is not None: url = 'http://%s:%s' % (HOSTNAME, displaysockport(hs)) self.socket.send(b''' If you meant to point your web browser to this server, then use the following address: %s ''' % (url.encode(), url.encode())) self.disconnect('protocol error', 'receive') def input_handler(self): try: data = self.socket.recv(2048) except error as e: self.disconnect(e, "socket.recv") else: if data: self.activity = NOW self.receive(data) elif not hasattr(self.socket, 'RECV_CAN_RETURN_EMPTY'): # safecheck that this means disconnected iwtd, owtd, ewtd = select([self.socket], [], [], 0.0) if self.socket in iwtd: self.disconnect('end of data', 'socket.recv') def disconnect(self, err=None, infn=None): removesocket('CLIENT', self.socket) if err: extra = ": " + str(err) else: extra = "" if infn: extra += " in " + infn print('Disconnected by', self.addr, extra) self.log('disconnected' + extra) for p in list(self.players.values()): p._playerleaves() try: del broadcast_clients[self] except KeyError: pass clients.remove(self) try: self.socket.close() except: pass self.socket = None if not clients and game is not None: game.FnDisconnected() def killplayer(self, player): for id, p in list(self.players.items()): if p is player: framemsgappend(message(MSG_PLAYER_KILL, id)) del self.players[id] if game: game.updateplayers() def joinplayer(self, id, *rest): if id in self.players: print("Note: player %s is already playing" % (self.addr+(id,),)) return if game is None: return # refusing new player before the game starts p = game.FnPlayers()[id] if p is None: print("Too many players. New player %s refused." % (self.addr+(id,),)) self.msgl.append(message(MSG_PLAYER_KILL, id)) elif p.isplaying(): print("Note: player %s is already played by another client" % (self.addr+(id,),)) else: print("New player %s" % (self.addr+(id,),)) p._client = self p.playerjoin() p.setplayername('') self.players[id] = p game.updateplayers() for c in clients: c.msgl.append(message(MSG_PLAYER_JOIN, id, c is self)) def remove_player(self, id, *rest): try: p = self.players[id] except KeyError: print("Note: player %s is not playing" % (self.addr+(id,),)) else: p._playerleaves() def set_player_name(self, id, name, *rest): p = game.FnPlayers()[id] p.setplayername(name) def set_udp_port(self, port, addr=None, *rest): if port == MSG_BROADCAST_PORT: self.log('set_udp_port: broadcast') broadcast_clients[self] = 1 #print "++++ Broadcasting ++++ to", self.addr else: try: del broadcast_clients[self] except KeyError: pass if port == MSG_INLINE_FRAME or port == 0: # client requests data in-line on the TCP stream self.dyncompress = None from . import udpovertcp self.udpsocket = udpovertcp.SocketMarshaller(self.socket, self) s = self.udpsocket.tcpsock self.log('set_udp_port: udp-over-tcp') else: try: if hasattr(self.socket, 'udp_over_udp_mixer'): # for SocketOverUdp self.udpsocket = self.socket.udp_over_udp_mixer() else: self.udpsocket = socket(AF_INET, SOCK_DGRAM) self.udpsocket.setblocking(0) addr = addr or self.addr[0] self.udpsocket.connect((addr, port)) except error as e: print("Cannot set UDP socket to", addr, str(e), file=sys.stderr) self.udpsocket = None self.udpsockcounter = sys.maxsize else: if self.proto >= 3: self.setup_dyncompress() s = self.udpsocket self.log('set_udp_port: %s:%d' % (addr, port)) if s: try: s.setsockopt(SOL_IP, IP_TOS, 0x10) # IPTOS_LOWDELAY except error as e: print("Cannot set IPTOS_LOWDELAY:", str(e), file=sys.stderr) def enable_sound(self, sound_mode=1, *rest): if sound_mode != self.has_sound: self.sounds = {} self.has_sound = sound_mode if self.has_sound > 0: for snd in list(samples.values()): snd.defall(self) #self.log('enable_sound %s' % sound_mode) def enable_music(self, mode, *rest): if mode != self.has_music: self.has_music = mode self.startmusic() #self.log('enable_music') def startmusic(self): if self.has_music: self.musicstreamer = time() for cde in currentmusics[1:]: if cde not in self.musicpos: msgl, self.musicpos[cde] = music_by_id[cde].initialsend(self) self.msgl += msgl if self.has_music > 1: self.sendmusicdata() self.msgl.append(message(MSG_PLAY_MUSIC, *currentmusics)) def sendmusicdata(self): for cde in currentmusics[1:]: if self.musicpos[cde] is not None: msgl, self.musicpos[cde] = music_by_id[cde].clientsend(self.musicpos[cde]) self.msgl += msgl return def ping(self, *rest): if self.initialized < 2: # send all current bitmap data self.initialized = 2 for b in list(bitmaps.values()): b.defall(self) self.finishinit(game) for id, p in list(game.FnPlayers().items()): if p.standardplayericon is not None: self.msgl.append(message(MSG_PLAYER_ICON, id, p.standardplayericon.code)) self.msgl.append(message(MSG_PONG, *rest)) def finishinit(self, game): pass def pong(self, *rest): pass def log(self, message): print(self.addr, message) def protocol_version(self, version, *rest): self.proto = version def md5_data_request(self, fileid, position, size, *rest): data = filereaders[fileid]((position, size)) data = zlib.compress(data) self.msgl.append(message(MSG_ZPATCH_FILE, fileid, position, data)) ## def def_file(self, filename, md5sum): ## fnp = [] ## while filename: ## filename, tail = os.path.split(filename) ## fnp.insert(0, tail) ## if fnp[:len(FnBasePath)] == FnBasePath: ## filename = os.path.join(*fnp[len(FnBasePath):]) ## self.known_files[filename] = md5sum MESSAGES = { CMSG_PROTO_VERSION: protocol_version, CMSG_ADD_PLAYER : joinplayer, CMSG_REMOVE_PLAYER: remove_player, CMSG_UDP_PORT : set_udp_port, CMSG_ENABLE_SOUND : enable_sound, CMSG_ENABLE_MUSIC : enable_music, CMSG_PING : ping, CMSG_PONG : pong, CMSG_DATA_REQUEST : md5_data_request, CMSG_PLAYER_NAME : set_player_name, ## CMSG_DEF_FILE : def_file, } class SimpleClient(Client): def finishinit(self, game): num = 0 for keyname, icolist, fn in game.FnKeys: self.msgl.append(message(MSG_DEF_KEY, keyname.encode(), num, *[ico.code for ico in icolist])) num += 1 def cmsg_key(self, pid, keynum): if game is not None: try: player = self.players[pid] fn = game.FnKeys[keynum][2] except (KeyError, IndexError): game.FnUnknown() else: getattr(player, fn) () MESSAGES = Client.MESSAGES.copy() MESSAGES.update({ CMSG_KEY: cmsg_key, }) MAX_CLIENTS = 32 clients = [] FnClient = SimpleClient broadcast_clients = {} filereaders = {} bitmaps = {} samples = {} music_by_id = {} currentmusics = [0] sprites = [b''] sprites_by_n = {} recording = None game = None serversockets = {} socketsbyrole = {} socketports = {} def framemsgappend(msg): for c in clients: c.msgl.append(msg) if recording: recording.write(msg) ##def sndframemsgappend(msg): ## for c in clients: ## if c.has_sound: ## c.msgl.append(msg) def set_udp_port(port): hostchooser.UDP_PORT = port def has_loop_music(): return currentmusics[0] < len(currentmusics)-1 def finalsegment(music1, music2): intro1 = music1[1:1+music1[0]] intro2 = music2[1:1+music2[0]] loop1 = music1[1+music1[0]:] loop2 = music2[1+music2[0]:] return loop1 == loop2 and intro1 == intro2[len(intro2)-len(intro1):] def set_musics(musics_intro, musics_loop, reset=1): mlist = [] loop_from = len(musics_intro) mlist.append(loop_from) for m in musics_intro + musics_loop: mlist.append(m.fileid) reset = reset or not finalsegment(mlist, currentmusics) currentmusics[:] = mlist if reset: for c in clients: c.startmusic() def fadeout(time=1.0): msg = message(MSG_FADEOUT, int(time*1000)) for c in clients: if c.has_music > 1: c.msgl.append(msg) currentmusics[:] = [0] def getbitmap(filename, colorkey=None): try: return bitmaps[filename] except: bmp = Bitmap(len(bitmaps), filename, colorkey) bitmaps[filename] = bmp return bmp def getsample(filename, freqfactor=1): try: return samples[filename, freqfactor] except: snd = Sample(len(samples), filename, freqfactor) samples[filename, freqfactor] = snd return snd def getmusic(filename, filerate=44100): try: return samples[filename] except: mus = Music(filename, filerate) samples[filename] = mus music_by_id[mus.fileid] = mus return mus def newbitmap(data, colorkey=None): bmp = MemoryBitmap(len(bitmaps), data, colorkey) bitmaps[bmp] = bmp return bmp def addsocket(role, socket, handler=None, port=None): if port is None: host, port = socket.getsockname() if handler is not None: serversockets[socket] = handler socketsbyrole.setdefault(role, []).append(socket) socketports[socket] = port def findsockets(role): return socketsbyrole.get(role, []) def findsocket(role): l = findsockets(role) if l: return l[-1] else: return None def removesocket(role, socket=None): if socket is None: for socket in socketsbyrole.get(role, [])[:]: removesocket(role, socket) return try: del serversockets[socket] except KeyError: pass try: socketsbyrole.get(role, []).remove(socket) except ValueError: pass try: del socketports[socket] except KeyError: pass def opentcpsocket(port=None): port = port or PORTS.get('LISTEN', INADDR_ANY) s = findsocket('LISTEN') if s is None: s = socket(AF_INET, SOCK_STREAM) try: s.bind(('', port)) s.listen(1) except error: if port == INADDR_ANY: for i in range(10): port = random.choice(range(8000, 12000)) try: s.bind(('', port)) s.listen(1) except error: pass else: break else: raise error("server cannot find a free TCP socket port") else: raise def tcpsocket_handler(s=s): conn, addr = s.accept() game.newclient(conn, addr) addsocket('LISTEN', s, tcpsocket_handler) return s def openpingsocket(only_port=None): only_port = only_port or PORTS.get('PING', None) s = findsocket('PING') if s is None: import hostchooser s = hostchooser.serverside_ping(only_port) if s is None: return None def pingsocket_handler(s=s): global game import hostchooser if game is not None: args = game.FnDesc, ('', game.address[1]), game.FnExtraDesc() else: ts = findsocket('LISTEN') if ts: address = '', displaysockport(ts) else: address = '', '' args = 'Not playing', address, '' hs = findsocket('HTTP') args = args + (displaysockport(hs),) hostchooser.answer_ping(s, *args) addsocket('PING', s, pingsocket_handler) return s def openhttpsocket(ServerClass=None, HandlerClass=None, port=None): port = port or PORTS.get('HTTP', None) s = findsocket('HTTP') if s is None: if ServerClass is None: from http.server import HTTPServer as ServerClass if HandlerClass is None: import javaserver from httpserver import MiniHandler as HandlerClass server_address = ('', port or 8000) try: httpd = ServerClass(server_address, HandlerClass) except error: if port is None: server_address = ('', INADDR_ANY) try: httpd = ServerClass(server_address, HandlerClass) except error as e: print("cannot start HTTP server", str(e), file=sys.stderr) return None else: raise s = httpd.socket addsocket('HTTP', s, httpd.handle_request) return s BROADCAST_PORT_RANGE = range(18000, 19000) #BROADCAST_MESSAGE comes from msgstruct BROADCAST_DELAY = 0.6180 BROADCAST_DELAY_INCR = 2.7183 def openbroadcastsocket(broadcastport=None): s = findsocket('BROADCAST') if s is None: try: s = socket(AF_INET, SOCK_DGRAM) s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) except error as e: print("Cannot broadcast", str(e), file=sys.stderr) return None port = broadcastport or random.choice(BROADCAST_PORT_RANGE) addsocket('BROADCAST', s, port=port) return s def displaysockport(s): return socketports.get(s, 'off') class Game: width = 640 height = 480 backcolor = 0x000000 FnDesc = "NoName" FnFrame = lambda self: 1.0 FnExcHandler=lambda self, k: 0 FnServerInfo=lambda self, s: None FnPlayers = lambda self: {} FnKeys = [] FnUnknown = lambda self: None FnDisconnected = lambda self: None def __init__(self): global game s = opentcpsocket() self.address = HOSTNAME, socketports[s] bs = self.broadcast_s = openbroadcastsocket() self.broadcast_port = socketports.get(bs) self.broadcast_next = None self.nextframe = time() clearsprites() game = self if recording: for b in list(bitmaps.values()): b.defall(recording) self.pendingclients = [] def openserver(self): ps = openpingsocket() print('%s server at %s:%d, Broadcast %d, UDP %d' % ( self.FnDesc, self.address[0], self.address[1], displaysockport(self.broadcast_s), displaysockport(ps))) hs = openhttpsocket() if hs: print('HTTP server: http://%s:%d' % ( self.address[0], displaysockport(hs))) try: from localmsg import autonotify except ImportError: pass else: autonotify(self.FnDesc, *self.address) if clients: for c in clients: c.opengame(self) if recording: recording.start() recording.write(self.deffieldmsg()) def trigger_broadcast(self): assert self.broadcast_s is not None game.broadcast_delay = BROADCAST_DELAY game.broadcast_next = time() + self.broadcast_delay def deffieldmsg(self): return message(MSG_DEF_PLAYFIELD, self.width, self.height, self.backcolor, self.FnDesc.encode()) def socketerrors(self, ewtd): for c in clients[:]: if c.socket in ewtd: del ewtd[c.socket] c.disconnect("error", "select") def mainstep(self): global NOW if self.pendingclients: self.newclient(*self.pendingclients.pop()) NOW = time() delay = self.nextframe - NOW if delay<=0.0: self.nextframe += self.FnFrame() self.sendudpdata() NOW = time() delay = self.nextframe - NOW if delay<0.0: self.nextframe = NOW delay = 0.0 if self.broadcast_next is not None and NOW >= self.broadcast_next: if not clients: self.broadcast_next = None else: try: self.broadcast_s.sendto(BROADCAST_MESSAGE, (b'', self.broadcast_port)) #print "Broadcast ping" except error: pass # ignore failed broadcasts self.broadcast_next = time() + self.broadcast_delay self.broadcast_delay *= BROADCAST_DELAY_INCR return delay def sendudpdata(self): sprites[0] = b'' udpdata = b''.join(sprites) if len(broadcast_clients) >= 2: broadcast_extras = {} else: broadcast_extras = None for c in clients[:]: c.emit(udpdata, broadcast_extras) if recording: recording.udpdata(udpdata) if broadcast_extras is not None: udpdata = ''.join(list(broadcast_extras.keys()) + [udpdata]) try: self.broadcast_s.sendto(udpdata, (b'', self.broadcast_port)) #print "Broadcast UDP data" except error: pass # ignore failed broadcasts def FnExtraDesc(self): players = 0 for c in clients: players += len(c.players) if players == 0: return 'no player' elif players == 1: return 'one player' else: return '%d players' % players def updateplayers(self): pass def updateboard(self): pass def newclient(self, conn, addr): if len(clients)==MAX_CLIENTS: print("Too many connections; refusing new connection from", addr) conn.close() else: try: addrname = (gethostbyaddr(addr[0])[0],) + addr[1:] except: addrname = addr print('Connected by', addrname) try: c = FnClient(conn, addrname) except error as e: print('Connexion already lost!', e) else: if game is not None: c.opengame(game) def newclient_threadsafe(self, conn, addr): self.pendingclients.insert(0, (conn, addr)) def recursiveloop(endtime, extra_sockets): global game timediff = 1 while timediff: if game is not None: delay = game.mainstep() else: delay = 5.0 iwtd = extra_sockets + list(serversockets.keys()) timediff = max(0.0, endtime - time()) iwtd, owtd, ewtd = select(iwtd, [], iwtd, min(delay, timediff)) if ewtd: if game: game.socketerrors(ewtd) if ewtd: print("Unexpected socket error reported", file=sys.stderr) for s in iwtd: if s in serversockets: serversockets[s]() # call handler elif s in extra_sockets: return s if not extra_sockets and timediff: return 1 return None SERVER_SHUTDOWN = 0.0 def mainloop(): global game, SERVER_SHUTDOWN servertimeout = None try: while serversockets: try: if game is not None: delay = game.mainstep() else: delay = SERVER_SHUTDOWN or 5.0 iwtd = list(serversockets.keys()) try: iwtd, owtd, ewtd = select(iwtd, [], iwtd, delay) except Exception as e: from select import error as select_error if not isinstance(e, select_error): raise iwtd, owtd, ewtd = [], [], [] if ewtd: if game: game.socketerrors(ewtd) if ewtd: print("Unexpected socket error reported", file=sys.stderr) servertimeout = None if iwtd: for s in iwtd: if s in serversockets: serversockets[s]() # call handler servertimeout = None elif SERVER_SHUTDOWN and not ewtd and not owtd: SERVER_SHUTDOWN -= delay if SERVER_SHUTDOWN <= 0.001: raise SystemExit("Server shutdown requested.") elif clients or getattr(game, 'autoreset', 0): servertimeout = None elif servertimeout is None: servertimeout = time() + SERVER_TIMEOUT elif time() > servertimeout: raise SystemExit("No more server activity, timing out.") except KeyboardInterrupt: if game is None or not game.FnExcHandler(1): raise except SystemExit: raise except: if game is None or not game.FnExcHandler(0): raise finally: removesocket('LISTEN') removesocket('PING') if clients: print("Server crash -- waiting for clients to terminate...") while clients: iwtd = [c.socket for c in clients] try: iwtd, owtd, ewtd = select(iwtd, [], iwtd, 120.0) except KeyboardInterrupt: break if not (iwtd or owtd or ewtd): break # timeout - give up for c in clients[:]: if c.socket in ewtd: c.disconnect("select reported an error") elif c.socket in iwtd: try: data = c.socket.recv(2048) except error as e: c.disconnect(e) else: if not data and not hasattr(c.socket, 'RECV_CAN_RETURN_EMPTY'): c.disconnect("end of data") print("Server closed.") def closeeverything(): global SERVER_SHUTDOWN SERVER_SHUTDOWN = 2.5 if game is not None: game.FnServerInfo("Server is stopping!") # ____________________________________________________________ try: from localmsg import recordfilename except ImportError: pass else: class RecordFile: proto = 2 has_sound = 0 def __init__(self, filename, sampling=1/7.77): self.filename = filename self.f = None self.sampling = sampling self.msgl = [] self.write = self.msgl.append def start(self): if not self.f: import gzip, atexit self.f = gzip.open(self.filename, 'wb') atexit.register(self.f.close) self.recnext = time() + self.sampling def udpdata(self, udpdata): if self.f: now = time() if now >= self.recnext: while now >= self.recnext: self.recnext += self.sampling self.write(message(MSG_RECORDED, udpdata)) self.f.write(''.join(self.msgl)) del self.msgl[:] recording = RecordFile(recordfilename) del recordfilename