mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-03-25 09:25:47 +00:00
Revert to AsyncParsableCommand with parse-as-library
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
3f4a7c864b
commit
17e73f12f2
5 changed files with 16 additions and 125 deletions
|
|
@ -22,7 +22,8 @@ let package = Package(
|
|||
.product(name: "ArgumentParser", package: "swift-argument-parser")
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency")
|
||||
.enableExperimentalFeature("StrictConcurrency"),
|
||||
.unsafeFlags(["-parse-as-library"])
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
|
|
|||
|
|
@ -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<T>: @unchecked Sendable {
|
||||
private var _result: Result<T, Error>?
|
||||
private let lock = NSLock()
|
||||
|
||||
var result: Result<T, Error>? {
|
||||
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<T: Sendable>(_ asyncWork: @escaping @Sendable () async throws -> T) throws -> T {
|
||||
let resultBox = ResultBox<T>()
|
||||
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 ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
Loading…
Reference in a new issue