@hasna/recordings 0.2.11 → 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 +349 -69
- 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 +1919 -98
- 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 +1973 -236
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
|
@@ -1,19 +1,245 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
set -
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
umask 077
|
|
4
|
+
export LC_ALL=C
|
|
5
|
+
export LANG=C
|
|
6
|
+
export TZ=UTC0
|
|
7
|
+
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
# Release installs never resolve integrity-sensitive tools through PATH. The
|
|
10
|
+
# test overrides are accepted only when the real host kernel is not Darwin, so
|
|
11
|
+
# an inherited environment cannot replace pinned macOS tools in production.
|
|
12
|
+
if ! REAL_HOST_KERNEL="$(/usr/bin/uname -s)"; then
|
|
13
|
+
echo "Could not determine the host platform with pinned /usr/bin/uname." >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
case "$REAL_HOST_KERNEL" in
|
|
17
|
+
Darwin|Linux) ;;
|
|
18
|
+
*)
|
|
19
|
+
echo "Unsupported host platform from pinned /usr/bin/uname: ${REAL_HOST_KERNEL:-<empty>}." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
if [ "$REAL_HOST_KERNEL" = "Darwin" ]; then
|
|
24
|
+
AWK_EXECUTABLE="/usr/bin/awk"
|
|
25
|
+
BASENAME_EXECUTABLE="/usr/bin/basename"
|
|
26
|
+
CHMOD_EXECUTABLE="/bin/chmod"
|
|
27
|
+
CODESIGN_EXECUTABLE="/usr/bin/codesign"
|
|
28
|
+
CP_EXECUTABLE="/bin/cp"
|
|
29
|
+
DATE_EXECUTABLE="/bin/date"
|
|
30
|
+
DD_EXECUTABLE="/bin/dd"
|
|
31
|
+
DF_EXECUTABLE="/bin/df"
|
|
32
|
+
DIFF_EXECUTABLE="/usr/bin/diff"
|
|
33
|
+
DIRNAME_EXECUTABLE="/usr/bin/dirname"
|
|
34
|
+
DITTO_EXECUTABLE="/usr/bin/ditto"
|
|
35
|
+
DU_EXECUTABLE="/usr/bin/du"
|
|
36
|
+
GREP_EXECUTABLE="/usr/bin/grep"
|
|
37
|
+
HEAD_EXECUTABLE="/usr/bin/head"
|
|
38
|
+
HOSTNAME_EXECUTABLE="/bin/hostname"
|
|
39
|
+
ID_EXECUTABLE="/usr/bin/id"
|
|
40
|
+
IOREG_EXECUTABLE="/usr/sbin/ioreg"
|
|
41
|
+
LS_EXECUTABLE="/bin/ls"
|
|
42
|
+
LSOF_EXECUTABLE="/usr/sbin/lsof"
|
|
43
|
+
MDFIND_EXECUTABLE="/usr/bin/mdfind"
|
|
44
|
+
MKDIR_EXECUTABLE="/bin/mkdir"
|
|
45
|
+
MKTEMP_EXECUTABLE="/usr/bin/mktemp"
|
|
46
|
+
MV_EXECUTABLE="/bin/mv"
|
|
47
|
+
OPEN_EXECUTABLE="/usr/bin/open"
|
|
48
|
+
PS_EXECUTABLE="/bin/ps"
|
|
49
|
+
RM_EXECUTABLE="/bin/rm"
|
|
50
|
+
RMDIR_EXECUTABLE="/bin/rmdir"
|
|
51
|
+
SED_EXECUTABLE="/usr/bin/sed"
|
|
52
|
+
SHASUM_EXECUTABLE="/usr/bin/shasum"
|
|
53
|
+
SLEEP_EXECUTABLE="/bin/sleep"
|
|
54
|
+
SPCTL_EXECUTABLE="/usr/sbin/spctl"
|
|
55
|
+
SQLITE3_EXECUTABLE="/usr/bin/sqlite3"
|
|
56
|
+
STAT_EXECUTABLE="/usr/bin/stat"
|
|
57
|
+
SW_VERS_EXECUTABLE="/usr/bin/sw_vers"
|
|
58
|
+
SYSPOLICY_CHECK_EXECUTABLE="/usr/bin/syspolicy_check"
|
|
59
|
+
TAIL_EXECUTABLE="/usr/bin/tail"
|
|
60
|
+
TR_EXECUTABLE="/usr/bin/tr"
|
|
61
|
+
UNAME_EXECUTABLE="/usr/bin/uname"
|
|
62
|
+
XCRUN_EXECUTABLE="/usr/bin/xcrun"
|
|
63
|
+
else
|
|
64
|
+
AWK_EXECUTABLE="${RECORDINGS_TEST_INSTALL_AWK_EXECUTABLE:-/usr/bin/awk}"
|
|
65
|
+
BASENAME_EXECUTABLE="${RECORDINGS_TEST_INSTALL_BASENAME_EXECUTABLE:-/usr/bin/basename}"
|
|
66
|
+
CHMOD_EXECUTABLE="${RECORDINGS_TEST_INSTALL_CHMOD_EXECUTABLE:-/bin/chmod}"
|
|
67
|
+
CODESIGN_EXECUTABLE="${RECORDINGS_TEST_INSTALL_CODESIGN_EXECUTABLE:-/usr/bin/codesign}"
|
|
68
|
+
CP_EXECUTABLE="${RECORDINGS_TEST_INSTALL_CP_EXECUTABLE:-/bin/cp}"
|
|
69
|
+
DATE_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DATE_EXECUTABLE:-/bin/date}"
|
|
70
|
+
DD_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DD_EXECUTABLE:-/bin/dd}"
|
|
71
|
+
DF_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DF_EXECUTABLE:-/bin/df}"
|
|
72
|
+
DIFF_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DIFF_EXECUTABLE:-/usr/bin/diff}"
|
|
73
|
+
DIRNAME_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DIRNAME_EXECUTABLE:-/usr/bin/dirname}"
|
|
74
|
+
DITTO_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DITTO_EXECUTABLE:-/usr/bin/ditto}"
|
|
75
|
+
DU_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DU_EXECUTABLE:-/usr/bin/du}"
|
|
76
|
+
GREP_EXECUTABLE="${RECORDINGS_TEST_INSTALL_GREP_EXECUTABLE:-/usr/bin/grep}"
|
|
77
|
+
HEAD_EXECUTABLE="${RECORDINGS_TEST_INSTALL_HEAD_EXECUTABLE:-/usr/bin/head}"
|
|
78
|
+
HOSTNAME_EXECUTABLE="${RECORDINGS_TEST_INSTALL_HOSTNAME_EXECUTABLE:-/bin/hostname}"
|
|
79
|
+
ID_EXECUTABLE="${RECORDINGS_TEST_INSTALL_ID_EXECUTABLE:-/usr/bin/id}"
|
|
80
|
+
IOREG_EXECUTABLE="${RECORDINGS_TEST_INSTALL_IOREG_EXECUTABLE:-/usr/sbin/ioreg}"
|
|
81
|
+
LS_EXECUTABLE="${RECORDINGS_TEST_INSTALL_LS_EXECUTABLE:-/bin/ls}"
|
|
82
|
+
LSOF_EXECUTABLE="${RECORDINGS_TEST_INSTALL_LSOF_EXECUTABLE:-/usr/sbin/lsof}"
|
|
83
|
+
MDFIND_EXECUTABLE="${RECORDINGS_TEST_INSTALL_MDFIND_EXECUTABLE:-/usr/bin/mdfind}"
|
|
84
|
+
MKDIR_EXECUTABLE="${RECORDINGS_TEST_INSTALL_MKDIR_EXECUTABLE:-/bin/mkdir}"
|
|
85
|
+
MKTEMP_EXECUTABLE="${RECORDINGS_TEST_INSTALL_MKTEMP_EXECUTABLE:-/usr/bin/mktemp}"
|
|
86
|
+
MV_EXECUTABLE="${RECORDINGS_TEST_INSTALL_MV_EXECUTABLE:-/bin/mv}"
|
|
87
|
+
OPEN_EXECUTABLE="${RECORDINGS_TEST_INSTALL_OPEN_EXECUTABLE:-/usr/bin/open}"
|
|
88
|
+
PS_EXECUTABLE="${RECORDINGS_TEST_INSTALL_PS_EXECUTABLE:-/bin/ps}"
|
|
89
|
+
RM_EXECUTABLE="${RECORDINGS_TEST_INSTALL_RM_EXECUTABLE:-/bin/rm}"
|
|
90
|
+
RMDIR_EXECUTABLE="${RECORDINGS_TEST_INSTALL_RMDIR_EXECUTABLE:-/bin/rmdir}"
|
|
91
|
+
SED_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SED_EXECUTABLE:-/usr/bin/sed}"
|
|
92
|
+
SHASUM_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SHASUM_EXECUTABLE:-/usr/bin/shasum}"
|
|
93
|
+
SLEEP_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SLEEP_EXECUTABLE:-/bin/sleep}"
|
|
94
|
+
SPCTL_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SPCTL_EXECUTABLE:-/usr/sbin/spctl}"
|
|
95
|
+
SQLITE3_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SQLITE3_EXECUTABLE:-/usr/bin/sqlite3}"
|
|
96
|
+
STAT_EXECUTABLE="${RECORDINGS_TEST_INSTALL_STAT_EXECUTABLE:-/usr/bin/stat}"
|
|
97
|
+
SW_VERS_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SW_VERS_EXECUTABLE:-/usr/bin/sw_vers}"
|
|
98
|
+
SYSPOLICY_CHECK_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SYSPOLICY_CHECK_EXECUTABLE:-/usr/bin/syspolicy_check}"
|
|
99
|
+
TAIL_EXECUTABLE="${RECORDINGS_TEST_INSTALL_TAIL_EXECUTABLE:-/usr/bin/tail}"
|
|
100
|
+
TR_EXECUTABLE="${RECORDINGS_TEST_INSTALL_TR_EXECUTABLE:-/usr/bin/tr}"
|
|
101
|
+
UNAME_EXECUTABLE="${RECORDINGS_TEST_INSTALL_UNAME_EXECUTABLE:-/usr/bin/uname}"
|
|
102
|
+
XCRUN_EXECUTABLE="${RECORDINGS_TEST_INSTALL_XCRUN_EXECUTABLE:-/usr/bin/xcrun}"
|
|
103
|
+
fi
|
|
104
|
+
BUN_EXECUTABLE="${RECORDINGS_BUN_EXECUTABLE:-}"
|
|
105
|
+
|
|
106
|
+
test_fault_hooks_enabled() {
|
|
107
|
+
[ "$REAL_HOST_KERNEL" != "Darwin" ] && \
|
|
108
|
+
[ "${RECORDINGS_TEST_ENABLE_RECOVERY_HOOKS:-0}" = 1 ]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
require_absolute_executable() {
|
|
112
|
+
local label="$1"
|
|
113
|
+
local executable="$2"
|
|
114
|
+
case "$executable" in
|
|
115
|
+
/*) ;;
|
|
116
|
+
*) echo "${label} must be an absolute executable path." >&2; exit 1 ;;
|
|
117
|
+
esac
|
|
118
|
+
[ -x "$executable" ] || {
|
|
119
|
+
echo "${label} is missing or is not executable: ${executable}" >&2
|
|
120
|
+
exit 1
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
require_absolute_canonical_owned_home() {
|
|
125
|
+
local candidate="${HOME:-}"
|
|
126
|
+
local canonical
|
|
127
|
+
case "$candidate" in
|
|
128
|
+
/*) ;;
|
|
129
|
+
*) echo "Recordings.app installation requires HOME to be an absolute canonical path." >&2; exit 1 ;;
|
|
130
|
+
esac
|
|
131
|
+
[ -d "$candidate" ] && [ ! -L "$candidate" ] || {
|
|
132
|
+
echo "Recordings.app installation requires HOME to be an absolute canonical path to a non-symlink directory." >&2
|
|
133
|
+
exit 1
|
|
134
|
+
}
|
|
135
|
+
canonical="$(cd "$candidate" 2>/dev/null && builtin pwd -P)" || {
|
|
136
|
+
echo "Recordings.app installation could not resolve HOME canonically." >&2
|
|
137
|
+
exit 1
|
|
138
|
+
}
|
|
139
|
+
[ "$candidate" = "$canonical" ] || {
|
|
140
|
+
echo "Recordings.app installation requires HOME to be an absolute canonical path." >&2
|
|
141
|
+
exit 1
|
|
142
|
+
}
|
|
143
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$candidate")" = "$("$ID_EXECUTABLE" -u)" ] || {
|
|
144
|
+
echo "Home ancestor has an unexpected owner." >&2
|
|
145
|
+
exit 1
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ARTIFACT_PATH=""
|
|
150
|
+
MANIFEST_PATH=""
|
|
151
|
+
EXPECTED_TEAM_ID="${RECORDINGS_EXPECTED_TEAM_IDENTIFIER:-}"
|
|
152
|
+
EXPECTED_MANIFEST_SHA256=""
|
|
153
|
+
EXPECTED_SOURCE_SHA=""
|
|
154
|
+
EXPECTED_VERSION=""
|
|
155
|
+
EXPECTED_OLD_IDENTITY_SHA256=""
|
|
156
|
+
EXPECTED_NEW_IDENTITY_SHA256=""
|
|
157
|
+
EXPECTED_HOSTNAME=""
|
|
158
|
+
EXPECTED_HOSTNAME_SET=0
|
|
159
|
+
ARTIFACT_POLICY="release"
|
|
160
|
+
APPROVED_TARGET="fleet"
|
|
161
|
+
APPROVED_TARGET_IDENTITY_KIND=""
|
|
162
|
+
APPROVED_TARGET_IDENTITY_SHA256="none"
|
|
163
|
+
ACKNOWLEDGE_LOCAL_SIGNING_AND_PERMISSIONS=0
|
|
164
|
+
ALLOW_IDENTITY_MIGRATION=0
|
|
165
|
+
LAUNCH_APP=0
|
|
166
|
+
LAUNCH_TIMEOUT_SECONDS="${RECORDINGS_LAUNCH_TIMEOUT_SECONDS:-10}"
|
|
6
167
|
|
|
7
168
|
while [ "$#" -gt 0 ]; do
|
|
8
169
|
case "$1" in
|
|
9
|
-
--
|
|
10
|
-
|
|
170
|
+
--artifact)
|
|
171
|
+
ARTIFACT_PATH="${2:-}"
|
|
172
|
+
shift 2
|
|
173
|
+
;;
|
|
174
|
+
--manifest)
|
|
175
|
+
MANIFEST_PATH="${2:-}"
|
|
176
|
+
shift 2
|
|
177
|
+
;;
|
|
178
|
+
--expected-team-id)
|
|
179
|
+
EXPECTED_TEAM_ID="${2:-}"
|
|
180
|
+
shift 2
|
|
181
|
+
;;
|
|
182
|
+
--manifest-sha256)
|
|
183
|
+
EXPECTED_MANIFEST_SHA256="${2:-}"
|
|
184
|
+
shift 2
|
|
185
|
+
;;
|
|
186
|
+
--expected-source-sha)
|
|
187
|
+
EXPECTED_SOURCE_SHA="${2:-}"
|
|
188
|
+
shift 2
|
|
189
|
+
;;
|
|
190
|
+
--expected-version)
|
|
191
|
+
EXPECTED_VERSION="${2:-}"
|
|
192
|
+
shift 2
|
|
193
|
+
;;
|
|
194
|
+
--expected-hostname)
|
|
195
|
+
EXPECTED_HOSTNAME="${2:-}"
|
|
196
|
+
EXPECTED_HOSTNAME_SET=1
|
|
197
|
+
shift 2
|
|
198
|
+
;;
|
|
199
|
+
--artifact-policy)
|
|
200
|
+
case "${2:-}" in
|
|
201
|
+
release) ARTIFACT_POLICY="release" ;;
|
|
202
|
+
local-only|local_only) ARTIFACT_POLICY="local_only" ;;
|
|
203
|
+
*) echo "Artifact policy must be release or local-only." >&2; exit 2 ;;
|
|
204
|
+
esac
|
|
205
|
+
shift 2
|
|
206
|
+
;;
|
|
207
|
+
--approved-target)
|
|
208
|
+
APPROVED_TARGET="${2:-}"
|
|
209
|
+
shift 2
|
|
210
|
+
;;
|
|
211
|
+
--approved-target-identity-sha256)
|
|
212
|
+
APPROVED_TARGET_IDENTITY_SHA256="${2:-}"
|
|
213
|
+
shift 2
|
|
214
|
+
;;
|
|
215
|
+
--approved-target-identity-kind)
|
|
216
|
+
APPROVED_TARGET_IDENTITY_KIND="${2:-}"
|
|
11
217
|
shift 2
|
|
12
218
|
;;
|
|
13
|
-
--
|
|
14
|
-
|
|
219
|
+
--acknowledge-local-signing-and-permissions)
|
|
220
|
+
ACKNOWLEDGE_LOCAL_SIGNING_AND_PERMISSIONS=1
|
|
15
221
|
shift
|
|
16
222
|
;;
|
|
223
|
+
--expected-old-identity-sha256)
|
|
224
|
+
EXPECTED_OLD_IDENTITY_SHA256="${2:-}"
|
|
225
|
+
shift 2
|
|
226
|
+
;;
|
|
227
|
+
--expected-new-identity-sha256)
|
|
228
|
+
EXPECTED_NEW_IDENTITY_SHA256="${2:-}"
|
|
229
|
+
shift 2
|
|
230
|
+
;;
|
|
231
|
+
--allow-signing-identity-migration)
|
|
232
|
+
ALLOW_IDENTITY_MIGRATION=1
|
|
233
|
+
shift
|
|
234
|
+
;;
|
|
235
|
+
--launch)
|
|
236
|
+
LAUNCH_APP=1
|
|
237
|
+
shift
|
|
238
|
+
;;
|
|
239
|
+
--launch-timeout)
|
|
240
|
+
LAUNCH_TIMEOUT_SECONDS="${2:-}"
|
|
241
|
+
shift 2
|
|
242
|
+
;;
|
|
17
243
|
*)
|
|
18
244
|
echo "Unknown argument: $1" >&2
|
|
19
245
|
exit 2
|
|
@@ -21,129 +247,1724 @@ while [ "$#" -gt 0 ]; do
|
|
|
21
247
|
esac
|
|
22
248
|
done
|
|
23
249
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
250
|
+
if [ "$REAL_HOST_KERNEL" = "Darwin" ] && [ "$ARTIFACT_POLICY" = "release" ]; then
|
|
251
|
+
echo "Release installation requires the root-owned Recordings updater broker; the user-owned shell installer is local-development only." >&2
|
|
252
|
+
exit 1
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
for tool_variable in \
|
|
256
|
+
AWK_EXECUTABLE BASENAME_EXECUTABLE CHMOD_EXECUTABLE CODESIGN_EXECUTABLE \
|
|
257
|
+
CP_EXECUTABLE DATE_EXECUTABLE DD_EXECUTABLE DF_EXECUTABLE DIFF_EXECUTABLE \
|
|
258
|
+
DIRNAME_EXECUTABLE DITTO_EXECUTABLE DU_EXECUTABLE GREP_EXECUTABLE \
|
|
259
|
+
HEAD_EXECUTABLE HOSTNAME_EXECUTABLE ID_EXECUTABLE IOREG_EXECUTABLE \
|
|
260
|
+
LS_EXECUTABLE LSOF_EXECUTABLE MDFIND_EXECUTABLE MKDIR_EXECUTABLE MKTEMP_EXECUTABLE \
|
|
261
|
+
MV_EXECUTABLE OPEN_EXECUTABLE PS_EXECUTABLE RM_EXECUTABLE RMDIR_EXECUTABLE \
|
|
262
|
+
SED_EXECUTABLE SHASUM_EXECUTABLE SLEEP_EXECUTABLE SPCTL_EXECUTABLE \
|
|
263
|
+
SQLITE3_EXECUTABLE STAT_EXECUTABLE SW_VERS_EXECUTABLE \
|
|
264
|
+
SYSPOLICY_CHECK_EXECUTABLE TAIL_EXECUTABLE TR_EXECUTABLE UNAME_EXECUTABLE \
|
|
265
|
+
XCRUN_EXECUTABLE; do
|
|
266
|
+
require_absolute_executable "$tool_variable" "${!tool_variable}"
|
|
267
|
+
done
|
|
268
|
+
require_absolute_executable "BUN_EXECUTABLE" "$BUN_EXECUTABLE"
|
|
269
|
+
require_absolute_canonical_owned_home
|
|
270
|
+
|
|
271
|
+
if [ "$("$UNAME_EXECUTABLE" -s)" != "Darwin" ]; then
|
|
272
|
+
echo "Recordings.app installation is only supported on macOS." >&2
|
|
273
|
+
exit 1
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
if [ -z "$ARTIFACT_PATH" ] || [ -z "$MANIFEST_PATH" ] || \
|
|
277
|
+
[ -z "$EXPECTED_MANIFEST_SHA256" ] || [ -z "$EXPECTED_SOURCE_SHA" ] || \
|
|
278
|
+
[ -z "$EXPECTED_VERSION" ]; then
|
|
279
|
+
echo "Install requires artifact, manifest, authenticated manifest SHA-256, exact source SHA, and exact version." >&2
|
|
280
|
+
exit 2
|
|
281
|
+
fi
|
|
282
|
+
if [ "$EXPECTED_HOSTNAME_SET" -eq 1 ] && [ -z "$EXPECTED_HOSTNAME" ]; then
|
|
283
|
+
echo "Expected hostname must not be empty." >&2
|
|
284
|
+
exit 2
|
|
285
|
+
fi
|
|
286
|
+
WORK_DIR="$("$MKTEMP_EXECUTABLE" -d /tmp/recordings-install.XXXXXX)"
|
|
287
|
+
cleanup_preflight_work() {
|
|
288
|
+
local status=$?
|
|
289
|
+
trap - EXIT
|
|
290
|
+
"$RM_EXECUTABLE" -rf "$WORK_DIR"
|
|
291
|
+
exit "$status"
|
|
292
|
+
}
|
|
293
|
+
trap cleanup_preflight_work EXIT
|
|
294
|
+
if [ -n "$EXPECTED_HOSTNAME" ]; then
|
|
295
|
+
ACTUAL_HOSTNAME="$("$HOSTNAME_EXECUTABLE" -s)"
|
|
296
|
+
if [ "$ACTUAL_HOSTNAME" != "$EXPECTED_HOSTNAME" ]; then
|
|
297
|
+
echo "Install target hostname ${ACTUAL_HOSTNAME} does not match the expected hostname ${EXPECTED_HOSTNAME}." >&2
|
|
298
|
+
exit 1
|
|
299
|
+
fi
|
|
300
|
+
fi
|
|
301
|
+
if [ -z "$APPROVED_TARGET_IDENTITY_KIND" ]; then
|
|
302
|
+
if [ "$ARTIFACT_POLICY" = "release" ]; then
|
|
303
|
+
APPROVED_TARGET_IDENTITY_KIND="none"
|
|
304
|
+
else
|
|
305
|
+
# Schema-v3 artifacts created before the discriminator used the platform UUID hash.
|
|
306
|
+
APPROVED_TARGET_IDENTITY_KIND="hardware_uuid_sha256"
|
|
307
|
+
fi
|
|
308
|
+
fi
|
|
309
|
+
PACKAGE_ROOT="$(cd "$("$DIRNAME_EXECUTABLE" "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
310
|
+
ARTIFACT_TOOL="${PACKAGE_ROOT}/scripts/macos_artifact.ts"
|
|
311
|
+
RUNTIME_SMOKE="${PACKAGE_ROOT}/scripts/smoke_macos_app.sh"
|
|
312
|
+
TAILSCALE_RESOLVER="${PACKAGE_ROOT}/scripts/resolve_tailscale_cli.sh"
|
|
313
|
+
if [ "$ARTIFACT_POLICY" = "release" ]; then
|
|
314
|
+
if [ -z "$EXPECTED_TEAM_ID" ]; then
|
|
315
|
+
echo "Release install requires --expected-team-id or RECORDINGS_EXPECTED_TEAM_IDENTIFIER." >&2
|
|
316
|
+
exit 2
|
|
317
|
+
fi
|
|
318
|
+
if [ "$APPROVED_TARGET" != "fleet" ]; then
|
|
319
|
+
echo "Release artifacts use the fleet target policy." >&2
|
|
320
|
+
exit 2
|
|
321
|
+
fi
|
|
322
|
+
if [ "$APPROVED_TARGET_IDENTITY_SHA256" != "none" ]; then
|
|
323
|
+
echo "Release artifacts do not accept a local-only target identity." >&2
|
|
324
|
+
exit 2
|
|
325
|
+
fi
|
|
326
|
+
if [ "$APPROVED_TARGET_IDENTITY_KIND" != "none" ]; then
|
|
327
|
+
echo "Release artifacts do not accept a local-only target identity kind." >&2
|
|
328
|
+
exit 2
|
|
329
|
+
fi
|
|
330
|
+
if [ "$ACKNOWLEDGE_LOCAL_SIGNING_AND_PERMISSIONS" -eq 1 ]; then
|
|
331
|
+
echo "Local-only acknowledgment cannot be supplied for a release artifact." >&2
|
|
332
|
+
exit 2
|
|
333
|
+
fi
|
|
334
|
+
else
|
|
335
|
+
if [ -n "$EXPECTED_TEAM_ID" ]; then
|
|
336
|
+
echo "Local-only artifacts do not accept --expected-team-id or a release TeamIdentifier environment value." >&2
|
|
337
|
+
exit 2
|
|
338
|
+
fi
|
|
339
|
+
if [ "$APPROVED_TARGET" != "station06" ]; then
|
|
340
|
+
echo "Local-only install is currently restricted to --approved-target station06." >&2
|
|
341
|
+
exit 2
|
|
342
|
+
fi
|
|
343
|
+
if ! [[ "$APPROVED_TARGET_IDENTITY_SHA256" =~ ^[a-f0-9]{64}$ ]]; then
|
|
344
|
+
echo "Local-only install requires --approved-target-identity-sha256 from the approved machine registry." >&2
|
|
345
|
+
exit 2
|
|
346
|
+
fi
|
|
347
|
+
case "$APPROVED_TARGET_IDENTITY_KIND" in
|
|
348
|
+
hardware_uuid_sha256|tailscale_node_id_sha256) ;;
|
|
349
|
+
*) echo "Local-only install requires a supported --approved-target-identity-kind." >&2; exit 2 ;;
|
|
350
|
+
esac
|
|
351
|
+
if [ "$ACKNOWLEDGE_LOCAL_SIGNING_AND_PERMISSIONS" -ne 1 ]; then
|
|
352
|
+
echo "Local-only install requires --acknowledge-local-signing-and-permissions because code identity can change and macOS may require Microphone or Accessibility reauthorization." >&2
|
|
353
|
+
exit 2
|
|
354
|
+
fi
|
|
355
|
+
ACTUAL_TARGET="$("$HOSTNAME_EXECUTABLE" -s)"
|
|
356
|
+
if [ "$ACTUAL_TARGET" != "$APPROVED_TARGET" ]; then
|
|
357
|
+
echo "Local-only artifact target ${APPROVED_TARGET} does not match this Mac (${ACTUAL_TARGET})." >&2
|
|
358
|
+
exit 1
|
|
359
|
+
fi
|
|
360
|
+
if [ "$APPROVED_TARGET_IDENTITY_KIND" = "tailscale_node_id_sha256" ]; then
|
|
361
|
+
if [ ! -r "$TAILSCALE_RESOLVER" ]; then
|
|
362
|
+
echo "Packaged Tailscale CLI resolver is missing." >&2
|
|
363
|
+
exit 1
|
|
364
|
+
fi
|
|
365
|
+
# shellcheck source=/dev/null
|
|
366
|
+
source "$TAILSCALE_RESOLVER"
|
|
367
|
+
if ! TAILSCALE_CLI="$(recordings_resolve_trusted_tailscale_app_cli "$WORK_DIR")"; then
|
|
368
|
+
echo "Tailscale is required to verify this local-only target identity." >&2
|
|
369
|
+
exit 1
|
|
370
|
+
fi
|
|
371
|
+
if ! ACTUAL_TARGET_IDENTITY_SHA256="$(recordings_run_trusted_tailscale_status "$TAILSCALE_CLI" "$WORK_DIR" | "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tailscale-node-id-sha256 --expected-hostname "$APPROVED_TARGET")"; then
|
|
372
|
+
echo "Could not verify the live Tailscale identity for this local-only target." >&2
|
|
373
|
+
exit 1
|
|
374
|
+
fi
|
|
375
|
+
else
|
|
376
|
+
ACTUAL_PLATFORM_ID="$("$IOREG_EXECUTABLE" -rd1 -c IOPlatformExpertDevice | "$AWK_EXECUTABLE" -F'"' '/IOPlatformUUID/ {print $(NF-1); exit}' | "$TR_EXECUTABLE" '[:upper:]' '[:lower:]')"
|
|
377
|
+
if [ -z "$ACTUAL_PLATFORM_ID" ]; then
|
|
378
|
+
echo "Could not read this Mac platform identity." >&2
|
|
379
|
+
exit 1
|
|
380
|
+
fi
|
|
381
|
+
ACTUAL_TARGET_IDENTITY_SHA256="$(printf '%s' "$ACTUAL_PLATFORM_ID" | "$SHASUM_EXECUTABLE" -a 256 | "$AWK_EXECUTABLE" '{print $1}')"
|
|
382
|
+
unset ACTUAL_PLATFORM_ID
|
|
383
|
+
fi
|
|
384
|
+
if [ "$ACTUAL_TARGET_IDENTITY_SHA256" != "$APPROVED_TARGET_IDENTITY_SHA256" ]; then
|
|
385
|
+
echo "Local-only artifact does not match this Mac's approved machine identity." >&2
|
|
386
|
+
exit 1
|
|
387
|
+
fi
|
|
388
|
+
if [ "$ALLOW_IDENTITY_MIGRATION" -eq 1 ] || [ -n "$EXPECTED_OLD_IDENTITY_SHA256" ] || [ -n "$EXPECTED_NEW_IDENTITY_SHA256" ]; then
|
|
389
|
+
echo "Release identity-migration flags are not valid for local-only artifacts." >&2
|
|
390
|
+
exit 2
|
|
391
|
+
fi
|
|
392
|
+
EXPECTED_TEAM_ID="ADHOC"
|
|
393
|
+
echo "WARNING: installing a non-notarized local-only artifact approved only for ${APPROVED_TARGET}." >&2
|
|
394
|
+
echo "WARNING: code identity can change; Microphone or Accessibility may require manual reauthorization." >&2
|
|
395
|
+
fi
|
|
396
|
+
if [ "$ARTIFACT_POLICY" = "release" ] && [ "$ALLOW_IDENTITY_MIGRATION" -eq 1 ] && \
|
|
397
|
+
{ [ -z "$EXPECTED_OLD_IDENTITY_SHA256" ] || [ -z "$EXPECTED_NEW_IDENTITY_SHA256" ]; }; then
|
|
398
|
+
echo "Identity migration requires exact --expected-old-identity-sha256 and --expected-new-identity-sha256 values." >&2
|
|
399
|
+
exit 2
|
|
400
|
+
fi
|
|
401
|
+
if [ "$ARTIFACT_POLICY" = "release" ] && [ "$ALLOW_IDENTITY_MIGRATION" -eq 0 ] && \
|
|
402
|
+
{ [ -n "$EXPECTED_OLD_IDENTITY_SHA256" ] || [ -n "$EXPECTED_NEW_IDENTITY_SHA256" ]; }; then
|
|
403
|
+
echo "Identity migration digests require --allow-signing-identity-migration." >&2
|
|
404
|
+
exit 2
|
|
405
|
+
fi
|
|
406
|
+
case "$LAUNCH_TIMEOUT_SECONDS" in
|
|
407
|
+
''|*[!0-9]*)
|
|
408
|
+
echo "Launch timeout must be an integer number of seconds." >&2
|
|
28
409
|
exit 2
|
|
29
410
|
;;
|
|
30
411
|
esac
|
|
31
|
-
|
|
32
|
-
|
|
412
|
+
if [ "$LAUNCH_TIMEOUT_SECONDS" -lt 1 ] || [ "$LAUNCH_TIMEOUT_SECONDS" -gt 120 ]; then
|
|
413
|
+
echo "Launch timeout must be between 1 and 120 seconds." >&2
|
|
414
|
+
exit 2
|
|
415
|
+
fi
|
|
416
|
+
if [ ! -f "$ARTIFACT_PATH" ] || [ ! -f "$MANIFEST_PATH" ]; then
|
|
417
|
+
echo "Artifact ZIP or manifest does not exist." >&2
|
|
418
|
+
exit 1
|
|
419
|
+
fi
|
|
420
|
+
[ -x "$RUNTIME_SMOKE" ] || { echo "Packaged runtime smoke verifier is missing." >&2; exit 1; }
|
|
421
|
+
# Fail before creating Applications, state, lock, or maintenance paths when the
|
|
422
|
+
# fixed production descriptor guard prebuild is absent or unsafe.
|
|
423
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" native-fs-guard-check
|
|
33
424
|
DATA_DIR="${HOME}/.hasna/recordings"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
425
|
+
DATA_PARENT="$("$DIRNAME_EXECUTABLE" "$DATA_DIR")"
|
|
426
|
+
APP_DEST="${HOME}/Applications/Recordings.app"
|
|
427
|
+
APP_PARENT="$("$DIRNAME_EXECUTABLE" "$APP_DEST")"
|
|
428
|
+
ROLLBACK_DIR="${DATA_DIR}/rollbacks"
|
|
429
|
+
JOURNAL_PATH="${APP_PARENT}/.Recordings-install-transaction.json"
|
|
430
|
+
LOCK_DIR="${APP_PARENT}/.Recordings-install-lock"
|
|
431
|
+
MAINTENANCE_DIR="${DATA_PARENT}/.recordings-install-maintenance"
|
|
432
|
+
READER_LEASES_DIR="${DATA_PARENT}/.recordings-store-readers"
|
|
433
|
+
LOCK_OWNED=0
|
|
434
|
+
MAINTENANCE_OWNED=0
|
|
435
|
+
MAINTENANCE_CLAIM_DIR=""
|
|
436
|
+
PRESERVE_MAINTENANCE_MARKER=0
|
|
437
|
+
RECOVERED_APP_RESTART_ON_ABORT=0
|
|
438
|
+
TRANSACTION_COMMITTED=0
|
|
439
|
+
STAGING_REMOVAL_DELEGATED=0
|
|
440
|
+
TRANSACTION_DIR=""
|
|
441
|
+
TRANSACTION_NONCE=""
|
|
442
|
+
STOPPED_RUNNING_APP=0
|
|
443
|
+
was_running=0
|
|
444
|
+
OLD_PIDS=""
|
|
445
|
+
RUNNING_EXECUTABLES=()
|
|
446
|
+
PRIOR_RUNNING_APP_PATHS=()
|
|
447
|
+
RECOVERED_RUNNING_APP_PATHS=()
|
|
448
|
+
SQLITE_BARRIER_ACTIVE=0
|
|
449
|
+
SQLITE_BARRIER_PID=""
|
|
450
|
+
SQLITE_BARRIER_DIR=""
|
|
451
|
+
MOVED_ORIGINALS=()
|
|
452
|
+
MOVED_PATHS=()
|
|
453
|
+
MOVED_DIGESTS=()
|
|
454
|
+
MOVED_ORIGINAL_COUNT=0
|
|
455
|
+
INSTALLER_OWNED_STATE_PATHS=()
|
|
456
|
+
INSTALLER_OWNED_STATE_DIGESTS=()
|
|
37
457
|
|
|
38
|
-
|
|
39
|
-
|
|
458
|
+
verify_secure_parent() {
|
|
459
|
+
local path="$1"
|
|
460
|
+
local private_mode="${2:-0}"
|
|
461
|
+
[ -d "$path" ] && [ ! -L "$path" ] || { echo "Secure install path must be a non-symlink directory: ${path}" >&2; exit 1; }
|
|
462
|
+
local actual_uid
|
|
463
|
+
local mode
|
|
464
|
+
actual_uid="$("$STAT_EXECUTABLE" -f '%u' "$path")"
|
|
465
|
+
mode="$("$STAT_EXECUTABLE" -f '%Lp' "$path")"
|
|
466
|
+
[ "$actual_uid" = "$("$ID_EXECUTABLE" -u)" ] || { echo "Secure install path has an unexpected owner: ${path}" >&2; exit 1; }
|
|
467
|
+
if [ "$private_mode" -eq 1 ] && [ "$mode" != 700 ]; then
|
|
468
|
+
echo "Private Recordings state path must have mode 700: ${path}" >&2
|
|
469
|
+
exit 1
|
|
470
|
+
fi
|
|
471
|
+
case "$mode" in
|
|
472
|
+
*[2367][0-7]|*[0-7][2367])
|
|
473
|
+
echo "Secure install path is group/world writable: ${path}" >&2
|
|
474
|
+
exit 1
|
|
475
|
+
;;
|
|
476
|
+
esac
|
|
477
|
+
if "$LS_EXECUTABLE" -lde "$path" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
|
|
478
|
+
echo "Secure install path has an unexpected ACL: ${path}" >&2
|
|
479
|
+
exit 1
|
|
480
|
+
fi
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
verify_secure_file() {
|
|
484
|
+
local path="$1"
|
|
485
|
+
local label="$2"
|
|
486
|
+
if [ -e "$path" ] || [ -L "$path" ]; then
|
|
487
|
+
[ -f "$path" ] && [ ! -L "$path" ] && \
|
|
488
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$path")" = "$("$ID_EXECUTABLE" -u)" ] && \
|
|
489
|
+
[ "$("$STAT_EXECUTABLE" -f '%Lp' "$path")" = 600 ] || {
|
|
490
|
+
echo "${label} has an unsafe type, owner, or mode." >&2
|
|
491
|
+
exit 1
|
|
492
|
+
}
|
|
493
|
+
if "$LS_EXECUTABLE" -lde "$path" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
|
|
494
|
+
echo "${label} has an unexpected ACL." >&2
|
|
495
|
+
exit 1
|
|
496
|
+
fi
|
|
497
|
+
fi
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
verify_safe_home_ancestor() {
|
|
501
|
+
local path="$1"
|
|
502
|
+
[ -d "$path" ] && [ ! -L "$path" ] || {
|
|
503
|
+
echo "Home ancestor must be a non-symlink directory." >&2
|
|
504
|
+
exit 1
|
|
505
|
+
}
|
|
506
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$path")" = "$("$ID_EXECUTABLE" -u)" ] || {
|
|
507
|
+
echo "Home ancestor has an unexpected owner." >&2
|
|
508
|
+
exit 1
|
|
509
|
+
}
|
|
510
|
+
home_mode="$("$STAT_EXECUTABLE" -f '%Lp' "$path")"
|
|
511
|
+
case "$home_mode" in
|
|
512
|
+
*[2367][0-7]|*[0-7][2367])
|
|
513
|
+
echo "Home ancestor is group/world writable." >&2
|
|
514
|
+
exit 1
|
|
515
|
+
;;
|
|
516
|
+
esac
|
|
517
|
+
home_acl="$("$LS_EXECUTABLE" -lde "$path" | "$TAIL_EXECUTABLE" -n +2)"
|
|
518
|
+
if [ -n "$home_acl" ] && printf '%s\n' "$home_acl" | "$GREP_EXECUTABLE" -v ' deny ' | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
|
|
519
|
+
echo "Home ancestor has an ACL that grants access." >&2
|
|
520
|
+
exit 1
|
|
521
|
+
fi
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
verify_existing_state_root() {
|
|
525
|
+
[ -e "$DATA_DIR" ] || [ -L "$DATA_DIR" ] || return 0
|
|
526
|
+
[ -d "$DATA_DIR" ] && [ ! -L "$DATA_DIR" ] || {
|
|
527
|
+
echo "Recordings state path must be a non-symlink directory." >&2
|
|
528
|
+
exit 1
|
|
529
|
+
}
|
|
530
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$DATA_DIR")" = "$("$ID_EXECUTABLE" -u)" ] || {
|
|
531
|
+
echo "Recordings state path has an unexpected owner." >&2
|
|
532
|
+
exit 1
|
|
533
|
+
}
|
|
534
|
+
state_mode="$("$STAT_EXECUTABLE" -f '%Lp' "$DATA_DIR")"
|
|
535
|
+
case "$state_mode" in
|
|
536
|
+
700|755) ;;
|
|
537
|
+
*) echo "Recordings state path must have mode 700 or the supported legacy mode 755." >&2; exit 1 ;;
|
|
538
|
+
esac
|
|
539
|
+
if "$LS_EXECUTABLE" -lde "$DATA_DIR" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
|
|
540
|
+
echo "Recordings state path has an unexpected ACL." >&2
|
|
541
|
+
exit 1
|
|
542
|
+
fi
|
|
543
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$DATA_DIR" --uid "$("$ID_EXECUTABLE" -u)"
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
# The target identity above and every existing state path are validated read-only
|
|
547
|
+
# before creating an install parent, lock, journal, or state child.
|
|
548
|
+
verify_safe_home_ancestor "$HOME"
|
|
549
|
+
if [ -e "$DATA_PARENT" ] || [ -L "$DATA_PARENT" ]; then
|
|
550
|
+
verify_secure_parent "$DATA_PARENT"
|
|
551
|
+
fi
|
|
552
|
+
if [ -e "$APP_PARENT" ] || [ -L "$APP_PARENT" ]; then
|
|
553
|
+
verify_secure_parent "$APP_PARENT"
|
|
554
|
+
fi
|
|
555
|
+
verify_existing_state_root
|
|
556
|
+
[ ! -L "$ROLLBACK_DIR" ] || { echo "Recordings rollback parent must not be a symlink." >&2; exit 1; }
|
|
557
|
+
|
|
558
|
+
if [ ! -e "$APP_PARENT" ]; then
|
|
559
|
+
"$MKDIR_EXECUTABLE" -m 700 "$APP_PARENT"
|
|
40
560
|
fi
|
|
561
|
+
verify_secure_parent "$APP_PARENT"
|
|
562
|
+
verify_secure_file "$JOURNAL_PATH" "Install transaction journal"
|
|
563
|
+
|
|
564
|
+
release_install_lock() {
|
|
565
|
+
if [ "$LOCK_OWNED" -eq 1 ] && [ -d "$LOCK_DIR" ] && [ ! -L "$LOCK_DIR" ]; then
|
|
566
|
+
owner_pid="$("$SED_EXECUTABLE" -n '1p' "$LOCK_DIR/owner" 2>/dev/null || true)"
|
|
567
|
+
[ "$owner_pid" != "$$" ] || "$RM_EXECUTABLE" -rf "$LOCK_DIR"
|
|
568
|
+
fi
|
|
569
|
+
LOCK_OWNED=0
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
release_maintenance_marker() {
|
|
573
|
+
local marker_removed=0
|
|
574
|
+
if [ "$MAINTENANCE_OWNED" -eq 1 ] && [ -d "$MAINTENANCE_DIR" ] && [ ! -L "$MAINTENANCE_DIR" ]; then
|
|
575
|
+
owner_pid="$("$SED_EXECUTABLE" -n '1p' "$MAINTENANCE_DIR/owner" 2>/dev/null || true)"
|
|
576
|
+
if [ "$owner_pid" = "$$" ]; then
|
|
577
|
+
"$RM_EXECUTABLE" -rf "$MAINTENANCE_DIR"
|
|
578
|
+
marker_removed=1
|
|
579
|
+
fi
|
|
580
|
+
fi
|
|
581
|
+
if [ -n "$MAINTENANCE_CLAIM_DIR" ] && [ -d "$MAINTENANCE_CLAIM_DIR" ] && \
|
|
582
|
+
[ ! -L "$MAINTENANCE_CLAIM_DIR" ]; then
|
|
583
|
+
"$RM_EXECUTABLE" -rf "$MAINTENANCE_CLAIM_DIR"
|
|
584
|
+
fi
|
|
585
|
+
MAINTENANCE_CLAIM_DIR=""
|
|
586
|
+
MAINTENANCE_OWNED=0
|
|
587
|
+
if [ "$marker_removed" -eq 1 ]; then
|
|
588
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$DATA_PARENT"
|
|
589
|
+
fi
|
|
590
|
+
}
|
|
41
591
|
|
|
42
|
-
|
|
592
|
+
restart_recorded_app_paths() {
|
|
593
|
+
local app_path
|
|
594
|
+
local executable
|
|
595
|
+
for app_path in ${PRIOR_RUNNING_APP_PATHS[@]+"${PRIOR_RUNNING_APP_PATHS[@]}"}; do
|
|
596
|
+
[ -d "$app_path" ] && [ ! -L "$app_path" ] || continue
|
|
597
|
+
executable="$app_path/Contents/MacOS/Recordings"
|
|
598
|
+
if [ -z "$(pids_for_exact_executable "$executable")" ]; then
|
|
599
|
+
"$OPEN_EXECUTABLE" -n "$app_path" >/dev/null 2>&1 || true
|
|
600
|
+
fi
|
|
601
|
+
done
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
release_install_coordination() {
|
|
605
|
+
local status=0
|
|
606
|
+
release_sqlite_barrier || status=1
|
|
607
|
+
if [ "$PRESERVE_MAINTENANCE_MARKER" -eq 0 ] && ! release_maintenance_marker; then
|
|
608
|
+
RECOVERED_APP_RESTART_ON_ABORT=0
|
|
609
|
+
status=1
|
|
610
|
+
fi
|
|
611
|
+
if [ "$PRESERVE_MAINTENANCE_MARKER" -eq 0 ] && \
|
|
612
|
+
[ "$RECOVERED_APP_RESTART_ON_ABORT" -eq 1 ]; then
|
|
613
|
+
restart_recorded_app_paths
|
|
614
|
+
fi
|
|
615
|
+
release_install_lock
|
|
616
|
+
return "$status"
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
acquire_install_lock() {
|
|
620
|
+
local stale_claim
|
|
621
|
+
local owner_pid
|
|
622
|
+
local owner_start
|
|
623
|
+
local actual_start
|
|
624
|
+
local lock_age
|
|
625
|
+
local now
|
|
626
|
+
local stale_seconds="${RECORDINGS_LOCK_STALE_SECONDS:-30}"
|
|
627
|
+
case "$stale_seconds" in
|
|
628
|
+
''|*[!0-9]*) echo "Install lock stale threshold must be an integer." >&2; exit 2 ;;
|
|
629
|
+
esac
|
|
630
|
+
if [ "$stale_seconds" -gt 3600 ]; then
|
|
631
|
+
echo "Install lock stale threshold must not exceed 3600 seconds." >&2
|
|
632
|
+
exit 2
|
|
633
|
+
fi
|
|
634
|
+
if [ "$stale_seconds" -lt 5 ]; then
|
|
635
|
+
echo "Install lock stale threshold must be at least 5 seconds." >&2
|
|
636
|
+
exit 2
|
|
637
|
+
fi
|
|
638
|
+
if ! "$MKDIR_EXECUTABLE" -m 700 "$LOCK_DIR" 2>/dev/null; then
|
|
639
|
+
[ -d "$LOCK_DIR" ] && [ ! -L "$LOCK_DIR" ] || {
|
|
640
|
+
echo "Install lock path is not a secure directory." >&2
|
|
641
|
+
exit 1
|
|
642
|
+
}
|
|
643
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$LOCK_DIR")" = "$("$ID_EXECUTABLE" -u)" ] && \
|
|
644
|
+
[ "$("$STAT_EXECUTABLE" -f '%Lp' "$LOCK_DIR")" = 700 ] || {
|
|
645
|
+
echo "Install lock has unsafe ownership or mode." >&2
|
|
646
|
+
exit 1
|
|
647
|
+
}
|
|
648
|
+
if "$LS_EXECUTABLE" -lde "$LOCK_DIR" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
|
|
649
|
+
echo "Install lock has an unexpected ACL." >&2
|
|
650
|
+
exit 1
|
|
651
|
+
fi
|
|
652
|
+
if [ -e "$LOCK_DIR/owner" ] || [ -L "$LOCK_DIR/owner" ]; then
|
|
653
|
+
[ -f "$LOCK_DIR/owner" ] && [ ! -L "$LOCK_DIR/owner" ] && \
|
|
654
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$LOCK_DIR/owner")" = "$("$ID_EXECUTABLE" -u)" ] && \
|
|
655
|
+
[ "$("$STAT_EXECUTABLE" -f '%Lp' "$LOCK_DIR/owner")" = 600 ] || {
|
|
656
|
+
echo "Install lock owner metadata has an unsafe type, owner, or mode." >&2
|
|
657
|
+
exit 1
|
|
658
|
+
}
|
|
659
|
+
if "$LS_EXECUTABLE" -lde "$LOCK_DIR/owner" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
|
|
660
|
+
echo "Install lock owner metadata has an unexpected ACL." >&2
|
|
661
|
+
exit 1
|
|
662
|
+
fi
|
|
663
|
+
fi
|
|
664
|
+
owner_pid="$("$SED_EXECUTABLE" -n '1p' "$LOCK_DIR/owner" 2>/dev/null || true)"
|
|
665
|
+
owner_start="$("$SED_EXECUTABLE" -n '2p' "$LOCK_DIR/owner" 2>/dev/null || true)"
|
|
666
|
+
actual_start=""
|
|
667
|
+
case "$owner_pid" in
|
|
668
|
+
''|*[!0-9]*) ;;
|
|
669
|
+
*)
|
|
670
|
+
if kill -0 "$owner_pid" 2>/dev/null; then
|
|
671
|
+
actual_start="$("$PS_EXECUTABLE" -o lstart= -p "$owner_pid" 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
|
|
672
|
+
if [ -z "$owner_start" ] || [ -z "$actual_start" ] || [ "$owner_start" = "$actual_start" ]; then
|
|
673
|
+
echo "Another Recordings.app installer owns the active install lock." >&2
|
|
674
|
+
exit 1
|
|
675
|
+
fi
|
|
676
|
+
fi
|
|
677
|
+
;;
|
|
678
|
+
esac
|
|
679
|
+
owner_valid=1
|
|
680
|
+
case "$owner_pid" in ''|*[!0-9]*) owner_valid=0 ;; esac
|
|
681
|
+
[ -n "$owner_start" ] || owner_valid=0
|
|
682
|
+
if [ "$owner_valid" -eq 0 ] || [ ! -f "$LOCK_DIR/owner" ] || [ -L "$LOCK_DIR/owner" ]; then
|
|
683
|
+
now="$("$DATE_EXECUTABLE" +%s)"
|
|
684
|
+
lock_age=$((now - $("$STAT_EXECUTABLE" -f '%m' "$LOCK_DIR")))
|
|
685
|
+
if [ "$lock_age" -lt "$stale_seconds" ]; then
|
|
686
|
+
echo "Recordings.app install lock is incomplete and too recent to reclaim safely." >&2
|
|
687
|
+
exit 1
|
|
688
|
+
fi
|
|
689
|
+
fi
|
|
690
|
+
stale_claim="${LOCK_DIR}.stale.$$"
|
|
691
|
+
if ! "$MV_EXECUTABLE" "$LOCK_DIR" "$stale_claim" 2>/dev/null; then
|
|
692
|
+
echo "Recordings.app install lock changed during stale-owner recovery." >&2
|
|
693
|
+
exit 1
|
|
694
|
+
fi
|
|
695
|
+
"$RM_EXECUTABLE" -rf "$stale_claim"
|
|
696
|
+
"$MKDIR_EXECUTABLE" -m 700 "$LOCK_DIR" || { echo "Could not acquire Recordings.app install lock." >&2; exit 1; }
|
|
697
|
+
fi
|
|
698
|
+
LOCK_OWNED=1
|
|
699
|
+
local start
|
|
700
|
+
start="$("$PS_EXECUTABLE" -o lstart= -p $$ 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
|
|
701
|
+
printf '%s\n%s\n' "$$" "$start" >"$LOCK_DIR/owner.tmp.$$"
|
|
702
|
+
"$CHMOD_EXECUTABLE" 600 "$LOCK_DIR/owner.tmp.$$"
|
|
703
|
+
"$MV_EXECUTABLE" "$LOCK_DIR/owner.tmp.$$" "$LOCK_DIR/owner"
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
acquire_maintenance_marker() {
|
|
707
|
+
local stale_claim
|
|
708
|
+
local owner_pid
|
|
709
|
+
local owner_start
|
|
710
|
+
local actual_start
|
|
711
|
+
local marker_age
|
|
712
|
+
local now
|
|
713
|
+
local owner_valid
|
|
714
|
+
local start
|
|
715
|
+
local stale_seconds="${RECORDINGS_MAINTENANCE_STALE_SECONDS:-30}"
|
|
716
|
+
case "$stale_seconds" in
|
|
717
|
+
''|*[!0-9]*) echo "Install maintenance stale threshold must be an integer." >&2; exit 2 ;;
|
|
718
|
+
esac
|
|
719
|
+
if [ "$stale_seconds" -gt 3600 ] || [ "$stale_seconds" -lt 5 ]; then
|
|
720
|
+
echo "Install maintenance stale threshold must be between 5 and 3600 seconds." >&2
|
|
721
|
+
exit 2
|
|
722
|
+
fi
|
|
43
723
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
724
|
+
if [ -e "$MAINTENANCE_DIR" ] || [ -L "$MAINTENANCE_DIR" ]; then
|
|
725
|
+
[ -d "$MAINTENANCE_DIR" ] && [ ! -L "$MAINTENANCE_DIR" ] || {
|
|
726
|
+
echo "Install maintenance marker is not a secure directory." >&2
|
|
727
|
+
exit 1
|
|
728
|
+
}
|
|
729
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$MAINTENANCE_DIR")" = "$("$ID_EXECUTABLE" -u)" ] && \
|
|
730
|
+
[ "$("$STAT_EXECUTABLE" -f '%Lp' "$MAINTENANCE_DIR")" = 700 ] || {
|
|
731
|
+
echo "Install maintenance marker has unsafe ownership or mode." >&2
|
|
732
|
+
exit 1
|
|
733
|
+
}
|
|
734
|
+
if "$LS_EXECUTABLE" -lde "$MAINTENANCE_DIR" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
|
|
735
|
+
echo "Install maintenance marker has an unexpected ACL." >&2
|
|
736
|
+
exit 1
|
|
737
|
+
fi
|
|
738
|
+
if [ -e "$MAINTENANCE_DIR/owner" ] || [ -L "$MAINTENANCE_DIR/owner" ]; then
|
|
739
|
+
[ -f "$MAINTENANCE_DIR/owner" ] && [ ! -L "$MAINTENANCE_DIR/owner" ] && \
|
|
740
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$MAINTENANCE_DIR/owner")" = "$("$ID_EXECUTABLE" -u)" ] && \
|
|
741
|
+
[ "$("$STAT_EXECUTABLE" -f '%Lp' "$MAINTENANCE_DIR/owner")" = 600 ] || {
|
|
742
|
+
echo "Install maintenance owner metadata has an unsafe type, owner, or mode." >&2
|
|
743
|
+
exit 1
|
|
744
|
+
}
|
|
745
|
+
if "$LS_EXECUTABLE" -lde "$MAINTENANCE_DIR/owner" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
|
|
746
|
+
echo "Install maintenance owner metadata has an unexpected ACL." >&2
|
|
747
|
+
exit 1
|
|
748
|
+
fi
|
|
749
|
+
fi
|
|
750
|
+
owner_pid="$("$SED_EXECUTABLE" -n '1p' "$MAINTENANCE_DIR/owner" 2>/dev/null || true)"
|
|
751
|
+
owner_start="$("$SED_EXECUTABLE" -n '2p' "$MAINTENANCE_DIR/owner" 2>/dev/null || true)"
|
|
752
|
+
actual_start=""
|
|
753
|
+
case "$owner_pid" in
|
|
754
|
+
''|*[!0-9]*) ;;
|
|
755
|
+
*)
|
|
756
|
+
if kill -0 "$owner_pid" 2>/dev/null; then
|
|
757
|
+
actual_start="$("$PS_EXECUTABLE" -o lstart= -p "$owner_pid" 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
|
|
758
|
+
if [ -z "$owner_start" ] || [ -z "$actual_start" ] || [ "$owner_start" = "$actual_start" ]; then
|
|
759
|
+
echo "Recordings storage is already in active installation maintenance." >&2
|
|
760
|
+
exit 1
|
|
761
|
+
fi
|
|
762
|
+
fi
|
|
763
|
+
;;
|
|
764
|
+
esac
|
|
765
|
+
owner_valid=1
|
|
766
|
+
case "$owner_pid" in ''|*[!0-9]*) owner_valid=0 ;; esac
|
|
767
|
+
[ -n "$owner_start" ] || owner_valid=0
|
|
768
|
+
if [ "$owner_valid" -eq 0 ] || [ ! -f "$MAINTENANCE_DIR/owner" ] || [ -L "$MAINTENANCE_DIR/owner" ]; then
|
|
769
|
+
echo "Install maintenance marker has incomplete ownership evidence; refusing automatic recovery." >&2
|
|
770
|
+
exit 1
|
|
771
|
+
fi
|
|
772
|
+
stale_claim="${MAINTENANCE_DIR}.stale.$$"
|
|
773
|
+
if ! "$MV_EXECUTABLE" "$MAINTENANCE_DIR" "$stale_claim" 2>/dev/null; then
|
|
774
|
+
echo "Install maintenance marker changed during stale-owner recovery." >&2
|
|
775
|
+
exit 1
|
|
776
|
+
fi
|
|
777
|
+
"$RM_EXECUTABLE" -rf "$stale_claim"
|
|
50
778
|
fi
|
|
51
|
-
|
|
779
|
+
|
|
780
|
+
# Publish only a complete marker. Store clients can now treat the canonical
|
|
781
|
+
# path as an atomic maintenance claim: owner metadata is private, durable,
|
|
782
|
+
# and validated before the directory becomes visible at MAINTENANCE_DIR.
|
|
783
|
+
start="$("$PS_EXECUTABLE" -o lstart= -p $$ 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
|
|
784
|
+
[ -n "$start" ] || {
|
|
785
|
+
echo "Could not establish maintenance marker process identity." >&2
|
|
786
|
+
exit 1
|
|
787
|
+
}
|
|
788
|
+
MAINTENANCE_CLAIM_DIR="$("$MKTEMP_EXECUTABLE" -d "${MAINTENANCE_DIR}.claim.XXXXXX")"
|
|
789
|
+
"$CHMOD_EXECUTABLE" 700 "$MAINTENANCE_CLAIM_DIR"
|
|
790
|
+
printf '%s\n%s\n' "$$" "$start" >"$MAINTENANCE_CLAIM_DIR/owner"
|
|
791
|
+
"$CHMOD_EXECUTABLE" 600 "$MAINTENANCE_CLAIM_DIR/owner"
|
|
792
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$MAINTENANCE_CLAIM_DIR"
|
|
793
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$DATA_PARENT"
|
|
794
|
+
if test_fault_hooks_enabled && \
|
|
795
|
+
[ "${RECORDINGS_TEST_CRASH_DURING_MAINTENANCE_CLAIM:-}" = "before-rename" ]; then
|
|
796
|
+
kill -KILL "$$"
|
|
797
|
+
fi
|
|
798
|
+
[ ! -e "$MAINTENANCE_DIR" ] && [ ! -L "$MAINTENANCE_DIR" ] || {
|
|
799
|
+
echo "Recordings storage maintenance marker appeared during atomic claim." >&2
|
|
800
|
+
exit 1
|
|
801
|
+
}
|
|
802
|
+
"$MV_EXECUTABLE" "$MAINTENANCE_CLAIM_DIR" "$MAINTENANCE_DIR"
|
|
803
|
+
MAINTENANCE_CLAIM_DIR=""
|
|
804
|
+
MAINTENANCE_OWNED=1
|
|
805
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$DATA_PARENT"
|
|
806
|
+
if test_fault_hooks_enabled && \
|
|
807
|
+
[ "${RECORDINGS_TEST_CRASH_DURING_MAINTENANCE_CLAIM:-}" = "after-rename" ]; then
|
|
808
|
+
kill -KILL "$$"
|
|
809
|
+
fi
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
drain_store_reader_leases() {
|
|
813
|
+
local timeout_ms="${RECORDINGS_READER_DRAIN_TIMEOUT_MS:-30000}"
|
|
814
|
+
local attempts
|
|
815
|
+
local attempt=0
|
|
816
|
+
local active_count
|
|
817
|
+
local lease
|
|
818
|
+
local owner_pid
|
|
819
|
+
local owner_start
|
|
820
|
+
local actual_start
|
|
821
|
+
local stale_claim
|
|
822
|
+
case "$timeout_ms" in
|
|
823
|
+
''|*[!0-9]*) echo "Store reader drain timeout must be an integer." >&2; exit 2 ;;
|
|
824
|
+
esac
|
|
825
|
+
if [ "$timeout_ms" -lt 1000 ] || [ "$timeout_ms" -gt 120000 ]; then
|
|
826
|
+
echo "Store reader drain timeout must be between 1000 and 120000 milliseconds." >&2
|
|
827
|
+
exit 2
|
|
828
|
+
fi
|
|
829
|
+
[ -e "$READER_LEASES_DIR" ] || [ -L "$READER_LEASES_DIR" ] || return 0
|
|
830
|
+
[ -d "$READER_LEASES_DIR" ] && [ ! -L "$READER_LEASES_DIR" ] && \
|
|
831
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$READER_LEASES_DIR")" = "$("$ID_EXECUTABLE" -u)" ] && \
|
|
832
|
+
[ "$("$STAT_EXECUTABLE" -f '%Lp' "$READER_LEASES_DIR")" = 700 ] || {
|
|
833
|
+
echo "Store reader lease root has an unsafe type, owner, or mode." >&2
|
|
834
|
+
exit 1
|
|
835
|
+
}
|
|
836
|
+
if "$LS_EXECUTABLE" -lde "$READER_LEASES_DIR" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
|
|
837
|
+
echo "Store reader lease root has an unexpected ACL." >&2
|
|
838
|
+
exit 1
|
|
839
|
+
fi
|
|
840
|
+
|
|
841
|
+
attempts=$((timeout_ms / 50))
|
|
842
|
+
while [ "$attempt" -lt "$attempts" ]; do
|
|
843
|
+
active_count=0
|
|
844
|
+
for lease in "$READER_LEASES_DIR"/lease-*; do
|
|
845
|
+
[ -e "$lease" ] || [ -L "$lease" ] || continue
|
|
846
|
+
[ -d "$lease" ] && [ ! -L "$lease" ] && \
|
|
847
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$lease" 2>/dev/null || true)" = "$("$ID_EXECUTABLE" -u)" ] && \
|
|
848
|
+
[ "$("$STAT_EXECUTABLE" -f '%Lp' "$lease" 2>/dev/null || true)" = 700 ] || {
|
|
849
|
+
[ ! -e "$lease" ] && continue
|
|
850
|
+
echo "Store reader lease has an unsafe type, owner, or mode." >&2
|
|
851
|
+
exit 1
|
|
852
|
+
}
|
|
853
|
+
if [ ! -e "$lease/owner" ] && [ ! -L "$lease/owner" ]; then
|
|
854
|
+
# A compliant reader may have completed and removed its lease between
|
|
855
|
+
# the directory scan and this check. A still-present incomplete lease
|
|
856
|
+
# is never reclaimed because its ownership cannot be proven.
|
|
857
|
+
[ ! -e "$lease" ] && continue
|
|
858
|
+
echo "Store reader lease is missing owner metadata; refusing maintenance." >&2
|
|
859
|
+
exit 1
|
|
860
|
+
fi
|
|
861
|
+
[ -f "$lease/owner" ] && [ ! -L "$lease/owner" ] && \
|
|
862
|
+
[ "$("$STAT_EXECUTABLE" -f '%u' "$lease/owner" 2>/dev/null || true)" = "$("$ID_EXECUTABLE" -u)" ] && \
|
|
863
|
+
[ "$("$STAT_EXECUTABLE" -f '%Lp' "$lease/owner" 2>/dev/null || true)" = 600 ] || {
|
|
864
|
+
[ ! -e "$lease" ] && continue
|
|
865
|
+
echo "Store reader lease owner metadata is unsafe." >&2
|
|
866
|
+
exit 1
|
|
867
|
+
}
|
|
868
|
+
owner_pid="$("$SED_EXECUTABLE" -n '1p' "$lease/owner" 2>/dev/null || true)"
|
|
869
|
+
owner_start="$("$SED_EXECUTABLE" -n '2p' "$lease/owner" 2>/dev/null || true)"
|
|
870
|
+
[ -e "$lease" ] || continue
|
|
871
|
+
case "$owner_pid" in
|
|
872
|
+
''|*[!0-9]*) echo "Store reader lease has an invalid owner PID." >&2; exit 1 ;;
|
|
873
|
+
esac
|
|
874
|
+
[ -n "$owner_start" ] || {
|
|
875
|
+
echo "Store reader lease has an invalid process identity." >&2
|
|
876
|
+
exit 1
|
|
877
|
+
}
|
|
878
|
+
actual_start="$("$PS_EXECUTABLE" -o lstart= -p "$owner_pid" 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
|
|
879
|
+
if [ "$actual_start" = "$owner_start" ]; then
|
|
880
|
+
active_count=$((active_count + 1))
|
|
881
|
+
continue
|
|
882
|
+
fi
|
|
883
|
+
if [ -z "$actual_start" ] && kill -0 "$owner_pid" 2>/dev/null; then
|
|
884
|
+
echo "Store reader process exists but its start identity cannot be verified." >&2
|
|
885
|
+
exit 1
|
|
886
|
+
fi
|
|
887
|
+
# Empty process identity means the recorded PID no longer exists; a
|
|
888
|
+
# different identity means the PID has been reused. In both cases the
|
|
889
|
+
# exact reader that created this lease is provably dead.
|
|
890
|
+
stale_claim="${lease}.stale.$$"
|
|
891
|
+
if ! "$MV_EXECUTABLE" "$lease" "$stale_claim" 2>/dev/null; then
|
|
892
|
+
[ ! -e "$lease" ] && continue
|
|
893
|
+
echo "Store reader lease changed during dead-owner recovery." >&2
|
|
894
|
+
exit 1
|
|
895
|
+
fi
|
|
896
|
+
"$RM_EXECUTABLE" -rf "$stale_claim"
|
|
897
|
+
done
|
|
898
|
+
[ "$active_count" -eq 0 ] && return 0
|
|
899
|
+
attempt=$((attempt + 1))
|
|
900
|
+
"$SLEEP_EXECUTABLE" 0.05
|
|
901
|
+
done
|
|
902
|
+
echo "Timed out waiting for active local Store operations to finish." >&2
|
|
52
903
|
exit 1
|
|
53
904
|
}
|
|
54
905
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
STAMP_FILE="${DATA_DIR}/.recordings-source-hash"
|
|
906
|
+
validate_sqlite_busy_timeout() {
|
|
907
|
+
local busy_timeout_ms="${RECORDINGS_SQLITE_BUSY_TIMEOUT_MS:-30000}"
|
|
908
|
+
case "$busy_timeout_ms" in
|
|
909
|
+
''|*[!0-9]*) echo "SQLite busy timeout must be an integer." >&2; return 2 ;;
|
|
910
|
+
esac
|
|
911
|
+
if [ "$busy_timeout_ms" -lt 1000 ] || [ "$busy_timeout_ms" -gt 120000 ]; then
|
|
912
|
+
echo "SQLite busy timeout must be between 1000 and 120000 milliseconds." >&2
|
|
913
|
+
return 2
|
|
914
|
+
fi
|
|
915
|
+
printf '%s\n' "$busy_timeout_ms"
|
|
916
|
+
}
|
|
67
917
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
918
|
+
canonical_database_is_sqlite() {
|
|
919
|
+
local database_path="${DATA_DIR}/recordings.db"
|
|
920
|
+
local database_header
|
|
921
|
+
[ -e "$database_path" ] || [ -L "$database_path" ] || return 1
|
|
922
|
+
[ -f "$database_path" ] && [ ! -L "$database_path" ] || {
|
|
923
|
+
echo "Canonical Recordings database must be a non-symlink regular file." >&2
|
|
924
|
+
return 2
|
|
925
|
+
}
|
|
926
|
+
# Legacy installs and lifecycle fixtures may preserve an opaque file at this
|
|
927
|
+
# path. Such a file cannot have SQLite readers/writers to drain; preserve it
|
|
928
|
+
# transactionally without asking sqlite3 to parse or mutate it.
|
|
929
|
+
database_header="$("$DD_EXECUTABLE" if="$database_path" bs=15 count=1 2>/dev/null || true)"
|
|
930
|
+
[ "$database_header" = "SQLite format 3" ]
|
|
931
|
+
}
|
|
76
932
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
933
|
+
acquire_sqlite_barrier() {
|
|
934
|
+
local database_path="${DATA_DIR}/recordings.db"
|
|
935
|
+
local checkpoint_output
|
|
936
|
+
local checkpoint_busy
|
|
937
|
+
local busy_timeout_ms
|
|
938
|
+
local parent_start
|
|
939
|
+
local attempts
|
|
940
|
+
local attempt=0
|
|
941
|
+
[ "$SQLITE_BARRIER_ACTIVE" -eq 0 ] || {
|
|
942
|
+
echo "Canonical Recordings database barrier is already active." >&2
|
|
943
|
+
return 1
|
|
944
|
+
}
|
|
945
|
+
if canonical_database_is_sqlite; then
|
|
946
|
+
:
|
|
947
|
+
else
|
|
948
|
+
local database_status=$?
|
|
949
|
+
[ "$database_status" -eq 1 ] && return 0
|
|
950
|
+
return "$database_status"
|
|
82
951
|
fi
|
|
952
|
+
busy_timeout_ms="$(validate_sqlite_busy_timeout)" || return $?
|
|
953
|
+
if ! checkpoint_output="$("$SQLITE3_EXECUTABLE" -batch "$database_path" 2>/dev/null <<SQL
|
|
954
|
+
.timeout ${busy_timeout_ms}
|
|
955
|
+
PRAGMA wal_checkpoint(TRUNCATE);
|
|
956
|
+
SQL
|
|
957
|
+
)"; then
|
|
958
|
+
echo "Could not checkpoint the canonical Recordings database; refusing maintenance." >&2
|
|
959
|
+
return 1
|
|
960
|
+
fi
|
|
961
|
+
checkpoint_busy="${checkpoint_output%%|*}"
|
|
962
|
+
if [ "$checkpoint_busy" != 0 ]; then
|
|
963
|
+
echo "Canonical Recordings database checkpoint remained busy; refusing maintenance." >&2
|
|
964
|
+
return 1
|
|
965
|
+
fi
|
|
966
|
+
|
|
967
|
+
SQLITE_BARRIER_DIR="$("$MKTEMP_EXECUTABLE" -d "${DATA_PARENT}/.recordings-sqlite-barrier.XXXXXX")"
|
|
968
|
+
"$CHMOD_EXECUTABLE" 700 "$SQLITE_BARRIER_DIR"
|
|
969
|
+
parent_start="$("$PS_EXECUTABLE" -o lstart= -p $$ 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
|
|
970
|
+
[ -n "$parent_start" ] || {
|
|
971
|
+
echo "Could not establish SQLite barrier owner identity." >&2
|
|
972
|
+
"$RM_EXECUTABLE" -rf "$SQLITE_BARRIER_DIR"
|
|
973
|
+
SQLITE_BARRIER_DIR=""
|
|
974
|
+
return 1
|
|
975
|
+
}
|
|
976
|
+
(
|
|
977
|
+
{
|
|
978
|
+
printf '%s\n' '.bail on' ".timeout ${busy_timeout_ms}" 'BEGIN EXCLUSIVE;' \
|
|
979
|
+
"SELECT 'RECORDINGS_SQLITE_BARRIER_READY';"
|
|
980
|
+
while [ ! -e "$SQLITE_BARRIER_DIR/release" ]; do
|
|
981
|
+
actual_parent_start="$("$PS_EXECUTABLE" -o lstart= -p $$ 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
|
|
982
|
+
[ "$actual_parent_start" = "$parent_start" ] || break
|
|
983
|
+
"$SLEEP_EXECUTABLE" 0.05
|
|
984
|
+
done
|
|
985
|
+
printf '%s\n' 'COMMIT;'
|
|
986
|
+
} | "$SQLITE3_EXECUTABLE" -batch "$database_path" \
|
|
987
|
+
>"$SQLITE_BARRIER_DIR/ready" 2>"$SQLITE_BARRIER_DIR/error"
|
|
988
|
+
) &
|
|
989
|
+
SQLITE_BARRIER_PID=$!
|
|
990
|
+
attempts=$((busy_timeout_ms / 50 + 20))
|
|
991
|
+
while [ "$attempt" -lt "$attempts" ]; do
|
|
992
|
+
if "$GREP_EXECUTABLE" -q '^RECORDINGS_SQLITE_BARRIER_READY$' \
|
|
993
|
+
"$SQLITE_BARRIER_DIR/ready" 2>/dev/null; then
|
|
994
|
+
SQLITE_BARRIER_ACTIVE=1
|
|
995
|
+
return 0
|
|
996
|
+
fi
|
|
997
|
+
if ! kill -0 "$SQLITE_BARRIER_PID" 2>/dev/null; then
|
|
998
|
+
wait "$SQLITE_BARRIER_PID" 2>/dev/null || true
|
|
999
|
+
echo "Could not acquire an exclusive canonical Recordings database barrier." >&2
|
|
1000
|
+
"$RM_EXECUTABLE" -rf "$SQLITE_BARRIER_DIR"
|
|
1001
|
+
SQLITE_BARRIER_PID=""
|
|
1002
|
+
SQLITE_BARRIER_DIR=""
|
|
1003
|
+
return 1
|
|
1004
|
+
fi
|
|
1005
|
+
attempt=$((attempt + 1))
|
|
1006
|
+
"$SLEEP_EXECUTABLE" 0.05
|
|
1007
|
+
done
|
|
1008
|
+
: >"$SQLITE_BARRIER_DIR/release"
|
|
1009
|
+
wait "$SQLITE_BARRIER_PID" 2>/dev/null || true
|
|
1010
|
+
echo "Timed out acquiring an exclusive canonical Recordings database barrier." >&2
|
|
1011
|
+
"$RM_EXECUTABLE" -rf "$SQLITE_BARRIER_DIR"
|
|
1012
|
+
SQLITE_BARRIER_PID=""
|
|
1013
|
+
SQLITE_BARRIER_DIR=""
|
|
1014
|
+
return 1
|
|
1015
|
+
}
|
|
83
1016
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
1017
|
+
release_sqlite_barrier() {
|
|
1018
|
+
local status=0
|
|
1019
|
+
if [ "$SQLITE_BARRIER_ACTIVE" -eq 1 ] && [ -n "$SQLITE_BARRIER_DIR" ]; then
|
|
1020
|
+
: >"$SQLITE_BARRIER_DIR/release"
|
|
1021
|
+
if ! wait "$SQLITE_BARRIER_PID"; then
|
|
1022
|
+
echo "Canonical Recordings database barrier did not close cleanly." >&2
|
|
1023
|
+
status=1
|
|
1024
|
+
fi
|
|
90
1025
|
fi
|
|
1026
|
+
SQLITE_BARRIER_ACTIVE=0
|
|
1027
|
+
SQLITE_BARRIER_PID=""
|
|
1028
|
+
if [ -n "$SQLITE_BARRIER_DIR" ]; then
|
|
1029
|
+
"$RM_EXECUTABLE" -rf "$SQLITE_BARRIER_DIR"
|
|
1030
|
+
fi
|
|
1031
|
+
SQLITE_BARRIER_DIR=""
|
|
1032
|
+
return "$status"
|
|
91
1033
|
}
|
|
92
1034
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{
|
|
97
|
-
# Version-aware: the package version line participates in the hash.
|
|
98
|
-
grep '"version"' "${PACKAGE_ROOT}/package.json" 2>/dev/null
|
|
99
|
-
# Source-aware: hash every native source file (relative paths, so the hash
|
|
100
|
-
# is independent of the install location), excluding build artifacts.
|
|
101
|
-
( cd "$NATIVE_DIR" && find . -type f ! -path './.build/*' -exec shasum {} + 2>/dev/null | sort )
|
|
102
|
-
} | shasum | awk '{print $1}'
|
|
1035
|
+
quiesce_canonical_database() {
|
|
1036
|
+
acquire_sqlite_barrier
|
|
1037
|
+
release_sqlite_barrier
|
|
103
1038
|
}
|
|
104
1039
|
|
|
105
|
-
|
|
1040
|
+
cleanup_install_coordination_and_work() {
|
|
1041
|
+
local status=$?
|
|
1042
|
+
trap - EXIT
|
|
1043
|
+
release_install_coordination || status=1
|
|
1044
|
+
"$RM_EXECUTABLE" -rf "$WORK_DIR"
|
|
1045
|
+
exit "$status"
|
|
1046
|
+
}
|
|
1047
|
+
trap cleanup_install_coordination_and_work EXIT
|
|
1048
|
+
acquire_install_lock
|
|
1049
|
+
if [ ! -e "$DATA_PARENT" ]; then
|
|
1050
|
+
"$MKDIR_EXECUTABLE" -m 700 "$DATA_PARENT"
|
|
1051
|
+
fi
|
|
1052
|
+
verify_secure_parent "$DATA_PARENT"
|
|
1053
|
+
acquire_maintenance_marker
|
|
1054
|
+
drain_store_reader_leases
|
|
1055
|
+
acquire_sqlite_barrier
|
|
1056
|
+
if test_fault_hooks_enabled && [ -n "${RECORDINGS_TEST_HOLD_AFTER_LOCK_SECONDS:-}" ]; then
|
|
1057
|
+
"$SLEEP_EXECUTABLE" "$RECORDINGS_TEST_HOLD_AFTER_LOCK_SECONDS"
|
|
1058
|
+
fi
|
|
106
1059
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1060
|
+
pids_for_exact_executable() {
|
|
1061
|
+
local expected="$1"
|
|
1062
|
+
local pid
|
|
1063
|
+
local command
|
|
1064
|
+
local start_identity
|
|
1065
|
+
local observed_executable
|
|
1066
|
+
while read -r pid command; do
|
|
1067
|
+
[ -n "${pid:-}" ] || continue
|
|
1068
|
+
case "$command" in
|
|
1069
|
+
"$expected"|"$expected "*)
|
|
1070
|
+
start_identity="$(process_start_identity "$pid")"
|
|
1071
|
+
observed_executable="$(observed_executable_for_pid "$pid" "$expected")"
|
|
1072
|
+
if [ -n "$start_identity" ] && [ "$observed_executable" = "$expected" ]; then
|
|
1073
|
+
printf '%s\t%s\t%s\n' "$pid" "$start_identity" "$observed_executable"
|
|
1074
|
+
fi
|
|
1075
|
+
;;
|
|
1076
|
+
esac
|
|
1077
|
+
done < <("$PS_EXECUTABLE" -axo pid=,command= 2>/dev/null || true)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
process_start_identity() {
|
|
1081
|
+
local pid="$1"
|
|
1082
|
+
"$PS_EXECUTABLE" -o lstart= -p "$pid" 2>/dev/null | \
|
|
1083
|
+
"$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
observed_executable_for_pid() {
|
|
1087
|
+
local pid="$1"
|
|
1088
|
+
local expected="$2"
|
|
1089
|
+
"$LSOF_EXECUTABLE" -a -p "$pid" -d txt -Fn 2>/dev/null | \
|
|
1090
|
+
"$SED_EXECUTABLE" -n 's/^n//p' | \
|
|
1091
|
+
"$AWK_EXECUTABLE" -v expected="$expected" '$0 == expected { found = 1 } END { if (found) print expected }' || true
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
process_identity_is_current() {
|
|
1095
|
+
local pid="$1"
|
|
1096
|
+
local expected_start="$2"
|
|
1097
|
+
local expected_executable="$3"
|
|
1098
|
+
local actual_start
|
|
1099
|
+
local actual_executable
|
|
1100
|
+
actual_start="$(process_start_identity "$pid")"
|
|
1101
|
+
[ -n "$actual_start" ] && [ "$actual_start" = "$expected_start" ] || return 1
|
|
1102
|
+
actual_executable="$(observed_executable_for_pid "$pid" "$expected_executable")"
|
|
1103
|
+
[ "$actual_executable" = "$expected_executable" ]
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
signal_process_if_current() {
|
|
1107
|
+
local signal="$1"
|
|
1108
|
+
local pid="$2"
|
|
1109
|
+
local expected_start="$3"
|
|
1110
|
+
local expected_executable="$4"
|
|
1111
|
+
process_identity_is_current "$pid" "$expected_start" "$expected_executable" || return 0
|
|
1112
|
+
kill "-${signal}" "$pid" 2>/dev/null || true
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
stop_uncommitted_candidate() {
|
|
1116
|
+
local expected="$APP_DEST/Contents/MacOS/Recordings"
|
|
1117
|
+
local candidate_records
|
|
1118
|
+
local pid
|
|
1119
|
+
local start_identity
|
|
1120
|
+
local observed_executable
|
|
1121
|
+
local attempts
|
|
1122
|
+
local attempt
|
|
1123
|
+
local remaining
|
|
1124
|
+
candidate_records="$(pids_for_exact_executable "$expected")"
|
|
1125
|
+
[ -n "$candidate_records" ] || return 0
|
|
1126
|
+
while IFS=$'\t' read -r pid start_identity observed_executable; do
|
|
1127
|
+
[ -n "$pid" ] || continue
|
|
1128
|
+
signal_process_if_current TERM "$pid" "$start_identity" "$observed_executable"
|
|
1129
|
+
done <<< "$candidate_records"
|
|
1130
|
+
attempts=$((LAUNCH_TIMEOUT_SECONDS * 10))
|
|
1131
|
+
for ((attempt = 0; attempt < attempts; attempt++)); do
|
|
1132
|
+
remaining="$(pids_for_exact_executable "$expected")"
|
|
1133
|
+
[ -z "$remaining" ] && return 0
|
|
1134
|
+
"$SLEEP_EXECUTABLE" 0.1
|
|
1135
|
+
done
|
|
1136
|
+
while IFS=$'\t' read -r pid start_identity observed_executable; do
|
|
1137
|
+
[ -n "$pid" ] || continue
|
|
1138
|
+
signal_process_if_current KILL "$pid" "$start_identity" "$observed_executable"
|
|
1139
|
+
done <<< "$remaining"
|
|
1140
|
+
"$SLEEP_EXECUTABLE" 0.1
|
|
1141
|
+
[ -z "$(pids_for_exact_executable "$expected")" ] || {
|
|
1142
|
+
echo "Uncommitted Recordings.app process could not be stopped before recovery." >&2
|
|
1143
|
+
return 1
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if [ -f "$JOURNAL_PATH" ]; then
|
|
1148
|
+
echo "Recovering incomplete Recordings.app installation transaction." >&2
|
|
1149
|
+
if ! RECOVER_WAS_RUNNING="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field was_running)" || \
|
|
1150
|
+
! RECOVER_PHASE="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field phase)" || \
|
|
1151
|
+
! RECOVER_RUNNING_APP_PATHS_JSON="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field prior_running_app_paths)"; then
|
|
1152
|
+
PRESERVE_MAINTENANCE_MARKER=1
|
|
1153
|
+
echo "Could not read startup recovery evidence; maintenance remains fail-closed." >&2
|
|
1154
|
+
exit 1
|
|
1155
|
+
fi
|
|
1156
|
+
case "$RECOVER_PHASE" in
|
|
1157
|
+
candidate-moving|candidate-installed|activated|launching)
|
|
1158
|
+
if ! stop_uncommitted_candidate; then
|
|
1159
|
+
PRESERVE_MAINTENANCE_MARKER=1
|
|
1160
|
+
echo "Could not stop the uncommitted candidate during startup recovery; maintenance remains fail-closed." >&2
|
|
1161
|
+
exit 1
|
|
1162
|
+
fi
|
|
1163
|
+
;;
|
|
1164
|
+
esac
|
|
1165
|
+
if ! "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-recover --journal "$JOURNAL_PATH"; then
|
|
1166
|
+
PRESERVE_MAINTENANCE_MARKER=1
|
|
1167
|
+
echo "Startup recovery failed; maintenance remains fail-closed." >&2
|
|
1168
|
+
exit 1
|
|
1169
|
+
fi
|
|
1170
|
+
if [ "$RECOVER_WAS_RUNNING" = 1 ]; then
|
|
1171
|
+
# Do not relaunch the recovered app while this install still owns storage
|
|
1172
|
+
# exclusivity. A later safe abort relaunches it after releasing the marker;
|
|
1173
|
+
# a successful install carries the running intent to the new app.
|
|
1174
|
+
was_running=1
|
|
1175
|
+
STOPPED_RUNNING_APP=1
|
|
1176
|
+
RECOVERED_APP_RESTART_ON_ABORT=1
|
|
1177
|
+
while IFS= read -r recovered_app_path; do
|
|
1178
|
+
[ -n "$recovered_app_path" ] || continue
|
|
1179
|
+
RECOVERED_RUNNING_APP_PATHS+=("$recovered_app_path")
|
|
1180
|
+
PRIOR_RUNNING_APP_PATHS+=("$recovered_app_path")
|
|
1181
|
+
done < <("$BUN_EXECUTABLE" -e '
|
|
1182
|
+
const paths = JSON.parse(process.argv[1]);
|
|
1183
|
+
if (!Array.isArray(paths)) process.exit(1);
|
|
1184
|
+
for (const path of paths) console.log(path);
|
|
1185
|
+
' "$RECOVER_RUNNING_APP_PATHS_JSON")
|
|
1186
|
+
fi
|
|
114
1187
|
fi
|
|
1188
|
+
release_sqlite_barrier
|
|
115
1189
|
|
|
116
|
-
|
|
117
|
-
|
|
1190
|
+
# Recheck under the install lock. New state is private from creation; legacy
|
|
1191
|
+
# state remains read-only until its original mode is in the durable journal.
|
|
1192
|
+
verify_existing_state_root
|
|
1193
|
+
if [ ! -e "$DATA_PARENT" ]; then
|
|
1194
|
+
"$MKDIR_EXECUTABLE" -m 700 "$DATA_PARENT"
|
|
1195
|
+
fi
|
|
1196
|
+
verify_secure_parent "$DATA_PARENT"
|
|
1197
|
+
if [ ! -e "$DATA_DIR" ]; then
|
|
1198
|
+
"$MKDIR_EXECUTABLE" -m 700 "$DATA_DIR"
|
|
118
1199
|
fi
|
|
1200
|
+
verify_existing_state_root
|
|
1201
|
+
ORIGINAL_STATE_MODE="$("$STAT_EXECUTABLE" -f '%Lp' "$DATA_DIR")"
|
|
119
1202
|
|
|
120
|
-
|
|
121
|
-
|
|
1203
|
+
UNPACK_DIR="${WORK_DIR}/unpacked"
|
|
1204
|
+
ARTIFACT_SNAPSHOT="${WORK_DIR}/$("$BASENAME_EXECUTABLE" "$ARTIFACT_PATH")"
|
|
1205
|
+
MANIFEST_SNAPSHOT="${WORK_DIR}/$("$BASENAME_EXECUTABLE" "$MANIFEST_PATH")"
|
|
1206
|
+
"$MKDIR_EXECUTABLE" -p "$UNPACK_DIR"
|
|
1207
|
+
"$CP_EXECUTABLE" "$ARTIFACT_PATH" "$ARTIFACT_SNAPSHOT"
|
|
1208
|
+
"$CP_EXECUTABLE" "$MANIFEST_PATH" "$MANIFEST_SNAPSHOT"
|
|
1209
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-archive \
|
|
1210
|
+
--archive "$ARTIFACT_SNAPSHOT" \
|
|
1211
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1212
|
+
--team-id "$EXPECTED_TEAM_ID" \
|
|
1213
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1214
|
+
--source-sha "$EXPECTED_SOURCE_SHA" \
|
|
1215
|
+
--version "$EXPECTED_VERSION" \
|
|
1216
|
+
--artifact-policy "$ARTIFACT_POLICY" \
|
|
1217
|
+
--approved-target "$APPROVED_TARGET" \
|
|
1218
|
+
--approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
|
|
1219
|
+
--approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
|
|
1220
|
+
MANIFEST_BUILDER_IDENTITY_KIND="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
|
|
1221
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1222
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1223
|
+
--field builder_identity_kind)"
|
|
1224
|
+
|
|
1225
|
+
CURRENT_MACOS="$("$SW_VERS_EXECUTABLE" -productVersion)"
|
|
1226
|
+
MINIMUM_MACOS="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
|
|
1227
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1228
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1229
|
+
--field minimum_macos)"
|
|
1230
|
+
if ! "$BUN_EXECUTABLE" -e '
|
|
1231
|
+
const parse = (value) => value.split(".").map(Number);
|
|
1232
|
+
const [actual, minimum] = process.argv.slice(1).map(parse);
|
|
1233
|
+
for (let i = 0; i < Math.max(actual.length, minimum.length); i++) {
|
|
1234
|
+
if ((actual[i] ?? 0) > (minimum[i] ?? 0)) process.exit(0);
|
|
1235
|
+
if ((actual[i] ?? 0) < (minimum[i] ?? 0)) process.exit(1);
|
|
1236
|
+
}
|
|
1237
|
+
' "$CURRENT_MACOS" "$MINIMUM_MACOS"; then
|
|
1238
|
+
echo "Recordings.app requires macOS ${MINIMUM_MACOS} or later; target is ${CURRENT_MACOS}." >&2
|
|
1239
|
+
exit 1
|
|
122
1240
|
fi
|
|
1241
|
+
TARGET_ARCH="$("$UNAME_EXECUTABLE" -m)"
|
|
1242
|
+
MANIFEST_ARCHITECTURES="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
|
|
1243
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1244
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1245
|
+
--field architectures)"
|
|
1246
|
+
case " ${MANIFEST_ARCHITECTURES} " in
|
|
1247
|
+
*" ${TARGET_ARCH} "*) ;;
|
|
1248
|
+
*) echo "Recordings.app artifact does not support target architecture ${TARGET_ARCH}." >&2; exit 1 ;;
|
|
1249
|
+
esac
|
|
1250
|
+
|
|
1251
|
+
verify_secure_parent "$APP_PARENT"
|
|
1252
|
+
verify_existing_state_root
|
|
1253
|
+
|
|
1254
|
+
STAGING_DIR="$("$MKTEMP_EXECUTABLE" -d "${APP_PARENT}/.Recordings-install.XXXXXX")"
|
|
1255
|
+
STAGED_APP="${STAGING_DIR}/Recordings.app"
|
|
1256
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-archive \
|
|
1257
|
+
--archive "$ARTIFACT_SNAPSHOT" \
|
|
1258
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1259
|
+
--team-id "$EXPECTED_TEAM_ID" \
|
|
1260
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1261
|
+
--source-sha "$EXPECTED_SOURCE_SHA" \
|
|
1262
|
+
--version "$EXPECTED_VERSION" \
|
|
1263
|
+
--artifact-policy "$ARTIFACT_POLICY" \
|
|
1264
|
+
--approved-target "$APPROVED_TARGET" \
|
|
1265
|
+
--approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
|
|
1266
|
+
--approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
|
|
1267
|
+
|
|
1268
|
+
add_unique_app() {
|
|
1269
|
+
local candidate="$1"
|
|
1270
|
+
local existing
|
|
1271
|
+
if [ -e "$candidate" ] || [ -L "$candidate" ]; then
|
|
1272
|
+
[ -d "$candidate" ] && [ ! -L "$candidate" ] || {
|
|
1273
|
+
echo "Recordings.app path is not a secure directory: ${candidate}" >&2
|
|
1274
|
+
exit 1
|
|
1275
|
+
}
|
|
1276
|
+
fi
|
|
1277
|
+
[ -d "$candidate" ] || return 0
|
|
1278
|
+
for existing in ${DISCOVERED_APPS[@]+"${DISCOVERED_APPS[@]}"}; do
|
|
1279
|
+
[ "$existing" = "$candidate" ] && return 0
|
|
1280
|
+
done
|
|
1281
|
+
DISCOVERED_APPS+=("$candidate")
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
process_records_for_known_apps() {
|
|
1285
|
+
local pid
|
|
1286
|
+
local command
|
|
1287
|
+
local executable
|
|
1288
|
+
local command_matches
|
|
1289
|
+
local observed_executable
|
|
1290
|
+
local start_identity
|
|
1291
|
+
while read -r pid command; do
|
|
1292
|
+
[ -n "${pid:-}" ] || continue
|
|
1293
|
+
command_matches=0
|
|
1294
|
+
for executable in ${RUNNING_EXECUTABLES[@]+"${RUNNING_EXECUTABLES[@]}"}; do
|
|
1295
|
+
case "$command" in
|
|
1296
|
+
"$executable"|"$executable "*)
|
|
1297
|
+
command_matches=1
|
|
1298
|
+
break
|
|
1299
|
+
;;
|
|
1300
|
+
esac
|
|
1301
|
+
done
|
|
1302
|
+
[ "$command_matches" -eq 1 ] || continue
|
|
1303
|
+
observed_executable=""
|
|
1304
|
+
for executable in ${RUNNING_EXECUTABLES[@]+"${RUNNING_EXECUTABLES[@]}"}; do
|
|
1305
|
+
observed_executable="$(observed_executable_for_pid "$pid" "$executable")"
|
|
1306
|
+
if [ -n "$observed_executable" ]; then
|
|
1307
|
+
break
|
|
1308
|
+
fi
|
|
1309
|
+
done
|
|
1310
|
+
[ -n "$observed_executable" ] || continue
|
|
1311
|
+
start_identity="$(process_start_identity "$pid")"
|
|
1312
|
+
[ -n "$start_identity" ] || continue
|
|
1313
|
+
printf '%s\t%s\t%s\n' "$pid" "$start_identity" "$observed_executable"
|
|
1314
|
+
done < <("$PS_EXECUTABLE" -axo pid=,command= 2>/dev/null || true)
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
stop_old_processes() {
|
|
1318
|
+
local journal_phase="${1:-processes-stopping}"
|
|
1319
|
+
local current_records
|
|
1320
|
+
local app_path
|
|
1321
|
+
local executable
|
|
1322
|
+
local already_recorded
|
|
1323
|
+
local index
|
|
1324
|
+
local pid
|
|
1325
|
+
local start_identity
|
|
1326
|
+
local observed_executable
|
|
1327
|
+
current_records="$(process_records_for_known_apps)"
|
|
1328
|
+
LAST_STOPPED_PROCESS_COUNT=0
|
|
1329
|
+
[ -n "$current_records" ] || return 0
|
|
1330
|
+
while IFS=$'\t' read -r pid start_identity observed_executable; do
|
|
1331
|
+
[ -n "$pid" ] || continue
|
|
1332
|
+
LAST_STOPPED_PROCESS_COUNT=$((LAST_STOPPED_PROCESS_COUNT + 1))
|
|
1333
|
+
done <<< "$current_records"
|
|
1334
|
+
if [ -n "$OLD_PIDS" ]; then
|
|
1335
|
+
while IFS=$'\t' read -r pid start_identity observed_executable; do
|
|
1336
|
+
[ -n "$pid" ] || continue
|
|
1337
|
+
OLD_PIDS="${OLD_PIDS}"$'\n'"${pid}"
|
|
1338
|
+
done <<< "$current_records"
|
|
1339
|
+
else
|
|
1340
|
+
OLD_PIDS="$(printf '%s\n' "$current_records" | "$AWK_EXECUTABLE" -F '\t' '{ print $1 }')"
|
|
1341
|
+
fi
|
|
1342
|
+
while IFS=$'\t' read -r pid start_identity observed_executable; do
|
|
1343
|
+
case "$observed_executable" in
|
|
1344
|
+
*/Contents/MacOS/Recordings) app_path="${observed_executable%/Contents/MacOS/Recordings}" ;;
|
|
1345
|
+
*/Contents/Helpers/recordings) app_path="${observed_executable%/Contents/Helpers/recordings}" ;;
|
|
1346
|
+
*) continue ;;
|
|
1347
|
+
esac
|
|
1348
|
+
for ((index = 0; index < MOVED_ORIGINAL_COUNT; index++)); do
|
|
1349
|
+
if [ "$app_path" = "${MOVED_PATHS[$index]}" ]; then
|
|
1350
|
+
app_path="${MOVED_ORIGINALS[$index]}"
|
|
1351
|
+
break
|
|
1352
|
+
fi
|
|
1353
|
+
done
|
|
1354
|
+
already_recorded=0
|
|
1355
|
+
for recorded_path in ${PRIOR_RUNNING_APP_PATHS[@]+"${PRIOR_RUNNING_APP_PATHS[@]}"}; do
|
|
1356
|
+
[ "$recorded_path" != "$app_path" ] || already_recorded=1
|
|
1357
|
+
done
|
|
1358
|
+
[ "$already_recorded" -eq 1 ] || PRIOR_RUNNING_APP_PATHS+=("$app_path")
|
|
1359
|
+
done <<< "$current_records"
|
|
1360
|
+
was_running=1
|
|
1361
|
+
STOPPED_RUNNING_APP=1
|
|
1362
|
+
# Before the authoritative state backup there is intentionally no journal:
|
|
1363
|
+
# no app or state mutation has happened yet, so a crash can only leave the
|
|
1364
|
+
# prior app stopped. Once a journal exists, record every later stop phase.
|
|
1365
|
+
[ ! -f "$JOURNAL_PATH" ] || write_journal "$journal_phase"
|
|
1366
|
+
while IFS=$'\t' read -r pid start_identity observed_executable; do
|
|
1367
|
+
[ -n "$pid" ] || continue
|
|
1368
|
+
signal_process_if_current TERM "$pid" "$start_identity" "$observed_executable"
|
|
1369
|
+
done <<< "$current_records"
|
|
1370
|
+
local attempts=$((LAUNCH_TIMEOUT_SECONDS * 10))
|
|
1371
|
+
local remaining=0
|
|
1372
|
+
local attempt
|
|
1373
|
+
for ((attempt = 0; attempt < attempts; attempt++)); do
|
|
1374
|
+
remaining=0
|
|
1375
|
+
while IFS=$'\t' read -r pid start_identity observed_executable; do
|
|
1376
|
+
if process_identity_is_current "$pid" "$start_identity" "$observed_executable"; then
|
|
1377
|
+
remaining=1
|
|
1378
|
+
break
|
|
1379
|
+
fi
|
|
1380
|
+
done <<< "$current_records"
|
|
1381
|
+
[ "$remaining" -eq 0 ] && break
|
|
1382
|
+
"$SLEEP_EXECUTABLE" 0.1
|
|
1383
|
+
done
|
|
1384
|
+
if [ "$remaining" -ne 0 ]; then
|
|
1385
|
+
echo "Existing Recordings.app process did not stop before replacement." >&2
|
|
1386
|
+
exit 1
|
|
1387
|
+
fi
|
|
1388
|
+
}
|
|
123
1389
|
|
|
124
|
-
(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
1390
|
+
quiesce_old_processes() {
|
|
1391
|
+
local journal_phase="$1"
|
|
1392
|
+
local required_empty_scans=10
|
|
1393
|
+
local consecutive_empty_scans=0
|
|
1394
|
+
local attempts=$((LAUNCH_TIMEOUT_SECONDS * 10 + required_empty_scans))
|
|
1395
|
+
local attempt
|
|
1396
|
+
for ((attempt = 0; attempt < attempts; attempt++)); do
|
|
1397
|
+
stop_old_processes "$journal_phase"
|
|
1398
|
+
if [ "$LAST_STOPPED_PROCESS_COUNT" -eq 0 ]; then
|
|
1399
|
+
consecutive_empty_scans=$((consecutive_empty_scans + 1))
|
|
1400
|
+
[ "$consecutive_empty_scans" -lt "$required_empty_scans" ] || return 0
|
|
1401
|
+
else
|
|
1402
|
+
consecutive_empty_scans=0
|
|
1403
|
+
fi
|
|
1404
|
+
"$SLEEP_EXECUTABLE" 0.1
|
|
1405
|
+
done
|
|
1406
|
+
echo "Existing Recordings.app processes did not remain quiescent before replacement." >&2
|
|
1407
|
+
exit 1
|
|
1408
|
+
}
|
|
128
1409
|
|
|
129
|
-
|
|
130
|
-
|
|
1410
|
+
restart_previous_app() {
|
|
1411
|
+
[ "$STOPPED_RUNNING_APP" -eq 1 ] || return 0
|
|
1412
|
+
restart_recorded_app_paths
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
cleanup() {
|
|
1416
|
+
local status=$?
|
|
1417
|
+
local restart_after_release=0
|
|
1418
|
+
trap - EXIT
|
|
1419
|
+
if [ "$TRANSACTION_COMMITTED" -ne 1 ] && [ -f "$JOURNAL_PATH" ]; then
|
|
1420
|
+
STAGING_REMOVAL_DELEGATED=1
|
|
1421
|
+
cleanup_phase="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field phase 2>/dev/null || true)"
|
|
1422
|
+
case "$cleanup_phase" in
|
|
1423
|
+
candidate-moving|candidate-installed|activated|launching)
|
|
1424
|
+
if ! stop_uncommitted_candidate; then
|
|
1425
|
+
echo "Automatic rollback could not stop the uncommitted candidate." >&2
|
|
1426
|
+
PRESERVE_MAINTENANCE_MARKER=1
|
|
1427
|
+
RECOVERED_APP_RESTART_ON_ABORT=0
|
|
1428
|
+
release_install_coordination
|
|
1429
|
+
exit 1
|
|
1430
|
+
fi
|
|
1431
|
+
;;
|
|
1432
|
+
esac
|
|
1433
|
+
if [ "$SQLITE_BARRIER_ACTIVE" -eq 0 ] && ! acquire_sqlite_barrier; then
|
|
1434
|
+
echo "Automatic rollback could not establish the SQLite writer barrier; preserved transaction evidence at ${JOURNAL_PATH}." >&2
|
|
1435
|
+
PRESERVE_MAINTENANCE_MARKER=1
|
|
1436
|
+
RECOVERED_APP_RESTART_ON_ABORT=0
|
|
1437
|
+
status=1
|
|
1438
|
+
elif ! "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-recover --journal "$JOURNAL_PATH"; then
|
|
1439
|
+
echo "Automatic rollback failed; preserved transaction evidence at ${JOURNAL_PATH}." >&2
|
|
1440
|
+
PRESERVE_MAINTENANCE_MARKER=1
|
|
1441
|
+
RECOVERED_APP_RESTART_ON_ABORT=0
|
|
1442
|
+
status=1
|
|
1443
|
+
else
|
|
1444
|
+
restart_after_release=1
|
|
1445
|
+
fi
|
|
1446
|
+
elif [ "$TRANSACTION_COMMITTED" -ne 1 ] && [ "$RECOVERED_APP_RESTART_ON_ABORT" -eq 1 ]; then
|
|
1447
|
+
restart_after_release=1
|
|
1448
|
+
fi
|
|
1449
|
+
if ! release_sqlite_barrier; then
|
|
1450
|
+
PRESERVE_MAINTENANCE_MARKER=1
|
|
1451
|
+
restart_after_release=0
|
|
1452
|
+
status=1
|
|
1453
|
+
fi
|
|
1454
|
+
if [ "$STAGING_REMOVAL_DELEGATED" -eq 0 ]; then
|
|
1455
|
+
"$RM_EXECUTABLE" -rf "$STAGING_DIR"
|
|
1456
|
+
fi
|
|
1457
|
+
"$RM_EXECUTABLE" -rf "$WORK_DIR"
|
|
1458
|
+
if [ -n "$TRANSACTION_DIR" ] && [ ! -f "$JOURNAL_PATH" ] && [ -d "$TRANSACTION_DIR" ]; then
|
|
1459
|
+
if ! "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" transaction-cleanup \
|
|
1460
|
+
--transaction-dir "$TRANSACTION_DIR" \
|
|
1461
|
+
--nonce "$TRANSACTION_NONCE"; then
|
|
1462
|
+
echo "Could not safely isolate the pre-journal transaction; preserving maintenance evidence." >&2
|
|
1463
|
+
PRESERVE_MAINTENANCE_MARKER=1
|
|
1464
|
+
status=1
|
|
1465
|
+
fi
|
|
1466
|
+
fi
|
|
1467
|
+
if [ "$PRESERVE_MAINTENANCE_MARKER" -eq 0 ]; then
|
|
1468
|
+
if ! release_maintenance_marker; then
|
|
1469
|
+
restart_after_release=0
|
|
1470
|
+
status=1
|
|
1471
|
+
fi
|
|
1472
|
+
[ "$restart_after_release" -eq 0 ] || restart_previous_app
|
|
1473
|
+
fi
|
|
1474
|
+
release_install_lock
|
|
1475
|
+
exit "$status"
|
|
1476
|
+
}
|
|
1477
|
+
trap cleanup EXIT
|
|
1478
|
+
|
|
1479
|
+
maybe_crash_after_phase() {
|
|
1480
|
+
local phase="$1"
|
|
1481
|
+
if test_fault_hooks_enabled && [ "${RECORDINGS_TEST_CRASH_AFTER_PHASE:-}" = "$phase" ]; then
|
|
1482
|
+
kill -KILL "$$"
|
|
1483
|
+
fi
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
write_journal() {
|
|
1487
|
+
local phase="$1"
|
|
1488
|
+
local -a arguments=(
|
|
1489
|
+
journal-write
|
|
1490
|
+
--journal "$JOURNAL_PATH"
|
|
1491
|
+
--phase "$phase"
|
|
1492
|
+
--transaction-dir "$TRANSACTION_DIR"
|
|
1493
|
+
--candidate-staging "$STAGING_DIR"
|
|
1494
|
+
--app-parent "$APP_PARENT"
|
|
1495
|
+
--app-destination "$APP_DEST"
|
|
1496
|
+
--data-dir "$DATA_DIR"
|
|
1497
|
+
--state-backup "$STATE_BACKUP"
|
|
1498
|
+
--state-backup-sha256 "$STATE_BACKUP_SHA256"
|
|
1499
|
+
--expected-manifest-sha256 "$EXPECTED_MANIFEST_SHA256"
|
|
1500
|
+
--expected-source-sha "$EXPECTED_SOURCE_SHA"
|
|
1501
|
+
--expected-version "$EXPECTED_VERSION"
|
|
1502
|
+
--artifact-policy "$ARTIFACT_POLICY"
|
|
1503
|
+
--approved-target "$APPROVED_TARGET"
|
|
1504
|
+
--approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND"
|
|
1505
|
+
--approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
|
|
1506
|
+
--builder-identity-kind "$MANIFEST_BUILDER_IDENTITY_KIND"
|
|
1507
|
+
--candidate-identity-sha256 "$candidate_identity_sha256"
|
|
1508
|
+
--candidate-tree-sha256 "$candidate_tree_sha256"
|
|
1509
|
+
--previous-identity-sha256 "$previous_identity_sha256"
|
|
1510
|
+
--original-state-mode "$ORIGINAL_STATE_MODE"
|
|
1511
|
+
)
|
|
1512
|
+
[ "$was_running" -eq 0 ] || arguments+=(--was-running)
|
|
1513
|
+
for prior_running_app_path in ${PRIOR_RUNNING_APP_PATHS[@]+"${PRIOR_RUNNING_APP_PATHS[@]}"}; do
|
|
1514
|
+
arguments+=(--prior-running-app-path "$prior_running_app_path")
|
|
1515
|
+
done
|
|
1516
|
+
local index
|
|
1517
|
+
for ((index = 0; index < MOVED_ORIGINAL_COUNT; index++)); do
|
|
1518
|
+
arguments+=(--original "${MOVED_ORIGINALS[$index]}" "${MOVED_PATHS[$index]}" "${MOVED_DIGESTS[$index]}")
|
|
1519
|
+
done
|
|
1520
|
+
for ((index = 0; index < ${#INSTALLER_OWNED_STATE_PATHS[@]}; index++)); do
|
|
1521
|
+
arguments+=(
|
|
1522
|
+
--installer-owned-state
|
|
1523
|
+
"${INSTALLER_OWNED_STATE_PATHS[$index]}"
|
|
1524
|
+
"${INSTALLER_OWNED_STATE_DIGESTS[$index]}"
|
|
1525
|
+
)
|
|
1526
|
+
done
|
|
1527
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" "${arguments[@]}"
|
|
1528
|
+
STAGING_REMOVAL_DELEGATED=1
|
|
1529
|
+
maybe_crash_after_phase "$phase"
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" extract-verified-archive \
|
|
1533
|
+
--archive "$ARTIFACT_SNAPSHOT" \
|
|
1534
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1535
|
+
--staging-target "$UNPACK_DIR" \
|
|
1536
|
+
--team-id "$EXPECTED_TEAM_ID" \
|
|
1537
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1538
|
+
--source-sha "$EXPECTED_SOURCE_SHA" \
|
|
1539
|
+
--version "$EXPECTED_VERSION" \
|
|
1540
|
+
--artifact-policy "$ARTIFACT_POLICY" \
|
|
1541
|
+
--approved-target "$APPROVED_TARGET" \
|
|
1542
|
+
--approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
|
|
1543
|
+
--approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
|
|
1544
|
+
CANDIDATE_APP="$UNPACK_DIR/Recordings.app"
|
|
1545
|
+
|
|
1546
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-app \
|
|
1547
|
+
--app "$CANDIDATE_APP" \
|
|
1548
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1549
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1550
|
+
--team-id "$EXPECTED_TEAM_ID" \
|
|
1551
|
+
--artifact-policy "$ARTIFACT_POLICY" \
|
|
1552
|
+
--approved-target "$APPROVED_TARGET" \
|
|
1553
|
+
--approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
|
|
1554
|
+
--approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
|
|
1555
|
+
if [ "$ARTIFACT_POLICY" = "release" ]; then
|
|
1556
|
+
"$XCRUN_EXECUTABLE" stapler validate "$CANDIDATE_APP"
|
|
1557
|
+
"$SPCTL_EXECUTABLE" --assess --type execute --verbose=2 "$CANDIDATE_APP"
|
|
1558
|
+
"$SYSPOLICY_CHECK_EXECUTABLE" distribution "$CANDIDATE_APP"
|
|
1559
|
+
fi
|
|
1560
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$CANDIDATE_APP" --uid "$("$ID_EXECUTABLE" -u)"
|
|
1561
|
+
|
|
1562
|
+
DISCOVERED_APPS=()
|
|
1563
|
+
add_unique_app "$APP_DEST"
|
|
1564
|
+
add_unique_app "${DATA_DIR}/Recordings.app"
|
|
1565
|
+
for candidate in "${HOME}"/Applications/Recordings.app.*; do
|
|
1566
|
+
add_unique_app "$candidate"
|
|
1567
|
+
done
|
|
1568
|
+
add_unique_app "/Applications/Recordings.app"
|
|
1569
|
+
if [ -x "$MDFIND_EXECUTABLE" ]; then
|
|
1570
|
+
while IFS= read -r candidate; do
|
|
1571
|
+
[ -n "$candidate" ] || continue
|
|
1572
|
+
add_unique_app "$candidate"
|
|
1573
|
+
done < <("$MDFIND_EXECUTABLE" "kMDItemCFBundleIdentifier == 'com.hasna.recordings'" 2>/dev/null || true)
|
|
1574
|
+
fi
|
|
1575
|
+
|
|
1576
|
+
MANAGEABLE_APPS=()
|
|
1577
|
+
for existing_app in ${DISCOVERED_APPS[@]+"${DISCOVERED_APPS[@]}"}; do
|
|
1578
|
+
case "$existing_app" in
|
|
1579
|
+
"$APP_DEST"|"${DATA_DIR}/Recordings.app"|"${HOME}/Applications/Recordings.app."*)
|
|
1580
|
+
echo "Duplicate disposition: archive transactionally, remove after activation: ${existing_app}"
|
|
1581
|
+
MANAGEABLE_APPS+=("$existing_app")
|
|
1582
|
+
;;
|
|
1583
|
+
*)
|
|
1584
|
+
echo "Duplicate Recordings.app at ${existing_app} is outside the transactional user install paths; archive or remove it before installing." >&2
|
|
1585
|
+
exit 1
|
|
1586
|
+
;;
|
|
1587
|
+
esac
|
|
1588
|
+
done
|
|
1589
|
+
|
|
1590
|
+
measure_kb() {
|
|
1591
|
+
local measured
|
|
1592
|
+
measured="$("$DU_EXECUTABLE" -sk "$@" 2>/dev/null | "$AWK_EXECUTABLE" '{sum += $1} END {print sum + 0}')"
|
|
1593
|
+
case "$measured" in ''|*[!0-9]*) echo "Could not measure transactional storage requirements." >&2; exit 1 ;; esac
|
|
1594
|
+
printf '%s\n' "$measured"
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
require_space() {
|
|
1598
|
+
local path="$1"
|
|
1599
|
+
local required="$2"
|
|
1600
|
+
local available
|
|
1601
|
+
available="$("$DF_EXECUTABLE" -Pk "$path" | "$AWK_EXECUTABLE" 'NR == 2 {print $4}')"
|
|
1602
|
+
case "$available" in ''|*[!0-9]*) echo "Could not determine free space for ${path}." >&2; exit 1 ;; esac
|
|
1603
|
+
if [ "$available" -lt "$required" ]; then
|
|
1604
|
+
echo "Insufficient free space for transactional app and state backups at ${path}." >&2
|
|
1605
|
+
exit 1
|
|
1606
|
+
fi
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
candidate_kb="$(measure_kb "$CANDIDATE_APP")"
|
|
1610
|
+
data_kb="$(measure_kb "$DATA_DIR")"
|
|
1611
|
+
existing_kb=0
|
|
1612
|
+
for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
|
|
1613
|
+
existing_kb=$((existing_kb + $(measure_kb "$existing_app")))
|
|
1614
|
+
done
|
|
1615
|
+
require_space "$APP_PARENT" $((candidate_kb * 3 + existing_kb * 2 + data_kb * 2 + 10240))
|
|
1616
|
+
require_space "$DATA_DIR" $((existing_kb * 2 + data_kb * 2 + 10240))
|
|
1617
|
+
|
|
1618
|
+
candidate_requirement="$("$CODESIGN_EXECUTABLE" -d -r- "$CANDIDATE_APP" 2>&1 | "$SED_EXECUTABLE" -n 's/^designated => //p' | "$HEAD_EXECUTABLE" -n 1 || true)"
|
|
1619
|
+
if [ "$ARTIFACT_POLICY" = "release" ] && [ -z "$candidate_requirement" ]; then
|
|
1620
|
+
echo "Candidate app has no designated requirement." >&2
|
|
1621
|
+
exit 1
|
|
1622
|
+
fi
|
|
1623
|
+
identity_migration=0
|
|
1624
|
+
candidate_identity_sha256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" requirement-digest --app "$CANDIDATE_APP" --artifact-policy "$ARTIFACT_POLICY")"
|
|
1625
|
+
candidate_tree_sha256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$CANDIDATE_APP")"
|
|
1626
|
+
[ "$candidate_identity_sha256" = "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
|
|
1627
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1628
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1629
|
+
--field identity)" ] || {
|
|
1630
|
+
echo "Candidate identity does not match the artifact manifest." >&2
|
|
1631
|
+
exit 1
|
|
1632
|
+
}
|
|
1633
|
+
previous_identity_sha256="none"
|
|
1634
|
+
for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
|
|
1635
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$existing_app" --uid "$("$ID_EXECUTABLE" -u)"
|
|
1636
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" assert-transition \
|
|
1637
|
+
--existing-app "$existing_app" \
|
|
1638
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1639
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256"
|
|
1640
|
+
existing_requirement="$("$CODESIGN_EXECUTABLE" -d -r- "$existing_app" 2>&1 | "$SED_EXECUTABLE" -n 's/^designated => //p' | "$HEAD_EXECUTABLE" -n 1 || true)"
|
|
1641
|
+
existing_identity_policy="release"
|
|
1642
|
+
if [ "$ARTIFACT_POLICY" = "local_only" ] && [ -z "$existing_requirement" ]; then
|
|
1643
|
+
existing_identity_policy="local_only"
|
|
1644
|
+
fi
|
|
1645
|
+
existing_identity_sha256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" requirement-digest --app "$existing_app" --artifact-policy "$existing_identity_policy")"
|
|
1646
|
+
if [ "$previous_identity_sha256" = "none" ]; then
|
|
1647
|
+
previous_identity_sha256="$existing_identity_sha256"
|
|
1648
|
+
elif [ "$previous_identity_sha256" != "$existing_identity_sha256" ]; then
|
|
1649
|
+
echo "Installed duplicates have multiple signing identities; automatic migration is unsafe." >&2
|
|
1650
|
+
exit 1
|
|
1651
|
+
fi
|
|
1652
|
+
if [ -z "$existing_requirement" ] || \
|
|
1653
|
+
! "$CODESIGN_EXECUTABLE" --verify --strict -R "$existing_requirement" "$CANDIDATE_APP" >/dev/null 2>&1 || \
|
|
1654
|
+
! "$CODESIGN_EXECUTABLE" --verify --strict -R "$candidate_requirement" "$existing_app" >/dev/null 2>&1; then
|
|
1655
|
+
identity_migration=1
|
|
1656
|
+
fi
|
|
1657
|
+
done
|
|
1658
|
+
if [ "$ARTIFACT_POLICY" = "release" ] && [ "$identity_migration" -eq 1 ] && [ "$ALLOW_IDENTITY_MIGRATION" -ne 1 ]; then
|
|
1659
|
+
echo "Candidate and existing app designated requirements are not mutually compatible; review the signer change and rerun once with --allow-signing-identity-migration." >&2
|
|
1660
|
+
exit 1
|
|
1661
|
+
fi
|
|
1662
|
+
if [ "$ARTIFACT_POLICY" = "release" ] && [ "$identity_migration" -eq 1 ] && {
|
|
1663
|
+
[ "$previous_identity_sha256" != "$EXPECTED_OLD_IDENTITY_SHA256" ] ||
|
|
1664
|
+
[ "$candidate_identity_sha256" != "$EXPECTED_NEW_IDENTITY_SHA256" ];
|
|
1665
|
+
}; then
|
|
1666
|
+
echo "Signing identity migration does not match the exact operator-approved old/new identities." >&2
|
|
1667
|
+
exit 1
|
|
1668
|
+
fi
|
|
1669
|
+
if [ "$ARTIFACT_POLICY" = "release" ] && [ "$identity_migration" -eq 0 ] && [ "$ALLOW_IDENTITY_MIGRATION" -eq 1 ]; then
|
|
1670
|
+
echo "Identity migration approval was supplied but no identity migration is required." >&2
|
|
1671
|
+
exit 1
|
|
1672
|
+
fi
|
|
1673
|
+
|
|
1674
|
+
TRANSACTION_DIR="$("$MKTEMP_EXECUTABLE" -d "${APP_PARENT}/.Recordings-transaction.XXXXXX")"
|
|
1675
|
+
TRANSACTION_NONCE="$("$BUN_EXECUTABLE" -e 'import { randomUUID } from "node:crypto"; process.stdout.write(randomUUID())')"
|
|
1676
|
+
printf '%s\n' "$TRANSACTION_NONCE" >"$TRANSACTION_DIR/.Recordings-transaction-owner"
|
|
1677
|
+
"$CHMOD_EXECUTABLE" 600 "$TRANSACTION_DIR/.Recordings-transaction-owner"
|
|
1678
|
+
"$CHMOD_EXECUTABLE" 700 "$TRANSACTION_DIR"
|
|
1679
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree \
|
|
1680
|
+
--path "$TRANSACTION_DIR/.Recordings-transaction-owner"
|
|
1681
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR"
|
|
1682
|
+
"$MKDIR_EXECUTABLE" -m 700 "$TRANSACTION_DIR/apps"
|
|
1683
|
+
"$MKDIR_EXECUTABLE" -m 700 "$TRANSACTION_DIR/archives"
|
|
1684
|
+
verify_secure_parent "$TRANSACTION_DIR"
|
|
1685
|
+
|
|
1686
|
+
acquire_sqlite_barrier
|
|
1687
|
+
STATE_BACKUP="$TRANSACTION_DIR/state.initial"
|
|
1688
|
+
"$DITTO_EXECUTABLE" "$DATA_DIR" "$STATE_BACKUP"
|
|
1689
|
+
if ! "$DIFF_EXECUTABLE" -qr "$DATA_DIR" "$STATE_BACKUP" >/dev/null; then
|
|
1690
|
+
echo "Recordings state backup verification failed before replacement." >&2
|
|
1691
|
+
exit 1
|
|
1692
|
+
fi
|
|
1693
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$STATE_BACKUP" --uid "$("$ID_EXECUTABLE" -u)"
|
|
1694
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$STATE_BACKUP"
|
|
1695
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR"
|
|
1696
|
+
STATE_BACKUP_SHA256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$STATE_BACKUP")"
|
|
1697
|
+
release_sqlite_barrier
|
|
1698
|
+
|
|
1699
|
+
MOVED_ORIGINALS=()
|
|
1700
|
+
MOVED_PATHS=()
|
|
1701
|
+
MOVED_DIGESTS=()
|
|
1702
|
+
MOVED_ORIGINAL_COUNT=0
|
|
1703
|
+
move_index=0
|
|
1704
|
+
for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
|
|
1705
|
+
move_index=$((move_index + 1))
|
|
1706
|
+
MOVED_ORIGINALS+=("$existing_app")
|
|
1707
|
+
MOVED_PATHS+=("$TRANSACTION_DIR/apps/original-${move_index}")
|
|
1708
|
+
MOVED_DIGESTS+=("$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$existing_app")")
|
|
1709
|
+
MOVED_ORIGINAL_COUNT=$move_index
|
|
1710
|
+
done
|
|
1711
|
+
write_journal prepared
|
|
1712
|
+
|
|
1713
|
+
ARCHIVE_SEQUENCE=0
|
|
1714
|
+
for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
|
|
1715
|
+
ARCHIVE_SEQUENCE=$((ARCHIVE_SEQUENCE + 1))
|
|
1716
|
+
stamp="$("$DATE_EXECUTABLE" -u +%Y%m%dT%H%M%SZ)-$$-${ARCHIVE_SEQUENCE}"
|
|
1717
|
+
rollback_archive="$TRANSACTION_DIR/archives/Recordings-pre-install-${stamp}.zip"
|
|
1718
|
+
rollback_destination="$ROLLBACK_DIR/Recordings-pre-install-${stamp}.zip"
|
|
1719
|
+
[ ! -e "$rollback_destination" ] && [ ! -L "$rollback_destination" ] || {
|
|
1720
|
+
echo "Refusing to overwrite an existing rollback archive destination." >&2
|
|
1721
|
+
exit 1
|
|
1722
|
+
}
|
|
1723
|
+
"$DITTO_EXECUTABLE" -c -k --sequesterRsrc --keepParent \
|
|
1724
|
+
"$existing_app" \
|
|
1725
|
+
"$rollback_archive"
|
|
1726
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$rollback_archive"
|
|
1727
|
+
INSTALLER_OWNED_STATE_PATHS+=("$rollback_destination")
|
|
1728
|
+
INSTALLER_OWNED_STATE_DIGESTS+=(
|
|
1729
|
+
"$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$rollback_archive")"
|
|
1730
|
+
)
|
|
1731
|
+
done
|
|
1732
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR/archives"
|
|
1733
|
+
|
|
1734
|
+
"$DITTO_EXECUTABLE" "$CANDIDATE_APP" "$STAGED_APP"
|
|
1735
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-app \
|
|
1736
|
+
--app "$STAGED_APP" \
|
|
1737
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1738
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1739
|
+
--team-id "$EXPECTED_TEAM_ID" \
|
|
1740
|
+
--artifact-policy "$ARTIFACT_POLICY" \
|
|
1741
|
+
--approved-target "$APPROVED_TARGET" \
|
|
1742
|
+
--approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
|
|
1743
|
+
--approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
|
|
1744
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$STAGED_APP" --uid "$("$ID_EXECUTABLE" -u)"
|
|
1745
|
+
[ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$STAGED_APP")" = \
|
|
1746
|
+
"$candidate_tree_sha256" ] || {
|
|
1747
|
+
echo "Staged candidate does not match the authenticated candidate tree." >&2
|
|
1748
|
+
exit 1
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
# Store readers are already drained. Archive and stage while the current app
|
|
1752
|
+
# remains available, then stop every known app and packaged helper immediately
|
|
1753
|
+
# before the authoritative stopped-state snapshot. Recovery deliberately does
|
|
1754
|
+
# not restore state for prepared/processes-stopping; only processes-stopped and
|
|
1755
|
+
# later phases bind rollback to the stopped snapshot.
|
|
1756
|
+
for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
|
|
1757
|
+
RUNNING_EXECUTABLES+=("$existing_app/Contents/MacOS/Recordings")
|
|
1758
|
+
RUNNING_EXECUTABLES+=("$existing_app/Contents/Helpers/recordings")
|
|
1759
|
+
done
|
|
1760
|
+
stop_old_processes
|
|
1761
|
+
acquire_sqlite_barrier
|
|
1762
|
+
# A previous release may ignore the new maintenance marker and relaunch from
|
|
1763
|
+
# its helper. Rescan immediately on both sides of the authoritative copy while
|
|
1764
|
+
# the exclusive SQLite transaction prevents any surviving writer from landing.
|
|
1765
|
+
stop_old_processes
|
|
1766
|
+
NEXT_STATE_BACKUP="$TRANSACTION_DIR/state.stopped"
|
|
1767
|
+
"$DITTO_EXECUTABLE" "$DATA_DIR" "$NEXT_STATE_BACKUP"
|
|
1768
|
+
stop_old_processes
|
|
1769
|
+
if ! "$DIFF_EXECUTABLE" -qr "$DATA_DIR" "$NEXT_STATE_BACKUP" >/dev/null; then
|
|
1770
|
+
echo "Stopped-state backup verification failed before replacement." >&2
|
|
1771
|
+
exit 1
|
|
1772
|
+
fi
|
|
1773
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$NEXT_STATE_BACKUP" --uid "$("$ID_EXECUTABLE" -u)"
|
|
1774
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$NEXT_STATE_BACKUP"
|
|
1775
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR"
|
|
1776
|
+
NEXT_STATE_BACKUP_SHA256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$NEXT_STATE_BACKUP")"
|
|
1777
|
+
maybe_crash_after_phase state-refresh-copied-before-journal
|
|
1778
|
+
STATE_BACKUP="$NEXT_STATE_BACKUP"
|
|
1779
|
+
STATE_BACKUP_SHA256="$NEXT_STATE_BACKUP_SHA256"
|
|
1780
|
+
write_journal processes-stopped
|
|
1781
|
+
# The durable journal boundary can race an older helper relaunch. Quiesce every
|
|
1782
|
+
# known bundle, then refresh into a new immutable snapshot. If another known
|
|
1783
|
+
# writer appears after that journal update, repeat with a fresh generation; an
|
|
1784
|
+
# already-journaled backup is never changed in place. Standalone writers that
|
|
1785
|
+
# cannot be identified here are merged conservatively during rollback.
|
|
1786
|
+
STOPPED_REFRESH_STABLE=0
|
|
1787
|
+
for ((refresh_attempt = 1; refresh_attempt <= 5; refresh_attempt++)); do
|
|
1788
|
+
quiesce_old_processes processes-stopped
|
|
1789
|
+
REFRESHED_STATE_BACKUP="$TRANSACTION_DIR/state.stopped.${refresh_attempt}"
|
|
1790
|
+
"$DITTO_EXECUTABLE" "$DATA_DIR" "$REFRESHED_STATE_BACKUP"
|
|
1791
|
+
stop_old_processes processes-stopped
|
|
1792
|
+
if [ "$LAST_STOPPED_PROCESS_COUNT" -ne 0 ] || \
|
|
1793
|
+
! "$DIFF_EXECUTABLE" -qr "$DATA_DIR" "$REFRESHED_STATE_BACKUP" >/dev/null; then
|
|
1794
|
+
continue
|
|
1795
|
+
fi
|
|
1796
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree \
|
|
1797
|
+
--path "$REFRESHED_STATE_BACKUP" --uid "$("$ID_EXECUTABLE" -u)"
|
|
1798
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$REFRESHED_STATE_BACKUP"
|
|
1799
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR"
|
|
1800
|
+
STATE_BACKUP="$REFRESHED_STATE_BACKUP"
|
|
1801
|
+
STATE_BACKUP_SHA256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$STATE_BACKUP")"
|
|
1802
|
+
write_journal processes-stopped
|
|
1803
|
+
stop_old_processes processes-stopped
|
|
1804
|
+
if [ "$LAST_STOPPED_PROCESS_COUNT" -eq 0 ]; then
|
|
1805
|
+
STOPPED_REFRESH_STABLE=1
|
|
1806
|
+
break
|
|
1807
|
+
fi
|
|
1808
|
+
done
|
|
1809
|
+
[ "$STOPPED_REFRESH_STABLE" -eq 1 ] || {
|
|
1810
|
+
echo "Existing Recordings.app writers did not remain quiescent around the stopped-state refresh." >&2
|
|
1811
|
+
exit 1
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
# This phase is durable before changing state mode, creating install-owned
|
|
1815
|
+
# directories, or moving exact archived bundles into the state tree.
|
|
1816
|
+
write_journal state-mutating
|
|
1817
|
+
|
|
1818
|
+
verify_existing_state_root
|
|
1819
|
+
[ "$("$STAT_EXECUTABLE" -f '%Lp' "$DATA_DIR")" = "$ORIGINAL_STATE_MODE" ] || {
|
|
1820
|
+
echo "Recordings state mode changed after transactional backup." >&2
|
|
1821
|
+
exit 1
|
|
1822
|
+
}
|
|
1823
|
+
if [ "$ORIGINAL_STATE_MODE" = 755 ]; then
|
|
1824
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" state-mode-harden --path "$DATA_DIR" --uid "$("$ID_EXECUTABLE" -u)"
|
|
1825
|
+
maybe_crash_after_phase state-mode-hardened
|
|
1826
|
+
if test_fault_hooks_enabled && \
|
|
1827
|
+
[ "${RECORDINGS_TEST_FAIL_AFTER_STATE_MODE_HARDEN:-0}" != 0 ]; then
|
|
1828
|
+
exit 1
|
|
1829
|
+
fi
|
|
131
1830
|
fi
|
|
1831
|
+
verify_secure_parent "$DATA_DIR" 1
|
|
1832
|
+
"$MKDIR_EXECUTABLE" -m 700 -p "${DATA_DIR}/audio" "$ROLLBACK_DIR"
|
|
1833
|
+
verify_secure_parent "$ROLLBACK_DIR" 1
|
|
1834
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$ROLLBACK_DIR"
|
|
1835
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$DATA_DIR"
|
|
132
1836
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
1837
|
+
for ((archive_index = 0; archive_index < ${#INSTALLER_OWNED_STATE_PATHS[@]}; archive_index++)); do
|
|
1838
|
+
rollback_destination="${INSTALLER_OWNED_STATE_PATHS[$archive_index]}"
|
|
1839
|
+
rollback_archive="$TRANSACTION_DIR/archives/$("$BASENAME_EXECUTABLE" "$rollback_destination")"
|
|
1840
|
+
[ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$rollback_archive")" = \
|
|
1841
|
+
"${INSTALLER_OWNED_STATE_DIGESTS[$archive_index]}" ] || {
|
|
1842
|
+
echo "Transactional rollback archive changed before state installation." >&2
|
|
1843
|
+
exit 1
|
|
1844
|
+
}
|
|
1845
|
+
[ ! -e "$rollback_destination" ] && [ ! -L "$rollback_destination" ] || {
|
|
1846
|
+
echo "Rollback archive destination appeared during installation." >&2
|
|
1847
|
+
exit 1
|
|
1848
|
+
}
|
|
1849
|
+
"$MV_EXECUTABLE" "$rollback_archive" "$rollback_destination"
|
|
1850
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$rollback_destination"
|
|
1851
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$ROLLBACK_DIR"
|
|
1852
|
+
done
|
|
136
1853
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
1854
|
+
write_journal originals-moving
|
|
1855
|
+
stop_old_processes originals-moving
|
|
1856
|
+
for ((move_index = 0; move_index < MOVED_ORIGINAL_COUNT; move_index++)); do
|
|
1857
|
+
existing_app="${MOVED_ORIGINALS[$move_index]}"
|
|
1858
|
+
moved_path="${MOVED_PATHS[$move_index]}"
|
|
1859
|
+
[ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$existing_app")" = "${MOVED_DIGESTS[$move_index]}" ] || {
|
|
1860
|
+
echo "Installed Recordings.app changed after transactional backup planning." >&2
|
|
1861
|
+
exit 1
|
|
1862
|
+
}
|
|
1863
|
+
RECORDINGS_TEST_INSTALLER_PID="$$" \
|
|
1864
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" install-archive-original \
|
|
1865
|
+
--journal "$JOURNAL_PATH" \
|
|
1866
|
+
--source "$existing_app" \
|
|
1867
|
+
--destination "$moved_path" \
|
|
1868
|
+
--expected-tree-sha256 "${MOVED_DIGESTS[$move_index]}"
|
|
1869
|
+
RUNNING_EXECUTABLES+=("$moved_path/Contents/MacOS/Recordings")
|
|
1870
|
+
RUNNING_EXECUTABLES+=("$moved_path/Contents/Helpers/recordings")
|
|
1871
|
+
"$CHMOD_EXECUTABLE" -R go-w "$moved_path"
|
|
1872
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$moved_path" --uid "$("$ID_EXECUTABLE" -u)"
|
|
1873
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$moved_path"
|
|
1874
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$("$DIRNAME_EXECUTABLE" "$existing_app")"
|
|
1875
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR/apps"
|
|
1876
|
+
done
|
|
1877
|
+
write_journal originals-moved
|
|
1878
|
+
# A process launched in the narrow rename window now resolves through lsof to
|
|
1879
|
+
# the moved bundle. Stop that exact start/executable identity before releasing
|
|
1880
|
+
# the SQLite barrier; PID reuse or a different executable is never signalled.
|
|
1881
|
+
quiesce_old_processes originals-moved
|
|
1882
|
+
release_sqlite_barrier
|
|
1883
|
+
|
|
1884
|
+
write_journal candidate-moving
|
|
1885
|
+
RECORDINGS_TEST_INSTALLER_PID="$$" \
|
|
1886
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" install-publish-candidate \
|
|
1887
|
+
--journal "$JOURNAL_PATH" \
|
|
1888
|
+
--staging "$STAGED_APP" \
|
|
1889
|
+
--destination "$APP_DEST" \
|
|
1890
|
+
--expected-tree-sha256 "$candidate_tree_sha256"
|
|
1891
|
+
maybe_crash_after_phase candidate-moved-before-journal
|
|
1892
|
+
[ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$APP_DEST")" = \
|
|
1893
|
+
"$candidate_tree_sha256" ] || {
|
|
1894
|
+
echo "Installed candidate changed during publication." >&2
|
|
1895
|
+
exit 1
|
|
1896
|
+
}
|
|
1897
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$APP_DEST"
|
|
1898
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$APP_PARENT"
|
|
1899
|
+
write_journal candidate-installed
|
|
1900
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-app \
|
|
1901
|
+
--app "$APP_DEST" \
|
|
1902
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1903
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1904
|
+
--team-id "$EXPECTED_TEAM_ID" \
|
|
1905
|
+
--artifact-policy "$ARTIFACT_POLICY" \
|
|
1906
|
+
--approved-target "$APPROVED_TARGET" \
|
|
1907
|
+
--approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
|
|
1908
|
+
--approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
|
|
1909
|
+
if [ "$ARTIFACT_POLICY" = "release" ]; then
|
|
1910
|
+
"$XCRUN_EXECUTABLE" stapler validate "$APP_DEST"
|
|
1911
|
+
"$SPCTL_EXECUTABLE" --assess --type execute --verbose=2 "$APP_DEST"
|
|
1912
|
+
"$SYSPOLICY_CHECK_EXECUTABLE" distribution "$APP_DEST"
|
|
141
1913
|
fi
|
|
1914
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$APP_DEST" --uid "$("$ID_EXECUTABLE" -u)"
|
|
1915
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-active \
|
|
1916
|
+
--app "$APP_DEST" \
|
|
1917
|
+
--manifest "$MANIFEST_SNAPSHOT" \
|
|
1918
|
+
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
|
1919
|
+
--team-id "$EXPECTED_TEAM_ID" \
|
|
1920
|
+
--artifact-policy "$ARTIFACT_POLICY" \
|
|
1921
|
+
--approved-target "$APPROVED_TARGET" \
|
|
1922
|
+
--approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
|
|
1923
|
+
--approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
|
|
1924
|
+
SMOKE_TOOL_PATH="/usr/bin:/bin:/usr/sbin:/sbin:$("$DIRNAME_EXECUTABLE" "$BUN_EXECUTABLE")"
|
|
1925
|
+
PATH="$SMOKE_TOOL_PATH" "$RUNTIME_SMOKE" "$APP_DEST" "$BUN_EXECUTABLE"
|
|
1926
|
+
write_journal activated
|
|
142
1927
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1928
|
+
write_journal committed
|
|
1929
|
+
STAGING_REMOVAL_DELEGATED=1
|
|
1930
|
+
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-recover --journal "$JOURNAL_PATH"
|
|
1931
|
+
TRANSACTION_COMMITTED=1
|
|
1932
|
+
STOPPED_RUNNING_APP=0
|
|
146
1933
|
|
|
147
|
-
|
|
1934
|
+
if [ "$LAUNCH_APP" -eq 1 ] || [ "$was_running" -eq 1 ]; then
|
|
1935
|
+
EXPECTED_EXECUTABLE="$APP_DEST/Contents/MacOS/Recordings"
|
|
1936
|
+
RUNNING_EXECUTABLES+=("$EXPECTED_EXECUTABLE")
|
|
1937
|
+
"$OPEN_EXECUTABLE" -n "$APP_DEST"
|
|
1938
|
+
attempts=$((LAUNCH_TIMEOUT_SECONDS * 10))
|
|
1939
|
+
launched_pid=""
|
|
1940
|
+
launched_start_identity=""
|
|
1941
|
+
for ((attempt = 0; attempt < attempts; attempt++)); do
|
|
1942
|
+
while IFS=$'\t' read -r pid start_identity observed_executable; do
|
|
1943
|
+
[ -n "$pid" ] || continue
|
|
1944
|
+
launched_pid="$pid"
|
|
1945
|
+
launched_start_identity="$start_identity"
|
|
1946
|
+
break
|
|
1947
|
+
done < <(pids_for_exact_executable "$EXPECTED_EXECUTABLE")
|
|
1948
|
+
[ -n "$launched_pid" ] && break
|
|
1949
|
+
"$SLEEP_EXECUTABLE" 0.1
|
|
1950
|
+
done
|
|
1951
|
+
if [ -z "$launched_pid" ]; then
|
|
1952
|
+
echo "Canonical app did not launch from ${EXPECTED_EXECUTABLE} within ${LAUNCH_TIMEOUT_SECONDS} seconds." >&2
|
|
1953
|
+
exit 1
|
|
1954
|
+
fi
|
|
1955
|
+
"$SLEEP_EXECUTABLE" 1
|
|
1956
|
+
if ! process_identity_is_current "$launched_pid" "$launched_start_identity" "$EXPECTED_EXECUTABLE"; then
|
|
1957
|
+
echo "Canonical app process exited before the stability window completed." >&2
|
|
1958
|
+
exit 1
|
|
1959
|
+
fi
|
|
1960
|
+
maybe_crash_after_phase candidate-launched-after-commit
|
|
1961
|
+
fi
|
|
148
1962
|
|
|
149
|
-
|
|
1963
|
+
if [ "$ARTIFACT_POLICY" = "local_only" ]; then
|
|
1964
|
+
echo "Installed local-only Recordings.app for ${APPROVED_TARGET}; this artifact is ad-hoc signed and non-notarized."
|
|
1965
|
+
echo "Microphone or Accessibility may require manual reauthorization after this code-identity change."
|
|
1966
|
+
elif [ "$identity_migration" -eq 1 ]; then
|
|
1967
|
+
echo "Installed a new signing identity; macOS will require one-time permission approval for this migration."
|
|
1968
|
+
else
|
|
1969
|
+
echo "Installed verified Recordings.app release artifact: ${APP_DEST}"
|
|
1970
|
+
fi
|