Unify shell opacity and add native authentication and system controls

This commit is contained in:
Samuel Onoja committed 2026-09-28 09:26:14 +01:00
1 parent 8832a9bf2e
commit c8e89fb758
35 files changed
+1674 -133

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
mod prompt;
mod session;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
};
use zbus::zvariant::{OwnedValue, Value};
const AGENT_PATH: &str = "/dev/ferese/PolkitAgent";
const AUTHORITY: &str = "org.freedesktop.PolicyKit1";
const AUTHORITY_PATH: &str = "/org/freedesktop/PolicyKit1/Authority";
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum PromptEvent {
Start { message: String, user: String },
Request { prompt: String, echo: bool },
Info { text: String },
Error { text: String },
Response { value: String },
Cancel,
}
type Identity = (String, HashMap<String, OwnedValue>);
#[derive(Default)]
struct Agent {
active: Mutex<HashMap<String, Arc<AtomicBool>>>,
}
#[zbus::interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
impl Agent {
async fn begin_authentication(
&self,
_action_id: String,
message: String,
_icon_name: String,
_details: HashMap<String, String>,
cookie: String,
identities: Vec<Identity>,
) -> zbus::fdo::Result<()> {
let uid = choose_identity(&identities).ok_or_else(|| {
zbus::fdo::Error::Failed("No supported authentication identity".into())
})?;
let cancelled = Arc::new(AtomicBool::new(false));
{
let mut active = self.active.lock().unwrap();
if active.contains_key(&cookie) {
return Err(zbus::fdo::Error::Failed("Duplicate request".into()));
}
active.insert(cookie.clone(), cancelled.clone());
}
let result = tokio::task::spawn_blocking({
let cookie = cookie.clone();
move || {
session::authenticate(uid, message.chars().take(240).collect(), cookie, cancelled)
}
})
.await
.map_err(|error| zbus::fdo::Error::Failed(error.to_string()))?;
self.active.lock().unwrap().remove(&cookie);
match result {
Ok(true) => Ok(()),
Ok(false) => Err(zbus::fdo::Error::Failed("Authentication cancelled".into())),
Err(error) => Err(zbus::fdo::Error::Failed(error)),
}
}
fn cancel_authentication(&self, cookie: String) {
if let Some(cancelled) = self.active.lock().unwrap().get(&cookie) {
cancelled.store(true, Ordering::Release);
}
}
}
fn choose_identity(identities: &[Identity]) -> Option<u32> {
let available: Vec<_> = identities
.iter()
.filter(|(kind, _)| kind == "unix-user")
.filter_map(|(_, details)| {
details
.get("uid")
.and_then(|uid| u32::try_from(uid.clone()).ok())
.filter(|uid| *uid <= i32::MAX as u32)
})
.collect();
let current = unsafe { libc::geteuid() };
available
.iter()
.copied()
.find(|uid| *uid == current)
.or_else(|| available.first().copied())
}
fn session_subject() -> Result<(String, HashMap<String, Value<'static>>), String> {
let id = std::env::var("XDG_SESSION_ID")
.map_err(|_| "XDG_SESSION_ID is required for polkit registration".to_owned())?;
if id.is_empty() {
return Err("XDG_SESSION_ID is empty".into());
}
let mut details = HashMap::new();
details.insert("session-id".to_owned(), Value::from(id));
Ok(("unix-session".into(), details))
}
async fn run_agent() -> Result<(), Box<dyn std::error::Error>> {
let subject = session_subject()?;
let connection = zbus::connection::Builder::system()?
.serve_at(AGENT_PATH, Agent::default())?
.build()
.await?;
let authority = zbus::Proxy::new(&connection, AUTHORITY, AUTHORITY_PATH, AUTHORITY).await?;
let locale = std::env::var("LANG").unwrap_or_default();
authority
.call::<_, _, ()>(
"RegisterAuthenticationAgent",
&(subject, locale, AGENT_PATH),
)
.await?;
std::future::pending::<()>().await;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
unsafe {
libc::prctl(libc::PR_SET_DUMPABLE, 0);
}
match std::env::args().nth(1).as_deref() {
None => tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(run_agent()),
Some("--prompt") => prompt::run(),
Some("--check") => session::check().map_err(Into::into),
_ => Err("Usage: ferese-polkit-agent [--check]".into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chooses_current_identity_when_available() {
let current = unsafe { libc::geteuid() };
let identity = |uid: u32| {
(
"unix-user".to_owned(),
HashMap::from([("uid".to_owned(), OwnedValue::from(uid))]),
)
};
assert_eq!(
choose_identity(&[identity(0), identity(current)]),
Some(current)
);
}
}
+489
View File
@@ -0,0 +1,489 @@
use crate::PromptEvent;
use cosmic::{
Element,
app::{Core, Settings, Task},
iced::{Alignment, Background, Border, Color, Length, Size, Subscription, window},
theme,
widget::{self, button, column, container, row, text},
};
use std::{
collections::VecDeque,
io::{BufRead, BufReader, Write},
sync::{Arc, Mutex},
time::Duration,
};
use zeroize::{Zeroize, Zeroizing};
#[derive(Clone)]
struct Appearance {
surface: Color,
translucent: bool,
text: Color,
muted: Color,
accent: Color,
radius: f32,
font: cosmic::font::Font,
}
impl Appearance {
fn load() -> Self {
let document = ferese_config::config_path()
.and_then(|path| std::fs::read_to_string(path).ok())
.and_then(|source| ferese_config::Document::parse(&source).ok());
let setting = |key: &str, fallback: &str| {
document
.as_ref()
.and_then(|doc| doc.get(key))
.and_then(|value| value.as_str())
.unwrap_or(fallback)
.to_owned()
};
let color = |key, fallback| parse_color(&setting(key, fallback), fallback);
let family: &'static str =
Box::leak(setting("theme.typography.font_family", "Inter").into_boxed_str());
let translucent = setting("theme.material.style", "solid") == "translucent";
let shell_opacity = document
.as_ref()
.and_then(|doc| doc.get("theme.material.opacity"))
.and_then(|value| value.as_f64())
.unwrap_or(ferese_config::DEFAULT_MATERIAL_OPACITY)
.clamp(0., 1.) as f32;
let mut surface = color("theme.colors.surface_base", "#111821");
surface.a = if translucent { shell_opacity } else { 1. };
Self {
surface,
translucent,
text: color("theme.colors.text_primary", "#F4F7FB"),
muted: color("theme.colors.text_muted", "#8793A2"),
accent: color("theme.colors.accent", "#3D7BE6"),
radius: document
.as_ref()
.and_then(|doc| doc.get("theme.geometry.shell_radius"))
.and_then(|value| value.as_f64())
.unwrap_or(14.)
.clamp(8., 36.) as f32,
font: cosmic::font::Font {
family: cosmic::iced::font::Family::Name(family),
..cosmic::font::default()
},
}
}
fn theme(&self) -> cosmic::Theme {
use cosmic::cosmic_theme::{ThemeBuilder, palette::Srgba};
let rgba = |color: Color| Srgba::new(color.r, color.g, color.b, color.a);
let builder = if self.surface.r + self.surface.g + self.surface.b > 1.5 {
ThemeBuilder::light()
} else {
ThemeBuilder::dark()
};
let mut corners = cosmic::cosmic_theme::CornerRadii::default();
corners.radius_xs = [self.radius.min(4.); 4];
corners.radius_s = [self.radius.min(8.); 4];
corners.radius_m = [self.radius; 4];
corners.radius_l = [self.radius; 4];
corners.radius_xl = [self.radius; 4];
cosmic::Theme::custom(Arc::new(
builder
.corner_radii(corners)
.bg_color(rgba(Color {
a: if self.translucent { 0. } else { 1. },
..self.surface
}))
.primary_container_bg(rgba(self.surface))
.text_tint(rgba(self.text).color)
.accent(rgba(self.accent).color)
.build(),
))
}
}
fn parse_color(input: &str, fallback: &str) -> Color {
let parse = |source: &str| {
let value = u32::from_str_radix(source.strip_prefix('#')?, 16).ok()?;
(source.len() == 7)
.then(|| Color::from_rgb8((value >> 16) as u8, (value >> 8) as u8, value as u8))
};
parse(input)
.or_else(|| parse(fallback))
.unwrap_or(Color::BLACK)
}
#[derive(Clone)]
enum Message {
Tick,
Input(String),
Submit,
Cancel,
}
impl std::fmt::Debug for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("AuthenticationPromptMessage")
}
}
struct Prompt {
core: Core,
appearance: Appearance,
incoming: Arc<Mutex<VecDeque<PromptEvent>>>,
description: String,
user: String,
question: String,
echo: bool,
answer: Zeroizing<String>,
waiting: bool,
error: Option<String>,
info: Option<String>,
height: f32,
}
fn content_height(description: &str, status: Option<&str>) -> f32 {
let lines: usize = description
.lines()
.map(|line| line.chars().count().div_ceil(48).max(1))
.sum();
let status_height = status.map_or(0, |message| {
14 + 18 * message.chars().count().div_ceil(48).clamp(1, 3)
});
(214 + 18 * lines.saturating_sub(1) + status_height).min(360) as f32
}
fn input_style(palette: &Appearance) -> theme::TextInput {
let text = palette.text;
let muted = palette.muted;
let accent = palette.accent;
let radius = palette.radius.min(10.);
let appearance = move |focused: bool| widget::text_input::Appearance {
background: Color::from_rgba(text.r, text.g, text.b, 0.045).into(),
border_radius: radius.into(),
border_width: 1.,
border_offset: None,
border_color: if focused {
accent.scale_alpha(0.82)
} else {
muted.scale_alpha(0.26)
},
icon_color: Some(muted),
text_color: Some(text),
placeholder_color: muted,
selected_text_color: text,
selected_fill: accent.scale_alpha(0.35),
label_color: text,
};
theme::TextInput::Custom {
active: Box::new(move |_| appearance(false)),
hovered: Box::new(move |_| appearance(true)),
focused: Box::new(move |_| appearance(true)),
error: Box::new(move |_| appearance(false)),
disabled: Box::new(move |_| appearance(false)),
}
}
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let stdin = std::io::stdin();
let mut lines = BufReader::new(stdin).lines();
let Some(Ok(first)) = lines.next() else {
return Err("Missing authentication request".into());
};
let PromptEvent::Start { message, user } = serde_json::from_str(&first)? else {
return Err("Invalid authentication request".into());
};
let incoming = Arc::new(Mutex::new(VecDeque::new()));
let queue = incoming.clone();
std::thread::spawn(move || {
for line in lines {
let Ok(line) = line else { break };
if let Ok(event) = serde_json::from_str(&line) {
queue.lock().unwrap().push_back(event);
}
}
queue.lock().unwrap().push_back(PromptEvent::Cancel);
});
let appearance = Appearance::load();
let height = content_height(&message, None);
cosmic::app::run::<Prompt>(
Settings::default()
.size(Size::new(434., height))
.client_decorations(false)
.transparent(true)
.theme(appearance.theme())
.default_font(appearance.font)
.default_text_size(14.)
.antialiasing(true),
(incoming, message, user, appearance),
)?;
Ok(())
}
fn reply(mut event: PromptEvent) {
if let Ok(mut line) = serde_json::to_string(&event) {
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{line}");
let _ = stdout.flush();
line.zeroize();
}
if let PromptEvent::Response { value } = &mut event {
value.zeroize();
}
}
fn focus() -> Task<Message> {
widget::text_input::focus(widget::Id::new("auth-response"))
}
impl Prompt {
fn resize_to_content(&mut self) -> Task<Message> {
let height = content_height(
&self.description,
self.error.as_deref().or(self.info.as_deref()),
);
if (height - self.height).abs() < 1.0 {
return Task::none();
}
self.height = height;
self.core
.main_window_id()
.map(|id| window::resize(id, Size::new(434., height)))
.unwrap_or_else(Task::none)
}
}
impl cosmic::Application for Prompt {
type Executor = cosmic::executor::Default;
type Flags = (
Arc<Mutex<VecDeque<PromptEvent>>>,
String,
String,
Appearance,
);
type Message = Message;
const APP_ID: &'static str = "dev.ferese.Authentication";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
fn init(
mut core: Core,
(incoming, description, user, appearance): Self::Flags,
) -> (Self, Task<Message>) {
core.window.show_headerbar = false;
let height = content_height(&description, None);
let bounds = core
.main_window_id()
.map(|id| window::set_max_size(id, Some(Size::new(434., 360.))))
.unwrap_or_else(Task::none);
(
Self {
core,
appearance,
incoming,
description,
user,
question: "Waiting for authentication…".into(),
echo: false,
answer: Zeroizing::new(String::new()),
waiting: true,
error: None,
info: None,
height,
},
Task::batch([focus(), bounds]),
)
}
fn subscription(&self) -> Subscription<Message> {
cosmic::iced::time::every(Duration::from_millis(30)).map(|_| Message::Tick)
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Tick => {
let events: Vec<_> = self.incoming.lock().unwrap().drain(..).collect();
let mut refocus = false;
for event in events {
match event {
PromptEvent::Request { prompt, echo } => {
self.answer.zeroize();
self.question = prompt;
self.echo = echo;
self.waiting = false;
refocus = true;
}
PromptEvent::Info { text } => self.info = Some(text),
PromptEvent::Error { text } => self.error = Some(text),
PromptEvent::Cancel => return cosmic::iced::exit(),
_ => {}
}
}
let resize = self.resize_to_content();
if refocus {
return Task::batch([resize, focus()]);
}
return resize;
}
Message::Input(mut value) => {
if !self.waiting && value.len() <= 4096 {
self.answer.zeroize();
*self.answer = std::mem::take(&mut value);
self.error = None;
}
value.zeroize();
}
Message::Submit if !self.waiting && !self.answer.is_empty() => {
let value = std::mem::take(&mut *self.answer);
reply(PromptEvent::Response { value });
self.waiting = true;
}
Message::Cancel => {
self.answer.zeroize();
reply(PromptEvent::Cancel);
return cosmic::iced::exit();
}
_ => {}
}
Task::none()
}
fn on_escape(&mut self) -> Task<Message> {
self.update(Message::Cancel)
}
fn view(&self) -> Element<'_, Message> {
let palette = &self.appearance;
let mut input = widget::text_input(&self.question, self.answer.as_str())
.id(widget::Id::new("auth-response"))
.font(palette.font)
.padding([7, 12])
.style(input_style(palette));
if !self.echo {
input = input.password();
}
if !self.waiting {
input = input.on_input(Message::Input).on_submit(|mut value| {
value.zeroize();
Message::Submit
});
}
let action = button::custom(text("Authenticate").font(palette.font).size(13))
.class(theme::Button::Suggested)
.height(Length::Fixed(36.))
.padding([8, 16])
.on_press_maybe((!self.waiting && !self.answer.is_empty()).then_some(Message::Submit));
let lock_svg = std::str::from_utf8(include_bytes!(
"../../ferese-shell/assets/icons/status/lock.svg"
))
.expect("embedded lock icon is valid UTF-8")
.replace(
"currentColor",
&format!(
"#{:02x}{:02x}{:02x}",
(palette.accent.r * 255.) as u8,
(palette.accent.g * 255.) as u8,
(palette.accent.b * 255.) as u8,
),
);
let lock_icon = widget::icon::from_svg_bytes(lock_svg.into_bytes())
.symbolic(false)
.icon()
.size(20);
let heading = row![
container(lock_icon)
.width(40)
.height(40)
.center_x(40)
.center_y(40)
.class(theme::Container::custom(move |_| {
widget::container::Style {
background: Some(Background::Color(palette.accent.scale_alpha(0.13))),
border: Border {
radius: 12.0.into(),
..Default::default()
},
..Default::default()
}
})),
column![
text("Authentication required").font(palette.font).size(18),
text(format!("Confirm as {}", self.user))
.font(palette.font)
.size(12)
.class(theme::Text::Color(palette.muted)),
]
.spacing(2),
]
.spacing(12)
.align_y(Alignment::Center);
let mut body = column![
heading,
text(&self.description).font(palette.font).size(13),
input
]
.spacing(14)
.width(Length::Fill);
if let Some(error) = &self.error {
body = body.push(
text(error)
.font(palette.font)
.size(12)
.class(theme::Text::Color(Color::from_rgb8(235, 98, 98))),
);
} else if let Some(info) = &self.info {
body = body.push(
text(info)
.font(palette.font)
.size(12)
.class(theme::Text::Color(palette.muted)),
);
}
body = body.push(
row![
button::custom(text("Cancel").font(palette.font).size(13))
.class(theme::Button::Text)
.padding([8, 14])
.on_press(Message::Cancel),
action
]
.spacing(8)
.align_y(Alignment::Center),
);
container(body)
.padding(20)
.width(Length::Fill)
.height(Length::Fill)
.class(theme::Container::custom(move |_| {
widget::container::Style {
background: Some(Background::Color(palette.surface)),
text_color: Some(palette.text),
border: Border {
radius: palette.radius.into(),
..Default::default()
},
..Default::default()
}
}))
.into()
}
}
#[cfg(test)]
mod tests {
use super::content_height;
#[test]
fn dialog_grows_for_long_messages_and_caps_its_height() {
assert_eq!(content_height("Short request", None), 214.);
assert!(content_height(&"Long message ".repeat(20), None) > 214.);
assert_eq!(content_height(&"Long message ".repeat(100), None), 360.);
assert!(content_height("Short request", Some("Password not accepted")) > 214.);
}
}
+325
View File
@@ -0,0 +1,325 @@
use crate::PromptEvent;
use glib::gobject_ffi;
use libloading::Library;
use std::{
ffi::{CStr, CString, c_char, c_int, c_void},
io::{BufRead, BufReader, BufWriter, Write},
process::{Child, ChildStdin, Command, Stdio},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc::{self, Sender},
},
time::Duration,
};
use zeroize::Zeroize;
enum SessionEvent {
Request(String, bool),
Info(String),
Error(String),
Completed(bool),
}
type Object = *mut c_void;
struct Api {
_agent: Library,
_polkit: Library,
user_new: unsafe extern "C" fn(c_int) -> Object,
session_new: unsafe extern "C" fn(Object, *const c_char) -> Object,
initiate: unsafe extern "C" fn(Object),
response: unsafe extern "C" fn(Object, *const c_char),
cancel: unsafe extern "C" fn(Object),
}
impl Api {
fn load() -> Result<Self, String> {
unsafe {
let agent = Library::new("libpolkit-agent-1.so.0").map_err(|e| e.to_string())?;
let polkit = Library::new("libpolkit-gobject-1.so.0").map_err(|e| e.to_string())?;
let user_new = *polkit
.get(b"polkit_unix_user_new\0")
.map_err(|e| e.to_string())?;
let session_new = *agent
.get(b"polkit_agent_session_new\0")
.map_err(|e| e.to_string())?;
let initiate = *agent
.get(b"polkit_agent_session_initiate\0")
.map_err(|e| e.to_string())?;
let response = *agent
.get(b"polkit_agent_session_response\0")
.map_err(|e| e.to_string())?;
let cancel = *agent
.get(b"polkit_agent_session_cancel\0")
.map_err(|e| e.to_string())?;
Ok(Self {
_agent: agent,
_polkit: polkit,
user_new,
session_new,
initiate,
response,
cancel,
})
}
}
}
pub fn check() -> Result<(), String> {
Api::load().map(|_| ())
}
fn username(uid: u32) -> String {
let output = Command::new("getent")
.args(["passwd", &uid.to_string()])
.output();
output
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.and_then(|entry| entry.split(':').next().map(str::to_owned))
.filter(|name| !name.is_empty())
.unwrap_or_else(|| uid.to_string())
}
fn send(writer: &mut BufWriter<ChildStdin>, event: &PromptEvent) -> Result<(), String> {
serde_json::to_writer(&mut *writer, event).map_err(|e| e.to_string())?;
writer.write_all(b"\n").map_err(|e| e.to_string())?;
writer.flush().map_err(|e| e.to_string())
}
fn prompt_process(
message: String,
user: String,
) -> Result<(Child, BufWriter<ChildStdin>, mpsc::Receiver<PromptEvent>), String> {
let mut child = Command::new(std::env::current_exe().map_err(|e| e.to_string())?)
.arg("--prompt")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|e| e.to_string())?;
let stdin = child.stdin.take().ok_or("Prompt has no input")?;
let stdout = child.stdout.take().ok_or("Prompt has no output")?;
let mut writer = BufWriter::new(stdin);
send(&mut writer, &PromptEvent::Start { message, user })?;
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
let Ok(mut line) = line else { break };
if let Ok(event) = serde_json::from_str(&line) {
line.zeroize();
if tx.send(event).is_err() {
break;
}
} else {
line.zeroize();
}
}
});
Ok((child, writer, rx))
}
unsafe extern "C" fn request(
_session: Object,
prompt: *const c_char,
echo: c_int,
data: *mut c_void,
) {
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
let prompt = unsafe { CStr::from_ptr(prompt) }
.to_string_lossy()
.into_owned();
let _ = tx.send(SessionEvent::Request(prompt, echo != 0));
}
unsafe extern "C" fn info(_session: Object, text: *const c_char, data: *mut c_void) {
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
let text = unsafe { CStr::from_ptr(text) }
.to_string_lossy()
.into_owned();
let _ = tx.send(SessionEvent::Info(text));
}
unsafe extern "C" fn error(_session: Object, text: *const c_char, data: *mut c_void) {
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
let text = unsafe { CStr::from_ptr(text) }
.to_string_lossy()
.into_owned();
let _ = tx.send(SessionEvent::Error(text));
}
unsafe extern "C" fn completed(_session: Object, success: c_int, data: *mut c_void) {
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
let _ = tx.send(SessionEvent::Completed(success != 0));
}
unsafe fn connect(
session: Object,
name: &'static CStr,
callback: gobject_ffi::GCallback,
data: *mut c_void,
) {
unsafe {
gobject_ffi::g_signal_connect_data(
session.cast(),
name.as_ptr(),
callback,
data,
None,
gobject_ffi::G_CONNECT_DEFAULT,
);
}
}
fn run_session(
api: &Api,
uid: u32,
cookie: &CString,
context: &glib::MainContext,
writer: &mut BufWriter<ChildStdin>,
input: &mpsc::Receiver<PromptEvent>,
cancelled: &AtomicBool,
child: &mut Child,
) -> Result<Option<bool>, String> {
let identity = unsafe { (api.user_new)(uid as c_int) };
if identity.is_null() {
return Err("Could not create polkit identity".into());
}
let session = unsafe { (api.session_new)(identity, cookie.as_ptr()) };
unsafe { gobject_ffi::g_object_unref(identity.cast()) };
if session.is_null() {
return Err("Could not create polkit session".into());
}
let (tx, rx) = mpsc::channel();
let signal_data = Box::into_raw(Box::new(tx)).cast();
unsafe {
connect(
session,
c"request",
Some(std::mem::transmute(
request as unsafe extern "C" fn(Object, *const c_char, c_int, *mut c_void),
)),
signal_data,
);
connect(
session,
c"show-info",
Some(std::mem::transmute(
info as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
)),
signal_data,
);
connect(
session,
c"show-error",
Some(std::mem::transmute(
error as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
)),
signal_data,
);
connect(
session,
c"completed",
Some(std::mem::transmute(
completed as unsafe extern "C" fn(Object, c_int, *mut c_void),
)),
signal_data,
);
(api.initiate)(session);
}
let mut outcome = None;
let mut stopped = false;
while outcome.is_none() {
while context.pending() {
context.iteration(false);
}
while let Ok(event) = rx.try_recv() {
match event {
SessionEvent::Request(prompt, echo) => {
send(writer, &PromptEvent::Request { prompt, echo })?;
}
SessionEvent::Info(text) => send(writer, &PromptEvent::Info { text })?,
SessionEvent::Error(text) => send(writer, &PromptEvent::Error { text })?,
SessionEvent::Completed(success) => outcome = Some(success),
}
}
if outcome.is_some() {
break;
}
while let Ok(event) = input.try_recv() {
match event {
PromptEvent::Response { mut value } => {
if let Ok(response) = CString::new(value.as_str()) {
unsafe { (api.response)(session, response.as_ptr()) };
}
value.zeroize();
}
PromptEvent::Cancel => cancelled.store(true, Ordering::Release),
_ => {}
}
}
if !stopped
&& (cancelled.load(Ordering::Acquire)
|| child.try_wait().map_err(|e| e.to_string())?.is_some())
{
unsafe { (api.cancel)(session) };
stopped = true;
}
std::thread::sleep(Duration::from_millis(10));
}
unsafe {
gobject_ffi::g_object_unref(session.cast());
drop(Box::from_raw(signal_data as *mut Sender<SessionEvent>));
}
Ok(outcome.filter(|_| !stopped))
}
pub fn authenticate(
uid: u32,
message: String,
cookie: String,
cancelled: Arc<AtomicBool>,
) -> Result<bool, String> {
let api = Api::load()?;
let cookie = CString::new(cookie).map_err(|_| "Invalid authentication cookie")?;
let context = glib::MainContext::new();
context
.with_thread_default(|| {
let (mut child, mut writer, input) = prompt_process(message, username(uid))?;
let result = loop {
match run_session(
&api,
uid,
&cookie,
&context,
&mut writer,
&input,
&cancelled,
&mut child,
) {
Ok(Some(true)) => break Ok(true),
Ok(Some(false)) if !cancelled.load(Ordering::Acquire) => {
if let Err(error) = send(
&mut writer,
&PromptEvent::Error {
text: "Password not accepted. Try again.".into(),
},
) {
break Err(error);
}
}
Ok(_) => break Ok(false),
Err(error) => break Err(error),
}
};
let _ = child.kill();
let _ = child.wait();
result
})
.map_err(|error| error.to_string())?
}