From 17e73f12f2111ae39de117bb1fef388b362e9912 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Jun 2025 12:09:43 +0100 Subject: [PATCH] Revert to AsyncParsableCommand with parse-as-library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove problematic AsyncAdapter that was causing continuation leaks - Use AsyncParsableCommand directly with @main attribute - Add -parse-as-library flag to Package.swift to enable @main - This fixes the Swift continuation leak issue Note: Integration tests still timeout in CI environment, likely due to screen capture permissions or environment differences. The CLI works correctly when run directly. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- peekaboo-cli/Package.swift | 3 +- .../Sources/peekaboo/AsyncAdapter.swift | 108 ------------------ .../Sources/peekaboo/ImageCommand.swift | 4 +- .../Sources/peekaboo/ListCommand.swift | 16 +-- peekaboo-cli/Sources/peekaboo/main.swift | 10 +- 5 files changed, 16 insertions(+), 125 deletions(-) delete mode 100644 peekaboo-cli/Sources/peekaboo/AsyncAdapter.swift diff --git a/peekaboo-cli/Package.swift b/peekaboo-cli/Package.swift index 3c3a427..dc17558 100644 --- a/peekaboo-cli/Package.swift +++ b/peekaboo-cli/Package.swift @@ -22,7 +22,8 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser") ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency") + .enableExperimentalFeature("StrictConcurrency"), + .unsafeFlags(["-parse-as-library"]) ] ), .testTarget( diff --git a/peekaboo-cli/Sources/peekaboo/AsyncAdapter.swift b/peekaboo-cli/Sources/peekaboo/AsyncAdapter.swift deleted file mode 100644 index ac761bf..0000000 --- a/peekaboo-cli/Sources/peekaboo/AsyncAdapter.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Foundation -import ArgumentParser - -// MARK: - Adapter for AsyncParsableCommand to ParsableCommand bridge - -protocol AsyncRunnable { - func runAsync() async throws -} - -// Thread-safe result container -private final class ResultBox: @unchecked Sendable { - private var _result: Result? - private let lock = NSLock() - - var result: Result? { - get { - lock.lock() - defer { lock.unlock() } - return _result - } - set { - lock.lock() - defer { lock.unlock() } - _result = newValue - } - } -} - -// Helper to run async code synchronously -private func runAsyncBlocking(_ asyncWork: @escaping @Sendable () async throws -> T) throws -> T { - let resultBox = ResultBox() - let semaphore = DispatchSemaphore(value: 0) - - Task.detached { - do { - let value = try await asyncWork() - resultBox.result = .success(value) - } catch { - resultBox.result = .failure(error) - } - semaphore.signal() - } - - semaphore.wait() - - switch resultBox.result { - case .success(let value): - return value - case .failure(let error): - throw error - case .none: - fatalError("Async operation did not complete") - } -} - -extension PeekabooCommand { - func run() throws { - try runAsyncBlocking { - try await (self as AsyncRunnable).runAsync() - return () - } - } -} - -extension ImageCommand { - func run() throws { - try runAsyncBlocking { - try await (self as AsyncRunnable).runAsync() - return () - } - } -} - -extension ListCommand { - func run() throws { - try runAsyncBlocking { - try await (self as AsyncRunnable).runAsync() - return () - } - } -} - -extension AppsSubcommand { - func run() throws { - try runAsyncBlocking { - try await (self as AsyncRunnable).runAsync() - return () - } - } -} - -extension WindowsSubcommand { - func run() throws { - try runAsyncBlocking { - try await (self as AsyncRunnable).runAsync() - return () - } - } -} - -extension ServerStatusSubcommand { - func run() throws { - try runAsyncBlocking { - try await (self as AsyncRunnable).runAsync() - return () - } - } -} \ No newline at end of file diff --git a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift index a48a7b0..bc29e40 100644 --- a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift @@ -19,7 +19,7 @@ struct FileHandleTextOutputStream: TextOutputStream { } } -struct ImageCommand: ParsableCommand, AsyncRunnable { +struct ImageCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "image", abstract: "Capture screen or window images" @@ -52,7 +52,7 @@ struct ImageCommand: ParsableCommand, AsyncRunnable { @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false - func runAsync() async throws { + func run() async throws { Logger.shared.setJsonOutputMode(jsonOutput) do { try PermissionsChecker.requireScreenRecordingPermission() diff --git a/peekaboo-cli/Sources/peekaboo/ListCommand.swift b/peekaboo-cli/Sources/peekaboo/ListCommand.swift index 811ad31..c0dee03 100644 --- a/peekaboo-cli/Sources/peekaboo/ListCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ListCommand.swift @@ -2,7 +2,7 @@ import AppKit import ArgumentParser import Foundation -struct ListCommand: ParsableCommand, AsyncRunnable { +struct ListCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "list", abstract: "List running applications or windows", @@ -10,12 +10,12 @@ struct ListCommand: ParsableCommand, AsyncRunnable { defaultSubcommand: AppsSubcommand.self ) - func runAsync() async throws { + func run() async throws { // Root command doesn't do anything, subcommands handle everything } } -struct AppsSubcommand: ParsableCommand, AsyncRunnable { +struct AppsSubcommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "apps", abstract: "List all running applications" @@ -24,7 +24,7 @@ struct AppsSubcommand: ParsableCommand, AsyncRunnable { @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false - func runAsync() async throws { + func run() async throws { Logger.shared.setJsonOutputMode(jsonOutput) do { @@ -102,7 +102,7 @@ struct AppsSubcommand: ParsableCommand, AsyncRunnable { } } -struct WindowsSubcommand: ParsableCommand, AsyncRunnable { +struct WindowsSubcommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "windows", abstract: "List windows for a specific application" @@ -117,7 +117,7 @@ struct WindowsSubcommand: ParsableCommand, AsyncRunnable { @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false - func runAsync() async throws { + func run() async throws { Logger.shared.setJsonOutputMode(jsonOutput) do { @@ -249,7 +249,7 @@ struct WindowsSubcommand: ParsableCommand, AsyncRunnable { } } -struct ServerStatusSubcommand: ParsableCommand, AsyncRunnable { +struct ServerStatusSubcommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "server_status", abstract: "Check server permissions status" @@ -258,7 +258,7 @@ struct ServerStatusSubcommand: ParsableCommand, AsyncRunnable { @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false - func runAsync() async throws { + func run() async throws { Logger.shared.setJsonOutputMode(jsonOutput) let screenRecording = PermissionsChecker.checkScreenRecordingPermission() diff --git a/peekaboo-cli/Sources/peekaboo/main.swift b/peekaboo-cli/Sources/peekaboo/main.swift index d6c3ed5..411558e 100644 --- a/peekaboo-cli/Sources/peekaboo/main.swift +++ b/peekaboo-cli/Sources/peekaboo/main.swift @@ -1,8 +1,9 @@ import ArgumentParser import Foundation +@main @available(macOS 10.15, *) -struct PeekabooCommand: ParsableCommand, AsyncRunnable { +struct PeekabooCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "peekaboo", abstract: "A macOS utility for screen capture, application listing, and window management", @@ -11,10 +12,7 @@ struct PeekabooCommand: ParsableCommand, AsyncRunnable { defaultSubcommand: ImageCommand.self ) - func runAsync() async throws { + func run() async throws { // Root command doesn't do anything, subcommands handle everything } -} - -// Entry point -PeekabooCommand.main() \ No newline at end of file +} \ No newline at end of file