mirror of
https://github.com/neovim/neovim.git
synced 2024-12-24 13:15:09 -07:00
Merge pull request #6680 from mhinz/listen/localhost
Use uv_getaddrinfo() for servers
This commit is contained in:
commit
9cc185dc6d
@ -6460,11 +6460,20 @@ serverlist() *serverlist()*
|
||||
nvim --cmd "echo serverlist()" --cmd "q"
|
||||
<
|
||||
serverstart([{address}]) *serverstart()*
|
||||
Opens a named pipe or TCP socket at {address} for clients to
|
||||
connect to and returns {address}. If no address is given, it
|
||||
is equivalent to: >
|
||||
Opens a TCP socket (IPv4/IPv6), Unix domain socket (Unix),
|
||||
or named pipe (Windows) at {address} for clients to connect
|
||||
to and returns {address}.
|
||||
|
||||
If {address} contains `:`, a TCP socket is used. Everything in
|
||||
front of the last occurrence of `:` is the IP or hostname,
|
||||
everything after it the port. If the port is empty or `0`,
|
||||
a random port will be assigned.
|
||||
|
||||
If no address is given, it is equivalent to: >
|
||||
:call serverstart(tempname())
|
||||
|
||||
< |$NVIM_LISTEN_ADDRESS| is set to {address} if not already set.
|
||||
|
||||
*--servername*
|
||||
The Vim command-line option `--servername` can be imitated: >
|
||||
nvim --cmd "let g:server_addr = serverstart('foo')"
|
||||
|
@ -14321,22 +14321,39 @@ static void f_serverstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
|
||||
return;
|
||||
}
|
||||
|
||||
char *address;
|
||||
// If the user supplied an address, use it, otherwise use a temp.
|
||||
if (argvars[0].v_type != VAR_UNKNOWN) {
|
||||
if (argvars[0].v_type != VAR_STRING) {
|
||||
EMSG(_(e_invarg));
|
||||
return;
|
||||
} else {
|
||||
rettv->vval.v_string = (char_u *)xstrdup(tv_get_string(argvars));
|
||||
address = xstrdup(tv_get_string(argvars));
|
||||
}
|
||||
} else {
|
||||
rettv->vval.v_string = (char_u *)server_address_new();
|
||||
address = server_address_new();
|
||||
}
|
||||
|
||||
int result = server_start((char *) rettv->vval.v_string);
|
||||
int result = server_start(address);
|
||||
xfree(address);
|
||||
|
||||
if (result != 0) {
|
||||
EMSG2("Failed to start server: %s", uv_strerror(result));
|
||||
EMSG2("Failed to start server: %s",
|
||||
result > 0 ? "Unknonwn system error" : uv_strerror(result));
|
||||
return;
|
||||
}
|
||||
|
||||
// Since it's possible server_start adjusted the given {address} (e.g.,
|
||||
// "localhost:" will now have a port), return the final value to the user.
|
||||
size_t n;
|
||||
char **addrs = server_address_list(&n);
|
||||
rettv->vval.v_string = (char_u *)addrs[n - 1];
|
||||
|
||||
n--;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
xfree(addrs[i]);
|
||||
}
|
||||
xfree(addrs);
|
||||
}
|
||||
|
||||
/// "serverstop()" function
|
||||
|
@ -17,60 +17,53 @@
|
||||
#include "nvim/path.h"
|
||||
#include "nvim/memory.h"
|
||||
#include "nvim/macros.h"
|
||||
#include "nvim/charset.h"
|
||||
#include "nvim/log.h"
|
||||
|
||||
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
||||
# include "event/socket.c.generated.h"
|
||||
#endif
|
||||
|
||||
#define NVIM_DEFAULT_TCP_PORT 7450
|
||||
|
||||
void socket_watcher_init(Loop *loop, SocketWatcher *watcher,
|
||||
const char *endpoint, void *data)
|
||||
FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(2) FUNC_ATTR_NONNULL_ARG(3)
|
||||
int socket_watcher_init(Loop *loop, SocketWatcher *watcher,
|
||||
const char *endpoint)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
// Trim to `ADDRESS_MAX_SIZE`
|
||||
if (xstrlcpy(watcher->addr, endpoint, sizeof(watcher->addr))
|
||||
>= sizeof(watcher->addr)) {
|
||||
// TODO(aktau): since this is not what the user wanted, perhaps we
|
||||
// should return an error here
|
||||
WLOG("Address was too long, truncated to %s", watcher->addr);
|
||||
xstrlcpy(watcher->addr, endpoint, sizeof(watcher->addr));
|
||||
char *addr = watcher->addr;
|
||||
char *host_end = strrchr(addr, ':');
|
||||
|
||||
if (host_end && addr != host_end) {
|
||||
// Split user specified address into two strings, addr(hostname) and port.
|
||||
// The port part in watcher->addr will be updated later.
|
||||
*host_end = '\0';
|
||||
char *port = host_end + 1;
|
||||
intmax_t iport;
|
||||
|
||||
int ret = getdigits_safe(&(char_u *){ (char_u *)port }, &iport);
|
||||
if (ret == FAIL || iport < 0 || iport > UINT16_MAX) {
|
||||
ELOG("Invalid port: %s", port);
|
||||
return UV_EINVAL;
|
||||
}
|
||||
|
||||
bool tcp = true;
|
||||
char ip[16], *ip_end = xstrchrnul(watcher->addr, ':');
|
||||
|
||||
// (ip_end - addr) is always > 0, so convert to size_t
|
||||
size_t addr_len = (size_t)(ip_end - watcher->addr);
|
||||
|
||||
if (addr_len > sizeof(ip) - 1) {
|
||||
// Maximum length of an IPv4 address buffer is 15 (eg: 255.255.255.255)
|
||||
addr_len = sizeof(ip) - 1;
|
||||
if (*port == NUL) {
|
||||
// When no port is given, (uv_)getaddrinfo expects NULL otherwise the
|
||||
// implementation may attempt to lookup the service by name (and fail)
|
||||
port = NULL;
|
||||
}
|
||||
|
||||
// Extract the address part
|
||||
xstrlcpy(ip, watcher->addr, addr_len + 1);
|
||||
int port = NVIM_DEFAULT_TCP_PORT;
|
||||
uv_getaddrinfo_t request;
|
||||
|
||||
if (*ip_end == ':') {
|
||||
// Extract the port
|
||||
long lport = strtol(ip_end + 1, NULL, 10); // NOLINT
|
||||
if (lport <= 0 || lport > 0xffff) {
|
||||
// Invalid port, treat as named pipe or unix socket
|
||||
tcp = false;
|
||||
} else {
|
||||
port = (int) lport;
|
||||
}
|
||||
int retval = uv_getaddrinfo(&loop->uv, &request, NULL, addr, port,
|
||||
&(struct addrinfo){
|
||||
.ai_family = AF_UNSPEC,
|
||||
.ai_socktype = SOCK_STREAM,
|
||||
});
|
||||
if (retval != 0) {
|
||||
ELOG("Host lookup failed: %s", endpoint);
|
||||
return retval;
|
||||
}
|
||||
watcher->uv.tcp.addrinfo = request.addrinfo;
|
||||
|
||||
if (tcp) {
|
||||
// Try to parse ip address
|
||||
if (uv_ip4_addr(ip, port, &watcher->uv.tcp.addr)) {
|
||||
// Invalid address, treat as named pipe or unix socket
|
||||
tcp = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (tcp) {
|
||||
uv_tcp_init(&loop->uv, &watcher->uv.tcp.handle);
|
||||
watcher->stream = STRUCT_CAST(uv_stream_t, &watcher->uv.tcp.handle);
|
||||
} else {
|
||||
@ -82,33 +75,60 @@ void socket_watcher_init(Loop *loop, SocketWatcher *watcher,
|
||||
watcher->cb = NULL;
|
||||
watcher->close_cb = NULL;
|
||||
watcher->events = NULL;
|
||||
watcher->data = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int socket_watcher_start(SocketWatcher *watcher, int backlog, socket_cb cb)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
watcher->cb = cb;
|
||||
int result;
|
||||
int result = UV_EINVAL;
|
||||
|
||||
if (watcher->stream->type == UV_TCP) {
|
||||
result = uv_tcp_bind(&watcher->uv.tcp.handle,
|
||||
(const struct sockaddr *)&watcher->uv.tcp.addr, 0);
|
||||
struct addrinfo *ai = watcher->uv.tcp.addrinfo;
|
||||
|
||||
for (; ai; ai = ai->ai_next) {
|
||||
result = uv_tcp_bind(&watcher->uv.tcp.handle, ai->ai_addr, 0);
|
||||
if (result != 0) {
|
||||
continue;
|
||||
}
|
||||
result = uv_listen(watcher->stream, backlog, connection_cb);
|
||||
if (result == 0) {
|
||||
struct sockaddr_storage sas;
|
||||
|
||||
// When the endpoint in socket_watcher_init() didn't specify a port
|
||||
// number, a free random port number will be assigned. sin_port will
|
||||
// contain 0 in this case, unless uv_tcp_getsockname() is used first.
|
||||
uv_tcp_getsockname(&watcher->uv.tcp.handle, (struct sockaddr *)&sas,
|
||||
&(int){ sizeof(sas) });
|
||||
uint16_t port = (uint16_t)((sas.ss_family == AF_INET)
|
||||
? ((struct sockaddr_in *)&sas)->sin_port
|
||||
: ((struct sockaddr_in6 *)&sas)->sin6_port);
|
||||
// v:servername uses the string from watcher->addr
|
||||
size_t len = strlen(watcher->addr);
|
||||
snprintf(watcher->addr+len, sizeof(watcher->addr)-len, ":%" PRIu16,
|
||||
ntohs(port));
|
||||
break;
|
||||
}
|
||||
}
|
||||
uv_freeaddrinfo(watcher->uv.tcp.addrinfo);
|
||||
} else {
|
||||
result = uv_pipe_bind(&watcher->uv.pipe.handle, watcher->addr);
|
||||
}
|
||||
|
||||
if (result == 0) {
|
||||
result = uv_listen(watcher->stream, backlog, connection_cb);
|
||||
}
|
||||
}
|
||||
|
||||
assert(result <= 0); // libuv should return negative error code or zero.
|
||||
if (result < 0) {
|
||||
if (result == -EACCES) {
|
||||
if (result == UV_EACCES) {
|
||||
// Libuv converts ENOENT to EACCES for Windows compatibility, but if
|
||||
// the parent directory does not exist, ENOENT would be more accurate.
|
||||
*path_tail((char_u *)watcher->addr) = NUL;
|
||||
if (!os_path_exists((char_u *)watcher->addr)) {
|
||||
result = -ENOENT;
|
||||
result = UV_ENOENT;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -20,7 +20,7 @@ struct socket_watcher {
|
||||
union {
|
||||
struct {
|
||||
uv_tcp_t handle;
|
||||
struct sockaddr_in addr;
|
||||
struct addrinfo *addrinfo;
|
||||
} tcp;
|
||||
struct {
|
||||
uv_pipe_t handle;
|
||||
|
@ -97,37 +97,47 @@ char *server_address_new(void)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Starts listening for API calls on the TCP address or pipe path `endpoint`.
|
||||
/// The socket type is determined by parsing `endpoint`: If it's a valid IPv4
|
||||
/// address in 'ip[:port]' format, then it will be TCP socket. The port is
|
||||
/// optional and if omitted defaults to NVIM_DEFAULT_TCP_PORT. Otherwise it
|
||||
/// will be a unix socket or named pipe.
|
||||
/// Starts listening for API calls.
|
||||
///
|
||||
/// @param endpoint Address of the server. Either a 'ip[:port]' string or an
|
||||
/// arbitrary identifier (trimmed to 256 bytes) for the unix socket or
|
||||
/// named pipe.
|
||||
/// The socket type is determined by parsing `endpoint`: If it's a valid IPv4
|
||||
/// or IPv6 address in 'ip:[port]' format, then it will be a TCP socket.
|
||||
/// Otherwise it will be a Unix socket or named pipe (Windows).
|
||||
///
|
||||
/// If no port is given, a random one will be assigned.
|
||||
///
|
||||
/// @param endpoint Address of the server. Either a 'ip:[port]' string or an
|
||||
/// arbitrary identifier (trimmed to 256 bytes) for the Unix
|
||||
/// socket or named pipe.
|
||||
/// @returns 0 on success, 1 on a regular error, and negative errno
|
||||
/// on failure to bind or connect.
|
||||
/// on failure to bind or listen.
|
||||
int server_start(const char *endpoint)
|
||||
{
|
||||
if (endpoint == NULL) {
|
||||
ELOG("Attempting to start server on NULL endpoint");
|
||||
if (endpoint == NULL || endpoint[0] == '\0') {
|
||||
ELOG("Empty or NULL endpoint");
|
||||
return 1;
|
||||
}
|
||||
|
||||
SocketWatcher *watcher = xmalloc(sizeof(SocketWatcher));
|
||||
socket_watcher_init(&main_loop, watcher, endpoint, NULL);
|
||||
|
||||
int result = socket_watcher_init(&main_loop, watcher, endpoint);
|
||||
if (result < 0) {
|
||||
xfree(watcher);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if a watcher for the endpoint already exists
|
||||
for (int i = 0; i < watchers.ga_len; i++) {
|
||||
if (!strcmp(watcher->addr, ((SocketWatcher **)watchers.ga_data)[i]->addr)) {
|
||||
ELOG("Already listening on %s", watcher->addr);
|
||||
if (watcher->stream->type == UV_TCP) {
|
||||
uv_freeaddrinfo(watcher->uv.tcp.addrinfo);
|
||||
}
|
||||
socket_watcher_close(watcher, free_server);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int result = socket_watcher_start(watcher, MAX_CONNECTIONS, connection_cb);
|
||||
result = socket_watcher_start(watcher, MAX_CONNECTIONS, connection_cb);
|
||||
if (result < 0) {
|
||||
ELOG("Failed to start server: %s", uv_strerror(result));
|
||||
socket_watcher_close(watcher, free_server);
|
||||
|
@ -1,20 +1,27 @@
|
||||
|
||||
local helpers = require('test.functional.helpers')(after_each)
|
||||
local nvim, eq, neq, eval = helpers.nvim, helpers.eq, helpers.neq, helpers.eval
|
||||
local eq, neq, eval = helpers.eq, helpers.neq, helpers.eval
|
||||
local command = helpers.command
|
||||
local clear, funcs, meths = helpers.clear, helpers.funcs, helpers.meths
|
||||
local os_name = helpers.os_name
|
||||
|
||||
local function clear_serverlist()
|
||||
for _, server in pairs(funcs.serverlist()) do
|
||||
funcs.serverstop(server)
|
||||
end
|
||||
end
|
||||
|
||||
describe('serverstart(), serverstop()', function()
|
||||
before_each(clear)
|
||||
|
||||
it('sets $NVIM_LISTEN_ADDRESS on first invocation', function()
|
||||
-- Unset $NVIM_LISTEN_ADDRESS
|
||||
nvim('command', 'let $NVIM_LISTEN_ADDRESS = ""')
|
||||
command('let $NVIM_LISTEN_ADDRESS = ""')
|
||||
|
||||
local s = eval('serverstart()')
|
||||
assert(s ~= nil and s:len() > 0, "serverstart() returned empty")
|
||||
eq(s, eval('$NVIM_LISTEN_ADDRESS'))
|
||||
nvim('command', "call serverstop('"..s.."')")
|
||||
command("call serverstop('"..s.."')")
|
||||
eq('', eval('$NVIM_LISTEN_ADDRESS'))
|
||||
end)
|
||||
|
||||
@ -47,10 +54,38 @@ describe('serverstart(), serverstop()', function()
|
||||
end)
|
||||
|
||||
it('serverstop() ignores invalid input', function()
|
||||
nvim('command', "call serverstop('')")
|
||||
nvim('command', "call serverstop('bogus-socket-name')")
|
||||
command("call serverstop('')")
|
||||
command("call serverstop('bogus-socket-name')")
|
||||
end)
|
||||
|
||||
it('parses endpoints correctly', function()
|
||||
clear_serverlist()
|
||||
eq({}, funcs.serverlist())
|
||||
|
||||
local s = funcs.serverstart('127.0.0.1:0') -- assign random port
|
||||
assert(string.match(s, '127.0.0.1:%d+'))
|
||||
eq(s, funcs.serverlist()[1])
|
||||
clear_serverlist()
|
||||
|
||||
s = funcs.serverstart('127.0.0.1:') -- assign random port
|
||||
assert(string.match(s, '127.0.0.1:%d+'))
|
||||
eq(s, funcs.serverlist()[1])
|
||||
clear_serverlist()
|
||||
|
||||
funcs.serverstart('127.0.0.1:12345')
|
||||
funcs.serverstart('127.0.0.1:12345') -- exists already; ignore
|
||||
funcs.serverstart('::1:12345')
|
||||
funcs.serverstart('::1:12345') -- exists already; ignore
|
||||
local expected = {
|
||||
'127.0.0.1:12345',
|
||||
'::1:12345',
|
||||
}
|
||||
eq(expected, funcs.serverlist())
|
||||
clear_serverlist()
|
||||
|
||||
funcs.serverstart('127.0.0.1:65536') -- invalid port
|
||||
eq({}, funcs.serverlist())
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('serverlist()', function()
|
||||
@ -75,7 +110,7 @@ describe('serverlist()', function()
|
||||
-- The new servers should be at the end of the list.
|
||||
for i = 1, #servs do
|
||||
eq(servs[i], new_servs[i + n])
|
||||
nvim('command', "call serverstop('"..servs[i].."')")
|
||||
command("call serverstop('"..servs[i].."')")
|
||||
end
|
||||
-- After serverstop() the servers should NOT be in the list.
|
||||
eq(n, eval('len(serverlist())'))
|
||||
|
Loading…
Reference in New Issue
Block a user