Skip to content

Instantly share code, notes, and snippets.

@tyru
Last active June 17, 2020 14:42
Show Gist options
  • Save tyru/485d0d21d102c6c503273338d3525701 to your computer and use it in GitHub Desktop.
Save tyru/485d0d21d102c6c503273338d3525701 to your computer and use it in GitHub Desktop.
WIP: [Preview] Chrome Debugging Protocol in Vim script
" 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()
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
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