Переглянути джерело

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 1 місяць тому
батько
коміт
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