From 8bfab754e02a6028a2571c7d815a4d612876ac0d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:02:19 -0700 Subject: [PATCH] Add early macOS startup diagnostics (#4504) --- config/scripts/macos-launch-diagnostics.sh | 366 +++++++++++++++++++++ electron.vite.config.ts | 134 +++++++- 2 files changed, 499 insertions(+), 1 deletion(-) create mode 100755 config/scripts/macos-launch-diagnostics.sh diff --git a/config/scripts/macos-launch-diagnostics.sh b/config/scripts/macos-launch-diagnostics.sh new file mode 100755 index 00000000000..de209f894d9 --- /dev/null +++ b/config/scripts/macos-launch-diagnostics.sh @@ -0,0 +1,366 @@ +#!/usr/bin/env bash +# Capture one-shot macOS launch diagnostics for a published Orca release. +# +# Usage: +# ORCA_DIAGNOSTIC_TAG=v1.4.42-rc.1 bash config/scripts/macos-launch-diagnostics.sh +# bash config/scripts/macos-launch-diagnostics.sh --tag v1.4.42-rc.1 +set -euo pipefail + +REPO="${ORCA_DIAGNOSTIC_REPO:-stablyai/orca}" +TAG="${ORCA_DIAGNOSTIC_TAG:-}" +KEEP=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --tag) + TAG="${2:-}" + shift 2 + ;; + --repo) + REPO="${2:-}" + shift 2 + ;; + --keep) + KEEP=1 + shift + ;; + -h|--help) + cat <<'EOF' +Capture one-shot macOS launch diagnostics for a published Orca release. + +Usage: + ORCA_DIAGNOSTIC_TAG=v1.4.42-rc.1 bash config/scripts/macos-launch-diagnostics.sh + bash config/scripts/macos-launch-diagnostics.sh --tag v1.4.42-rc.1 +EOF + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + echo "Usage: $0 --tag vX.Y.Z-rc.N [--repo owner/repo] [--keep]" >&2 + exit 2 + ;; + esac +done + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "This diagnostic script only supports macOS." >&2 + exit 2 +fi + +if [[ -z "$TAG" ]]; then + echo "Set ORCA_DIAGNOSTIC_TAG or pass --tag vX.Y.Z-rc.N." >&2 + exit 2 +fi + +ARCH="$(uname -m)" +case "$ARCH" in + arm64) ASSET="orca-macos-arm64.dmg" ;; + x86_64) ASSET="orca-macos-x64.dmg" ;; + *) + echo "Unsupported macOS architecture: $ARCH" >&2 + exit 2 + ;; +esac + +TIMESTAMP="$(date -u '+%Y%m%dT%H%M%SZ')" +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/orca-launch-diagnostics.XXXXXXXX")" +OUT_DIR="$WORK_DIR/output" +MOUNT_DIR="$WORK_DIR/mount" +APP_DIR="$WORK_DIR/Orca.app" +DMG_PATH="$WORK_DIR/$ASSET" +mkdir -p "$OUT_DIR" "$MOUNT_DIR" + +if [[ -d "$HOME/Desktop" ]]; then + ZIP_PATH="$HOME/Desktop/orca-launch-diagnostics-${TAG}-${TIMESTAMP}.zip" +else + ZIP_PATH="$PWD/orca-launch-diagnostics-${TAG}-${TIMESTAMP}.zip" +fi + +log() { + printf '[orca-diagnostics] %s\n' "$*" +} + +run_capture() { + local name="$1" + shift + { + echo "\$ $*" + "$@" + } >"$OUT_DIR/$name.out" 2>"$OUT_DIR/$name.err" || { + local code=$? + echo "exit=$code" >>"$OUT_DIR/$name.err" + return 0 + } +} + +cleanup() { + if mount | grep -F " on $MOUNT_DIR " >/dev/null 2>&1; then + hdiutil detach "$MOUNT_DIR" -quiet >/dev/null 2>&1 || true + fi + if [[ "$KEEP" -eq 0 ]]; then + rm -rf "$WORK_DIR" + else + log "kept working directory: $WORK_DIR" + fi +} +trap cleanup EXIT + +write_environment_report() { + { + echo "tag=$TAG" + echo "repo=$REPO" + echo "asset=$ASSET" + echo "timestamp_utc=$TIMESTAMP" + echo + echo "## sw_vers" + sw_vers || true + echo + echo "## uname" + uname -a || true + echo + echo "## shell" + echo "$SHELL" + echo + echo "## current Orca processes before diagnostics" + pgrep -fl 'Orca|orca' || true + } >"$OUT_DIR/environment.txt" +} + +ensure_no_existing_orca() { + local existing + existing="$(pgrep -x Orca || true)" + if [[ -z "$existing" ]]; then + return 0 + fi + + { + echo "An Orca process is already running. Close Orca and run this script again." + echo + ps -p "$(printf '%s' "$existing" | paste -sd, -)" -o pid=,ppid=,command= || true + } | tee "$OUT_DIR/existing-orca-process.txt" >&2 + exit 2 +} + +download_and_copy_app() { + local url="https://github.com/$REPO/releases/download/$TAG/$ASSET" + log "downloading $url" + curl -fL --retry 3 --retry-delay 2 -o "$DMG_PATH" "$url" + shasum -a 256 "$DMG_PATH" >"$OUT_DIR/dmg.sha256" || true + + log "mounting DMG" + hdiutil attach "$DMG_PATH" -nobrowse -readonly -mountpoint "$MOUNT_DIR" >"$OUT_DIR/hdiutil-attach.txt" + local source_app + source_app="$(find "$MOUNT_DIR" -maxdepth 1 -name '*.app' -type d | head -n 1)" + if [[ -z "$source_app" ]]; then + echo "No .app bundle found in $DMG_PATH" >&2 + exit 1 + fi + + log "copying app to isolated temp path" + ditto "$source_app" "$APP_DIR" + hdiutil detach "$MOUNT_DIR" -quiet +} + +write_app_report() { + { + echo "app=$APP_DIR" + echo + echo "## Info.plist" + plutil -p "$APP_DIR/Contents/Info.plist" || true + echo + echo "## spctl" + spctl -a -vvv "$APP_DIR" || true + echo + echo "## codesign display" + codesign -dv --verbose=4 "$APP_DIR" || true + echo + echo "## codesign verify" + codesign --verify --strict --deep "$APP_DIR" || true + echo + echo "## xattr" + xattr -lr "$APP_DIR" || true + echo + echo "## native modules" + find "$APP_DIR/Contents/Resources" -name '*.node' -print || true + } >"$OUT_DIR/app-report.txt" 2>&1 +} + +start_log_stream() { + local file="$1" + local predicate='process == "Orca" OR eventMessage CONTAINS[c] "Orca" OR eventMessage CONTAINS[c] "com.stablyai.orca"' + if command -v log >/dev/null 2>&1; then + log stream --style compact --predicate "$predicate" >"$file" 2>&1 & + echo "$!" + else + echo "" + fi +} + +stop_log_stream() { + local pid="$1" + if [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1; then + kill "$pid" >/dev/null 2>&1 || true + wait "$pid" >/dev/null 2>&1 || true + fi +} + +latest_orca_pid_for_app() { + pgrep -nf "$APP_DIR/Contents/MacOS/Orca" || true +} + +sample_process_once() { + local pid="$1" + local file="$2" + if [[ -n "$pid" ]] && command -v sample >/dev/null 2>&1; then + sample "$pid" 1 -file "$file" >"$file.command.out" 2>"$file.command.err" || true + fi +} + +terminate_app_processes() { + local pid + pid="$(latest_orca_pid_for_app)" + if [[ -n "$pid" ]]; then + kill "$pid" >/dev/null 2>&1 || true + sleep 1 + if kill -0 "$pid" >/dev/null 2>&1; then + kill -9 "$pid" >/dev/null 2>&1 || true + fi + fi +} + +wait_for_probe() { + local runner_pid="$1" + local label="$2" + local sample_file="$OUT_DIR/$label.sample.txt" + local sampled=0 + local i + + for i in $(seq 1 200); do + local app_pid + app_pid="$(latest_orca_pid_for_app)" + if [[ "$sampled" -eq 0 && -n "$app_pid" ]]; then + sampled=1 + echo "sampled_pid=$app_pid" >>"$OUT_DIR/$label.meta" + sample_process_once "$app_pid" "$sample_file" + fi + + if ! kill -0 "$runner_pid" >/dev/null 2>&1; then + set +e + wait "$runner_pid" + local runner_exit=$? + set -e + echo "runner_exit=$runner_exit" >>"$OUT_DIR/$label.meta" + return 0 + fi + sleep 0.1 + done + + echo "runner_timeout_after_seconds=20" >>"$OUT_DIR/$label.meta" + terminate_app_processes + wait "$runner_pid" >/dev/null 2>&1 || true +} + +run_launchservices_probe() { + local label="$1" + local extra_env_name="${2:-}" + local extra_env_value="${3:-}" + local stderr_file="$OUT_DIR/$label.stderr.log" + local stdout_file="$OUT_DIR/$label.stdout.log" + local bootstrap_file="$OUT_DIR/$label.bootstrap.log" + local stream_file="$OUT_DIR/$label.system-stream.log" + local stream_pid + + log "running LaunchServices probe: $label" + stream_pid="$(start_log_stream "$stream_file")" + { + echo "label=$label" + echo "started_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + echo "bootstrap_file=$bootstrap_file" + [[ -n "$extra_env_name" ]] && echo "$extra_env_name=$extra_env_value" + } >"$OUT_DIR/$label.meta" + + if [[ -n "$extra_env_name" ]]; then + open -n -W \ + --stdout "$stdout_file" \ + --stderr "$stderr_file" \ + --env ELECTRON_ENABLE_LOGGING=1 \ + --env ORCA_STARTUP_DIAGNOSTICS=trace \ + --env ORCA_STARTUP_DIAGNOSTICS_TRACE_LIMIT=30000 \ + --env ORCA_STARTUP_DIAGNOSTICS_FILE="$bootstrap_file" \ + --env "$extra_env_name=$extra_env_value" \ + "$APP_DIR" & + else + open -n -W \ + --stdout "$stdout_file" \ + --stderr "$stderr_file" \ + --env ELECTRON_ENABLE_LOGGING=1 \ + --env ORCA_STARTUP_DIAGNOSTICS=trace \ + --env ORCA_STARTUP_DIAGNOSTICS_TRACE_LIMIT=30000 \ + --env ORCA_STARTUP_DIAGNOSTICS_FILE="$bootstrap_file" \ + "$APP_DIR" & + fi + local runner_pid="$!" + wait_for_probe "$runner_pid" "$label" + echo "ended_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >>"$OUT_DIR/$label.meta" + stop_log_stream "$stream_pid" +} + +run_direct_exec_probe() { + local label="direct-exec" + local stderr_file="$OUT_DIR/$label.stderr.log" + local stdout_file="$OUT_DIR/$label.stdout.log" + local bootstrap_file="$OUT_DIR/$label.bootstrap.log" + local stream_file="$OUT_DIR/$label.system-stream.log" + local stream_pid + + log "running direct exec probe" + stream_pid="$(start_log_stream "$stream_file")" + { + echo "label=$label" + echo "started_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + echo "bootstrap_file=$bootstrap_file" + } >"$OUT_DIR/$label.meta" + + ELECTRON_ENABLE_LOGGING=1 \ + ORCA_STARTUP_DIAGNOSTICS=trace \ + ORCA_STARTUP_DIAGNOSTICS_TRACE_LIMIT=30000 \ + ORCA_STARTUP_DIAGNOSTICS_FILE="$bootstrap_file" \ + "$APP_DIR/Contents/MacOS/Orca" >"$stdout_file" 2>"$stderr_file" & + local runner_pid="$!" + wait_for_probe "$runner_pid" "$label" + echo "ended_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >>"$OUT_DIR/$label.meta" + stop_log_stream "$stream_pid" +} + +write_system_log_snapshot() { + local predicate='process == "Orca" OR eventMessage CONTAINS[c] "Orca" OR eventMessage CONTAINS[c] "com.stablyai.orca"' + if command -v log >/dev/null 2>&1; then + log "capturing recent unified log snapshot" + log show --style syslog --last 10m --predicate "$predicate" >"$OUT_DIR/system-log-last-10m.log" 2>&1 || true + fi +} + +package_results() { + { + echo "Diagnostics completed." + echo "Tag: $TAG" + echo "Output zip: $ZIP_PATH" + echo + echo "Please attach this zip to the GitHub issue:" + echo " $ZIP_PATH" + } >"$OUT_DIR/README.txt" + + log "creating zip" + (cd "$OUT_DIR" && zip -qry "$ZIP_PATH" .) + log "wrote $ZIP_PATH" +} + +write_environment_report +ensure_no_existing_orca +download_and_copy_app +write_app_report +run_launchservices_probe "launchservices-trace" +run_launchservices_probe "launchservices-bypass-lock" "ORCA_BYPASS_SINGLE_INSTANCE_LOCK" "1" +run_direct_exec_probe +write_system_log_snapshot +package_results diff --git a/electron.vite.config.ts b/electron.vite.config.ts index c715965c40d..b8a66ca2f5e 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -33,6 +33,137 @@ const ORCA_DIAGNOSTICS_TOKEN_URL_LITERAL = ? JSON.stringify(orcaDiagnosticsTokenUrl) : 'null' +function createStartupDiagnosticsBanner(chunkName: string): string { + return ` +;(() => { + const env = typeof process !== 'undefined' ? process.env : undefined + const mode = env?.ORCA_STARTUP_DIAGNOSTICS + if (mode !== '1' && mode !== 'trace') { + return + } + const safeJson = (value) => { + try { + return JSON.stringify(value) + } catch { + return '""' + } + } + let closeSync + let diagnosticFileDescriptor + let openSync + let writeSync + try { + const fs = require('node:fs') + closeSync = fs.closeSync + openSync = fs.openSync + writeSync = fs.writeSync + } catch { + closeSync = undefined + openSync = undefined + writeSync = undefined + } + const diagnosticFile = env?.ORCA_STARTUP_DIAGNOSTICS_FILE + if (typeof diagnosticFile === 'string' && diagnosticFile.length > 0 && typeof openSync === 'function') { + try { + diagnosticFileDescriptor = openSync(diagnosticFile, 'a', 0o600) + } catch { + diagnosticFileDescriptor = undefined + } + } + const writeLine = (message) => { + try { + const line = message.endsWith('\\n') ? message : message + '\\n' + if (typeof writeSync === 'function') { + writeSync(2, line) + if (typeof diagnosticFileDescriptor === 'number') { + writeSync(diagnosticFileDescriptor, line) + } + } + } catch { + // Diagnostics must never affect startup. + } + } + const chunkName = ${JSON.stringify(chunkName)} + writeLine('[bootstrap] bundle-enter chunk=' + safeJson(chunkName) + ' pid=' + process.pid + ' ppid=' + process.ppid + ' execPath=' + safeJson(process.execPath) + ' argv=' + safeJson(process.argv) + ' electronRunAsNode=' + safeJson(env?.ELECTRON_RUN_AS_NODE ?? null)) + if (!globalThis.__ORCA_BOOTSTRAP_EXIT_LOG_INSTALLED__) { + globalThis.__ORCA_BOOTSTRAP_EXIT_LOG_INSTALLED__ = true + process.once('exit', (code) => { + writeLine('[bootstrap] process-exit code=' + code) + if (typeof closeSync === 'function' && typeof diagnosticFileDescriptor === 'number') { + try { + closeSync(diagnosticFileDescriptor) + } catch { + // Diagnostics must never affect shutdown. + } + } + }) + process.on('uncaughtExceptionMonitor', (error, origin) => { + const message = error && typeof error === 'object' && 'stack' in error ? error.stack : error + writeLine('[bootstrap] uncaught-exception origin=' + safeJson(origin) + ' error=' + safeJson(String(message))) + }) + process.on('unhandledRejection', (reason) => { + const message = reason && typeof reason === 'object' && 'stack' in reason ? reason.stack : reason + writeLine('[bootstrap] unhandled-rejection error=' + safeJson(String(message))) + }) + } + if (mode === 'trace' && !globalThis.__ORCA_BOOTSTRAP_REQUIRE_TRACE_INSTALLED__) { + globalThis.__ORCA_BOOTSTRAP_REQUIRE_TRACE_INSTALLED__ = true + try { + const Module = require('node:module') + const originalLoad = Module._load + const parsedTraceLimit = Number(env?.ORCA_STARTUP_DIAGNOSTICS_TRACE_LIMIT ?? 20000) + const traceLimit = Number.isFinite(parsedTraceLimit) && parsedTraceLimit > 0 ? parsedTraceLimit : 20000 + let traceLineCount = 0 + let traceLimitReported = false + const writeTraceLine = (message) => { + if (traceLineCount >= traceLimit) { + if (!traceLimitReported) { + traceLimitReported = true + writeLine('[bootstrap] require-trace-limit-reached limit=' + safeJson(traceLimit)) + } + return + } + traceLineCount += 1 + writeLine(message) + } + Module._load = function (request, parent, isMain) { + const parentName = parent && parent.filename ? parent.filename : null + writeTraceLine('[bootstrap] require-start request=' + safeJson(request) + ' parent=' + safeJson(parentName) + ' isMain=' + safeJson(Boolean(isMain))) + try { + const result = Reflect.apply(originalLoad, this, arguments) + writeTraceLine('[bootstrap] require-ok request=' + safeJson(request)) + return result + } catch (error) { + const message = error && typeof error === 'object' && 'stack' in error ? error.stack : error + writeTraceLine('[bootstrap] require-error request=' + safeJson(request) + ' error=' + safeJson(String(message))) + throw error + } + } + } catch (error) { + writeLine('[bootstrap] require-trace-install-error error=' + safeJson(String(error))) + } + } +})(); +` +} + +function createStartupDiagnosticsBootstrapPlugin() { + return { + name: 'orca-startup-diagnostics-bootstrap', + generateBundle(_options, bundle) { + const mainChunk = bundle['index.js'] + if (!mainChunk || mainChunk.type !== 'chunk') { + return + } + + // Why: source-level startup diagnostics run after Rollup's generated + // prelude and require() list. Mutate the final emitted chunk so macOS + // launch failures can identify the earliest JS boundary reached. + mainChunk.code = createStartupDiagnosticsBanner(mainChunk.fileName) + mainChunk.code + } + } +} + export default defineConfig({ main: { build: { @@ -54,7 +185,8 @@ export default defineConfig({ 'agent-hooks/managed-agent-hook-controls': resolve( 'src/main/agent-hooks/managed-agent-hook-controls.ts' ) - } + }, + plugins: [createStartupDiagnosticsBootstrapPlugin()] } }, // Why: compile-time substitution for the telemetry gate. See the block