feat(print): capture native print ticket on panel OK (#201 Phase 2) #204

Merged
gronod merged 1 commits from feat/201-ticket-capture into milestone/m12-native-spool 2026-09-17 13:50:47 +01:00
Owner

Summary

Phase 2 of the M12 megaplan for #201: capture the native print ticket when the user confirms the bound NSPrintPanel, so the Phase 4 NSPrintOperation spooler can replay the vendor PDE state lp -o could never carry.

Changes

  • New PrintTicket.swift — PrintTicket (queue, printSettings, pageFormat, printInfoPlist, capturedAt; Equatable, Sendable, session-only) and PanelCaptureResult (PrintPropertiesResult + optional ticket).
  • PMTicketBridge.serialise — PMPrintSettingsCreateDataRepresentation / PMPageFormatCreateDataRepresentation with kPMDataFormatXMLDefault (+1 CFData out-params consumed via takeRetainedValue()), plus a PropertyListSerialization binary-plist fallback of a recursively plist-filtered NSPrintInfo.dictionary() (one non-plist attribute cannot fail the snapshot).
  • PMTicketBridge.restore — refuses cross-queue replay (R3: compares ticket.queue against the session's current printer; unbound destination accepts); create → PMCopyPrintSettings → PMSessionValidatePrintSettings (logs .info when changed) → updateFromPMPrintSettings; same for the page format, warn-only.
  • PrintPanelService.showProperties returns PanelCaptureResult?; adds layer ⑦ (try? serialise, warn-only so a failure can't lose the Stage 2 mirror).
  • PrintSessionViewModel — @Published capturedTickets: [String: PrintTicket]; stored keyed by ticket.queue.

Bug found & fixed

PMSessionGetCurrentPrinter hands back the session's own printer (borrowed), but the Phase-1-extracted currentPrinterID was PMRelease-ing it — an over-release that dangled the session and reproducibly crashed AppKit's _printerInPrintSession and NSPrintInfo teardown under XCTest's memory checker. Fixed and documented in the ownership contract.

Gate evidence

  • xcodebuild test -only-testing:ICCeryCoreTests (x86_64): 488/488 pass — includes the 5 new PrintTicketTests (round-trip retains EPIJ_Qual=305, <?xml prefix, cross-queue refusal leaves target untouched, printInfoPlist round-trip contains com.apple.print.PrintSettings, 200× serialise stability canary).
  • ASan + Malloc Scribble (-enableAddressSanitizer YES, MallocScribble=1): all 5 PrintTicketTests pass with no sanitizer diagnostics — confirms takeRetainedValue/PMRelease ownership is balanced.
## Summary Phase 2 of the M12 megaplan for #201: capture the native print ticket when the user confirms the bound `NSPrintPanel`, so the Phase 4 `NSPrintOperation` spooler can replay the vendor PDE state `lp -o` could never carry. ## Changes - **New `PrintTicket.swift`** — `PrintTicket` (`queue`, `printSettings`, `pageFormat`, `printInfoPlist`, `capturedAt`; `Equatable`, `Sendable`, session-only) and `PanelCaptureResult` (`PrintPropertiesResult` + optional ticket). - **`PMTicketBridge.serialise`** — `PMPrintSettingsCreateDataRepresentation` / `PMPageFormatCreateDataRepresentation` with `kPMDataFormatXMLDefault` (+1 CFData out-params consumed via `takeRetainedValue()`), plus a `PropertyListSerialization` binary-plist fallback of a recursively plist-filtered `NSPrintInfo.dictionary()` (one non-plist attribute cannot fail the snapshot). - **`PMTicketBridge.restore`** — refuses cross-queue replay (R3: compares `ticket.queue` against the session's current printer; unbound destination accepts); create → `PMCopyPrintSettings` → `PMSessionValidatePrintSettings` (logs `.info` when `changed`) → `updateFromPMPrintSettings`; same for the page format, warn-only. - **`PrintPanelService.showProperties`** returns `PanelCaptureResult?`; adds layer ⑦ (`try? serialise`, warn-only so a failure can't lose the Stage 2 mirror). - **`PrintSessionViewModel`** — `@Published capturedTickets: [String: PrintTicket]`; stored keyed by `ticket.queue`. ## Bug found & fixed `PMSessionGetCurrentPrinter` hands back the session's **own** printer (borrowed), but the Phase-1-extracted `currentPrinterID` was `PMRelease`-ing it — an over-release that dangled the session and reproducibly crashed AppKit's `_printerInPrintSession` and `NSPrintInfo` teardown under XCTest's memory checker. Fixed and documented in the ownership contract. ## Gate evidence - `xcodebuild test -only-testing:ICCeryCoreTests` (x86_64): **488/488 pass** — includes the 5 new `PrintTicketTests` (round-trip retains `EPIJ_Qual=305`, `<?xml` prefix, cross-queue refusal leaves target untouched, `printInfoPlist` round-trip contains `com.apple.print.PrintSettings`, 200× serialise stability canary). - **ASan + Malloc Scribble** (`-enableAddressSanitizer YES`, `MallocScribble=1`): all 5 `PrintTicketTests` pass with **no sanitizer diagnostics** — confirms `takeRetainedValue`/`PMRelease` ownership is balanced.
gronod added 1 commit 2026-09-17 13:47:47 +01:00
Serialise PMPrintSettings/PMPageFormat to XML data plus a plist-safe
NSPrintInfo dictionary snapshot (PrintTicket), restore via
PM*CreateWithDataRepresentation -> PMCopy* -> PMSessionValidate* ->
updateFromPM*, and refuse cross-queue replay (R3).

PrintPanelService.showProperties now returns PanelCaptureResult; the
view model stores tickets in capturedTickets keyed by queue.

Also fixes a latent over-release: PMSessionGetCurrentPrinter hands back
the session's own printer (borrowed) — the extracted currentPrinterID
was PMRelease-ing it, which dangled the session and crashed AppKit's
_printerInPrintSession path and session teardown.
gronod merged commit 1d6032456d into milestone/m12-native-spool 2026-09-17 13:50:47 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gronod/iccery-v2-mac#204