Skip to content

Commit

Permalink
Merge pull request #10 from mach-kernel/launchctl-enable-disable
Browse files Browse the repository at this point in the history
Enable/disable command
  • Loading branch information
mach-kernel authored May 17, 2021
2 parents 4edb194 + 89b35a1 commit 1b56ddb
Show file tree
Hide file tree
Showing 21 changed files with 1,000 additions and 404 deletions.
126 changes: 105 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,90 @@ Should work on macOS 10.10+ according to the availability sec. [in the docs](htt
#### Features

- Poll XPC for jobs and display changes as they happen
- Filter by system (/System/Library/), "global" (/Library), user (~/) `LaunchAgents` and `LaunchDaemons`
- Filter by `LaunchAgents` and `LaunchDaemons` in scopes:
- System (/System/Library/)
- Global (/Library)
- User (~/)
- fsnotify detection for new plists added to above directories
- `:load/:unload` -> `launchctl load/unload`
- `:edit` -> Open plist in `$EDITOR`, defaulting to `vim`. Supports binary plists -> shown as XML for edit, then marshalled back into binary format on save.

### xpc-sys crate

There is some "convenience glue" for dealing with XPC objects. Eventually, this will be broken out into its own crate. Most of the tests (for now) are written around not breaking data going across the FFI barrier.
There is some "convenience glue" for dealing with XPC objects. Eventually, this will be broken out into its own crate. Some tests exist for not breaking data to/from FFI.

##### Object lifecycle

XPCObject wraps `xpc_object_t` in an `Arc`. `Drop` will invoke `xpc_release()` on objects being dropped with no other [strong refs](https://doc.rust-lang.org/std/sync/struct.Arc.html#method.strong_count).

#### xpc_pipe_routine
**NOTE**: When using Objective-C blocks with the [block crate](https://crates.io/crates/block) (e.g. looping over an array), make sure to invoke `xpc_retain()` on any object you wish to keep after the closure is dropped, or else the XPC objects in the closure will be dropped as well! See the `XPCDictionary` implementation for more details. xpc-sys handles this for you for its conversions.

#### XPCDictionary and QueryBuilder

While we can go from `HashMap<&str, XPCObject>` to `XPCObject`, it can be a little verbose. A `QueryBuilder` trait exposes some builder methods to make building an XPC dictionary a little easier (without all of the `into()`s, and some additional error checking).

To write the query for `launchctl list`:

```rust
let LIST_SERVICES: XPCDictionary = XPCDictionary::new()
// "list com.apple.Spotlight" (if specified)
// .entry("name", "com.apple.Spotlight");
.entry("subsystem", 3 as u64)
.entry("handle", 0 as u64)
.entry("routine", 815 as u64)
.entry("legacy", true);

let reply: Result<XPCDictionary, XPCError> = 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<String> | _xpc_type_string |
| HashMap<Into<String>, Into<XPCObject>> | _xpc_type_dictionary |
| Vec<Into<XPCObject>> | _xpc_type_array |

Make XPC objects for anything with `From<T>`. 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<u64, XPCError> = an_i64.xpc_value();
assert_eq!(
as_u64.err().unwrap(),
XPCValueError("Cannot get int64 as uint64".to_string())
);
}
```

##### XPC Dictionaries

Form your messages as a HashMap:
Go from a `HashMap` to `xpc_object_t` with the `XPCObject` type:

```rust
let mut message: HashMap<&str, XPCObject> = HashMap::new();
Expand Down Expand Up @@ -88,27 +158,41 @@ let XPCDictionary(hm) = response.unwrap();
let whatever = hm.get("...");
```

#### Making XPC Objects
##### XPC Arrays

An XPC array can be made from either `Vec<XPCObject>` or `Vec<Into<XPCObject>>`:

Make XPC objects for anything with `From<T>`. From earlier example, even Mach ports:
```rust
let mut message: HashMap<&str, XPCObject> = HashMap::new();
let xpc_array = XPCObject::from(vec![XPCObject::from("eins"), XPCObject::from("zwei"), XPCObject::from("polizei")]);

message.insert(
"domain-port",
XPCObject::from(get_bootstrap_port() as mach_port_t),
);
let xpc_array = XPCObject::from(vec!["eins", "zwei", "polizei"]);
```

Go from an XPC object to value via `TryXPCValue`. It checks your object's type via `xpc_get_type()` and yields a clear error if you're using the wrong type:
Go back to `Vec` using `xpc_value`:

```rust
#[test]
fn deserialize_as_wrong_type() {
let an_i64: XPCObject = XPCObject::from(42 as i64);
let as_u64: Result<u64, XPCError> = an_i64.xpc_value();
assert_eq!(
as_u64.err().unwrap(),
XPCValueError("Cannot get int64 as uint64".to_string())
);
}
let rs_vec: Vec<XPCObject> = xpc_array.xpc_value().unwrap();
```

### Credits

A big thanks to these open source projects and general resources:

- [block](https://crates.io/crates/block) Obj-C block support, necessary for any XPC function taking `xpc_*_applier_t`
- [Cursive](https://github.com/gyscos/cursive) TUI
- [tokio](https://github.com/tokio-rs/tokio) ASIO
- [plist](https://crates.io/crates/plist) Parsing & validation for XML and binary plists
- [notify](https://docs.rs/notify/4.0.16/notify/) fsnotify
- [bitflags](https://docs.rs/bitflags/1.2.1/bitflags/)

- [Apple Developer XPC services](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html)
- [Apple Developer XPC API reference](https://developer.apple.com/documentation/xpc?language=objc)
- [MOXIL / launjctl](http://newosxbook.com/articles/jlaunchctl.html)
- [geosnow - A Long Evening With macOS' sandbox](https://geosn0w.github.io/A-Long-Evening-With-macOS%27s-Sandbox/)
- [Bits of launchd - @5aelo](https://saelo.github.io/presentations/bits_of_launchd.pdf)
- [Audit tokens explained (e.g. ASID)](https://knight.sc/reverse%20engineering/2020/03/20/audit-tokens-explained.html)
- [objc.io XPC guide](https://www.objc.io/issues/14-mac/xpc/)
- The various source links found in comments, from Chrome's sandbox and other headers with definitions for private API functions.
- Last but not least, this is Apple's launchd after all, right :>)? I did not know systemd was inspired by launchd until I read [this HN comment](https://news.ycombinator.com/item?id=2565780), which sent me down this eventual rabbit hole :)

Everything else (C) David Stancu & Contributors 2021
108 changes: 105 additions & 3 deletions doc/launchctl_messages.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# launchctl messages

This is a handy reference of XPC messages sent by `launchctl` for some basic commands, as presented by `xpc_copy_description`.
XPC messages sent by `launchctl` for some basic commands as presented by `xpc_copy_description`.

#### `launchctl print system`

Expand Down Expand Up @@ -177,6 +177,28 @@ type 8
strerr Domain does not support specified action
```

Response:

```
"EnableTransactions" => <bool: 0x7fff9464d490>: true
"Sockets" => <dictionary: 0x7fcc6462dc80> { count = 1, transaction: 0, voucher = 0x0, contents =
"Listeners" => <array: 0x7fcc64630370> { count = 1, capacity = 1, contents =
0: <fd: 0x7fcc6462e3c0> { type = (invalid descriptor), path = (invalid path) }
}
}
"LimitLoadToSessionType" => <string: 0x7fcc6462dd90> { length = 6, contents = "System" }
"Label" => <string: 0x7fcc6462de00> { length = 17, contents = "com.apple.usbmuxd" }
"OnDemand" => <bool: 0x7fff9464d4b0>: false
"LastExitStatus" => <int64: 0x97301c3994c391b1>: 0
"PID" => <int64: 0x97301c3994c9c1b1>: 165
"Program" => <string: 0x7fcc64631050> { length = 85, contents = "/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/usbmuxd" }
"ProgramArguments" => <array: 0x7fcc6462dea0> { count = 2, capacity = 2, contents =
0: <string: 0x7fcc6462df30> { length = 85, contents = "/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/usbmuxd" }
1: <string: 0x7fcc64630f80> { length = 8, contents = "-launchd" }
}
}
```

#### `launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.elasticsearch.plist`

```
Expand All @@ -196,8 +218,6 @@ strerr Domain does not support specified action
"domain-port" => <mach send right: 0x100304fa0> { name = 1799, right = send, urefs = 5 }
```

Type seems same even if trying from `/Library`:

```
<dictionary: 0x100604420> { count = 11, transaction: 0, voucher = 0x0, contents =
"subsystem" => <uint64: 0x45e43765185d939b>: 3
Expand Down Expand Up @@ -250,3 +270,85 @@ As root:
"domain-port" => <mach send right: 0x100604540> { name = 1799, right = send, urefs = 5 }
```

#### `launchctl disable user/501/homebrew.mxcl.postgresql`

- postgresql runs in the `Aqua` domain
- Interesting that both `name` and `names` are sent!

```
<dictionary: 0x100404340> { count = 6, transaction: 0, voucher = 0x0, contents =
"subsystem" => <uint64: 0x2d24e480c52e0073>: 3
"handle" => <uint64: 0x2d24e480c5316073>: 501
"routine" => <uint64: 0x2d24e480c51ca073>: 809
"name" => <string: 0x100404440> { length = 24, contents = "homebrew.mxcl.postgresql" }
"type" => <uint64: 0x2d24e480c52e1073>: 2
"names" => <array: 0x1004044a0> { count = 1, capacity = 8, contents =
0: <string: 0x100404560> { length = 24, contents = "homebrew.mxcl.postgresql" }
}
```


#### `launchctl enable user/501/homebrew.mxcl.postgresql`

```
<dictionary: 0x1004042b0> { count = 6, transaction: 0, voucher = 0x0, contents =
"subsystem" => <uint64: 0xd49d6ee83bb4aaf3>: 3
"handle" => <uint64: 0xd49d6ee83babcaf3>: 501
"routine" => <uint64: 0xd49d6ee83b861af3>: 808
"name" => <string: 0x1004043f0> { length = 24, contents = "homebrew.mxcl.postgresql" }
"type" => <uint64: 0xd49d6ee83bb4baf3>: 2
"names" => <array: 0x100404450> { count = 1, capacity = 8, contents =
0: <string: 0x100404520> { length = 24, contents = "homebrew.mxcl.postgresql" }
}
```

#### `launchctl disable system/com.apple.FontWorker`

- Must run as root
- Type `1`

```
<dictionary: 0x100204480> { count = 6, transaction: 0, voucher = 0x0, contents =
"subsystem" => <uint64: 0xe11a6f0157820409>: 3
"handle" => <uint64: 0xe11a6f0157823409>: 0
"routine" => <uint64: 0xe11a6f0157b0a409>: 809
"name" => <string: 0x100204640> { length = 20, contents = "com.apple.FontWorker" }
"type" => <uint64: 0xe11a6f0157822409>: 1
"names" => <array: 0x1002046a0> { count = 1, capacity = 8, contents =
0: <string: 0x100204760> { length = 20, contents = "com.apple.FontWorker" }
}
```

#### `launchctl enable system/com.apple.FontWorker`

- This has a different `LimitLoadToSessionType` set as background, wanted to see if `type` would change

```
<dictionary: 0x100404140> { count = 6, transaction: 0, voucher = 0x0, contents =
"subsystem" => <uint64: 0xf489c28b42fa66cd>: 3
"handle" => <uint64: 0xf489c28b42fa56cd>: 0
"routine" => <uint64: 0xf489c28b42c8d6cd>: 808
"name" => <string: 0x100404310> { length = 20, contents = "com.apple.FontWorker" }
"type" => <uint64: 0xf489c28b42fa46cd>: 1
"names" => <array: 0x100404370> { count = 1, capacity = 8, contents =
0: <string: 0x1004045b0> { length = 20, contents = "com.apple.FontWorker" }
}
```

Using a `gui/` domain target:

```
<dictionary: 0x100404080> { count = 6, transaction: 0, voucher = 0x0, contents =
"subsystem" => <uint64: 0x199d6ee9dd75252d>: 3
"handle" => <uint64: 0x199d6ee9dd6a452d>: 501
"routine" => <uint64: 0x199d6ee9dd47952d>: 808
"name" => <string: 0x1004042c0> { length = 17, contents = "com.docker.vmnetd" }
"type" => <uint64: 0x199d6ee9dd75952d>: 8
"names" => <array: 0x100404320> { count = 1, capacity = 8, contents =
0: <string: 0x1004043e0> { length = 17, contents = "com.docker.vmnetd" }
}
```

1: System
2: User
8: Login (GUI)?
21 changes: 15 additions & 6 deletions launchk/src/launchd/entry_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use std::convert::TryInto;
use std::sync::Mutex;
use std::time::{Duration, SystemTime};

use crate::launchd::enums::{SessionType, DomainType};
use crate::launchd::plist::LaunchdPlist;
use crate::launchd::query::{find_in_all, LimitLoadToSessionType};
use crate::launchd::query::find_in_all;
use xpc_sys::traits::xpc_value::TryXPCValue;

const ENTRY_INFO_QUERY_TTL: Duration = Duration::from_secs(15);
Expand All @@ -17,7 +18,8 @@ lazy_static! {
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct LaunchdEntryStatus {
pub plist: Option<LaunchdPlist>,
pub limit_load_to_session_type: LimitLoadToSessionType,
pub limit_load_to_session_type: SessionType,
pub domain: DomainType,
// So, there is a pid_t, but it's i32, and the XPC response has an i64?
pub pid: i64,
tick: SystemTime,
Expand All @@ -26,7 +28,8 @@ pub struct LaunchdEntryStatus {
impl Default for LaunchdEntryStatus {
fn default() -> Self {
LaunchdEntryStatus {
limit_load_to_session_type: LimitLoadToSessionType::Unknown,
limit_load_to_session_type: SessionType::Unknown,
domain: DomainType::Unknown,
plist: None,
pid: 0,
tick: SystemTime::now(),
Expand Down Expand Up @@ -63,21 +66,27 @@ fn build_entry_status<S: Into<String>>(label: S) -> LaunchdEntryStatus {
let pid: i64 = response
.as_ref()
.map_err(|e| e.clone())
.and_then(|r| r.get(&["service", "PID"]))
.and_then(|(_, r)| r.get(&["service", "PID"]))
.and_then(|o| o.xpc_value())
.unwrap_or(0);

let limit_load_to_session_type = response
.as_ref()
.map_err(|e| e.clone())
.and_then(|r| r.get(&["service", "LimitLoadToSessionType"]))
.and_then(|(_, r)| r.get(&["service", "LimitLoadToSessionType"]))
.and_then(|o| o.try_into())
.unwrap_or(LimitLoadToSessionType::Unknown);
.unwrap_or(SessionType::Unknown);

let domain = response
.as_ref()
.map(|(d, _)| d.clone())
.unwrap_or(DomainType::Unknown);

let entry_config = crate::launchd::plist::for_label(label_string.clone());

LaunchdEntryStatus {
limit_load_to_session_type,
domain,
plist: entry_config,
pid,
tick: SystemTime::now(),
Expand Down
Loading

0 comments on commit 1b56ddb

Please sign in to comment.