Просмотр исходного кода

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 месяц назад
Родитель
Сommit
cb2757d11f
3 измененных файлов с 162 добавлено и 286 удалено
  1. 8 9
      smart_printer/ScannerOverlayView.swift
  2. 153 276
      smart_printer/ScannerService.swift
  3. 1 1
      smart_printer/ViewController.swift

+ 8 - 9
smart_printer/ScannerOverlayView.swift

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

+ 153 - 276
smart_printer/ScannerService.swift

@@ -48,14 +48,11 @@ final class ScannerService: NSObject {
     private var accumulatedBands: [ICScannerBandData] = []
     private var accumulatedBands: [ICScannerBandData] = []
     private var isClosingSession = false
     private var isClosingSession = false
     private var isOpeningSession = false
     private var isOpeningSession = false
-    private var sessionOpenRetryCount = 0
     private var lastSelectedDeviceName: String?
     private var lastSelectedDeviceName: String?
     private var pendingPreferredDeviceName: String?
     private var pendingPreferredDeviceName: String?
     private var sessionCloseContinuation: CheckedContinuation<Void, Never>?
     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() {
     private override init() {
         super.init()
         super.init()
@@ -67,91 +64,167 @@ final class ScannerService: NSObject {
         browser.browsedDeviceTypeMask = ICDeviceTypeMask(rawValue: maskRaw) ?? .scanner
         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 {
     func prepareForScanning() async {
         clearConnectionTimeout()
         clearConnectionTimeout()
         isOpeningSession = false
         isOpeningSession = false
-        sessionOpenRetryCount = 0
+        isClosingSession = false
         didFinishCurrentScan = false
         didFinishCurrentScan = false
         shouldScanWhenReady = false
         shouldScanWhenReady = false
         scanCompletion = nil
         scanCompletion = nil
         scannedImageURL = nil
         scannedImageURL = nil
         accumulatedBands = []
         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
         state = .browsing
     }
     }
 
 
-    func stopBrowsing() async {
+    /// Panel closed — keep the scanner session alive for the next open.
+    func detachFromUI() {
         clearConnectionTimeout()
         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 {
         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
         isClosingSession = false
         isOpeningSession = false
         isOpeningSession = false
         pendingPreferredDeviceName = nil
         pendingPreferredDeviceName = nil
-        resetDeviceLists()
+        selectedDevice = nil
+        localDevices = []
+        sharedDevices = []
         state = .idle
         state = .idle
-        sessionOpenRetryCount = 0
-        scanCompletion = nil
         scannedImageURL = nil
         scannedImageURL = nil
         didFinishCurrentScan = false
         didFinishCurrentScan = false
         accumulatedBands = []
         accumulatedBands = []
         shouldScanWhenReady = false
         shouldScanWhenReady = false
     }
     }
 
 
-    private func closeOpenSessionIfNeeded() async {
-        guard let device = selectedDevice else {
-            isClosingSession = false
-            return
-        }
+    private func closeSession(on device: ICScannerDevice) async {
         device.delegate = nil
         device.delegate = nil
+        if state == .scanning {
+            device.cancelScan()
+        }
         guard device.hasOpenSession else {
         guard device.hasOpenSession else {
             isClosingSession = false
             isClosingSession = false
             return
             return
         }
         }
         isClosingSession = true
         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 {
         if !isClosingSession, selectedDevice?.hasOpenSession != true {
             return
             return
         }
         }
@@ -171,37 +244,34 @@ final class ScannerService: NSObject {
         continuation.resume()
         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 {
         do {
-            try await device.requestCloseSession()
-            processSessionClosed(on: device, error: nil)
+            try await device.requestOpenSession()
+            processSessionOpened(on: device, error: nil)
         } catch {
         } catch {
-            processSessionClosed(on: device, error: error)
+            processSessionOpened(on: device, error: error)
         }
         }
-        await waitForSessionToClose()
     }
     }
 
 
     private func processSessionOpened(on scanner: ICScannerDevice, error: Error?) {
     private func processSessionOpened(on scanner: ICScannerDevice, error: Error?) {
@@ -212,24 +282,12 @@ final class ScannerService: NSObject {
         isOpeningSession = false
         isOpeningSession = false
 
 
         if let error {
         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)
             state = .error(error.localizedDescription)
             finishScan(with: .failure(error))
             finishScan(with: .failure(error))
             return
             return
         }
         }
 
 
         isClosingSession = false
         isClosingSession = false
-        sessionOpenRetryCount = 0
         scanner.delegate = self
         scanner.delegate = self
         if shouldScanWhenReady {
         if shouldScanWhenReady {
             shouldScanWhenReady = false
             shouldScanWhenReady = false
@@ -242,160 +300,27 @@ final class ScannerService: NSObject {
     }
     }
 
 
     private func processSessionClosed(on device: ICDevice, error: Error?) {
     private func processSessionClosed(on device: ICDevice, error: Error?) {
-        sessionOpenRetryCount = 0
+        let wasClosing = isClosingSession
         resumeSessionCloseWait()
         resumeSessionCloseWait()
+        guard wasClosing else { return }
         if selectedDevice === device, state != .idle {
         if selectedDevice === device, state != .idle {
             state = .browsing
             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()
         clearConnectionTimeout()
         scanCompletion = nil
         scanCompletion = nil
         scannedImageURL = nil
         scannedImageURL = nil
         didFinishCurrentScan = false
         didFinishCurrentScan = false
         accumulatedBands = []
         accumulatedBands = []
         shouldScanWhenReady = false
         shouldScanWhenReady = false
-        sessionOpenRetryCount = 0
         isOpeningSession = false
         isOpeningSession = false
 
 
         if let device = selectedDevice, device.hasOpenSession {
         if let device = selectedDevice, device.hasOpenSession {
             device.delegate = self
             device.delegate = self
             state = .ready
             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)
             try? await Task.sleep(nanoseconds: connectionTimeoutSeconds * 1_000_000_000)
             guard !Task.isCancelled else { return }
             guard !Task.isCancelled else { return }
             guard selectedDevice === device, case .connecting = state 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() {
     private func clearConnectionTimeout() {
@@ -428,34 +343,6 @@ final class ScannerService: NSObject {
         connectionTimeoutTask = nil
         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) {
     private func configureFunctionalUnit(on device: ICScannerDevice) {
         let unit = device.selectedFunctionalUnit
         let unit = device.selectedFunctionalUnit
         unit.pixelDataType = .RGB
         unit.pixelDataType = .RGB
@@ -547,18 +434,7 @@ final class ScannerService: NSObject {
         guard !didFinishCurrentScan else { return }
         guard !didFinishCurrentScan else { return }
         didFinishCurrentScan = true
         didFinishCurrentScan = true
         scanCompletion?(result)
         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? {
     private func imageFromScanURL(_ url: URL) -> NSImage? {
@@ -667,6 +543,7 @@ extension ScannerService: ICDeviceDelegate, ICScannerDeviceDelegate {
     nonisolated func scannerDeviceDidBecomeAvailable(_ scanner: ICScannerDevice) {
     nonisolated func scannerDeviceDidBecomeAvailable(_ scanner: ICScannerDevice) {
         Task { @MainActor in
         Task { @MainActor in
             guard selectedDevice === scanner else { return }
             guard selectedDevice === scanner else { return }
+            guard state == .connecting || isOpeningSession else { return }
             clearConnectionTimeout()
             clearConnectionTimeout()
             isOpeningSession = false
             isOpeningSession = false
             isClosingSession = false
             isClosingSession = false

+ 1 - 1
smart_printer/ViewController.swift

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