Skip to content

Releases: sohonetlabs/testfs

v0.1.28

Choose a tag to compare

@benroeder benroeder released this 30 Apr 16:17

Added

  • Examples ▾ picker next to the Source JSON chooser. The host
    bundles 16 example tree JSON files but only File ▸ Try an example…
    exposed them, which most users never noticed. The new inline menu
    is data-driven from a directory listing of Contents/Resources/Examples/,
    so any JSON dropped into research/test_json_fs/example/ shows up
    on the next build with no code change.

Internal

  • BundledExamples.sortedJSONURLs(in:) helper in TestFSCore (pure
    Foundation, unit-testable).
  • ContentView+Helpers.swift was a few lines over SwiftLint's
    400-line file budget after the additions; split into
    ContentView+Helpers.swift (statics, the re-registration actor,
    ContentView's non-view helpers) and ContentView+UI.swift
    (banners, Examples menu, panel pickers, System Settings deep link).
  • 4 new tests in BundledExamplesTests covering the filter and sort.
    swift test now reports 106 tests (was 102).

v0.1.27

Choose a tag to compare

@benroeder benroeder released this 30 Apr 15:24

Fixed

  • "Restart required" banner after install / Sparkle update. macOS 26
    caches FSKit XPC connections in fskit_agent and doesn't flush the
    cache when an extension exits, so the first mount after a Sparkle
    update fails with extensionKit.errorDomain error 2 until the
    system reboots. The host now detects the post-install state (bundle
    mtime > last boot time) and shows a banner explaining it, instead
    of letting mount(8) fail with the cryptic error. Mount click is
    short-circuited with the same guidance. Banner clears automatically
    after reboot.

  • friendlyMountError extensionKit branch now points at reboot as the
    primary recovery, with toggle off+on as the fallback.

v0.1.26

Choose a tag to compare

@benroeder benroeder released this 30 Apr 14:37

Fixed

  • Mount no longer fails after a Sparkle update with
    extensionKit.errorDomain error 2 / "File system named testfs not
    found". Sparkle replaces the app bundle but doesn't terminate the
    live appex; extensionkitd then routes mounts to the pre-update PID
    whose cdhash no longer matches the new bundle. The host now reaps
    any orphan TestFSExtension on the post-update path so extensionkitd
    spawns a fresh instance from the updated appex on the next mount.

    The orphan reaper uses libproc with exact-path equality
    (proc_pidpath + string compare against the resolved appex binary)
    rather than pkill -f, so an unrelated process that mentions the
    appex path — a grep in a dev shell, a log viewer's window title —
    can't be matched.

  • Restored the extensionKit.errorDomain branch in friendlyMountError
    that 0.1.25 dropped on the (incorrect) assumption pre-flight catches
    every disabled-state symptom. If the orphan reap doesn't reach
    (e.g., the process is owned by another user), the user now gets
    toggle-off-then-on guidance instead of TCC privacy text.

v0.1.25

Choose a tag to compare

@benroeder benroeder released this 30 Apr 14:01

Fixed

  • Bundled big_list_of_naughty_strings_fs.json example now mounts.
    The fixture had a duplicated top-level directory entry from a
    hand-generated tree -J -s run, so File ▸ Try an example… on this
    one fixture failed with unexpectedTrailingEntry. Patched upstream
    in test_json_fs@5615573; submodule pointer bumped here. All 329
    naughty filenames are preserved (sha256-verified).

Internal

  • Test parse coverage for every shipped example. The 9 archive_torture
    fixtures + bad_windows_extended weren't being exercised by any test —
    added testEveryBundledFixtureParses that enumerates every JSON
    shipped under Fixtures/ and asserts it parses to a directory root.
  • Trim dead state from the 0.1.24 watcher revert: drop unused
    mountpoint parameter on friendlyMountError, drop write-only
    @AppStorage verifiedMountedVersion binding (replaced with a
    direct UserDefaults.set at the mount-success site), inline
    FSKitEnabledWatcher.plistDir as a local.

TestFS 0.1.22

Choose a tag to compare

@benroeder benroeder released this 29 Apr 17:18

What's new in 0.1.22

Completes the first-launch self-heal across all three caching
layers that macOS 26's stricter ExtensionKit can resolve into.
0.1.21 cleared the LaunchServices database and killed orphan
appex processes; affected machines still hit extensionKit error 2 / Cocoa 4099 because fskit_agent (the long-running per-user
FSKit broker) held an in-memory XPC connection cache referencing
sibling helpers that runningboard tore down during the orphan
kill. Every subsequent spawn routed through the stale cache and
failed with "connection to service with pid <N> was invalidated".

Fix

  • Reregister cleanup now also kills fskit_agent to flush its
    per-user XPC cache. fskit_agent is a launchd user service
    (com.apple.fskit.fskit_agent) — auto-respawns on demand, no
    harm done. The kill happens between the orphan appex kill and
    the pluginkit toggle, so the toggle re-establishes every cached
    connection from a clean state. (#72)

Three layers, three killers

For the record, the cleanup chain is now:

  1. sweepStaleRegistrations — LaunchServices database (#68 / #70).
  2. killOrphanExtensionProcesses — live appex from prior version (#70).
  3. killFSKitAgent — per-user XPC cache (this release).
  4. lsregister -f / pluginkit -e ignoreuse — re-adjudicate.

Every layer that could reference the previous bundle's UUIDs is
forced to re-init.

Auto-update

Existing 0.1.21 installs (and any 0.1.15+) will pick this up via
Sparkle within ~24h or immediately via App ▸ Check for Updates….

Tracked follow-ups

  • Manual drag-installs over a running mount still bypass the
    Sparkle pre-install hook (#65); a launch-time stale-state
    detector for that case is in scope for a later release.
  • release.sh self-bumping .build_number (#67).

TestFS 0.1.21

Choose a tag to compare

@benroeder benroeder released this 29 Apr 17:00

What's new in 0.1.21

Completes the first-launch self-heal that 0.1.20 started. 0.1.20's
sweep used NSWorkspace.urlsForApplications for enumeration, which
filters out registrations whose underlying file is gone — exactly
the dead-file entries (unmounted DMGs, deleted Trash items) most
likely to confuse macOS 26's stricter ExtensionKit. Affected
machines still hit mount exit 69 / extensionKit error 2 after
upgrading to 0.1.20 because the sweep couldn't see those entries.

Fixes

  • Sweep now enumerates via lsregister -dump parsing instead
    of urlsForApplications. Catches every registration regardless
    of whether the underlying bundle still exists on disk. Batched
    lsregister -u <p1> <p2> ... removes them in a single
    subprocess. (#70)
  • Orphan extension processes are killed during the reregister
    cycle.
    Sparkle's bundle replace doesn't tear down running
    ExtensionKit instances; the pluginkit toggle alone doesn't reap
    them either. The leftover process holds a stale UUID and
    extensionkitd refuses to spawn a fresh instance with Cocoa 4099 / NSXPCConnectionInvalid. pkill -f against the resolved
    appex binary path forces a clean replacement on the next mount.
    (#70)
  • Cleanup gated behind the verifiedMountedVersion stamp so
    stable installs don't pay the multi-MB lsregister -dump +
    pkill cost on every launch. The stamp resets on every release,
    so post-upgrade always runs the cleanup. (#70)

Auto-update

Existing 0.1.20 installs (and any earlier 0.1.15+) will pick this
up via Sparkle within ~24h or immediately via App ▸ Check for
Updates….

Tracked follow-ups

  • Manual drag-installs over a running mount still bypass the
    Sparkle pre-install hook (#65); a launch-time stale-state
    detector for that case is in scope for a later release.
  • release.sh self-bumping .build_number (#67) — the regression
    class that broke 0.1.15 → 0.1.18.

TestFS 0.1.20

Choose a tag to compare

@benroeder benroeder released this 29 Apr 16:35

What's new in 0.1.20

A targeted fix for the most common silent-failure mode left over
after 0.1.19's monotonic-build-number fix: macOS 26's stricter
ExtensionKit hard-fails when LaunchServices accumulates stale
registrations for the host bundle, even when pluginkit lists only
the canonical bundle as +-enabled. Symptom is mount exit 69: mount: Loading resource: extensionKit.errorDomain error 2 / File system named testfs not found, with the underlying cause showing
up in fskitd logs as NSCocoaErrorDomain Code=4099
(NSXPCConnectionInvalid).

Fix

  • First-launch sweep of stale LaunchServices registrations:
    the host enumerates NSWorkspace.urlsForApplications for its
    own bundle ID at first launch, batches a single lsregister -u
    for every path that isn't the running bundle (resolving symlinks
    to handle Gatekeeper translocation and /var ↔ /private/var
    variants), then forces an extensionkitd toggle if anything was
    removed — the toggle drops cached UUIDs that may still point at
    the just-unregistered paths. Converts "user runs lsregister -u
    from the terminal to fix mount" into "user opens the app, sweep
    runs, next mount works." (#68)

Auto-update

Existing 0.1.19 installs will pick this up via Sparkle within
~24h or immediately via App ▸ Check for Updates….

Tracked follow-ups

  • release.sh self-bumping .build_number (the regression class
    that broke 0.1.15 → 0.1.18) is captured as #67.
  • Manual drag-installs over a running mount still bypass the
    Sparkle pre-install hook (#65); a launch-time stale-state
    detector for that case is in scope for a later release.

TestFS 0.1.19

Choose a tag to compare

@benroeder benroeder released this 29 Apr 16:13

What's new in 0.1.19

A re-release of 0.1.18's contents with the build-number monotonic-
ity bug fixed, so Sparkle actually offers the update.

Fix

  • .build_number on main had been stuck at 89 since 0.1.14 —
    every release.sh run bumped it to 90 in the archive but never
    committed the new value back, so consecutive releases shared a
    CFBundleVersion. Locally-iterated builds (e.g. the in-the-wild
    0.1.15 at build 94) ended up with a higher build number than
    any subsequently published release. Sparkle's "newer build wins"
    logic then refused every "update" as a downgrade. .build_number
    bumped to 100 so this 0.1.19 archive lands at 101, above the
    affected installs.

Carried from 0.1.18

  • Sparkle pre-install hook gates updates on a clean unmount:
    active testfs mounts no longer pin the FSKit appex into a half-
    applied install. On any unmount failure the install is aborted
    loudly via an alert, leaving the user on the previous version
    with a clear message instead of a silently-stale bundle (#65).

Auto-update

Existing 0.1.15+ installs should now correctly detect this update
via Sparkle within ~24h or immediately via App ▸ Check for Updates….
Updates into 0.1.19 use the pre-hook flow — unmount any active
testfs volumes before accepting the prompt this one time.

Tracked follow-up

  • release.sh will be hardened to commit .build_number along with
    appcast.xml so the baseline on main stays current after every
    release, preventing this regression class.

TestFS 0.1.18

Choose a tag to compare

@benroeder benroeder released this 29 Apr 15:41

What's new in 0.1.18

A targeted release covering one fix that closes a silent-failure
class affecting Sparkle auto-update.

Reliability

  • Sparkle pre-install hook gates updates on a clean unmount:
    an active testfs mount keeps the FSKit appex pinned in the kernel,
    and without intervention Sparkle's bundle replace fails silently —
    the user sees the new version in About box but /Applications/TestFS.app
    stays at the old version on disk. The host now runs an unmount
    sweep before allowing the install to proceed; on any failure the
    install is aborted loudly via an alert, leaving the user on the
    previous version with a clear message instead of a half-applied
    update (#65).

Auto-update

Existing 0.1.17 installs will pick this up via Sparkle within ~24 h
or immediately via App ▸ Check for Updates…. The hook is in 0.1.18,
so the next update from 0.1.18 onward (and every one after) is
gated by it. Updates into 0.1.18 still use the pre-hook flow —
unmount any active testfs volumes before accepting the prompt.

Out of scope (tracked)

  • Manual drag-installs (no in-process hook) can still fail silently
    if a mount is active. Workaround: unmount first, then drag.

TestFS 0.1.17

Choose a tag to compare

@benroeder benroeder released this 29 Apr 14:53

What's new in 0.1.17

A focus release covering 5 issues filed in round-4 audit against
0.1.16, plus the host-side pre-flight that #56 added (which was
deferred from 0.1.16 once #54 reverted the original attempt).

Security

  • mount.sh privilege-escalation fix: the admin osascript that
    chowns the dev node no longer interpolates $USER and $DEV
    unquoted — both are routed through AppleScript's quoted form of
    via system attribute (and $USER is replaced with a kernel-
    trusted value), so a tampered environment can't escape into the
    privileged shell command (#57).

Reliability

  • runSilently SIGKILL escalation: re-registration helpers now
    use the same SIGTERM → wait → SIGKILL → wait shape ShellRunner
    uses, sharing a single ShellRunner.terminate(_:exitSem:) helper
    so the two timeout paths can't drift again. A wedged
    lsregister/pluginkit no longer survives the watchdog and
    stack against subsequent mount attempts (#58).
  • Failure-marker channel: per-attempt token: the marker channel
    added in 0.1.16 is now scoped to a per-mount UUID, so a slow-
    failing prior loadResource on a reused /dev/diskN can't write
    a marker that triggers false rollback on the next mount. Same
    diskN-reuse hardening class as 0.1.15's #31/#41 (#59).
  • Pre-flight off the MountManager actor: the host-side
    TreeBuilder.parseAndBuild pre-flight (re-added via host-target
    membership for the parsers — #56) now runs in a detached task so
    large-fixture parsing doesn't pin the actor and block concurrent
    unmounts/sweep/registry refresh (#60).

Auto-update

Existing 0.1.16 installs will pick this up via Sparkle within ~24 h
or immediately via App ▸ Check for Updates….