mirror of
https://github.com/neovim/neovim.git
synced 2026-11-06 05:16:52 +00:00
feat(remote-ssh): supervise server in Lua, "Update Nvim" for nightly #42706
Problem: - remote_ssh.sh detects ssh disconnect by writing a heartbeat every 1s, which the local side buffers for the whole session. - The server's stdout (e.g. `io.write()` from a random plugin) writes to the tunnel, which in theory may contain a bogus "NVIM_READY". - A nightly install is never updated on the remote. Solution: - Supervise the server in `vim.net._ssh.serve()` (Lua). It detects disconnect via stdin EOF and discards server stdout. The script only finds or installs `nvim`. - Also a step toward Windows remotes. - User may choose "Update Nvim" to reinstall the nightly.
This commit is contained in:
1 parent
67f6f0503c
commit
893d90fced
7 files changed
+479
-335
No files matched your search
+3
-1
@@ -160,7 +160,9 @@ Connect UI to a different server
|
||||
- Config: `~/.config/nvim-remote/`
|
||||
- Data: `~/.local/share/nvim-remote/`, kept across sessions
|
||||
for fast reconnects. Caches installed releases, e.g.
|
||||
`releases/v0.12.0-linux-x86_64/`.
|
||||
`releases/v0.12.0-linux-x86_64/`. Nightly is installed once;
|
||||
choose "Update Nvim" at the prompt to refresh it (may break
|
||||
other sessions using it).
|
||||
|
||||
Requirements:
|
||||
- Local: `ssh` (OpenSSH 7.6+). Win32-OpenSSH lacks
|
||||
|
||||
@@ -876,11 +876,9 @@ local function check_sysinfo()
|
||||
vim.wo[win].winbar =
|
||||
'%#WarningMsg#%@v:lua.nvim_health_bugreport_open@▶ Create Bug Report on GitHub%X%*'
|
||||
|
||||
vim.api.nvim_create_autocmd('BufDelete', {
|
||||
buf = buf,
|
||||
once = true,
|
||||
command = 'lua _G.nvim_health_bugreport_open = nil',
|
||||
})
|
||||
nvim_on('BufDelete', nil, { buf = buf, once = true }, function()
|
||||
_G.nvim_health_bugreport_open = nil
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
+108
-15
@@ -1,6 +1,8 @@
|
||||
-- Converted into Lua from https://github.com/cyjake/ssh-config
|
||||
-- TODO (siddhantdev): deal with include directives
|
||||
|
||||
local nvim_on = require('vim._core.util').nvim_on
|
||||
|
||||
local M = {}
|
||||
|
||||
local whitespace_pattern = '%s'
|
||||
@@ -382,7 +384,8 @@ local function get_ssh_cmd(ssh_uri, opts)
|
||||
end
|
||||
|
||||
--- Runs the local `$VIMRUNTIME/scripts/remote_ssh.sh` on the remote host. The script goes to
|
||||
--- `sh -s` over stdin, so the remote login shell (any shell) parses only `args`.
|
||||
--- `sh -s` over stdin, so the remote login shell (any shell) parses only `args`. Stdin stays open:
|
||||
--- its EOF tells the remote (`M.serve()`) that ssh disconnected.
|
||||
---
|
||||
---@param uri vim.Uri & {host:string}
|
||||
---@param args string[] Script args: plain words, which no shell expands, so they need no quoting.
|
||||
@@ -399,7 +402,11 @@ local function run_script(uri, args, opts, on_exit)
|
||||
local remote_cmd = vim.list_extend({ 'sh', '-s', '--' }, args)
|
||||
local ssh_cmd = get_ssh_cmd(uri, { ssh_args = opts.ssh_args, remote_cmd = remote_cmd })
|
||||
log().debug('run_script: running', ssh_cmd)
|
||||
return vim.system(ssh_cmd, { stdin = script }, on_exit)
|
||||
local obj = vim.system(ssh_cmd, { stdin = true }, on_exit)
|
||||
-- Write the script to stdin, `sh -s -` executes it.
|
||||
-- Stdin stays open and the remote Nvim inherits it (see remote_ssh.sh).
|
||||
obj:write(script)
|
||||
return obj
|
||||
end
|
||||
|
||||
--- Gets the operating system and architecture from the remote system.
|
||||
@@ -454,7 +461,7 @@ end
|
||||
local function get_release(os, arch)
|
||||
local ver = vim.version()
|
||||
local base = ('v%d.%d.%d'):format(ver.major, ver.minor, ver.patch)
|
||||
-- prereleases: install the current nightly, once per version
|
||||
-- Prerelease: installs the current nightly once per version, until "Update" (see `M.connect()`).
|
||||
local tag = ver.prerelease and 'nightly' or base
|
||||
local install_name = ver.prerelease and ('nightly-%s'):format(base) or base
|
||||
|
||||
@@ -496,7 +503,8 @@ end
|
||||
--- See `M.connect()`.
|
||||
---@param chan? integer
|
||||
---@param report fun(status: 'running'|'success'|'failed', msg: string)
|
||||
local function connect(uri_str, chan, report)
|
||||
---@param update boolean Re-installs the release.
|
||||
local function connect(uri_str, chan, report, update)
|
||||
local uri = vim.uri.parse(uri_str)
|
||||
if not (uri and uri.host and uri.host ~= '') then
|
||||
error(('No host in SSH URI: %s'):format(uri_str), 0)
|
||||
@@ -556,12 +564,11 @@ local function connect(uri_str, chan, report)
|
||||
|
||||
report('running', 'Starting remote Nvim...')
|
||||
log().debug('connect: local_sock', local_sock)
|
||||
tunnel = run_script(
|
||||
uri,
|
||||
{ remote_sock, dir, url, version },
|
||||
{ ssh_args = { '-L', forward } },
|
||||
on_exit
|
||||
)
|
||||
local args = { remote_sock, dir, url, version }
|
||||
if update then
|
||||
table.insert(args, 1, '--update')
|
||||
end
|
||||
tunnel = run_script(uri, args, { ssh_args = { '-L', forward } }, on_exit)
|
||||
local res = tunnel:wait(300000, function(stdout)
|
||||
return stdout:match('NVIM_READY') ~= nil
|
||||
end)
|
||||
@@ -593,9 +600,7 @@ local function connect(uri_str, chan, report)
|
||||
end
|
||||
|
||||
report('success', ('Connected to %s'):format(uri_str))
|
||||
vim.api.nvim_create_autocmd('VimLeavePre', {
|
||||
callback = teardown,
|
||||
})
|
||||
nvim_on('VimLeavePre', nil, teardown)
|
||||
|
||||
return local_sock, teardown
|
||||
end
|
||||
@@ -612,11 +617,16 @@ function M.connect(uri_str, chan)
|
||||
uri_str,
|
||||
remote_data_dir
|
||||
)
|
||||
if vim.fn.confirm(msg, 'Connect? &Yes\n&No', 1, 'Question') ~= 1 then
|
||||
local prerelease = vim.version().prerelease ~= nil
|
||||
local choices = prerelease and 'Connect? &Yes\n&Update Nvim\n&No' -- Nightly is refreshed if "(U)pdate".
|
||||
or 'Connect? &Yes\n&No'
|
||||
local choice = vim.fn.confirm(msg, choices, 1, 'Question')
|
||||
local update = prerelease and choice == 2
|
||||
if choice ~= 1 and not update then
|
||||
return nil
|
||||
end
|
||||
local report = new_progress_report()
|
||||
local ok, local_sock, teardown = pcall(connect, uri_str, chan, report)
|
||||
local ok, local_sock, teardown = pcall(connect, uri_str, chan, report, update)
|
||||
if not ok then
|
||||
report('failed', ('Failed to connect to %s'):format(uri_str))
|
||||
error(local_sock, 0)
|
||||
@@ -624,4 +634,87 @@ function M.connect(uri_str, chan)
|
||||
return local_sock, teardown
|
||||
end
|
||||
|
||||
--- Gets the PID of the server at `sock`, or nil if none answers.
|
||||
---@param sock string
|
||||
---@return integer?
|
||||
local function server_pid(sock)
|
||||
local ok, chan = pcall(vim.fn.sockconnect, 'pipe', sock, { rpc = true, trust = false })
|
||||
if not ok or chan == 0 then
|
||||
return nil
|
||||
end
|
||||
---@cast chan integer
|
||||
local _, pid = pcall(vim.rpcrequest, chan, { timeout = 5000 }, 'nvim_call_function', 'getpid', {})
|
||||
pcall(vim.fn.chanclose, chan)
|
||||
return type(pid) == 'number' and pid --[[@as integer]] or nil
|
||||
end
|
||||
|
||||
--- Runs on the remote host: starts the server at `sock` and supervises it, following a `:restart`ed
|
||||
--- server (which re-binds `sock`).
|
||||
---
|
||||
--- PROTOCOL: `remote_ssh.sh` of other (older) Nvim versions calls this, so keep it compatible, or
|
||||
--- extend it with an `opts` param (e.g. a protocol version):
|
||||
--- - Called as `nvim … +"lua require('vim.net._ssh').serve(sock)" +qa!`, with stdin from ssh.
|
||||
--- - Creates the dir of `sock`, which must not exist (for security), with mode 0700.
|
||||
--- - Prints "NVIM_READY" once the server listens, or "NVIM_CRASHED".
|
||||
--- - Returns when stdin closes (ssh disconnect) or the server disappears.
|
||||
--- - On exit (also SIGTERM, SIGHUP): stops the server (SIGTERM) and removes the dir.
|
||||
---@param sock string
|
||||
function M.serve(sock)
|
||||
local dir = vim.fs.dirname(sock)
|
||||
-- Fresh private dir (0700): fails if it exists, so nobody can plant a socket.
|
||||
assert(vim.uv.fs_mkdir(dir, 448))
|
||||
-- Not a peer (`serverlist()`) of the server.
|
||||
vim.fn.serverstop(vim.v.servername)
|
||||
|
||||
-- No pty, so no SIGHUP when ssh disconnects: stdin EOF instead (see `run_script()`).
|
||||
local disconnected = false
|
||||
local stdin = assert(vim.uv.new_pipe())
|
||||
stdin:open(0)
|
||||
stdin:read_start(function(_, data)
|
||||
disconnected = disconnected or data == nil
|
||||
end)
|
||||
|
||||
local pid --- @type integer? Current server.
|
||||
-- Cleanup: stops the server and removes `dir` when Nvim exits (SIGTERM/SIGHUP).
|
||||
nvim_on('VimLeavePre', nil, function()
|
||||
local cur = pid and (server_pid(sock) or pid)
|
||||
if cur then
|
||||
-- SIGTERM (unlike `:qall!`) preserves swap files.
|
||||
vim.uv.kill(cur, 'sigterm')
|
||||
vim.wait(5000, function()
|
||||
return vim.uv.kill(cur, 0) ~= 0
|
||||
end, 50)
|
||||
end
|
||||
vim.fs.rm(dir, { recursive = true, force = true })
|
||||
stdin:close() -- Release stdin handle, else EXITFREE build will complain.
|
||||
end)
|
||||
|
||||
vim.env.__NVIM_STARTREASON = 'remote-ssh'
|
||||
vim.env.NVIM_APPNAME = 'nvim-remote'
|
||||
local server = vim.system({ vim.v.progpath, '--headless', '--listen', sock }, {
|
||||
stdout = false,
|
||||
stderr = function(_, data)
|
||||
io.stderr:write(data or '')
|
||||
end,
|
||||
})
|
||||
vim.wait(vim._maxint, function()
|
||||
return disconnected or server:is_closing() or vim.uv.fs_stat(sock) ~= nil
|
||||
end, 50)
|
||||
io.stdout:write(server:is_closing() and 'NVIM_CRASHED\n' or 'NVIM_READY\n')
|
||||
io.stdout:flush()
|
||||
|
||||
pid = server.pid
|
||||
-- Supervises the server (polls every 1s) at `sock` until ssh disconnects, including a new server after `:restart`.
|
||||
while pid and not disconnected do
|
||||
local cur = pid
|
||||
vim.wait(vim._maxint, function()
|
||||
return disconnected or vim.uv.kill(cur, 0) ~= 0
|
||||
end, 1000)
|
||||
if not disconnected then
|
||||
-- The server exited. A `:restart`ed server re-binds `sock`.
|
||||
pid = server_pid(sock)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Runs the headless Nvim server of a |remote-ssh| session, until it exits or ssh disconnects.
|
||||
# Prints NVIM_READY once the server listens (or NVIM_CRASHED).
|
||||
# Bootstraps the headless Nvim server of a |remote-ssh| session: finds or installs `nvim`, which then
|
||||
# runs and supervises the server (vim.net._ssh.serve()).
|
||||
#
|
||||
# Usage: remote_ssh.sh {sock} {dir} {url} [{version}]
|
||||
# Usage: remote_ssh.sh [--update] {sock} {dir} {url} [{version}]
|
||||
# Runs `nvim` from $PATH if it is {version} or newer, else the release {url}, which it installs
|
||||
# to {dir} (relative to $HOME) unless present. E.g.
|
||||
# to {dir} (relative to $HOME) unless present, or always with --update. E.g.
|
||||
# remote_ssh.sh /tmp/nvim-remote.0123abcd/nvim.sock .local/share/nvim-remote/releases/v0.12.0-linux-x86_64 https://…/nvim-linux-x86_64.tar.gz v0.12.0
|
||||
|
||||
set -eu
|
||||
@@ -28,55 +28,32 @@ system_nvim() {
|
||||
echo "$nvim"
|
||||
}
|
||||
|
||||
# Installs the release to $dest, unless present.
|
||||
# Installs the release to $dest, unless present (and not $update).
|
||||
install_release() (
|
||||
if [ -x "$dest/bin/nvim" ]; then
|
||||
if [ -x "$dest/bin/nvim" ] && [ -z "$update" ]; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Installing $dest..." >&2
|
||||
mkdir -p "${dest%/*}"
|
||||
tmp=$(mktemp -d "$dest.XXXXXX")
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
curl -fsSL "$url" | tar -xzf - -C "$tmp"
|
||||
mkdir "$tmp/new"
|
||||
curl -fsSL "$url" | tar -xzf - -C "$tmp/new"
|
||||
if [ -n "$update" ]; then
|
||||
# Removed with $tmp. Gone already if a concurrent update moved it.
|
||||
mv "$dest" "$tmp/old" 2>/dev/null || :
|
||||
fi
|
||||
# The archive holds one top-level dir, e.g. `nvim-linux-x86_64/`. Atomic rename; keeps a
|
||||
# concurrent install that finished first.
|
||||
[ -e "$dest" ] || mv "$tmp"/* "$dest"
|
||||
[ -e "$dest" ] || mv "$tmp"/new/* "$dest"
|
||||
)
|
||||
|
||||
# Starts `nvim` ($1) as the server at $sock, in a private dir. Both are removed on exit.
|
||||
# Prints NVIM_READY once it listens, or NVIM_CRASHED.
|
||||
start_server() {
|
||||
dir=${sock%/*}
|
||||
# Fresh private dir: `mkdir` fails if it exists, so nobody can plant a socket.
|
||||
mkdir -m 700 "$dir"
|
||||
nvim_pid=
|
||||
trap 'kill "$nvim_pid" 2>/dev/null || true; rm -rf "$dir"' EXIT
|
||||
trap 'exit 1' HUP INT TERM PIPE
|
||||
__NVIM_STARTREASON=remote-ssh NVIM_APPNAME=nvim-remote "$1" --headless --listen "$sock" </dev/null &
|
||||
nvim_pid=$!
|
||||
while [ ! -S "$sock" ]; do
|
||||
if ! kill -0 "$nvim_pid" 2>/dev/null; then
|
||||
echo "NVIM_CRASHED"
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
echo "NVIM_READY"
|
||||
}
|
||||
|
||||
# Waits until the server exits or ssh disconnects. No pty, so no SIGHUP on disconnect: a failed
|
||||
# heartbeat write triggers the EXIT trap.
|
||||
heartbeat() {
|
||||
while kill -0 "$nvim_pid" 2>/dev/null; do
|
||||
# `wait` (unlike a foreground `sleep`) lets a signal run its trap at once. The `sleep` must not
|
||||
# hold stdout, else the closing ssh channel waits for it.
|
||||
sleep 1 >/dev/null 2>&1 &
|
||||
wait $!
|
||||
printf '\n' 2>/dev/null || exit 1
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
update=
|
||||
if [ "$1" = --update ]; then
|
||||
update=1
|
||||
shift
|
||||
fi
|
||||
sock=$1
|
||||
dest="$HOME/$2"
|
||||
url=$3
|
||||
@@ -85,8 +62,8 @@ main() {
|
||||
install_release
|
||||
nvim="$dest/bin/nvim"
|
||||
fi
|
||||
start_server "$nvim"
|
||||
heartbeat
|
||||
# Nvim inherits stdin from `sh -s` (i.e. from ssh), so EOF tells serve() that ssh disconnected.
|
||||
exec "$nvim" --clean -n --headless +"lua require('vim.net._ssh').serve('$sock')" +qa!
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+2
-1
@@ -4896,7 +4896,8 @@ static void ex_exmode(exarg_T *eap)
|
||||
/// ":restart +cmd <command>": restart the Nvim server using ":cmd" and runs <command> in the new server.
|
||||
static void ex_restart(exarg_T *eap)
|
||||
{
|
||||
// XXX: The UI reaches this server through an SSH tunnel, which cannot reach the new server.
|
||||
// XXX: The UI reaches this server through an SSH tunnel, but the "restart" event gives it the new
|
||||
// server's (remote) address.
|
||||
if (strequal(get_vim_var_str(VV_STARTREASON), "remote-ssh")) {
|
||||
emsg(_("E6103: :restart is not supported with remote-ssh"));
|
||||
return;
|
||||
|
||||
@@ -33,10 +33,18 @@ elseif not args:find(' -L ', 1, true) then
|
||||
fail(('Unexpected SSH command: %s'):format(args))
|
||||
end
|
||||
|
||||
args = ('%s %s'):format(args, io.stdin:read('*a'))
|
||||
-- The script on stdin, which stays open (see `run_script()`).
|
||||
local script = {} --- @type string[]
|
||||
for line in io.stdin:lines() do
|
||||
script[#script + 1] = line
|
||||
if line == 'main "$@"' then
|
||||
break
|
||||
end
|
||||
end
|
||||
args = ('%s %s'):format(args, table.concat(script, '\n'))
|
||||
if not args:find('ControlMaster', 1, true) then
|
||||
fail('FAIL: Multiplexing flags missing!')
|
||||
elseif not args:find(' sh %-s .*NVIM_READY') then
|
||||
elseif not args:find(' sh %-s .*main "%$@"') then
|
||||
fail('FAIL: tunnel script not fed to sh -s')
|
||||
end
|
||||
write('tunnel.args', args)
|
||||
|
||||
+333
-268
@@ -61,8 +61,8 @@ describe('parse_ssh_config()', function()
|
||||
end)
|
||||
|
||||
it('ignores repeated and trailing separators', function()
|
||||
-- Runs of separators between values, and trailing whitespace before the
|
||||
-- line break, must not yield empty hostnames.
|
||||
-- Runs of separators between values, and trailing whitespace before the line break, must not
|
||||
-- yield empty aliases.
|
||||
local config = table.concat({
|
||||
'Host alpha beta ',
|
||||
' HostName example.com',
|
||||
@@ -106,7 +106,7 @@ describe('parse_ssh_config()', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('vim.net._ssh', function()
|
||||
describe('remote-ssh', function()
|
||||
local fake_dir --- @type string
|
||||
|
||||
--- Makes `vim.net._ssh` run a fake `ssh` (fixtures/fake-ssh.lua), and stubs `confirm()`.
|
||||
@@ -156,292 +156,357 @@ describe('vim.net._ssh', function()
|
||||
clear()
|
||||
end)
|
||||
|
||||
describe('remote-ssh (Introspection)', function()
|
||||
it('detects linux x86_64', function()
|
||||
setup_fake_ssh({
|
||||
uname = 'Linux\nx86_64\n',
|
||||
})
|
||||
local res = n.exec_lua([[
|
||||
return { require('vim.net._ssh').get_system_info({ host = 'server' }) }
|
||||
]])
|
||||
eq('linux', res[1])
|
||||
eq('x86_64', res[2])
|
||||
end)
|
||||
it('get_system_info() detects OS and arch', function()
|
||||
for _, c in ipairs({
|
||||
{ 'Linux\nx86_64\n', { 'linux', 'x86_64' } },
|
||||
{ 'Darwin\narm64\n', { 'darwin', 'arm64' } },
|
||||
}) do
|
||||
setup_fake_ssh({ uname = c[1] })
|
||||
eq(
|
||||
c[2],
|
||||
n.exec_lua(function()
|
||||
return { require('vim.net._ssh').get_system_info({ host = 'server' }) }
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
it('detects macos arm64', function()
|
||||
setup_fake_ssh({
|
||||
uname = 'Darwin\narm64\n',
|
||||
})
|
||||
local res = n.exec_lua([[
|
||||
return { require('vim.net._ssh').get_system_info({ host = 'server' }) }
|
||||
]])
|
||||
eq('darwin', res[1])
|
||||
eq('arm64', res[2])
|
||||
end)
|
||||
|
||||
it('fails fast on Windows', function()
|
||||
setup_fake_ssh({
|
||||
uname = 'MSYS_NT-10.0-19045\nx86_64\n',
|
||||
})
|
||||
local status, err = pcall(function()
|
||||
n.exec_lua([[
|
||||
require('vim.net._ssh').get_system_info({ host = 'server' })
|
||||
]])
|
||||
setup_fake_ssh({ uname = 'MSYS_NT-10.0-19045\nx86_64\n' })
|
||||
t.matches(
|
||||
'Windows targets are not supported',
|
||||
t.pcall_err(n.exec_lua, function()
|
||||
require('vim.net._ssh').get_system_info({ host = 'server' })
|
||||
end)
|
||||
eq(false, status)
|
||||
assert(string.match(err, 'Windows targets are not supported'))
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
describe('remote-ssh (Orchestration)', function()
|
||||
it('returns local socket with key-based auth', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
setup_fake_ssh()
|
||||
local sock, logfile, progress = unpack(n.exec_lua([[
|
||||
local logfile = vim.fs.joinpath(vim.fn.stdpath('log'), 'remote-ssh.log')
|
||||
os.remove(logfile)
|
||||
local progress = {}
|
||||
vim.api.nvim_create_autocmd('Progress', {
|
||||
pattern = 'remote-ssh',
|
||||
callback = function(ev)
|
||||
table.insert(progress, ('%s: %s'):format(ev.data.status, table.concat(ev.data.text)))
|
||||
end,
|
||||
})
|
||||
local sock, teardown = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
teardown()
|
||||
return { sock, logfile, progress }
|
||||
]]))
|
||||
|
||||
assert(sock:match('_remote_nvim%.sock$'))
|
||||
eq({
|
||||
'running: Connecting to ssh://user@test-server...',
|
||||
'running: Starting remote Nvim...',
|
||||
'success: Connected to ssh://user@test-server',
|
||||
}, progress)
|
||||
local msg = 'This will install Nvim on ssh://user@test-server (in ~/.local/share/nvim-remote/), '
|
||||
.. 'if missing.\n'
|
||||
eq({ { msg, 'Connect? &Yes\n&No', 1, 'Question' } }, n.exec_lua('return _G.confirm_log'))
|
||||
t.matches('Connected to ssh://user@test%-server', t.read_file(logfile))
|
||||
local tunnel_args = t.read_file(fake_dir .. '/tunnel.args')
|
||||
t.matches('BatchMode=yes', tunnel_args)
|
||||
t.matches('RemoteCommand=none', tunnel_args)
|
||||
t.matches('StrictHostKeyChecking=accept%-new', tunnel_args)
|
||||
t.matches('ServerAliveInterval=15', tunnel_args)
|
||||
-- Teardown stops the tunnel and cancels its forward, but not the local server.
|
||||
eq(true, tunnel_exited())
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
n.poke_eventloop()
|
||||
n.assert_alive()
|
||||
|
||||
-- Explicit ssh_config values are kept. The target is decoded, with an IPv6 host unbracketed.
|
||||
setup_fake_ssh({ ssh_config = 'stricthostkeychecking yes\nserveraliveinterval 60\n' })
|
||||
n.exec_lua(function()
|
||||
local _, teardown = require('vim.net._ssh').connect('ssh://a%40b@[::1]:2222/x')
|
||||
teardown()
|
||||
end)
|
||||
local explicit_args = t.read_file(fake_dir .. '/tunnel.args')
|
||||
t.matches(' %-p 2222 %-%- a@b@::1 sh ', explicit_args)
|
||||
eq(nil, explicit_args:find('StrictHostKeyChecking', 1, true))
|
||||
eq(nil, explicit_args:find('ServerAliveInterval', 1, true))
|
||||
|
||||
-- Stops the session if the UI detached meanwhile.
|
||||
setup_fake_ssh()
|
||||
t.matches(
|
||||
'UI detached: channel 999',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server', 999)]])
|
||||
)
|
||||
eq(true, tunnel_exited())
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
end)
|
||||
|
||||
it('stops the local server when the session ends, unless it is in use', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
setup_fake_ssh()
|
||||
local logfile =
|
||||
n.exec_lua([[return vim.fs.joinpath(vim.fn.stdpath('log'), 'remote-ssh.log')]])
|
||||
it('connect() returns the local socket of the tunnel', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
setup_fake_ssh()
|
||||
local sock, logfile, progress = unpack(n.exec_lua([[
|
||||
local logfile = vim.fs.joinpath(vim.fn.stdpath('log'), 'remote-ssh.log')
|
||||
os.remove(logfile)
|
||||
-- Connects, then ends the session as the remote does: its Nvim quits.
|
||||
local session = [[
|
||||
local sock = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
vim.rpcnotify(vim.fn.sockconnect('pipe', sock, { rpc = true }), 'nvim_command', 'qall!')
|
||||
]]
|
||||
local progress = {}
|
||||
vim.api.nvim_create_autocmd('Progress', {
|
||||
pattern = 'remote-ssh',
|
||||
callback = function(ev)
|
||||
table.insert(progress, ('%s: %s'):format(ev.data.status, table.concat(ev.data.text)))
|
||||
end,
|
||||
})
|
||||
local sock, teardown = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
teardown()
|
||||
return { sock, logfile, progress }
|
||||
]]))
|
||||
|
||||
n.command('set modified')
|
||||
n.exec_lua(session)
|
||||
t.retry(nil, 5000, function()
|
||||
t.matches('local server in use, keeping it', t.read_file(logfile))
|
||||
end)
|
||||
n.assert_alive()
|
||||
assert(sock:match('_remote_nvim%.sock$'))
|
||||
eq({
|
||||
'running: Connecting to ssh://user@test-server...',
|
||||
'running: Starting remote Nvim...',
|
||||
'success: Connected to ssh://user@test-server',
|
||||
}, progress)
|
||||
local msg = 'This will install Nvim on ssh://user@test-server (in ~/.local/share/nvim-remote/), '
|
||||
.. 'if missing.\n'
|
||||
local choices = n.exec_lua('return vim.version().prerelease')
|
||||
and 'Connect? &Yes\n&Update Nvim\n&No'
|
||||
or 'Connect? &Yes\n&No'
|
||||
eq({ { msg, choices, 1, 'Question' } }, n.exec_lua('return _G.confirm_log'))
|
||||
t.matches('Connected to ssh://user@test%-server', t.read_file(logfile))
|
||||
local tunnel_args = t.read_file(fake_dir .. '/tunnel.args')
|
||||
t.matches('BatchMode=yes', tunnel_args)
|
||||
t.matches('RemoteCommand=none', tunnel_args)
|
||||
t.matches('StrictHostKeyChecking=accept%-new', tunnel_args)
|
||||
t.matches('ServerAliveInterval=15', tunnel_args)
|
||||
-- Teardown stops the tunnel and cancels its forward, but not the local server.
|
||||
eq(true, tunnel_exited())
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
n.poke_eventloop()
|
||||
n.assert_alive()
|
||||
|
||||
n.command('set nomodified')
|
||||
n.expect_exit(5000, n.exec_lua, session)
|
||||
-- Explicit ssh_config values are kept. The target is decoded, with an IPv6 host unbracketed.
|
||||
setup_fake_ssh({ ssh_config = 'stricthostkeychecking yes\nserveraliveinterval 60\n' })
|
||||
n.exec_lua(function()
|
||||
local _, teardown = require('vim.net._ssh').connect('ssh://a%40b@[::1]:2222/x')
|
||||
teardown()
|
||||
end)
|
||||
local explicit_args = t.read_file(fake_dir .. '/tunnel.args')
|
||||
t.matches(' %-p 2222 %-%- a@b@::1 sh ', explicit_args)
|
||||
eq(nil, explicit_args:find('StrictHostKeyChecking', 1, true))
|
||||
eq(nil, explicit_args:find('ServerAliveInterval', 1, true))
|
||||
eq(
|
||||
'No host in SSH URI: ssh:///x',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh:///x')]])
|
||||
)
|
||||
|
||||
it('passes the matching release to the remote script', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
setup_fake_ssh()
|
||||
local base, prerelease = unpack(n.exec_lua(function()
|
||||
-- Stops the session if the UI detached meanwhile.
|
||||
setup_fake_ssh()
|
||||
t.matches(
|
||||
'UI detached: channel 999',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server', 999)]])
|
||||
)
|
||||
eq(true, tunnel_exited())
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
end)
|
||||
|
||||
it('stops the local server when the session ends, unless it is in use', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
setup_fake_ssh()
|
||||
local logfile = n.exec_lua([[return vim.fs.joinpath(vim.fn.stdpath('log'), 'remote-ssh.log')]])
|
||||
os.remove(logfile)
|
||||
-- Connects, then ends the session as the remote does: its Nvim quits.
|
||||
local session = [[
|
||||
local sock = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
vim.rpcnotify(vim.fn.sockconnect('pipe', sock, { rpc = true }), 'nvim_command', 'qall!')
|
||||
]]
|
||||
|
||||
n.command('set modified')
|
||||
n.exec_lua(session)
|
||||
t.retry(nil, 5000, function()
|
||||
t.matches('local server in use, keeping it', t.read_file(logfile))
|
||||
end)
|
||||
n.assert_alive()
|
||||
|
||||
n.command('set nomodified')
|
||||
n.expect_exit(5000, n.exec_lua, session)
|
||||
end)
|
||||
|
||||
it('passes the matching release to the remote script', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
setup_fake_ssh()
|
||||
local base, prerelease = unpack(n.exec_lua(function()
|
||||
local _, teardown = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
teardown()
|
||||
local v = vim.version()
|
||||
return { ('v%d.%d.%d'):format(v.major, v.minor, v.patch), v.prerelease and true or false }
|
||||
end))
|
||||
local url = 'https://github.com/neovim/neovim/releases/download/%s/nvim-linux-x86_64.tar.gz'
|
||||
local dir = '.local/share/nvim-remote/releases/%s-linux-x86_64'
|
||||
-- A prerelease installs the nightly, and passes no version, so a system `nvim` is never used.
|
||||
local args = prerelease and { dir:format('nightly-' .. base), url:format('nightly') }
|
||||
or { dir:format(base), url:format(base), base }
|
||||
eq(
|
||||
table.concat(args, ' '),
|
||||
t.read_file(fake_dir .. '/tunnel.args')
|
||||
:match(' sh %-s %-%- /tmp/nvim%-remote%.%x+/nvim%.sock (.-) #!/bin/sh')
|
||||
)
|
||||
|
||||
-- "Update Nvim" (prerelease only) re-installs the nightly.
|
||||
if prerelease then
|
||||
setup_fake_ssh({ confirm = 2 })
|
||||
n.exec_lua(function()
|
||||
local _, teardown = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
teardown()
|
||||
local v = vim.version()
|
||||
return { ('v%d.%d.%d'):format(v.major, v.minor, v.patch), v.prerelease and true or false }
|
||||
end))
|
||||
local url = 'https://github.com/neovim/neovim/releases/download/%s/nvim-linux-x86_64.tar.gz'
|
||||
local dir = '.local/share/nvim-remote/releases/%s-linux-x86_64'
|
||||
-- A prerelease installs the current nightly, and passes no version for a system `nvim`.
|
||||
local args = prerelease and { dir:format('nightly-' .. base), url:format('nightly') }
|
||||
or { dir:format(base), url:format(base), base }
|
||||
eq(
|
||||
table.concat(args, ' '),
|
||||
t.read_file(fake_dir .. '/tunnel.args')
|
||||
:match(' sh %-s %-%- /tmp/nvim%-remote%.%x+/nvim%.sock (.-) #!/bin/sh')
|
||||
)
|
||||
end)
|
||||
|
||||
it('remote script runs `nvim` from $PATH if new enough, else installs the release', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
local home = vim.fs.abspath(t.tmpname(false))
|
||||
t.finally(function()
|
||||
n.rmdir(home)
|
||||
end)
|
||||
local nvim_prog = vim.fs.abspath(n.nvim_prog)
|
||||
n.mkdir_p(home .. '/bin')
|
||||
vim.uv.fs_symlink(nvim_prog, home .. '/bin/nvim')
|
||||
-- Release archive: its `nvim` marks the server it runs.
|
||||
local release = home .. '/src/nvim-test'
|
||||
n.mkdir_p(release .. '/bin')
|
||||
t.write_file(
|
||||
release .. '/bin/nvim',
|
||||
('#!/bin/sh\nexport NVIM_TEST_RELEASE=1\nexec %s "$@"\n'):format(nvim_prog)
|
||||
)
|
||||
vim.uv.fs_chmod(release .. '/bin/nvim', 493)
|
||||
local tarball = home .. '/nvim-test.tar.gz'
|
||||
eq(0, vim.system({ 'tar', '-czf', tarball, '-C', home .. '/src', 'nvim-test' }):wait().code)
|
||||
local dir = 'releases/v1-test'
|
||||
t.matches(' sh %-s %-%- %-%-update /tmp/', t.read_file(fake_dir .. '/tunnel.args'))
|
||||
end
|
||||
end)
|
||||
|
||||
--- Runs the script locally, as the remote does, and asks its server for `[$NVIM_APPNAME,
|
||||
--- $NVIM_TEST_RELEASE, v:startreason]`.
|
||||
local function serve(version)
|
||||
return n.exec_lua(function(...)
|
||||
local sock = ('%s/sock/nvim.sock'):format(home)
|
||||
local cmd =
|
||||
{ 'env', ('HOME=%s'):format(home), ('PATH=%s/bin:%s'):format(home, vim.env.PATH) }
|
||||
vim.list_extend(cmd, { 'sh', 'runtime/scripts/remote_ssh.sh', sock, ... })
|
||||
local obj = vim.system(cmd)
|
||||
obj:wait(10000, function(stdout)
|
||||
return stdout:match('NVIM_READY') ~= nil
|
||||
end)
|
||||
local ch = vim.fn.sockconnect('pipe', sock, { rpc = true })
|
||||
local env =
|
||||
vim.rpcrequest(ch, 'nvim_eval', '[$NVIM_APPNAME, $NVIM_TEST_RELEASE, v:startreason]')
|
||||
vim.fn.chanclose(ch)
|
||||
obj:kill('sigterm')
|
||||
local res = obj:wait(5000)
|
||||
-- The script removes its socket dir on exit.
|
||||
local removed = vim.uv.fs_stat(vim.fs.dirname(sock)) == nil
|
||||
return { env, removed, res.stderr:match('Installing %S+') }
|
||||
end, dir, ('file://%s'):format(tarball), version)
|
||||
end
|
||||
|
||||
eq({ { 'nvim-remote', '', 'remote-ssh' }, true }, serve('v0.1.0'))
|
||||
eq(nil, vim.uv.fs_stat(('%s/%s'):format(home, dir)))
|
||||
local installing = ('Installing %s/%s...'):format(home, dir)
|
||||
eq({ { 'nvim-remote', '1', 'remote-ssh' }, true, installing }, serve('v999.0.0'))
|
||||
-- No version (a prerelease): never uses $PATH; the install is kept.
|
||||
eq({ { 'nvim-remote', '1', 'remote-ssh' }, true }, serve())
|
||||
|
||||
-- The UI could not reach a restarted server through the tunnel.
|
||||
n.clear({ env = { __NVIM_STARTREASON = 'remote-ssh' } })
|
||||
eq(
|
||||
'Vim(restart):E6103: :restart is not supported with remote-ssh',
|
||||
t.pcall_err(n.command, 'restart')
|
||||
)
|
||||
it('remote script runs acceptable `nvim` from $PATH, else installs the release', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
local home = vim.fs.abspath(t.tmpname(false))
|
||||
t.finally(function()
|
||||
n.rmdir(home)
|
||||
end)
|
||||
local nvim_prog = vim.fs.abspath(n.nvim_prog)
|
||||
n.mkdir_p(home .. '/bin')
|
||||
vim.uv.fs_symlink(nvim_prog, home .. '/bin/nvim')
|
||||
-- Release archive: its `nvim` marks the server it runs.
|
||||
local release = home .. '/src/nvim-test'
|
||||
n.mkdir_p(release .. '/bin')
|
||||
t.write_file(
|
||||
release .. '/bin/nvim',
|
||||
('#!/bin/sh\nexport NVIM_TEST_RELEASE=1\nexec %s "$@"\n'):format(nvim_prog)
|
||||
)
|
||||
vim.uv.fs_chmod(release .. '/bin/nvim', 493)
|
||||
local tarball = home .. '/nvim-test.tar.gz'
|
||||
eq(0, vim.system({ 'tar', '-czf', tarball, '-C', home .. '/src', 'nvim-test' }):wait().code)
|
||||
local dir = 'releases/v1-test'
|
||||
|
||||
it('fails when ssh exits before the remote Nvim is ready', function()
|
||||
setup_fake_ssh({
|
||||
tunnel = { stderr = 'channel 0: open failed: administratively prohibited\n', exit = 255 },
|
||||
})
|
||||
--- Runs the script locally, as the remote does. Returns `[$NVIM_APPNAME, $NVIM_TEST_RELEASE,
|
||||
--- v:startreason]` of its server, whether its socket dir was removed on exit, and its
|
||||
--- `Installing …` message, if any.
|
||||
local function serve(version, update)
|
||||
return n.exec_lua(function(...)
|
||||
local sock = ('%s/sock/nvim.sock'):format(home)
|
||||
local cmd =
|
||||
{ 'env', ('HOME=%s'):format(home), ('PATH=%s/bin:%s'):format(home, vim.env.PATH) }
|
||||
vim.list_extend(cmd, { 'sh', 'runtime/scripts/remote_ssh.sh' })
|
||||
vim.list_extend(cmd, update and { '--update', sock, ... } or { sock, ... })
|
||||
local obj = vim.system(cmd, { stdin = true }) -- Use stdin to signal disconnect.
|
||||
obj:wait(10000, function(stdout)
|
||||
return stdout:match('NVIM_READY') ~= nil
|
||||
end)
|
||||
local ch = vim.fn.sockconnect('pipe', sock, { rpc = true })
|
||||
local env =
|
||||
vim.rpcrequest(ch, 'nvim_eval', '[$NVIM_APPNAME, $NVIM_TEST_RELEASE, v:startreason]')
|
||||
vim.fn.chanclose(ch)
|
||||
obj:write(nil) -- ssh disconnects (stdin closed).
|
||||
local res = obj:wait(5000)
|
||||
-- `vim.net._ssh.serve()` removes its socket dir on exit.
|
||||
local removed = vim.uv.fs_stat(vim.fs.dirname(sock)) == nil
|
||||
return { env, removed, res.stderr:match('Installing %S+') }
|
||||
end, dir, ('file://%s'):format(tarball), version)
|
||||
end
|
||||
|
||||
eq({ { 'nvim-remote', '', 'remote-ssh' }, true }, serve('v0.1.0'))
|
||||
eq(nil, vim.uv.fs_stat(('%s/%s'):format(home, dir)))
|
||||
local installing = ('Installing %s/%s...'):format(home, dir)
|
||||
eq({ { 'nvim-remote', '1', 'remote-ssh' }, true, installing }, serve('v999.0.0'))
|
||||
-- No version (a prerelease): never uses $PATH; reuses the install.
|
||||
eq({ { 'nvim-remote', '1', 'remote-ssh' }, true }, serve())
|
||||
-- --update replaces the install, and leaves no temp dirs.
|
||||
t.write_file(('%s/%s/stale'):format(home, dir), '')
|
||||
eq({ { 'nvim-remote', '1', 'remote-ssh' }, true, installing }, serve(nil, true))
|
||||
eq({ 'bin' }, n.fn.readdir(('%s/%s'):format(home, dir)))
|
||||
eq({ 'v1-test' }, n.fn.readdir(home .. '/releases'))
|
||||
|
||||
-- Follows a restarted server, which re-binds the socket. The socket dir is private.
|
||||
eq(
|
||||
{ tonumber('700', 8), true, true },
|
||||
n.exec_lua(function()
|
||||
vim.api.nvim_create_autocmd('Progress', {
|
||||
pattern = 'remote-ssh',
|
||||
callback = function(ev)
|
||||
_G.last_progress = ('%s: %s'):format(ev.data.status, table.concat(ev.data.text))
|
||||
end,
|
||||
local sock = ('%s/sock/nvim.sock'):format(home)
|
||||
local cmd =
|
||||
{ 'env', ('HOME=%s'):format(home), ('PATH=%s/bin:%s'):format(home, vim.env.PATH) }
|
||||
vim.list_extend(cmd, { 'sh', 'runtime/scripts/remote_ssh.sh', sock, dir, '', 'v0.1.0' })
|
||||
local obj = vim.system(cmd, { stdin = true })
|
||||
obj:wait(10000, function(stdout)
|
||||
return stdout:match('NVIM_READY') ~= nil
|
||||
end)
|
||||
local mode = bit.band(assert(vim.uv.fs_stat(vim.fs.dirname(sock))).mode, tonumber('777', 8))
|
||||
-- As `:restart`: the old server stops listening, a new one listens, the old one exits.
|
||||
local old = vim.fn.sockconnect('pipe', sock, { rpc = true })
|
||||
vim.rpcrequest(old, 'nvim_call_function', 'serverstop', { sock })
|
||||
local count_chans = 'let g:chans = 0 | autocmd ChanOpen * let g:chans += 1'
|
||||
local new = vim.system({
|
||||
vim.v.progpath,
|
||||
'--clean',
|
||||
'--headless',
|
||||
'--cmd',
|
||||
count_chans,
|
||||
'--listen',
|
||||
sock,
|
||||
})
|
||||
vim.wait(5000, function()
|
||||
return vim.uv.fs_stat(sock) ~= nil
|
||||
end)
|
||||
local ch = vim.fn.sockconnect('pipe', sock, { rpc = true })
|
||||
vim.rpcnotify(old, 'nvim_command', 'qall!')
|
||||
-- `vim.net._ssh.serve()` connects to the new server (to get its PID).
|
||||
assert(vim.wait(5000, function()
|
||||
return vim.rpcrequest(ch, 'nvim_eval', 'g:chans') > 1
|
||||
end))
|
||||
obj:write(nil) -- ssh disconnects: stops the new server.
|
||||
return { mode, new:wait(5000).code ~= 124, obj:wait(5000).code == 0 }
|
||||
end)
|
||||
t.matches(
|
||||
'SSH tunnel exited before the remote Nvim was ready: channel 0: open failed',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server')]])
|
||||
)
|
||||
eq(
|
||||
'failed: Failed to connect to ssh://user@test-server',
|
||||
n.exec_lua('return _G.last_progress')
|
||||
)
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
end)
|
||||
)
|
||||
|
||||
it('fails when the remote Nvim crashes during startup, or is unreachable', function()
|
||||
setup_fake_ssh({
|
||||
tunnel = { stdout = 'NVIM_CRASHED\n', exit = 1 },
|
||||
})
|
||||
t.matches(
|
||||
'Remote Nvim crashed during startup',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server')]])
|
||||
)
|
||||
eq(
|
||||
'No host in SSH URI: ssh:///x',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh:///x')]])
|
||||
)
|
||||
|
||||
-- The script runs, but sshd refused the forward.
|
||||
setup_fake_ssh({
|
||||
tunnel = { stdout = 'NVIM_READY\n' },
|
||||
})
|
||||
t.matches(
|
||||
'Remote Nvim is not reachable through the SSH forward',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server')]])
|
||||
)
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
end)
|
||||
|
||||
it('serves each session from a fresh private remote directory', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
setup_fake_ssh()
|
||||
local function remote_dir()
|
||||
n.exec_lua([[
|
||||
local _, teardown = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
teardown()
|
||||
]])
|
||||
local args = t.read_file(fake_dir .. '/tunnel.args')
|
||||
return args:match(':(/tmp/nvim%-remote%.%x+)/nvim%.sock ')
|
||||
end
|
||||
|
||||
local dir1, dir2 = remote_dir(), remote_dir()
|
||||
assert(dir1 and dir2, 'remote socket path not found')
|
||||
assert(dir1 ~= dir2, 'remote socket directory reused across sessions')
|
||||
end)
|
||||
|
||||
it(':connect ssh:// does nothing if it cannot attach, or is declined', function()
|
||||
setup_fake_ssh({ confirm = 2 })
|
||||
eq('Vim(connect):E477: No ! allowed', t.pcall_err(n.command, 'connect! ssh://test-server'))
|
||||
eq('Vim(connect):E5768: No UI attached', t.pcall_err(n.command, 'connect ssh://test-server'))
|
||||
-- Typed by a client that is not a UI.
|
||||
n.command('let v:errmsg = ""')
|
||||
local client = n.connect(n.eval('v:servername'))
|
||||
client:request('nvim_input', ':connect ssh://test-server<CR>')
|
||||
t.retry(nil, nil, function()
|
||||
eq('E5768: No UI attached', n.eval('v:errmsg'))
|
||||
-- Stops the server and removes the socket dir also on a signal.
|
||||
eq(
|
||||
{ true, true },
|
||||
n.exec_lua(function()
|
||||
local sock = ('%s/sock/nvim.sock'):format(home)
|
||||
local cmd =
|
||||
{ 'env', ('HOME=%s'):format(home), ('PATH=%s/bin:%s'):format(home, vim.env.PATH) }
|
||||
vim.list_extend(cmd, { 'sh', 'runtime/scripts/remote_ssh.sh', sock, dir, '', 'v0.1.0' })
|
||||
local obj = vim.system(cmd, { stdin = true })
|
||||
obj:wait(10000, function(stdout)
|
||||
return stdout:match('NVIM_READY') ~= nil
|
||||
end)
|
||||
local ch = vim.fn.sockconnect('pipe', sock, { rpc = true })
|
||||
local server = vim.rpcrequest(ch, 'nvim_call_function', 'getpid', {})
|
||||
vim.uv.kill(obj.pid, 'sigterm') -- The script `exec`s the supervisor.
|
||||
obj:wait(5000)
|
||||
return {
|
||||
vim.uv.fs_stat(vim.fs.dirname(sock)) == nil,
|
||||
(vim.wait(5000, function()
|
||||
return vim.uv.kill(server, 0) ~= 0
|
||||
end)),
|
||||
}
|
||||
end)
|
||||
client:close()
|
||||
eq({}, n.exec_lua('return _G.confirm_log'))
|
||||
)
|
||||
|
||||
Screen.new()
|
||||
-- Scheme is case-insensitive.
|
||||
n.command('connect SSH://test-server')
|
||||
eq(1, #n.exec_lua('return _G.confirm_log'))
|
||||
eq(1, #n.api.nvim_list_uis())
|
||||
eq(nil, vim.uv.fs_stat(fake_dir .. '/tunnel.args'))
|
||||
-- The UI could not reach a restarted server through the tunnel.
|
||||
n.clear({ env = { __NVIM_STARTREASON = 'remote-ssh' } })
|
||||
eq(
|
||||
'Vim(restart):E6103: :restart is not supported with remote-ssh',
|
||||
t.pcall_err(n.command, 'restart')
|
||||
)
|
||||
end)
|
||||
|
||||
it('fails when ssh exits before the remote Nvim is ready', function()
|
||||
setup_fake_ssh({
|
||||
tunnel = { stderr = 'channel 0: open failed: administratively prohibited\n', exit = 255 },
|
||||
})
|
||||
n.exec_lua(function()
|
||||
vim.api.nvim_create_autocmd('Progress', {
|
||||
pattern = 'remote-ssh',
|
||||
callback = function(ev)
|
||||
_G.last_progress = ('%s: %s'):format(ev.data.status, table.concat(ev.data.text))
|
||||
end,
|
||||
})
|
||||
end)
|
||||
t.matches(
|
||||
'SSH tunnel exited before the remote Nvim was ready: channel 0: open failed',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server')]])
|
||||
)
|
||||
eq('failed: Failed to connect to ssh://user@test-server', n.exec_lua('return _G.last_progress'))
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
end)
|
||||
|
||||
it('fails when the remote Nvim crashes during startup, or is unreachable', function()
|
||||
setup_fake_ssh({
|
||||
tunnel = { stdout = 'NVIM_CRASHED\n', exit = 1 },
|
||||
})
|
||||
t.matches(
|
||||
'Remote Nvim crashed during startup',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server')]])
|
||||
)
|
||||
|
||||
-- The script runs, but sshd refused the forward.
|
||||
setup_fake_ssh({
|
||||
tunnel = { stdout = 'NVIM_READY\n' },
|
||||
})
|
||||
t.matches(
|
||||
'Remote Nvim is not reachable through the SSH forward',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server')]])
|
||||
)
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
end)
|
||||
|
||||
it('serves each session from a new remote directory', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
setup_fake_ssh()
|
||||
local function remote_dir()
|
||||
n.exec_lua([[
|
||||
local _, teardown = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
teardown()
|
||||
]])
|
||||
local args = t.read_file(fake_dir .. '/tunnel.args')
|
||||
return args:match(':(/tmp/nvim%-remote%.%x+)/nvim%.sock ')
|
||||
end
|
||||
|
||||
local dir1, dir2 = remote_dir(), remote_dir()
|
||||
assert(dir1 and dir2, 'remote socket path not found')
|
||||
assert(dir1 ~= dir2, 'remote socket directory reused across sessions')
|
||||
end)
|
||||
|
||||
it(':connect ssh:// does nothing without a UI, or if the user declines', function()
|
||||
setup_fake_ssh({ confirm = 0 })
|
||||
eq('Vim(connect):E477: No ! allowed', t.pcall_err(n.command, 'connect! ssh://test-server'))
|
||||
eq('Vim(connect):E5768: No UI attached', t.pcall_err(n.command, 'connect ssh://test-server'))
|
||||
-- Typed by a client that is not a UI.
|
||||
n.command('let v:errmsg = ""')
|
||||
local client = n.connect(n.eval('v:servername'))
|
||||
client:request('nvim_input', ':connect ssh://test-server<CR>')
|
||||
t.retry(nil, nil, function()
|
||||
eq('E5768: No UI attached', n.eval('v:errmsg'))
|
||||
end)
|
||||
client:close()
|
||||
eq({}, n.exec_lua('return _G.confirm_log'))
|
||||
|
||||
Screen.new()
|
||||
-- Scheme is case-insensitive.
|
||||
n.command('connect SSH://test-server')
|
||||
eq(1, #n.exec_lua('return _G.confirm_log'))
|
||||
eq(1, #n.api.nvim_list_uis())
|
||||
eq(nil, vim.uv.fs_stat(fake_dir .. '/tunnel.args'))
|
||||
end)
|
||||
end)
|
||||
Reference in new issue
Block a user