From f92010b555058158f898910924a6213bacabc0d0 Mon Sep 17 00:00:00 2001 From: Dirk Grunwald Date: Sat, 11 Nov 2017 11:52:55 -0700 Subject: [PATCH 1/5] changes to make this work in Cloud9 with the CS50 VM --- remi/server.py | 59 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/remi/server.py b/remi/server.py index cc215456..89bc06e0 100644 --- a/remi/server.py +++ b/remi/server.py @@ -133,11 +133,11 @@ def __init__(self, *args, **kwargs): def setup(self): global clients socketserver.StreamRequestHandler.setup(self) - self._log.info('connection established: %r' % (self.client_address,)) + self._log.info('WebSocket connection established: %r' % (self.client_address,)) self.handshake_done = False def handle(self): - self._log.debug('handle') + self._log.debug('WebSocket handle') # on some systems like ROS, the default socket timeout # is less than expected, we force it to infinite (None) as default socket value self.request.settimeout(None) @@ -147,10 +147,15 @@ def handle(self): else: if not self.read_next_message(): k = get_instance_key(self) - clients[k].websockets.remove(self) - self.handshake_done = False - self._log.debug('ws ending websocket service') - break + try: + clients[k].websockets.remove(self) + self.handshake_done = False + self._log.debug('ws ending websocket service') + break + except: + self.handshake_done = True + self._log.debug('Tried to remove self when not in client...') + return @staticmethod def bytetonum(b): @@ -165,6 +170,10 @@ def read_next_message(self): length = self.rfile.read(2) except ValueError: # socket was closed, just return without errors + self._log.debug('Socket was closed, returning without errors') + return False + self._log.debug('WS response, length is %r' % length) + if len(length) < 1: return False length = self.bytetonum(length[1]) & 127 if length == 126: @@ -175,10 +184,13 @@ def read_next_message(self): decoded = '' for char in self.rfile.read(length): decoded += chr(self.bytetonum(char) ^ masks[len(decoded) % 4]) + self._log.debug('Decoded WebSocket message %r' % decoded) self.on_message(from_websocket(decoded)) except socket.timeout: + self._log.debug('WebSocket timeout') return False - except Exception: + except Exception as ex: + self._log.debug('Other exception %r' % ex ) return False return True @@ -211,17 +223,25 @@ def send_message(self, message): self.request.send(out) def handshake(self): - self._log.debug('handshake') + self._log.debug('WebSocket handshake') data = self.request.recv(1024).strip() - key = data.decode().split('Sec-WebSocket-Key: ')[1].split('\r\n')[0] + #self._log.debug('WebSocket handshake data is %s' % data.decode()) + # + # Code used to look for Sec-WebSocket-Key, some clients respond in lower case + # + if 'C9_HOSTNAME' in os.environ: + key = data.decode().split('sec-websocket-key: ')[1].split('\r\n')[0] + else: + key = data.decode().split('Sec-WebSocket-Key: ')[1].split('\r\n')[0] digest = hashlib.sha1((key.encode("utf-8")+self.magic)) + self._log.debug('key is ' + key) digest = digest.digest() digest = base64.b64encode(digest) response = 'HTTP/1.1 101 Switching Protocols\r\n' response += 'Upgrade: websocket\r\n' response += 'Connection: Upgrade\r\n' response += 'Sec-WebSocket-Accept: %s\r\n\r\n' % digest.decode("utf-8") - self._log.info('handshake complete') + self._log.info('WebSocket handshake complete') self.request.sendall(response.encode("utf-8")) self.handshake_done = True @@ -345,7 +365,6 @@ def run(self): # noinspection PyPep8Naming class App(BaseHTTPRequestHandler, object): - """ This class will handles any incoming request from the browser The main application class can subclass this @@ -382,6 +401,7 @@ def log_message(self, format_string, *args): def log_error(self, format_string, *args): msg = format_string % args self._log.error("%s %s" % (self.address_string(), msg)) + raise('Exception') def _instance(self): global clients @@ -636,6 +656,8 @@ def _instance(self): }; """ % (net_interface_ip, wsport, pending_messages_queue_length, websocket_timeout_timer_ms) + self._log.debug('Prpeare javascript with interface %s and port %s' % (net_interface_ip, wsport)) + # add built in js, extend with user js clients[k].js_body_end += ('\n' + '\n'.join(self._get_list_from_app_args('js_body_end'))) # use the default css, but append a version based on its hash, to stop browser caching @@ -932,9 +954,14 @@ def __init__(self, server_address, RequestHandlerClass, websocket_address, class Server(object): # noinspection PyShadowingNames - def __init__(self, gui_class, title='', start=True, address='127.0.0.1', port=8081, username=None, password=None, + def __init__(self, gui_class, title='', start=True, + address=os.getenv('IP','0.0.0.0'), + port=int(os.getenv('PORT',8080)), + username=None, password=None, multiple_instance=False, enable_file_cache=True, update_interval=0.1, start_browser=True, - websocket_timeout_timer_ms=1000, websocket_port=0, host_name=None, + websocket_timeout_timer_ms=1000, + websocket_port=int(os.getenv('PORT',8080))+1, + host_name=os.getenv('C9_HOSTNAME', None), pending_messages_queue_length=1000, userdata=()): global http_server_instance http_server_instance = self @@ -998,8 +1025,10 @@ def start(self): self._title, *self._userdata) shost, sport = self._sserver.socket.getsockname()[:2] # when listening on multiple net interfaces the browsers connects to localhost - if shost == '0.0.0.0': + if shost == '0.0.0.0' and 'C9_IP' not in os.environ: shost = '127.0.0.1' + else: + os.environ['BROWSER'] = '/bin/true' # prevent curses based display not visible on c9 self._base_address = 'http://%s:%s/' % (shost,sport) self._log.info('Started httpserver %s' % self._base_address) if self._start_browser: @@ -1051,7 +1080,7 @@ def stop(self): class StandaloneServer(Server): def __init__(self, gui_class, title='', width=800, height=600, resizable=True, fullscreen=False, start=True, userdata=()): - Server.__init__(self, gui_class, title=title, start=False, address='127.0.0.1', port=0, username=None, + Server.__init__(self, gui_class, title=title, start=False, address='0.0.0.0', port=0, username=None, password=None, multiple_instance=False, enable_file_cache=True, update_interval=0.1, start_browser=False, websocket_timeout_timer_ms=1000, websocket_port=0, host_name=None, From 931b62fefb6bdd0774494d7364a3d3028a2180fa Mon Sep 17 00:00:00 2001 From: Dirk Grunwald Date: Sat, 11 Nov 2017 12:43:49 -0700 Subject: [PATCH 2/5] suppress launch webbrowser on c9 in better way --- remi/server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/remi/server.py b/remi/server.py index 89bc06e0..d1b63217 100644 --- a/remi/server.py +++ b/remi/server.py @@ -1027,8 +1027,6 @@ def start(self): # when listening on multiple net interfaces the browsers connects to localhost if shost == '0.0.0.0' and 'C9_IP' not in os.environ: shost = '127.0.0.1' - else: - os.environ['BROWSER'] = '/bin/true' # prevent curses based display not visible on c9 self._base_address = 'http://%s:%s/' % (shost,sport) self._log.info('Started httpserver %s' % self._base_address) if self._start_browser: @@ -1039,7 +1037,7 @@ def start(self): # use default browser instead of always forcing IE on Windows if os.name == 'nt': webbrowser.get('windows-default').open(self._base_address) - else: + elif 'C9_IP' not in os.environ: webbrowser.open(self._base_address) self._sth = threading.Thread(target=self._sserver.serve_forever) self._sth.daemon = False From 9a9cc32790320426c45abb771b964675255f47af Mon Sep 17 00:00:00 2001 From: Dirk Grunwald Date: Sat, 11 Nov 2017 12:50:02 -0700 Subject: [PATCH 3/5] add readme --- README-c9.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 README-c9.md diff --git a/README-c9.md b/README-c9.md new file mode 100644 index 00000000..2a581291 --- /dev/null +++ b/README-c9.md @@ -0,0 +1,19 @@ +## Modifictions for cloud9 + +Cloud9 is web IDE system that can proxy web & websockets connections +to applications. Because of the proxying, the base REMI system does +not work -- for example, the original REMI code uses the IP address of +the server to configure the JavaScript code to specify the host that +the WS should connect to. In C9, this is the internal (non-routed) IP +address. Thus, a number of small changes are made to make the code work +better in C9. + +Another change has to do with the key exchange in WS. For reasons not +completely clear to me, the C9 proxy appears to lower-case the HTTP +response received by the server. This causes the base REMI code to +fail, so I added a specific for extracting the WS key when the lower +can version on C9. + +To install on C9, use `sudo pip install git+https://github.com/dirkcgrunwald/remi.git@c9` + +You should then be able to run any of the examples. From 3349677ecb5d774d536a0d0a65cf1707478ccc86 Mon Sep 17 00:00:00 2001 From: Dirk Grunwald Date: Wed, 15 Nov 2017 09:51:44 -0700 Subject: [PATCH 4/5] make c9 change a little more robust --- remi/server.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/remi/server.py b/remi/server.py index d1b63217..7b303801 100644 --- a/remi/server.py +++ b/remi/server.py @@ -229,10 +229,16 @@ def handshake(self): # # Code used to look for Sec-WebSocket-Key, some clients respond in lower case # - if 'C9_HOSTNAME' in os.environ: - key = data.decode().split('sec-websocket-key: ')[1].split('\r\n')[0] - else: + try: key = data.decode().split('Sec-WebSocket-Key: ')[1].split('\r\n')[0] + except IndexError: + try: + # + # Try Cloud9 method.. + # + key = data.decode().split('sec-websocket-key: ')[1].split('\r\n')[0] + except: + return digest = hashlib.sha1((key.encode("utf-8")+self.magic)) self._log.debug('key is ' + key) digest = digest.digest() From c6fc362ad85cb032e7375e8d82840a2acc3bd657 Mon Sep 17 00:00:00 2001 From: Dirk Grunwald Date: Wed, 15 Nov 2017 09:52:28 -0700 Subject: [PATCH 5/5] add ellipse SVG object --- remi/gui.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/remi/gui.py b/remi/gui.py index aa6228f6..f00558a0 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -2975,6 +2975,41 @@ def set_position(self, x, y): self.attributes['cx'] = str(x) self.attributes['cy'] = str(y) +class SvgEllipse(SvgShape): + """svg ellipse - an ellipse represented filled and with a stroke.""" + + @decorate_constructor_parameter_types([int, int, int]) + def __init__(self, x, y, rx, ry, **kwargs): + """ + Args: + x (int): the x center point of the circle + y (int): the y center point of the circle + rx (int): radius in x + ry (int): radius in y + kwargs: See Widget.__init__() + """ + super(SvgEllipse, self).__init__(x, y, **kwargs) + self.set_radius(rx, ry) + self.type = 'ellipse' + + def set_radius(self, rx, ry): + """Sets the circle radius. + + Args: + radius (int): the circle radius + """ + self.attributes['rx'] = str(rx) + self.attributes['ry'] = str(ry) + + def set_position(self, x, y): + """Sets the circle position. + + Args: + x (int): the x coordinate + y (int): the y coordinate + """ + self.attributes['cx'] = str(x) + self.attributes['cy'] = str(y) class SvgLine(Widget):