patch 9.2.0153: No support to act as a channel server
Commit:
https://github.com/vim/vim/commit/ba861f8c533023edb3cde6ff7c91926f0a248e54
Author: Yasuhiro Matsumoto <[email protected]>
Date: Fri Mar 13 19:05:45 2026 +0000
patch 9.2.0153: No support to act as a channel server
Problem: Vim can only act as a channel client (ch_open). There is
no way for an external process to initiate a connection
to a running Vim instance using the Channel API.
Solution: Implement ch_listen() and the underlying server-side
socket logic. This allows Vim to listen on a port or
Unix domain socket. When a client connects, a new
channel is automatically created and passed to a
user-defined callback (Yasuhiro Matsumoto).
closes: #19231
Co-authored-by: Christian Brabandt <[email protected]>
Co-authored-by: Copilot <[email protected]>
Signed-off-by: Yasuhiro Matsumoto <[email protected]>
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index d95f459a9..45072ab16 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -1,4 +1,4 @@
-*builtin.txt* For Vim version 9.2. Last change: 2026 Feb 14
+*builtin.txt* For Vim version 9.2. Last change: 2026 Mar 13
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -106,6 +106,8 @@ ch_evalraw({handle}, {string} [, {options}])
ch_getbufnr({handle}, {what}) Number get buffer number for {handle}/{what}
ch_getjob({channel}) Job get the Job of {channel}
ch_info({handle}) String info about channel {handle}
+ch_listen({address} [, {options}])
+ Channel listen on {address}
ch_log({msg} [, {handle}]) none write {msg} in the channel log file
ch_logfile({fname} [, {mode}]) none start logging channel activity
ch_open({address} [, {options}])
diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt
index 36694af9f..7ad10fa0c 100644
--- a/runtime/doc/channel.txt
+++ b/runtime/doc/channel.txt
@@ -1,4 +1,4 @@
-*channel.txt* For Vim version 9.2. Last change: 2026 Feb 25
+*channel.txt* For Vim version 9.2. Last change: 2026 Mar 13
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -114,6 +114,32 @@ Vim to write lines in log file: >
call ch_logfile('channellog', 'w')
See |ch_logfile()|.
+You can also make Vim act as a server using |ch_listen()|. This does not
+require an external server program.
+
+ *channel-listen-demo*
+Start Vim and create a listening channel: >
+ func OnAccept(channel, clientaddr)
+ " Log the connection
+ echomsg "Accepted connection from " .. a:clientaddr
+
+ " Get current time and send it to the client
+ let current_time = strftime("%Y-%m-%d %H:%M:%S")
+ call ch_sendraw(a:channel, "Vim Server Time: " .. current_time .. "
")
+
+ " Optional: close immediately if you only want to provide the time
+ call ch_close(a:channel)
+ endfunc
+
+ " Start listening on port 8765
+ let server = ch_listen('localhost:8765', {"callback": "OnAccept"})
+
+From another Vim instance (or any program) you can connect to it: >
+ let channel = ch_open('localhost:8765')
+
+When done, close the server channel: >
+ call ch_close(server)
+
==============================================================================
3. Opening a channel *channel-open*
@@ -641,6 +667,33 @@ ch_info({handle})
*ch_info()*
<
Return type: dict<any>
+ch_listen({address} [, {options}]) *E1573* *E1574* *ch_listen()*
+ Listen on {address} for incoming channel connections.
+ This creates a server-side channel, unlike |ch_open()|
+ which connects to an existing server.
+ Returns a Channel. Use |ch_status()| to check for failure.
+
+ {address} is a String, see |channel-address| for the possible
+ accepted forms. Note: IPv6 is not yet supported.
+
+ If {options} is given it must be a |Dictionary|.
+ See |channel-open-options|.
+ The "callback" in {options} is invoked when a new
+ connection is accepted. It receives two arguments: the
+ new Channel and the client address as a String (e.g.
+ "127.0.0.1:12345").
+
+ Use |ch_open()| to connect to an existing server instead.
+
+ See |channel-listen-demo| for an example.
+
+ Can also be used as a |method|: >
+ GetAddress()->ch_listen()
+<
+ {only available when compiled with the |+channel| feature}
+
+ Return type: channel
+
ch_log({msg} [, {handle}]) *ch_log()*
Write String {msg} in the channel log file, if it was opened
with |ch_logfile()|.
@@ -695,6 +748,9 @@ ch_open({address} [, {options}])
*ch_open()*
If {options} is given it must be a |Dictionary|.
See |channel-open-options|.
+ Use |ch_listen()| to listen for incoming connections
+ instead.
+
Can also be used as a |method|: >
GetAddress()->ch_open()
<
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 723880e1b..f5950e085 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -4770,6 +4770,8 @@ E157 sign.txt /*E157*
E1570 builtin.txt /*E1570*
E1571 builtin.txt /*E1571*
E1572 options.txt /*E1572*
+E1573 channel.txt /*E1573*
+E1574 channel.txt /*E1574*
E158 sign.txt /*E158*
E159 sign.txt /*E159*
E16 cmdline.txt /*E16*
@@ -6584,6 +6586,7 @@ ch_evalraw() channel.txt /*ch_evalraw()*
ch_getbufnr() channel.txt /*ch_getbufnr()*
ch_getjob() channel.txt /*ch_getjob()*
ch_info() channel.txt /*ch_info()*
+ch_listen() channel.txt /*ch_listen()*
ch_log() channel.txt /*ch_log()*
ch_logfile() channel.txt /*ch_logfile()*
ch_open() channel.txt /*ch_open()*
@@ -6634,6 +6637,7 @@ channel-demo channel.txt /*channel-demo*
channel-drop channel.txt /*channel-drop*
channel-functions usr_41.txt /*channel-functions*
channel-functions-details channel.txt /*channel-functions-details*
+channel-listen-demo channel.txt /*channel-listen-demo*
channel-mode channel.txt /*channel-mode*
channel-more channel.txt /*channel-more*
channel-noblock channel.txt /*channel-noblock*
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index b5c572a07..e6ca80457 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -1287,23 +1287,24 @@ Testing:
*test-functions*
Inter-process communication: *channel-functions*
ch_canread() check if there is something to read
- ch_open() open a channel
ch_close() close a channel
ch_close_in() close the in part of a channel
+ ch_evalexpr() evaluate an expression over channel
+ ch_evalraw() evaluate a raw string over channel
+ ch_getbufnr() get the buffer number for a channel
+ ch_getjob() get the Job of a channel
+ ch_info() get information about a channel
+ ch_listen() listen on a channel
+ ch_log() write a message in the channel log
+ ch_logfile() start logging channel activity
+ ch_open() open a channel
ch_read() read a message from a channel
ch_readblob() read a Blob from a channel
ch_readraw() read a raw message from a channel
ch_sendexpr() send a JSON message over a channel
ch_sendraw() send a raw message over a channel
- ch_evalexpr() evaluate an expression over channel
- ch_evalraw() evaluate a raw string over channel
+ ch_setoptions() set channel options
ch_status() get status of a channel
- ch_getbufnr() get the buffer number of a channel
- ch_getjob() get the job associated with a channel
- ch_info() get channel information
- ch_log() write a message in the channel log file
- ch_logfile() set the channel log file
- ch_setoptions() set the options for a channel
json_encode() encode an expression to a JSON string
json_decode() decode a JSON string to Vim types
js_encode() encode an expression to a JSON string
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 1e7dcf70c..cee00a282 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 Mar 12
+*version9.txt* For Vim version 9.2. Last change: 2026 Mar 13
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52606,6 +52606,10 @@ Added ~
-----
Various syntax, indent and other plugins were added.
+Functions: ~
+
+|ch_listen()| listen on {address}
+
Autocommands: ~
|SessionLoadPre| before loading a |Session| file
diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim
index dc0356eae..ed7ce5fb0 100644
--- a/runtime/syntax/vim.vim
+++ b/runtime/syntax/vim.vim
@@ -2,7 +2,7 @@
" Language: Vim script
" Maintainer: Hirohito Higashi <h.east.727 ATMARK gmail.com>
" Doug Kearns <[email protected]>
-" Last Change: 2026 Mar 11
+" Last Change: 2026 Mar 13
" Former Maintainer: Charles E. Campbell
" DO NOT CHANGE DIRECTLY.
@@ -153,8 +153,8 @@ syn case match
" Function Names {{{2
" GEN_SYN_VIM: vimFuncName, START_STR='syn keyword vimFuncName contained',
END_STR=''
-syn keyword vimFuncName contained abs acos add and append appendbufline argc
argidx arglistid argv asin assert_beeps assert_equal assert_equalfile
assert_exception assert_fails assert_false assert_inrange assert_match
assert_nobeep assert_notequal assert_notmatch assert_report assert_true atan
atan2 autocmd_add autocmd_delete autocmd_get balloon_gettext balloon_show
balloon_split base64_decode base64_encode bindtextdomain blob2list blob2str
browse browsedir bufadd bufexists buflisted bufload bufloaded bufname bufnr
bufwinid bufwinnr byte2line byteidx byteidxcomp call ceil ch_canread ch_close
ch_close_in ch_evalexpr ch_evalraw ch_getbufnr ch_getjob ch_info ch_log
ch_logfile ch_open ch_read ch_readblob ch_readraw ch_sendexpr ch_sendraw
ch_setoptions ch_status changenr
-syn keyword vimFuncName contained char2nr charclass charcol charidx chdir
cindent clearmatches cmdcomplete_info col complete complete_add complete_check
complete_info confirm copy cos cosh count cscope_connection cursor debugbreak
deepcopy delete deletebufline did_filetype diff diff_filler diff_hlID
digraph_get digraph_getlist digraph_set digraph_setlist echoraw empty environ
err_teapot escape eval eventhandler executable execute exepath exists
exists_compiled exp expand expandcmd extend extendnew feedkeys filecopy
filereadable filewritable filter finddir findfile flatten flattennew float2nr
floor fmod fnameescape fnamemodify foldclosed foldclosedend foldlevel foldtext
foldtextresult foreach foreground fullcommand funcref function garbagecollect
get getbufinfo
+syn keyword vimFuncName contained abs acos add and append appendbufline argc
argidx arglistid argv asin assert_beeps assert_equal assert_equalfile
assert_exception assert_fails assert_false assert_inrange assert_match
assert_nobeep assert_notequal assert_notmatch assert_report assert_true atan
atan2 autocmd_add autocmd_delete autocmd_get balloon_gettext balloon_show
balloon_split base64_decode base64_encode bindtextdomain blob2list blob2str
browse browsedir bufadd bufexists buflisted bufload bufloaded bufname bufnr
bufwinid bufwinnr byte2line byteidx byteidxcomp call ceil ch_canread ch_close
ch_close_in ch_evalexpr ch_evalraw ch_getbufnr ch_getjob ch_info ch_listen
ch_log ch_logfile ch_open ch_read ch_readblob ch_readraw ch_sendexpr ch_sendraw
ch_setoptions ch_status
+syn keyword vimFuncName contained changenr char2nr charclass charcol charidx
chdir cindent clearmatches cmdcomplete_info col complete complete_add
complete_check complete_info confirm copy cos cosh count cscope_connection
cursor debugbreak deepcopy delete deletebufline did_filetype diff diff_filler
diff_hlID digraph_get digraph_getlist digraph_set digraph_setlist echoraw empty
environ err_teapot escape eval eventhandler executable execute exepath exists
exists_compiled exp expand expandcmd extend extendnew feedkeys filecopy
filereadable filewritable filter finddir findfile flatten flattennew float2nr
floor fmod fnameescape fnamemodify foldclosed foldclosedend foldlevel foldtext
foldtextresult foreach foreground fullcommand funcref function garbagecollect
get getbufinfo
syn keyword vimFuncName contained getbufline getbufoneline getbufvar
getcellpixels getcellwidths getchangelist getchar getcharmod getcharpos
getcharsearch getcharstr getcmdcomplpat getcmdcompltype getcmdline getcmdpos
getcmdprompt getcmdscreenpos getcmdtype getcmdwintype getcompletion
getcompletiontype getcurpos getcursorcharpos getcwd getenv getfontname getfperm
getfsize getftime getftype getimstatus getjumplist getline getloclist
getmarklist getmatches getmousepos getmouseshape getpid getpos getqflist getreg
getreginfo getregion getregionpos getregtype getscriptinfo getstacktrace
gettabinfo gettabvar gettabwinvar gettagstack gettext getwininfo getwinpos
getwinposx getwinposy getwinvar glob glob2regpat globpath has has_key
haslocaldir hasmapto histadd histdel
syn keyword vimFuncName contained histget histnr hlID hlexists hlget hlset
hostname iconv id indent index indexof input inputdialog inputlist inputrestore
inputsave inputsecret insert instanceof interrupt invert isabsolutepath
isdirectory isinf islocked isnan items job_getchannel job_info job_setoptions
job_start job_status job_stop join js_decode js_encode json_decode json_encode
keys keytrans len libcall libcallnr line line2byte lispindent list2blob
list2str list2tuple listener_add listener_flush listener_remove localtime log
log10 luaeval map maparg mapcheck maplist mapnew mapset match matchadd
matchaddpos matcharg matchbufline matchdelete matchend matchfuzzy matchfuzzypos
matchlist matchstr matchstrlist matchstrpos max menu_info min mkdir mode mzeval
nextnonblank
syn keyword vimFuncName contained ngettext nr2char or pathshorten perleval
popup_atcursor popup_beval popup_clear popup_close popup_create popup_dialog
popup_filter_menu popup_filter_yesno popup_findecho popup_findinfo
popup_findpreview popup_getoptions popup_getpos popup_hide popup_list
popup_locate popup_menu popup_move popup_notification popup_setbuf
popup_setoptions popup_settext popup_show pow preinserted prevnonblank printf
prompt_getprompt prompt_setcallback prompt_setinterrupt prompt_setprompt
prop_add prop_add_list prop_clear prop_find prop_list prop_remove prop_type_add
prop_type_change prop_type_delete prop_type_get prop_type_list pum_getpos
pumvisible py3eval pyeval pyxeval rand range readblob readdir readdirex
readfile redraw_listener_add redraw_listener_remove
diff --git a/src/channel.c b/src/channel.c
index 92b95f9b6..78a462cc9 100644
--- a/src/channel.c
+++ b/src/channel.c
@@ -1394,7 +1394,307 @@ theend:
return channel;
}
- void
+/*
+ * Implements ch_listen().
+ */
+ channel_T *
+channel_listen_func(typval_T *argvars)
+{
+ char_u *address;
+ char_u *p;
+ char *rest;
+ int port;
+ int is_unix = FALSE;
+ jobopt_T opt;
+ channel_T *channel = NULL;
+
+ if (in_vim9script()
+ && (check_for_string_arg(argvars, 0) == FAIL
+ || check_for_opt_dict_arg(argvars, 1) == FAIL))
+ return NULL;
+
+ address = tv_get_string(&argvars[0]);
+ if (argvars[1].v_type != VAR_UNKNOWN
+ && check_for_nonnull_dict_arg(argvars, 1) == FAIL)
+ return NULL;
+
+ if (*address == NUL)
+ {
+ semsg(_(e_invalid_argument_str), address);
+ return NULL;
+ }
+
+ if (!STRNCMP(address, "unix:", 5))
+ {
+ is_unix = TRUE;
+ address += 5;
+ port = 0;
+ }
+ else if (*address == '[')
+ {
+ // ipv6 address
+ p = vim_strchr(address + 1, ']');
+ if (p == NULL || *++p != ':')
+ {
+ semsg(_(e_invalid_argument_str), address);
+ return NULL;
+ }
+ port = strtol((char *)(p + 1), &rest, 10);
+ if (port <= 0 || port >= 65536 || *rest != NUL)
+ {
+ semsg(_(e_invalid_argument_str), address);
+ return NULL;
+ }
+ // strip '[' and ']'
+ ++address;
+ *(p - 1) = NUL;
+ }
+ else
+ {
+ // ipv4 address
+ p = vim_strchr(address, ':');
+ if (p == NULL)
+ {
+ semsg(_(e_invalid_argument_str), address);
+ return NULL;
+ }
+ port = strtol((char *)(p + 1), &rest, 10);
+ if (port <= 0 || port >= 65536 || *rest != NUL)
+ {
+ semsg(_(e_invalid_argument_str), address);
+ return NULL;
+ }
+ *p = NUL;
+ }
+
+ // parse options
+ clear_job_options(&opt);
+ opt.jo_mode = CH_MODE_JSON;
+ opt.jo_timeout = 2000;
+ if (get_job_options(&argvars[1], &opt,
+ JO_MODE_ALL + JO_CB_ALL + JO_TIMEOUT_ALL, 0) == FAIL)
+ goto theend;
+ if (opt.jo_timeout < 0)
+ {
+ emsg(_(e_invalid_argument));
+ goto theend;
+ }
+
+ if (is_unix)
+ channel = channel_listen_unix((char *)address, NULL);
+ else
+ channel = channel_listen((char *)address, port, NULL);
+ if (channel != NULL)
+ {
+ opt.jo_set = JO_ALL;
+ channel_set_options(channel, &opt);
+ }
+theend:
+ free_job_options(&opt);
+ return channel;
+}
+
+/*
+ * Listen to a socket for connections.
+ * Returns the channel for success.
+ * Returns NULL for failure.
+ */
+ channel_T *
+channel_listen(
+ char *hostname,
+ int port_in,
+ void (*nb_close_cb)(void))
+{
+ int sd = -1;
+ struct sockaddr_in server;
+ struct hostent *host;
+ int val = 1;
+ channel_T *channel;
+
+ channel = add_channel();
+ if (channel == NULL)
+ {
+ ch_error(NULL, "Cannot allocate channel.");
+ return NULL;
+ }
+
+ // Get the server internet address and put into addr structure
+ // fill in the socket address structure and bind to port
+ vim_memset((char *)&server, 0, sizeof(server));
+ server.sin_family = AF_INET;
+ server.sin_port = htons(port_in);
+ if (hostname != NULL && *hostname != NUL)
+ {
+ if ((host = gethostbyname(hostname)) == NULL)
+ {
+ ch_error(channel, "in gethostbyname() in channel_listen()");
+ PERROR(_(e_gethostbyname_in_channel_listen));
+ channel_free(channel);
+ return NULL;
+ }
+ {
+ char *p;
+
+ // When using host->h_addr_list[0] directly ubsan warns for it to
+ // not be aligned. First copy the pointer to avoid that.
+ memcpy(&p, &host->h_addr_list[0], sizeof(p));
+ memcpy((char *)&server.sin_addr, p, host->h_length);
+ }
+ }
+ else
+ server.sin_addr.s_addr = htonl(INADDR_ANY);
+
+ sd = socket(AF_INET, SOCK_STREAM, 0);
+ if (sd == -1)
+ {
+ SOCK_ERRNO;
+ ch_error(channel, "in socket() in channel_listen().");
+ PERROR(_(e_cannot_listen_on_port));
+ channel_free(channel);
+ return NULL;
+ }
+
+#ifdef MSWIN
+ if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR,
+ (const char *)&val, sizeof(val)) < 0)
+#else
+ if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR,
+ &val, sizeof(val)) < 0)
+#endif
+ {
+ SOCK_ERRNO;
+ ch_error(channel, "in setsockopt() in channel_listen().");
+ PERROR(_(e_cannot_listen_on_port));
+ sock_close(sd);
+ channel_free(channel);
+ return NULL;
+ }
+
+ // Bind the socket to the port
+ if (bind(sd, (struct sockaddr *)&server, sizeof(server)) < 0)
+ {
+ SOCK_ERRNO;
+ ch_error(channel, "in bind() in channel_listen().");
+ PERROR(_(e_cannot_listen_on_port));
+ sock_close(sd);
+ channel_free(channel);
+ return NULL;
+ }
+
+ if (listen(sd, 5) < 0)
+ {
+ SOCK_ERRNO;
+ ch_error(channel, "in listen() in channel_listen().");
+ PERROR(_(e_cannot_listen_on_port));
+ sock_close(sd);
+ channel_free(channel);
+ return NULL;
+ }
+
+ channel->ch_listen = TRUE;
+ channel->CH_SOCK_FD = (sock_T)sd;
+ channel->ch_nb_close_cb = nb_close_cb;
+ channel->ch_hostname = (char *)vim_strsave((char_u *)hostname);
+ channel->ch_port = port_in;
+ channel->ch_to_be_closed |= (1U << PART_SOCK);
+
+#ifdef FEAT_GUI
+ channel_gui_register_one(channel, PART_SOCK);
+#endif
+
+ return channel;
+}
+
+/*
+ * Listen to a Unix domain socket channel.
+ * Returns the channel for success.
+ * Returns NULL for failure.
+ */
+ channel_T *
+channel_listen_unix(
+ char *path,
+ void (*nb_close_cb)(void))
+{
+ int sd = -1;
+ struct sockaddr_un server;
+ channel_T *channel;
+ size_t path_len;
+ size_t server_len;
+
+ if (path == NULL || *path == NUL)
+ {
+ semsg(_(e_invalid_argument_str), path == NULL ? (char *)"" : path);
+ return NULL;
+ }
+
+ path_len = STRLEN(path);
+ if (path_len >= sizeof(server.sun_path))
+ {
+ semsg(_(e_invalid_argument_str), path);
+ return NULL;
+ }
+
+ channel = add_channel();
+ if (channel == NULL)
+ {
+ ch_error(NULL, "Cannot allocate channel.");
+ return NULL;
+ }
+
+ CLEAR_FIELD(server);
+ server.sun_family = AF_UNIX;
+ STRNCPY(server.sun_path, path, sizeof(server.sun_path) - 1);
+
+ sd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sd == -1)
+ {
+ SOCK_ERRNO;
+ ch_error(channel, "in socket() in channel_listen_unix().");
+ PERROR(_(e_cannot_listen_on_port));
+ channel_free(channel);
+ return NULL;
+ }
+
+ // 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;
+ if (bind(sd, (struct sockaddr *)&server, (int)server_len) < 0)
+ {
+ SOCK_ERRNO;
+ ch_error(channel, "in bind() in channel_listen_unix().");
+ PERROR(_(e_cannot_listen_on_port));
+ sock_close(sd);
+ channel_free(channel);
+ return NULL;
+ }
+
+ if (listen(sd, 5) < 0)
+ {
+ SOCK_ERRNO;
+ ch_error(channel, "in listen() in channel_listen_unix().");
+ PERROR(_(e_cannot_listen_on_port));
+ sock_close(sd);
+ channel_free(channel);
+ return NULL;
+ }
+
+ channel->ch_listen = TRUE;
+ channel->CH_SOCK_FD = (sock_T)sd;
+ channel->ch_nb_close_cb = nb_close_cb;
+ channel->ch_hostname = (char *)vim_strsave((char_u *)path);
+ channel->ch_port = 0;
+ channel->ch_to_be_closed |= (1U << PART_SOCK);
+
+#ifdef FEAT_GUI
+ channel_gui_register_one(channel, PART_SOCK);
+#endif
+
+ return channel;
+}
+
+ void
ch_close_part(channel_T *channel, ch_part_T part)
{
sock_T *fd = &channel->ch_part[part].ch_fd;
@@ -3861,6 +4161,65 @@ channel_read(channel_T *channel, ch_part_T part, char
*func)
{
if (channel_wait(channel, fd, 0) != CW_READY)
break;
+ if (channel->ch_listen)
+ {
+ sock_T newfd;
+ socklen_t socklen;
+ channel_T *newchannel;
+ typval_T argv[2];
+ char_u namebuf[256];
+ struct sockaddr_storage client;
+
+ newchannel = add_channel();
+ if (newchannel == NULL)
+ {
+ ch_error(NULL, "Cannot allocate channel.");
+ return;
+ }
+ socklen = sizeof(client);
+ newfd = accept(fd, (struct sockaddr*)&client, &socklen);
+ if (newfd < 0)
+ {
+ ch_error(NULL, "Cannot accept channel.");
+ channel_free(newchannel);
+ return;
+ }
+ newchannel->CH_SOCK_FD = (sock_T)newfd;
+ newchannel->ch_to_be_closed |= (1U << PART_SOCK);
+
+ if (client.ss_family == AF_INET)
+ vim_snprintf((char *)namebuf, sizeof(namebuf), "%s:%d",
+ inet_ntoa(((struct sockaddr_in*)&client)->sin_addr),
+ ntohs(((struct sockaddr_in*)&client)->sin_port));
+#ifdef HAVE_INET_NTOP
+ else if (client.ss_family == AF_INET6)
+ {
+ char addr[INET6_ADDRSTRLEN];
+
+ inet_ntop(AF_INET6,
+ &((struct sockaddr_in6*)&client)->sin6_addr,
+ addr, sizeof(addr));
+ vim_snprintf((char *)namebuf, sizeof(namebuf), "[%s]:%d",
+ addr,
+ ntohs(((struct sockaddr_in6*)&client)->sin6_port));
+ }
+#endif
+ else if (client.ss_family == AF_UNIX)
+ vim_snprintf((char *)namebuf, sizeof(namebuf),
+ "unix:anonymous");
+ else
+ vim_snprintf((char *)namebuf, sizeof(namebuf), "unknown");
+ ++safe_to_invoke_callback;
+ ++newchannel->ch_refcount;
+ argv[0].v_type = VAR_CHANNEL;
+ argv[0].vval.v_channel = newchannel;
+ argv[1].v_type = VAR_STRING;
+ argv[1].vval.v_string = vim_strsave(namebuf);
+ invoke_callback(newchannel, &channel->ch_callback, argv);
+ --safe_to_invoke_callback;
+ clear_tv(&argv[1]);
+ return;
+ }
if (use_socket)
len = sock_read(fd, (char *)buf, MAXMSGSIZE);
else
@@ -5298,6 +5657,18 @@ f_ch_info(typval_T *argvars, typval_T *rettv UNUSED)
channel_info(channel, rettv->vval.v_dict);
}
+/*
+ * "ch_listen()" function
+ */
+ void
+f_ch_listen(typval_T *argvars, typval_T *rettv)
+{
+ rettv->v_type = VAR_CHANNEL;
+ if (check_restricted() || check_secure())
+ return;
+ rettv->vval.v_channel = channel_listen_func(argvars);
+}
+
/*
* "ch_open()" function
*/
diff --git a/src/errors.h b/src/errors.h
index b909c0fbf..9c4e485ef 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -3807,3 +3807,9 @@ EXTERN char e_no_redraw_listener_callbacks_defined[]
#endif
EXTERN char e_leadtab_requires_tab[]
INIT(= N_("E1572: 'listchars' field \"leadtab\" requires \"tab\" to be
specified"));
+#ifdef FEAT_JOB_CHANNEL
+EXTERN char e_cannot_listen_on_port[]
+ INIT(= N_("E1573: Cannot listen on port"));
+EXTERN char e_gethostbyname_in_channel_listen[]
+ INIT(= N_("E1574: gethostbyname(): cannot resolve hostname in
channel_listen()"));
+#endif
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 3c872af56..9b7008415 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -2083,6 +2083,8 @@ static const funcentry_T global_functions[] =
ret_job, JOB_FUNC(f_ch_getjob)},
{"ch_info", 1, 1, FEARG_1, arg1_chan_or_job,
ret_dict_any, JOB_FUNC(f_ch_info)},
+ {"ch_listen", 1, 2, FEARG_1, arg2_string_dict,
+ ret_channel, JOB_FUNC(f_ch_listen)},
{"ch_log", 1, 2, FEARG_1, arg2_string_chan_or_job,
ret_void, f_ch_log},
{"ch_logfile", 1, 2, FEARG_1, arg2_string,
diff --git a/src/po/vim.pot b/src/po/vim.pot
index c9d9a0aa8..446a0b91f 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Vim
"
"Report-Msgid-Bugs-To: [email protected]
"
-"POT-Creation-Date: 2026-03-11 20:02+0000
"
+"POT-Creation-Date: 2026-03-13 18:26+0000
"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
"Language-Team: LANGUAGE <[email protected]>
"
@@ -8848,6 +8848,12 @@ msgstr ""
msgid "E1572: 'listchars' field \"leadtab\" requires \"tab\" to be specified"
msgstr ""
+msgid "E1573: Cannot listen on port"
+msgstr ""
+
+msgid "E1574: gethostbyname(): cannot resolve hostname in channel_listen()"
+msgstr ""
+
#. type of cmdline window or 0
#. result of cmdline window or 0
#. buffer of cmdline window or NULL
diff --git a/src/proto/channel.pro b/src/proto/channel.pro
index 794e60b22..d0c2cd042 100644
--- a/src/proto/channel.pro
+++ b/src/proto/channel.pro
@@ -8,6 +8,9 @@ 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(const char *hostname, int port, int waittime, void
(*nb_close_cb)(void));
+channel_T *channel_listen_func(typval_T *argvars);
+channel_T *channel_listen(char *hostname, int port_in, void
(*nb_close_cb)(void));
+channel_T *channel_listen_unix(char *path, void (*nb_close_cb)(void));
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);
@@ -44,6 +47,7 @@ void f_ch_close_in(typval_T *argvars, typval_T *rettv);
void f_ch_getbufnr(typval_T *argvars, typval_T *rettv);
void f_ch_getjob(typval_T *argvars, typval_T *rettv);
void f_ch_info(typval_T *argvars, typval_T *rettv);
+void f_ch_listen(typval_T *argvars, typval_T *rettv);
void f_ch_open(typval_T *argvars, typval_T *rettv);
void f_ch_read(typval_T *argvars, typval_T *rettv);
void f_ch_readblob(typval_T *argvars, typval_T *rettv);
diff --git a/src/structs.h b/src/structs.h
index 5f4e1fc16..2b8cb3db1 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -2708,10 +2708,11 @@ struct channel_S {
int ch_to_be_freed; // When TRUE channel must be freed when
it's
// safe to invoke callbacks.
int ch_error; // When TRUE an error was reported.
Avoids
- // giving pages full of error messages when
- // the other side has exited, only mention the
- // first error until the connection works
- // again.
+ // giving pages full of error messages when
+ // the other side has exited, only mention the
+ // first error until the connection works
+ // again.
+ int ch_listen; // When TRUE channel is listen socket.
void (*ch_nb_close_cb)(void);
// callback for Netbeans when channel is
diff --git a/src/testdir/test_channel.vim b/src/testdir/test_channel.vim
index 8ee72c9f3..431eaab26 100644
--- a/src/testdir/test_channel.vim
+++ b/src/testdir/test_channel.vim
@@ -556,7 +556,7 @@ func Test_raw_pipe()
" ch_canread().
" Also test the non-blocking option.
let job = job_start(s:python . " test_channel_pipe.py",
- \ {'mode': 'raw', 'drop': 'never', 'noblock': 1})
+ \ {'mode': 'raw', 'drop': 'never', 'noblock': 1})
call assert_equal(v:t_job, type(job))
call assert_equal("run", job_status(job))
@@ -639,7 +639,7 @@ func Test_raw_pipe_blob()
" ch_canread().
" Also test the non-blocking option.
let job = job_start(s:python . " test_channel_pipe.py",
- \ {'mode': 'raw', 'drop': 'never', 'noblock': 1})
+ \ {'mode': 'raw', 'drop': 'never', 'noblock': 1})
call assert_equal(v:t_job, type(job))
call assert_equal("run", job_status(job))
@@ -713,7 +713,7 @@ endfunc
func Test_nl_read_file()
call writefile(['echo something', 'echoerr wrong', 'double this'], 'Xinput',
'D')
let g:job = job_start(s:python . " test_channel_pipe.py",
- \ {'in_io': 'file', 'in_name': 'Xinput'})
+ \ {'in_io': 'file', 'in_name': 'Xinput'})
call assert_equal("run", job_status(g:job))
try
let handle = job_getchannel(g:job)
@@ -729,7 +729,7 @@ endfunc
func Test_nl_write_out_file()
let g:job = job_start(s:python . " test_channel_pipe.py",
- \ {'out_io': 'file', 'out_name': 'Xoutput'})
+ \ {'out_io': 'file', 'out_name': 'Xoutput'})
call assert_equal("run", job_status(g:job))
try
let handle = job_getchannel(g:job)
@@ -746,7 +746,7 @@ endfunc
func Test_nl_write_err_file()
let g:job = job_start(s:python . " test_channel_pipe.py",
- \ {'err_io': 'file', 'err_name': 'Xoutput'})
+ \ {'err_io': 'file', 'err_name': 'Xoutput'})
call assert_equal("run", job_status(g:job))
try
let handle = job_getchannel(g:job)
@@ -762,7 +762,7 @@ endfunc
func Test_nl_write_both_file()
let g:job = job_start(s:python . " test_channel_pipe.py",
- \ {'out_io': 'file', 'out_name': 'Xoutput', 'err_io': 'out'})
+ \ {'out_io': 'file', 'out_name': 'Xoutput', 'err_io': 'out'})
call assert_equal("run", job_status(g:job))
try
let handle = job_getchannel(g:job)
@@ -929,7 +929,7 @@ endfunc
func Test_pipe_both_to_buffer()
let job = job_start(s:python . " test_channel_pipe.py",
- \ {'out_io': 'buffer', 'out_name': 'pipe-err', 'err_io': 'out'})
+ \ {'out_io': 'buffer', 'out_name': 'pipe-err', 'err_io': 'out'})
call assert_equal("run", job_status(job))
let handle = job_getchannel(job)
call assert_equal(bufnr('pipe-err'), ch_getbufnr(handle, 'out'))
@@ -1037,7 +1037,7 @@ endfunc
func Test_pipe_to_nameless_buffer()
let job = job_start(s:python . " test_channel_pipe.py",
- \ {'out_io': 'buffer'})
+ \ {'out_io': 'buffer'})
call assert_equal("run", job_status(job))
try
let handle = job_getchannel(job)
@@ -1056,7 +1056,7 @@ func Test_pipe_to_buffer_json()
CheckFunction reltimefloat
let job = job_start(s:python . " test_channel_pipe.py",
- \ {'out_io': 'buffer', 'out_mode': 'json'})
+ \ {'out_io': 'buffer', 'out_mode': 'json'})
call assert_equal("run", job_status(job))
try
let handle = job_getchannel(job)
@@ -1089,9 +1089,9 @@ func Test_pipe_io_two_buffers()
set buftype=nofile
let job = job_start(s:python . " test_channel_pipe.py",
- \ {'in_io': 'buffer', 'in_name': 'pipe-input', 'in_top': 0,
- \ 'out_io': 'buffer', 'out_name': 'pipe-output',
- \ 'block_write': 1})
+ \ {'in_io': 'buffer', 'in_name': 'pipe-input', 'in_top': 0,
+ \ 'out_io': 'buffer', 'out_name': 'pipe-output',
+ \ 'block_write': 1})
call assert_equal("run", job_status(job))
try
exe "normal Gaecho hello\<CR>"
@@ -1120,9 +1120,9 @@ func Test_pipe_io_one_buffer()
set buftype=nofile
let job = job_start(s:python . " test_channel_pipe.py",
- \ {'in_io': 'buffer', 'in_name': 'pipe-io', 'in_top': 0,
- \ 'out_io': 'buffer', 'out_name': 'pipe-io',
- \ 'block_write': 1})
+ \ {'in_io': 'buffer', 'in_name': 'pipe-io', 'in_top': 0,
+ \ 'out_io': 'buffer', 'out_name': 'pipe-io',
+ \ 'block_write': 1})
call assert_equal("run", job_status(job))
try
exe "normal Goecho hello\<CR>"
@@ -1151,9 +1151,9 @@ func Test_write_to_buffer_and_scroll()
wincmd w
call deletebufline('Xscrollbuffer', 1, '$')
if has('win32')
- let cmd = ['cmd', '/c', 'echo sometext']
+ let cmd = ['cmd', '/c', 'echo sometext']
else
- let cmd = [&shell, &shellcmdflag, 'echo sometext']
+ let cmd = [&shell, &shellcmdflag, 'echo sometext']
endif
call job_start(cmd, #{out_io: 'buffer', out_name: 'Xscrollbuffer'})
END
@@ -1170,7 +1170,7 @@ func Test_pipe_null()
" We cannot check that no I/O works, we only check that the job starts
" properly.
let job = job_start(s:python . " test_channel_pipe.py something",
- \ {'in_io': 'null'})
+ \ {'in_io': 'null'})
call assert_equal("run", job_status(job))
try
call assert_equal('something', ch_read(job))
@@ -1179,7 +1179,7 @@ func Test_pipe_null()
endtry
let job = job_start(s:python . " test_channel_pipe.py err-out",
- \ {'out_io': 'null'})
+ \ {'out_io': 'null'})
call assert_equal("run", job_status(job))
try
call assert_equal('err-out', ch_read(job, {"part": "err"}))
@@ -1188,7 +1188,7 @@ func Test_pipe_null()
endtry
let job = job_start(s:python . " test_channel_pipe.py something",
- \ {'err_io': 'null'})
+ \ {'err_io': 'null'})
call assert_equal("run", job_status(job))
try
call assert_equal('something', ch_read(job))
@@ -1271,10 +1271,10 @@ func Test_out_cb()
let g:Ch_errmsg = self.thisis . a:msg
endfunc
let job = job_start(s:python . " test_channel_pipe.py",
- \ {'out_cb': dict.outHandler,
- \ 'out_mode': 'json',
- \ 'err_cb': dict.errHandler,
- \ 'err_mode': 'json'})
+ \ {'out_cb': dict.outHandler,
+ \ 'out_mode': 'json',
+ \ 'err_cb': dict.errHandler,
+ \ 'err_mode': 'json'})
call assert_equal("run", job_status(job))
call test_garbagecollect_now()
try
@@ -1320,8 +1320,8 @@ func Test_out_close_cb()
let s:counter += 1
endfunc
let job = job_start(s:python . " test_channel_pipe.py quit now",
- \ {'out_cb': 'OutHandler',
- \ 'close_cb': 'CloseHandler'})
+ \ {'out_cb': 'OutHandler',
+ \ 'close_cb': 'CloseHandler'})
" the job may be done quickly, also accept "dead"
call assert_match('^\%(dead\|run\)$', job_status(job))
try
@@ -1340,7 +1340,7 @@ func Test_read_in_close_cb()
let g:Ch_received = ch_read(a:chan)
endfunc
let job = job_start(s:python . " test_channel_pipe.py quit now",
- \ {'close_cb': 'CloseHandler'})
+ \ {'close_cb': 'CloseHandler'})
" the job may be done quickly, also accept "dead"
call assert_match('^\%(dead\|run\)$', job_status(job))
try
@@ -1360,7 +1360,7 @@ func Test_read_in_close_cb_incomplete()
endwhile
endfunc
let job = job_start(s:python . " test_channel_pipe.py incomplete",
- \ {'close_cb': 'CloseHandler'})
+ \ {'close_cb': 'CloseHandler'})
" the job may be done quickly, also accept "dead"
call assert_match('^\%(dead\|run\)$', job_status(job))
try
@@ -1427,7 +1427,7 @@ func Test_exit_cb_wipes_buf()
let g:wipe_buf = bufnr('')
let job = job_start(has('win32') ? 'cmd /D /c echo:' : ['true'],
- \ {'exit_cb': 'ExitCbWipe'})
+ \ {'exit_cb': 'ExitCbWipe'})
let timer = timer_start(300, {-> feedkeys("\<Esc>", 'nt')}, {'repeat': 5})
call feedkeys(repeat('g', 1000) . 'o', 'ntx!')
call WaitForAssert({-> assert_equal("dead", job_status(job))})
@@ -2265,16 +2265,16 @@ func Test_zz_nl_err_to_out_pipe()
let found_stop = 0
for l in loglines
if l =~ 'Test_zz_nl_err_to_out_pipe'
- let found_test = 1
+ let found_test = 1
endif
if l =~ 'SEND on.*echo something'
- let found_send = 1
+ let found_send = 1
endif
if l =~ 'RECV on.*something'
- let found_recv = 1
+ let found_recv = 1
endif
if l =~ 'Stopping job with'
- let found_stop = 1
+ let found_stop = 1
endif
endfor
call assert_equal(1, found_test)
@@ -2758,6 +2758,62 @@ func LspTests(port)
" call ch_logfile('', 'w')
endfunc
+let g:server_received_addr = ''
+let g:server_received_msg = ''
+
+func s:test_listen_accept(ch, addr)
+ let g:server_received_addr = a:addr
+ let g:server_received_msg = ch_readraw(a:ch)
+endfunction
+
+func Test_listen()
+ call ch_log('Test_listen()')
+ let server = ch_listen('127.0.0.1:12345', {'callback':
function('s:test_listen_accept')})
+ if ch_status(server) == 'fail'
+ call assert_report("Can't listen channel")
+ return
+ endif
+ let handle = ch_open('127.0.0.1:12345', s:chopt)
+ if ch_status(handle) == 'fail'
+ call assert_report("Can't open channel")
+ return
+ endif
+ call ch_sendraw(handle, 'hello')
+ call WaitFor('"" != g:server_received_msg')
+ call ch_close(handle)
+ call ch_close(server)
+ call assert_equal('hello', g:server_received_msg)
+ call assert_match('127.0.0.1:', g:server_received_addr)
+endfunc
+
+func Test_listen_invalid_address()
+ call ch_log('Test_listen_invalid_address()')
+
+ " empty address
+ call assert_fails("call ch_listen('')", 'E475:')
+
+ " missing port
+ call assert_fails("call ch_listen('localhost')", 'E475:')
+
+ " port number too large
+ call assert_fails("call ch_listen('localhost:99999')", 'E475:')
+
+ " port number zero
+ call assert_fails("call ch_listen('localhost:0')", 'E475:')
+
+ " port number negative
+ call assert_fails("call ch_listen('localhost:-1')", 'E475:')
+
+ " invalid ipv6 format (missing closing bracket)
+ call assert_fails("call ch_listen('[::1:8765')", 'E475:')
+
+ " invalid ipv6 format (missing port)
+ call assert_fails("call ch_listen('[::1]')", 'E475:')
+
+ " TODO: IPv6 should actually work
+ call assert_fails("call ch_listen('[::1]:9999')", 'E1574:')
+endfunc
+
func Test_channel_lsp_mode()
" The channel lsp mode test is flaky and gives the same error.
let g:giveup_same_error = 0
diff --git a/src/version.c b/src/version.c
index 78491aa86..184a7d327 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 153,
/**/
152,
/**/
--
--
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php
---
You received this message because you are subscribed to the Google Groups
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion visit
https://groups.google.com/d/msgid/vim_dev/E1w18D0-003pSQ-MF%40256bit.org.