Skip to main content

browser_control/registry/
foreground.rs

1//! `foreground_holders` SQLite table: one row per tab whose foreground
2//! emulation is currently held by a `browser-control tab foreground-hold`
3//! process (see `crate::session::foreground`). Rows whose `pid` is dead are
4//! evicted lazily on read, like `bidi_locks`.
5
6use anyhow::Result;
7
8use crate::registry::{db, now_epoch_s, pid_alive, Registry};
9
10/// In-memory view of a `foreground_holders` row.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ForegroundRow {
13    pub browser_name: String,
14    pub target_id: String,
15    pub pid: u32,
16    pub started_at_epoch_s: i64,
17    /// When the holder stops on its own.
18    pub expires_at_epoch_s: i64,
19}
20
21fn row_to_fg(r: &rusqlite::Row<'_>) -> Result<ForegroundRow> {
22    Ok(ForegroundRow {
23        browser_name: r.get(0)?,
24        target_id: r.get(1)?,
25        pid: r.get::<_, i64>(2)? as u32,
26        started_at_epoch_s: r.get(3)?,
27        expires_at_epoch_s: r.get(4)?,
28    })
29}
30
31impl Registry {
32    /// Record that `pid` holds foreground emulation for a tab.
33    pub fn foreground_upsert(
34        &self,
35        browser_name: &str,
36        target_id: &str,
37        pid: u32,
38        expires_at_epoch_s: i64,
39    ) -> Result<()> {
40        db::execute(
41            &self.conn,
42            "INSERT INTO foreground_holders \
43                (browser_name, target_id, pid, started_at_epoch_s, expires_at_epoch_s) \
44             VALUES (?1, ?2, ?3, ?4, ?5) \
45             ON CONFLICT(browser_name, target_id) DO UPDATE SET \
46                pid = excluded.pid, \
47                started_at_epoch_s = excluded.started_at_epoch_s, \
48                expires_at_epoch_s = excluded.expires_at_epoch_s",
49            rusqlite::params![
50                browser_name,
51                target_id,
52                pid as i64,
53                now_epoch_s(),
54                expires_at_epoch_s
55            ],
56            || format!("upsert foreground holder {browser_name}/{target_id}"),
57        )
58    }
59
60    /// The live holder for a tab, evicting a stale row whose process died.
61    pub fn foreground_get(
62        &self,
63        browser_name: &str,
64        target_id: &str,
65    ) -> Result<Option<ForegroundRow>> {
66        let row = db::query_optional(
67            &self.conn,
68            "SELECT browser_name, target_id, pid, started_at_epoch_s, expires_at_epoch_s \
69             FROM foreground_holders WHERE browser_name = ?1 AND target_id = ?2",
70            rusqlite::params![browser_name, target_id],
71            row_to_fg,
72        )?;
73        match row {
74            Some(r) if pid_alive(r.pid) => Ok(Some(r)),
75            Some(r) => {
76                self.foreground_delete(browser_name, target_id)?;
77                let _ = r;
78                Ok(None)
79            }
80            None => Ok(None),
81        }
82    }
83
84    /// All live holders for a browser; dead rows are pruned.
85    pub fn foreground_list(&self, browser_name: &str) -> Result<Vec<ForegroundRow>> {
86        let rows = db::query_vec(
87            &self.conn,
88            "SELECT browser_name, target_id, pid, started_at_epoch_s, expires_at_epoch_s \
89             FROM foreground_holders WHERE browser_name = ?1 ORDER BY target_id",
90            [browser_name],
91            row_to_fg,
92        )?;
93        let mut live = Vec::with_capacity(rows.len());
94        for r in rows {
95            if pid_alive(r.pid) {
96                live.push(r);
97            } else {
98                self.foreground_delete(&r.browser_name, &r.target_id)?;
99            }
100        }
101        Ok(live)
102    }
103
104    /// Delete a holder row. Idempotent.
105    pub fn foreground_delete(&self, browser_name: &str, target_id: &str) -> Result<()> {
106        db::execute_bare(
107            &self.conn,
108            "DELETE FROM foreground_holders WHERE browser_name = ?1 AND target_id = ?2",
109            rusqlite::params![browser_name, target_id],
110        )
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn tmp_registry() -> (tempfile::TempDir, Registry) {
119        let td = tempfile::TempDir::new().unwrap();
120        let reg = Registry::open_at(&td.path().join("registry.db")).unwrap();
121        (td, reg)
122    }
123
124    #[test]
125    fn upsert_get_list_and_prune_dead_pid() {
126        let (_td, reg) = tmp_registry();
127        let me = std::process::id();
128        reg.foreground_upsert("brave-x", "T1", me, 0).unwrap();
129        reg.foreground_upsert("brave-x", "T2", 999_999_999, 0)
130            .unwrap();
131        let live = reg.foreground_get("brave-x", "T1").unwrap().unwrap();
132        assert_eq!(live.pid, me);
133        assert!(reg.foreground_get("brave-x", "T2").unwrap().is_none());
134        let list = reg.foreground_list("brave-x").unwrap();
135        assert_eq!(list.len(), 1);
136        assert_eq!(list[0].target_id, "T1");
137        reg.foreground_delete("brave-x", "T1").unwrap();
138        assert!(reg.foreground_list("brave-x").unwrap().is_empty());
139    }
140}