Repository navigation
Automatically install and relaunch macOS updates - #6
Conversation
Stage the matching signed app from a verified DMG beside the running bundle, then hand replacement and relaunch to a detached helper after host shutdown. Keep the manual installation flow for protected and translocated apps. Use same-filesystem renames, restore the old bundle on replacement failure, and preserve backups and diagnostic logs when recovery or relaunch fails. Add a shutdown callback and explicit macOS relaunch environment overrides so applications can persist state and retain custom settings directories. Prepare updates off the UI thread and defer completion until native progress calls return, avoiding dialog destruction and RefCell reentrancy panics. Cover helper success and recovery paths, cancellation, timeouts, competing installers, bundle identity and signatures, and progress-dialog lifetime. Include a disposable signed fixture demo and document integration and limits. Co-Authored-By: Codex <noreply@openai.com>
Use ship-shape's installer-ready callback to force-close the main window through Paperback's normal persistence and cleanup handler, then leave the application event loop. This saves settings, document positions and window geometry, stops audio and timers, and bypasses macOS's hide-on-close veto so the update helper can replace and relaunch the app. Pass the resolved settings directory through Launch Services when the new app is launched, preserving custom and isolated configurations. Pin the companion ship-shape PR commit so this draft builds without a local checkout. Replace the temporary Git patch with the published crate version after trypsynth/ship-shape#6 is merged and released. Co-Authored-By: Codex <noreply@openai.com>
trypsynth
left a comment
There was a problem hiding this comment.
Thanks for this, it's careful work. I read all of it, ran the 62 tests on macOS (all pass), and ran the demo end to end: v1 was replaced by v2, relaunched, and nothing was left next to the bundle. A few things before it can go in.
Needs fixing
- The signature check accepts an app signed by anyone.
codesign --verify --deep --strictonly checks that the replacement is validly signed, not by whom. An ad-hoc signature passes: the demo's installed app ends upSignature=adhoc,TeamIdentifier=not set. Minisign on the DMG is still the real guard, but please require the replacement to carry the running app's Team ID, or to satisfy the running bundle's designated requirement (codesign -d -r-, thencodesign --verify -R=...). If the running app has no Team ID, as with an unsigned dev build, fall back to the manual flow. cargo clippy --all-targets -- -D warningsfails on two lints in the new code: missing backticks in a doc comment atsrc/ui/progress.rs:71, and a missing;atsrc/ui.rs:244. Master is clean. The lints that matter are the ones in this crate's ownCargo.toml, not Paperback's workspace.
Should fix
- Stale staging directories.
.ship-shape-*is created next to the app, so usually in/Applications, and holds a full copy of the app. If the helper dies during its window (logout, power loss), nothing ever removes it. Please sweep stale.ship-shape-*directories next to the bundle on the next update, or on launch. - Logs accumulate. Every update leaves a
~/Library/Logs/ship-shape-update-*.log, even a successful one. Remove it on success, or keep only the last few.
Minor
- The demo includes
src/platform/macos.rsthrough#[path]and redefinesInstallOutcometo make it compile. That's brittle, and it gets published with the crate unlessexamples/is excluded from the package. - This adds about 20 translatable strings, which is fine, just flagging it for translators.
Done well
- The helper only takes paths as arguments, never as shell source, and passes alert text to
osascriptas argv. - It uses atomic renames, restores the old bundle on failure, and only proceeds after an explicit commit.
- It rechecks the bundle's inode before replacing it.
- It skips translocated, symlinked and unwritable bundles.
- The DMG is mounted private and read-only, must contain exactly one app with the same bundle ID, and the executable must sit inside that bundle.
- Cancelling at any stage is clean.
- The progress lifecycle fix and its tests deal with a real re-entrancy problem.
- Windows and Linux keep their existing behaviour.
Require the running and replacement bundles to satisfy an Apple signing chain requirement for the same developer Team ID. Keep unsigned and ad-hoc hosts on the manual path, and validate the replacement before and after staging. Test rejection of valid ad-hoc signatures and provide an optional read-only check for an existing developer-signed release. Register stage ownership and host/helper PIDs. On the next automatic update, sweep stages older than 24 hours only for the same bundle and user, with no live host/helper and no recovery backup. Preserve uncertain metadata, symlinks and other apps' stages. Remove successful/canceled helper logs and retain failure diagnostics. Cover these cleanup rules in the helper tests. Correct the standalone clippy findings, keep native relaunch environment values intact, and exclude the repository demo from published packages. Allow the demo to use an Apple-issued signing identity and document its manual fallback when an ad-hoc signature is used. Co-Authored-By: Codex <noreply@openai.com>
|
Pushed bbbdc57 with the review fixes:
The repository demo now accepts Validation: 64 automated tests pass, plus the optional read-only signed-release test, which accepts an existing notarized Paperback release for its own team and rejects a different team. Formatting and whitespace checks pass. I have not rerun automatic v1-to-v2 demo installation with a developer signing identity; the earlier ad-hoc end-to-end demo no longer qualifies for automatic installation under the new policy. Translation note: the new macOS user-facing messages use |
* Save state before macOS update installation Use ship-shape's installer-ready callback to force-close the main window through Paperback's normal persistence and cleanup handler, then leave the application event loop. This saves settings, document positions and window geometry, stops audio and timers, and bypasses macOS's hide-on-close veto so the update helper can replace and relaunch the app. Pass the resolved settings directory through Launch Services when the new app is launched, preserving custom and isolated configurations. Pin the companion ship-shape PR commit so this draft builds without a local checkout. Replace the temporary Git patch with the published crate version after trypsynth/ship-shape#6 is merged and released. Co-Authored-By: Codex <noreply@openai.com> * Announce update restarts and preserve explicit overrides Forward PAPERBACK_CONFIG_DIR only when it is present in this process's environment. Preserve its native value so relaunch does not synthesize an override for the default settings directory or lose non-UTF-8 path bytes. Post a translated high-priority live-region announcement before updater shutdown. Disable the frame and keep the UI event loop running for a brief grace period so VoiceOver can receive the asynchronous notification, then run the existing forced-close persistence and cleanup path. Update the pinned ship-shape revision and lockfile to the reviewed signer, stale-stage cleanup and log-retention fixes. Keep the integration draft pending the upstream crate release. Co-Authored-By: Codex <noreply@openai.com> * Use ship-shape 0.4.0 from crates.io instead of the pinned PR commit --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Quin Gillespie <trypsynth@gmail.com>
Problem and behavior
On macOS, accepting an update currently opens the verified DMG and leaves the user to quit the app and drag its replacement into Applications. This change prepares and installs the update automatically for writable, Apple developer-signed app bundles, replacing the app at its existing path and relaunching it. Custom folders and renamed bundles are supported. Protected, mounted-image, Gatekeeper-translocated, unsigned and ad-hoc installations keep the manual flow; no administrator authorization is requested.
Implementation
~/Library/Logs/ship-shape-update-*.log.run_update_check_with_exit_handlerso the host can save state and quit normally, andwith_macos_relaunch_envto preserve explicit native environment values through Launch Services. The existing update-check entry point keeps its immediate-exit behavior.The README documents integration, signer requirements, manual fallback, recovery, timeouts and testing. The repository demo creates disposable v1/v2 apps and a DMG; automatic installation requires an Apple-issued signing identity, while ad-hoc fixtures use manual fallback. Its shared-source harness is excluded from the published crate package and does not add a production local-file installation API.
Validation
cargo clippy --all-targets -- -D warningspasses with this crate's declared lint settings and normal published wxDragon dependency. Formatting and whitespace checks pass.cargo package --listconfirms the demo is excluded and the installer helper assets are included.The original replacement/relaunch mechanism was exercised end to end with a disposable Paperback copy and the ad-hoc v1/v2 demo. After tightening the signer policy, automatic demo installation requires a developer signing identity and has not been rerun here; the new signer check was verified separately against a notarized release.
Limits and translation
The helper allows 60 seconds for host shutdown. Launch Services success confirms that macOS accepted the request; it does not detect a later application crash. Failed launch preserves the old bundle rather than automatically rolling back an app that may already have started. Recovery backups are not automatically swept.
The new macOS user-facing strings use
patois::tand will need catalog translations in consuming apps.Companion integration: trypsynth/paperback#1004, a draft using the shutdown callback to save application state before replacement and announce the restart to VoiceOver. It is pinned to this PR's reviewed commit and will switch to a published crate dependency after this change is merged and released.