mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 03:18:16 +00:00
feat: try to recover from missing tempdir #22573
Problem:
If vim_tempdir mysteriously goes missing (typically by "antivirus" on
Windows), any plugins using tempname() will be broken for the rest of
the session. #1432 #9833 https://groups.google.com/g/vim_use/c/ef55jNm5czI
Steps:
mkdir foo
TMPDIR=./foo nvim
:echo tempname()
!rm -r foo
:echo tempname()
tempname() still uses the foo path even though it was deleted.
Solution:
- Don't assume that vim_tempdir exists.
- If it goes missing once, retry vim_mktempdir and log (silently) an error.
- If it goes missing again, retry vim_mktempdir and show an error.
Rejected in Vim for performance reasons:
https://groups.google.com/g/vim_use/c/qgRob9SWDv8/m/FAOFVVcDTv0J
https://groups.google.com/g/vim_dev/c/cogp-Vye4oo/m/d_SVFXBbnnoJ
But, logging shows that `vim_gettempdir` is not called frequently.
Fixes #1432
Fixes #9833
Fixes #11250
Related: stdpath("run") f50135a32e
This commit is contained in:
@@ -8688,13 +8688,11 @@ taglist({expr} [, {filename}]) *taglist()*
|
||||
GetTagpattern()->taglist()
|
||||
|
||||
tempname() *tempname()* *temp-file-name*
|
||||
The result is a String, which is the name of a file that
|
||||
doesn't exist. It can be used for a temporary file. Example: >
|
||||
Generates a (non-existent) filename located in the Nvim root
|
||||
|tempdir|. Scripts can use the filename as a temporary file.
|
||||
Example: >
|
||||
:let tmpfile = tempname()
|
||||
:exe "redir > " .. tmpfile
|
||||
< For Unix, the file will be in a private directory |tempfile|.
|
||||
For MS-Windows forward slashes are used when the 'shellslash'
|
||||
option is set or when 'shellcmdflag' starts with '-'.
|
||||
|
||||
termopen({cmd} [, {opts}]) *termopen()*
|
||||
Spawns {cmd} in a new pseudo-terminal session connected
|
||||
|
@@ -576,18 +576,29 @@ with ".". Vim does not recognize a comment (starting with '"') after the
|
||||
{Visual}= Filter the highlighted lines like with ={motion}.
|
||||
|
||||
|
||||
*tempfile* *setuid*
|
||||
Vim uses temporary files for filtering, generating diffs and also for
|
||||
tempname(). For Unix, the file will be in a private directory (only
|
||||
accessible by the current user) to avoid security problems (e.g., a symlink
|
||||
attack or other people reading your file). When Vim exits the directory and
|
||||
all files in it are deleted. When Vim has the setuid bit set this may cause
|
||||
problems, the temp file is owned by the setuid user but the filter command
|
||||
probably runs as the original user.
|
||||
Directory for temporary files is created in the first possible directory of:
|
||||
*tempdir* *tempfile* *setuid*
|
||||
Nvim uses temporary files for filtering and generating diffs. Plugins also
|
||||
commonly use |tempname()| for their own purposes. On the first request for
|
||||
a temporary file, Nvim creates a common directory (the "Nvim tempdir"), to
|
||||
serve as storage for all temporary files (including `stdpath("run")` files
|
||||
|$XDG_RUNTIME_DIR|) in the current session.
|
||||
|
||||
The Nvim tempdir is created in the first available system tempdir:
|
||||
Unix: $TMPDIR, /tmp, current-dir, $HOME.
|
||||
Windows: $TMPDIR, $TMP, $TEMP, $USERPROFILE, current-dir.
|
||||
|
||||
On unix the tempdir is created with permissions 0700 (only accessible by the
|
||||
current user) to avoid security problems (e.g. symlink attacks). On exit,
|
||||
Nvim deletes the tempdir and its contents.
|
||||
*E5431*
|
||||
If you see an error or |log| message like: >
|
||||
E5431: tempdir disappeared (2 times)
|
||||
this means an external process on your system deleted the Nvim tempdir.
|
||||
Typically this is caused by "antivirus" or a misconfigured cleanup service.
|
||||
|
||||
If Nvim has the setuid bit set this may cause problems: the temp file
|
||||
is owned by the setuid user but the filter command probably runs as the
|
||||
original user.
|
||||
|
||||
|
||||
4.2 Substitute *:substitute*
|
||||
|
@@ -1397,7 +1397,7 @@ Note: Similarly to the $XDG environment variables, when
|
||||
`$XDG_CONFIG_HOME/nvim` is mentionned, it should be understood as
|
||||
`$XDG_CONFIG_HOME/$NVIM_APPNAME`.
|
||||
|
||||
LOG FILE *$NVIM_LOG_FILE* *E5430*
|
||||
LOG FILE *log* *$NVIM_LOG_FILE* *E5430*
|
||||
Besides 'debug' and 'verbose', Nvim keeps a general log file for internal
|
||||
debugging, plugins and RPC clients. >
|
||||
:echo $NVIM_LOG_FILE
|
||||
|
@@ -237,6 +237,7 @@ Functions:
|
||||
|stdpath()|
|
||||
|system()|, |systemlist()| can run {cmd} directly (without 'shell')
|
||||
|matchadd()| can be called before highlight group is defined
|
||||
|tempname()| tries to recover if the Nvim |tempdir| disappears.
|
||||
|writefile()| with "p" flag creates parent directories.
|
||||
|
||||
Highlight groups:
|
||||
|
@@ -5369,10 +5369,21 @@ void vim_deltempdir(void)
|
||||
/// Creates the directory on the first call.
|
||||
char *vim_gettempdir(void)
|
||||
{
|
||||
if (vim_tempdir == NULL) {
|
||||
static int notfound = 0;
|
||||
bool exists = false;
|
||||
if (vim_tempdir == NULL || !(exists = os_isdir(vim_tempdir))) {
|
||||
if (vim_tempdir != NULL && !exists) {
|
||||
notfound++;
|
||||
if (notfound == 1) {
|
||||
ELOG("tempdir disappeared (antivirus or broken cleanup job?): %s", vim_tempdir);
|
||||
}
|
||||
if (notfound > 1) {
|
||||
msg_schedule_semsg("E5431: tempdir disappeared (%d times)", notfound);
|
||||
}
|
||||
XFREE_CLEAR(vim_tempdir);
|
||||
}
|
||||
vim_mktempdir();
|
||||
}
|
||||
|
||||
return vim_tempdir;
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,7 @@ local request = helpers.request
|
||||
local retry = helpers.retry
|
||||
local rmdir = helpers.rmdir
|
||||
local matches = helpers.matches
|
||||
local meths = helpers.meths
|
||||
local mkdir = helpers.mkdir
|
||||
local sleep = helpers.sleep
|
||||
local read_file = helpers.read_file
|
||||
@@ -261,13 +262,13 @@ end)
|
||||
describe('tmpdir', function()
|
||||
local tmproot_pat = [=[.*[/\\]nvim%.[^/\\]+]=]
|
||||
local testlog = 'Xtest_tmpdir_log'
|
||||
local faketmp
|
||||
local os_tmpdir
|
||||
|
||||
before_each(function()
|
||||
-- Fake /tmp dir so that we can mess it up.
|
||||
faketmp = tmpname()
|
||||
os.remove(faketmp)
|
||||
mkdir(faketmp)
|
||||
os_tmpdir = tmpname()
|
||||
os.remove(os_tmpdir)
|
||||
mkdir(os_tmpdir)
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
@@ -275,16 +276,21 @@ describe('tmpdir', function()
|
||||
os.remove(testlog)
|
||||
end)
|
||||
|
||||
it('failure modes', function()
|
||||
clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=faketmp, } })
|
||||
assert_nolog('tempdir is not a directory', testlog)
|
||||
assert_nolog('tempdir has invalid permissions', testlog)
|
||||
|
||||
local function get_tmproot()
|
||||
-- Tempfiles typically look like: "…/nvim.<user>/xxx/0".
|
||||
-- - "…/nvim.<user>/xxx/" is the per-process tmpdir, not shared with other Nvims.
|
||||
-- - "…/nvim.<user>/" is the tmpdir root, shared by all Nvims (normally).
|
||||
local tmproot = (funcs.tempname()):match(tmproot_pat)
|
||||
ok(tmproot:len() > 4, 'tmproot like "nvim.foo"', tmproot)
|
||||
return tmproot
|
||||
end
|
||||
|
||||
it('failure modes', function()
|
||||
clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=os_tmpdir, } })
|
||||
assert_nolog('tempdir is not a directory', testlog)
|
||||
assert_nolog('tempdir has invalid permissions', testlog)
|
||||
|
||||
local tmproot = get_tmproot()
|
||||
|
||||
-- Test how Nvim handles invalid tmpdir root (by hostile users or accidents).
|
||||
--
|
||||
@@ -292,7 +298,7 @@ describe('tmpdir', function()
|
||||
expect_exit(command, ':qall!')
|
||||
rmdir(tmproot)
|
||||
write_file(tmproot, '') -- Not a directory, vim_mktempdir() should skip it.
|
||||
clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=faketmp, } })
|
||||
clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=os_tmpdir, } })
|
||||
matches(tmproot_pat, funcs.stdpath('run')) -- Tickle vim_mktempdir().
|
||||
-- Assert that broken tmpdir root was handled.
|
||||
assert_log('tempdir root not a directory', testlog, 100)
|
||||
@@ -303,18 +309,52 @@ describe('tmpdir', function()
|
||||
os.remove(tmproot)
|
||||
mkdir(tmproot)
|
||||
funcs.setfperm(tmproot, 'rwxr--r--') -- Invalid permissions, vim_mktempdir() should skip it.
|
||||
clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=faketmp, } })
|
||||
clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=os_tmpdir, } })
|
||||
matches(tmproot_pat, funcs.stdpath('run')) -- Tickle vim_mktempdir().
|
||||
-- Assert that broken tmpdir root was handled.
|
||||
assert_log('tempdir root has invalid permissions', testlog, 100)
|
||||
end)
|
||||
|
||||
it('too long', function()
|
||||
local bigname = ('%s/%s'):format(faketmp, ('x'):rep(666))
|
||||
local bigname = ('%s/%s'):format(os_tmpdir, ('x'):rep(666))
|
||||
mkdir(bigname)
|
||||
clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=bigname, } })
|
||||
matches(tmproot_pat, funcs.stdpath('run')) -- Tickle vim_mktempdir().
|
||||
local len = (funcs.tempname()):len()
|
||||
ok(len > 4 and len < 256, '4 < len < 256', tostring(len))
|
||||
end)
|
||||
|
||||
it('disappeared #1432', function()
|
||||
clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=os_tmpdir, } })
|
||||
assert_nolog('tempdir disappeared', testlog)
|
||||
|
||||
local function rm_tmpdir()
|
||||
local tmpname1 = funcs.tempname()
|
||||
local tmpdir1 = funcs.fnamemodify(tmpname1, ':h')
|
||||
eq(funcs.stdpath('run'), tmpdir1)
|
||||
|
||||
rmdir(tmpdir1)
|
||||
retry(nil, 1000, function()
|
||||
eq(0, funcs.isdirectory(tmpdir1))
|
||||
end)
|
||||
local tmpname2 = funcs.tempname()
|
||||
local tmpdir2 = funcs.fnamemodify(tmpname2, ':h')
|
||||
neq(tmpdir1, tmpdir2)
|
||||
end
|
||||
|
||||
-- Your antivirus hates you...
|
||||
rm_tmpdir()
|
||||
assert_log('tempdir disappeared', testlog, 100)
|
||||
funcs.tempname()
|
||||
funcs.tempname()
|
||||
funcs.tempname()
|
||||
eq('', meths.get_vvar('errmsg'))
|
||||
rm_tmpdir()
|
||||
funcs.tempname()
|
||||
funcs.tempname()
|
||||
funcs.tempname()
|
||||
eq('E5431: tempdir disappeared (2 times)', meths.get_vvar('errmsg'))
|
||||
rm_tmpdir()
|
||||
eq('E5431: tempdir disappeared (3 times)', meths.get_vvar('errmsg'))
|
||||
end)
|
||||
end)
|
||||
|
Reference in New Issue
Block a user