From 83ef9b7d2b89730307edb39145bcd7a9f5c54377 Mon Sep 17 00:00:00 2001 From: Chance Russell Date: Mon, 13 May 2024 16:54:28 -0500 Subject: [PATCH] Enable Unix domain socket support This patch allows users to connect to nREPL servers running on Unix domain sockets by modifying :FireplaceConnect to let users provide a connection string pointing to a socket file on the filesystem. This is accomplished by modifying the setup code to identify such connection strings and skip the URL building process and updating the Python script to look for non-URL strings and attempt to connect to them as socket type `AF_UNIX`. For now, socket connections are assumed to be using the `nrepl` scheme, and the current directory (or `b:java_root`) is used to populate the "Scope connection to: " prompt. Also removes the vestigial second argument to the Python script. --- autoload/fireplace.vim | 48 +++++++++++++++++++++++++------- autoload/fireplace/transport.vim | 31 +++++++-------------- pythonx/fireplace.py | 43 ++++++++++++++++++++-------- 3 files changed, 79 insertions(+), 43 deletions(-) diff --git a/autoload/fireplace.vim b/autoload/fireplace.vim index 4f990c23..d739c2cb 100644 --- a/autoload/fireplace.vim +++ b/autoload/fireplace.vim @@ -587,8 +587,9 @@ function! fireplace#register_port_file(portfile, ...) abort endif if empty(old) && getfsize(portfile) > 0 let port = matchstr(readfile(portfile, 'b', 1)[0], '\d\+') + let url = s:build_url(port) try - let transport = fireplace#transport#connect(port) + let transport = fireplace#transport#connect(url, 'nrepl') let session = transport.Clone() let s:repl_portfiles[portfile] = { \ 'time': getftime(portfile), @@ -626,10 +627,24 @@ function! fireplace#ConnectComplete(A, L, P) abort return options endfunction +function! s:build_url(arg) abort + let url = a:arg + if url =~# '^\d\+$' + let url = 'nrepl://localhost:' . url + elseif url =~# '^[^:/@]\+\(:\d\+\)\=$' + let url = 'nrepl://' . url + elseif url !~# '^\a\+://' + throw "Fireplace: invalid connection string " . string(a:arg) + endif + let url = substitute(url, '^\a\+://[^/]*\zs$', '/', '') + let url = substitute(url, '^nrepl://[^/:]*\zs/', ':7888/', '') + return url +endfunction + function! fireplace#ConnectCommand(line1, line2, range, bang, mods, arg, args) abort let str = get(a:args, 0, '') if empty(str) - let str = input('Port or URL: ') + let str = input('Port, path, or URL: ') if empty(str) return '' endif @@ -638,17 +653,30 @@ function! fireplace#ConnectCommand(line1, line2, range, bang, mods, arg, args) a if str =~# '^[%#]' let str = expand(str) endif - if str !~# '^\d\+$\|:\d\|:[\/][\/]' && filereadable(str) - let path = fnamemodify(str, ':p:h') - let str = readfile(str, '', 1)[0] - elseif str !~# '^\d\+$\|:\d\|:[\/][\/]' && filereadable(str . '/.nrepl-port') - let path = fnamemodify(str, ':p:h') - let str = readfile(str . '/.nrepl-port', '', 1)[0] - else + + let filestring = str !~# '^\d\+$\|:\d\|:[\/][\/]' + if filestring && getftype(resolve(str)) == 'socket' + " User specified a path resolving to an AF_UNIX socket, so pass the path + " through without modification let path = fnamemodify(exists('b:java_root') ? b:java_root : getcwd(), ':~') + let scheme = 'nrepl' + else + " User specified a URL or a string pointing to a file that contains a + " port number. Construct a URL then use it to determine the scheme + if filestring && filereadable(str) + let path = fnamemodify(str, ':p:h') + let str = readfile(str, '', 1)[0] + elseif filestring && filereadable(str . '/.nrepl-port') + let path = fnamemodify(str, ':p:h') + let str = readfile(str . '/.nrepl-port', '', 1)[0] + else + let path = fnamemodify(exists('b:java_root') ? b:java_root : getcwd(), ':~') + endif + let str = s:build_url(str) + let scheme = matchstr(str, '^\a\+') endif try - let transport = fireplace#transport#connect(str) + let transport = fireplace#transport#connect(str, scheme) catch /.*/ return 'echoerr '.string(v:exception) endtry diff --git a/autoload/fireplace/transport.vim b/autoload/fireplace/transport.vim index e398471d..bd3e2787 100644 --- a/autoload/fireplace/transport.vim +++ b/autoload/fireplace/transport.vim @@ -168,35 +168,24 @@ augroup fireplace_transport \ | endfor augroup END -function! fireplace#transport#connect(arg) abort - let url = substitute(a:arg, '#.*', '', '') - if url =~# '^\d\+$' - let url = 'nrepl://localhost:' . url - elseif url =~# '^[^:/@]\+\(:\d\+\)\=$' - let url = 'nrepl://' . url - elseif url !~# '^\a\+://' - throw "Fireplace: invalid connection string " . string(a:arg) +function! fireplace#transport#connect(str, scheme) abort + if has_key(s:urls, a:str) + return s:urls[a:str].transport endif - let url = substitute(url, '^\a\+://[^/]*\zs$', '/', '') - let url = substitute(url, '^nrepl://[^/:]*\zs/', ':7888/', '') - if has_key(s:urls, url) - return s:urls[url].transport - endif - let scheme = matchstr(url, '^\a\+') - if scheme ==# 'nrepl' + if a:scheme ==# 'nrepl' let command = [g:fireplace_python_executable, s:python_dir.'/fireplace.py'] - elseif exists('g:fireplace_argv_' . scheme) - let command = g:fireplace_argv_{scheme} + elseif exists('g:fireplace_argv_' . a:scheme) + let command = g:fireplace_argv_{a:scheme} else - throw 'Fireplace: unsupported protocol ' . scheme + throw 'Fireplace: unsupported protocol ' . a:scheme endif let transport = deepcopy(s:transport) - let transport.url = url + let transport.url = a:str let transport.state = {} let transport.sessions = {} let transport.requests = {} - let cb_args = [url, transport.state, transport.requests, transport.sessions] - let transport.job = s:json_start(command + [url], function('s:json_callback', cb_args), function('s:exit_callback', cb_args)) + let cb_args = [a:str, transport.state, transport.requests, transport.sessions] + let transport.job = s:json_start(command + [a:str], function('s:json_callback', cb_args), function('s:exit_callback', cb_args)) while !has_key(transport.state, 'status') && transport.Alive() sleep 1m endwhile diff --git a/pythonx/fireplace.py b/pythonx/fireplace.py index a5a5549e..47315789 100644 --- a/pythonx/fireplace.py +++ b/pythonx/fireplace.py @@ -2,6 +2,7 @@ import os import re import socket +import stat import sys import traceback import threading @@ -92,18 +93,39 @@ def quickfix(t, e, tb): 'text': line}) return {'title': str(e), 'items': items} + +def parse_dest(dest): + match = re.search('//([^:/@]+)(?::(\d+))?', dest) + if match is not None: + host = match.groups()[0] + port = match.groups()[1] + return (None, host, int(port or 7888)) + elif os.path.exists(dest) and stat.S_ISSOCK(os.stat(dest).st_mode): + return (dest, None, None) + else: + raise ValueError("Connection string must be a URL or a path to a socket file") + + class Connection: - def __init__(self, host, port, keepalive_file=None): + def __init__(self, dest, keepalive_file=None): + parsed_dest = parse_dest(dest) + (path, host, port) = parse_dest(dest) + self.path = path + self.host = host + self.port = port self.keepalive_file = keepalive_file self.connected = False - self.host = host - self.port = int(port) def socket(self): if not self.connected: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(8) - s.connect((self.host, self.port)) + if self.path: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(8) + s.connect(self.path) + else: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(8) + s.connect((self.host, self.port)) s.setblocking(1) self._socket = s self.connected = True @@ -176,13 +198,10 @@ def tunnel(self): self.notify(["exception", quickfix(*sys.exc_info())]) os._exit(3) -def main(host = None, port = None, *args): + +def main(dest = None, *args): try: - match = re.search('//([^:/@]+)(?::(\d+))?', host) - if match: - host = match.groups()[0] - port = match.groups()[1] - conn = Connection(host, int(port or 7888)) + conn = Connection(dest) try: conn.tunnel() finally: