difftreelog
refactor merge well-known endpoints into a single crate
in: trunk
36 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2065,10 +2065,10 @@
"futures-util",
"nix",
"rand 0.10.1",
+ "remowt-endpoints",
"remowt-link-shared",
"remowt-plugin",
"remowt-polkit-shared",
- "remowt-pty",
"remowt-ui-prompt",
"serde",
"tempfile",
@@ -2090,6 +2090,7 @@
"bifrostlink-ports",
"bytes",
"camino",
+ "remowt-endpoints",
"remowt-link-shared",
"russh",
"russh-config",
@@ -2101,16 +2102,21 @@
]
[[package]]
-name = "remowt-fs"
+name = "remowt-endpoints"
version = "0.1.1"
dependencies = [
+ "anyhow",
"bifrostlink",
"bifrostlink-macros",
"camino",
+ "nix",
"serde",
"tempfile",
"thiserror",
"tokio",
+ "tracing",
+ "uuid",
+ "zbus",
]
[[package]]
@@ -2120,30 +2126,11 @@
"bifrostlink",
"bytes",
"camino",
- "remowt-fs",
- "remowt-pty",
- "remowt-systemd",
"remowt-ui-prompt",
"serde",
"serde_json",
- "thiserror",
- "tokio",
-]
-
-[[package]]
-name = "remowt-nix-daemon"
-version = "0.1.1"
-dependencies = [
- "anyhow",
- "bifrostlink",
- "bifrostlink-macros",
- "camino",
- "remowt-client",
- "serde",
"thiserror",
"tokio",
- "tracing",
- "uuid",
]
[[package]]
@@ -2171,20 +2158,6 @@
]
[[package]]
-name = "remowt-pty"
-version = "0.1.1"
-dependencies = [
- "bifrostlink",
- "bifrostlink-macros",
- "camino",
- "nix",
- "serde",
- "thiserror",
- "tokio",
- "tracing",
-]
-
-[[package]]
name = "remowt-ssh"
version = "0.1.1"
dependencies = [
@@ -2210,17 +2183,6 @@
"tracing",
"tracing-subscriber",
"uuid",
-]
-
-[[package]]
-name = "remowt-systemd"
-version = "0.1.1"
-dependencies = [
- "bifrostlink",
- "bifrostlink-macros",
- "serde",
- "thiserror",
- "zbus",
]
[[package]]
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,14 +9,12 @@
repository = "https://gitlab.delta.directory/iam/remowt"
[workspace.dependencies]
-remowt-fs = { version = "0.1.1", path = "crates/remowt-fs" }
-remowt-pty = { version = "0.1.1", path = "crates/remowt-pty" }
-remowt-systemd = { version = "0.1.1", path = "crates/remowt-systemd" }
remowt-client = { version = "0.1.1", path = "crates/remowt-client" }
remowt-polkit-shared = { version = "0.1.1", path = "crates/polkit-shared" }
remowt-link-shared = { version = "0.1.1", path = "crates/remowt-link-shared" }
remowt-plugin = { version = "0.1.1", path = "crates/remowt-plugin" }
-remowt-ui-prompt = { version = "0.1.1", path = "crates/ui-prompt" }
+remowt-ui-prompt = { version = "0.1.1", path = "crates/remowt-ui-prompt" }
+remowt-endpoints = { version = "0.1.1", path = "crates/remowt-endpoints" }
bifrostlink = "0.2.0"
bifrostlink-macros = "0.2.0"
cmds/polkit-dbus-helper/src/main.rsdiffbeforeafterboth--- a/cmds/polkit-dbus-helper/src/main.rs
+++ b/cmds/polkit-dbus-helper/src/main.rs
@@ -8,10 +8,10 @@
use nix::unistd::{setuid, Uid, User};
use pam_client::{Context, ConversationHandler, ErrorCode, Flag};
use remowt_polkit_shared::BackendRequest;
+use remowt_ui_prompt::dbus::DbusPrompterProxyBlocking;
+use remowt_ui_prompt::BlockingPrompter;
use tokio::task::{block_in_place, spawn_blocking};
use tracing::trace;
-use remowt_ui_prompt::dbus::DbusPrompterProxyBlocking;
-use remowt_ui_prompt::BlockingPrompter;
use zbus::fdo;
use zbus::message::Header;
use zbus::zvariant::OwnedValue;
cmds/remowt-agent/Cargo.tomldiffbeforeafterboth--- a/cmds/remowt-agent/Cargo.toml
+++ b/cmds/remowt-agent/Cargo.toml
@@ -17,7 +17,6 @@
rand.workspace = true
remowt-link-shared.workspace = true
remowt-plugin.workspace = true
-remowt-pty.workspace = true
serde = { workspace = true, features = ["derive"] }
tempfile.workspace = true
tokio = { workspace = true, features = [
@@ -36,3 +35,4 @@
uuid = { workspace = true, features = ["v4"] }
zbus = { workspace = true, features = ["tokio"] }
zbus_polkit = { workspace = true, features = ["tokio"] }
+remowt-endpoints.workspace = true
cmds/remowt-agent/src/helper/protocol.rsdiffbeforeafterboth--- a/cmds/remowt-agent/src/helper/protocol.rs
+++ b/cmds/remowt-agent/src/helper/protocol.rs
@@ -3,10 +3,10 @@
use anyhow::bail;
use futures::stream::Peekable;
use futures::StreamExt as _;
+use remowt_ui_prompt::Prompter;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _};
use tokio::select;
use tokio_util::codec::{FramedRead, LinesCodec};
-use remowt_ui_prompt::Prompter;
pub async fn run_conversation<R, W, P>(reader: R, mut writer: W, prompt: P) -> anyhow::Result<()>
where
cmds/remowt-agent/src/main.rsdiffbeforeafterboth1use std::borrow::Cow;2use std::collections::{BTreeMap, HashMap};3use std::fs::Permissions;4use std::future::pending;5use std::os::unix::fs::PermissionsExt as _;6use std::path::PathBuf;7use std::sync::{Arc, Mutex, OnceLock};89use bifrostlink::declarative::RemoteEndpoints;10use bifrostlink::Rpc;11use bifrostlink_ports::stdio::from_stdio;12use bifrostlink_ports::unix_socket::from_socket;13use clap::Parser;14use remowt_endpoints::{fs::Fs, pty::Pty, systemd::Systemd};15use remowt_link_shared::{editor::EditorEndpointsClient, Address, BifConfig};16use remowt_polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};17use remowt_ui_prompt::bifrost::PromptEndpointsClient;18use remowt_ui_prompt::rofi::RofiPrompter;19use remowt_ui_prompt::{PrependSourcePrompter, Prompter, Source};20use tokio::fs;21use tokio::net::UnixStream;22use tokio::runtime::Builder;23use tokio::task::AbortHandle;24use tracing::{debug, info, trace};25use zbus::fdo;26use zbus::zvariant::{OwnedValue, Str};27use zbus::{interface, proxy, Connection};28use zbus_polkit::policykit1::Subject;2930use self::helper::{Helper, SocketHelper, SuidHelper};3132pub mod askpass;33pub mod bus;34pub mod editor;35pub mod helper;3637struct CancelTaskOnDrop {38 tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,39 handle: String,40}41impl Drop for CancelTaskOnDrop {42 fn drop(&mut self) {43 debug!("cancel on drop");44 if let Some(task) = self45 .tasks46 .lock()47 .expect("not poisoned")48 .remove(&self.handle)49 {50 task.abort();51 }52 }53}5455struct Agent<H, P> {56 tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,57 helper: H,58 prompter: P,59}60impl<H, P> Agent<H, P> {61 fn new(helper: H, prompter: P) -> Self {62 Agent {63 tasks: Arc::new(Mutex::new(HashMap::new())),64 helper,65 prompter,66 }67 }68}6970#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]71impl<H, P> Agent<H, P>72where73 H: Helper + Clone + Send + Sync + 'static,74 P: Prompter + Clone + Send + Sync + 'static,75{76 /// BeginAuthentication method77 #[allow(clippy::too_many_arguments)]78 async fn begin_authentication(79 &self,80 action_id: String,81 message: String,82 _icon_name: String,83 mut details: BTreeMap<String, String>,84 cookie: String,85 identities: Vec<Identity>,86 ) -> zbus::fdo::Result<()> {87 use std::fmt::Write;88 info!("begin auth");89 let _cancel_guard = Arc::new(OnceLock::new());90 let task = {91 let helper = self.helper.clone();92 let prompter = self.prompter.clone();93 let cookie = cookie.clone();94 let _cancel_guard = _cancel_guard.clone();95 tokio::task::spawn(async move {96 let _cancel_guard = _cancel_guard.clone();97 trace!("conversation task");98 let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);99 if let Some(subject) = details.remove("polkit.caller-pid") {100 let _ = write!(description, "\n<b>Caller:</b> ");101 if let Ok(pid) = subject.parse::<u32>() {102 let _ = write!(description, "{}", PidDisplay(pid));103 } else {104 let _ = write!(description, "{}", emphasize("invalid pid"));105 }106 }107 if let Some(subject) = details.remove("polkit.subject-pid") {108 let _ = write!(description, "\n<b>Subject:</b> ");109 if let Ok(pid) = subject.parse::<u32>() {110 let _ = write!(description, "{}", PidDisplay(pid));111 } else {112 let _ = write!(description, "{}", emphasize("invalid pid"));113 }114 }115 let mut prompter = PrependSourcePrompter {116 source: vec![Source(Cow::Borrowed("polkit agent"))],117 description: description.clone(),118 prompter,119 };120121 let identity_displays: Vec<String> =122 identities.iter().map(|v| v.to_string()).collect();123 let identity_displays: Vec<&str> =124 identity_displays.iter().map(|v| v.as_str()).collect();125 debug!("choose identity");126 let choosen_identity = match identity_displays.len() {127 0 => {128 return Err(fdo::Error::AuthFailed(129 "no identity to authenticate as".to_owned(),130 ))131 }132 1 => 0,133 _ => {134 prompter135 .prompt_enum(136 "Identity",137 "Select identity to use for polkit authorization",138 &identity_displays,139 &[],140 )141 .await?142 }143 };144 debug!("identity chosen");145146 let _ = write!(147 description,148 "\n<b>Identity:</b> {}",149 identities[choosen_identity as usize]150 );151 prompter.description = description;152153 prompter.source.push(Source(Cow::Borrowed("polkit daemon")));154155 helper156 .help_me(157 &cookie,158 prompter,159 identities[choosen_identity as usize].clone(),160 )161 .await162 .map_err(|e| fdo::Error::Failed(e.to_string()))?;163 // let connection = Connection::system().await?;164 // let helper = PolkitHelperProxy::new(&connection).await?;165166 Ok(())167 })168 };169 self.tasks170 .lock()171 .unwrap()172 .insert(cookie.clone(), task.abort_handle());173 debug!("abort handle stored");174 let _ = _cancel_guard.set(CancelTaskOnDrop {175 tasks: self.tasks.clone(),176 handle: cookie.clone(),177 });178179 let _ = task.await;180181 Ok(())182 }183184 /// CancelAuthentication method185 async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {186 debug!("auth cancelled");187 if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {188 debug!("abort handle found");189 abort.abort();190 }191 // debug!("Authentication cancled ! {cookie}");192 Ok(())193 }194}195196const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";197198#[proxy(199 interface = "lach.PolkitHelper",200 default_service = "lach.polkit.helper1",201 default_path = "/lach/PolkitHelper"202)]203trait PolkitHelper {204 fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;205}206207#[derive(Parser)]208enum Opts {209 AskPass {210 prompt: String,211 description: String,212 },213 Editor {214 /// Argument to nvim215 path: String,216 },217 RealAgent {218 #[arg(long)]219 path: Option<PathBuf>,220 /// Expect own address to be AgentPrivileged, skip installing polkit agent221 #[arg(long)]222 privileged: bool,223 },224 LocalAgent,225}226227fn main() -> anyhow::Result<()> {228 // Log to stderr: `privileged-agent` uses stdout as the bifrost transport,229 // so anything written there would corrupt the stream.230 tracing_subscriber::fmt()231 .with_writer(std::io::stderr)232 .without_time()233 .init();234 let opts = Opts::parse();235236 let runtime = Builder::new_current_thread().enable_all().build()?;237238 match opts {239 Opts::AskPass {240 prompt,241 description,242 } => runtime.block_on(askpass::ask(&prompt, description)),243 Opts::LocalAgent => runtime.block_on(main_real()),244 Opts::Editor { path } => runtime.block_on(editor::edit(path)),245 Opts::RealAgent { path, privileged } => runtime.block_on(main_real_agent(path, privileged)),246 }247}248async fn main_real() -> anyhow::Result<()> {249 let conn = Connection::system().await?;250 let helper = SocketHelper {251 fallback: SuidHelper,252 };253 register_auth_agent(&conn, Agent::new(helper, RofiPrompter)).await?;254255 let _conn = conn;256 pending().await257}258async fn main_real_agent(path: Option<PathBuf>, privileged: bool) -> anyhow::Result<()> {259 let address = if privileged {260 Address::AgentPrivileged261 } else {262 Address::Agent263 };264 let mut rpc = Rpc::<BifConfig>::new(address);265266 Fs::new().register_endpoints(&mut rpc);267 Systemd.register_endpoints(&mut rpc);268 Pty::new().register_endpoints(&mut rpc);269270 remowt_plugin::host::serve(&mut rpc);271272 let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));273 let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));274275 let bus = bus::spawn().await?;276 askpass::serve(&bus.conn, user_prompter.clone()).await?;277 editor::serve(&bus.conn, editor_client).await?;278279 let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;280 let exe = std::env::current_exe()?;281 let askpass_helper = helpers.path().join("remowt-askpass");282 let editor_helper = helpers.path().join("remowt-editor");283 {284 let script = format!(285 "#!/bin/sh\nexec {} ask-pass \"password\" \"$1\"\n",286 sh_quote(&exe.to_string_lossy())287 );288 fs::write(&askpass_helper, script).await?;289 fs::set_permissions(&askpass_helper, Permissions::from_mode(0o755)).await?;290 }291 {292 let script = format!(293 "#!/bin/sh\nexec {} editor \"$1\"\n",294 sh_quote(&exe.to_string_lossy())295 );296 fs::write(&editor_helper, script).await?;297 fs::set_permissions(&editor_helper, Permissions::from_mode(0o755)).await?;298 }299300 // Safety: Hoping tokio own threads won't read any of those...301 unsafe {302 prepend_path(helpers.path());303 std::env::set_var("SUDO_ASKPASS", &askpass_helper);304 std::env::set_var("SSH_ASKPASS", &askpass_helper);305 std::env::set_var("SSH_ASKPASS_REQUIRE", "force");306 std::env::set_var("EDITOR", &editor_helper);307 std::env::set_var("VISUAL", &editor_helper);308 std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);309 }310311 let port = match path {312 Some(path) => from_socket(UnixStream::connect(path).await?),313 None => from_stdio(),314 };315 rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));316317 let polkit_conn = if !privileged {318 // The unprivileged agent doubles as a polkit authentication agent so319 // `run0` (e.g. our own elevation) routes its prompt to the User over320 // bifrost instead of failing on a tty-less session.321 let conn = Connection::system().await?;322 let helper = SocketHelper {323 fallback: SuidHelper,324 };325 register_auth_agent(&conn, Agent::new(helper, user_prompter)).await?;326 Some(conn)327 } else {328 None329 };330331 let _keep_alive = (bus, helpers, polkit_conn);332 pending().await333}334335async fn register_auth_agent<H, P>(conn: &Connection, agent: Agent<H, P>) -> anyhow::Result<()>336where337 H: Helper + Clone + Send + Sync + 'static,338 P: Prompter + Clone + Send + Sync + 'static,339{340 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;341 conn.object_server().at(OBJ_PATH, agent).await?;342343 let subject = auth_agent_subject()?;344 proxy345 .register_authentication_agent(&subject, "C", OBJ_PATH)346 .await?;347 debug!(kind = subject.subject_kind, "registered polkit agent");348 Ok(())349}350351fn auth_agent_subject() -> anyhow::Result<Subject> {352 let mut details = HashMap::new();353 if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {354 let val: OwnedValue = Str::from(session_id).into();355 details.insert("session-id".to_string(), val);356 return Ok(Subject {357 subject_kind: "unix-session".to_string(),358 subject_details: details,359 });360 }361362 details.insert("pid".to_string(), OwnedValue::from(std::process::id()));363 Ok(Subject {364 subject_kind: "unix-process".to_string(),365 subject_details: details,366 })367}368369fn sh_quote(s: &str) -> String {370 format!("'{}'", s.replace('\'', "'\\''"))371}372373/// Prepend `dir` to the process `PATH`.374///375/// # SAFETY376///377/// Same as `set_var`378unsafe fn prepend_path(dir: &std::path::Path) {379 let value = match std::env::var_os("PATH") {380 Some(existing) => {381 let mut v = dir.as_os_str().to_owned();382 v.push(":");383 v.push(existing);384 v385 }386 None => dir.as_os_str().to_owned(),387 };388 unsafe {389 std::env::set_var("PATH", value);390 }391}cmds/remowt-ssh/src/main.rsdiffbeforeafterboth--- a/cmds/remowt-ssh/src/main.rs
+++ b/cmds/remowt-ssh/src/main.rs
@@ -13,13 +13,13 @@
use remowt_client::editor::SshEditor;
use remowt_client::{AgentBundle, Remowt};
use remowt_link_shared::editor::serve_editor;
+use remowt_ui_prompt::bifrost::serve_prompts;
+use remowt_ui_prompt::rofi::RofiPrompter;
+use remowt_ui_prompt::{PrependSourcePrompter, Source};
use tokio::io::unix::AsyncFd;
use tokio::io::{AsyncRead, ReadBuf};
use tokio::signal::unix::{signal, SignalKind};
use tracing::info;
-use remowt_ui_prompt::bifrost::serve_prompts;
-use remowt_ui_prompt::rofi::RofiPrompter;
-use remowt_ui_prompt::{PrependSourcePrompter, Source};
#[derive(Parser)]
struct Opts {
crates/remowt-client/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-client/Cargo.toml
+++ b/crates/remowt-client/Cargo.toml
@@ -19,3 +19,4 @@
tokio = { workspace = true, features = ["net", "io-util", "rt", "sync", "macros", "process"] }
tracing.workspace = true
uuid = { workspace = true, features = ["v4"] }
+remowt-endpoints.workspace = true
crates/remowt-client/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-client/src/lib.rs
+++ b/crates/remowt-client/src/lib.rs
@@ -1,7 +1,7 @@
use std::collections::HashMap;
-use std::io;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
+use std::{env, io};
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use bifrostlink::declarative::RemoteEndpoints;
@@ -9,11 +9,13 @@
use bifrostlink_ports::unix_socket::from_socket;
use bytes::{Bytes, BytesMut};
use camino::{Utf8Path, Utf8PathBuf};
+use remowt_endpoints::{
+ fs::Fs,
+ pty::{Pty, PtyClient, ShellId},
+ systemd::Systemd,
+};
use remowt_link_shared::plugin::PluginEndpointsClient;
-use remowt_link_shared::{
- Address, BifConfig, ElevateEndpoints, ElevateError, Elevator, Fs, Pty, PtyClient, ShellId,
- Systemd,
-};
+use remowt_link_shared::{Address, BifConfig, ElevateEndpoints, ElevateError, Elevator};
use russh::client::{connect, Config, Handle, Handler, Msg, Session};
use russh::keys::agent::client::AgentClient;
use russh::keys::agent::AgentIdentity;
@@ -220,8 +222,8 @@
}
fn find_in_path(name: &str) -> Option<std::path::PathBuf> {
- let path = std::env::var_os("PATH")?;
- std::env::split_paths(&path)
+ let path = env::var_os("PATH")?;
+ env::split_paths(&path)
.map(|dir| dir.join(name))
.find(|p| p.is_file())
}
@@ -383,7 +385,7 @@
impl Remowt {
pub async fn connect(host: &str, bundle: &AgentBundle) -> Result<Self> {
let conf = russh_config::parse_home(host)?;
- let port = conf.host_config.port.unwrap_or(22);
+ let port = conf.host_config.port.or(conf.port).unwrap_or(22);
let hostname = conf
.host_config
.hostname
@@ -392,7 +394,7 @@
let user = conf
.user
.clone()
- .unwrap_or_else(|| std::env::var("USER").unwrap_or_else(|_| "root".to_owned()));
+ .unwrap_or_else(|| env::var("USER").unwrap_or_else(|_| "root".to_owned()));
let subs: Subs = Arc::new(Mutex::new(HashMap::new()));
let mut sess = connect(
@@ -548,7 +550,7 @@
port_from_channel(ch)
}
Transport::Local { agent_path, .. } => {
- let sock = std::env::temp_dir()
+ let sock = env::temp_dir()
.join(format!("remowt-priv-{}.sock", uuid::Uuid::new_v4()));
let _ = std::fs::remove_file(&sock);
let listener = UnixListener::bind(&sock)?;
crates/remowt-endpoints/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "remowt-endpoints"
+description = "Nix daemon proxy"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bifrostlink-macros.workspace = true
+camino.workspace = true
+serde = { workspace = true }
+tempfile.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = ["net", "io-util", "rt", "process"] }
+tracing.workspace = true
+uuid.workspace = true
+nix = { workspace = true, features = ["process", "term"] }
+zbus.workspace = true
crates/remowt-endpoints/src/fs.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/fs.rs
@@ -0,0 +1,105 @@
+use std::io::ErrorKind;
+use std::str::FromStr;
+use std::sync::Mutex;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use serde::{Deserialize, Serialize};
+use tempfile::TempDir;
+
+#[derive(Default)]
+pub struct Fs {
+ tempdirs: Mutex<Vec<TempDir>>,
+}
+
+impl Fs {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("file not found")]
+ NotFound,
+ #[error("file name/contents is not utf8")]
+ InvalidUtf8,
+ #[error("unknown fs error")]
+ Unknown,
+}
+
+#[endpoints(ns = 1)]
+impl Fs {
+ #[endpoints(id = 1)]
+ async fn read_file_tiny(&self, path: Utf8PathBuf) -> Result<Vec<u8>, Error> {
+ match tokio::fs::read(path).await {
+ Ok(v) => Ok(v),
+ Err(e) if e.kind() == ErrorKind::NotFound => Err(Error::NotFound),
+ _ => Err(Error::Unknown),
+ }
+ }
+ #[endpoints(id = 2)]
+ async fn file_exists(&self, path: Utf8PathBuf) -> bool {
+ tokio::fs::try_exists(path).await.unwrap_or(false)
+ }
+ #[endpoints(id = 3)]
+ async fn read_dir_raw(&self, path: Utf8PathBuf) -> Result<Vec<Utf8PathBuf>, Error> {
+ let mut dir = match tokio::fs::read_dir(path).await {
+ Ok(dir) => dir,
+ Err(e) if e.kind() == ErrorKind::NotFound => return Err(Error::NotFound),
+ Err(_) => return Err(Error::Unknown),
+ };
+ let mut out = Vec::new();
+ while let Ok(Some(entry)) = dir.next_entry().await {
+ let name = Utf8PathBuf::try_from(entry.file_name()).map_err(|_| Error::InvalidUtf8)?;
+ out.push(name);
+ }
+ Ok(out)
+ }
+ #[endpoints(id = 4)]
+ async fn mktemp_dir_raw(&self) -> Result<Utf8PathBuf, Error> {
+ let dir = tempfile::Builder::new()
+ .prefix("remowt.")
+ .tempdir()
+ .map_err(|_| Error::Unknown)?;
+ let mut tempdirs = self.tempdirs.lock().expect("not poisoned");
+ let path = Utf8PathBuf::try_from(dir.path().to_owned()).map_err(|_| Error::InvalidUtf8);
+ tempdirs.push(dir);
+ path
+ }
+ #[endpoints(id = 5)]
+ async fn rm_file(&self, path: Utf8PathBuf) -> Result<(), Error> {
+ match tokio::fs::remove_file(path).await {
+ Ok(()) => Ok(()),
+ Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
+ Err(_) => Err(Error::Unknown),
+ }
+ }
+}
+
+impl<C: Config> FsClient<C> {
+ pub async fn read_file_text(&self, path: impl Into<Utf8PathBuf>) -> Result<String, Error> {
+ let v = self
+ .read_file_tiny(path.into())
+ .await
+ .map_err(|_| Error::Unknown)?;
+ let v = v?;
+ String::from_utf8(v).map_err(|_| Error::InvalidUtf8)
+ }
+ pub async fn read_file_value<T: FromStr>(
+ &self,
+ path: impl Into<Utf8PathBuf>,
+ ) -> Result<Result<T, T::Err>, Error> {
+ let text = self.read_file_text(path).await?;
+ Ok(T::from_str(&text))
+ }
+ pub async fn mktemp_dir(&self) -> Result<Utf8PathBuf, Error> {
+ self.mktemp_dir_raw().await.map_err(|_| Error::Unknown)?
+ }
+ pub async fn read_dir(&self, path: impl Into<Utf8PathBuf>) -> Result<Vec<Utf8PathBuf>, Error> {
+ self.read_dir_raw(path.into())
+ .await
+ .map_err(|_| Error::Unknown)?
+ }
+}
crates/remowt-endpoints/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/lib.rs
@@ -0,0 +1,4 @@
+pub mod fs;
+pub mod nix_daemon;
+pub mod pty;
+pub mod systemd;
crates/remowt-endpoints/src/nix_daemon.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/nix_daemon.rs
@@ -0,0 +1,65 @@
+use std::process::Stdio;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use serde::{Deserialize, Serialize};
+use std::result::Result;
+use tokio::process::Command;
+
+pub const NIX_DAEMON_SOCKET: &str = "/nix/var/nix/daemon-socket/socket";
+
+pub struct NixDaemon;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("nix daemon unavailable: {0}")]
+ DaemonUnavailable(String),
+ #[error("tunnel socket unavailable: {0}")]
+ Tunnel(String),
+}
+
+#[endpoints(ns = 4)]
+impl NixDaemon {
+ #[endpoints(id = 1)]
+ async fn connect_daemon(&self, socket: String) -> Result<(), Error> {
+ let mut daemon = tokio::net::UnixStream::connect(NIX_DAEMON_SOCKET)
+ .await
+ .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
+ let mut tunnel = tokio::net::UnixStream::connect(&socket)
+ .await
+ .map_err(|e| Error::Tunnel(e.to_string()))?;
+ tokio::spawn(async move {
+ if let Err(e) = tokio::io::copy_bidirectional(&mut daemon, &mut tunnel).await {
+ tracing::debug!("nix daemon tunnel ended: {e}");
+ }
+ });
+ Ok(())
+ }
+
+ #[endpoints(id = 2)]
+ async fn serve_store(&self, store: String, socket: String) -> Result<(), Error> {
+ let mut child = Command::new("nix-daemon")
+ .arg("--stdio")
+ .arg("--store")
+ .arg(&store)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
+ let tunnel = tokio::net::UnixStream::connect(&socket)
+ .await
+ .map_err(|e| Error::Tunnel(e.to_string()))?;
+ let mut stdin = child.stdin.take().expect("piped");
+ let mut stdout = child.stdout.take().expect("piped");
+ tokio::spawn(async move {
+ let mut tunnel = tunnel;
+ let (mut tr, mut tw) = tunnel.split();
+ let _ = tokio::join!(
+ tokio::io::copy(&mut tr, &mut stdin),
+ tokio::io::copy(&mut stdout, &mut tw),
+ );
+ let _ = child.wait().await;
+ });
+ Ok(())
+ }
+}
crates/remowt-endpoints/src/pty.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/pty.rs
@@ -0,0 +1,256 @@
+use std::collections::HashMap;
+use std::io;
+use std::os::fd::{AsRawFd, OwnedFd};
+use std::pin::Pin;
+use std::process::Stdio;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use nix::libc;
+use nix::pty::{openpty, OpenptyResult, Winsize};
+use serde::{Deserialize, Serialize};
+use tokio::io::unix::AsyncFd;
+use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
+use tokio::net::UnixStream;
+use tracing::{info, warn};
+
+pub type ShellId = u64;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("openpty failed: {0}")]
+ Open(String),
+ #[error("failed to spawn shell: {0}")]
+ Spawn(String),
+ #[error("failed to connect to forwarded socket: {0}")]
+ Connect(String),
+ #[error("no shell with that id")]
+ NoSuchShell,
+ #[error("resize failed: {0}")]
+ Resize(String),
+ #[error("io error: {0}")]
+ Io(String),
+}
+
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Self {
+ Error::Io(e.to_string())
+ }
+}
+
+#[derive(Clone, Default)]
+pub struct Pty {
+ shells: Arc<Mutex<HashMap<ShellId, OwnedFd>>>,
+ next_id: Arc<AtomicU64>,
+}
+
+impl Pty {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+#[endpoints(ns = 7)]
+impl Pty {
+ #[endpoints(id = 1)]
+ async fn open_shell(
+ &self,
+ socket_path: Utf8PathBuf,
+ term: String,
+ cols: u16,
+ rows: u16,
+ ) -> Result<ShellId, Error> {
+ let ws = Winsize {
+ ws_row: rows,
+ ws_col: cols,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ let OpenptyResult { master, slave } =
+ openpty(Some(&ws), None).map_err(|e| Error::Open(e.to_string()))?;
+
+ let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_owned());
+
+ let slave_in = slave.try_clone()?;
+ let slave_out = slave.try_clone()?;
+ let slave_err = slave;
+
+ let mut cmd = tokio::process::Command::new(&shell);
+ cmd.env("TERM", &term);
+ if let Ok(home) = std::env::var("HOME") {
+ cmd.current_dir(home);
+ }
+ cmd.stdin(Stdio::from(slave_in));
+ cmd.stdout(Stdio::from(slave_out));
+ cmd.stderr(Stdio::from(slave_err));
+ // SAFETY: only async-signal-safe calls (setsid, ioctl) before exec.
+ unsafe {
+ cmd.pre_exec(|| {
+ nix::unistd::setsid().map_err(|e| io::Error::from_raw_os_error(e as i32))?;
+ if libc::ioctl(0, libc::TIOCSCTTY as _, 0) < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(())
+ });
+ }
+
+ let mut child = cmd.spawn().map_err(|e| Error::Spawn(e.to_string()))?;
+
+ let resize_fd = master.try_clone()?;
+ let id = self.next_id.fetch_add(1, Ordering::Relaxed);
+ self.shells
+ .lock()
+ .expect("not poisoned")
+ .insert(id, resize_fd);
+
+ let sock = match UnixStream::connect(&socket_path).await {
+ Ok(s) => s,
+ Err(e) => {
+ self.shells.lock().expect("not poisoned").remove(&id);
+ let _ = child.kill().await;
+ return Err(Error::Connect(e.to_string()));
+ }
+ };
+ let pty = AsyncPty::new(master)?;
+
+ info!(id, shell, "shell opened");
+ let shells = self.shells.clone();
+ tokio::spawn(async move {
+ let mut pty = pty;
+ let mut sock = sock;
+ if let Err(e) = tokio::io::copy_bidirectional(&mut pty, &mut sock).await {
+ warn!(id, "shell pump ended: {e}");
+ }
+ let _ = child.kill().await;
+ shells.lock().expect("not poisoned").remove(&id);
+ info!(id, "shell closed");
+ });
+
+ Ok(id)
+ }
+
+ #[endpoints(id = 2)]
+ async fn resize(&self, id: ShellId, cols: u16, rows: u16) -> Result<(), Error> {
+ let ws = libc::winsize {
+ ws_row: rows,
+ ws_col: cols,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ let shells = self.shells.lock().expect("not poisoned");
+ let fd = shells.get(&id).ok_or(Error::NoSuchShell)?;
+ // SAFETY: `fd` is a live PTY master
+ let rc = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCSWINSZ as _, &ws) };
+ if rc < 0 {
+ return Err(Error::Resize(io::Error::last_os_error().to_string()));
+ }
+ Ok(())
+ }
+}
+
+struct AsyncPty {
+ fd: AsyncFd<OwnedFd>,
+}
+
+impl AsyncPty {
+ fn new(fd: OwnedFd) -> io::Result<Self> {
+ let raw = fd.as_raw_fd();
+ // SAFETY: standard F_GETFL/F_SETFL round-trip on a valid fd.
+ unsafe {
+ let flags = libc::fcntl(raw, libc::F_GETFL);
+ if flags < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ if libc::fcntl(raw, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ }
+ Ok(Self {
+ fd: AsyncFd::new(fd)?,
+ })
+ }
+}
+
+impl AsyncRead for AsyncPty {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_read_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let unfilled = buf.initialize_unfilled();
+ let res = guard.try_io(|inner| {
+ let fd = inner.get_ref().as_raw_fd();
+ // SAFETY: writing into `unfilled`'s own backing storage.
+ let n = unsafe { libc::read(fd, unfilled.as_mut_ptr().cast(), unfilled.len()) };
+ if n < 0 {
+ let err = io::Error::last_os_error();
+ if err.raw_os_error() == Some(libc::EIO) {
+ Ok(0)
+ } else {
+ Err(err)
+ }
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(Ok(n)) => {
+ buf.advance(n);
+ return Poll::Ready(Ok(()));
+ }
+ Ok(Err(e)) => return Poll::Ready(Err(e)),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+}
+
+impl AsyncWrite for AsyncPty {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_write_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let res = guard.try_io(|inner| {
+ let fd = inner.get_ref().as_raw_fd();
+ // SAFETY: reading from `buf` for `buf.len()` bytes.
+ let n = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
+ if n < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(r) => return Poll::Ready(r),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+
+ fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ Poll::Ready(Ok(()))
+ }
+
+ fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ Poll::Ready(Ok(()))
+ }
+}
crates/remowt-endpoints/src/systemd.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/systemd.rs
@@ -0,0 +1,54 @@
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use serde::{Deserialize, Serialize};
+use zbus::proxy;
+use zbus::zvariant::OwnedObjectPath;
+
+pub struct Systemd;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("systemd request failed: {0}")]
+ Failed(String),
+}
+
+#[proxy(
+ interface = "org.freedesktop.systemd1.Manager",
+ default_service = "org.freedesktop.systemd1",
+ default_path = "/org/freedesktop/systemd1"
+)]
+trait Manager {
+ fn start_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
+ fn stop_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
+}
+
+async fn manager() -> Result<ManagerProxy<'static>, Error> {
+ let conn = zbus::Connection::system()
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ ManagerProxy::new(&conn)
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))
+}
+
+#[endpoints(ns = 5)]
+impl Systemd {
+ #[endpoints(id = 1)]
+ async fn start(&self, unit: String) -> Result<(), Error> {
+ manager()
+ .await?
+ .start_unit(&unit, "replace")
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ Ok(())
+ }
+ #[endpoints(id = 2)]
+ async fn stop(&self, unit: String) -> Result<(), Error> {
+ manager()
+ .await?
+ .stop_unit(&unit, "replace")
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ Ok(())
+ }
+}
crates/remowt-fs/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-fs/Cargo.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-[package]
-name = "remowt-fs"
-description = "Filesystem endpoint for remowt/bifrostlink"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-camino = { workspace = true, features = ["serde1"] }
-serde = { workspace = true, features = ["derive"] }
-tempfile.workspace = true
-thiserror.workspace = true
-tokio = { workspace = true, features = ["fs"] }
crates/remowt-fs/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-fs/src/lib.rs
+++ /dev/null
@@ -1,105 +0,0 @@
-use std::io::ErrorKind;
-use std::str::FromStr;
-use std::sync::Mutex;
-
-use bifrostlink::declarative::endpoints;
-use bifrostlink::Config;
-use camino::Utf8PathBuf;
-use serde::{Deserialize, Serialize};
-use tempfile::TempDir;
-
-#[derive(Default)]
-pub struct Fs {
- tempdirs: Mutex<Vec<TempDir>>,
-}
-
-impl Fs {
- pub fn new() -> Self {
- Self::default()
- }
-}
-
-#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
-pub enum Error {
- #[error("file not found")]
- NotFound,
- #[error("file name/contents is not utf8")]
- InvalidUtf8,
- #[error("unknown fs error")]
- Unknown,
-}
-
-#[endpoints(ns = 1)]
-impl Fs {
- #[endpoints(id = 1)]
- async fn read_file_tiny(&self, path: Utf8PathBuf) -> Result<Vec<u8>, Error> {
- match tokio::fs::read(path).await {
- Ok(v) => Ok(v),
- Err(e) if e.kind() == ErrorKind::NotFound => Err(Error::NotFound),
- _ => Err(Error::Unknown),
- }
- }
- #[endpoints(id = 2)]
- async fn file_exists(&self, path: Utf8PathBuf) -> bool {
- tokio::fs::try_exists(path).await.unwrap_or(false)
- }
- #[endpoints(id = 3)]
- async fn read_dir_raw(&self, path: Utf8PathBuf) -> Result<Vec<Utf8PathBuf>, Error> {
- let mut dir = match tokio::fs::read_dir(path).await {
- Ok(dir) => dir,
- Err(e) if e.kind() == ErrorKind::NotFound => return Err(Error::NotFound),
- Err(_) => return Err(Error::Unknown),
- };
- let mut out = Vec::new();
- while let Ok(Some(entry)) = dir.next_entry().await {
- let name = Utf8PathBuf::try_from(entry.file_name()).map_err(|_| Error::InvalidUtf8)?;
- out.push(name);
- }
- Ok(out)
- }
- #[endpoints(id = 4)]
- async fn mktemp_dir_raw(&self) -> Result<Utf8PathBuf, Error> {
- let dir = tempfile::Builder::new()
- .prefix("remowt.")
- .tempdir()
- .map_err(|_| Error::Unknown)?;
- let mut tempdirs = self.tempdirs.lock().expect("not poisoned");
- let path = Utf8PathBuf::try_from(dir.path().to_owned()).map_err(|_| Error::InvalidUtf8);
- tempdirs.push(dir);
- path
- }
- #[endpoints(id = 5)]
- async fn rm_file(&self, path: Utf8PathBuf) -> Result<(), Error> {
- match tokio::fs::remove_file(path).await {
- Ok(()) => Ok(()),
- Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
- Err(_) => Err(Error::Unknown),
- }
- }
-}
-
-impl<C: Config> FsClient<C> {
- pub async fn read_file_text(&self, path: impl Into<Utf8PathBuf>) -> Result<String, Error> {
- let v = self
- .read_file_tiny(path.into())
- .await
- .map_err(|_| Error::Unknown)?;
- let v = v?;
- String::from_utf8(v).map_err(|_| Error::InvalidUtf8)
- }
- pub async fn read_file_value<T: FromStr>(
- &self,
- path: impl Into<Utf8PathBuf>,
- ) -> Result<Result<T, T::Err>, Error> {
- let text = self.read_file_text(path).await?;
- Ok(T::from_str(&text))
- }
- pub async fn mktemp_dir(&self) -> Result<Utf8PathBuf, Error> {
- self.mktemp_dir_raw().await.map_err(|_| Error::Unknown)?
- }
- pub async fn read_dir(&self, path: impl Into<Utf8PathBuf>) -> Result<Vec<Utf8PathBuf>, Error> {
- self.read_dir_raw(path.into())
- .await
- .map_err(|_| Error::Unknown)?
- }
-}
crates/remowt-link-shared/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-link-shared/Cargo.toml
+++ b/crates/remowt-link-shared/Cargo.toml
@@ -12,8 +12,5 @@
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["fs"] }
-remowt-fs.workspace = true
-remowt-systemd.workspace = true
remowt-ui-prompt.workspace = true
camino = { workspace = true, features = ["serde1"] }
-remowt-pty.workspace = true
crates/remowt-link-shared/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-link-shared/src/lib.rs
+++ b/crates/remowt-link-shared/src/lib.rs
@@ -21,10 +21,6 @@
pub mod plugin;
-pub use remowt_fs::{Error as FsError, Fs, FsClient};
-pub use remowt_pty::{Error as PtyError, Pty, PtyClient, ShellId};
-pub use remowt_systemd::{Error as SystemdError, Systemd, SystemdClient};
-
#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
pub enum ElevateError {
#[error("elevation failed: {0}")]
crates/remowt-nix-daemon/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-nix-daemon/Cargo.toml
+++ /dev/null
@@ -1,18 +0,0 @@
-[package]
-name = "remowt-nix-daemon"
-description = "Nix daemon proxy"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-anyhow.workspace = true
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-camino.workspace = true
-remowt-client.workspace = true
-serde = { workspace = true }
-thiserror.workspace = true
-tokio = { workspace = true, features = ["net", "io-util", "rt", "process"] }
-tracing.workspace = true
-uuid.workspace = true
crates/remowt-nix-daemon/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-nix-daemon/src/lib.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-use std::process::Stdio;
-
-use bifrostlink::declarative::endpoints;
-use bifrostlink::Config;
-use serde::{Deserialize, Serialize};
-use std::result::Result;
-use tokio::process::Command;
-
-pub const NIX_DAEMON_SOCKET: &str = "/nix/var/nix/daemon-socket/socket";
-
-pub struct NixDaemon;
-
-#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
-pub enum Error {
- #[error("nix daemon unavailable: {0}")]
- DaemonUnavailable(String),
- #[error("tunnel socket unavailable: {0}")]
- Tunnel(String),
-}
-
-#[endpoints(ns = 4)]
-impl NixDaemon {
- #[endpoints(id = 1)]
- async fn connect_daemon(&self, socket: String) -> Result<(), Error> {
- let mut daemon = tokio::net::UnixStream::connect(NIX_DAEMON_SOCKET)
- .await
- .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
- let mut tunnel = tokio::net::UnixStream::connect(&socket)
- .await
- .map_err(|e| Error::Tunnel(e.to_string()))?;
- tokio::spawn(async move {
- if let Err(e) = tokio::io::copy_bidirectional(&mut daemon, &mut tunnel).await {
- tracing::debug!("nix daemon tunnel ended: {e}");
- }
- });
- Ok(())
- }
-
- #[endpoints(id = 2)]
- async fn serve_store(&self, store: String, socket: String) -> Result<(), Error> {
- let mut child = Command::new("nix-daemon")
- .arg("--stdio")
- .arg("--store")
- .arg(&store)
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .spawn()
- .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
- let tunnel = tokio::net::UnixStream::connect(&socket)
- .await
- .map_err(|e| Error::Tunnel(e.to_string()))?;
- let mut stdin = child.stdin.take().expect("piped");
- let mut stdout = child.stdout.take().expect("piped");
- tokio::spawn(async move {
- let mut tunnel = tunnel;
- let (mut tr, mut tw) = tunnel.split();
- let _ = tokio::join!(
- tokio::io::copy(&mut tr, &mut stdin),
- tokio::io::copy(&mut stdout, &mut tw),
- );
- let _ = child.wait().await;
- });
- Ok(())
- }
-}
crates/remowt-plugin/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-plugin/src/lib.rs
+++ b/crates/remowt-plugin/src/lib.rs
@@ -8,7 +8,7 @@
pub mod host;
pub use bifrostlink;
-pub use remowt_link_shared::{self, Address, BifConfig, Fs, Pty, Systemd};
+pub use remowt_link_shared::{self, Address, BifConfig};
pub fn plugin_index() -> Result<u16> {
let arg = std::env::args()
crates/remowt-pty/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-pty/Cargo.toml
+++ /dev/null
@@ -1,23 +0,0 @@
-[package]
-name = "remowt-pty"
-description = "PTY/shell endpoint for remowt"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-camino = { workspace = true, features = ["serde1"] }
-nix = { workspace = true, features = ["process", "term"] }
-serde = { workspace = true, features = ["derive"] }
-thiserror.workspace = true
-tokio = { workspace = true, features = [
- "net",
- "io-util",
- "rt",
- "macros",
- "process",
- "sync",
-] }
-tracing.workspace = true
crates/remowt-pty/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-pty/src/lib.rs
+++ /dev/null
@@ -1,256 +0,0 @@
-use std::collections::HashMap;
-use std::io;
-use std::os::fd::{AsRawFd, OwnedFd};
-use std::pin::Pin;
-use std::process::Stdio;
-use std::sync::atomic::{AtomicU64, Ordering};
-use std::sync::{Arc, Mutex};
-use std::task::{Context, Poll};
-
-use bifrostlink::declarative::endpoints;
-use bifrostlink::Config;
-use camino::Utf8PathBuf;
-use nix::libc;
-use nix::pty::{openpty, OpenptyResult, Winsize};
-use serde::{Deserialize, Serialize};
-use tokio::io::unix::AsyncFd;
-use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
-use tokio::net::UnixStream;
-use tracing::{info, warn};
-
-pub type ShellId = u64;
-
-#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
-pub enum Error {
- #[error("openpty failed: {0}")]
- Open(String),
- #[error("failed to spawn shell: {0}")]
- Spawn(String),
- #[error("failed to connect to forwarded socket: {0}")]
- Connect(String),
- #[error("no shell with that id")]
- NoSuchShell,
- #[error("resize failed: {0}")]
- Resize(String),
- #[error("io error: {0}")]
- Io(String),
-}
-
-impl From<io::Error> for Error {
- fn from(e: io::Error) -> Self {
- Error::Io(e.to_string())
- }
-}
-
-#[derive(Clone, Default)]
-pub struct Pty {
- shells: Arc<Mutex<HashMap<ShellId, OwnedFd>>>,
- next_id: Arc<AtomicU64>,
-}
-
-impl Pty {
- pub fn new() -> Self {
- Self::default()
- }
-}
-
-#[endpoints(ns = 7)]
-impl Pty {
- #[endpoints(id = 1)]
- async fn open_shell(
- &self,
- socket_path: Utf8PathBuf,
- term: String,
- cols: u16,
- rows: u16,
- ) -> Result<ShellId, Error> {
- let ws = Winsize {
- ws_row: rows,
- ws_col: cols,
- ws_xpixel: 0,
- ws_ypixel: 0,
- };
- let OpenptyResult { master, slave } =
- openpty(Some(&ws), None).map_err(|e| Error::Open(e.to_string()))?;
-
- let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_owned());
-
- let slave_in = slave.try_clone()?;
- let slave_out = slave.try_clone()?;
- let slave_err = slave;
-
- let mut cmd = tokio::process::Command::new(&shell);
- cmd.env("TERM", &term);
- if let Ok(home) = std::env::var("HOME") {
- cmd.current_dir(home);
- }
- cmd.stdin(Stdio::from(slave_in));
- cmd.stdout(Stdio::from(slave_out));
- cmd.stderr(Stdio::from(slave_err));
- // SAFETY: only async-signal-safe calls (setsid, ioctl) before exec.
- unsafe {
- cmd.pre_exec(|| {
- nix::unistd::setsid().map_err(|e| io::Error::from_raw_os_error(e as i32))?;
- if libc::ioctl(0, libc::TIOCSCTTY as _, 0) < 0 {
- return Err(io::Error::last_os_error());
- }
- Ok(())
- });
- }
-
- let mut child = cmd.spawn().map_err(|e| Error::Spawn(e.to_string()))?;
-
- let resize_fd = master.try_clone()?;
- let id = self.next_id.fetch_add(1, Ordering::Relaxed);
- self.shells
- .lock()
- .expect("not poisoned")
- .insert(id, resize_fd);
-
- let sock = match UnixStream::connect(&socket_path).await {
- Ok(s) => s,
- Err(e) => {
- self.shells.lock().expect("not poisoned").remove(&id);
- let _ = child.kill().await;
- return Err(Error::Connect(e.to_string()));
- }
- };
- let pty = AsyncPty::new(master)?;
-
- info!(id, shell, "shell opened");
- let shells = self.shells.clone();
- tokio::spawn(async move {
- let mut pty = pty;
- let mut sock = sock;
- if let Err(e) = tokio::io::copy_bidirectional(&mut pty, &mut sock).await {
- warn!(id, "shell pump ended: {e}");
- }
- let _ = child.kill().await;
- shells.lock().expect("not poisoned").remove(&id);
- info!(id, "shell closed");
- });
-
- Ok(id)
- }
-
- #[endpoints(id = 2)]
- async fn resize(&self, id: ShellId, cols: u16, rows: u16) -> Result<(), Error> {
- let ws = libc::winsize {
- ws_row: rows,
- ws_col: cols,
- ws_xpixel: 0,
- ws_ypixel: 0,
- };
- let shells = self.shells.lock().expect("not poisoned");
- let fd = shells.get(&id).ok_or(Error::NoSuchShell)?;
- // SAFETY: `fd` is a live PTY master
- let rc = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCSWINSZ as _, &ws) };
- if rc < 0 {
- return Err(Error::Resize(io::Error::last_os_error().to_string()));
- }
- Ok(())
- }
-}
-
-struct AsyncPty {
- fd: AsyncFd<OwnedFd>,
-}
-
-impl AsyncPty {
- fn new(fd: OwnedFd) -> io::Result<Self> {
- let raw = fd.as_raw_fd();
- // SAFETY: standard F_GETFL/F_SETFL round-trip on a valid fd.
- unsafe {
- let flags = libc::fcntl(raw, libc::F_GETFL);
- if flags < 0 {
- return Err(io::Error::last_os_error());
- }
- if libc::fcntl(raw, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
- return Err(io::Error::last_os_error());
- }
- }
- Ok(Self {
- fd: AsyncFd::new(fd)?,
- })
- }
-}
-
-impl AsyncRead for AsyncPty {
- fn poll_read(
- self: Pin<&mut Self>,
- cx: &mut Context<'_>,
- buf: &mut ReadBuf<'_>,
- ) -> Poll<io::Result<()>> {
- let this = self.get_mut();
- loop {
- let mut guard = match this.fd.poll_read_ready(cx) {
- Poll::Ready(Ok(g)) => g,
- Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
- Poll::Pending => return Poll::Pending,
- };
- let unfilled = buf.initialize_unfilled();
- let res = guard.try_io(|inner| {
- let fd = inner.get_ref().as_raw_fd();
- // SAFETY: writing into `unfilled`'s own backing storage.
- let n = unsafe { libc::read(fd, unfilled.as_mut_ptr().cast(), unfilled.len()) };
- if n < 0 {
- let err = io::Error::last_os_error();
- if err.raw_os_error() == Some(libc::EIO) {
- Ok(0)
- } else {
- Err(err)
- }
- } else {
- Ok(n as usize)
- }
- });
- match res {
- Ok(Ok(n)) => {
- buf.advance(n);
- return Poll::Ready(Ok(()));
- }
- Ok(Err(e)) => return Poll::Ready(Err(e)),
- Err(_would_block) => continue,
- }
- }
- }
-}
-
-impl AsyncWrite for AsyncPty {
- fn poll_write(
- self: Pin<&mut Self>,
- cx: &mut Context<'_>,
- buf: &[u8],
- ) -> Poll<io::Result<usize>> {
- let this = self.get_mut();
- loop {
- let mut guard = match this.fd.poll_write_ready(cx) {
- Poll::Ready(Ok(g)) => g,
- Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
- Poll::Pending => return Poll::Pending,
- };
- let res = guard.try_io(|inner| {
- let fd = inner.get_ref().as_raw_fd();
- // SAFETY: reading from `buf` for `buf.len()` bytes.
- let n = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
- if n < 0 {
- Err(io::Error::last_os_error())
- } else {
- Ok(n as usize)
- }
- });
- match res {
- Ok(r) => return Poll::Ready(r),
- Err(_would_block) => continue,
- }
- }
- }
-
- fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
- Poll::Ready(Ok(()))
- }
-
- fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
- Poll::Ready(Ok(()))
- }
-}
crates/remowt-systemd/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-systemd/Cargo.toml
+++ /dev/null
@@ -1,13 +0,0 @@
-[package]
-name = "remowt-systemd"
-description = "systemd control endpoint for remowt/bifrostlink (over D-Bus)"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-serde = { workspace = true, features = ["derive"] }
-thiserror.workspace = true
-zbus = { workspace = true, features = ["tokio"] }
crates/remowt-systemd/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-systemd/src/lib.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-use bifrostlink::declarative::endpoints;
-use bifrostlink::Config;
-use serde::{Deserialize, Serialize};
-use zbus::proxy;
-use zbus::zvariant::OwnedObjectPath;
-
-pub struct Systemd;
-
-#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
-pub enum Error {
- #[error("systemd request failed: {0}")]
- Failed(String),
-}
-
-#[proxy(
- interface = "org.freedesktop.systemd1.Manager",
- default_service = "org.freedesktop.systemd1",
- default_path = "/org/freedesktop/systemd1"
-)]
-trait Manager {
- fn start_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
- fn stop_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
-}
-
-async fn manager() -> Result<ManagerProxy<'static>, Error> {
- let conn = zbus::Connection::system()
- .await
- .map_err(|e| Error::Failed(e.to_string()))?;
- ManagerProxy::new(&conn)
- .await
- .map_err(|e| Error::Failed(e.to_string()))
-}
-
-#[endpoints(ns = 5)]
-impl Systemd {
- #[endpoints(id = 1)]
- async fn start(&self, unit: String) -> Result<(), Error> {
- manager()
- .await?
- .start_unit(&unit, "replace")
- .await
- .map_err(|e| Error::Failed(e.to_string()))?;
- Ok(())
- }
- #[endpoints(id = 2)]
- async fn stop(&self, unit: String) -> Result<(), Error> {
- manager()
- .await?
- .stop_unit(&unit, "replace")
- .await
- .map_err(|e| Error::Failed(e.to_string()))?;
- Ok(())
- }
-}
crates/remowt-ui-prompt/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "remowt-ui-prompt"
+description = "Interactive UI prompt endpoint for remowt (D-Bus)"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+bifrostlink.workspace = true
+bifrostlink-macros.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
+tracing.workspace = true
+zbus = { workspace = true, optional = true }
+
+[features]
+default = ["dbus"]
+dbus = ["dep:zbus"]
crates/remowt-ui-prompt/src/bifrost.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/src/bifrost.rs
@@ -0,0 +1,109 @@
+use bifrostlink::{Config, Rpc};
+use bifrostlink_macros::endpoints;
+use serde::{Deserialize, Serialize};
+
+use crate::{Error, Prompter, Source};
+
+pub struct PromptEndpoints<P>(pub P);
+
+#[endpoints(ns = 2)]
+impl<P> PromptEndpoints<P>
+where
+ P: Prompter + Send + Sync + 'static,
+{
+ #[endpoints(id = 1, cancel)]
+ async fn prompt_enum(
+ &self,
+ prompt: String,
+ description: String,
+ variants: Vec<String>,
+ source: Vec<Source>,
+ ) -> Result<u32, Error> {
+ let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
+ self.0
+ .prompt_enum(&prompt, &description, &variants, &source)
+ .await
+ }
+
+ #[endpoints(id = 2, cancel)]
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: String,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<String, Error> {
+ self.0
+ .prompt_text(echo, &prompt, &description, &source)
+ .await
+ }
+
+ #[endpoints(id = 3, cancel)]
+ async fn display_text(
+ &self,
+ error: bool,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<(), Error> {
+ self.0.display_text(error, &description, &source).await
+ }
+}
+
+impl<C: Config> Prompter for PromptEndpointsClient<C>
+where
+ Error: ToString,
+{
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> crate::Result<u32> {
+ self.prompt_enum(
+ prompt.to_owned(),
+ description.to_owned(),
+ variants.iter().map(|v| (*v).to_owned()).collect(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> crate::Result<String> {
+ self.prompt_text(
+ echo,
+ prompt.to_owned(),
+ description.to_owned(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> crate::Result<()> {
+ self.display_text(error, description.to_owned(), source.to_vec())
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+}
+
+pub fn serve_prompts<P, C>(rpc: &mut Rpc<C>, prompt: P)
+where
+ P: Prompter + Send + Sync + 'static,
+ C: Config,
+ C::Error: From<Error>,
+{
+ PromptEndpoints(prompt).register_endpoints(rpc);
+}
crates/remowt-ui-prompt/src/dbus.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/src/dbus.rs
@@ -0,0 +1,135 @@
+use zbus::interface;
+use zbus::{fdo, proxy};
+
+use crate::Source;
+use crate::{BlockingPrompter, Result};
+use crate::{Error, Prompter};
+
+pub struct DbusPrompterInterface<P>(pub P);
+
+#[interface(name = "lach.PolkitInputHandler")]
+impl<P: Prompter + Send + Sync + 'static> DbusPrompterInterface<P> {
+ async fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<bool> {
+ Ok(self.0.prompt_radio(prompt, description, &source).await?)
+ }
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<String> {
+ Ok(self
+ .0
+ .prompt_text(echo, prompt, description, &source)
+ .await?)
+ }
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<()> {
+ Ok(self.0.display_text(error, description, &source).await?)
+ }
+}
+
+#[proxy(interface = "lach.PolkitInputHandler")]
+pub trait DbusPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> fdo::Result<u32>;
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<String>;
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<()>;
+}
+
+impl Prompter for DbusPrompterProxy<'_> {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self
+ .prompt_enum(prompt, description, variants, source)
+ .await?)
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source).await?)
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source).await?)
+ }
+}
+impl BlockingPrompter for DbusPrompterProxyBlocking<'_> {
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self.prompt_enum(prompt, description, variants, source)?)
+ }
+
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source)?)
+ }
+
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source)?)
+ }
+}
+
+impl From<fdo::Error> for Error {
+ fn from(value: fdo::Error) -> Self {
+ if matches!(value, fdo::Error::NoReply(_)) {
+ return Self::Cancel;
+ }
+ Self::InputError(format!("{value}"))
+ }
+}
+impl From<Error> for fdo::Error {
+ fn from(value: Error) -> Self {
+ match value {
+ Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
+ Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),
+ Error::InputError(e) => fdo::Error::Failed(e),
+ }
+ }
+}
crates/remowt-ui-prompt/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/src/lib.rs
@@ -0,0 +1,201 @@
+use core::fmt;
+use std::borrow::Cow;
+use std::future::Future;
+use std::result;
+
+pub mod bifrost;
+pub mod dbus;
+pub mod rofi;
+
+#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
+pub enum Error {
+ #[error("user has cancelled input")]
+ Cancel,
+ #[error("input error: {0}")]
+ InputError(String),
+ #[error("unknown remote error: {0}")]
+ Remote(String),
+}
+
+pub type Result<T, E = Error> = result::Result<T, E>;
+
+#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
+pub struct Source(pub Cow<'static, str>);
+impl fmt::Display for Source {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "<u>{}</u>", self.0)
+ }
+}
+
+pub trait Prompter: Send + Sync {
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<bool>> + Send {
+ let fut = self.prompt_enum(prompt, description, &["No", "Yes"], source);
+ async { fut.await.map(|v| v == 1) }
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send;
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send;
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send;
+}
+pub trait BlockingPrompter {
+ fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {
+ self.prompt_enum(prompt, description, &["No", "Yes"], source)
+ .map(|v| v == 1)
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32>;
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String>;
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;
+}
+impl<P> Prompter for &P
+where
+ P: Prompter,
+{
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<bool>> + Send {
+ (*self).prompt_radio(prompt, description, source)
+ }
+
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send {
+ (*self).prompt_enum(prompt, description, variants, source)
+ }
+
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send {
+ (*self).prompt_text(echo, prompt, description, source)
+ }
+
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send {
+ (*self).display_text(error, description, source)
+ }
+}
+
+pub struct PrependSourcePrompter<P> {
+ pub prompter: P,
+ pub source: Vec<Source>,
+ pub description: String,
+}
+impl<P> PrependSourcePrompter<P> {
+ fn source(&self, input: &[Source]) -> Vec<Source> {
+ let mut out = self.source.clone();
+ out.extend(input.iter().cloned());
+ out
+ }
+ fn description(&self, input: &str) -> String {
+ if self.description.is_empty() {
+ input.to_owned()
+ } else if input.is_empty() {
+ self.description.to_owned()
+ } else {
+ format!("{input}\n\n{}", self.description)
+ }
+ }
+}
+impl<P> Prompter for PrependSourcePrompter<P>
+where
+ P: Prompter + Sync,
+{
+ async fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<bool> {
+ self.prompter
+ .prompt_radio(prompt, &self.description(description), &self.source(source))
+ .await
+ }
+
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ self.prompter
+ .prompt_enum(
+ prompt,
+ &self.description(description),
+ variants,
+ &self.source(source),
+ )
+ .await
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ self.prompter
+ .prompt_text(
+ echo,
+ prompt,
+ &self.description(description),
+ &self.source(source),
+ )
+ .await
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ self.prompter
+ .display_text(error, &self.description(description), &self.source(source))
+ .await
+ }
+}
crates/remowt-ui-prompt/src/rofi.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/src/rofi.rs
@@ -0,0 +1,208 @@
+use std::process::Stdio;
+
+use tokio::io::AsyncWriteExt;
+use tokio::process::Command;
+use tracing::trace;
+
+use crate::{Error, Prompter, Result, Source};
+
+#[derive(Clone)]
+pub struct RofiPrompter;
+
+fn fixup_prompt(prompt: &str) -> &str {
+ // Rofi always appends such suffix
+ prompt.strip_suffix(": ").unwrap_or(prompt)
+}
+
+fn rofi_command() -> Command {
+ Command::new(option_env!("ROFI").unwrap_or("rofi"))
+}
+
+impl Prompter for RofiPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ trace!("rofi radio");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args([
+ "-dmenu",
+ "-mesg",
+ &mesg,
+ "-sync",
+ "-only-match",
+ "-p",
+ fixup_prompt(prompt),
+ "-format",
+ "i",
+ "-markup-rows",
+ ]);
+ cmd.stdin(Stdio::piped());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ let mut stdin = child.stdin.take().expect("stdin is piped");
+ for var in variants {
+ stdin
+ .write_all(var.replace('\n', " ").as_bytes())
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ stdin
+ .write_all(b"\n")
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ }
+ // write_all already flushes, just to be sure.
+ let _ = stdin.flush().await;
+ drop(stdin);
+
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
+
+ let id: u32 = String::from_utf8(stdout)
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?
+ .parse()
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;
+ if id as usize >= variants.len() {
+ return Err(Error::InputError("invalid rofi response".to_owned()));
+ }
+
+ Ok(id)
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ trace!("rofi text");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);
+ if !echo {
+ cmd.arg("-password");
+ }
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
+
+ Ok(String::from_utf8_lossy(&stdout).to_string())
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ trace!("rofi display");
+ let mut cmd = rofi_command();
+ let mut mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Coming from ",);
+ for s in source.iter() {
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ if error {
+ mesg.insert_str(0, "<span color=\"red\">");
+ mesg.push_str("</span>");
+ }
+ cmd.args(["-e", &mesg, "-markup"]);
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::null());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ child
+ .wait()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::borrow::Cow;
+
+ use crate::rofi::RofiPrompter;
+ use crate::{PrependSourcePrompter, Prompter as _, Source};
+
+ // #[tokio::test]
+ #[tokio::test]
+ #[ignore = "interactive"]
+ async fn test() {
+ let prompter = PrependSourcePrompter {
+ prompter: RofiPrompter,
+ description: "test".to_owned(),
+ source: vec![Source(Cow::Borrowed("ssh"))],
+ };
+ prompter
+ .prompt_radio("Enable", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .prompt_text(false, "Password", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .display_text(true, "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ }
+}
crates/ui-prompt/Cargo.tomldiffbeforeafterboth--- a/crates/ui-prompt/Cargo.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-[package]
-name = "remowt-ui-prompt"
-description = "Interactive UI prompt endpoint for remowt (D-Bus)"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-thiserror.workspace = true
-tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
-tracing.workspace = true
-zbus = { workspace = true, optional = true }
-
-[features]
-default = ["dbus"]
-dbus = ["dep:zbus"]
crates/ui-prompt/src/bifrost.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/bifrost.rs
+++ /dev/null
@@ -1,109 +0,0 @@
-use bifrostlink::{Config, Rpc};
-use bifrostlink_macros::endpoints;
-use serde::{Deserialize, Serialize};
-
-use crate::{Error, Prompter, Source};
-
-pub struct PromptEndpoints<P>(pub P);
-
-#[endpoints(ns = 2)]
-impl<P> PromptEndpoints<P>
-where
- P: Prompter + Send + Sync + 'static,
-{
- #[endpoints(id = 1, cancel)]
- async fn prompt_enum(
- &self,
- prompt: String,
- description: String,
- variants: Vec<String>,
- source: Vec<Source>,
- ) -> Result<u32, Error> {
- let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
- self.0
- .prompt_enum(&prompt, &description, &variants, &source)
- .await
- }
-
- #[endpoints(id = 2, cancel)]
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: String,
- description: String,
- source: Vec<Source>,
- ) -> Result<String, Error> {
- self.0
- .prompt_text(echo, &prompt, &description, &source)
- .await
- }
-
- #[endpoints(id = 3, cancel)]
- async fn display_text(
- &self,
- error: bool,
- description: String,
- source: Vec<Source>,
- ) -> Result<(), Error> {
- self.0.display_text(error, &description, &source).await
- }
-}
-
-impl<C: Config> Prompter for PromptEndpointsClient<C>
-where
- Error: ToString,
-{
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> crate::Result<u32> {
- self.prompt_enum(
- prompt.to_owned(),
- description.to_owned(),
- variants.iter().map(|v| (*v).to_owned()).collect(),
- source.to_vec(),
- )
- .await
- .map_err(|e| Error::Remote(e.to_string()))?
- }
-
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> crate::Result<String> {
- self.prompt_text(
- echo,
- prompt.to_owned(),
- description.to_owned(),
- source.to_vec(),
- )
- .await
- .map_err(|e| Error::Remote(e.to_string()))?
- }
-
- async fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> crate::Result<()> {
- self.display_text(error, description.to_owned(), source.to_vec())
- .await
- .map_err(|e| Error::Remote(e.to_string()))?
- }
-}
-
-pub fn serve_prompts<P, C>(rpc: &mut Rpc<C>, prompt: P)
-where
- P: Prompter + Send + Sync + 'static,
- C: Config,
- C::Error: From<Error>,
-{
- PromptEndpoints(prompt).register_endpoints(rpc);
-}
crates/ui-prompt/src/dbus.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/dbus.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-use zbus::interface;
-use zbus::{fdo, proxy};
-
-use crate::Source;
-use crate::{BlockingPrompter, Result};
-use crate::{Error, Prompter};
-
-pub struct DbusPrompterInterface<P>(pub P);
-
-#[interface(name = "lach.PolkitInputHandler")]
-impl<P: Prompter + Send + Sync + 'static> DbusPrompterInterface<P> {
- async fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: Vec<Source>,
- ) -> fdo::Result<bool> {
- Ok(self.0.prompt_radio(prompt, description, &source).await?)
- }
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: Vec<Source>,
- ) -> fdo::Result<String> {
- Ok(self
- .0
- .prompt_text(echo, prompt, description, &source)
- .await?)
- }
- async fn display_text(
- &self,
- error: bool,
- description: &str,
- source: Vec<Source>,
- ) -> fdo::Result<()> {
- Ok(self.0.display_text(error, description, &source).await?)
- }
-}
-
-#[proxy(interface = "lach.PolkitInputHandler")]
-pub trait DbusPrompter {
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> fdo::Result<u32>;
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> fdo::Result<String>;
- async fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> fdo::Result<()>;
-}
-
-impl Prompter for DbusPrompterProxy<'_> {
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- Ok(self
- .prompt_enum(prompt, description, variants, source)
- .await?)
- }
-
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- Ok(self.prompt_text(echo, prompt, description, source).await?)
- }
-
- async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- Ok(self.display_text(error, description, source).await?)
- }
-}
-impl BlockingPrompter for DbusPrompterProxyBlocking<'_> {
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- Ok(self.prompt_enum(prompt, description, variants, source)?)
- }
-
- fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- Ok(self.prompt_text(echo, prompt, description, source)?)
- }
-
- fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- Ok(self.display_text(error, description, source)?)
- }
-}
-
-impl From<fdo::Error> for Error {
- fn from(value: fdo::Error) -> Self {
- if matches!(value, fdo::Error::NoReply(_)) {
- return Self::Cancel;
- }
- Self::InputError(format!("{value}"))
- }
-}
-impl From<Error> for fdo::Error {
- fn from(value: Error) -> Self {
- match value {
- Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
- Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),
- Error::InputError(e) => fdo::Error::Failed(e),
- }
- }
-}
crates/ui-prompt/src/lib.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/lib.rs
+++ /dev/null
@@ -1,201 +0,0 @@
-use core::fmt;
-use std::borrow::Cow;
-use std::future::Future;
-use std::result;
-
-pub mod bifrost;
-pub mod dbus;
-pub mod rofi;
-
-#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
-pub enum Error {
- #[error("user has cancelled input")]
- Cancel,
- #[error("input error: {0}")]
- InputError(String),
- #[error("unknown remote error: {0}")]
- Remote(String),
-}
-
-pub type Result<T, E = Error> = result::Result<T, E>;
-
-#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]
-#[derive(serde::Serialize, serde::Deserialize, Clone)]
-pub struct Source(pub Cow<'static, str>);
-impl fmt::Display for Source {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "<u>{}</u>", self.0)
- }
-}
-
-pub trait Prompter: Send + Sync {
- fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<bool>> + Send {
- let fut = self.prompt_enum(prompt, description, &["No", "Yes"], source);
- async { fut.await.map(|v| v == 1) }
- }
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> impl Future<Output = Result<u32>> + Send;
- fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<String>> + Send;
- fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<()>> + Send;
-}
-pub trait BlockingPrompter {
- fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {
- self.prompt_enum(prompt, description, &["No", "Yes"], source)
- .map(|v| v == 1)
- }
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32>;
- fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String>;
- fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;
-}
-impl<P> Prompter for &P
-where
- P: Prompter,
-{
- fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<bool>> + Send {
- (*self).prompt_radio(prompt, description, source)
- }
-
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> impl Future<Output = Result<u32>> + Send {
- (*self).prompt_enum(prompt, description, variants, source)
- }
-
- fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<String>> + Send {
- (*self).prompt_text(echo, prompt, description, source)
- }
-
- fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<()>> + Send {
- (*self).display_text(error, description, source)
- }
-}
-
-pub struct PrependSourcePrompter<P> {
- pub prompter: P,
- pub source: Vec<Source>,
- pub description: String,
-}
-impl<P> PrependSourcePrompter<P> {
- fn source(&self, input: &[Source]) -> Vec<Source> {
- let mut out = self.source.clone();
- out.extend(input.iter().cloned());
- out
- }
- fn description(&self, input: &str) -> String {
- if self.description.is_empty() {
- input.to_owned()
- } else if input.is_empty() {
- self.description.to_owned()
- } else {
- format!("{input}\n\n{}", self.description)
- }
- }
-}
-impl<P> Prompter for PrependSourcePrompter<P>
-where
- P: Prompter + Sync,
-{
- async fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<bool> {
- self.prompter
- .prompt_radio(prompt, &self.description(description), &self.source(source))
- .await
- }
-
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- self.prompter
- .prompt_enum(
- prompt,
- &self.description(description),
- variants,
- &self.source(source),
- )
- .await
- }
-
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- self.prompter
- .prompt_text(
- echo,
- prompt,
- &self.description(description),
- &self.source(source),
- )
- .await
- }
-
- async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- self.prompter
- .display_text(error, &self.description(description), &self.source(source))
- .await
- }
-}
crates/ui-prompt/src/rofi.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/rofi.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-use std::process::Stdio;
-
-use tokio::io::AsyncWriteExt;
-use tokio::process::Command;
-use tracing::trace;
-
-use crate::{Error, Prompter, Result, Source};
-
-#[derive(Clone)]
-pub struct RofiPrompter;
-
-fn fixup_prompt(prompt: &str) -> &str {
- // Rofi always appends such suffix
- prompt.strip_suffix(": ").unwrap_or(prompt)
-}
-
-fn rofi_command() -> Command {
- Command::new(option_env!("ROFI").unwrap_or("rofi"))
-}
-
-impl Prompter for RofiPrompter {
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- trace!("rofi radio");
- let mut cmd = rofi_command();
- let mesg = if source.is_empty() {
- description.to_owned()
- } else {
- let mut out = format!("{description}\n\n<b>Requested on ",);
- for (i, s) in source.iter().enumerate() {
- if i != 0 {
- out.push_str(" -> ");
- }
- out.push_str(&s.to_string());
- }
- out.push_str("</b>");
- out
- };
- cmd.args([
- "-dmenu",
- "-mesg",
- &mesg,
- "-sync",
- "-only-match",
- "-p",
- fixup_prompt(prompt),
- "-format",
- "i",
- "-markup-rows",
- ]);
- cmd.stdin(Stdio::piped());
- cmd.stdout(Stdio::piped());
- cmd.kill_on_drop(true);
- let mut child = cmd
- .spawn()
- .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
-
- let mut stdin = child.stdin.take().expect("stdin is piped");
- for var in variants {
- stdin
- .write_all(var.replace('\n', " ").as_bytes())
- .await
- .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
- stdin
- .write_all(b"\n")
- .await
- .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
- }
- // write_all already flushes, just to be sure.
- let _ = stdin.flush().await;
- drop(stdin);
-
- let out = child
- .wait_with_output()
- .await
- .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
- let stdout = out
- .stdout
- .strip_suffix(b"\n")
- .unwrap_or(&out.stdout)
- .to_owned();
-
- let id: u32 = String::from_utf8(stdout)
- .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?
- .parse()
- .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;
- if id as usize >= variants.len() {
- return Err(Error::InputError("invalid rofi response".to_owned()));
- }
-
- Ok(id)
- }
-
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- trace!("rofi text");
- let mut cmd = rofi_command();
- let mesg = if source.is_empty() {
- description.to_owned()
- } else {
- let mut out = format!("{description}\n\n<b>Requested on ",);
- for (i, s) in source.iter().enumerate() {
- if i != 0 {
- out.push_str(" -> ");
- }
- out.push_str(&s.to_string());
- }
- out.push_str("</b>");
- out
- };
- cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);
- if !echo {
- cmd.arg("-password");
- }
- cmd.stdin(Stdio::null());
- cmd.stdout(Stdio::piped());
- cmd.kill_on_drop(true);
- let child = cmd
- .spawn()
- .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
-
- let out = child
- .wait_with_output()
- .await
- .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
- let stdout = out
- .stdout
- .strip_suffix(b"\n")
- .unwrap_or(&out.stdout)
- .to_owned();
-
- Ok(String::from_utf8_lossy(&stdout).to_string())
- }
-
- async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- trace!("rofi display");
- let mut cmd = rofi_command();
- let mut mesg = if source.is_empty() {
- description.to_owned()
- } else {
- let mut out = format!("{description}\n\n<b>Coming from ",);
- for s in source.iter() {
- out.push_str(&s.to_string());
- }
- out.push_str("</b>");
- out
- };
- if error {
- mesg.insert_str(0, "<span color=\"red\">");
- mesg.push_str("</span>");
- }
- cmd.args(["-e", &mesg, "-markup"]);
- cmd.stdin(Stdio::null());
- cmd.stdout(Stdio::null());
- cmd.kill_on_drop(true);
- let mut child = cmd
- .spawn()
- .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
-
- child
- .wait()
- .await
- .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
-
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::borrow::Cow;
-
- use crate::rofi::RofiPrompter;
- use crate::{PrependSourcePrompter, Prompter as _, Source};
-
- // #[tokio::test]
- #[tokio::test]
- #[ignore = "interactive"]
- async fn test() {
- let prompter = PrependSourcePrompter {
- prompter: RofiPrompter,
- description: "test".to_owned(),
- source: vec![Source(Cow::Borrowed("ssh"))],
- };
- prompter
- .prompt_radio("Enable", "Polkit needs access", &[])
- .await
- .expect("rofi");
- prompter
- .prompt_text(false, "Password", "Polkit needs access", &[])
- .await
- .expect("rofi");
- prompter
- .display_text(true, "Polkit needs access", &[])
- .await
- .expect("rofi");
- }
-}