From e9c793bebcad2b585b3f5d25ca6108bcb48772fe Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Fri, 22 May 2026 18:30:52 +0000 Subject: [PATCH] patch 9.2.0512: clientserver uses binary protocol Problem: clientserver feature uses binary protocol and is hard to understand Solution: Rewrite the code based on channels and JSON messages (Foxe Chen). closes: #19782 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- Filelist | 2 + runtime/doc/remote.txt | 79 +- runtime/doc/tags | 3 +- runtime/doc/version9.txt | 3 +- runtime/doc/vim.1 | 7 +- runtime/doc/vim.man | 7 +- src/Make_cyg_ming.mak | 1 + src/Make_mvc.mak | 4 + src/Makefile | 11 + src/auto/configure | 32 - src/channel.c | 132 +- src/clientserver.c | 366 ++--- src/configure.ac | 16 - src/errors.h | 12 +- src/feature.h | 22 +- src/gc.c | 3 + src/getchar.c | 3 + src/globals.h | 19 +- src/gui.c | 6 - src/gui_gtk_x11.c | 55 - src/json.c | 2 + src/main.c | 19 +- src/os_unix.c | 1735 +------------------- src/po/vim.pot | 26 +- src/proto.h | 3 + src/proto/channel.pro | 10 +- src/proto/gui_gtk_x11.pro | 2 - src/proto/os_unix.pro | 11 - src/proto/socketserver.pro | 11 + src/socketserver.c | 1404 ++++++++++++++++ src/structs.h | 7 + src/testdir/dumps/Test_clientserver_1.dump | 8 + src/testdir/test_clientserver.vim | 307 +++- src/testdir/test_remote.vim | 20 +- src/testdir/test_usercommands.vim | 1 - src/testdir/test_vim9_builtin.vim | 31 +- src/testdir/util/check.vim | 11 - src/testdir/util/socketserver.vim | 17 - src/ui.c | 18 +- src/version.c | 2 + 40 files changed, 2128 insertions(+), 2300 deletions(-) create mode 100644 src/proto/socketserver.pro create mode 100644 src/socketserver.c create mode 100644 src/testdir/dumps/Test_clientserver_1.dump delete mode 100644 src/testdir/util/socketserver.vim diff --git a/Filelist b/Filelist index dfc828a759..4ea946c841 100644 --- a/Filelist +++ b/Filelist @@ -145,6 +145,7 @@ SRC_ALL = \ src/session.c \ src/sha256.c \ src/sign.c \ + src/socketserver.c \ src/sound.c \ src/spell.c \ src/spell.h \ @@ -344,6 +345,7 @@ SRC_ALL = \ src/proto/session.pro \ src/proto/sha256.pro \ src/proto/sign.pro \ + src/proto/socketserver.pro \ src/proto/sound.pro \ src/proto/spell.pro \ src/proto/spellfile.pro \ diff --git a/runtime/doc/remote.txt b/runtime/doc/remote.txt index 2f77fcaa57..260fc864f6 100644 --- a/runtime/doc/remote.txt +++ b/runtime/doc/remote.txt @@ -79,9 +79,10 @@ The following command line arguments are available: *--clientserver* --clientserver {method} Use the specified method {method} as the backend for clientserver functionality. Can - either be "socket" or "x11". + either be "socket", "x11", or "mswin". {only available when compiled with both |+X11| - and |+socketserver| features} + and |+socketserver| features, or + |+socketserver| on MS-Windows} Examples ~ @@ -220,12 +221,16 @@ When using gvim, the --remote-wait only works properly this way: > < ============================================================================== 4. Socket server specific items *socketserver-clientserver* - *E1563* *E1564* *E1565* *E1566* *E1567* + *E1564* *E1565* *E1566* -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 +NOTE: Vim version before patch 9.2.511 use a different socketserver backend +which is incompatible with the new one, which is based on channels and JSON. + +The communication between client and server is done using channels internally. +When using the traditional/generic clientserver naming (only available on +Unix), sockets are placed in these directories in the following order of availability: - 1. "$XDG_RUTIME_DIR/vim" if $XDG_RUNTIME_DIR is set in the environment. + 1. "$XDG_RUNTIME_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. @@ -237,45 +242,55 @@ absolute or relative path. If the server id starts with either a "/" 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. +be invalid. This feature is only available on Unix. -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. + *socketserver-address* +In addition, the socketserver can also be created as a |channel-address|. To +do this, prefix the address with "channel:" (which will be ignored). This is +available on both Unix and MS-Windows, and is the only available naming for the +socketserver backend on Windows. However, note that |ch_listen()| +restrictions apply, meaning only port numbers can be used for TCP sockets. + +If the name is prefixed with "name:", then the legacy servername behaviour is +used, so "name:VIM" is the same as "VIM". + +Unless Vim is compiled with |+autoservername| feature, the socket server will +have to started explicitly, similar to X11. This is unless a X11 GUI Vim is +being used. 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. +new name. This is only relevant for Unix. *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. +Most of the functionality is the same as the other clientserver backends. -Additionally, the server id or client id will not be a number like X11 or +However, 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 socket. This can be seen via the |v:servername| variable. If the name is +a channel address, then it will be the address with the "a/" prefix as well. -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. +Note when using |--remote-wait|, the client id will look like "t/". This naming is specifically for this command, and attempting to use +it as an address will fail. -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. +If on Unix, 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 on MS-Windows, then the |--serverlist| argument will output nothing. + +The |+socketserver| feature is automatically enabled when on Unix or +MS-Windows, and the |+channel| feature is enabled *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. +If Vim is compiled with |+socketserver| and another clientserver backend, then +deciding which backend to use is done at startup time, via the +|--clientserver| argument. By default if it is not specified, then the native +backend (X11 or MS-Window messages) will be used, if available. A Vim +instance using a socket server cannot communicate with one using another +clientserver backend. vim:tw=78:sw=4:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/tags b/runtime/doc/tags index 7a4c09031b..9c0298aedd 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -4762,11 +4762,9 @@ 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* E1568 options.txt /*E1568* E1569 builtin.txt /*E1569* E157 sign.txt /*E157* @@ -10377,6 +10375,7 @@ 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-address remote.txt /*socketserver-address* socketserver-clientserver remote.txt /*socketserver-clientserver* socketserver-differences remote.txt /*socketserver-differences* socketserver-name remote.txt /*socketserver-name* diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index 1e7d1c7135..4f7b9b62da 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -1,4 +1,4 @@ -*version9.txt* For Vim version 9.2. Last change: 2026 May 19 +*version9.txt* For Vim version 9.2. Last change: 2026 May 22 VIM REFERENCE MANUAL by Bram Moolenaar @@ -52665,6 +52665,7 @@ Changed ~ if needed, see |wayland-primary-selection| for an example. - On Unix, filename completion for single-file Ex commands now treats embedded whitespace as part of the filename, like on other platforms. +- Rewrite the clientserver socketserver backend to use channels and JSON. *added-9.3* diff --git a/runtime/doc/vim.1 b/runtime/doc/vim.1 index cf9a080482..cb1d3fc762 100644 --- a/runtime/doc/vim.1 +++ b/runtime/doc/vim.1 @@ -504,9 +504,10 @@ 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 +Use {backend} as the backend for clientserver functionality, either "socket", +"x11", or "mswin" respectively. Only available when compiled with both +socketserver and X11 features present, or if compiled with socketserver on +MS-Windows. .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 9a5881b3db..ec4058f913 100644 --- a/runtime/doc/vim.man +++ b/runtime/doc/vim.man @@ -385,9 +385,10 @@ OPTIONS --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 + ity, either "socket", "x11", or "mswin" respectively. Only + available when compiled with both socketserver and X11 fea‐ + tures present, or if compiled with socketserver on MS-Win‐ + dows. --socketid {id} GTK GUI only: Use the GtkPlug mechanism to run gVim in an‐ diff --git a/src/Make_cyg_ming.mak b/src/Make_cyg_ming.mak index 7f3d519bf1..24db4ccfff 100644 --- a/src/Make_cyg_ming.mak +++ b/src/Make_cyg_ming.mak @@ -877,6 +877,7 @@ OBJ = \ $(OUTDIR)/session.o \ $(OUTDIR)/sha256.o \ $(OUTDIR)/sign.o \ + $(OUTDIR)/socketserver.o \ $(OUTDIR)/spell.o \ $(OUTDIR)/spellfile.o \ $(OUTDIR)/spellsuggest.o \ diff --git a/src/Make_mvc.mak b/src/Make_mvc.mak index 8994143c92..3b9ccb98f8 100644 --- a/src/Make_mvc.mak +++ b/src/Make_mvc.mak @@ -772,6 +772,7 @@ OBJ = \ $(OUTDIR)\session.obj \ $(OUTDIR)\sha256.obj \ $(OUTDIR)\sign.obj \ + $(OUTDIR)\socketserver.obj \ $(OUTDIR)\spell.obj \ $(OUTDIR)\spellfile.obj \ $(OUTDIR)\spellsuggest.obj \ @@ -1767,6 +1768,8 @@ $(OUTDIR)/sha256.obj: $(OUTDIR) sha256.c $(INCL) $(OUTDIR)/sign.obj: $(OUTDIR) sign.c $(INCL) +$(OUTDIR)/socketserver.obj: $(OUTDIR) socketserver.c $(INCL) + $(OUTDIR)/spell.obj: $(OUTDIR) spell.c $(INCL) $(OUTDIR)/spellfile.obj: $(OUTDIR) spellfile.c $(INCL) @@ -2017,6 +2020,7 @@ proto.h: \ proto/session.pro \ proto/sha256.pro \ proto/sign.pro \ + proto/socketserver.pro \ proto/spell.pro \ proto/spellfile.pro \ proto/spellsuggest.pro \ diff --git a/src/Makefile b/src/Makefile index 4e00a2eb88..dd94e74180 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1571,6 +1571,7 @@ BASIC_SRC = \ session.c \ sha256.c \ sign.c \ + socketserver.c \ sound.c \ spell.c \ spellfile.c \ @@ -1749,6 +1750,7 @@ OBJ_COMMON = \ objects/session.o \ objects/sha256.o \ objects/sign.o \ + objects/socketserver.o \ objects/sound.o \ objects/spell.o \ objects/spellfile.o \ @@ -3592,6 +3594,9 @@ objects/sha256.o: sha256.c objects/sign.o: sign.c $(CCC) -o $@ sign.c +objects/socketserver.o: socketserver.c + $(CCC) -o $@ socketserver.c + objects/sound.o: sound.c $(CCC) -o $@ sound.c @@ -4267,6 +4272,11 @@ objects/sign.o: auto/osdef.h sign.c vim.h protodef.h auto/config.h feature.h os_ structs.h regexp.h gui.h libvterm/include/vterm.h \ libvterm/include/vterm_keycodes.h alloc.h ex_cmds.h spell.h proto.h \ globals.h errors.h +objects/socketserver.o: socketserver.c vim.h protodef.h auto/config.h feature.h \ + os_unix.h auto/osdef.h ascii.h keymap.h termdefs.h macros.h option.h \ + beval.h structs.h regexp.h gui.h libvterm/include/vterm.h \ + libvterm/include/vterm_keycodes.h xdiff/xdiff.h xdiff/../vim.h alloc.h \ + ex_cmds.h spell.h proto.h globals.h errors.h objects/sound.o: auto/osdef.h sound.c vim.h protodef.h auto/config.h feature.h os_unix.h \ ascii.h keymap.h termdefs.h macros.h option.h beval.h \ structs.h regexp.h gui.h libvterm/include/vterm.h \ @@ -4793,6 +4803,7 @@ proto/search.pro: search.c proto/session.pro: session.c proto/sha256.pro: sha256.c proto/sign.pro: sign.c +proto/socketserver.pro: socketserver.c proto/sound.pro: sound.c proto/spell.pro: spell.c proto/spellfile.pro: spellfile.c diff --git a/src/auto/configure b/src/auto/configure index 90110a812b..a65e2182fa 100755 --- a/src/auto/configure +++ b/src/auto/configure @@ -850,7 +850,6 @@ enable_netbeans enable_channel enable_terminal enable_autoservername -enable_socketserver enable_multibyte enable_rightleft enable_arabic @@ -1532,7 +1531,6 @@ 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. @@ -9077,36 +9075,6 @@ 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_msg - { 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=yes ;; -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" = "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/channel.c b/src/channel.c index 579087b374..27aa259e2b 100644 --- a/src/channel.c +++ b/src/channel.c @@ -820,7 +820,7 @@ channel_connect( * Returns the channel for success. * Returns NULL for failure. */ - static channel_T * + channel_T * channel_open_unix( const char *path, void (*nb_close_cb)(void)) @@ -1290,6 +1290,46 @@ channel_set_options(channel_T *channel, jobopt_T *opt) channel->ch_part[PART_IN].ch_io = opt->jo_io[PART_IN]; } +/* + * Parse the address for socketserver and store port in "port", or the path in + * "unix_path" if it is a unix domain socket. Returns OK on success and FAIL on + * failure. + */ + int +channel_parse_socketserver_address( + char_u *address, + int *port, + char_u **unix_path, + bool quiet) +{ + char *rest; + long val; + + if (*address == NUL) + goto fail; + + if (!STRNCMP(address, "unix:", 5)) + { + *unix_path = vim_strsave(address + 5); + return OK; + } + + val = strtol((char *)(address), &rest, 10); + if (val < 0 || val >= 65536 || *rest != NUL) + { + if (!quiet) + semsg(_(e_invalid_argument_str), address); + return FAIL; + } + *port = (int)val; + + return OK; +fail: + if (!quiet) + semsg(_(e_invalid_argument_str), address); + return FAIL; +} + /* * Implements ch_open(). */ @@ -1449,7 +1489,7 @@ channel_listen_func(typval_T *argvars) } if (is_unix) - channel = channel_listen_unix((char *)arg, NULL); + channel = channel_listen_unix((char *)arg, NULL, true); else channel = channel_listen(port, NULL); if (channel != NULL) @@ -1571,13 +1611,15 @@ channel_listen( /* * Listen to a Unix domain socket channel. + * If "replace" is true, then unlink the path first beforehand. * Returns the channel for success. * Returns NULL for failure. */ channel_T * channel_listen_unix( char *path, - void (*nb_close_cb)(void)) + void (*nb_close_cb)(void), + bool replace) { int sd = -1; struct sockaddr_un server; @@ -1619,8 +1661,9 @@ channel_listen_unix( return NULL; } - // Unlink the socket in case it already exists - unlink(server.sun_path); + if (replace) + // Unlink the socket in case it already exists + unlink(server.sun_path); // Bind the socket to the path server_len = offsetof(struct sockaddr_un, sun_path) + path_len + 1; @@ -1983,7 +2026,7 @@ channel_buffer_free(buf_T *buf) /* * Write any lines waiting to be written to "channel". */ - static void + void channel_write_input(channel_T *channel) { chanpart_T *in_part = &channel->ch_part[PART_IN]; @@ -2364,7 +2407,7 @@ channel_save(channel_T *channel, ch_part_T part, char_u *buf, int len, * Try to fill the buffer of "reader". * Returns FALSE when nothing was added. */ - static int + int channel_fill(js_read_T *reader) { channel_T *channel = (channel_T *)reader->js_cookie; @@ -2484,11 +2527,12 @@ channel_process_lspdap_http_hdr(js_read_T *reader) /* * Use the read buffer of "channel"/"part" and parse a JSON message that is - * complete. The messages are added to the queue. + * complete. The messages are added to the queue. If "socketserver" is true, + * then ignore the channel mode. * Return TRUE if there is more to read. */ - static int -channel_parse_json(channel_T *channel, ch_part_T part) + int +channel_parse_json(channel_T *channel, ch_part_T part, bool socketserver) { js_read_T reader; typval_T listtv; @@ -2507,8 +2551,8 @@ channel_parse_json(channel_T *channel, ch_part_T part) reader.js_cookie = channel; reader.js_cookie_arg = part; - if (chanpart->ch_mode == CH_MODE_LSP - || chanpart->ch_mode == CH_MODE_DAP) + if (!socketserver && (chanpart->ch_mode == CH_MODE_LSP + || chanpart->ch_mode == CH_MODE_DAP)) status = channel_process_lspdap_http_hdr(&reader); // When a message is incomplete we wait for a short while for more to @@ -2526,14 +2570,16 @@ channel_parse_json(channel_T *channel, ch_part_T part) { // Only accept the response when it is a list with at least two // items. - if ((chanpart->ch_mode == CH_MODE_LSP || chanpart->ch_mode == CH_MODE_DAP) + if (!socketserver && (chanpart->ch_mode == CH_MODE_LSP + || chanpart->ch_mode == CH_MODE_DAP) && listtv.v_type != VAR_DICT) { ch_error(channel, "Did not receive a LSP dict, discarding"); clear_tv(&listtv); } - else if (chanpart->ch_mode != CH_MODE_LSP && chanpart->ch_mode != CH_MODE_DAP - && (listtv.v_type != VAR_LIST || listtv.vval.v_list->lv_len < 2)) + else if (!socketserver && chanpart->ch_mode != CH_MODE_LSP + && chanpart->ch_mode != CH_MODE_DAP + && (listtv.v_type != VAR_LIST || listtv.vval.v_list->lv_len < 2)) { if (listtv.v_type != VAR_LIST) ch_error(channel, "Did not receive a list, discarding"); @@ -2668,7 +2714,7 @@ remove_cb_node(cbq_T *head, cbq_T *node) * Remove "node" from the queue that it is in and free it. * Caller should have freed or used node->jq_value. */ - static void + void remove_json_node(jsonq_T *head, jsonq_T *node) { if (node->jq_prev == NULL) @@ -3227,8 +3273,12 @@ may_invoke_callback(channel_T *channel, ch_part_T part) int called_otc; // one time callbackup int raw_len = 0; - if (channel->ch_nb_close_cb != NULL) - // this channel is handled elsewhere (netbeans) + if (channel->ch_nb_close_cb != NULL +#ifdef FEAT_SOCKETSERVER + || channel->ch_socketserver +#endif + ) + // this channel is handled elsewhere (netbeans or socketserver) return FALSE; // Use a message-specific callback, part callback or channel callback @@ -3267,7 +3317,7 @@ may_invoke_callback(channel_T *channel, ch_part_T part) (void)channel_collapse(channel, part, FALSE); // Parse readahead, return when there is still no message. - channel_parse_json(channel, part); + channel_parse_json(channel, part, false); if (channel_get_json(channel, part, -1, FALSE, &listtv) == FAIL) return FALSE; } @@ -3595,7 +3645,7 @@ channel_readahead_pointer(channel_T *channel, ch_part_T part) if (head->jq_next == NULL) // Parse json from readahead, there might be a complete message to // process. - channel_parse_json(channel, part); + channel_parse_json(channel, part, false); return head->jq_next; } @@ -3842,6 +3892,11 @@ channel_close(channel_T *channel, int invoke_close_cb) } channel->ch_nb_close_cb = NULL; +#ifdef FEAT_SOCKETSERVER + channel->ch_socketserver = false; + channel->ch_ss_accept_cb = NULL; + channel->ch_ss_close_cb = NULL; +#endif #ifdef FEAT_TERMINAL term_channel_closed(channel); @@ -4181,6 +4236,10 @@ channel_close_now(channel_T *channel) ch_log(channel, "Closing channel because all readable fds are closed"); if (channel->ch_nb_close_cb != NULL) (*channel->ch_nb_close_cb)(); +#ifdef FEAT_SOCKETSERVER + if (channel->ch_ss_close_cb != NULL) + channel->ch_ss_close_cb(channel); +#endif channel_close(channel, TRUE); } @@ -4252,6 +4311,14 @@ channel_read(channel_T *channel, ch_part_T part, char *func) channel_gui_register_one(newchannel, PART_SOCK); #endif +#ifdef FEAT_SOCKETSERVER + if (channel->ch_ss_accept_cb != NULL) + { + channel->ch_ss_accept_cb(newchannel); + return; + } +#endif + if (client.ss_family == AF_INET) { #ifdef HAVE_INET_NTOP @@ -4323,6 +4390,16 @@ channel_read(channel_T *channel, ch_part_T part, char *func) #endif } +#ifdef FEAT_SOCKETSERVER + + void +channel_check(channel_T *channel, ch_part_T part) +{ + channel_read(channel, part, "channel_check"); +} + +#endif + /* * Read from RAW or NL "channel"/"part". Blocks until there is something to * read or the timeout expires. @@ -4465,7 +4542,7 @@ channel_read_json_block( // received messages. (void)channel_collapse(channel, part, FALSE); - more = channel_parse_json(channel, part); + more = channel_parse_json(channel, part, false); // search for message "id" if (channel_get_json(channel, part, id, TRUE, rettv) == OK) @@ -5915,4 +5992,17 @@ channel_to_string_buf(typval_T *varp, char_u *buf) return buf; } +/* + * Return the channel with the given ID. Returns NULL if not found. + */ + channel_T * +channel_find(int ch_id) +{ + channel_T *ch; + FOR_ALL_CHANNELS(ch) + if (ch->ch_id == ch_id) + return ch; + return NULL; +} + #endif // FEAT_JOB_CHANNEL diff --git a/src/clientserver.c b/src/clientserver.c index 9186f4db16..ae72a694f1 100644 --- a/src/clientserver.c +++ b/src/clientserver.c @@ -15,11 +15,6 @@ #if defined(FEAT_CLIENTSERVER) -# 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); @@ -206,23 +201,6 @@ 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 @@ -242,10 +220,12 @@ exec_on_server(mparm_T *parmp) parmp->servername = serverMakeName(parmp->serverName_arg, parmp->argv[0]); # ifdef MSWIN - if (parmp->servername != NULL) + if (parmp->servername != NULL && clientserver_method == CLIENTSERVER_METHOD_MSWIN) { serverSetName(parmp->servername); +# ifndef FEAT_SOCKETSERVER vim_free(parmp->servername); +# endif } # endif } @@ -262,16 +242,31 @@ prepare_server(mparm_T *parmp) * Only register nongui-vim's with an explicit --servername argument, * or when compiling with autoservername. * When running as root --servername is also required. + * + * If we are Windows and socketserver is being used, then don't use + * autoservername, since traditional naming isn't supported. */ if ( -# ifdef FEAT_X11 - X_DISPLAY != NULL && +# if defined(FEAT_X11) || (defined(FEAT_SOCKETSERVER) && !defined(MSWIN)) + ( +# ifdef FEAT_X11 + X_DISPLAY != NULL +# endif +# if defined(FEAT_X11) && defined(FEAT_SOCKETSERVER) && !defined(MSWIN) + || +# endif +# if defined(FEAT_SOCKETSERVER) && !defined(MSWIN) + clientserver_method == CLIENTSERVER_METHOD_SOCKET +# endif + ) && # endif - parmp->servername != NULL && ( # if defined(FEAT_AUTOSERVERNAME) || defined(FEAT_GUI) ( +# if defined(FEAT_SOCKETSERVER) && defined(MSWIN) + clientserver_method != CLIENTSERVER_METHOD_SOCKET && +# endif # if defined(FEAT_AUTOSERVERNAME) 1 # else @@ -284,12 +279,10 @@ prepare_server(mparm_T *parmp) # endif parmp->serverName_arg != NULL)) { + # ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) - { - if (socket_server_init(parmp->servername) == OK) - TIME_MSG("initialize socket server"); - } + socketserver_start(parmp->servername, false); # endif # ifdef FEAT_X11 if (clientserver_method == CLIENTSERVER_METHOD_X11) @@ -300,10 +293,15 @@ prepare_server(mparm_T *parmp) # endif vim_free(parmp->servername); } -# ifdef FEAT_X11 else - serverDelayedStartName = parmp->servername; + { +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) + serverDelayedStartName = parmp->servername; + else # endif + vim_free(parmp->servername); + } # endif /* @@ -343,9 +341,6 @@ cmdsrv_main( # define ARGTYPE_SEND 3 int silent = FALSE; int tabs = FALSE; -# ifdef FEAT_SOCKETSERVER - char_u *receiver; -# endif # ifdef MSWIN HWND srv; # elif defined(FEAT_X11) @@ -353,6 +348,9 @@ cmdsrv_main( setup_term_clip(); # endif +# ifdef FEAT_SOCKETSERVER + channel_T *ch = NULL; +# endif sname = serverMakeName(serverName_arg, argv[0]); if (sname == NULL) @@ -434,9 +432,8 @@ cmdsrv_main( # ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) - ret = socket_server_send( - sname, *serverStr, NULL, &receiver, - 0, -1, silent); + ret = socketserver_send(sname, *serverStr, NULL, false, -1, + silent, &ch); # endif # ifdef FEAT_X11 if (clientserver_method == CLIENTSERVER_METHOD_X11) @@ -452,8 +449,10 @@ cmdsrv_main( } # endif # ifdef MSWIN - // Win32 always works? - ret = serverSendToVim(sname, *serverStr, NULL, &srv, 0, 0, silent); + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) + // Win32 always works? + ret = serverSendToVim(sname, *serverStr, NULL, &srv, 0, 0, + silent); # endif if (ret < 0) { @@ -513,23 +512,23 @@ cmdsrv_main( char_u *p = NULL; int j; # ifdef MSWIN - p = serverGetReply(srv, NULL, TRUE, TRUE, 0); - if (p == NULL) - break; -# else -# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) + p = serverGetReply(srv, NULL, TRUE, TRUE, 0); +# endif +# ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET - && socket_server_read_reply(receiver, &p, -1) == FAIL) + && (ch == NULL + || socketserver_read_reply(sname, &p, -1, true) + == FAIL)) break; -# endif -# ifdef FEAT_X11 +# endif +# ifdef FEAT_X11 if (clientserver_method == CLIENTSERVER_METHOD_X11 && serverReadReply(xterm_dpy, srv, &p, TRUE, -1) < 0) break; -# endif +# endif if (p == NULL) break; -# endif j = atoi((char *)p); vim_free(p); if (j >= 0 && j < numFiles) @@ -543,6 +542,13 @@ cmdsrv_main( done[j] = 1; } } +# ifdef FEAT_SOCKETSERVER + if (ch != NULL) + { + channel_close(ch, false); + channel_clear(ch); + } +# endif # ifdef FEAT_GUI_MSWIN Shell_NotifyIcon(NIM_DELETE, &ni); # endif @@ -551,25 +557,24 @@ cmdsrv_main( } else if (STRICMP(argv[i], "--remote-expr") == 0) { + int status = OK; + if (i == *argc - 1) mainerr_arg_missing((char_u *)argv[i]); # ifdef MSWIN // Win32 always works? - if (serverSendToVim(sname, (char_u *)argv[i + 1], + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN + && serverSendToVim(sname, (char_u *)argv[i + 1], &res, NULL, 1, 0, FALSE) < 0) -# else -# 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 + status = FAIL; +# endif +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET + && socketserver_send(sname, (char_u *)argv[i + 1], + &res, 1, 0, false, NULL) < 0) + status = FAIL; +# endif +# ifdef FEAT_X11 if (clientserver_method == CLIENTSERVER_METHOD_X11) { if (xterm_dpy == NULL) @@ -577,15 +582,11 @@ cmdsrv_main( else if (serverSendToVim(xterm_dpy, sname, (char_u *)argv[i + 1], &res, NULL, 1, 0, 1, FALSE) < 0) - goto expr_fail; + status = FAIL; } -# endif - if (FALSE) # endif + if (status == FAIL) { -# if !defined(MSWIN) -expr_fail: -# endif if (res != NULL && *res != NUL) { // Output error from remote @@ -598,28 +599,22 @@ expr_fail: else if (STRICMP(argv[i], "--serverlist") == 0) { # ifdef MSWIN - // Win32 always works? - res = serverGetVimNames(); -# else -# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) + // Win32 always works? + res = serverGetVimNames(); +# endif +# 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); - } +# ifdef MSWIN + res = vim_strsave((char_u *)""); +# else + res = socketserver_list(); # endif -# ifdef FEAT_X11 +# 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"); @@ -648,9 +643,6 @@ expr_fail: if (didone) { -# ifdef FEAT_SOCKETSERVER - socket_server_uninit(); -# endif display_errors(); // display any collected messages exit(exiterr); // Mission accomplished - get out } @@ -807,12 +799,12 @@ serverMakeName(char_u *arg, char *cmd) 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. + // When using socketserver backend, do not change the name if path or + // channel address. if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) { - if (arg[0] == '/' || STRNCMP(arg, "./", 2) == 0 || - STRNCMP(arg, "../", 3) == 0) + if (STRNICMP(arg, "channel:", 8) == 0 || arg[0] == '/' || + STRNCMP(arg, "./", 2) == 0 || STRNCMP(arg, "../", 3) == 0) p = vim_strsave(arg); else p = vim_strsave_up(arg); @@ -853,6 +845,8 @@ make_connection(void) static int check_connection(void) { + if (clientserver_method != CLIENTSERVER_METHOD_X11) + return OK; make_connection(); if (X_DISPLAY == NULL) { @@ -878,10 +872,8 @@ remote_common(typval_T *argvars, typval_T *rettv, int expr) # ifdef FEAT_X11 Window w; # endif -# ifdef FEAT_SOCKETSERVER - char_u *client = NULL; -# endif # endif + int ret = OK; if (check_restricted() || check_secure()) return; @@ -899,35 +891,29 @@ remote_common(typval_T *argvars, typval_T *rettv, int expr) return; // type error; errmsg already given keys = tv_get_string_buf(&argvars[1], buf); # ifdef MSWIN - if (serverSendToVim(server_name, keys, &r, &w, expr, timeout, TRUE) < 0) -# else -# ifdef FEAT_SOCKETSERVER - if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) - if (socket_server_send(server_name, keys, &r, &client, expr, - timeout * 1000, TRUE) < 0) - goto fail; -# endif -# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN + && serverSendToVim(server_name, keys, &r, &w, expr, + timeout, TRUE) < 0) + ret = FAIL; +# endif +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET + && socketserver_send(server_name, keys, &r, expr, + timeout * 1000, true, NULL) < 0) + ret = FAIL; +# 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 fail; -# endif + ret = FAIL; # endif -# if !defined(MSWIN) - if (FALSE) + if (ret == FAIL) { -fail: -# 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); @@ -939,40 +925,42 @@ fail: 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]; +# if defined(MSWIN) || defined(FEAT_X11) + char_u sbuf[30]; # endif + char_u *str = NULL; char_u *idvar; idvar = tv_get_string_chk(&argvars[2]); if (idvar != NULL && *idvar != NUL) { - str[0] = 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 + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) + { + sprintf((char *)sbuf, PRINTF_HEX_LONG_U, (long_u)w); + str = sbuf; + } # endif +# ifdef FEAT_X11 + if (clientserver_method == CLIENTSERVER_METHOD_X11) + { + sprintf((char *)sbuf, PRINTF_HEX_LONG_U, (long_u)w); + str = sbuf; + } +# endif +# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) + str = server_name; +# endif + if (str == NULL) + str = (char_u *)""; + 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 @@ -1054,20 +1042,23 @@ f_remote_peek(typval_T *argvars UNUSED, typval_T *rettv) if (serverid == NULL) return; // type error; errmsg already given # ifdef MSWIN - sscanf((const char *)serverid, SCANF_HEX_LONG_U, &n); - if (n == 0) - rettv->vval.v_number = -1; - else + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) { - s = serverGetReply((HWND)n, FALSE, FALSE, FALSE, 0); - rettv->vval.v_number = (s != NULL); + sscanf((const char *)serverid, SCANF_HEX_LONG_U, &n); + if (n == 0) + rettv->vval.v_number = -1; + else + { + s = serverGetReply((HWND)n, FALSE, FALSE, FALSE, 0); + rettv->vval.v_number = (s != NULL); + } } -# else -# ifdef FEAT_SOCKETSERVER +# endif +# ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) - rettv->vval.v_number = socket_server_peek_reply(serverid, &s); -# endif -# ifdef FEAT_X11 + rettv->vval.v_number = socketserver_peek_reply(serverid, &s); +# endif +# ifdef FEAT_X11 if (clientserver_method == CLIENTSERVER_METHOD_X11) { if (check_connection() == FAIL) @@ -1076,7 +1067,6 @@ f_remote_peek(typval_T *argvars UNUSED, typval_T *rettv) rettv->vval.v_number = serverPeekReply(X_DISPLAY, serverStrToWin(serverid), &s); } -# endif # endif if (argvars[1].v_type != VAR_UNKNOWN && rettv->vval.v_number > 0) @@ -1121,24 +1111,27 @@ f_remote_read(typval_T *argvars UNUSED, typval_T *rettv) timeout = tv_get_number(&argvars[1]); # ifdef MSWIN - sscanf((char *)serverid, SCANF_HEX_LONG_U, &n); - if (n != 0) - r = serverGetReply((HWND)n, FALSE, TRUE, TRUE, timeout); - if (r == NULL) - emsg(_(e_unable_to_read_server_reply)); -# else -# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) + { + sscanf((char *)serverid, SCANF_HEX_LONG_U, &n); + if (n != 0) + r = serverGetReply((HWND)n, FALSE, TRUE, TRUE, timeout); + if (r == NULL) + emsg(_(e_unable_to_read_server_reply)); + } +# endif +# ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET && - socket_server_read_reply(serverid, &r, timeout * 1000) == FAIL) + socketserver_read_reply(serverid, &r, timeout * 1000, false) + == FAIL) emsg(_(e_unable_to_read_server_reply)); -# endif -# ifdef FEAT_X11 +# 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 @@ -1184,17 +1177,17 @@ f_remote_startserver(typval_T *argvars UNUSED, typval_T *rettv UNUSED) char_u *server = tv_get_string_chk(&argvars[0]); # ifdef MSWIN - serverSetName(server); -# else -# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) + serverSetName(server); +# endif +# ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) - socket_server_init(server); -# endif -# ifdef FEAT_X11 + socketserver_start(server, true); +# endif +# ifdef FEAT_X11 if (clientserver_method == CLIENTSERVER_METHOD_X11 && check_connection() == OK) serverRegisterName(X_DISPLAY, server); -# endif # endif # else @@ -1209,6 +1202,7 @@ f_server2client(typval_T *argvars UNUSED, typval_T *rettv) char_u buf[NUMBUFLEN]; char_u *server; char_u *reply; + int ret = OK; rettv->vval.v_number = -1; if (check_restricted() || check_secure()) @@ -1226,28 +1220,22 @@ f_server2client(typval_T *argvars UNUSED, typval_T *rettv) # ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET && - socket_server_send_reply(server, reply) == FAIL) - goto fail; + socketserver_send_reply(server, reply) == FAIL) + ret = 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) + ret = FAIL; # endif # ifdef MSWIN - if (serverSendReply(server, reply) < 0) -# endif -# if defined(FEAT_SOCKETSERVER) && !defined(FEAT_X11) && !defined(MSWIN) - if (FALSE) + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN + && serverSendReply(server, reply) < 0) + ret = FAIL; # endif + + if (ret == FAIL) { -# ifdef FEAT_SOCKETSERVER -fail: -# endif emsg(_(e_unable_to_send_to_client)); return; } @@ -1264,20 +1252,24 @@ f_serverlist(typval_T *argvars UNUSED, typval_T *rettv) # ifdef FEAT_CLIENTSERVER # ifdef MSWIN - r = serverGetVimNames(); -# else -# ifdef FEAT_SOCKETSERVER + if (clientserver_method == CLIENTSERVER_METHOD_MSWIN) + r = serverGetVimNames(); +# endif +# ifdef FEAT_SOCKETSERVER if (clientserver_method == CLIENTSERVER_METHOD_SOCKET) - r = socket_server_list_sockets(); +# ifdef MSWIN + r = vim_strsave((char_u *)""); +# else + r = socketserver_list(); # endif -# ifdef FEAT_X11 +# 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/configure.ac b/src/configure.ac index 6f6590f6da..27a8943bda 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -2363,22 +2363,6 @@ 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_msg - AC_MSG_RESULT([cannot use socketserver with tiny features])], - [enable_socketserver=yes])) - -if test "$enable_socketserver" = "yes"; then - AC_DEFINE(WANT_SOCKETSERVER) - AC_MSG_RESULT([yes]) -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 4e261c9232..0013a43ee8 100644 --- a/src/errors.h +++ b/src/errors.h @@ -1139,7 +1139,7 @@ EXTERN char e_cant_read_postscript_resource_file_str[] INIT(= N_("E457: Can't read PostScript resource file \"%s\"")); #endif // E458 unused -#if defined(UNIX) || defined(FEAT_SESSION) +#if defined(UNIX) || defined(FEAT_SESSION) || defined(FEAT_SOCKETSERVER) EXTERN char e_cannot_go_back_to_previous_directory[] INIT(= N_("E459: Cannot go back to previous directory")); #endif @@ -3777,16 +3777,14 @@ 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")); + INIT(= N_("E1564: Socket name '%s' 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")); + INIT(= N_("E1566: Failed connecting to socket '%s'")); +EXTERN char e_socket_server_version_mismatch[] + INIT(= N_("E1567: Socket server protocol version mismatch, check what Vim version you are using")); #endif EXTERN char e_osc_response_timed_out[] INIT(= N_("E1568: OSC command response timed out: %.*s")); diff --git a/src/feature.h b/src/feature.h index 115db703d2..46508f74bb 100644 --- a/src/feature.h +++ b/src/feature.h @@ -953,9 +953,16 @@ #endif /* - * +socketserver Use UNIX domain sockets for clientserver communication + * The +channel feature requires +eval. */ -#if defined(UNIX) && defined(WANT_SOCKETSERVER) +#if !defined(FEAT_EVAL) && defined(FEAT_JOB_CHANNEL) +# undef FEAT_JOB_CHANNEL +#endif + +/* + * +socketserver Use channels for clientserver communication + */ +#if (defined(UNIX) || defined(MSWIN)) && defined(FEAT_JOB_CHANNEL) # define FEAT_SOCKETSERVER #endif @@ -966,6 +973,9 @@ #if (defined(MSWIN) || defined(FEAT_XCLIPBOARD) || defined(FEAT_SOCKETSERVER)) \ && defined(FEAT_EVAL) # define FEAT_CLIENTSERVER +# if defined(FEAT_SOCKETSERVER) && (defined(FEAT_XCLIPBOARD) || defined(MSWIN)) +# define FEAT_CLIENTSERVER_BACKENDS +# endif #endif /* @@ -1053,14 +1063,6 @@ * +tgetent */ - -/* - * The +channel feature requires +eval. - */ -#if !defined(FEAT_EVAL) && defined(FEAT_JOB_CHANNEL) -# undef FEAT_JOB_CHANNEL -#endif - /* * The Netbeans feature requires +eval and +job_channel */ diff --git a/src/gc.c b/src/gc.c index f15d1dffe0..f1bd6b7245 100644 --- a/src/gc.c +++ b/src/gc.c @@ -208,6 +208,9 @@ garbage_collect(int testing) #ifdef FEAT_NETBEANS_INTG abort = abort || set_ref_in_nb_channel(copyID); #endif +#ifdef FEAT_SOCKETSERVER + abort = abort || set_ref_in_socketserver_channel(copyID); +#endif #ifdef FEAT_TIMERS abort = abort || set_ref_in_timer(copyID); diff --git a/src/getchar.c b/src/getchar.c index b995698128..47ba62ad6c 100644 --- a/src/getchar.c +++ b/src/getchar.c @@ -2650,6 +2650,9 @@ parse_queued_messages(void) // Process the queued netbeans messages. netbeans_parse_messages(); # endif +# ifdef FEAT_SOCKETSERVER + socketserver_parse_messages(); +# endif # ifdef FEAT_JOB_CHANNEL // Write any buffer lines still to be written. channel_write_any_lines(); diff --git a/src/globals.h b/src/globals.h index 2fdc944648..0a4a09d65d 100644 --- a/src/globals.h +++ b/src/globals.h @@ -2122,25 +2122,28 @@ EXTERN int wayland_no_connect INIT(= FALSE); #endif -#if defined(FEAT_CLIENTSERVER) && !defined(MSWIN) +#if defined(FEAT_CLIENTSERVER) // Backend for clientserver functionality typedef enum { CLIENTSERVER_METHOD_NONE, +# ifdef FEAT_X11 CLIENTSERVER_METHOD_X11, +# endif +# ifdef MSWIN + CLIENTSERVER_METHOD_MSWIN, +# endif +# ifdef FEAT_SOCKETSERVER CLIENTSERVER_METHOD_SOCKET +# endif } 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(MSWIN) +INIT(= CLIENTSERVER_METHOD_MSWIN); # elif defined(FEAT_SOCKETSERVER) INIT(= CLIENTSERVER_METHOD_SOCKET); # else diff --git a/src/gui.c b/src/gui.c index 1448c86bdb..b04f132163 100644 --- a/src/gui.c +++ b/src/gui.c @@ -158,12 +158,6 @@ gui_start(char_u *arg UNUSED) choose_clipmethod(); #endif -#if defined(FEAT_SOCKETSERVER) && defined(FEAT_GUI_GTK) - // Install socket server listening socket if we are running it - if (socket_server_valid()) - gui_gtk_init_socket_server(); -#endif - #ifdef FEAT_GUI_MSWIN // Enable fullscreen mode if (vim_strchr(p_go, GO_FULLSCREEN) != NULL) diff --git a/src/gui_gtk_x11.c b/src/gui_gtk_x11.c index 2dab629394..af452c8468 100644 --- a/src/gui_gtk_x11.c +++ b/src/gui_gtk_x11.c @@ -80,13 +80,6 @@ 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. */ @@ -2690,54 +2683,6 @@ global_event_filter(GdkXEvent *xev, } #endif // !USE_GNOME_SESSION -#if defined(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 - static GdkPixbuf * pixbuf_new_from_png_data(const unsigned char *data, unsigned int len) { diff --git a/src/json.c b/src/json.c index a3e58043d8..b9cbc0d594 100644 --- a/src/json.c +++ b/src/json.c @@ -51,6 +51,8 @@ json_encode(typval_T *val, int options) // Store bytes in the growarray. ga_init2(&ga, 1, 4000); json_encode_gap(&ga, val, options); + if (options & JSON_NL) + ga_append(&ga, NL); ga_append(&ga, NUL); return ga.ga_data; } diff --git a/src/main.c b/src/main.c index cac5db145e..d2fe22ebe5 100644 --- a/src/main.c +++ b/src/main.c @@ -1844,6 +1844,9 @@ getout(int exitval) #ifdef FEAT_NETBEANS_INTG netbeans_end(); #endif +#ifdef FEAT_SOCKETSERVER + socketserver_stop(); +#endif #ifdef FEAT_CSCOPE cs_end(); #endif @@ -1910,7 +1913,7 @@ early_arg_scan(mparm_T *parmp UNUSED) gui.dofork = false; # endif } -# if defined(FEAT_X11) && defined(FEAT_SOCKETSERVER) +# ifdef FEAT_CLIENTSERVER_BACKENDS else if (STRNICMP(argv[i], "--clientserver", 14) == 0) { char_u *arg; @@ -1920,8 +1923,14 @@ early_arg_scan(mparm_T *parmp UNUSED) if (STRICMP(arg, "socket") == 0) clientserver_method = CLIENTSERVER_METHOD_SOCKET; +# ifdef FEAT_X11 else if (STRICMP(arg, "x11") == 0) clientserver_method = CLIENTSERVER_METHOD_X11; +# endif +# ifdef MSWIN + else if (STRICMP(arg, "mswin") == 0) + clientserver_method = CLIENTSERVER_METHOD_MSWIN; +# endif else mainerr(ME_UNKNOWN_OPTION, arg); } @@ -2247,9 +2256,9 @@ command_line_scan(mparm_T *parmp) ; // already processed -- no arg else if (STRNICMP(argv[0] + argv_idx, "servername", 10) == 0 || STRNICMP(argv[0] + argv_idx, "serversend", 10) == 0 -# if defined(FEAT_X11) && defined(FEAT_SOCKETSERVER) + // Don't put this under FEAT_CLIENTSERVER_BACKENDS, just + // let it be ignored. Makes tests less complicated || STRNICMP(argv[0] + argv_idx, "clientserver", 12) == 0 -# endif ) { // already processed -- snatch the following arg @@ -3747,8 +3756,8 @@ 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")); +# ifdef FEAT_CLIENTSERVER_BACKENDS + 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")); diff --git a/src/os_unix.c b/src/os_unix.c index 5cfc422b7a..ab58dda7eb 100644 --- a/src/os_unix.c +++ b/src/os_unix.c @@ -147,123 +147,6 @@ Window x11_window = 0; Display *x11_display = NULL; #endif -#if defined(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; - -static 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); @@ -3782,10 +3665,6 @@ mch_exit(int r) x11_export_final_selection(); #endif -#ifdef FEAT_SOCKETSERVER - socket_server_uninit(); -#endif - #ifdef FEAT_GUI if (!gui.in_use) #endif @@ -6792,9 +6671,6 @@ 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 @@ -6821,16 +6697,6 @@ 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 if ((wayland_fd = wayland_prepare_read()) >= 0) { @@ -6886,19 +6752,6 @@ RealWaitForChar(int fd, long msec, int *check_for_gpm UNUSED, int *interrupted) finished = FALSE; # endif -# ifdef FEAT_SOCKETSERVER - if (socket_server_idx >= 0) - { - if (fds[socket_server_idx].revents & POLLIN) - { - if (socket_server_accept_client() == FAIL) - socket_server_uninit(); - } - else if (fds[socket_server_idx].revents & (POLLHUP | POLLERR)) - socket_server_uninit(); - } -# endif - # ifdef FEAT_WAYLAND if (wayland_idx >= 0) wayland_poll_check(fds[wayland_idx].revents); @@ -6984,16 +6837,6 @@ 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 if ((wayland_fd = wayland_prepare_read()) >= 0) { @@ -7093,13 +6936,6 @@ select_eintr: finished = FALSE; # endif -# ifdef FEAT_SOCKETSERVER - if (ret > 0 && socket_server_fd != -1 - && FD_ISSET(socket_server_fd, &rfds) - && socket_server_accept_client() == FAIL) - socket_server_uninit(); -# endif - # ifdef FEAT_WAYLAND if (wayland_fd != -1) wayland_select_check(ret > 0 && FD_ISSET(wayland_fd, &rfds)); @@ -7161,16 +6997,9 @@ select_eintr: if (finished || msec == 0) break; -# if defined(FEAT_CLIENTSERVER) -# ifdef FEAT_X11 +# if defined(FEAT_CLIENTSERVER) && defined(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 @@ -9251,1565 +9080,3 @@ mch_create_anon_file(void) } return fd; } - -#if defined(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: - close(fd); - 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; - string_T buf; - string_T 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.string = alloc(sizeof(addr.sun_path))) == NULL) - return NULL; - if ((path.string = alloc(sizeof(addr.sun_path))) == NULL) - { - vim_free(buf.string); - return NULL; - } - buf.length = 0; - path.length = 0; - - 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)) - path.length = vim_snprintf_safelen((char *)path.string, sizeof(addr.sun_path), - "%s/vim-%lu", dir, (unsigned long int)getuid()); - else - path.length = vim_snprintf_safelen((char *)path.string, sizeof(addr.sun_path), - "%s/vim", dir); - - dirp = opendir((char *)path.string); - if (dirp == NULL) - continue; - - // Loop through directory - while ((dp = readdir(dirp)) != NULL) - { - if (STRCMP(dp->d_name, ".") == 0 || STRCMP(dp->d_name, "..") == 0) - continue; - - buf.length = vim_snprintf_safelen((char *)buf.string, sizeof(addr.sun_path), - "%s/%s", path.string, dp->d_name); - - // Don't want to send to ourselves, but we do want to list our - // server name (if we are a server). - if (socket_server_path == NULL - || STRCMP(socket_server_path, buf.string) != 0) - { - // Try sending an ALIVE command. This is more assuring than a - // simple connect, and *also seems to make tests less flaky*. - // - // We could also use a lock file which may be better, but this - // has worked fine so far... - 64bitman - if (!socket_server_check_alive(buf.string)) - continue; - } - - ga_concat_len(&str, (char_u *)dp->d_name, buf.length - (path.length + 1)); - ga_append(&str, '\n'); - } - - closedir(dirp); - - break; - } - - vim_free(path.string); - vim_free(buf.string); - - 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. Returns OK if client was - * accepted, else FAIL. - */ - int -socket_server_accept_client(void) -{ - int fd = accept(socket_server_fd, NULL, NULL); - ss_cmd_T cmd; - - if (fd == -1) - return FAIL; - - 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); - return OK; -} - -/* - * 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 expression 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; -# ifdef ELAPSED_FUNC - elapsed_T start_tv; -# endif - - 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 response - return 0; - } - - ss_pending_cmd_T pending; - - socket_server_init_pending_cmd(&pending); - -# ifdef ELAPSED_FUNC - ELAPSED_INIT(start_tv); -# endif - - // Wait for server to send back result - while (socket_server_dispatch(500) >= 0) - { - if (pending.result != NULL) - break; - -# ifdef ELAPSED_FUNC - if (ELAPSED_FUNC(start_tv) >= (timeout > 0 ? timeout : 1000)) - break; -# endif - } - - 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 UNUSED) -{ - ss_reply_T *reply = NULL; -# ifdef ELAPSED_FUNC - elapsed_T start_tv; -# endif - - if (!socket_server_name_is_valid(client)) - return -1; - - if (!socket_server_valid()) - return -1; - -# ifdef ELAPSED_FUNC - if (timeout > 0) - ELAPSED_INIT(start_tv); -# endif - - // Try seeing if there already is a reply in the queue - goto get_reply; - - while (socket_server_dispatch(500) >= 0) - { - int fd; - -# ifdef ELAPSED_FUNC - if (timeout > 0 && ELAPSED_FUNC(start_tv) >= timeout) - break; -# endif - -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); - vim_free(final); - 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) - { - vim_free(socket_path); - return -1; - } - - 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)); - close(socket_fd); - vim_free(socket_path); - return -1; - } - - if (path != NULL) - *path = socket_path; - else - vim_free(socket_path); - - return socket_fd; - -} - -/* - * 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 success 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; -# ifdef ELAPSED_FUNC - elapsed_T start_tv; -# endif - - // 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)); - -# ifdef ELAPSED_FUNC - ELAPSED_INIT(start_tv); -# endif - - 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: -# ifdef ELAPSED_FUNC - if (ELAPSED_FUNC(start_tv) >= timeout) - goto fail; -# endif - } - - // 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; -# ifdef ELAPSED_FUNC - elapsed_T start_tv; -# endif -# 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 - -# ifdef ELAPSED_FUNC - ELAPSED_INIT(start_tv); -# endif - - 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: -# ifdef ELAPSED_FUNC - if (ELAPSED_FUNC(start_tv) >= timeout) - return FAIL; -# endif - } - - 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 != 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 occurred, 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 responsive 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 ad805b30b7..946ce08831 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -254,9 +254,6 @@ msgstr "" msgid "%d of %d edited" msgstr "" -msgid "Socket server not online:Send expression failed" -msgstr "" - msgid "No display: Send expression failed.\n" msgstr "" @@ -1722,7 +1719,8 @@ msgstr "" msgid "-Y\t\t\tDo not connect to Wayland compositor" msgstr "" -msgid "--clientserver Backend for clientserver communication" +msgid "" +"--clientserver Backend for clientserver communication" msgstr "" msgid "--remote \tEdit in a Vim server if possible" @@ -2531,10 +2529,6 @@ msgstr "" msgid "XSMP SmcOpenConnection failed: %s" msgstr "" -#, c-format -msgid "Failed creating socket directory: %s" -msgstr "" - msgid "At line" msgstr "" @@ -2806,6 +2800,10 @@ msgstr "" msgid " (not supported)" msgstr "" +#, c-format +msgid "Failed creating socket directory: %s" +msgstr "" + #, c-format msgid "Warning: Cannot find word list \"%s_%s.spl\" or \"%s_ascii.spl\"" msgstr "" @@ -8834,20 +8832,20 @@ 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" +#, c-format +msgid "E1564: Socket name '%s' 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" +msgid "E1566: Failed connecting to socket '%s'" msgstr "" -msgid "E1567: Cannot start socket server, socket path is unavailable" +msgid "" +"E1567: Socket server protocol version mismatch, check what Vim version you " +"are using" msgstr "" #, c-format diff --git a/src/proto.h b/src/proto.h index ed7a5a054e..8dd9c30cbe 100644 --- a/src/proto.h +++ b/src/proto.h @@ -277,6 +277,9 @@ void mbyte_im_set_active(int active_arg); # include "job.pro" # include "channel.pro" # endif +# ifdef FEAT_SOCKETSERVER +# include "socketserver.pro" +# endif # ifdef FEAT_EVAL // Not generated automatically so that we can add an extra attribute. diff --git a/src/proto/channel.pro b/src/proto/channel.pro index 0b5d17a137..cd58f2c317 100644 --- a/src/proto/channel.pro +++ b/src/proto/channel.pro @@ -7,15 +7,18 @@ int channel_unref(channel_T *channel); int free_unused_channels_contents(int copyID, int mask); void free_unused_channels(int copyID, int mask); void channel_gui_register_all(void); +channel_T *channel_open_unix(const char *path, void (*nb_close_cb)(void)); channel_T *channel_open(const char *hostname, int port, int waittime, void (*nb_close_cb)(void)); +int channel_parse_socketserver_address(char_u *address, int *port, char_u **unix_path, bool quiet); channel_T *channel_listen_func(typval_T *argvars); channel_T *channel_listen(int port_in, void (*nb_close_cb)(void)); -channel_T *channel_listen_unix(char *path, void (*nb_close_cb)(void)); +channel_T *channel_listen_unix(char *path, void (*nb_close_cb)(void), bool replace); void ch_close_part(channel_T *channel, ch_part_T part); void channel_set_pipes(channel_T *channel, sock_T in, sock_T out, sock_T err); void channel_set_job(channel_T *channel, job_T *job, jobopt_T *options); void channel_write_in(channel_T *channel); void channel_buffer_free(buf_T *buf); +void channel_write_input(channel_T *channel); void channel_write_any_lines(void); void channel_write_new_lines(buf_T *buf); readq_T *channel_peek(channel_T *channel, ch_part_T part); @@ -23,11 +26,15 @@ char_u *channel_first_nl(readq_T *node); char_u *channel_get(channel_T *channel, ch_part_T part, int *outlen); void channel_consume(channel_T *channel, ch_part_T part, int len); int channel_collapse(channel_T *channel, ch_part_T part, int want_nl); +int channel_fill(js_read_T *reader); +int channel_parse_json(channel_T *channel, ch_part_T part, bool socketserver); +void remove_json_node(jsonq_T *head, jsonq_T *node); int channel_can_write_to(channel_T *channel); int channel_is_open(channel_T *channel); void channel_close(channel_T *channel, int invoke_close_cb); void channel_clear(channel_T *channel); void channel_free_all(void); +void channel_check(channel_T *channel, ch_part_T part); int channel_in_blocking_wait(void); channel_T *get_channel_arg(typval_T *tv, int check_open, int reading, ch_part_T part); void channel_handle_events(int only_keep_open); @@ -59,4 +66,5 @@ void f_ch_sendraw(typval_T *argvars, typval_T *rettv); void f_ch_setoptions(typval_T *argvars, typval_T *rettv); void f_ch_status(typval_T *argvars, typval_T *rettv); char_u *channel_to_string_buf(typval_T *varp, char_u *buf); +channel_T *channel_find(int ch_id); /* vim: set ft=c : */ diff --git a/src/proto/gui_gtk_x11.pro b/src/proto/gui_gtk_x11.pro index 5a57240135..696f6a07af 100644 --- a/src/proto/gui_gtk_x11.pro +++ b/src/proto/gui_gtk_x11.pro @@ -8,8 +8,6 @@ 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/os_unix.pro b/src/proto/os_unix.pro index 7513e400f0..aca289d1f7 100644 --- a/src/proto/os_unix.pro +++ b/src/proto/os_unix.pro @@ -96,15 +96,4 @@ 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 *name); -void socket_server_uninit(void); -char_u *socket_server_list_sockets(void); -int socket_server_accept_client(void); -int socket_server_valid(void); -int socket_server_send(char_u *name, char_u *str, char_u **result, char_u **receiver, int is_expr, int timeout, int silent); -int socket_server_read_reply(char_u *client, 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/proto/socketserver.pro b/src/proto/socketserver.pro new file mode 100644 index 0000000000..69d292c3ab --- /dev/null +++ b/src/proto/socketserver.pro @@ -0,0 +1,11 @@ +/* socketserver.c */ +int socketserver_start(char_u *name, bool quiet); +void socketserver_stop(void); +char_u *socketserver_list(void); +int set_ref_in_socketserver_channel(int copyID); +void socketserver_parse_messages(void); +int socketserver_send(char_u *name, char_u *str, char_u **result, bool is_expr, int timeout, bool silent, channel_T **ch); +int socketserver_send_reply(char_u *client, char_u *str); +int socketserver_read_reply(char_u *client, char_u **str, int timeout, bool remotewait); +int socketserver_peek_reply(char_u *sender, char_u **str); +/* vim: set ft=c : */ diff --git a/src/socketserver.c b/src/socketserver.c new file mode 100644 index 0000000000..7c4e19b536 --- /dev/null +++ b/src/socketserver.c @@ -0,0 +1,1404 @@ +/* vi:set ts=8 sts=4 sw=4 noet: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + */ + +/* + * socketserver.c: Socketserver clientserver functionality + */ + +#include "vim.h" + +#ifdef FEAT_SOCKETSERVER + +// Always greater than one. +# define PROTOCOL_VER 1 + +/* + * Any message sent to a Vim server or received from a Vim server is a simple + * JSON object with the following fields: + * { + * "version": PROTOCOL_VER -- Protocol version, if different, then ignore. + * + * "type": "expr"|"keystrokes"|"notify"|"reply"|"ver" -- The type of message + * + * "str": string -- What to execute for the command or contents of result + * + * ?"code": number -- Return code for expression + * + * ?"sender": string -- Address of client that sent command if it is a server. + * + * ?"wait": bool -- Used by --remote-wait since the client is not a server. + * } + */ + +/* + * 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. + */ +typedef struct +{ + char_u *sender; // Includes "type:" prefix if any + garray_T strings; +} ss_reply_T; + +static channel_T *server_channel = NULL; +static char_u *server_address = NULL; // Includes "type:" prefix if any +static bool server_is_unix = false; +static char_u *server_addr_cwd = NULL; // CWD when server was started, used + // to handle relative file paths. +static garray_T server_replies; + +static channel_T *client_channels = NULL; + +# define FOR_ALL_CLIENTS(ch) \ + for (ch = client_channels; ch != NULL; ch = ch->ch_ss_next) + +static void socketserver_cleanup(void); +static char_u *socketserver_create_path(char_u *name, bool quiet); +static char_u *socketserver_get_path(char_u *name, bool new, bool quiet, bool *fatal); +static void socketserver_accept(channel_T *channel); +static void socketserver_close(channel_T *channel); +static ss_reply_T *socketserver_add_reply(char_u *sender); + +/* + * Start the socketserver using the given name. Returns OK on success and FAIL + * on failure. + */ + int +socketserver_start(char_u *name, bool quiet) +{ + char_u *address = NULL; + int port; + bool is_unix = false; + channel_T *channel; + char_u *buf; + char_u dirbuf[MAXPATHL]; + + if (server_channel != NULL) + return OK; + + if (STRNICMP(name, "channel:", 8) == 0) + { + if (channel_parse_socketserver_address(name + 8, &port, &address, quiet) + == FAIL) + return FAIL; + if (address != NULL) + is_unix = true; + } + else + { + address = socketserver_create_path(name, quiet); + if (address == NULL) + return FAIL; + is_unix = true; + } + + if (is_unix) + channel = channel_listen_unix((char *)address, NULL, false); + else + channel = channel_listen(port, NULL); + + if (channel == NULL) + { + vim_free(address); + return FAIL; + } + + channel->ch_socketserver = true; + channel->ch_ss_accept_cb = socketserver_accept; + channel->ch_ss_close_cb = socketserver_close; + + server_channel = channel; + server_is_unix = is_unix; + + VIM_CLEAR(serverName); + + if (STRNICMP(name, "channel:", 8) == 0) + buf = vim_strsave(name); + else + { + buf = alloc(MAXPATHL + 1); + if (buf != NULL) + { + buf[0] = NUL; + mch_FullName(address, buf, MAXPATHL, false); + } + } + vim_free(address); + + if (buf != NULL) + { + server_address = vim_strsave(buf); + serverName = buf; + set_vim_var_string(VV_SEND_SERVER, serverName, -1); + } + + vim_free(server_addr_cwd); + if (mch_dirname(dirbuf, sizeof(dirbuf)) == OK) + server_addr_cwd = vim_strsave(dirbuf); + else + server_addr_cwd = NULL; + + ga_init2(&server_replies, sizeof(ss_reply_T), 2); + + ch_log(NULL, "socketserver: started server at %s", name); + + return OK; +} + +/* + * Stop running the socketserver if it is. Note that this does not stop Vim from + * becoming a client. + */ + void +socketserver_stop(void) +{ + if (server_channel == NULL) + return; + + channel_close(server_channel, false); + channel_clear(server_channel); + + socketserver_cleanup(); + + ch_log(NULL, "socketserver: shutting down server"); +} + +/* + * Cleanup server stuff. Do not close client channels, as those are not part of + * the actual server. + */ + static void +socketserver_cleanup(void) +{ +# ifdef UNIX + if (server_is_unix && server_address != NULL) + { + char_u *path = server_address; + char_u dirbuf[MAXPATHL]; + + if (STRNICMP(path, "channel:unix:", 13) == 0) + path += 13; + else if (STRNICMP(path, "name:", 5) == 0) + path += 5; + + if (*path == '/') + mch_remove(path); + // Go to the directory where the server was started. This is to handle + // when Vim changes directories and the servername is a relative path. + else if (server_addr_cwd != NULL + && mch_dirname(dirbuf, sizeof(dirbuf)) == OK) + { + if (mch_chdir((char *)server_addr_cwd) == 0) + { + mch_remove(path); + if (mch_chdir((char *)dirbuf) < 0) + emsg(_(e_cannot_go_back_to_previous_directory)); + } + } + } +# endif + + server_channel = NULL; + VIM_CLEAR(server_address); + VIM_CLEAR(server_addr_cwd); + + // Free all replies + for (int i = 0; i < server_replies.ga_len; i++) + { + ss_reply_T *reply = (ss_reply_T *)server_replies.ga_data + i; + + vim_free(reply->sender); + ga_clear_strings(&reply->strings); + } + ga_clear(&server_replies); +} + +/* + * 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 * +socketserver_list(void) +{ +# ifdef MSWIN + // Only support addresses on Windows + return vim_strsave((char_u *)""); +# else + garray_T str; + string_T buf; + string_T path; + DIR *dirp; + struct dirent *dp; + const char_u *known_dirs[] = { + mch_getenv("XDG_RUNTIME_DIR"), + mch_getenv("TMPDIR"), + (char_u *)"/tmp" + }; + + if ((buf.string = alloc(MAXPATHL)) == NULL) + return NULL; + if ((path.string = alloc(MAXPATHL)) == NULL) + { + vim_free(buf.string); + return NULL; + } + buf.length = 0; + path.length = 0; + + ga_init2(&str, 1, 100); + + for (size_t i = 0 ; i < ARRAY_LENGTH(known_dirs); i++) + { + const 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)) + path.length = vim_snprintf_safelen((char *)path.string, MAXPATHL, + "%s/vim-%lu", dir, (unsigned long int)getuid()); + else + path.length = vim_snprintf_safelen((char *)path.string, MAXPATHL, + "%s/vim", dir); + + dirp = opendir((char *)path.string); + if (dirp == NULL) + continue; + + // Loop through directory + while ((dp = readdir(dirp)) != NULL) + { + if (STRCMP(dp->d_name, ".") == 0 || STRCMP(dp->d_name, "..") == 0) + continue; + + buf.length = vim_snprintf_safelen((char *)buf.string, MAXPATHL, + "%s/%s", path.string, dp->d_name); + + ga_concat_len(&str, (char_u *)dp->d_name, + buf.length - (path.length + 1)); + ga_append(&str, '\n'); + } + + closedir(dirp); + + break; + } + + vim_free(path.string); + vim_free(buf.string); + + ga_append(&str, NUL); + + return str.ga_data; +# endif +} + +/* + * 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. Returns alloced string or NULL on failure. + */ + static char_u * +socketserver_create_path(char_u *name, bool quiet) +{ +# ifdef MSWIN + // Only support channel addresses on Windows + if (STRNICMP(name, "channel:", 8) == 0 && STRLEN(name) > 8) + return vim_strsave(name + 8); + else + { + if (!quiet) + semsg(_(e_invalid_argument_str), name); + return NULL; + } +# else + char_u *buf = NULL; + int buflen = STRLEN(name) + NUMBUFLEN; + char_u *path = NULL; + bool fatal = false; + + if (STRNICMP(name, "channel:", 8) == 0 && STRLEN(name) > 8) + return vim_strsave(name + 8); + if (STRNICMP(name, "name:", 5) == 0) + name += 5; + + for (int i = 0; i < 1000; i++) + { + if (buf != NULL) + { + vim_snprintf((char *)buf, buflen, "%s%d", name, i); + path = socketserver_get_path(buf, true, quiet, &fatal); + } + else + path = socketserver_get_path(name, true, quiet, &fatal); + + if (fatal) + break; + + if (path == NULL) + { + if (buf == NULL) + { + buf = alloc(buflen); + if (buf == NULL) + { + semsg(_(e_out_of_memory_allocating_nr_bytes), buflen); + return NULL; + } + } + continue; + } + break; + } + vim_free(buf); + + return path; +# endif +} + +/* + * 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. + * + * If "name" starts with "channel:", then return the address part + * + * If "new" is true, then return a path if the name does not exist in the known + * location. + * + * If "fatal" is not NULL, then it is set to true if error is fatal. + * + * Returns path on success and NULL on failure. + */ + static char_u * +socketserver_get_path(char_u *name, bool new UNUSED, bool quiet, bool *fatal) +{ +# ifdef MSWIN + // Only support channel addresses on Windows + if (STRNICMP(name, "channel:", 8) == 0 && STRLEN(name) > 8) + return vim_strsave(name + 8); + else + { + if (!quiet) + semsg(_(e_invalid_argument_str), name); + if (fatal != NULL) + *fatal = true; + return NULL; + } +# else + char_u *buf; + bool res = false; + channel_T *channel; + const char_u *known_dirs[] = { + mch_getenv("XDG_RUNTIME_DIR"), + mch_getenv("TMPDIR"), + (char_u *)"/tmp" + }; + + if (name == NULL) + { + if (fatal != NULL) + *fatal = true; + return NULL; + } + + // Ignore if name is a path + if (name[0] == '/' || STRNCMP(name, "./", 2) == 0 || + STRNCMP(name, "../", 3) == 0) + return vim_strsave(name); + + if (STRNICMP(name, "channel:", 8) == 0 && STRLEN(name) > 8) + return vim_strsave(name + 8); + + if (STRNICMP(name, "name:", 5) == 0) + name += 5; + + if (vim_strchr(name, '/') != NULL) + { + if (!quiet) + semsg(_(e_socket_name_no_slashes), name); + if (fatal != NULL) + *fatal = true; + return NULL; + } + + buf = alloc(MAXPATHL); + + if (buf == NULL) + { + if (fatal != NULL) + *fatal = true; + semsg(_(e_out_of_memory_allocating_nr_bytes), MAXPATHL); + return NULL; + } + + for (size_t i = 0; i < ARRAY_LENGTH(known_dirs); i++) + { + const char_u *dir = known_dirs[i]; + bool got = false; + + if (dir == NULL) + continue; + else if (STRCMP(dir, "/tmp") == 0 || + (known_dirs[1] != NULL && STRCMP(dir, known_dirs[1]) == 0)) + { + // "/tmp" or $TMPDIR, must suffix dir with uid + vim_snprintf((char *)buf, MAXPATHL, "%s/vim-%lu", dir, + (unsigned long int)getuid()); + if (vim_mkdir(buf, 0700) == -1 && errno != EEXIST) + continue; + + vim_snprintf((char *)buf, MAXPATHL, "%s/vim-%lu/%s", dir, + (unsigned long int)getuid(), name); + } + else + { + vim_snprintf((char *)buf, MAXPATHL, "%s/vim", dir); + if (vim_mkdir(buf, 0700) == -1 && errno != EEXIST) + continue; + + vim_snprintf((char *)buf, MAXPATHL, "%s/vim/%s", dir, name); + } + + // If looking for a new socket path, and "buf" currently exists, check + // if it is a dead socket, if it is then remove it. + if (new) + { + emsg_silent++; + channel = channel_open_unix((char *)buf, NULL); + emsg_silent--; + + if (channel != NULL) + { + channel_close(channel, false); + channel_clear(channel); + } + else + { + mch_remove(buf); + got = true; + } + } + + res = true; + if (got || (!new && mch_access((char *)buf, F_OK) == 0)) + { + if (server_address != NULL && STRCMP(buf, server_address) == 0) + // Can't connect to itself + break; + return buf; + } + break; + } + + if (!quiet) + { + if (!res) + semsg(_("Failed creating socket directory: %s"), strerror(errno)); + else + semsg(_(e_invalid_server_id_used_str), name); + } + if (!res && fatal != NULL) + *fatal = true; + + vim_free(buf); + return NULL; +# endif +} + +/* + * Callback for when client channel is closed + */ + static void +socketserver_client_close(channel_T *channel) +{ + if (channel == client_channels) + client_channels = channel->ch_ss_next; + if (channel->ch_ss_prev != NULL) + channel->ch_ss_prev->ch_ss_next = channel->ch_ss_next; + if (channel->ch_ss_next != NULL) + channel->ch_ss_next->ch_ss_prev = channel->ch_ss_prev; + + ch_log(NULL, "socketserver: client channel closed"); +} + +/* + * Callback for when server channel accepted new client. + */ + static void +socketserver_accept(channel_T *channel) +{ + channel->ch_socketserver = true; + channel->ch_ss_close_cb = socketserver_client_close; + + channel->ch_ss_next = client_channels; + channel->ch_ss_prev = NULL; + if (client_channels != NULL) + client_channels->ch_ss_prev = channel; + client_channels = channel; + + // We will read the command from the client later in the input loop. + ch_log(NULL, "socketserver: accepted new client"); +} + +/* + * Callback for when server channel is closed + */ + static void +socketserver_close(channel_T *channel UNUSED) +{ + socketserver_cleanup(); + + ch_log(NULL, "socketserver: server channel closed"); +} + +/* + * Mark references to socketserver channels + */ + int +set_ref_in_socketserver_channel(int copyID) +{ + bool abort = false; + channel_T *channel; + typval_T tv; + + tv.v_type = VAR_CHANNEL; + tv.vval.v_channel = server_channel; + abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL, NULL); + + FOR_ALL_CLIENTS(channel) + { + if (abort) + break; + + tv.v_type = VAR_CHANNEL; + tv.vval.v_channel = channel; + abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL, NULL); + } + return abort; +} + +/* + * Serialize "msg" into a string and send it over the given channel "ch". Also + * adds version header to "msg", if "ver" is true. Returns OK on success and + * FAIL on failure. + */ + static int +socketserver_send_message(channel_T *ch, dict_T *msg, char *func, bool ver) +{ + typval_T tv; + char_u *buf; + int ret; + + if (ver) + dict_add_number(msg, "version", PROTOCOL_VER); + + tv.v_type = VAR_DICT; + tv.vval.v_dict = msg; + buf = json_encode(&tv, JSON_NL); + + if (buf != NULL) + { + emsg_silent++; + ret = channel_send(ch, PART_SOCK, buf, (int)STRLEN(buf), func); + emsg_silent--; + } + else + return FAIL; + vim_free(buf); + return ret; +} + +/* + * Execute the JSON message represented by "dict". + */ + static void +socketserver_exec(channel_T *channel, dict_T *message) +{ + dictitem_T *di; + char_u *type; + char_u *str = NULL; + char_u *sender = NULL; + char_u idbuf[11 + NUMBUFLEN]; + + di = dict_find(message, (char_u *)"type", -1); + if (di == NULL || di->di_tv.v_type != VAR_STRING) + return; + else + type = di->di_tv.vval.v_string; + + di = dict_find(message, (char_u *)"str", -1); + if (di != NULL && di->di_tv.v_type == VAR_STRING) + str = di->di_tv.vval.v_string; + + di = dict_find(message, (char_u *)"sender", -1); + if (di != NULL && di->di_tv.v_type == VAR_STRING) + { + sender = di->di_tv.vval.v_string; + + // Save in global + vim_free(client_socket); + client_socket = vim_strsave(sender); + } + + di = dict_find(message, (char_u *)"wait", -1); + if (di != NULL && di->di_tv.v_type == VAR_BOOL && di->di_tv.vval.v_number) + { + // Client is not a server, but still wants a response later. Save the + // ID of the channel connection that we will use to send back a response, + vim_snprintf((char *)idbuf, sizeof(idbuf), "remotewait:%d", + channel->ch_id); + + sender = idbuf; + vim_free(client_socket); + client_socket = vim_strsave(sender); + } + + ch_log(NULL, "socketserver_exec(): result: %s", + str == NULL ? (char_u *)"(null)" : str); + + if (STRCMP(type, "expr") == 0 && str != NULL) + { + // Evaluate expression and send back reply + dict_T *dict; + char_u *result; + int code; + + dict = dict_alloc(); + if (dict == NULL) + return; + + result = eval_client_expr_to_string(str); + code = result == NULL ? -1 : 0; + + dict_add_string(dict, "type", (char_u *)"reply"); + if (result != NULL) + { + dict_add_string(dict, "str", result); + vim_free(result); + } + else + // Error occured, return error message + dict_add_string(dict, "str", + (char_u *)_(e_invalid_expression_received)); + + dict_add_number(dict, "code", code); + + socketserver_send_message(channel, dict, "socketserver_exec", true); + dict_unref(dict); + } + else if (STRCMP(type, "keystrokes") == 0 && str != NULL) + { + // Execute keystrokes + server_to_input_buf(str); + } + else if (STRCMP(type, "notify") == 0) + { + // Notification, execute autocommands and save the reply for later use + if (sender != NULL && str != NULL) + { + ss_reply_T *reply; + + reply = socketserver_add_reply(sender); + + if (reply != NULL) + ga_copy_string(&reply->strings, str); + + apply_autocmds(EVENT_REMOTEREPLY, sender, str, TRUE, curbuf); + } + } + else if (STRCMP(type, "ver") == 0) + { + // A message we sent previously had the wrong version. Emit an error + // message to alert the user. + emsg(_(e_socket_server_version_mismatch)); + } + else + ch_error(NULL, "socketserver: unknown command type '%s'", type); +} + + static int +socketserver_get_message(channel_T *channel, typval_T **tv) +{ + jsonq_T *head = &channel->ch_part[PART_SOCK].ch_json_head; + jsonq_T *json_msg = head->jq_next; + int ver; + + if (json_msg == NULL) + { + // Check the readahead buffer + channel_parse_json(channel, PART_SOCK, true); + json_msg = head->jq_next; + } + if (json_msg == NULL) + return FAIL; + *tv = json_msg->jq_value; + remove_json_node(head, json_msg); + + if ((*tv)->v_type != VAR_DICT) + { + ch_error(NULL, "socketserver: message is not a JSON object"); + free_tv(*tv); + return FAIL; + } + + ver = dict_get_number((*tv)->vval.v_dict, "version"); + + if (ver != 0 && ver != PROTOCOL_VER) + { + dict_T *dict; + + ch_error(NULL, "socketserver: message has different version %d", ver); + free_tv(*tv); + + // Send back a special error + dict = dict_alloc(); + if (dict != NULL) + { + dict_add_string(dict, "type", (char_u *)"ver"); + socketserver_send_message(channel, dict, + "socketserver_get_message", false); + dict_unref(dict); + } + return FAIL; + } + + return OK; +} + +/* + * Parse any commands in the queue and execute them. + */ + void +socketserver_parse_messages(void) +{ + typval_T *tv; + + for (channel_T *ch = client_channels; ch != NULL;) + { + // Make sure to save next channel in case "ch" is freed. Not sure if + // this can actually happen but be safe. + channel_T *next = ch->ch_ss_next; + + // Get the JSON message if there is any from the queue for this channel. + if (socketserver_get_message(ch, &tv) == FAIL) + { + ch = next; + continue; + } + + socketserver_exec(ch, tv->vval.v_dict); + free_tv(tv); + ch = next; + } +} + + +/* + * Poll until there is something to read on "channel". Also handle other + * socketserver channels in the meantime. If "channel" is NULL, then poll all + * channels once then exit. If "timeout" is -1, then wait forever unless + * interrupted. + * + * Return OK on success and FAIL on failure or timeout. + */ + static int +socketserver_wait(channel_T *channel, int timeout) +{ + while (true) + { + int ret; + channel_T *ch; +# ifdef HAVE_SELECT + fd_set rfds; + struct timeval tv; + int maxfd = -1; + + if (timeout != -1) + { + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + } + + FD_ZERO(&rfds); + + if (channel != NULL) + { + if (channel->CH_SOCK_FD == INVALID_FD) + // Shouldn't happen + return FAIL; + + maxfd = channel->CH_SOCK_FD; + FD_SET(channel->CH_SOCK_FD, &rfds); + } + if (server_channel != NULL && server_channel->CH_SOCK_FD != INVALID_FD) + { + FD_SET(server_channel->CH_SOCK_FD, &rfds); + if (server_channel->CH_SOCK_FD > maxfd) + maxfd = server_channel->CH_SOCK_FD; + } + + FOR_ALL_CLIENTS(ch) + { + if (ch->CH_SOCK_FD != INVALID_FD) + { + FD_SET(ch->CH_SOCK_FD, &rfds); + if (maxfd < (int)ch->CH_SOCK_FD) + maxfd = (int)ch->CH_SOCK_FD; + } + } + + if (maxfd == -1) + return FAIL; + + ret = select(maxfd + 1, &rfds, NULL, NULL, timeout == -1 ? NULL : &tv); + +# ifdef EINTR + if (ret == -1 && errno == EINTR) + { + if (got_int) + break; + continue; + } +# endif + + if (ret > 0) + { + if (server_channel != NULL + && server_channel->CH_SOCK_FD != INVALID_FD + && FD_ISSET(server_channel->CH_SOCK_FD, &rfds)) + channel_check(server_channel, PART_SOCK); + + FOR_ALL_CLIENTS(ch) + if (ch->CH_SOCK_FD != INVALID_FD + && FD_ISSET(ch->CH_SOCK_FD, &rfds)) + channel_check(ch, PART_SOCK); + + socketserver_parse_messages(); + + if (channel == NULL) + return OK; + + if (channel->CH_SOCK_FD != INVALID_FD + && FD_ISSET(channel->CH_SOCK_FD, &rfds)) + { + channel_check(channel, PART_SOCK); + return OK; + } + continue; + } +# else + struct pollfd fds[MAX_OPEN_CHANNELS + 1]; + int nfd = 0; + int channel_idx = -1; + int server_idx = -1; + + if (channel != NULL) + { + if (channel->CH_SOCK_FD == INVALID_FD) + // Shouldn't happen + return FAIL; + + channel_idx = nfd; + fds[nfd].fd = channel->CH_SOCK_FD; + fds[nfd++].events = POLLIN; + } + if (server_channel != NULL && server_channel->CH_SOCK_FD != INVALID_FD) + { + server_idx = nfd; + fds[nfd].fd = server_channel->CH_SOCK_FD; + fds[nfd++].events = POLLIN; + } + + FOR_ALL_CLIENTS(ch) + if (ch->CH_SOCK_FD != INVALID_FD) + { + fds[nfd].fd = ch->CH_SOCK_FD; + fds[nfd].events = POLLIN; + ch->ch_part[PART_SOCK].ch_poll_idx = nfd; + nfd++; + } + + ret = poll(fds, nfd, timeout); + +# ifdef EINTR + if (ret == -1 && errno == EINTR) + { + if (got_int) + break; + continue; + } +# endif + + if (ret > 0) + { + if (server_channel != NULL + && server_channel->CH_SOCK_FD != INVALID_FD + && fds[server_idx].revents & POLLIN) + channel_check(server_channel, PART_SOCK); + + FOR_ALL_CLIENTS(ch) + if (ch->CH_SOCK_FD != INVALID_FD + && fds[ch->ch_part[PART_SOCK].ch_poll_idx] + .revents & POLLIN) + channel_check(ch, PART_SOCK); + + socketserver_parse_messages(); + + if (channel == NULL) + return OK; + + if (channel->CH_SOCK_FD != INVALID_FD + && fds[channel_idx].revents & POLLIN) + { + channel_check(channel, PART_SOCK); + return OK; + } + continue; + } +# endif + break; + } + return FAIL; +} + +/* + * Parse "name" and create or get the channel connection for it. If "wait" is + * not NULL, then if the client name is a channel ID, then it will be set to + * true. Returns NULL on + * failure. + */ + static channel_T * +socketserver_get_channel(char_u *name, bool quiet, bool *wait) +{ + char_u *address = NULL; + int port; + bool is_unix = false; + channel_T *channel; + + if (STRNICMP(name, "channel:", 8) == 0) + { + if (channel_parse_socketserver_address(name + 8, &port, &address, true) + == FAIL) + return NULL; + if (address != NULL) + is_unix = true; + } + else if (STRNICMP(name, "remotewait:", 11) == 0) + { + // Channel ID name, find channel with that ID. + int id; + + if (name[11] == NUL) + return NULL; + id = strtol((char *)name + 11, NULL, 10); + if (wait != NULL) + *wait = true; + + return channel_find(id); + } + else + { + address = socketserver_get_path(name, false, quiet, NULL); + if (address == NULL) + return NULL; + is_unix = true; + } + + if (is_unix) + channel = channel_open_unix((char *)address, NULL); + else + channel = channel_open("localhost", port, 1000, NULL); + + if (channel == NULL && !quiet) + semsg(_(e_socket_server_failed_connecting), name); + + vim_free(address); + + return channel; +} + +/* + * Send command to address "name". If "ch" is not NULL, it is set to the channel + * for the connection between us and the server, and the channel will not just + * be closed immediately, this is used for --remote-wait. Returns 0 for OK, -1 + * on error. + */ + int +socketserver_send( + char_u *name, + char_u *str, + char_u **result, + bool is_expr, + int timeout, + bool silent, + channel_T **ch) +{ + int rcode = -1; + channel_T *channel; + dict_T *dict; + dictitem_T *di; + typval_T *resp_tv = NULL; + + if (*name == NUL) + { + semsg(_(e_unable_to_send_to_str), name); + return -1; + } + + // Execute locally if target is ourselves + if (serverName != NULL && STRICMP(name, serverName) == 0) + return sendToLocalVim(str, is_expr, result); + + channel = socketserver_get_channel(name, silent, NULL); + if (channel == NULL) + return -1; + + dict = dict_alloc(); + if (dict == NULL) + goto exit; + + dict_add_string(dict, "type", (char_u *)(is_expr ? "expr" : "keystrokes")); + dict_add_string(dict, "str", str); + + // Tell server who we are so it can save our socket path internally for + // later use with server2client. Only do this if we are actually a server. + // + // If we are not a server, then --remote-wait will not work. To handle this + // case, we add "wait" to the JSON message set to true, so that the server + // will create an internal address for our connection to it. This is + // only used for --remote-wait, not exposed to user. + if (server_address != NULL) + dict_add_string(dict, "sender", server_address); + else if (ch != NULL) + dict_add_bool(dict, "wait", true); + + if (socketserver_send_message(channel, dict, "socketserver_send", true) + == FAIL) + { + semsg(_(e_unable_to_send_to_str), name); + dict_unref(dict); + goto exit; + } + dict_unref(dict); + + if (!is_expr) + { + // Exit, we aren't waiting for a response + rcode = 0; + + if (ch != NULL) + { + *ch = channel; + + channel->ch_ss_next = client_channels; + channel->ch_ss_prev = NULL; + channel->ch_ss_close_cb = socketserver_client_close; + client_channels = channel; + + return rcode; + } + goto exit; + } + + if (timeout == 0) + timeout = 1000; + + // To handle recursive calls, we must handle any socketserver channels as + // well. + while (socketserver_wait(channel, timeout) == OK) + if (socketserver_get_message(channel, &resp_tv) == OK) + break; + + if (resp_tv == NULL) + { + // Channel closed, don't make this an error, because this may be the + // result of the expression (e.g. --remote-expr 'execute("qa!")') + rcode = 0; + goto exit; + } + + dict = resp_tv->vval.v_dict; + + di = dict_find(dict, (char_u *)"type", -1); + if (di == NULL || di->di_tv.v_type != VAR_STRING || + (STRCMP(di->di_tv.vval.v_string, "reply") != 0 + && STRCMP(di->di_tv.vval.v_string, "ver") != 0)) + { + ch_error(NULL, "socketserver: unknown reply type"); + free_tv(resp_tv); + goto exit; + } + + if (STRCMP(di->di_tv.vval.v_string, "ver") == 0) + { + emsg(_(e_socket_server_version_mismatch)); + free_tv(resp_tv); + goto exit; + } + + if (result != NULL) + { + di = dict_find(dict, (char_u *)"str", -1); + if (di != NULL && di->di_tv.v_type == VAR_STRING) + *result = vim_strsave(di->di_tv.vval.v_string); + else + { + free_tv(resp_tv); + goto exit; + } + } + + di = dict_find(dict, (char_u *)"code", -1); + if (di != NULL && di->di_tv.v_type == VAR_NUMBER) + rcode = di->di_tv.vval.v_number; + + free_tv(resp_tv); + +exit: + channel_close(channel, false); + channel_clear(channel); + return rcode; +} + + static ss_reply_T * +socketserver_get_reply(char_u *sender, int *index) +{ + for (int i = 0; i < server_replies.ga_len; i++) + { + ss_reply_T *reply = ((ss_reply_T *)server_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 * +socketserver_add_reply(char_u *sender) +{ + ss_reply_T *reply; + + if (server_replies.ga_growsize == 0) + ga_init2(&server_replies, sizeof(ss_reply_T), 1); + + reply = socketserver_get_reply(sender, NULL); + + if (reply == NULL && ga_grow(&server_replies, 1) == OK) + { + reply = ((ss_reply_T *)server_replies.ga_data) + server_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 +socketserver_remove_reply(char_u *sender) +{ + int index; + ss_reply_T *reply = socketserver_get_reply(sender, &index); + + if (reply != NULL) + { + ss_reply_T *arr = server_replies.ga_data; + int len = server_replies.ga_len; + + // Free strings + vim_free(reply->sender); + ga_clear_strings(&reply->strings); + + // Move all elements after the removed reply forward by one + if (len > 1) + mch_memmove(arr + index, arr + index + 1, + sizeof(ss_reply_T) * (len - index - 1)); + server_replies.ga_len--; + } +} + +/* + * Send a string to "client" as a reply (notification). Returns OK on success + * and FAIL on failure. + */ + int +socketserver_send_reply(char_u *client, char_u *str) +{ + dict_T *dict; + channel_T *channel; + int ret = OK; + bool wait = false; + + if (*client == NUL) + { + semsg(_(e_invalid_server_id_used_str), client); + return FAIL; + } + + if (server_channel == NULL || server_address == NULL) + { + emsg(_(e_socket_server_not_online)); + return FAIL; + } + + channel = socketserver_get_channel(client, false, &wait); + if (channel == NULL) + return FAIL; + + dict = dict_alloc(); + if (dict == NULL) + { + ret = FAIL; + goto exit; + } + + dict_add_string(dict, "type", (char_u *)"notify"); + dict_add_string(dict, "str", str); + if (server_address != NULL) + dict_add_string(dict, "sender", server_address); + + ret = socketserver_send_message(channel, dict, + "socketserver_send_reply", true); + dict_unref(dict); + +exit: + // Don't want to close the channel if client is referenced by channel ID. + // This allows --remote-wait to work with multiple files. + if (!wait) + { + channel_close(channel, false); + channel_clear(channel); + } + + return ret; +} + +/* + * Wait for reply from "client" and place result in "str". Returns OK on success + * and FAIL on failure. Timeout is in milliseconds + */ + int +socketserver_read_reply( + char_u *client, + char_u **str, + int timeout, + bool remotewait) +{ + ss_reply_T *reply = NULL; + char_u *actual; + + if (*client == NUL) + { + semsg(_(e_invalid_server_id_used_str), client); + return FAIL; + } + + if (!remotewait && (server_channel == NULL || server_address == NULL)) + { + emsg(_(e_socket_server_not_online)); + return FAIL; + } + + actual = socketserver_get_path(client, false, false, NULL); + if (actual == NULL) + return FAIL; + + while (true) + { + reply = socketserver_get_reply(actual, NULL); + if (reply != NULL) + break; + if (socketserver_wait(NULL, timeout) == FAIL) + break; + } + + if (reply == NULL || reply->strings.ga_len == 0) + { + vim_free(actual); + return FAIL; + } + + // Consume the string + *str = ((char_u **)reply->strings.ga_data)[0]; + + if (reply->strings.ga_len > 1) + mch_memmove((char_u **)reply->strings.ga_data, + ((char_u **)reply->strings.ga_data) + 1, + sizeof(char_u *) * (reply->strings.ga_len - 1)); + reply->strings.ga_len--; + + if (reply->strings.ga_len < 1) + // Last string removed, remove the reply + socketserver_remove_reply(actual); + + vim_free(actual); + + return OK; +} + +/* + * Check for any replies for "sender". Returns 1 if there is and places the + * reply in "str" without consuming it (note that a copy is not created). + * Returns 0 if otherwise and -1 on + * error. + */ + int +socketserver_peek_reply(char_u *sender, char_u **str) +{ + ss_reply_T *reply; + char_u *actual; + + if (*sender == NUL) + { + semsg(_(e_invalid_server_id_used_str), sender); + return FAIL; + } + + if (server_channel == NULL || server_address == NULL) + { + emsg(_(e_socket_server_not_online)); + return FAIL; + } + + actual = socketserver_get_path(sender, false, false, NULL); + if (actual == NULL) + return FAIL; + + reply = socketserver_get_reply(actual, NULL); + vim_free(actual); + + if (reply != NULL && reply->strings.ga_len > 0) + { + if (str != NULL) + *str = ((char_u **)reply->strings.ga_data)[0]; + return 1; + } + return 0; +} + +#endif // FEAT_SOCKETSERVER diff --git a/src/structs.h b/src/structs.h index 6c6ea8c0e4..8cb9b3f91d 100644 --- a/src/structs.h +++ b/src/structs.h @@ -2795,6 +2795,13 @@ struct channel_S { void (*ch_nb_close_cb)(void); // callback for Netbeans when channel is // closed +#ifdef FEAT_SOCKETSERVER + bool ch_socketserver; // If channel is used by socketserver + void (*ch_ss_close_cb)(channel_T *); + void (*ch_ss_accept_cb)(channel_T *); + channel_T *ch_ss_next; + channel_T *ch_ss_prev; +#endif #ifdef MSWIN int ch_named_pipe; // using named pipe instead of pty diff --git a/src/testdir/dumps/Test_clientserver_1.dump b/src/testdir/dumps/Test_clientserver_1.dump new file mode 100644 index 0000000000..a4a0b42564 --- /dev/null +++ b/src/testdir/dumps/Test_clientserver_1.dump @@ -0,0 +1,8 @@ +|~+0#4040ff13#ffffff0| @73 +|~| @73 +|~| @73 +|E+0#ffffff16#e000002|1|5|6|7|:| |S|o|c|k|e|t| |s|e|r|v|e|r| |p|r|o|t|o|c|o|l| |v|e|r|s|i|o|n| |m|i|s|m|a|t|c|h|,| |c|h|e|c|k| |w|h|a|t| |V|i|m| |v|e|r|s|i|o|n| |y|o|u| +|a|r|e| |u|s|i|n|g| +0#0000000#ffffff0@65 +|E+0#ffffff16#e000002|2|4|1|:| |U|n|a|b|l|e| |t|o| |s|e|n|d| |t|o| |c|h|a|n@1|e|l|:|2|0@2| +0#0000000#ffffff0@38 +@75 +|P+0#00e0003&|r|e|s@1| |E|N|T|E|R| |o|r| |t|y|p|e| |c|o|m@1|a|n|d| |t|o| |c|o|n|t|i|n|u|e> +0#0000000&@35 diff --git a/src/testdir/test_clientserver.vim b/src/testdir/test_clientserver.vim index 0ced9cbc41..15918f7267 100644 --- a/src/testdir/test_clientserver.vim +++ b/src/testdir/test_clientserver.vim @@ -1,5 +1,6 @@ " Tests for the +clientserver feature. +source util/screendump.vim CheckFeature job if !has('clientserver') @@ -10,10 +11,6 @@ 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. -RunSocketServer - func Check_X11_Connection() if has('x11') CheckX11 @@ -192,9 +189,9 @@ func Test_client_server() " 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']) + if (has('socketserver') && !has('X11') && !has('win32')) || index(v:argv, "socket") != -1 + call assert_fails("let x = remote_read('vim/10')", ['E1564:']) + call assert_fails("call server2client('x/b/c', 'xyz')", ['E1564:']) else call assert_fails("let x = remote_read('vim10')", \ has('unix') ? ['E573:.*vim10'] : 'E277:') @@ -246,89 +243,65 @@ 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 +" Test if socket server, X11, and mswin backends can be chosen and work properly. +func Test_client_server_multiple_backends() + CheckFeature socketserver - let g:test_is_flaky = 1 - let cmd = GetVimCommand() + 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_server_socket_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') + if cmd == '' + throw 'GetVimCommand() failed' endif - endtry -endfunc + call Check_X11_Connection() + + let types = [ + \ ['socket', "channel:2000"], + \ ['x11', "TEST"], + \ ['mswin', "TEST"], + \ ] + + for [type, expected] in types + if (type == 'x11' && (!has('x11') || !empty($WAYLAND_DISPLAY) || empty($DISPLAY)) + \ || (type == 'mswin' && !has('win32'))) + continue + endif + if has('win32') && has('gui_running') + " Windows gVim --remote-expr shows a dialog window, which blocks tests + " from running. Using --gui-dialog-file does not seem to work either. + continue + endif + + let actual_cmd = cmd .. ' --clientserver ' .. type + let actual_cmd ..= ' --servername ' .. expected + let job = job_start(actual_cmd, {'stoponexit': 'kill', 'out_io': 'null'}) + + call WaitForAssert({-> assert_equal("run", job_status(job))}) + call assert_match(expected, system(actual_cmd .. ' --remote-expr "v:servername"')) + + " On Windows using --remote-expr causes E282, possibly due to some shell + " escaping quirk? When gtk gui is running, using system() seems to cause a + " deadlock when using the x11 backend only... don't use it for now... + if has('win32') || has('gui_running') + call job_stop(job, 'kill') + else + call system(actual_cmd .. " --remote-expr 'execute(\"qa!\")'") + endif + 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 custom paths work for socketserver -func Test_client_socket_server_custom_path() - CheckNotMSWindows +func Test_client_server_socketserver_custom_path() CheckFeature socketserver - CheckNotFeature x11 + CheckNotMSWindows let g:test_is_flaky = 1 let cmd = GetVimCommand() @@ -342,7 +315,7 @@ func Test_client_socket_server_custom_path() let paths = ['./' .. name, '../testdir/' .. name, getcwd(-1) .. '/' .. name] for path in paths - let actual = cmd .. ' --servername ' .. path + let actual = cmd .. ' --clientserver socket --servername ' .. path let job = job_start(actual, {'stoponexit': 'kill', 'out_io': 'null'}) @@ -361,6 +334,170 @@ func Test_client_socket_server_custom_path() endfor endfunc +" Test if "channel:" prefix works correctly to use channel address for +" socketserver. +func Test_client_server_socketserver_address() + CheckFeature socketserver + + let g:test_is_flaky = 1 + let cmd = GetVimCommand() + + if cmd == '' + throw 'GetVimCommand() failed' + endif + + let actual = cmd .. ' --clientserver socket --servername channel:2000' + + let job = job_start(actual, {'stoponexit': 'kill', 'out_io': 'null'}) + + call WaitForAssert({-> assert_equal("run", job_status(job))}) + + if !has('win32') || !has('gui_running') + " Does not work with gVim on Windows because it shows an OK dialog box which + " blocks tests from running + call assert_match('channel:2000', system(actual .. ' --remote-expr "v:servername"')) + endif + + if has('win32') + call job_stop(job, 'kill') + else + call system(actual .. " --remote-expr 'execute(\"qa!\")'") + endif + 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 --remote-wait works properly with multiple files +func Test_client_server_multiple_remote_wait() + CheckRunVimInTerminal + + call Check_X11_Connection() + + let buf = RunVimInTerminal('--servername TEST', {'rows': 8}) + call TermWait(buf) + + call writefile(["1"], 'XRemoteOne', 'D') + call writefile(["2"], 'XRemoteTwo', 'D') + + let cmd = GetVimCommand() + + if cmd == '' + throw 'GetVimCommand() failed' + endif + + let actual = cmd .. ' -n --servername TEST --remote-wait ./XRemoteOne ./XRemoteTwo' + let job = job_start(actual, {'stoponexit': 'kill', 'out_io': 'null'}) + + sleep 500m " Wait for server to receive request + call assert_equal("run", job_status(job)) + + call term_sendkeys(buf, "\:next\") + call TermWait(buf) + call assert_equal("run", job_status(job)) + + call term_sendkeys(buf, "\:q!\") " Don't use qa! because we only want to quit one file + call WaitForAssert({-> assert_equal("dead", job_status(job))}) +endfunc + +" Check if socket is removed even if Vim changes directory +func Test_client_server_socketserver_chdir() + CheckFeature socketserver + CheckRunVimInTerminal + CheckNotMSWindows + + let buf = RunVimInTerminal('--clientserver socket --servername ./TEST', + \ {'rows': 8}) + call TermWait(buf) + + call term_sendkeys(buf, "\:cd ../\") + call StopVimInTerminal(buf) + + call assert_equal("", glob("./TEST")) +endfunc + +func DummyServerCallback(ch, addr) + let msg = json_encode(#{type: "ver"}) .. "\n" + call ch_sendraw(a:ch, msg) +endfunc + +" Test if commands with different version are ignored and handled properly. +func Test_client_server_socketserver_version_mismatch() + CheckFeature socketserver + CheckRunVimInTerminal + CheckScreendump + + let cmd = GetVimCommand() + + if cmd == '' + throw 'GetVimCommand() failed' + endif + + let buf = RunVimInTerminal('--clientserver socket --servername + \ channel:2000', {'rows': 8}) + call TermWait(buf) + + let ch = ch_open('localhost:2000', #{mode: 'nl'}) + let msg = json_encode(#{ + \ type: "expr", + \ str: "v:servername", + \ version: 999999999 + \ }) .. "\n" + + call ch_sendraw(ch, msg) + let resp = ch_readraw(ch) + + call assert_equal(#{type: "ver"}, json_decode(resp)) + + call StopVimInTerminal(buf) + + let ch = ch_listen("2000", #{ + \ mode: 'nl', + \ callback: function('DummyServerCallback') + \ }) + + let buf = RunVimInTerminal('--clientserver socket --servername channel:3000', {'rows': 8}) + call TermWait(buf) + + call term_sendkeys(buf, "\:echo remote_expr('channel:2000', 'v:servername', '', 1)\") + call TermWait(buf) + + call VerifyScreenDump(buf, 'Test_clientserver_1', #{wait: 3000}) + + call StopVimInTerminal(buf) +endfunc + +" Test if invalid messages do not crash Vim socketserver. +func Test_clientserver_socketserver_invalid_msg() + CheckFeature socketserver + CheckRunVimInTerminal + + let cmd = GetVimCommand() + + if cmd == '' + throw 'GetVimCommand() failed' + endif + + let buf = RunVimInTerminal('--clientserver socket --servername + \ channel:3000', {'rows': 8}) + call TermWait(buf) + + let ch = ch_open('localhost:3000', #{mode: 'nl'}) + + call ch_sendraw(ch, "wjdaljdsjalsj\n") + call ch_sendraw(ch, "{\"type\": \"unknown\"}\n") + + call assert_match("channel:3000", system(cmd .. " --clientserver socket --servername channel:3000 --remote-expr 'v:servername'")) + call assert_equal("running", term_getstatus(buf)) + + call StopVimInTerminal(buf) +endfunc + " Uncomment this line to get a debugging log " call ch_logfile('channellog', 'w') diff --git a/src/testdir/test_remote.vim b/src/testdir/test_remote.vim index ad959e629c..9f23bb0ce1 100644 --- a/src/testdir/test_remote.vim +++ b/src/testdir/test_remote.vim @@ -16,18 +16,6 @@ func Verify_remote_feature_works() let buf = RunVimInTerminal('--servername XVIMTEST', {'rows': 8}) call TermWait(buf) - " For some reason when the socket server is being used, the terminal Vim never - " receives the `:w! XVimRemoteTest.txt` command from term_sendkeys. - if has('socketserver') && !has('X11') - if match(serverlist(), "XVIMTEST") == -1 - call StopVimInTerminal(buf) - throw s:skip - endif - - let s:remote = 1 - return - endif - let cmd = GetVimCommandCleanTerm() .. '--serverlist' call term_sendkeys(buf, ":r! " .. cmd .. "\") call TermWait(buf) @@ -71,6 +59,14 @@ func Test_remote_servername() " open XTEST.txt, if wildignore setting is not ignored, the server " will continue with the Xdummy.log file let buf2 = RunVimInTerminal('--servername XVIMTEST --remote-silent XTEST.txt', {'rows': 5, 'wait_for_ruler': 0}) + + " It may take a while for the command to be sent to XVIMTEST and be executed. + " Do a blocking --remote-expr so that is assured the command was executed. + botright new + let buf3 = RunVimInTerminal('--servername XVIMTEST --remote-expr "v:servername"', {'rows': 5, 'wait_for_ruler': 0}) + call WaitForAssert({-> assert_equal("finished", term_getstatus(buf3))}) + exe buf3 .. 'bw!' + " job should be no-longer running, so we can just close it exe buf2 .. 'bw!' call term_sendkeys(buf, ":sil :3,$d\") diff --git a/src/testdir/test_usercommands.vim b/src/testdir/test_usercommands.vim index 6ad39787e3..8b90a826e4 100644 --- a/src/testdir/test_usercommands.vim +++ b/src/testdir/test_usercommands.vim @@ -402,7 +402,6 @@ func Test_CmdCompletion() exe 'delcommand ' .. cmd endfor delcommand MissingFeature - delcommand RunSocketServer command! DoCmd1 : command! DoCmd2 : diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim index b74f324aef..3f95ee8b25 100644 --- a/src/testdir/test_vim9_builtin.vim +++ b/src/testdir/test_vim9_builtin.vim @@ -1,7 +1,6 @@ " Test using builtin functions in the Vim9 script language. source util/screendump.vim -source util/socketserver.vim import './util/vim9.vim' as v9 " Test for passing too many or too few arguments to builtin functions @@ -3525,11 +3524,12 @@ enddef def Test_remote_expr() CheckFeature clientserver - TrySocketServer + CheckNotMSWindows - if !g:socketserver_only + if has("x11") CheckEnv DISPLAY endif + v9.CheckSourceDefAndScriptFailure(['remote_expr(1, "b")'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1']) v9.CheckSourceDefAndScriptFailure(['remote_expr("a", 2)'], ['E1013: Argument 2: type mismatch, expected string but got number', 'E1174: String required for argument 2']) v9.CheckSourceDefAndScriptFailure(['remote_expr("a", "b", 3)'], ['E1013: Argument 3: type mismatch, expected string but got number', 'E1174: String required for argument 3']) @@ -3551,10 +3551,12 @@ enddef def Test_remote_peek() CheckFeature clientserver - TrySocketServer - if !g:socketserver_only + CheckNotMSWindows + + if has("x11") CheckEnv DISPLAY endif + v9.CheckSourceDefAndScriptFailure(['remote_peek(0z10)'], ['E1013: Argument 1: type mismatch, expected string but got blob', 'E1174: String required for argument 1']) v9.CheckSourceDefAndScriptFailure(['remote_peek("a5b6c7", [1])'], ['E1013: Argument 2: type mismatch, expected string but got list', 'E1174: String required for argument 2']) v9.CheckSourceDefExecAndScriptFailure(['remote_peek("")'], 'E573: Invalid server id used') @@ -3562,7 +3564,12 @@ enddef def Test_remote_read() CheckFeature clientserver - CheckEnv DISPLAY + CheckNotMSWindows + + if has("x11") + CheckEnv DISPLAY + endif + v9.CheckSourceDefAndScriptFailure(['remote_read(1)'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1']) v9.CheckSourceDefAndScriptFailure(['remote_read("a", "x")'], ['E1013: Argument 2: type mismatch, expected number but got string', 'E1210: Number required for argument 2']) v9.CheckSourceDefExecAndScriptFailure(['remote_read("")'], 'E573: Invalid server id used') @@ -3570,10 +3577,12 @@ enddef def Test_remote_send() CheckFeature clientserver - TrySocketServer - if !g:socketserver_only + CheckNotMSWindows + + if has("x11") CheckEnv DISPLAY endif + v9.CheckSourceDefAndScriptFailure(['remote_send(1, "b")'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1']) v9.CheckSourceDefAndScriptFailure(['remote_send("a", 2)'], ['E1013: Argument 2: type mismatch, expected string but got number', 'E1174: String required for argument 2']) v9.CheckSourceDefAndScriptFailure(['remote_send("a", "b", 3)'], ['E1013: Argument 3: type mismatch, expected string but got number', 'E1174: String required for argument 3']) @@ -3582,10 +3591,12 @@ enddef def Test_remote_startserver() CheckFeature clientserver - TrySocketServer - if !g:socketserver_only + CheckNotMSWindows + + if has("x11") CheckEnv DISPLAY endif + v9.CheckSourceDefAndScriptFailure(['remote_startserver({})'], ['E1013: Argument 1: type mismatch, expected string but got dict', 'E1174: String required for argument 1']) enddef diff --git a/src/testdir/util/check.vim b/src/testdir/util/check.vim index 8df985091c..c69d399441 100644 --- a/src/testdir/util/check.vim +++ b/src/testdir/util/check.vim @@ -348,17 +348,6 @@ func CheckGithubActions() endif endfunc -command RunSocketServer call RunSocketServer() -func RunSocketServer() - if has("socketserver") && v:servername == "" - try - call remote_startserver('VIMSOCKETSERVERTEST') - catch " not possible to start a remote server - throw 'Skipped: Cannot start remote server' - endtry - endif -endfunc - let &cpo = s:cpo_save unlet s:cpo_save " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/testdir/util/socketserver.vim b/src/testdir/util/socketserver.vim deleted file mode 100644 index 5492db626d..0000000000 --- a/src/testdir/util/socketserver.vim +++ /dev/null @@ -1,17 +0,0 @@ -" Check if only the socketserver backend is available for clientserver (only on -" Unix), and set g:socketserver_only to v:true along with starting the -" socketserver. -command TrySocketServer call TrySocketServer() -func TrySocketServer() - if has("socketserver") && !has("x11") - let g:socketserver_only = v:true - - if v:servername == "" - call remote_startserver('VIMSOCKETSERVERTEST') - endif - else - let g:socketserver_only = v:false - endif -endfunc - -" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/ui.c b/src/ui.c index aa985e4ee8..f277ba4075 100644 --- a/src/ui.c +++ b/src/ui.c @@ -406,20 +406,10 @@ inchar_loop( # endif if ((resize_func != NULL && resize_func(TRUE)) -# if defined(FEAT_CLIENTSERVER) && defined(UNIX) - || ( -# 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 - ) +# if defined(FEAT_CLIENTSERVER) && defined(FEAT_X11) + || + (clientserver_method == CLIENTSERVER_METHOD_X11 && + server_waiting()) # endif # ifdef MESSAGE_QUEUE || interrupted diff --git a/src/version.c b/src/version.c index 7be1f150ba..ce8c620a92 100644 --- a/src/version.c +++ b/src/version.c @@ -729,6 +729,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 512, /**/ 511, /**/