Refactor display/menu plumbing and fix crash-prone edge cases - #5
Conversation
Fixes: - Arm64DDC retry loop trapped with arithmetic overflow when the retry count was 255 (exactly what OtherDisplay.readDDCValues clamps to); do the +1 in Int arithmetic. - IntelDDC.read trapped on an invalid 1...0 range when tries was 0; clamp to a single try instead. - Replace force-unwraps of Bundle.main.bundleIdentifier in AppDelegate.setStartAtLogin and MainPrefsViewController. - Remove the KVO observer MenuslidersPrefsViewController registers, and fix two log typos (Addig/destory). Refactors (no behavior change): - Remove dead DisplayManager.sortDisplays() and the misleading doc comment on getAllDisplays(); document why sortDisplaysByFriendlyName intentionally returns descending order. - Simplify addDisplayCounterSuffixes with Dictionary(grouping:by:). - Drop the duplicated default-volume assignment in OtherDisplay.setupCurrentAndMaxValues. - Use NSMenu.removeAllItems() in MenuHandler.clearMenu. - Replace manual min/max/count loops in SliderHandler.setValue and MediaKeyTapManager's hasExternalDisplay scan with stdlib calls. - Flatten trivial boolean if/else returns (Display.isBuiltIn, Display.isSwBrightnessNotDefault, OtherDisplay.isSw) and simplify Display.getKey, Arm64DDC.read, and UpdaterDelegate. - Remove IntelDDC.read's unused replyTransactionType parameter.
Greptile SummaryThe PR refactors display and menu plumbing while hardening several crash-prone edge cases.
Confidence Score: 5/5The PR appears safe to merge, with no outstanding correctness or repository-rule issues identified. The polling-count crash reported previously is fixed by clamping persisted values before unsigned conversion; that thread was manually resolved without explanation. The subsequent KVO cleanup is balanced correctly for both loaded and unloaded controllers, and no new actionable failures remain.
|
| Filename | Overview |
|---|---|
| MonitorControl/Model/OtherDisplay.swift | Clamps invalid polling preferences before unsigned conversion and treats zero polling attempts as no read. |
| MonitorControl/Support/Arm64DDC.swift | Moves retry-bound arithmetic to Int and simplifies DDC read handling. |
| MonitorControl/Support/IntelDDC.swift | Prevents an invalid closed range when the requested try count is zero. |
| MonitorControl/View Controllers/Preferences/MenuslidersPrefsViewController.swift | Removes the menu-icon observer only when view loading registered it. |
| MonitorControl/Support/DisplayManager.swift | Removes dead sorting code and simplifies duplicate-display suffix assignment. |
| MonitorControl/Support/SliderHandler.swift | Derives aggregate slider state directly from stored display values. |
Reviews (6): Last reviewed commit: "Fix observer removal crash when pane vie..." | Re-trigger Greptile
User-facing (key changed in source and in all 19 Localizable.strings catalogs so no translation falls back to English): - 'An other app seems to change...' -> 'Another app seems to be changing..., which causes issues' in the gamma interference alert. - 'gammatable' -> 'gamma table' in the virtual display status text. - 'supports native Apple brightness protocol' -> 'supports Apple's native brightness protocol'. Comments and one log message: - PrefKey: fix two copy-pasted 'Show volume sliders' comments and two 'Disable slider snapping' comments that named the wrong setting; fix 'Is the specific control is set' grammar. - 'deprectated', 'orther' (MainPrefsViewController), 'Firendly name' (DisplaysPrefsViewController), 'more the 2 displays' (SliderHandler), 'Never let sound to mute accidentally' (OtherDisplay), 'why is the brightness suddenly changes' (DisplayManager), 'reconfigreID' log typo (AppDelegate), and 'MacOS 10' -> 'macOS 10'.
|
Added a follow-up commit fixing spelling/wording. Note on localization: three of the fixed strings were actual Left alone on purpose: |
The custom polling count is a free-form text field in Settings > Displays, and its value was stored unvalidated. A negative number trapped the app at launch: setupCurrentAndMaxValues converts the count with UInt(self.pollingCount), and UInt(-1) is a fatal 'Negative value is not representable' crash (both on the startup DDC read path and on the mute setup path). - Clamp the value when saving in pollingCountValueChanged (negative becomes 0, matching the 'no polling' behaviour). - Guard readDDCValues as well, so a stale negative value already in preferences from an older version is contained instead of trapped.
|
Follow-up from the second review pass: found and fixed a fatal crash on the custom DDC polling count (commit 3 of this PR). The bug: the custom polling count in Settings > Displays is a free-form text field, stored with no validation. A negative value crashed the app at launch — Investigated and confirmed NOT bugs:
|
…rash Review follow-up: the previous fix clamped in the Settings editor and guarded readDDCValues, but both setup paths convert the stored count with UInt(self.pollingCount) *before* readDDCValues is called, so a stale negative preference still trapped at startup. Clamp in the pollingCount getter (and setter) instead, which is the single source every consumer reads through: - UInt(self.pollingCount) at both call sites is now always safe. - A stale negative value reads back as 0, i.e. 'no polling'. The redundant safeTries clamp inside readDDCValues goes back to a plain zero-tries early return.
|
Good catch — fixed in the latest commit. The clamp now lives in the |
|
@greptile-apps[bot] re-review |
- The getter's default arm returned PollingMode.none.rawValue (-2) for
an out-of-range stored polling mode. That passed the '!= 0' guards
and still trapped at UInt(-2). Return 0 ('no polling') instead.
- Add an upper bound of 255 to the custom count, matching the Arm64
retry cap, so a huge stored value cannot hang the Intel DDC read
loop (1 ... tries) for minutes at startup.
|
Want your agent to iterate on Greptile's feedback? Start a greploop in Codex and it will work through the open comments and keep going until this PR reviews clean. |
|
Second-opinion review complete (independent reviewer + Greptile). Two further holes were found in the polling count hardening and fixed in the latest commit:
Also verified clean by the second review: all 19 localization catalogs byte-exact match the 3 changed keys, Arm64/Intel DDC loop fixes off-by-one free, and all refactors behavior-preserving. Noted but not fixed (pre-existing, unreachable via normal use): |
Summary
A pass over the app with two goals: remove the leftovers and dead code that had accumulated, and harden the few spots where a plausible value could crash the app. No intentional behavior changes beyond the crash fixes; every edit is syntax-checked, and the macOS CI build/tests gate the merge.
Fixes
UInt8overflow trap inArm64DDC.performDDCCommunication— the retry loop computed(numOfRetryAttemps ?? 4) + 1onUInt8.OtherDisplay.readDDCValuesclamps its retry count to exactly 255, and255 + 1is a fatal arithmetic overflow. The loop now does its math inInt. (Custom polling counts ≥ 255 on an Apple Silicon Mac hit this.)IntelDDC.read—for i in 1 ... triestraps whentries == 0(Range requires lowerBound <= upperBound). The loop count is now clamped to at least one try.Bundle.main.bundleIdentifierinAppDelegate.setStartAtLoginandMainPrefsViewController.populateSettings— replaced with safe unwrapping.MenuslidersPrefsViewControllerregisters an observer formenuIconbut never removed it; it is now removed indeinit.Refactors (no behavior change)
DisplayManager.sortDisplays()(never called, debug-era comments) and the misleading comment ongetAllDisplays(). Added a doc note explaining whysortDisplaysByFriendlyNamedeliberately returns descending order (callers insert menu blocks at index 0, so that renders ascending) — it currently reads like a bug.addDisplayCounterSuffixes()rewritten withDictionary(grouping:by:)+enumerated().OtherDisplay.setupCurrentAndMaxValues(theswitchalready sets it).MenuHandler.clearMenu()usesNSMenu.removeAllItems(); removed a? true : false,count > 0→!isEmpty.SliderHandler.setValuecomputes min/max/count fromvalues.valuesinstead of a manual key loop.MediaKeyTapManager'shasExternalDisplayscan replaced withcontains(where:).Display.isBuiltIn,Display.isSwBrightnessNotDefault,OtherDisplay.isSw) and simplifiedDisplay.getKey,Arm64DDC.read(early return), andUpdaterDelegate.allowedChannels.IntelDDC.read's unusedreplyTransactionTypeparameter (no caller passed it).Validation
swiftc -parsepasses on all 12 edited files (full build requires macOS, so CI is the gate).XDRBrightnessTests,SettingsPanesTests,IntelDDCTests) do not touch any changed code path; the two DDC fixes only affect inputs that previously trapped.