diff --git a/syncplay/client.py b/syncplay/client.py index dd9064c..aa4645e 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -22,21 +22,31 @@ class SyncClientProtocol(CommandProtocol): def connectionLost(self, reason): self.manager.protocol = None - def handle_connected_state(self, arg): - arg = parse_state(arg) - if not arg: + def handle_error(self, args): + self.manager.stop() + CommandProtocol.handle_error(self, args) + + def handle_connected_state(self, args): + args = parse_state(args) + if not args: self.drop_with_error('Malformed state attributes') return - counter, paused, position, name = arg + counter, paused, position, name = args self.manager.update_global_state(counter, paused, position, name) - def handle_connected_ping(self, arg): - self.send_message('pong', arg) + def handle_connected_ping(self, args): + if not len(args) == 1: + self.drop_with_error('Invalid arguments') + return + self.send_message('pong', args[0]) def send_state(self, counter, paused, position): - self.send_message('state', counter, ('paused' if paused else 'playing'), int(position*100)) + self.send_message('state', counter, ('paused' if paused else 'playing'), int(position*1000)) + + def send_playing(self, filename): + self.send_message('playing', filename) states = dict( @@ -44,6 +54,7 @@ class SyncClientProtocol(CommandProtocol): state = 'handle_connected_state', seek = 'handle_connected_seek', ping = 'handle_connected_ping', + playing = 'handle_connected_playing', ), ) initial_state = 'connected' @@ -63,7 +74,7 @@ class SyncClientFactory(ClientFactory): def clientConnectionLost(self, connector, reason): if self.retry: print 'Connection lost, reconnecting' - connector.connect() + reactor.callLater(0.1, connector.connect) else: print 'Disconnected' @@ -150,7 +161,9 @@ class Manager(object): def init_protocol(self, protocol): self.protocol = protocol self.schedule_send_status() - self.make_player(self) + self.send_filename() + if self.player is None: + self.make_player(self) def schedule_ask_player(self, when=0.2): @@ -181,6 +194,10 @@ class Manager(object): self.protocol.send_state(self.counter, self.player_paused, self.player_position) self.schedule_send_status() + def send_filename(self): + if self.protocol and self.player_filename: + self.protocol.send_playing(self.player_filename) + def update_player_status(self, paused, position): old_paused = self.player_paused @@ -215,6 +232,7 @@ class Manager(object): def update_filename(self, filename): self.player_filename = filename + self.send_filename() def update_global_state(self, counter, paused, position, name): curtime = time.time() diff --git a/syncplay/network_utils.py b/syncplay/network_utils.py index 498a8bb..1c58632 100644 --- a/syncplay/network_utils.py +++ b/syncplay/network_utils.py @@ -15,6 +15,11 @@ from twisted.web.iweb import IBodyProducer from zope.interface import implements +from .utils import ( + join_args, + split_args, +) + class CommandProtocol(LineReceiver): states = None @@ -25,11 +30,16 @@ class CommandProtocol(LineReceiver): line = line.strip() if not line: return - line = line.split(None, 1) - if len(line) != 2: + #print '>>>', line + args = split_args(line) + if not args: self.drop_with_error('Malformed line') return - command, arg = line + command = args.pop(0) + + if command == 'error': + self.handle_error(args) + return available_commands = self.states.get(self._state) handler = available_commands.get(command) @@ -39,7 +49,10 @@ class CommandProtocol(LineReceiver): self.drop_with_error('Unknown command: `%s`' % command) return # TODO log it too - handler(arg) + handler(args) + + def handle_error(self, args): + print 'Error received from other side:', args def change_state(self, new_state): if new_state not in self.states: @@ -47,11 +60,9 @@ class CommandProtocol(LineReceiver): self._state = new_state def send_message(self, *args): - self.sendLine(' '.join( - (arg if isinstance(arg, basestring) else str(arg)) - for arg in args - if arg is not None - )) + line = join_args(args) + #print '<<<', line + self.sendLine(line) def drop(self): self.transport.loseConnection() diff --git a/syncplay/server.py b/syncplay/server.py index 192308d..c25be65 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -25,32 +25,48 @@ class SyncServerProtocol(CommandProtocol): def connectionLost(self, reason): self.factory.remove_watcher(self) - def handle_init_iam(self, arg): - self.factory.add_watcher(self, arg.strip()) + def handle_init_iam(self, args): + if not len(args) == 1: + self.drop_with_error('Invalid arguments') + return + self.factory.add_watcher(self, args[0]) self.change_state('connected') - def handle_connected_state(self, arg): - arg = parse_state(arg) - if not arg: + def handle_connected_state(self, args): + args = parse_state(args) + if not args: self.drop_with_error('Malformed state attributes') return - counter, paused, position, _ = arg + counter, paused, position, _ = args self.factory.update_state(self, counter, paused, position) - def handle_connected_seek(self, arg): + def handle_connected_seek(self, args): + if not len(args) == 1: + self.drop_with_error('Invalid arguments') + return + try: - position = int(arg) + position = int(args[0]) except ValueError: self.drop_with_error('Invalid position numeral') - position /= 100.0 + position /= 1000.0 self.factory.seek(self, position) - def handle_connected_pong(self, arg): - self.factory.pong_received(self, arg) + def handle_connected_pong(self, args): + if not len(args) == 1: + self.drop_with_error('Invalid arguments') + return + self.factory.pong_received(self, args[0]) + + def handle_connected_playing(self, args): + if not len(args) == 1: + self.drop_with_error('Invalid arguments') + return + #self.factory.pong_received(self, args[0]) def __hash__(self): return hash('|'.join(( @@ -60,10 +76,15 @@ class SyncServerProtocol(CommandProtocol): def send_state(self, counter, paused, position, who_last_changed): - self.send_message('state', counter, ('paused' if paused else 'playing'), int(position*100), who_last_changed) + paused = 'paused' if paused else 'playing' + position = int(position*1000) + if who_last_changed is None: + self.send_message('state', counter, paused, position) + else: + self.send_message('state', counter, paused, position, who_last_changed) def send_seek(self, position, who_seeked): - self.send_message('seek', int(position*100), who_seeked) + self.send_message('seek', int(position*1000), who_seeked) def send_ping(self, value): self.send_message('ping', value) @@ -77,6 +98,7 @@ class SyncServerProtocol(CommandProtocol): state = 'handle_connected_state', seek = 'handle_connected_seek', pong = 'handle_connected_pong', + playing = 'handle_connected_playing', ), ) initial_state = 'init' diff --git a/syncplay/utils.py b/syncplay/utils.py index 8850f10..e36b5bd 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -1,19 +1,55 @@ #coding:utf8 import os +import re +RE_ARG = re.compile(r"('(?:[^\\']+|\\\\|\\')*'|[^\s']+)(?:\s+|\Z)") +RE_NEED_QUOTING = re.compile(r"[\s'\\]") +RE_QUOTABLE = re.compile(r"['\\]") +RE_UNQUOTABLE = re.compile(r"\\(['\\])") + +class InvalidArgumentException(Exception): + pass + +def quote_arg(arg): + if isinstance(arg, unicode): + arg = arg.encode('utf8') + elif not isinstance(arg, str): + arg = str(arg) + + if not arg or RE_NEED_QUOTING.search(arg): + return "'%s'" % RE_QUOTABLE.sub(r'\\\g<0>', arg) + return arg + +def unqote_arg(arg): + if arg.startswith("'") and len(arg) > 1: + arg = RE_UNQUOTABLE.sub(r'\1', arg[1:-1]) + return arg.decode('utf8', 'replace') + +def _split_args(args): + pos = 0 + while pos < len(args): + match = RE_ARG.match(args, pos) + if not match: + raise InvalidArgumentException() + pos = match.end() + yield unqote_arg(match.group(1)) + +def split_args(args): + try: + return list(_split_args(args)) + except InvalidArgumentException: + return None + +def join_args(args): + return ' '.join(quote_arg(arg) for arg in args) -def split_args(args, number): - # FIXME Make argument format smarter - return args.split(None, number-1) def parse_state(args): - args = split_args(args, 4) - l = len(args) - if l == 3: + if len(args) == 3: counter, state, position = args who_changed_state = None - elif l == 4: + elif len(args) == 4: counter, state, position, who_changed_state = args else: return @@ -33,7 +69,7 @@ def parse_state(args): except ValueError: return - position /= 100.0 + position /= 1000.0 return counter, paused, position, who_changed_state