This patch adds ch_listen() function to Vim, allowing scripts to create listening sockets and accept incoming network connections. This enables Vim to act as a simple network server for various use cases.
ch_listen(address [, options]) - Listen on a socket and accept connections
channel_listen_unix(path, callback) - Unix domain socket support
channel_listen_func(argvars) - Vim script interface
ch_listen field to struct channel_S to mark listening socketschannel_read() to handle accept() on listening socketsfunction! OnConnect(channel, address) echomsg 'Client connected from: ' . a:address call ch_setoptions(a:channel, { \ 'callback': function('OnData'), \ }) endfunction function! OnData(channel, data) echomsg 'Received: ' . a:data endfunction let server = ch_listen('127.0.0.1:8080', { \ 'callback': function('OnConnect'), \ })
Test_listen() in test_channel.vimThis complements the existing ch_open() function:
ch_open() - connect to a serverch_listen() - accept connections as a serverThis is demonstration of HTTP server written in Vim script.
image.png (view on web)https://github.com/vim/vim/pull/19231
(7 files)
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I'll fix test fail in later.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@mattn pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Thanks that is nice. But I'll move this after the Vim 9.2 release.
Can a few people try it out? I'd also think that we should have a very simple practical example for this in the help.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
It is not clear from the docs how ch_listen() differs from ch_open({address} [, {options}])? See :h channel-address for possible values of address.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@chrisbra This is simple practice example working as HTTP file server.
https://gist.github.com/mattn/3e691419c37fe7ce501dc37e2a1bb545
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Thanks.
ch_listen(address [, options]) - Listen on a socket and accept connections
Returns a channel representing the listening socket
Calls a callback function when a new client connects
Supports both IPv4 and Unix domain sockets
channel_listen_unix(path, callback) - Unix domain socket support
Why do we need channel_list_unix()? It looks like this is already supported via ch_listen() anyhow, no?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I wonder if this would help @yegappan lsp to support lsp servers without stdio, e.g. godot lsp server that works only over http?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Call tree is below. Should we rename channel_listen to channel_listen_tcp ?
f_ch_listen
channel_listen_func
channel_listen (TCP)
channel_listen_unix (Unix Domain Socket)
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I think we should have one single ch_listen() function, that accepts either a TCP address or a Unix domain socket, similar to ch_open(). I don't think we need separate vim script functions for tcp and unix domain sockets.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I wonder if this would help @yegappan lsp to support lsp servers without
stdio, e.g. godot lsp server that works only over http?
This is already possible, see :h channel-address. You need to open a channel with ch_open() and then pass that channel to job_start() using its channel option (explained under :h job-options [below :h job-term] ). There's also an example in the docs under :h channel-demo using a Python server.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@chrisbra
The current implementation already follows this approach. ch_listen() accepts both TCP addresses and Unix domain sockets using the same pattern as ch_open():
ch_listen('127.0.0.1:12345', ...)ch_listen('unix:/path/to/socket', ...)The channel_listen_func() detects the unix: prefix and internally calls either channel_listen_unix() or channel_listen() accordingly. There is no separate ch_listen_tcp() or ch_listen_unix() function exposed to Vim script.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I think we should have one single
ch_listen()function, that accepts either a TCP address or a Unix domain socket, similar toch_open(). I don't think we need separate vim script functions for tcp and unix domain sockets.
ch_open() is the only builtin Vim script function added, the others are implementation functions.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@Copilot commented on this pull request.
This PR adds server socket functionality to Vim by implementing the ch_listen() function, enabling Vim scripts to accept incoming network connections. This complements the existing ch_open() function, which allows Vim to act as a client.
Changes:
ch_listen() function with support for IPv4, IPv6, and Unix domain socketstest_channel.vimCopilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file| File | Description |
|---|---|
| src/channel.c | Core implementation of listen functionality and accept handling |
| src/structs.h | Added ch_listen field to mark listening sockets |
| src/proto/channel.pro | Function prototypes for new channel listening functions |
| src/evalfunc.c | Registered ch_listen as a Vim script function |
| src/errors.h | Added error messages for listen failures |
| src/testdir/test_channel.vim | Added test case and fixed indentation throughout file |
| runtime/doc/usr_41.txt | Updated function list and alphabetical ordering |
| runtime/doc/channel.txt | Added documentation for ch_listen() |
| runtime/doc/builtin.txt | Added ch_listen() to built-in function list |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
In src/channel.c:
> + else + sprintf((char *)namebuf, "unix:%s", + ((struct sockaddr_un*)&client)->sun_path);
For Unix domain sockets, the client address structure populated by accept() does not contain a meaningful path in sun_path. Unix domain sockets only provide the path for the listening socket, not for accepted connections. This will likely result in garbage or empty data being passed to the callback. Consider passing an empty string or a generic identifier like 'unix:anonymous' for Unix domain socket clients.
- else - sprintf((char *)namebuf, "unix:%s", - ((struct sockaddr_un*)&client)->sun_path); + else if (client.ss_family == AF_UNIX) + sprintf((char *)namebuf, "unix:anonymous"); + else + sprintf((char *)namebuf, "unknown");
In src/channel.c:
> + if (client.ss_family == AF_INET) + sprintf((char *)namebuf, "%s:%d", + inet_ntoa(((struct sockaddr_in*)&client)->sin_addr), + ntohs(((struct sockaddr_in*)&client)->sin_port)); + else + sprintf((char *)namebuf, "unix:%s", + ((struct sockaddr_un*)&client)->sun_path);
The else branch assumes all non-IPv4 connections are Unix domain sockets, but the code supports IPv6 addresses (as shown in channel_listen_func). When an IPv6 client connects, this will incorrectly try to interpret the address as a Unix socket path. Add explicit handling for AF_INET6 using inet_ntop() to format IPv6 addresses.
In src/channel.c:
> + sprintf((char *)namebuf, "%s:%d", + inet_ntoa(((struct sockaddr_in*)&client)->sin_addr), + ntohs(((struct sockaddr_in*)&client)->sin_port)); + else + sprintf((char *)namebuf, "unix:%s",
Using sprintf without bounds checking creates a buffer overflow risk. The namebuf is declared as 256 bytes, but sprintf doesn't verify this limit. Use snprintf instead to ensure the formatted string doesn't exceed the buffer size.
⬇️ Suggested change- sprintf((char *)namebuf, "%s:%d", - inet_ntoa(((struct sockaddr_in*)&client)->sin_addr), - ntohs(((struct sockaddr_in*)&client)->sin_port)); - else - sprintf((char *)namebuf, "unix:%s", + snprintf((char *)namebuf, sizeof(namebuf), "%s:%d", + inet_ntoa(((struct sockaddr_in*)&client)->sin_addr), + ntohs(((struct sockaddr_in*)&client)->sin_port)); + else + snprintf((char *)namebuf, sizeof(namebuf), "unix:%s",
In src/testdir/test_channel.vim:
> + func s:test_listen_accept(ch, addr) + let g:server_received_addr = a:addr + let g:server_received_msg = ch_readraw(a:ch) + endfunction
The test doesn't verify that g:server_received_addr contains the expected client address. Add an assertion like call assert_match('127.0.0.1:', g:server_received_addr) to ensure the callback receives correct address information.
In src/testdir/test_channel.vim:
> + 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
The test doesn't verify error handling for invalid addresses or port conflicts. Consider adding test cases for scenarios like invalid port numbers, ports already in use, or malformed addresses to ensure proper error reporting.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()