Skip to main content

qcode/ui/workspace/
freezing.rs

1//! The screen's side of freezing: when the workspace screen looks at what its profiles are doing,
2//! and what it does with what it finds.
3//!
4//! [`freeze`] says when a profile's container may be frozen and what freezing one is. This is
5//! everything around that question: the once-asked question of whether this machine can freeze at
6//! all, the minute that brings the screen back to look, the readings a look is a difference from,
7//! and the containers QCode has frozen and not woken yet.
8//!
9//! Nothing here decides on its own what a profile is doing. A look reads the machine's side of
10//! [`freeze::Signals`] on a background thread and the screen builds the rest of them when the
11//! answer comes, so what is judged is the screen as it is then rather than as it was when the
12//! reading went out. A reading that cannot be had is a no rather than a maybe: a container whose
13//! control group or whose `/proc` cannot be read is left exactly as it is, which is the whole
14//! point of the rules in [`freeze`].
15//!
16//! The lengths the rules are judged by — [`freeze::QUIET`], [`freeze::CPU_WINDOW`] and the minute
17//! between looks — and where the machine's control groups are read from are the screen's own
18//! [`Where`], so that a test may shorten the first and point the last at a folder of its own. The
19//! product uses the measured values; nothing else may.
20
21use std::collections::{HashMap, HashSet};
22use std::path::{Path, PathBuf};
23use std::time::{Duration, Instant};
24
25use qframe::prelude::*;
26use qframe::runtime::Task;
27
28use crate::engine::{Engine, EngineCommand};
29use crate::profile::Profile;
30
31use super::freeze::{self, CPU_WINDOW, Lengths, QUIET, Signals};
32use super::{Msg, OpenWorkspace, Tab, TabKey, TabKind, WorkspaceScreen, relay, shared};
33
34/// How often the screen looks at what its profiles are doing.
35///
36/// A minute is short enough that a container which grows quiet is frozen soon after the last thing
37/// in it finished, and long enough that a screen left open all day is not asking the engine and
38/// the kernel about itself hundreds of times. The rules are measured in tens of minutes, so the
39/// finest this reading has to be is whether a moment of work is behind it. It is shorter than
40/// [`CPU_WINDOW`] on purpose: the CPU is then read from a reading kept across looks (see
41/// `Freezing::spent`), so the window is judged every second look rather than never.
42pub(super) const EVERY: Duration = Duration::from_secs(60);
43
44/// What one look's reading of a container found, which is what [`Msg::Looked`] carries.
45///
46/// `None` for either field is the engine or the kernel refusing, and either way it is not a yes: a
47/// container whose control group or whose `/proc` cannot be read is not judged on what it might
48/// have been doing.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Read {
51    /// The microseconds of CPU everything in the container had used when the reading was taken.
52    pub used: Option<u64>,
53    /// The command line of every process in the container.
54    pub processes: Option<Vec<String>>,
55}
56
57/// A container's CPU as of a moment: what it had used, and when that was read. Kept per container
58/// between looks, since the reading is only a difference against the one before it.
59#[derive(Debug, Clone, Copy)]
60struct Taken {
61    used: u64,
62    at: Instant,
63}
64
65/// The lengths a look is judged by and the folder the machine's control groups are read from.
66///
67/// The product's are the measured [`freeze`] constants and the machine's own cgroup root; a test
68/// shortens the first and points the last at a folder of its own, since a test cannot wait ten
69/// minutes and cannot write to the real one.
70#[derive(Debug, Clone)]
71pub(super) struct Where {
72    /// How long every tab of a profile must have written nothing.
73    pub(super) quiet: Duration,
74    /// The shortest window the container's CPU use is read over.
75    pub(super) window: Duration,
76    /// How often the screen looks.
77    pub(super) every: Duration,
78    /// Where this machine keeps every control group, which is the only place a frozen container's
79    /// pages can be handed back from.
80    pub(super) cgroups: PathBuf,
81}
82
83impl Default for Where {
84    fn default() -> Self {
85        Self { quiet: QUIET, window: CPU_WINDOW, every: EVERY, cgroups: PathBuf::from(freeze::CGROUP_ROOT) }
86    }
87}
88
89impl From<&Where> for Lengths {
90    fn from(where_: &Where) -> Self {
91        Self { quiet: where_.quiet, window: where_.window }
92    }
93}
94
95/// What the screen knows about freezing: what this machine can do, what has been frozen, and where
96/// each container's CPU was last read.
97#[derive(Debug, Default)]
98pub(super) struct Freezing {
99    /// Whether this machine can freeze a container at all, asked once when the screen got its
100    /// engine and never again: a session does not change its engine or its swap underneath it.
101    /// `None` until the answer comes, and `None` means no timer runs.
102    pub(super) supported: Option<bool>,
103    /// The containers QCode has frozen and not woken since, by name. A container here is one the
104    /// look leaves alone and the panel shows as frozen.
105    pub(super) frozen: HashSet<String>,
106    /// The last CPU reading of each container this screen has read, and when it was taken.
107    readings: HashMap<String, Taken>,
108    /// Whether the next look is timed, so that one is timed at a time.
109    pub(super) timed: bool,
110    /// The containers the engine has been asked to wake and has not answered for yet. A second
111    /// wake of one of them is not asked: the engine refuses to unpause a container that is awake,
112    /// and that refusal, coming after the first wake's success, would have the screen call an awake
113    /// container frozen again — and hold every message for it for ever.
114    waking: HashSet<String>,
115}
116
117impl Freezing {
118    /// Whether `container` is one QCode has frozen and not woken since.
119    #[must_use]
120    pub(super) fn is_frozen(&self, container: &str) -> bool {
121        self.frozen.contains(container)
122    }
123
124    /// Notes that `container` is frozen, or that it is not.
125    ///
126    /// A freeze that failed leaves nothing behind: no container noted, and nothing said to the
127    /// person, so the next look reads the container again and tries once more. A thaw that failed
128    /// leaves the container in the set, since it is still frozen and every later thing that needs
129    /// it must ask the engine about it again.
130    pub(super) fn note(&mut self, container: &str, frozen: bool) {
131        if frozen {
132            self.frozen.insert(container.to_owned());
133        } else {
134            self.frozen.remove(container);
135        }
136    }
137
138    /// Forgets where the CPU of `container` was last read: a woken container's next reading is a
139    /// difference from the moment it woke, not from the reading taken before it was put to sleep.
140    pub(super) fn forgot(&mut self, container: &str) {
141        self.readings.remove(container);
142    }
143
144    /// The CPU of `container` as a difference against its last reading, over the window between
145    /// the two, and keeps this one. `None` for a container seen for the first time, which is not
146    /// frozen on this look: there is nothing to take a difference from yet.
147    /// Notes that the engine answered for the wake of `container`, whatever it said.
148    pub(super) fn woke(&mut self, container: &str) {
149        self.waking.remove(container);
150    }
151
152    /// Takes out of the frozen set every container the engine has just listed as anything but
153    /// paused. The panel's own Stop and Restart, a crash of the engine or the person's own `podman
154    /// unpause` all wake or end a container without the screen hearing of it, and a container the
155    /// screen still called frozen would never be looked at again.
156    pub(super) fn listed(&mut self, containers: &[crate::engine::Container]) {
157        for container in containers {
158            if !matches!(container.state, crate::engine::ContainerState::Paused) {
159                self.frozen.remove(&container.name);
160            }
161        }
162    }
163
164    /// The CPU `container` has used since the reading this one is measured from, and over how long.
165    ///
166    /// That reading is kept until it is `window` old. The screen looks more often than the window
167    /// is long, so measuring from the look before would give a window one look long on every look,
168    /// shorter than the rule asks for, and no container would ever be frozen. Kept, the reading
169    /// gives a window that grows look by look until it is long enough to be judged on, and only
170    /// then does the new reading take its place.
171    fn spent(&mut self, container: &str, used: u64, at: Instant, window: Duration) -> Option<(u64, Duration)> {
172        let since = |before: &Taken| (used.saturating_sub(before.used), at.saturating_duration_since(before.at));
173        match self.readings.get(container) {
174            Some(before) if at.saturating_duration_since(before.at) < window => Some(since(before)),
175            _ => self.readings.insert(container.to_owned(), Taken { used, at }).map(|before| since(&before)),
176        }
177    }
178}
179
180/// Forgets where the CPU of every container of the workspace at `index` was last read, which is
181/// leaving the rail: a container of a workspace that is not on the screen is not looked at again,
182/// and one that comes back is read from this moment rather than from a reading taken before its
183/// tabs were closed.
184pub(super) fn left(screen: &mut WorkspaceScreen, index: usize) {
185    let Some(workspace) = screen.workspaces.get(index) else { return };
186    let containers: Vec<String> =
187        workspace.profiles.iter().filter_map(|profile| container_of(workspace, profile)).collect();
188    for container in containers {
189        screen.freezing.forgot(&container);
190    }
191}
192
193/// Asks once, on a background thread, whether this machine can freeze a container at all, and keeps
194/// the answer: podman, running as the person who started it, and with swap. Nothing else about
195/// freezing happens without it, and no timer is run at all when it says no.
196///
197/// Runs the engine, so it belongs on a background thread.
198pub(super) fn ask(engine: Engine) -> Command<Msg> {
199    Command::perform(move || Msg::Freezable(freeze::supported(&engine)))
200}
201
202/// The next look, timed once, while there is something to look at: the person wants quiet profiles
203/// frozen, this machine can freeze one, and a workspace is open. One timer at a time, so a look
204/// that is already timed is not timed again.
205pub(super) fn again(screen: &mut WorkspaceScreen) -> Command<Msg> {
206    if !may_look(screen) || screen.freezing.timed {
207        return Command::none();
208    }
209    screen.freezing.timed = true;
210    let every = screen.where_.every;
211    Command::task(Task::new(t!("workspace.freezing.looking"), move |cx| {
212        if cx.sleep(every) { Ok(Msg::Look) } else { Err(String::new()) }
213    }))
214}
215
216/// Whether there is anything for a look to be about: the person turned freezing on, this machine can
217/// do it, and there is an engine to ask.
218#[must_use]
219pub(super) fn may_look(screen: &WorkspaceScreen) -> bool {
220    screen.freezes_idle() && screen.freezing.supported == Some(true) && screen.engine().is_some()
221}
222
223/// One look: for every open workspace, every profile of it that has a terminal tab and whose
224/// container is not frozen already is read out of its container and its `/proc`.
225///
226/// The reading is what the engine and the kernel are asked, and it goes to a background thread of
227/// its own for each profile, so a slow one holds up nothing but the profile it is about and no
228/// part of the screen waits for it. What the profile's tabs are doing is not read here: it is built
229/// when the answer comes, so what is judged is the screen as it is then.
230///
231/// A look that was timed before the person turned freezing off, or before this machine turned out
232/// not to be able to freeze anything, reads nothing when it comes round: the person is not waiting
233/// for a timer to finish before their choice takes hold, and no timer is timed again either.
234pub(super) fn look(screen: &mut WorkspaceScreen) -> Command<Msg> {
235    if !may_look(screen) {
236        return Command::none();
237    }
238    let Some(engine) = screen.engine.clone() else { return Command::none() };
239    let cgroups = screen.where_.cgroups.clone();
240    let mut commands = Vec::new();
241    for (index, workspace) in screen.workspaces.iter().enumerate() {
242        for profile in &workspace.profiles {
243            let Some(container) = container_of(workspace, profile) else { continue };
244            if screen.freezing.is_frozen(&container) {
245                continue;
246            }
247            let (id, name) = (workspace.id().to_owned(), profile.name.to_string());
248            let (engine, container) = (engine.clone(), container.clone());
249            let cgroups = cgroups.clone();
250            commands.push(Command::perform(move || {
251                Msg::Looked(index, id, name, read(&engine, &container, &cgroups), Instant::now())
252            }));
253        }
254    }
255    Command::batch(commands)
256}
257
258/// What came of a look's reading: the signals the screen builds now for the profile, the CPU it
259/// spent since the reading before it, and whether the container may be frozen.
260///
261/// A profile whose workspace has closed, or which the store no longer carries, is left alone: there
262/// is no container of it on the screen to do anything with. A reading that could not be had carries
263/// no `cpu` at all, so a container whose control group would not say is never frozen on a reading
264/// that says nothing.
265pub(super) fn looked(
266    screen: &mut WorkspaceScreen,
267    index: usize,
268    id: &str,
269    name: &str,
270    read: Read,
271    at: Instant,
272) -> Command<Msg> {
273    let Some(workspace) = screen.workspaces.get(index).filter(|workspace| workspace.id() == id) else {
274        return Command::none();
275    };
276    let Some(profile) = workspace.profiles.iter().find(|profile| profile.name.as_str() == name) else {
277        return Command::none();
278    };
279    let Some(container) = container_of(workspace, profile) else { return Command::none() };
280    let mut signals = signals(screen, index, workspace, profile);
281    let window = screen.where_.window;
282    signals.cpu = read.used.and_then(|used| screen.freezing.spent(&container, used, at, window));
283    signals.processes = read.processes;
284    if !freeze::may_freeze(&signals, profile.harness, Lengths::from(&screen.where_)) {
285        return Command::none();
286    }
287    let Some(engine) = screen.engine.clone() else { return Command::none() };
288    let cgroups = screen.where_.cgroups.clone();
289    Command::perform(move || {
290        // Paused is frozen, whatever came of the reclaim after it: a container the engine paused and
291        // the kernel took nothing back from is still one whose tabs answer nothing until it is woken,
292        // and the screen has to know so to wake it.
293        let paused = !matches!(freeze::freeze(&engine, &container, &cgroups), Err(freeze::FreezeTrouble::Engine(_)));
294        Msg::Frozen(container, paused)
295    })
296}
297
298/// Wakes the container of the tab the person has just brought into view, when QCode froze it: a
299/// paused container answers nothing to the harness in it and refuses every command the screen runs
300/// there, and the person has come back to this tab.
301///
302/// Every place the screen changes which tab is shown comes through here, so there is one answer to
303/// "the shown tab is a profile's, and that profile's container is frozen" rather than one for each
304/// way of showing a tab. A tab that is still starting is left to the engine bringing it up, which
305/// wakes a frozen container itself: asking twice would have one of the two refused, and a refusal
306/// is what a tab shows as its own failure.
307#[must_use]
308pub(super) fn shown(screen: &mut WorkspaceScreen) -> Command<Msg> {
309    let Some(tab) = screen.workspace().and_then(OpenWorkspace::active_tab) else { return Command::none() };
310    if tab.state().is_starting() {
311        return Command::none();
312    }
313    let Some(workspace) = screen.workspace() else { return Command::none() };
314    let Some(container) = container_of_tab(workspace, tab) else { return Command::none() };
315    thawing(screen, container)
316}
317
318/// The container of the tab `key` being closed, when QCode froze it, and `None` for every other
319/// tab: what the command that ends that tab's work runs in has to be awake for, since a paused
320/// container refuses it and a harness nobody is left to read goes on working.
321#[must_use]
322pub(super) fn closing(screen: &WorkspaceScreen, key: TabKey) -> Option<String> {
323    let workspace = screen.owner(key)?;
324    let tab = workspace.tabs.iter().find(|tab| tab.key() == key)?;
325    let container = container_of_tab(workspace, tab)?;
326    screen.freezing.is_frozen(&container).then_some(container)
327}
328
329/// The container of `tab`, when QCode froze it and a message is waiting in the tab to be typed into
330/// its harness, and `None` for every other case: a paused container would take the paste into a
331/// program that is not running, and a message is better a while in the tab where the person can
332/// read it. A tab with no message waiting is not a reason to wake anything.
333#[must_use]
334pub(super) fn waiting_in(screen: &WorkspaceScreen, workspace: &OpenWorkspace, tab: &Tab) -> Option<String> {
335    if tab.letters().is_empty() {
336        return None;
337    }
338    let container = container_of_tab(workspace, tab)?;
339    screen.freezing.is_frozen(&container).then_some(container)
340}
341
342/// Notes that the container of the tab `key` is up, which is what the engine's own bringing a tab
343/// up does to a container QCode froze: the screen stops calling it frozen, and the CPU it had used
344/// before it slept says nothing about what it has done since.
345pub(super) fn up(screen: &mut WorkspaceScreen, key: TabKey) {
346    let container = screen.owner(key).and_then(|workspace| {
347        let tab = workspace.tabs.iter().find(|tab| tab.key() == key)?;
348        container_of_tab(workspace, tab)
349    });
350    if let Some(container) = container {
351        screen.freezing.note(&container, false);
352        screen.freezing.forgot(&container);
353    }
354}
355
356/// The waking of `container`, which the screen has frozen, on a background thread. A container
357/// whose wake is already on its way is not asked for again.
358#[must_use]
359pub(super) fn thawing(screen: &mut WorkspaceScreen, container: String) -> Command<Msg> {
360    if !screen.freezing.is_frozen(&container) || screen.freezing.waking.contains(&container) {
361        return Command::none();
362    }
363    let Some(engine) = screen.engine.clone() else { return Command::none() };
364    screen.freezing.waking.insert(container.clone());
365    Command::perform(move || Msg::Thawed(container.clone(), awake(&engine, &container)))
366}
367
368/// Wakes `container` and answers whether it is awake now. A wake the engine refused is asked about
369/// once more, since the refusal may only mean that something else woke it first — the engine
370/// bringing a tab up, the panel's own Stop, the person's own `podman unpause` — and an awake
371/// container the screen still called frozen would hold every message for it.
372///
373/// Runs engine commands, so it belongs on a background thread.
374fn awake(engine: &Engine, container: &str) -> bool {
375    freeze::thaw(engine, container).is_ok()
376        || crate::engine::run::capture(&engine.container_state(container)).is_ok_and(|word| {
377            !matches!(crate::engine::ContainerState::parse(&word), crate::engine::ContainerState::Paused)
378        })
379}
380
381/// The waking of `container` on the screen's own account followed by the work that has to run in it,
382/// both on one background thread and in that order: a command run in a paused container is refused,
383/// and a second command would not wait for the first.
384///
385/// A container the engine would not wake refuses the second command as well, which costs nothing:
386/// there was nothing running in it to end, and the next thing that needs it asks again.
387pub(super) fn thawing_then(screen: &mut WorkspaceScreen, container: String, then: EngineCommand) -> Command<Msg> {
388    let Some(engine) = screen.engine.clone() else { return Command::none() };
389    screen.freezing.waking.insert(container.clone());
390    Command::perform(move || {
391        let awake = awake(&engine, &container);
392        let _ = crate::engine::run::capture(&then);
393        Msg::Thawed(container, awake)
394    })
395}
396
397/// The engine commands that wake every container the screen has frozen, one name each and in an
398/// order of their own, for the moment QCode is going.
399///
400/// A container left paused is one the next QCode finds asleep with every tab in it still holding
401/// what it held, and one the reaper cannot stop, since a stop of a paused container is refused.
402/// QCode waits for the engine over these, since there is no later to run them in.
403#[must_use]
404pub fn waking_on_the_way_out(screen: &WorkspaceScreen) -> Vec<EngineCommand> {
405    let Some(engine) = screen.engine.as_ref() else { return Vec::new() };
406    let mut names: Vec<&str> = screen.freezing.frozen.iter().map(String::as_str).collect();
407    names.sort_unstable();
408    names.into_iter().map(|name| engine.unpause_container(name)).collect()
409}
410
411/// Builds what the screen knows about `profile` of `workspace`, which is every rule but the two that
412/// are a difference over time and the two the machine is asked about.
413///
414/// A profile that opens a window is never frozen: its window is a container of its own and the
415/// person is looking at it. A profile with no terminal tab is not judged either, since
416/// [`Signals::quiet_for`] is `None` without one, which is the rules' own answer.
417fn signals(screen: &WorkspaceScreen, index: usize, workspace: &OpenWorkspace, profile: &Profile) -> Signals {
418    let now = Instant::now();
419    let tabs: Vec<&Tab> = workspace.tabs.iter().filter(|tab| owns(tab, profile)).collect();
420    let quiet_for = tabs
421        .iter()
422        .filter_map(|tab| tab.session())
423        .map(|session| now.saturating_duration_since(session.last_output()))
424        .min();
425    let (relay_busy, relay_quiet_for) = relay_of(workspace, profile, now);
426    Signals {
427        // Only the tab the person is looking at is on screen, and only the open workspace has one:
428        // a tab of a workspace behind another is not on the screen, and a profile of the same name in
429        // another workspace is a container of its own that nothing of this one can be on the screen
430        // of.
431        on_screen: index == screen.active
432            && screen.workspace().and_then(OpenWorkspace::active_tab).is_some_and(|tab| owns(tab, profile)),
433        // A line the person has not sent, a message being pasted, or a message waiting to be
434        // delivered: each is work in a tab of this profile whatever its container is doing.
435        typing: tabs.iter().any(|tab| tab.person_typed() || tab.owed().is_some() || !tab.letters().is_empty()),
436        quiet_for,
437        relay_busy,
438        relay_quiet_for,
439        cpu: None,
440        processes: None,
441        desktop: profile.harness.desktop().is_some(),
442    }
443}
444
445/// The profile whose container `tab` runs in, when it runs in one: a harness in a terminal, or the
446/// administrator's shell, which is a terminal in the same container. A window's tab is not one — it
447/// has a container of its own, which is never frozen — and neither is a file, a shell or a blank
448/// tab.
449#[must_use]
450fn of(tab: &Tab) -> Option<&str> {
451    match tab.kind() {
452        TabKind::Profile(name) | TabKind::Admin(name) => Some(name.as_str()),
453        _ => None,
454    }
455}
456
457/// Whether `tab` is a terminal tab of `profile`, which is what a profile's container is entered
458/// with.
459#[must_use]
460fn owns(tab: &Tab, profile: &Profile) -> bool {
461    of(tab).is_some_and(|name| name == profile.name.as_str())
462}
463
464/// The container `profile`'s terminal tabs of `workspace` run in, and `None` when it has none: a
465/// profile nobody opened a terminal tab of has no container to read, and a window's own container is
466/// a thing of its own that is never frozen.
467#[must_use]
468fn container_of(workspace: &OpenWorkspace, profile: &Profile) -> Option<String> {
469    workspace
470        .tabs
471        .iter()
472        .any(|tab| owns(tab, profile))
473        .then(|| crate::engine::names::profile_container(workspace.id(), profile.name.as_str()))
474}
475
476/// The container `tab` runs in, and `None` for a tab that runs in none or in a window's own.
477#[must_use]
478fn container_of_tab(workspace: &OpenWorkspace, tab: &Tab) -> Option<String> {
479    let name = of(tab)?;
480    let profile = workspace.profiles.iter().find(|profile| profile.name.as_str() == name)?;
481    container_of(workspace, profile)
482}
483
484/// What the relay is doing for `profile`'s tokens, and how long ago the last request of one of them
485/// finished.
486///
487/// The tokens are each tab's own, and, for opencode's shared server, the server's own token as
488/// well: a request of a shared server is carried by the server's token, not by the tab that asked
489/// for it. A profile that does not run on a provider carries `None` for both, which skips the
490/// relay's rules rather than failing them, and so does one whose relay is not listening, since
491/// nothing of it can be on its way through a relay that is not there.
492fn relay_of(workspace: &OpenWorkspace, profile: &Profile, now: Instant) -> (Option<bool>, Option<Duration>) {
493    if profile.provider.is_none() {
494        return (None, None);
495    }
496    let relay::Link::On { activity, .. } = &workspace.relay else { return (None, None) };
497    let mut tokens: Vec<String> =
498        workspace.tabs.iter().filter(|tab| owns(tab, profile)).map(|tab| tab.token().to_owned()).collect();
499    if shared::shares(profile) {
500        tokens.push(workspace.servers.token(profile.name.as_str()));
501    }
502    let busy = tokens.iter().any(|token| activity.busy(token));
503    let quiet_for = tokens
504        .iter()
505        .filter_map(|token| activity.last_finished(token))
506        .map(|at| now.saturating_duration_since(at))
507        .min();
508    (Some(busy), quiet_for)
509}
510
511/// The microseconds of CPU everything in `container` has used, read out of the control group the
512/// engine says it has under `cgroups`, and the command line of every process in it. Either may be
513/// missing when the engine or the kernel would not say, and either way it is not a yes.
514fn read(engine: &Engine, container: &str, cgroups: &Path) -> Read {
515    Read {
516        used: freeze::cgroup(engine, container, cgroups).as_deref().and_then(freeze::cpu_used),
517        processes: freeze::processes(engine, container),
518    }
519}