mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-13 21:44:16 +00:00
Return Result from wopen_cloexec
This commit is contained in:
parent
86afc7832d
commit
23301e4895
12 changed files with 298 additions and 236 deletions
|
@ -30,7 +30,7 @@ errno = "0.2.8"
|
|||
lazy_static = "1.4.0"
|
||||
libc = "0.2.137"
|
||||
lru = "0.10.0"
|
||||
nix = { version = "0.25.0", default-features = false, features = ["inotify", "resource"] }
|
||||
nix = { version = "0.25.0", default-features = false, features = ["inotify", "resource", "fs"] }
|
||||
num-traits = "0.2.15"
|
||||
once_cell = "1.17.0"
|
||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||
|
|
|
@ -336,8 +336,7 @@ fn test_autoload() {
|
|||
}
|
||||
|
||||
fn touch_file(path: &wstr) {
|
||||
let fd = wopen_cloexec(path, O_RDWR | O_CREAT, 0o666);
|
||||
assert!(fd >= 0);
|
||||
let fd = wopen_cloexec(path, O_RDWR | O_CREAT, 0o666).unwrap();
|
||||
write_loop(&fd, "Hello".as_bytes()).unwrap();
|
||||
unsafe { libc::close(fd) };
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
};
|
||||
use errno::{self, Errno};
|
||||
use libc::{fchdir, EACCES, ELOOP, ENOENT, ENOTDIR, EPERM, O_RDONLY};
|
||||
use std::sync::Arc;
|
||||
use std::{os::fd::AsRawFd, sync::Arc};
|
||||
|
||||
// The cd builtin. Changes the current directory to the one specified or to $HOME if none is
|
||||
// specified. The directory can be relative to any directory in the CDPATH variable.
|
||||
|
@ -86,32 +86,47 @@ pub fn cd(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Optio
|
|||
|
||||
errno::set_errno(Errno(0));
|
||||
|
||||
// We need to keep around the fd for this directory, in the parser.
|
||||
let dir_fd = Arc::new(AutoCloseFd::new(wopen_cloexec(&norm_dir, O_RDONLY, 0)));
|
||||
let res = wopen_cloexec(&norm_dir, O_RDONLY, 0)
|
||||
.map(AutoCloseFd::new)
|
||||
.map_err(|err| err as i32);
|
||||
|
||||
if !(dir_fd.is_valid() && unsafe { fchdir(dir_fd.fd()) } == 0) {
|
||||
// Some errors we skip and only report if nothing worked.
|
||||
// ENOENT in particular is very low priority
|
||||
// - if in another directory there was a *file* by the correct name
|
||||
// we prefer *that* error because it's more specific
|
||||
if errno::errno().0 == ENOENT {
|
||||
let tmp = wreadlink(&norm_dir);
|
||||
// clippy doesn't like this is_some/unwrap pair, but using if let is harder to read IMO
|
||||
#[allow(clippy::unnecessary_unwrap)]
|
||||
if broken_symlink.is_empty() && tmp.is_some() {
|
||||
broken_symlink = norm_dir;
|
||||
broken_symlink_target = tmp.unwrap();
|
||||
} else if best_errno == 0 {
|
||||
best_errno = errno::errno().0;
|
||||
}
|
||||
continue;
|
||||
} else if errno::errno().0 == ENOTDIR {
|
||||
best_errno = errno::errno().0;
|
||||
continue;
|
||||
let res = res.and_then(|fd| {
|
||||
if unsafe { fchdir(fd.as_raw_fd()) } == 0 {
|
||||
Ok(fd)
|
||||
} else {
|
||||
Err(errno::errno().0)
|
||||
}
|
||||
best_errno = errno::errno().0;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
let fd = match res {
|
||||
Ok(raw_fd) => raw_fd,
|
||||
Err(err) => {
|
||||
// Some errors we skip and only report if nothing worked.
|
||||
// ENOENT in particular is very low priority
|
||||
// - if in another directory there was a *file* by the correct name
|
||||
// we prefer *that* error because it's more specific
|
||||
if err == ENOENT {
|
||||
let tmp = wreadlink(&norm_dir);
|
||||
// clippy doesn't like this is_some/unwrap pair, but using if let is harder to read IMO
|
||||
#[allow(clippy::unnecessary_unwrap)]
|
||||
if broken_symlink.is_empty() && tmp.is_some() {
|
||||
broken_symlink = norm_dir;
|
||||
broken_symlink_target = tmp.unwrap();
|
||||
} else if best_errno == 0 {
|
||||
best_errno = errno::errno().0;
|
||||
}
|
||||
continue;
|
||||
} else if err == ENOTDIR {
|
||||
best_errno = err;
|
||||
continue;
|
||||
}
|
||||
best_errno = err;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// We need to keep around the fd for this directory, in the parser.
|
||||
let dir_fd = Arc::new(fd);
|
||||
|
||||
// Stash the fd for the cwd in the parser.
|
||||
parser.libdata_mut().cwd_fd = Some(dir_fd);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||||
|
||||
use crate::{
|
||||
common::{escape, scoped_push_replacer, FilenameRef},
|
||||
fds::{wopen_cloexec, AutoCloseFd},
|
||||
fds::wopen_cloexec,
|
||||
nix::isatty,
|
||||
parser::Block,
|
||||
reader::reader_read,
|
||||
|
@ -48,8 +50,7 @@ pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O
|
|||
func_filename = FilenameRef::new(L!("-").to_owned());
|
||||
fd = streams.stdin_fd;
|
||||
} else {
|
||||
opened_fd = AutoCloseFd::new(wopen_cloexec(args[optind], O_RDONLY, 0));
|
||||
if !opened_fd.is_valid() {
|
||||
let Ok(raw_fd) = wopen_cloexec(args[optind], O_RDONLY, 0) else {
|
||||
let esc = escape(args[optind]);
|
||||
streams.err.append(wgettext_fmt!(
|
||||
"%ls: Error encountered while sourcing file '%ls':\n",
|
||||
|
@ -58,9 +59,10 @@ pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O
|
|||
));
|
||||
builtin_wperror(cmd, streams);
|
||||
return STATUS_CMD_ERROR;
|
||||
}
|
||||
};
|
||||
opened_fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
|
||||
|
||||
fd = opened_fd.fd();
|
||||
fd = opened_fd.as_raw_fd();
|
||||
let mut buf: libc::stat = unsafe { std::mem::zeroed() };
|
||||
if unsafe { libc::fstat(fd, &mut buf) } == -1 {
|
||||
let esc = escape(args[optind]);
|
||||
|
|
|
@ -19,12 +19,12 @@ use crate::wutil::{
|
|||
wunlink, FileId, INVALID_FILE_ID,
|
||||
};
|
||||
use errno::{errno, Errno};
|
||||
use libc::{EINTR, ENOTSUP, EOPNOTSUPP, LOCK_EX, O_CREAT, O_RDONLY, O_RDWR};
|
||||
use libc::{EINTR, LOCK_EX, O_CREAT, O_RDONLY, O_RDWR};
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::CString;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::os::fd::RawFd;
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::os::unix::prelude::MetadataExt;
|
||||
|
||||
// Pull in the O_EXLOCK constant if it is defined, otherwise set it to 0.
|
||||
|
@ -236,14 +236,13 @@ impl EnvUniversal {
|
|||
|
||||
// Open the file.
|
||||
let vars_fd = self.open_and_acquire_lock();
|
||||
if !vars_fd.is_valid() {
|
||||
let Some(vars_fd) = vars_fd else {
|
||||
FLOG!(uvar_file, "universal log open_and_acquire_lock() failed");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Read from it.
|
||||
assert!(vars_fd.is_valid());
|
||||
self.load_from_fd(vars_fd.fd(), callbacks);
|
||||
self.load_from_fd(vars_fd.as_raw_fd(), callbacks);
|
||||
|
||||
if self.ok_to_save {
|
||||
self.save(&directory)
|
||||
|
@ -389,18 +388,17 @@ impl EnvUniversal {
|
|||
return true;
|
||||
}
|
||||
|
||||
let mut result = false;
|
||||
let fd = AutoCloseFd::new(open_cloexec(&self.narrow_vars_path, O_RDONLY, 0));
|
||||
if fd.is_valid() {
|
||||
FLOG!(uvar_file, "universal log reading from file");
|
||||
self.load_from_fd(fd.fd(), callbacks);
|
||||
result = true;
|
||||
}
|
||||
result
|
||||
let Ok(raw_fd) = open_cloexec(&self.narrow_vars_path, O_RDONLY, 0) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(raw_fd) };
|
||||
FLOG!(uvar_file, "universal log reading from file");
|
||||
self.load_from_fd(fd.as_raw_fd(), callbacks);
|
||||
true
|
||||
}
|
||||
|
||||
fn load_from_fd(&mut self, fd: RawFd, callbacks: &mut CallbackDataList) {
|
||||
assert!(fd >= 0);
|
||||
// Get the dev / inode.
|
||||
let current_file = file_id_for_fd(fd);
|
||||
if current_file == self.last_read_file {
|
||||
|
@ -426,7 +424,7 @@ impl EnvUniversal {
|
|||
}
|
||||
|
||||
// Functions concerned with saving.
|
||||
fn open_and_acquire_lock(&mut self) -> AutoCloseFd {
|
||||
fn open_and_acquire_lock(&mut self) -> Option<OwnedFd> {
|
||||
// Attempt to open the file for reading at the given path, atomically acquiring a lock. On BSD,
|
||||
// we can use O_EXLOCK. On Linux, we open the file, take a lock, and then compare fstat() to
|
||||
// stat(); if they match, it means that the file was not replaced before we acquired the lock.
|
||||
|
@ -441,57 +439,62 @@ impl EnvUniversal {
|
|||
locked_by_open = true;
|
||||
}
|
||||
|
||||
let mut fd = AutoCloseFd::empty();
|
||||
while !fd.is_valid() {
|
||||
fd = AutoCloseFd::new(wopen_cloexec(&self.vars_path, flags, 0o644));
|
||||
|
||||
if !fd.is_valid() {
|
||||
let err = errno();
|
||||
if err.0 == EINTR {
|
||||
continue; // signaled; try again
|
||||
}
|
||||
|
||||
if O_EXLOCK != 0 {
|
||||
if (flags & O_EXLOCK) != 0 && [ENOTSUP, EOPNOTSUPP].contains(&err.0) {
|
||||
// Filesystem probably does not support locking. Give up on locking.
|
||||
// Note that on Linux the two errno symbols have the same value but on BSD they're
|
||||
// different.
|
||||
flags &= !O_EXLOCK;
|
||||
self.do_flock = false;
|
||||
locked_by_open = false;
|
||||
continue;
|
||||
let mut res_fd = None;
|
||||
while res_fd.is_none() {
|
||||
let raw = match wopen_cloexec(&self.vars_path, flags, 0o644) {
|
||||
Ok(raw) => raw,
|
||||
Err(err) => {
|
||||
if err == nix::Error::EINTR {
|
||||
continue; // signaled; try again
|
||||
}
|
||||
}
|
||||
FLOG!(
|
||||
error,
|
||||
wgettext_fmt!(
|
||||
"Unable to open universal variable file '%s': %s",
|
||||
&self.vars_path,
|
||||
err.to_string()
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
assert!(fd.is_valid(), "Should have a valid fd here");
|
||||
if O_EXLOCK != 0 {
|
||||
if (flags & O_EXLOCK) != 0
|
||||
&& [nix::Error::ENOTSUP, nix::Error::EOPNOTSUPP].contains(&err)
|
||||
{
|
||||
// Filesystem probably does not support locking. Give up on locking.
|
||||
// Note that on Linux the two errno symbols have the same value but on BSD they're
|
||||
// different.
|
||||
flags &= !O_EXLOCK;
|
||||
self.do_flock = false;
|
||||
locked_by_open = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
FLOG!(
|
||||
error,
|
||||
wgettext_fmt!(
|
||||
"Unable to open universal variable file '%s': %s",
|
||||
&self.vars_path,
|
||||
err.to_string()
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
assert!(raw >= 0, "Should have a valid fd here");
|
||||
let fd = unsafe { OwnedFd::from_raw_fd(raw) };
|
||||
|
||||
// Lock if we want to lock and open() didn't do it for us.
|
||||
// If flock fails, give up on locking forever.
|
||||
if self.do_flock && !locked_by_open {
|
||||
if !flock_uvar_file(fd.fd()) {
|
||||
if !flock_uvar_file(fd.as_raw_fd()) {
|
||||
self.do_flock = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hopefully we got the lock. However, it's possible the file changed out from under us
|
||||
// while we were waiting for the lock. Make sure that didn't happen.
|
||||
if file_id_for_fd(fd.fd()) != file_id_for_path(&self.vars_path) {
|
||||
if file_id_for_fd(fd.as_raw_fd()) != file_id_for_path(&self.vars_path) {
|
||||
// Oops, it changed! Try again.
|
||||
fd.close();
|
||||
drop(fd);
|
||||
} else {
|
||||
res_fd = Some(fd);
|
||||
}
|
||||
}
|
||||
|
||||
fd
|
||||
res_fd
|
||||
}
|
||||
|
||||
fn open_temporary_file(&mut self, directory: &wstr, out_path: &mut WString) -> AutoCloseFd {
|
||||
|
|
|
@ -367,7 +367,7 @@ pub fn is_thompson_shell_script(path: &CStr) -> bool {
|
|||
let e = errno();
|
||||
let mut res = false;
|
||||
let fd = open_cloexec(path, O_RDONLY | O_NOCTTY, 0);
|
||||
if fd != -1 {
|
||||
if let Ok(fd) = fd {
|
||||
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||
let mut buf = [b'\0'; 256];
|
||||
if let Ok(got) = file.read(&mut buf) {
|
||||
|
|
28
src/fds.rs
28
src/fds.rs
|
@ -10,7 +10,7 @@ use libc::{
|
|||
c_int, EINTR, FD_CLOEXEC, F_DUPFD_CLOEXEC, F_GETFD, F_GETFL, F_SETFD, F_SETFL, O_CLOEXEC,
|
||||
O_NONBLOCK,
|
||||
};
|
||||
use nix::unistd;
|
||||
use nix::{fcntl::OFlag, unistd};
|
||||
use std::ffi::CStr;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::unix::prelude::*;
|
||||
|
@ -222,12 +222,14 @@ pub fn set_cloexec(fd: RawFd, should_set: bool /* = true */) -> c_int {
|
|||
|
||||
/// Wide character version of open() that also sets the close-on-exec flag (atomically when
|
||||
/// possible).
|
||||
pub fn wopen_cloexec(pathname: &wstr, flags: i32, mode: libc::c_int) -> RawFd {
|
||||
pub fn wopen_cloexec(pathname: &wstr, flags: i32, mode: libc::c_int) -> nix::Result<RawFd> {
|
||||
open_cloexec(wcs2zstring(pathname).as_c_str(), flags, mode)
|
||||
}
|
||||
|
||||
/// Narrow versions of wopen_cloexec.
|
||||
pub fn open_cloexec(path: &CStr, flags: i32, mode: libc::c_int) -> RawFd {
|
||||
pub fn open_cloexec(path: &CStr, flags: i32, mode: libc::c_int) -> nix::Result<RawFd> {
|
||||
let flags = unsafe { OFlag::from_bits_unchecked(flags) };
|
||||
let mode = unsafe { nix::sys::stat::Mode::from_bits_unchecked(mode as u32) };
|
||||
// Port note: the C++ version of this function had a fallback for platforms where
|
||||
// O_CLOEXEC is not supported, using fcntl. In 2023, this is no longer needed.
|
||||
let saved_errno = errno();
|
||||
|
@ -236,15 +238,21 @@ pub fn open_cloexec(path: &CStr, flags: i32, mode: libc::c_int) -> RawFd {
|
|||
// if we get EINTR and it's not a SIGINIT, we continue.
|
||||
// If it is that's our cancel signal, so we abort.
|
||||
loop {
|
||||
let ret = unsafe { libc::open(path.as_ptr(), flags | O_CLOEXEC, mode) };
|
||||
if ret >= 0 {
|
||||
set_errno(saved_errno);
|
||||
return ret;
|
||||
let ret = nix::fcntl::open(path, flags | OFlag::O_CLOEXEC, mode);
|
||||
|
||||
match ret {
|
||||
Ok(ret) => {
|
||||
set_errno(saved_errno);
|
||||
return Ok(ret);
|
||||
}
|
||||
Err(err) => {
|
||||
if err != nix::Error::EINTR || signal_check_cancel() != 0 {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errno::errno().0 != EINTR || signal_check_cancel() != 0 {
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
200
src/history.rs
200
src/history.rs
|
@ -25,7 +25,7 @@ use std::{
|
|||
mem,
|
||||
num::NonZeroUsize,
|
||||
ops::ControlFlow,
|
||||
os::fd::RawFd,
|
||||
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
|
||||
sync::{Arc, Mutex, MutexGuard},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
@ -480,33 +480,35 @@ impl HistoryImpl {
|
|||
|
||||
let _profiler = TimeProfiler::new("load_old");
|
||||
if let Some(filename) = history_filename(&self.name, L!("")) {
|
||||
let file = AutoCloseFd::new(wopen_cloexec(&filename, O_RDONLY, 0));
|
||||
let fd = file.fd();
|
||||
if fd >= 0 {
|
||||
// Take a read lock to guard against someone else appending. This is released after
|
||||
// getting the file's length. We will read the file after releasing the lock, but that's
|
||||
// not a problem, because we never modify already written data. In short, the purpose of
|
||||
// this lock is to ensure we don't see the file size change mid-update.
|
||||
//
|
||||
// We may fail to lock (e.g. on lockless NFS - see issue #685. In that case, we proceed
|
||||
// as if it did not fail. The risk is that we may get an incomplete history item; this
|
||||
// is unlikely because we only treat an item as valid if it has a terminating newline.
|
||||
let locked = unsafe { Self::maybe_lock_file(fd, LOCK_SH) };
|
||||
self.file_contents = HistoryFileContents::create(fd);
|
||||
self.history_file_id = if self.file_contents.is_some() {
|
||||
file_id_for_fd(fd)
|
||||
} else {
|
||||
INVALID_FILE_ID
|
||||
};
|
||||
if locked {
|
||||
unsafe {
|
||||
Self::unlock_file(fd);
|
||||
}
|
||||
}
|
||||
let Ok(raw_fd) = wopen_cloexec(&filename, O_RDONLY, 0) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _profiler = TimeProfiler::new("populate_from_file_contents");
|
||||
self.populate_from_file_contents();
|
||||
let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
|
||||
|
||||
// Take a read lock to guard against someone else appending. This is released after
|
||||
// getting the file's length. We will read the file after releasing the lock, but that's
|
||||
// not a problem, because we never modify already written data. In short, the purpose of
|
||||
// this lock is to ensure we don't see the file size change mid-update.
|
||||
//
|
||||
// We may fail to lock (e.g. on lockless NFS - see issue #685. In that case, we proceed
|
||||
// as if it did not fail. The risk is that we may get an incomplete history item; this
|
||||
// is unlikely because we only treat an item as valid if it has a terminating newline.
|
||||
let locked = unsafe { Self::maybe_lock_file(fd.as_raw_fd(), LOCK_SH) };
|
||||
self.file_contents = HistoryFileContents::create(fd.as_raw_fd());
|
||||
self.history_file_id = if self.file_contents.is_some() {
|
||||
file_id_for_fd(fd.as_raw_fd())
|
||||
} else {
|
||||
INVALID_FILE_ID
|
||||
};
|
||||
if locked {
|
||||
unsafe {
|
||||
Self::unlock_file(fd.as_raw_fd());
|
||||
}
|
||||
}
|
||||
|
||||
let _profiler = TimeProfiler::new("populate_from_file_contents");
|
||||
self.populate_from_file_contents();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -670,42 +672,46 @@ impl HistoryImpl {
|
|||
if done {
|
||||
break;
|
||||
}
|
||||
|
||||
let target_fd_before =
|
||||
wopen_cloexec(&target_name, O_RDONLY | O_CREAT, HISTORY_FILE_MODE)
|
||||
.map(|raw_fd| unsafe { OwnedFd::from_raw_fd(raw_fd) });
|
||||
|
||||
let orig_file_id = target_fd_before
|
||||
.as_ref()
|
||||
.map(|fd| file_id_for_fd(fd.as_raw_fd()))
|
||||
.unwrap_or(INVALID_FILE_ID);
|
||||
|
||||
// Open any target file, but do not lock it right away
|
||||
let mut target_fd_before = AutoCloseFd::new(wopen_cloexec(
|
||||
&target_name,
|
||||
O_RDONLY | O_CREAT,
|
||||
HISTORY_FILE_MODE,
|
||||
));
|
||||
let orig_file_id = file_id_for_fd(target_fd_before.fd()); // possibly invalid
|
||||
if !self.rewrite_to_temporary_file(
|
||||
if target_fd_before.is_valid() {
|
||||
Some(target_fd_before.fd())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
target_fd_before.as_ref().map(AsRawFd::as_raw_fd).ok(),
|
||||
tmp_fd,
|
||||
) {
|
||||
// Failed to write, no good
|
||||
break;
|
||||
}
|
||||
target_fd_before.close();
|
||||
drop(target_fd_before);
|
||||
|
||||
// The crux! We rewrote the history file; see if the history file changed while we
|
||||
// were rewriting it. Make an effort to take the lock before checking, to avoid racing.
|
||||
// If the open fails, then proceed; this may be because there is no current history
|
||||
let mut new_file_id = INVALID_FILE_ID;
|
||||
let target_fd_after = AutoCloseFd::new(wopen_cloexec(&target_name, O_RDONLY, 0));
|
||||
if target_fd_after.is_valid() {
|
||||
|
||||
let target_fd_after = wopen_cloexec(&target_name, O_RDONLY, 0)
|
||||
.map(|raw_fd| unsafe { OwnedFd::from_raw_fd(raw_fd) });
|
||||
|
||||
if let Ok(target_fd_after) = target_fd_after.as_ref() {
|
||||
// critical to take the lock before checking file IDs,
|
||||
// and hold it until after we are done replacing.
|
||||
// Also critical to check the file at the path, NOT based on our fd.
|
||||
// It's only OK to replace the file while holding the lock.
|
||||
// Note any lock is released when target_fd_after is closed.
|
||||
unsafe {
|
||||
Self::maybe_lock_file(target_fd_after.fd(), LOCK_EX);
|
||||
Self::maybe_lock_file(target_fd_after.as_raw_fd(), LOCK_EX);
|
||||
}
|
||||
new_file_id = file_id_for_path(&target_name);
|
||||
}
|
||||
|
||||
let can_replace_file = new_file_id == orig_file_id || new_file_id == INVALID_FILE_ID;
|
||||
if !can_replace_file {
|
||||
// The file has changed, so we're going to re-read it
|
||||
|
@ -728,23 +734,23 @@ impl HistoryImpl {
|
|||
// 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).
|
||||
let mut sbuf: libc::stat = unsafe { mem::zeroed() };
|
||||
if target_fd_after.is_valid()
|
||||
&& unsafe { fstat(target_fd_after.fd(), &mut sbuf) } >= 0
|
||||
{
|
||||
if unsafe { fchown(tmp_fd, sbuf.st_uid, sbuf.st_gid) } == -1 {
|
||||
FLOGF!(
|
||||
history_file,
|
||||
"Error %d when changing ownership of history file",
|
||||
errno::errno().0
|
||||
);
|
||||
}
|
||||
if unsafe { fchmod(tmp_fd, sbuf.st_mode) } == -1 {
|
||||
FLOGF!(
|
||||
history_file,
|
||||
"Error %d when changing mode of history file",
|
||||
errno::errno().0,
|
||||
);
|
||||
if let Ok(target_fd_after) = target_fd_after.as_ref() {
|
||||
let mut sbuf: libc::stat = unsafe { mem::zeroed() };
|
||||
if unsafe { fstat(target_fd_after.as_raw_fd(), &mut sbuf) } >= 0 {
|
||||
if unsafe { fchown(tmp_fd, sbuf.st_uid, sbuf.st_gid) } == -1 {
|
||||
FLOGF!(
|
||||
history_file,
|
||||
"Error %d when changing ownership of history file",
|
||||
errno::errno().0
|
||||
);
|
||||
}
|
||||
if unsafe { fchmod(tmp_fd, sbuf.st_mode) } == -1 {
|
||||
FLOGF!(
|
||||
history_file,
|
||||
"Error %d when changing mode of history file",
|
||||
errno::errno().0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -762,6 +768,8 @@ impl HistoryImpl {
|
|||
// We did it
|
||||
done = true;
|
||||
}
|
||||
|
||||
drop(target_fd_after);
|
||||
}
|
||||
|
||||
// Ensure we never leave the old file around
|
||||
|
@ -804,34 +812,36 @@ impl HistoryImpl {
|
|||
// After locking it, we need to stat the file at the path; if there is a new file there, it
|
||||
// means the file was replaced and we have to try again.
|
||||
// Limit our max tries so we don't do this forever.
|
||||
let mut history_fd = AutoCloseFd::new(-1);
|
||||
let mut history_fd = None;
|
||||
for _i in 0..MAX_SAVE_TRIES {
|
||||
let fd = AutoCloseFd::new(wopen_cloexec(&history_path, O_WRONLY | O_APPEND, 0));
|
||||
if !fd.is_valid() {
|
||||
let Ok(fd) = wopen_cloexec(&history_path, O_WRONLY | O_APPEND, 0) else {
|
||||
// can't open, we're hosed
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
|
||||
// Exclusive lock on the entire file. This is released when we close the file (below). This
|
||||
// may fail on (e.g.) lockless NFS. If so, proceed as if it did not fail; the risk is that
|
||||
// we may get interleaved history items, which is considered better than no history, or
|
||||
// forcing everything through the slow copy-move mode. We try to minimize this possibility
|
||||
// by writing with O_APPEND.
|
||||
unsafe {
|
||||
Self::maybe_lock_file(fd.fd(), LOCK_EX);
|
||||
Self::maybe_lock_file(fd.as_raw_fd(), LOCK_EX);
|
||||
}
|
||||
let file_id = file_id_for_fd(fd.fd());
|
||||
let file_id = file_id_for_fd(fd.as_raw_fd());
|
||||
if file_id_for_path(&history_path) == file_id {
|
||||
// File IDs match, so the file we opened is still at that path
|
||||
// We're going to use this fd
|
||||
if file_id != self.history_file_id {
|
||||
file_changed = true;
|
||||
}
|
||||
history_fd = fd;
|
||||
history_fd = Some(fd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if history_fd.is_valid() {
|
||||
if let Some(history_fd) = history_fd {
|
||||
// We (hopefully successfully) took the exclusive lock. Append to the file.
|
||||
// Note that this is sketchy for a few reasons:
|
||||
// - Another shell may have appended its own items with a later timestamp, so our file may
|
||||
|
@ -860,7 +870,11 @@ impl HistoryImpl {
|
|||
let item = &self.new_items[self.first_unwritten_new_item_index];
|
||||
if item.should_write_to_disk() {
|
||||
append_history_item_to_buffer(item, &mut buffer);
|
||||
res = flush_to_fd(&mut buffer, history_fd.fd(), HISTORY_OUTPUT_BUFFER_SIZE);
|
||||
res = flush_to_fd(
|
||||
&mut buffer,
|
||||
history_fd.as_raw_fd(),
|
||||
HISTORY_OUTPUT_BUFFER_SIZE,
|
||||
);
|
||||
if res.is_err() {
|
||||
break;
|
||||
}
|
||||
|
@ -870,7 +884,7 @@ impl HistoryImpl {
|
|||
}
|
||||
|
||||
if res.is_ok() {
|
||||
res = flush_to_fd(&mut buffer, history_fd.fd(), 0);
|
||||
res = flush_to_fd(&mut buffer, history_fd.as_raw_fd(), 0);
|
||||
}
|
||||
|
||||
// Since we just modified the file, update our history_file_id to match its current state
|
||||
|
@ -878,11 +892,12 @@ impl HistoryImpl {
|
|||
// write.
|
||||
// We don't update the mapping since we only appended to the file, and everything we
|
||||
// appended remains in our new_items
|
||||
self.history_file_id = file_id_for_fd(history_fd.fd());
|
||||
self.history_file_id = file_id_for_fd(history_fd.as_raw_fd());
|
||||
|
||||
ok = res.is_ok();
|
||||
|
||||
drop(history_fd);
|
||||
}
|
||||
history_fd.close();
|
||||
|
||||
// If someone has replaced the file, forget our file state.
|
||||
if file_changed {
|
||||
|
@ -1090,28 +1105,33 @@ impl HistoryImpl {
|
|||
old_file.push_utfstr(&self.name);
|
||||
old_file.push_str("_history");
|
||||
|
||||
let mut src_fd = AutoCloseFd::new(wopen_cloexec(&old_file, O_RDONLY, 0));
|
||||
if src_fd.is_valid() {
|
||||
// Clear must come after we've retrieved the new_file name, and before we open
|
||||
// destination file descriptor, since it destroys the name and the file.
|
||||
self.clear();
|
||||
let Ok(src_fd) = wopen_cloexec(&old_file, O_RDONLY, 0) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut dst_fd = AutoCloseFd::new(wopen_cloexec(
|
||||
&new_file,
|
||||
O_WRONLY | O_CREAT,
|
||||
HISTORY_FILE_MODE,
|
||||
));
|
||||
let mut src_fd = unsafe { std::fs::File::from_raw_fd(src_fd) };
|
||||
|
||||
let mut buf = [0; libc::BUFSIZ as usize];
|
||||
while let Ok(n) = src_fd.read(&mut buf) {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
if dst_fd.write(&buf[..n]).is_err() {
|
||||
// This message does not have high enough priority to be shown by default.
|
||||
FLOG!(history_file, "Error when writing history file");
|
||||
break;
|
||||
}
|
||||
// Clear must come after we've retrieved the new_file name, and before we open
|
||||
// destination file descriptor, since it destroys the name and the file.
|
||||
self.clear();
|
||||
|
||||
let Ok(dst_fd) = wopen_cloexec(&new_file, O_WRONLY | O_CREAT, HISTORY_FILE_MODE) else {
|
||||
FLOG!(history_file, "Error when writing history file");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut dst_fd = unsafe { std::fs::File::from_raw_fd(dst_fd) };
|
||||
|
||||
let mut buf = [0; libc::BUFSIZ as usize];
|
||||
while let Ok(n) = src_fd.read(&mut buf) {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if dst_fd.write(&buf[..n]).is_err() {
|
||||
// This message does not have high enough priority to be shown by default.
|
||||
FLOG!(history_file, "Error when writing history file");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
89
src/io.rs
89
src/io.rs
|
@ -17,7 +17,7 @@ use crate::topic_monitor::topic_t;
|
|||
use crate::wchar::prelude::*;
|
||||
use crate::wutil::{perror, perror_io, wdirname, wstat, wwrite_to_fd};
|
||||
use errno::Errno;
|
||||
use libc::{EAGAIN, EEXIST, EINTR, ENOENT, ENOTDIR, EPIPE, EWOULDBLOCK, O_EXCL, STDOUT_FILENO};
|
||||
use libc::{EAGAIN, EINTR, ENOENT, ENOTDIR, EPIPE, EWOULDBLOCK, O_EXCL, STDOUT_FILENO};
|
||||
use std::cell::{RefCell, UnsafeCell};
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
@ -657,52 +657,61 @@ impl IoChain {
|
|||
// Mark it as CLO_EXEC because we don't want it to be open in any child.
|
||||
let path = path_apply_working_directory(&spec.target, pwd);
|
||||
let oflags = spec.oflags();
|
||||
let file = AutoCloseFd::new(wopen_cloexec(&path, oflags, OPEN_MASK));
|
||||
if !file.is_valid() {
|
||||
if (oflags & O_EXCL) != 0 && errno::errno().0 == EEXIST {
|
||||
FLOGF!(warning, NOCLOB_ERROR, spec.target);
|
||||
} else {
|
||||
if should_flog!(warning) {
|
||||
let err = errno::errno().0;
|
||||
// If the error is that the file doesn't exist
|
||||
// or there's a non-directory component,
|
||||
// find the first problematic component for a better message.
|
||||
if [ENOENT, ENOTDIR].contains(&err) {
|
||||
FLOGF!(warning, FILE_ERROR, spec.target);
|
||||
let mut dname: &wstr = &spec.target;
|
||||
while !dname.is_empty() {
|
||||
let next: &wstr = wdirname(dname);
|
||||
if let Ok(md) = wstat(next) {
|
||||
if !md.is_dir() {
|
||||
FLOGF!(
|
||||
warning,
|
||||
"Path '%ls' is not a directory",
|
||||
next
|
||||
);
|
||||
} else {
|
||||
FLOGF!(warning, "Path '%ls' does not exist", dname);
|
||||
|
||||
match wopen_cloexec(&path, oflags, OPEN_MASK) {
|
||||
Ok(raw_fd) => {
|
||||
let file = AutoCloseFd::new(raw_fd);
|
||||
self.push(Arc::new(IoFile::new(spec.fd, file)));
|
||||
}
|
||||
Err(err) => {
|
||||
if (oflags & O_EXCL) != 0 && err == nix::Error::EEXIST {
|
||||
FLOGF!(warning, NOCLOB_ERROR, spec.target);
|
||||
} else {
|
||||
if should_flog!(warning) {
|
||||
let err = errno::errno().0;
|
||||
// If the error is that the file doesn't exist
|
||||
// or there's a non-directory component,
|
||||
// find the first problematic component for a better message.
|
||||
if [ENOENT, ENOTDIR].contains(&err) {
|
||||
FLOGF!(warning, FILE_ERROR, spec.target);
|
||||
let mut dname: &wstr = &spec.target;
|
||||
while !dname.is_empty() {
|
||||
let next: &wstr = wdirname(dname);
|
||||
if let Ok(md) = wstat(next) {
|
||||
if !md.is_dir() {
|
||||
FLOGF!(
|
||||
warning,
|
||||
"Path '%ls' is not a directory",
|
||||
next
|
||||
);
|
||||
} else {
|
||||
FLOGF!(
|
||||
warning,
|
||||
"Path '%ls' does not exist",
|
||||
dname
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
dname = next;
|
||||
}
|
||||
dname = next;
|
||||
} else if err != EINTR {
|
||||
// If we get EINTR we had a cancel signal.
|
||||
// That's expected (ctrl-c on the commandline),
|
||||
// so no warning.
|
||||
FLOGF!(warning, FILE_ERROR, spec.target);
|
||||
perror("open");
|
||||
}
|
||||
} else if err != EINTR {
|
||||
// If we get EINTR we had a cancel signal.
|
||||
// That's expected (ctrl-c on the commandline),
|
||||
// so no warning.
|
||||
FLOGF!(warning, FILE_ERROR, spec.target);
|
||||
perror("open");
|
||||
}
|
||||
}
|
||||
// If opening a file fails, insert a closed FD instead of the file redirection
|
||||
// and return false. This lets execution potentially recover and at least gives
|
||||
// the shell a chance to gracefully regain control of the shell (see #7038).
|
||||
self.push(Arc::new(IoClose::new(spec.fd)));
|
||||
have_error = true;
|
||||
continue;
|
||||
}
|
||||
// If opening a file fails, insert a closed FD instead of the file redirection
|
||||
// and return false. This lets execution potentially recover and at least gives
|
||||
// the shell a chance to gracefully regain control of the shell (see #7038).
|
||||
self.push(Arc::new(IoClose::new(spec.fd)));
|
||||
have_error = true;
|
||||
continue;
|
||||
}
|
||||
self.push(Arc::new(IoFile::new(spec.fd, file)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -357,12 +357,16 @@ impl Parser {
|
|||
profile_items: RefCell::default(),
|
||||
global_event_blocks: AtomicU64::new(0),
|
||||
});
|
||||
let cwd = open_cloexec(CStr::from_bytes_with_nul(b".\0").unwrap(), O_RDONLY, 0);
|
||||
if cwd < 0 {
|
||||
perror("Unable to open the current working directory");
|
||||
} else {
|
||||
result.libdata_mut().cwd_fd = Some(Arc::new(AutoCloseFd::new(cwd)));
|
||||
|
||||
match open_cloexec(CStr::from_bytes_with_nul(b".\0").unwrap(), O_RDONLY, 0) {
|
||||
Ok(raw_fd) => {
|
||||
result.libdata_mut().cwd_fd = Some(Arc::new(AutoCloseFd::new(raw_fd)));
|
||||
}
|
||||
Err(_) => {
|
||||
perror("Unable to open the current working directory");
|
||||
}
|
||||
}
|
||||
|
||||
result.base.initialize(&result);
|
||||
result
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ use std::cell::UnsafeCell;
|
|||
use std::io::BufReader;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Range;
|
||||
use std::os::fd::RawFd;
|
||||
use std::os::fd::{FromRawFd, RawFd};
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
@ -4658,10 +4658,12 @@ impl ReaderData {
|
|||
let mut path =
|
||||
var.map_or_else(|| L!("~/.bash_history").to_owned(), |var| var.as_string());
|
||||
expand_tilde(&mut path, self.vars());
|
||||
let file = AutoCloseFd::new(wopen_cloexec(&path, O_RDONLY, 0));
|
||||
if !file.is_valid() {
|
||||
|
||||
let Ok(raw_fd) = wopen_cloexec(&path, O_RDONLY, 0) else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let file = unsafe { std::fs::File::from_raw_fd(raw_fd) };
|
||||
self.history.populate_from_bash(BufReader::new(file));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -594,8 +594,8 @@ fn test_history_formats() {
|
|||
"echo foo".into(),
|
||||
];
|
||||
let test_history_imported_from_bash = History::with_name(L!("bash_import"));
|
||||
let file = AutoCloseFd::new(wopen_cloexec(L!("tests/history_sample_bash"), O_RDONLY, 0));
|
||||
assert!(file.is_valid());
|
||||
let file =
|
||||
AutoCloseFd::new(wopen_cloexec(L!("tests/history_sample_bash"), O_RDONLY, 0).unwrap());
|
||||
test_history_imported_from_bash.populate_from_bash(BufReader::new(file));
|
||||
assert_eq!(test_history_imported_from_bash.get_history(), expected);
|
||||
test_history_imported_from_bash.clear();
|
||||
|
|
Loading…
Reference in a new issue