From 5ef123dcb805b61ce58e0c9d99e88bb849af85c5 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sat, 1 May 2021 10:16:16 -0400 Subject: [PATCH 01/22] return of the termcap issue! added cb_sink back FocusServiceList -> OmniboxCommand more reuse of omniboxcomamnd chaining add explicit reload command --- doc/launchctl_messages.md | 31 ++++++++++++++++++++++++++++ launchk/src/tui/dialog.rs | 9 ++++---- launchk/src/tui/omnibox/command.rs | 6 +++++- launchk/src/tui/omnibox/view.rs | 23 +++++++++------------ launchk/src/tui/root.rs | 9 +++++++- launchk/src/tui/service_list/view.rs | 17 ++++++++++----- 6 files changed, 71 insertions(+), 24 deletions(-) diff --git a/doc/launchctl_messages.md b/doc/launchctl_messages.md index 7108815..6680726 100644 --- a/doc/launchctl_messages.md +++ b/doc/launchctl_messages.md @@ -250,3 +250,34 @@ As root: "domain-port" => { name = 1799, right = send, urefs = 5 } ``` +#### `launchctl disable user/501/homebrew.mxcl.postgresql` + +- postgresql runs in the `Aqua` domain +- Interesting that both `name` and `names` are sent! + +``` + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 501 + "routine" => : 809 + "name" => { length = 24, contents = "homebrew.mxcl.postgresql" } + "type" => : 2 + "names" => { count = 1, capacity = 8, contents = + 0: { length = 24, contents = "homebrew.mxcl.postgresql" } + } +``` + + +#### `launchctl enable user/501/homebrew.mxcl.postgresql` + +``` + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 501 + "routine" => : 808 + "name" => { length = 24, contents = "homebrew.mxcl.postgresql" } + "type" => : 2 + "names" => { count = 1, capacity = 8, contents = + 0: { length = 24, contents = "homebrew.mxcl.postgresql" } + } +``` diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index 97db63a..89f2f07 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -32,10 +32,11 @@ pub fn show_prompt( let cl = move |siv: &mut Cursive| { let ask = Dialog::around(TextView::new(prompt.clone())) .button("Yes", move |s| { - for cmd in &commands { - tx.send(OmniboxEvent::Command(cmd.clone())) - .expect("Must send command"); - } + commands + .iter() + .try_for_each(|c| tx.send(OmniboxEvent::Command(c.clone()))) + .expect("Must sent commands"); + s.pop_layer(); }) .button("No", |s| { diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index b157db1..f69b401 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -2,11 +2,14 @@ use std::fmt; #[derive(Debug, Clone, Eq, PartialEq)] pub enum OmniboxCommand { + Chain(Vec), Load, Unload, + Reload, Edit, // (message, on ok) Prompt(String, Vec), + FocusServiceList, Quit, } @@ -16,12 +19,13 @@ impl fmt::Display for OmniboxCommand { } } -pub static OMNIBOX_COMMANDS: [(OmniboxCommand, &str); 4] = [ +pub static OMNIBOX_COMMANDS: [(OmniboxCommand, &str); 5] = [ (OmniboxCommand::Load, "▶️ Load highlighted job"), (OmniboxCommand::Unload, "⏏️ Unload highlighted job"), ( OmniboxCommand::Edit, "✍️ Edit plist with $EDITOR, then reload job", ), + (OmniboxCommand::Reload ,"🔄 Reload highlighted job"), (OmniboxCommand::Quit, "🚪 see ya!"), ]; diff --git a/launchk/src/tui/omnibox/view.rs b/launchk/src/tui/omnibox/view.rs index 39075f0..776d632 100644 --- a/launchk/src/tui/omnibox/view.rs +++ b/launchk/src/tui/omnibox/view.rs @@ -15,11 +15,10 @@ use crate::launchd::job_type_filter::JobTypeFilter; use crate::tui::omnibox::command::OmniboxCommand; use crate::tui::omnibox::state::OmniboxState; -/// Consumers impl OmniboxSubscriber receive these commands +/// Consumers impl OmniboxSubscriber receive these events /// via a channel in a wrapped view #[derive(Debug, Clone, Eq, PartialEq)] pub enum OmniboxEvent { - FocusServiceList, StateUpdate(OmniboxState), Command(OmniboxCommand), } @@ -62,7 +61,7 @@ async fn tick(state: Arc>, tx: Sender) { let new = match *mode { OmniboxMode::CommandFilter | OmniboxMode::CommandConfirm(_) => { - read.with_new(Some(OmniboxMode::Idle), None, Some("".to_string()), None) + read.with_new(Some(OmniboxMode::Idle), None, Some("".to_string()), None) } _ => read.with_new(Some(OmniboxMode::Idle), None, None, None), }; @@ -70,14 +69,12 @@ async fn tick(state: Arc>, tx: Sender) { drop(read); let mut write = state.write().expect("Must write"); - let out = [ - tx.send(OmniboxEvent::FocusServiceList), - tx.send(OmniboxEvent::StateUpdate(new.clone())), - ]; - - for msg in out.iter() { - msg.as_ref().expect("Must send"); - } + [ + OmniboxEvent::Command(OmniboxCommand::FocusServiceList), + OmniboxEvent::StateUpdate(new.clone()), + ].iter() + .try_for_each(|e| tx.send(e.clone())) + .expect("Must send events"); log::debug!("[omnibox/tick]: New state: {:?}", &new); @@ -359,13 +356,13 @@ impl View for OmniboxView { let new_state = match (event, mode) { (Event::CtrlChar('u'), _) => { self.tx - .send(OmniboxEvent::FocusServiceList) + .send(OmniboxEvent::Command(OmniboxCommand::FocusServiceList)) .expect("Must focus"); Some(OmniboxState::default()) } (Event::Key(Key::Esc), _) => { self.tx - .send(OmniboxEvent::FocusServiceList) + .send(OmniboxEvent::Command(OmniboxCommand::FocusServiceList)) .expect("Must focus"); Some(state.with_new(Some(OmniboxMode::Idle), None, Some("".to_string()), None)) } diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index 22b09f5..cb99322 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -228,6 +228,13 @@ impl ViewWrapper for RootLayout { impl OmniboxSubscriber for RootLayout { fn on_omnibox(&mut self, cmd: OmniboxEvent) -> OmniboxResult { match cmd { + OmniboxEvent::Command(OmniboxCommand::Chain(cmds)) => { + cmds + .iter() + .try_for_each(|c| self.omnibox_tx.send(OmniboxEvent::Command(c.clone()))) + .expect("Must send commands"); + Ok(None) + } OmniboxEvent::Command(OmniboxCommand::Quit) => { self.cbsink_channel .send(Box::new(|s| { @@ -237,7 +244,7 @@ impl OmniboxSubscriber for RootLayout { Ok(None) } // Triggered when toggling to idle - OmniboxEvent::FocusServiceList => { + OmniboxEvent::Command(OmniboxCommand::FocusServiceList) => { self.layout .set_focus_index(RootLayoutChildren::ServiceList as usize) .expect("Must focus SL"); diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index 679ad22..fb0e16d 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::cmp::Ordering; use std::collections::HashSet; use std::rc::Rc; -use std::sync::mpsc::Sender; +use std::sync::mpsc::{Sender}; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -45,6 +45,7 @@ async fn poll_running_jobs(svcs: Arc>>, cb_sink: Sender, running_jobs: Arc>>, table_list_view: TableListView, label_filter: RefCell, @@ -55,10 +56,12 @@ impl ServiceListView { pub fn new(runtime_handle: &Handle, cb_sink: Sender) -> Self { let arc_svc = Arc::new(RwLock::new(HashSet::new())); let ref_clone = arc_svc.clone(); + let cb_sink_clone = cb_sink.clone(); - runtime_handle.spawn(async move { poll_running_jobs(ref_clone, cb_sink).await }); + runtime_handle.spawn(async move { poll_running_jobs(ref_clone, cb_sink_clone).await }); Self { + cb_sink, running_jobs: arc_svc.clone(), label_filter: RefCell::new("".into()), job_type_filter: RefCell::new(JobTypeFilter::launchk_default()), @@ -183,9 +186,13 @@ impl ServiceListView { .ok_or_else(|| OmniboxError::CommandError("Cannot find plist".to_string()))?; edit_and_replace(plist).map_err(OmniboxError::CommandError)?; + + // Clear term / display issues after editor exit + self.cb_sink.send(Box::new(Cursive::clear)).expect("Must clear"); + Ok(Some(OmniboxCommand::Prompt( format!("Reload {}?", name), - vec![OmniboxCommand::Unload, OmniboxCommand::Load], + vec![OmniboxCommand::Reload], ))) } OmniboxCommand::Load | OmniboxCommand::Unload => { @@ -209,7 +216,8 @@ impl ServiceListView { xpc_query(name, &plist.plist_path) .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) - } + }, + OmniboxCommand::Reload => Ok(Some(OmniboxCommand::Chain(vec![OmniboxCommand::Unload, OmniboxCommand::Load]))), _ => Ok(None), } } @@ -236,7 +244,6 @@ impl OmniboxSubscriber for ServiceListView { match event { OmniboxEvent::StateUpdate(state) => self.handle_state_update(state), OmniboxEvent::Command(cmd) => self.handle_command(cmd), - _ => Ok(None), } } } From d27c98078693b93947922a43dcf73a9184073f8b Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sat, 1 May 2021 10:39:04 -0400 Subject: [PATCH 02/22] fix careless path check bug --- doc/launchctl_messages.md | 34 ++++++++++++++++++++++++++++++++++ launchk/src/launchd/plist.rs | 10 +++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/doc/launchctl_messages.md b/doc/launchctl_messages.md index 6680726..610d907 100644 --- a/doc/launchctl_messages.md +++ b/doc/launchctl_messages.md @@ -281,3 +281,37 @@ As root: 0: { length = 24, contents = "homebrew.mxcl.postgresql" } } ``` + +#### `launchctl disable system/com.apple.FontWorker` + +- Must run as root +- Type `1` + +``` + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 0 + "routine" => : 809 + "name" => { length = 20, contents = "com.apple.FontWorker" } + "type" => : 1 + "names" => { count = 1, capacity = 8, contents = + 0: { length = 20, contents = "com.apple.FontWorker" } + } +``` + +#### `launchctl enable system/com.apple.FontWorker` + +- This has a different `LimitLoadToSessionType` set as background, wanted to see if `type` would change + +``` + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 0 + "routine" => : 808 + "name" => { length = 20, contents = "com.apple.FontWorker" } + "type" => : 1 + "names" => { count = 1, capacity = 8, contents = + 0: { length = 20, contents = "com.apple.FontWorker" } + } +``` + diff --git a/launchk/src/launchd/plist.rs b/launchk/src/launchd/plist.rs index 2302b9c..7cef494 100644 --- a/launchk/src/launchd/plist.rs +++ b/launchk/src/launchd/plist.rs @@ -154,18 +154,18 @@ fn build_label_map_entry(plist_path: DirEntry) -> Option<(String, LaunchdPlist)> .and_then(|d| d.get("Label")) .and_then(|v| v.as_string()); - let entry_type = if path_string.contains(ADMIN_LAUNCH_DAEMONS) - || path_string.contains(GLOBAL_LAUNCH_DAEMONS) + let entry_type = if path_string.starts_with(ADMIN_LAUNCH_DAEMONS) + || path_string.starts_with(GLOBAL_LAUNCH_DAEMONS) { LaunchdEntryType::Daemon } else { LaunchdEntryType::Agent }; - let entry_location = if path_string.contains(USER_LAUNCH_AGENTS) { + let entry_location = if path_string.starts_with(USER_LAUNCH_AGENTS) { LaunchdEntryLocation::User - } else if path_string.contains(GLOBAL_LAUNCH_AGENTS) - || path_string.contains(ADMIN_LAUNCH_DAEMONS) + } else if path_string.starts_with(GLOBAL_LAUNCH_AGENTS) + || path_string.starts_with(ADMIN_LAUNCH_DAEMONS) { LaunchdEntryLocation::Global } else { From e3753d4c6de8b8cbc41a3e27420a7fe0d424d4d9 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sat, 1 May 2021 10:47:47 -0400 Subject: [PATCH 03/22] moar xpc frame --- doc/launchctl_messages.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/launchctl_messages.md b/doc/launchctl_messages.md index 610d907..e6ac6ae 100644 --- a/doc/launchctl_messages.md +++ b/doc/launchctl_messages.md @@ -315,3 +315,20 @@ As root: } ``` +Using a `gui/` domain target: + +``` + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 501 + "routine" => : 808 + "name" => { length = 17, contents = "com.docker.vmnetd" } + "type" => : 8 + "names" => { count = 1, capacity = 8, contents = + 0: { length = 17, contents = "com.docker.vmnetd" } + } +``` + +1: System +2: User +8: Login (GUI) \ No newline at end of file From cff724ee9cda4bc0403181b715ecd01bd300c737 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Mon, 3 May 2021 15:09:35 -0400 Subject: [PATCH 04/22] prepare for prompting for lltst et al --- doc/launchctl_messages.md | 2 +- launchk/src/launchd/message.rs | 20 +++++ launchk/src/launchd/query.rs | 105 ++++++++++++++-------- launchk/src/tui/omnibox/command.rs | 11 ++- launchk/src/tui/service_list/list_item.rs | 2 +- launchk/src/tui/service_list/view.rs | 60 +++++++------ 6 files changed, 134 insertions(+), 66 deletions(-) diff --git a/doc/launchctl_messages.md b/doc/launchctl_messages.md index e6ac6ae..b06aa61 100644 --- a/doc/launchctl_messages.md +++ b/doc/launchctl_messages.md @@ -317,7 +317,7 @@ As root: Using a `gui/` domain target: -``` +``` { count = 6, transaction: 0, voucher = 0x0, contents = "subsystem" => : 3 "handle" => : 501 diff --git a/launchk/src/launchd/message.rs b/launchk/src/launchd/message.rs index 2579578..a54d3d3 100644 --- a/launchk/src/launchd/message.rs +++ b/launchk/src/launchd/message.rs @@ -55,6 +55,26 @@ lazy_static! { msg }; + + pub static ref ENABLE_NAMES: HashMap<&'static str, XPCObject> = { + let mut msg = HashMap::new(); + msg.insert("routine", XPCObject::from(808 as u64)); + msg.insert("subsystem", XPCObject::from(3 as u64)); + // UID or ASID + msg.insert("handle", XPCObject::from(0 as u64)); + + msg + }; + + pub static ref DISABLE_NAMES: HashMap<&'static str, XPCObject> = { + let mut msg = HashMap::new(); + msg.insert("routine", XPCObject::from(809 as u64)); + msg.insert("subsystem", XPCObject::from(3 as u64)); + // UID or ASID + msg.insert("handle", XPCObject::from(0 as u64)); + + msg + }; } pub fn from_msg<'a>(proto: &HashMap<&'a str, XPCObject>) -> HashMap<&'a str, XPCObject> { diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index 45a3e75..f3705f7 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -14,14 +14,14 @@ use xpc_sys::objects::xpc_dictionary::XPCDictionary; use xpc_sys::objects::xpc_error::XPCError; use xpc_sys::objects::xpc_type::check_xpc_type; -#[link(name = "c")] -extern "C" { - fn geteuid() -> u32; -} +// #[link(name = "c")] +// extern "C" { +// fn geteuid() -> u32; +// } -lazy_static! { - static ref IS_ROOT: bool = unsafe { geteuid() } == 0; -} +// lazy_static! { +// static ref IS_ROOT: bool = unsafe { geteuid() } == 0; +// } /// LimitLoadToSessionType key in XPC response /// https://developer.apple.com/library/archive/technotes/tn2083/_index.html @@ -71,11 +71,46 @@ impl TryFrom for LimitLoadToSessionType { } } +// Huge thanks to: https://saelo.github.io/presentations/bits_of_launchd.pdf +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum DomainType { + System = 1, + User = 2, + UserLogin = 3, + Session = 4, + PID = 5, + RequestorUserDomain = 6, + RequestorDomain = 7, + Unknown, +} + +impl From for DomainType { + fn from(value: u64) -> Self { + match value { + 1 => DomainType::System, + 2 => DomainType::User, + 3 => DomainType::UserLogin, + 4 => DomainType::Session, + 5 => DomainType::PID, + 6 => DomainType::RequestorDomain, + 7 => DomainType::RequestorUserDomain, + _ => DomainType::Unknown, + } + } +} + +impl fmt::Display for DomainType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +// TODO: reuse list_all() pub fn find_in_all>(label: S) -> Result { let label_string = label.into(); - for domain_type in 0..8 { - let response = list(domain_type, Some(label_string.clone())); + for domain_type in DomainType::System as u64..DomainType::RequestorDomain as u64 { + let response = list(domain_type.into(), Some(label_string.clone())); if response.is_ok() { return response; } @@ -84,9 +119,10 @@ pub fn find_in_all>(label: S) -> Result Err(XPCError::NotFound) } -pub fn list(domain_type: u64, name: Option) -> Result { +/// Query for jobs in a domain +pub fn list(domain_type: DomainType, name: Option) -> Result { let mut msg = from_msg(&LIST_SERVICES); - msg.insert("type", domain_type.into()); + msg.insert("type", XPCObject::from(domain_type as u64)); if name.is_some() { msg.insert("name", name.unwrap().into()); @@ -103,10 +139,11 @@ pub fn list(domain_type: u64, name: Option) -> Result HashSet { - let everything = (0..8) + let everything = (DomainType::System as u64..DomainType::RequestorDomain as u64) .filter_map(|t| { - let svc_for_type = list(t as u64, None) + let svc_for_type = list(t.into(), None) .and_then(|d| d.get_as_dictionary(&["services"])) .map(|XPCDictionary(ref hm)| hm.keys().map(|k| k.clone()).collect()); @@ -117,22 +154,21 @@ pub fn list_all() -> HashSet { HashSet::from_iter(everything) } -pub fn load>(label: S, plist_path: S) -> XPCPipeResult { +pub fn load>( + label: S, + plist_path: S, + domain_type: Option, + limit_load_to_session_type: Option, + handle: Option +) -> XPCPipeResult { let mut message: HashMap<&str, XPCObject> = from_msg(&LOAD_PATHS); let label_string = label.into(); - message.insert( - "type", - if *IS_ROOT { - XPCObject::from(1 as u64) - } else { - XPCObject::from(7 as u64) - }, - ); - + message.insert("type", XPCObject::from(domain_type.unwrap_or(DomainType::RequestorDomain) as u64)); + message.insert("handle", XPCObject::from(handle.unwrap_or(0))); + message.insert("session", XPCObject::from(limit_load_to_session_type.map(|lltst| lltst.to_string()).unwrap_or("Aqua".to_string()))); let paths = vec![XPCObject::from(plist_path.into())]; message.insert("paths", XPCObject::from(paths)); - message.insert("session", XPCObject::from("Aqua")); let message: XPCObject = message.into(); @@ -144,22 +180,21 @@ pub fn load>(label: S, plist_path: S) -> XPCPipeResult { handle_load_unload_errors(label_string, message.pipe_routine()?) } -pub fn unload>(label: S, plist_path: S) -> XPCPipeResult { +pub fn unload>( + label: S, + plist_path: S, + domain_type: Option, + limit_load_to_session_type: Option, + handle: Option +) -> XPCPipeResult { let mut message: HashMap<&str, XPCObject> = from_msg(&UNLOAD_PATHS); let label_string = label.into(); - message.insert( - "type", - if *IS_ROOT { - XPCObject::from(1 as u64) - } else { - XPCObject::from(7 as u64) - }, - ); - + message.insert("type", XPCObject::from(domain_type.unwrap_or(DomainType::RequestorDomain) as u64)); + message.insert("handle", XPCObject::from(handle.unwrap_or(0))); + message.insert("session", XPCObject::from(limit_load_to_session_type.map(|lltst| lltst.to_string()).unwrap_or("Aqua".to_string()))); let paths = vec![XPCObject::from(plist_path.into())]; message.insert("paths", XPCObject::from(paths)); - message.insert("session", XPCObject::from("Aqua")); let message: XPCObject = message.into(); diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index f69b401..9a9611d 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -1,11 +1,18 @@ use std::fmt; +use crate::launchd::query::{DomainType, LimitLoadToSessionType}; + #[derive(Debug, Clone, Eq, PartialEq)] pub enum OmniboxCommand { Chain(Vec), + // Load(DomainType, Option, LimitLoadToSessionType), + // Unload(DomainType, Option), Load, Unload, + // Reuses domain, handle, limit load to session type from existing Reload, + Enable, + Disable, Edit, // (message, on ok) Prompt(String, Vec), @@ -19,9 +26,11 @@ impl fmt::Display for OmniboxCommand { } } -pub static OMNIBOX_COMMANDS: [(OmniboxCommand, &str); 5] = [ +pub static OMNIBOX_COMMANDS: [(OmniboxCommand, &str); 7] = [ (OmniboxCommand::Load, "▶️ Load highlighted job"), (OmniboxCommand::Unload, "⏏️ Unload highlighted job"), + (OmniboxCommand::Enable, "▶️ Enable highlighted job (enables load)"), + (OmniboxCommand::Disable, "⏏️ Disable highlighted job (prevents load)"), ( OmniboxCommand::Edit, "✍️ Edit plist with $EDITOR, then reload job", diff --git a/launchk/src/tui/service_list/list_item.rs b/launchk/src/tui/service_list/list_item.rs index 27cf03c..1de8b35 100644 --- a/launchk/src/tui/service_list/list_item.rs +++ b/launchk/src/tui/service_list/list_item.rs @@ -4,7 +4,7 @@ use crate::launchd::entry_status::LaunchdEntryStatus; use crate::launchd::job_type_filter::JobTypeFilter; use crate::tui::table::table_list_view::TableListItem; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ServiceListItem { pub name: String, pub status: LaunchdEntryStatus, diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index fb0e16d..92b0e36 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -13,7 +13,7 @@ use cursive::{Cursive, View, XY}; use tokio::runtime::Handle; use tokio::time::interval; -use crate::launchd::entry_status::get_entry_status; +use crate::launchd::{entry_status::get_entry_status, plist::LaunchdPlist}; use crate::launchd::job_type_filter::JobTypeFilter; use crate::launchd::plist::{edit_and_replace, LABEL_TO_ENTRY_CONFIG}; use crate::launchd::query::{list_all, load, unload}; @@ -171,52 +171,56 @@ impl ServiceListView { .ok_or_else(|| OmniboxError::CommandError("Cannot get highlighted row".to_string())) } - fn handle_command(&mut self, cmd: OmniboxCommand) -> OmniboxResult { - match cmd { - OmniboxCommand::Edit => { - let ServiceListItem { - name, - status: entry_info, - .. - } = &*self.get_active_list_item()?; + fn with_active_item_plist(&self) -> Result<(ServiceListItem, LaunchdPlist), OmniboxError> { + let item = &*self.get_active_list_item()?; + let plist = item.status + .plist + .as_ref() + .ok_or_else(|| OmniboxError::CommandError("Cannot find plist".to_string()))?; - let plist = entry_info - .plist - .as_ref() - .ok_or_else(|| OmniboxError::CommandError("Cannot find plist".to_string()))?; + Ok((item.clone(), plist.clone())) + } - edit_and_replace(plist).map_err(OmniboxError::CommandError)?; + fn handle_command(&self, cmd: OmniboxCommand) -> OmniboxResult { + match cmd { + OmniboxCommand::Edit => { + let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; + edit_and_replace(&plist).map_err(OmniboxError::CommandError)?; - // Clear term / display issues after editor exit + // Clear term self.cb_sink.send(Box::new(Cursive::clear)).expect("Must clear"); Ok(Some(OmniboxCommand::Prompt( format!("Reload {}?", name), vec![OmniboxCommand::Reload], ))) - } + }, OmniboxCommand::Load | OmniboxCommand::Unload => { - let ServiceListItem { - name, - status: entry_info, - .. - } = &*self.get_active_list_item()?; - - let plist = entry_info - .plist - .as_ref() - .ok_or_else(|| OmniboxError::CommandError("Cannot find plist".to_string()))?; - + let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; let xpc_query = if cmd == OmniboxCommand::Load { load } else { unload }; - xpc_query(name, &plist.plist_path) + xpc_query(name, plist.plist_path, None, None, None) .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) }, + OmniboxCommand::Enable | OmniboxCommand::Disable => { + let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; + // let xpc_query = if cmd == OmniboxCommand::Enable { + // enable + // } else { + // disable + // }; + + // xpc_query(name, &plist.plist_path) + // .map(|_| None) + // .map_err(|e| OmniboxError::CommandError(e.to_string())) + + Ok(None) + }, OmniboxCommand::Reload => Ok(Some(OmniboxCommand::Chain(vec![OmniboxCommand::Unload, OmniboxCommand::Load]))), _ => Ok(None), } From 0e397d1db81ec45488684fc0b1fb6fb7b6ee4cd3 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Mon, 3 May 2021 18:50:29 -0400 Subject: [PATCH 05/22] ask user for domain and limit load to session type! --- launchk/src/launchd/query.rs | 6 +-- launchk/src/tui/dialog.rs | 61 +++++++++++++++++++++++++++- launchk/src/tui/omnibox/command.rs | 32 ++++++++------- launchk/src/tui/omnibox/state.rs | 4 +- launchk/src/tui/omnibox/view.rs | 8 ++-- launchk/src/tui/root.rs | 8 +++- launchk/src/tui/service_list/view.rs | 30 ++++---------- 7 files changed, 100 insertions(+), 49 deletions(-) diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index f3705f7..65a51ba 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -92,8 +92,8 @@ impl From for DomainType { 3 => DomainType::UserLogin, 4 => DomainType::Session, 5 => DomainType::PID, - 6 => DomainType::RequestorDomain, - 7 => DomainType::RequestorUserDomain, + 6 => DomainType::RequestorUserDomain, + 7 => DomainType::RequestorDomain, _ => DomainType::Unknown, } } @@ -184,7 +184,6 @@ pub fn unload>( label: S, plist_path: S, domain_type: Option, - limit_load_to_session_type: Option, handle: Option ) -> XPCPipeResult { let mut message: HashMap<&str, XPCObject> = from_msg(&UNLOAD_PATHS); @@ -192,7 +191,6 @@ pub fn unload>( message.insert("type", XPCObject::from(domain_type.unwrap_or(DomainType::RequestorDomain) as u64)); message.insert("handle", XPCObject::from(handle.unwrap_or(0))); - message.insert("session", XPCObject::from(limit_load_to_session_type.map(|lltst| lltst.to_string()).unwrap_or("Aqua".to_string()))); let paths = vec![XPCObject::from(plist_path.into())]; message.insert("paths", XPCObject::from(paths)); diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index 89f2f07..dc7a932 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -1,9 +1,9 @@ use std::sync::mpsc::Sender; -use cursive::views::{Dialog, TextView}; +use cursive::{CbSink, views::{Dialog, DummyView, LinearLayout, RadioGroup, TextView}}; use cursive::Cursive; -use crate::tui::omnibox::command::OmniboxCommand; +use crate::{launchd::query::{DomainType, LimitLoadToSessionType}, tui::omnibox::command::OmniboxCommand}; use crate::tui::omnibox::view::OmniboxEvent; use crate::tui::root::CbSinkMessage; @@ -49,3 +49,60 @@ pub fn show_prompt( Box::new(cl) } + +pub fn domain_session_prompt( + tx: Sender, + f: fn(DomainType, LimitLoadToSessionType) -> Vec, +) -> CbSinkMessage { + let cl = move |siv: &mut Cursive| { + let mut domain_group: RadioGroup = RadioGroup::new(); + let mut lltst_group: RadioGroup = RadioGroup::new(); + + let ask = Dialog::new() + .title("Choose domain and LimitLoadToSessionType") + .content( + LinearLayout::horizontal() + .child( + LinearLayout::vertical() + .child(TextView::new("Domain Type")) + .child(domain_group.button(DomainType::System, "System (1)")) + .child(domain_group.button(DomainType::User, "User (2)")) + .child(domain_group.button(DomainType::UserLogin, "UserLogin (3)")) + .child(domain_group.button(DomainType::Session, "Session (4)")) + // TODO: Ask for handle + .child(domain_group.button(DomainType::PID, "PID (5)").disabled()) + .child(domain_group.button(DomainType::RequestorUserDomain, "Requestor User Domain (6)")) + // TODO: Is this a sane default? + .child(domain_group.button(DomainType::RequestorDomain, "Requestor Domain (7)").selected()) + ) + .child(DummyView) + .child( + LinearLayout::vertical() + .child(TextView::new("Domain Type")) + .child(lltst_group.button(LimitLoadToSessionType::Aqua, LimitLoadToSessionType::Aqua.to_string())) + .child(lltst_group.button(LimitLoadToSessionType::StandardIO, LimitLoadToSessionType::StandardIO.to_string())) + .child(lltst_group.button(LimitLoadToSessionType::Background, LimitLoadToSessionType::Background.to_string())) + .child(lltst_group.button(LimitLoadToSessionType::LoginWindow, LimitLoadToSessionType::LoginWindow.to_string())) + .child(lltst_group.button(LimitLoadToSessionType::System, LimitLoadToSessionType::System.to_string())) + ), + ) + .button("OK", move |s| { + let dt = domain_group.selection().as_ref().clone(); + let lltst = lltst_group.selection().as_ref().clone(); + + f(dt, lltst) + .iter() + .try_for_each(|c| tx.send(OmniboxEvent::Command(c.clone()))) + .expect("Must sent commands"); + + s.pop_layer(); + }) + .button("Cancel", |s| { + s.pop_layer(); + }); + + siv.add_layer(ask); + }; + + Box::new(cl) +} \ No newline at end of file diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index 9a9611d..5174bc1 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -4,18 +4,17 @@ use crate::launchd::query::{DomainType, LimitLoadToSessionType}; #[derive(Debug, Clone, Eq, PartialEq)] pub enum OmniboxCommand { - Chain(Vec), - // Load(DomainType, Option, LimitLoadToSessionType), - // Unload(DomainType, Option), - Load, - Unload, + // Chain(Vec), + Load(LimitLoadToSessionType, DomainType, Option), + Unload(DomainType, Option), // Reuses domain, handle, limit load to session type from existing Reload, Enable, Disable, Edit, // (message, on ok) - Prompt(String, Vec), + Confirm(String, Vec), + DomainSessionPrompt(fn(DomainType, LimitLoadToSessionType) -> Vec), FocusServiceList, Quit, } @@ -26,15 +25,20 @@ impl fmt::Display for OmniboxCommand { } } -pub static OMNIBOX_COMMANDS: [(OmniboxCommand, &str); 7] = [ - (OmniboxCommand::Load, "▶️ Load highlighted job"), - (OmniboxCommand::Unload, "⏏️ Unload highlighted job"), - (OmniboxCommand::Enable, "▶️ Enable highlighted job (enables load)"), - (OmniboxCommand::Disable, "⏏️ Disable highlighted job (prevents load)"), +pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ + ("load", "▶️ Load highlighted job", OmniboxCommand::DomainSessionPrompt(|dt, lltst| vec![ + OmniboxCommand::Load(lltst, dt, None) + ])), + ("unload", "⏏️ Unload highlighted job", OmniboxCommand::DomainSessionPrompt(|dt, _| vec![ + OmniboxCommand::Unload(dt, None) + ])), + ("enable", "▶️ Enable highlighted job (enables load)", OmniboxCommand::Enable), + ("disable", "⏏️ Disable highlighted job (prevents load)", OmniboxCommand::Disable), ( - OmniboxCommand::Edit, + "edit", "✍️ Edit plist with $EDITOR, then reload job", + OmniboxCommand::Edit, ), - (OmniboxCommand::Reload ,"🔄 Reload highlighted job"), - (OmniboxCommand::Quit, "🚪 see ya!"), + ("thing" ,"🔄 Reload highlighted job", OmniboxCommand::Reload), + ("thing", "🚪 see ya!", OmniboxCommand::Quit), ]; diff --git a/launchk/src/tui/omnibox/state.rs b/launchk/src/tui/omnibox/state.rs index c56a792..ad9f3c9 100644 --- a/launchk/src/tui/omnibox/state.rs +++ b/launchk/src/tui/omnibox/state.rs @@ -32,7 +32,7 @@ impl OmniboxState { } /// Suggest a command based on name filter - pub fn suggest_command(&self) -> Option<(OmniboxCommand, &str)> { + pub fn suggest_command(&self) -> Option<(&str, &str, OmniboxCommand)> { let OmniboxState { mode, command_filter, @@ -45,7 +45,7 @@ impl OmniboxState { OMNIBOX_COMMANDS .iter() - .filter(|(c, _)| c.to_string().contains(command_filter)) + .filter(|(c, _, _)| c.to_string().contains(command_filter)) .next() .map(|s| s.clone()) } diff --git a/launchk/src/tui/omnibox/view.rs b/launchk/src/tui/omnibox/view.rs index 776d632..87342f6 100644 --- a/launchk/src/tui/omnibox/view.rs +++ b/launchk/src/tui/omnibox/view.rs @@ -124,8 +124,8 @@ impl OmniboxView { let matched_command = suggested_command .as_ref() - .filter(|(cmd, _)| cmd.to_string() == *command_filter) - .map(|(cmd, _)| cmd.clone()); + .filter(|(cmd, _, _)| *cmd == *command_filter) + .map(|(_, _, oc)| oc.clone()); let (lf_char, cf_char) = match (event, mode) { (Event::Char(c), OmniboxMode::LabelFilter) => { @@ -164,7 +164,7 @@ impl OmniboxView { } // Complete suggestion (Event::Key(Key::Tab), OmniboxMode::CommandFilter) if suggested_command.is_some() => { - let (cmd, _) = suggested_command.unwrap(); + let (cmd, _, _) = suggested_command.unwrap(); // Can submit from here, but catching a glimpse of the whole command // highlighting before flushing back out is confirmation that it did something @@ -269,7 +269,7 @@ impl OmniboxView { if suggestion.is_none() { return; } - let (cmd, desc) = suggestion.unwrap(); + let (cmd, desc, ..) = suggestion.unwrap(); let cmd_string = cmd.to_string().replace(&state.command_filter, ""); printer.with_style(Style::from(Color::Light(BaseColor::Black)), |p| { diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index cb99322..f5a6ff9 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -250,12 +250,18 @@ impl OmniboxSubscriber for RootLayout { .expect("Must focus SL"); Ok(None) } - OmniboxEvent::Command(OmniboxCommand::Prompt(p, c)) => { + OmniboxEvent::Command(OmniboxCommand::Confirm(p, c)) => { self.cbsink_channel .send(dialog::show_prompt(self.omnibox_tx.clone(), p, c)) .expect("Must show prompt"); Ok(None) } + OmniboxEvent::Command(OmniboxCommand::DomainSessionPrompt(f)) => { + self.cbsink_channel + .send(dialog::domain_session_prompt(self.omnibox_tx.clone(), f)) + .expect("Must show prompt"); + Ok(None) + } _ => Ok(None), } } diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index 92b0e36..9b7ef0e 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -190,38 +190,24 @@ impl ServiceListView { // Clear term self.cb_sink.send(Box::new(Cursive::clear)).expect("Must clear"); - Ok(Some(OmniboxCommand::Prompt( + Ok(Some(OmniboxCommand::Confirm( format!("Reload {}?", name), vec![OmniboxCommand::Reload], ))) }, - OmniboxCommand::Load | OmniboxCommand::Unload => { + OmniboxCommand::Load(lltst, dt, _handle) => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; - let xpc_query = if cmd == OmniboxCommand::Load { - load - } else { - unload - }; - - xpc_query(name, plist.plist_path, None, None, None) + load(name, plist.plist_path, Some(dt), Some(lltst), None) .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) }, - OmniboxCommand::Enable | OmniboxCommand::Disable => { + OmniboxCommand::Unload(dt, _handle) => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; - // let xpc_query = if cmd == OmniboxCommand::Enable { - // enable - // } else { - // disable - // }; - - // xpc_query(name, &plist.plist_path) - // .map(|_| None) - // .map_err(|e| OmniboxError::CommandError(e.to_string())) - - Ok(None) + unload(name, plist.plist_path, Some(dt), None) + .map(|_| None) + .map_err(|e| OmniboxError::CommandError(e.to_string())) }, - OmniboxCommand::Reload => Ok(Some(OmniboxCommand::Chain(vec![OmniboxCommand::Unload, OmniboxCommand::Load]))), + // OmniboxCommand::Reload => Ok(Some(OmniboxCommand::Chain(vec![OmniboxCommand::Unload, OmniboxCommand::Load]))), _ => Ok(None), } } From 2d76d603d2671ef55912fc5ffad106a7a7894e3c Mon Sep 17 00:00:00 2001 From: David Stancu Date: Mon, 3 May 2021 19:12:54 -0400 Subject: [PATCH 06/22] better prompt styling --- doc/launchctl_messages.md | 6 ++---- launchk/src/tui/dialog.rs | 31 +++++++++++++++++------------- launchk/src/tui/omnibox/command.rs | 2 +- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/doc/launchctl_messages.md b/doc/launchctl_messages.md index b06aa61..541d948 100644 --- a/doc/launchctl_messages.md +++ b/doc/launchctl_messages.md @@ -1,6 +1,6 @@ # launchctl messages -This is a handy reference of XPC messages sent by `launchctl` for some basic commands, as presented by `xpc_copy_description`. +XPC messages sent by `launchctl` for some basic commands as presented by `xpc_copy_description`. #### `launchctl print system` @@ -196,8 +196,6 @@ strerr Domain does not support specified action "domain-port" => { name = 1799, right = send, urefs = 5 } ``` -Type seems same even if trying from `/Library`: - ``` { count = 11, transaction: 0, voucher = 0x0, contents = "subsystem" => : 3 @@ -331,4 +329,4 @@ Using a `gui/` domain target: 1: System 2: User -8: Login (GUI) \ No newline at end of file +8: Login (GUI)? \ No newline at end of file diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index dc7a932..c1c220d 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -1,6 +1,6 @@ use std::sync::mpsc::Sender; -use cursive::{CbSink, views::{Dialog, DummyView, LinearLayout, RadioGroup, TextView}}; +use cursive::{CbSink, theme::Effect, view::Margins, views::{Dialog, DummyView, LinearLayout, RadioGroup, TextView}}; use cursive::Cursive; use crate::{launchd::query::{DomainType, LimitLoadToSessionType}, tui::omnibox::command::OmniboxCommand}; @@ -59,26 +59,30 @@ pub fn domain_session_prompt( let mut lltst_group: RadioGroup = RadioGroup::new(); let ask = Dialog::new() - .title("Choose domain and LimitLoadToSessionType") + .title("Please choose") .content( LinearLayout::horizontal() .child( LinearLayout::vertical() - .child(TextView::new("Domain Type")) - .child(domain_group.button(DomainType::System, "System (1)")) - .child(domain_group.button(DomainType::User, "User (2)")) - .child(domain_group.button(DomainType::UserLogin, "UserLogin (3)")) - .child(domain_group.button(DomainType::Session, "Session (4)")) + .child(TextView::new("Domain Type") + .effect(Effect::Bold)) + .child(DummyView) + .child(domain_group.button(DomainType::System, "1: System")) + .child(domain_group.button(DomainType::User, "2: User")) + .child(domain_group.button(DomainType::UserLogin, "3: UserLogin")) + .child(domain_group.button(DomainType::Session, "4: Session")) // TODO: Ask for handle - .child(domain_group.button(DomainType::PID, "PID (5)").disabled()) - .child(domain_group.button(DomainType::RequestorUserDomain, "Requestor User Domain (6)")) + .child(domain_group.button(DomainType::PID, "5: PID").disabled()) + .child(domain_group.button(DomainType::RequestorUserDomain, "6: Requestor User Domain")) // TODO: Is this a sane default? - .child(domain_group.button(DomainType::RequestorDomain, "Requestor Domain (7)").selected()) + .child(domain_group.button(DomainType::RequestorDomain, "7: Requestor Domain").selected()) ) .child(DummyView) .child( LinearLayout::vertical() - .child(TextView::new("Domain Type")) + .child(TextView::new("Limit Load To Session Type") + .effect(Effect::Bold)) + .child(DummyView) .child(lltst_group.button(LimitLoadToSessionType::Aqua, LimitLoadToSessionType::Aqua.to_string())) .child(lltst_group.button(LimitLoadToSessionType::StandardIO, LimitLoadToSessionType::StandardIO.to_string())) .child(lltst_group.button(LimitLoadToSessionType::Background, LimitLoadToSessionType::Background.to_string())) @@ -93,13 +97,14 @@ pub fn domain_session_prompt( f(dt, lltst) .iter() .try_for_each(|c| tx.send(OmniboxEvent::Command(c.clone()))) - .expect("Must sent commands"); + .expect("Must send commands"); s.pop_layer(); }) .button("Cancel", |s| { s.pop_layer(); - }); + }) + .padding(Margins::trbl(5, 5, 5, 5)); siv.add_layer(ask); }; diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index 5174bc1..1edeffd 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -4,7 +4,7 @@ use crate::launchd::query::{DomainType, LimitLoadToSessionType}; #[derive(Debug, Clone, Eq, PartialEq)] pub enum OmniboxCommand { - // Chain(Vec), + Chain(Vec), Load(LimitLoadToSessionType, DomainType, Option), Unload(DomainType, Option), // Reuses domain, handle, limit load to session type from existing From 5a90538b9917fec8712315f86d7c62aa3410b1f4 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Tue, 4 May 2021 22:29:09 -0400 Subject: [PATCH 07/22] extend xpcdictionary with builder methods, easier message handling find solution for enums: move to "enums.rs"! --- launchk/src/launchd/entry_status.rs | 9 +- launchk/src/launchd/enums.rs | 89 ++++++++++++ launchk/src/launchd/message.rs | 113 ++++++--------- launchk/src/launchd/mod.rs | 1 + launchk/src/launchd/query.rs | 197 +++++--------------------- launchk/src/tui/dialog.rs | 16 +-- launchk/src/tui/omnibox/command.rs | 7 +- launchk/src/tui/service_list/view.rs | 2 +- xpc-sys/src/objects/xpc_dictionary.rs | 114 +++++++++------ xpc-sys/src/traits/xpc_pipeable.rs | 28 +++- 10 files changed, 279 insertions(+), 297 deletions(-) create mode 100644 launchk/src/launchd/enums.rs diff --git a/launchk/src/launchd/entry_status.rs b/launchk/src/launchd/entry_status.rs index 01298d0..e04e01d 100644 --- a/launchk/src/launchd/entry_status.rs +++ b/launchk/src/launchd/entry_status.rs @@ -4,7 +4,8 @@ use std::sync::Mutex; use std::time::{Duration, SystemTime}; use crate::launchd::plist::LaunchdPlist; -use crate::launchd::query::{find_in_all, LimitLoadToSessionType}; +use crate::launchd::query::find_in_all; +use crate::launchd::enums::SessionType; use xpc_sys::traits::xpc_value::TryXPCValue; const ENTRY_INFO_QUERY_TTL: Duration = Duration::from_secs(15); @@ -17,7 +18,7 @@ lazy_static! { #[derive(Debug, Clone, Eq, PartialEq)] pub struct LaunchdEntryStatus { pub plist: Option, - pub limit_load_to_session_type: LimitLoadToSessionType, + pub limit_load_to_session_type: SessionType, // So, there is a pid_t, but it's i32, and the XPC response has an i64? pub pid: i64, tick: SystemTime, @@ -26,7 +27,7 @@ pub struct LaunchdEntryStatus { impl Default for LaunchdEntryStatus { fn default() -> Self { LaunchdEntryStatus { - limit_load_to_session_type: LimitLoadToSessionType::Unknown, + limit_load_to_session_type: SessionType::Unknown, plist: None, pid: 0, tick: SystemTime::now(), @@ -72,7 +73,7 @@ fn build_entry_status>(label: S) -> LaunchdEntryStatus { .map_err(|e| e.clone()) .and_then(|r| r.get(&["service", "LimitLoadToSessionType"])) .and_then(|o| o.try_into()) - .unwrap_or(LimitLoadToSessionType::Unknown); + .unwrap_or(SessionType::Unknown); let entry_config = crate::launchd::plist::for_label(label_string.clone()); diff --git a/launchk/src/launchd/enums.rs b/launchk/src/launchd/enums.rs new file mode 100644 index 0000000..697142f --- /dev/null +++ b/launchk/src/launchd/enums.rs @@ -0,0 +1,89 @@ +use std::fmt; +use xpc_sys::objects::xpc_object::XPCObject; +use xpc_sys::objects::xpc_type; +use xpc_sys::objects::xpc_type::check_xpc_type; +use xpc_sys::traits::xpc_value::TryXPCValue; +use xpc_sys::objects::xpc_error::XPCError; +use std::convert::TryFrom; + +/// LimitLoadToSessionType key in XPC response +/// https://developer.apple.com/library/archive/technotes/tn2083/_index.html +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum SessionType { + Aqua, + StandardIO, + Background, + LoginWindow, + System, + Unknown, +} + +impl fmt::Display for SessionType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +// TODO: This feels terrible +impl From for SessionType { + fn from(value: String) -> Self { + let aqua: String = SessionType::Aqua.to_string(); + let standard_io: String = SessionType::StandardIO.to_string(); + let background: String = SessionType::Background.to_string(); + let login_window: String = SessionType::LoginWindow.to_string(); + let system: String = SessionType::System.to_string(); + + match value { + s if s == aqua => SessionType::Aqua, + s if s == standard_io => SessionType::StandardIO, + s if s == background => SessionType::Background, + s if s == login_window => SessionType::LoginWindow, + s if s == system => SessionType::System, + _ => SessionType::Unknown, + } + } +} + +impl TryFrom for SessionType { + type Error = XPCError; + + fn try_from(value: XPCObject) -> Result { + check_xpc_type(&value, &xpc_type::String)?; + let string: String = value.xpc_value().unwrap(); + Ok(string.into()) + } +} + +// Huge thanks to: https://saelo.github.io/presentations/bits_of_launchd.pdf +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum DomainType { + System = 1, + User = 2, + UserLogin = 3, + Session = 4, + PID = 5, + RequestorUserDomain = 6, + RequestorDomain = 7, + Unknown, +} + +impl From for DomainType { + fn from(value: u64) -> Self { + match value { + 1 => DomainType::System, + 2 => DomainType::User, + 3 => DomainType::UserLogin, + 4 => DomainType::Session, + 5 => DomainType::PID, + 6 => DomainType::RequestorUserDomain, + 7 => DomainType::RequestorDomain, + _ => DomainType::Unknown, + } + } +} + +impl fmt::Display for DomainType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/launchk/src/launchd/message.rs b/launchk/src/launchd/message.rs index a54d3d3..753b02d 100644 --- a/launchk/src/launchd/message.rs +++ b/launchk/src/launchd/message.rs @@ -1,84 +1,51 @@ use std::collections::HashMap; use xpc_sys::objects::xpc_object::XPCObject; +use xpc_sys::objects::xpc_dictionary::XPCDictionary; use xpc_sys::{get_bootstrap_port, mach_port_t}; lazy_static! { /// launchctl list [name] - pub static ref LIST_SERVICES: HashMap<&'static str, XPCObject> = { + pub static ref LIST_SERVICES: XPCDictionary = XPCDictionary::new() // "list com.apple.Spotlight" (if specified) - // msg.insert("name", XPCObject::from("com.apple.Spotlight")); - - let mut msg = HashMap::new(); - msg.insert("subsystem", XPCObject::from(3 as u64)); - msg.insert("handle", XPCObject::from(0 as u64)); - msg.insert("routine", XPCObject::from(815 as u64)); - msg.insert("type", XPCObject::from(1 as u64)); - - msg.insert("legacy", XPCObject::from(true)); - msg - }; + // .entry("name", "com.apple.Spotlight"); + .entry("subsystem", 3 as u64) + .entry("handle", 0 as u64) + .entry("routine", 815 as u64) + .entry("type", 1 as u64) + .entry("legacy", true); /// launchctl load [path] - pub static ref LOAD_PATHS: HashMap<&'static str, XPCObject> = { - let mut msg = HashMap::new(); - msg.insert("routine", XPCObject::from(800 as u64)); - msg.insert("subsystem", XPCObject::from(3 as u64)); - msg.insert("handle", XPCObject::from(0 as u64)); - msg.insert("legacy", XPCObject::from(true)); - msg.insert("legacy-load", XPCObject::from(true)); - msg.insert("enable", XPCObject::from(false)); - msg.insert("no-einprogress", XPCObject::from(true)); - - msg.insert( - "domain-port", - XPCObject::from(get_bootstrap_port() as mach_port_t), - ); - - msg - }; + pub static ref LOAD_PATHS: XPCDictionary = XPCDictionary::new() + .with_domain_port() + .entry("routine", 800 as u64) + .entry("subsystem", 3 as u64) + .entry("handle", 0 as u64) + .entry("legacy", true) + .entry("legacy-load", true) + .entry("enable", false) + .entry("no-einprogress", true); /// launchctl unload [path] - pub static ref UNLOAD_PATHS: HashMap<&'static str, XPCObject> = { - let mut msg = HashMap::new(); - msg.insert("routine", XPCObject::from(801 as u64)); - msg.insert("subsystem", XPCObject::from(3 as u64)); - msg.insert("handle", XPCObject::from(0 as u64)); - msg.insert("legacy", XPCObject::from(true)); - msg.insert("legacy-load", XPCObject::from(true)); - msg.insert("enable", XPCObject::from(false)); - msg.insert("no-einprogress", XPCObject::from(true)); - - msg.insert( - "domain-port", - XPCObject::from(get_bootstrap_port() as mach_port_t), - ); - - msg - }; - - pub static ref ENABLE_NAMES: HashMap<&'static str, XPCObject> = { - let mut msg = HashMap::new(); - msg.insert("routine", XPCObject::from(808 as u64)); - msg.insert("subsystem", XPCObject::from(3 as u64)); - // UID or ASID - msg.insert("handle", XPCObject::from(0 as u64)); - - msg - }; - - pub static ref DISABLE_NAMES: HashMap<&'static str, XPCObject> = { - let mut msg = HashMap::new(); - msg.insert("routine", XPCObject::from(809 as u64)); - msg.insert("subsystem", XPCObject::from(3 as u64)); - // UID or ASID - msg.insert("handle", XPCObject::from(0 as u64)); - - msg - }; -} - -pub fn from_msg<'a>(proto: &HashMap<&'a str, XPCObject>) -> HashMap<&'a str, XPCObject> { - let mut new_msg: HashMap<&str, XPCObject> = HashMap::new(); - new_msg.extend(proto.iter().map(|(k, v)| (k.clone(), v.clone()))); - new_msg -} + pub static ref UNLOAD_PATHS: XPCDictionary = XPCDictionary::new() + .with_domain_port() + .entry("routine", 801 as u64) + .entry("subsystem", 3 as u64) + .entry("handle", 0 as u64) + .entry("legacy", true) + .entry("legacy-load", true) + .entry("enable", false) + .entry("no-einprogress", true); + + + pub static ref ENABLE_NAMES: XPCDictionary = XPCDictionary::new() + .with_domain_port() + // .entry("handle", UID or ASID) + .entry("routine", 808 as u64) + .entry("subsystem", 3 as u64); + + pub static ref DISABLE_NAMES: XPCDictionary = XPCDictionary::new() + .with_domain_port() + // .entry("handle", UID or ASID) + .entry("routine", 809 as u64) + .entry("subsystem", 3 as u64); +} \ No newline at end of file diff --git a/launchk/src/launchd/mod.rs b/launchk/src/launchd/mod.rs index e1d0ed5..25857d0 100644 --- a/launchk/src/launchd/mod.rs +++ b/launchk/src/launchd/mod.rs @@ -7,3 +7,4 @@ pub mod entry_status; pub mod job_type_filter; /// plist management pub mod plist; +pub mod enums; diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index 65a51ba..20bd1cc 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -1,4 +1,4 @@ -use crate::launchd::message::{from_msg, LIST_SERVICES, LOAD_PATHS, UNLOAD_PATHS}; +use crate::launchd::message::{LIST_SERVICES, LOAD_PATHS, UNLOAD_PATHS}; use std::collections::{HashMap, HashSet}; use std::convert::{TryFrom, TryInto}; use std::fmt; @@ -13,104 +13,20 @@ use std::iter::FromIterator; use xpc_sys::objects::xpc_dictionary::XPCDictionary; use xpc_sys::objects::xpc_error::XPCError; use xpc_sys::objects::xpc_type::check_xpc_type; +use crate::launchd::enums::{DomainType, SessionType}; -// #[link(name = "c")] -// extern "C" { -// fn geteuid() -> u32; -// } - -// lazy_static! { -// static ref IS_ROOT: bool = unsafe { geteuid() } == 0; -// } - -/// LimitLoadToSessionType key in XPC response -/// https://developer.apple.com/library/archive/technotes/tn2083/_index.html -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum LimitLoadToSessionType { - Aqua, - StandardIO, - Background, - LoginWindow, - System, - Unknown, -} - -impl fmt::Display for LimitLoadToSessionType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -// TODO: This feels terrible -impl From for LimitLoadToSessionType { - fn from(value: String) -> Self { - let aqua: String = LimitLoadToSessionType::Aqua.to_string(); - let standard_io: String = LimitLoadToSessionType::StandardIO.to_string(); - let background: String = LimitLoadToSessionType::Background.to_string(); - let login_window: String = LimitLoadToSessionType::LoginWindow.to_string(); - let system: String = LimitLoadToSessionType::System.to_string(); - - match value { - s if s == aqua => LimitLoadToSessionType::Aqua, - s if s == standard_io => LimitLoadToSessionType::StandardIO, - s if s == background => LimitLoadToSessionType::Background, - s if s == login_window => LimitLoadToSessionType::LoginWindow, - s if s == system => LimitLoadToSessionType::System, - _ => LimitLoadToSessionType::Unknown, - } - } -} - -impl TryFrom for LimitLoadToSessionType { - type Error = XPCError; - - fn try_from(value: XPCObject) -> Result { - check_xpc_type(&value, &xpc_type::String)?; - let string: String = value.xpc_value().unwrap(); - Ok(string.into()) - } -} - -// Huge thanks to: https://saelo.github.io/presentations/bits_of_launchd.pdf -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum DomainType { - System = 1, - User = 2, - UserLogin = 3, - Session = 4, - PID = 5, - RequestorUserDomain = 6, - RequestorDomain = 7, - Unknown, -} - -impl From for DomainType { - fn from(value: u64) -> Self { - match value { - 1 => DomainType::System, - 2 => DomainType::User, - 3 => DomainType::UserLogin, - 4 => DomainType::Session, - 5 => DomainType::PID, - 6 => DomainType::RequestorUserDomain, - 7 => DomainType::RequestorDomain, - _ => DomainType::Unknown, - } - } -} - -impl fmt::Display for DomainType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} // TODO: reuse list_all() pub fn find_in_all>(label: S) -> Result { let label_string = label.into(); for domain_type in DomainType::System as u64..DomainType::RequestorDomain as u64 { - let response = list(domain_type.into(), Some(label_string.clone())); + let response = XPCDictionary::new() + .extend(&LIST_SERVICES) + .entry("type", domain_type) + .entry("name", label_string.clone()) + .pipe_routine_with_error_handling(); + if response.is_ok() { return response; } @@ -121,22 +37,11 @@ pub fn find_in_all>(label: S) -> Result /// Query for jobs in a domain pub fn list(domain_type: DomainType, name: Option) -> Result { - let mut msg = from_msg(&LIST_SERVICES); - msg.insert("type", XPCObject::from(domain_type as u64)); - - if name.is_some() { - msg.insert("name", name.unwrap().into()); - } - - let msg: XPCObject = msg.into(); - msg.pipe_routine() - .and_then(|o| o.try_into()) - .and_then(|dict: XPCDictionary| { - dict.0 - .get("error") - .map(|e| Err(XPCError::PipeError(e.to_string()))) - .unwrap_or(Ok(dict)) - }) + XPCDictionary::new() + .extend(&LIST_SERVICES) + .entry("type", domain_type as u64) + .entry_if_present("name", name) + .pipe_routine_with_error_handling() } /// Query for jobs across all domain types @@ -158,73 +63,41 @@ pub fn load>( label: S, plist_path: S, domain_type: Option, - limit_load_to_session_type: Option, + session: Option, handle: Option -) -> XPCPipeResult { - let mut message: HashMap<&str, XPCObject> = from_msg(&LOAD_PATHS); - let label_string = label.into(); - - message.insert("type", XPCObject::from(domain_type.unwrap_or(DomainType::RequestorDomain) as u64)); - message.insert("handle", XPCObject::from(handle.unwrap_or(0))); - message.insert("session", XPCObject::from(limit_load_to_session_type.map(|lltst| lltst.to_string()).unwrap_or("Aqua".to_string()))); - let paths = vec![XPCObject::from(plist_path.into())]; - message.insert("paths", XPCObject::from(paths)); - - let message: XPCObject = message.into(); - +) -> Result { ENTRY_STATUS_CACHE .lock() .expect("Must invalidate") - .remove(&label_string); - - handle_load_unload_errors(label_string, message.pipe_routine()?) + .remove(&label.into()); + + XPCDictionary::new() + .extend(&LOAD_PATHS) + .entry("type", domain_type.unwrap_or(DomainType::RequestorDomain) as u64) + .entry("handle", handle.unwrap_or(0)) + .entry("session", session.map(|s| s.to_string()).unwrap_or("Aqua".to_string())) + .entry("paths", vec![XPCObject::from(plist_path.into())]) + .pipe_routine_with_error_handling() } pub fn unload>( label: S, plist_path: S, domain_type: Option, + session: Option, handle: Option -) -> XPCPipeResult { - let mut message: HashMap<&str, XPCObject> = from_msg(&UNLOAD_PATHS); - let label_string = label.into(); - - message.insert("type", XPCObject::from(domain_type.unwrap_or(DomainType::RequestorDomain) as u64)); - message.insert("handle", XPCObject::from(handle.unwrap_or(0))); - let paths = vec![XPCObject::from(plist_path.into())]; - message.insert("paths", XPCObject::from(paths)); - - let message: XPCObject = message.into(); - +) -> Result { ENTRY_STATUS_CACHE .lock() .expect("Must invalidate") - .remove(&label_string); - - handle_load_unload_errors(label_string, message.pipe_routine()?) + .remove(&label.into()); + + XPCDictionary::new() + .extend(&UNLOAD_PATHS) + .entry("type", domain_type.unwrap_or(DomainType::RequestorDomain) as u64) + .entry("handle", handle.unwrap_or(0)) + .entry("session", session.map(|s| s.to_string()).unwrap_or("Aqua".to_string())) + .entry("paths", vec![XPCObject::from(plist_path.into())]) + .pipe_routine_with_error_handling() } -fn handle_load_unload_errors(label: String, result: XPCObject) -> XPCPipeResult { - let dict: XPCDictionary = result.clone().try_into()?; - let error_dict = dict.get_as_dictionary(&["errors"]); - - if error_dict.is_err() { - Ok(result) - } else { - let mut error_string = "".to_string(); - let XPCDictionary(hm) = error_dict.unwrap(); - - if hm.is_empty() { - return Ok(result); - } - - for (_, errcode) in hm { - let errcode: i64 = errcode.xpc_value().unwrap(); - error_string.push_str( - format!("{}: {}\n", label, xpc_sys::rs_xpc_strerror(errcode as i32)).as_str(), - ); - } - - Err(XPCError::QueryError(error_string)) - } -} diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index c1c220d..1e0e6e9 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -3,7 +3,7 @@ use std::sync::mpsc::Sender; use cursive::{CbSink, theme::Effect, view::Margins, views::{Dialog, DummyView, LinearLayout, RadioGroup, TextView}}; use cursive::Cursive; -use crate::{launchd::query::{DomainType, LimitLoadToSessionType}, tui::omnibox::command::OmniboxCommand}; +use crate::{launchd::enums::{DomainType, SessionType}, tui::omnibox::command::OmniboxCommand}; use crate::tui::omnibox::view::OmniboxEvent; use crate::tui::root::CbSinkMessage; @@ -52,11 +52,11 @@ pub fn show_prompt( pub fn domain_session_prompt( tx: Sender, - f: fn(DomainType, LimitLoadToSessionType) -> Vec, + f: fn(DomainType, SessionType) -> Vec, ) -> CbSinkMessage { let cl = move |siv: &mut Cursive| { let mut domain_group: RadioGroup = RadioGroup::new(); - let mut lltst_group: RadioGroup = RadioGroup::new(); + let mut lltst_group: RadioGroup = RadioGroup::new(); let ask = Dialog::new() .title("Please choose") @@ -83,11 +83,11 @@ pub fn domain_session_prompt( .child(TextView::new("Limit Load To Session Type") .effect(Effect::Bold)) .child(DummyView) - .child(lltst_group.button(LimitLoadToSessionType::Aqua, LimitLoadToSessionType::Aqua.to_string())) - .child(lltst_group.button(LimitLoadToSessionType::StandardIO, LimitLoadToSessionType::StandardIO.to_string())) - .child(lltst_group.button(LimitLoadToSessionType::Background, LimitLoadToSessionType::Background.to_string())) - .child(lltst_group.button(LimitLoadToSessionType::LoginWindow, LimitLoadToSessionType::LoginWindow.to_string())) - .child(lltst_group.button(LimitLoadToSessionType::System, LimitLoadToSessionType::System.to_string())) + .child(lltst_group.button(SessionType::Aqua, SessionType::Aqua.to_string())) + .child(lltst_group.button(SessionType::StandardIO, SessionType::StandardIO.to_string())) + .child(lltst_group.button(SessionType::Background, SessionType::Background.to_string())) + .child(lltst_group.button(SessionType::LoginWindow, SessionType::LoginWindow.to_string())) + .child(lltst_group.button(SessionType::System, SessionType::System.to_string())) ), ) .button("OK", move |s| { diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index 1edeffd..208d0fa 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -1,11 +1,10 @@ use std::fmt; - -use crate::launchd::query::{DomainType, LimitLoadToSessionType}; +use crate::launchd::enums::{DomainType, SessionType}; #[derive(Debug, Clone, Eq, PartialEq)] pub enum OmniboxCommand { Chain(Vec), - Load(LimitLoadToSessionType, DomainType, Option), + Load(SessionType, DomainType, Option), Unload(DomainType, Option), // Reuses domain, handle, limit load to session type from existing Reload, @@ -14,7 +13,7 @@ pub enum OmniboxCommand { Edit, // (message, on ok) Confirm(String, Vec), - DomainSessionPrompt(fn(DomainType, LimitLoadToSessionType) -> Vec), + DomainSessionPrompt(fn(DomainType, SessionType) -> Vec), FocusServiceList, Quit, } diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index 9b7ef0e..a9b5c56 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -203,7 +203,7 @@ impl ServiceListView { }, OmniboxCommand::Unload(dt, _handle) => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; - unload(name, plist.plist_path, Some(dt), None) + unload(name, plist.plist_path, Some(dt), None, None) .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) }, diff --git a/xpc-sys/src/objects/xpc_dictionary.rs b/xpc-sys/src/objects/xpc_dictionary.rs index b7a3149..46e64e1 100644 --- a/xpc-sys/src/objects/xpc_dictionary.rs +++ b/xpc-sys/src/objects/xpc_dictionary.rs @@ -9,50 +9,17 @@ use std::rc::Rc; use crate::objects::xpc_error::XPCError; use crate::objects::xpc_error::XPCError::DictionaryError; use crate::objects::xpc_object::XPCObject; -use crate::{objects, xpc_retain}; +use crate::{objects, xpc_retain, get_bootstrap_port, mach_port_t}; use crate::{xpc_dictionary_apply, xpc_dictionary_create, xpc_dictionary_set_value, xpc_object_t}; use block::ConcreteBlock; +#[derive(Debug, Clone)] pub struct XPCDictionary(pub HashMap); impl XPCDictionary { - /// Reify xpc_object_t dictionary as a Rust HashMap - pub fn new(object: &XPCObject) -> Result { - let XPCObject(_, object_type) = *object; - - if object_type != *objects::xpc_type::Dictionary { - return Err(DictionaryError( - "Only XPC_TYPE_DICTIONARY allowed".to_string(), - )); - } - - let map: Rc>> = Rc::new(RefCell::new(HashMap::new())); - let map_rc_clone = map.clone(); - - let block = ConcreteBlock::new(move |key: *const c_char, value: xpc_object_t| { - // Prevent xpc_release() collection on block exit - unsafe { xpc_retain(value) }; - - let str_key = unsafe { CStr::from_ptr(key).to_string_lossy().to_string() }; - map_rc_clone.borrow_mut().insert(str_key, value.into()); - }); - let block = block.copy(); - - let ok = unsafe { xpc_dictionary_apply(object.as_ptr(), &*block as *const _ as *mut _) }; - - // Explicitly drop the block so map is the only live reference - // so we can collect it below - std::mem::drop(block); - - if ok { - match Rc::try_unwrap(map) { - Ok(cell) => Ok(XPCDictionary(cell.into_inner())), - Err(_) => Err(DictionaryError("Unable to unwrap Rc".to_string())), - } - } else { - Err(DictionaryError("xpc_dictionary_apply failed".to_string())) - } + pub fn new() -> Self{ + XPCDictionary(HashMap::new()) } /// Get value from XPCDictionary with support for nesting @@ -95,6 +62,38 @@ impl XPCDictionary { { self.get(items).and_then(|r| XPCDictionary::try_from(r)) } + + pub fn entry, O: Into>( + mut self, + key: S, + value: O + ) -> XPCDictionary { + let Self(hm) = &mut self; + hm.insert(key.into(), value.into()); + self + } + + pub fn entry_if_present, O: Into>( + self, + key: S, + value: Option + ) -> XPCDictionary { + if value.is_none() { self } + else { + self.entry(key, value.unwrap()) + } + } + + pub fn extend(mut self, other: &XPCDictionary) -> XPCDictionary { + let Self(self_hm) = &mut self; + let Self(other_hm) = other; + self_hm.extend(other_hm.iter().map(|(s, o)| (s.clone(), o.clone()))); + self + } + + pub fn with_domain_port(mut self) -> XPCDictionary { + self.entry("domain-port", get_bootstrap_port() as mach_port_t) + } } impl From> for XPCDictionary { @@ -106,8 +105,41 @@ impl From> for XPCDictionary { impl TryFrom<&XPCObject> for XPCDictionary { type Error = XPCError; - fn try_from(value: &XPCObject) -> Result { - XPCDictionary::new(value) + fn try_from(object: &XPCObject) -> Result { + let XPCObject(_, object_type) = *object; + + if object_type != *objects::xpc_type::Dictionary { + return Err(DictionaryError( + "Only XPC_TYPE_DICTIONARY allowed".to_string(), + )); + } + + let map: Rc>> = Rc::new(RefCell::new(HashMap::new())); + let map_rc_clone = map.clone(); + + let block = ConcreteBlock::new(move |key: *const c_char, value: xpc_object_t| { + // Prevent xpc_release() collection on block exit + unsafe { xpc_retain(value) }; + + let str_key = unsafe { CStr::from_ptr(key).to_string_lossy().to_string() }; + map_rc_clone.borrow_mut().insert(str_key, value.into()); + }); + let block = block.copy(); + + let ok = unsafe { xpc_dictionary_apply(object.as_ptr(), &*block as *const _ as *mut _) }; + + // Explicitly drop the block so map is the only live reference + // so we can collect it below + std::mem::drop(block); + + if ok { + match Rc::try_unwrap(map) { + Ok(cell) => Ok(XPCDictionary(cell.into_inner())), + Err(_) => Err(DictionaryError("Unable to unwrap Rc".to_string())), + } + } else { + Err(DictionaryError("xpc_dictionary_apply failed".to_string())) + } } } @@ -115,7 +147,7 @@ impl TryFrom for XPCDictionary { type Error = XPCError; fn try_from(value: XPCObject) -> Result { - XPCDictionary::new(&value) + (&value).try_into() } } @@ -126,7 +158,7 @@ impl TryFrom for XPCDictionary { /// related to passing in objects other than XPC_TYPE_DICTIONARY fn try_from(value: xpc_object_t) -> Result { let obj: XPCObject = value.into(); - XPCDictionary::new(&obj) + obj.try_into() } } diff --git a/xpc-sys/src/traits/xpc_pipeable.rs b/xpc-sys/src/traits/xpc_pipeable.rs index 1894d7b..dd8f968 100644 --- a/xpc-sys/src/traits/xpc_pipeable.rs +++ b/xpc-sys/src/traits/xpc_pipeable.rs @@ -2,12 +2,11 @@ use crate::objects::xpc_dictionary::XPCDictionary; use crate::objects::xpc_error::XPCError; use crate::objects::xpc_error::XPCError::PipeError; use crate::objects::xpc_object::XPCObject; -use crate::{ - get_xpc_bootstrap_pipe, rs_strerror, xpc_object_t, xpc_pipe_routine, - xpc_pipe_routine_with_flags, -}; +use crate::{get_xpc_bootstrap_pipe, rs_strerror, xpc_object_t, xpc_pipe_routine, xpc_pipe_routine_with_flags, rs_xpc_strerror}; use std::ptr::null_mut; +use std::convert::TryInto; +use crate::traits::xpc_value::TryXPCValue; pub type XPCPipeResult = Result; @@ -15,6 +14,27 @@ pub trait XPCPipeable { fn pipe_routine(&self) -> XPCPipeResult; fn pipe_routine_with_flags(&self, flags: u64) -> XPCPipeResult; + /// Pipe routine expecting XPC dictionary reply, with checking of "error" and "errors" keys + fn pipe_routine_with_error_handling(&self) -> Result { + let response: XPCDictionary = self.pipe_routine()?.try_into()?; + let XPCDictionary(hm) = &response; + + if hm.contains_key("error") { + let errcode: i64 = response.get(&["error"])?.xpc_value()?; + Err(XPCError::QueryError(rs_xpc_strerror(errcode as i32))) + } else if hm.contains_key("errors") { + let XPCDictionary(errors_hm) = response.get_as_dictionary(&["errors"])?; + let errors: Vec = errors_hm.iter().flat_map(|(_, e)| { + let e: Result = e.xpc_value(); + e.map(|e_i64| rs_xpc_strerror(e_i64 as i32)) + }).collect(); + + Err(XPCError::QueryError(errors.join("\n"))) + } else { + Ok(response) + } + } + fn handle_pipe_routine(ptr: xpc_object_t, errno: i32) -> XPCPipeResult { if errno == 0 { Ok(ptr.into()) From a0a50f1af3a5bb2cdabf53febc8dab472a80473a Mon Sep 17 00:00:00 2001 From: David Stancu Date: Wed, 5 May 2021 09:28:19 -0400 Subject: [PATCH 08/22] don't ret err if errors dict empty, show err codes --- xpc-sys/src/traits/xpc_pipeable.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/xpc-sys/src/traits/xpc_pipeable.rs b/xpc-sys/src/traits/xpc_pipeable.rs index dd8f968..5562fb4 100644 --- a/xpc-sys/src/traits/xpc_pipeable.rs +++ b/xpc-sys/src/traits/xpc_pipeable.rs @@ -16,17 +16,19 @@ pub trait XPCPipeable { /// Pipe routine expecting XPC dictionary reply, with checking of "error" and "errors" keys fn pipe_routine_with_error_handling(&self) -> Result { - let response: XPCDictionary = self.pipe_routine()?.try_into()?; + let response = self.pipe_routine()?.try_into()?; let XPCDictionary(hm) = &response; if hm.contains_key("error") { let errcode: i64 = response.get(&["error"])?.xpc_value()?; - Err(XPCError::QueryError(rs_xpc_strerror(errcode as i32))) + Err(XPCError::QueryError(format!("{}: {}", errcode, rs_xpc_strerror(errcode as i32)))) } else if hm.contains_key("errors") { let XPCDictionary(errors_hm) = response.get_as_dictionary(&["errors"])?; + if errors_hm.is_empty() { return Ok(response); } + let errors: Vec = errors_hm.iter().flat_map(|(_, e)| { let e: Result = e.xpc_value(); - e.map(|e_i64| rs_xpc_strerror(e_i64 as i32)) + e.map(|e_i64| format!("{}: {}", e_i64, rs_xpc_strerror(e_i64 as i32))) }).collect(); Err(XPCError::QueryError(errors.join("\n"))) From 1664b6379b079b0c18153f47183603ccf29aa925 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Wed, 5 May 2021 16:18:49 -0400 Subject: [PATCH 09/22] add reload back with prompt, clearer plist err message --- doc/launchctl_messages.md | 22 ++++++++++++++++++++ launchk/src/launchd/plist.rs | 2 +- launchk/src/tui/dialog.rs | 30 ++++++++++++---------------- launchk/src/tui/omnibox/command.rs | 8 ++++---- launchk/src/tui/service_list/view.rs | 17 +++++++++++----- 5 files changed, 52 insertions(+), 27 deletions(-) diff --git a/doc/launchctl_messages.md b/doc/launchctl_messages.md index 541d948..702c695 100644 --- a/doc/launchctl_messages.md +++ b/doc/launchctl_messages.md @@ -177,6 +177,28 @@ type 8 strerr Domain does not support specified action ``` +Response: + +``` + "EnableTransactions" => : true + "Sockets" => { count = 1, transaction: 0, voucher = 0x0, contents = + "Listeners" => { count = 1, capacity = 1, contents = + 0: { type = (invalid descriptor), path = (invalid path) } + } + } + "LimitLoadToSessionType" => { length = 6, contents = "System" } + "Label" => { length = 17, contents = "com.apple.usbmuxd" } + "OnDemand" => : false + "LastExitStatus" => : 0 + "PID" => : 165 + "Program" => { length = 85, contents = "/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/usbmuxd" } + "ProgramArguments" => { count = 2, capacity = 2, contents = + 0: { length = 85, contents = "/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/usbmuxd" } + 1: { length = 8, contents = "-launchd" } + } + } +``` + #### `launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.elasticsearch.plist` ``` diff --git a/launchk/src/launchd/plist.rs b/launchk/src/launchd/plist.rs index 7cef494..c778aaa 100644 --- a/launchk/src/launchd/plist.rs +++ b/launchk/src/launchd/plist.rs @@ -291,7 +291,7 @@ pub fn edit_and_replace(plist_meta: &LaunchdPlist) -> Result<(), String> { } // temp file -> validate with crate -> original - let plist = plist::Value::from_file(&temp_path).map_err(|e| e.to_string())?; + let plist = plist::Value::from_file(&temp_path).map_err(|e| format!("Changes not saved: {}", e))?; let writer = if is_binary { plist::Value::to_file_binary } else { diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index 1e0e6e9..4b4cac9 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -39,9 +39,7 @@ pub fn show_prompt( s.pop_layer(); }) - .button("No", |s| { - s.pop_layer(); - }) + .dismiss_button("No") .title("Notice"); siv.add_layer(ask); @@ -56,7 +54,7 @@ pub fn domain_session_prompt( ) -> CbSinkMessage { let cl = move |siv: &mut Cursive| { let mut domain_group: RadioGroup = RadioGroup::new(); - let mut lltst_group: RadioGroup = RadioGroup::new(); + let mut st_group: RadioGroup = RadioGroup::new(); let ask = Dialog::new() .title("Please choose") @@ -73,37 +71,35 @@ pub fn domain_session_prompt( .child(domain_group.button(DomainType::Session, "4: Session")) // TODO: Ask for handle .child(domain_group.button(DomainType::PID, "5: PID").disabled()) - .child(domain_group.button(DomainType::RequestorUserDomain, "6: Requestor User Domain")) + .child(domain_group.button(DomainType::RequestorUserDomain, "6: Requester User Domain")) // TODO: Is this a sane default? - .child(domain_group.button(DomainType::RequestorDomain, "7: Requestor Domain").selected()) + .child(domain_group.button(DomainType::RequestorDomain, "7: Requester Domain").selected()) ) .child(DummyView) .child( LinearLayout::vertical() - .child(TextView::new("Limit Load To Session Type") + .child(TextView::new("Session Type") .effect(Effect::Bold)) .child(DummyView) - .child(lltst_group.button(SessionType::Aqua, SessionType::Aqua.to_string())) - .child(lltst_group.button(SessionType::StandardIO, SessionType::StandardIO.to_string())) - .child(lltst_group.button(SessionType::Background, SessionType::Background.to_string())) - .child(lltst_group.button(SessionType::LoginWindow, SessionType::LoginWindow.to_string())) - .child(lltst_group.button(SessionType::System, SessionType::System.to_string())) + .child(st_group.button(SessionType::Aqua, SessionType::Aqua.to_string())) + .child(st_group.button(SessionType::StandardIO, SessionType::StandardIO.to_string())) + .child(st_group.button(SessionType::Background, SessionType::Background.to_string())) + .child(st_group.button(SessionType::LoginWindow, SessionType::LoginWindow.to_string())) + .child(st_group.button(SessionType::System, SessionType::System.to_string())) ), ) .button("OK", move |s| { let dt = domain_group.selection().as_ref().clone(); - let lltst = lltst_group.selection().as_ref().clone(); + let st = st_group.selection().as_ref().clone(); - f(dt, lltst) + f(dt, st) .iter() .try_for_each(|c| tx.send(OmniboxEvent::Command(c.clone()))) .expect("Must send commands"); s.pop_layer(); }) - .button("Cancel", |s| { - s.pop_layer(); - }) + .dismiss_button("Cancel") .padding(Margins::trbl(5, 5, 5, 5)); siv.add_layer(ask); diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index 208d0fa..666c3ba 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -25,8 +25,8 @@ impl fmt::Display for OmniboxCommand { } pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ - ("load", "▶️ Load highlighted job", OmniboxCommand::DomainSessionPrompt(|dt, lltst| vec![ - OmniboxCommand::Load(lltst, dt, None) + ("load", "▶️ Load highlighted job", OmniboxCommand::DomainSessionPrompt(|dt, st| vec![ + OmniboxCommand::Load(st, dt, None) ])), ("unload", "⏏️ Unload highlighted job", OmniboxCommand::DomainSessionPrompt(|dt, _| vec![ OmniboxCommand::Unload(dt, None) @@ -38,6 +38,6 @@ pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ "✍️ Edit plist with $EDITOR, then reload job", OmniboxCommand::Edit, ), - ("thing" ,"🔄 Reload highlighted job", OmniboxCommand::Reload), - ("thing", "🚪 see ya!", OmniboxCommand::Quit), + ("reload" ,"🔄 Reload highlighted job", OmniboxCommand::Reload), + ("exit", "🚪 see ya!", OmniboxCommand::Quit), ]; diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index a9b5c56..16a5fe4 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -13,7 +13,7 @@ use cursive::{Cursive, View, XY}; use tokio::runtime::Handle; use tokio::time::interval; -use crate::launchd::{entry_status::get_entry_status, plist::LaunchdPlist}; +use crate::launchd::{entry_status::get_entry_status, plist::LaunchdPlist, entry_status::LaunchdEntryStatus}; use crate::launchd::job_type_filter::JobTypeFilter; use crate::launchd::plist::{edit_and_replace, LABEL_TO_ENTRY_CONFIG}; use crate::launchd::query::{list_all, load, unload}; @@ -195,19 +195,26 @@ impl ServiceListView { vec![OmniboxCommand::Reload], ))) }, - OmniboxCommand::Load(lltst, dt, _handle) => { + OmniboxCommand::Load(st, dt, _handle) => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; - load(name, plist.plist_path, Some(dt), Some(lltst), None) + load(name, plist.plist_path, Some(dt), Some(st), None) .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) }, OmniboxCommand::Unload(dt, _handle) => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; - unload(name, plist.plist_path, Some(dt), None, None) + let LaunchdEntryStatus { limit_load_to_session_type, .. } = get_entry_status(&name); + + unload(name, plist.plist_path, Some(dt), Some(limit_load_to_session_type), None) .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) }, - // OmniboxCommand::Reload => Ok(Some(OmniboxCommand::Chain(vec![OmniboxCommand::Unload, OmniboxCommand::Load]))), + OmniboxCommand::Reload => { + Ok(Some(OmniboxCommand::DomainSessionPrompt(|dt, st| vec![ + OmniboxCommand::Unload(dt.clone(), None), + OmniboxCommand::Load(st, dt, None) + ]))) + }, _ => Ok(None), } } From 199a162167d105bc5872fcab6ff9888aec6fdd71 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Wed, 5 May 2021 16:47:16 -0400 Subject: [PATCH 10/22] fix/fmt --- launchk/src/launchd/entry_status.rs | 2 +- launchk/src/launchd/enums.rs | 4 +- launchk/src/launchd/message.rs | 5 +-- launchk/src/launchd/mod.rs | 2 +- launchk/src/launchd/plist.rs | 3 +- launchk/src/launchd/query.rs | 37 ++++++++++------- launchk/src/tui/dialog.rs | 60 +++++++++++++++++++-------- launchk/src/tui/omnibox/command.rs | 36 +++++++++++----- launchk/src/tui/omnibox/view.rs | 9 ++-- launchk/src/tui/root.rs | 3 +- launchk/src/tui/service_list/view.rs | 50 ++++++++++++++-------- xpc-sys/src/objects/xpc_dictionary.rs | 19 ++++----- xpc-sys/src/traits/xpc_pipeable.rs | 32 +++++++++----- 13 files changed, 166 insertions(+), 96 deletions(-) diff --git a/launchk/src/launchd/entry_status.rs b/launchk/src/launchd/entry_status.rs index e04e01d..1084d24 100644 --- a/launchk/src/launchd/entry_status.rs +++ b/launchk/src/launchd/entry_status.rs @@ -3,9 +3,9 @@ use std::convert::TryInto; use std::sync::Mutex; use std::time::{Duration, SystemTime}; +use crate::launchd::enums::SessionType; use crate::launchd::plist::LaunchdPlist; use crate::launchd::query::find_in_all; -use crate::launchd::enums::SessionType; use xpc_sys::traits::xpc_value::TryXPCValue; const ENTRY_INFO_QUERY_TTL: Duration = Duration::from_secs(15); diff --git a/launchk/src/launchd/enums.rs b/launchk/src/launchd/enums.rs index 697142f..db949ae 100644 --- a/launchk/src/launchd/enums.rs +++ b/launchk/src/launchd/enums.rs @@ -1,10 +1,10 @@ +use std::convert::TryFrom; use std::fmt; +use xpc_sys::objects::xpc_error::XPCError; use xpc_sys::objects::xpc_object::XPCObject; use xpc_sys::objects::xpc_type; use xpc_sys::objects::xpc_type::check_xpc_type; use xpc_sys::traits::xpc_value::TryXPCValue; -use xpc_sys::objects::xpc_error::XPCError; -use std::convert::TryFrom; /// LimitLoadToSessionType key in XPC response /// https://developer.apple.com/library/archive/technotes/tn2083/_index.html diff --git a/launchk/src/launchd/message.rs b/launchk/src/launchd/message.rs index 753b02d..1ec1003 100644 --- a/launchk/src/launchd/message.rs +++ b/launchk/src/launchd/message.rs @@ -1,7 +1,4 @@ -use std::collections::HashMap; -use xpc_sys::objects::xpc_object::XPCObject; use xpc_sys::objects::xpc_dictionary::XPCDictionary; -use xpc_sys::{get_bootstrap_port, mach_port_t}; lazy_static! { /// launchctl list [name] @@ -48,4 +45,4 @@ lazy_static! { // .entry("handle", UID or ASID) .entry("routine", 809 as u64) .entry("subsystem", 3 as u64); -} \ No newline at end of file +} diff --git a/launchk/src/launchd/mod.rs b/launchk/src/launchd/mod.rs index 25857d0..66bec61 100644 --- a/launchk/src/launchd/mod.rs +++ b/launchk/src/launchd/mod.rs @@ -4,7 +4,7 @@ pub mod message; pub mod query; pub mod entry_status; +pub mod enums; pub mod job_type_filter; /// plist management pub mod plist; -pub mod enums; diff --git a/launchk/src/launchd/plist.rs b/launchk/src/launchd/plist.rs index c778aaa..393fd74 100644 --- a/launchk/src/launchd/plist.rs +++ b/launchk/src/launchd/plist.rs @@ -291,7 +291,8 @@ pub fn edit_and_replace(plist_meta: &LaunchdPlist) -> Result<(), String> { } // temp file -> validate with crate -> original - let plist = plist::Value::from_file(&temp_path).map_err(|e| format!("Changes not saved: {}", e))?; + let plist = + plist::Value::from_file(&temp_path).map_err(|e| format!("Changes not saved: {}", e))?; let writer = if is_binary { plist::Value::to_file_binary } else { diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index 20bd1cc..9467db9 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -1,20 +1,16 @@ use crate::launchd::message::{LIST_SERVICES, LOAD_PATHS, UNLOAD_PATHS}; -use std::collections::{HashMap, HashSet}; -use std::convert::{TryFrom, TryInto}; -use std::fmt; +use std::collections::HashSet; use xpc_sys::objects::xpc_object::XPCObject; -use xpc_sys::objects::xpc_type; -use xpc_sys::traits::xpc_pipeable::{XPCPipeResult, XPCPipeable}; -use xpc_sys::traits::xpc_value::TryXPCValue; + +use xpc_sys::traits::xpc_pipeable::XPCPipeable; use crate::launchd::entry_status::ENTRY_STATUS_CACHE; use std::iter::FromIterator; use xpc_sys::objects::xpc_dictionary::XPCDictionary; use xpc_sys::objects::xpc_error::XPCError; -use xpc_sys::objects::xpc_type::check_xpc_type; -use crate::launchd::enums::{DomainType, SessionType}; +use crate::launchd::enums::{DomainType, SessionType}; // TODO: reuse list_all() pub fn find_in_all>(label: S) -> Result { @@ -64,7 +60,7 @@ pub fn load>( plist_path: S, domain_type: Option, session: Option, - handle: Option + handle: Option, ) -> Result { ENTRY_STATUS_CACHE .lock() @@ -73,9 +69,15 @@ pub fn load>( XPCDictionary::new() .extend(&LOAD_PATHS) - .entry("type", domain_type.unwrap_or(DomainType::RequestorDomain) as u64) + .entry( + "type", + domain_type.unwrap_or(DomainType::RequestorDomain) as u64, + ) .entry("handle", handle.unwrap_or(0)) - .entry("session", session.map(|s| s.to_string()).unwrap_or("Aqua".to_string())) + .entry( + "session", + session.map(|s| s.to_string()).unwrap_or("Aqua".to_string()), + ) .entry("paths", vec![XPCObject::from(plist_path.into())]) .pipe_routine_with_error_handling() } @@ -85,7 +87,7 @@ pub fn unload>( plist_path: S, domain_type: Option, session: Option, - handle: Option + handle: Option, ) -> Result { ENTRY_STATUS_CACHE .lock() @@ -94,10 +96,15 @@ pub fn unload>( XPCDictionary::new() .extend(&UNLOAD_PATHS) - .entry("type", domain_type.unwrap_or(DomainType::RequestorDomain) as u64) + .entry( + "type", + domain_type.unwrap_or(DomainType::RequestorDomain) as u64, + ) .entry("handle", handle.unwrap_or(0)) - .entry("session", session.map(|s| s.to_string()).unwrap_or("Aqua".to_string())) + .entry( + "session", + session.map(|s| s.to_string()).unwrap_or("Aqua".to_string()), + ) .entry("paths", vec![XPCObject::from(plist_path.into())]) .pipe_routine_with_error_handling() } - diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index 4b4cac9..90ec267 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -1,11 +1,18 @@ use std::sync::mpsc::Sender; -use cursive::{CbSink, theme::Effect, view::Margins, views::{Dialog, DummyView, LinearLayout, RadioGroup, TextView}}; use cursive::Cursive; +use cursive::{ + theme::Effect, + view::Margins, + views::{Dialog, DummyView, LinearLayout, RadioGroup, TextView}, +}; -use crate::{launchd::enums::{DomainType, SessionType}, tui::omnibox::command::OmniboxCommand}; use crate::tui::omnibox::view::OmniboxEvent; use crate::tui::root::CbSinkMessage; +use crate::{ + launchd::enums::{DomainType, SessionType}, + tui::omnibox::command::OmniboxCommand, +}; /// The XPC error key sometimes contains information that is not necessarily a failure, /// so let's just call it "Notice" until we figure out what to do next? @@ -59,11 +66,10 @@ pub fn domain_session_prompt( let ask = Dialog::new() .title("Please choose") .content( - LinearLayout::horizontal() + LinearLayout::horizontal() .child( - LinearLayout::vertical() - .child(TextView::new("Domain Type") - .effect(Effect::Bold)) + LinearLayout::vertical() + .child(TextView::new("Domain Type").effect(Effect::Bold)) .child(DummyView) .child(domain_group.button(DomainType::System, "1: System")) .child(domain_group.button(DomainType::User, "2: User")) @@ -71,21 +77,41 @@ pub fn domain_session_prompt( .child(domain_group.button(DomainType::Session, "4: Session")) // TODO: Ask for handle .child(domain_group.button(DomainType::PID, "5: PID").disabled()) - .child(domain_group.button(DomainType::RequestorUserDomain, "6: Requester User Domain")) + .child(domain_group.button( + DomainType::RequestorUserDomain, + "6: Requester User Domain", + )) // TODO: Is this a sane default? - .child(domain_group.button(DomainType::RequestorDomain, "7: Requester Domain").selected()) + .child( + domain_group + .button(DomainType::RequestorDomain, "7: Requester Domain") + .selected(), + ), ) .child(DummyView) .child( - LinearLayout::vertical() - .child(TextView::new("Session Type") - .effect(Effect::Bold)) + LinearLayout::vertical() + .child(TextView::new("Session Type").effect(Effect::Bold)) .child(DummyView) - .child(st_group.button(SessionType::Aqua, SessionType::Aqua.to_string())) - .child(st_group.button(SessionType::StandardIO, SessionType::StandardIO.to_string())) - .child(st_group.button(SessionType::Background, SessionType::Background.to_string())) - .child(st_group.button(SessionType::LoginWindow, SessionType::LoginWindow.to_string())) - .child(st_group.button(SessionType::System, SessionType::System.to_string())) + .child( + st_group.button(SessionType::Aqua, SessionType::Aqua.to_string()), + ) + .child(st_group.button( + SessionType::StandardIO, + SessionType::StandardIO.to_string(), + )) + .child(st_group.button( + SessionType::Background, + SessionType::Background.to_string(), + )) + .child(st_group.button( + SessionType::LoginWindow, + SessionType::LoginWindow.to_string(), + )) + .child( + st_group + .button(SessionType::System, SessionType::System.to_string()), + ), ), ) .button("OK", move |s| { @@ -106,4 +132,4 @@ pub fn domain_session_prompt( }; Box::new(cl) -} \ No newline at end of file +} diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index 666c3ba..f8e30f3 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -1,5 +1,5 @@ -use std::fmt; use crate::launchd::enums::{DomainType, SessionType}; +use std::fmt; #[derive(Debug, Clone, Eq, PartialEq)] pub enum OmniboxCommand { @@ -25,19 +25,35 @@ impl fmt::Display for OmniboxCommand { } pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ - ("load", "▶️ Load highlighted job", OmniboxCommand::DomainSessionPrompt(|dt, st| vec![ - OmniboxCommand::Load(st, dt, None) - ])), - ("unload", "⏏️ Unload highlighted job", OmniboxCommand::DomainSessionPrompt(|dt, _| vec![ - OmniboxCommand::Unload(dt, None) - ])), - ("enable", "▶️ Enable highlighted job (enables load)", OmniboxCommand::Enable), - ("disable", "⏏️ Disable highlighted job (prevents load)", OmniboxCommand::Disable), + ( + "load", + "▶️ Load highlighted job", + OmniboxCommand::DomainSessionPrompt(|dt, st| vec![OmniboxCommand::Load(st, dt, None)]), + ), + ( + "unload", + "⏏️ Unload highlighted job", + OmniboxCommand::DomainSessionPrompt(|dt, _| vec![OmniboxCommand::Unload(dt, None)]), + ), + ( + "enable", + "▶️ Enable highlighted job (enables load)", + OmniboxCommand::Enable, + ), + ( + "disable", + "⏏️ Disable highlighted job (prevents load)", + OmniboxCommand::Disable, + ), ( "edit", "✍️ Edit plist with $EDITOR, then reload job", OmniboxCommand::Edit, ), - ("reload" ,"🔄 Reload highlighted job", OmniboxCommand::Reload), + ( + "reload", + "🔄 Reload highlighted job", + OmniboxCommand::Reload, + ), ("exit", "🚪 see ya!", OmniboxCommand::Quit), ]; diff --git a/launchk/src/tui/omnibox/view.rs b/launchk/src/tui/omnibox/view.rs index 87342f6..e9a92c9 100644 --- a/launchk/src/tui/omnibox/view.rs +++ b/launchk/src/tui/omnibox/view.rs @@ -61,7 +61,7 @@ async fn tick(state: Arc>, tx: Sender) { let new = match *mode { OmniboxMode::CommandFilter | OmniboxMode::CommandConfirm(_) => { - read.with_new(Some(OmniboxMode::Idle), None, Some("".to_string()), None) + read.with_new(Some(OmniboxMode::Idle), None, Some("".to_string()), None) } _ => read.with_new(Some(OmniboxMode::Idle), None, None, None), }; @@ -72,9 +72,10 @@ async fn tick(state: Arc>, tx: Sender) { [ OmniboxEvent::Command(OmniboxCommand::FocusServiceList), OmniboxEvent::StateUpdate(new.clone()), - ].iter() - .try_for_each(|e| tx.send(e.clone())) - .expect("Must send events"); + ] + .iter() + .try_for_each(|e| tx.send(e.clone())) + .expect("Must send events"); log::debug!("[omnibox/tick]: New state: {:?}", &new); diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index f5a6ff9..f604310 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -229,8 +229,7 @@ impl OmniboxSubscriber for RootLayout { fn on_omnibox(&mut self, cmd: OmniboxEvent) -> OmniboxResult { match cmd { OmniboxEvent::Command(OmniboxCommand::Chain(cmds)) => { - cmds - .iter() + cmds.iter() .try_for_each(|c| self.omnibox_tx.send(OmniboxEvent::Command(c.clone()))) .expect("Must send commands"); Ok(None) diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index 16a5fe4..fbd7675 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::cmp::Ordering; use std::collections::HashSet; use std::rc::Rc; -use std::sync::mpsc::{Sender}; +use std::sync::mpsc::Sender; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -13,10 +13,12 @@ use cursive::{Cursive, View, XY}; use tokio::runtime::Handle; use tokio::time::interval; -use crate::launchd::{entry_status::get_entry_status, plist::LaunchdPlist, entry_status::LaunchdEntryStatus}; use crate::launchd::job_type_filter::JobTypeFilter; use crate::launchd::plist::{edit_and_replace, LABEL_TO_ENTRY_CONFIG}; use crate::launchd::query::{list_all, load, unload}; +use crate::launchd::{ + entry_status::get_entry_status, entry_status::LaunchdEntryStatus, plist::LaunchdPlist, +}; use crate::tui::omnibox::command::OmniboxCommand; use crate::tui::omnibox::state::OmniboxState; use crate::tui::omnibox::subscribed_view::{OmniboxResult, OmniboxSubscriber}; @@ -173,7 +175,8 @@ impl ServiceListView { fn with_active_item_plist(&self) -> Result<(ServiceListItem, LaunchdPlist), OmniboxError> { let item = &*self.get_active_list_item()?; - let plist = item.status + let plist = item + .status .plist .as_ref() .ok_or_else(|| OmniboxError::CommandError("Cannot find plist".to_string()))?; @@ -183,38 +186,49 @@ impl ServiceListView { fn handle_command(&self, cmd: OmniboxCommand) -> OmniboxResult { match cmd { - OmniboxCommand::Edit => { + OmniboxCommand::Edit => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; edit_and_replace(&plist).map_err(OmniboxError::CommandError)?; // Clear term - self.cb_sink.send(Box::new(Cursive::clear)).expect("Must clear"); + self.cb_sink + .send(Box::new(Cursive::clear)) + .expect("Must clear"); Ok(Some(OmniboxCommand::Confirm( format!("Reload {}?", name), vec![OmniboxCommand::Reload], ))) - }, + } OmniboxCommand::Load(st, dt, _handle) => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; load(name, plist.plist_path, Some(dt), Some(st), None) .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) - }, + } OmniboxCommand::Unload(dt, _handle) => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; - let LaunchdEntryStatus { limit_load_to_session_type, .. } = get_entry_status(&name); - - unload(name, plist.plist_path, Some(dt), Some(limit_load_to_session_type), None) - .map(|_| None) - .map_err(|e| OmniboxError::CommandError(e.to_string())) - }, - OmniboxCommand::Reload => { - Ok(Some(OmniboxCommand::DomainSessionPrompt(|dt, st| vec![ + let LaunchdEntryStatus { + limit_load_to_session_type, + .. + } = get_entry_status(&name); + + unload( + name, + plist.plist_path, + Some(dt), + Some(limit_load_to_session_type), + None, + ) + .map(|_| None) + .map_err(|e| OmniboxError::CommandError(e.to_string())) + } + OmniboxCommand::Reload => Ok(Some(OmniboxCommand::DomainSessionPrompt(|dt, st| { + vec![ OmniboxCommand::Unload(dt.clone(), None), - OmniboxCommand::Load(st, dt, None) - ]))) - }, + OmniboxCommand::Load(st, dt, None), + ] + }))), _ => Ok(None), } } diff --git a/xpc-sys/src/objects/xpc_dictionary.rs b/xpc-sys/src/objects/xpc_dictionary.rs index 46e64e1..d903ec6 100644 --- a/xpc-sys/src/objects/xpc_dictionary.rs +++ b/xpc-sys/src/objects/xpc_dictionary.rs @@ -9,7 +9,7 @@ use std::rc::Rc; use crate::objects::xpc_error::XPCError; use crate::objects::xpc_error::XPCError::DictionaryError; use crate::objects::xpc_object::XPCObject; -use crate::{objects, xpc_retain, get_bootstrap_port, mach_port_t}; +use crate::{get_bootstrap_port, mach_port_t, objects, xpc_retain}; use crate::{xpc_dictionary_apply, xpc_dictionary_create, xpc_dictionary_set_value, xpc_object_t}; use block::ConcreteBlock; @@ -18,7 +18,7 @@ use block::ConcreteBlock; pub struct XPCDictionary(pub HashMap); impl XPCDictionary { - pub fn new() -> Self{ + pub fn new() -> Self { XPCDictionary(HashMap::new()) } @@ -63,11 +63,7 @@ impl XPCDictionary { self.get(items).and_then(|r| XPCDictionary::try_from(r)) } - pub fn entry, O: Into>( - mut self, - key: S, - value: O - ) -> XPCDictionary { + pub fn entry, O: Into>(mut self, key: S, value: O) -> XPCDictionary { let Self(hm) = &mut self; hm.insert(key.into(), value.into()); self @@ -76,10 +72,11 @@ impl XPCDictionary { pub fn entry_if_present, O: Into>( self, key: S, - value: Option + value: Option, ) -> XPCDictionary { - if value.is_none() { self } - else { + if value.is_none() { + self + } else { self.entry(key, value.unwrap()) } } @@ -91,7 +88,7 @@ impl XPCDictionary { self } - pub fn with_domain_port(mut self) -> XPCDictionary { + pub fn with_domain_port(self) -> XPCDictionary { self.entry("domain-port", get_bootstrap_port() as mach_port_t) } } diff --git a/xpc-sys/src/traits/xpc_pipeable.rs b/xpc-sys/src/traits/xpc_pipeable.rs index 5562fb4..60cc47e 100644 --- a/xpc-sys/src/traits/xpc_pipeable.rs +++ b/xpc-sys/src/traits/xpc_pipeable.rs @@ -2,11 +2,14 @@ use crate::objects::xpc_dictionary::XPCDictionary; use crate::objects::xpc_error::XPCError; use crate::objects::xpc_error::XPCError::PipeError; use crate::objects::xpc_object::XPCObject; -use crate::{get_xpc_bootstrap_pipe, rs_strerror, xpc_object_t, xpc_pipe_routine, xpc_pipe_routine_with_flags, rs_xpc_strerror}; +use crate::{ + get_xpc_bootstrap_pipe, rs_strerror, rs_xpc_strerror, xpc_object_t, xpc_pipe_routine, + xpc_pipe_routine_with_flags, +}; -use std::ptr::null_mut; -use std::convert::TryInto; use crate::traits::xpc_value::TryXPCValue; +use std::convert::TryInto; +use std::ptr::null_mut; pub type XPCPipeResult = Result; @@ -21,15 +24,24 @@ pub trait XPCPipeable { if hm.contains_key("error") { let errcode: i64 = response.get(&["error"])?.xpc_value()?; - Err(XPCError::QueryError(format!("{}: {}", errcode, rs_xpc_strerror(errcode as i32)))) + Err(XPCError::QueryError(format!( + "{}: {}", + errcode, + rs_xpc_strerror(errcode as i32) + ))) } else if hm.contains_key("errors") { let XPCDictionary(errors_hm) = response.get_as_dictionary(&["errors"])?; - if errors_hm.is_empty() { return Ok(response); } - - let errors: Vec = errors_hm.iter().flat_map(|(_, e)| { - let e: Result = e.xpc_value(); - e.map(|e_i64| format!("{}: {}", e_i64, rs_xpc_strerror(e_i64 as i32))) - }).collect(); + if errors_hm.is_empty() { + return Ok(response); + } + + let errors: Vec = errors_hm + .iter() + .flat_map(|(_, e)| { + let e: Result = e.xpc_value(); + e.map(|e_i64| format!("{}: {}", e_i64, rs_xpc_strerror(e_i64 as i32))) + }) + .collect(); Err(XPCError::QueryError(errors.join("\n"))) } else { From 578ae2bd7599325552fd3c613e251af2efe416f3 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Thu, 6 May 2021 11:57:11 -0400 Subject: [PATCH 11/22] query stuff xpcd->querybuilder, more readme --- README.md | 98 +++++++++++++++++++++------ launchk/src/launchd/message.rs | 2 +- launchk/src/launchd/mod.rs | 1 + launchk/src/launchd/query.rs | 31 +++------ launchk/src/launchd/query_builder.rs | 58 ++++++++++++++++ xpc-sys/src/objects/xpc_dictionary.rs | 29 -------- xpc-sys/src/objects/xpc_object.rs | 8 +-- xpc-sys/src/traits/xpc_value.rs | 11 +++ 8 files changed, 163 insertions(+), 75 deletions(-) create mode 100644 launchk/src/launchd/query_builder.rs diff --git a/README.md b/README.md index 433a1b9..c205e74 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,78 @@ Should work on macOS 10.10+ according to the availability sec. [in the docs](htt There is some "convenience glue" for dealing with XPC objects. Eventually, this will be broken out into its own crate. Most of the tests (for now) are written around not breaking data going across the FFI barrier. +##### Object lifecycle + XPCObject wraps `xpc_object_t` in an `Arc`. `Drop` will invoke `xpc_release()` on objects being dropped with no other [strong refs](https://doc.rust-lang.org/std/sync/struct.Arc.html#method.strong_count). -#### xpc_pipe_routine +**NOTE**: When using Objective-C blocks with the [block crate](https://crates.io/crates/block) (e.g. looping over an array), make sure to invoke `xpc_retain()` on any object you wish to keep after the closure is dropped, or else the XPC objects in the closure will be dropped as well! See the `XPCDictionary` implementation for more details. xpc-sys handles this for you for its conversions. + +#### XPCDictionary and QueryBuilder + +While we can go from `HashMap<&str, XPCObject>` to `XPCObject`, it can be a little verbose. A `QueryBuilder` trait exposes some builder methods to make building an XPC dictionary a little easier (without all of the `into()`s, and some additional error checking). + +To write the query for `launchctl list`: + +```rust + let LIST_SERVICES: XPCDictionary = XPCDictionary::new() + // "list com.apple.Spotlight" (if specified) + // .entry("name", "com.apple.Spotlight"); + .entry("subsystem", 3 as u64) + .entry("handle", 0 as u64) + .entry("routine", 815 as u64) + .entry("legacy", true); + + let reply: Result = XPCDictionary::new() + // LIST_SERVICES is a proto + .extend(&LIST_SERVICES) + // Specify the domain type, or fall back on requester domain + .with_domain_type_or_default(Some(domain_type)) + .entry_if_present("name", name) + .pipe_routine_with_error_handling(); +``` + +In addition to checking `errno` is 0, `pipe_routine_with_error_handling` also looks for possible `error` and `errors` keys in the response dictionary and provides an `Err()` with `xpc_strerror` contents. + +#### FFI Type Conversions + +Conversions to/from Rust/XPC objects uses the [xpc.h functions documented on Apple Developer](https://developer.apple.com/documentation/xpc/xpc_services_xpc_h?language=objc) using the `From` trait. + +| Rust | XPC | +|----------------------------------------|-----------------------| +| i64 | _xpc_type_int64 | +| u64 | _xpc_type_uint64 | +| f64 | _xpc_type_double | +| bool | _xpc_bool_true/false | +| Into | _xpc_type_string | +| HashMap, Into> | _xpc_type_dictionary | +| Vec> | _xpc_type_array | + +Make XPC objects for anything with `From`. From earlier example, even Mach ports: +```rust +let mut message: HashMap<&str, XPCObject> = HashMap::new(); + +message.insert( + "domain-port", + XPCObject::from(get_bootstrap_port() as mach_port_t), +); +``` + +Go from an XPC object to value via the `TryXPCValue` trait. It checks your object's type via `xpc_get_type()` and yields a clear error if you're using the wrong type: +```rust +#[test] +fn deserialize_as_wrong_type() { + let an_i64: XPCObject = XPCObject::from(42 as i64); + let as_u64: Result = an_i64.xpc_value(); + assert_eq!( + as_u64.err().unwrap(), + XPCValueError("Cannot get int64 as uint64".to_string()) + ); +} +``` + +##### XPC Dictionaries -Form your messages as a HashMap: +Go from a `HashMap` to `xpc_object_t` with the `XPCObject` type: ```rust let mut message: HashMap<&str, XPCObject> = HashMap::new(); @@ -88,27 +155,18 @@ let XPCDictionary(hm) = response.unwrap(); let whatever = hm.get("..."); ``` -#### Making XPC Objects +##### XPC Arrays + +An XPC array can be made from either `Vec` or `Vec>`: -Make XPC objects for anything with `From`. From earlier example, even Mach ports: ```rust -let mut message: HashMap<&str, XPCObject> = HashMap::new(); +let xpc_array = XPCObject::from(vec![XPCObject::from("eins"), XPCObject::from("zwei"), XPCObject::from("polizei")]); -message.insert( - "domain-port", - XPCObject::from(get_bootstrap_port() as mach_port_t), -); +let xpc_array = XPCObject::from(vec!["eins", "zwei", "polizei"]) ``` -Go from an XPC object to value via `TryXPCValue`. It checks your object's type via `xpc_get_type()` and yields a clear error if you're using the wrong type: +Go back to `Vec` using `xpc_value`: + ```rust -#[test] -fn deserialize_as_wrong_type() { - let an_i64: XPCObject = XPCObject::from(42 as i64); - let as_u64: Result = an_i64.xpc_value(); - assert_eq!( - as_u64.err().unwrap(), - XPCValueError("Cannot get int64 as uint64".to_string()) - ); -} -``` +let rs_vec: Vec = xpc_array.xpc_value().unwrap(); +``` \ No newline at end of file diff --git a/launchk/src/launchd/message.rs b/launchk/src/launchd/message.rs index 1ec1003..8ff9747 100644 --- a/launchk/src/launchd/message.rs +++ b/launchk/src/launchd/message.rs @@ -1,4 +1,5 @@ use xpc_sys::objects::xpc_dictionary::XPCDictionary; +use crate::launchd::query_builder::QueryBuilder; lazy_static! { /// launchctl list [name] @@ -8,7 +9,6 @@ lazy_static! { .entry("subsystem", 3 as u64) .entry("handle", 0 as u64) .entry("routine", 815 as u64) - .entry("type", 1 as u64) .entry("legacy", true); /// launchctl load [path] diff --git a/launchk/src/launchd/mod.rs b/launchk/src/launchd/mod.rs index 66bec61..afa2ed0 100644 --- a/launchk/src/launchd/mod.rs +++ b/launchk/src/launchd/mod.rs @@ -8,3 +8,4 @@ pub mod enums; pub mod job_type_filter; /// plist management pub mod plist; +pub mod query_builder; diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index 9467db9..8940b4b 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -11,6 +11,7 @@ use xpc_sys::objects::xpc_dictionary::XPCDictionary; use xpc_sys::objects::xpc_error::XPCError; use crate::launchd::enums::{DomainType, SessionType}; +use crate::launchd::query_builder::QueryBuilder; // TODO: reuse list_all() pub fn find_in_all>(label: S) -> Result { @@ -35,7 +36,7 @@ pub fn find_in_all>(label: S) -> Result pub fn list(domain_type: DomainType, name: Option) -> Result { XPCDictionary::new() .extend(&LIST_SERVICES) - .entry("type", domain_type as u64) + .with_domain_type_or_default(Some(domain_type)) .entry_if_present("name", name) .pipe_routine_with_error_handling() } @@ -69,16 +70,10 @@ pub fn load>( XPCDictionary::new() .extend(&LOAD_PATHS) - .entry( - "type", - domain_type.unwrap_or(DomainType::RequestorDomain) as u64, - ) - .entry("handle", handle.unwrap_or(0)) - .entry( - "session", - session.map(|s| s.to_string()).unwrap_or("Aqua".to_string()), - ) - .entry("paths", vec![XPCObject::from(plist_path.into())]) + .with_domain_type_or_default(domain_type) + .with_session_type_or_default(session) + .with_handle_or_default(handle) + .entry("paths", vec![plist_path.into()]) .pipe_routine_with_error_handling() } @@ -96,15 +91,9 @@ pub fn unload>( XPCDictionary::new() .extend(&UNLOAD_PATHS) - .entry( - "type", - domain_type.unwrap_or(DomainType::RequestorDomain) as u64, - ) - .entry("handle", handle.unwrap_or(0)) - .entry( - "session", - session.map(|s| s.to_string()).unwrap_or("Aqua".to_string()), - ) - .entry("paths", vec![XPCObject::from(plist_path.into())]) + .with_domain_type_or_default(domain_type) + .with_session_type_or_default(session) + .with_handle_or_default(handle) + .entry("paths", vec![plist_path.into()]) .pipe_routine_with_error_handling() } diff --git a/launchk/src/launchd/query_builder.rs b/launchk/src/launchd/query_builder.rs new file mode 100644 index 0000000..95a1517 --- /dev/null +++ b/launchk/src/launchd/query_builder.rs @@ -0,0 +1,58 @@ +use xpc_sys::objects::xpc_object::XPCObject; +use xpc_sys::objects::xpc_dictionary::XPCDictionary; +use xpc_sys::{get_bootstrap_port, mach_port_t}; +use crate::launchd::enums::{SessionType, DomainType}; + +pub trait QueryBuilder { + fn entry, O: Into>(self, key: S, value: O) -> XPCDictionary; + fn entry_if_present, O: Into>( + self, + key: S, + value: Option, + ) -> XPCDictionary; + + fn extend(self, other: &XPCDictionary) -> XPCDictionary; + + fn with_domain_port(self) -> XPCDictionary where Self: Sized { + self.entry("domain-port", get_bootstrap_port() as mach_port_t) + } + + fn with_session_type_or_default(self, session: Option) -> XPCDictionary where Self: Sized { + self.entry("session", session.unwrap_or(SessionType::Aqua).to_string()) + } + + fn with_handle_or_default(self, session: Option) -> XPCDictionary where Self: Sized { + self.entry("handle", session.unwrap_or(0)) + } + + fn with_domain_type_or_default(self, t: Option) -> XPCDictionary where Self: Sized { + self.entry("type", t.unwrap_or(DomainType::RequestorDomain) as u64) + } +} + +impl QueryBuilder for XPCDictionary { + fn entry, O: Into>(mut self, key: S, value: O) -> XPCDictionary { + let Self(hm) = &mut self; + hm.insert(key.into(), value.into()); + self + } + + fn entry_if_present, O: Into>( + self, + key: S, + value: Option, + ) -> XPCDictionary { + if value.is_none() { + self + } else { + self.entry(key, value.unwrap()) + } + } + + fn extend(mut self, other: &XPCDictionary) -> XPCDictionary { + let Self(self_hm) = &mut self; + let Self(other_hm) = other; + self_hm.extend(other_hm.iter().map(|(s, o)| (s.clone(), o.clone()))); + self + } +} \ No newline at end of file diff --git a/xpc-sys/src/objects/xpc_dictionary.rs b/xpc-sys/src/objects/xpc_dictionary.rs index d903ec6..2e2f122 100644 --- a/xpc-sys/src/objects/xpc_dictionary.rs +++ b/xpc-sys/src/objects/xpc_dictionary.rs @@ -62,35 +62,6 @@ impl XPCDictionary { { self.get(items).and_then(|r| XPCDictionary::try_from(r)) } - - pub fn entry, O: Into>(mut self, key: S, value: O) -> XPCDictionary { - let Self(hm) = &mut self; - hm.insert(key.into(), value.into()); - self - } - - pub fn entry_if_present, O: Into>( - self, - key: S, - value: Option, - ) -> XPCDictionary { - if value.is_none() { - self - } else { - self.entry(key, value.unwrap()) - } - } - - pub fn extend(mut self, other: &XPCDictionary) -> XPCDictionary { - let Self(self_hm) = &mut self; - let Self(other_hm) = other; - self_hm.extend(other_hm.iter().map(|(s, o)| (s.clone(), o.clone()))); - self - } - - pub fn with_domain_port(self) -> XPCDictionary { - self.entry("domain-port", get_bootstrap_port() as mach_port_t) - } } impl From> for XPCDictionary { diff --git a/xpc-sys/src/objects/xpc_object.rs b/xpc-sys/src/objects/xpc_object.rs index 3534f75..a77a5bc 100644 --- a/xpc-sys/src/objects/xpc_object.rs +++ b/xpc-sys/src/objects/xpc_object.rs @@ -102,11 +102,11 @@ impl From<&str> for XPCObject { } } -impl From> for XPCObject { - fn from(slice: Vec) -> Self { +impl> From> for XPCObject { + fn from(value: Vec) -> Self { let xpc_array = unsafe { xpc_array_create(null_mut(), 0) }; - for object in slice { - unsafe { xpc_array_append_value(xpc_array, object.as_ptr()) } + for object in value { + unsafe { xpc_array_append_value(xpc_array, object.into().as_ptr()) } } XPCObject::new(xpc_array) diff --git a/xpc-sys/src/traits/xpc_value.rs b/xpc-sys/src/traits/xpc_value.rs index 187cc4c..5b66865 100644 --- a/xpc-sys/src/traits/xpc_value.rs +++ b/xpc-sys/src/traits/xpc_value.rs @@ -130,4 +130,15 @@ mod tests { let rs_u64: u64 = xpc_u64.xpc_value().unwrap(); assert_eq!(std::u64::MAX, rs_u64); } + + #[test] + fn xpc_value_array() { + let xpc_array = XPCObject::from(vec!["eins", "zwei", "polizei"]); + let rs_vec: Vec = xpc_array.xpc_value().unwrap(); + + assert_eq!( + rs_vec.iter().map(|o| o.xpc_value().unwrap()).collect::>(), + vec!["eins".to_string(), "zwei".to_string(), "polizei".to_string()] + ); + } } From ee122eb8c62637417bf9ed3e68cd31a492524010 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Fri, 7 May 2021 08:55:33 -0400 Subject: [PATCH 12/22] finally wire up enable/disable, but getting errno 22 more readme --- README.md | 32 +++++++- launchk/src/launchd/query.rs | 24 +++++- launchk/src/tui/dialog.rs | 108 ++++++++++++++------------- launchk/src/tui/omnibox/command.rs | 15 ++-- launchk/src/tui/root.rs | 4 +- launchk/src/tui/service_list/view.rs | 18 ++++- 6 files changed, 135 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index c205e74..9821df2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,10 @@ Should work on macOS 10.10+ according to the availability sec. [in the docs](htt #### Features - Poll XPC for jobs and display changes as they happen -- Filter by system (/System/Library/), "global" (/Library), user (~/) `LaunchAgents` and `LaunchDaemons` +- Filter by `LaunchAgents` and `LaunchDaemons` in scopes: + - System (/System/Library/) + - Global (/Library) + - User (~/) - fsnotify detection for new plists added to above directories - `:load/:unload` -> `launchctl load/unload` - `:edit` -> Open plist in `$EDITOR`, defaulting to `vim`. Supports binary plists -> shown as XML for edit, then marshalled back into binary format on save. @@ -162,11 +165,34 @@ An XPC array can be made from either `Vec` or `Vec>`: ```rust let xpc_array = XPCObject::from(vec![XPCObject::from("eins"), XPCObject::from("zwei"), XPCObject::from("polizei")]); -let xpc_array = XPCObject::from(vec!["eins", "zwei", "polizei"]) +let xpc_array = XPCObject::from(vec!["eins", "zwei", "polizei"]); ``` Go back to `Vec` using `xpc_value`: ```rust let rs_vec: Vec = xpc_array.xpc_value().unwrap(); -``` \ No newline at end of file +``` + +### Credits + +A big thanks to these open source projects and general resources: + +- [block](https://crates.io/crates/block) Obj-C block support, necessary for any XPC function taking `xpc_*_applier_t` +- [Cursive](https://github.com/gyscos/cursive) TUI +- [tokio](https://github.com/tokio-rs/tokio) ASIO +- [plist](https://crates.io/crates/plist) Parsing & validation for XML and binary plists +- [notify](https://docs.rs/notify/4.0.16/notify/) fsnotify +- [bitflags](https://docs.rs/bitflags/1.2.1/bitflags/) + +- [Apple Developer XPC services](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html) +- [Apple Developer XPC API reference](https://developer.apple.com/documentation/xpc?language=objc) +- [MOXIL / launjctl](http://newosxbook.com/articles/jlaunchctl.html) +- [geosnow - A Long Evening With macOS' sandbox](https://geosn0w.github.io/A-Long-Evening-With-macOS%27s-Sandbox/) +- [Bits of launchd - @5aelo](https://saelo.github.io/presentations/bits_of_launchd.pdf) +- [Audit tokens explained (e.g. ASID)](https://knight.sc/reverse%20engineering/2020/03/20/audit-tokens-explained.html) +- [objc.io XPC guide](https://www.objc.io/issues/14-mac/xpc/) +- The various source links found in comments, from Chrome's sandbox and other headers with definitions for private API functions. +- Last but not least, this is Apple's launchd after all, right :>)? I did not know systemd was inspired by launchd until I read [this HN comment](https://news.ycombinator.com/item?id=2565780), which sent me down this eventual rabbit hole :) + +Everything else (C) David Stancu & Contributors 2021 \ No newline at end of file diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index 8940b4b..97584f4 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -1,4 +1,4 @@ -use crate::launchd::message::{LIST_SERVICES, LOAD_PATHS, UNLOAD_PATHS}; +use crate::launchd::message::{LIST_SERVICES, LOAD_PATHS, UNLOAD_PATHS, ENABLE_NAMES, DISABLE_NAMES}; use std::collections::HashSet; use xpc_sys::objects::xpc_object::XPCObject; @@ -97,3 +97,25 @@ pub fn unload>( .entry("paths", vec![plist_path.into()]) .pipe_routine_with_error_handling() } + +pub fn enable>(label: S, domain_type: DomainType) -> Result { + let label_string = label.into(); + + XPCDictionary::new() + .extend(&ENABLE_NAMES) + .with_domain_type_or_default(Some(domain_type)) + .entry("name", label_string.clone()) + .entry("names", vec![label_string]) + .pipe_routine_with_error_handling() +} + +pub fn disable>(label: S, domain_type: DomainType) -> Result { + let label_string = label.into(); + + XPCDictionary::new() + .extend(&DISABLE_NAMES) + .with_domain_type_or_default(Some(domain_type)) + .entry("name", label_string.clone()) + .entry("names", vec![label_string]) + .pipe_routine_with_error_handling() +} \ No newline at end of file diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index 90ec267..e283a79 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -56,67 +56,75 @@ pub fn show_prompt( } pub fn domain_session_prompt( + domain_only: bool, tx: Sender, - f: fn(DomainType, SessionType) -> Vec, + f: fn(DomainType, Option) -> Vec, ) -> CbSinkMessage { let cl = move |siv: &mut Cursive| { let mut domain_group: RadioGroup = RadioGroup::new(); let mut st_group: RadioGroup = RadioGroup::new(); - let ask = Dialog::new() - .title("Please choose") - .content( - LinearLayout::horizontal() - .child( - LinearLayout::vertical() - .child(TextView::new("Domain Type").effect(Effect::Bold)) - .child(DummyView) - .child(domain_group.button(DomainType::System, "1: System")) - .child(domain_group.button(DomainType::User, "2: User")) - .child(domain_group.button(DomainType::UserLogin, "3: UserLogin")) - .child(domain_group.button(DomainType::Session, "4: Session")) - // TODO: Ask for handle - .child(domain_group.button(DomainType::PID, "5: PID").disabled()) - .child(domain_group.button( - DomainType::RequestorUserDomain, - "6: Requester User Domain", - )) - // TODO: Is this a sane default? - .child( - domain_group - .button(DomainType::RequestorDomain, "7: Requester Domain") - .selected(), - ), - ) + let mut layout = LinearLayout::horizontal() + .child( + LinearLayout::vertical() + .child(TextView::new("Domain Type").effect(Effect::Bold)) .child(DummyView) + .child(domain_group.button(DomainType::System, "1: System")) + .child(domain_group.button(DomainType::User, "2: User")) + .child(domain_group.button(DomainType::UserLogin, "3: UserLogin")) + .child(domain_group.button(DomainType::Session, "4: Session")) + // TODO: Ask for handle + .child(domain_group.button(DomainType::PID, "5: PID").disabled()) + .child(domain_group.button( + DomainType::RequestorUserDomain, + "6: Requester User Domain", + )) + // TODO: Is this a sane default? .child( - LinearLayout::vertical() - .child(TextView::new("Session Type").effect(Effect::Bold)) - .child(DummyView) - .child( - st_group.button(SessionType::Aqua, SessionType::Aqua.to_string()), - ) - .child(st_group.button( - SessionType::StandardIO, - SessionType::StandardIO.to_string(), - )) - .child(st_group.button( - SessionType::Background, - SessionType::Background.to_string(), - )) - .child(st_group.button( - SessionType::LoginWindow, - SessionType::LoginWindow.to_string(), - )) - .child( - st_group - .button(SessionType::System, SessionType::System.to_string()), - ), + domain_group + .button(DomainType::RequestorDomain, "7: Requester Domain") + .selected(), ), - ) + ); + + if !domain_only { + layout = layout.child(DummyView) + .child( + LinearLayout::vertical() + .child(TextView::new("Session Type").effect(Effect::Bold)) + .child(DummyView) + .child( + st_group.button(SessionType::Aqua, SessionType::Aqua.to_string()), + ) + .child(st_group.button( + SessionType::StandardIO, + SessionType::StandardIO.to_string(), + )) + .child(st_group.button( + SessionType::Background, + SessionType::Background.to_string(), + )) + .child(st_group.button( + SessionType::LoginWindow, + SessionType::LoginWindow.to_string(), + )) + .child( + st_group + .button(SessionType::System, SessionType::System.to_string()), + ), + ); + } + + let mut ask = Dialog::new() + .title("Please choose") + .content(layout) .button("OK", move |s| { let dt = domain_group.selection().as_ref().clone(); - let st = st_group.selection().as_ref().clone(); + let st = if domain_only { + None + } else { + Some(st_group.selection().as_ref().clone()) + }; f(dt, st) .iter() diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index f8e30f3..094be54 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -8,12 +8,13 @@ pub enum OmniboxCommand { Unload(DomainType, Option), // Reuses domain, handle, limit load to session type from existing Reload, - Enable, - Disable, + Enable(DomainType), + Disable(DomainType), Edit, // (message, on ok) Confirm(String, Vec), - DomainSessionPrompt(fn(DomainType, SessionType) -> Vec), + // (prompt for domain only?, action gen fn) + DomainSessionPrompt(bool, fn(DomainType, Option) -> Vec), FocusServiceList, Quit, } @@ -28,22 +29,22 @@ pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ ( "load", "▶️ Load highlighted job", - OmniboxCommand::DomainSessionPrompt(|dt, st| vec![OmniboxCommand::Load(st, dt, None)]), + OmniboxCommand::DomainSessionPrompt(false, |dt, st| vec![OmniboxCommand::Load(st.expect("Must be provided"), dt, None)]), ), ( "unload", "⏏️ Unload highlighted job", - OmniboxCommand::DomainSessionPrompt(|dt, _| vec![OmniboxCommand::Unload(dt, None)]), + OmniboxCommand::DomainSessionPrompt(false, |dt, _| vec![OmniboxCommand::Unload(dt, None)]), ), ( "enable", "▶️ Enable highlighted job (enables load)", - OmniboxCommand::Enable, + OmniboxCommand::DomainSessionPrompt(true, |dt, _| vec![OmniboxCommand::Enable(dt)]), ), ( "disable", "⏏️ Disable highlighted job (prevents load)", - OmniboxCommand::Disable, + OmniboxCommand::DomainSessionPrompt(true, |dt, _| vec![OmniboxCommand::Disable(dt)]), ), ( "edit", diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index f604310..d57b3da 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -255,9 +255,9 @@ impl OmniboxSubscriber for RootLayout { .expect("Must show prompt"); Ok(None) } - OmniboxEvent::Command(OmniboxCommand::DomainSessionPrompt(f)) => { + OmniboxEvent::Command(OmniboxCommand::DomainSessionPrompt(domain_only, f)) => { self.cbsink_channel - .send(dialog::domain_session_prompt(self.omnibox_tx.clone(), f)) + .send(dialog::domain_session_prompt(domain_only, self.omnibox_tx.clone(), f)) .expect("Must show prompt"); Ok(None) } diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index fbd7675..2568c0e 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -15,7 +15,7 @@ use tokio::time::interval; use crate::launchd::job_type_filter::JobTypeFilter; use crate::launchd::plist::{edit_and_replace, LABEL_TO_ENTRY_CONFIG}; -use crate::launchd::query::{list_all, load, unload}; +use crate::launchd::query::{list_all, load, unload, enable, disable}; use crate::launchd::{ entry_status::get_entry_status, entry_status::LaunchdEntryStatus, plist::LaunchdPlist, }; @@ -223,12 +223,24 @@ impl ServiceListView { .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) } - OmniboxCommand::Reload => Ok(Some(OmniboxCommand::DomainSessionPrompt(|dt, st| { + OmniboxCommand::Reload => Ok(Some(OmniboxCommand::DomainSessionPrompt(false, |dt, st| { vec![ OmniboxCommand::Unload(dt.clone(), None), - OmniboxCommand::Load(st, dt, None), + OmniboxCommand::Load(st.expect("Must provide"), dt, None), ] }))), + OmniboxCommand::Enable(dt) => { + let (ServiceListItem { name, .. }, _) = self.with_active_item_plist()?; + enable(name, dt) + .map(|_| None) + .map_err(|e| OmniboxError::CommandError(e.to_string())) + }, + OmniboxCommand::Disable(dt) => { + let (ServiceListItem { name, .. }, _) = self.with_active_item_plist()?; + disable(name, dt) + .map(|_| None) + .map_err(|e| OmniboxError::CommandError(e.to_string())) + } _ => Ok(None), } } From e431e015cbeb0d9b68dbb50670b8adc119a84a96 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Fri, 7 May 2021 17:40:42 -0400 Subject: [PATCH 13/22] xpcdictionary -> xpcobject for easier logging, add missing handle param --- README.md | 2 +- launchk/src/launchd/mod.rs | 6 ++++-- launchk/src/launchd/query.rs | 2 ++ xpc-sys/src/objects/xpc_object.rs | 9 +++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9821df2..f8115ea 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Should work on macOS 10.10+ according to the availability sec. [in the docs](htt ### xpc-sys crate -There is some "convenience glue" for dealing with XPC objects. Eventually, this will be broken out into its own crate. Most of the tests (for now) are written around not breaking data going across the FFI barrier. +There is some "convenience glue" for dealing with XPC objects. Eventually, this will be broken out into its own crate. Some tests exist for not breaking data to/from FFI. ##### Object lifecycle diff --git a/launchk/src/launchd/mod.rs b/launchk/src/launchd/mod.rs index afa2ed0..9a3d91f 100644 --- a/launchk/src/launchd/mod.rs +++ b/launchk/src/launchd/mod.rs @@ -2,10 +2,12 @@ pub mod message; /// queries (sorta?) pub mod query; - +pub mod query_builder; pub mod entry_status; + pub mod enums; pub mod job_type_filter; + /// plist management pub mod plist; -pub mod query_builder; + diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index 97584f4..fb184cb 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -106,6 +106,7 @@ pub fn enable>(label: S, domain_type: DomainType) -> Result>(label: S, domain_type: DomainType) -> Result, pub XPCType); @@ -121,6 +122,14 @@ impl From for XPCObject { } } +impl From for XPCObject { + /// Use From, XPCObject>> + fn from(xpcd: XPCDictionary) -> Self { + let XPCDictionary(hm) = xpcd; + hm.into() + } +} + /// Cloning an XPC object will clone the underlying Arc -- we will /// call xpc_release() only if we are the last valid reference /// (and underlying data is not null) From ba5dfa967de9b0d0ffdecaaafaa955da3d63bd5b Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sat, 8 May 2021 12:34:59 -0400 Subject: [PATCH 14/22] refocus omnibox on backspace --- launchk/src/tui/root.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index d57b3da..5198506 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -197,7 +197,8 @@ impl ViewWrapper for RootLayout { | Event::Char('u') | Event::Char('a') | Event::Char('d') - | Event::Char('l') => self.focus_and_forward(RootLayoutChildren::Omnibox, event), + | Event::Char('l') + | Event::Key(Key::Backspace) => self.focus_and_forward(RootLayoutChildren::Omnibox, event), // TODO: wtf? // After exiting $EDITOR, for some reason we get a termcap issue. iTerm and Apple Terminal // exhibit the same behavior. This was the easiest way to solve the problem for now. From 5c5addefcc8817a051bf1b885dd1bfa47faac0fd Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sat, 8 May 2021 12:44:09 -0400 Subject: [PATCH 15/22] fmt --- launchk/src/launchd/message.rs | 2 +- launchk/src/launchd/mod.rs | 3 +- launchk/src/launchd/query.rs | 18 ++++-- launchk/src/launchd/query_builder.rs | 26 +++++--- launchk/src/tui/dialog.rs | 85 ++++++++++++--------------- launchk/src/tui/omnibox/command.rs | 13 +++- launchk/src/tui/root.rs | 10 +++- launchk/src/tui/service_list/view.rs | 19 +++--- xpc-sys/src/objects/xpc_dictionary.rs | 2 +- xpc-sys/src/objects/xpc_object.rs | 2 +- xpc-sys/src/traits/xpc_value.rs | 11 +++- 11 files changed, 113 insertions(+), 78 deletions(-) diff --git a/launchk/src/launchd/message.rs b/launchk/src/launchd/message.rs index 8ff9747..0270728 100644 --- a/launchk/src/launchd/message.rs +++ b/launchk/src/launchd/message.rs @@ -1,5 +1,5 @@ -use xpc_sys::objects::xpc_dictionary::XPCDictionary; use crate::launchd::query_builder::QueryBuilder; +use xpc_sys::objects::xpc_dictionary::XPCDictionary; lazy_static! { /// launchctl list [name] diff --git a/launchk/src/launchd/mod.rs b/launchk/src/launchd/mod.rs index 9a3d91f..e57dcd1 100644 --- a/launchk/src/launchd/mod.rs +++ b/launchk/src/launchd/mod.rs @@ -1,13 +1,12 @@ pub mod message; +pub mod entry_status; /// queries (sorta?) pub mod query; pub mod query_builder; -pub mod entry_status; pub mod enums; pub mod job_type_filter; /// plist management pub mod plist; - diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index fb184cb..6ef15d6 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -1,8 +1,8 @@ -use crate::launchd::message::{LIST_SERVICES, LOAD_PATHS, UNLOAD_PATHS, ENABLE_NAMES, DISABLE_NAMES}; +use crate::launchd::message::{ + DISABLE_NAMES, ENABLE_NAMES, LIST_SERVICES, LOAD_PATHS, UNLOAD_PATHS, +}; use std::collections::HashSet; -use xpc_sys::objects::xpc_object::XPCObject; - use xpc_sys::traits::xpc_pipeable::XPCPipeable; use crate::launchd::entry_status::ENTRY_STATUS_CACHE; @@ -98,7 +98,10 @@ pub fn unload>( .pipe_routine_with_error_handling() } -pub fn enable>(label: S, domain_type: DomainType) -> Result { +pub fn enable>( + label: S, + domain_type: DomainType, +) -> Result { let label_string = label.into(); XPCDictionary::new() @@ -110,7 +113,10 @@ pub fn enable>(label: S, domain_type: DomainType) -> Result>(label: S, domain_type: DomainType) -> Result { +pub fn disable>( + label: S, + domain_type: DomainType, +) -> Result { let label_string = label.into(); XPCDictionary::new() @@ -120,4 +126,4 @@ pub fn disable>(label: S, domain_type: DomainType) -> Result, O: Into>(self, key: S, value: O) -> XPCDictionary; @@ -13,19 +13,31 @@ pub trait QueryBuilder { fn extend(self, other: &XPCDictionary) -> XPCDictionary; - fn with_domain_port(self) -> XPCDictionary where Self: Sized { + fn with_domain_port(self) -> XPCDictionary + where + Self: Sized, + { self.entry("domain-port", get_bootstrap_port() as mach_port_t) } - fn with_session_type_or_default(self, session: Option) -> XPCDictionary where Self: Sized { + fn with_session_type_or_default(self, session: Option) -> XPCDictionary + where + Self: Sized, + { self.entry("session", session.unwrap_or(SessionType::Aqua).to_string()) } - fn with_handle_or_default(self, session: Option) -> XPCDictionary where Self: Sized { + fn with_handle_or_default(self, session: Option) -> XPCDictionary + where + Self: Sized, + { self.entry("handle", session.unwrap_or(0)) } - fn with_domain_type_or_default(self, t: Option) -> XPCDictionary where Self: Sized { + fn with_domain_type_or_default(self, t: Option) -> XPCDictionary + where + Self: Sized, + { self.entry("type", t.unwrap_or(DomainType::RequestorDomain) as u64) } } @@ -55,4 +67,4 @@ impl QueryBuilder for XPCDictionary { self_hm.extend(other_hm.iter().map(|(s, o)| (s.clone(), o.clone()))); self } -} \ No newline at end of file +} diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index e283a79..8225929 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -64,58 +64,51 @@ pub fn domain_session_prompt( let mut domain_group: RadioGroup = RadioGroup::new(); let mut st_group: RadioGroup = RadioGroup::new(); - let mut layout = LinearLayout::horizontal() - .child( + let mut layout = LinearLayout::horizontal().child( + LinearLayout::vertical() + .child(TextView::new("Domain Type").effect(Effect::Bold)) + .child(DummyView) + .child(domain_group.button(DomainType::System, "1: System")) + .child(domain_group.button(DomainType::User, "2: User")) + .child(domain_group.button(DomainType::UserLogin, "3: UserLogin")) + .child(domain_group.button(DomainType::Session, "4: Session")) + // TODO: Ask for handle + .child(domain_group.button(DomainType::PID, "5: PID").disabled()) + .child( + domain_group + .button(DomainType::RequestorUserDomain, "6: Requester User Domain"), + ) + // TODO: Is this a sane default? + .child( + domain_group + .button(DomainType::RequestorDomain, "7: Requester Domain") + .selected(), + ), + ); + + if !domain_only { + layout = layout.child(DummyView).child( LinearLayout::vertical() - .child(TextView::new("Domain Type").effect(Effect::Bold)) + .child(TextView::new("Session Type").effect(Effect::Bold)) .child(DummyView) - .child(domain_group.button(DomainType::System, "1: System")) - .child(domain_group.button(DomainType::User, "2: User")) - .child(domain_group.button(DomainType::UserLogin, "3: UserLogin")) - .child(domain_group.button(DomainType::Session, "4: Session")) - // TODO: Ask for handle - .child(domain_group.button(DomainType::PID, "5: PID").disabled()) - .child(domain_group.button( - DomainType::RequestorUserDomain, - "6: Requester User Domain", - )) - // TODO: Is this a sane default? + .child(st_group.button(SessionType::Aqua, SessionType::Aqua.to_string())) .child( - domain_group - .button(DomainType::RequestorDomain, "7: Requester Domain") - .selected(), - ), + st_group + .button(SessionType::StandardIO, SessionType::StandardIO.to_string()), + ) + .child( + st_group + .button(SessionType::Background, SessionType::Background.to_string()), + ) + .child(st_group.button( + SessionType::LoginWindow, + SessionType::LoginWindow.to_string(), + )) + .child(st_group.button(SessionType::System, SessionType::System.to_string())), ); - - if !domain_only { - layout = layout.child(DummyView) - .child( - LinearLayout::vertical() - .child(TextView::new("Session Type").effect(Effect::Bold)) - .child(DummyView) - .child( - st_group.button(SessionType::Aqua, SessionType::Aqua.to_string()), - ) - .child(st_group.button( - SessionType::StandardIO, - SessionType::StandardIO.to_string(), - )) - .child(st_group.button( - SessionType::Background, - SessionType::Background.to_string(), - )) - .child(st_group.button( - SessionType::LoginWindow, - SessionType::LoginWindow.to_string(), - )) - .child( - st_group - .button(SessionType::System, SessionType::System.to_string()), - ), - ); } - let mut ask = Dialog::new() + let ask = Dialog::new() .title("Please choose") .content(layout) .button("OK", move |s| { diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index 094be54..62b0d66 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -14,7 +14,10 @@ pub enum OmniboxCommand { // (message, on ok) Confirm(String, Vec), // (prompt for domain only?, action gen fn) - DomainSessionPrompt(bool, fn(DomainType, Option) -> Vec), + DomainSessionPrompt( + bool, + fn(DomainType, Option) -> Vec, + ), FocusServiceList, Quit, } @@ -29,7 +32,13 @@ pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ ( "load", "▶️ Load highlighted job", - OmniboxCommand::DomainSessionPrompt(false, |dt, st| vec![OmniboxCommand::Load(st.expect("Must be provided"), dt, None)]), + OmniboxCommand::DomainSessionPrompt(false, |dt, st| { + vec![OmniboxCommand::Load( + st.expect("Must be provided"), + dt, + None, + )] + }), ), ( "unload", diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index 5198506..dbc077c 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -198,7 +198,9 @@ impl ViewWrapper for RootLayout { | Event::Char('a') | Event::Char('d') | Event::Char('l') - | Event::Key(Key::Backspace) => self.focus_and_forward(RootLayoutChildren::Omnibox, event), + | Event::Key(Key::Backspace) => { + self.focus_and_forward(RootLayoutChildren::Omnibox, event) + } // TODO: wtf? // After exiting $EDITOR, for some reason we get a termcap issue. iTerm and Apple Terminal // exhibit the same behavior. This was the easiest way to solve the problem for now. @@ -258,7 +260,11 @@ impl OmniboxSubscriber for RootLayout { } OmniboxEvent::Command(OmniboxCommand::DomainSessionPrompt(domain_only, f)) => { self.cbsink_channel - .send(dialog::domain_session_prompt(domain_only, self.omnibox_tx.clone(), f)) + .send(dialog::domain_session_prompt( + domain_only, + self.omnibox_tx.clone(), + f, + )) .expect("Must show prompt"); Ok(None) } diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index 2568c0e..2dac6cd 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -15,7 +15,7 @@ use tokio::time::interval; use crate::launchd::job_type_filter::JobTypeFilter; use crate::launchd::plist::{edit_and_replace, LABEL_TO_ENTRY_CONFIG}; -use crate::launchd::query::{list_all, load, unload, enable, disable}; +use crate::launchd::query::{disable, enable, list_all, load, unload}; use crate::launchd::{ entry_status::get_entry_status, entry_status::LaunchdEntryStatus, plist::LaunchdPlist, }; @@ -223,18 +223,21 @@ impl ServiceListView { .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) } - OmniboxCommand::Reload => Ok(Some(OmniboxCommand::DomainSessionPrompt(false, |dt, st| { - vec![ - OmniboxCommand::Unload(dt.clone(), None), - OmniboxCommand::Load(st.expect("Must provide"), dt, None), - ] - }))), + OmniboxCommand::Reload => Ok(Some(OmniboxCommand::DomainSessionPrompt( + false, + |dt, st| { + vec![ + OmniboxCommand::Unload(dt.clone(), None), + OmniboxCommand::Load(st.expect("Must provide"), dt, None), + ] + }, + ))), OmniboxCommand::Enable(dt) => { let (ServiceListItem { name, .. }, _) = self.with_active_item_plist()?; enable(name, dt) .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) - }, + } OmniboxCommand::Disable(dt) => { let (ServiceListItem { name, .. }, _) = self.with_active_item_plist()?; disable(name, dt) diff --git a/xpc-sys/src/objects/xpc_dictionary.rs b/xpc-sys/src/objects/xpc_dictionary.rs index 2e2f122..da90531 100644 --- a/xpc-sys/src/objects/xpc_dictionary.rs +++ b/xpc-sys/src/objects/xpc_dictionary.rs @@ -9,7 +9,7 @@ use std::rc::Rc; use crate::objects::xpc_error::XPCError; use crate::objects::xpc_error::XPCError::DictionaryError; use crate::objects::xpc_object::XPCObject; -use crate::{get_bootstrap_port, mach_port_t, objects, xpc_retain}; +use crate::{objects, xpc_retain}; use crate::{xpc_dictionary_apply, xpc_dictionary_create, xpc_dictionary_set_value, xpc_object_t}; use block::ConcreteBlock; diff --git a/xpc-sys/src/objects/xpc_object.rs b/xpc-sys/src/objects/xpc_object.rs index f34f3cc..18afed6 100644 --- a/xpc-sys/src/objects/xpc_object.rs +++ b/xpc-sys/src/objects/xpc_object.rs @@ -8,8 +8,8 @@ use std::ffi::{CStr, CString}; use std::ptr::null_mut; use std::sync::Arc; -use std::fmt; use crate::objects::xpc_dictionary::XPCDictionary; +use std::fmt; #[derive(Debug, Clone, PartialEq, Eq)] pub struct XPCObject(pub Arc, pub XPCType); diff --git a/xpc-sys/src/traits/xpc_value.rs b/xpc-sys/src/traits/xpc_value.rs index 5b66865..5d60d99 100644 --- a/xpc-sys/src/traits/xpc_value.rs +++ b/xpc-sys/src/traits/xpc_value.rs @@ -137,8 +137,15 @@ mod tests { let rs_vec: Vec = xpc_array.xpc_value().unwrap(); assert_eq!( - rs_vec.iter().map(|o| o.xpc_value().unwrap()).collect::>(), - vec!["eins".to_string(), "zwei".to_string(), "polizei".to_string()] + rs_vec + .iter() + .map(|o| o.xpc_value().unwrap()) + .collect::>(), + vec![ + "eins".to_string(), + "zwei".to_string(), + "polizei".to_string() + ] ); } } From 72f1e13a3b666666ab1901ac3f54d43192d4cb07 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sat, 8 May 2021 12:49:49 -0400 Subject: [PATCH 16/22] include domain in entry status --- launchk/src/launchd/entry_status.rs | 14 +++++++++++--- launchk/src/launchd/query.rs | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/launchk/src/launchd/entry_status.rs b/launchk/src/launchd/entry_status.rs index 1084d24..3d6b67f 100644 --- a/launchk/src/launchd/entry_status.rs +++ b/launchk/src/launchd/entry_status.rs @@ -3,7 +3,7 @@ use std::convert::TryInto; use std::sync::Mutex; use std::time::{Duration, SystemTime}; -use crate::launchd::enums::SessionType; +use crate::launchd::enums::{SessionType, DomainType}; use crate::launchd::plist::LaunchdPlist; use crate::launchd::query::find_in_all; use xpc_sys::traits::xpc_value::TryXPCValue; @@ -19,6 +19,7 @@ lazy_static! { pub struct LaunchdEntryStatus { pub plist: Option, pub limit_load_to_session_type: SessionType, + pub domain: DomainType, // So, there is a pid_t, but it's i32, and the XPC response has an i64? pub pid: i64, tick: SystemTime, @@ -28,6 +29,7 @@ impl Default for LaunchdEntryStatus { fn default() -> Self { LaunchdEntryStatus { limit_load_to_session_type: SessionType::Unknown, + domain: DomainType::Unknown, plist: None, pid: 0, tick: SystemTime::now(), @@ -64,21 +66,27 @@ fn build_entry_status>(label: S) -> LaunchdEntryStatus { let pid: i64 = response .as_ref() .map_err(|e| e.clone()) - .and_then(|r| r.get(&["service", "PID"])) + .and_then(|(_, r)| r.get(&["service", "PID"])) .and_then(|o| o.xpc_value()) .unwrap_or(0); let limit_load_to_session_type = response .as_ref() .map_err(|e| e.clone()) - .and_then(|r| r.get(&["service", "LimitLoadToSessionType"])) + .and_then(|(_, r)| r.get(&["service", "LimitLoadToSessionType"])) .and_then(|o| o.try_into()) .unwrap_or(SessionType::Unknown); + let domain = response + .as_ref() + .map(|(d, _)| d.clone()) + .unwrap_or(DomainType::Unknown); + let entry_config = crate::launchd::plist::for_label(label_string.clone()); LaunchdEntryStatus { limit_load_to_session_type, + domain, plist: entry_config, pid, tick: SystemTime::now(), diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index 6ef15d6..bd7a3fd 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -14,7 +14,7 @@ use crate::launchd::enums::{DomainType, SessionType}; use crate::launchd::query_builder::QueryBuilder; // TODO: reuse list_all() -pub fn find_in_all>(label: S) -> Result { +pub fn find_in_all>(label: S) -> Result<(DomainType, XPCDictionary), XPCError> { let label_string = label.into(); for domain_type in DomainType::System as u64..DomainType::RequestorDomain as u64 { @@ -25,7 +25,7 @@ pub fn find_in_all>(label: S) -> Result .pipe_routine_with_error_handling(); if response.is_ok() { - return response; + return response.map(|r| (domain_type.into(), r)); } } From 261f59bb17bc486b1c4d11884568fa7d2590cc18 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sun, 9 May 2021 06:57:24 -0400 Subject: [PATCH 17/22] wip/preselect domain info if available --- launchk/src/launchd/query.rs | 1 - launchk/src/tui/dialog.rs | 80 ++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index bd7a3fd..03b02fc 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -13,7 +13,6 @@ use xpc_sys::objects::xpc_error::XPCError; use crate::launchd::enums::{DomainType, SessionType}; use crate::launchd::query_builder::QueryBuilder; -// TODO: reuse list_all() pub fn find_in_all>(label: S) -> Result<(DomainType, XPCDictionary), XPCError> { let label_string = label.into(); diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index 8225929..d05018b 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -13,6 +13,7 @@ use crate::{ launchd::enums::{DomainType, SessionType}, tui::omnibox::command::OmniboxCommand, }; +use crate::launchd::entry_status::{get_entry_status, LaunchdEntryStatus}; /// The XPC error key sometimes contains information that is not necessarily a failure, /// so let's just call it "Notice" until we figure out what to do next? @@ -55,7 +56,8 @@ pub fn show_prompt( Box::new(cl) } -pub fn domain_session_prompt( +pub fn domain_session_prompt>( + label: S, domain_only: bool, tx: Sender, f: fn(DomainType, Option) -> Vec, @@ -64,50 +66,46 @@ pub fn domain_session_prompt( let mut domain_group: RadioGroup = RadioGroup::new(); let mut st_group: RadioGroup = RadioGroup::new(); - let mut layout = LinearLayout::horizontal().child( - LinearLayout::vertical() - .child(TextView::new("Domain Type").effect(Effect::Bold)) - .child(DummyView) - .child(domain_group.button(DomainType::System, "1: System")) - .child(domain_group.button(DomainType::User, "2: User")) - .child(domain_group.button(DomainType::UserLogin, "3: UserLogin")) - .child(domain_group.button(DomainType::Session, "4: Session")) - // TODO: Ask for handle - .child(domain_group.button(DomainType::PID, "5: PID").disabled()) - .child( - domain_group - .button(DomainType::RequestorUserDomain, "6: Requester User Domain"), - ) - // TODO: Is this a sane default? - .child( - domain_group - .button(DomainType::RequestorDomain, "7: Requester Domain") - .selected(), - ), - ); + // Build domain type list + let mut domain_type_layout = LinearLayout::vertical() + .child(TextView::new("Domain Type").effect(Effect::Bold)) + .child(DummyView); + + let LaunchdEntryStatus { + limit_load_to_session_type, + domain, + .. + } = get_entry_status(&label); + + for d in DomainType::System..DomainType::RequestorDomain { + let mut button = domain_group.button(d, format!("{}: {}", &d as u64, &d)); + if d == domain { + button = button.selected(); + } + + domain_type_layout = domain_type_layout.child(button); + } + + let mut session_type_layout = LinearLayout::vertical(); if !domain_only { - layout = layout.child(DummyView).child( - LinearLayout::vertical() - .child(TextView::new("Session Type").effect(Effect::Bold)) - .child(DummyView) - .child(st_group.button(SessionType::Aqua, SessionType::Aqua.to_string())) - .child( - st_group - .button(SessionType::StandardIO, SessionType::StandardIO.to_string()), - ) - .child( - st_group - .button(SessionType::Background, SessionType::Background.to_string()), - ) - .child(st_group.button( - SessionType::LoginWindow, - SessionType::LoginWindow.to_string(), - )) - .child(st_group.button(SessionType::System, SessionType::System.to_string())), - ); + session_type_layout = session_type_layout + .child(TextView::new("Session Type").effect(Effect::Bold)) + .child(DummyView); + + for s in SessionType::Aqua..SessionType::Unknown { + let mut button = st_group.button(s, s.to_string()); + if s == limit_load_to_session_type { + button = button.selected(); + } + session_type_layout = session_type_layout.child(button); + } } + let mut layout = LinearLayout::horizontal() + .child(domain_type_layout) + .child(session_type_layout); + let ask = Dialog::new() .title("Please choose") .content(layout) From 38ece42e21115bd3a818924fc570af69c0743761 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sun, 9 May 2021 10:39:29 -0400 Subject: [PATCH 18/22] [2/?] airport break: fix some typos -- how do we get the label here?! --- launchk/src/launchd/enums.rs | 15 ++++++++++++++- launchk/src/tui/dialog.rs | 27 +++++++++++++++------------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/launchk/src/launchd/enums.rs b/launchk/src/launchd/enums.rs index db949ae..d4e1ae2 100644 --- a/launchk/src/launchd/enums.rs +++ b/launchk/src/launchd/enums.rs @@ -10,7 +10,7 @@ use xpc_sys::traits::xpc_value::TryXPCValue; /// https://developer.apple.com/library/archive/technotes/tn2083/_index.html #[derive(Debug, Clone, Eq, PartialEq)] pub enum SessionType { - Aqua, + Aqua = 0, StandardIO, Background, LoginWindow, @@ -24,6 +24,19 @@ impl fmt::Display for SessionType { } } +impl From for SessionType { + fn from(session_type: u64) -> Self { + match session_type { + 0 => SessionType::Aqua, + 1 => SessionType::StandardIO, + 2 => SessionType::Background, + 3 => SessionType::LoginWindow, + 4 => SessionType::System, + _ => SessionType::Unknown, + } + } +} + // TODO: This feels terrible impl From for SessionType { fn from(value: String) -> Self { diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index d05018b..3f3e63c 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -62,6 +62,12 @@ pub fn domain_session_prompt>( tx: Sender, f: fn(DomainType, Option) -> Vec, ) -> CbSinkMessage { + let LaunchdEntryStatus { + limit_load_to_session_type, + domain, + .. + } = get_entry_status(label); + let cl = move |siv: &mut Cursive| { let mut domain_group: RadioGroup = RadioGroup::new(); let mut st_group: RadioGroup = RadioGroup::new(); @@ -71,15 +77,10 @@ pub fn domain_session_prompt>( .child(TextView::new("Domain Type").effect(Effect::Bold)) .child(DummyView); - let LaunchdEntryStatus { - limit_load_to_session_type, - domain, - .. - } = get_entry_status(&label); - - for d in DomainType::System..DomainType::RequestorDomain { - let mut button = domain_group.button(d, format!("{}: {}", &d as u64, &d)); - if d == domain { + for d in DomainType::System as u64..DomainType::RequestorDomain as u64 { + let as_domain: DomainType = d.into(); + let mut button = domain_group.button(as_domain, format!("{}: {}", d, as_domain)); + if as_domain == domain { button = button.selected(); } @@ -93,9 +94,11 @@ pub fn domain_session_prompt>( .child(TextView::new("Session Type").effect(Effect::Bold)) .child(DummyView); - for s in SessionType::Aqua..SessionType::Unknown { - let mut button = st_group.button(s, s.to_string()); - if s == limit_load_to_session_type { + for s in SessionType::Aqua as u64..SessionType::Unknown as u64 { + let as_session: SessionType = s.into(); + + let mut button = st_group.button(as_session, as_session.to_string()); + if as_session == limit_load_to_session_type { button = button.selected(); } session_type_layout = session_type_layout.child(button); From baa4c2b05d7ab6d70935801aa6f7643dbf6e1063 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sun, 9 May 2021 12:53:07 -0400 Subject: [PATCH 19/22] prompt user for domain/session only if not in entry status cache --- launchk/src/tui/dialog.rs | 8 +-- launchk/src/tui/omnibox/command.rs | 24 ++++---- launchk/src/tui/omnibox/view.rs | 2 +- launchk/src/tui/root.rs | 7 ++- launchk/src/tui/service_list/view.rs | 92 +++++++++++++++++++++++++--- 5 files changed, 104 insertions(+), 29 deletions(-) diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index 3f3e63c..c417298 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -77,9 +77,9 @@ pub fn domain_session_prompt>( .child(TextView::new("Domain Type").effect(Effect::Bold)) .child(DummyView); - for d in DomainType::System as u64..DomainType::RequestorDomain as u64 { + for d in DomainType::System as u64..DomainType::Unknown as u64 { let as_domain: DomainType = d.into(); - let mut button = domain_group.button(as_domain, format!("{}: {}", d, as_domain)); + let mut button = domain_group.button(as_domain.clone(), format!("{}: {}", d, &as_domain)); if as_domain == domain { button = button.selected(); } @@ -97,7 +97,7 @@ pub fn domain_session_prompt>( for s in SessionType::Aqua as u64..SessionType::Unknown as u64 { let as_session: SessionType = s.into(); - let mut button = st_group.button(as_session, as_session.to_string()); + let mut button = st_group.button(as_session.clone(), as_session.to_string()); if as_session == limit_load_to_session_type { button = button.selected(); } @@ -110,7 +110,7 @@ pub fn domain_session_prompt>( .child(session_type_layout); let ask = Dialog::new() - .title("Please choose") + .title("Please select to continue") .content(layout) .button("OK", move |s| { let dt = domain_group.selection().as_ref().clone(); diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index 62b0d66..9c30df8 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -4,6 +4,12 @@ use std::fmt; #[derive(Debug, Clone, Eq, PartialEq)] pub enum OmniboxCommand { Chain(Vec), + // Try to see if we have session type & domain in entry_status, + // to avoid having to prompt the user + LoadRequest, + UnloadRequest, + EnableRequest, + DisableRequest, Load(SessionType, DomainType, Option), Unload(DomainType, Option), // Reuses domain, handle, limit load to session type from existing @@ -13,8 +19,9 @@ pub enum OmniboxCommand { Edit, // (message, on ok) Confirm(String, Vec), - // (prompt for domain only?, action gen fn) + // (unit label, prompt for domain only?, action gen fn) DomainSessionPrompt( + String, bool, fn(DomainType, Option) -> Vec, ), @@ -32,28 +39,23 @@ pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ ( "load", "▶️ Load highlighted job", - OmniboxCommand::DomainSessionPrompt(false, |dt, st| { - vec![OmniboxCommand::Load( - st.expect("Must be provided"), - dt, - None, - )] - }), + OmniboxCommand::LoadRequest, ), ( "unload", "⏏️ Unload highlighted job", - OmniboxCommand::DomainSessionPrompt(false, |dt, _| vec![OmniboxCommand::Unload(dt, None)]), + OmniboxCommand::UnloadRequest, + // OmniboxCommand::DomainSessionPrompt(false, |dt, _| vec![OmniboxCommand::Unload(dt, None)]), ), ( "enable", "▶️ Enable highlighted job (enables load)", - OmniboxCommand::DomainSessionPrompt(true, |dt, _| vec![OmniboxCommand::Enable(dt)]), + OmniboxCommand::EnableRequest, ), ( "disable", "⏏️ Disable highlighted job (prevents load)", - OmniboxCommand::DomainSessionPrompt(true, |dt, _| vec![OmniboxCommand::Disable(dt)]), + OmniboxCommand::DisableRequest ), ( "edit", diff --git a/launchk/src/tui/omnibox/view.rs b/launchk/src/tui/omnibox/view.rs index e9a92c9..6ab0d52 100644 --- a/launchk/src/tui/omnibox/view.rs +++ b/launchk/src/tui/omnibox/view.rs @@ -41,7 +41,7 @@ pub enum OmniboxMode { /// Move OmniboxState back to idle some time after the user stops /// interacting with it async fn tick(state: Arc>, tx: Sender) { - let mut tick_rate = interval(Duration::from_millis(500)); + let mut tick_rate = interval(Duration::from_millis(250)); loop { tick_rate.tick().await; diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index dbc077c..a2e966c 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -7,7 +7,7 @@ use cursive::event::{Event, EventResult, Key}; use cursive::traits::{Resizable, Scrollable}; use cursive::view::ViewWrapper; use cursive::views::{LinearLayout, NamedView, Panel}; -use cursive::{Cursive, Vec2, View}; +use cursive::{Cursive, Vec2, View, Printer}; use tokio::runtime::Handle; use tokio::time::interval; @@ -183,7 +183,7 @@ impl RootLayout { impl ViewWrapper for RootLayout { wrap_impl!(self.layout: LinearLayout); - + fn wrap_on_event(&mut self, event: Event) -> EventResult { log::debug!("[root/event]: {:?}", event); @@ -258,9 +258,10 @@ impl OmniboxSubscriber for RootLayout { .expect("Must show prompt"); Ok(None) } - OmniboxEvent::Command(OmniboxCommand::DomainSessionPrompt(domain_only, f)) => { + OmniboxEvent::Command(OmniboxCommand::DomainSessionPrompt(label, domain_only, f)) => { self.cbsink_channel .send(dialog::domain_session_prompt( + label, domain_only, self.omnibox_tx.clone(), f, diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index 2dac6cd..a2ae7e8 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -26,6 +26,8 @@ use crate::tui::omnibox::view::{OmniboxError, OmniboxEvent, OmniboxMode}; use crate::tui::root::CbSinkMessage; use crate::tui::service_list::list_item::ServiceListItem; use crate::tui::table::table_list_view::TableListView; +use crate::launchd::enums::{SessionType, DomainType}; +use crate::tui::omnibox::command::OmniboxCommand::DomainSessionPrompt; /// Polls XPC for job list async fn poll_running_jobs(svcs: Arc>>, cb_sink: Sender) { @@ -186,6 +188,85 @@ impl ServiceListView { fn handle_command(&self, cmd: OmniboxCommand) -> OmniboxResult { match cmd { + OmniboxCommand::Reload => { + let (ServiceListItem { name, .. }, ..) = self.with_active_item_plist()?; + let LaunchdEntryStatus { + limit_load_to_session_type, + domain, + .. + } = get_entry_status(&name); + + match (limit_load_to_session_type, domain) { + (_, DomainType::Unknown) | (SessionType::Unknown, _) => { + Ok(Some(OmniboxCommand::DomainSessionPrompt( + name.clone(), + false, + |dt, st| { + vec![ + OmniboxCommand::Unload(dt.clone(), None), + OmniboxCommand::Load(st.expect("Must provide"), dt, None), + ] + }, + ))) + }, + (st, dt) => Ok(Some(OmniboxCommand::Chain(vec![ + OmniboxCommand::Unload(dt.clone(), None), + OmniboxCommand::Load(st, dt, None), + ]))) + } + }, + OmniboxCommand::LoadRequest => { + let (ServiceListItem { name, .. }, ..) = self.with_active_item_plist()?; + Ok(Some(OmniboxCommand::DomainSessionPrompt( + name.clone(), + false, + |dt, st| { + vec![OmniboxCommand::Load( + st.expect("Must be provided"), + dt, + None, + )] + } + ))) + }, + OmniboxCommand::UnloadRequest => { + let (ServiceListItem { name, .. }, ..) = self.with_active_item_plist()?; + let LaunchdEntryStatus { + domain, + .. + } = get_entry_status(&name); + + match domain { + DomainType::Unknown => Ok(Some(OmniboxCommand::DomainSessionPrompt(name.clone(), true, |dt, _| vec![OmniboxCommand::Unload(dt, None)]))), + _ => Ok(Some(OmniboxCommand::Unload(domain, None))) + } + }, + OmniboxCommand::EnableRequest => { + let (ServiceListItem { name, .. }, ..) = self.with_active_item_plist()?; + Ok(Some(OmniboxCommand::DomainSessionPrompt( + name.clone(), + true, + |dt, _| vec![OmniboxCommand::Enable(dt)] + ))) + } + OmniboxCommand::DisableRequest => { + let (ServiceListItem { name, .. }, ..) = self.with_active_item_plist()?; + let LaunchdEntryStatus { + domain, + .. + } = get_entry_status(&name); + + match domain { + DomainType::Unknown => Ok(Some(OmniboxCommand::DomainSessionPrompt( + name.clone(), + true, + |dt, _| vec![OmniboxCommand::Disable(dt)] + ))), + _ => Ok(Some(OmniboxCommand::Chain(vec![ + OmniboxCommand::Disable(domain) + ]))) + } + } OmniboxCommand::Edit => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; edit_and_replace(&plist).map_err(OmniboxError::CommandError)?; @@ -205,7 +286,7 @@ impl ServiceListView { load(name, plist.plist_path, Some(dt), Some(st), None) .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) - } + }, OmniboxCommand::Unload(dt, _handle) => { let (ServiceListItem { name, .. }, plist) = self.with_active_item_plist()?; let LaunchdEntryStatus { @@ -223,15 +304,6 @@ impl ServiceListView { .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) } - OmniboxCommand::Reload => Ok(Some(OmniboxCommand::DomainSessionPrompt( - false, - |dt, st| { - vec![ - OmniboxCommand::Unload(dt.clone(), None), - OmniboxCommand::Load(st.expect("Must provide"), dt, None), - ] - }, - ))), OmniboxCommand::Enable(dt) => { let (ServiceListItem { name, .. }, _) = self.with_active_item_plist()?; enable(name, dt) From d85f30129b40f086d013e886ee4c8fecc1d91711 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sat, 15 May 2021 22:06:23 -0400 Subject: [PATCH 20/22] make root layout named, use cbsink to pass through omnibox messages, remove unnecessary sleep + closures in other poll futures introduced filter delay issue + modals all broken now --- launchk/src/launchd/plist.rs | 2 +- launchk/src/main.rs | 9 +++-- launchk/src/tui/dialog.rs | 2 +- launchk/src/tui/omnibox/view.rs | 2 +- launchk/src/tui/root.rs | 51 ++++++++++++++-------------- launchk/src/tui/service_list/view.rs | 5 +-- 6 files changed, 35 insertions(+), 36 deletions(-) diff --git a/launchk/src/launchd/plist.rs b/launchk/src/launchd/plist.rs index 393fd74..e8fe1a3 100644 --- a/launchk/src/launchd/plist.rs +++ b/launchk/src/launchd/plist.rs @@ -243,7 +243,7 @@ pub fn init_plist_map(runtime_handle: &Handle) { insert_plists(plists); // Spawn fsnotify subscriber - runtime_handle.spawn(async { fsnotify_subscriber().await }); + runtime_handle.spawn(fsnotify_subscriber()); } /// Get plist for a label diff --git a/launchk/src/main.rs b/launchk/src/main.rs index 05a6bd1..c56c5d2 100644 --- a/launchk/src/main.rs +++ b/launchk/src/main.rs @@ -9,8 +9,8 @@ extern crate bitflags; extern crate plist; -use cursive::view::Resizable; -use cursive::views::Panel; +use cursive::view::{Resizable, AnyView}; +use cursive::views::{Panel, NamedView}; use cursive::Cursive; use std::process::exit; @@ -28,14 +28,17 @@ fn main() { .build() .expect("Must build tokio runtime"); - // Cache launchd job plists, spawn fsnotify to keep up with changes + // Cache launchd job plist paths, spawn fsnotify to keep up with changes PLIST_MAP_INIT.call_once(|| init_plist_map(runtime.handle())); let mut siv: Cursive = cursive::default(); siv.load_toml(include_str!("tui/style.toml")) .expect("Must load styles"); + siv.set_autorefresh(true); + let root_layout = RootLayout::new(&mut siv, runtime.handle()); + let root_layout = NamedView::new("root_layout", root_layout); let panel = Panel::new(root_layout) .title("launchk") diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index c417298..cdb2b9e 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -105,7 +105,7 @@ pub fn domain_session_prompt>( } } - let mut layout = LinearLayout::horizontal() + let layout = LinearLayout::horizontal() .child(domain_type_layout) .child(session_type_layout); diff --git a/launchk/src/tui/omnibox/view.rs b/launchk/src/tui/omnibox/view.rs index 6ab0d52..f8d8a26 100644 --- a/launchk/src/tui/omnibox/view.rs +++ b/launchk/src/tui/omnibox/view.rs @@ -100,7 +100,7 @@ impl OmniboxView { let tx_state = state.clone(); let tx_tick = tx.clone(); - handle.spawn(async move { tick(tx_state, tx_tick).await }); + handle.spawn(tick(tx_state, tx_tick)); ( Self { diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index a2e966c..5fb2121 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -5,7 +5,7 @@ use std::time::Duration; use cursive::event::{Event, EventResult, Key}; use cursive::traits::{Resizable, Scrollable}; -use cursive::view::ViewWrapper; +use cursive::view::{ViewWrapper, AnyView}; use cursive::views::{LinearLayout, NamedView, Panel}; use cursive::{Cursive, Vec2, View, Printer}; @@ -25,9 +25,7 @@ pub type CbSinkMessage = Box; pub struct RootLayout { layout: LinearLayout, - omnibox_rx: Receiver, omnibox_tx: Sender, - last_focus_index: RefCell, runtime_handle: Handle, cbsink_channel: Sender, key_ring: VecDeque, @@ -41,16 +39,35 @@ enum RootLayoutChildren { ServiceList, } +async fn poll_omnibox(cb_sink: Sender, rx: Receiver) { + loop { + let recv = rx + .recv() + .expect("Must receive event"); + + log::info!("[root_layout/poll_omnibox]: RECV {:?}", recv); + + cb_sink.send(Box::new(|siv| { + siv.call_on_name("root_layout", |v: &mut NamedView| { + v.get_mut().handle_omnibox_event(recv); + }); + + siv.refresh(); + })); + } +} + impl RootLayout { pub fn new(siv: &mut Cursive, runtime_handle: &Handle) -> Self { let (omnibox, omnibox_tx, omnibox_rx) = OmniboxView::new(runtime_handle); + let cbsink_channel = RootLayout::cbsink_channel(siv, runtime_handle); + + runtime_handle.spawn(poll_omnibox(cbsink_channel.clone(), omnibox_rx)); let mut new = Self { omnibox_tx, - omnibox_rx, + cbsink_channel, layout: LinearLayout::vertical(), - last_focus_index: RefCell::new(RootLayoutChildren::ServiceList as usize), - cbsink_channel: RootLayout::cbsink_channel(siv, runtime_handle), runtime_handle: runtime_handle.clone(), key_ring: VecDeque::with_capacity(3), }; @@ -89,11 +106,7 @@ impl RootLayout { let sink = siv.cb_sink().clone(); handle.spawn(async move { - let mut interval = interval(Duration::from_millis(500)); - loop { - interval.tick().await; - if let Ok(cb_sink_msg) = rx.recv() { sink.send(cb_sink_msg) .expect("Cannot forward CbSink message") @@ -111,24 +124,14 @@ impl RootLayout { self.layout.on_event(event) } - /// Poll for Omnibox commands without blocking - fn poll_omnibox(&mut self) { - let recv = self.omnibox_rx.try_recv(); - - if recv.is_err() { - return; - } - - let recv = recv.unwrap(); - log::info!("[root/poll_omnibox]: {:?}", recv); - + fn handle_omnibox_event(&mut self, recv: OmniboxEvent) { self.on_omnibox(recv.clone()) .expect("Root for effects only"); // The Omnibox command is sent to the actively focused view let target = self .layout - .get_child_mut(*self.last_focus_index.borrow()) + .get_child_mut(self.layout.get_focus_index()) .and_then(|v| v.as_any_mut().downcast_mut::()); if target.is_none() { @@ -187,7 +190,6 @@ impl ViewWrapper for RootLayout { fn wrap_on_event(&mut self, event: Event) -> EventResult { log::debug!("[root/event]: {:?}", event); - self.poll_omnibox(); let ev = match event { Event::Char('/') | Event::Char(':') @@ -217,13 +219,10 @@ impl ViewWrapper for RootLayout { _ => self.layout.on_event(event), }; - self.poll_omnibox(); - ev } fn wrap_layout(&mut self, size: Vec2) { - self.poll_omnibox(); self.layout.layout(size) } } diff --git a/launchk/src/tui/service_list/view.rs b/launchk/src/tui/service_list/view.rs index a2ae7e8..d6de41d 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -59,10 +59,7 @@ pub struct ServiceListView { impl ServiceListView { pub fn new(runtime_handle: &Handle, cb_sink: Sender) -> Self { let arc_svc = Arc::new(RwLock::new(HashSet::new())); - let ref_clone = arc_svc.clone(); - let cb_sink_clone = cb_sink.clone(); - - runtime_handle.spawn(async move { poll_running_jobs(ref_clone, cb_sink_clone).await }); + runtime_handle.spawn(poll_running_jobs(arc_svc.clone(), cb_sink.clone())); Self { cb_sink, From 4e896d4ec75884b4f73ee8bf34a0f480e58cba73 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Sat, 15 May 2021 22:22:24 -0400 Subject: [PATCH 21/22] fix bug / better polling --- launchk/src/main.rs | 4 +--- launchk/src/tui/root.rs | 14 ++++---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/launchk/src/main.rs b/launchk/src/main.rs index c56c5d2..6d5a669 100644 --- a/launchk/src/main.rs +++ b/launchk/src/main.rs @@ -34,9 +34,7 @@ fn main() { let mut siv: Cursive = cursive::default(); siv.load_toml(include_str!("tui/style.toml")) .expect("Must load styles"); - - siv.set_autorefresh(true); - + let root_layout = RootLayout::new(&mut siv, runtime.handle()); let root_layout = NamedView::new("root_layout", root_layout); diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index 5fb2121..be9a74e 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -51,8 +51,6 @@ async fn poll_omnibox(cb_sink: Sender, rx: Receiver siv.call_on_name("root_layout", |v: &mut NamedView| { v.get_mut().handle_omnibox_event(recv); }); - - siv.refresh(); })); } } @@ -128,17 +126,13 @@ impl RootLayout { self.on_omnibox(recv.clone()) .expect("Root for effects only"); - // The Omnibox command is sent to the actively focused view let target = self .layout - .get_child_mut(self.layout.get_focus_index()) - .and_then(|v| v.as_any_mut().downcast_mut::()); - - if target.is_none() { - return; - } + .get_child_mut(RootLayoutChildren::ServiceList as usize) + .and_then(|v| v.as_any_mut().downcast_mut::()) + .expect("Must forward to ServiceList"); - match target.unwrap().on_omnibox(recv) { + match target.on_omnibox(recv) { // Forward Omnibox command responses from view Ok(Some(c)) => self .omnibox_tx From 89b35a11fd868f7d4d8266dc32c41f565aae1003 Mon Sep 17 00:00:00 2001 From: David Stancu Date: Mon, 17 May 2021 16:58:55 -0400 Subject: [PATCH 22/22] fix messy exit by dropping siv instance before exit --- launchk/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/launchk/src/main.rs b/launchk/src/main.rs index 6d5a669..82ebdc9 100644 --- a/launchk/src/main.rs +++ b/launchk/src/main.rs @@ -34,7 +34,7 @@ fn main() { let mut siv: Cursive = cursive::default(); siv.load_toml(include_str!("tui/style.toml")) .expect("Must load styles"); - + let root_layout = RootLayout::new(&mut siv, runtime.handle()); let root_layout = NamedView::new("root_layout", root_layout); @@ -45,6 +45,10 @@ fn main() { siv.add_layer(panel); siv.run(); + siv.quit(); + // Fix reset on exit + // https://github.com/gyscos/cursive/issues/415 + drop(siv); exit(0); }