diff --git a/Changelogs/v2.1.0.md b/Changelogs/v2.1.0.md new file mode 100644 index 00000000..6a0fcccb --- /dev/null +++ b/Changelogs/v2.1.0.md @@ -0,0 +1,388 @@ +# v2.1.0 + +The first stable release on the **2.1** line, built up over nine public betas +and three release candidates and consolidated here. If you have been running +the stable channel since v2.0.1, everything below is new to you. + +Headline work spans runtime inspection, background indexing, multi-tab +navigation, appearance, and code injection — plus a late pass over what the app +looks like *while* it is working, which removed the flashes and grey blinks that +appeared whenever you clicked from one type to another. + +One upgrade step needs your attention: the privileged helper daemon reinstalls +itself on first launch. See **Upgrade notes** at the bottom — this applies to +release-candidate users too, not only to those coming from 2.0.x. + +--- + +## Highlights + +- **Multi-tab content navigation** with a window tab bar and per-tab history +- **Generic type specialization** — inspect Swift generics with concrete arguments filled in +- **Background indexing** — the sidebar is ready when you click into it, not seconds later +- **Inspector → Relationships** — see who references a type, and what a type depends on +- **Batch export** interfaces for multiple images in one pass +- **Theme settings** with a data-driven color model and a built-in Xcode preset +- **Jump to Definition** for Swift type references +- **Line numbers** in the content view +- **Injection into strict-seatbelt daemons** via `mach_vm_remap`, and into Apple applications via the same path +- **iOS Simulator runtime installer** built in + +--- + +## What's new + +### Runtime inspection + +#### Generic Type Specialization +Pick any Swift generic from the sidebar and a new **Specialize…** sheet lets +you fill in its type parameters. The Inspector renders the fully specialized +interface alongside the original unspecialized form. + +- Nested generics work — if an argument is itself generic, you can drill in + and fill those parameters in the same sheet. +- Type definitions can come from a different binary than the generic itself, + so you can specialize types in framework *A* with a model from framework + *B* loaded in the same document. +- Invalid combinations are caught up front with a clear error instead of + silently producing garbage. + +#### Inspector → Relationships +A new **Relationships** tab sits between *Hierarchy* and *Specialization*. It +lists inbound references (who uses this type) and outbound references (what +this type depends on), drawn across every binary the document has loaded. + +#### Jump to Definition for Swift types +Command-click a Swift type reference in the content view to navigate to its +definition, the same way you already could for Objective-C. + +#### Enum layout fixed-bit masks +The Swift enum-layout view now surfaces fixed-bit masks for each case, giving +you the exact bit pattern the runtime uses to discriminate cases without +having to read it out of the raw metadata. + +--- + +### Indexing & batch operations + +#### Background Indexing +Once a document opens, RuntimeViewer walks the dylib dependency graph and +starts indexing related binaries in the background, so by the time you click +into them they are ready to inspect. + +- A new toolbar progress popover shows what is currently indexing, lets you + cancel any individual batch, and keeps a history of recent runs. +- Selecting a binary that is still in the queue immediately bumps it to the + front. +- Background work runs at low priority and never makes the main UI stutter. +- A new **Settings → Indexing** page controls how aggressively background + indexing runs (or turns it off entirely). + +#### Always Index list + master switch +The Indexing settings page grew a master switch plus two independently +toggleable sub-modes: + +- **Heuristic Discovery** — main-executable BFS at document open and engine + swap, with configurable depth (1–5). +- **Always Index** — a user-editable list where each row pins a single image + (full path or file-name shorthand). Toggle **Follow Dependencies** on a row + to also walk its dependency closure. + +The batches popover groups its active and history sections by reason +(*Heuristic Discovery* / *Always Index* / *Manual*) under collapsible headers. + +#### Batch Export +Export the interfaces of multiple images in one pass without opening each +document individually. + +- New **File → Export Multiple Images…** entry walks you through image + selection, format selection, and destination directory in a single sheet. +- Per-language format choices (Objective-C `.h`, Swift surface, etc.) are + remembered across runs. +- The completion summary uses stat cards to show how many files succeeded, + failed, or were skipped, and lists each failure individually so you can + re-run only what broke. +- Long Swift-mangled names no longer produce APFS `NAME_MAX` errors mid-batch. + +#### iOS Simulator Runtime Installer +A new built-in installer downloads and installs missing iOS Simulator runtime +images directly from the app — no more chasing them down through Xcode. + +--- + +### Navigation & windowing + +#### Multi-Tab Content Navigation +The main window now hosts multiple content tabs, so several runtime objects +can be open side by side instead of replacing one another in a single pane. + +- **Window tab bar** drives content navigation directly from the title bar. +- **Open in New Tab** is available both from content-view type links and from + sidebar rows, with a matching menu icon. +- Each tab keeps its own navigation timeline. +- Tabs are keyed by identity, and **Close Tab / Close Window** with **⌘W** + re-bind against the current key window automatically. + +#### Selection History & Navigation Menu +The toolbar drives back / forward navigation off a true selection-history +cursor, the way Xcode and Finder do. + +- Picking a sidebar item, drilling into a type, and switching between + Hierarchy / Relationships / Specialization tabs all push onto the same + stack. +- A **Navigation History** menu exposes the full per-tab timeline. +- The buttons enable / disable based on what's actually in the history, so + you never end up clicking back into nothing. +- The active inspector tab is preserved across selections. + +#### Smarter Sidebar Expansion +- Single-clicking a non-leaf row now expands it (matching the rest of macOS). +- A new setting caps how deep a double-click expands an entire subtree, so + you can preview a framework without exploding hundreds of children. + +#### Sidebar Grouping & Filter Scope +- Runtime objects are grouped into kind sections (classes, protocols, + categories…) instead of a flat list. +- A new **Filter Scope** popover in the sidebar lets you prefilter by kind + and property before typing, so the search corpus stays focused. + +#### Quit After Closing Last Window +**Settings → General → Windows** gains a *Quit After Closing Last Window* +toggle. It is off by default, which is the existing behaviour: the app stays +resident with no window open and a Dock icon click brings a window back. Turn +it on to have RuntimeViewer behave like a single-purpose utility that exits +with its last window. + +--- + +### Appearance & editing + +#### Theme Settings +A new **Theme** settings panel with a data-driven color model. Pick from +built-in presets (including an Xcode-matching one) or edit colors directly. +Themes drive the content view, inspector, and all syntax-highlighted +surfaces from a single source, and changes apply live. + +#### Line Numbers in the Content View +A line-number gutter runs down the left edge of the interface text view, +handy for cross-referencing decompiled or exported output. + +#### Content-shaped loading placeholders +Panes that fetch their content asynchronously now stand in a placeholder shaped +like the content it replaces, instead of leaving the previous type's data on +screen until the new one arrives. + +The placeholder is deliberately hard to catch sight of. Engine calls that hit a +warm cache return in single-digit milliseconds, so anything drawn the moment +work starts would appear and vanish inside one frame — a flash in its own right. +Work that finishes quickly now shows no placeholder at all, and a placeholder +that does appear stays long enough to read, measured from when it appeared, so +genuinely slow work is never delayed further. + +--- + +### Code injection + +#### Injection into strict-seatbelt daemons and Apple applications +System daemons that opt into the strict seatbelt (e.g. `sharingd`, `rapportd`) +can now be injected via a `mach_vm_remap`-based path, which previously refused +to load the runtime dylib. Apple applications, which enforce AMFI library +validation, take the same path — see the corresponding bug fix below. +Everyday injection targets continue to work unchanged; the new path only kicks +in when the standard route is blocked. + +--- + +## Improvements + +- Popovers (generation options, MCP status, background-indexing, type + picker) now size themselves to their content instead of defaulting to a + fixed 500×500 box. +- The Specialization type picker no longer blocks the UI when a generic + parameter has a wide constraint (10k+ candidates) — the list is built + off the main thread with a loading indicator, and rows are constructed + lazily as you scroll. +- Sidebar filtering skips the debounce when the query is empty, so + clearing the field feels instantaneous. +- The Mirror menu icon was refreshed and the toolbar no longer shows an + empty overflow chevron on macOS 15 when its items are hidden. + +--- + +## Bug fixes + +### Objective-C protocols reappear for UIKitCore, Foundation, and friends + +Opening UIKitCore showed an empty protocol list. Foundation showed no +`NSCoding`, `NSCopying`, or `NSFastEnumeration`. Classes, categories, and C +structs were listed normally, so the image itself parsed fine — only protocols +went missing, and they went missing entirely rather than partially. + +The cause was a protocol-ownership heuristic. Because the compiler emits a full +`protocol_t` into every image that saw a `@protocol` declaration at compile +time, `__objc_protolist` carries plenty of protocols an image merely imported. +The heuristic dropped a protocol whenever some other carrier of it sat in the +image's transitive dependency closure — which relied on an image never +appearing in its own closure. dyld's upward dependencies +(`LC_LOAD_UPWARD_DYLIB`) exist precisely to express cycles, so +UIKitCore ↔ PrintKitUI/ShareSheet and Foundation ↔ CoreFoundation each put an +image inside its own closure. Every protocol it carried was then classified as +imported, and the listing came back empty. + +Nothing in the Mach-O format records a defining image for a protocol, so +attribution can only ever be a heuristic — and this one failed silently, with +no error to hint that anything had been dropped. The filter is gone; +`__objc_protolist` is now listed verbatim. Some imported protocols reappear as +noise you can see and judge for yourself, which is the better failure mode: a +protocol that is silently absent is not. + +### Code injection picks its path from the whole rule + +Attaching to Music failed silently — nothing loaded on the target side, and no +error was reported. Two causes stacked. Routing between the two injection paths +consulted only the target's sandbox, which catches strict-seatbelt daemons but +says nothing about AMFI library validation; and Music, like every Apple +application, enforces library validation, which refuses a developer-signed +payload regardless of what the sandbox allows. The `dlopen` it was routed onto +could never have loaded anything, while the injector's shellcode reported +success anyway. + +The decision now lives in the helper daemon, which consults both the seatbelt +predicate and the code-signing flag, so Apple applications are routed onto the +`mach_vm_remap` path instead of one that cannot work. The investigation — the +`csops` status words separating the two target classes, the controlled +experiment isolating library validation from the payload's signature — is +written up in `Documentations/ResolvedIssues/`. + +### The Inspector no longer blanks when you switch types + +Clicking from one runtime object to another emptied the Inspector's list and +refilled it once the query returned. Relationships unions its results across +every indexed image, so that gap was long enough to read as a flash rather than +a redraw. + +The Inspector had been reusing its view controllers, but it still built a fresh +ViewModel per object and rebound it, which tore down the table's data binding +and installed a fresh one starting from an empty list. The tab ViewModels are +now bound once and handed the new object instead. Re-entering the same object — +closing a tab, moving the history cursor — short-circuits entirely, so it +neither refetches nor flashes a placeholder over content that is already +correct. + +This also fixed a real defect hiding behind the visual one: the old load had no +cancellation, so on rapid selection an earlier query could return late and +overwrite a newer object's rows. + +### Sidebar selection no longer blinks grey + +Two separate causes, both of which repainted the sidebar's selected row in the +inactive grey for a frame or two: + +- **Clicking a related type stole keyboard focus.** A row highlight is drawn + unemphasized whenever its table is not the first responder, so the Inspector + taking focus greyed out the sidebar row the user was actually looking at — for + focus the app handed straight back some 50ms later. The Relationships and + Specializations lists exist only to be clicked through, so they now refuse + first responder and paint no highlight of their own. Clicking, selecting and + navigating are unaffected; only arrow-key traversal inside those two lists is + gone, and every arrow press there navigated away anyway. +- **Jumping to a type selected the row before scrolling to it.** An outline view + materializes a row only as it scrolls into view and applies the emphasized + selection style after that, so the target row was drawn once in grey and + flipped to the accent colour on the next frame. Scrolling first — and forcing + the layout the scroll would otherwise defer — puts both in the same frame. + +### The tab strip no longer flashes when the tab set changes + +Changing the inspected kind rebuilds which Inspector tabs exist — selecting a +protocol after a class drops the Hierarchy tab. The strip used to remove every +tab and add the new set back, which swapped the visible view several times +inside one runloop pass. Tabs that survive the change now keep their view +throughout, and only tabs that genuinely appear or disappear cost anything. + +### Clicking the Dock icon reopens a window + +AppKit's built-in reopen handling only creates a window when it sees no visible +window at all, and it counts every window the app owns. With Settings or a +Sparkle update alert still on screen, clicking the Dock icon after closing every +document window therefore appeared to do nothing. RuntimeViewer now decides on +document windows alone, restoring a minimized or ordered-out one before falling +back to a new untitled document. + +### Attached daemons show their real icon + +Attaching to a daemon such as `launchservicesd` left the source menu showing an +App Store placeholder symbol. The icon cache is populated only from Launch +Services registered GUI applications, so a daemon never landed in it and every +lookup fell through to the placeholder. Daemons now fall back to the generic +executable icon — the same one the attach picker already shows for them. + +### A second instance no longer reports a phantom MCP server + +Binding the MCP bridge port fails whenever another RuntimeViewer instance +already holds it. That failure used to stop at a log line: the losing instance +still wrote the port file, still reported itself as running, and kept its fully +allocated network transport — 28 event loops, 56 threads, roughly 21 MiB — +resident for the lifetime of the process. It now tears the transport down, +truthfully reports itself stopped, and leaves the winning instance's port file +alone. A regression test covers the contention scenario end to end. + +### Smaller fixes + +- **Bonjour crash on batched receives.** Inspecting an iOS / visionOS / + Apple TV target over Bonjour, or pushing a batch-export through a + Bonjour connection, no longer crashes when a single network read + carries many small frames. +- **Attached-app and Mac Catalyst inspection.** The Inspector no longer + fails with *"An unknown error occurred"* when the runtime engine is + reached through a proxy hop; loading progress now streams across the + hop instead of getting stuck at *"Preparing…"*. +- **Nested sidebar specializations** keep their identity across sidebar + rebuilds instead of collapsing or re-shuffling. +- **Text view re-entry** rebuilds a fresh content view when returning + from another scene, fixing a case where the pane appeared blank. +- **Export module metadata resolution** fixed (#72). +- **The Xcode theme preset** gained a light-mode selection color. +- **The Specialization type picker popover** no longer animates out of place. +- Numerous smaller stability fixes across the sidebar, inspector, and + document lifecycle. + +--- + +## Build & Tooling + +- Both package manifests agree on the same `swift-helper-service` floor, so the + two halves of the app can no longer resolve different helper protocols. +- The Core package pins every remote dependency it parses Mach-O with, so a + fresh checkout reproduces a known-good set instead of resolving freely. +- The release process, the documentation index, and the branching model are + written down under `Documentations/`. +- GitHub Releases are titled with the bare version tag. + +--- + +## Platform requirements + +- **Main app**: macOS 15+ +- **MCP features**: macOS 15+ +- **iOS variant**: iOS 18+ + +--- + +## Upgrade notes + +- **Helper Service reinstalls itself** — the privileged helper daemon moves to + protocol version `1.5.0`. On first launch after upgrade you will be prompted + for admin credentials once and then asked to restart the app. + + This applies **even if you were running `v2.1.0-RC.3`**. The daemon-side + change that routes Apple applications onto the `mach_vm_remap` injection path + shipped in RC.3, but the protocol version did not move with it, so an already + installed helper was never replaced and RC.3 users have been running the old + daemon. Bumping it here is what actually delivers that fix. + +- **Beta and RC channel users** — this build supersedes `v2.1.0-RC.3`; you can + switch back to the stable channel with no reinstall of the app itself. + +- **From v2.0.x** — no manual migration steps. Bookmarks, settings, and + connections carry over as-is. diff --git a/Documentations/SparkleRelease.md b/Documentations/SparkleRelease.md index 133984bc..f4a308e5 100644 --- a/Documentations/SparkleRelease.md +++ b/Documentations/SparkleRelease.md @@ -22,6 +22,45 @@ all users. Release notes live at `Changelogs/.md` and are picked up automatically by `release.yml` when that file exists. +## Pre-flight: the helper daemon protocol version + +`RuntimeViewerServiceVersion` in +`RuntimeViewerCore/Sources/RuntimeViewerCommunication/RuntimeRequestResponse.swift` +is the **only** signal that makes an installed client replace its privileged +helper. `HelperServiceManager.checkServiceVersionAndReinstallIfNeeded()` +compares the running daemon's reported version against this constant on every +launch and reinstalls only on a mismatch. A new app build ships a new daemon +binary, but nothing installs it while the version string is unchanged. + +So before cutting any release, diff the daemon-side sources against the commit +that last bumped the constant, and bump it (minor, e.g. `1.4.0` → `1.5.0`) +whenever anything there changed: + +```bash +# -G, not -S: bumping the value leaves the number of occurrences unchanged, so +# a pickaxe search walks straight past every bump commit to the one that +# introduced the constant. +LAST_BUMP=$(git log -1 --format=%H -G'RuntimeViewerServiceVersion: String = ' \ + -- RuntimeViewerCore/Sources/RuntimeViewerCommunication/RuntimeRequestResponse.swift) + +git diff --stat "$LAST_BUMP"..HEAD -- \ + RuntimeViewerCore/Sources/RuntimeViewerCommunication \ + RuntimeViewerPackages/Sources/RuntimeViewerService \ + RuntimeViewerUsingAppKit/com.JH.RuntimeViewerService \ + RuntimeViewerServer +``` + +Ignore `Package.resolved` churn in that output; judge on source files. + +Getting this wrong fails silently in the worst direction: the release notes +describe a daemon-side fix, the build genuinely contains it, and no installed +client ever runs it. `v2.1.0-RC.3` shipped exactly that way — the injection +routing moved into the daemon while the constant stayed at `1.4.0`, so every RC +user kept running the old helper until `v2.1.0` bumped it to `1.5.0`. + +Nothing enforces this, unlike `MARKETING_VERSION`, which `ArchiveScript.sh` +verifies against `--version-tag`. It is a manual check. + ## Local release (manual) Substitute the version tag in the example below for whichever release you are diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCommunication/RuntimeRequestResponse.swift b/RuntimeViewerCore/Sources/RuntimeViewerCommunication/RuntimeRequestResponse.swift index eb649127..ebda901a 100644 --- a/RuntimeViewerCore/Sources/RuntimeViewerCommunication/RuntimeRequestResponse.swift +++ b/RuntimeViewerCore/Sources/RuntimeViewerCommunication/RuntimeRequestResponse.swift @@ -30,7 +30,7 @@ public let RuntimeViewerMachServiceName = "com.mxiris.runtimeviewer.service" /// Protocol version shared between the app and the helper service daemon. /// Bump this whenever the service binary changes in a way that requires reinstallation. -public let RuntimeViewerServiceVersion: String = "1.4.0" +public let RuntimeViewerServiceVersion: String = "1.5.0" #if canImport(AppKit) && !targetEnvironment(macCatalyst)