@@ -24,11 +24,12 @@ private struct LaunchMessage: Encodable {
2424}
2525
2626private struct LaunchResult : Encodable {
27- let action = " launch "
27+ let action : String
2828 var outcome = " refused "
2929 var native_outcome : DesktopActionOutcome ?
3030 var application : LaunchedApplication ?
3131 var requires_fresh_observation = false
32+ var message : String ?
3233 var error : LaunchMessage ?
3334}
3435
@@ -38,25 +39,27 @@ private struct LaunchReply: Encodable {
3839 let target_receipt : Receipt ?
3940}
4041
41- func nativeLaunch( _ client: PeekabooBridgeClient , handshake: PeekabooBridgeHandshakeResponse ) async throws -> Data {
42- var result = LaunchResult ( )
42+ func nativeLaunch( _ client: PeekabooBridgeClient , handshake: PeekabooBridgeHandshakeResponse , opensItem : Bool = false ) async throws -> Data {
43+ var result = LaunchResult ( action : opensItem ? " open " : " launch " )
4344 var receipt : Receipt ?
4445 var invoked = false
45- let uncertain = " The app may still open later. Observe desktop_apps before any further action; do not blindly repeat the launch. "
46+ let uncertain = opensItem
47+ ? " The item may still open later. Observe desktop_apps before any further action; do not blindly repeat the open. "
48+ : " The app may still open later. Observe desktop_apps before any further action; do not blindly repeat the launch. "
4649 do {
47- let request = try readLaunch ( )
50+ let request = try readLaunch ( opensItem : opensItem )
4851 guard handshake. supportedOperations. contains ( . launchApplicationWithOptions) ,
4952 ( handshake. enabledOperations ?? handshake. supportedOperations) . contains ( . launchApplicationWithOptions)
5053 else { throw LaunchError ( " The desktop runtime does not support attested application launch. Update the runtime before acting. " ) }
5154 let session = CGSessionCopyCurrentDictionary ( ) as NSDictionary ?
5255 if session ? [ " CGSSessionScreenIsLocked " ] as? Bool == true {
5356 throw DesktopActionFailure . preDispatchRefusal (
5457 reason: . targetUnavailable,
55- message: " The macOS GUI session is locked. Application launch was not dispatched. " ,
58+ message: " The macOS GUI session is locked. The opening operation was not dispatched. " ,
5659 hint: " Unlock the active user session before choosing a new action. "
5760 )
5861 }
59- // Verify signed operation support before a launch whose process does not exist yet .
62+ // The receiving process may not exist until LaunchServices returns it .
6063 _ = try await client. listApplicationMutationInventory ( )
6164 guard let preflight = await client. lastOperationReceipt ( ) , preflight. payload. operation == PeekabooBridgeRequest . listApplicationMutationInventory. operation else {
6265 throw LaunchError ( " The native runtime cannot attest application targets. Update the desktop runtime before acting. " )
@@ -75,10 +78,18 @@ func nativeLaunch(_ client: PeekabooBridgeClient, handshake: PeekabooBridgeHands
7578 result. application = LaunchedApplication ( name: action. payload. name, bundle_id: action. payload. bundleIdentifier,
7679 path: action. payload. bundlePath, target: ManagementTarget ( process) )
7780 result. requires_fresh_observation = outcome. dispatchState. mutationDispatched
78- switch outcome. state {
79- case . confirmedChange, . confirmedNoChange: result. outcome = " completed "
80- case . refused: result. outcome = " refused "
81- default : result. outcome = " unknown "
81+ if opensItem {
82+ guard outcome. state == . dispatchedUnverified, outcome. evidence == . deliveryAccepted else {
83+ throw LaunchError ( " The native runtime did not distinguish accepted item delivery from its effect. Inspect the receiving app before acting again. " )
84+ }
85+ result. outcome = " completed "
86+ result. message = " macOS accepted the item opening request. Its effect is unverified; inspect the receiving app before any further action. "
87+ } else {
88+ switch outcome. state {
89+ case . confirmedChange, . confirmedNoChange: result. outcome = " completed "
90+ case . refused: result. outcome = " refused "
91+ default : result. outcome = " unknown "
92+ }
8293 }
8394 } catch let failure as DesktopActionFailure {
8495 let dispatched = failure. outcome. dispatchState. mutationDispatched
@@ -96,29 +107,57 @@ func nativeLaunch(_ client: PeekabooBridgeClient, handshake: PeekabooBridgeHands
96107 return try JSONEncoder ( ) . encode ( LaunchReply ( data: result, target_receipt: receipt) )
97108}
98109
99- private func readLaunch( ) throws -> ApplicationLaunchRequest {
110+ private func readLaunch( opensItem : Bool ) throws -> ApplicationLaunchRequest {
100111 var data = Data ( )
101112 while let chunk = try FileHandle . standardInput. read ( upToCount: 4096 ) , !chunk. isEmpty {
102113 data. append ( chunk)
103- guard data. count <= 16_384 else { throw LaunchError ( " The launch request exceeds 16 KiB. " ) }
114+ guard data. count <= 16_384 else { throw LaunchError ( " The opening request exceeds 16 KiB. " ) }
104115 }
116+ let operation = opensItem ? " open " : " launch "
117+ let keys : Set < String > = opensItem ? [ " op " , " item " , " application " ] : [ " op " , " application " ]
105118 guard let request = try JSONSerialization . jsonObject ( with: data) as? [ String : Any ] ,
106- Set ( request. keys) == [ " op " , " application " ] , request [ " op " ] as? String == " launch " ,
107- let application = request [ " application " ] as? [ String : Any ] , application. count == 1
108- else { throw LaunchError ( " Launch accepts exactly one application path or bundle_id. " ) }
109- let path = application [ " path " ] as? String
110- let bundleID = application [ " bundle_id " ] as? String
111- if let path {
112- var directory : ObjCBool = false
113- guard path. utf16. count <= 4096 , path. hasPrefix ( " / " ) , path. lowercased ( ) . hasSuffix ( " .app " ) , !path. contains ( " \0 " ) ,
114- FileManager . default. fileExists ( atPath: path, isDirectory: & directory) , directory. boolValue,
115- Bundle ( url: URL ( fileURLWithPath: path) ) ? . executableURL != nil
116- else { throw LaunchError ( " Use an existing absolute .app path for launch. " ) }
117- } else {
118- guard let bundleID, bundleID. utf16. count <= 256 ,
119- bundleID. range ( of: " ^[A-Za-z0-9.-]+$ " , options: . regularExpression) != nil
120- else { throw LaunchError ( " Use an exact application bundle ID for launch. " ) }
119+ Set ( request. keys) . isSubset ( of: keys) , request [ " op " ] as? String == operation
120+ else { throw LaunchError ( " Use one item and optional application for open, or one application for launch. " ) }
121+ var path : String ?
122+ var bundleID : String ?
123+ if !opensItem || request [ " application " ] != nil {
124+ guard let application = request [ " application " ] as? [ String : Any ] , application. count == 1 else {
125+ throw LaunchError ( " Use exactly one application path or bundle_id. " )
126+ }
127+ path = application [ " path " ] as? String
128+ bundleID = application [ " bundle_id " ] as? String
129+ if let path {
130+ var directory : ObjCBool = false
131+ guard path. utf16. count <= 4096 , path. hasPrefix ( " / " ) , path. lowercased ( ) . hasSuffix ( " .app " ) , !path. contains ( " \0 " ) ,
132+ FileManager . default. fileExists ( atPath: path, isDirectory: & directory) , directory. boolValue,
133+ Bundle ( url: URL ( fileURLWithPath: path) ) ? . executableURL != nil
134+ else { throw LaunchError ( " Use an existing absolute .app path for launch. " ) }
135+ } else {
136+ guard let bundleID, bundleID. utf16. count <= 256 ,
137+ bundleID. range ( of: " ^[A-Za-z0-9.-]+$ " , options: . regularExpression) != nil
138+ else { throw LaunchError ( " Use an exact application bundle ID for launch. " ) }
139+ }
140+ }
141+ var urls : [ URL ] = [ ]
142+ if opensItem {
143+ guard let item = request [ " item " ] as? [ String : Any ] , item. count == 1 else {
144+ throw LaunchError ( " Open accepts exactly one item path or url. " )
145+ }
146+ if let path = item [ " path " ] as? String {
147+ guard path. utf16. count <= 4096 , path. hasPrefix ( " / " ) , !path. contains ( " \0 " ) ,
148+ FileManager . default. fileExists ( atPath: path)
149+ else { throw LaunchError ( " Use an existing absolute item path. " ) }
150+ urls = [ URL ( fileURLWithPath: path) ]
151+ } else {
152+ guard let raw = item [ " url " ] as? String , raw. utf16. count <= 4096 ,
153+ !raw. unicodeScalars. contains ( where: { CharacterSet . controlCharacters. contains ( $0) } ) ,
154+ let url = URL ( string: raw, encodingInvalidCharacters: false ) , let scheme = url. scheme,
155+ scheme. range ( of: " ^[A-Za-z][A-Za-z0-9+.-]*$ " , options: . regularExpression) != nil ,
156+ url. absoluteString == raw
157+ else { throw LaunchError ( " Use a correctly encoded absolute URL with an explicit scheme and no control characters. " ) }
158+ urls = [ url]
159+ }
121160 }
122161 return ApplicationLaunchRequest ( applicationIdentifier: path, applicationBundleIdentifier: bundleID,
123- activates: true , waitUntilReady: true , waitForWindow: false , createsNewInstance: false )
162+ openURLs : urls , activates: true , waitUntilReady: true , waitForWindow: false , createsNewInstance: false )
124163}
0 commit comments