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.
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-spool2026-09-17 13:50:47 +01:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Phase 2 of the M12 megaplan for #201: capture the native print ticket when the user confirms the bound
NSPrintPanel, so the Phase 4NSPrintOperationspooler can replay the vendor PDE statelp -ocould never carry.Changes
PrintTicket.swift—PrintTicket(queue,printSettings,pageFormat,printInfoPlist,capturedAt;Equatable,Sendable, session-only) andPanelCaptureResult(PrintPropertiesResult+ optional ticket).PMTicketBridge.serialise—PMPrintSettingsCreateDataRepresentation/PMPageFormatCreateDataRepresentationwithkPMDataFormatXMLDefault(+1 CFData out-params consumed viatakeRetainedValue()), plus aPropertyListSerializationbinary-plist fallback of a recursively plist-filteredNSPrintInfo.dictionary()(one non-plist attribute cannot fail the snapshot).PMTicketBridge.restore— refuses cross-queue replay (R3: comparesticket.queueagainst the session's current printer; unbound destination accepts); create →PMCopyPrintSettings→PMSessionValidatePrintSettings(logs.infowhenchanged) →updateFromPMPrintSettings; same for the page format, warn-only.PrintPanelService.showPropertiesreturnsPanelCaptureResult?; adds layer ⑦ (try? serialise, warn-only so a failure can't lose the Stage 2 mirror).PrintSessionViewModel—@Published capturedTickets: [String: PrintTicket]; stored keyed byticket.queue.Bug found & fixed
PMSessionGetCurrentPrinterhands back the session's own printer (borrowed), but the Phase-1-extractedcurrentPrinterIDwasPMRelease-ing it — an over-release that dangled the session and reproducibly crashed AppKit's_printerInPrintSessionandNSPrintInfoteardown 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 newPrintTicketTests(round-trip retainsEPIJ_Qual=305,<?xmlprefix, cross-queue refusal leaves target untouched,printInfoPlistround-trip containscom.apple.print.PrintSettings, 200× serialise stability canary).-enableAddressSanitizer YES,MallocScribble=1): all 5PrintTicketTestspass with no sanitizer diagnostics — confirmstakeRetainedValue/PMReleaseownership is balanced.