RuntimeLauncher.swift
157 lines 6.4 KB
Raw
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700 feat(companion): add production macOS distribution path Human minor ⚠ breaking 24 days ago
1 import Darwin
2 import Foundation
3
4 public enum RuntimeLaunchError: Error, CustomStringConvertible {
5 case appNotInstalled
6 case capabilityUnavailable(String)
7 case spawnFailed(Int32)
8 case invalidRuntime(String)
9
10 public var description: String {
11 switch self {
12 case .appNotInstalled:
13 return "Knowtation.app is not installed at /Applications/Knowtation.app"
14 case .capabilityUnavailable(let reason):
15 return "custody capability unavailable: \(reason)"
16 case .spawnFailed(let status):
17 return "bundled runtime could not start (status \(status))"
18 case .invalidRuntime(let reason):
19 return "bundled runtime verification failed: \(reason)"
20 }
21 }
22 }
23
24 public final class RuntimeLauncher {
25 public let mode: KnowtationRuntimeMode
26
27 public init(mode: KnowtationRuntimeMode) {
28 self.mode = mode
29 }
30
31 @discardableResult
32 public func run(arguments: [String]) throws -> Int32 {
33 let appURL = try resolveAppURL()
34 let resourcesURL = appURL.appendingPathComponent("Contents/Resources", isDirectory: true)
35 let runtimeURL = resourcesURL.appendingPathComponent("runtime", isDirectory: true)
36 let manifestURL = resourcesURL.appendingPathComponent("runtime-manifest.json", isDirectory: false)
37 let nodeURL = runtimeURL.appendingPathComponent("node/bin/node", isDirectory: false)
38 let entryURL = runtimeURL.appendingPathComponent("companion/runtime/main.mjs", isDirectory: false)
39
40 do {
41 let teamIdentifier = try CallerIdentityValidator.hostTeamIdentifier()
42 try RuntimeVerifier.verifyCodeSignature(
43 at: appURL,
44 expectedIdentifier: KnowtationReleaseContract.appIdentifier,
45 expectedTeamIdentifier: teamIdentifier
46 )
47 let manifest = try RuntimeVerifier.loadManifest(at: manifestURL)
48 try RuntimeVerifier.verifyTree(root: resourcesURL, manifest: manifest)
49 try RuntimeVerifier.verifyCodeSignature(
50 at: nodeURL,
51 expectedIdentifier: KnowtationReleaseContract.nodeIdentifier,
52 expectedTeamIdentifier: teamIdentifier
53 )
54 } catch {
55 throw RuntimeLaunchError.invalidRuntime(String(describing: error))
56 }
57
58 let capability = try requestCapability()
59 return try spawnAndWait(
60 executable: nodeURL.path,
61 entrypoint: entryURL.path,
62 appRoot: appURL.path,
63 capability: capability,
64 arguments: arguments
65 )
66 }
67
68 private func resolveAppURL() throws -> URL {
69 if mode == .app, Bundle.main.bundleURL.pathExtension == "app" {
70 return Bundle.main.bundleURL
71 }
72 let fixed = URL(fileURLWithPath: "/Applications/Knowtation.app", isDirectory: true)
73 guard FileManager.default.fileExists(atPath: fixed.path) else {
74 throw RuntimeLaunchError.appNotInstalled
75 }
76 return fixed
77 }
78
79 private func requestCapability() throws -> FileHandle {
80 try CustodyCapability.request(mode: mode)
81 }
82
83 private func spawnAndWait(
84 executable: String,
85 entrypoint: String,
86 appRoot: String,
87 capability: FileHandle,
88 arguments: [String]
89 ) throws -> Int32 {
90 let argv = [executable, entrypoint, "--mode", mode.rawValue, "--"] + arguments
91 var environment = ProcessInfo.processInfo.environment
92 for key in environment.keys where Self.deniedEnvironmentKey(key) {
93 environment.removeValue(forKey: key)
94 }
95 environment["KNOWTATION_CUSTODY_FD"] = "3"
96 environment["KNOWTATION_APP_ROOT"] = appRoot
97 environment["KNOWTATION_RELEASE_VERSION"] = KnowtationReleaseContract.version
98 environment["KNOWTATION_RELEASE_BUILD"] = String(KnowtationReleaseContract.build)
99 environment["PATH"] = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin"
100
101 var actions: posix_spawn_file_actions_t?
102 posix_spawn_file_actions_init(&actions)
103 defer { posix_spawn_file_actions_destroy(&actions) }
104 let capabilityFD = capability.fileDescriptor
105 guard posix_spawn_file_actions_adddup2(&actions, capabilityFD, 3) == 0 else {
106 throw RuntimeLaunchError.spawnFailed(EINVAL)
107 }
108
109 let cArguments = argv.map { strdup($0) } + [nil]
110 let environmentStrings = environment.map { "\($0.key)=\($0.value)" }.sorted()
111 let cEnvironment = environmentStrings
112 .sorted()
113 .map { strdup($0) } + [nil]
114 defer {
115 cArguments.compactMap { $0 }.forEach { free($0) }
116 cEnvironment.compactMap { $0 }.forEach { free($0) }
117 }
118
119 var pid: pid_t = 0
120 let spawnStatus = executable.withCString { executablePointer in
121 cArguments.withUnsafeBufferPointer { argumentBuffer in
122 cEnvironment.withUnsafeBufferPointer { environmentBuffer in
123 posix_spawn(
124 &pid,
125 executablePointer,
126 &actions,
127 nil,
128 UnsafeMutablePointer(mutating: argumentBuffer.baseAddress),
129 UnsafeMutablePointer(mutating: environmentBuffer.baseAddress)
130 )
131 }
132 }
133 }
134 guard spawnStatus == 0 else { throw RuntimeLaunchError.spawnFailed(spawnStatus) }
135
136 var waitStatus: Int32 = 0
137 while waitpid(pid, &waitStatus, 0) == -1 && errno == EINTR {}
138 let signal = waitStatus & 0x7f
139 if signal == 0 { return (waitStatus >> 8) & 0xff }
140 if signal != 0x7f { return 128 + signal }
141 return 1
142 }
143
144 private static func deniedEnvironmentKey(_ key: String) -> Bool {
145 let exactDenied: Set<String> = [
146 "NODE_OPTIONS", "NODE_PATH", "NODE_EXTRA_CA_CERTS", "NODE_TLS_REJECT_UNAUTHORIZED",
147 "NODE_DEBUG", "NODE_USE_ENV_PROXY", "KNOWTATION_CUSTODY_FD", "KNOWTATION_HUB_URL",
148 "KNOWTATION_HUB_TOKEN", "SSL_CERT_FILE", "SSL_CERT_DIR", "SSLKEYLOGFILE", "OPENSSL_CONF",
149 "OPENSSL_MODULES", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY",
150 "http_proxy", "https_proxy", "all_proxy", "no_proxy",
151 ]
152 if exactDenied.contains(key) {
153 return true
154 }
155 return key.hasPrefix("DYLD_") || key.hasPrefix("LD_")
156 }
157 }
File History 1 commit
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700 feat(companion): add production macOS distribution path Human minor ⚠ 24 days ago