Feature: Add support for named-pipe sockets for LSPs (#3509)

* Add support for using named pipes for lsp 'socket' servers; documentation updated accordingly
* Add tests for connecting to named pipe sockets
This commit is contained in:
Kevin Svetlitski 2021-01-26 14:43:17 -06:00 committed by GitHub
parent 3a1728297a
commit cab4280d02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 103 additions and 12 deletions

View file

@ -72,9 +72,8 @@ function! ale#socket#Open(address, options) abort
elseif exists('*chansend') && exists('*sockconnect') elseif exists('*chansend') && exists('*sockconnect')
" NeoVim 0.3+ " NeoVim 0.3+
try try
let l:channel_id = sockconnect('tcp', a:address, { let l:channel_id = sockconnect(stridx(a:address, ':') != -1 ? 'tcp' : 'pipe',
\ 'on_data': function('s:NeoVimOutputCallback'), \ a:address, {'on_data': function('s:NeoVimOutputCallback')})
\})
let l:channel_info.last_line = '' let l:channel_info.last_line = ''
catch /connection failed/ catch /connection failed/
let l:channel_id = -1 let l:channel_id = -1

View file

@ -228,8 +228,8 @@ A minimal configuration for a language server linter might look so. >
\ 'project_root': '/path/to/root_of_project', \ 'project_root': '/path/to/root_of_project',
\}) \})
< <
For language servers that use a TCP socket connection, you should define the For language servers that use a TCP or named pipe socket connection, you
address to connect to instead. > should define the address to connect to instead. >
call ale#linter#Define('filetype_here', { call ale#linter#Define('filetype_here', {
\ 'name': 'any_name_you_want', \ 'name': 'any_name_you_want',
@ -3852,7 +3852,7 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()*
When this argument is set to `'socket'`, then the When this argument is set to `'socket'`, then the
linter will be defined as an LSP linter via a TCP linter will be defined as an LSP linter via a TCP
socket connection. `address` must be set. or named pipe socket connection. `address` must be set.
ALE will not start a server automatically. ALE will not start a server automatically.
@ -3877,7 +3877,10 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()*
`address` A |String| representing an address to connect to, `address` A |String| representing an address to connect to,
or a |Funcref| accepting a buffer number and or a |Funcref| accepting a buffer number and
returning the |String|. returning the |String|. If the value contains a
colon, it is interpreted as referring to a TCP
socket; otherwise it is interpreted as the path of a
named pipe.
The result can be computed with |ale#command#Run()|. The result can be computed with |ale#command#Run()|.

View file

@ -0,0 +1,42 @@
"""
This Python script creates a named pipe server that does nothing but send its input
back to the client that connects to it. Only one argument must be given, the path
of a named pipe to bind to.
"""
import os
import socket
import sys
def main():
if len(sys.argv) < 2:
sys.exit('You must specify a filepath')
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if os.path.exists(sys.argv[1]):
os.remove(sys.argv[1])
sock.bind(sys.argv[1])
sock.listen(0)
pid = os.fork()
if pid:
print(pid)
sys.exit()
while True:
connection = sock.accept()[0]
connection.settimeout(5)
while True:
try:
connection.send(connection.recv(1024))
except socket.timeout:
break
connection.close()
if __name__ == "__main__":
main()

View file

@ -28,11 +28,17 @@ Before:
endfunction endfunction
let g:port = 10347 let g:port = 10347
let g:pid = str2nr(system( let g:pid_tcp = str2nr(system(
\ 'python' \ 'python'
\ . ' ' . ale#Escape(g:dir . '/dumb_tcp_server.py') \ . ' ' . ale#Escape(g:dir . '/dumb_tcp_server.py')
\ . ' ' . g:port \ . ' ' . g:port
\)) \))
let g:pipe_path = 'tmp_named_pipe'
let g:pid_pipe = str2nr(system(
\ 'python'
\ . ' ' . ale#Escape(g:dir . '/dumb_named_pipe_server.py')
\ . ' ' . g:pipe_path
\))
endif endif
After: After:
@ -46,17 +52,23 @@ After:
delfunction WaitForData delfunction WaitForData
delfunction TestCallback delfunction TestCallback
if has_key(g:, 'pid') if has_key(g:, 'pid_tcp')
call system('kill ' . g:pid) call system('kill ' . g:pid_tcp)
endif endif
unlet! g:pid if has_key(g:, 'pid_pipe')
call system('kill ' . g:pid_pipe)
endif
unlet! g:pid_tcp
unlet! g:port unlet! g:port
unlet! g:pid_pipe
unlet! g:pipe_path
endif endif
unlet! g:can_run_socket_tests unlet! g:can_run_socket_tests
Execute(Sending and receiving connections to sockets should work): Execute(Sending and receiving connections to tcp sockets should work):
if g:can_run_socket_tests if g:can_run_socket_tests
let g:channel_id = ale#socket#Open( let g:channel_id = ale#socket#Open(
\ '127.0.0.1:' . g:port, \ '127.0.0.1:' . g:port,
@ -90,3 +102,38 @@ Execute(Sending and receiving connections to sockets should work):
\ {'callback': function('function')} \ {'callback': function('function')}
\) \)
endif endif
Execute(Sending and receiving connections to named pipe sockets should work):
if g:can_run_socket_tests && has('nvim')
let g:channel_id = ale#socket#Open(
\ g:pipe_path,
\ {'callback': function('TestCallback')}
\)
Assert g:channel_id >= 0, 'The socket was not opened!'
call ale#socket#Send(g:channel_id, 'hello')
call ale#socket#Send(g:channel_id, ' world')
AssertEqual 1, ale#socket#IsOpen(g:channel_id)
" Wait up to 1 second for the expected data to arrive.
call WaitForData('hello world', 1000)
AssertEqual g:channel_id, g:channel_id_received
AssertEqual 'hello world', g:data_received
AssertEqual g:pipe_path, ale#socket#GetAddress(g:channel_id)
call ale#socket#Close(g:channel_id)
AssertEqual 0, ale#socket#IsOpen(g:channel_id)
AssertEqual '', ale#socket#GetAddress(g:channel_id)
endif
" NeoVim versions which can't connect to sockets should just fail.
if has('nvim') && !exists('*chanclose')
AssertEqual -1, ale#socket#Open(
\ 'tmp_named_pipe',
\ {'callback': function('function')}
\)
endif