use std::time::{Duration, Instant};
use anyhow::Result;
use crossbeam_channel::Sender;
use super::{Event, Server};
use crate::layout::PaneId;
use crate::paths;
use crate::persist::{self, SavedScrollback, SessionSnapshot};
const SCROLLBACK_EVERY: Duration = Duration::from_secs(10);
const SCROLLBACK_BUDGET: Duration = Duration::from_millis(4);
impl Server {
pub(super) fn persist(&mut self) {
self.persist_dirty = false;
self.last_persist = Instant::now();
if self.workspaces.is_empty() {
return;
}
if !self.config.save_scrollback {
self.scrollback.clear();
}
let panes = &self.panes;
self.scrollback.retain(|id, _| panes.contains_key(id));
let snap = persist::snapshot(self);
let _ = self.persister.send(Persist::Save(self.session.clone(), snap));
}
pub(super) fn save_scrollback(&mut self, all: bool) {
if !self.config.save_scrollback || self.workspaces.is_empty() {
return;
}
let start = Instant::now();
let mut due: Vec<(Option<Instant>, PaneId)> = self
.panes
.values()
.filter(|p| p.is_terminal() && !p.suspended)
.filter_map(|p| {
let taken = self.scrollback.get(&p.id).map(|s| s.taken);
let changed = taken.is_none_or(|t| p.last_output > t);
let ripe = all || taken.is_none_or(|t| start >= t + SCROLLBACK_EVERY);
(changed && ripe).then_some((taken, p.id))
})
.collect();
due.sort();
for (_, id) in due {
if !all && start.elapsed() > SCROLLBACK_BUDGET {
break;
}
let taken = Instant::now();
let Some(text) = self.panes.get(&id).and_then(|p| p.scrollback_ansi()) else { continue };
let had = self.scrollback.get(&id).and_then(|s| s.file.clone());
if text.is_empty() {
self.persist_dirty |= had.is_some();
self.scrollback.insert(id, SavedScrollback { file: None, taken });
continue;
}
self.persist_dirty |= had.is_none();
let file = had.unwrap_or_else(|| format!("{}-{id}.ans", self.scrollback_tag));
let job = Persist::Scrollback { session: self.session.clone(), file: file.clone(), text };
let _ = self.persister.send(job);
self.scrollback.insert(id, SavedScrollback { file: Some(file), taken });
}
}
pub fn mark_persist(&mut self) {
self.persist_dirty = true;
}
pub(super) fn restore(&mut self) {
let interrupted = crate::runbooks::recover_interrupted(&self.session);
if !interrupted.is_empty() {
self.warnings.push(format!(
"kraal stopped while these runbooks ran: {} (their logs say how far they got)",
interrupted.join(", ")
));
}
let snap = match persist::load(&self.session) {
Ok(Some(s)) => s,
Ok(None) => return,
Err(e) => {
let path = paths::session_state_path(&self.session);
let aside = path.with_extension(format!("json.unreadable-{}", crate::accounts::now()));
let _ = std::fs::rename(&path, &aside);
self.warnings.push(format!(
"could not restore the previous session ({e:#}); kept it as {}",
paths::shorten_home(&aside)
));
return;
}
};
if let Err(e) = self.apply_snapshot(snap) {
self.warnings.push(format!("restore: {e:#}"));
}
}
pub(super) fn apply_snapshot(&mut self, snap: SessionSnapshot) -> Result<()> {
persist::apply(self, snap)
}
}
pub(super) enum Persist {
Save(String, SessionSnapshot),
Scrollback {
session: String,
file: String,
text: Vec<u8>,
},
Delete(String),
Stop(Sender<Result<(), String>>),
}
pub(super) fn spawn_persister(events: Sender<Event>) -> Sender<Persist> {
let (tx, rx) = crossbeam_channel::unbounded::<Persist>();
let _ = std::thread::Builder::new().name("persist".into()).spawn(move || {
let mut last: Result<(), String> = Ok(());
let mut scrollback_failed = false;
while let Ok(first) = rx.recv() {
let mut queue = vec![first];
queue.extend(rx.try_iter());
for (i, job) in queue.iter().enumerate() {
match job {
Persist::Save(..) if matches!(queue.get(i + 1), Some(Persist::Save(..))) => {}
Persist::Save(session, snap) => {
last = persist::save(session, snap).map_err(|e| format!("{e:#}"));
match &last {
Err(e) => {
let _ = events.send(Event::Warning(format!("saving session state: {e}")));
}
Ok(()) => persist::clean_scrollback(session, snap),
}
}
Persist::Scrollback { session, file, .. }
if queue[i + 1..].iter().take_while(|j| !matches!(j, Persist::Save(..))).any(|j| {
matches!(j, Persist::Scrollback { session: s, file: f, .. } if s == session && f == file)
}) => {}
Persist::Scrollback { session, file, text } => {
let written = persist::save_scrollback(session, file, text);
if let Err(e) = &written
&& !scrollback_failed
{
let _ = events.send(Event::Warning(format!("saving a pane's scrollback: {e:#}")));
}
scrollback_failed = written.is_err();
}
Persist::Delete(session) => {
let _ = std::fs::remove_file(paths::session_state_path(session));
let _ = persist::forget_scrollback(session);
last = Ok(());
}
Persist::Stop(done) => {
let _ = done.send(last.clone());
return;
}
}
}
}
});
tx
}