Repository navigation
Expand file tree
/
Copy pathModuleTestServer.swift
More file actions
252 lines (233 loc) · 8.85 KB
/
Copy pathModuleTestServer.swift
File metadata and controls
252 lines (233 loc) · 8.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import Foundation
import Network
/// Body reader with the same shape Embassy's SWSGI input had: call it with a
/// chunk callback; the callback receives the body then an empty Data as EOF.
typealias ModuleTestServerInput = (@escaping (Data) -> Void) -> Void
/// Minimal loopback HTTP/1.1 server for the runtime test suite, built on
/// Network.framework so request handling rides ordinary GCD instead of a
/// vendored selector event loop. It exists to answer exactly two clients —
/// the runtime's module loader (NSURLSession + the synchronous NSURLConnection
/// fallback) and the Jasmine reporter — so the HTTP surface is deliberately
/// small: one request per connection, full body buffered before dispatch,
/// `Connection: close` on every response.
final class ModuleTestServer {
typealias StartResponse = (String, [(String, String)]) -> Void
typealias SendBody = (Data) -> Void
typealias Handler = ([String: Any], @escaping StartResponse, @escaping SendBody) -> Void
private let listener: NWListener
private let queue = DispatchQueue(label: "org.nativescript.TestRunner.ModuleTestServer")
private let handler: Handler
private var connections: [ObjectIdentifier: NWConnection] = [:]
enum StartError: Error {
case timedOut
case noPort
}
/// The loopback port the system assigned; valid once `start()` returned.
private(set) var port: UInt16 = 0
/// The port is left to the system rather than fixed: simulators share the
/// host's loopback interface, so two test runs on one machine would
/// otherwise answer each other's requests.
init(handler: @escaping Handler) throws {
self.handler = handler
let params = NWParameters.tcp
params.requiredLocalEndpoint = NWEndpoint.hostPort(
host: NWEndpoint.Host("127.0.0.1"),
port: .any)
listener = try NWListener(using: params)
listener.newConnectionHandler = { [weak self] connection in
self?.accept(connection)
}
}
/// Blocks until the listener is bound, because the port is only known then.
func start() throws {
let settled = DispatchSemaphore(value: 0)
var failure: Error?
listener.stateUpdateHandler = { state in
switch state {
case .ready:
settled.signal()
case .failed(let error):
failure = error
settled.signal()
default:
break
}
}
listener.start(queue: queue)
let outcome = settled.wait(timeout: .now() + 10)
listener.stateUpdateHandler = nil
if outcome == .timedOut {
throw StartError.timedOut
}
if let failure = failure {
throw failure
}
guard let bound = listener.port?.rawValue, bound != 0 else {
throw StartError.noPort
}
port = bound
}
func stop() {
queue.sync {
for connection in connections.values {
connection.cancel()
}
connections.removeAll()
}
listener.cancel()
}
// All connection work runs on `queue`.
private func accept(_ connection: NWConnection) {
connections[ObjectIdentifier(connection)] = connection
connection.stateUpdateHandler = { [weak self] state in
switch state {
case .failed, .cancelled:
self?.connections.removeValue(forKey: ObjectIdentifier(connection))
default:
break
}
}
connection.start(queue: queue)
receive(connection, buffered: Data())
}
private func receive(_ connection: NWConnection, buffered: Data) {
connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) {
[weak self] data, _, isComplete, error in
guard let self = self else { return }
var buffer = buffered
if let data = data {
buffer.append(data)
}
if error != nil {
connection.cancel()
return
}
if let headerEnd = buffer.range(of: Data("\r\n\r\n".utf8)) {
let headerData = buffer.subdata(in: buffer.startIndex..<headerEnd.lowerBound)
let bodySoFar = buffer.subdata(in: headerEnd.upperBound..<buffer.endIndex)
guard let head = ParsedRequestHead(headerData) else {
self.respondAndClose(connection, status: "400 Bad Request",
headers: [], body: Data())
return
}
self.receiveBody(connection, head: head, body: bodySoFar)
return
}
if isComplete {
connection.cancel()
return
}
self.receive(connection, buffered: buffer)
}
}
private func receiveBody(_ connection: NWConnection, head: ParsedRequestHead, body: Data) {
if body.count >= head.contentLength {
dispatch(connection, head: head, body: body)
return
}
connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) {
[weak self] data, _, isComplete, error in
guard let self = self else { return }
var body = body
if let data = data {
body.append(data)
}
if error != nil || (isComplete && body.count < head.contentLength) {
connection.cancel()
return
}
self.receiveBody(connection, head: head, body: body)
}
}
private func dispatch(_ connection: NWConnection, head: ParsedRequestHead, body: Data) {
let input: ModuleTestServerInput = { deliver in
deliver(body)
deliver(Data())
}
let environ: [String: Any] = [
"REQUEST_METHOD": head.method,
"PATH_INFO": head.path,
"QUERY_STRING": head.query,
"swsgi.input": input,
]
// The handler may respond later from another queue (the delayed
// timeout.mjs route), so the response closures are thread-safe and
// single-shot.
let responseLock = NSLock()
var status = "200 OK"
var headers: [(String, String)] = []
var sent = false
let startResponse: StartResponse = { s, h in
responseLock.lock()
status = s
headers = h
responseLock.unlock()
}
let sendBody: SendBody = { [weak self] data in
responseLock.lock()
let alreadySent = sent
sent = true
let responseStatus = status
let responseHeaders = headers
responseLock.unlock()
if alreadySent {
return
}
self?.queue.async {
self?.respondAndClose(connection, status: responseStatus,
headers: responseHeaders, body: data)
}
}
handler(environ, startResponse, sendBody)
}
private func respondAndClose(_ connection: NWConnection, status: String,
headers: [(String, String)], body: Data) {
var response = "HTTP/1.1 \(status)\r\n"
for (name, value) in headers {
response += "\(name): \(value)\r\n"
}
response += "Content-Length: \(body.count)\r\nConnection: close\r\n\r\n"
var payload = Data(response.utf8)
payload.append(body)
connection.send(content: payload, completion: .contentProcessed { _ in
connection.cancel()
})
}
}
private struct ParsedRequestHead {
let method: String
let path: String
let query: String
let contentLength: Int
init?(_ headerData: Data) {
guard let text = String(data: headerData, encoding: .utf8) else {
return nil
}
let lines = text.components(separatedBy: "\r\n")
guard let requestLine = lines.first else {
return nil
}
let parts = requestLine.split(separator: " ")
guard parts.count >= 2 else {
return nil
}
method = String(parts[0])
let target = String(parts[1])
if let queryStart = target.firstIndex(of: "?") {
path = String(target[target.startIndex..<queryStart])
query = String(target[target.index(after: queryStart)...])
} else {
path = target
query = ""
}
var length = 0
for line in lines.dropFirst() {
let lowered = line.lowercased()
if lowered.hasPrefix("content-length:") {
length = Int(line.dropFirst("content-length:".count)
.trimmingCharacters(in: .whitespaces)) ?? 0
}
}
contentLength = length
}
}