Implement native inhibition and acknowledged session-ending confirmation
This commit is contained in:
1 parent
cc441c049e
commit
29a71e9974
27 files changed
+1915
-142
No files matched your search
@@ -0,0 +1,221 @@
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
os::unix::net::UnixStream,
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub(super) fn call(command: &str) -> Result<Value, String> {
|
||||
request(command, json!({}))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SessionConnection {
|
||||
stream: std::sync::Mutex<UnixStream>,
|
||||
}
|
||||
|
||||
impl SessionConnection {
|
||||
fn connect() -> Result<Self, String> {
|
||||
let path = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.ok_or("Missing runtime directory")?
|
||||
.join("ferese/control.sock");
|
||||
let stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(Self {
|
||||
stream: std::sync::Mutex::new(stream),
|
||||
})
|
||||
}
|
||||
|
||||
fn request(&self, command: &str, args: Value) -> Result<Value, String> {
|
||||
let mut stream = self
|
||||
.stream
|
||||
.lock()
|
||||
.map_err(|_| "Compositor connection failed")?;
|
||||
let request = ferese_ipc::Request {
|
||||
version: ferese_ipc::VERSION,
|
||||
id: 1,
|
||||
kind: "command".into(),
|
||||
command: command.into(),
|
||||
args,
|
||||
};
|
||||
ferese_ipc::write_frame(&mut *stream, &request).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response =
|
||||
ferese_ipc::read_frame(&mut *stream).map_err(|error| error.to_string())?;
|
||||
if let Some(error) = response.error {
|
||||
return Err(error.message);
|
||||
}
|
||||
response.result.ok_or("Missing compositor response".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn request(command: &str, args: Value) -> Result<Value, String> {
|
||||
SessionConnection::connect()?.request(command, args)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||
pub(super) enum Mode {
|
||||
#[default]
|
||||
Guide,
|
||||
Logout,
|
||||
Shutdown,
|
||||
Suspend,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(super) struct Approval {
|
||||
pub reasons: Vec<String>,
|
||||
pub revision: u32,
|
||||
pub token: u32,
|
||||
mode: Mode,
|
||||
external: Vec<String>,
|
||||
connection: Option<std::sync::Arc<SessionConnection>>,
|
||||
}
|
||||
|
||||
impl Approval {
|
||||
pub fn force(&self) -> bool {
|
||||
!self.reasons.is_empty()
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
if self.mode == Mode::Shutdown {
|
||||
let _ = request("cancel-session-end", json!({"token": self.token}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare(&self) -> Result<(), String> {
|
||||
if matches!(self.mode, Mode::Shutdown | Mode::Suspend)
|
||||
&& login_inhibitors(self.mode)? != self.external
|
||||
{
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
}
|
||||
if self.mode == Mode::Shutdown {
|
||||
self.connection.as_ref().ok_or("Missing session-ending lease")?.request(
|
||||
"validate-session-end",
|
||||
json!({"token": self.token, "inhibitor-revision": self.revision, "force": self.force()}),
|
||||
)?;
|
||||
} else if self.mode == Mode::Suspend {
|
||||
let state = call("get-session-state")?;
|
||||
if state["inhibitor-revision"].as_u64() != Some(u64::from(self.revision)) {
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn login_inhibitors(mode: Mode) -> Result<Vec<String>, String> {
|
||||
if !matches!(mode, Mode::Shutdown | Mode::Suspend) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let connection = zbus::blocking::connection::Builder::system()
|
||||
.map_err(|error| error.to_string())?
|
||||
.method_timeout(Duration::from_secs(2))
|
||||
.build()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let proxy = zbus::blocking::Proxy::new(
|
||||
&connection,
|
||||
"org.freedesktop.login1",
|
||||
"/org/freedesktop/login1",
|
||||
"org.freedesktop.login1.Manager",
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let inhibitors: Vec<(String, String, String, String, u32, u32)> = proxy
|
||||
.call("ListInhibitors", &())
|
||||
.map_err(|error| error.to_string())?;
|
||||
let what = if mode == Mode::Suspend {
|
||||
"sleep"
|
||||
} else {
|
||||
"shutdown"
|
||||
};
|
||||
let mut reasons = inhibitors
|
||||
.into_iter()
|
||||
.filter(|(flags, _, _, inhibition, _, _)| {
|
||||
inhibition == "block" && flags.split(':').any(|flag| flag == what)
|
||||
})
|
||||
.map(|(_, app, reason, _, _, _)| format!("{app}: {reason}"))
|
||||
.collect::<Vec<_>>();
|
||||
reasons.sort();
|
||||
reasons.dedup();
|
||||
Ok(reasons)
|
||||
}
|
||||
|
||||
pub(super) fn inhibitors(mode: Mode) -> Result<Approval, String> {
|
||||
let connection = if mode == Mode::Shutdown {
|
||||
Some(std::sync::Arc::new(SessionConnection::connect()?))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut state = match &connection {
|
||||
Some(connection) => connection.request("begin-session-end", json!({}))?,
|
||||
None => call("get-session-state")?,
|
||||
};
|
||||
let token = state["query-token"]
|
||||
.as_u64()
|
||||
.and_then(|token| u32::try_from(token).ok())
|
||||
.unwrap_or(0);
|
||||
let result = (|| {
|
||||
if matches!(mode, Mode::Logout | Mode::Shutdown) {
|
||||
let deadline = Instant::now() + Duration::from_millis(1500);
|
||||
while state["query-ready"] != true {
|
||||
if state["session-state"] != 2
|
||||
|| state["query-token"].as_u64() != Some(u64::from(token))
|
||||
|| Instant::now() >= deadline
|
||||
{
|
||||
return Err("Session-ending request was cancelled or timed out".into());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
state = match &connection {
|
||||
Some(connection) => connection.request("get-session-state", json!({}))?,
|
||||
None => call("get-session-state")?,
|
||||
};
|
||||
}
|
||||
}
|
||||
let flags = if mode == Mode::Suspend { 4 } else { 1 };
|
||||
let entries = state["inhibitors"]
|
||||
.as_array()
|
||||
.ok_or("Invalid inhibitor list")?;
|
||||
let mut reasons = entries
|
||||
.iter()
|
||||
.filter(|entry| entry["flags"].as_u64().unwrap_or(0) & flags != 0)
|
||||
.map(|entry| {
|
||||
let app = entry["app"]
|
||||
.as_str()
|
||||
.filter(|app| !app.is_empty())
|
||||
.unwrap_or("Application");
|
||||
let reason = entry["reason"]
|
||||
.as_str()
|
||||
.unwrap_or("Requested session inhibition");
|
||||
format!("{app}: {reason}")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let external = login_inhibitors(mode)?;
|
||||
reasons.extend(external.iter().cloned());
|
||||
reasons.sort();
|
||||
reasons.dedup();
|
||||
Ok(Approval {
|
||||
reasons,
|
||||
revision: state["inhibitor-revision"]
|
||||
.as_u64()
|
||||
.and_then(|revision| u32::try_from(revision).ok())
|
||||
.ok_or("Missing inhibitor revision")?,
|
||||
token,
|
||||
mode,
|
||||
external,
|
||||
connection,
|
||||
})
|
||||
})();
|
||||
if result.is_err() && mode == Mode::Shutdown {
|
||||
let _ = request("cancel-session-end", json!({"token": token}));
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -81,7 +81,7 @@ impl ShellControl {
|
||||
let connection = control_connection()?;
|
||||
let (globals, mut queue) = registry_queue_init::<ControlState>(&connection)?;
|
||||
let qh = queue.handle();
|
||||
let manager = globals.bind::<FereseShellManagerV1, _, _>(&qh, 1..=3, ())?;
|
||||
let manager = globals.bind::<FereseShellManagerV1, _, _>(&qh, 4..=4, ())?;
|
||||
let shell = manager.get_shell(&qh, ());
|
||||
let (sender, updates) = mpsc::channel();
|
||||
let mut state = ControlState::new(sender);
|
||||
@@ -150,11 +150,10 @@ impl ShellControl {
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
|
||||
pub(crate) fn confirm_logout(&self, serial: u32) {
|
||||
if self.shell.version() >= 3 {
|
||||
self.shell.confirm_logout(serial);
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
pub(crate) fn confirm_logout(&self, serial: u32, revision: u32, token: u32, force: bool) {
|
||||
self.shell
|
||||
.confirm_logout_with_inhibitors(serial, revision, token, u32::from(force));
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_logout(&self, serial: u32) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::{os::unix::net::UnixStream, path::PathBuf, time::Duration};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
|
||||
pub(super) struct Entry {
|
||||
pub keys: String,
|
||||
@@ -7,32 +5,9 @@ pub(super) struct Entry {
|
||||
}
|
||||
|
||||
pub(super) fn load() -> Result<Vec<Entry>, String> {
|
||||
let path = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.ok_or("Missing runtime directory")?
|
||||
.join("ferese/control.sock");
|
||||
let mut stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| error.to_string())?;
|
||||
let request = ferese_ipc::Request {
|
||||
version: ferese_ipc::VERSION,
|
||||
id: 1,
|
||||
kind: "command".into(),
|
||||
command: "get-keybindings".into(),
|
||||
args: serde_json::json!({}),
|
||||
};
|
||||
ferese_ipc::write_frame(&mut stream, &request).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response =
|
||||
ferese_ipc::read_frame(&mut stream).map_err(|error| error.to_string())?;
|
||||
if let Some(error) = response.error {
|
||||
return Err(error.message);
|
||||
}
|
||||
let entries: Vec<Entry> = serde_json::from_value(response.result.ok_or("Missing keybindings")?)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let entries: Vec<Entry> =
|
||||
serde_json::from_value(crate::compositor_ipc::call("get-keybindings")?)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if entries.len() > 256
|
||||
|| entries
|
||||
.iter()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod compositor_ipc;
|
||||
mod config;
|
||||
mod control;
|
||||
mod keybinding_guide;
|
||||
@@ -256,6 +257,7 @@ enum Message {
|
||||
Control(status::Action),
|
||||
ShowGuide,
|
||||
GuideLoaded(Result<Vec<keybinding_guide::Entry>, String>),
|
||||
SystemInhibitors(window::Id, Result<compositor_ipc::Approval, String>),
|
||||
ConfirmPower(status::Action),
|
||||
CancelPower,
|
||||
ExecutePower,
|
||||
@@ -690,6 +692,7 @@ impl cosmic::Application for FereseShell {
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SystemInhibitors(id, result) => self.set_system_inhibitors(id, result),
|
||||
Message::ConfirmPower(action) => {
|
||||
if let Some(action) = system_modal::PowerAction::from_status(action) {
|
||||
return self.open_system_modal(action, None);
|
||||
|
||||
@@ -523,14 +523,19 @@ fn notifications(bus: &mut StatusBus) -> Option<Notifications> {
|
||||
Some(Notifications { count, dnd })
|
||||
}
|
||||
|
||||
pub(super) fn execute_power(action: Action) -> Result<(), String> {
|
||||
pub(super) fn execute_power(action: Action, force: bool) -> Result<(), String> {
|
||||
let action = match action {
|
||||
Action::Poweroff => "poweroff",
|
||||
Action::Reboot => "reboot",
|
||||
Action::Suspend => "suspend",
|
||||
_ => return Err("Invalid power action".to_owned()),
|
||||
};
|
||||
run_with_timeout("systemctl", &[action], "120s").map(|_| ())
|
||||
let check = if force {
|
||||
"--check-inhibitors=no"
|
||||
} else {
|
||||
"--check-inhibitors=yes"
|
||||
};
|
||||
run_with_timeout("systemctl", &[check, action], "120s").map(|_| ())
|
||||
}
|
||||
|
||||
fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
|
||||
|
||||
@@ -65,6 +65,7 @@ pub(super) struct SystemModal {
|
||||
surfaces: Vec<ModalSurface>,
|
||||
pub(super) motion: motion::PopupMotion,
|
||||
error: Option<String>,
|
||||
inhibitors: Option<compositor_ipc::Approval>,
|
||||
}
|
||||
|
||||
impl SystemModal {
|
||||
@@ -183,15 +184,65 @@ impl FereseShell {
|
||||
);
|
||||
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
|
||||
}
|
||||
let mode = match content {
|
||||
Content::Power(PowerAction::Suspend) => Some(compositor_ipc::Mode::Suspend),
|
||||
Content::Power(PowerAction::Logout(_)) => Some(compositor_ipc::Mode::Logout),
|
||||
Content::Power(_) => Some(compositor_ipc::Mode::Shutdown),
|
||||
Content::Guide(_) => None,
|
||||
};
|
||||
if let Some(mode) = mode {
|
||||
let id = surfaces[0].id;
|
||||
tasks.push(Task::perform(
|
||||
async move {
|
||||
tokio::task::spawn_blocking(move || compositor_ipc::inhibitors(mode))
|
||||
.await
|
||||
.map_err(|error| error.to_string())
|
||||
.and_then(|result| result)
|
||||
},
|
||||
move |result| cosmic::Action::App(Message::SystemInhibitors(id, result)),
|
||||
));
|
||||
}
|
||||
self.system_modal = Some(SystemModal {
|
||||
content,
|
||||
surfaces,
|
||||
motion: motion::PopupMotion::new(self.config.animations),
|
||||
error: None,
|
||||
inhibitors: mode.is_none().then(compositor_ipc::Approval::default),
|
||||
});
|
||||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
pub(super) fn set_system_inhibitors(
|
||||
&mut self,
|
||||
id: window::Id,
|
||||
result: Result<compositor_ipc::Approval, String>,
|
||||
) -> Task<Message> {
|
||||
if let Some(modal) = &mut self.system_modal
|
||||
&& modal.contains(id)
|
||||
&& matches!(modal.content, Content::Power(_))
|
||||
&& !modal.motion.closing()
|
||||
{
|
||||
match result {
|
||||
Ok(inhibitors) => modal.inhibitors = Some(inhibitors),
|
||||
Err(error) => {
|
||||
modal.error = Some(error);
|
||||
}
|
||||
}
|
||||
} else if let Ok(approval) = result {
|
||||
return Self::cancel_query(approval);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn cancel_query(approval: compositor_ipc::Approval) -> Task<Message> {
|
||||
Task::perform(
|
||||
async move {
|
||||
let _ = tokio::task::spawn_blocking(move || approval.cancel()).await;
|
||||
},
|
||||
|_| cosmic::Action::App(Message::AnimatePower),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn attach_power_material(
|
||||
&mut self,
|
||||
id: window::Id,
|
||||
@@ -232,7 +283,13 @@ impl FereseShell {
|
||||
{
|
||||
control.cancel_logout(serial);
|
||||
}
|
||||
let cancel = modal
|
||||
.inhibitors
|
||||
.take()
|
||||
.map(Self::cancel_query)
|
||||
.unwrap_or_else(Task::none);
|
||||
modal.motion.retarget(0.0, Instant::now());
|
||||
return Task::batch([cancel, self.animate_system_modal()]);
|
||||
}
|
||||
self.animate_system_modal()
|
||||
}
|
||||
@@ -259,12 +316,15 @@ impl FereseShell {
|
||||
{
|
||||
control.cancel_logout(serial);
|
||||
}
|
||||
Task::batch(
|
||||
modal
|
||||
.surfaces
|
||||
.into_iter()
|
||||
.map(|surface| destroy_layer_surface(surface.id)),
|
||||
)
|
||||
let mut tasks = modal
|
||||
.surfaces
|
||||
.into_iter()
|
||||
.map(|surface| destroy_layer_surface(surface.id))
|
||||
.collect::<Vec<_>>();
|
||||
if cancel && let Some(approval) = modal.inhibitors {
|
||||
tasks.push(Self::cancel_query(approval));
|
||||
}
|
||||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
pub(super) fn cancel_logout_modal(&mut self, serial: u32) -> Task<Message> {
|
||||
@@ -283,16 +343,22 @@ impl FereseShell {
|
||||
let Some(modal) = &mut self.system_modal else {
|
||||
return Task::none();
|
||||
};
|
||||
if modal.motion.closing() || self.pending_power.is_some() {
|
||||
if modal.motion.closing() || self.pending_power.is_some() || modal.inhibitors.is_none() {
|
||||
return Task::none();
|
||||
}
|
||||
let Content::Power(power) = modal.content else {
|
||||
return self.close_system_modal();
|
||||
};
|
||||
let approval = modal.inhibitors.as_ref().unwrap().clone();
|
||||
let action = match power {
|
||||
PowerAction::Logout(serial) => {
|
||||
if let Some(control) = &self.control {
|
||||
control.confirm_logout(serial);
|
||||
control.confirm_logout(
|
||||
serial,
|
||||
approval.revision,
|
||||
approval.token,
|
||||
approval.force(),
|
||||
);
|
||||
return self.destroy_system_modal(false);
|
||||
}
|
||||
modal.error = Some("Ferese shell control is unavailable".to_owned());
|
||||
@@ -307,10 +373,18 @@ impl FereseShell {
|
||||
let destroy = self.destroy_system_modal(false);
|
||||
let execute = Task::perform(
|
||||
async move {
|
||||
tokio::task::spawn_blocking(move || status::execute_power(action))
|
||||
.await
|
||||
.map_err(|error| error.to_string())
|
||||
.and_then(|result| result)
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let result = approval
|
||||
.prepare()
|
||||
.and_then(|()| status::execute_power(action, approval.force()));
|
||||
if result.is_err() {
|
||||
approval.cancel();
|
||||
}
|
||||
result
|
||||
})
|
||||
.await
|
||||
.map_err(|error| error.to_string())
|
||||
.and_then(|result| result)
|
||||
},
|
||||
move |result| cosmic::Action::App(Message::PowerCompleted(id, result)),
|
||||
);
|
||||
@@ -369,6 +443,33 @@ impl FereseShell {
|
||||
.class(theme::Text::Color(palette.muted)),
|
||||
]
|
||||
.spacing(12);
|
||||
if let Some(inhibitors) = &modal.inhibitors
|
||||
&& !inhibitors.reasons.is_empty()
|
||||
{
|
||||
rows = rows.push(
|
||||
text("Applications requested that this action be prevented:")
|
||||
.font(shell_font())
|
||||
.size(13)
|
||||
.class(theme::Text::Color(palette.muted)),
|
||||
);
|
||||
let items = inhibitors.reasons.iter().fold(
|
||||
column::with_capacity(inhibitors.reasons.len()).spacing(6),
|
||||
|rows, reason| rows.push(text(reason).font(shell_font()).size(13)),
|
||||
);
|
||||
rows = rows.push(
|
||||
container(cosmic::widget::scrollable(items).height(Length::Shrink))
|
||||
.max_height(160),
|
||||
);
|
||||
}
|
||||
let label = if modal
|
||||
.inhibitors
|
||||
.as_ref()
|
||||
.is_some_and(|approval| approval.force())
|
||||
{
|
||||
format!("{} anyway", action.label())
|
||||
} else {
|
||||
action.label().to_owned()
|
||||
};
|
||||
if let Some(error) = &modal.error {
|
||||
rows = rows.push(text(error).size(13));
|
||||
}
|
||||
@@ -378,9 +479,14 @@ impl FereseShell {
|
||||
button::text("Cancel")
|
||||
.class(ferese_theme::controls::button_style(palette, false))
|
||||
.on_press(cosmic::Action::App(Message::CancelPower)),
|
||||
button::text(action.label())
|
||||
button::text(label)
|
||||
.class(ferese_theme::controls::button_style(palette, true))
|
||||
.on_press(cosmic::Action::App(Message::ExecutePower)),
|
||||
.on_press_maybe(
|
||||
modal
|
||||
.inhibitors
|
||||
.is_some()
|
||||
.then_some(cosmic::Action::App(Message::ExecutePower))
|
||||
),
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user