Explorar el Código

Fix repeat scans by keeping the scanner session alive between panel opens.

Stop tearing down and reconnecting on every close, which broke icdd and failed the second scan; only fully release when leaving the scan feature.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 hace 1 mes
padre
commit
cb2757d11f

+ 8 - 9
smart_printer/ScannerOverlayView.swift

@@ -24,7 +24,7 @@ final class ScannerPanelController: NSObject, NSWindowDelegate {
     ) {
         Task { @MainActor in
             if let active = activeController {
-                await active.teardownPanel()
+                active.teardownPanel()
             }
             let controller = ScannerPanelController(
                 parentWindow: parentWindow,
@@ -93,8 +93,8 @@ final class ScannerPanelController: NSObject, NSWindowDelegate {
         panel.makeKeyAndOrderFront(nil)
     }
 
-    private func teardownPanel() async {
-        await panelView?.teardown()
+    private func teardownPanel() {
+        panelView?.teardown()
         panel?.orderOut(nil)
         panel = nil
         panelView = nil
@@ -127,12 +127,12 @@ final class ScannerPanelController: NSObject, NSWindowDelegate {
     func windowWillClose(_ notification: Notification) {
         let cancelled = !didComplete && panelView?.hasResult != true
         Task { @MainActor in
-            await finish(cancelled: cancelled)
+            finish(cancelled: cancelled)
         }
     }
 
-    private func finish(cancelled: Bool) async {
-        await panelView?.teardown()
+    private func finish(cancelled: Bool) {
+        panelView?.teardown()
         panel?.orderOut(nil)
         panel = nil
         panelView = nil
@@ -235,12 +235,12 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         updateStateUI(scanner.state)
     }
 
-    func teardown() async {
+    func teardown() {
         scanner.onStateChanged = nil
         scanner.onDevicesChanged = nil
         scanner.onSelectionChanged = nil
         scanner.onScanComplete = nil
-        await scanner.stopBrowsing()
+        scanner.detachFromUI()
     }
 
     func showResult(_ image: NSImage) {
@@ -249,7 +249,6 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
         resultImageView.image = image
         scanPhaseView.isHidden = true
         resultPhaseView.isHidden = false
-        // Session stays open until the panel closes; avoids breaking the next scan.
     }
 
     func refreshAppearance() {

+ 153 - 276
smart_printer/ScannerService.swift

@@ -48,14 +48,11 @@ final class ScannerService: NSObject {
     private var accumulatedBands: [ICScannerBandData] = []
     private var isClosingSession = false
     private var isOpeningSession = false
-    private var sessionOpenRetryCount = 0
     private var lastSelectedDeviceName: String?
     private var pendingPreferredDeviceName: String?
     private var sessionCloseContinuation: CheckedContinuation<Void, Never>?
-    private let maxSessionOpenRetries = 4
-    private let connectionTimeoutSeconds: UInt64 = 15
-    private let sessionCloseTimeoutSeconds: UInt64 = 4
-    private let browserRestartDelayNanoseconds: UInt64 = 700_000_000
+    private let connectionTimeoutSeconds: UInt64 = 20
+    private let sessionCloseTimeoutSeconds: UInt64 = 6
 
     private override init() {
         super.init()
@@ -67,91 +64,167 @@ final class ScannerService: NSObject {
         browser.browsedDeviceTypeMask = ICDeviceTypeMask(rawValue: maskRaw) ?? .scanner
     }
 
-    func startBrowsing() {
-        switch state {
-        case .scanning:
-            return
-        case .idle, .error:
-            state = .browsing
-            browser.start()
-        case .browsing, .connecting, .ready:
-            if localDevices.isEmpty && sharedDevices.isEmpty {
-                browser.start()
-            }
-        }
-    }
-
-    /// Prepares a completely fresh scanner session, same as the first scan.
+    /// Opens the scanner panel. Reuses an existing session when one is already open.
     func prepareForScanning() async {
         clearConnectionTimeout()
         isOpeningSession = false
-        sessionOpenRetryCount = 0
+        isClosingSession = false
         didFinishCurrentScan = false
         shouldScanWhenReady = false
         scanCompletion = nil
         scannedImageURL = nil
         accumulatedBands = []
-        pendingPreferredDeviceName = lastSelectedDeviceName
 
-        browser.stop()
-        await closeOpenSessionIfNeeded()
-        resetDeviceLists()
+        if let device = selectedDevice, device.hasOpenSession {
+            device.delegate = self
+            state = .ready
+            ensureBrowsing()
+            return
+        }
+
+        if let device = deviceMatchingLastSelection(), device.hasOpenSession {
+            selectedDevice = device
+            device.delegate = self
+            state = .ready
+            ensureBrowsing()
+            return
+        }
 
-        try? await Task.sleep(nanoseconds: browserRestartDelayNanoseconds)
-        browser.start()
+        pendingPreferredDeviceName = lastSelectedDeviceName
+        ensureBrowsing()
         state = .browsing
     }
 
-    func stopBrowsing() async {
+    /// Panel closed — keep the scanner session alive for the next open.
+    func detachFromUI() {
         clearConnectionTimeout()
-        browser.stop()
-        rememberSelectedDeviceName()
-        await closeOpenSessionIfNeeded()
-        resetFlagsAfterShutdown()
+        scanCompletion = nil
+        shouldScanWhenReady = false
     }
 
-    private func rememberSelectedDeviceName() {
+    /// Fully releases the scanner when leaving the scan feature entirely.
+    func releaseScanner() async {
+        clearConnectionTimeout()
+        detachFromUI()
+
         if let device = selectedDevice {
-            let name = device.name ?? device.locationDescription
-            if let name { lastSelectedDeviceName = name }
+            await closeSession(on: device)
         }
+
+        if browser.isBrowsing {
+            browser.stop()
+        }
+
+        resetAfterFullShutdown()
     }
 
-    private func resetDeviceLists() {
-        selectedDevice = nil
-        localDevices = []
-        sharedDevices = []
+    func selectDevice(_ device: ICScannerDevice) {
+        let name = device.name ?? device.locationDescription
+        if let name { lastSelectedDeviceName = name }
+        pendingPreferredDeviceName = nil
+
+        if selectedDevice === device {
+            if device.hasOpenSession {
+                device.delegate = self
+                state = .ready
+            } else if case .connecting = state {
+                return
+            } else {
+                Task { await openSession(on: device) }
+            }
+            return
+        }
+
+        selectedDevice = device
+
+        if device.hasOpenSession {
+            device.delegate = self
+            state = .ready
+        } else {
+            Task { await openSession(on: device) }
+        }
     }
 
-    private func resetFlagsAfterShutdown() {
+    func scan() {
+        guard let device = selectedDevice else { return }
+        scanCompletion = onScanComplete
+        scannedImageURL = nil
+        didFinishCurrentScan = false
+        accumulatedBands = []
+
+        if case .connecting = state {
+            shouldScanWhenReady = true
+            return
+        }
+
+        device.delegate = self
+        if device.hasOpenSession {
+            configureFunctionalUnit(on: device)
+            state = .scanning
+            device.requestScan()
+        } else {
+            shouldScanWhenReady = true
+            Task { await openSession(on: device) }
+        }
+    }
+
+    func cancelScan() {
+        selectedDevice?.cancelScan()
+        scanCompletion?(.failure(ScanError.cancelled))
+        scanCompletion = nil
+        scannedImageURL = nil
+        didFinishCurrentScan = true
+        accumulatedBands = []
+        state = selectedDevice?.hasOpenSession == true ? .ready : .browsing
+    }
+
+    private func ensureBrowsing() {
+        if !browser.isBrowsing {
+            browser.start()
+        }
+    }
+
+    private func deviceMatchingLastSelection() -> ICScannerDevice? {
+        guard let name = lastSelectedDeviceName else { return nil }
+        return (localDevices + sharedDevices).first {
+            ($0.name ?? $0.locationDescription) == name
+        }
+    }
+
+    private func resetAfterFullShutdown() {
         isClosingSession = false
         isOpeningSession = false
         pendingPreferredDeviceName = nil
-        resetDeviceLists()
+        selectedDevice = nil
+        localDevices = []
+        sharedDevices = []
         state = .idle
-        sessionOpenRetryCount = 0
-        scanCompletion = nil
         scannedImageURL = nil
         didFinishCurrentScan = false
         accumulatedBands = []
         shouldScanWhenReady = false
     }
 
-    private func closeOpenSessionIfNeeded() async {
-        guard let device = selectedDevice else {
-            isClosingSession = false
-            return
-        }
+    private func closeSession(on device: ICScannerDevice) async {
         device.delegate = nil
+        if state == .scanning {
+            device.cancelScan()
+        }
         guard device.hasOpenSession else {
             isClosingSession = false
             return
         }
         isClosingSession = true
-        await closeDeviceSessionAndWait(device)
+        do {
+            try await device.requestCloseSession()
+            processSessionClosed(on: device, error: nil)
+        } catch {
+            processSessionClosed(on: device, error: error)
+        }
+        await waitForSessionToCloseAsync()
     }
 
-    private func waitForSessionToClose() async {
+    private func waitForSessionToCloseAsync() async {
         if !isClosingSession, selectedDevice?.hasOpenSession != true {
             return
         }
@@ -171,37 +244,34 @@ final class ScannerService: NSObject {
         continuation.resume()
     }
 
-    private func sendOpenSession(to device: ICScannerDevice) {
-        Task { @MainActor in
-            guard selectedDevice === device else { return }
-            do {
-                try await device.requestOpenSession()
-                processSessionOpened(on: device, error: nil)
-            } catch {
-                processSessionOpened(on: device, error: error)
-            }
-        }
-    }
+    private func openSession(on device: ICScannerDevice) async {
+        guard selectedDevice === device else { return }
 
-    private func sendCloseSession(for device: ICScannerDevice) {
-        Task { @MainActor in
-            do {
-                try await device.requestCloseSession()
-                processSessionClosed(on: device, error: nil)
-            } catch {
-                processSessionClosed(on: device, error: error)
+        clearConnectionTimeout()
+        device.delegate = self
+
+        if device.hasOpenSession {
+            isOpeningSession = false
+            state = .ready
+            if shouldScanWhenReady {
+                shouldScanWhenReady = false
+                configureFunctionalUnit(on: device)
+                state = .scanning
+                device.requestScan()
             }
+            return
         }
-    }
 
-    private func closeDeviceSessionAndWait(_ device: ICScannerDevice) async {
+        isOpeningSession = true
+        state = .connecting
+        scheduleConnectionTimeout(for: device)
+
         do {
-            try await device.requestCloseSession()
-            processSessionClosed(on: device, error: nil)
+            try await device.requestOpenSession()
+            processSessionOpened(on: device, error: nil)
         } catch {
-            processSessionClosed(on: device, error: error)
+            processSessionOpened(on: device, error: error)
         }
-        await waitForSessionToClose()
     }
 
     private func processSessionOpened(on scanner: ICScannerDevice, error: Error?) {
@@ -212,24 +282,12 @@ final class ScannerService: NSObject {
         isOpeningSession = false
 
         if let error {
-            if sessionOpenRetryCount < maxSessionOpenRetries {
-                sessionOpenRetryCount += 1
-                let delay = UInt64(sessionOpenRetryCount) * 600_000_000
-                Task { @MainActor in
-                    try? await Task.sleep(nanoseconds: delay)
-                    guard self.selectedDevice === scanner else { return }
-                    self.openSession(on: scanner)
-                }
-                return
-            }
-            sessionOpenRetryCount = 0
             state = .error(error.localizedDescription)
             finishScan(with: .failure(error))
             return
         }
 
         isClosingSession = false
-        sessionOpenRetryCount = 0
         scanner.delegate = self
         if shouldScanWhenReady {
             shouldScanWhenReady = false
@@ -242,160 +300,27 @@ final class ScannerService: NSObject {
     }
 
     private func processSessionClosed(on device: ICDevice, error: Error?) {
-        sessionOpenRetryCount = 0
+        let wasClosing = isClosingSession
         resumeSessionCloseWait()
+        guard wasClosing else { return }
         if selectedDevice === device, state != .idle {
             state = .browsing
         }
     }
 
-    /// Keeps the scanner session alive for subsequent scans (e.g. Add Page).
-    func resetAfterScan() {
+    /// Keeps the scanner session open after each scan for the next one.
+    private func resetAfterScan() {
         clearConnectionTimeout()
         scanCompletion = nil
         scannedImageURL = nil
         didFinishCurrentScan = false
         accumulatedBands = []
         shouldScanWhenReady = false
-        sessionOpenRetryCount = 0
         isOpeningSession = false
 
         if let device = selectedDevice, device.hasOpenSession {
             device.delegate = self
             state = .ready
-        } else if let device = selectedDevice {
-            if isClosingSession {
-                waitForSessionClose { [weak self] in
-                    guard let self, self.selectedDevice === device else { return }
-                    self.openSession(on: device)
-                }
-            } else {
-                openSession(on: device)
-            }
-        } else if state == .idle {
-            startBrowsing()
-        }
-    }
-
-    func selectDevice(_ device: ICScannerDevice) {
-        let name = device.name ?? device.locationDescription
-        if let name { lastSelectedDeviceName = name }
-        pendingPreferredDeviceName = nil
-
-        if selectedDevice === device {
-            retryConnection(to: device)
-            return
-        }
-
-        clearConnectionTimeout()
-        isOpeningSession = false
-        closeSessionIfNeeded()
-        selectedDevice = device
-
-        let connect = { [weak self] in
-            guard let self, self.selectedDevice === device else { return }
-            self.openSession(on: device)
-        }
-        if isClosingSession {
-            waitForSessionClose(then: connect)
-        } else {
-            connect()
-        }
-    }
-
-    func scan() {
-        guard let device = selectedDevice else { return }
-        scanCompletion = onScanComplete
-        scannedImageURL = nil
-        didFinishCurrentScan = false
-        accumulatedBands = []
-
-        let performScan = { [weak self] in
-            guard let self, let device = self.selectedDevice else { return }
-            device.delegate = self
-            if device.hasOpenSession {
-                self.configureFunctionalUnit(on: device)
-                self.state = .scanning
-                device.requestScan()
-            } else {
-                self.shouldScanWhenReady = true
-                self.openSession(on: device)
-            }
-        }
-
-        if isClosingSession {
-            waitForSessionClose(then: performScan)
-        } else if case .connecting = state {
-            shouldScanWhenReady = true
-        } else {
-            performScan()
-        }
-    }
-
-    func cancelScan() {
-        selectedDevice?.cancelScan()
-        scanCompletion?(.failure(ScanError.cancelled))
-        scanCompletion = nil
-        scannedImageURL = nil
-        didFinishCurrentScan = true
-        accumulatedBands = []
-        state = selectedDevice?.hasOpenSession == true ? .ready : .idle
-    }
-
-    private func openSession(on device: ICScannerDevice) {
-        guard selectedDevice === device else { return }
-
-        clearConnectionTimeout()
-        isOpeningSession = false
-        device.delegate = self
-
-        if device.hasOpenSession {
-            state = .ready
-            if shouldScanWhenReady {
-                shouldScanWhenReady = false
-                configureFunctionalUnit(on: device)
-                state = .scanning
-                device.requestScan()
-            }
-            return
-        }
-
-        isOpeningSession = true
-        state = .connecting
-        sendOpenSession(to: device)
-        scheduleConnectionTimeout(for: device)
-    }
-
-    private func retryConnection(to device: ICScannerDevice) {
-        if device.hasOpenSession {
-            clearConnectionTimeout()
-            isOpeningSession = false
-            device.delegate = self
-            state = .ready
-            return
-        }
-
-        clearConnectionTimeout()
-        isOpeningSession = false
-        device.delegate = nil
-
-        let reconnect = { [weak self] in
-            guard let self, self.selectedDevice === device else { return }
-            self.openSession(on: device)
-        }
-
-        if device.hasOpenSession || isClosingSession {
-            if device.hasOpenSession {
-                isClosingSession = true
-                sendCloseSession(for: device)
-            }
-            waitForSessionClose(then: reconnect)
-        } else if case .connecting = state {
-            reconnect()
-        } else if !isClosingSession {
-            reconnect()
-        } else {
-            waitForSessionClose(then: reconnect)
         }
     }
 
@@ -407,20 +332,10 @@ final class ScannerService: NSObject {
             try? await Task.sleep(nanoseconds: connectionTimeoutSeconds * 1_000_000_000)
             guard !Task.isCancelled else { return }
             guard selectedDevice === device, case .connecting = state else { return }
-            handleConnectionTimeout(for: device)
-        }
-    }
-
-    private func handleConnectionTimeout(for device: ICScannerDevice) {
-        isOpeningSession = false
-        device.delegate = nil
-        if device.hasOpenSession {
-            isClosingSession = true
-            sendCloseSession(for: device)
-        } else {
-            isClosingSession = false
+            isOpeningSession = false
+            device.delegate = nil
+            state = .error("Could not connect to the scanner. Select it again to retry.")
         }
-        state = .error("Could not connect to the scanner. Select it again to retry.")
     }
 
     private func clearConnectionTimeout() {
@@ -428,34 +343,6 @@ final class ScannerService: NSObject {
         connectionTimeoutTask = nil
     }
 
-    private func waitForSessionClose(then action: @escaping () -> Void) {
-        Task { @MainActor in
-            for _ in 0..<40 {
-                if !isClosingSession { break }
-                if selectedDevice?.hasOpenSession != true {
-                    isClosingSession = false
-                    break
-                }
-                try? await Task.sleep(nanoseconds: 250_000_000)
-            }
-            isClosingSession = false
-            action()
-        }
-    }
-
-    private func closeSessionIfNeeded() {
-        guard let device = selectedDevice else { return }
-        device.delegate = nil
-        if device.hasOpenSession {
-            isClosingSession = true
-            sendCloseSession(for: device)
-        }
-    }
-
-    private func disconnectCurrentDevice() {
-        closeSessionIfNeeded()
-    }
-
     private func configureFunctionalUnit(on device: ICScannerDevice) {
         let unit = device.selectedFunctionalUnit
         unit.pixelDataType = .RGB
@@ -547,18 +434,7 @@ final class ScannerService: NSObject {
         guard !didFinishCurrentScan else { return }
         didFinishCurrentScan = true
         scanCompletion?(result)
-        scanCompletion = nil
-        scannedImageURL = nil
-        accumulatedBands = []
-
-        if let device = selectedDevice {
-            device.delegate = nil
-            if device.hasOpenSession {
-                isClosingSession = true
-                sendCloseSession(for: device)
-            }
-        }
-        state = .browsing
+        resetAfterScan()
     }
 
     private func imageFromScanURL(_ url: URL) -> NSImage? {
@@ -667,6 +543,7 @@ extension ScannerService: ICDeviceDelegate, ICScannerDeviceDelegate {
     nonisolated func scannerDeviceDidBecomeAvailable(_ scanner: ICScannerDevice) {
         Task { @MainActor in
             guard selectedDevice === scanner else { return }
+            guard state == .connecting || isOpeningSession else { return }
             clearConnectionTimeout()
             isOpeningSession = false
             isClosingSession = false

+ 1 - 1
smart_printer/ViewController.swift

@@ -175,7 +175,7 @@ class ViewController: NSViewController {
         overlay.onDismiss = { [weak self] in
             self?.scanFileOverlay = nil
             Task { @MainActor in
-                await ScannerService.shared.stopBrowsing()
+                await ScannerService.shared.releaseScanner()
             }
         }
         scanFileOverlay = overlay