mirror of
https://github.com/MobAI-App/simslim.git
synced 2026-11-02 23:11:05 +01:00
* feat: support Apple TV and Apple Watch simulator slimming * feat: support Apple Vision Pro simulator slimming and speed up no-reboot slims (#56) * perf: stop a no-reboot slim's daemons with one launchctl spawn `on --no-reboot` runs a `launchctl disable` and a `launchctl bootout` spawn per label, 340 spawns for the full 170-label profile. A spawn is cheap on a quiet Mac but can take seconds on a host busy with other simulators and builds, and a live slim then needs minutes. `launchctl unload -w` takes many job plists at once: it writes the same persistent disable override as `disable` and removes the loaded job like `bootout`. Simulator processes see the host file system, so the plists under the runtime's RuntimeRoot can be passed by path, in chunks of 64. `Device` now carries its `RuntimeID` to find that RuntimeRoot. A plist is only used when its own `Label` key matches the label, so a misnamed file can never unload a daemon outside the allowlist. The state is read back afterwards (`print-disabled` and `launchctl list`), and anything still enabled or loaded takes the existing per-label path. A chunk that outlasts its spawn timeout ends the batch, so a slow host keeps its remaining budget for that path. * feat: support Apple Vision Pro simulator slimming Builds on the Apple TV and Apple Watch support: CoreSimulator names the visionOS runtime `xrOS`, so `parseRuntime` now maps it to the `visionOS` platform, and every command, the JSON output, and the macOS app pick it up through the existing platform plumbing. **visionOS keeps `com.apple.mobileassetd` enabled.** The simulated surroundings and the Home View are mobile assets. With mobileassetd disabled a visionOS 27.0 simulator boots to a black space with no Home View, while apps still launch, so a boot check alone does not notice it. `desiredForDevice` skips it like homed on watchOS, so slimming, verification, status totals, and the app's count agree, and a device slimmed before this exception gets the service back on its next `on`. **Persistence gate.** visionOS 2.5 is the release generation of iOS 18.5, following the tvOS and watchOS cutoffs. Like those, it is not an independent measurement; every platform is still read back after boot. * fix: reuse the resolved device for batch unloads and note the visionOS no-reboot cutoff EnableSlimNoReboot already resolves the device, so unloadInBatch takes its RuntimeID instead of running a second simctl list. listDevicesInSet sets RuntimeID in its main loop, and the --no-reboot help names visionOS < 2.5. --------- Co-authored-by: Cihat Gündüz <github@cihatguenduez.de>
101 lines
3.2 KiB
Go
101 lines
3.2 KiB
Go
package simslim
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// launchdJobDirs are the RuntimeRoot directories holding the job plists that
|
|
// launchd_sim bootstraps into the simulator's system domain.
|
|
var launchdJobDirs = []string{"System/Library/LaunchDaemons", "System/Library/LaunchAgents"}
|
|
|
|
// unloadChunk bounds how many plist paths go into one `launchctl unload`, so
|
|
// the argument list stays far below ARG_MAX however long the profile grows.
|
|
const unloadChunk = 64
|
|
|
|
func parseRuntimeRoot(out []byte, runtimeID string) (string, error) {
|
|
var parsed struct {
|
|
Runtimes []struct {
|
|
Identifier string `json:"identifier"`
|
|
RuntimeRoot string `json:"runtimeRoot"`
|
|
} `json:"runtimes"`
|
|
}
|
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
|
return "", fmt.Errorf("parse simctl list runtimes: %w", err)
|
|
}
|
|
for _, r := range parsed.Runtimes {
|
|
if r.Identifier == runtimeID && r.RuntimeRoot != "" {
|
|
return r.RuntimeRoot, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("runtime %s has no runtimeRoot", runtimeID)
|
|
}
|
|
|
|
// launchdJobPlists maps each label to the job plist that defines exactly that
|
|
// label. A plist is only used when its own Label key matches, so a renamed or
|
|
// shared file can never unload a daemon outside the allowlist; labels without
|
|
// a matching plist are left for the per-label path.
|
|
func launchdJobPlists(ctx context.Context, root string, labels []string, plistLabel func(context.Context, string) (string, error)) map[string]string {
|
|
paths := make(map[string]string, len(labels))
|
|
for _, label := range labels {
|
|
if ctx.Err() != nil {
|
|
break
|
|
}
|
|
for _, dir := range launchdJobDirs {
|
|
path := filepath.Join(root, dir, label+".plist")
|
|
if _, err := os.Stat(path); err != nil {
|
|
continue
|
|
}
|
|
if got, err := plistLabel(ctx, path); err == nil && got == label {
|
|
paths[label] = path
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return paths
|
|
}
|
|
|
|
// readPlistLabel reads a job plist's Label with the host's plutil, which
|
|
// handles both XML and binary plists.
|
|
func readPlistLabel(ctx context.Context, path string) (string, error) {
|
|
out, err := exec.CommandContext(ctx, "plutil", "-extract", "Label", "raw", "-o", "-", path).Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
// parseLaunchctlList reads `launchctl list` rows: "PID<TAB>Status<TAB>Label",
|
|
// with "-" as the PID of a loaded job that is not running.
|
|
func parseLaunchctlList(output string) map[string]bool {
|
|
loaded := map[string]bool{}
|
|
for _, line := range strings.Split(output, "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) != 3 || fields[2] == "Label" {
|
|
continue
|
|
}
|
|
loaded[fields[2]] = true
|
|
}
|
|
return loaded
|
|
}
|
|
|
|
// stillToStop keeps the labels the batch did not finish: no disable override
|
|
// yet, or still loaded. A few jobs stay listed even with their override in
|
|
// place (on a slim visionOS 27.0 simulator, for example siri.context.service
|
|
// and TrustedPeersHelper), so they always take the per-label bootout; the old
|
|
// path booted out every label, so this is never more work than before.
|
|
func stillToStop(labels []string, disabled, loaded map[string]bool) []string {
|
|
var rest []string
|
|
for _, l := range labels {
|
|
if !disabled[l] || loaded[l] {
|
|
rest = append(rest, l)
|
|
}
|
|
}
|
|
return rest
|
|
}
|