Skip to content

macOS: updates keep their permissions, and selection conversion works - #55

Merged
vstrelnikof merged 21 commits into
Just-Code-NET:mainfrom
iga566-gh:pr/macos-all
Sep 2, 2026
Merged

macOS: updates keep their permissions, and selection conversion works#55
vstrelnikof merged 21 commits into
Just-Code-NET:mainfrom
iga566-gh:pr/macos-all

Conversation

@iga566-gh

Copy link
Copy Markdown

macOS: updates keep their permissions, and selection conversion works

This is the macOS cluster from a fork where I chased issue #42's tail to the end on an M1 Pro (macOS 26.5), with repeated clean-machine end-to-end runs — fresh TCC, fresh keychain, fresh app data: install → grant → update. 21 commits, each carrying its measurement in the message.

Updates and permissions

  • The relaunch race from macOS: every self-update invalidates Accessibility and Input Monitoring, and "Ask macOS now" cannot restore them #42 is the Settings window surviving the swap. It is a subprocess of the same executable; it outlives the main process, so install.sh's open finds the app "already running" and merely activates the orphaned old-version window — the updated build never starts. The main process now takes every child window along on the update hand-off and on Quit, and the pane's new Restart button starts the bundle from a detached helper after the settings process exits, for the same reason.
  • Whole-bundle replacement loses TCC grants even under a stable signature. Measured with the same identity on both sides: an in-place content change plus re-sign keeps Accessibility and Input Monitoring; the mv-aside-mv-in dance loses them. The installer now swaps Contents inside the bundle and never moves the .app itself (same rollback, Contents.old inside the bundle). The .app.new Spotlight sighting goes away with it.
  • Grants can survive updates today, without a Developer ID. [updates].local_signing_identity names a keychain identity; the installer re-signs the swapped bundle with it, and TCC keys the grants to certificate + identifier. A Setup step creates the identity in one click — system LibreSSL, PEM-by-PEM import (security(1) refuses LibreSSL's PKCS#12), codesign pre-authorised, and the one-time keychain password prompt triggered right at the button, not mid-update where it reads as malware. With no identity configured, the installer drops the two stale TCC records after a successful swap instead, so the Ask buttons genuinely raise the system prompts right after an update.
  • Smaller, same series: check_and_stage no longer discards a staged version ahead of the feed; the identity probe lists without -v (which hides self-signed identities and had the pane offering to create a duplicate); Ask always opens the exact Settings pane; on current macOS one Accessibility grant satisfies the Input Monitoring probe too, so the pane folds the second Ask into "covered by the grant above" while keeping the button for the older systems this project supports; a notifications Setup step points at the right pane and warns when Focus is silencing banners; the relaunch open retries once.

Selection conversion on macOS

  • send_chord exists now, so the selection_support() gate is gone: modifiers travel as flags on the key events (how macOS matches menu shortcuts — nothing is left held), with an SC-1→Apple reverse keycode table that refuses what it cannot name, and the copy/paste chords pick Cmd on macOS in the one constant that owns them. Validated live: conversions both ways, clipboard restored.
  • What it took to trust it: the tap watches caret-moving clicks now — the one listener of the three that didn't, so the switch-last stash lived out its full TTL and the hotkey backspaced through an active selection; transliterate_to refuses text with no letter the source layout owns (a wrong-direction press mangled exactly the characters sitting on different punctuation across layouts — a ю drifting through . to / and never back); backspaces are paced by their own tap echoes instead of a timer (Spotlight drops timer-paced deletes and keeps the word's first letter standing); the notification sender registers its bundle id exactly once before the first toast (otherwise mac-notification-sys resolves an app literally named use_default and macOS opens a modal application chooser; a second set_application is a hard error that kills every later toast); and [engine].hold_keys exposes the key gate as a config knob — default off, per the project's documented latency stance; the knob is the new part.

Windows and Linux paths are untouched. Tests extend the existing suites; all green (poltertype-update 49, poltertype-input 39, plus the app and core suites).

iga566 added 21 commits August 31, 2026 02:54
…stale TCC records

The owner's two asks, both testable in the script's shape: with
[updates].local_signing_identity set, the installer re-signs the
swapped bundle (grants survive — proven by the dev-signing experiment),
resetting the records only on the one transition where they cannot
match; with it empty, the installer drops the two stale records so the
Setup pane's Ask buttons genuinely work right after an update.

(cherry picked from commit f0049bb79d6e672320dc43a48ca101bfbed9d051)
…macOS)

The owner asked for both to live where the Ask buttons live. The
signing step creates (or adopts) a private keychain identity with the
system LibreSSL — PEM-by-PEM import, since security(1) refuses
LibreSSL PKCS#12 and -legacy is an OpenSSL-3-ism — writes its name to
[updates].local_signing_identity, and the updater re-signs every
swapped bundle with it from then on. The notifications step says what
can honestly be said: whether Focus is silencing banners right now
(read from the session DND assertions), and where the per-app switches
live, since macOS offers no public query for allow-plus-style.

(cherry picked from commit 5eed85f66bb72d466ae202536ddcb4d49f2b04ee)
(cherry picked from commit e27b0dab45a09e6f8416ba82d03f48fcc39f8c43)
… policy yet signs fine, and -v hid it (the step then offered to create a duplicate)

(cherry picked from commit eb0b91ef4cfd555e01f9288ed28abb0fdb2b5c2b)
Re-staging used to fire whenever staged != available, which also
covered staged AHEAD of the feed — a local build, a rollback, a
pre-release. The worker silently replaced such an artifact with
whatever GitHub served, and the next Restart-to-update installed
something the user never staged (measured twice today, the hard way).
Now only a feed version strictly newer than the staged one re-stages.

(cherry picked from commit 2d849f54eb18fdee5466476bec32f6092f3690e6)
The 'codesign wants to access key' password prompt is the keychain
confirming first use of the fresh key — and it used to fire in the
middle of the first background update, where an unexplained system
dialog about a key reads as malware. The step's text now says exactly
what will appear and what to press, and the creation flow signs a
scratch copy of /usr/bin/true immediately, so the dialog lands the
moment the user pressed the button and expects to be asked.

(cherry picked from commit 8ea89881eb24dc7dede5ef8b67a05a97a51dcf64)
After a TCC reset the permission lists hold no PolterType row at all,
and the user is left staring at an empty list guessing (the owner's
words, deservedly sharper). Ask now also opens the exact pane — the
dialog, when it comes, appears over it; when it does not, the user is
standing where the '+' button lives — and both step texts spell out
the add-by-hand fallback.

(cherry picked from commit d909cee378fc6392366c85daf9f17372739be285)
…ter the swap-and-resign; second try by hand always worked

(cherry picked from commit 1d503ada5692385475ed4e96e2e3da1c3326975b)
Measured on macOS 26 (2026-08-31): with the same untrusted signing
identity, an in-place content change keeps the TCC grants and the
whole-bundle mv dance loses them — which is why every update killed
the permissions even after the re-signing work. The bundle directory
now stays put and its Contents is what swaps, with the same
rollback (Contents.old inside the bundle).

(cherry picked from commit 11ef82e7180e414cef235bfc44f4800c2d049329)
The Settings GUI is a subprocess of the same executable. Left alive
past the main process it is worse than a leak: the updater's relaunch
open(1) sees the app as already running, reports success, and brings
the orphaned old-version settings window to the front — the updated
app never starts (measured twice; also explains the ghost settings
window after a plain Quit). The main process now terminates it on the
update handoff and on Quit; the waiter thread reaps as usual.

(cherry picked from commit 871ef03b7314b33e10ef9cb4764ea4a2a6bd30d0)
…after this process exits — while the settings process lives, LaunchServices reads the app as running and open() merely activates it (the ghost again); shipped as 0.31.2

(cherry picked from commit ad2815e39e39c7265e199d30bf09e7160a809f3b)
Measured twice on macOS 26: the Accessibility grant satisfies the
Input Monitoring probe too — two Ask buttons for one decision read as
two decisions, so the second shows only in the old-macOS case where
Accessibility is done and the listen probe still says no (the project
supports Big Sur, where the switches are genuinely separate). And the
pane grows the button the owner asked for three times: Restart
PolterType — permissions are read at startup, and hunting Quit in the
tray after every grant was the complaint.

(cherry picked from commit a39f237e3bb3fd3043388d544ceb8c9c5a73f3db)
This 0.28-based fork has no second-window guard, so a Settings window
and a Setup-alert window coexist while the pid slot remembers only the
latest. The survivor is what kept LaunchServices treating the app as
running — the updater relaunch then merely raised an orphaned
old-version window (measured live: poltertype --setup outliving the
swap). kill_settings_ui now also pkills every process of this
executable with a window flag. Shipped as 0.31.4.

(cherry picked from commit 5b0d9e2ae8fc592d680e0f4fdb197b527659e0d3)
The emitter posts the chord's key with the modifiers as CGEventFlags on
the key events themselves — how macOS matches menu shortcuts, and
nothing is left held. The chord arrives in SC-1 space, so codes.rs
grows the reverse table (main block only, no identity fallback: the two
keycode spaces overlap with different meanings). COPY/PASTE chords pick
Cmd over Ctrl on macOS in the one constant that owns them, and the
selection_support() gate falls back to the clipboard probe now that
every desktop emitter can press a chord.

(cherry picked from commit ba1a1312e31deef54105d3cba13b12b47cd2d29d)
(cherry picked from commit 48022b66a4f90790ad28a6ecc55f88d498f4cfc2)
… everywhere else

The tap listened to the keyboard only — the one listener of the three
that did. A click never invalidated the stash, so it lived out its full
60-second TTL and the hotkey kept taking the word path; with a selection
active that path backspaces straight through the selection. Mouse downs
(left, right, other; scroll untouched, same reasoning as X11's button
filter) now translate to the pointer marker the engine already knows.

(cherry picked from commit a21dacb741b0116219574270f8e4a02ff1f4402c)
(cherry picked from commit 9d903217df973a722c5fd446f0f9d6cd283aa753)
…transliterate refuses text the source layout never typed

Two live findings from the same session. mac-notification-sys, when
nobody called set_application, resolves an app literally named
'use_default' and macOS answers with a modal 'Where is use_default?'
application chooser — and the updater's error toasts bypass the
show_notifications gate, so the dialog could appear with notifications
off. The bridge now registers our own CFBundleIdentifier once, and
skips the toast when there is no bundle to speak as.

transliterate_to now requires at least one letter the source layout
owns: without it, a wrong-direction press passed letters through but
'converted' the punctuation — ru→en on 'ghjdthz.' turned the period
(ex-ю) into a slash through ru's own period key, and no later pass
could bring the letter back.

(cherry picked from commit de85354b1b1f6174d1f803300cbea8a71fd435c3)
(cherry picked from commit e69c5c78c793343c6480d90312a6c25c2f5e2d1a)
… sequence the AlreadySet(Finder) failure

(cherry picked from commit dd49bed68a492bfb88dcbbf28714fad2f1293409)
(cherry picked from commit a53a1294952c10120e33286438967ca3631a9142)
A fixed KEY_STEP was measured enough for ordinary fields, but Spotlight
re-queries on every keystroke and still dropped deletes posted on a
timer — the word's first letter survived with the correction glued to
it. The emitter now waits for each delete's own echo at the session tap
(the one in-process proof the window server sequenced it), with a 25 ms
timeout per key that degrades to the old timer pacing when the tap is
dead or listen-only.

(cherry picked from commit 7f5fbe71a1c1c52d9078d0861ee8839c69670a3f)
(cherry picked from commit 23bea8a228e35bf0feadd3f8cf12212a728b61fb)
The gate switch moves where the user can see it: a config key, default
on in this fork, with the env var demoted to an emergency override in
either direction. Wired to the macOS gate only — the Windows gate keeps
its env-only switch until the change can go through a Windows test
run.

(cherry picked from commit 09a5943e74d7c8a1db74029a3caf8f71f3409fa7)
(cherry picked from commit f06e36fa02571644193287f688ad803fabf9aa26)
…oject's documented stance; the config key is the new part

(cherry picked from commit dbc2753c4130482ee2ca92c6fe2ce9611c9aeb19)
@vstrelnikof
vstrelnikof merged commit 9eb33b4 into Just-Code-NET:main Sep 2, 2026
1 of 4 checks passed
vstrelnikof added a commit that referenced this pull request Sep 2, 2026
KNOWN-GAPS is re-stamped and says which session the pass actually
covered: Cinnamon X11, the one all three Linux reports name, with the
default grabbed chord rather than a modifier-only stand-in. The earlier
note that both #51 fixes were unrun on that session is no longer true
and says so. The 0.24.0 paragraph calling macOS 'not a gap here for
once' is history now that it has a send_chord, and carries a marker
saying so.

CHANGELOG picks up this cycle's fixes and the macOS work from #55.
@vstrelnikof

Copy link
Copy Markdown
Member

Merged, and shipped in v0.30.0. Thank you — twenty-one commits each carrying its own measurement is the kind of PR that can be reviewed honestly without the hardware, and the diagnosis in #42 that started it saved the work twice over.

Two changes on the way in, both mine, neither touching what the branch was for:

CI. NOTIFICATIONS_PANE_URL was declared without the #[cfg(target_os = "macos")] its two neighbours in the same file carry. Invisible on a Mac; everywhere else it is an unused constant and -D warnings makes that a build failure — which is exactly the shape the run had, check (macos-latest) green and Linux and Windows both red. Plus cargo fmt --all over twelve drifted spots.

The chord constants. COPY_CHORD and PASTE_CHORD grew a cfg!(target_os = "macos") in poltertype-core, and that crate holds no platform conditional of any kind — it is a rule the project checks with a grep, and it was still true right up to this branch. Your reasoning for keeping the split in the constants rather than in the emitters is right and is kept verbatim in the comment; only the address changed. They now live in poltertype-input, one of the crates allowed to know which OS it is on, and the engine re-exports them.

One thing your branch did not fix and v0.30.0 does, because it would have bitten the macOS selection path too: SystemClipboard opened an arboard handle per write and dropped it on return, and both X11 and Wayland serve the clipboard from the owning process — so every write destroyed itself. See #51.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants