@hasna/recordings 0.2.10 → 0.2.13
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 +350 -35
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1922 -77
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1990 -29
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Darwin
|
|
3
|
+
import Foundation
|
|
4
|
+
import RecordingsUpdateProtocol
|
|
5
|
+
|
|
6
|
+
struct AtomicActivator {
|
|
7
|
+
/// Persist the exact activation binding before MonotonicReleaseStateStore writes
|
|
8
|
+
/// its highest-seen barrier. A crash can therefore never leave an unrecoverable
|
|
9
|
+
/// `seen` state whose signed envelope must be replayed after expiry.
|
|
10
|
+
func prepareActivation(
|
|
11
|
+
candidatePath: String,
|
|
12
|
+
stagedUpdate: StagedUpdate,
|
|
13
|
+
payload: ReleaseEnvelopePayload,
|
|
14
|
+
excludingAuthenticatedClientPID: pid_t
|
|
15
|
+
) throws {
|
|
16
|
+
try validateApplicationsDirectory()
|
|
17
|
+
do {
|
|
18
|
+
try ApplicationProcessQuiescence.requireQuiescence(
|
|
19
|
+
excludingAuthenticatedClientPID: excludingAuthenticatedClientPID
|
|
20
|
+
)
|
|
21
|
+
} catch {
|
|
22
|
+
throw AtomicActivationError.activationDeferred
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let applicationPath = RecordingsUpdateConstants.applicationPath
|
|
26
|
+
let previousPath = stagedUpdate.directory + "/previous.app"
|
|
27
|
+
let failedCandidatePath = stagedUpdate.directory + "/failed-candidate.app"
|
|
28
|
+
let previousDigest = try digestIfPresent(applicationPath)
|
|
29
|
+
let candidateMode = try applicationMode(candidatePath)
|
|
30
|
+
let previousMode = previousDigest == nil ? nil : try applicationMode(applicationPath)
|
|
31
|
+
let candidateExecutableModes = try applicationExecutableModes(candidatePath)
|
|
32
|
+
let previousExecutableModes = previousDigest == nil
|
|
33
|
+
? nil
|
|
34
|
+
: try applicationExecutableModes(applicationPath)
|
|
35
|
+
guard try digestIfPresent(candidatePath) == payload.candidateTreeSHA256,
|
|
36
|
+
try digestIfPresent(previousPath) == nil,
|
|
37
|
+
try digestIfPresent(failedCandidatePath) == nil
|
|
38
|
+
else {
|
|
39
|
+
throw AtomicActivationError.invalidPreparedState
|
|
40
|
+
}
|
|
41
|
+
let envelopePayloadDigest = try SHA256.hash(data: payload.canonicalData())
|
|
42
|
+
.map { String(format: "%02x", $0) }
|
|
43
|
+
.joined()
|
|
44
|
+
let journal = BrokerInstallJournal(
|
|
45
|
+
schemaVersion: RecordingsUpdateConstants.protocolVersion,
|
|
46
|
+
transactionID: stagedUpdate.transactionID.uuidString.lowercased(),
|
|
47
|
+
phase: "prepared",
|
|
48
|
+
releaseID: payload.releaseID,
|
|
49
|
+
releaseSequence: payload.releaseSequence,
|
|
50
|
+
keyEpoch: payload.keyEpoch,
|
|
51
|
+
envelopePayloadSHA256: envelopePayloadDigest,
|
|
52
|
+
artifactSHA256: stagedUpdate.archive.sha256,
|
|
53
|
+
manifestSHA256: stagedUpdate.manifest.sha256,
|
|
54
|
+
cohortPackageSHA256: payload.packageSHA256,
|
|
55
|
+
candidateTreeSHA256: payload.candidateTreeSHA256,
|
|
56
|
+
minimumOSVersion: payload.minimumOSVersion,
|
|
57
|
+
candidateApplicationMode: candidateMode,
|
|
58
|
+
candidateExecutableModes: candidateExecutableModes,
|
|
59
|
+
previousTreeSHA256: previousDigest,
|
|
60
|
+
previousApplicationMode: previousMode,
|
|
61
|
+
previousExecutableModes: previousExecutableModes,
|
|
62
|
+
previousApplicationPath: previousDigest == nil ? nil : previousPath,
|
|
63
|
+
candidateApplicationPath: candidatePath,
|
|
64
|
+
applicationPath: applicationPath
|
|
65
|
+
)
|
|
66
|
+
try DurableInstallJournal(transactionDirectory: stagedUpdate.directory).write(journal)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func activatePrepared(
|
|
70
|
+
stagedUpdate: StagedUpdate,
|
|
71
|
+
payload: ReleaseEnvelopePayload,
|
|
72
|
+
excludingAuthenticatedClientPID: pid_t
|
|
73
|
+
) throws {
|
|
74
|
+
let durableJournal = DurableInstallJournal(transactionDirectory: stagedUpdate.directory)
|
|
75
|
+
guard var journal = try durableJournal.read(),
|
|
76
|
+
journal.phase == "prepared",
|
|
77
|
+
journal.transactionID == stagedUpdate.transactionID.uuidString.lowercased(),
|
|
78
|
+
journal.releaseID == payload.releaseID,
|
|
79
|
+
journal.releaseSequence == payload.releaseSequence,
|
|
80
|
+
journal.keyEpoch == payload.keyEpoch,
|
|
81
|
+
journal.artifactSHA256 == stagedUpdate.archive.sha256,
|
|
82
|
+
journal.manifestSHA256 == stagedUpdate.manifest.sha256,
|
|
83
|
+
journal.cohortPackageSHA256 == payload.packageSHA256,
|
|
84
|
+
journal.candidateTreeSHA256 == payload.candidateTreeSHA256,
|
|
85
|
+
journal.minimumOSVersion == payload.minimumOSVersion,
|
|
86
|
+
journal.envelopePayloadSHA256 == envelopeDigest(payload)
|
|
87
|
+
else {
|
|
88
|
+
throw AtomicActivationError.recoveryRequired
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let namespace: ApplicationNamespace
|
|
92
|
+
do {
|
|
93
|
+
namespace = try ApplicationNamespace(journal: journal)
|
|
94
|
+
try ApplicationProcessQuiescence.requireQuiescence(
|
|
95
|
+
excludingAuthenticatedClientPID: excludingAuthenticatedClientPID
|
|
96
|
+
)
|
|
97
|
+
} catch is ApplicationProcessQuiescenceError {
|
|
98
|
+
throw AtomicActivationError.activationDeferred
|
|
99
|
+
} catch {
|
|
100
|
+
throw AtomicActivationError.recoveryRequired
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
do {
|
|
104
|
+
journal.phase = "launch-barrier-pending"
|
|
105
|
+
try durableJournal.write(journal)
|
|
106
|
+
try namespace.engageLaunchBarrier(journal: journal)
|
|
107
|
+
journal.phase = "launch-barrier-held"
|
|
108
|
+
try durableJournal.write(journal)
|
|
109
|
+
do {
|
|
110
|
+
try ApplicationProcessQuiescence.requireQuiescence(
|
|
111
|
+
excludingAuthenticatedClientPID: excludingAuthenticatedClientPID
|
|
112
|
+
)
|
|
113
|
+
} catch {
|
|
114
|
+
throw AtomicActivationError.activationDeferred
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if let previousDigest = journal.previousTreeSHA256 {
|
|
118
|
+
guard ActivationRecoveryPolicy.commitAction(
|
|
119
|
+
snapshot: try Self.namespaceSnapshot(journal),
|
|
120
|
+
previousDigest: previousDigest,
|
|
121
|
+
candidateDigest: journal.candidateTreeSHA256
|
|
122
|
+
) == .exchangeCandidateAndLive else {
|
|
123
|
+
throw AtomicActivationError.invalidPreparedState
|
|
124
|
+
}
|
|
125
|
+
journal.phase = "swap-pending"
|
|
126
|
+
try durableJournal.write(journal)
|
|
127
|
+
try namespace.exchangeCandidateAndLive()
|
|
128
|
+
guard ActivationRecoveryPolicy.commitAction(
|
|
129
|
+
snapshot: try Self.namespaceSnapshot(journal),
|
|
130
|
+
previousDigest: previousDigest,
|
|
131
|
+
candidateDigest: journal.candidateTreeSHA256
|
|
132
|
+
) == .retainPrevious else {
|
|
133
|
+
throw AtomicActivationError.bindingChanged
|
|
134
|
+
}
|
|
135
|
+
journal.phase = "swapped"
|
|
136
|
+
try durableJournal.write(journal)
|
|
137
|
+
|
|
138
|
+
journal.phase = "previous-retaining"
|
|
139
|
+
try durableJournal.write(journal)
|
|
140
|
+
try namespace.retainSwappedPreviousExclusively()
|
|
141
|
+
guard ActivationRecoveryPolicy.commitAction(
|
|
142
|
+
snapshot: try Self.namespaceSnapshot(journal),
|
|
143
|
+
previousDigest: previousDigest,
|
|
144
|
+
candidateDigest: journal.candidateTreeSHA256
|
|
145
|
+
) == .ready else {
|
|
146
|
+
throw AtomicActivationError.bindingChanged
|
|
147
|
+
}
|
|
148
|
+
journal.phase = "previous-retained"
|
|
149
|
+
try durableJournal.write(journal)
|
|
150
|
+
} else {
|
|
151
|
+
guard ActivationRecoveryPolicy.commitAction(
|
|
152
|
+
snapshot: try Self.namespaceSnapshot(journal),
|
|
153
|
+
previousDigest: nil,
|
|
154
|
+
candidateDigest: journal.candidateTreeSHA256
|
|
155
|
+
) == .installCandidateExclusively else {
|
|
156
|
+
throw AtomicActivationError.invalidPreparedState
|
|
157
|
+
}
|
|
158
|
+
journal.phase = "first-install-pending"
|
|
159
|
+
try durableJournal.write(journal)
|
|
160
|
+
try namespace.installCandidateExclusively()
|
|
161
|
+
guard ActivationRecoveryPolicy.commitAction(
|
|
162
|
+
snapshot: try Self.namespaceSnapshot(journal),
|
|
163
|
+
previousDigest: nil,
|
|
164
|
+
candidateDigest: journal.candidateTreeSHA256
|
|
165
|
+
) == .ready else {
|
|
166
|
+
throw AtomicActivationError.bindingChanged
|
|
167
|
+
}
|
|
168
|
+
journal.phase = "first-installed"
|
|
169
|
+
try durableJournal.write(journal)
|
|
170
|
+
}
|
|
171
|
+
journal.phase = "launch-barrier-releasing"
|
|
172
|
+
try durableJournal.write(journal)
|
|
173
|
+
try namespace.releaseCommittedLaunchBarrier(journal: journal)
|
|
174
|
+
try Self.requireCommittedNamespaceState(journal)
|
|
175
|
+
journal.phase = "launch-barrier-released"
|
|
176
|
+
try durableJournal.write(journal)
|
|
177
|
+
journal.phase = "activated"
|
|
178
|
+
try durableJournal.write(journal)
|
|
179
|
+
journal.phase = "committed"
|
|
180
|
+
try durableJournal.write(journal)
|
|
181
|
+
} catch {
|
|
182
|
+
if Self.requiresCommitRecovery(phase: journal.phase) {
|
|
183
|
+
// Once release of the launch barrier begins, the candidate may be
|
|
184
|
+
// executable. Never roll back underneath a process that could have
|
|
185
|
+
// started; startup recovery completes this exact bound candidate.
|
|
186
|
+
throw AtomicActivationError.recoveryRequired
|
|
187
|
+
}
|
|
188
|
+
do {
|
|
189
|
+
try rollback(
|
|
190
|
+
journal: &journal,
|
|
191
|
+
durableJournal: durableJournal,
|
|
192
|
+
namespace: namespace
|
|
193
|
+
)
|
|
194
|
+
} catch {
|
|
195
|
+
throw AtomicActivationError.recoveryRequired
|
|
196
|
+
}
|
|
197
|
+
if error is ApplicationProcessQuiescenceError {
|
|
198
|
+
throw AtomicActivationError.activationDeferred
|
|
199
|
+
}
|
|
200
|
+
if let activationError = error as? AtomicActivationError {
|
|
201
|
+
throw activationError
|
|
202
|
+
}
|
|
203
|
+
throw AtomicActivationError.namespaceMutationFailed
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private func rollback(
|
|
208
|
+
journal: inout BrokerInstallJournal,
|
|
209
|
+
durableJournal: DurableInstallJournal,
|
|
210
|
+
namespace: ApplicationNamespace
|
|
211
|
+
) throws {
|
|
212
|
+
journal.phase = "rollback-started"
|
|
213
|
+
try durableJournal.write(journal)
|
|
214
|
+
try Self.restoreRollbackTerminal(journal: journal, namespace: namespace)
|
|
215
|
+
try namespace.releaseRolledBackLaunchBarrier(journal: journal)
|
|
216
|
+
try Self.requireRollbackTerminalState(journal: journal)
|
|
217
|
+
journal.phase = "rolled-back"
|
|
218
|
+
try durableJournal.write(journal)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
static func restoreRollbackTerminal(
|
|
222
|
+
journal: BrokerInstallJournal,
|
|
223
|
+
namespace: ApplicationNamespace
|
|
224
|
+
) throws {
|
|
225
|
+
for _ in 0..<4 {
|
|
226
|
+
let snapshot = try namespaceSnapshot(journal)
|
|
227
|
+
switch ActivationRecoveryPolicy.rollbackAction(
|
|
228
|
+
snapshot: snapshot,
|
|
229
|
+
previousDigest: journal.previousTreeSHA256,
|
|
230
|
+
candidateDigest: journal.candidateTreeSHA256
|
|
231
|
+
) {
|
|
232
|
+
case .exchangeCandidateAndLive:
|
|
233
|
+
try namespace.exchangeCandidateAndLive()
|
|
234
|
+
case .exchangePreviousAndLive:
|
|
235
|
+
try namespace.exchangeRetainedPreviousAndLive()
|
|
236
|
+
case .retainFailedFromPrevious:
|
|
237
|
+
try namespace.retainFailedCandidateFromPreviousSlotExclusively()
|
|
238
|
+
case .retainFailedFromLive:
|
|
239
|
+
try namespace.retainFailedCandidateFromLiveExclusively()
|
|
240
|
+
case .ready:
|
|
241
|
+
return
|
|
242
|
+
case .invalid:
|
|
243
|
+
throw AtomicActivationError.unprovenNamespaceDuringRollback
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw AtomicActivationError.unprovenNamespaceDuringRollback
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
static func requireRollbackTerminalState(journal: BrokerInstallJournal) throws {
|
|
250
|
+
let snapshot = try normalNamespaceSnapshot(journal)
|
|
251
|
+
guard ActivationRecoveryPolicy.rollbackAction(
|
|
252
|
+
snapshot: snapshot,
|
|
253
|
+
previousDigest: journal.previousTreeSHA256,
|
|
254
|
+
candidateDigest: journal.candidateTreeSHA256
|
|
255
|
+
) == .ready else {
|
|
256
|
+
throw AtomicActivationError.unprovenNamespaceDuringRollback
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
static func normalNamespaceSnapshot(
|
|
261
|
+
_ journal: BrokerInstallJournal
|
|
262
|
+
) throws -> ApplicationNamespaceSnapshot {
|
|
263
|
+
ApplicationNamespaceSnapshot(
|
|
264
|
+
live: try digestIfPresent(journal.applicationPath),
|
|
265
|
+
candidateSlot: try digestIfPresent(journal.candidateApplicationPath),
|
|
266
|
+
previousSlot: try digestIfPresent(journal.previousApplicationPath),
|
|
267
|
+
failedSlot: try digestIfPresent(failedCandidatePath(journal))
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
static func requireCommittedNamespaceState(journal: BrokerInstallJournal) throws {
|
|
272
|
+
guard try digestIfPresent(journal.applicationPath) == journal.candidateTreeSHA256,
|
|
273
|
+
try digestIfPresent(journal.candidateApplicationPath) == nil,
|
|
274
|
+
try digestIfPresent(journal.previousApplicationPath) == journal.previousTreeSHA256,
|
|
275
|
+
try digestIfPresent(failedCandidatePath(journal)) == nil
|
|
276
|
+
else {
|
|
277
|
+
throw AtomicActivationError.bindingChanged
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static func namespaceSnapshot(
|
|
282
|
+
_ journal: BrokerInstallJournal
|
|
283
|
+
) throws -> ApplicationNamespaceSnapshot {
|
|
284
|
+
ApplicationNamespaceSnapshot(
|
|
285
|
+
live: try barrierBoundDigestIfPresent(journal.applicationPath, journal: journal),
|
|
286
|
+
candidateSlot: try barrierBoundDigestIfPresent(
|
|
287
|
+
journal.candidateApplicationPath,
|
|
288
|
+
journal: journal
|
|
289
|
+
),
|
|
290
|
+
previousSlot: try barrierBoundDigestIfPresent(
|
|
291
|
+
journal.previousApplicationPath,
|
|
292
|
+
journal: journal
|
|
293
|
+
),
|
|
294
|
+
failedSlot: try barrierBoundDigestIfPresent(
|
|
295
|
+
failedCandidatePath(journal),
|
|
296
|
+
journal: journal
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
static func barrierBoundDigestIfPresent(
|
|
302
|
+
_ path: String?,
|
|
303
|
+
journal: BrokerInstallJournal
|
|
304
|
+
) throws -> String? {
|
|
305
|
+
guard let path else { return nil }
|
|
306
|
+
var metadata = stat()
|
|
307
|
+
if lstat(path, &metadata) != 0 {
|
|
308
|
+
if errno == ENOENT { return nil }
|
|
309
|
+
throw AtomicActivationError.bindingChanged
|
|
310
|
+
}
|
|
311
|
+
let applicationMode = UInt16(metadata.st_mode & 0o777)
|
|
312
|
+
let allowedApplicationModes = Set(
|
|
313
|
+
[0, journal.candidateApplicationMode, journal.previousApplicationMode ?? 0]
|
|
314
|
+
)
|
|
315
|
+
guard allowedApplicationModes.contains(applicationMode) else {
|
|
316
|
+
throw AtomicActivationError.bindingChanged
|
|
317
|
+
}
|
|
318
|
+
var actualExecutableModes: [String: UInt16] = [:]
|
|
319
|
+
for relativePath in journal.candidateExecutableModes.keys {
|
|
320
|
+
var executableMetadata = stat()
|
|
321
|
+
guard lstat(path + "/" + relativePath, &executableMetadata) == 0,
|
|
322
|
+
(executableMetadata.st_mode & S_IFMT) == S_IFREG
|
|
323
|
+
else {
|
|
324
|
+
throw AtomicActivationError.bindingChanged
|
|
325
|
+
}
|
|
326
|
+
let executableMode = UInt16(executableMetadata.st_mode & 0o777)
|
|
327
|
+
let allowedExecutableModes = Set([
|
|
328
|
+
0,
|
|
329
|
+
journal.candidateExecutableModes[relativePath] ?? 0,
|
|
330
|
+
journal.previousExecutableModes?[relativePath] ?? 0,
|
|
331
|
+
])
|
|
332
|
+
guard allowedExecutableModes.contains(executableMode) else {
|
|
333
|
+
throw AtomicActivationError.bindingChanged
|
|
334
|
+
}
|
|
335
|
+
actualExecutableModes[relativePath] = executableMode
|
|
336
|
+
}
|
|
337
|
+
if applicationMode != 0,
|
|
338
|
+
actualExecutableModes.values.allSatisfy({ $0 != 0 }) {
|
|
339
|
+
return try CanonicalTree.digest(at: path)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
var candidateOverrides = journal.candidateExecutableModes
|
|
343
|
+
candidateOverrides["."] = journal.candidateApplicationMode
|
|
344
|
+
let candidateDigest = try CanonicalTree.digest(
|
|
345
|
+
at: path,
|
|
346
|
+
modeOverrides: candidateOverrides
|
|
347
|
+
)
|
|
348
|
+
if candidateDigest == journal.candidateTreeSHA256 { return candidateDigest }
|
|
349
|
+
if let previousDigest = journal.previousTreeSHA256,
|
|
350
|
+
let previousMode = journal.previousApplicationMode {
|
|
351
|
+
guard var previousOverrides = journal.previousExecutableModes else {
|
|
352
|
+
throw AtomicActivationError.bindingChanged
|
|
353
|
+
}
|
|
354
|
+
previousOverrides["."] = previousMode
|
|
355
|
+
let restoredDigest = try CanonicalTree.digest(
|
|
356
|
+
at: path,
|
|
357
|
+
modeOverrides: previousOverrides
|
|
358
|
+
)
|
|
359
|
+
if restoredDigest == previousDigest { return restoredDigest }
|
|
360
|
+
}
|
|
361
|
+
throw AtomicActivationError.bindingChanged
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
static func digestIfPresent(_ path: String?) throws -> String? {
|
|
365
|
+
guard let path else { return nil }
|
|
366
|
+
var metadata = stat()
|
|
367
|
+
if lstat(path, &metadata) != 0 {
|
|
368
|
+
if errno == ENOENT { return nil }
|
|
369
|
+
throw AtomicActivationError.bindingChanged
|
|
370
|
+
}
|
|
371
|
+
return try CanonicalTree.digest(at: path)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
static func applicationMode(_ path: String) throws -> UInt16 {
|
|
375
|
+
let descriptor = Darwin.open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
|
|
376
|
+
guard descriptor >= 0 else { throw AtomicActivationError.bindingChanged }
|
|
377
|
+
defer { Darwin.close(descriptor) }
|
|
378
|
+
var metadata = stat()
|
|
379
|
+
guard fstat(descriptor, &metadata) == 0,
|
|
380
|
+
(metadata.st_mode & S_IFMT) == S_IFDIR,
|
|
381
|
+
metadata.st_uid == 0,
|
|
382
|
+
(metadata.st_mode & 0o022) == 0,
|
|
383
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
|
|
384
|
+
else {
|
|
385
|
+
throw AtomicActivationError.bindingChanged
|
|
386
|
+
}
|
|
387
|
+
return UInt16(metadata.st_mode & 0o777)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
static func applicationExecutableModes(
|
|
391
|
+
_ applicationPath: String
|
|
392
|
+
) throws -> [String: UInt16] {
|
|
393
|
+
let paths = [
|
|
394
|
+
"Contents/MacOS/Recordings",
|
|
395
|
+
"Contents/Helpers/recordings",
|
|
396
|
+
"Contents/Helpers/recordings-update-client",
|
|
397
|
+
]
|
|
398
|
+
var modes: [String: UInt16] = [:]
|
|
399
|
+
for relativePath in paths {
|
|
400
|
+
let descriptor = Darwin.open(
|
|
401
|
+
applicationPath + "/" + relativePath,
|
|
402
|
+
O_RDONLY | O_CLOEXEC | O_NOFOLLOW
|
|
403
|
+
)
|
|
404
|
+
guard descriptor >= 0 else { throw AtomicActivationError.bindingChanged }
|
|
405
|
+
var metadata = stat()
|
|
406
|
+
let valid = fstat(descriptor, &metadata) == 0 &&
|
|
407
|
+
(metadata.st_mode & S_IFMT) == S_IFREG &&
|
|
408
|
+
metadata.st_uid == 0 &&
|
|
409
|
+
(metadata.st_mode & 0o022) == 0 &&
|
|
410
|
+
metadata.st_nlink == 1 &&
|
|
411
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
|
|
412
|
+
Darwin.close(descriptor)
|
|
413
|
+
guard valid else { throw AtomicActivationError.bindingChanged }
|
|
414
|
+
modes[relativePath] = UInt16(metadata.st_mode & 0o777)
|
|
415
|
+
}
|
|
416
|
+
return modes
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
static func failedCandidatePath(_ journal: BrokerInstallJournal) -> String {
|
|
420
|
+
URL(fileURLWithPath: journal.candidateApplicationPath)
|
|
421
|
+
.deletingLastPathComponent()
|
|
422
|
+
.deletingLastPathComponent()
|
|
423
|
+
.appendingPathComponent("failed-candidate.app").path
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private func validateApplicationsDirectory() throws {
|
|
427
|
+
guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
|
|
428
|
+
to: "/Applications"
|
|
429
|
+
) else {
|
|
430
|
+
throw AtomicActivationError.unsafeApplicationsDirectory
|
|
431
|
+
}
|
|
432
|
+
let descriptor = Darwin.open("/Applications", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
|
|
433
|
+
guard descriptor >= 0 else { throw AtomicActivationError.unsafeApplicationsDirectory }
|
|
434
|
+
defer { Darwin.close(descriptor) }
|
|
435
|
+
guard DarwinACLValidator.descriptorIsSafeRootOwnedDirectory(
|
|
436
|
+
descriptor,
|
|
437
|
+
exactPath: "/Applications"
|
|
438
|
+
) else {
|
|
439
|
+
throw AtomicActivationError.unsafeApplicationsDirectory
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private func envelopeDigest(_ payload: ReleaseEnvelopePayload) throws -> String {
|
|
444
|
+
try SHA256.hash(data: payload.canonicalData())
|
|
445
|
+
.map { String(format: "%02x", $0) }
|
|
446
|
+
.joined()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private static func requiresCommitRecovery(phase: String) -> Bool {
|
|
450
|
+
phase == "launch-barrier-releasing" ||
|
|
451
|
+
phase == "launch-barrier-released" ||
|
|
452
|
+
phase == "activated" ||
|
|
453
|
+
phase == "committed"
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
enum AtomicActivationError: Error, CustomStringConvertible {
|
|
458
|
+
case unsafeApplicationsDirectory
|
|
459
|
+
case activationDeferred
|
|
460
|
+
case invalidPreparedState
|
|
461
|
+
case namespaceMutationFailed
|
|
462
|
+
case bindingChanged
|
|
463
|
+
case unprovenNamespaceDuringRollback
|
|
464
|
+
case recoveryRequired
|
|
465
|
+
|
|
466
|
+
var description: String {
|
|
467
|
+
switch self {
|
|
468
|
+
case .unsafeApplicationsDirectory: "The Applications directory is unsafe"
|
|
469
|
+
case .activationDeferred: "Activation is deferred until live-bundle processes exit"
|
|
470
|
+
case .invalidPreparedState: "The prepared activation namespace is invalid"
|
|
471
|
+
case .namespaceMutationFailed: "The atomic application namespace mutation failed"
|
|
472
|
+
case .bindingChanged: "An application tree changed across activation"
|
|
473
|
+
case .unprovenNamespaceDuringRollback: "Rollback found an unproven application namespace"
|
|
474
|
+
case .recoveryRequired: "Activation rollback requires broker startup recovery"
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|