Skip to content

Commit 588f46e

Browse files
committed
feat(background): read running apps from /proc cgroups instead of systemd
1 parent b3eacb0 commit 588f46e

4 files changed

Lines changed: 144 additions & 205 deletions

File tree

src/background.rs

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ use zbus::{fdo, object_server::SignalEmitter, zvariant};
1515
use crate::{
1616
PortalResponse, Request,
1717
app::CosmicPortal,
18+
cgroup,
1819
config::{self, background::PermissionDialog},
19-
fl, subscription, systemd,
20+
fl, subscription,
2021
wayland::WaylandHelper,
2122
};
2223

@@ -67,11 +68,8 @@ impl Background {
6768
#[zbus::interface(name = "org.freedesktop.impl.portal.Background")]
6869
impl Background {
6970
/// Status on running apps (active, running, or background)
70-
async fn get_app_state(
71-
&self,
72-
#[zbus(connection)] connection: &zbus::Connection,
73-
) -> HashMap<String, AppStatus> {
74-
get_app_state_impl(connection, self.wayland_helper.clone())
71+
async fn get_app_state(&self) -> HashMap<String, AppStatus> {
72+
get_app_state_impl(self.wayland_helper.clone())
7573
.await
7674
.inspect_err(|_| log::error!("Failed to enumerate running apps"))
7775
.unwrap_or_default()
@@ -260,22 +258,16 @@ impl Background {
260258

261259
/// Internal implementation of [`Background::get_app_state`].
262260
async fn get_app_state_impl(
263-
connection: &zbus::Connection,
264261
wayland_helper: WaylandHelper,
265262
) -> fdo::Result<HashMap<String, AppStatus>> {
266-
let apps: HashMap<_, _> = systemd::Systemd1Proxy::new(connection)
263+
let apps: HashMap<_, _> = cgroup::running_app_ids()
267264
.await
268-
.inspect_err(|e| log::error!("Error connecting to systemd proxy: {e}"))?
269-
.list_units()
270-
.await
271-
.inspect_err(|e| log::error!("Error fetching units from systemd: {e}"))?
265+
.inspect_err(|e| log::error!("Error reading running apps from /proc: {e}"))
266+
.map_err(|e| fdo::Error::IOError(e.to_string()))?
272267
.into_iter()
273268
// Apps launched by COSMIC/Flatpak are considered to be running in the
274269
// background by default as they don't have open top levels.
275-
.filter_map(|unit| {
276-
unit.cosmic_flatpak_name()
277-
.map(|app_id| (app_id.to_owned(), AppStatus::Background))
278-
})
270+
.map(|app_id| (app_id, AppStatus::Background))
279271
.chain(
280272
wayland_helper
281273
.toplevels()

src/cgroup.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
3+
use std::{collections::HashSet, io};
4+
5+
use tokio::fs;
6+
7+
static COSMIC_SCOPE: &str = "app-cosmic-";
8+
static FLATPAK_SCOPE: &str = "app-flatpak-";
9+
10+
/// Returns appid if COSMIC or Flatpak launched the process in this cgroup scope.
11+
///
12+
/// COSMIC and Flatpak place launched apps in a cgroup scope named
13+
/// `app-cosmic-{appid}-{PID}.scope` / `app-flatpak-{appid}-{PID}.scope`.
14+
fn cosmic_flatpak_name(scope: &str) -> Option<&str> {
15+
scope
16+
.strip_prefix(COSMIC_SCOPE)
17+
.or_else(|| scope.strip_prefix(FLATPAK_SCOPE))?
18+
.rsplit_once('-')
19+
.and_then(|(appid, pid_scope)| {
20+
// Check if scope ends in `-{PID}.scope`
21+
_ = pid_scope.strip_suffix(".scope")?.parse::<u32>().ok()?;
22+
Some(appid)
23+
})
24+
}
25+
26+
/// Extract the app id from the contents of a `/proc/{PID}/cgroup` file.
27+
fn app_id_from_cgroup(contents: &str) -> Option<&str> {
28+
contents.lines().find_map(|line| {
29+
// Each line is `hierarchy-ID:controller-list:cgroup-path`. An app's
30+
// processes can live in a child cgroup of its scope, so scan every path
31+
// component for the `app-cosmic-{appid}-{PID}.scope` scope rather than
32+
// assuming it's the leaf.
33+
let path = line.rsplit_once(':').map(|(_, path)| path)?;
34+
path.split('/').find_map(cosmic_flatpak_name)
35+
})
36+
}
37+
38+
/// Enumerate the appids of COSMIC/Flatpak apps that are running, by reading each
39+
/// process's cgroup from `/proc/{PID}/cgroup`.
40+
///
41+
/// This reads the process cgroups directly instead of querying systemd over
42+
/// D-Bus, so it also works on Linux distributions that aren't using systemd.
43+
pub async fn running_app_ids() -> io::Result<HashSet<String>> {
44+
let mut proc = fs::read_dir("/proc").await?;
45+
let mut app_ids = HashSet::new();
46+
47+
while let Some(entry) = proc.next_entry().await? {
48+
// Process directories are named after their PID.
49+
if entry
50+
.file_name()
51+
.to_str()
52+
.is_none_or(|name| name.parse::<u32>().is_err())
53+
{
54+
continue;
55+
}
56+
57+
let cgroup = entry.path().join("cgroup");
58+
match fs::read_to_string(&cgroup).await {
59+
Ok(contents) => {
60+
if let Some(app_id) = app_id_from_cgroup(&contents) {
61+
app_ids.insert(app_id.to_owned());
62+
}
63+
}
64+
// Processes come and go while we iterate; ignore ones that vanished.
65+
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
66+
Err(e) => log::trace!("Skipping {}: {e}", cgroup.display()),
67+
}
68+
}
69+
70+
Ok(app_ids)
71+
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use super::{app_id_from_cgroup, cosmic_flatpak_name};
76+
77+
const APPID: &str = "com.system76.CosmicFiles";
78+
79+
#[test]
80+
fn parse_appid_without_scope_fails() {
81+
let name = cosmic_flatpak_name(APPID);
82+
assert!(
83+
name.is_none(),
84+
"Only apps launched by COSMIC or Flatpak should be parsed; got: {name:?}"
85+
);
86+
}
87+
88+
#[test]
89+
fn parse_appid_with_scope_pid() {
90+
let scope = format!("app-cosmic-{APPID}-1234.scope");
91+
let name = cosmic_flatpak_name(&scope).expect("Should parse app launched by COSMIC");
92+
assert_eq!(APPID, name);
93+
}
94+
95+
#[test]
96+
fn parse_appid_with_scope_no_pid_fails() {
97+
let scope = format!("app-cosmic-{APPID}.scope");
98+
let name = cosmic_flatpak_name(&scope);
99+
assert!(
100+
name.is_none(),
101+
"Apps launched by COSMIC/Flatpak should have a PID in its scope name"
102+
);
103+
}
104+
105+
#[test]
106+
fn parse_appid_from_cgroup_v2() {
107+
let contents = format!(
108+
"0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-cosmic-{APPID}-1234.scope\n"
109+
);
110+
assert_eq!(Some(APPID), app_id_from_cgroup(&contents));
111+
}
112+
113+
#[test]
114+
fn parse_appid_from_cgroup_flatpak() {
115+
let contents = format!(
116+
"0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-flatpak-{APPID}-99.scope\n"
117+
);
118+
assert_eq!(Some(APPID), app_id_from_cgroup(&contents));
119+
}
120+
121+
#[test]
122+
fn parse_appid_from_nested_cgroup() {
123+
// A process sitting in a child cgroup beneath its app scope.
124+
let contents = format!(
125+
"0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-cosmic-{APPID}-1234.scope/tab-7\n"
126+
);
127+
assert_eq!(Some(APPID), app_id_from_cgroup(&contents));
128+
}
129+
130+
#[test]
131+
fn parse_appid_from_unrelated_cgroup_fails() {
132+
let contents = "0::/user.slice/user-1000.slice/session.slice/foo.service\n";
133+
assert!(app_id_from_cgroup(contents).is_none());
134+
}
135+
}

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod access;
1111
mod app;
1212
mod background;
1313
mod buffer;
14+
mod cgroup;
1415
mod documents;
1516
mod file_chooser;
1617
mod localize;
@@ -19,7 +20,6 @@ mod screencast_dialog;
1920
mod screencast_thread;
2021
mod screenshot;
2122
mod subscription;
22-
mod systemd;
2323
mod wayland;
2424
mod widget;
2525

src/systemd.rs

Lines changed: 0 additions & 188 deletions
This file was deleted.

0 commit comments

Comments
 (0)