432 lines
14 KiB
Python
432 lines
14 KiB
Python
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
|
|
|
|
# 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
|