import sublime,sublime_plugin,threading,json,os,threading,subprocess,socket,shutil from base64 import b64encode, b64decode # python 2.6 differences try: from hashlib import md5, sha1 except: from md5 import md5; from sha import sha as sha1 from SimpleHTTPServer import SimpleHTTPRequestHandler from struct import pack, unpack_from import array, struct, os import urllib2 s2a = lambda s: [ord(c) for c in s] settings = sublime.load_settings('LiveReload.sublime-settings') port = settings.get('port') version = settings.get('version') ##LOAD latest livereload.js from github (for v2 of protocol) or if this fails local version try: req = urllib2.urlopen(urllib2.Request("http://raw.github.com/livereload/livereload-js/master/dist/livereload.js")) source_livereload_js = req.read() if not "http://livereload.com/protocols/official-6" in source_livereload_js: raise Exception("Something wrong with download!") except Exception, u: print u try: path = os.path.join(sublime.packages_path(), "LiveReload") local = open(os.path.join(path, "livereload.js"), "rU") source_livereload_js = local.read() except IOError, e: print e sublime.error_message("livereload.js is missing from LiveReload package install") class LiveReload(threading.Thread): def run(self): global LivereloadFactory threading.Thread.__init__(self) LivereloadFactory = WebSocketServer(port,version) LivereloadFactory.start() class LiveReloadChange(sublime_plugin.EventListener): def __init__ (self): LiveReload().start() def __del__(self): global LivereloadFactory LivereloadFactory.stop() def on_post_save(self, view): global LivereloadFactory settings = sublime.load_settings('LiveReload.sublime-settings') filename = view.file_name() if view.file_name().find('.scss') > 0 or view.file_name().find('.sass') > 0: compiler = CompassThread(filename,LivereloadFactory) compiler.start() else: filename = os.path.normcase(filename) filename = os.path.split(filename)[1] filename = filename.replace('.scss','.css').replace('.styl','.css').replace('.less','.css') filename = filename.replace('.coffee','.js') data = json.dumps(["refresh", { "path": filename, "apply_js_live": settings.get('apply_js_live'), "apply_css_live": settings.get('apply_css_live'), "apply_images_live": settings.get('apply_images_live') }]) sublime.set_timeout(lambda: LivereloadFactory.send_all(data), int(settings.get('delay_ms'))) sublime.set_timeout(lambda: sublime.status_message("Sent LiveReload command for file: "+filename), int(settings.get('delay_ms'))) class CompassThread(threading.Thread): def __init__(self, filename,LivereloadFactory): self.dirname = os.path.dirname(filename) self.filename = filename.replace('.scss','.css').replace('.sass','.css') self.LivereloadFactory = LivereloadFactory self.stdout = None self.stderr = None threading.Thread.__init__(self) def run(self): global LivereloadFactory print 'compass compile ' + self.dirname # autocreate config.rb for compass if not os.path.exists(os.path.join(self.dirname, "config.rb")): print "Generating config.rb" shutil.copy(os.path.join(sublime.packages_path(), "LiveReload","assets","config.rb"), self.dirname) # compass compile p = subprocess.Popen(['compass compile ' + self.dirname.replace('\\','/')],shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) if p.stdout.read() : self.LivereloadFactory.send_all(json.dumps(["refresh", { "path": self.filename.replace('\\','/'), "apply_js_live": True, "apply_css_live": True, "apply_images_live": True }])) class WebSocketServer: """ Handle the Server, bind and accept new connections, open and close clients connections. """ def __init__(self, port, version): self.clients = [] self.port = port self.version = version self.s = None def stop(self): [client.close() for client in self.clients] l = threading.Lock() l.acquire() l.release() def start(self): """ Start the server. """ try: self.s = socket.socket() self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.s.bind(('', self.port)) self.s.listen(1) except Exception, e: self.stop() try: while 1: conn, addr = self.s.accept() newClient = WebSocketClient(conn, addr, self) self.clients.append(newClient) newClient.start() except Exception, e: self.stop() def send_all(self, data): """ Send a message to all the currenly connected clients. """ [client.send(data) for client in self.clients] def remove(self, client): """ Remove a client from the connected list. """ try: l = threading.Lock() l.acquire() self.clients.remove(client) l.release() except Exception, e: pass class WebSocketClient(threading.Thread): """ A single connection (client) of the program """ # Handshaking, create the WebSocket connection server_handshake_hybi = """HTTP/1.1 101 Switching Protocols\r Upgrade: websocket\r Connection: Upgrade\r Sec-WebSocket-Accept: %s\r """ GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" def __init__(self, sock, addr, server): threading.Thread.__init__(self) self.s = sock self.addr = addr self.server = server def run(self): wsh = WSRequestHandler(self.s, self.addr, False) h = self.headers = wsh.headers ver = h.get('Sec-WebSocket-Version') if ver: # HyBi/IETF version of the protocol # HyBi-07 report version 7 # HyBi-08 - HyBi-12 report version 8 # HyBi-13 reports version 13 if ver in ['7', '8', '13']: self.version = "hybi-%02d" % int(ver) else: raise Exception('Unsupported protocol version %s' % ver) key = h['Sec-WebSocket-Key'] print key # Generate the hash value for the accept header accept = b64encode(sha1(key + self.GUID).digest()) response = self.server_handshake_hybi % accept response += "\r\n" print response self.s.send(response.encode()) self.new_client() # Receive and handle data while 1: try: data = self.s.recv(1024) except Exception, e: break if not data: break dec = WebSocketClient.decode_hybi(data) if dec["opcode"] == 8: self.close() else: self.onreceive(dec) # Close the client connection self.close() @staticmethod def unmask(buf, f): pstart = f['hlen'] + 4 pend = pstart + f['length'] # Slower fallback data = array.array('B') mask = s2a(f['mask']) data.fromstring(buf[pstart:pend]) for i in range(len(data)): data[i] ^= mask[i % 4] return data.tostring() @staticmethod def encode_hybi(buf, opcode, base64=False): """ Encode a HyBi style WebSocket frame. Optional opcode: 0x0 - continuation 0x1 - text frame (base64 encode buf) 0x2 - binary frame (use raw buf) 0x8 - connection close 0x9 - ping 0xA - pong """ if base64: buf = b64encode(buf) b1 = 0x80 | (opcode & 0x0f) # FIN + opcode payload_len = len(buf) if payload_len <= 125: header = pack('>BB', b1, payload_len) elif payload_len > 125 and payload_len < 65536: header = pack('>BBH', b1, 126, payload_len) elif payload_len >= 65536: header = pack('>BBQ', b1, 127, payload_len) print("Encoded: %s" % repr(header + buf)) return header + buf, len(header), 0 @staticmethod def decode_hybi(buf, base64=False): """ Decode HyBi style WebSocket packets. Returns: {'fin' : 0_or_1, 'opcode' : number, 'mask' : 32_bit_number, 'hlen' : header_bytes_number, 'length' : payload_bytes_number, 'payload' : decoded_buffer, 'left' : bytes_left_number, 'close_code' : number, 'close_reason' : string} """ f = {'fin' : 0, 'opcode' : 0, 'mask' : 0, 'hlen' : 2, 'length' : 0, 'payload' : None, 'left' : 0, 'close_code' : None, 'close_reason' : None} blen = len(buf) f['left'] = blen if blen < f['hlen']: return f # Incomplete frame header b1, b2 = unpack_from(">BB", buf) f['opcode'] = b1 & 0x0f f['fin'] = (b1 & 0x80) >> 7 has_mask = (b2 & 0x80) >> 7 f['length'] = b2 & 0x7f if f['length'] == 126: f['hlen'] = 4 if blen < f['hlen']: return f # Incomplete frame header (f['length'],) = unpack_from('>xxH', buf) elif f['length'] == 127: f['hlen'] = 10 if blen < f['hlen']: return f # Incomplete frame header (f['length'],) = unpack_from('>xxQ', buf) full_len = f['hlen'] + has_mask * 4 + f['length'] if blen < full_len: # Incomplete frame return f # Incomplete frame header # Number of bytes that are part of the next frame(s) f['left'] = blen - full_len # Process 1 frame if has_mask: # unmask payload f['mask'] = buf[f['hlen']:f['hlen']+4] f['payload'] = WebSocketClient.unmask(buf, f) else: print("Unmasked frame: %s" % repr(buf)) f['payload'] = buf[(f['hlen'] + has_mask * 4):full_len] if base64 and f['opcode'] in [1, 2]: try: f['payload'] = b64decode(f['payload']) except: print("Exception while b64decoding buffer: %s" % repr(buf)) raise if f['opcode'] == 0x08: if f['length'] >= 2: f['close_code'] = unpack_from(">H", f['payload']) if f['length'] > 3: f['close_reason'] = f['payload'][2:] return f def close(self): """ Close this connection """ self.server.remove(self) self.s.close() def send(self, msg): """ Send a message to this client """ msg = WebSocketClient.encode_hybi(msg, 0x1, False) self.s.send(msg[0]) def onreceive(self, data): """ Event called when a message is received from this client """ try: print data if "payload" in data: print "payload true" if "hello" in data.get("payload"): sublime.set_timeout(lambda: sublime.status_message("New LiveReload v2 client connected"), 100) self.send('{"command":"hello","protocols":["http://livereload.com/protocols/connection-check-1","http://livereload.com/protocols/official-6"]}') else: sublime.set_timeout(lambda: sublime.status_message("New LiveReload v1 client connected"), 100) self.send("!!ver:" + str(self.server.version)) except Exception, e: print e def new_client(self): """ Event called when handshake is compleated """ #self.send("!!ver:"+str(self.server.version)) def _clean(self, msg): """ Remove special chars used for the transmission """ msg = msg.replace(b'\x00', b'', 1) msg = msg.replace(b'\xff', b'', 1) return msg # HTTP handler with WebSocket upgrade support class WSRequestHandler(SimpleHTTPRequestHandler): def __init__(self, req, addr, only_upgrade=True): self.only_upgrade = only_upgrade # only allow upgrades SimpleHTTPRequestHandler.__init__(self, req, addr, object()) def do_GET(self): if (self.headers.get('upgrade') and self.headers.get('upgrade').lower() == 'websocket'): if (self.headers.get('sec-websocket-key1') or self.headers.get('websocket-key1')): # For Hixie-76 read out the key hash self.headers.__setitem__('key3', self.rfile.read(8)) # Just indicate that an WebSocket upgrade is needed self.last_code = 101 self.last_message = "101 Switching Protocols" elif self.only_upgrade: # Normal web request responses are disabled self.last_code = 405 self.last_message = "405 Method Not Allowed" else: #Self injecting plugin if "livereload.js" in self.path: self.send_response(200, 'OK') self.send_header('Content-type', 'text/javascript') self.send_header("Content-Length", len(source_livereload_js)) self.end_headers() self.wfile.write(bytes(source_livereload_js)) return else: #Disable other requests self.send_response(405, 'Method Not Allowed') self.send_header('Content-type', 'text/plain') self.send_header("Content-Length", len("Method Not Allowed")) self.end_headers() self.wfile.write(bytes("Method Not Allowed")) return def send_response(self, code, message=None): # Save the status code self.last_code = code SimpleHTTPRequestHandler.send_response(self, code, message) def log_message(self, f, *args): # Save instead of printing self.last_message = f % args