diff --git a/Cargo.lock b/Cargo.lock index ce9d3c9..2981c7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -651,9 +651,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.86" +version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "libloading" @@ -1390,6 +1390,7 @@ dependencies = [ "bitflags", "block", "lazy_static", + "libc", "log", "xcrun", ] diff --git a/README.md b/README.md index f8115ea..638612a 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ [![Rust](https://github.com/mach-kernel/launchk/actions/workflows/rust.yml/badge.svg?branch=master)](https://github.com/mach-kernel/launchk/actions/workflows/rust.yml) -A WIP [Cursive](https://github.com/gyscos/cursive) TUI that makes XPC queries & helps manage launchd jobs. +A [Cursive](https://github.com/gyscos/cursive) TUI that makes XPC queries & helps manage launchd jobs. Should work on macOS 10.10+ according to the availability sec. [in the docs](https://developer.apple.com/documentation/xpc?language=objc). - + #### Features @@ -16,183 +16,41 @@ Should work on macOS 10.10+ according to the availability sec. [in the docs](htt - 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. +- `load` +- `unload` +- `dumpstate` (opens in `$PAGER`) +- `dumpjpcategory` (opens in `$PAGER`) +- `procinfo` (opens in `$PAGER`, does not require root!) +- `edit` plist in `$EDITOR` with support for binary plists +- `csrinfo` show all CSR flags and their values -### xpc-sys crate +#### xpc-sys -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. +While building launchk, XPC convenience glue was placed in `xpc-sys`. -##### 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). - -**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 - -Go from a `HashMap` to `xpc_object_t` with the `XPCObject` type: - -```rust -let mut message: HashMap<&str, XPCObject> = HashMap::new(); -message.insert("type", XPCObject::from(1 as u64)); -message.insert("handle", XPCObject::from(0 as u64)); -message.insert("subsystem", XPCObject::from(3 as u64)); -message.insert("routine", XPCObject::from(815 as u64)); -message.insert("legacy", XPCObject::from(true)); - -let xpc_object: XPCObject = message.into(); -``` - -Call `xpc_pipe_routine` and receive `Result`: - -```rust -let xpc_object: XPCObject = message.into(); - -match xpc_object.pipe_routine() { - Ok(xpc_object) => { /* do stuff and things */ }, - Err(XPCError::PipeError(err)) => { /* err is a string w/strerror(errno) */ } -} -``` - -The response is likely an XPC dictionary -- go back to a HashMap: - -```rust -let xpc_object: XPCObject = message.into(); -let response: Result = xpc_object - .pipe_routine() - .and_then(|r| r.try_into()); - -let XPCDictionary(hm) = response.unwrap(); -let whatever = hm.get("..."); -``` - -Response dictionaries can be nested, so `XPCDictionary` has a helper included for this scenario: - -```rust -let xpc_object: XPCObject = message.into(); - -// A string: either "Aqua", "StandardIO", "Background", "LoginWindow", "System" -let response: Result = xpc_object - .pipe_routine() - .and_then(|r: XPCObject| r.try_into()); - .and_then(|d: XPCDictionary| d.get(&["service", "LimitLoadToSessionType"]) - .and_then(|lltst: XPCObject| lltst.xpc_value()); -``` - -Or, retrieve the `service` key (a child XPC Dictionary) from this response: - -```rust -let xpc_object: XPCObject = message.into(); - -// A string: either "Aqua", "StandardIO", "Background", "LoginWindow", "System" -let response: Result = xpc_object - .pipe_routine() - .and_then(|r: XPCObject| r.try_into()); - .and_then(|d: XPCDictionary| d.get_as_dictionary(&["service"]); - -let XPCDictionary(hm) = response.unwrap(); -let whatever = hm.get("..."); -``` - -##### XPC Arrays - -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"]); -``` - -Go back to `Vec` using `xpc_value`: - -```rust -let rs_vec: Vec = xpc_array.xpc_value().unwrap(); -``` +[[See its README here]](xpc-sys/README.md) ### 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/) +- [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/) +- [libc](https://crates.io/crates/libc) +- [lazy_static](https://crates.io/crates/lazy_static) +- [xcrun](https://crates.io/crates/xcrun) +- [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 :) +- 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 +Everything else (C) David Stancu & Contributors 2021 diff --git a/doc/launchctl_messages.md b/doc/launchctl_messages.md index 702c695..ba1d8cb 100644 --- a/doc/launchctl_messages.md +++ b/doc/launchctl_messages.md @@ -351,4 +351,187 @@ Using a `gui/` domain target: 1: System 2: User -8: Login (GUI)? \ No newline at end of file +8: Login (GUI)? + +#### `launchctl dumpstate` + +``` +(lldb) p printf("%s",(char*) xpc_copy_description($rsi)) + { count = 5, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 0 + "shmem" => : 20971520 bytes (5121 pages) + "routine" => : 834 + "type" => : 1 +(int) $0 = 328 +``` + +Get the shmem key from the dictionary, map the shmem region, then continue so it can be filled, then it can be read from: + +``` +expr void * $my_shmem = ((void *) xpc_dictionary_get_value($rsi, "shmem")); +expr void * $my_region = 0; +expr size_t $my_shsize = (size_t) xpc_shmem_map($my_shmem, &$my_region); +c +(lldb) mem read $my_region $my_region+100 +0x106580000: 63 6f 6d 2e 61 70 70 6c 65 2e 78 70 63 2e 6c 61 com.apple.xpc.la +0x106580010: 75 6e 63 68 64 2e 64 6f 6d 61 69 6e 2e 73 79 73 unchd.domain.sys +0x106580020: 74 65 6d 20 3d 20 7b 0a 09 74 79 70 65 20 3d 20 tem = {..type = +0x106580030: 73 79 73 74 65 6d 0a 09 68 61 6e 64 6c 65 20 3d system..handle = +0x106580040: 20 30 0a 09 61 63 74 69 76 65 20 63 6f 75 6e 74 0..active count +0x106580050: 20 3d 20 35 37 35 0a 09 6f 6e 2d 64 65 6d 61 6e = 575..on-deman +0x106580060: 64 20 63 6f d co +``` + +#### `launchctl procinfo 7578` + +This makes a whole _bunch_ of XPC calls! First it enumerates some ports: + +``` +2021-05-18 20:44:57.098359-0400 launchctl[7578:42112] [All] launchctl procinfo: launchctl procinfo 7475 +program path = /usr/local/Cellar/redis/6.2.1/bin/redis-server +mach info = { +(lldb) p printf("%s",(char*) xpc_copy_description($rsi)) + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 0 + "routine" => : 822 + "process" => : 7578 + "name" => : 3335 + "type" => : 1 +(int) $6 = 360 +``` + +``` +} task-kernel port = 0xd07 (unknown) +(lldb) p printf("%s",(char*) xpc_copy_description($rsi)) + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 0 + "routine" => : 822 + "process" => : 7578 + "name" => : 4611 + "type" => : 1 +(int) $7 = 360 +``` + +``` +} task-host port = 0x1203 (unknown) +Process 7578 stopped +* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.3 + frame #0: 0x00007fff2005e841 libxpc.dylib`xpc_pipe_routine_with_flags +(lldb) p printf("%s",(char*) xpc_copy_description($rsi)) + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 0 + "routine" => : 822 + "process" => : 7578 + "name" => : 5635 + "type" => : 1 +``` + +``` +} task-name port = 0x1603 (unknown) +(lldb) p printf("%s",(char*) xpc_copy_description($rsi)) + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 0 + "routine" => : 822 + "process" => : 7578 + "name" => : 5123 + "type" => : 1 +(int) $9 = 360 +``` + +``` +} task-bootstrap port = 0x1403 (unknown) +(lldb) p printf("%s",(char*) xpc_copy_description($rsi)) + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 0 + "routine" => : 822 + "process" => : 7578 + "name" => : 5639 + "type" => : 1 +``` + +``` +} task-(null) port = 0x1607 (unknown) +(lldb) p printf("%s",(char*) xpc_copy_description($rsi)) + { count = 6, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 3 + "handle" => : 0 + "routine" => : 822 + "process" => : 7578 + "name" => : 5643 + "type" => : 1 +``` + +Now for our old shmem / stdout friend: + +``` +argument count = 3 +argument vector = { + [0] = /usr/local/opt/redis/bin/redis-server 127.0.0.1:6379 + [1] = XPC_FLAGS=1 + [2] = LOGNAME=mach +} +environment vector = { + USER => mach + HOME => /Users/mach + SHELL => /bin/zsh + TMPDIR => /var/folders/sl/4tlmgdgj60j2wgykq7q10pdw0000gn/T/ +} +bsd proc info = { + pid = 7475 + unique pid = 7475 + ppid = 1 + pgid = 7475 + status = stopped + flags = 64-bit + uid = 501 + svuid = 501 + ruid = 501 + gid = 20 + svgid = 20 + rgid = 20 + comm name = redis-server + long name = redis-server + controlling tty devnode = 0xffffffff + controlling tty pgid = 0 +} +audit info + session id = 100006 + uid = 501 + success mask = 0x3000 + failure mask = 0x3000 + flags = has_graphic_access,has_tty,has_console_access,has_authenticated +sandboxed = no +container = (no container) + +responsible pid = 7475 +responsible unique pid = 7475 +responsible path = /usr/local/Cellar/redis/6.2.1/bin/redis-server + +pressured exit info = { + dirty state tracked = 0 + dirty = 0 + pressured-exit capable = 0 +} + +jetsam priority = 3: background +jetsam memory limit = -1 +jetsam state = (normal memory state) + +entitlements = (no entitlements) + +code signing info = (none) + +(lldb) p printf("%s",(char*) xpc_copy_description($rsi)) + { count = 4, transaction: 0, voucher = 0x0, contents = + "subsystem" => : 2 + "fd" => { type = (invalid descriptor), path = /dev/ttys003 } + "routine" => : 708 + "pid" => : 7475 +(int) $14 = 302 +``` diff --git a/launchk/Cargo.toml b/launchk/Cargo.toml index 1e318bc..94630ff 100644 --- a/launchk/Cargo.toml +++ b/launchk/Cargo.toml @@ -16,4 +16,4 @@ plist = "1.1.0" bitflags = "1.2.1" notify = "4.0.16" log = "0.4.14" -env_logger = "0.8.3" \ No newline at end of file +env_logger = "0.8.3" diff --git a/launchk/src/launchd/entry_status.rs b/launchk/src/launchd/entry_status.rs index 3d6b67f..7882982 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, DomainType}; +use crate::launchd::enums::{DomainType, SessionType}; use crate::launchd::plist::LaunchdPlist; use crate::launchd::query::find_in_all; use xpc_sys::traits::xpc_value::TryXPCValue; diff --git a/launchk/src/launchd/message.rs b/launchk/src/launchd/message.rs index 0270728..0ac32f8 100644 --- a/launchk/src/launchd/message.rs +++ b/launchk/src/launchd/message.rs @@ -1,6 +1,8 @@ use crate::launchd::query_builder::QueryBuilder; use xpc_sys::objects::xpc_dictionary::XPCDictionary; +// A bunch of XPCDictionary 'protos' that can be extended to make XPC queries + lazy_static! { /// launchctl list [name] pub static ref LIST_SERVICES: XPCDictionary = XPCDictionary::new() @@ -13,7 +15,7 @@ lazy_static! { /// launchctl load [path] pub static ref LOAD_PATHS: XPCDictionary = XPCDictionary::new() - .with_domain_port() + .with_domain_port_as_bootstrap_port() .entry("routine", 800 as u64) .entry("subsystem", 3 as u64) .entry("handle", 0 as u64) @@ -24,7 +26,7 @@ lazy_static! { /// launchctl unload [path] pub static ref UNLOAD_PATHS: XPCDictionary = XPCDictionary::new() - .with_domain_port() + .with_domain_port_as_bootstrap_port() .entry("routine", 801 as u64) .entry("subsystem", 3 as u64) .entry("handle", 0 as u64) @@ -34,15 +36,39 @@ lazy_static! { .entry("no-einprogress", true); + /// launchctl enable pub static ref ENABLE_NAMES: XPCDictionary = XPCDictionary::new() - .with_domain_port() + .with_domain_port_as_bootstrap_port() // .entry("handle", UID or ASID) .entry("routine", 808 as u64) .entry("subsystem", 3 as u64); + /// launchctl disable pub static ref DISABLE_NAMES: XPCDictionary = XPCDictionary::new() - .with_domain_port() + .with_domain_port_as_bootstrap_port() // .entry("handle", UID or ASID) .entry("routine", 809 as u64) .entry("subsystem", 3 as u64); + + /// launchctl dumpstate + /// Requires a shmem xpc_object_t member, see XPCShmem for more details + pub static ref DUMPSTATE: XPCDictionary = XPCDictionary::new() + .entry("subsystem", 3 as u64) + .entry("routine", 834 as u64) + .entry("type", 1 as u64) + .with_handle_or_default(None); + + /// launchctl dumpjpcategory + /// Requires a FD".entry("fd", 1 as RawFd)" + pub static ref DUMPJPCATEGORY: XPCDictionary = XPCDictionary::new() + .entry("subsystem", 3 as u64) + .entry("routine", 837 as u64) + .entry("type", 1 as u64) + .with_handle_or_default(None); + + /// launchctl procinfo + /// Requires a FD".entry("fd", 1 as RawFd)" + pub static ref PROCINFO: XPCDictionary = XPCDictionary::new() + .entry("subsystem", 2 as u64) + .entry("routine", 708 as u64); } diff --git a/launchk/src/launchd/plist.rs b/launchk/src/launchd/plist.rs index e8fe1a3..d48b8de 100644 --- a/launchk/src/launchd/plist.rs +++ b/launchk/src/launchd/plist.rs @@ -21,11 +21,9 @@ lazy_static! { pub static ref LABEL_TO_ENTRY_CONFIG: RwLock> = RwLock::new(HashMap::new()); static ref EDITOR: &'static str = option_env!("EDITOR").unwrap_or("vim"); + static ref TMP_DIR: &'static str = option_env!("TMPDIR").unwrap_or("/tmp"); } -// TODO: fall back on /tmp -static TMP_DIR: &str = env!("TMPDIR"); - /* od -xc binary.plist 0000000 7062 696c 7473 3030 @@ -277,7 +275,7 @@ pub fn edit_and_replace(plist_meta: &LaunchdPlist) -> Result<(), String> { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Must get ts"); - let temp_path = Path::new(TMP_DIR).join(format!("{}", now.as_secs())); + let temp_path = Path::new(*TMP_DIR).join(format!("{}", now.as_secs())); plist.to_file_xml(&temp_path).map_err(|e| e.to_string())?; // Start $EDITOR diff --git a/launchk/src/launchd/query.rs b/launchk/src/launchd/query.rs index 03b02fc..fc3be94 100644 --- a/launchk/src/launchd/query.rs +++ b/launchk/src/launchd/query.rs @@ -1,9 +1,15 @@ use crate::launchd::message::{ - DISABLE_NAMES, ENABLE_NAMES, LIST_SERVICES, LOAD_PATHS, UNLOAD_PATHS, + DISABLE_NAMES, DUMPJPCATEGORY, DUMPSTATE, ENABLE_NAMES, LIST_SERVICES, LOAD_PATHS, PROCINFO, + UNLOAD_PATHS, }; -use std::collections::HashSet; +use std::convert::TryFrom; +use std::{collections::HashSet, os::unix::prelude::RawFd}; -use xpc_sys::traits::xpc_pipeable::XPCPipeable; +use xpc_sys::{ + objects::xpc_shmem::XPCShmem, + traits::{xpc_pipeable::XPCPipeable, xpc_value::TryXPCValue}, + MAP_SHARED, +}; use crate::launchd::entry_status::ENTRY_STATUS_CACHE; use std::iter::FromIterator; @@ -126,3 +132,39 @@ pub fn disable>( .with_handle_or_default(None) .pipe_routine_with_error_handling() } + +/// Create a shared shmem region for the XPC routine to write +/// dumpstate contents into, and return the bytes written and +/// shmem region +pub fn dumpstate() -> Result<(usize, XPCShmem), XPCError> { + let shmem = XPCShmem::new_task_self( + 0x1400000, + i32::try_from(MAP_SHARED).expect("Must conv flags"), + )?; + + log::info!("Made shmem {:?}", shmem); + + let response = XPCDictionary::new() + .extend(&DUMPSTATE) + .entry("shmem", shmem.xpc_object.clone()) + .pipe_routine_with_error_handling()?; + + let bytes_written: u64 = response.get(&["bytes-written"])?.xpc_value()?; + + Ok((usize::try_from(bytes_written).unwrap(), shmem)) +} + +pub fn dumpjpcategory(fd: RawFd) -> Result { + XPCDictionary::new() + .extend(&DUMPJPCATEGORY) + .entry("fd", fd) + .pipe_routine_with_error_handling() +} + +pub fn procinfo(pid: i64, fd: RawFd) -> Result { + XPCDictionary::new() + .extend(&PROCINFO) + .entry("fd", fd) + .entry("pid", pid) + .pipe_routine_with_error_handling() +} diff --git a/launchk/src/launchd/query_builder.rs b/launchk/src/launchd/query_builder.rs index dda5cf0..0c80e52 100644 --- a/launchk/src/launchd/query_builder.rs +++ b/launchk/src/launchd/query_builder.rs @@ -1,25 +1,36 @@ use crate::launchd::enums::{DomainType, SessionType}; use xpc_sys::objects::xpc_dictionary::XPCDictionary; +use xpc_sys::objects::xpc_object::MachPortType; use xpc_sys::objects::xpc_object::XPCObject; use xpc_sys::{get_bootstrap_port, mach_port_t}; +/// Builder methods for XPCDictionary to make querying easier pub trait QueryBuilder { + /// Add entry to query fn entry, O: Into>(self, key: S, value: O) -> XPCDictionary; + + /// Add entry if option is Some() fn entry_if_present, O: Into>( self, key: S, value: Option, ) -> XPCDictionary; + /// Extend an existing XPCDictionary fn extend(self, other: &XPCDictionary) -> XPCDictionary; - fn with_domain_port(self) -> XPCDictionary + /// Adds "domain_port" with get_bootstrap_port() -> _xpc_type_mach_send + fn with_domain_port_as_bootstrap_port(self) -> XPCDictionary where Self: Sized, { - self.entry("domain-port", get_bootstrap_port() as mach_port_t) + self.entry( + "domain-port", + (MachPortType::Send, get_bootstrap_port() as mach_port_t), + ) } + /// Adds provided session type or falls back on Aqua fn with_session_type_or_default(self, session: Option) -> XPCDictionary where Self: Sized, @@ -27,13 +38,15 @@ pub trait QueryBuilder { self.entry("session", session.unwrap_or(SessionType::Aqua).to_string()) } - fn with_handle_or_default(self, session: Option) -> XPCDictionary + /// Adds provided handle or falls back on 0 + fn with_handle_or_default(self, handle: Option) -> XPCDictionary where Self: Sized, { - self.entry("handle", session.unwrap_or(0)) + self.entry("handle", handle.unwrap_or(0)) } + /// Adds provided DomainType, falls back on 7 (requestor's domain) fn with_domain_type_or_default(self, t: Option) -> XPCDictionary where Self: Sized, diff --git a/launchk/src/main.rs b/launchk/src/main.rs index 82ebdc9..0b008d8 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, AnyView}; -use cursive::views::{Panel, NamedView}; +use cursive::view::Resizable; +use cursive::views::{NamedView, Panel}; use cursive::Cursive; use std::process::exit; diff --git a/launchk/src/tui/dialog.rs b/launchk/src/tui/dialog.rs index cdb2b9e..0587b0b 100644 --- a/launchk/src/tui/dialog.rs +++ b/launchk/src/tui/dialog.rs @@ -7,16 +7,17 @@ use cursive::{ views::{Dialog, DummyView, LinearLayout, RadioGroup, TextView}, }; +use crate::launchd::entry_status::{get_entry_status, LaunchdEntryStatus}; +use crate::tui::omnibox::command::OMNIBOX_COMMANDS; use crate::tui::omnibox::view::OmniboxEvent; use crate::tui::root::CbSinkMessage; use crate::{ launchd::enums::{DomainType, SessionType}, tui::omnibox::command::OmniboxCommand, }; -use crate::launchd::entry_status::{get_entry_status, LaunchdEntryStatus}; +use xpc_sys::csr::{csr_check, CsrConfig}; -/// 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? +/// XPC "error" key can be present with no failure..."notice"? pub fn show_error(err: String) -> CbSinkMessage { let cl = |siv: &mut Cursive| { let dialog = Dialog::around(TextView::new(err)) @@ -56,6 +57,8 @@ pub fn show_prompt( Box::new(cl) } +/// Don't know how to get this info when job is not running, +/// so we can ask user and suggest a default (domain 7, aqua) pub fn domain_session_prompt>( label: S, domain_only: bool, @@ -79,7 +82,8 @@ pub fn domain_session_prompt>( 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.clone(), format!("{}: {}", d, &as_domain)); + let mut button = + domain_group.button(as_domain.clone(), format!("{}: {}", d, &as_domain)); if as_domain == domain { button = button.selected(); } @@ -135,3 +139,39 @@ pub fn domain_session_prompt>( Box::new(cl) } + +pub fn show_csr_info() -> CbSinkMessage { + let csr_flags = (0..11) + .map(|s| { + let mask = CsrConfig::from_bits(1 << s).expect("Must be in CsrConfig"); + format!("{:?}: {}", mask, unsafe { csr_check(mask.bits()) } == 0) + }) + .collect::>(); + + Box::new(move |siv| { + siv.add_layer( + Dialog::new() + .title("CSR Info") + .content(TextView::new(csr_flags.join("\n"))) + .dismiss_button("OK") + .padding(Margins::trbl(4, 4, 4, 4)), + ) + }) +} + +pub fn show_help() -> CbSinkMessage { + let commands = OMNIBOX_COMMANDS + .iter() + .map(|(cmd, desc, _)| format!("{}: {}", cmd, desc)) + .collect::>(); + + Box::new(move |siv| { + siv.add_layer( + Dialog::new() + .title("Help") + .content(TextView::new(commands.join("\n"))) + .dismiss_button("OK") + .padding(Margins::trbl(4, 4, 4, 4)), + ) + }) +} diff --git a/launchk/src/tui/mod.rs b/launchk/src/tui/mod.rs index 6872ea2..9d78d19 100644 --- a/launchk/src/tui/mod.rs +++ b/launchk/src/tui/mod.rs @@ -1,5 +1,6 @@ mod dialog; mod omnibox; +mod pager; pub mod root; mod service_list; mod sysinfo; diff --git a/launchk/src/tui/omnibox/command.rs b/launchk/src/tui/omnibox/command.rs index 9c30df8..01273db 100644 --- a/launchk/src/tui/omnibox/command.rs +++ b/launchk/src/tui/omnibox/command.rs @@ -26,6 +26,11 @@ pub enum OmniboxCommand { fn(DomainType, Option) -> Vec, ), FocusServiceList, + CSRInfo, + DumpState, + DumpJetsamPropertiesCategory, + ProcInfo, + Help, Quit, } @@ -35,7 +40,7 @@ impl fmt::Display for OmniboxCommand { } } -pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ +pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 12] = [ ( "load", "▶️ Load highlighted job", @@ -45,7 +50,6 @@ pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ "unload", "⏏️ Unload highlighted job", OmniboxCommand::UnloadRequest, - // OmniboxCommand::DomainSessionPrompt(false, |dt, _| vec![OmniboxCommand::Unload(dt, None)]), ), ( "enable", @@ -55,7 +59,7 @@ pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ ( "disable", "⏏️ Disable highlighted job (prevents load)", - OmniboxCommand::DisableRequest + OmniboxCommand::DisableRequest, ), ( "edit", @@ -67,5 +71,22 @@ pub static OMNIBOX_COMMANDS: [(&str, &str, OmniboxCommand); 7] = [ "🔄 Reload highlighted job", OmniboxCommand::Reload, ), + ("csrinfo", "ℹ️ See all CSR flags", OmniboxCommand::CSRInfo), + ( + "dumpstate", + "ℹ️ launchctl dumpstate", + OmniboxCommand::DumpState, + ), + ( + "dumpjpcategory", + "ℹ️ launchctl dumpjpcategory", + OmniboxCommand::DumpJetsamPropertiesCategory, + ), + ( + "procinfo", + "ℹ️ launchctl procinfo for highlighted process", + OmniboxCommand::ProcInfo, + ), + ("help", "🤔 Show all commands", OmniboxCommand::Help), ("exit", "🚪 see ya!", OmniboxCommand::Quit), ]; diff --git a/launchk/src/tui/omnibox/state.rs b/launchk/src/tui/omnibox/state.rs index ad9f3c9..3c982dd 100644 --- a/launchk/src/tui/omnibox/state.rs +++ b/launchk/src/tui/omnibox/state.rs @@ -45,7 +45,7 @@ impl OmniboxState { OMNIBOX_COMMANDS .iter() - .filter(|(c, _, _)| c.to_string().contains(command_filter)) + .filter(|(c, _, _)| c.to_string().starts_with(command_filter)) .next() .map(|s| s.clone()) } diff --git a/launchk/src/tui/omnibox/view.rs b/launchk/src/tui/omnibox/view.rs index f8d8a26..dae2ab0 100644 --- a/launchk/src/tui/omnibox/view.rs +++ b/launchk/src/tui/omnibox/view.rs @@ -113,6 +113,7 @@ impl OmniboxView { ) } + /// Commands fn handle_active(event: &Event, state: &OmniboxState) -> Option { let OmniboxState { mode, @@ -128,7 +129,8 @@ impl OmniboxView { .filter(|(cmd, _, _)| *cmd == *command_filter) .map(|(_, _, oc)| oc.clone()); - let (lf_char, cf_char) = match (event, mode) { + // Avoid extra clauses below, use same options for string filters + let (lf_char_update, cf_char_update) = match (event, mode) { (Event::Char(c), OmniboxMode::LabelFilter) => { (Some(format!("{}{}", label_filter, c)), None) } @@ -139,6 +141,7 @@ impl OmniboxView { }; match (event, mode) { + // Toggle back to query from bitmask filters (Event::Char(':'), OmniboxMode::JobTypeFilter) => { Some(state.with_new(Some(OmniboxMode::CommandFilter), None, None, None)) } @@ -149,7 +152,7 @@ impl OmniboxView { // User -> string filters (Event::Char(_), OmniboxMode::LabelFilter) | (Event::Char(_), OmniboxMode::CommandFilter) => { - Some(state.with_new(None, lf_char, cf_char, None)) + Some(state.with_new(None, lf_char_update, cf_char_update, None)) } (Event::Key(Key::Backspace), OmniboxMode::LabelFilter) if !label_filter.is_empty() => { let mut lf = label_filter.clone(); @@ -184,6 +187,7 @@ impl OmniboxView { } } + /// Toggle bitmask on key fn handle_job_type_filter(event: &Event, state: &OmniboxState) -> Option { let mut jtf = state.job_type_filter.clone(); @@ -200,6 +204,7 @@ impl OmniboxView { Some(state.with_new(Some(OmniboxMode::JobTypeFilter), None, None, Some(jtf))) } + /// Leave idle state fn handle_idle(event: &Event, state: &OmniboxState) -> Option { match event { Event::Char('/') => Some(state.with_new( @@ -271,7 +276,7 @@ impl OmniboxView { return; } let (cmd, desc, ..) = suggestion.unwrap(); - let cmd_string = cmd.to_string().replace(&state.command_filter, ""); + let cmd_string = cmd.to_string().replacen(&state.command_filter, "", 1); printer.with_style(Style::from(Color::Light(BaseColor::Black)), |p| { p.print(XY::new(0, 0), cmd_string.as_str()) @@ -289,15 +294,16 @@ impl OmniboxView { .. } = &*read; - let jtf_ofs = if *mode != OmniboxMode::JobTypeFilter { - // "[sguadl]" - 8 + let mut jtf_ofs = if *mode != OmniboxMode::JobTypeFilter { + "[sguadl]".len() } else { - // "[system global user agent daemon loaded]" - 40 + "[system global user agent daemon loaded]".len() }; - let mut jtf_ofs = self.last_size.borrow().x - jtf_ofs; + if jtf_ofs < self.last_size.borrow().x { + jtf_ofs = self.last_size.borrow().x - jtf_ofs; + } + printer.print(XY::new(jtf_ofs, 0), "["); jtf_ofs += 1; diff --git a/launchk/src/tui/pager.rs b/launchk/src/tui/pager.rs new file mode 100644 index 0000000..e82182c --- /dev/null +++ b/launchk/src/tui/pager.rs @@ -0,0 +1,36 @@ +use std::io::Write; +use std::process::{Command, Stdio}; +use std::sync::mpsc::Sender; + +use cursive::Cursive; + +use super::root::CbSinkMessage; +lazy_static! { + static ref PAGER: &'static str = option_env!("PAGER").unwrap_or("less"); +} + +/// Show $PAGER (or less), write buf, and clear Cursive after exiting +pub fn show_pager(cbsink: &Sender, buf: &[u8]) -> Result<(), String> { + let mut pager = Command::new(*PAGER) + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| e.to_string())?; + + // Broken pipe unless scroll to end, do not throw an error + pager + .stdin + .take() + .expect("Must get pager stdin") + .write_all(buf) + .unwrap_or(()); + + let res = pager.wait().map_err(|e| e.to_string())?; + + cbsink.send(Box::new(Cursive::clear)).expect("Must clear"); + + if res.success() { + Ok(()) + } else { + Err(format!("{} exited {:?}", *PAGER, res)) + } +} diff --git a/launchk/src/tui/root.rs b/launchk/src/tui/root.rs index be9a74e..a902370 100644 --- a/launchk/src/tui/root.rs +++ b/launchk/src/tui/root.rs @@ -1,25 +1,36 @@ -use std::cell::RefCell; use std::collections::VecDeque; +use std::os::unix::prelude::RawFd; +use std::ptr::slice_from_raw_parts; use std::sync::mpsc::{channel, Receiver, Sender}; -use std::time::Duration; +use std::sync::Arc; use cursive::event::{Event, EventResult, Key}; use cursive::traits::{Resizable, Scrollable}; -use cursive::view::{ViewWrapper, AnyView}; +use cursive::view::ViewWrapper; use cursive::views::{LinearLayout, NamedView, Panel}; -use cursive::{Cursive, Vec2, View, Printer}; +use cursive::{Cursive, Vec2, View}; use tokio::runtime::Handle; -use tokio::time::interval; -use crate::tui::dialog; +use xpc_sys::objects::unix_fifo::UnixFifo; + use crate::tui::omnibox::command::OmniboxCommand; use crate::tui::omnibox::subscribed_view::{ OmniboxResult, OmniboxSubscribedView, OmniboxSubscriber, Subscribable, }; use crate::tui::omnibox::view::{OmniboxError, OmniboxEvent, OmniboxView}; +use crate::tui::pager::show_pager; use crate::tui::service_list::view::ServiceListView; use crate::tui::sysinfo::SysInfo; +use crate::{ + launchd::query::dumpjpcategory, + tui::dialog::{show_csr_info, show_help}, +}; +use crate::{launchd::query::dumpstate, tui::dialog}; + +lazy_static! { + static ref PAGER: &'static str = option_env!("PAGER").unwrap_or("less"); +} pub type CbSinkMessage = Box; @@ -41,17 +52,17 @@ enum RootLayoutChildren { async fn poll_omnibox(cb_sink: Sender, rx: Receiver) { loop { - let recv = rx - .recv() - .expect("Must receive event"); + 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); - }); - })); + cb_sink + .send(Box::new(|siv| { + siv.call_on_name("root_layout", |v: &mut NamedView| { + v.get_mut().handle_omnibox_event(recv); + }); + })) + .expect("Must forward to root") } } @@ -98,7 +109,7 @@ impl RootLayout { .unwrap_or(()); } - /// Cursive uses a different crate for its channel, so this is some glue + /// Cursive uses a different crate for its channels (?), so this is some glue fn cbsink_channel(siv: &mut Cursive, handle: &Handle) -> Sender { let (tx, rx): (Sender, Receiver) = channel(); let sink = siv.cb_sink().clone(); @@ -123,8 +134,7 @@ impl RootLayout { } fn handle_omnibox_event(&mut self, recv: OmniboxEvent) { - self.on_omnibox(recv.clone()) - .expect("Root for effects only"); + let self_event = self.on_omnibox(recv.clone()); let target = self .layout @@ -132,18 +142,22 @@ impl RootLayout { .and_then(|v| v.as_any_mut().downcast_mut::()) .expect("Must forward to ServiceList"); - match target.on_omnibox(recv) { - // Forward Omnibox command responses from view - Ok(Some(c)) => self - .omnibox_tx - .send(OmniboxEvent::Command(c)) - .expect("Must send response commands"), - Err(OmniboxError::CommandError(s)) => self - .cbsink_channel - .send(dialog::show_error(s)) - .expect("Must show error"), - _ => {} - }; + let omnibox_events = [self_event, target.on_omnibox(recv)]; + + for omnibox_event in &omnibox_events { + match omnibox_event { + // Forward Omnibox command responses from view + Ok(Some(c)) => self + .omnibox_tx + .send(OmniboxEvent::Command(c.clone())) + .expect("Must send response commands"), + Err(OmniboxError::CommandError(s)) => self + .cbsink_channel + .send(dialog::show_error(s.clone())) + .expect("Must show error"), + _ => {} + } + } } fn ring_to_arrows(&mut self) -> Option { @@ -180,7 +194,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); @@ -262,6 +276,53 @@ impl OmniboxSubscriber for RootLayout { .expect("Must show prompt"); Ok(None) } + OmniboxEvent::Command(OmniboxCommand::CSRInfo) => { + self.cbsink_channel + .send(show_csr_info()) + .expect("Must show prompt"); + + Ok(None) + } + OmniboxEvent::Command(OmniboxCommand::DumpState) => { + let (size, shmem) = + dumpstate().map_err(|e| OmniboxError::CommandError(e.to_string()))?; + + log::info!("shmem response sz {}", size); + + show_pager(&self.cbsink_channel, unsafe { + &*slice_from_raw_parts(shmem.region as *mut u8, size) + }) + .map_err(|e| OmniboxError::CommandError(e))?; + + Ok(None) + } + OmniboxEvent::Command(OmniboxCommand::DumpJetsamPropertiesCategory) => { + let fifo = + Arc::new(UnixFifo::new(0o777).map_err(|e| OmniboxError::CommandError(e))?); + + let fifo_clone = fifo.clone(); + + // Spawn pipe reader + let fd_read_thread = std::thread::spawn(move || fifo_clone.block_and_read_bytes()); + + fifo.with_writer(|fd_write| dumpjpcategory(fd_write as RawFd)) + .map_err(|e| OmniboxError::CommandError(e.to_string()))?; + + // Join reader thread (and close fd) + let jetsam_data = fd_read_thread.join().expect("Must read jetsam data"); + + show_pager(&self.cbsink_channel, &jetsam_data) + .map_err(|e| OmniboxError::CommandError(e))?; + + Ok(None) + } + OmniboxEvent::Command(OmniboxCommand::Help) => { + self.cbsink_channel + .send(show_help()) + .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 d6de41d..e4c4624 100644 --- a/launchk/src/tui/service_list/view.rs +++ b/launchk/src/tui/service_list/view.rs @@ -12,22 +12,25 @@ use cursive::{Cursive, View, XY}; use tokio::runtime::Handle; use tokio::time::interval; +use xpc_sys::objects::unix_fifo::UnixFifo; +use crate::launchd::enums::{DomainType, SessionType}; use crate::launchd::job_type_filter::JobTypeFilter; use crate::launchd::plist::{edit_and_replace, LABEL_TO_ENTRY_CONFIG}; +use crate::launchd::query::procinfo; use crate::launchd::query::{disable, enable, 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}; use crate::tui::omnibox::view::{OmniboxError, OmniboxEvent, OmniboxMode}; +use crate::tui::pager::show_pager; 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) { @@ -44,7 +47,7 @@ async fn poll_running_jobs(svcs: Arc>>, cb_sink: Sender OmniboxResult { match cmd { OmniboxCommand::Reload => { - let (ServiceListItem { name, .. }, ..) = self.with_active_item_plist()?; + let (ServiceListItem { name, status, .. }, ..) = self.with_active_item_plist()?; let LaunchdEntryStatus { limit_load_to_session_type, domain, .. - } = get_entry_status(&name); + } = status; 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), - ] - }, - ))) - }, + (_, 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( @@ -223,45 +222,43 @@ impl ServiceListView { dt, None, )] - } + }, ))) - }, + } OmniboxCommand::UnloadRequest => { - let (ServiceListItem { name, .. }, ..) = self.with_active_item_plist()?; - let LaunchdEntryStatus { - domain, - .. - } = get_entry_status(&name); + let (ServiceListItem { name, status, .. }, ..) = self.with_active_item_plist()?; + let LaunchdEntryStatus { domain, .. } = status; match domain { - DomainType::Unknown => Ok(Some(OmniboxCommand::DomainSessionPrompt(name.clone(), true, |dt, _| vec![OmniboxCommand::Unload(dt, None)]))), - _ => Ok(Some(OmniboxCommand::Unload(domain, None))) + 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)] + |dt, _| vec![OmniboxCommand::Enable(dt)], ))) } OmniboxCommand::DisableRequest => { - let (ServiceListItem { name, .. }, ..) = self.with_active_item_plist()?; - let LaunchdEntryStatus { - domain, - .. - } = get_entry_status(&name); + let (ServiceListItem { name, status, .. }, ..) = self.with_active_item_plist()?; + let LaunchdEntryStatus { domain, .. } = status; match domain { DomainType::Unknown => Ok(Some(OmniboxCommand::DomainSessionPrompt( name.clone(), true, - |dt, _| vec![OmniboxCommand::Disable(dt)] + |dt, _| vec![OmniboxCommand::Disable(dt)], ))), - _ => Ok(Some(OmniboxCommand::Chain(vec![ - OmniboxCommand::Disable(domain) - ]))) + _ => Ok(Some(OmniboxCommand::Chain(vec![OmniboxCommand::Disable( + domain, + )]))), } } OmniboxCommand::Edit => { @@ -283,13 +280,14 @@ 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 (ServiceListItem { name, status, .. }, plist) = + self.with_active_item_plist()?; let LaunchdEntryStatus { limit_load_to_session_type, .. - } = get_entry_status(&name); + } = status; unload( name, @@ -313,6 +311,32 @@ impl ServiceListView { .map(|_| None) .map_err(|e| OmniboxError::CommandError(e.to_string())) } + OmniboxCommand::ProcInfo => { + let (ServiceListItem { name, status, .. }, _) = self.with_active_item_plist()?; + + if status.pid == 0 { + return Err(OmniboxError::CommandError(format!("No PID for {}", name))); + } + + let fifo = + Arc::new(UnixFifo::new(0o777).map_err(|e| OmniboxError::CommandError(e))?); + + let fifo_clone = fifo.clone(); + + // Spawn pipe reader + let fd_read_thread = std::thread::spawn(move || fifo_clone.block_and_read_bytes()); + + fifo.with_writer(|fd_write| procinfo(status.pid, fd_write)) + .map_err(|e| OmniboxError::CommandError(e.to_string()))?; + + // Join reader thread (and close fd) + let procinfo_data = fd_read_thread.join().expect("Must read procinfo data"); + + show_pager(&self.cb_sink, &procinfo_data) + .map_err(|e| OmniboxError::CommandError(e))?; + + Ok(None) + } _ => Ok(None), } } diff --git a/launchk/src/tui/table/column_sizer.rs b/launchk/src/tui/table/column_sizer.rs new file mode 100644 index 0000000..63b7018 --- /dev/null +++ b/launchk/src/tui/table/column_sizer.rs @@ -0,0 +1,94 @@ +use std::{cell::Cell, collections::HashMap, sync::Arc}; + +/// Width oriented column sizing utility +pub struct ColumnSizer { + /// Non user defined columns are an even split of space remaining from + /// x - user_sizes_total + pub dynamic_column_size: Cell, + /// TODO; wtf do I mean by padding + pub padding: Cell, + /// Column index -> width + pub user_sizes: HashMap, + pub num_columns: usize, + + num_dynamic_columns: usize, + /// Sum of user size widths + user_sizes_total: usize, +} + +impl ColumnSizer { + /// Create a new ColumnSizer + pub fn new(columns: I) -> Arc + where + I: IntoIterator)> + Clone, + K: AsRef, + { + let num_columns = columns.clone().into_iter().count(); + let column_iter = columns.into_iter(); + + let user_sizes: HashMap = column_iter + .zip(0..num_columns) + .filter_map(|((_, user_len), i)| user_len.map(|ul| (i, ul))) + .collect(); + + let user_sizes_total = user_sizes.values().sum(); + let num_dynamic_columns = num_columns - user_sizes.len(); + + let cs = Self { + num_dynamic_columns, + num_columns, + user_sizes, + user_sizes_total, + dynamic_column_size: Default::default(), + padding: Default::default(), + }; + + Arc::new(cs) + } + + /// Get the width for a column by index + pub fn width_for_index(&self, i: usize) -> usize { + let size = self + .user_sizes + .get(&i) + .map(Clone::clone) + .unwrap_or(self.dynamic_column_size.get()); + + // I have 'sized' my user defined columns around how much + // space I need to just display the font, and the rest by + // blindly dividing space, only apply padding to UDCs + let size = if self.user_sizes.contains_key(&i) { + size + self.padding.get() + } else { + size + }; + + if size > 1 { + size + } else { + 1 + } + } + + /// Call when x changes to recompute dynamic_column_size and padding + pub fn update_x(&self, x: usize) { + let mut remaining = if x > self.user_sizes_total { + x - self.user_sizes_total + } else { + 0 + }; + + let mut dcs = remaining / self.num_dynamic_columns; + if dcs > 35 { + dcs = 35; + } + + if remaining > (self.num_dynamic_columns * dcs) { + remaining = remaining - (self.num_dynamic_columns * dcs); + } + + self.dynamic_column_size.set(dcs); + self.padding + .set(remaining / (self.num_dynamic_columns + self.user_sizes.len())); + } +} diff --git a/launchk/src/tui/table/mod.rs b/launchk/src/tui/table/mod.rs index a9a8371..4968171 100644 --- a/launchk/src/tui/table/mod.rs +++ b/launchk/src/tui/table/mod.rs @@ -1,2 +1,3 @@ +mod column_sizer; mod table_headers; pub mod table_list_view; diff --git a/launchk/src/tui/table/table_headers.rs b/launchk/src/tui/table/table_headers.rs index f298722..02d3b96 100644 --- a/launchk/src/tui/table/table_headers.rs +++ b/launchk/src/tui/table/table_headers.rs @@ -1,62 +1,42 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::sync::mpsc::Receiver; use std::sync::Arc; use cursive::theme::{BaseColor, Color, Effect, Style}; use cursive::{Printer, View, XY}; +use super::column_sizer::ColumnSizer; + +/// Draw column headers from their names + a column sizer pub struct TableHeaders { columns: Vec, - user_col_sizes: Arc<(HashMap, usize)>, - dynamic_col_sz_rx: Receiver<(usize, usize)>, - dynamic_cols_sz: RefCell<(usize, usize)>, + column_sizer: Arc, } impl TableHeaders { pub fn new>( columns: impl Iterator, - user_col_sizes: Arc<(HashMap, usize)>, - dynamic_col_sz_rx: Receiver<(usize, usize)>, + column_sizer: Arc, ) -> Self { Self { columns: columns.map(|f| f.into()).collect(), - dynamic_cols_sz: RefCell::new((0, 0)), - user_col_sizes, - dynamic_col_sz_rx, + column_sizer, } } } impl View for TableHeaders { fn draw(&self, printer: &Printer<'_, '_>) { - if let Ok(dcs) = self.dynamic_col_sz_rx.try_recv() { - self.dynamic_cols_sz.replace(dcs); - } - let bold = Style::from(Color::Dark(BaseColor::Blue)).combine(Effect::Bold); - let (dyn_max, padding) = *self.dynamic_cols_sz.borrow(); - if dyn_max < 1 { - return; - } - - let (ucs, _) = &*self.user_col_sizes; - let headers: String = self .columns .iter() .enumerate() .map(|(i, column)| { - let width = ucs.get(&i).map(|s| s.clone()).unwrap_or(dyn_max); - - let pad = if ucs.contains_key(&i) { - width + padding - } else { - width - }; - - format!("{:pad$}", column, pad = pad) + format!( + "{:with_padding$}", + column, + with_padding = self.column_sizer.width_for_index(i) + ) }) .collect::>() .join(""); diff --git a/launchk/src/tui/table/table_list_view.rs b/launchk/src/tui/table/table_list_view.rs index 5c262d1..cf901c5 100644 --- a/launchk/src/tui/table/table_list_view.rs +++ b/launchk/src/tui/table/table_list_view.rs @@ -1,70 +1,47 @@ -use std::cell::RefCell; -use std::collections::HashMap; use std::marker::PhantomData; use std::rc::Rc; -use std::sync::mpsc::{channel, Receiver, Sender}; + use std::sync::Arc; use cursive::event::{Event, EventResult}; use cursive::traits::{Resizable, Scrollable}; use cursive::view::ViewWrapper; use cursive::views::{LinearLayout, ResizedView, ScrollView, SelectView}; -use cursive::{Vec2, View, XY}; +use cursive::{Vec2, View}; use crate::tui::table::table_headers::TableHeaders; +use super::column_sizer::ColumnSizer; pub trait TableListItem { fn as_row(&self) -> Vec; } +/// A "table" implemented on top of SelectView where we +/// divvy up x into columns pub struct TableListView { + column_sizer: Arc, linear_layout: LinearLayout, - last_layout_size: RefCell>, - num_columns: usize, - // User override cols, total size - user_col_sizes: Arc<(HashMap, usize)>, - // Precompute dynamic sizes once on replace - // (Max dynamic col size, Padding between columns) - dynamic_cols_sz: RefCell<(usize, usize)>, - // Share it - dynamic_cols_sz_tx: Sender<(usize, usize)>, - // Don't swallow type, presumably needed later + // LinearLayout swallows T from , but we still need it inner: PhantomData, } impl TableListView { - fn build_user_col_sizes( - columns: &Vec<(&str, Option)>, - ) -> Arc<(HashMap, usize)> { - let mut user_col_sizes: HashMap = HashMap::new(); - let mut user_col_size_total: usize = 0; - - for (i, (_, sz)) in columns.iter().enumerate() { - if sz.is_none() { - continue; - } - let sz = sz.unwrap(); - user_col_size_total += sz; - user_col_sizes.insert(i, sz); - } - - Arc::new((user_col_sizes, user_col_size_total)) - } - - pub fn new(columns: Vec<(&str, Option)>) -> Self { - let (dynamic_cols_sz_tx, rx): (Sender<(usize, usize)>, Receiver<(usize, usize)>) = - channel(); - let user_col_sizes = Self::build_user_col_sizes(&columns); + pub fn new(columns: I) -> TableListView + where + I: IntoIterator)> + Clone, + K: AsRef, + { + let column_names = columns + .clone() + .into_iter() + .map(|(n, _)| n.as_ref().to_string()); + let column_sizer = ColumnSizer::new(columns); let mut linear_layout = LinearLayout::vertical(); linear_layout.add_child( - TableHeaders::new( - columns.iter().map(|(n, _)| n.to_string()), - user_col_sizes.clone(), - rx, - ) - .full_width() - .max_height(1), + TableHeaders::new(column_names, column_sizer.clone()) + .full_width() + .max_height(1), ); linear_layout.add_child( SelectView::::new() @@ -72,14 +49,9 @@ impl TableListView { .full_height() .scrollable(), ); - Self { linear_layout, - user_col_sizes, - dynamic_cols_sz_tx, - dynamic_cols_sz: RefCell::new((0, 0)), - last_layout_size: RefCell::new(XY::new(0, 0)), - num_columns: *&columns.len(), + column_sizer, inner: PhantomData::default(), } } @@ -88,35 +60,19 @@ impl TableListView { where I: IntoIterator, { - // self.compute_sizes(); - - let (dyn_max, padding) = *self.dynamic_cols_sz.borrow(); - let (user_col_sizes, _) = &*self.user_col_sizes; - let rows: Vec<(String, T)> = items .into_iter() .map(|item: T| { let presented: Vec = item .as_row() .iter() - .take(self.num_columns) + .take(self.column_sizer.num_columns) .enumerate() .map(|(i, field)| { + let wfi = self.column_sizer.width_for_index(i); let mut truncated = field.clone(); - let field_width = user_col_sizes - .get(&i) - .clone() - .map(|s| s.clone()) - .unwrap_or(dyn_max.clone()); - - let pad = if user_col_sizes.contains_key(&i) { - field_width + padding - } else { - field_width - }; - - truncated.truncate(field_width - 1); - format!("{:pad$}", truncated, pad = pad) + truncated.truncate(wfi - 1); + format!("{:with_padding$}", truncated, with_padding = wfi) }) .collect(); @@ -136,36 +92,6 @@ impl TableListView { self.get_selectview().selection() } - /// "Responsive" - fn compute_sizes(&mut self) { - let (user_col_sizes, user_col_sizes_total) = &*self.user_col_sizes; - - let num_dynamic = self.num_columns - user_col_sizes.len(); - - // All sizes are static - if num_dynamic < 1 { - return; - } - - let remaining = self.last_layout_size.borrow().x - user_col_sizes_total; - let mut per_dynamic_col = remaining / num_dynamic; - - // Max col sz = 35 - if per_dynamic_col > 35 { - per_dynamic_col = 35; - } - - // After user col reservations, remove dyn cols, and distribute that space btw - // the user provided column sizes. - let remain_padding = - (remaining - (per_dynamic_col * num_dynamic)) / (self.num_columns - num_dynamic); - self.dynamic_cols_sz - .replace((per_dynamic_col, remain_padding)); - self.dynamic_cols_sz_tx - .send((per_dynamic_col, remain_padding)) - .expect("Must update dynamic cols"); - } - /// Get the index of the SelectView and unwrap it out of /// ScrollView>>> fn get_mut_selectview(&mut self) -> &mut SelectView { @@ -208,8 +134,7 @@ impl ViewWrapper for TableListView { } fn wrap_layout(&mut self, size: Vec2) { - self.last_layout_size.replace(size); + self.column_sizer.update_x(size.x); self.linear_layout.layout(size); - self.compute_sizes(); } } diff --git a/xpc-sys/Cargo.toml b/xpc-sys/Cargo.toml index fe94dd8..c524301 100644 --- a/xpc-sys/Cargo.toml +++ b/xpc-sys/Cargo.toml @@ -1,16 +1,24 @@ [package] name = "xpc-sys" +description = "Conveniently call routines with wrappers for xpc_pipe_routine() and go from Rust types to XPC objects and back!" version = "0.1.0" authors = ["David Stancu "] +license = "MIT" edition = "2018" +keywords = ["apple", "xpc", "xpc-dictionary"] +categories = ["external-ffi-bindings", "os::macos-apis"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[package.metadata.docs.rs] +# This sets the default target to `x86_64-unknown-linux-gnu` +# and only builds that target +targets = ["x86_64-apple-darwin"] [dependencies] block = "0.1.6" lazy_static = "1.4.0" log = "0.4.14" bitflags = "1.2.1" +libc = "0.2.94" [build-dependencies] bindgen = "0.53.1" diff --git a/xpc-sys/README.md b/xpc-sys/README.md new file mode 100644 index 0000000..c44e163 --- /dev/null +++ b/xpc-sys/README.md @@ -0,0 +1,232 @@ +# xpc-sys + +[![Rust](https://github.com/mach-kernel/launchk/actions/workflows/rust.yml/badge.svg?branch=master)](https://github.com/mach-kernel/launchk/actions/workflows/rust.yml) ![crates.io](https://img.shields.io/crates/v/xpc-sys.svg) + +Various utilities for conveniently dealing with XPC in Rust. + +- [Object lifecycle](#object-lifecycle) +- [QueryBuilder](#query-builder) +- [XPC Dictionary](#xpc-dictionary) +- [XPC Array](#xpc-array) +- [XPC Shmem](#xpc-shmem) + +#### Getting Started + +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. Complex types such as arrays and shared memory objects described in greater detail below. + +| 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 | +| std::os::unix::prelude::RawFd | _xpc_type_fd | +| (MachPortType::Send, mach_port_t) | _xpc_type_mach_send | +| (MachPortType::Recv, mach_port_t) | _xpc_type_mach_recv | +| XPCShmem | _xpc_type_shmem | + +Make XPC objects for anything with `From`. Make sure to use the correct type for file descriptors and Mach ports: +```rust +let mut message: HashMap<&str, XPCObject> = HashMap::new(); + +message.insert( + "domain-port", + XPCObject::from((MachPortType::Send, 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()) + ); +} +``` + +[Top](#xpc-sys) + +#### 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). + +**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. + +[Top](#xpc-sys) + +#### 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. + +[Top](#xpc-sys) + +#### XPC Dictionary + +Go from a `HashMap` to `xpc_object_t` with the `XPCObject` type: + +```rust +let mut message: HashMap<&str, XPCObject> = HashMap::new(); +message.insert("type", XPCObject::from(1 as u64)); +message.insert("handle", XPCObject::from(0 as u64)); +message.insert("subsystem", XPCObject::from(3 as u64)); +message.insert("routine", XPCObject::from(815 as u64)); +message.insert("legacy", XPCObject::from(true)); + +let xpc_object: XPCObject = message.into(); +``` + +Call `xpc_pipe_routine` and receive `Result`: + +```rust +let xpc_object: XPCObject = message.into(); + +match xpc_object.pipe_routine() { + Ok(xpc_object) => { /* do stuff and things */ }, + Err(XPCError::PipeError(err)) => { /* err is a string w/strerror(errno) */ } +} +``` + +The response is likely an XPC dictionary -- go back to a HashMap: + +```rust +let xpc_object: XPCObject = message.into(); +let response: Result = xpc_object + .pipe_routine() + .and_then(|r| r.try_into()); + +let XPCDictionary(hm) = response.unwrap(); +let whatever = hm.get("..."); +``` + +Response dictionaries can be nested, so `XPCDictionary` has a helper included for this scenario: + +```rust +let xpc_object: XPCObject = message.into(); + +// A string: either "Aqua", "StandardIO", "Background", "LoginWindow", "System" +let response: Result = xpc_object + .pipe_routine() + .and_then(|r: XPCObject| r.try_into()); + .and_then(|d: XPCDictionary| d.get(&["service", "LimitLoadToSessionType"]) + .and_then(|lltst: XPCObject| lltst.xpc_value()); +``` + +Or, retrieve the `service` key (a child XPC Dictionary) from this response: + +```rust +let xpc_object: XPCObject = message.into(); + +// A string: either "Aqua", "StandardIO", "Background", "LoginWindow", "System" +let response: Result = xpc_object + .pipe_routine() + .and_then(|r: XPCObject| r.try_into()); + .and_then(|d: XPCDictionary| d.get_as_dictionary(&["service"]); + +let XPCDictionary(hm) = response.unwrap(); +let whatever = hm.get("..."); +``` + +[Top](#xpc-sys) + +#### XPC Array + +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"]); +``` + +Go back to `Vec` using `xpc_value`: + +```rust +let rs_vec: Vec = xpc_array.xpc_value().unwrap(); +``` + +[Top](#xpc-sys) + +#### XPC Shmem + +Make XPC shared memory objects by providing a size and vm_allocate/mmap flags. [`vm_allocate`](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/ManagingMemory/Articles/MemoryAlloc.html) is used under the hood: + +```rust +let shmem = XPCShmem::new_task_self( + 0x1400000, + i32::try_from(MAP_SHARED).expect("Must conv flags"), +)?; + +// Use as _xpc_type_shmem argument in XPCDictionary +let response = XPCDictionary::new() + .extend(&DUMPSTATE) + .entry("shmem", shmem.xpc_object.clone()) + .pipe_routine_with_error_handling()?; +``` + +To work with the shmem region, use [`slice_from_raw_parts`](https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html): + +```rust +let bytes: &[u8] = unsafe { + &*slice_from_raw_parts(shmem.region as *mut u8, size) +}; + +// Make a string from bytes in the shmem +let mut hey_look_a_string = String::new(); +bytes.read_to_string(buf); +``` + +[Top](#xpc-sys) + +### 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/) +- [libc](https://crates.io/crates/libc) +- [lazy_static](https://crates.io/crates/lazy_static) +- [xcrun](https://crates.io/crates/xcrun) +- [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/xpc-sys/src/lib.rs b/xpc-sys/src/lib.rs index 128a4d0..101d8de 100644 --- a/xpc-sys/src/lib.rs +++ b/xpc-sys/src/lib.rs @@ -41,7 +41,14 @@ extern "C" { pub fn xpc_pipe_routine(pipe: xpc_pipe_t, msg: xpc_object_t, reply: *mut xpc_object_t) -> c_int; + // https://grep.app/search?q=_xpc_type_mach_.%2A®exp=true pub fn xpc_mach_send_create(port: mach_port_t) -> xpc_object_t; + pub fn xpc_mach_recv_create(port: mach_port_t) -> xpc_object_t; + pub fn xpc_mach_send_get_right(object: xpc_object_t) -> mach_port_t; + + pub static _xpc_type_mach_send: _xpc_type_s; + pub static _xpc_type_mach_recv: _xpc_type_s; + pub fn xpc_dictionary_set_mach_send( object: xpc_object_t, name: *const c_char, diff --git a/xpc-sys/src/objects/mod.rs b/xpc-sys/src/objects/mod.rs index 5129a30..ca35acb 100644 --- a/xpc-sys/src/objects/mod.rs +++ b/xpc-sys/src/objects/mod.rs @@ -7,7 +7,6 @@ pub mod xpc_dictionary; /// xpc_object_t -> xpc_type_t pub mod xpc_type; -/// xpc_pipe_t -pub mod xpc_pipe; - +pub mod unix_fifo; pub mod xpc_error; +pub mod xpc_shmem; diff --git a/xpc-sys/src/objects/unix_fifo.rs b/xpc-sys/src/objects/unix_fifo.rs new file mode 100644 index 0000000..02c7f57 --- /dev/null +++ b/xpc-sys/src/objects/unix_fifo.rs @@ -0,0 +1,60 @@ +use libc::{mkfifo, mode_t, open, tmpnam, O_RDONLY, O_WRONLY}; +use std::os::unix::prelude::RawFd; +use std::{ + ffi::{CStr, CString}, + fs::{remove_file, File}, + io::Read, + os::unix::prelude::FromRawFd, + ptr::null_mut, +}; + +use crate::rs_strerror; + +/// A simple wrapper around a UNIX FIFO +pub struct UnixFifo(pub CString); + +impl UnixFifo { + /// Create a new FIFO, make sure mode_t is 0oXXX! + pub fn new(mode: mode_t) -> Result { + let fifo_name = unsafe { CStr::from_ptr(tmpnam(null_mut())) }; + let err = unsafe { mkfifo(fifo_name.as_ptr(), mode) }; + + if err == 0 { + Ok(UnixFifo(fifo_name.to_owned())) + } else { + Err(rs_strerror(err)) + } + } + + /// Open the FIFO as O_RDONLY, read until EOF, clean up fd before returning the buffer. + pub fn block_and_read_bytes(&self) -> Vec { + let Self(fifo_name) = self; + + let fifo_fd_read = unsafe { open(fifo_name.as_ptr(), O_RDONLY) }; + let mut file = unsafe { File::from_raw_fd(fifo_fd_read) }; + + let mut buf: Vec = Vec::new(); + file.read_to_end(&mut buf).expect("Must read bytes"); + + unsafe { libc::close(fifo_fd_read) }; + + buf + } + + /// Open O_WRONLY, pass to fn and clean up before returning. + pub fn with_writer(&self, f: impl Fn(RawFd) -> T) -> T { + let Self(fifo_name) = self; + let fifo_fd_write = unsafe { open(fifo_name.as_ptr(), O_WRONLY) }; + let response = f(fifo_fd_write); + unsafe { libc::close(fifo_fd_write) }; + response + } +} + +impl Drop for UnixFifo { + fn drop(&mut self) { + let Self(fifo_name) = self; + + remove_file(&fifo_name.to_string_lossy().to_string()).expect("Must tear down FIFO"); + } +} diff --git a/xpc-sys/src/objects/xpc_dictionary.rs b/xpc-sys/src/objects/xpc_dictionary.rs index da90531..caa0733 100644 --- a/xpc-sys/src/objects/xpc_dictionary.rs +++ b/xpc-sys/src/objects/xpc_dictionary.rs @@ -4,13 +4,16 @@ use std::convert::{TryFrom, TryInto}; use std::ffi::{CStr, CString}; use std::os::raw::c_char; use std::ptr::{null, null_mut}; -use std::rc::Rc; +use std::sync::Arc; use crate::objects::xpc_error::XPCError; use crate::objects::xpc_error::XPCError::DictionaryError; use crate::objects::xpc_object::XPCObject; +use crate::rs_strerror; +use crate::{ + errno, xpc_dictionary_apply, xpc_dictionary_create, xpc_dictionary_set_value, xpc_object_t, +}; use crate::{objects, xpc_retain}; -use crate::{xpc_dictionary_apply, xpc_dictionary_create, xpc_dictionary_set_value, xpc_object_t}; use block::ConcreteBlock; @@ -82,31 +85,36 @@ impl TryFrom<&XPCObject> for XPCDictionary { )); } - let map: Rc>> = Rc::new(RefCell::new(HashMap::new())); - let map_rc_clone = map.clone(); + let map: Arc>> = Arc::new(RefCell::new(HashMap::new())); + let map_block_clone = map.clone(); + // https://developer.apple.com/documentation/xpc/1505404-xpc_dictionary_apply?language=objc 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()); + map_block_clone.borrow_mut().insert(str_key, value.into()); + + // Must return true + true }); 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); + drop(block); if ok { - match Rc::try_unwrap(map) { + match Arc::try_unwrap(map) { Ok(cell) => Ok(XPCDictionary(cell.into_inner())), - Err(_) => Err(DictionaryError("Unable to unwrap Rc".to_string())), + Err(_) => Err(DictionaryError("Unable to unwrap Arc".to_string())), } } else { - Err(DictionaryError("xpc_dictionary_apply failed".to_string())) + Err(DictionaryError(format!( + "xpc_dictionary_apply failed: {}", + rs_strerror(unsafe { errno }) + ))) } } } @@ -136,7 +144,7 @@ where { /// Creates a XPC dictionary /// - /// Values must be XPCObject newtype but can encapsulate any + /// Values must be XPCObject but can encapsulate any /// valid xpc_object_t fn from(message: HashMap) -> Self { let dict = unsafe { xpc_dictionary_create(null(), null_mut(), 0) }; diff --git a/xpc-sys/src/objects/xpc_error.rs b/xpc-sys/src/objects/xpc_error.rs index f61b4e0..b15b7de 100644 --- a/xpc-sys/src/objects/xpc_error.rs +++ b/xpc-sys/src/objects/xpc_error.rs @@ -1,4 +1,6 @@ -use crate::objects::xpc_error::XPCError::{DictionaryError, PipeError, QueryError}; +use crate::objects::xpc_error::XPCError::{ + DictionaryError, IOError, PipeError, QueryError, ValueError, +}; use std::error::Error; use std::fmt::{Display, Formatter}; @@ -8,6 +10,7 @@ pub enum XPCError { PipeError(String), ValueError(String), QueryError(String), + IOError(String), StandardError, NotFound, } @@ -18,6 +21,8 @@ impl Display for XPCError { DictionaryError(e) => e, PipeError(e) => e, QueryError(e) => e, + ValueError(e) => e, + IOError(e) => e, _ => "", }; diff --git a/xpc-sys/src/objects/xpc_object.rs b/xpc-sys/src/objects/xpc_object.rs index 18afed6..0039e61 100644 --- a/xpc-sys/src/objects/xpc_object.rs +++ b/xpc-sys/src/objects/xpc_object.rs @@ -1,10 +1,11 @@ use crate::objects::xpc_type::XPCType; use crate::{ mach_port_t, xpc_array_append_value, xpc_array_create, xpc_bool_create, xpc_copy_description, - xpc_double_create, xpc_int64_create, xpc_mach_send_create, xpc_object_t, xpc_release, - xpc_string_create, xpc_uint64_create, + xpc_double_create, xpc_fd_create, xpc_int64_create, xpc_mach_recv_create, xpc_mach_send_create, + xpc_object_t, xpc_release, xpc_string_create, xpc_uint64_create, }; use std::ffi::{CStr, CString}; +use std::os::unix::prelude::RawFd; use std::ptr::null_mut; use std::sync::Arc; @@ -40,7 +41,8 @@ impl Default for XPCObject { } impl fmt::Display for XPCObject { - /// Use xpc_copy_description to get an easy snapshot of a dictionary + /// Use xpc_copy_description to show as a string, for + /// _xpc_type_dictionary contents are shown! fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let XPCObject(arc, _) = self; @@ -81,10 +83,23 @@ impl From for XPCObject { } } -impl From for XPCObject { - /// Create XPCObject via xpc_uint64_create - fn from(value: mach_port_t) -> Self { - unsafe { XPCObject::new(xpc_mach_send_create(value)) } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum MachPortType { + Send, + Recv, +} + +impl From<(MachPortType, mach_port_t)> for XPCObject { + /// Create XPCObject via xpc_mach_send_create or xpc_mach_recv_create + fn from((mpt, value): (MachPortType, mach_port_t)) -> Self { + let xpc_object = unsafe { + match mpt { + MachPortType::Send => xpc_mach_send_create(value), + MachPortType::Recv => xpc_mach_recv_create(value), + } + }; + + XPCObject::new(xpc_object) } } @@ -104,6 +119,7 @@ impl From<&str> for XPCObject { } impl> From> for XPCObject { + /// Create XPCObject via xpc_array_create fn from(value: Vec) -> Self { let xpc_array = unsafe { xpc_array_create(null_mut(), 0) }; for object in value { @@ -130,6 +146,23 @@ impl From for XPCObject { } } +impl> From for XPCObject { + /// Create XPCObject from another ref + fn from(other: R) -> Self { + let other_ref = other.as_ref(); + let XPCObject(ref arc, ref xpc_type) = other_ref; + XPCObject(arc.clone(), xpc_type.clone()) + } +} + +impl From for XPCObject { + /// Use std::os::unix::prelude type for xpc_fd_create + fn from(value: RawFd) -> Self { + log::info!("Making FD from {}", value); + unsafe { XPCObject::new(xpc_fd_create(value)) } + } +} + /// 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) diff --git a/xpc-sys/src/objects/xpc_pipe.rs b/xpc-sys/src/objects/xpc_pipe.rs deleted file mode 100644 index 66ff44f..0000000 --- a/xpc-sys/src/objects/xpc_pipe.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::xpc_pipe_t; - -#[repr(transparent)] -#[derive(Clone, PartialEq, Eq)] -pub struct XPCPipe(pub xpc_pipe_t); - -unsafe impl Send for XPCPipe {} -unsafe impl Sync for XPCPipe {} diff --git a/xpc-sys/src/objects/xpc_shmem.rs b/xpc-sys/src/objects/xpc_shmem.rs new file mode 100644 index 0000000..bc8b716 --- /dev/null +++ b/xpc-sys/src/objects/xpc_shmem.rs @@ -0,0 +1,63 @@ +use crate::objects::xpc_error::XPCError; +use crate::objects::xpc_object::XPCObject; +use crate::{ + mach_port_t, mach_task_self_, rs_strerror, vm_address_t, vm_allocate, vm_deallocate, vm_size_t, + xpc_shmem_create, +}; +use std::ffi::c_void; +use std::os::raw::c_int; +use std::ptr::null_mut; + +/// Wrapper around vm_allocate() vm_deallocate() with an XPCObject +/// member of XPC type _xpc_type_shmem +#[derive(Debug, Clone)] +pub struct XPCShmem { + pub task: mach_port_t, + pub size: vm_size_t, + pub region: *mut c_void, + pub xpc_object: XPCObject, +} + +unsafe impl Send for XPCShmem {} + +impl XPCShmem { + pub fn new(task: mach_port_t, size: vm_size_t, flags: c_int) -> Result { + let mut region: *mut c_void = null_mut(); + let err = unsafe { + vm_allocate( + task, + &mut region as *const _ as *mut vm_address_t, + size, + flags, + ) + }; + + if err > 0 { + Err(XPCError::IOError(rs_strerror(err))) + } else { + Ok(XPCShmem { + task, + size, + region, + xpc_object: unsafe { xpc_shmem_create(region as *mut c_void, size as u64).into() }, + }) + } + } + + pub fn new_task_self(size: vm_size_t, flags: c_int) -> Result { + unsafe { Self::new(mach_task_self_, size, flags) } + } +} + +impl Drop for XPCShmem { + fn drop(&mut self) { + let XPCShmem { + size, task, region, .. + } = self; + if *region == null_mut() { + return; + } + + unsafe { vm_deallocate(*task, *region as vm_address_t, *size) }; + } +} diff --git a/xpc-sys/src/objects/xpc_type.rs b/xpc-sys/src/objects/xpc_type.rs index 18ecbd1..f8ce1b2 100644 --- a/xpc-sys/src/objects/xpc_type.rs +++ b/xpc-sys/src/objects/xpc_type.rs @@ -1,7 +1,7 @@ use crate::{ - _xpc_type_array, _xpc_type_bool, _xpc_type_dictionary, _xpc_type_double, _xpc_type_int64, - _xpc_type_s, _xpc_type_string, _xpc_type_uint64, xpc_get_type, xpc_object_t, xpc_type_get_name, - xpc_type_t, + _xpc_type_array, _xpc_type_bool, _xpc_type_dictionary, _xpc_type_double, _xpc_type_fd, + _xpc_type_int64, _xpc_type_mach_recv, _xpc_type_mach_send, _xpc_type_s, _xpc_type_shmem, + _xpc_type_string, _xpc_type_uint64, xpc_get_type, xpc_object_t, xpc_type_get_name, xpc_type_t, }; use crate::objects::xpc_error::XPCError; @@ -63,6 +63,12 @@ lazy_static! { pub static ref String: XPCType = unsafe { (&_xpc_type_string as *const _xpc_type_s).into() }; pub static ref Bool: XPCType = unsafe { (&_xpc_type_bool as *const _xpc_type_s).into() }; pub static ref Array: XPCType = unsafe { (&_xpc_type_array as *const _xpc_type_s).into() }; + pub static ref MachSend: XPCType = + unsafe { (&_xpc_type_mach_send as *const _xpc_type_s).into() }; + pub static ref MachRecv: XPCType = + unsafe { (&_xpc_type_mach_recv as *const _xpc_type_s).into() }; + pub static ref Fd: XPCType = unsafe { (&_xpc_type_fd as *const _xpc_type_s).into() }; + pub static ref Shmem: XPCType = unsafe { (&_xpc_type_shmem as *const _xpc_type_s).into() }; } /// Runtime type check for XPC object. I do not know if possible/advantageous to represent diff --git a/xpc-sys/src/traits/xpc_value.rs b/xpc-sys/src/traits/xpc_value.rs index 5d60d99..5b1257a 100644 --- a/xpc-sys/src/traits/xpc_value.rs +++ b/xpc-sys/src/traits/xpc_value.rs @@ -3,11 +3,12 @@ use std::cell::RefCell; use std::ffi::CStr; use std::rc::Rc; -use crate::objects::xpc_object::XPCObject; +use crate::objects::xpc_object::{MachPortType, XPCObject}; use crate::objects::xpc_type; use crate::{ - size_t, xpc_array_apply, xpc_bool_get_value, xpc_int64_get_value, xpc_object_t, xpc_retain, - xpc_string_get_string_ptr, xpc_uint64_get_value, + mach_port_t, size_t, xpc_array_apply, xpc_bool_get_value, xpc_double_get_value, + xpc_int64_get_value, xpc_mach_send_get_right, xpc_object_t, xpc_retain, + xpc_string_get_string_ptr, xpc_type_get_name, xpc_uint64_get_value, }; use crate::objects::xpc_error::XPCError; @@ -36,6 +37,14 @@ impl TryXPCValue for XPCObject { } } +impl TryXPCValue for XPCObject { + fn xpc_value(&self) -> Result { + check_xpc_type(&self, &xpc_type::Double)?; + let XPCObject(obj_pointer, _) = self; + Ok(unsafe { xpc_double_get_value(**obj_pointer) }) + } +} + impl TryXPCValue for XPCObject { fn xpc_value(&self) -> Result { check_xpc_type(&self, &xpc_type::String)?; @@ -54,6 +63,30 @@ impl TryXPCValue for XPCObject { } } +impl TryXPCValue<(MachPortType, mach_port_t)> for XPCObject { + fn xpc_value(&self) -> Result<(MachPortType, mach_port_t), XPCError> { + let XPCObject(obj_pointer, xpc_type) = self; + + let types = [ + check_xpc_type(&self, &xpc_type::MachSend).map(|()| MachPortType::Send), + check_xpc_type(&self, &xpc_type::MachRecv).map(|()| MachPortType::Recv), + ]; + + for check in &types { + if check.is_ok() { + return Ok((*check.as_ref().unwrap(), unsafe { + xpc_mach_send_get_right(**obj_pointer) + })); + } + } + + Err(XPCError::ValueError(format!( + "Object is {} and neither _xpc_type_mach_send nor _xpc_type_mach_recv", + unsafe { CStr::from_ptr(xpc_type_get_name(xpc_type.0)).to_string_lossy() } + ))) + } +} + impl TryXPCValue> for XPCObject { fn xpc_value(&self) -> Result, XPCError> { check_xpc_type(&self, &xpc_type::Array)?; @@ -86,8 +119,11 @@ impl TryXPCValue> for XPCObject { #[cfg(test)] mod tests { + use crate::get_bootstrap_port; + use crate::mach_port_t; use crate::objects::xpc_error::XPCError; use crate::objects::xpc_error::XPCError::ValueError; + use crate::objects::xpc_object::MachPortType; use crate::objects::xpc_object::XPCObject; use crate::traits::xpc_value::TryXPCValue; @@ -131,6 +167,34 @@ mod tests { assert_eq!(std::u64::MAX, rs_u64); } + #[test] + fn xpc_value_f64() { + let xpc_f64 = XPCObject::from(std::f64::MAX); + let rs_f64: f64 = xpc_f64.xpc_value().unwrap(); + assert_eq!(std::f64::MAX, rs_f64); + } + + #[test] + fn xpc_value_mach_send() { + let xpc_bootstrap_port = + XPCObject::from((MachPortType::Send, get_bootstrap_port() as mach_port_t)); + let (mpt, port): (MachPortType, mach_port_t) = xpc_bootstrap_port.xpc_value().unwrap(); + + assert_eq!(MachPortType::Send, mpt); + assert_eq!(get_bootstrap_port(), port); + } + + // Can't find any example in the wild, the value is 0 vs the provided 42, it likely + // does some kind of validation. + #[test] + fn xpc_value_mach_recv() { + let xpc_mach_recv = XPCObject::from((MachPortType::Recv, 42 as mach_port_t)); + let (mpt, _port): (MachPortType, mach_port_t) = xpc_mach_recv.xpc_value().unwrap(); + + assert_eq!(MachPortType::Recv, mpt); + // assert_eq!(42, port); + } + #[test] fn xpc_value_array() { let xpc_array = XPCObject::from(vec!["eins", "zwei", "polizei"]);