@lattices/cli 0.4.2 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +90 -34
- package/app/Sources/AppShellView.swift +2 -0
- package/app/Sources/AppTypeClassifier.swift +36 -0
- package/app/Sources/AppUpdater.swift +92 -0
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CliActionLauncher.swift +50 -0
- package/app/Sources/CommandModeView.swift +4 -24
- package/app/Sources/CompanionActivityLog.swift +70 -0
- package/app/Sources/CompanionKeyboardController.swift +141 -0
- package/app/Sources/DesktopModel.swift +4 -0
- package/app/Sources/HandsOffSession.swift +15 -4
- package/app/Sources/HomeDashboardView.swift +18 -10
- package/app/Sources/HotkeyStore.swift +8 -5
- package/app/Sources/IntentEngine.swift +7 -1
- package/app/Sources/LatticesApi.swift +125 -4
- package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/LatticesDeckHost.swift +1463 -0
- package/app/Sources/LatticesRuntime.swift +61 -0
- package/app/Sources/MainView.swift +351 -191
- package/app/Sources/MouseFinder.swift +335 -30
- package/app/Sources/MouseGestureConfig.swift +364 -0
- package/app/Sources/MouseGestureController.swift +1203 -0
- package/app/Sources/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/MouseInputEventViewer.swift +272 -0
- package/app/Sources/MouseShortcutStore.swift +107 -0
- package/app/Sources/OmniSearchView.swift +136 -2
- package/app/Sources/OmniSearchWindow.swift +65 -5
- package/app/Sources/OnboardingView.swift +30 -16
- package/app/Sources/PaletteCommand.swift +26 -6
- package/app/Sources/PermissionChecker.swift +76 -2
- package/app/Sources/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/PiAuthPromptCard.swift +90 -0
- package/app/Sources/PiChatDock.swift +137 -74
- package/app/Sources/PiChatSession.swift +608 -108
- package/app/Sources/PiInstallCallout.swift +86 -0
- package/app/Sources/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/PiWorkspaceView.swift +174 -77
- package/app/Sources/Preferences.swift +78 -0
- package/app/Sources/ScreenMapState.swift +91 -31
- package/app/Sources/ScreenMapView.swift +510 -524
- package/app/Sources/ScreenMapWindowController.swift +12 -4
- package/app/Sources/SettingsView.swift +869 -152
- package/app/Sources/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/VoiceCommandWindow.swift +23 -2
- package/app/Sources/WindowDragSnapController.swift +628 -0
- package/app/Sources/WindowTiler.swift +328 -65
- package/app/Sources/WorkspaceManager.swift +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +2 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import Darwin
|
|
2
|
+
import DeckKit
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
final class LatticesCompanionBridgeServer: NSObject {
|
|
6
|
+
static let shared = LatticesCompanionBridgeServer()
|
|
7
|
+
|
|
8
|
+
static let bonjourType = "_lattices-companion._tcp."
|
|
9
|
+
static let defaultPort: UInt16 = 5287
|
|
10
|
+
|
|
11
|
+
private let queue = DispatchQueue(label: "lattices.companion.bridge", qos: .userInitiated)
|
|
12
|
+
private let encoder = JSONEncoder()
|
|
13
|
+
private let decoder = JSONDecoder()
|
|
14
|
+
|
|
15
|
+
private var serverFd: Int32 = -1
|
|
16
|
+
private var acceptSource: DispatchSourceRead?
|
|
17
|
+
private var service: NetService?
|
|
18
|
+
|
|
19
|
+
private override init() {
|
|
20
|
+
super.init()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func start() {
|
|
24
|
+
guard acceptSource == nil else { return }
|
|
25
|
+
|
|
26
|
+
let diag = DiagnosticLog.shared
|
|
27
|
+
serverFd = socket(AF_INET, SOCK_STREAM, 0)
|
|
28
|
+
guard serverFd >= 0 else {
|
|
29
|
+
diag.error("CompanionBridge: socket() failed — errno \(errno)")
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var yes: Int32 = 1
|
|
34
|
+
setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &yes, socklen_t(MemoryLayout<Int32>.size))
|
|
35
|
+
|
|
36
|
+
var addr = sockaddr_in()
|
|
37
|
+
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
38
|
+
addr.sin_family = sa_family_t(AF_INET)
|
|
39
|
+
addr.sin_port = Self.defaultPort.bigEndian
|
|
40
|
+
addr.sin_addr.s_addr = INADDR_ANY.bigEndian
|
|
41
|
+
|
|
42
|
+
let bindResult = withUnsafePointer(to: &addr) {
|
|
43
|
+
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
44
|
+
Darwin.bind(serverFd, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
guard bindResult == 0 else {
|
|
48
|
+
diag.error("CompanionBridge: bind() failed — errno \(errno)")
|
|
49
|
+
close(serverFd)
|
|
50
|
+
serverFd = -1
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
guard listen(serverFd, 8) == 0 else {
|
|
55
|
+
diag.error("CompanionBridge: listen() failed — errno \(errno)")
|
|
56
|
+
close(serverFd)
|
|
57
|
+
serverFd = -1
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let flags = fcntl(serverFd, F_GETFL)
|
|
62
|
+
_ = fcntl(serverFd, F_SETFL, flags | O_NONBLOCK)
|
|
63
|
+
|
|
64
|
+
let source = DispatchSource.makeReadSource(fileDescriptor: serverFd, queue: queue)
|
|
65
|
+
source.setEventHandler { [weak self] in
|
|
66
|
+
self?.acceptConnection()
|
|
67
|
+
}
|
|
68
|
+
source.setCancelHandler { [weak self] in
|
|
69
|
+
guard let self else { return }
|
|
70
|
+
if self.serverFd >= 0 {
|
|
71
|
+
close(self.serverFd)
|
|
72
|
+
self.serverFd = -1
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
source.resume()
|
|
76
|
+
acceptSource = source
|
|
77
|
+
|
|
78
|
+
publishBonjour()
|
|
79
|
+
diag.success("CompanionBridge: listening on http://0.0.0.0:\(Self.defaultPort)")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func stop() {
|
|
83
|
+
acceptSource?.cancel()
|
|
84
|
+
acceptSource = nil
|
|
85
|
+
service?.stop()
|
|
86
|
+
service = nil
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private extension LatticesCompanionBridgeServer {
|
|
91
|
+
struct HTTPRequest {
|
|
92
|
+
let method: String
|
|
93
|
+
let path: String
|
|
94
|
+
let headers: [String: String]
|
|
95
|
+
let body: Data
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
struct HealthResponse: Codable {
|
|
99
|
+
let ok: Bool
|
|
100
|
+
let name: String
|
|
101
|
+
let serviceType: String
|
|
102
|
+
let hostName: String
|
|
103
|
+
let port: UInt16
|
|
104
|
+
let version: String
|
|
105
|
+
let mode: String
|
|
106
|
+
let bridgePublicKey: String
|
|
107
|
+
let bridgeFingerprint: String
|
|
108
|
+
let requestSigningRequired: Bool
|
|
109
|
+
let payloadEncryptionRequired: Bool
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func publishBonjour() {
|
|
113
|
+
let advertisedName = Host.current().localizedName ?? "Lattices Companion"
|
|
114
|
+
let service = NetService(
|
|
115
|
+
domain: "local.",
|
|
116
|
+
type: Self.bonjourType,
|
|
117
|
+
name: advertisedName,
|
|
118
|
+
port: Int32(Self.defaultPort)
|
|
119
|
+
)
|
|
120
|
+
service.includesPeerToPeer = true
|
|
121
|
+
service.publish()
|
|
122
|
+
self.service = service
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func acceptConnection() {
|
|
126
|
+
while true {
|
|
127
|
+
var clientAddr = sockaddr_in()
|
|
128
|
+
var addrLen = socklen_t(MemoryLayout<sockaddr_in>.size)
|
|
129
|
+
|
|
130
|
+
let clientFd = withUnsafeMutablePointer(to: &clientAddr) {
|
|
131
|
+
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
132
|
+
accept(serverFd, $0, &addrLen)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if clientFd < 0 {
|
|
137
|
+
if errno != EAGAIN && errno != EWOULDBLOCK {
|
|
138
|
+
DiagnosticLog.shared.error("CompanionBridge: accept() failed — errno \(errno)")
|
|
139
|
+
}
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let clientFlags = fcntl(clientFd, F_GETFL)
|
|
144
|
+
if clientFlags >= 0 {
|
|
145
|
+
_ = fcntl(clientFd, F_SETFL, clientFlags & ~O_NONBLOCK)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
queue.async { [weak self] in
|
|
149
|
+
self?.handleClient(fd: clientFd)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func handleClient(fd: Int32) {
|
|
155
|
+
defer { close(fd) }
|
|
156
|
+
|
|
157
|
+
guard let request = readRequest(from: fd) else {
|
|
158
|
+
sendError(status: 400, message: "Invalid HTTP request", to: fd)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
do {
|
|
163
|
+
try route(request, to: fd)
|
|
164
|
+
} catch let error as LatticesCompanionSecurityError {
|
|
165
|
+
let status: Int
|
|
166
|
+
switch error {
|
|
167
|
+
case .untrustedDevice:
|
|
168
|
+
status = 403
|
|
169
|
+
case .missingHeader, .staleRequest, .replayedRequest, .invalidSignature, .invalidEnvelope, .invalidDeviceKey:
|
|
170
|
+
status = 401
|
|
171
|
+
}
|
|
172
|
+
sendError(status: status, message: error.localizedDescription, to: fd)
|
|
173
|
+
} catch {
|
|
174
|
+
sendError(status: 500, message: error.localizedDescription, to: fd)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func route(_ request: HTTPRequest, to fd: Int32) throws {
|
|
179
|
+
switch (request.method, request.path) {
|
|
180
|
+
case ("GET", "/health"):
|
|
181
|
+
let security = LatticesDeckHost.shared.securityConfiguration
|
|
182
|
+
let response = HealthResponse(
|
|
183
|
+
ok: true,
|
|
184
|
+
name: Host.current().localizedName ?? "Lattices Companion",
|
|
185
|
+
serviceType: Self.bonjourType,
|
|
186
|
+
hostName: localHostName(),
|
|
187
|
+
port: Self.defaultPort,
|
|
188
|
+
version: LatticesRuntime.appVersion,
|
|
189
|
+
mode: "local-network-secure",
|
|
190
|
+
bridgePublicKey: LatticesCompanionSecurityCoordinator.shared.bridgePublicKeyBase64,
|
|
191
|
+
bridgeFingerprint: LatticesCompanionSecurityCoordinator.shared.bridgeFingerprint,
|
|
192
|
+
requestSigningRequired: security.requestSigningRequired,
|
|
193
|
+
payloadEncryptionRequired: security.payloadEncryptionRequired
|
|
194
|
+
)
|
|
195
|
+
try sendJSON(status: 200, value: response, to: fd)
|
|
196
|
+
|
|
197
|
+
case ("GET", "/deck/manifest"):
|
|
198
|
+
try sendJSON(status: 200, value: LatticesDeckHost.shared.manifestSync(), to: fd)
|
|
199
|
+
|
|
200
|
+
case ("POST", "/pairing/request"):
|
|
201
|
+
let pairingRequest = try decoder.decode(DeckPairingRequest.self, from: request.body)
|
|
202
|
+
let response = LatticesCompanionSecurityCoordinator.shared.handlePairingRequest(pairingRequest)
|
|
203
|
+
let status = response.disposition == .denied ? 403 : 200
|
|
204
|
+
try sendJSON(status: status, value: response, to: fd)
|
|
205
|
+
|
|
206
|
+
case ("GET", "/deck/snapshot"):
|
|
207
|
+
let auth = try authorizeProtectedRequest(request)
|
|
208
|
+
let snapshot = try LatticesDeckHost.shared.runtimeSnapshotSync()
|
|
209
|
+
let response = try LatticesCompanionSecurityCoordinator.shared.encodeProtectedResponse(
|
|
210
|
+
snapshot,
|
|
211
|
+
auth: auth,
|
|
212
|
+
status: 200,
|
|
213
|
+
path: request.path
|
|
214
|
+
)
|
|
215
|
+
try sendJSON(status: 200, value: response, to: fd)
|
|
216
|
+
|
|
217
|
+
case ("POST", "/deck/perform"):
|
|
218
|
+
let auth = try authorizeProtectedRequest(request)
|
|
219
|
+
let action = try LatticesCompanionSecurityCoordinator.shared.decodeProtectedBody(
|
|
220
|
+
DeckActionRequest.self,
|
|
221
|
+
body: request.body,
|
|
222
|
+
auth: auth,
|
|
223
|
+
method: request.method,
|
|
224
|
+
path: request.path
|
|
225
|
+
)
|
|
226
|
+
let result = try LatticesDeckHost.shared.performSync(action)
|
|
227
|
+
let response = try LatticesCompanionSecurityCoordinator.shared.encodeProtectedResponse(
|
|
228
|
+
result,
|
|
229
|
+
auth: auth,
|
|
230
|
+
status: 200,
|
|
231
|
+
path: request.path
|
|
232
|
+
)
|
|
233
|
+
try sendJSON(status: 200, value: response, to: fd)
|
|
234
|
+
|
|
235
|
+
case ("POST", "/deck/trackpad"):
|
|
236
|
+
let auth = try authorizeProtectedRequest(request)
|
|
237
|
+
let eventRequest = try LatticesCompanionSecurityCoordinator.shared.decodeProtectedBody(
|
|
238
|
+
DeckTrackpadEventRequest.self,
|
|
239
|
+
body: request.body,
|
|
240
|
+
auth: auth,
|
|
241
|
+
method: request.method,
|
|
242
|
+
path: request.path
|
|
243
|
+
)
|
|
244
|
+
let result = LatticesCompanionTrackpadController.shared.perform(eventRequest)
|
|
245
|
+
let response = try LatticesCompanionSecurityCoordinator.shared.encodeProtectedResponse(
|
|
246
|
+
result,
|
|
247
|
+
auth: auth,
|
|
248
|
+
status: 200,
|
|
249
|
+
path: request.path
|
|
250
|
+
)
|
|
251
|
+
try sendJSON(status: 200, value: response, to: fd)
|
|
252
|
+
|
|
253
|
+
default:
|
|
254
|
+
sendError(status: 404, message: "Unknown route", to: fd)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
func authorizeProtectedRequest(_ request: HTTPRequest) throws -> AuthorizedBridgeRequest {
|
|
259
|
+
let security = LatticesDeckHost.shared.securityConfiguration
|
|
260
|
+
guard security.requestSigningRequired else {
|
|
261
|
+
throw LatticesCompanionSecurityError.untrustedDevice
|
|
262
|
+
}
|
|
263
|
+
return try LatticesCompanionSecurityCoordinator.shared.authorize(
|
|
264
|
+
method: request.method,
|
|
265
|
+
path: request.path,
|
|
266
|
+
headers: request.headers,
|
|
267
|
+
body: request.body
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func readRequest(from fd: Int32) -> HTTPRequest? {
|
|
272
|
+
var buffer = Data()
|
|
273
|
+
let deadline = DispatchTime.now().uptimeNanoseconds + 2_000_000_000
|
|
274
|
+
let delimiterCRLF = Data([13, 10, 13, 10])
|
|
275
|
+
let delimiterLF = Data([10, 10])
|
|
276
|
+
|
|
277
|
+
var headerRange = buffer.range(of: delimiterCRLF) ?? buffer.range(of: delimiterLF)
|
|
278
|
+
while headerRange == nil {
|
|
279
|
+
guard let count = readChunk(from: fd, deadline: deadline, into: &buffer) else {
|
|
280
|
+
return nil
|
|
281
|
+
}
|
|
282
|
+
guard count > 0 else { return nil }
|
|
283
|
+
if buffer.count > 128 * 1024 {
|
|
284
|
+
return nil
|
|
285
|
+
}
|
|
286
|
+
headerRange = buffer.range(of: delimiterCRLF) ?? buffer.range(of: delimiterLF)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
guard let headerRange else { return nil }
|
|
290
|
+
let headerData = buffer.subdata(in: 0..<headerRange.lowerBound)
|
|
291
|
+
guard let headerText = String(data: headerData, encoding: .utf8) else {
|
|
292
|
+
return nil
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let lines = headerText
|
|
296
|
+
.replacingOccurrences(of: "\r\n", with: "\n")
|
|
297
|
+
.components(separatedBy: "\n")
|
|
298
|
+
guard let requestLine = lines.first else { return nil }
|
|
299
|
+
let requestParts = requestLine.split(separator: " ", omittingEmptySubsequences: true)
|
|
300
|
+
guard requestParts.count >= 2 else { return nil }
|
|
301
|
+
|
|
302
|
+
let method = String(requestParts[0]).uppercased()
|
|
303
|
+
let rawPath = String(requestParts[1])
|
|
304
|
+
let path = rawPath.split(separator: "?", maxSplits: 1).first.map(String.init) ?? rawPath
|
|
305
|
+
|
|
306
|
+
var headers: [String: String] = [:]
|
|
307
|
+
for line in lines.dropFirst() {
|
|
308
|
+
guard let separator = line.firstIndex(of: ":") else { continue }
|
|
309
|
+
let name = line[..<separator].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
310
|
+
let value = line[line.index(after: separator)...].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
311
|
+
headers[name] = value
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let contentLength = Int(headers["content-length"] ?? "") ?? 0
|
|
315
|
+
var body = Data(buffer[headerRange.upperBound...])
|
|
316
|
+
while body.count < contentLength {
|
|
317
|
+
guard let count = readChunk(
|
|
318
|
+
from: fd,
|
|
319
|
+
deadline: deadline,
|
|
320
|
+
chunkSize: min(4096, contentLength - body.count),
|
|
321
|
+
into: &body
|
|
322
|
+
) else {
|
|
323
|
+
return nil
|
|
324
|
+
}
|
|
325
|
+
guard count > 0 else { return nil }
|
|
326
|
+
}
|
|
327
|
+
if body.count > contentLength {
|
|
328
|
+
body = body.prefix(contentLength)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return HTTPRequest(method: method, path: path, headers: headers, body: body)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
func readChunk(
|
|
335
|
+
from fd: Int32,
|
|
336
|
+
deadline: UInt64,
|
|
337
|
+
chunkSize: Int = 4096,
|
|
338
|
+
into buffer: inout Data
|
|
339
|
+
) -> Int? {
|
|
340
|
+
var chunk = [UInt8](repeating: 0, count: chunkSize)
|
|
341
|
+
|
|
342
|
+
while true {
|
|
343
|
+
let count = Darwin.read(fd, &chunk, chunk.count)
|
|
344
|
+
if count > 0 {
|
|
345
|
+
buffer.append(contentsOf: chunk[..<count])
|
|
346
|
+
return count
|
|
347
|
+
}
|
|
348
|
+
if count == 0 {
|
|
349
|
+
return 0
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if errno == EINTR {
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
if (errno == EAGAIN || errno == EWOULDBLOCK) &&
|
|
356
|
+
DispatchTime.now().uptimeNanoseconds < deadline {
|
|
357
|
+
usleep(10_000)
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
DiagnosticLog.shared.error("CompanionBridge: read() failed — errno \(errno)")
|
|
362
|
+
return nil
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func sendJSON<T: Encodable>(status: Int, value: T, to fd: Int32) throws {
|
|
367
|
+
let body = try encoder.encode(value)
|
|
368
|
+
sendResponse(status: status, contentType: "application/json; charset=utf-8", body: body, to: fd)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
func sendError(status: Int, message: String, to fd: Int32) {
|
|
372
|
+
let payload: [String: Any] = ["ok": false, "error": message]
|
|
373
|
+
guard let body = try? JSONSerialization.data(withJSONObject: payload, options: []) else { return }
|
|
374
|
+
sendResponse(status: status, contentType: "application/json; charset=utf-8", body: body, to: fd)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
func sendResponse(status: Int, contentType: String, body: Data, to fd: Int32) {
|
|
378
|
+
let reason = reasonPhrase(for: status)
|
|
379
|
+
let header = [
|
|
380
|
+
"HTTP/1.1 \(status) \(reason)",
|
|
381
|
+
"Content-Type: \(contentType)",
|
|
382
|
+
"Content-Length: \(body.count)",
|
|
383
|
+
"Connection: close",
|
|
384
|
+
"",
|
|
385
|
+
""
|
|
386
|
+
].joined(separator: "\r\n")
|
|
387
|
+
writeAll(Data(header.utf8), to: fd)
|
|
388
|
+
writeAll(body, to: fd)
|
|
389
|
+
_ = shutdown(fd, SHUT_WR)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func writeAll(_ data: Data, to fd: Int32) {
|
|
393
|
+
data.withUnsafeBytes { rawBuffer in
|
|
394
|
+
guard var pointer = rawBuffer.bindMemory(to: UInt8.self).baseAddress else { return }
|
|
395
|
+
var remaining = data.count
|
|
396
|
+
while remaining > 0 {
|
|
397
|
+
let written = write(fd, pointer, remaining)
|
|
398
|
+
guard written > 0 else { return }
|
|
399
|
+
remaining -= written
|
|
400
|
+
pointer = pointer.advanced(by: written)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func reasonPhrase(for status: Int) -> String {
|
|
406
|
+
switch status {
|
|
407
|
+
case 200: return "OK"
|
|
408
|
+
case 400: return "Bad Request"
|
|
409
|
+
case 401: return "Unauthorized"
|
|
410
|
+
case 403: return "Forbidden"
|
|
411
|
+
case 404: return "Not Found"
|
|
412
|
+
default: return "Internal Server Error"
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
func localHostName() -> String {
|
|
417
|
+
let process = Process()
|
|
418
|
+
process.executableURL = URL(fileURLWithPath: "/usr/sbin/scutil")
|
|
419
|
+
process.arguments = ["--get", "LocalHostName"]
|
|
420
|
+
|
|
421
|
+
let pipe = Pipe()
|
|
422
|
+
process.standardOutput = pipe
|
|
423
|
+
process.standardError = Pipe()
|
|
424
|
+
|
|
425
|
+
do {
|
|
426
|
+
try process.run()
|
|
427
|
+
process.waitUntilExit()
|
|
428
|
+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
429
|
+
if let name = String(data: data, encoding: .utf8)?
|
|
430
|
+
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
431
|
+
!name.isEmpty {
|
|
432
|
+
return "\(name).local"
|
|
433
|
+
}
|
|
434
|
+
} catch { }
|
|
435
|
+
|
|
436
|
+
return Host.current().localizedName ?? "localhost"
|
|
437
|
+
}
|
|
438
|
+
}
|