Last active
June 17, 2020 14:42
-
-
Save tyru/485d0d21d102c6c503273338d3525701 to your computer and use it in GitHub Desktop.
WIP: [Preview] Chrome Debugging Protocol in Vim script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
" Run: | |
" 1. mkdir tmp | |
" 2. {chrome} --remote-debugging-port=9222 --no-first-run --no-default-browser-check --user-data-dir=tmp | |
" | |
" In another shell: | |
" 1. vim -S client.vim | |
" | |
" ref. https://developer.mozilla.org/ja/docs/Tools/Remote_Debugging/Chrome_Desktop | |
source client/request.vim | |
source client/websocket.vim | |
let s:V = vital#vital#new() | |
let s:URI = s:V.import('Web.URI') | |
unlet s:V | |
let s:CHROME_PORT = 9222 | |
function! s:run() abort | |
let uri = s:URI.new('http://localhost:' . s:CHROME_PORT . '/json') | |
call g:Request.new() | |
\.get(uri) | |
\.then({res -> json_decode(res.body)}) | |
\.then({ | |
\ tabs -> s:URI.new(tabs[0].webSocketDebuggerUrl) | |
\}).then({ | |
\ ws_url -> s:start_websocket(ws_url) | |
\}).catch({err -> execute('echom "failed:" string(err)', '')}) | |
endfunction | |
function! s:start_websocket(ws_url) abort | |
let ws = s:WebSocket.new() | |
call ws.on('message', {msg -> execute('echom "received message" string(msg)', '')}) | |
call ws.on('open', {-> [ | |
\ execute('echom "opened"', ''), | |
\ ws.send({'id': 1, 'method': 'Timeline.start'}), | |
\]}) | |
call ws.on('close', {-> execute('echom "closed"', '')}) | |
call ws.connect(a:ws_url) | |
endfunction | |
call s:run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let s:V = vital#vital#new() | |
let s:P = s:V.import('Async.Promise') | |
unlet s:V | |
let s:CRLF = "\r\n" | |
let s:Request = {} | |
function! s:Request.new() abort | |
let req = {} | |
for method in [ | |
\ 'head', | |
\ 'get', | |
\ 'put', | |
\ 'post', | |
\ 'patch', | |
\] | |
let req[method] = funcref('s:Request_' . method, req) | |
endfor | |
let req._chunk = s:P.resolve('') | |
let req._read_requests = [] | |
let req._timer = v:null | |
let req._handler = v:null | |
let req._res = {} | |
return req | |
endfunction | |
function! s:Request_head(uri) abort dict | |
return s:P.new({resolve, reject -> | |
\ s:_dial(s:_init(self, resolve, reject), 'HEAD', a:uri, '') | |
\}) | |
endfunction | |
function! s:Request_get(uri) abort dict | |
return s:P.new({resolve, reject -> | |
\ s:_dial(s:_init(self, resolve, reject), 'GET', a:uri, '') | |
\}) | |
endfunction | |
function! s:Request_put(uri, body) abort dict | |
return s:P.new({resolve, reject -> | |
\ s:_dial(s:_init(self, resolve, reject), 'PUT', a:uri, a:body) | |
\}) | |
endfunction | |
function! s:Request_post(uri, body) abort dict | |
return s:P.new({resolve, reject -> | |
\ s:_dial(s:_init(self, resolve, reject), 'POST', a:uri, a:body) | |
\}) | |
endfunction | |
function! s:Request_patch(uri, body) abort dict | |
return s:P.new({resolve, reject -> | |
\ s:_dial(s:_init(self, resolve, reject), 'PATCH', a:uri, a:body) | |
\}) | |
endfunction | |
function! s:_init(self, resolve, reject) abort | |
let self = a:self | |
let self._resolve = a:resolve | |
let self._reject = a:reject | |
return self | |
endfunction | |
function! s:_dial(self, method, uri, body) abort | |
let [self, method, uri] = [a:self, a:method, a:uri] | |
if uri.scheme() ==# 'https' | |
" TODO: | |
" * should Web.URI detect non-plain protocols (like Web.URI.is_secure()) | |
" * should detect other non-plain protocols? | |
throw 'Request: cannot ' . a:method . ' https:// URI' | |
endif | |
let addr = uri.host() . ':' . uri.port() | |
let self._handler = ch_open(addr, { | |
\ 'mode': 'raw', | |
\ 'drop': 'never', | |
\ 'callback': {_, msg -> s:_on_msg(self, msg)}, | |
\}) | |
let req = method . ' ' . uri.path() . ' HTTP/1.1' . s:CRLF . | |
\ 'Host: ' . addr . s:CRLF . | |
\ s:CRLF . | |
\ a:body | |
call ch_sendraw(self._handler, req) | |
let self._timer = timer_start(200, {-> ch_status(self._handler)}, {'repeat': -1}) | |
endfunction | |
function! s:_on_msg(self, msg) abort | |
let self = a:self | |
let self._chunk = self._chunk.then({chunk -> chunk . a:msg}) | |
call s:_parse(self).catch({err -> s:_reject(self, err)}) | |
while !empty(self._read_requests) | |
let resolve = remove(self._read_requests, -1) | |
call resolve() | |
endwhile | |
endfunction | |
function! s:_parse(self) abort | |
let self = a:self | |
return s:_parse_status_line(self) | |
endfunction | |
function! s:_parse_status_line(self) abort | |
let self = a:self | |
return s:_read_until(self, s:CRLF).then({ | |
\ line -> s:_do_parse_status_line(self, line) | |
\}) | |
endfunction | |
function! s:_do_parse_status_line(self, line) abort | |
let self = a:self | |
let STATUS_LINE = '^HTTP/\([0-9]\+\.[0-9]\+\)[[:blank:]]\+\([0-9]\{3}\)[[:blank:]]\+\([^\r\n]*\)$' | |
let m = matchlist(a:line, STATUS_LINE) | |
if empty(m) | |
return s:P.reject('failed to parse status line') | |
endif | |
let self._res.http_version = m[1] | |
let self._res.status_code = m[2] | |
let self._res.status_text = m[3] | |
return s:_parse_headers(self) | |
endfunction | |
function! s:_parse_headers(self) abort | |
let self = a:self | |
if !has_key(self._res, 'headers') | |
let self._res.headers = {} | |
endif | |
return s:_read_until(self, s:CRLF).then({ | |
\ line -> s:_do_parse_header(self, line) | |
\}) | |
endfunction | |
function! s:_do_parse_header(self, line) abort | |
let self = a:self | |
if a:line ==# '' " the line only CRLF | |
return s:_parse_body(self) | |
endif | |
" TODO: | |
" * Stricter pattern | |
" * Header field can wrap over multiple lines (LWS) | |
let HEADER_LINE = '^\(\S\+\):[[:space:]]*\(.*\)$' | |
let m = matchlist(a:line, HEADER_LINE) | |
if empty(m) | |
return s:P.reject('failed to parse header: ' . string(line)) | |
endif | |
let [key, value] = m[1:2] | |
let self._res.headers[tolower(key)] = value | |
" next header | |
return s:_parse_headers(self) | |
endfunction | |
function! s:_parse_body(self) abort | |
let self = a:self | |
if !has_key(self._res.headers, 'content-length') | |
return s:P.reject('Content-Length header doesn''t exist') | |
endif | |
let length = self._res.headers['content-length'] | |
return s:_read(self, length).then({ | |
\ body -> s:_do_parse_body(self, body) | |
\}) | |
endfunction | |
function! s:_do_parse_body(self, body) abort | |
let self = a:self | |
let self._res.body = a:body | |
" Parsing was end. now it's time to return response. | |
call s:_resolve(self, self._res) | |
endfunction | |
function! s:_read(self, n) abort | |
let self = a:self | |
if a:n <=# 0 | |
return s:P.reject('Request: read(): invalid argument was given: ' . string(a:n)) | |
endif | |
return self._chunk.then({ | |
\ chunk -> len(chunk) >=# a:n ? | |
\ s:_update_chunk_n(self, chunk, a:n) : | |
\ s:_wait_until_next_msg(self).then({ | |
\ -> s:_read(self, a:n) | |
\ }) | |
\}) | |
endfunction | |
function! s:_update_chunk_n(self, chunk, n) abort | |
let self = a:self | |
let self._chunk = s:P.resolve(a:chunk[a:n :]) | |
return a:chunk[: a:n-1] | |
endfunction | |
function! s:_read_until(self, needle) abort | |
let self = a:self | |
return self._chunk.then({ | |
\ chunk -> { | |
\ idx -> idx isnot# -1 ? | |
\ s:_update_chunk_until(self, chunk, idx, a:needle) : | |
\ s:_wait_until_next_msg(self).then({ | |
\ -> s:_read_until(self, a:needle) | |
\ }) | |
\ }(stridx(chunk, a:needle)) | |
\}) | |
endfunction | |
function! s:_update_chunk_until(self, chunk, idx, needle) abort | |
let self = a:self | |
if a:idx is# 0 | |
" a:chunk starts with a:needle | |
let self._chunk = s:P.resolve(a:chunk[strlen(a:needle) :]) | |
return s:P.resolve('') | |
else | |
" a:chunk has a:needle in the middle | |
let self._chunk = s:P.resolve(a:chunk[a:idx + strlen(a:needle) :]) | |
return s:P.resolve(a:chunk[: a:idx-1]) | |
endif | |
endfunction | |
function! s:_wait_until_next_msg(self) abort | |
let self = a:self | |
return s:P.new({ | |
\ resolve -> add(self._read_requests, resolve) | |
\}) | |
endfunction | |
function! s:_resolve(self, ...) abort | |
let self = a:self | |
call s:_finalize(self) | |
call call(self._resolve, a:000) | |
endfunction | |
function! s:_reject(self, ...) abort | |
let self = a:self | |
call s:_finalize(self) | |
call call(self._reject, a:000) | |
endfunction | |
function! s:_finalize(self) abort | |
let self = a:self | |
if self._timer isnot# v:null | |
call timer_stop(self._timer) | |
let self._timer = v:null | |
endif | |
if self._handler isnot# v:null | |
call ch_close(self._handler) | |
let self._handler = v:null | |
endif | |
endfunction | |
" for local script | |
" TODO: define as vital module | |
let g:Request = s:Request |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let s:V = vital#vital#new() | |
let s:P = s:V.import('Async.Promise') | |
unlet s:V | |
let s:CRLF = "\r\n" | |
let s:WebSocket = {} | |
function! s:WebSocket_new() abort | |
let self = {} | |
let self._interval = 200 | |
let self._events = {} | |
let self._connected_info = {} | |
let self.connect = funcref('s:WebSocket_connect') | |
let self.close = funcref('s:WebSocket_close') | |
let self.is_connected = funcref('s:WebSocket_is_connected') | |
let self.on = funcref('s:WebSocket_on') | |
let self.send = funcref('s:WebSocket_send') | |
return self | |
endfunction | |
let s:WebSocket.new = funcref('s:WebSocket_new') | |
function! s:WebSocket_connect(uri) abort dict | |
if self.is_connected() | |
return | |
endif | |
if a:uri.scheme() isnot# 'ws' | |
throw 'WebSocket: connect(): not ws:// URI was given: ' . a:uri.to_string() | |
endif | |
let addr = a:uri.host() . ':' . a:uri.port() | |
let handler = ch_open(addr, { | |
\ 'mode': 'raw', | |
\ 'callback': {_, msg -> s:_parse_chunk(self, msg)}, | |
\}) | |
" TODO: Generate Sec-WebSocket-Key | |
let req = 'GET ' . a:uri.path() . ' HTTP/1.1' . s:CRLF . | |
\ 'Host: ' . addr . s:CRLF . | |
\ 'Upgrade: websocket' . s:CRLF . | |
\ 'Connection: Upgrade' . s:CRLF . | |
\ 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' . s:CRLF . | |
\ 'Sec-WebSocket-Version: 13' . s:CRLF . | |
\ s:CRLF | |
call ch_sendraw(handler, req) | |
" Start timer | |
let timer = timer_start(self._interval, {-> ch_status(handler)}, {'repeat': -1}) | |
let self._connected_info = { | |
\ 'handler': handler, | |
\ 'timer': timer, | |
\} | |
if has_key(self._events, 'open') | |
call call(self._events.open, []) | |
endif | |
endfunction | |
" TODO: Parse headers and check "Sec-WebSocket-Accept" | |
function! s:_parse_chunk(self, msg) abort | |
let self = a:self | |
" TODO: unpack frame | |
echom 's:_parse_chunk()' strtrans(a:msg) | |
if has_key(self._events, 'message') | |
call call(self._events.message, [msg]) | |
endif | |
endfunction | |
function! s:WebSocket_close() abort dict | |
if !self.is_connected() | |
return | |
endif | |
" TODO: Send FIN frame? | |
call timer_stop(self._connected_info.timer) | |
call ch_close(self._connected_info.handler) | |
let self._connected_info = {} | |
endfunction | |
function! s:WebSocket_is_connected() abort dict | |
return !empty(self._connected_info) | |
endfunction | |
function! s:WebSocket_on(event, f) abort dict | |
let t = type(a:f) | |
if t isnot# v:t_string && t isnot# v:t_func | |
throw 'WebSocket: on(): String nor Funcref was given' | |
endif | |
let self._events[a:event] += [a:f] | |
endfunction | |
function! s:WebSocket_send(value) abort dict | |
if !self.is_connected() | |
return | |
endif | |
endfunction | |
" for local script | |
" TODO: define as vital module | |
let g:WebSocket = s:WebSocket |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment