]> granicus.if.org Git - vim/commitdiff
patch 8.2.4684: cannot open a channel on a Unix domain socket v8.2.4684
authorLemonBoy <thatlemon@gmail.com>
Mon, 4 Apr 2022 14:46:58 +0000 (15:46 +0100)
committerBram Moolenaar <Bram@vim.org>
Mon, 4 Apr 2022 14:46:58 +0000 (15:46 +0100)
Problem:    Cannot open a channel on a Unix domain socket.
Solution:   Add Unix domain socket support. (closes #10062)

runtime/doc/channel.txt
src/channel.c
src/testdir/check.vim
src/testdir/shared.vim
src/testdir/test_channel.py
src/testdir/test_channel.vim
src/testdir/test_channel_unix.py [new file with mode: 0644]
src/testdir/test_cmdline.vim
src/version.c

index f112119dc63f0169ce606c20c3a8dd195c3f0f43..47cda3ff1011ff4db157eb46e91c004a6c7ffe9a 100644 (file)
@@ -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()*
                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|.
index 66ba55df65aab9f1a9098653f03d45b5a84c2c3c..75fc89560ccc136d0161d08ceac89373c403c961 100644 (file)
 # 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 <netdb.h>
 # include <netinet/in.h>
 # include <arpa/inet.h>
 # include <sys/socket.h>
+# include <sys/un.h>
 # ifdef HAVE_LIBGEN_H
 #  include <libgen.h>
 # endif
@@ -928,6 +935,67 @@ channel_connect(
     return sd;
 }
 
+/*
+ * 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.
@@ -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)
-    {
-       semsg(_(e_invalid_argument_str), address);
-       return NULL;
-    }
-    if (is_ipv6)
+
+    if (!is_unix)
     {
-       // strip '[' and ']'
-       ++address;
-       *(p - 1) = NUL;
+       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;
     }
-    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 *dict)
 
     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
index 55a64bd7abc30dd0cb45b21cf35aa5ce5fa6bba9..aff0918454a74542018e0f6319af83693cca52e3 100644 (file)
@@ -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')
index 01d741c08e29424f4e4412d0a5ba927e31e930a7..8e3dfc9c3a1d623c29b340b4600af6f22ac9ec93 100644 (file)
@@ -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
index 36aad2b778a0aa78c130382cdff6d8afdafce222..b0c3140927efed8b85d13139c71c9b0f0be030a6 100644 (file)
@@ -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 ===")
index 122bc570aadfbfd6697fb43d50586ea89fade97a..6ad9dc0e162b26f6d5bd2e1128b9c011e7220ce7 100644 (file)
@@ -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 (file)
index 0000000..4836e26
--- /dev/null
@@ -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")
index e38a56963db7526f4074c831ca35db5c038d54f9..8034bdb3e0a03c9df8340b87e80aee2356d2c1ef 100644 (file)
@@ -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)
index bf104110aaa05a72565580fa39479ad6b031c52e..a7df559c58c1adb337fd93ba43fdbcf0216789cb 100644 (file)
@@ -750,6 +750,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    4684,
 /**/
     4683,
 /**/