From 2f6b290084a34800d0a408a173f6a28778f797ec Mon Sep 17 00:00:00 2001 From: Aryan Sjet Date: Wed, 28 Feb 2024 13:49:28 +0800 Subject: [PATCH] linux: fix invalid cross-device link error (#8437) This PR fix the "invalid cross-device link" error occurred in linux when trying to write the settings file atomically, like when click the "Enable vim mode" checkbox at first start. ```plain [2024-02-26T22:59:25+08:00 ERROR util] .../zed/crates/settings/src/settings_file.rs:135: Failed to write settings to file "/home/$USER/.config/zed/settings.json" Caused by: 0: failed to persist temporary file: Invalid cross-device link (os error 18) 1: Invalid cross-device link (os error 18) ``` Currently the `fs::RealFs::atomic_write()` method write to a temp file created with `NamedTempFile::new()` and then call `persist()` method to write to the config file path, which actually do a `rename` syscall under the hood. As the [issue](https://github.com/Stebalien/tempfile/issues/245) said > `NamedTempFile::new()` will create a temporary file in your system's temporary file directory. You need `NamedTempFile::new_in()`. The temporary file directory in linux is in `/tmp`, which is mounted to `tmpfs` filesystem, and in most case(all case I guess) `$HOME/.config/zed` is mounted to a different filesystem. And the `rename` syscall between different filesystems will return a `EXDEV` errno, as described in the man page [rename(2)](https://man7.org/linux/man-pages/man2/renameat2.2.html): ```plain EXDEV oldpath and newpath are not on the same mounted filesystem. (Linux permits a filesystem to be mounted at multiple points, but rename() does not work across different mount points, even if the same filesystem is mounted on both.) ``` And as the issue above said, use a different temp dir with `NamedTempFile::new_in()` for linux platform might be a solution, since the `rename` syscall provides atomicity. Release Notes: - Fix `settings.json` save failed with invalid cross-device link error in linux --- crates/fs/src/fs.rs | 11 +++++++++-- crates/util/src/paths.rs | 1 + crates/zed/src/main.rs | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 6452b08127..632247f57e 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -30,7 +30,7 @@ use std::{ }; use tempfile::{NamedTempFile, TempDir}; use text::LineEnding; -use util::ResultExt; +use util::{paths, ResultExt}; #[cfg(any(test, feature = "test-support"))] use collections::{btree_map, BTreeMap}; @@ -189,7 +189,14 @@ impl Fs for RealFs { async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { smol::unblock(move || { - let mut tmp_file = NamedTempFile::new()?; + let mut tmp_file = if cfg!(target_os = "linux") { + // Use the directory of the destination as temp dir to avoid + // invalid cross-device link error, and XDG_CACHE_DIR for fallback. + // See https://github.com/zed-industries/zed/pull/8437 for more details. + NamedTempFile::new_in(path.parent().unwrap_or(&paths::TEMP_DIR)) + } else { + NamedTempFile::new() + }?; tmp_file.write_all(data.as_bytes())?; tmp_file.persist(path)?; Ok::<(), anyhow::Error>(()) diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 9a1bc73c9d..002c0998b9 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -44,6 +44,7 @@ lazy_static::lazy_static! { pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json"); + pub static ref TEMP_DIR: PathBuf = HOME.join(".cache").join("zed"); } pub trait PathExt { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c31c768858..c509a8d0c1 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -493,6 +493,8 @@ fn init_paths() { std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path"); std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path"); std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path"); + #[cfg(target_os = "linux")] + std::fs::create_dir_all(&*util::paths::TEMP_DIR).expect("could not create tmp path"); } fn init_logger() {