git.delta.rocks / remowt / refs/commits / 5f64fb1ffdf0

difftreelog

feat socket polkit helper

vpnuwsmwYaroslav Bolyukin2026-01-25parent: #4516008.patch.diff
in: trunk

5 files changed

modifiedcmds/remowt-agent/src/helper/dbus.rsdiffbeforeafterboth
before · cmds/remowt-agent/src/helper/dbus.rs
1use std::collections::HashMap;2use std::marker::PhantomData;34use polkit_shared::{BackendRequest, Identity};5use tokio::runtime::Handle;6use ui_prompt::dbus::DbusPrompterInterface;7use ui_prompt::Prompter;8use zbus::Connection;910use crate::PolkitHelperProxy;1112use super::Helper;131415struct TemporaryPrompterInterface<P: Prompter + 'static> {16    connection: Connection,17    path: String,18    _marker: PhantomData<P>,19}20impl<P: Prompter + 'static> TemporaryPrompterInterface<P> {21    async fn new(connection: Connection, prompter: P) -> Self {22        let path = format!(23            "/remowt/prompters/{}",24            uuid::Uuid::new_v4().to_string().replace("-", "_")25        );26        let _ = connection27            .object_server()28            .at(path.clone(), DbusPrompterInterface(prompter))29            .await;30        Self {31            connection,32            path,33            _marker: PhantomData,34        }35    }36}37impl<P: Prompter + Send + Sync + 'static> Drop for TemporaryPrompterInterface<P> {38    fn drop(&mut self) {39        // FIXME: block_in_place prevents to moving to current_thread runtime40        // There should be a blocking way to remove ObjectServer listener.41        // As far as I can see, it is only async because of async RwLock, shouldn't it be42        // just a sync lock?43        tokio::task::block_in_place(move || {44            Handle::current().block_on(async {45                let _ = self46                    .connection47                    .object_server()48                    .remove::<DbusPrompterInterface<P>, String>(self.path.clone())49                    .await;50            });51        });52    }53}5455pub struct DbusHelper {56    connection: Connection,57    helper: PolkitHelperProxy<'static>,58}59impl Helper for DbusHelper {60    async fn help_me<P: Prompter + Send + Sync + 'static>(61        &self,62        cookie: &str,63        prompter: P,64        identity: Identity,65    ) -> anyhow::Result<()> {66        let prompter = TemporaryPrompterInterface::new(self.connection.clone(), prompter).await;67        self.helper68            .init_conversation(69                BackendRequest {70                    cookie: cookie.to_owned(),71                    environment: HashMap::new(),72                    prompter_path: prompter.path.clone(),73                    identity,74                }, // cookie.to_owned(), HashMap::new(), prompter.path.clone()75            )76            .await?;77        Ok(())78    }79}
after · cmds/remowt-agent/src/helper/dbus.rs
1use std::collections::HashMap;2use std::marker::PhantomData;34use polkit_shared::{BackendRequest, Identity};5use ui_prompt::dbus::DbusPrompterInterface;6use ui_prompt::Prompter;7use zbus::Connection;89use crate::PolkitHelperProxy;1011use super::Helper;1213struct TemporaryPrompterInterface<P: Prompter + 'static> {14	connection: Connection,15	path: String,16	_marker: PhantomData<P>,17}18impl<P: Prompter + 'static> TemporaryPrompterInterface<P> {19	async fn new(connection: Connection, prompter: P) -> Self {20		let path = format!(21			"/remowt/prompters/{}",22			uuid::Uuid::new_v4().to_string().replace("-", "_")23		);24		let _ = connection25			.object_server()26			.at(path.clone(), DbusPrompterInterface(prompter))27			.await;28		Self {29			connection,30			path,31			_marker: PhantomData,32		}33	}34}35impl<P: Prompter + Send + Sync + 'static> Drop for TemporaryPrompterInterface<P> {36	fn drop(&mut self) {37		// Removal is async because of async RwLock used inside...38		// We should not care about its reuse39		let connection = self.connection.clone();40		let path = std::mem::take(&mut self.path);41		tokio::spawn(async move {42			let _ = connection43				.object_server()44				.remove::<DbusPrompterInterface<P>, String>(path)45				.await;46		});47	}48}4950#[derive(Clone)]51pub struct DbusHelper {52	connection: Connection,53	helper: PolkitHelperProxy<'static>,54}55impl DbusHelper {56	pub async fn new(connection: Connection) -> zbus::Result<Self> {57		let helper = PolkitHelperProxy::new(&connection).await?;58		Ok(Self { connection, helper })59	}60}61impl Helper for DbusHelper {62	async fn help_me<P: Prompter + Send + Sync + 'static>(63		&self,64		cookie: &str,65		prompter: P,66		identity: Identity,67	) -> anyhow::Result<()> {68		let prompter = TemporaryPrompterInterface::new(self.connection.clone(), prompter).await;69		self.helper70			.init_conversation(71				BackendRequest {72					cookie: cookie.to_owned(),73					environment: HashMap::new(),74					prompter_path: prompter.path.clone(),75					identity,76				}, // cookie.to_owned(), HashMap::new(), prompter.path.clone()77			)78			.await?;79		Ok(())80	}81}
modifiedcmds/remowt-agent/src/helper/mod.rsdiffbeforeafterboth
--- a/cmds/remowt-agent/src/helper/mod.rs
+++ b/cmds/remowt-agent/src/helper/mod.rs
@@ -2,17 +2,20 @@
 use polkit_shared::Identity;
 use ui_prompt::Prompter;
 
-mod suid;
 mod dbus;
+mod protocol;
+mod socket;
+mod suid;
 
+pub use dbus::DbusHelper;
+pub use socket::SocketHelper;
 pub use suid::SuidHelper;
-pub use dbus::DbusHelper;
 
 pub trait Helper {
-    fn help_me<P: Prompter + Send + Sync + 'static>(
-        &self,
-        cookie: &str,
-        prompt: P,
-        identity: Identity,
-    ) -> impl Future<Output = anyhow::Result<()>> + Send;
+	fn help_me<P: Prompter + Send + Sync + 'static>(
+		&self,
+		cookie: &str,
+		prompt: P,
+		identity: Identity,
+	) -> impl Future<Output = anyhow::Result<()>> + Send;
 }
addedcmds/remowt-agent/src/helper/protocol.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-agent/src/helper/protocol.rs
@@ -0,0 +1,50 @@
+use std::pin::pin;
+
+use anyhow::bail;
+use futures::stream::Peekable;
+use futures::StreamExt as _;
+use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _};
+use tokio::select;
+use tokio_util::codec::{FramedRead, LinesCodec};
+use ui_prompt::Prompter;
+
+pub async fn run_conversation<R, W, P>(reader: R, mut writer: W, prompt: P) -> anyhow::Result<()>
+where
+	R: AsyncRead,
+	W: AsyncWrite + Unpin,
+	P: Prompter,
+{
+	let mut lines = pin!(FramedRead::new(reader, LinesCodec::new()).peekable());
+
+	while let Some(line) = lines.next().await {
+		let line = line?;
+		let res = if let Some(prompt_text) = line.strip_prefix("PAM_PROMPT_ECHO_OFF ") {
+			prompt.prompt_text(false, prompt_text, "", &[]).await?
+		} else if let Some(prompt_text) = line.strip_prefix("PAM_PROMPT_ECHO_ON ") {
+			prompt.prompt_text(true, prompt_text, "", &[]).await?
+		} else if let Some(msg_text) = line.strip_prefix("PAM_ERROR_MSG ") {
+			prompt.display_text(true, msg_text, &[]).await?;
+			String::new()
+		} else if let Some(msg_text) = line.strip_prefix("PAM_TEXT_INFO ") {
+			select! {
+				_ = Peekable::peek(lines.as_mut()) => {},
+				r = prompt.display_text(false, msg_text, &[]) => {r?}
+			}
+			String::new()
+		} else if line == "SUCCESS" {
+			return Ok(());
+		} else if line == "FAILURE" {
+			bail!("helper reported failure")
+		} else {
+			bail!("unknown agent request: {line}")
+		};
+
+		if res.contains('\n') {
+			bail!("response should not include newline")
+		}
+
+		writer.write_all(res.as_bytes()).await?;
+		writer.write_all(b"\n").await?;
+	}
+	bail!("agent finished unexpectedly")
+}
addedcmds/remowt-agent/src/helper/socket.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/remowt-agent/src/helper/socket.rs
@@ -0,0 +1,53 @@
+use anyhow::{anyhow, bail};
+use nix::unistd::User;
+use polkit_shared::Identity;
+use tokio::io::AsyncWriteExt as _;
+use tokio::net::UnixStream;
+use tracing::debug;
+use ui_prompt::Prompter;
+
+use super::protocol::run_conversation;
+use super::Helper;
+
+/// Polkit 127 introduced an alternative backend similar to `lach.PolkitHelper`
+const SOCKET_PATH: &str = "/run/polkit/agent-helper.socket";
+
+#[derive(Clone)]
+pub struct SocketHelper<F> {
+	pub fallback: F,
+}
+
+impl<F: Helper + Sync> Helper for SocketHelper<F> {
+	async fn help_me<P: Prompter + Send + Sync + 'static>(
+		&self,
+		cookie: &str,
+		prompt: P,
+		identity: Identity,
+	) -> anyhow::Result<()> {
+		let Some(uid) = identity.uid() else {
+			bail!("can't process identity");
+		};
+
+		let stream = match UnixStream::connect(SOCKET_PATH).await {
+			Ok(stream) => stream,
+			Err(e) => {
+				debug!("agent-helper.socket unavailable ({e}), using fallback helper");
+				return self.fallback.help_me(cookie, prompt, identity).await;
+			}
+		};
+
+		let user = User::from_uid(uid)
+			.map_err(|e| anyhow!("error querying user: {e}"))?
+			.ok_or_else(|| anyhow!("user not found"))?;
+
+		assert!(!cookie.contains('\n'));
+		let (reader, mut writer) = stream.into_split();
+
+		writer.write_all(user.name.as_bytes()).await?;
+		writer.write_all(b"\n").await?;
+		writer.write_all(cookie.as_bytes()).await?;
+		writer.write_all(b"\n").await?;
+
+		run_conversation(reader, writer, prompt).await
+	}
+}
modifiedcmds/remowt-agent/src/helper/suid.rsdiffbeforeafterboth
--- a/cmds/remowt-agent/src/helper/suid.rs
+++ b/cmds/remowt-agent/src/helper/suid.rs
@@ -1,83 +1,46 @@
-use std::pin::pin;
 use std::process::Stdio;
 
-use anyhow::{bail, anyhow};
-use futures::stream::Peekable;
-use futures::StreamExt as _;
+use anyhow::{anyhow, bail};
 use nix::unistd::User;
 use polkit_shared::Identity;
 use tokio::io::AsyncWriteExt as _;
 use tokio::process::Command;
-use tokio::select;
-use tokio_util::codec::{FramedRead, LinesCodec};
 use ui_prompt::Prompter;
 
+use super::protocol::run_conversation;
 use super::Helper;
 
 #[derive(Clone)]
 pub struct SuidHelper;
 impl Helper for SuidHelper {
-    async fn help_me<P: Prompter + 'static>(
-        &self,
-        cookie: &str,
-        prompt: P,
-        identity: Identity,
-    ) -> anyhow::Result<()> {
-        let Some(uid) = dbg!(identity.uid()) else {
-            bail!("can't process identity");
-        };
-        let user = User::from_uid(dbg!(uid))
-            .map_err(|e| anyhow!("error querying user: {e}"))?
-            .ok_or_else(|| anyhow!("user not found"))?;
-
-        let mut cmd = Command::new("polkit-agent-helper-1");
-        cmd.arg(user.name);
-        cmd.stdin(Stdio::piped());
-        cmd.stdout(Stdio::piped());
-        cmd.kill_on_drop(true);
-        let mut child = cmd.spawn()?;
-        let mut stdin = child.stdin.take().expect("piped");
-        let mut stdout =
-            pin!(
-                FramedRead::new(child.stdout.take().expect("piped"), LinesCodec::new()).peekable()
-            );
-
-        assert!(!cookie.contains("\n"));
-        stdin.write_all(cookie.as_bytes()).await?;
-        stdin.write_all(b"\n").await?;
+	async fn help_me<P: Prompter + 'static>(
+		&self,
+		cookie: &str,
+		prompt: P,
+		identity: Identity,
+	) -> anyhow::Result<()> {
+		let Some(uid) = identity.uid() else {
+			bail!("can't process identity");
+		};
+		let user = User::from_uid(uid)
+			.map_err(|e| anyhow!("error querying user: {e}"))?
+			.ok_or_else(|| anyhow!("user not found"))?;
 
-        while let Some(line) = stdout.next().await {
-            let line = dbg!(line?);
-            // TODO: Dedicated codec?
-            let res = if let Some(prompt_text) = line.strip_prefix("PAM_PROMPT_ECHO_OFF ") {
-                prompt.prompt_text(false, prompt_text, "", &[]).await?
-            } else if let Some(prompt_text) = line.strip_prefix("PAM_PROMPT_ECHO_ON ") {
-                prompt.prompt_text(true, prompt_text, "", &[]).await?
-            } else if let Some(msg_text) = line.strip_prefix("PAM_ERROR_MSG ") {
-                prompt.display_text(true, msg_text, &[]).await?;
-                String::new()
-            } else if let Some(msg_text) = line.strip_prefix("PAM_TEXT_INFO ") {
-                select! {
-                    _ = Peekable::peek(stdout.as_mut()) => {},
-                    r = prompt.display_text(false, msg_text, &[]) => {r?}
-                }
-                String::new()
-            } else if line == "SUCCESS" {
-                return Ok(());
-            } else if line == "FAILURE" {
-                bail!("helper binary reported failure")
-            } else {
-                // TODO: Success/failure handling
-                bail!("unknown agent request");
-            };
+		let mut cmd = Command::new("polkit-agent-helper-1");
+		cmd.arg(user.name);
+		cmd.stdin(Stdio::piped());
+		cmd.stdout(Stdio::piped());
+		cmd.kill_on_drop(true);
+		let mut child = cmd.spawn()?;
+		let mut stdin = child.stdin.take().expect("piped");
+		let stdout = child.stdout.take().expect("piped");
 
-            if res.contains("\n") {
-                bail!("response should not include newline")
-            }
+		assert!(!cookie.contains('\n'));
+		stdin.write_all(cookie.as_bytes()).await?;
+		stdin.write_all(b"\n").await?;
 
-            stdin.write_all(res.as_bytes()).await?;
-            stdin.write_all(b"\n").await?;
-        }
-        bail!("agent finished unexpectedly")
-    }
+		let res = run_conversation(stdout, stdin, prompt).await;
+		drop(child);
+		res
+	}
 }