diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-08-23 18:31:35 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-08-23 18:31:35 +0200 |
| commit | 1d933f1a458c510be9329edaab2b1381e098ac48 (patch) | |
| tree | be288232fa592c284c3eb0e1d291a07f12794e5d /pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch | |
| parent | modules/starship: Migrate to `by-name` (diff) | |
| download | nixos-config-1d933f1a458c510be9329edaab2b1381e098ac48.zip | |
modules/fish: Init
Diffstat (limited to 'pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch')
| -rw-r--r-- | pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch | 843 |
1 files changed, 843 insertions, 0 deletions
diff --git a/pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch b/pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch new file mode 100644 index 00000000..3e98b298 --- /dev/null +++ b/pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch @@ -0,0 +1,843 @@ +From 26e8fbb5a24dd8b9cac67adcb59e714b32f95aa6 Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Sat, 22 Aug 2026 22:42:44 +0200 +Subject: [PATCH 3/3] feat(history): Add turtle as history backend + +--- + Cargo.toml | 4 + + src/history/history.rs | 285 ++--------------------------- + src/history/mod.rs | 2 + + src/history/turtle.rs | 400 +++++++++++++++++++++++++++++++++++++++++ + src/parser.rs | 5 + + src/reader/reader.rs | 4 +- + 6 files changed, 424 insertions(+), 276 deletions(-) + create mode 100644 src/history/turtle.rs + +diff --git a/Cargo.toml b/Cargo.toml +index aa91b61b7..137fbed24 100644 +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -11,6 +11,7 @@ repository = "https://github.com/fish-shell/fish-shell" + license = "GPL-2.0-only AND LGPL-2.0-or-later AND MIT AND PSF-2.0" + + [workspace.dependencies] ++turtle-api = { version = "20.0.0" } + anstyle = "1.0.13" + anyhow = "1.0.102" + assert_matches = "1.5.0" +@@ -115,6 +116,9 @@ homepage = "https://fishshell.com" + readme = "README.rst" + + [dependencies] ++turtle-api.workspace = true ++tokio = { version = "1.53.1", features = ["rt"] } ++uuid = { version = "1.24.0", features = ["v7"] } + assert_matches.workspace = true + bitflags.workspace = true + cfg-if.workspace = true +diff --git a/src/history/history.rs b/src/history/history.rs +index e1d04ad49..90cbfe6a9 100644 +--- a/src/history/history.rs ++++ b/src/history/history.rs +@@ -14,6 +14,7 @@ + //! `src/fs.rs`. By default, `flock()` is used for locking. If that is unavailable, an imperfect + //! fallback solution attempts to detect races and retries if a race is detected. + ++use crate::history::turtle::HistoryDb; + use crate::{ + ast::{self, Kind, Node as _}, + common::valid_var_name, +@@ -334,7 +335,7 @@ struct HistoryImpl { + /// Deleted item contents, and the scope of the deletion. + deleted_items: HashMap<WString, DeletionScope>, + /// The history file contents. +- file_contents: Option<HistoryFile>, ++ file_contents: Option<HistoryDb>, + /// The file ID of the history file. + history_file_id: FileId, // INVALID_FILE_ID + /// The boundary timestamp distinguishes old items from new items. Items whose timestamps are <= +@@ -358,6 +359,8 @@ fn history_file_path(&self) -> std::io::Result<Option<WString>> { + return Ok(None); + } + ++ return Ok(Some(L!("/run/user/1000/turtle.sock").to_owned())); ++ + let mut path = if let Some(custom_dir) = &self.custom_directory { + custom_dir.clone() + } else { +@@ -422,10 +425,7 @@ fn add(&mut self, item: HistoryItem, pending: bool, do_save: bool) { + } + + /// Internal function. +- fn clear_file_state(&mut self) { +- // Erase everything we know about our file. +- self.file_contents = None; +- } ++ fn clear_file_state(&mut self) {} + + /// Returns a timestamp for new items - see the implementation for a subtlety. + fn timestamps_as_of_now(&self) -> Timestamps { +@@ -451,32 +451,16 @@ fn timestamps_as_of_now(&self) -> Timestamps { + + /// Loads old items if necessary. + /// Return a reference to the loaded history file. +- fn load_old_if_needed(&mut self) -> &HistoryFile { ++ fn load_old_if_needed(&mut self) -> &HistoryDb { + if let Some(ref file_contents) = self.file_contents { + return file_contents; + } + let Ok(Some(history_path)) = self.history_file_path() else { +- return self.file_contents.insert(HistoryFile::create_empty()); ++ return self.file_contents.insert(HistoryDb::create_empty()); + }; + + let _profiler = TimeProfiler::new("load_old"); +- let file_contents = match lock_and_load(&history_path, RawHistoryFile::create) { +- Ok((file_id, history_file)) => { +- self.history_file_id = file_id; +- let _profiler = TimeProfiler::new("populate_from_file_contents"); +- let file_contents = history_file.decode(Some(self.boundary_timestamp)); +- flogf!( +- history, +- "Loaded %u old items", +- file_contents.offsets().len() +- ); +- file_contents +- } +- Err(e) => { +- flog!(history_file, "Error reading from history file:", e); +- HistoryFile::create_empty() +- } +- }; ++ let file_contents = HistoryDb::load(&history_path, self.boundary_timestamp); + self.file_contents.insert(file_contents) + } + +@@ -524,260 +508,11 @@ fn remove_ephemeral_items(&mut self) { + usize::min(self.first_unwritten_new_item_index, self.new_items.len()); + } + +- /// Given an existing history file, write a new history file to `dst`. +- fn rewrite_to_temporary_file( +- &self, +- existing_file: &File, +- dst: &mut File, +- ) -> std::io::Result<()> { +- // We are reading FROM existing_file and writing TO dst +- +- // Make an LRU cache to save only the last N elements. +- +- /// When we rewrite the history, the number of items we keep. +- const HISTORY_SAVE_MAX: NonZeroUsize = NonZeroUsize::new(1024 * 256).unwrap(); +- let mut lru = LruCache::new(HISTORY_SAVE_MAX); +- +- // Read in existing items (which may have changed out from underneath us, so don't trust our +- // old file contents). +- let file_id = file_id_for_file(existing_file); +- if let Ok(local_file) = RawHistoryFile::create(existing_file, file_id) { +- for offset in local_file.offsets(None) { +- // Try decoding an old item. +- let Some(old_item) = local_file.decode_item(offset) else { +- continue; +- }; +- if old_item.is_empty() { +- continue; +- } +- +- // Check if this item should be deleted. +- if let Some(&scope) = self.deleted_items.get(old_item.str()) { +- // If old item is newer than session always erase if in deleted. +- // If old item is older and in deleted items don't erase if added by clear_session. +- let delete = old_item.first_added_timestamp() > self.boundary_timestamp +- || scope == DeletionScope::AllSessions; +- if delete { +- continue; +- } +- } +- lru.add_item(old_item); +- } +- } +- +- // Insert any unwritten new items +- for item in self +- .new_items +- .iter() +- .skip(self.first_unwritten_new_item_index) +- { +- if item.should_write_to_disk() { +- lru.add_item(item.clone()); +- } +- } +- +- // Stable-sort our items by timestamp +- // This is because we may have read "old" items with a later timestamp than our "new" items +- // This is the essential step that roughly orders items by history +- let mut items: Vec<_> = lru.into_iter().map(|(_key, item)| item).collect(); +- items.sort_by_key(HistoryItem::last_added_timestamp); +- +- /// Default buffer size for flushing to the history file. +- const HISTORY_OUTPUT_BUFFER_SIZE: usize = 64 * 1024; +- // Write them out. +- let mut buffer = BufWriter::with_capacity(HISTORY_OUTPUT_BUFFER_SIZE + 128, dst); +- for item in items { +- item.write_to(&mut buffer)?; +- } +- buffer.flush()?; +- Ok(()) +- } +- +- /// Saves history by rewriting the file. +- fn save_internal_via_rewrite(&mut self, history_path: &wstr) -> std::io::Result<()> { +- flogf!( +- history, +- "Saving %u items via rewrite", +- self.new_items.len() - self.first_unwritten_new_item_index +- ); +- +- let rewrite = +- |old_file: &File, tmp_file: &mut File| -> std::io::Result<PotentialUpdate<()>> { +- let result = self.rewrite_to_temporary_file(old_file, tmp_file); +- if let Err(err) = result { +- flog!( +- history_file, +- "Error writing to temporary history file:", +- err +- ); +- return Err(err); +- } +- Ok(PotentialUpdate { +- do_save: true, +- data: (), +- }) +- }; +- +- let (file_id, _) = rewrite_via_temporary_file(history_path, rewrite)?; +- self.history_file_id = file_id; +- +- // We've saved everything, so we have no more unsaved items. +- self.first_unwritten_new_item_index = self.new_items.len(); +- +- // We deleted our deleted items. +- self.deleted_items.clear(); +- +- // Our history has been written to the file, so clear our state so we can re-reference the +- // file. +- self.clear_file_state(); +- +- Ok(()) +- } +- +- /// Saves history by appending to the file. +- fn save_internal_via_appending(&mut self, history_path: &wstr) -> std::io::Result<()> { +- flogf!( +- history, +- "Saving %u items via appending", +- self.new_items.len() - self.first_unwritten_new_item_index +- ); +- // No deleting allowed. +- assert!(self.deleted_items.is_empty()); +- +- let mut locked_history_file = +- LockedFile::new(LockingMode::Exclusive(WriteMethod::Append), history_path)?; +- +- // Check if the file was modified since it was last read. +- // If someone has replaced the file, forget our file state. +- if file_id_for_file(locked_history_file.get()) != self.history_file_id { +- self.clear_file_state(); +- } +- +- // We 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 +- // no longer be sorted by timestamp. +- // - Another shell may have appended the same items, so our file may now contain +- // duplicates. +- // +- // Originally we always rewrote the file on saving, which avoided both of these problems. +- // However, appending allows us to save history after every command, which is nice! +- // +- // Periodically we "clean up" the file by rewriting it, so that most of the time it doesn't +- // have duplicates, although we don't yet sort by timestamp (the timestamp isn't really used +- // for much anyways). +- +- // So far so good. Write all items at or after first_unwritten_new_item_index. Note that we +- // write even a pending item - pending items are ignored by history within the command +- // itself, but should still be written to the file. +- // Use a small buffer size for appending, as we usually only have 1 item. +- // Buffer everything and then write it all at once to avoid tearing writes (O_APPEND). +- let mut buffer = Vec::new(); +- let mut new_first_index = self.first_unwritten_new_item_index; +- while new_first_index < self.new_items.len() { +- let item = &self.new_items[new_first_index]; +- if item.should_write_to_disk() { +- // Can't error writing to a buffer. +- item.write_to(&mut buffer).unwrap(); +- } +- // We wrote or skipped this item, hooray. +- new_first_index += 1; +- } +- locked_history_file.get_mut().write_all(&buffer)?; +- fsync(locked_history_file.get())?; +- self.first_unwritten_new_item_index = new_first_index; +- +- // Since we just modified the file, update our history_file_id to match its current state +- // Otherwise we'll think the file has been changed by someone else the next time we go to +- // write. +- // We don't update `self.file_contents` since we only appended to the file, and everything we +- // appended remains in our new_items +- self.history_file_id = file_id_for_file(locked_history_file.get()); +- +- Ok(()) +- } +- + /// Saves history. +- fn save(&mut self, vacuum: bool) { +- // Nothing to do if there's no new items. +- if self.first_unwritten_new_item_index >= self.new_items.len() +- && self.deleted_items.is_empty() +- { +- return; +- } +- +- // Compact our new items so we don't have duplicates. +- self.compact_new_items(); +- +- if self.name.is_empty() { +- // We're in the "incognito" mode. Pretend we've saved the history. +- self.first_unwritten_new_item_index = self.new_items.len(); +- self.deleted_items.clear(); +- self.clear_file_state(); +- return; +- } +- +- let history_path = match self.history_file_path() { +- Ok(history_path) => history_path.unwrap(), +- Err(e) => { +- flog!(history, "Saving history failed:", e); +- return; +- } +- }; +- +- // Try saving. If we have items to delete, we have to rewrite the file. If we do not, we can +- // append to it. +- let mut ok = false; +- if !vacuum && self.deleted_items.is_empty() { +- // Try doing a fast append. +- if let Err(e) = self.save_internal_via_appending(&history_path) { +- flog!(history, "Appending to history failed:", e); +- } else { +- ok = true; +- } +- } +- if !ok { +- // We did not or could not append; rewrite the file ("vacuum" it). +- if let Err(e) = self.save_internal_via_rewrite(&history_path) { +- flog!(history, "Rewriting history failed:", e); +- } +- } +- } ++ fn save(&mut self, vacuum: bool) {} + + /// Saves history unless doing so is disabled. +- fn save_unless_disabled(&mut self) { +- // Respect disable_automatic_save_counter. +- if self.disable_automatic_save_counter > 0 { +- return; +- } +- +- // We may or may not vacuum. We try to vacuum every `VACUUM_FREQUENCY` items, but start the +- // countdown at a random number so that even if the user never runs more than 25 commands, we'll +- // eventually vacuum. If countdown_to_vacuum is None, it means we haven't yet picked a value for +- // the counter. +- let countdown_to_vacuum = self +- .countdown_to_vacuum +- .get_or_insert_with(|| rand::rng().random_range(0..VACUUM_FREQUENCY)); +- +- // Determine if we're going to vacuum. +- let mut vacuum = false; +- if *countdown_to_vacuum == 0 { +- *countdown_to_vacuum = VACUUM_FREQUENCY; +- vacuum = true; +- } +- +- // Update our countdown. +- assert!(*countdown_to_vacuum > 0); +- *countdown_to_vacuum -= 1; +- +- // This might be a good candidate for moving to a background thread. +- let _profiler = TimeProfiler::new(if vacuum { +- "save vacuum" +- } else { +- "save no vacuum" +- }); +- self.save(vacuum); +- } ++ fn save_unless_disabled(&mut self) {} + + fn new(name: WString, custom_directory: Option<WString>) -> Self { + Self { +diff --git a/src/history/mod.rs b/src/history/mod.rs +index 7ef4d4417..0de867740 100644 +--- a/src/history/mod.rs ++++ b/src/history/mod.rs +@@ -3,4 +3,6 @@ + mod history; + mod yaml_backend; + ++pub(crate) mod turtle; ++ + pub use history::*; +diff --git a/src/history/turtle.rs b/src/history/turtle.rs +new file mode 100644 +index 000000000..ffcdea108 +--- /dev/null ++++ b/src/history/turtle.rs +@@ -0,0 +1,400 @@ ++use std::{ ++ collections::HashMap, ++ sync::{ ++ Arc, OnceLock, RwLock, ++ atomic::{AtomicBool, Ordering}, ++ mpsc, ++ }, ++ thread::{self, JoinHandle, Thread}, ++ time::{Duration, SystemTime, UNIX_EPOCH}, ++}; ++ ++use fish_widestring::{L, WString}; ++use turtle_api::{ ++ client::{HistoryClient, OffsetDateTime, Probe, Range}, ++ history::{History, HistoryId}, ++}; ++ ++use crate::{env::Environment as _, flog, flogf, history::HistoryItem}; ++ ++pub(crate) struct HistoryDb; ++ ++#[derive(Debug)] ++struct HistoryDbInner { ++ range: Option<Range>, ++ session: uuid::Uuid, ++ ++ loaded_history: Vec<History>, ++ ++ handler: Handler, ++} ++ ++impl HistoryDbInner { ++ fn loaded_history(&mut self) -> &[History] { ++ let should_wait = if self.loaded_history.is_empty() { ++ // We are probably running this the first time. ++ // Make sure, that we actually load something. ++ true ++ } else { ++ false ++ }; ++ ++ if NEW_LOADED_HISTORY_AVAILABLE.load(Ordering::Relaxed) || should_wait { ++ if let Some(pre_loaded_history) = self.handler.load_history_resp(should_wait) { ++ self.loaded_history = pre_loaded_history; ++ } ++ ++ flogf!( ++ history, ++ "Loaded history was requested, returning %d entries.", ++ self.loaded_history.len() ++ ); ++ ++ NEW_LOADED_HISTORY_AVAILABLE.store(false, Ordering::Relaxed); ++ } ++ ++ &self.loaded_history ++ } ++} ++ ++impl HistoryDb { ++ fn with_inner_mut<T>(mut fun: impl FnMut(&mut HistoryDbInner) -> T) -> T { ++ let mut inner = INNER.write().expect("Should not be poisioned"); ++ let inner = (*inner).get_mut().expect("Should be initialized"); ++ ++ let output = fun(inner); ++ ++ output ++ } ++} ++ ++impl HistoryDb { ++ /// Create an empty history file. ++ pub(super) fn create_empty() -> Self { ++ flog!(history, "turtle: Creating new empty hist"); ++ ++ Self ++ } ++ ++ /// Return the offsets of items in this file. ++ pub(super) fn offsets(&self) -> Vec<HistoryId> { ++ let out: Vec<_> = Self::with_inner_mut(|inner| { ++ inner ++ .loaded_history() ++ .iter() ++ .map(|h| h.id.clone()) ++ .collect() ++ }); ++ out ++ } ++ ++ /// Return whether this file is empty. ++ pub(super) fn is_empty(&self) -> bool { ++ self.offsets().is_empty() ++ } ++ ++ /// Load from on-disk file. ++ pub(super) fn load(history_path: &WString, _boundary_timestamp: SystemTime) -> Self { ++ flogf!(history, "turtle: Loading hist from %s", history_path); ++ ++ let inner = { ++ // let range = Some(Range { ++ // start: OffsetDateTime::from_unix_timestamp( ++ // boundary_timestamp ++ // .duration_since(UNIX_EPOCH) ++ // .expect("Also valid") ++ // .as_secs() as i64, ++ // ) ++ // .expect("Valid"), ++ // end: OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()), ++ // }); ++ let range = None; ++ ++ let handler = Handler::start(history_path.to_string()); ++ handler.emit(HandleHistoryCmd::Load { range }); ++ ++ let inner = HistoryDbInner { ++ session: uuid::Uuid::now_v7(), ++ range, ++ loaded_history: vec![], ++ handler, ++ }; ++ ++ inner ++ }; ++ ++ let static_inner = INNER.write().expect("should not be poisened"); ++ (*static_inner) ++ .set(inner) ++ .expect("Should not have been initialized before"); ++ ++ Self ++ } ++ ++ /// Decode an item at a given offset. ++ pub(super) fn decode_item(&self, id: HistoryId) -> Option<HistoryItem> { ++ Self::with_inner_mut(|inner| { ++ inner.loaded_history().iter().find(|h| h.id == id).map(|h| { ++ HistoryItem::new( ++ WString::from_str(&h.command), ++ super::Timestamps { ++ last_added: UNIX_EPOCH ++ + Duration::from_nanos_u128(h.timestamp.unix_timestamp_nanos() as u128), ++ first_added: UNIX_EPOCH ++ + Duration::from_nanos_u128(h.timestamp.unix_timestamp_nanos() as u128), ++ }, ++ super::PersistenceMode::Disk, ++ ) ++ }) ++ }) ++ } ++} ++ ++static INNER: RwLock<OnceLock<HistoryDbInner>> = RwLock::new(OnceLock::new()); ++ ++pub(crate) fn handle_preexec(parser: &mut crate::parser::Parser, command: WString) { ++ HistoryDb::with_inner_mut(|inner| { ++ let now = OffsetDateTime::now_local().expect("Should have local time zone"); ++ let command = command.to_string(); ++ let history: History = History::daemon() ++ .timestamp(now) ++ .command(command) ++ .cwd(parser.vars().get_pwd_slash().to_string()) ++ .session(inner.session) ++ .hostname( ++ parser ++ .vars() ++ .get(L!("hostname")) ++ .map_or(String::from("<unknown>"), |s| s.as_string().to_string()), ++ ) ++ .author( ++ parser ++ .vars() ++ .get(L!("USER")) ++ .map_or(String::from("<unknown>"), |s| s.as_string().to_string()), ++ ) ++ .build() ++ .into(); ++ ++ inner.handler.emit(HandleHistoryCmd::Start { history }); ++ }); ++} ++ ++pub(crate) fn handle_postexec(parser: &mut crate::parser::Parser, command: WString) { ++ HistoryDb::with_inner_mut(|inner| { ++ inner.handler.emit(HandleHistoryCmd::End { ++ command: command.to_string(), ++ when: OffsetDateTime::now_local().expect("to work"), ++ exit_code: i64::from(parser.last_statuses().status), ++ }); ++ inner ++ .handler ++ .emit(HandleHistoryCmd::Load { range: inner.range }); ++ }); ++} ++ ++pub(crate) fn handle_shutdow() { ++ flog!(history, "Shutting down history db"); ++ ++ // If the `load` function was never called, inner will be None. ++ // Therefore we only need to run shutdown, when inner was loaded. ++ let inner_is_initialized = { ++ let read = INNER.read().expect("should be not-poisioned"); ++ read.get().is_some() ++ }; ++ ++ if inner_is_initialized { ++ HistoryDb::with_inner_mut(|inner| inner.handler.stop()); ++ } ++} ++ ++#[derive(Debug)] ++struct Handler { ++ thread: Option<JoinHandle<()>>, ++ ++ cmd_tx: Option<mpsc::Sender<HandleHistoryCmd>>, ++ ++ returned_loaded_history: Arc<RwLock<Option<Vec<History>>>>, ++} ++ ++#[derive(Debug)] ++enum HandleHistoryCmd { ++ Start { ++ history: History, ++ }, ++ End { ++ command: String, ++ when: OffsetDateTime, ++ exit_code: i64, ++ }, ++ Load { ++ range: Option<Range>, ++ }, ++} ++ ++static HANDLER_IS_SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); ++static LOAD_HISTORY_THREAD: RwLock<Option<Thread>> = RwLock::new(None); ++static LOAD_HISTORY_WAITING: AtomicBool = AtomicBool::new(false); ++static NEW_LOADED_HISTORY_AVAILABLE: AtomicBool = AtomicBool::new(false); ++ ++impl Handler { ++ fn start(daemon_socket: String) -> Self { ++ let (cmd_tx, cmd_rx) = mpsc::channel(); ++ let returned_loaded_history = Arc::new(RwLock::new(None)); ++ ++ let loaded_history_return = Arc::clone(&returned_loaded_history); ++ let thread = std::thread::spawn(move || { ++ let mut running_history: HashMap<String, (HistoryId, OffsetDateTime)> = HashMap::new(); ++ ++ let rt = { ++ let mut b = tokio::runtime::Builder::new_current_thread(); ++ b.enable_all(); ++ b.build() ++ .expect("to work, as all the fish code is otherwise sync.") ++ }; ++ let mut client = match rt.block_on(turtle_api::client::probe(daemon_socket.clone())) { ++ Probe::Ready(_) => rt ++ .block_on(HistoryClient::new(daemon_socket)) ++ .expect("We probed, this client should be accessible"), ++ Probe::NeedsRestart(err) => { ++ flogf!(history, "Turtle daemon needs restart: %s", err); ++ todo!(); ++ } ++ Probe::Unreachable(report) => { ++ flogf!(history, "Turtle daemon unreachable: %s", report.to_string()); ++ todo!(); ++ } ++ }; ++ ++ while let Ok(cmd) = cmd_rx.recv() { ++ match cmd { ++ HandleHistoryCmd::Start { history } => { ++ let command = history.command.clone(); ++ ++ flogf!(history, " > Starting cmd '%s'", command); ++ ++ let history_id = history.id; ++ let start_time = history.timestamp; ++ ++ rt.block_on(client.start_history(history)) ++ .expect("client to still work"); ++ ++ running_history.insert(command, (history_id, start_time)); ++ } ++ HandleHistoryCmd::End { ++ command, ++ when: now, ++ exit_code, ++ } => { ++ let (id, start_time) = running_history ++ .remove(command.as_str()) ++ .expect("to contain it"); ++ ++ let duration = Duration::from_nanos_u128( ++ (now - start_time).whole_nanoseconds() as u128, ++ ); ++ ++ flogf!( ++ history, ++ " < Ending cmd '%s' with exit %d and duration %d ms", ++ command, ++ exit_code, ++ duration.as_millis() as u64, ++ ); ++ ++ rt.block_on(client.end_history(id.to_string(), duration, exit_code)) ++ .expect("client to still work"); ++ } ++ HandleHistoryCmd::Load { range } => { ++ if !HANDLER_IS_SHUTTING_DOWN.load(Ordering::Relaxed) { ++ let loaded_history = { ++ let base = rt.block_on(client.history(range)); ++ base.expect("the client to still work") ++ }; ++ ++ let mut output = loaded_history_return.write().expect("not poisioned"); ++ (*output) = Some(loaded_history); ++ NEW_LOADED_HISTORY_AVAILABLE.store(true, Ordering::Relaxed); ++ ++ if LOAD_HISTORY_WAITING.load(Ordering::Relaxed) { ++ let read = LOAD_HISTORY_THREAD.read().expect("not poisioned"); ++ let t = read ++ .as_ref() ++ .expect("is some, as a thread is marked as waiting"); ++ ++ LOAD_HISTORY_WAITING.store(false, Ordering::Relaxed); ++ t.unpark(); ++ } ++ } ++ } ++ } ++ } ++ }); ++ ++ Self { ++ thread: Some(thread), ++ cmd_tx: Some(cmd_tx), ++ returned_loaded_history, ++ } ++ } ++ ++ /// This is effectively a [`Drop`] impl, but we can't use the trait directly as statics don't ++ /// get dropped. ++ fn stop(&mut self) { ++ HANDLER_IS_SHUTTING_DOWN.store(true, Ordering::Relaxed); ++ ++ let thread = self.thread.take().expect("is some"); ++ let cmd_tx = self.cmd_tx.take().expect("is some"); ++ ++ // Tell the handler, that we won't send more cmds ++ drop(cmd_tx); ++ ++ thread.join().expect("should not panic"); ++ ++ flog!(history, "History db shutdown completed."); ++ } ++ ++ fn load_history_resp(&self, should_wait: bool) -> Option<Vec<History>> { ++ let mut output = None; ++ let rx = &self.returned_loaded_history; ++ ++ if let Ok(read) = rx.try_read() { ++ read.clone_into(&mut output); ++ } ++ ++ if should_wait && output.is_none() { ++ { ++ { ++ let me = thread::current(); ++ let mut write = LOAD_HISTORY_THREAD.write().expect("not poisioned"); ++ (*write) = Some(me); ++ } ++ ++ LOAD_HISTORY_WAITING.store(true, Ordering::Relaxed); ++ while LOAD_HISTORY_WAITING.load(Ordering::Relaxed) { ++ thread::park(); ++ } ++ ++ { ++ let mut write = LOAD_HISTORY_THREAD.write().expect("not poisioned"); ++ (*write) = None; ++ } ++ } ++ ++ let read = rx.read().expect("not poisioned"); ++ read.clone_into(&mut output); ++ } ++ ++ output ++ } ++ ++ fn emit(&self, cmd: HandleHistoryCmd) { ++ flogf!(history, "Emitting handler cmd: %s ", format!("{:?}", cmd)); ++ ++ self.cmd_tx ++ .as_ref() ++ .expect("was initialized") ++ .send(cmd) ++ .expect("receiver should not have hung up"); ++ } ++} +diff --git a/src/parser.rs b/src/parser.rs +index ffbc28633..6440b7476 100644 +--- a/src/parser.rs ++++ b/src/parser.rs +@@ -437,6 +437,11 @@ pub struct Parser { + #[cfg(test)] + pub test_only_suppress_stderr: bool, + } ++impl Drop for Parser { ++ fn drop(&mut self) { ++ crate::history::turtle::handle_shutdow(); ++ } ++} + + #[derive(Copy, Clone, Default)] + pub struct ParserEnvSetMode { +diff --git a/src/reader/reader.rs b/src/reader/reader.rs +index 14fbc89c0..6848a7f1c 100644 +--- a/src/reader/reader.rs ++++ b/src/reader/reader.rs +@@ -871,6 +871,7 @@ fn read_i(parser: &mut Parser) { + L!("fish_preexec").to_owned(), + vec![command.clone()], + ); ++ crate::history::turtle::handle_preexec(reader.parser, command.clone()); + let eval_res = reader_run_command(reader.parser, &command); + signal_clear_cancel(); + if !eval_res.no_status { +@@ -884,6 +885,7 @@ fn read_i(parser: &mut Parser) { + BufferedOutputter::new(Outputter::stdoutput()).write_command(Osc133CommandFinished { + exit_status: reader.parser.last_status(), + }); ++ crate::history::turtle::handle_postexec(reader.parser, command.clone()); + event::fire_generic(reader.parser, L!("fish_postexec").to_owned(), vec![command]); + // Allow any pending history items to be returned in the history array. + reader.history.resolve_pending(); +@@ -2663,7 +2665,7 @@ fn readline( + self.clear_pager(); + } + +- if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as _ { ++ if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as u8 { + // The order of the two conditions below is important. Try to restore the mode + // in all cases, but only complain if interactive. + if let Some(old_modes) = old_modes { +-- +2.55.0 + |
