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"]);