@@ -23,20 +23,6 @@ extension Logger {
23
23
static var automationServer = { Logger ( subsystem: Bundle . main. bundleIdentifier ?? " DuckDuckGo " , category: " Automation Server " ) } ( )
24
24
}
25
25
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
- }
40
26
41
27
class AutomationServer {
42
28
let listener : NWListener
@@ -45,9 +31,7 @@ class AutomationServer {
45
31
init ( main: MainViewController , port: Int ? ) {
46
32
var port = port ?? 8786
47
33
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) " )
51
35
listener = try ! NWListener ( using: . tcp, on: NWEndpoint . Port ( integerLiteral: UInt16 ( port) ) )
52
36
listener. newConnectionHandler = handleConnection
53
37
listener. start ( queue: . main)
@@ -72,12 +56,12 @@ class AutomationServer {
72
56
return
73
57
}
74
58
Logger . automationServer. info ( " Received request! \( String ( describing: content) ) \( isComplete) \( String ( describing: error) ) " )
75
-
59
+
76
60
if let error {
77
61
Logger . automationServer. error ( " Error: \( error) " )
78
62
return
79
63
}
80
-
64
+
81
65
if let content {
82
66
Logger . automationServer. info ( " Handling content " )
83
67
Task {
@@ -105,6 +89,10 @@ class AutomationServer {
105
89
self . handleConnection ( connection, content)
106
90
}
107
91
92
+ func getQueryStringParameter( url: URLComponents , param: String ) -> String ? {
93
+ return url. queryItems? . first ( where: { $0. name == param } ) ? . value
94
+ }
95
+
108
96
@MainActor
109
97
func handleConnection( _ connection: NWConnection , _ content: Data ) {
110
98
Logger . automationServer. info ( " Handling request! " )
@@ -113,128 +101,157 @@ class AutomationServer {
113
101
if let firstLine = stringContent. components ( separatedBy: CharacterSet . newlines) . first {
114
102
Logger . automationServer. info ( " First line: \( firstLine) " )
115
103
}
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
+
121
105
// Get url parameter from path
122
106
// GET / HTTP/1.1
123
107
if #available( iOS 16 . 0 , * ) {
124
108
let path = /^ ( GET|POST) ( \/ [ ^ ] * ) HTTP/
125
109
if let match = stringContent. firstMatch ( of: path) {
126
110
Logger . automationServer. info ( " Path: \( match. 2 ) " )
127
111
// 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 ) " )
130
114
return // Or handle the error appropriately
131
115
}
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 " :
157
122
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 " )
224
135
}
225
136
} else {
226
- self . respond ( on: connection, response : " unknown method " )
137
+ self . respondError ( on: connection, error : " unknown method " )
227
138
}
228
139
} 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
+ }
230
168
}
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) \" } " )
231
248
}
232
249
233
250
func executeScript( _ script: String , args: [ String : Any ] , on connection: NWConnection ) async {
234
251
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)
236
253
Logger . automationServer. info ( " Have result to execute script: \( String ( describing: result) ) " )
237
- guard var result else {
254
+ guard let result else {
238
255
return
239
256
}
240
257
do {
0 commit comments