From 08153ddd1c149c867948f4681846531d53ba7759 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 8 Sep 2024 07:07:19 -0700 Subject: [PATCH] fix(startup): ignore broken $XDG_RUNTIME_DIR #30285 Problem: $XDG_RUNTIME_DIR may be broken on WSL, which prevents starting (and even building) Nvim. #30282 Solution: - When startup fails, mention the servername in the error message. - If an autogenerated server address fails, log an error and continue with an empty `v:servername`. It's only fatal if a user provides a bad `--listen` or `$NVIM_LISTEN_ADDRESS` address. Before: $ nvim --headless --listen ./hello.sock nvim: Failed to --listen: "address already in use" $ NVIM_LISTEN_ADDRESS='./hello.sock' ./build/bin/nvim --headless nvim: Failed to --listen: "address already in use" After: $ nvim --headless --listen ./hello.sock nvim: Failed to --listen: address already in use: "./hello.sock" $ NVIM_LISTEN_ADDRESS='./hello.sock' ./build/bin/nvim --headless nvim: Failed $NVIM_LISTEN_ADDRESS: address already in use: "./hello.sock" --- src/nvim/main.c | 58 ++++++------ src/nvim/msgpack_rpc/server.c | 41 ++++++-- test/functional/vimscript/server_spec.lua | 110 +++++++++++++++------- 3 files changed, 134 insertions(+), 75 deletions(-) diff --git a/src/nvim/main.c b/src/nvim/main.c index a45ee81c81..b28667346b 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -266,7 +266,7 @@ int main(int argc, char **argv) if (argc > 1 && STRICMP(argv[1], "-ll") == 0) { if (argc == 2) { - print_mainerr(err_arg_missing, argv[1]); + print_mainerr(err_arg_missing, argv[1], NULL); exit(1); } nlua_run_script(argv, argc, 3); @@ -357,10 +357,8 @@ int main(int argc, char **argv) assert(!ui_client_channel_id && !use_builtin_ui); // Nvim server... - int listen_rv = server_init(params.listen_addr); - if (listen_rv != 0) { - mainerr("Failed to --listen", listen_rv < 0 - ? os_strerror(listen_rv) : (listen_rv == 1 ? "empty address" : NULL)); + if (!server_init(params.listen_addr)) { + mainerr(IObuff, NULL, NULL); } TIME_MSG("expanding arguments"); @@ -1053,7 +1051,7 @@ static void command_line_scan(mparm_T *parmp) // "+" or "+{number}" or "+/{pat}" or "+{command}" argument. if (argv[0][0] == '+' && !had_minmin) { if (parmp->n_commands >= MAX_ARG_CMDS) { - mainerr(err_extra_cmd, NULL); + mainerr(err_extra_cmd, NULL, NULL); } argv_idx = -1; // skip to next argument if (argv[0][1] == NUL) { @@ -1074,7 +1072,7 @@ static void command_line_scan(mparm_T *parmp) parmp->no_swap_file = true; } else { if (parmp->edit_type > EDIT_STDIN) { - mainerr(err_too_many_args, argv[0]); + mainerr(err_too_many_args, argv[0], NULL); } parmp->had_stdin_file = true; parmp->edit_type = EDIT_STDIN; @@ -1137,7 +1135,7 @@ static void command_line_scan(mparm_T *parmp) nlua_disable_preload = true; } else { if (argv[0][argv_idx]) { - mainerr(err_opt_unknown, argv[0]); + mainerr(err_opt_unknown, argv[0], NULL); } had_minmin = true; } @@ -1211,7 +1209,7 @@ static void command_line_scan(mparm_T *parmp) break; case 'q': // "-q" QuickFix mode if (parmp->edit_type != EDIT_NONE) { - mainerr(err_too_many_args, argv[0]); + mainerr(err_too_many_args, argv[0], NULL); } parmp->edit_type = EDIT_QF; if (argv[0][argv_idx]) { // "-q{errorfile}" @@ -1240,7 +1238,7 @@ static void command_line_scan(mparm_T *parmp) break; case 't': // "-t {tag}" or "-t{tag}" jump to tag if (parmp->edit_type != EDIT_NONE) { - mainerr(err_too_many_args, argv[0]); + mainerr(err_too_many_args, argv[0], NULL); } parmp->edit_type = EDIT_TAG; if (argv[0][argv_idx]) { // "-t{tag}" @@ -1274,7 +1272,7 @@ static void command_line_scan(mparm_T *parmp) case 'c': // "-c{command}" or "-c {command}" exec command if (argv[0][argv_idx] != NUL) { if (parmp->n_commands >= MAX_ARG_CMDS) { - mainerr(err_extra_cmd, NULL); + mainerr(err_extra_cmd, NULL, NULL); } parmp->commands[parmp->n_commands++] = argv[0] + argv_idx; argv_idx = -1; @@ -1291,19 +1289,19 @@ static void command_line_scan(mparm_T *parmp) break; default: - mainerr(err_opt_unknown, argv[0]); + mainerr(err_opt_unknown, argv[0], NULL); } // Handle option arguments with argument. if (want_argument) { // Check for garbage immediately after the option letter. if (argv[0][argv_idx] != NUL) { - mainerr(err_opt_garbage, argv[0]); + mainerr(err_opt_garbage, argv[0], NULL); } argc--; if (argc < 1 && c != 'S') { // -S has an optional argument - mainerr(err_arg_missing, argv[0]); + mainerr(err_arg_missing, argv[0], NULL); } argv++; argv_idx = -1; @@ -1312,7 +1310,7 @@ static void command_line_scan(mparm_T *parmp) case 'c': // "-c {command}" execute command case 'S': // "-S {file}" execute Vim script if (parmp->n_commands >= MAX_ARG_CMDS) { - mainerr(err_extra_cmd, NULL); + mainerr(err_extra_cmd, NULL, NULL); } if (c == 'S') { char *a; @@ -1343,7 +1341,7 @@ static void command_line_scan(mparm_T *parmp) if (strequal(argv[-1], "--cmd")) { // "--cmd {command}" execute command if (parmp->n_pre_commands >= MAX_ARG_CMDS) { - mainerr(err_extra_cmd, NULL); + mainerr(err_extra_cmd, NULL, NULL); } parmp->pre_commands[parmp->n_pre_commands++] = argv[0]; } else if (strequal(argv[-1], "--listen")) { @@ -1425,7 +1423,7 @@ scripterror: // Check for only one type of editing. if (parmp->edit_type > EDIT_STDIN) { - mainerr(err_too_many_args, argv[0]); + mainerr(err_too_many_args, argv[0], NULL); } parmp->edit_type = EDIT_FILE; @@ -1472,7 +1470,7 @@ scripterror: } if (embedded_mode && (silent_mode || parmp->luaf)) { - mainerr(_("--embed conflicts with -es/-Es/-l"), NULL); + mainerr(_("--embed conflicts with -es/-Es/-l"), NULL, NULL); } // If there is a "+123" or "-c" command, set v:swapcommand to the first one. @@ -2135,28 +2133,30 @@ static int execute_env(char *env) return OK; } -/// Prints the following then exits: -/// - An error message `errstr` -/// - A string `str` if not null +/// Prints a message of the form "{msg1}: {msg2}: {msg3}", then exits with code 1. /// -/// @param errstr string containing an error message -/// @param str string to append to the primary error message, or NULL -static void mainerr(const char *errstr, const char *str) +/// @param msg1 error message +/// @param msg2 extra message, or NULL +/// @param msg3 extra message, or NULL +static void mainerr(const char *msg1, const char *msg2, const char *msg3) FUNC_ATTR_NORETURN { - print_mainerr(errstr, str); + print_mainerr(msg1, msg2, msg3); os_exit(1); } -static void print_mainerr(const char *errstr, const char *str) +static void print_mainerr(const char *msg1, const char *msg2, const char *msg3) { char *prgname = path_tail(argv0); signal_stop(); // kill us with CTRL-C here, if you like - fprintf(stderr, "%s: %s", prgname, _(errstr)); - if (str != NULL) { - fprintf(stderr, ": \"%s\"", str); + fprintf(stderr, "%s: %s", prgname, _(msg1)); + if (msg2 != NULL) { + fprintf(stderr, ": \"%s\"", msg2); + } + if (msg3 != NULL) { + fprintf(stderr, ": \"%s\"", msg3); } fprintf(stderr, _("\nMore info with \"")); fprintf(stderr, "%s -h\"\n", prgname); diff --git a/src/nvim/msgpack_rpc/server.c b/src/nvim/msgpack_rpc/server.c index ae34829181..9cfe46454d 100644 --- a/src/nvim/msgpack_rpc/server.c +++ b/src/nvim/msgpack_rpc/server.c @@ -11,12 +11,14 @@ #include "nvim/event/socket.h" #include "nvim/garray.h" #include "nvim/garray_defs.h" +#include "nvim/globals.h" #include "nvim/log.h" #include "nvim/main.h" #include "nvim/memory.h" #include "nvim/msgpack_rpc/server.h" #include "nvim/os/os.h" #include "nvim/os/stdpaths_defs.h" +#include "nvim/types_defs.h" #define MAX_CONNECTIONS 32 #define ENV_LISTEN "NVIM_LISTEN_ADDRESS" // deprecated @@ -27,20 +29,24 @@ static garray_T watchers = GA_EMPTY_INIT_VALUE; # include "msgpack_rpc/server.c.generated.h" #endif -/// Initializes the module +/// Initializes resources, handles `--listen`, starts the primary server at v:servername. /// -/// @returns 0: success, 1: validation error, 2: already listening, -errno: failed to bind/listen. -int server_init(const char *listen_addr) +/// @returns true on success, false on fatal error (message stored in IObuff) +bool server_init(const char *listen_addr) { + bool ok = true; bool must_free = false; + TriState user_arg = kTrue; // User-provided --listen arg. ga_init(&watchers, sizeof(SocketWatcher *), 1); // $NVIM_LISTEN_ADDRESS (deprecated) if ((!listen_addr || listen_addr[0] == '\0') && os_env_exists(ENV_LISTEN)) { + user_arg = kFalse; // User-provided env var. listen_addr = os_getenv(ENV_LISTEN); } if (!listen_addr || listen_addr[0] == '\0') { + user_arg = kNone; // Autogenerated server address. listen_addr = server_address_new(NULL); must_free = true; } @@ -51,12 +57,6 @@ int server_init(const char *listen_addr) int rv = server_start(listen_addr); - if (os_env_exists(ENV_LISTEN)) { - // Unset $NVIM_LISTEN_ADDRESS, it's a liability hereafter. It is "input only", it must not be - // leaked to child jobs or :terminal. - os_unsetenv(ENV_LISTEN); - } - // TODO(justinmk): this is for logging_spec. Can remove this after nvim_log #7062 is merged. if (os_env_exists("__NVIM_TEST_LOG")) { ELOG("test log message"); @@ -66,7 +66,28 @@ int server_init(const char *listen_addr) xfree((char *)listen_addr); } - return rv; + if (rv == 0 || user_arg == kNone) { + // The autogenerated servername can fail if the user has a broken $XDG_RUNTIME_DIR. #30282 + // But that is not fatal (startup will continue, logged in $NVIM_LOGFILE, empty v:servername). + goto end; + } + + (void)snprintf(IObuff, IOSIZE, + user_arg == + kTrue ? "Failed to --listen: %s: \"%s\"" + : "Failed $NVIM_LISTEN_ADDRESS: %s: \"%s\"", + rv < 0 ? os_strerror(rv) : (rv == 1 ? "empty address" : "?"), + listen_addr ? listen_addr : "(empty)"); + ok = false; + +end: + if (os_env_exists(ENV_LISTEN)) { + // Unset $NVIM_LISTEN_ADDRESS, it's a liability hereafter. It is "input only", it must not be + // leaked to child jobs or :terminal. + os_unsetenv(ENV_LISTEN); + } + + return ok; } /// Teardown a single server diff --git a/test/functional/vimscript/server_spec.lua b/test/functional/vimscript/server_spec.lua index f3c72b7da8..85a179b3d5 100644 --- a/test/functional/vimscript/server_spec.lua +++ b/test/functional/vimscript/server_spec.lua @@ -41,6 +41,21 @@ describe('server', function() end end) + it('broken $XDG_RUNTIME_DIR is not fatal #30282', function() + clear { + args_rm = { '--listen' }, + env = { NVIM_LOG_FILE = testlog, XDG_RUNTIME_DIR = '/non-existent-dir/subdir//' }, + } + + if is_os('win') then + -- Windows pipes have a special namespace and thus aren't decided by $XDG_RUNTIME_DIR. + matches('nvim', api.nvim_get_vvar('servername')) + else + eq('', api.nvim_get_vvar('servername')) + t.assert_log('Failed to start server%: no such file or directory', testlog, 100) + end + end) + it('serverstart(), serverstop() does not set $NVIM', function() clear() local s = eval('serverstart()') @@ -175,56 +190,79 @@ describe('server', function() end) describe('startup --listen', function() + -- Tests Nvim output when failing to start, with and without "--headless". + -- TODO(justinmk): clear() should have a way to get stdout if Nvim fails to start. + local function _test(args, env, expected) + local function run(cmd) + return n.exec_lua(function(cmd_, env_) + return vim + .system(cmd_, { + text = true, + env = vim.tbl_extend( + 'force', + -- Avoid noise in the logs; we expect failures for these tests. + { NVIM_LOG_FILE = testlog }, + env_ or {} + ), + }) + :wait() + end, cmd, env) --[[@as vim.SystemCompleted]] + end + + local cmd = vim.list_extend({ n.nvim_prog, '+qall!', '--headless' }, args) + local r = run(cmd) + eq(1, r.code) + matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' ')) + + if is_os('win') then + return -- On Windows, output without --headless is garbage. + end + table.remove(cmd, 3) -- Remove '--headless'. + assert(not vim.tbl_contains(cmd, '--headless')) + r = run(cmd) + eq(1, r.code) + matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' ')) + end + it('validates', function() clear { env = { NVIM_LOG_FILE = testlog } } - - -- Tests args with and without "--headless". - local function _test(args, expected) - local function run(cmd) - return n.exec_lua(function(cmd_) - return vim - .system(cmd_, { - text = true, - env = { - -- Avoid noise in the logs; we expect failures for these tests. - NVIM_LOG_FILE = testlog, - }, - }) - :wait() - end, cmd) --[[@as vim.SystemCompleted]] - end - - local cmd = vim.list_extend({ n.nvim_prog, '+qall!', '--headless' }, args) - local r = run(cmd) - eq(1, r.code) - matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' ')) - - if is_os('win') then - return -- On Windows, output without --headless is garbage. - end - table.remove(cmd, 3) -- Remove '--headless'. - assert(not vim.tbl_contains(cmd, '--headless')) - r = run(cmd) - eq(1, r.code) - matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' ')) - end + local in_use = n.eval('v:servername') ---@type string Address already used by another server. t.assert_nolog('Failed to start server', testlog, 100) t.assert_nolog('Host lookup failed', testlog, 100) - _test({ '--listen' }, 'nvim.*: Argument missing after: "%-%-listen"') - _test({ '--listen2' }, 'nvim.*: Garbage after option argument: "%-%-listen2"') - _test({ '--listen', n.eval('v:servername') }, 'nvim.*: Failed to %-%-listen: ".* already .*"') - _test({ '--listen', '/' }, 'nvim.*: Failed to %-%-listen: ".*"') + _test({ '--listen' }, nil, 'nvim.*: Argument missing after: "%-%-listen"') + _test({ '--listen2' }, nil, 'nvim.*: Garbage after option argument: "%-%-listen2"') + _test( + { '--listen', in_use }, + nil, + ('nvim.*: Failed to %%-%%-listen: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use)) + ) + _test({ '--listen', '/' }, nil, 'nvim.*: Failed to %-%-listen: [^:]+: "/"') _test( { '--listen', 'https://example.com' }, - ('nvim.*: Failed to %%-%%-listen: "%s"'):format( + nil, + ('nvim.*: Failed to %%-%%-listen: %s: "https://example.com"'):format( is_os('mac') and 'unknown node or service' or 'service not available for socket type' ) ) t.assert_log('Failed to start server', testlog, 100) t.assert_log('Host lookup failed', testlog, 100) + + _test( + {}, + { NVIM_LISTEN_ADDRESS = in_use }, + ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use)) + ) + _test({}, { NVIM_LISTEN_ADDRESS = '/' }, 'nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+: "/"') + _test( + {}, + { NVIM_LISTEN_ADDRESS = 'https://example.com' }, + ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: %s: "https://example.com"'):format( + is_os('mac') and 'unknown node or service' or 'service not available for socket type' + ) + ) end) it('sets v:servername, overrides $NVIM_LISTEN_ADDRESS', function()