@@ -23,20 +23,6 @@ extension Logger {
2323 static var automationServer = { Logger ( subsystem: Bundle . main. bundleIdentifier ?? " DuckDuckGo " , category: " Automation Server " ) } ( )
2424}
2525
26- struct Log : TextOutputStream {
27-
28- func write( _ string: String ) {
29- let fm = FileManager . default
30- let log = fm. urls ( for: . documentDirectory, in: . userDomainMask) [ 0 ] . appendingPathComponent ( " log-automation.txt " )
31- if let handle = try ? FileHandle ( forWritingTo: log) {
32- handle. seekToEndOfFile ( )
33- handle. write ( string. data ( using: . utf8) !)
34- handle. closeFile ( )
35- } else {
36- try ? string. data ( using: . utf8) ? . write ( to: log)
37- }
38- }
39- }
4026
4127class AutomationServer {
4228 let listener : NWListener
@@ -45,9 +31,7 @@ class AutomationServer {
4531 init ( main: MainViewController , port: Int ? ) {
4632 var port = port ?? 8786
4733 self . main = main
48- var log = Log ( )
49- print ( " Starting automation server on port \( port) " , to: & log)
50- print ( " Bundle: \( Bundle . main. bundleIdentifier) " , to: & log)
34+ Logger . automationServer. info ( " Starting automation server on port \( port) " )
5135 listener = try ! NWListener ( using: . tcp, on: NWEndpoint . Port ( integerLiteral: UInt16 ( port) ) )
5236 listener. newConnectionHandler = handleConnection
5337 listener. start ( queue: . main)
@@ -72,12 +56,12 @@ class AutomationServer {
7256 return
7357 }
7458 Logger . automationServer. info ( " Received request! \( String ( describing: content) ) \( isComplete) \( String ( describing: error) ) " )
75-
59+
7660 if let error {
7761 Logger . automationServer. error ( " Error: \( error) " )
7862 return
7963 }
80-
64+
8165 if let content {
8266 Logger . automationServer. info ( " Handling content " )
8367 Task {
@@ -105,6 +89,10 @@ class AutomationServer {
10589 self . handleConnection ( connection, content)
10690 }
10791
92+ func getQueryStringParameter( url: URLComponents , param: String ) -> String ? {
93+ return url. queryItems? . first ( where: { $0. name == param } ) ? . value
94+ }
95+
10896 @MainActor
10997 func handleConnection( _ connection: NWConnection , _ content: Data ) {
11098 Logger . automationServer. info ( " Handling request! " )
@@ -113,128 +101,157 @@ class AutomationServer {
113101 if let firstLine = stringContent. components ( separatedBy: CharacterSet . newlines) . first {
114102 Logger . automationServer. info ( " First line: \( firstLine) " )
115103 }
116-
117- func getQueryStringParameter( url: String , param: String ) -> String ? {
118- guard let url = URLComponents ( string: url) else { return nil }
119- return url. queryItems? . first ( where: { $0. name == param } ) ? . value
120- }
104+
121105 // Get url parameter from path
122106 // GET / HTTP/1.1
123107 if #available( iOS 16 . 0 , * ) {
124108 let path = /^ ( GET|POST) ( \/ [ ^ ] * ) HTTP/
125109 if let match = stringContent. firstMatch ( of: path) {
126110 Logger . automationServer. info ( " Path: \( match. 2 ) " )
127111 // Convert the path into a URL object
128- guard let url = URL ( string: String ( match. 2 ) ) else {
129- print ( " Invalid URL: \( match. 2 ) " )
112+ guard let url = URLComponents ( string: String ( match. 2 ) ) else {
113+ Logger . automationServer . error ( " Invalid URL: \( match. 2 ) " )
130114 return // Or handle the error appropriately
131115 }
132- if url. path == " /navigate " {
133- let navigateUrlString = getQueryStringParameter ( url: String ( match. 2 ) , param: " url " ) ?? " "
134- let navigateUrl = URL ( string: navigateUrlString) !
135- self . main. loadUrl ( navigateUrl)
136- self . respond ( on: connection, response: " done " )
137- } else if url. path == " /execute " {
138- let script = getQueryStringParameter ( url: String ( match. 2 ) , param: " script " ) ?? " "
139- var args : [ String : String ] = [ : ]
140- // json decode args
141- if let argsString = getQueryStringParameter ( url: String ( match. 2 ) , param: " args " ) {
142- if let argsData = argsString. data ( using: . utf8) {
143- do {
144- let jsonDecoder = JSONDecoder ( )
145- args = try jsonDecoder. decode ( [ String : String ] . self, from: argsData)
146- } catch {
147- self . respond ( on: connection, response: " { \" error \" : \" \( error. localizedDescription) \" , \" args \" : \" \( argsString) \" } " )
148- }
149- } else {
150- self . respond ( on: connection, response: " { \" error \" : \" Unable to decode args \" } " )
151- }
152- }
153- Task {
154- await self . executeScript ( script, args: args, on: connection)
155- }
156- } else if url. path == " /getUrl " {
116+ switch url. path {
117+ case " /navigate " :
118+ self . navigate ( on: connection, url: url)
119+ case " /execute " :
120+ self . execute ( on: connection, url: url)
121+ case " /getUrl " :
157122 self . respond ( on: connection, response: self . main. currentUrl ( ) ?? " " )
158- } else if url. path == " /getWindowHandles " {
159- // TODO get all tabs
160- let handle = self . main. tabManager. current ( createIfNeeded: true )
161- guard let handle else {
162- self . respond ( on: connection, response: " no window " )
163- return
164- }
165-
166- let handles = self . main. tabManager. model. tabs. map ( { tab in
167- let tabView = self . main. tabManager. controller ( for: tab) !
168- return String ( UInt ( bitPattern: ObjectIdentifier ( tabView) ) )
169- } )
170-
171- if let jsonData = try ? JSONEncoder ( ) . encode ( handles) ,
172- let jsonString = String ( data: jsonData, encoding: . utf8) {
173- self . respond ( on: connection, response: jsonString)
174- } else {
175- // Handle JSON encoding failure
176- self . respond ( on: connection, response: " { \" error \" : \" Failed to encode response \" } " )
177- }
178- } else if url. path == " /closeWindow " {
179- self . main. closeTab ( self . main. currentTab!. tabModel)
180- self . respond ( on: connection, response: " { \" success \" :true} " )
181- } else if url. path == " /switchToWindow " {
182- if let handleString = getQueryStringParameter ( url: String ( match. 2 ) , param: " handle " ) {
183- Logger . automationServer. info ( " Switch to window \( handleString) " )
184- let tabToSelect : TabViewController ? = nil
185- if let tabIndex = self . main. tabManager. model. tabs. firstIndex ( where: { tab in
186- guard let tabView = self . main. tabManager. controller ( for: tab) else {
187- return false
188- }
189- return String ( UInt ( bitPattern: ObjectIdentifier ( tabView) ) ) == handleString
190- } ) {
191- Logger . automationServer. info ( " found tab \( tabIndex) " )
192- self . main. tabManager. select ( tabAt: tabIndex)
193- self . respond ( on: connection, response: " { \" success \" :true} " )
194- } else {
195- self . respond ( on: connection, response: " { \" error \" : \" Invalid window handle \" } " )
196- }
197- } else {
198- self . respond ( on: connection, response: " { \" error \" : \" Invalid window handle \" } " )
199- }
200- } else if url. path == " /newWindow " {
201- self . main. newTab ( )
202- let handle = self . main. tabManager. current ( createIfNeeded: true )
203- guard let handle else {
204- self . respond ( on: connection, response: " no window " )
205- return
206- }
207- // Response {handle: "", type: "tab"}
208- let response : [ String : String ] = [ " handle " : String ( UInt ( bitPattern: ObjectIdentifier ( handle) ) ) , " type " : " tab " ]
209- if let jsonData = try ? JSONEncoder ( ) . encode ( response) ,
210- let jsonString = String ( data: jsonData, encoding: . utf8) {
211- self . respond ( on: connection, response: jsonString)
212- } else {
213- self . respond ( on: connection, response: " { \" error \" : \" Failed to encode response \" } " )
214- }
215- } else if url. path == " /getWindowHandle " {
216- let handle = self . main. currentTab
217- guard let handle else {
218- self . respond ( on: connection, response: " no window " )
219- return
220- }
221- self . respond ( on: connection, response: String ( UInt ( bitPattern: ObjectIdentifier ( handle) ) ) )
222- } else {
223- self . respond ( on: connection, response: " unknown " )
123+ case " /getWindowHandles " :
124+ self . getWindowHandles ( on: connection, url: url)
125+ case " /closeWindow " :
126+ self . closeWindow ( on: connection, url: url)
127+ case " /switchToWindow " :
128+ self . switchToWindow ( on: connection, url: url)
129+ case " /newWindow " :
130+ self . newWindow ( on: connection, url: url)
131+ case " /getWindowHandle " :
132+ self . getWindowHandle ( on: connection, url: url)
133+ default :
134+ self . respondError ( on: connection, error: " unknown " )
224135 }
225136 } else {
226- self . respond ( on: connection, response : " unknown method " )
137+ self . respondError ( on: connection, error : " unknown method " )
227138 }
228139 } else {
229- self . respond ( on: connection, response: " unhandled " )
140+ self . respondError ( on: connection, error: " unhandled " )
141+ }
142+ }
143+
144+ @MainActor
145+ func navigate( on connection: NWConnection , url: URLComponents ) {
146+ let navigateUrlString = getQueryStringParameter ( url: url, param: " url " ) ?? " "
147+ let navigateUrl = URL ( string: navigateUrlString) !
148+ self . main. loadUrl ( navigateUrl)
149+ self . respond ( on: connection, response: " done " )
150+ }
151+
152+ @MainActor
153+ func execute( on connection: NWConnection , url: URLComponents ) {
154+ let script = getQueryStringParameter ( url: url, param: " script " ) ?? " "
155+ var args : [ String : String ] = [ : ]
156+ // json decode args
157+ if let argsString = getQueryStringParameter ( url: url, param: " args " ) {
158+ if let argsData = argsString. data ( using: . utf8) {
159+ do {
160+ let jsonDecoder = JSONDecoder ( )
161+ args = try jsonDecoder. decode ( [ String : String ] . self, from: argsData)
162+ } catch {
163+ self . respondError ( on: connection, error: error. localizedDescription)
164+ }
165+ } else {
166+ self . respondError ( on: connection, error: " Unable to decode args " )
167+ }
230168 }
169+ Task {
170+ await self . executeScript ( script, args: args, on: connection)
171+ }
172+ }
173+
174+ @MainActor
175+ func getWindowHandle( on connection: NWConnection , url: URLComponents ) {
176+ let handle = self . main. currentTab
177+ guard let handle else {
178+ self . respondError ( on: connection, error: " no window " )
179+ return
180+ }
181+ self . respond ( on: connection, response: String ( UInt ( bitPattern: ObjectIdentifier ( handle) ) ) )
182+ }
183+
184+ @MainActor
185+ func getWindowHandles( on connection: NWConnection , url: URLComponents ) {
186+ let handles = self . main. tabManager. model. tabs. map ( { tab in
187+ let tabView = self . main. tabManager. controller ( for: tab) !
188+ return String ( UInt ( bitPattern: ObjectIdentifier ( tabView) ) )
189+ } )
190+
191+ if let jsonData = try ? JSONEncoder ( ) . encode ( handles) ,
192+ let jsonString = String ( data: jsonData, encoding: . utf8) {
193+ self . respond ( on: connection, response: jsonString)
194+ } else {
195+ // Handle JSON encoding failure
196+ self . respondError ( on: connection, error: " Failed to encode response " )
197+ }
198+ }
199+
200+ @MainActor
201+ func closeWindow( on connection: NWConnection , url: URLComponents ) {
202+ self . main. closeTab ( self . main. currentTab!. tabModel)
203+ self . respond ( on: connection, response: " { \" success \" :true} " )
204+ }
205+
206+ @MainActor
207+ func switchToWindow( on connection: NWConnection , url: URLComponents ) {
208+ if let handleString = getQueryStringParameter ( url: url, param: " handle " ) {
209+ Logger . automationServer. info ( " Switch to window \( handleString) " )
210+ let tabToSelect : TabViewController ? = nil
211+ if let tabIndex = self . main. tabManager. model. tabs. firstIndex ( where: { tab in
212+ guard let tabView = self . main. tabManager. controller ( for: tab) else {
213+ return false
214+ }
215+ return String ( UInt ( bitPattern: ObjectIdentifier ( tabView) ) ) == handleString
216+ } ) {
217+ Logger . automationServer. info ( " found tab \( tabIndex) " )
218+ self . main. tabManager. select ( tabAt: tabIndex)
219+ self . respond ( on: connection, response: " { \" success \" :true} " )
220+ } else {
221+ self . respondError ( on: connection, error: " Invalid window handle " )
222+ }
223+ } else {
224+ self . respondError ( on: connection, error: " Invalid window handle " )
225+ }
226+ }
227+
228+ @MainActor
229+ func newWindow( on connection: NWConnection , url: URLComponents ) {
230+ self . main. newTab ( )
231+ let handle = self . main. tabManager. current ( createIfNeeded: true )
232+ guard let handle else {
233+ self . respondError ( on: connection, error: " no window " )
234+ return
235+ }
236+ // Response {handle: "", type: "tab"}
237+ let response : [ String : String ] = [ " handle " : String ( UInt ( bitPattern: ObjectIdentifier ( handle) ) ) , " type " : " tab " ]
238+ if let jsonData = try ? JSONEncoder ( ) . encode ( response) ,
239+ let jsonString = String ( data: jsonData, encoding: . utf8) {
240+ self . respond ( on: connection, response: jsonString)
241+ } else {
242+ self . respondError ( on: connection, error: " Failed to encode response " )
243+ }
244+ }
245+
246+ func respondError( on connection: NWConnection , error: String ) {
247+ self . respond ( on: connection, response: " { \" error \" : \" \( error) \" } " )
231248 }
232249
233250 func executeScript( _ script: String , args: [ String : Any ] , on connection: NWConnection ) async {
234251 Logger . automationServer. info ( " Going to execute script: \( script) " )
235- var result = await main. executeScript ( script, args: args)
252+ let result = await main. executeScript ( script, args: args)
236253 Logger . automationServer. info ( " Have result to execute script: \( String ( describing: result) ) " )
237- guard var result else {
254+ guard let result else {
238255 return
239256 }
240257 do {
0 commit comments