# HG changeset patch # User Bram Moolenaar # Date 1649084404 -7200 # Node ID d32dc906dd2c72d069492fd80b85d1cf47cc87ab # Parent 6ce57599817a3b2b4a3fdd4a793de6797205a0e8 patch 8.2.4684: cannot open a channel on a Unix domain socket Commit: https://github.com/vim/vim/commit/cc766a85f460ebb7f8c915508447548b5f5b99bc Author: LemonBoy Date: Mon Apr 4 15:46:58 2022 +0100 patch 8.2.4684: cannot open a channel on a Unix domain socket Problem: Cannot open a channel on a Unix domain socket. Solution: Add Unix domain socket support. (closes https://github.com/vim/vim/issues/10062) diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt --- a/runtime/doc/channel.txt +++ b/runtime/doc/channel.txt @@ -119,10 +119,13 @@ To open a channel: > Use |ch_status()| to see if the channel could be opened. -{address} has the form "hostname:port". E.g., "localhost:8765". - -When using an IPv6 address, enclose it within square brackets. E.g., -"[2001:db8::1]:8765". + *channel-address* +{address} can be a domain name or an IP address, followed by a port number, or +a Unix-domain socket path prefixed by "unix:". E.g. > + www.example.com:80 " domain + port + 127.0.0.1:1234 " IPv4 + port + [2001:db8::1]:8765 " IPv6 + port + unix:/tmp/my-socket " Unix-domain socket path {options} is a dictionary with optional entries: *channel-open-options* @@ -579,10 +582,15 @@ ch_info({handle}) *ch_info()* When opened with ch_open(): "hostname" the hostname of the address "port" the port of the address + "path" the path of the Unix-domain socket "sock_status" "open" or "closed" "sock_mode" "NL", "RAW", "JSON" or "JS" "sock_io" "socket" "sock_timeout" timeout in msec + + Note that "pair" is only present for Unix-domain sockets, for + regular ones "hostname" and "port" are present instead. + When opened with job_start(): "out_status" "open", "buffered" or "closed" "out_mode" "NL", "RAW", "JSON" or "JS" @@ -641,11 +649,8 @@ ch_open({address} [, {options}]) *ch_ Open a channel to {address}. See |channel|. Returns a Channel. Use |ch_status()| to check for failure. - {address} is a String and has the form "hostname:port", e.g., - "localhost:8765". - - When using an IPv6 address, enclose it within square brackets. - E.g., "[2001:db8::1]:8765". + {address} is a String, see |channel-address| for the possible + accepted forms. If {options} is given it must be a |Dictionary|. See |channel-open-options|. diff --git a/src/channel.c b/src/channel.c --- a/src/channel.c +++ b/src/channel.c @@ -44,11 +44,18 @@ # define sock_write(sd, buf, len) send((SOCKET)sd, buf, len, 0) # define sock_read(sd, buf, len) recv((SOCKET)sd, buf, len, 0) # define sock_close(sd) closesocket((SOCKET)sd) +// Support for Unix-domain sockets was added in Windows SDK 17061. +# define UNIX_PATH_MAX 108 +typedef struct sockaddr_un { + ADDRESS_FAMILY sun_family; + char sun_path[UNIX_PATH_MAX]; +} SOCKADDR_UN, *PSOCKADDR_UN; #else # include # include # include # include +# include # ifdef HAVE_LIBGEN_H # include # endif @@ -929,6 +936,67 @@ channel_connect( } /* + * Open a socket channel to the UNIX socket at "path". + * Returns the channel for success. + * Returns NULL for failure. + */ + static channel_T * +channel_open_unix( + const char *path, + void (*nb_close_cb)(void)) +{ + channel_T *channel = NULL; + int sd = -1; + size_t path_len = STRLEN(path); + struct sockaddr_un server; + size_t server_len; + int waittime = -1; + + if (*path == NUL || path_len >= sizeof(server.sun_path)) + { + semsg(_(e_invalid_argument_str), path); + return NULL; + } + + channel = add_channel(); + if (channel == NULL) + { + ch_error(NULL, "Cannot allocate channel."); + return NULL; + } + + CLEAR_FIELD(server); + server.sun_family = AF_UNIX; + STRNCPY(server.sun_path, path, sizeof(server.sun_path) - 1); + + ch_log(channel, "Trying to connect to %s", path); + + server_len = offsetof(struct sockaddr_un, sun_path) + path_len + 1; + sd = channel_connect(channel, (struct sockaddr *)&server, (int)server_len, + &waittime); + + if (sd < 0) + { + channel_free(channel); + return NULL; + } + + ch_log(channel, "Connection made"); + + channel->CH_SOCK_FD = (sock_T)sd; + channel->ch_nb_close_cb = nb_close_cb; + channel->ch_hostname = (char *)vim_strsave((char_u *)path); + channel->ch_port = 0; + channel->ch_to_be_closed |= (1U << PART_SOCK); + +#ifdef FEAT_GUI + channel_gui_register_one(channel, PART_SOCK); +#endif + + return channel; +} + +/* * Open a socket channel to "hostname":"port". * "waittime" is the time in msec to wait for the connection. * When negative wait forever. @@ -1301,8 +1369,9 @@ channel_open_func(typval_T *argvars) char_u *address; char_u *p; char *rest; - int port; + int port = 0; int is_ipv6 = FALSE; + int is_unix = FALSE; jobopt_T opt; channel_T *channel = NULL; @@ -1319,8 +1388,18 @@ channel_open_func(typval_T *argvars) return NULL; } - // parse address - if (*address == '[') + if (*address == NUL) + { + semsg(_(e_invalid_argument_str), address); + return NULL; + } + + if (!STRNCMP(address, "unix:", 5)) + { + is_unix = TRUE; + address += 5; + } + else if (*address == '[') { // ipv6 address is_ipv6 = TRUE; @@ -1333,6 +1412,7 @@ channel_open_func(typval_T *argvars) } else { + // ipv4 address p = vim_strchr(address, ':'); if (p == NULL) { @@ -1340,27 +1420,32 @@ channel_open_func(typval_T *argvars) return NULL; } } - port = strtol((char *)(p + 1), &rest, 10); - if (*address == NUL || port <= 0 || port >= 65536 || *rest != NUL) + + if (!is_unix) { - semsg(_(e_invalid_argument_str), address); - return NULL; + port = strtol((char *)(p + 1), &rest, 10); + if (port <= 0 || port >= 65536 || *rest != NUL) + { + semsg(_(e_invalid_argument_str), address); + return NULL; + } + if (is_ipv6) + { + // strip '[' and ']' + ++address; + *(p - 1) = NUL; + } + else + *p = NUL; } - if (is_ipv6) - { - // strip '[' and ']' - ++address; - *(p - 1) = NUL; - } - else - *p = NUL; // parse options clear_job_options(&opt); opt.jo_mode = MODE_JSON; opt.jo_timeout = 2000; if (get_job_options(&argvars[1], &opt, - JO_MODE_ALL + JO_CB_ALL + JO_WAITTIME + JO_TIMEOUT_ALL, 0) == FAIL) + JO_MODE_ALL + JO_CB_ALL + JO_TIMEOUT_ALL + + (is_unix? 0 : JO_WAITTIME), 0) == FAIL) goto theend; if (opt.jo_timeout < 0) { @@ -1368,7 +1453,10 @@ channel_open_func(typval_T *argvars) goto theend; } - channel = channel_open((char *)address, port, opt.jo_waittime, NULL); + if (is_unix) + channel = channel_open_unix((char *)address, NULL); + else + channel = channel_open((char *)address, port, opt.jo_waittime, NULL); if (channel != NULL) { opt.jo_set = JO_ALL; @@ -3268,8 +3356,14 @@ channel_info(channel_T *channel, dict_T if (channel->ch_hostname != NULL) { - dict_add_string(dict, "hostname", (char_u *)channel->ch_hostname); - dict_add_number(dict, "port", channel->ch_port); + if (channel->ch_port) + { + dict_add_string(dict, "hostname", (char_u *)channel->ch_hostname); + dict_add_number(dict, "port", channel->ch_port); + } + else + // Unix-domain socket. + dict_add_string(dict, "path", (char_u *)channel->ch_hostname); channel_part_info(channel, dict, "sock", PART_SOCK); } else diff --git a/src/testdir/check.vim b/src/testdir/check.vim --- a/src/testdir/check.vim +++ b/src/testdir/check.vim @@ -95,7 +95,7 @@ func CheckUnix() endif endfunc -" Command to check for running on Linix +" Command to check for running on Linux command CheckLinux call CheckLinux() func CheckLinux() if !has('linux') diff --git a/src/testdir/shared.vim b/src/testdir/shared.vim --- a/src/testdir/shared.vim +++ b/src/testdir/shared.vim @@ -15,10 +15,16 @@ func PythonProg() if has('unix') " We also need the job feature or the pkill command to make sure the server " can be stopped. - if !(executable('python') && (has('job') || executable('pkill'))) + if !(has('job') || executable('pkill')) return '' endif - let s:python = 'python' + if executable('python') + let s:python = 'python' + elseif executable('python3') + let s:python = 'python3' + else + return '' + end elseif has('win32') " Use Python Launcher for Windows (py.exe) if available. " NOTE: if you get a "Python was not found" error, disable the Python diff --git a/src/testdir/test_channel.py b/src/testdir/test_channel.py --- a/src/testdir/test_channel.py +++ b/src/testdir/test_channel.py @@ -22,7 +22,8 @@ except ImportError: class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def setup(self): - self.request.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + if self.server.address_family != socket.AF_UNIX: + self.request.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) def handle(self): print("=== socket opened ===") diff --git a/src/testdir/test_channel.vim b/src/testdir/test_channel.vim --- a/src/testdir/test_channel.vim +++ b/src/testdir/test_channel.vim @@ -23,6 +23,9 @@ func SetUp() if g:testfunc =~ '_ipv6()$' let s:localhost = '[::1]:' let s:testscript = 'test_channel_6.py' + elseif g:testfunc =~ '_unix()$' + let s:localhost = 'unix:Xtestsocket' + let s:testscript = 'test_channel_unix.py' else let s:localhost = 'localhost:' let s:testscript = 'test_channel.py' @@ -39,6 +42,15 @@ func s:run_server(testfunc, ...) call RunServer(s:testscript, a:testfunc, a:000) endfunc +" Returns the address of the test server. +func s:address(port) + if s:localhost =~ '^unix:' + return s:localhost + else + return s:localhost . a:port + end +endfunc + " Return a list of open files. " Can be used to make sure no resources leaked. " Returns an empty list on systems where this is not supported. @@ -65,7 +77,7 @@ func Ch_communicate(port) let s:chopt.drop = 'never' " Also add the noblock flag to try it out. let s:chopt.noblock = 1 - let handle = ch_open(s:localhost . a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return @@ -77,7 +89,10 @@ func Ch_communicate(port) let dict = handle->ch_info() call assert_true(dict.id != 0) call assert_equal('open', dict.status) - call assert_equal(a:port, string(dict.port)) + if has_key(dict, 'port') + " Channels using Unix sockets have no 'port' entry. + call assert_equal(a:port, string(dict.port)) + end call assert_equal('open', dict.sock_status) call assert_equal('socket', dict.sock_io) @@ -252,13 +267,19 @@ endfunc func Test_communicate_ipv6() CheckIPv6 - call Test_communicate() endfunc +func Test_communicate_unix() + CheckUnix + call Test_communicate() + call delete('Xtestsocket') +endfunc + + " Test that we can open two channels. func Ch_two_channels(port) - let handle = ch_open(s:localhost . a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) call assert_equal(v:t_channel, type(handle)) if handle->ch_status() == "fail" call assert_report("Can't open channel") @@ -267,7 +288,7 @@ func Ch_two_channels(port) call assert_equal('got it', ch_evalexpr(handle, 'hello!')) - let newhandle = ch_open(s:localhost . a:port, s:chopt) + let newhandle = ch_open(s:address(a:port), s:chopt) if ch_status(newhandle) == "fail" call assert_report("Can't open second channel") return @@ -292,9 +313,15 @@ func Test_two_channels_ipv6() call Test_two_channels() endfunc +func Test_two_channels_unix() + CheckUnix + call Test_two_channels() + call delete('Xtestsocket') +endfunc + " Test that a server crash is handled gracefully. func Ch_server_crash(port) - let handle = ch_open(s:localhost . a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return @@ -314,6 +341,12 @@ func Test_server_crash_ipv6() call Test_server_crash() endfunc +func Test_server_crash_unix() + CheckUnix + call Test_server_crash() + call delete('Xtestsocket') +endfunc + """"""""" func Ch_handler(chan, msg) @@ -323,7 +356,7 @@ func Ch_handler(chan, msg) endfunc func Ch_channel_handler(port) - let handle = ch_open(s:localhost . a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return @@ -352,6 +385,12 @@ func Test_channel_handler_ipv6() call Test_channel_handler() endfunc +func Test_channel_handler_unix() + CheckUnix + call Test_channel_handler() + call delete('Xtestsocket') +endfunc + """"""""" let g:Ch_reply = '' @@ -367,7 +406,7 @@ func Ch_oneHandler(chan, msg) endfunc func Ch_channel_zero(port) - let handle = (s:localhost .. a:port)->ch_open(s:chopt) + let handle = (s:address(a:port))->ch_open(s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return @@ -415,6 +454,13 @@ func Test_zero_reply_ipv6() call Test_zero_reply() endfunc +func Test_zero_reply_unix() + CheckUnix + call Test_zero_reply() + call delete('Xtestsocket') +endfunc + + """"""""" let g:Ch_reply1 = "" @@ -436,7 +482,7 @@ func Ch_handleRaw3(chan, msg) endfunc func Ch_raw_one_time_callback(port) - let handle = ch_open(s:localhost . a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return @@ -462,6 +508,12 @@ func Test_raw_one_time_callback_ipv6() call Test_raw_one_time_callback() endfunc +func Test_raw_one_time_callback_unix() + CheckUnix + call Test_raw_one_time_callback() + call delete('Xtestsocket') +endfunc + """"""""" " Test that trying to connect to a non-existing port fails quickly. @@ -1398,7 +1450,7 @@ endfunc " Test that "unlet handle" in a handler doesn't crash Vim. func Ch_unlet_handle(port) - let s:channelfd = ch_open(s:localhost . a:port, s:chopt) + let s:channelfd = ch_open(s:address(a:port), s:chopt) eval s:channelfd->ch_sendexpr("test", {'callback': function('s:UnletHandler')}) call WaitForAssert({-> assert_equal('what?', g:Ch_unletResponse)}) endfunc @@ -1422,7 +1474,7 @@ endfunc " Test that "unlet handle" in a handler doesn't crash Vim. func Ch_close_handle(port) - let s:channelfd = ch_open(s:localhost . a:port, s:chopt) + let s:channelfd = ch_open(s:address(a:port), s:chopt) call ch_sendexpr(s:channelfd, "test", {'callback': function('Ch_CloseHandler')}) call WaitForAssert({-> assert_equal('what?', g:Ch_unletResponse)}) endfunc @@ -1439,7 +1491,7 @@ endfunc """""""""" func Ch_open_ipv6(port) - let handle = ch_open('[::1]:' .. a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) call assert_notequal('fail', ch_status(handle)) endfunc @@ -1479,7 +1531,7 @@ endfunc func Ch_open_delay(port) " Wait up to a second for the port to open. let s:chopt.waittime = 1000 - let channel = ch_open(s:localhost . a:port, s:chopt) + let channel = ch_open(s:address(a:port), s:chopt) if ch_status(channel) == "fail" call assert_report("Can't open channel") return @@ -1505,7 +1557,7 @@ function MyFunction(a,b,c) endfunc function Ch_test_call(port) - let handle = ch_open(s:localhost . a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return @@ -1529,6 +1581,12 @@ func Test_call_ipv6() call Test_call() endfunc +func Test_call_unix() + CheckUnix + call Test_call() + call delete('Xtestsocket') +endfunc + """"""""" let g:Ch_job_exit_ret = 'not yet' @@ -1605,7 +1663,7 @@ function MyCloseCb(ch) endfunc function Ch_test_close_callback(port) - let handle = ch_open(s:localhost . a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return @@ -1625,8 +1683,14 @@ func Test_close_callback_ipv6() call Test_close_callback() endfunc +func Test_close_callback_unix() + CheckUnix + call Test_close_callback() + call delete('Xtestsocket') +endfunc + function Ch_test_close_partial(port) - let handle = ch_open(s:localhost . a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return @@ -1651,6 +1715,12 @@ func Test_close_partial_ipv6() call Test_close_partial() endfunc +func Test_close_partial_unix() + CheckUnix + call Test_close_partial() + call delete('Xtestsocket') +endfunc + func Test_job_start_fails() " this was leaking memory call assert_fails("call job_start([''])", "E474:") @@ -1920,7 +1990,7 @@ func Test_cwd() endfunc function Ch_test_close_lambda(port) - let handle = ch_open(s:localhost . a:port, s:chopt) + let handle = ch_open(s:address(a:port), s:chopt) if ch_status(handle) == "fail" call assert_report("Can't open channel") return @@ -1942,6 +2012,12 @@ func Test_close_lambda_ipv6() call Test_close_lambda() endfunc +func Test_close_lambda_unix() + CheckUnix + call Test_close_lambda() + call delete('Xtestsocket') +endfunc + func s:test_list_args(cmd, out, remove_lf) try let g:out = '' @@ -2243,6 +2319,8 @@ func Test_job_trailing_space_unix() let job = job_start("cat ", #{in_io: 'null'}) call WaitForAssert({-> assert_equal("dead", job_status(job))}) call assert_equal(0, job_info(job).exitval) + + call delete('Xtestsocket') endfunc func Test_ch_getbufnr() diff --git a/src/testdir/test_channel_unix.py b/src/testdir/test_channel_unix.py new file mode 100644 --- /dev/null +++ b/src/testdir/test_channel_unix.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# +# Server that will accept connections from a Vim channel. +# Used by test_channel.vim. +# +# This requires Python 2.6 or later. + +from __future__ import print_function +from test_channel import ThreadedTCPServer, ThreadedTCPRequestHandler, \ + writePortInFile +import socket +import threading +import os + +try: + FileNotFoundError +except NameError: + # Python 2 + FileNotFoundError = (IOError, OSError) + +class ThreadedUnixServer(ThreadedTCPServer): + address_family = socket.AF_UNIX + +def main(path): + server = ThreadedUnixServer(path, ThreadedTCPRequestHandler) + + # Start a thread with the server. That thread will then start a new thread + # for each connection. + server_thread = threading.Thread(target=server.serve_forever) + server_thread.start() + + # Signal the test harness we're ready, the port value has no meaning. + writePortInFile(1234) + + print("Listening on {0}".format(server.server_address)) + + # Main thread terminates, but the server continues running + # until server.shutdown() is called. + try: + while server_thread.is_alive(): + server_thread.join(1) + except (KeyboardInterrupt, SystemExit): + server.shutdown() + +if __name__ == "__main__": + try: + os.remove("Xtestsocket") + except FileNotFoundError: + pass + main("Xtestsocket") diff --git a/src/testdir/test_cmdline.vim b/src/testdir/test_cmdline.vim --- a/src/testdir/test_cmdline.vim +++ b/src/testdir/test_cmdline.vim @@ -620,8 +620,8 @@ func Test_fullcommand() \ ':5s': 'substitute', \ "'<,'>s": 'substitute', \ ":'<,'>s": 'substitute', - \ 'CheckUni': 'CheckUnix', - \ 'CheckUnix': 'CheckUnix', + \ 'CheckLin': 'CheckLinux', + \ 'CheckLinux': 'CheckLinux', \ } for [in, want] in items(tests) diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -751,6 +751,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 4684, +/**/ 4683, /**/ 4682,