Skip to content

Commit

Permalink
Evaluate background apps using systemd
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuamegnauth54 committed Oct 13, 2024
1 parent bcc91df commit 2d371c1
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 21 deletions.
2 changes: 1 addition & 1 deletion examples/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl cosmic::Application for App {
})
.width(Length::Fill)
.into(),
widget::button("Run in background")
widget::button::standard("Run in background")
.on_press(Message::RequestBackground)
.padding(8.0)
.into(),
Expand Down
70 changes: 50 additions & 20 deletions src/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

use std::{
borrow::Cow,
collections::HashSet,
hash::{Hash, Hasher},
io,
path::Path,
sync::{Arc, Condvar, Mutex},
Expand All @@ -21,7 +23,7 @@ use zbus::{fdo, object_server::SignalContext, zvariant};
use crate::{
app::CosmicPortal,
config::{self, background::PermissionDialog},
fl, subscription,
fl, subscription, systemd,
wayland::WaylandHelper,
PortalResponse,
};
Expand Down Expand Up @@ -92,46 +94,62 @@ impl Background {
.await?;

file.write_all(desktop_entry.to_string().as_bytes()).await?;
/// Shouldn't be needed, but the file never seemed to flush to disk until I did it manually
// Shouldn't be needed, but the file never seemed to flush to disk until I did it manually
file.flush().await
}
}

#[zbus::interface(name = "org.freedesktop.impl.portal.Background")]
impl Background {
/// Status on running apps (active, running, or background)
async fn get_app_state(&self) -> fdo::Result<AppStates> {
let apps: Vec<_> = self
async fn get_app_state(
&self,
#[zbus(connection)] connection: &zbus::Connection,
) -> fdo::Result<AppStates> {
let apps: HashSet<_> = self
.wayland_helper
.toplevels()
.into_iter()
// Evaluate apps with top levels first as our initial state
.map(|(_, info)| {
let status = if info
.state
.contains(&zcosmic_toplevel_handle_v1::State::Activated)
{
AppStatus::Active
} else if !info.state.is_empty() {
AppStatus::Running
} else {
// xxx Is this the correct way to determine if a program is running in the
// background? If a toplevel exists but isn't running, activated, et cetera,
// then it logically must be in the background (?)
AppStatus::Background
AppStatus::Running
};

AppState {
app_id: info.app_id,
status,
}
})
.chain(
systemd::Systemd1Proxy::new(connection)
.await?
.list_units()
.await?
.into_iter()
// Apps launched by COSMIC/Flatpak are considered to be running in the
// background by default as they don't have open top levels
.filter_map(|unit| {
unit.cosmic_flatpak_name().map(|app_id| AppState {
app_id: app_id.to_owned(),
status: AppStatus::Background,
})
}),
)
.collect();

log::debug!("GetAppState returning {} toplevels", apps.len());
log::debug!("GetAppState is returning {} open apps", apps.len());
#[cfg(debug_assertions)]
log::trace!("App status: {apps:#?}");
log::trace!("App statuses: {apps:#?}");

Ok(AppStates { apps })
Ok(AppStates {
apps: apps.into_iter().collect(),
})
}

/// Notifies the user that an app is running in the background
Expand Down Expand Up @@ -192,7 +210,7 @@ impl Background {
///
/// Deprecated in terms of the portal but seemingly still in use
/// Spec: https://specifications.freedesktop.org/autostart-spec/latest/
pub async fn enable_autostart(
async fn enable_autostart(
&self,
appid: String,
enable: bool,
Expand Down Expand Up @@ -269,8 +287,8 @@ impl Background {
log::debug!("{appid} sanitized autostart command line: {exec}");
autostart_fde.add_desktop_entry("Exec", &exec);

/// xxx Replace with enumflags later when it's added as a dependency instead of adding
/// it now for one bit (literally)
// xxx Replace with enumflags later when it's added as a dependency instead of adding
// it now for one bit (literally)
let dbus_activation = flags & 0x1 == 1;
if dbus_activation {
autostart_fde.add_desktop_entry("DBusActivatable", "true");
Expand All @@ -297,9 +315,9 @@ impl Background {
pub async fn running_applications_changed(context: &SignalContext<'_>) -> zbus::Result<()>;
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, zvariant::Type)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, zvariant::Type)]
#[zvariant(signature = "u")]
pub enum AppStatus {
enum AppStatus {
/// No open windows
Background = 0,
/// At least one opened window
Expand All @@ -314,17 +332,29 @@ struct AppStates {
apps: Vec<AppState>,
}

#[derive(Clone, Debug, zvariant::SerializeDict, zvariant::Type)]
#[derive(Clone, Debug, Eq, zvariant::SerializeDict, zvariant::Type)]
#[zvariant(signature = "{sv}")]
struct AppState {
app_id: String,
status: AppStatus,
}

impl Hash for AppState {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write(self.app_id.as_bytes());
}
}

impl PartialEq for AppState {
fn eq(&self, other: &Self) -> bool {
self.app_id == other.app_id
}
}

/// Result vardict for [`Background::notify_background`]
#[derive(Clone, Debug, zvariant::SerializeDict, zvariant::Type)]
#[zvariant(signature = "a{sv}")]
pub struct NotifyBackgroundResult {
struct NotifyBackgroundResult {
result: PermissionResponse,
}

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod screencast_dialog;
mod screencast_thread;
mod screenshot;
mod subscription;
mod systemd;
mod wayland;
mod widget;

Expand Down
188 changes: 188 additions & 0 deletions src/systemd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// SPDX-License-Identifier: GPL-3.0-only

use serde::Deserialize;
use zbus::{zvariant, Result};

static COSMIC_SCOPE: &str = "app-cosmic-";
static FLATPAK_SCOPE: &str = "app-flatpak-";

/// Proxy for the `org.freedesktop.systemd1.Manager` interface
#[zbus::proxy(
default_service = "org.freedesktop.systemd1",
default_path = "/org/freedesktop/systemd1",
interface = "org.freedesktop.systemd1.Manager"
)]
trait Systemd1 {
fn list_units(&self) -> Result<Vec<Unit>>;
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, zvariant::Type)]
#[cfg_attr(test, derive(Default))]
#[zvariant(signature = "(ssssssouso)")]
pub struct Unit {
pub name: String,
pub description: String,
pub load_state: LoadState,
pub active_state: ActiveState,
pub sub_state: String,
pub following: String,
pub unit_object: zvariant::OwnedObjectPath,
pub job_id: u32,
pub job_type: String,
pub job_object: zvariant::OwnedObjectPath,
}

impl Unit {
/// Returns appid if COSMIC or Flatpak launched this unit
pub fn cosmic_flatpak_name(&self) -> Option<&str> {
self.name
.strip_prefix(COSMIC_SCOPE)
.or_else(|| self.name.strip_prefix(FLATPAK_SCOPE))?
.rsplit_once('-')
.and_then(|(appid, pid_scope)| {
// Check if unit name ends in `-{PID}.scope`
_ = pid_scope.strip_suffix(".scope")?.parse::<u32>().ok()?;
Some(appid)
})
}
}

/// Load state for systemd units
///
/// Source: https://github.com/systemd/systemd/blob/main/man/systemctl.xml
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, zvariant::Type)]
#[cfg_attr(test, derive(Default))]
#[zvariant(signature = "s")]
#[serde(rename_all = "kebab-case")]
pub enum LoadState {
#[cfg_attr(test, default)]
Stub,
Loaded,
NotFound,
BadSetting,
Error,
Merged,
Masked,
}

/// Sub-state for systemd units
///
/// Source: https://github.com/systemd/systemd/blob/main/man/systemctl.xml
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, zvariant::Type)]
#[cfg_attr(test, derive(Default))]
#[zvariant(signature = "s")]
#[serde(rename_all = "kebab-case")]
pub enum SubState {
#[cfg_attr(test, default)]
Dead,
Active,
Waiting,
Running,
Failed,
Cleaning,
Tentative,
Plugged,
Mounting,
MountingDone,
Mounted,
Remounting,
Unmounting,
RemountingSigterm,
RemountingSigkill,
UnmountingSigterm,
UnmountingSigkill,
Stop,
StopWatchdog,
StopSigterm,
StopSigkill,
StartChown,
Abandoned,
Condition,
Start,
StartPre,
StartPost,
StopPre,
StopPreSigterm,
StopPreSigkill,
StopPost,
Exited,
Reload,
ReloadSignal,
ReloadNotify,
FinalWatchdog,
FinalSigterm,
FinalSigkill,
DeadBeforeAutoRestart,
FailedBeforeAutoRestart,
DeadResourcesPinned,
AutoRestart,
AutoRestartQueued,
Listening,
Activating,
ActivatingDone,
Deactivating,
DeactivatingSigterm,
DeactivatingSigkill,
Elapsed,
}

/// Activated state for systemd units
///
/// Source: https://github.com/systemd/systemd/blob/main/man/systemctl.xml
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, zvariant::Type)]
#[cfg_attr(test, derive(Default))]
#[zvariant(signature = "s")]
#[serde(rename_all = "kebab-case")]
pub enum ActiveState {
Active,
Reloading,
#[cfg_attr(test, default)]
Inactive,
Failed,
Activating,
Deactivating,
Maintenance,
}

#[cfg(test)]
mod tests {
use super::Unit;

const APPID: &str = "com.system76.CosmicFiles";

fn unit_with_name(name: &str) -> Unit {
Unit {
name: name.to_owned(),
..Default::default()
}
}

#[test]
fn parse_appid_without_scope_fails() {
let unit = unit_with_name(APPID);
let name = unit.cosmic_flatpak_name();
assert!(
name.is_none(),
"Only apps launched by COSMIC or Flatpak should be parsed; got: {name:?}"
);
}

#[test]
fn parse_appid_with_scope_pid() {
let unit = unit_with_name(&format!("app-cosmic-{APPID}-1234.scope"));
let name = unit
.cosmic_flatpak_name()
.expect("Should parse app launched by COSMIC");
assert_eq!(APPID, name);
}

#[test]
fn parse_appid_with_scope_no_pid_fails() {
let unit = unit_with_name(&format!("app-cosmic-{APPID}.scope"));
let name = unit.cosmic_flatpak_name();
assert!(
name.is_none(),
"Apps launched by COSMIC/Flatpak should have a PID in its scope name"
);
}
}

0 comments on commit 2d371c1

Please sign in to comment.