repo_description: The user-friendly command line shell.
repo_topics: fish rust shell terminal
code: use crate::{
      fds::wopen_cloexec,
      flog, flogf,
      path::{DirRemoteness, path_remoteness},
      prelude::*,
      wutil::{FileId, INVALID_FILE_ID, file_id_for_file, file_id_for_path, wdirname, wunlink},
  };
  use fish_tempfile::random_filename;
  use fish_widestring::{osstr2wcstring, wcs2bytes, wcs2osstring};
  use libc::{LOCK_EX, LOCK_SH, c_int};
  use nix::{fcntl::OFlag, sys::stat::Mode};
  use std::{
      ffi::OsString,
      fs::{File, OpenOptions},
      os::{
          fd::AsRawFd as _,
          unix::{ffi::OsStringExt as _, fs::MetadataExt as _},
      },
      path::PathBuf,
  };
  
  /// Creates a temporary file in the same directory as as `original_path`, meaning `original_path`
  /// must be a valid file path. The filename will be created by appending random alphanumeric ASCII
  /// chars to the `original_filename`.
  fn create_temporary_file(original_path: &wstr) -> std::io::Result<(File, WString)> {
      let original_path = PathBuf::from(OsString::from_vec(wcs2bytes(original_path)));
      // original path must be a valid file path, so file_name should never return None.
      let prefix = original_path.file_name().unwrap().to_owned();
      let dir = original_path.parent().unwrap();
      let (path, result) =
          fish_tempfile::create_file_with_retry(|| dir.join(random_filename(prefix.clone())));
      match result {
          Ok(file) => Ok((file, osstr2wcstring(path))),
          Err(e) => {
              flog!(
                  error,
                  wgettext_fmt!(
                      "Unable to create temporary file '%s': %s",
                      // TODO(MSRV>=1.87): use OsString::display()
                      format!("{:?}", path),
                      e
                  )
              );
              Err(e)
          }
      }
  }
  
  /// Use this struct for all accesses to file which need mutual exclusion.
  /// Otherwise, races on the file are possible.
  /// The lock is released when this struct is dropped.
  pub struct LockedFile {
      /// This is the file which requires mutual exclusion.
      /// It should only be accessed through this struct,
      /// because the locks used here do not protect from other accesses to the file.
      data_file: File,
      /// The file descriptor of the parent directory, used for locking.
      /// The lock is not placed on the data file directly due to issues with renaming.
      /// If the data file is renamed after opening and before locking it,
      /// There are two independent files around, whose locks do not interact.
      /// In some cases this can be identified by checking file identifiers and timestamps,
      /// but even with such checks races and corresponding file corruption can occur.
      /// It is simpler to lock a different path, which does not change.
      ///
      /// We may fail to lock (e.g. on lockless NFS - see issue #685.
      /// In that case, we proceed as if locking succeeded.
      /// This might result in corruption,
      /// but the alternative of not being able to access the file at all is not desirable either.
      _locked_fd: File,
  }
  
  pub const LOCKED_FILE_MODE: Mode = Mode::from_bits_truncate(0o600);
  
  pub enum LockingMode {
      Shared,
      Exclusive(WriteMethod),
  }
  pub enum WriteMethod {
      Append,
      RenameIntoPlace,
  }
  
  impl LockingMode {
      pub fn flock_op(&self) -> c_int {
          match self {
              Self::Shared => LOCK_SH,
              Self::Exclusive(_) => LOCK_EX,
          }
      }
  
      pub fn file_flags(&self) -> OFlag {
          match self {
              Self::Shared => OFlag::O_RDONLY,
              Self::Exclusive(WriteMethod::Append) => {
                  OFlag::O_WRONLY | OFlag::O_APPEND | OFlag::O_CREAT
              }
              Self::Exclusive(WriteMethod::RenameIntoPlace) => OFlag::O_RDWR | OFlag::O_CREAT,
          }
      }
  }
  
  impl LockedFile {
      /// Creates a [`LockedFile`].
      /// Use this for any access to a which requires mutual exclusion, as it ensures correct locking.
      /// Use [`LockingMode::Exclusive`] if you want to modify the file in any way.
      /// Otherwise you should use [`LockingMode::Shared`].
      /// Two modes of modification are supported:
      /// - Appending
      /// - Writing to a temporary file which is then renamed into place.
      ///   File flags are derived from the [`LockingMode`].
      ///   `file_name` should just be a name, not a full path.
      pub fn new(locking_mode: LockingMode, file_path: &wstr) -> std::io::Result<Self> {
          let dir_path = wdirname(file_path);
  
          if path_remoteness(dir_path) == DirRemoteness::Remote {
              return Err(std::io::Error::new(
                  std::io::ErrorKind::Unsupported,
                  "Directory considered remote. Locking is disabled on remote file systems.",
              ));
          }
  
          // We start by locking the directory.
          // This is required to avoid racing modifications by other threads/processes.
          let dir_fd = wopen_cloexec(dir_path, OFlag::O_RDONLY, Mode::empty())?;
  
          {
              // Cygwin's `flock` is currently not thread safe (#11933)
              #[cfg(cygwin)]
              static FLOCK_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
              #[cfg(cygwin)]
              let _lock = FLOCK_LOCK.lock().unwrap();
  
              // Try locking the directory. Retry if locking was interrupted.
              while unsafe { libc::flock(dir_fd.as_raw_fd(), locking_mode.flock_op()) } == -1 {
                  let err = std::io::Error::last_os_error();
                  if err.kind() != std::io::ErrorKind::Interrupted {
                      return Err(err);
                  }
              }
          }
  
          // Open the data file
          let data_file = wopen_cloexec(file_path, locking_mode.file_flags(), LOCKED_FILE_MODE)?;
  
          Ok(Self {
              data_file,
              _locked_fd: dir_fd,
          })
      }
  
      pub fn get(&self) -> &File {
          &self.data_file
      }
  
      pub fn get_mut(&mut self) -> &mut File {
          &mut self.data_file
      }
  
      /// Calling `fsync` and then closing the file is required for correctness on some file systems
      /// before renaming the file.
      /// <https://www.comp.nus.edu.sg/~lijl/papers/ferrite-asplos16.pdf>
      /// <https://archive.kernel.org/oldwiki/btrfs.wiki.kernel.org/index.php/FAQ.html#What_are_the_crash_guarantees_of_overwrite-by-rename.3F>
      /// Returns the lock file so that the lock can be kept.
      pub fn fsync_close_and_keep_lock(self) -> std::io::Result<File> {
          fsync(&self.data_file)?;
          Ok(self._locked_fd)
      }
  }
  
  /// Reimplementation of `std::sys::fs::unix::File::fsync` using publicly accessible functionality.
  /// This function is used instead of `sync_all` due to concerns of that being too slow on macOS,
  /// since there `libc::fcntl(fd, libc::F_FULLFSYNC)` is used internally.
  /// This weakens our guarantees on macOS.
  pub fn fsync(file: &File) -> std::io::Result<()> {
      let fd = file.as_raw_fd();
      loop {
          match unsafe { libc::fsync(fd) } {
              0 => return Ok(()),
              -1 => {
                  let os_error = std::io::Error::last_os_error();
                  if os_error.kind() != std::io::ErrorKind::Interrupted {
                      flogf!(synced_file_access, "fsync failed: %s", os_error);
                      return Err(os_error);
                  }
              }
              _ => panic!("fsync should only ever return 0 or -1"),
          }
      }
  }
  
  /// Runs the `load` function, which should see a consistent state of the file at `path`.
  /// To ensure a consistent state, we use locks to prevent modifications by others.
  /// If locking is unavailable, the `load` function might be executed multiple times,
  /// until it manages a run without the file being modified in the meantime, or until the maximum
  /// number of allowed attempts is reached.
  /// If the file does not exist this function will return an error.
  pub fn lock_and_load<F, UserData>(path: &wstr, load: F) -> std::io::Result<(FileId, UserData)>
  where
      F: Fn(&File, FileId) -> std::io::Result<UserData>,
  {
      match LockedFile::new(LockingMode::Shared, path) {
          Ok(locked_file) => {
              let file_id = file_id_for_file(locked_file.get());
              let user_data = load(locked_file.get(), file_id.clone())?;
              return Ok((file_id, user_data));
          }
          Err(e) => {
              flogf!(
                  synced_file_access,
                  "Error acquiring shared lock on the directory of '%s': %s",
                  path,
                  e,
              );
              // This function might be called when the file does not exist.
              if e.kind() == std::io::ErrorKind::NotFound {
                  // There is no point in continuing in this function if the file does not
                  // exist.
                  return Err(e);
              }
          }
      }
  
      flog!(
          synced_file_access,
          "flock-based locking is disabled. Using fallback implementation."
      );
  
      // Fallback implementation for situations where locking is unavailable.
      let max_attempts = 1000;
      for _ in 0..max_attempts {
          // If we cannot open the file, there is nothing we can do,
          // so just return immediately.
          let file = wopen_cloexec(path, OFlag::O_RDONLY, Mode::empty())?;
          let initial_file_id = file_id_for_file(&file);
          let loaded_data = match load(&file, initial_file_id.clone()) {
              Ok(update_data) => update_data,
              Err(_) => {
                  // Retry if load function failed. Because we do not hold a lock, this might be
                  // caused by concurrent modifications.
                  continue;
              }
          };
  
          let final_file_id = file_id_for_path(path);
          if initial_file_id != final_file_id {
              continue;
          }
          // If the file id did not change, we assume that we loaded a consistent state.
          return Ok((final_file_id, loaded_data));
      }
      Err(std::io::Error::other(
          "Failed to update the file. Locking is disabled, and the fallback code did not succeed within the permissible number of attempts.",
      ))
  }
  
  pub struct PotentialUpdate<UserData> {
      pub do_save: bool,
      pub data: UserData,
  }
  
  /// Use this function for updating a file based on its current content,
  /// for files which might be accessed at the same time by other threads/processes.
  /// The basic principle is to create a temporary file, take a lock on the file to be rewritten,
  /// do the rewrite (which might involve reading the old file contents),
  /// and finally release the lock.
  /// Error handling (especially the case where locking is not possible) makes the implementation
  /// non-straightforward.
  ///
  /// # Arguments
  ///
  /// - `path`: The path to the file which should be updated.
  /// - `rewrite`: The function which handles reading from the file and writing to a temporary file.
  ///   The first argument is for the file to read from, the second for the temporary file to
  ///   write to.  On success, the value returned by `rewrite` is included in this functions
  ///   return value. Be careful about side effects of `rewrite`. It might get executed multiple
  ///   times. Try to avoid side effects and instead extract any data you might need and return
  ///   them on success.  Then, apply the desired side effects once this function has returned
  ///   successfully.
  ///
  /// # Return value
  ///
  /// On success, the [`FileId`] of the rewritten file is returned, alongside the value returned by
  /// `rewrite`. Note that if locking is unavailable, the [`FileId`] might be the id of a different
  /// version of the file, which was written after this function renamed the temporary file to `path`
  /// but before we obtained the [`FileId`] from `path`. This is a race condition we do not detect.
  pub fn rewrite_via_temporary_file<F, UserData>(
      path: &wstr,
      rewrite: F,
  ) -> std::io::Result<(FileId, PotentialUpdate<UserData>)>
  where
      F: Fn(&File, &mut File) -> std::io::Result<PotentialUpdate<UserData>>,
  {
      /// Updates the metadata of the `new_file` to match the `old_file`.
      /// Also updates the mtime of the `new_file` to the current time manually, to work around
      /// operating systems which do not update the internal time used for such time stamps
      /// frequently enough. This is important for [`FileId`] comparisons.
      fn update_metadata(old_file: &File, new_file: &File) {
          // Ensure we maintain the ownership and permissions of the original (#2355). If the
          // stat fails, we assume (hope) our default permissions are correct. This
          // corresponds to e.g. someone running sudo -E as the very first command. If they
          // did, it would be tricky to set the permissions correctly. (bash doesn't get this
          // case right either).
          if let Ok(md) = old_file.metadata() {
              if let Err(e) = std::os::unix::fs::fchown(new_file, Some(md.uid()), Some(md.gid())) {
                  flog!(
                      synced_file_access,
                      "Error when changing ownership of file:",
                      e
                  );
              }
              if let Err(e) = new_file.set_permissions(md.permissions()) {
                  flog!(synced_file_access, "Error when changing mode of file:", e);
              }
          } else {
              flog!(synced_file_access, "Could not get metadata for file");
          }
          // Linux by default stores the mtime with low precision, low enough that updates that occur
          // in quick succession may result in the same mtime (even the nanoseconds field). So
          // manually set the mtime of the new file to a high-precision clock. Note that this is only
          // necessary because Linux aggressively reuses inodes, causing the ABA problem; on other
          // platforms we tend to notice the file has changed due to a different inode.
          //
          // The current time within the Linux kernel is cached, and generally only updated on a timer
          // interrupt. So if the timer interrupt is running at 10 milliseconds, the cached time will
          // only be updated once every 10 milliseconds.
          #[cfg(any(target_os = "linux", target_os = "android"))]
          {
              let mut times: [libc::timespec; 2] = unsafe { std::mem::zeroed() };
              times[0].tv_nsec = libc::UTIME_OMIT; // don't change atime
              if unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut times[1]) } == 0 {
                  unsafe {
                      // This accesses both times[0] and times[1]. Check `utimensat(2)` for details.
                      libc::futimens(new_file.as_raw_fd(), &times[0]);
                  }
              }
          }
      }
  
      /// Renames a file from `old_name` to `new_name`.
      fn rename(old_name: &wstr, new_name: &wstr) -> std::io::Result<()> {
          if let Err(e) = std::fs::rename(wcs2osstring(old_name), wcs2osstring(new_name)) {
              flog!(
                  error,
                  wgettext_fmt!("Error when renaming file: %s", e.to_string())
              );
              return Err(e);
          }
          Ok(())
      }
  
      // This contains the main body of the surrounding function.
      // It is wrapped as its own function to allow unlinking the tmpfile reliably.
      // This can be achieved more concisely using a `ScopeGuard` which unlinks the file when it is
      // closed, but this does not work if the file should be closed before renaming, which is
      // necessary for the whole process to work reliably on some filesystems.
      fn try_rewriting<F, UserData>(
          path: &wstr,
          rewrite: F,
          tmp_name: &wstr,
          mut tmp_file: File,
      ) -> std::io::Result<(FileId, PotentialUpdate<UserData>)>
      where
          F: Fn(&File, &mut File) -> std::io::Result<PotentialUpdate<UserData>>,
      {
          // We want to rewrite the file.
          // To avoid issues with crashes during writing,
          // we write to a temporary file and once we are done, this file is renamed such that it
          // replaces the original file.
          // To avoid races, we need to have exclusive access to the file for the entire
          // duration, which we get via an exclusive lock on the parent directory.
          // Taking a shared lock first and later upgrading to an exclusive one could result in a
          // deadlock, so we take an exclusive one immediately.
          match LockedFile::new(LockingMode::Exclusive(WriteMethod::RenameIntoPlace), path) {
              Ok(locked_file) => {
                  let potential_update = rewrite(locked_file.get(), &mut tmp_file)?;
                  // In case the `LockedFile` is destroyed, this variable keeps the lock file in
                  // scope, to prevent releasing the lock too early.
                  let mut _lock_file = None;
                  if potential_update.do_save {
                      update_metadata(locked_file.get(), &tmp_file);
                      _lock_file = Some(locked_file.fsync_close_and_keep_lock()?);
                      rename(tmp_name, path)?;
                  }
                  return Ok((file_id_for_path(path), potential_update));
              }
              Err(e) => {
                  flogf!(
                      synced_file_access,
                      "Error acquiring exclusive lock on the directory of '%s': %s",
                      path,
                      e,
                  );
              }
          }
  
          // If this is reached, we assume that locking is not available so we use a fallback
          // implementation which tries to avoid race conditions, but in the case of contention it is
          // possible that some writes are lost.
  
          flog!(
              synced_file_access,
              "flock-based locking is disabled. Using fallback implementation."
          );
  
          // Give up after this many unsuccessful attempts.
          // TODO: which value, should this be a constant shared with other retrying logic?
          let max_attempts = 1000;
          for _ in 0..max_attempts {
              // Reopen the tmpfile. This is important because it might have been closed by
              // explicitly dropping it.
              tmp_file = OpenOptions::new()
                  .write(true)
                  .truncate(true)
                  .open(wcs2osstring(tmp_name))?;
  
              // If the file does not exist yet, this will be `INVALID_FILE_ID`.
              let initial_file_id = file_id_for_path(path);
              // If we cannot open the file, there is nothing we can do,
              // so just return immediately.
              let old_file = wopen_cloexec(path, OFlag::O_RDONLY | OFlag::O_CREAT, LOCKED_FILE_MODE)?;
              let opened_file_id = file_id_for_file(&old_file);
              if initial_file_id != INVALID_FILE_ID && initial_file_id != opened_file_id {
                  // File ID changed (and not just because the file was created by us).
                  continue;
              }
              let Ok(potential_update) = rewrite(&old_file, &mut tmp_file) else {
                  // Retry if rewrite function failed. Because we do not hold a lock, this might be
                  // caused by concurrent modifications.
                  continue;
              };
  
              if potential_update.do_save {
                  update_metadata(&old_file, &tmp_file);
                  // fsync + close as described in
                  // https://archive.kernel.org/oldwiki/btrfs.wiki.kernel.org/index.php/FAQ.html#What_are_the_crash_guarantees_of_overwrite-by-rename.3F
                  fsync(&tmp_file)?;
                  std::mem::drop(tmp_file);
              }
  
              let mut final_file_id = file_id_for_path(path);
              if opened_file_id != final_file_id {
                  continue;
              }
  
              // If we reach this point, the file ID did not change while we read the old file and wrote
              // to `tmp_file`. Now we replace the old file with the `tmp_file`.
              // Note that we cannot prevent races here.
              // If the file is modified by someone else between the syscall for determining the [`FileId`]
              // and the rename syscall, these modifications will be lost.
  
              if potential_update.do_save {
                  // Do not retry on rename failures, as it is unlikely that these will disappear if we retry.
                  rename(tmp_name, path)?;
                  final_file_id = file_id_for_path(path);
              }
              // Note that this might not match the version of the file we just wrote.
              // (If we did write.)
              return Ok((final_file_id, potential_update));
          }
          Err(std::io::Error::other(
              "Failed to update the file. Locking is disabled, and the fallback code did not succeed within the permissible number of attempts.",
          ))
      }
  
      let (tmp_file, tmp_name) = create_temporary_file(path)?;
      let result = try_rewriting(path, rewrite, &tmp_name, tmp_file);
      // Do not leave the tmpfile around.
      // Note that we do not unlink when renaming succeeded.
      // In that case, it would be unnecessary, because the file will no longer exist at the path,
      // but it also enables a race condition, where after renaming succeeded, a different fish
      // instance creates a tmpfile with the same name (unlikely but possible), which we would then
      // delete here.
      if result.is_err()
          || result
              .as_ref()
              .is_ok_and(|(_file_id, potential_update)| !potential_update.do_save)
      {
          let _ = wunlink(&tmp_name);
      }
      result
  }
  