use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicU32, Ordering};
use super::FlatpakUpdate;
use crate::linux_dbus::{Conn, Msg, T_ERROR, T_RETURN, T_SIGNAL, Value};
const PORTAL: &str = "org.freedesktop.portal.Flatpak";
const PORTAL_PATH: &str = "/org/freedesktop/portal/Flatpak";
const MONITOR: &str = "org.freedesktop.portal.Flatpak.UpdateMonitor";
const CALL_MS: i32 = 20_000;
const SPAWN_LATEST_VERSION: u32 = 2;
const PS_RUNNING: u32 = 0;
const PS_EMPTY: u32 = 1;
const PS_DONE: u32 = 2;
static TOKEN: AtomicU32 = AtomicU32::new(0);
fn field<'a>(dict: Option<&'a Value>, key: &str) -> Option<&'a Value> {
let Some(Value::A(_, kv)) = dict else { return None };
kv.iter().find_map(|e| match e {
Value::E(k, v) if k.as_str() == Some(key) => Some(match &**v {
Value::V(inner) => &**inner,
other => other,
}),
_ => None,
})
}
fn text(dict: Option<&Value>, key: &str) -> String {
field(dict, key).and_then(Value::as_str).map(String::from).unwrap_or_default()
}
fn number(dict: Option<&Value>, key: &str) -> Option<u32> {
field(dict, key).and_then(Value::as_u32)
}
fn error_text(m: &Msg) -> String {
let msg = m.body.first().and_then(Value::as_str).unwrap_or("");
if msg.is_empty() { m.error.clone() } else { format!("{}: {}", m.error, msg) }
}
fn call(c: &mut Conn, m: &Msg, signal: &mut dyn FnMut(&Msg)) -> Result<Msg, String> {
let serial = c.send(m).ok_or_else(|| String::from("the session bus closed the connection"))?;
let end = crate::time_us() + CALL_MS as u64 * 1000;
loop {
let now = crate::time_us();
if now >= end {
return Err(format!("{} did not answer {}", PORTAL, m.member));
}
let Some(r) = c.read(((end - now) / 1000) as i32 + 1) else {
if c.broken {
return Err(String::from("the session bus closed the connection"));
}
continue;
};
match r.typ {
T_RETURN if r.reply_to == serial => return Ok(r),
T_ERROR if r.reply_to == serial => return Err(error_text(&r)),
T_SIGNAL => signal(&r),
_ => {}
}
}
}
pub struct FlatpakMonitor {
c: Conn,
handle: String,
seen: Option<FlatpakUpdate>,
}
impl FlatpakMonitor {
pub fn create() -> Result<FlatpakMonitor, String> {
let mut c = Conn::session().ok_or_else(|| String::from("no session bus"))?;
let token = format!("jfgui_upd{}", TOKEN.fetch_add(1, Ordering::Relaxed) + 1);
let sender = c.unique.trim_start_matches(':').replace('.', "_");
let handle = format!("{PORTAL_PATH}/update_monitor/{sender}/{token}");
let rule = format!("type='signal',interface='{MONITOR}',path='{handle}'");
c.send(&Msg::call("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "AddMatch", alloc::vec![Value::S(rule)]));
let m = Msg::call(PORTAL, PORTAL_PATH, PORTAL, "CreateUpdateMonitor", alloc::vec![Value::dict(alloc::vec![("handle_token", Value::S(token))])]);
let mut early = None;
let r = call(&mut c, &m, &mut |s| {
if s.member == "UpdateAvailable" && s.path == handle {
early = Some(update_of(s));
}
})?;
let got = r.body.first().and_then(Value::as_str).unwrap_or("");
if got != handle {
return Err(format!("CreateUpdateMonitor answered {got}, not {handle}"));
}
Ok(FlatpakMonitor { c, handle, seen: early })
}
pub fn poll(&mut self, ms: i32) -> Option<FlatpakUpdate> {
let end = crate::time_us() + ms.max(0) as u64 * 1000;
let mut got = self.seen.take();
loop {
let now = crate::time_us();
let left = if now >= end { 0 } else { ((end - now) / 1000) as i32 + 1 };
let Some(m) = self.c.read(left) else { return got };
if m.typ == T_SIGNAL && m.member == "UpdateAvailable" && m.path == self.handle {
got = Some(update_of(&m));
}
}
}
pub fn update(&mut self, progress: &mut dyn FnMut(u32, u32, u32), cancel: &dyn Fn() -> bool) -> Result<(), String> {
let handle = self.handle.clone();
let mut end_state: Option<Result<(), String>> = None;
let mut on_signal = |s: &Msg, seen: &mut Option<FlatpakUpdate>, end_state: &mut Option<Result<(), String>>| {
if s.path != handle {
return;
}
if s.member == "UpdateAvailable" {
*seen = Some(update_of(s));
return;
}
if s.member != "Progress" {
return;
}
let d = s.body.first();
let status = number(d, "status").unwrap_or(PS_RUNNING);
progress(number(d, "op").unwrap_or(0), number(d, "n_ops").unwrap_or(0), number(d, "progress").unwrap_or(0).min(100));
match status {
PS_EMPTY | PS_DONE => *end_state = Some(Ok(())),
PS_RUNNING => {}
_ => {
let msg = text(d, "error_message");
*end_state = Some(Err(if msg.is_empty() { text(d, "error") } else { msg }));
}
}
};
let m = Msg::call(PORTAL, &handle, MONITOR, "Update", alloc::vec![Value::str(""), Value::dict(Vec::new())]);
let mut seen = self.seen.take();
call(&mut self.c, &m, &mut |s| on_signal(s, &mut seen, &mut end_state))?;
loop {
if let Some(r) = end_state.take() {
self.seen = seen;
return r;
}
if cancel() {
self.seen = seen;
return Err(String::from("cancelled"));
}
match self.c.read(500) {
Some(s) if s.typ == T_SIGNAL => on_signal(&s, &mut seen, &mut end_state),
Some(_) => {}
None if self.c.broken => return Err(String::from("the session bus closed the connection")),
None => {}
}
}
}
}
impl Drop for FlatpakMonitor {
fn drop(&mut self) {
let m = Msg { flags: crate::linux_dbus::F_NO_REPLY, ..Msg::call(PORTAL, &self.handle, MONITOR, "Close", Vec::new()) };
self.c.send(&m);
}
}
fn update_of(m: &Msg) -> FlatpakUpdate {
let d = m.body.first();
FlatpakUpdate { running: text(d, "running-commit"), local: text(d, "local-commit"), remote: text(d, "remote-commit") }
}
fn bytes(s: &str) -> Value {
Value::A(String::from("y"), s.bytes().chain(core::iter::once(0)).map(Value::Y).collect())
}
pub fn spawn_latest(args: &[&str]) -> Result<u32, String> {
let mut c = Conn::session().ok_or_else(|| String::from("no session bus"))?;
let program = crate::current_exe().ok_or_else(|| String::from("no path of the program"))?;
let program = String::from(program.strip_suffix(" (deleted)").unwrap_or(&program));
let cwd = Some(crate::cwd()).filter(|c| c.starts_with('/')).unwrap_or_else(|| String::from("/"));
let argv: Vec<Value> = core::iter::once(program.as_str()).chain(args.iter().copied()).map(bytes).collect();
let m = Msg::call(PORTAL, PORTAL_PATH, PORTAL, "Spawn", alloc::vec![
bytes(&cwd),
Value::A(String::from("ay"), argv),
Value::A(String::from("{uh}"), Vec::new()),
Value::A(String::from("{ss}"), Vec::new()),
Value::U(SPAWN_LATEST_VERSION),
Value::dict(Vec::new()),
]);
let r = call(&mut c, &m, &mut |_| {})?;
r.body.first().and_then(Value::as_u32).ok_or_else(|| String::from("Spawn answered no pid"))
}