From 96b2154b73f1c0e607c237bd366bdb7f50320b83 Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Mon, 18 Aug 2025 21:40:40 +0200 Subject: [PATCH] patch 9.1.1651: Cannot use clientserver over socket Problem: Cannot use clientserver over Unix domain socket Solution: Implement socketserver functionality (Foxe Chen). fixes: #3509 closes: #17839 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- .github/workflows/ci.yml | 17 +- runtime/doc/builtin.txt | 1 + runtime/doc/remote.txt | 79 +- runtime/doc/tags | 11 + runtime/doc/various.txt | 4 +- runtime/doc/version9.txt | 5 + runtime/doc/vim.1 | 9 +- runtime/doc/vim.man | 11 +- runtime/plugin/rrhelper.vim | 3 +- runtime/syntax/vim.vim | 2 +- src/auto/configure | 36 + src/clientserver.c | 308 +++++- src/config.h.in | 6 + src/configure.ac | 18 + src/errors.h | 12 + src/evalfunc.c | 7 + src/ex_docmd.c | 20 + src/feature.h | 11 +- src/globals.h | 34 +- src/gui.c | 6 + src/gui_gtk_x11.c | 56 +- src/if_xcmdsrv.c | 2 +- src/main.c | 28 +- src/os_unix.c | 1717 ++++++++++++++++++++++++++++- src/po/vim.pot | 28 +- src/proto/gui_gtk_x11.pro | 2 + src/proto/option.pro | 1 + src/proto/optionstr.pro | 2 + src/proto/os_unix.pro | 11 + src/testdir/test_clientserver.vim | 137 ++- src/testdir/test_vim9_builtin.vim | 5 + src/testdir/test_wayland.vim | 20 + src/ui.c | 14 +- src/version.c | 7 + 34 files changed, 2562 insertions(+), 68 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af9e575fb8..c6f32b0804 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,12 @@ jobs: - features: normal compiler: gcc extra: [vimtags] + - features: huge + compiler: gcc + extra: [no_x11] + - features: huge + compiler: gcc + extra: [socketserver] steps: - name: Checkout repository from github @@ -220,7 +226,7 @@ jobs: tiny) echo "TEST=testtiny" if ${{ contains(matrix.extra, 'nogui') }}; then - echo "CONFOPT=--disable-gui" + CONFOPT="--disable-gui" fi ;; normal) @@ -232,10 +238,16 @@ jobs: PYTHON3_CONFOPT="--with-python3-stable-abi=3.8" fi # The ubuntu-24.04 CI runner does not provide a python2 package. - echo "CONFOPT=--enable-perlinterp=${INTERFACE} --enable-pythoninterp=no --enable-python3interp=${INTERFACE} --enable-rubyinterp=${INTERFACE} --enable-luainterp=${INTERFACE} --enable-tclinterp=${INTERFACE} ${PYTHON3_CONFOPT}" + CONFOPT="--enable-perlinterp=${INTERFACE} --enable-pythoninterp=no --enable-python3interp=${INTERFACE} --enable-rubyinterp=${INTERFACE} --enable-luainterp=${INTERFACE} --enable-tclinterp=${INTERFACE} ${PYTHON3_CONFOPT}" ;; esac + if ${{ contains(matrix.extra, 'no_x11') }}; then + CONFOPT="${CONFOPT} --without-x --disable-gui" + fi + if ${{ contains(matrix.extra, 'socketserver') }}; then + CONFOPT="${CONFOPT} --enable-socketserver" + fi if ${{ matrix.coverage == true }}; then CFLAGS="${CFLAGS} --coverage -DUSE_GCOV_FLUSH" echo "LDFLAGS=--coverage" @@ -259,6 +271,7 @@ jobs: echo "TEST=-C runtime/doc vimtags VIMEXE=../../${SRCDIR}/vim" fi echo "CFLAGS=${CFLAGS}" + echo "CONFOPT=${CONFOPT}" # Disables GTK attempt to integrate with the accessibility service that does run in CI. echo "NO_AT_BRIDGE=1" ) >> $GITHUB_ENV diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index d82b4e69b2..44818dd59f 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -13046,6 +13046,7 @@ scrollbind Compiled with 'scrollbind' support. (always true) showcmd Compiled with 'showcmd' support. signs Compiled with |:sign| support. smartindent Compiled with 'smartindent' support. (always true) +socketserver Compiled with socket server functionality. (Unix only) sodium Compiled with libsodium for better crypt support sound Compiled with sound support, e.g. `sound_playevent()` spell Compiled with spell checking support |spell|. diff --git a/runtime/doc/remote.txt b/runtime/doc/remote.txt index 5a6898c251..cf17e35498 100644 --- a/runtime/doc/remote.txt +++ b/runtime/doc/remote.txt @@ -1,4 +1,4 @@ -*remote.txt* For Vim version 9.1. Last change: 2022 Feb 17 +*remote.txt* For Vim version 9.1. Last change: 2025 Aug 18 VIM REFERENCE MANUAL by Bram Moolenaar @@ -61,7 +61,10 @@ The following command line arguments are available: --servername {name} Become the server {name}. When used together with one of the --remote commands: connect to server {name} instead of the default (see - below). The name used will be uppercase. + below). The name used will be uppercase. If + using the socketserver, you can specify a + path, see |socketserver-name| for more + details. *--remote-send* --remote-send {keys} Send {keys} to server and exit. The {keys} are not mapped. Special key names are @@ -72,6 +75,12 @@ The following command line arguments are available: on stdout. *--serverlist* --serverlist Output a list of server names. + *--clientserver* + --clientserver {method} Use the specified method {method} as the + backend for clientserver functionality. Can + either be "socket" or "x11". + {only available when Vim is compiled with both + |+X11| and |+socketserver| features} Examples ~ @@ -105,7 +114,8 @@ specified name is not available, a postfix is applied until a free name is encountered, i.e. "gvim1" for the second invocation of gvim on a particular X-server. The resulting name is available in the servername builtin variable |v:servername|. The case of the server name is ignored, thus "gvim" and -"GVIM" are considered equal. +"GVIM" are considered equal. Note if a socket server is being used, there are +some differences, see |socketserver-differences|. When Vim is invoked with --remote, --remote-wait or --remote-send it will try to locate the server name determined by the invocation name and --servername @@ -119,7 +129,8 @@ itself. This way it is not necessary to know whether gvim is already started when sending command to it. The --serverlist argument will cause Vim to print a list of registered command -servers on the standard output (stdout) and exit. +servers on the standard output (stdout) and exit. If a socket server is being +used, there are caveats, see |socketserver-differences|. *{server}* The {server} argument is used by several functions. When this is an empty string then on Unix the default server name is used, which is "GVIM". On @@ -206,4 +217,64 @@ When using gvim, the --remote-wait only works properly this way: > start /w gvim --remote-wait file.txt < +============================================================================== +3. Socket server specific items *socketserver-clientserver* + *E1563* *E1564* *E1565* *E1566* *E1567* + +The communication between client and server is done using Unix domain sockets. +These sockets are either placed in these directories in the following order of +availability: + 1. "$XDG_RUTIME_DIR/vim" if $XDG_RUNTIME_DIR is set in the environment. + 2. "$TMPDIR/vim-[uid]", where "[uid]" is the uid of the user. This + directory will have the access permissions set to 700 so only the user + can read or write from/to it. If $TMPDIR is not set, "/tmp" is used. + + *socketserver-name* +When specifying the server id/name, it can be taken as a generic name or an +absolute or relative path. If the server id starts with either a "/" +(absolute) or "./" | "../" (relative), then it is taken as path to the socket. +Otherwise the server id will be the filename of the socket which will be +placed in the above common directories. Note that a server id/name can only +contain slashes "/" if it is taken as a path, so names such as "abc/dir" will +be invalid. + +Socket server functionality is available in both GTK GUI and terminal versions of +Vim. Unless Vim is compiled with |+autoservername| feature, the socket server +will have to started explicitly, just like X11, even in the GUI. + +If Vim crashes or does not exit cleanly, the socket server will not remove the +socket file and it will be left around. This is generally not a problem, +because if a socket name is taken, Vim checks if the socket in its place is +dead (not attached to any process), and can replace it instead of finding a +new name. + +To send commands to a Vim socket server from another application, read the +source file src/os_unix.c, there is detailed description of the protocol used. + + *socketserver-differences* +Most of the functionality is the same as X11, however unlike X11, where the +client does not need to be a server in order to communicate with another +server, the socket server requires the server to be running even as a client. +The exception is |serverlist()| or the |--serverlist| argument, which does not +require the server to be running. + +Additionally, the server id or client id will not be a number like X11 or +MS-Windows (shown in hex representation), instead it is the absolute path to +the socket. This can be seen via the |v:servername| variable. + +The |--serverlist| argument will act just like X11, however it only checks the +given common directories above. If a custom path is used for a socket, it +will not be detected, such as a path either not in $XDG_RUNTIME_DIR or +<$TMPDIR or /tmp>/vim of the |--serverlist| Vim process. + +If you have both |+socketserver| and |+X11| compiled, you will need to add +|--clientserver| set to "socket" in combination with |--serverlist| to list +the available servers. You cannot list both types of backends in one command. + + *socketserver-x11* +If Vim is compiled with both |+X11| and |+socketserver|, then deciding which +backend to use is done at startup time, via the |--clientserver| argument. By +default if it is not specified, then X11 will be used. A Vim instance using a +socket server cannot communicate with one using X11. + vim:tw=78:sw=4:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/tags b/runtime/doc/tags index 63c1506f15..fa3382f288 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -1497,6 +1497,7 @@ $quote eval.txt /*$quote* +scrollbind various.txt /*+scrollbind* +signs various.txt /*+signs* +smartindent various.txt /*+smartindent* ++socketserver various.txt /*+socketserver* +sodium various.txt /*+sodium* +sound various.txt /*+sound* +spell various.txt /*+spell* @@ -1557,6 +1558,7 @@ $quote eval.txt /*$quote* -- starting.txt /*--* --- starting.txt /*---* --clean starting.txt /*--clean* +--clientserver remote.txt /*--clientserver* --cmd starting.txt /*--cmd* --echo-wid starting.txt /*--echo-wid* --gui-dialog-file starting.txt /*--gui-dialog-file* @@ -4697,6 +4699,11 @@ E156 sign.txt /*E156* E1560 vim9.txt /*E1560* E1561 vim9.txt /*E1561* E1562 options.txt /*E1562* +E1563 remote.txt /*E1563* +E1564 remote.txt /*E1564* +E1565 remote.txt /*E1565* +E1566 remote.txt /*E1566* +E1567 remote.txt /*E1567* E157 sign.txt /*E157* E158 sign.txt /*E158* E159 sign.txt /*E159* @@ -10225,6 +10232,10 @@ slow-fast-terminal term.txt /*slow-fast-terminal* slow-start starting.txt /*slow-start* slow-terminal term.txt /*slow-terminal* socket-interface channel.txt /*socket-interface* +socketserver-clientserver remote.txt /*socketserver-clientserver* +socketserver-differences remote.txt /*socketserver-differences* +socketserver-name remote.txt /*socketserver-name* +socketserver-x11 remote.txt /*socketserver-x11* sort() builtin.txt /*sort()* sorting change.txt /*sorting* sound-functions usr_41.txt /*sound-functions* diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt index 7ace61f1e1..9d631ad92f 100644 --- a/runtime/doc/various.txt +++ b/runtime/doc/various.txt @@ -1,4 +1,4 @@ -*various.txt* For Vim version 9.1. Last change: 2025 Aug 06 +*various.txt* For Vim version 9.1. Last change: 2025 Aug 18 VIM REFERENCE MANUAL by Bram Moolenaar @@ -487,6 +487,8 @@ m *+ruby/dyn* Ruby interface |ruby-dynamic| |/dyn| T *+scrollbind* 'scrollbind' N *+signs* |:sign| T *+smartindent* 'smartindent' +N *+socketserver* Unix only: socket server backend for clientserver + functionality H *+sodium* compiled with libsodium for better encryption support H *+sound* |sound_playevent()|, |sound_playfile()| functions, etc. N *+spell* spell checking support, see |spell| diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index 6dcce6bec9..d69afab2ef 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -41745,6 +41745,10 @@ Others: ~ Unicode 16. - Two additional digraphs have been added: LEFT ANGLE BRACKET "<[" and RIGHT ANGLE BRACKET "]>". +- Support for Unix domain sockets have been added for the clientserver + feature, see |socketserver-clientserver|. + +Platform specific ~ - MS-Winodws: Paths like "\Windows" and "/Windows" are now considered to be absolute paths (to the current drive) and no longer relative. @@ -41849,6 +41853,7 @@ Options: ~ Vim Arguments: ~ |-Y| Do not connect to the Wayland compositor. +|--clientserver| Specify backend for clientserver functionality. ============================================================================== diff --git a/runtime/doc/vim.1 b/runtime/doc/vim.1 index 1371cb1fe0..df39fb2b24 100644 --- a/runtime/doc/vim.1 +++ b/runtime/doc/vim.1 @@ -499,7 +499,14 @@ List the names of all Vim servers that can be found. .TP \-\-servername {name} Use {name} as the server name. Used for the current Vim, unless used with a -\-\-remote argument, then it's the name of the server to connect to. +\-\-remote argument, then it's the name of the server to connect to. If the +socketserver backend is being used, if the name starts with "/", "./", or "../", +it is taken as either an absolute, relative or relative path to the socket. +.TP +\-\-clientserver {backend} +Use {backend} as the backend for clientserver functionality, either "socket" or +"x11" respectively. Only available when compiled with both socketserver and X11 +features present .TP \-\-socketid {id} GTK GUI only: Use the GtkPlug mechanism to run gVim in another window. diff --git a/runtime/doc/vim.man b/runtime/doc/vim.man index 6d9cfe626c..c1484bebb0 100644 --- a/runtime/doc/vim.man +++ b/runtime/doc/vim.man @@ -378,7 +378,16 @@ OPTIONS --servername {name} Use {name} as the server name. Used for the current Vim, unless used with a --remote argument, then it's the name of - the server to connect to. + the server to connect to. If the socketserver backend is + being used, if the name starts with "/", "./", or "../", it + is taken as either an absolute, relative or relative path + to the socket. + + --clientserver {backend} + Use {backend} as the backend for clientserver functional‐ + ity, either "socket" or "x11" respectively. Only available + when compiled with both socketserver and X11 features + present --socketid {id} GTK GUI only: Use the GtkPlug mechanism to run gVim in an‐ diff --git a/runtime/plugin/rrhelper.vim b/runtime/plugin/rrhelper.vim index b09cbc10b9..5e23eef1b1 100644 --- a/runtime/plugin/rrhelper.vim +++ b/runtime/plugin/rrhelper.vim @@ -16,9 +16,10 @@ function SetupRemoteReplies() let max = argc() let id = expand("") - if id == 0 + if (type(id) == v:t_number && id == 0) || (type(id) == v:t_string && id == '') return endif + while cnt < max " Handle same file from more clients and file being more than once " on the command line by encoding this stuff in the group name diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim index cf7f1a43a7..eab86d27e6 100644 --- a/runtime/syntax/vim.vim +++ b/runtime/syntax/vim.vim @@ -2,7 +2,7 @@ " Language: Vim script " Maintainer: Hirohito Higashi " Doug Kearns -" Last Change: 2025 Aug 16 +" Last Change: 2025 Aug 18 " Former Maintainer: Charles E. Campbell " DO NOT CHANGE DIRECTLY. diff --git a/src/auto/configure b/src/auto/configure index 06c3ca3a0c..44ce307048 100755 --- a/src/auto/configure +++ b/src/auto/configure @@ -854,6 +854,7 @@ enable_netbeans enable_channel enable_terminal enable_autoservername +enable_socketserver enable_multibyte enable_rightleft enable_arabic @@ -1534,6 +1535,7 @@ Optional Features: --disable-channel Disable process communication support. --enable-terminal Enable terminal emulation support. --enable-autoservername Automatically define servername at vim startup. + --enable-socketserver Use sockets for clientserver communication. --enable-multibyte Include multibyte editing support. --disable-rightleft Do not include Right-to-Left language support. --disable-arabic Do not include Arabic language support. @@ -9084,6 +9086,40 @@ if test "$enable_autoservername" = "yes"; then fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking --enable-socketserver argument" >&5 +printf %s "checking --enable-socketserver argument... " >&6; } +# Check whether --enable-socketserver was given. +if test ${enable_socketserver+y} +then : + enableval=$enable_socketserver; enable_socketserver=$enableval +else case e in #( + e) if test "x$features" = xtiny +then : + enable_socketserver=no_auto + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: cannot use socketserver with tiny features" >&5 +printf "%s\n" "cannot use socketserver with tiny features" >&6; } +else case e in #( + e) enable_socketserver=auto ;; +esac +fi ;; +esac +fi + +if test "$enable_socketserver" = "yes"; then + printf "%s\n" "#define WANT_SOCKETSERVER 1" >>confdefs.h + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } +elif test "$enable_socketserver" = "auto"; then + printf "%s\n" "#define MAYBE_SOCKETSERVER 1" >>confdefs.h + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: auto" >&5 +printf "%s\n" "auto" >&6; } +elif test "$enable_socketserver" = "no"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } +fi + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking --enable-multibyte argument" >&5 printf %s "checking --enable-multibyte argument... " >&6; } # Check whether --enable-multibyte was given. diff --git a/src/clientserver.c b/src/clientserver.c index b19cd447fa..bc3fe2d3db 100644 --- a/src/clientserver.c +++ b/src/clientserver.c @@ -15,6 +15,11 @@ #if defined(FEAT_CLIENTSERVER) || defined(PROTO) +#ifdef FEAT_SOCKETSERVER +# include +# include "sys/un.h" +#endif + static void cmdsrv_main(int *argc, char **argv, char_u *serverName_arg, char_u **serverStr); static char_u *serverMakeName(char_u *arg, char *cmd); @@ -191,6 +196,8 @@ static char_u *build_drop_cmd(int filec, char **filev, int tabs, int sendReply); void exec_on_server(mparm_T *parmp) { + int made_name = FALSE; + if (parmp->serverName_arg != NULL && *parmp->serverName_arg == NUL) return; @@ -199,6 +206,23 @@ exec_on_server(mparm_T *parmp) serverInitMessaging(); # endif +#ifdef FEAT_SOCKETSERVER + // If servername is specified and we are using sockets, always init the + // sockt server. We may need to receive replies back to us. If --serverlist + // is passed, the socket server will be uninitialized before listing + // sockets then initialized after. This is so we don't add our own socket + // in the list. This does not happen in serverlist(). + if ((parmp->serverArg || parmp->serverName_arg != NULL) && + clientserver_method == CLIENTSERVER_METHOD_SOCKET) + { + parmp->servername = serverMakeName(parmp->serverName_arg, + parmp->argv[0]); + if (socket_server_init(parmp->servername) == OK) + TIME_MSG("initialize socket server"); + made_name = TRUE; + } +#endif + /* * When a command server argument was found, execute it. This may * exit Vim when it was successful. Otherwise it's executed further @@ -214,8 +238,9 @@ exec_on_server(mparm_T *parmp) // If we're still running, get the name to register ourselves. // On Win32 can register right now, for X11 need to setup the // clipboard first, it's further down. - parmp->servername = serverMakeName(parmp->serverName_arg, - parmp->argv[0]); + if (!made_name && parmp->servername == NULL) + parmp->servername = serverMakeName(parmp->serverName_arg, + parmp->argv[0]); # ifdef MSWIN if (parmp->servername != NULL) { @@ -224,14 +249,13 @@ exec_on_server(mparm_T *parmp) } # endif } - /* * Prepare for running as a Vim server. */ void prepare_server(mparm_T *parmp) { -# if defined(FEAT_X11) +# if defined(FEAT_X11) || defined(FEAT_SOCKETSERVER) /* * Register for remote command execution with :serversend and --remote * unless there was a -X or a --servername '' on the command line. @@ -239,7 +263,13 @@ prepare_server(mparm_T *parmp) * or when compiling with autoservername. * When running as root --servername is also required. */ - if (X_DISPLAY != NULL && parmp->servername != NULL && ( + + if ( +# ifdef FEAT_X11 + X_DISPLAY != NULL && +# endif + + parmp->servername != NULL && ( # if defined(FEAT_AUTOSERVERNAME) || defined(FEAT_GUI) ( # if defined(FEAT_AUTOSERVERNAME) @@ -254,12 +284,26 @@ prepare_server(mparm_T *parmp) # endif parmp->serverName_arg != NULL)) { - (void)serverRegisterName(X_DISPLAY, parmp->servername); +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + { + if (socket_server_init(parmp->servername) == OK) + TIME_MSG("initialize socket server"); + } +# endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) + { + (void)serverRegisterName(X_DISPLAY, parmp->servername); + TIME_MSG("register x11 server name"); + } +# endif vim_free(parmp->servername); - TIME_MSG("register server name"); } +#ifdef FEAT_X11 else serverDelayedStartName = parmp->servername; +#endif # endif /* @@ -299,9 +343,12 @@ cmdsrv_main( #define ARGTYPE_SEND 3 int silent = FALSE; int tabs = FALSE; -# ifndef FEAT_X11 +#ifdef FEAT_SOCKETSERVER + char_u *receiver; +#endif +# ifdef MSWIN HWND srv; -# else +# elif defined(FEAT_X11) Window srv; setup_term_clip(); @@ -384,16 +431,27 @@ cmdsrv_main( } Argc = i; } -# ifdef FEAT_X11 - if (xterm_dpy == NULL) + +#ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + ret = socket_server_send( + sname, *serverStr, NULL, &receiver, + 0, -1, silent); +#endif +#ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) { - mch_errmsg(_("No display")); - ret = -1; + if (xterm_dpy == NULL) + { + mch_errmsg(_("No display")); + ret = -1; + } + else + ret = serverSendToVim(xterm_dpy, sname, *serverStr, + NULL, &srv, 0, 0, 0, silent); } - else - ret = serverSendToVim(xterm_dpy, sname, *serverStr, - NULL, &srv, 0, 0, 0, silent); -# else +#endif +#ifdef MSWIN // Win32 always works? ret = serverSendToVim(sname, *serverStr, NULL, &srv, 0, 0, silent); # endif @@ -452,14 +510,24 @@ cmdsrv_main( vim_memset(done, 0, numFiles); while (memchr(done, 0, numFiles) != NULL) { - char_u *p; + char_u *p = NULL; int j; # ifdef MSWIN p = serverGetReply(srv, NULL, TRUE, TRUE, 0); if (p == NULL) break; # else - if (serverReadReply(xterm_dpy, srv, &p, TRUE, -1) < 0) +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET + && socket_server_read_reply(receiver, &p, -1) == FAIL) + break; +# endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11 + && serverReadReply(xterm_dpy, srv, &p, TRUE, -1) < 0) + break; +# endif + if (p == NULL) break; # endif j = atoi((char *)p); @@ -490,12 +558,34 @@ cmdsrv_main( if (serverSendToVim(sname, (char_u *)argv[i + 1], &res, NULL, 1, 0, FALSE) < 0) # else - if (xterm_dpy == NULL) - mch_errmsg(_("No display: Send expression failed.\n")); - else if (serverSendToVim(xterm_dpy, sname, (char_u *)argv[i + 1], - &res, NULL, 1, 0, 1, FALSE) < 0) +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + { + if (!socket_server_valid()) + mch_errmsg(_("Socket server not online:" + "Send expression failed")); + else if (socket_server_send(sname, (char_u *)argv[i + 1], + &res, NULL, 1, 0, FALSE) < 0) + goto expr_fail; + } +# endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) + { + if (xterm_dpy == NULL) + mch_errmsg(_("No display: Send expression failed.\n")); + else if (serverSendToVim(xterm_dpy, sname, + (char_u *)argv[i + 1], &res, + NULL, 1, 0, 1, FALSE) < 0) + goto expr_fail; + } +# endif + if (FALSE) # endif { +# if !defined(MSWIN) +expr_fail: +# endif if (res != NULL && *res != NUL) { // Output error from remote @@ -511,8 +601,25 @@ cmdsrv_main( // Win32 always works? res = serverGetVimNames(); # else - if (xterm_dpy != NULL) +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + { + int was_init = socket_server_valid(); + + // Don't want to add ourselves to the list. So shutdown the + // server before listing then startup back again. + socket_server_uninit(); + res = socket_server_list_sockets(); + + if (was_init) + socket_server_init(NULL); + } +# endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11 && + xterm_dpy != NULL) res = serverGetVimNames(xterm_dpy); +# endif # endif if (did_emsg) mch_errmsg("\n"); @@ -541,6 +648,9 @@ cmdsrv_main( if (didone) { +#ifdef FEAT_SOCKETSERVER + socket_server_uninit(); +#endif display_errors(); // display any collected messages exit(exiterr); // Mission accomplished - get out } @@ -694,7 +804,24 @@ serverMakeName(char_u *arg, char *cmd) char_u *p; if (arg != NULL && *arg != NUL) + { +#ifdef FEAT_SOCKETSERVER + // If we are using a socket server, we want to preserve the original + // name if it is a path, else uppercase it if its just a generic name. + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + { + if (arg[0] == '/' || STRNCMP(arg, "./", 2) == 0 || + STRNCMP(arg, "../", 3) == 0) + p = vim_strsave(arg); + else + p = vim_strsave_up(arg); + } + else + p = vim_strsave_up(arg); +#else p = vim_strsave_up(arg); +#endif + } else { p = vim_strsave_up(gettail((char_u *)cmd)); @@ -747,7 +874,12 @@ remote_common(typval_T *argvars, typval_T *rettv, int expr) # ifdef MSWIN HWND w; # else +#ifdef FEAT_X11 Window w; +#endif +#ifdef FEAT_SOCKETSERVER + char_u *client = NULL; +#endif # endif if (check_restricted() || check_secure()) @@ -768,14 +900,33 @@ remote_common(typval_T *argvars, typval_T *rettv, int expr) # ifdef MSWIN if (serverSendToVim(server_name, keys, &r, &w, expr, timeout, TRUE) < 0) # else - if (serverSendToVim(X_DISPLAY, server_name, keys, &r, &w, expr, timeout, - 0, TRUE) < 0) +#ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + if (socket_server_send(server_name, keys, &r, &client, expr, + timeout * 1000, TRUE) < 0) + goto stuff; +#endif +#ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) + if (serverSendToVim(X_DISPLAY, server_name, keys, &r, &w, expr, timeout, + 0, TRUE) < 0) + goto stuff; +#endif # endif +#if !defined(MSWIN) + if (FALSE) { +stuff: +#else + { +#endif if (r != NULL) { emsg((char *)r); // sending worked but evaluation failed vim_free(r); +#ifdef FEAT_SOCKETSERVER + vim_free(client); +#endif } else semsg(_(e_unable_to_send_to_str), server_name); @@ -787,19 +938,39 @@ remote_common(typval_T *argvars, typval_T *rettv, int expr) if (argvars[2].v_type != VAR_UNKNOWN) { dictitem_T v; +#if defined(FEAT_SOCKETSERVER) + struct sockaddr_un addr; + char_u str[sizeof(addr.sun_path)]; +#else char_u str[30]; +#endif char_u *idvar; idvar = tv_get_string_chk(&argvars[2]); if (idvar != NULL && *idvar != NUL) { +#ifdef MSWIN sprintf((char *)str, PRINTF_HEX_LONG_U, (long_u)w); +#else +#ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) + sprintf((char *)str, PRINTF_HEX_LONG_U, (long_u)w); +#endif +#ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + vim_snprintf((char *)str, sizeof(addr.sun_path), + "%s", client); +#endif +#endif v.di_tv.v_type = VAR_STRING; v.di_tv.vval.v_string = vim_strsave(str); set_var(idvar, &v.di_tv, FALSE); vim_free(v.di_tv.vval.v_string); } } +#ifdef FEAT_SOCKETSERVER + vim_free(client); +#endif } #endif @@ -890,11 +1061,20 @@ f_remote_peek(typval_T *argvars UNUSED, typval_T *rettv) rettv->vval.v_number = (s != NULL); } # else - if (check_connection() == FAIL) - return; +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + rettv->vval.v_number = socket_server_peek_reply(serverid, &s); +# endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) + { + if (check_connection() == FAIL) + return; - rettv->vval.v_number = serverPeekReply(X_DISPLAY, - serverStrToWin(serverid), &s); + rettv->vval.v_number = serverPeekReply(X_DISPLAY, + serverStrToWin(serverid), &s); + } +# endif # endif if (argvars[1].v_type != VAR_UNKNOWN && rettv->vval.v_number > 0) @@ -943,12 +1123,21 @@ f_remote_read(typval_T *argvars UNUSED, typval_T *rettv) if (n != 0) r = serverGetReply((HWND)n, FALSE, TRUE, TRUE, timeout); if (r == NULL) -# else - if (check_connection() == FAIL - || serverReadReply(X_DISPLAY, serverStrToWin(serverid), - &r, FALSE, timeout) < 0) -# endif emsg(_(e_unable_to_read_server_reply)); +# else +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET && + socket_server_read_reply(serverid, &r, timeout * 1000) == FAIL) + emsg(_(e_unable_to_read_server_reply)); +# endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11 && + (check_connection() == FAIL + || serverReadReply(X_DISPLAY, serverStrToWin(serverid), + &r, FALSE, timeout) < 0)) + emsg(_(e_unable_to_read_server_reply)); +# endif +# endif } #endif rettv->v_type = VAR_STRING; @@ -992,11 +1181,18 @@ f_remote_startserver(typval_T *argvars UNUSED, typval_T *rettv UNUSED) } char_u *server = tv_get_string_chk(&argvars[0]); -# ifdef FEAT_X11 - if (check_connection() == OK) - serverRegisterName(X_DISPLAY, server); -# else +# ifdef MSWIN serverSetName(server); +# else +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + socket_server_init(server); +# endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11 && + check_connection() == OK) + serverRegisterName(X_DISPLAY, server); +# endif # endif #else @@ -1026,13 +1222,30 @@ f_server2client(typval_T *argvars UNUSED, typval_T *rettv) if (server == NULL || reply == NULL) return; -# ifdef FEAT_X11 - if (check_connection() == FAIL) - return; -# endif +#ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET && + socket_server_send_reply(server, reply) == FAIL) + goto fail; +#endif +#ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11 && + check_connection() == FAIL) + return; + + if (clientserver_method == CLIENTSERVER_METHOD_X11 && + serverSendReply(server, reply) < 0) +#endif +#ifdef MSWIN if (serverSendReply(server, reply) < 0) +#endif +#if defined(FEAT_SOCKETSERVER) && !defined(FEAT_X11) && !defined(MSWIN) + if (FALSE) +#endif { +#ifdef FEAT_SOCKETSERVER +fail: +#endif emsg(_(e_unable_to_send_to_client)); return; } @@ -1051,9 +1264,18 @@ f_serverlist(typval_T *argvars UNUSED, typval_T *rettv) # ifdef MSWIN r = serverGetVimNames(); # else +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + r = socket_server_list_sockets(); +# endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) + { make_connection(); if (X_DISPLAY != NULL) r = serverGetVimNames(X_DISPLAY); + } +# endif # endif #endif rettv->v_type = VAR_STRING; diff --git a/src/config.h.in b/src/config.h.in index fce8d5f44f..e8775d98c3 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -406,6 +406,12 @@ /* Define if you want to always define a server name at vim startup. */ #undef FEAT_AUTOSERVERNAME +/* Define if you want to use sockets for clientserver communication. */ +#undef WANT_SOCKETSERVER + +/* Define if you want to use sockets for clientserver communication if it makes sense. */ +#undef MAYBE_SOCKETSERVER + /* Define if you want to include fontset support. */ #undef FEAT_XFONTSET diff --git a/src/configure.ac b/src/configure.ac index 4d91a074fc..cdb8185199 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -2340,6 +2340,24 @@ if test "$enable_autoservername" = "yes"; then AC_DEFINE(FEAT_AUTOSERVERNAME) fi +AC_MSG_CHECKING(--enable-socketserver argument) +AC_ARG_ENABLE(socketserver, + [ --enable-socketserver Use sockets for clientserver communication.], + [enable_socketserver=$enableval], + AS_IF([test "x$features" = xtiny], + [enable_socketserver=no_auto + AC_MSG_RESULT([cannot use socketserver with tiny features])], + [enable_socketserver=auto])) +if test "$enable_socketserver" = "yes"; then + AC_DEFINE(WANT_SOCKETSERVER) + AC_MSG_RESULT([yes]) +elif test "$enable_socketserver" = "auto"; then + AC_DEFINE(MAYBE_SOCKETSERVER) + AC_MSG_RESULT([auto]) +elif test "$enable_socketserver" = "no"; then + AC_MSG_RESULT([no]) +fi + AC_MSG_CHECKING(--enable-multibyte argument) AC_ARG_ENABLE(multibyte, [ --enable-multibyte Include multibyte editing support.], , diff --git a/src/errors.h b/src/errors.h index 050fb42199..5d6867464b 100644 --- a/src/errors.h +++ b/src/errors.h @@ -3782,3 +3782,15 @@ EXTERN char e_duplicate_type_var_name_str[] EXTERN char e_diff_anchors_with_hidden_windows[] INIT(= N_("E1562: Diff anchors cannot be used with hidden diff windows")); #endif +#ifdef FEAT_SOCKETSERVER +EXTERN char e_socket_path_too_big[] + INIT(= N_("E1563: Socket path is too big")); +EXTERN char e_socket_name_no_slashes[] + INIT(= N_("E1564: Socket name cannot have slashes in it without being a path")); +EXTERN char e_socket_server_not_online[] + INIT(= N_("E1565: Socket server is not online, call remote_startserver() first")); +EXTERN char e_socket_server_failed_connecting[] + INIT(= N_("E1566: Failed connecting to socket %s: %s")); +EXTERN char e_socket_server_unavailable[] + INIT(= N_("E1567: Cannot start socket server, socket path is unavailable")); +#endif diff --git a/src/evalfunc.c b/src/evalfunc.c index 7211048d11..419780f38c 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -6765,6 +6765,13 @@ f_has(typval_T *argvars, typval_T *rettv) 1 #else 0 +#endif + }, + {"socketserver", +#ifdef FEAT_SOCKETSERVER + 1 +#else + 0 #endif }, {"balloon_eval", diff --git a/src/ex_docmd.c b/src/ex_docmd.c index 87b390aa87..3e48aed795 100644 --- a/src/ex_docmd.c +++ b/src/ex_docmd.c @@ -10039,9 +10039,29 @@ eval_vars( #ifdef FEAT_CLIENTSERVER case SPEC_CLIENT: // Source of last submitted input +#ifdef MSWIN sprintf((char *)strbuf, PRINTF_HEX_LONG_U, (long_u)clientWindow); result = strbuf; +#else +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + { + if (client_socket == NULL) + result = (char_u *)""; + else + result = client_socket; + } +# endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) + { + sprintf((char *)strbuf, PRINTF_HEX_LONG_U, + (long_u)clientWindow); + result = strbuf; + } +# endif +#endif break; #endif diff --git a/src/feature.h b/src/feature.h index e5df7fab7c..826adbe2bd 100644 --- a/src/feature.h +++ b/src/feature.h @@ -945,11 +945,20 @@ # define FIND_REPLACE_DIALOG 1 #endif +/* + * +socketserver Use UNIX domain sockets for clientserver communication + */ +#if defined(UNIX) && (defined(WANT_SOCKETSERVER) || \ + (defined(MAYBE_SOCKETSERVER) && !defined(HAVE_X11))) +#define FEAT_SOCKETSERVER +#endif + /* * +clientserver Remote control via the remote_send() function * and the --remote argument */ -#if (defined(MSWIN) || defined(FEAT_XCLIPBOARD)) && defined(FEAT_EVAL) +#if (defined(MSWIN) || defined(FEAT_XCLIPBOARD) || defined(FEAT_SOCKETSERVER)) \ + && defined(FEAT_EVAL) # define FEAT_CLIENTSERVER #endif diff --git a/src/globals.h b/src/globals.h index 9e3a681200..8d031028ea 100644 --- a/src/globals.h +++ b/src/globals.h @@ -1868,7 +1868,7 @@ EXTERN Window commWindow INIT(= None); EXTERN Window clientWindow INIT(= None); EXTERN Atom commProperty INIT(= None); EXTERN char_u *serverDelayedStartName INIT(= NULL); -# else +# elif defined(MSWIN) # ifdef PROTO typedef int HWND; # endif @@ -2090,3 +2090,35 @@ EXTERN char *wayland_display_name INIT(= NULL); EXTERN int wayland_display_fd; #endif + +#if defined(FEAT_CLIENTSERVER) && !defined(MSWIN) + +// Backend for clientserver functionality +typedef enum { + CLIENTSERVER_METHOD_NONE, + CLIENTSERVER_METHOD_X11, + CLIENTSERVER_METHOD_SOCKET +} clientserver_method_T; + +// Default to X11 if compiled with support for it, else use socket server. +# if defined(FEAT_X11) && defined(FEAT_SOCKETSERVER) +EXTERN clientserver_method_T clientserver_method +# else +// Since we aren't going to be changing clientserver_method, make it constant to +// allow compiler optimizations. +EXTERN const clientserver_method_T clientserver_method +# endif +# ifdef FEAT_X11 +INIT(= CLIENTSERVER_METHOD_X11); +# elif defined(FEAT_SOCKETSERVER) +INIT(= CLIENTSERVER_METHOD_SOCKET); +# else +INIT(= CLIENTSERVER_METHOD_NONE); +# endif + +#endif + +#ifdef FEAT_SOCKETSERVER +// Path to socket of last client that communicated with us +EXTERN char_u *client_socket INIT(= NULL); +#endif diff --git a/src/gui.c b/src/gui.c index e125f87336..13ce0523b6 100644 --- a/src/gui.c +++ b/src/gui.c @@ -150,6 +150,12 @@ gui_start(char_u *arg UNUSED) // Reset clipmethod to CLIPMETHOD_NONE choose_clipmethod(); +#ifdef FEAT_SOCKETSERVER + // Install socket server listening socket if we are running it + if (socket_server_valid()) + gui_gtk_init_socket_server(); +#endif + vim_free(old_term); // If the GUI started successfully, trigger the GUIEnter event, otherwise diff --git a/src/gui_gtk_x11.c b/src/gui_gtk_x11.c index f52f385548..2e3af0421f 100644 --- a/src/gui_gtk_x11.c +++ b/src/gui_gtk_x11.c @@ -99,6 +99,13 @@ extern void bonobo_dock_item_set_behavior(BonoboDockItem *dock_item, BonoboDockI # include #endif +#ifdef FEAT_SOCKETSERVER +# include + +// Used to track the source for the listening socket +static uint socket_server_source_id = 0; +#endif + /* * Easy-to-use macro for multihead support. */ @@ -2688,6 +2695,53 @@ global_event_filter(GdkXEvent *xev, } #endif // !USE_GNOME_SESSION +#ifdef FEAT_SOCKETSERVER + +/* + * Callback for new events from the socket server listening socket + */ + static int +socket_server_poll_in(int fd UNUSED, GIOCondition cond, void *user_data UNUSED) +{ + if (cond & G_IO_IN) + socket_server_accept_client(); + else if (cond & (G_IO_ERR | G_IO_HUP)) + { + socket_server_uninit(); + return FALSE; + } + + return TRUE; +} + +/* + * Initialize socket server for use in the GUI (does not actually initialize the + * socket server, only attaches a source). + */ + void +gui_gtk_init_socket_server(void) +{ + if (socket_server_source_id > 0) + return; + // Register source for file descriptor to global default context + socket_server_source_id = g_unix_fd_add(socket_server_get_fd(), + G_IO_IN | G_IO_ERR | G_IO_HUP, socket_server_poll_in, NULL); +} + +/* + * Remove the source for the socket server listening socket. + */ + void +gui_gtk_uninit_socket_server(void) +{ + if (socket_server_source_id > 0) + { + g_source_remove(socket_server_source_id); + socket_server_source_id = 0; + } +} + +#endif /* * Setup the window icon & xcmdsrv comm after the main window has been realized. @@ -2754,7 +2808,7 @@ mainwin_realize(GtkWidget *widget UNUSED, gpointer data UNUSED) setup_save_yourself(); #ifdef FEAT_CLIENTSERVER - if (gui_mch_get_display()) + if (clientserver_method == CLIENTSERVER_METHOD_X11 && gui_mch_get_display()) { if (serverName == NULL && serverDelayedStartName != NULL) { diff --git a/src/if_xcmdsrv.c b/src/if_xcmdsrv.c index 716665a7be..ddb0579a2c 100644 --- a/src/if_xcmdsrv.c +++ b/src/if_xcmdsrv.c @@ -14,7 +14,7 @@ #include "vim.h" #include "version.h" -#if defined(FEAT_CLIENTSERVER) || defined(PROTO) +#if (defined(FEAT_CLIENTSERVER) && defined(FEAT_X11)) || defined(PROTO) # ifdef FEAT_X11 # include diff --git a/src/main.c b/src/main.c index 34576c3e25..c77454a26f 100644 --- a/src/main.c +++ b/src/main.c @@ -1855,7 +1855,8 @@ getout(int exitval) * Get the name of the display, before gui_prepare() removes it from * argv[]. Used for the xterm-clipboard display. * - * Also find the --server... arguments and --socketid and --windowid + * Also find the --server, --clientserver... arguments and --socketid and + * --windowid */ static void early_arg_scan(mparm_T *parmp UNUSED) @@ -1900,6 +1901,22 @@ early_arg_scan(mparm_T *parmp UNUSED) gui.dofork = FALSE; # endif } +# if defined(FEAT_X11) && defined(FEAT_SOCKETSERVER) + else if (STRNICMP(argv[i], "--clientserver", 14) == 0) + { + char_u *arg; + if (i == argc - 1) + mainerr_arg_missing((char_u *)argv[i]); + arg = (char_u *)argv[++i]; + + if (STRICMP(arg, "socket") == 0) + clientserver_method = CLIENTSERVER_METHOD_SOCKET; + else if (STRICMP(arg, "x11") == 0) + clientserver_method = CLIENTSERVER_METHOD_X11; + else + mainerr(ME_UNKNOWN_OPTION, arg); + } +# endif # endif # if defined(FEAT_GUI_GTK) || defined(FEAT_GUI_MSWIN) @@ -2220,7 +2237,11 @@ command_line_scan(mparm_T *parmp) else if (STRNICMP(argv[0] + argv_idx, "serverlist", 10) == 0) ; // already processed -- no arg else if (STRNICMP(argv[0] + argv_idx, "servername", 10) == 0 - || STRNICMP(argv[0] + argv_idx, "serversend", 10) == 0) + || STRNICMP(argv[0] + argv_idx, "serversend", 10) == 0 +# if defined(FEAT_X11) && defined(FEAT_SOCKETSERVER) + || STRNICMP(argv[0] + argv_idx, "clientserver", 12) == 0 +# endif + ) { // already processed -- snatch the following arg if (argc > 1) @@ -3712,6 +3733,9 @@ usage(void) main_msg(_("-Y\t\t\tDo not connect to Wayland compositor")); #endif #ifdef FEAT_CLIENTSERVER +# if defined(FEAT_X11) && defined(FEAT_SOCKETSERVER) + main_msg(_("--clientserver Backend for clientserver communication")); +# endif main_msg(_("--remote \tEdit in a Vim server if possible")); main_msg(_("--remote-silent Same, don't complain if there is no server")); main_msg(_("--remote-wait As --remote but wait for files to have been edited")); diff --git a/src/os_unix.c b/src/os_unix.c index 3e4c6b339a..cc1db3d198 100644 --- a/src/os_unix.c +++ b/src/os_unix.c @@ -147,6 +147,123 @@ Window x11_window = 0; Display *x11_display = NULL; #endif +#ifdef FEAT_SOCKETSERVER +# include +# include + +# define SOCKET_SERVER_MAX_BACKLOG 5 +# define SOCKET_SERVER_MAX_CMD_SIZE 16384 +# define SOCKET_SERVER_MAX_MSG 6 + +static int socket_server_fd = -1; +static char_u *socket_server_path = NULL; + +typedef enum { + SS_MSG_TYPE_ENCODING = 'e', // Encoding of message. + SS_MSG_TYPE_STRING = 'c', // Script to execute or reply string. + SS_MSG_TYPE_SERIAL = 's', // Serial of pending command + SS_MSG_TYPE_CODE = 'r', // Result code for an expression sent + SS_MSG_TYPE_SENDER = 'd' // Location of socket for the client that + // sent the command. +} ss_msg_type_T; + +typedef enum { + SS_CMD_TYPE_EXPR = 'E', // An expression + SS_CMD_TYPE_KEYSTROKES = 'K', // Series of keystrokes + SS_CMD_TYPE_REPLY = 'R', // Reply from an expression + SS_CMD_TYPE_NOTIFY = 'N', // A notification + SS_CMD_TYPE_ALIVE = 'A', // Check if server is still responsive +} ss_cmd_type_T; + +// Represents a message in a command. A command can contain multiple messages. +// Each message starts with a single byte representing the type, then a uint32 +// representing the length of the contents, and then the actual contents. +// Everything is in native byte order. +// +// While contents may contain NULL characters, such as when it is a number, it +// is always NULL terminated. Note that the NULL terminator does not count in +// the length. +typedef struct { + char_u msg_type; // Type of message + uint32_t msg_len; // Total length of contents + char_u *msg_contents; // Actual contents of message +} ss_msg_T; + +// Represents a command sent over a socket. Each socket starts with a byte +// representing the type, then a uint32 representing the number of messages, +// then a uint32 representing the total size of the messages in bytes, and then +// the actual messages. Everything is in native byte order. +typedef struct { + char_u cmd_type; // Type of command + uint32_t cmd_num; // Number of messages + uint32_t cmd_len; // Combined size of all + // messages + ss_msg_T cmd_msgs[SOCKET_SERVER_MAX_MSG]; // Array of messages +} ss_cmd_T; + +#define SS_CMD_INFO_SIZE (sizeof(char_u) + (sizeof(uint32_t) * 2)) +#define SS_MSG_INFO_SIZE (sizeof(char_u) + sizeof(uint32_t)) + +// Represents a pending reply from a command sent to a Vim server. When a +// command is sent out, we generate unique serial number with it. When we +// receive any reply, we check which pending command has a matching serial +// number, and is therefore the reply for that pending command. +// +// The reason we just don't use the existing fd created by the connect() call, +// and communicate using that, is that it can't handle recursive calls, ex: +// call remote_expr('B', 'remote_expr("A", "")') +// +// This idea is taken from the existing X server functionality +typedef struct ss_pending_cmd_S { + uint32_t serial; // Serial number expected in result + char_u code; // Result code, can be 0 or -1. + char_u *result; // Result of command + + struct ss_pending_cmd_S *next; // Next in list +} ss_pending_cmd_T; + +ss_pending_cmd_T *ss_pending_cmds; + +// Serial is always greater than zero +static uint32_t ss_serial = 0; + +// Represents a reply from a server2client call. Each client that calls a +// server2client call to us has its own ss_reply_T. Each time a client sends +// data using server2client, Vim creates a ss_reply_T if it doesn't exist and +// adds the string to the array. When remote_read is called, the server id is +// used to find the specific ss_reply_T, and a single string is popped from the +// array. +// +// This idea is taken from the existing X server functionality +typedef struct { + char_u *sender; + garray_T strings; +} ss_reply_T; + +static garray_T ss_replies; + +static char_u *socket_server_get_path_from_name(char_u *name); +static int socket_server_connect(char_u *name, char_u **path, int silent); +static void socket_server_init_pending_cmd(ss_pending_cmd_T *pending); +static void socket_server_pop_pending_cmd(ss_pending_cmd_T *pending); +static void socket_server_init_cmd(ss_cmd_T *cmd, ss_cmd_type_T type); +static int socket_server_append_msg(ss_cmd_T *cmd, char_u type, + char_u *contents, int len); +static void socket_server_free_cmd(ss_cmd_T *cmd); +static char_u *socket_server_encode_cmd(ss_cmd_T *cmd, size_t *sz); +static int socket_server_decode_cmd(ss_cmd_T *cmd, int socket_fd, int timeout); +static int socket_server_write(int sock_fd, char_u *data, size_t sz, + int timeout); +static ss_reply_T *socket_server_get_reply(char_u *sender, int *index); +static ss_reply_T *socket_server_add_reply(char_u *sender); +static void socket_server_remove_reply(char_u *sender); +static void socket_server_exec_cmd(ss_cmd_T *cmd, int fd); +static int socket_server_dispatch(int timeout); +static int socket_server_check_alive(char_u *name); +static int socket_server_name_is_valid(char_u *name); + +#endif // FEAT_SOCKETSERVER + static int ignore_sigtstp = FALSE; static int get_x11_title(int); @@ -3653,6 +3770,10 @@ mch_exit(int r) x11_export_final_selection(); #endif +#ifdef FEAT_SOCKETSERVER + socket_server_uninit(); +#endif + #ifdef FEAT_GUI if (!gui.in_use) #endif @@ -6540,6 +6661,9 @@ RealWaitForChar(int fd, long msec, int *check_for_gpm UNUSED, int *interrupted) // each channel may use in, out and err struct pollfd fds[7 + 3 * MAX_OPEN_CHANNELS]; int nfd; +# ifdef FEAT_SOCKETSERVER + int socket_server_idx = -1; +# endif # ifdef FEAT_WAYLAND_CLIPBOARD int wayland_idx = -1; # endif @@ -6566,6 +6690,16 @@ RealWaitForChar(int fd, long msec, int *check_for_gpm UNUSED, int *interrupted) fds[0].events = POLLIN; nfd = 1; +# ifdef FEAT_SOCKETSERVER + if (socket_server_fd != -1) + { + socket_server_idx = nfd; + fds[nfd].fd = socket_server_fd; + fds[nfd].events = POLLIN; + nfd++; + } +# endif + # ifdef FEAT_WAYLAND_CLIPBOARD if (wayland_may_restore_connection()) { @@ -6621,6 +6755,17 @@ RealWaitForChar(int fd, long msec, int *check_for_gpm UNUSED, int *interrupted) finished = FALSE; # endif +# ifdef FEAT_SOCKETSERVER + if (socket_server_fd != -1) + { + if (fds[socket_server_idx].revents & POLLIN) + socket_server_accept_client(); + else if (fds[socket_server_idx].revents & (POLLHUP | POLLERR)) + socket_server_uninit(); + } + +# endif + # ifdef FEAT_WAYLAND_CLIPBOARD // Technically we should first call wl_display_prepare_read() before // polling the fd, then read and dispatch after we poll. However that is @@ -6710,6 +6855,16 @@ select_eintr: # endif maxfd = fd; +# ifdef FEAT_SOCKETSERVER + if (socket_server_fd != -1) + { + FD_SET(socket_server_fd, &rfds); + + if (maxfd < socket_server_fd) + maxfd = socket_server_fd; + } +# endif + # ifdef FEAT_WAYLAND_CLIPBOARD if (wayland_may_restore_connection()) @@ -6810,6 +6965,16 @@ select_eintr: finished = FALSE; # endif +# ifdef FEAT_SOCKETSERVER + if (socket_server_fd != -1 && ret > 0) + { + if (FD_ISSET(socket_server_fd, &rfds)) + socket_server_accept_client(); + else if (FD_ISSET(socket_server_fd, &efds)) + socket_server_uninit(); + } +# endif + # ifdef FEAT_WAYLAND_CLIPBOARD // Technically we should first call wl_display_prepare_read() before // polling the fd, then read and dispatch after we poll. However that is @@ -6875,9 +7040,16 @@ select_eintr: if (finished || msec == 0) break; -# ifdef FEAT_CLIENTSERVER - if (server_waiting()) +# if defined(FEAT_CLIENTSERVER) +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11 && server_waiting()) break; +# endif +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET && + socket_server_waiting_accept()) + break; +# endif # endif // We're going to loop around again, find out for how long @@ -8961,3 +9133,1544 @@ mch_create_anon_file(void) } return fd; } + +#ifdef FEAT_SOCKETSERVER + +/* + * Initialize socket server called "name" (the socket filename). If "name" is a + * path (starts with a '/', './', or '../'), it is assumed to be the path to + * the desired socket. If the socket path is already taken, append an + * incrementing number to the path until we find a socket filename that can be + * used. If NULL is passed as the name, the previous socket path is used (only + * if not NULL). Returns OK on success and FAIL on failure. + */ + int +socket_server_init(char_u *name) +{ + struct sockaddr_un addr; + char_u *path; + int num_printed; + int fd; + int i = 1; + + if (socket_server_valid() || (name == NULL && socket_server_path == NULL)) + return FAIL; + if (name == NULL) + name = socket_server_path; + + path = alloc(sizeof(addr.sun_path)); + + if (path == NULL) + return FAIL; + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + + if (fd == -1) + { + vim_free(path); + return FAIL; + } + + addr.sun_family = AF_UNIX; + + // If name is not a path, find a common directory to place the + // socket. + if (name[0] == '/' || STRNCMP(name, "./", 2) == 0 || + STRNCMP(name, "../", 3) == 0) + num_printed = + vim_snprintf((char *)path, sizeof(addr.sun_path), "%s", name); + else + { + const char_u *dir; + char_u *buf; + + // Check if there are slashes in the name + if (vim_strchr(name, '/') != NULL) + { + emsg(_(e_socket_name_no_slashes)); + goto fail; + } + + dir = mch_getenv("XDG_RUNTIME_DIR"); + + if (dir == NULL) + { + // Use $TMPDIR or /tmp if $XDG_RUNTIME_DIR is not set. + const char_u *tmpdir = mch_getenv("TMPDIR"); + size_t sz; + + if (tmpdir != NULL) + dir = tmpdir; + else + dir = (char_u *)"/tmp"; + + sz = STRLEN(dir) + 25; + buf = alloc(sz); + + if (buf == NULL) + goto fail; + + vim_snprintf((char *)buf, sz, "%s/vim-%lu", dir, + (unsigned long int)getuid()); + } + else + { + buf = alloc(STRLEN(dir) + STRLEN("vim") + 2); + + if (buf == NULL) + goto fail; + + sprintf((char *)buf, "%s/vim", dir); + } + + // Always set directory permissions to 0700 for security + if (vim_mkdir(buf, 0700) == -1 && errno != EEXIST) + { + semsg(_("Failed creating socket directory: %s"), strerror(errno)); + vim_free(buf); + goto fail; + } + + num_printed = vim_snprintf((char *)path, sizeof(addr.sun_path), + "%s/%s", buf, name); + + vim_free(buf); + } + + // Check if path was too big + if ((size_t)num_printed >= sizeof(addr.sun_path)) + { + emsg(_(e_socket_path_too_big)); + goto fail; + } + + vim_snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path); + + // Bind to a suitable path/address + while (i < 1000) + { + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) + == -1) + { + int fd2; + + if (errno != EADDRINUSE) + { + emsg(_(e_socket_server_unavailable)); + goto fail; + } + + // If the socket is dead, remove it and try again + fd2 = socket_server_connect((char_u *)addr.sun_path, NULL, TRUE); + + if (fd2 == -1) + { + mch_remove(addr.sun_path); + continue; + } + else + close(fd2); + } + else + break; + + num_printed = vim_snprintf(addr.sun_path, sizeof(addr.sun_path), + "%s%d", path, i); + + if ((size_t)num_printed >= sizeof(addr.sun_path)) + { + // Address too big + emsg(_(e_socket_path_too_big)); + goto fail; + } + + i++; + } + + if (i >= 1000) + { + emsg(_(e_socket_server_unavailable)); + goto fail; + } + + // Start listening for connections + if (listen(fd, SOCKET_SERVER_MAX_BACKLOG) == -1) + goto fail; + + // Set global path and vvar to the absolute path + if ((socket_server_path = alloc(MAXPATHL)) == NULL) + goto fail; + + socket_server_path[0] = NUL; + + if (mch_FullName((char_u *)addr.sun_path, socket_server_path, + MAXPATHL, FALSE) == FAIL) + { + vim_free(socket_server_path); + goto fail; + } + + serverName = vim_strsave(socket_server_path); +#ifdef FEAT_EVAL + set_vim_var_string(VV_SEND_SERVER, serverName, -1); +#endif + + socket_server_fd = fd; + +#ifdef FEAT_GUI_GTK + if (gui.in_use) + // Initialize source for GUI if we are using it + gui_gtk_init_socket_server(); +#endif + + vim_free(path); + return OK; +fail: + vim_free(path); + socket_server_uninit(); + return FAIL; +} + + void +socket_server_uninit(void) +{ + if (socket_server_fd != -1) + { + close(socket_server_fd); + socket_server_fd = -1; + } + + if (socket_server_path != NULL) + { + mch_remove(socket_server_path); + vim_free(socket_server_path); + socket_server_path = NULL; + } +#ifdef FEAT_GUI_GTK + if (gui.in_use) + gui_gtk_uninit_socket_server(); +#endif +} + +/* + * List available sockets that can be connected to, only in common directories + * that Vim knows about. Vim instances with custom socket paths will not be + * detected. Returns a newline separated string on success and NULL on failure. + */ + char_u * +socket_server_list_sockets(void) +{ + garray_T str; + char_u *buf; + char_u *path; + DIR *dirp; + struct dirent *dp; + struct sockaddr_un addr; + char_u *known_dirs[] = { + mch_getenv("XDG_RUNTIME_DIR"), + mch_getenv("TMPDIR"), + (char_u *)"/tmp" + }; + + if ((buf = alloc(sizeof(addr.sun_path))) == NULL) + return NULL; + if ((path = alloc(sizeof(addr.sun_path))) == NULL) + { + vim_free(buf); + return NULL; + } + + ga_init2(&str, 1, 100); + + for (size_t i = 0 ; i < ARRAY_LENGTH(known_dirs); i++) + { + char_u *dir = known_dirs[i]; + + if (dir == NULL) + continue; + + if (STRCMP(dir, "/tmp") == 0 || + (known_dirs[1] != NULL && STRCMP(dir, known_dirs[1]) == 0)) + vim_snprintf((char *)path, sizeof(addr.sun_path), "%s/vim-%lu", + dir, (unsigned long int)getuid()); + else + vim_snprintf((char *)path, sizeof(addr.sun_path), "%s/vim", dir); + + dir = path; + + dirp = opendir((char *)dir); + + if (dirp == NULL) + continue; + + // Loop through directory + while ((dp = readdir(dirp)) != NULL) + { + if (STRCMP(dp->d_name, ".") == 0 || STRCMP(dp->d_name, "..") == 0) + continue; + + vim_snprintf((char *)buf, sizeof(addr.sun_path), "%s/%s", + dir, dp->d_name); + + // Try sending an ALIVE command. This is more assuring than a + // simple connect, and *also seems to make tests less flaky*. + if (!socket_server_check_alive(buf)) + continue; + + ga_concat(&str, (char_u *)dp->d_name); + ga_append(&str, '\n'); + } + + closedir(dirp); + + break; + } + + vim_free(path); + vim_free(buf); + + ga_append(&str, NUL); + + return str.ga_data; +} + +/* + * Called when the server has received a new command. If so, parse it and do the + * stuff it says, and possibly send back a reply. + */ + void +socket_server_accept_client(void) +{ + int fd = accept(socket_server_fd, NULL, NULL); + ss_cmd_T cmd; + + if (fd == -1) + return; + + if (socket_server_decode_cmd(&cmd, fd, 1000) == FAIL) + goto exit; + +#ifdef FEAT_EVAL + ch_log(NULL, "accepted new client on socket %s", socket_server_path); +#endif + + socket_server_exec_cmd(&cmd, fd); + socket_server_free_cmd(&cmd); + +exit: + close(fd); +} + +/* + * Check if socket server is able to be used + */ + int +socket_server_valid(void) +{ + return socket_server_fd != -1 && socket_server_path != NULL; +} + +/* + * If "name" is a pathless name such as "VIM", search known directories for the + * socket named "name", and return the alloc'ed path to it. If "name" starts + * with a '/', './' or '../', then a copy of "name" is returned. Returns NULL + * on failure or if no socket was found. + */ + static char_u * +socket_server_get_path_from_name(char_u *name) +{ + char_u *buf; + stat_T s; + const char_u *known_dirs[] = { + mch_getenv("XDG_RUNTIME_DIR"), + mch_getenv("TMPDIR"), + (char_u *)"/tmp" + }; + + if (name == NULL) + return NULL; + + // Ignore if name is a path + if (name[0] == '/' || STRNCMP(name, "./", 2) == 0 || + STRNCMP(name, "../", 3) == 0) + return vim_strsave(name); + + buf = alloc(MAXPATHL); + + if (buf == NULL) + return NULL; + + for (size_t i = 0; i < ARRAY_LENGTH(known_dirs); i++) + { + const char_u *dir = known_dirs[i]; + + if (dir == NULL) + continue; + else if (STRCMP(dir, "/tmp") == 0 || + (known_dirs[1] != NULL && STRCMP(dir, known_dirs[1]) == 0)) + vim_snprintf((char *)buf, MAXPATHL, "%s/vim-%lu/%s", dir, + (unsigned long int)getuid(), name); + else + vim_snprintf((char *)buf, MAXPATHL, "%s/vim/%s", dir, name); + + if (mch_stat((char *)buf,&s) == 0 && S_ISSOCK(s.st_mode)) + { + if (STRCMP(buf, socket_server_path) == 0) + // Can't connect to itself + break; + return buf; + } + } + + vim_free(buf); + return NULL; +} + +/* + * Send command to socket named "name". Returns 0 for OK, -1 on error. + */ + int +socket_server_send( + char_u *name, // Socket path or a general name + char_u *str, // What to send + char_u **result, // Set to result of expr + char_u **receiver, // Full path of "name" + int is_expr, // Is it an expresison or keystrokes? + int timeout, // In milliseconds + int silent) // Don't complain if socket doesn't exist +{ + ss_cmd_T cmd; + int socket_fd; + size_t sz; + char_u *final; + char_u *path; + struct timeval start, now; + + + if (!socket_server_valid()) + { + emsg(_(e_socket_server_not_online)); + return -1; + } + + socket_fd = socket_server_connect(name, &path, silent); + + if (socket_fd == -1) + return -1; + +#ifdef FEAT_EVAL + ch_log(NULL, "socket_server_send(%s, %s)", path, str); +#endif + + // Execute locally if target is ourselves + if (serverName != NULL && STRICMP(path, serverName) == 0) + { + vim_free(path); + close(socket_fd); + return sendToLocalVim(str, is_expr, result); + } + + socket_server_init_cmd(&cmd, + is_expr ? SS_CMD_TYPE_EXPR : SS_CMD_TYPE_KEYSTROKES); + + socket_server_append_msg(&cmd, SS_MSG_TYPE_ENCODING, p_enc, STRLEN(p_enc)); + + // Add +1 in case of empty string + socket_server_append_msg(&cmd, SS_MSG_TYPE_STRING, str, STRLEN(str) + 1); + + // Tell server who we are so it can save our socket path internally for + // later use with server2client + socket_server_append_msg(&cmd, SS_MSG_TYPE_SENDER, socket_server_path, + STRLEN(socket_server_path)); + + if (is_expr) + { + ss_serial++; + socket_server_append_msg(&cmd, SS_MSG_TYPE_SERIAL, + (char_u *)&ss_serial, sizeof(ss_serial)); + } + + final = socket_server_encode_cmd(&cmd, &sz); + + if (final == NULL || + socket_server_write(socket_fd, final, sz, 1000) == FAIL) + { + if (final != NULL) + emsg(_(e_failed_to_send_command_to_destination_program)); + + vim_free(path); + socket_server_free_cmd(&cmd); + close(socket_fd); + vim_free(final); + return -1; + } + socket_server_free_cmd(&cmd); + vim_free(final); + + + close(socket_fd); + if (!is_expr) + { + if (receiver != NULL) + *receiver = path; + else + vim_free(path); + + // Exit, we aren't waiting for a reponse + return 0; + } + + ss_pending_cmd_T pending; + + socket_server_init_pending_cmd(&pending); + + gettimeofday(&start, NULL); + + // Wait for server to send back result + while (socket_server_dispatch(500) >= 0) + { + if (pending.result != NULL) + break; + + gettimeofday(&now, NULL); + + if ((now.tv_sec * 1000000 + now.tv_usec) - + (start.tv_sec * 1000000 + start.tv_usec) >= + (timeout > 0 ? timeout * 1000 : 1000 * 1000)) + break; + } + + if (pending.result == NULL) + { + socket_server_pop_pending_cmd(&pending); + vim_free(path); + return -1; + } + + if (result != NULL) + *result = pending.result; + else + vim_free(pending.result); + + if (receiver != NULL) + *receiver = path; + else + vim_free(path); + + socket_server_pop_pending_cmd(&pending); + + return pending.code == 0 ? 0 : -1; +} + +/* + * Wait for replies from "client" and place result in "str". Returns OK on + * success and FAIL on failure. Timeout is in milliseconds + */ + int +socket_server_read_reply(char_u *client, char_u **str, int timeout) +{ + ss_reply_T *reply = NULL; + struct timeval start, now; + + if (!socket_server_name_is_valid(client)) + return -1; + + if (!socket_server_valid()) + return -1; + + if (timeout > 0) + gettimeofday(&start, NULL); + + // Try seeing if there already is a reply in the queue + goto get_reply; + + while (socket_server_dispatch(500) >= 0) + { + int fd; + + if (timeout > 0) + gettimeofday(&now, NULL); + + if (timeout > 0) + if ((now.tv_sec * 1000000 + now.tv_usec) - + (start.tv_sec * 1000000 + start.tv_usec) >= timeout * 1000) + break; + +get_reply: + reply = socket_server_get_reply(client, NULL); + + if (reply != NULL) + break; + + // Check if sender is down by connecting to it as a test. A simple + // connect will do. + fd = socket_server_connect(client, NULL, TRUE); + + if (fd == -1) + return FAIL; + else + close(fd); + } + + if (reply == NULL || reply->strings.ga_data == NULL || + reply->strings.ga_len <= 0) + { + return FAIL; + } + + // Consume the string + *str = ((char_u **)reply->strings.ga_data)[0]; + + for (int i = 1; i < reply->strings.ga_len; i++) + { + ((char_u **)reply->strings.ga_data)[i - 1] = + ((char_u **)reply->strings.ga_data)[i]; + } + reply->strings.ga_len--; + + if (reply->strings.ga_len < 1) + // Last string removed, remove the reply + socket_server_remove_reply(client); + + + return OK; +} + +/* + * Check for any replies for "sender". Returns 1 if there is and places the + * reply in "str" without consuming it. Returns 0 if otherwise and -1 on + * error. + */ + int +socket_server_peek_reply(char_u *sender, char_u **str) +{ + ss_reply_T *reply; + + if (!socket_server_name_is_valid(sender)) + return -1; + + if (!socket_server_valid()) + return 0; + + reply = socket_server_get_reply(sender, NULL); + + if (reply != NULL && reply->strings.ga_len > 0) + { + if (str != NULL) + *str = ((char_u **)reply->strings.ga_data)[0]; + return 1; + } + + return 0; +} + +/* + * Send a string to "client" as a reply (notification). Returns OK on success + * and FAIL on failure. + */ + int +socket_server_send_reply(char_u *client, char_u *str) +{ + int socket_fd; + ss_cmd_T cmd; + size_t sz; + char_u *final; + + if (!socket_server_name_is_valid(client)) + return FAIL; + + if (!socket_server_valid()) + { + emsg(_(e_socket_server_not_online)); + return FAIL; + } + + socket_fd = socket_server_connect(client, NULL, TRUE); + + if (socket_fd == -1) + return FAIL; + + socket_server_init_cmd(&cmd, SS_CMD_TYPE_NOTIFY); + + socket_server_append_msg(&cmd, SS_MSG_TYPE_ENCODING, p_enc, STRLEN(p_enc)); + socket_server_append_msg(&cmd, SS_MSG_TYPE_STRING, str, STRLEN(str)); + socket_server_append_msg(&cmd, SS_MSG_TYPE_SENDER, + socket_server_path, STRLEN(socket_server_path)); + + final = socket_server_encode_cmd(&cmd, &sz); + + if (final == NULL || + socket_server_write(socket_fd, final, sz, 1000) == FAIL) + { + socket_server_free_cmd(&cmd); + close(socket_fd); + return FAIL; + } + + socket_server_free_cmd(&cmd); + vim_free(final); + close(socket_fd); + + return OK; +} + +/* + * Connect to a socket using "name". "path" is set to the full path of "name" + * used to create the socket, only if its not NULL. Returns fd on success and -1 + * on failure. + */ + static int +socket_server_connect(char_u *name, char_u **path, int silent) +{ + int socket_fd; + int res; + struct sockaddr_un addr; + + char_u *socket_path = socket_server_get_path_from_name(name); + + if (socket_path == NULL) + { + if (!silent) + semsg(_(e_no_registered_server_named_str), name); + return -1; + } + if (STRLEN(socket_path) >= sizeof(addr.sun_path)) + { + // Path too big + vim_free(socket_path); + return -1; + } + + socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + + if (socket_fd == -1) + goto fail; + + addr.sun_family = AF_UNIX; + vim_snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_path); + + res = connect(socket_fd, (struct sockaddr *)&addr, sizeof(addr)); + + if (res == -1) + { + if (!silent) + semsg(_(e_socket_server_failed_connecting), socket_path, + strerror(errno)); + goto fail; + } + + if (path != NULL) + *path = socket_path; + else + vim_free(socket_path); + + return socket_fd; +fail: + close(socket_fd); + vim_free(socket_path); + return -1; + +} + +/* + * Add a new pending command to the list of pending commands. Returns OK on + * success and FAIL on failure + */ + static void +socket_server_init_pending_cmd(ss_pending_cmd_T *pending) +{ + pending->code = 0; + pending->result = NULL; + pending->serial = ss_serial; + pending->next = ss_pending_cmds; + ss_pending_cmds = pending; +} + +/* + * Remove pending command from the list, does not free the result string. + */ + static void +socket_server_pop_pending_cmd(ss_pending_cmd_T *pending) +{ + if (ss_pending_cmds == pending) + { + ss_pending_cmds = pending->next; + return; + } + + for (ss_pending_cmd_T *cmd = ss_pending_cmds; cmd != NULL; cmd = cmd->next) + { + if (cmd->next == pending) + { + cmd->next = pending->next; + return; + } + } +} + +/* + * Initialize command structure to empty state + */ + static void +socket_server_init_cmd(ss_cmd_T *cmd, ss_cmd_type_T type) +{ + cmd->cmd_len = 0; + cmd->cmd_num = 0; + cmd->cmd_type = type; +} + +/* + * Append a message to a command. Note that "len" is the length of contents. + * Returns OK on sucess and FAIL on failure + */ + static int +socket_server_append_msg(ss_cmd_T *cmd, char_u type, char_u *contents, int len) +{ + ss_msg_T *msg = cmd->cmd_msgs + cmd->cmd_num; + + if (cmd->cmd_num >= SOCKET_SERVER_MAX_MSG) + return FAIL; + + // Check if command will be too big. + if (SS_CMD_INFO_SIZE + cmd->cmd_len + SS_MSG_INFO_SIZE + len + > SOCKET_SERVER_MAX_CMD_SIZE) + return FAIL; + + msg->msg_contents = alloc(len); + + if (msg->msg_contents == NULL) + return FAIL; + + msg->msg_type = type; + msg->msg_len = len; + memcpy(msg->msg_contents, contents, len); + + cmd->cmd_len += SS_MSG_INFO_SIZE + len; + cmd->cmd_num++; + + return OK; +} + +/* + * Free all resources associated with a command object. + */ + static void +socket_server_free_cmd(ss_cmd_T *cmd) +{ + for (uint32_t i = 0; i < cmd->cmd_num; i++) + { + ss_msg_T *msg = cmd->cmd_msgs + i; + + vim_free(msg->msg_contents); + } +} + +/* + * Encode command struct and return the final message to send. Returns NULL on + * failure. + */ + static char_u * +socket_server_encode_cmd(ss_cmd_T *cmd, size_t *sz) +{ + size_t size; + char_u *buf; + char_u *start; + + size = SS_CMD_INFO_SIZE + cmd->cmd_len; + buf = alloc(size); + + if (buf == NULL) + return NULL; + + start = buf; + memcpy(start, &cmd->cmd_type, sizeof(cmd->cmd_type)); + start += sizeof(cmd->cmd_type); + memcpy(start, &cmd->cmd_num, sizeof(cmd->cmd_num)); + start += sizeof(cmd->cmd_num); + memcpy(start, &cmd->cmd_len, sizeof(cmd->cmd_len)); + start += sizeof(cmd->cmd_len); + + // Append messages to buffer + for (uint32_t i = 0; i < cmd->cmd_num; i++) + { + ss_msg_T *msg = cmd->cmd_msgs + i; + + memcpy(start, &msg->msg_type, sizeof(msg->msg_type)); + start += sizeof(msg->msg_type); + memcpy(start, &msg->msg_len, sizeof(msg->msg_len)); + start += sizeof(msg->msg_len); + + memcpy(start, msg->msg_contents, msg->msg_len); + start += msg->msg_len; + } + + *sz = size; + + return buf; +} + +/* + * Read from "socket_fd" an entire command and return the result in "cmd". The + * socket fd should be at the start of the command. Returns OK on success and + * FAIL on failure. + */ + static int +socket_server_decode_cmd(ss_cmd_T *cmd, int socket_fd, int timeout) +{ + int got_cmd_info = FALSE; // Consists of type, num, and len + size_t total_r = 0; + char_u *buf; + char_u *cur; + struct timeval start, now; + + // We also poll the socket server listening file descriptor to handle + // recursive remote calls between Vim instances, such as when one Vim + // instance calls remote_expr for an expression that calls remote_expr to + // itself again. +#ifndef HAVE_SELECT + struct pollfd pfd; + + pfd.fd = socket_fd; + pfd.events = POLLIN; +#else + fd_set rfds; + struct timeval tv; + + FD_ZERO(&rfds); + FD_SET(socket_fd, &rfds); +#endif + + buf = alloc(SS_CMD_INFO_SIZE); + + if (buf == NULL) + return FAIL; + + // We may exit in the middle of the loop and free the messages, we don't + // want to free an uninitialized pointer. + memset(cmd, 0, sizeof(*cmd)); + + gettimeofday(&start, NULL); + + while (TRUE) + { + int ret; + ssize_t r = 0; + +#ifndef HAVE_SELECT + ret = poll(&pfd, 1, timeout); +#else + tv.tv_sec = 0; + tv.tv_usec = 500 * 1000; + ret = select(socket_fd + 1, &rfds, NULL, NULL, &tv); +#endif + if (ret < 0) + goto fail; + if (ret == 0) + goto continue_loop; + + // Get cmd info first so we know the total size of all messages, and + // can read it all in one go. + if (!got_cmd_info) + { + r = read(socket_fd, buf + total_r, SS_CMD_INFO_SIZE - total_r); + + if ((size_t)r >= SS_CMD_INFO_SIZE - total_r) + { + char_u *tmp; + + got_cmd_info = TRUE; + + memcpy(&cmd->cmd_type, buf, sizeof(cmd->cmd_type)); + memcpy(&cmd->cmd_num, buf + sizeof(cmd->cmd_type), + sizeof(cmd->cmd_num)); + memcpy(&cmd->cmd_len, + buf + sizeof(cmd->cmd_type) + sizeof(cmd->cmd_num), + sizeof(cmd->cmd_len)); + + if (cmd->cmd_num > SOCKET_SERVER_MAX_MSG) + // Too many messages to handle or invalid number + goto fail; + + if (cmd->cmd_num == 0) + // No messages to read + goto exit; + + // Now that we now the total size of messages, we can realloc + // the buffer to contain all data + tmp = vim_realloc(buf, SS_CMD_INFO_SIZE + cmd->cmd_len); + + if (tmp == NULL) + goto fail; + + buf = tmp; + cur = buf + SS_CMD_INFO_SIZE; + + continue; + } + } + else + { + // Read message data + r = read(socket_fd, cur + total_r, cmd->cmd_len - total_r); + + if ((size_t)r >= cmd->cmd_len - total_r) + break; + } + + if (r == -1 || r == 0) + goto fail; + + total_r += r; + +continue_loop: + gettimeofday(&now, NULL); + + if ((now.tv_sec * 1000000 + now.tv_usec) - + (start.tv_sec * 1000000 + start.tv_usec) >= timeout * 1000) + goto fail; + } + + // Parse message data + for (uint32_t i = 0; i < cmd->cmd_num; i++) + { + ss_msg_T *msg = cmd->cmd_msgs + i; + + memcpy(&msg->msg_type, cur, sizeof(msg->msg_type)); + cur += sizeof(msg->msg_type); + memcpy(&msg->msg_len, cur, sizeof(msg->msg_len)); + cur += sizeof(msg->msg_len); + + msg->msg_contents = alloc(msg->msg_len + 1); + + if (msg->msg_contents == NULL) + goto fail; + + memcpy(msg->msg_contents, cur, msg->msg_len); + msg->msg_contents[msg->msg_len] = 0; // NULL terminate it + + // Move pointer to start of next message + cur += msg->msg_len; + } + +exit: + vim_free(buf); + return OK; +fail: + socket_server_free_cmd(cmd); + vim_free(buf); + return FAIL; +} + +/* + * Low level function that writes to a socket with a timeout in milliseconds. + * Returns OK on success and FAIL on failure. + */ + static int +socket_server_write(int socket_fd, char_u *data, size_t sz, int timeout) +{ + char_u *cur = data; + size_t total_w = 0; + struct timeval start, now; +#ifndef HAVE_SELECT + struct pollfd pfd; + + pfd.fd = socket_fd; + pfd.events = POLLOUT; +#else + fd_set wfds; + struct timeval tv; + + FD_ZERO(&wfds); + FD_SET(socket_fd, &wfds); +#endif + + gettimeofday(&start, NULL); + + while (total_w < sz) + { + int ret; + ssize_t written; + + errno = 0; +#ifndef HAVE_SELECT + ret = poll(&pfd, 1, timeout); +#else + tv.tv_sec = 0; + tv.tv_usec = 500 * 1000; + ret = select(socket_fd + 1, NULL, &wfds, NULL, &tv); +#endif + if (ret < 0) + return FAIL; + else if (ret == 0) + goto continue_loop; + + written = write(socket_fd, cur, sz - total_w); + + if (written == -1) + return FAIL; + + total_w += written; + + +continue_loop: + gettimeofday(&now, NULL); + + if ((now.tv_sec * 1000000 + now.tv_usec) - + (start.tv_sec * 1000000 + start.tv_usec) >= timeout * 1000) + return FAIL; + } + + return OK; +} + + static ss_reply_T * +socket_server_get_reply(char_u *sender, int *index) +{ + for (int i = 0; i < ss_replies.ga_len; i++) + { + ss_reply_T *reply = ((ss_reply_T *)ss_replies.ga_data) + i; + + if (STRCMP(reply->sender, sender) == 0) + { + if (index != NULL) + *index = i; + return reply; + } + } + return NULL; +} + +/* + * Add reply to list of replies. Returns a pointer to the ss_reply_T that was + * initialized or was found. + */ + static ss_reply_T * +socket_server_add_reply(char_u *sender) +{ + ss_reply_T *reply; + + if (ss_replies.ga_growsize == 0) + ga_init2(&ss_replies, sizeof(ss_reply_T), 1); + + reply = socket_server_get_reply(sender, NULL); + + if (reply == NULL && ga_grow(&ss_replies, 1) == OK) + { + reply = ((ss_reply_T *)ss_replies.ga_data) + ss_replies.ga_len++; + + reply->sender = vim_strsave(sender); + + if (reply->sender == NULL) + return NULL; + + ga_init2(&reply->strings, sizeof(char_u *), 5); + } + + return reply; +} + + static void +socket_server_remove_reply(char_u *sender) +{ + int index; + ss_reply_T *reply = socket_server_get_reply(sender, &index); + + if (reply != NULL) + { + ss_reply_T *arr = ss_replies.ga_data; + + // Free strings + vim_free(reply->sender); + ga_clear_strings(&reply->strings); + + // Move all elements after the removed reply forward by one + for (int i = index + 1; i < ss_replies.ga_len; i++) + arr[i - 1] = arr[i]; + ss_replies.ga_len--; + } +} + +/* + * Execute the actions given by command. "fd" is the socket of the client that + * sent the command. + */ + static void +socket_server_exec_cmd(ss_cmd_T *cmd, int fd) +{ + char_u *str = NULL; + char_u *enc = NULL; + char_u *sender = NULL; + uint32_t serial = 0; + char_u rcode = 0; + char_u *to_free; + char_u *to_free2; + + for (uint32_t i = 0; i < cmd->cmd_num; i++) + { + ss_msg_T *msg = cmd->cmd_msgs + i; + + if (msg->msg_type == SS_MSG_TYPE_STRING) + str = msg->msg_contents; + if (msg->msg_type == SS_MSG_TYPE_ENCODING) + enc = msg->msg_contents; + if (msg->msg_type == SS_MSG_TYPE_SERIAL) + memcpy(&serial, msg->msg_contents, sizeof(serial)); + if (msg->msg_type == SS_MSG_TYPE_CODE) + memcpy(&rcode, msg->msg_contents, sizeof(rcode)); + else if (msg->msg_type == SS_MSG_TYPE_SENDER) + { + sender = msg->msg_contents; + + // Save in global + vim_free(client_socket); + client_socket = vim_strsave(sender); + } + } + +#ifdef FEAT_EVAL + ch_log(NULL, "socket_server_exec_cmd(): encoding: %s, result: %s", + enc == NULL ? (char_u *)"(null)" : enc, + str == NULL ? (char_u *)"(null)" : str); +#endif + + if (cmd->cmd_type == SS_CMD_TYPE_EXPR || + cmd->cmd_type == SS_CMD_TYPE_KEYSTROKES) + { + // Either an expression or keystrokes. + if (socket_server_valid() && enc != NULL) + { + str = serverConvert(enc, str, &to_free); + + if (cmd->cmd_type == SS_CMD_TYPE_KEYSTROKES) + server_to_input_buf(str); + else if (sender != NULL) + { + // Evaluate expression and send reply containing result + char_u *result; + size_t sz; + char_u *buf; + char_u code; + + result = eval_client_expr_to_string(str); + + code = result == NULL ? -1 : 0; + + // Send reply + ss_cmd_T rcmd; + + socket_server_init_cmd(&rcmd, SS_CMD_TYPE_REPLY); + + // Don't care about errors, server will just ignore command if + // its missing something. + if (result != NULL) + socket_server_append_msg(&rcmd, SS_MSG_TYPE_STRING, result, + STRLEN(result) + 1); // We add +1 in case "result" + // is an empty string. + else + // An error occured, return an error msg instead + socket_server_append_msg(&rcmd, SS_MSG_TYPE_STRING, + (char_u *)_(e_invalid_expression_received), + STRLEN(e_invalid_expression_received)); + + socket_server_append_msg(&rcmd, SS_MSG_TYPE_CODE, + &code, sizeof(code)); + + socket_server_append_msg(&rcmd, SS_MSG_TYPE_ENCODING, p_enc, + STRLEN(p_enc)); + + socket_server_append_msg(&rcmd, SS_MSG_TYPE_SERIAL, + (char_u *)&serial, sizeof(serial)); + + buf = socket_server_encode_cmd(&rcmd, &sz); + + if (buf != NULL) + { + int fd2 = socket_server_connect(sender, NULL, TRUE); + + if (fd2 >= 0) + socket_server_write(fd2, buf, sz, 1000); + vim_free(buf); + close(fd2); + } + + socket_server_free_cmd(&rcmd); + vim_free(result); + } + vim_free(to_free); + } + return; + } + else if (cmd->cmd_type == SS_CMD_TYPE_REPLY) + { + // A reply from a previous command we set up, update the corresponding + // pending command. + if (serial > 0 && str != NULL) + { + for (ss_pending_cmd_T *pending = ss_pending_cmds; pending != NULL; + pending = pending->next) + { + if (serial == pending->serial && pending->result == NULL) + { + str = serverConvert(enc, str, &to_free); + + pending->code = rcode; + + if (to_free == NULL) + pending->result = vim_strsave(str); + else + pending->result = str; + break; + } + } + } + return; + } + else if (cmd->cmd_type == SS_CMD_TYPE_NOTIFY) + { + // Notification, execute autocommands and save the reply for later use + if (sender != NULL && str != NULL && enc != NULL) + { + ss_reply_T *reply; + + str = serverConvert(enc, str, &to_free); + sender = serverConvert(enc, sender, &to_free2); + + reply = socket_server_add_reply(sender); + + if (reply != NULL) + ga_copy_string(&reply->strings, str); + + apply_autocmds(EVENT_REMOTEREPLY, sender, str, TRUE, curbuf); + + vim_free(to_free); + vim_free(to_free2); + } + return; + } + else if (cmd->cmd_type == SS_CMD_TYPE_ALIVE) + { + // Client wants to check if we are still responsive, send back a single + // byte as a YES. + char_u buf[1] = {1}; +#ifndef HAVE_SELECT + struct pollfd pfd; + + pfd.fd = fd; + pfd.events = POLLIN; +#else + fd_set rfds; + struct timeval tv; + + FD_ZERO(&rfds); + FD_SET(fd, &rfds); +#endif + + if (write(fd, buf, 1) == -1) + return; + + // Poll until client closes their end + +#ifndef HAVE_SELECT + poll(&pfd, 1, 1000); +#else + tv.tv_sec = 1; + tv.tv_usec = 0; + select(fd + 1, &rfds, NULL, NULL, &tv); +#endif + return; + } + + // Command type is invalid, do nothing + return; +} + +/* + * Poll the socket server fd until a new connection is accepted. Returns 0 on + * success, 1 if it timed out or if poll returned empty, and -1 on error. + */ + static int +socket_server_dispatch(int timeout) +{ + int ret; +#ifndef HAVE_SELECT + struct pollfd pfd; + + pfd.fd = socket_server_fd; + pfd.events = POLLIN; +#else + fd_set rfds; + fd_set efds; + struct timeval tv; + + FD_ZERO(&rfds); + FD_ZERO(&efds); + FD_SET(socket_server_fd, &rfds); + FD_SET(socket_server_fd, &efds); +#endif + +#ifndef HAVE_SELECT + ret = poll(&pfd, 1, timeout); +#else + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + ret = select(socket_server_fd + 1, &rfds, NULL, &efds, &tv); +#endif + + if (ret < 0) + return -1; + else if (ret == 0) + return 1; + +#ifndef HAVE_SELECT + if (pfd.revents & POLLIN) +#else + if (FD_ISSET(socket_server_fd, &rfds)) +#endif + { + socket_server_accept_client(); + return 0; + } +#ifndef HAVE_SELECT + else if (pfd.revents & (POLLHUP | POLLERR)) +#else + else if (FD_ISSET(socket_server_fd, &efds)) +#endif + // Connection was closed + return -1; + else + return 1; + + return -1; +} + +/* + * Check if socket "name" is reponsive by sending an ALIVE command. This does + * not require the socket server to be active. + */ + static int +socket_server_check_alive(char_u *name) +{ + int socket_fd; + int ret; + size_t sz; + char_u *final; + char_u buf[1] = {0}; +#ifndef HAVE_SELECT + struct pollfd pfd; +#else + fd_set rfds; + struct timeval tv; +#endif + + socket_fd = socket_server_connect(name, NULL, TRUE); + + if (socket_fd == -1) + return FALSE; + +#ifndef HAVE_SELECT + pfd.fd = socket_fd; + pfd.events = POLLIN; +#else + FD_ZERO(&rfds); + FD_SET(socket_fd, &rfds); +#endif + + ss_cmd_T cmd; + + socket_server_init_cmd(&cmd, SS_CMD_TYPE_ALIVE); + + final = socket_server_encode_cmd(&cmd, &sz); + + if (final == NULL || + socket_server_write(socket_fd, final, sz, 1000) == FAIL) + { + vim_free(final); + close(socket_fd); + return FALSE; + } + vim_free(final); + + // Poll for response +#ifndef HAVE_SELECT + ret = poll(&pfd, 1, 1000); +#else + tv.tv_sec = 1; + tv.tv_usec = 0; + ret = select(socket_fd + 1, &rfds, NULL, NULL, &tv); +#endif + + if (ret > 0) + if (read(socket_fd, buf, 1) == -1) + { + close(socket_fd); + return FALSE; + } + + close(socket_fd); + return buf[0] == 1; +} + +/* + * Get file descriptor of listening socket + */ + int +socket_server_get_fd(void) +{ + return socket_server_fd; +} + + +/* + * Check if socket name is a valid name + */ + static int +socket_server_name_is_valid(char_u *name) +{ + if (STRLEN(name) == 0 || (name[0] != '/' && vim_strchr(name, '/') != NULL)) + { + semsg(_(e_invalid_server_id_used_str), name); + return FALSE; + } + return TRUE; +} + +/* + * Returns TRUE if there are clients queued in the listening socket waiting to + * be accepted + */ + int +socket_server_waiting_accept(void) +{ + int ret; +#ifndef HAVE_SELECT + struct pollfd pfd; + + pfd.fd = socket_server_fd; + pfd.events = POLLIN; + + ret = poll(&pfd, 1, 0); + + if (ret > 0 && pfd.revents & POLLIN) + return TRUE; +#else + fd_set rfds; + struct timeval tv; + + if (socket_server_fd == -1) + return FALSE; + + FD_ZERO(&rfds); + FD_SET(socket_server_fd, &rfds); + + tv.tv_sec = 0; + tv.tv_usec = 0; + ret = select(socket_server_fd + 1, &rfds, NULL, NULL, &tv); + + if (ret > 0 && FD_ISSET(socket_server_fd, &rfds)) + return TRUE; +#endif + + return FALSE; +} + +#endif // FEAT_SOCKETSERVER diff --git a/src/po/vim.pot b/src/po/vim.pot index b77bd8fc49..0982f92458 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Vim\n" "Report-Msgid-Bugs-To: vim-dev@vim.org\n" -"POT-Creation-Date: 2025-08-16 17:57+0200\n" +"POT-Creation-Date: 2025-08-18 21:30+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -254,6 +254,9 @@ msgstr "" msgid "%d of %d edited" msgstr "" +msgid "Socket server not online:Send expression failed" +msgstr "" + msgid "No display: Send expression failed.\n" msgstr "" @@ -1711,6 +1714,9 @@ msgstr "" msgid "-Y\t\t\tDo not connect to Wayland compositor" msgstr "" +msgid "--clientserver Backend for clientserver communication" +msgstr "" + msgid "--remote \tEdit in a Vim server if possible" msgstr "" @@ -2507,6 +2513,10 @@ msgstr "" msgid "XSMP SmcOpenConnection failed: %s" msgstr "" +#, c-format +msgid "Failed creating socket directory: %s" +msgstr "" + msgid "At line" msgstr "" @@ -8806,6 +8816,22 @@ msgstr "" msgid "E1562: Diff anchors cannot be used with hidden diff windows" msgstr "" +msgid "E1563: Socket path is too big" +msgstr "" + +msgid "E1564: Socket name cannot have slashes in it without being a path" +msgstr "" + +msgid "E1565: Socket server is not online, call remote_startserver() first" +msgstr "" + +#, c-format +msgid "E1566: Failed connecting to socket %s: %s" +msgstr "" + +msgid "E1567: Cannot start socket server, socket path is unavailable" +msgstr "" + #. type of cmdline window or 0 #. result of cmdline window or 0 #. buffer of cmdline window or NULL diff --git a/src/proto/gui_gtk_x11.pro b/src/proto/gui_gtk_x11.pro index d019cf0a3f..5e7452d4d1 100644 --- a/src/proto/gui_gtk_x11.pro +++ b/src/proto/gui_gtk_x11.pro @@ -8,6 +8,8 @@ void gui_mch_stop_blink(int may_call_gui_update_cursor); void gui_mch_start_blink(void); int gui_mch_early_init_check(int give_message); int gui_mch_init_check(void); +void gui_gtk_init_socket_server(void); +void gui_gtk_uninit_socket_server(void); void gui_mch_set_dark_theme(int dark); void gui_mch_show_tabline(int showit); int gui_mch_showing_tabline(void); diff --git a/src/proto/option.pro b/src/proto/option.pro index f1f8a33a46..8e544184a2 100644 --- a/src/proto/option.pro +++ b/src/proto/option.pro @@ -68,6 +68,7 @@ char *did_set_shellslash(optset_T *args); char *did_set_shiftwidth_tabstop(optset_T *args); char *did_set_showtabline(optset_T *args); char *did_set_smoothscroll(optset_T *args); +char *did_set_socktimeoutlen(optset_T *args); char *did_set_spell(optset_T *args); char *did_set_swapfile(optset_T *args); char *did_set_termguicolors(optset_T *args); diff --git a/src/proto/optionstr.pro b/src/proto/optionstr.pro index 734959b60b..4da92f77c9 100644 --- a/src/proto/optionstr.pro +++ b/src/proto/optionstr.pro @@ -34,6 +34,8 @@ int expand_set_casemap(optexpand_T *args, int *numMatches, char_u ***matches); int expand_set_clipboard(optexpand_T *args, int *numMatches, char_u ***matches); char *did_set_clipmethod(optset_T *args); int expand_set_clipmethod(optexpand_T *args, int *numMatches, char_u ***matches); +char *did_set_clientserver(optset_T *args UNUSED); +int expand_set_clientserver(optexpand_T *args, int *numMatches, char_u ***matches); char *did_set_chars_option(optset_T *args); int expand_set_chars_option(optexpand_T *args, int *numMatches, char_u ***matches); char *did_set_cinoptions(optset_T *args); diff --git a/src/proto/os_unix.pro b/src/proto/os_unix.pro index ae0cdbba41..329e1dbbdd 100644 --- a/src/proto/os_unix.pro +++ b/src/proto/os_unix.pro @@ -94,4 +94,15 @@ void stop_timeout(void); volatile sig_atomic_t *start_timeout(long msec); void delete_timer(void); int mch_create_anon_file(void); +int socket_server_init(char_u *sock_path); +void socket_server_uninit(void); +char_u *socket_server_list_sockets(void); +void socket_server_accept_client(void); +int socket_server_valid(void); +int socket_server_send(char_u *sock_path, char_u *cmd, char_u **result, char_u **receiver, int is_expr, int timeout, int silent); +int socket_server_read_reply(char_u *sender, char_u **str, int timeout); +int socket_server_peek_reply(char_u *sender, char_u **str); +int socket_server_send_reply(char_u *client, char_u *str); +int socket_server_get_fd(void); +int socket_server_waiting_accept(void); /* vim: set ft=c : */ diff --git a/src/testdir/test_clientserver.vim b/src/testdir/test_clientserver.vim index 6fe0f69693..4bda0caef7 100644 --- a/src/testdir/test_clientserver.vim +++ b/src/testdir/test_clientserver.vim @@ -10,6 +10,12 @@ CheckFeature clientserver source util/shared.vim +" Unlike X11, we need the socket server running if we want to send commands to +" a server via sockets. +if v:servername == "" + call remote_startserver('VIMSOCKETSERVERTEST') +endif + func Check_X11_Connection() if has('x11') CheckX11 @@ -184,10 +190,18 @@ func Test_client_server() call assert_fails('call remote_startserver("")', 'E1175:') call assert_fails('call remote_startserver([])', 'E1174:') call assert_fails("let x = remote_peek([])", 'E730:') - call assert_fails("let x = remote_read('vim10')", - \ has('unix') ? ['E573:.*vim10'] : 'E277:') - call assert_fails("call server2client('abc', 'xyz')", - \ has('unix') ? ['E573:.*abc'] : 'E258:') + + " When using socket server, server id is not a number, but the path to the + " socket. + if has('socketserver') && !has('X11') + call assert_fails("let x = remote_read('vim/10')", ['E573:.*vim/10']) + call assert_fails("call server2client('a/b/c', 'xyz')", ['E573:.*a/b/c']) + else + call assert_fails("let x = remote_read('vim10')", + \ has('unix') ? ['E573:.*vim10'] : 'E277:') + call assert_fails("call server2client('abc', 'xyz')", + \ has('unix') ? ['E573:.*abc'] : 'E258:') + endif endfunc func Test_client_server_stopinsert() @@ -231,6 +245,121 @@ func Test_client_server_stopinsert() endtry endfunc +" Test if socket server and X11 backends can be chosen and work properly. +func Test_client_server_x11_and_socket_server() + CheckNotMSWindows + CheckFeature socketserver + CheckFeature x11 + + let g:test_is_flaky = 1 + let cmd = GetVimCommand() + + if cmd == '' + throw 'GetVimCommand() failed' + endif + call Check_X11_Connection() + + let types = ['socket', 'x11'] + + for type in types + let name = 'VIMTEST_' .. toupper(type) + let actual_cmd = cmd .. ' --clientserver ' .. type + let actual_cmd .= ' --servername ' .. name + let job = job_start(actual_cmd, {'stoponexit': 'kill', 'out_io': 'null'}) + + call WaitForAssert({-> assert_equal("run", job_status(job))}) + call WaitForAssert({-> assert_match(name, system(cmd .. ' --clientserver ' .. type .. ' --serverlist'))}) + + call assert_match(name, system(actual_cmd .. ' --remote-expr "v:servername"')) + + call system(actual_cmd .. " --remote-expr 'execute(\"qa!\")'") + try + call WaitForAssert({-> assert_equal("dead", job_status(job))}) + finally + if job_status(job) != 'dead' + call assert_report('Server did not exit') + call job_stop(job, 'kill') + endif + endtry + endfor +endfunc + +" Test if socket server works in the GUI +func Test_client_socket_server_server_gui() + CheckNotMSWindows + CheckFeature socketserver + CheckFeature gui_gtk + + let g:test_is_flaky = 1 + let cmd = GetVimCommand() + + if cmd == '' + throw 'GetVimCommand() failed' + endif + call Check_X11_Connection() + + let name = 'VIMTESTSOCKET' + let cmd .= ' --clientserver socket' + let cmd .= ' --servername ' .. name + + let job = job_start(cmd, {'stoponexit': 'kill', 'out_io': 'null'}) + + call WaitForAssert({-> assert_equal("run", job_status(job))}) + call WaitForAssert({-> assert_match(name, system(cmd .. ' --serverlist'))}) + + call system(cmd .. " --remote-expr 'execute(\"gui\")'") + + call assert_match('1', system(cmd .. " --remote-expr 'has(\"gui_running\")'")) + call assert_match(name, system(cmd .. ' --remote-expr "v:servername"')) + + call system(cmd .. " --remote-expr 'execute(\"qa!\")'") + try + call WaitForAssert({-> assert_equal("dead", job_status(job))}) + finally + if job_status(job) != 'dead' + call assert_report('Server did not exit') + call job_stop(job, 'kill') + endif + endtry +endfunc + +" Test if custom paths work for socketserver +func Test_client_socket_server_custom_path() + CheckNotMSWindows + CheckFeature socketserver + CheckNotFeature x11 + + let g:test_is_flaky = 1 + let cmd = GetVimCommand() + + if cmd == '' + throw 'GetVimCommand() failed' + endif + + let name = 'VIMTESTSOCKET2' + + let paths = ['./' .. name, '../testdir/' .. name, getcwd(-1) .. '/' .. name] + + for path in paths + let actual = cmd .. ' --servername ' .. path + + let job = job_start(actual, {'stoponexit': 'kill', 'out_io': 'null'}) + + call WaitForAssert({-> assert_equal("run", job_status(job))}) + call WaitForAssert({-> assert_equal(path, glob(path))}) + + call system(actual .. " --remote-expr 'execute(\"qa!\")'") + try + call WaitForAssert({-> assert_equal("dead", job_status(job))}) + finally + if job_status(job) != 'dead' + call assert_report('Server did not exit') + call job_stop(job, 'kill') + endif + endtry + endfor +endfunc + " Uncomment this line to get a debugging log " call ch_logfile('channellog', 'w') diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim index 40cc15f1a2..eec62536f0 100644 --- a/src/testdir/test_vim9_builtin.vim +++ b/src/testdir/test_vim9_builtin.vim @@ -3,6 +3,11 @@ source util/screendump.vim import './util/vim9.vim' as v9 +" Socket backend for remote functions require the socket server to be running +if v:servername == "" + call remote_startserver('VIMSOCKETSERVERTEST') +endif + " Test for passing too many or too few arguments to builtin functions func Test_internalfunc_arg_error() let l =<< trim END diff --git a/src/testdir/test_wayland.vim b/src/testdir/test_wayland.vim index b60934c370..3308b2a936 100644 --- a/src/testdir/test_wayland.vim +++ b/src/testdir/test_wayland.vim @@ -79,6 +79,10 @@ func Test_wayland_startup() call s:PreTest() call s:CheckXConnection() + if v:servername == "" + call remote_startserver('VIMSOCKETSERVER') + endif + let l:name = 'WLVIMTEST' let l:cmd = GetVimCommand() .. ' --servername ' .. l:name let l:job = job_start(cmd, {'stoponexit': 'kill', 'out_io': 'null'}) @@ -372,6 +376,10 @@ func Test_wayland_autoselect_works() call writefile(l:lines, 'Wltester', 'D') + if v:servername == "" + call remote_startserver('VIMSOCKETSERVER') + endif + let l:name = 'WLVIMTEST' let l:cmd = GetVimCommand() .. ' -S Wltester --servername ' .. l:name let l:job = job_start(cmd, {'stoponexit': 'kill', 'out_io': 'null'}) @@ -415,6 +423,10 @@ func Test_no_wayland_connect_cmd_flag() call s:PreTest() call s:CheckXConnection() + if v:servername == "" + call remote_startserver('VIMSOCKETSERVER') + endif + let l:name = 'WLFLAGVIMTEST' let l:cmd = GetVimCommand() .. ' -Y --servername ' .. l:name let l:job = job_start(cmd, {'stoponexit': 'kill', 'out_io': 'null'}) @@ -453,6 +465,10 @@ func Test_wayland_become_inactive() call s:PreTest() call s:CheckXConnection() + if v:servername == "" + call remote_startserver('VIMSOCKETSERVER') + endif + let l:name = 'WLLOSEVIMTEST' let l:cmd = GetVimCommand() .. ' --servername ' .. l:name let l:job = job_start(cmd, { @@ -544,6 +560,10 @@ func Test_wayland_bad_environment() let l:old = $XDG_RUNTIME_DIR unlet $XDG_RUNTIME_DIR + if v:servername == "" + call remote_startserver('VIMSOCKETSERVER') + endif + let l:name = 'WLVIMTEST' let l:cmd = GetVimCommand() .. ' --servername ' .. l:name let l:job = job_start(cmd, { diff --git a/src/ui.c b/src/ui.c index ea9125280d..45a42434e7 100644 --- a/src/ui.c +++ b/src/ui.c @@ -407,7 +407,19 @@ inchar_loop( if ((resize_func != NULL && resize_func(TRUE)) #if defined(FEAT_CLIENTSERVER) && defined(UNIX) - || server_waiting() + || ( +# ifdef FEAT_X11 + (clientserver_method == CLIENTSERVER_METHOD_X11 && + server_waiting()) +# endif +# if defined(FEAT_X11) && defined(FEAT_SOCKETSERVER) + || +# endif +# ifdef FEAT_SOCKETSERVER + (clientserver_method == CLIENTSERVER_METHOD_SOCKET && + socket_server_waiting_accept()) +# endif + ) #endif #ifdef MESSAGE_QUEUE || interrupted diff --git a/src/version.c b/src/version.c index a50755f53f..f04f283d82 100644 --- a/src/version.c +++ b/src/version.c @@ -516,6 +516,11 @@ static char *(features[]) = "-signs", #endif "+smartindent", +#ifdef FEAT_SOCKETSERVER + "+socketserver", +#else + "-socketserver", +#endif #ifdef FEAT_SODIUM # ifdef DYNAMIC_SODIUM "+sodium/dyn", @@ -719,6 +724,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 1651, /**/ 1650, /**/