mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
516 lines
21 KiB
TypeScript
516 lines
21 KiB
TypeScript
import {
|
|
APP_KEYS,
|
|
SIDECAR_MESSAGES,
|
|
SIDECAR_MODES,
|
|
SIDECAR_SOURCES,
|
|
type SidecarSource,
|
|
} from "@open-design/sidecar-proto";
|
|
import {
|
|
parseLauncherAfterQuitArgs,
|
|
parseLauncherDelegatedArgs,
|
|
parseLauncherHandoffResumeArgs,
|
|
} from "@open-design/launcher-proto";
|
|
import {
|
|
bootstrapSidecarProcess,
|
|
isCurrentSidecarLauncher,
|
|
readCurrentSidecarStamp,
|
|
registerSidecarProcess,
|
|
resolveSidecarLauncherExitCode,
|
|
SidecarFactory,
|
|
type SidecarClient,
|
|
type SidecarRuntimeContext,
|
|
type SidecarStamp,
|
|
} from "@open-design/sidecar";
|
|
import {
|
|
recordIncomingUpdateLifecycle,
|
|
applyLoopbackConnectionLimitSwitch,
|
|
applyOsLocaleSwitch,
|
|
createSplashWindow,
|
|
setSplashStage,
|
|
type DesktopMainHandle,
|
|
} from "@open-design/desktop/main";
|
|
import { releaseChannelFromNamespace, releaseChannelFromVersion } from "@open-design/release";
|
|
import { join } from "node:path";
|
|
import { app, dialog } from "electron";
|
|
|
|
import { readPackagedConfig } from "./config.js";
|
|
import {
|
|
claimPackagedDownloadAttribution,
|
|
discoverPackagedDownloadAttribution,
|
|
} from "./download-attribution.js";
|
|
import {
|
|
parsePackagedHeadlessRequest,
|
|
resolvePackagedMcpBootstrapLaunch,
|
|
runPackagedMcpActionAgainstExistingDaemon,
|
|
} from "./headless-runtime.js";
|
|
import { PackagedPathAccessError } from "./errors.js";
|
|
import {
|
|
exitPackagedLauncherForExistingDesktop,
|
|
findExistingPackagedDesktopOwner,
|
|
inspectExistingDesktopForLauncher,
|
|
waitForLauncherAfterQuit,
|
|
} from "./launcher-after-quit.js";
|
|
import { confirmPackagedLauncherRuntime, resolvePackagedLauncherRuntime } from "./launcher-runtime.js";
|
|
import {
|
|
applyPackagedElectronPathOverrides,
|
|
claimPackagedSingleInstanceLock,
|
|
createPackagedSecondInstanceHandoff,
|
|
ensurePackagedNamespacePaths,
|
|
stabilizePackagedWorkingDirectory,
|
|
} from "./launch.js";
|
|
import {
|
|
attachPackagedDesktopProcessLogging,
|
|
createPackagedDesktopLogger,
|
|
type PackagedDesktopLogger,
|
|
} from "./logging.js";
|
|
import { resolvePackagedNamespacePaths } from "./paths.js";
|
|
import { createObsoleteInstalledOuterRetirement } from "./obsolete-installed-outer.js";
|
|
import { findPackagedDeeplinkArg, launchPackagedPayloadDesktop } from "./payload-desktop-launch.js";
|
|
import { packagedEntryUrl, registerOdProtocol } from "./protocol.js";
|
|
import { startPackagedSidecars } from "./sidecars.js";
|
|
import { reportStartupFailure, resolveStartupDistinctId } from "./startup-telemetry.js";
|
|
import { resolvePackagedWindowTitle } from "./window-title.js";
|
|
import { syncWindowsUninstallDisplayVersion } from "./windows-lifecycle.js";
|
|
|
|
let packagedLogger: PackagedDesktopLogger | null = null;
|
|
const secondInstanceHandoff = createPackagedSecondInstanceHandoff();
|
|
|
|
// Telemetry context for the fatal-exit path. Populated once config + launcher
|
|
// runtime are resolved so the `main().catch` below can report a startup failure
|
|
// even though the daemon (the PostHog host) never came up. Null until then —
|
|
// failures earlier than config resolution simply skip telemetry. See
|
|
// `startup-telemetry.ts` for the zero-startup-side-effect contract.
|
|
let startupTelemetryContext:
|
|
| {
|
|
posthogKey: string | null;
|
|
posthogHost: string | null;
|
|
appVersion: string | null;
|
|
namespace: string;
|
|
source: string;
|
|
installationRoot: string;
|
|
nativeModulePath: string | null;
|
|
}
|
|
| null = null;
|
|
|
|
function applyPackagedUpdaterEnv(updateMetadataUrl: string | null): void {
|
|
if (updateMetadataUrl == null) return;
|
|
if (process.env.OD_UPDATE_METADATA_URL != null && process.env.OD_UPDATE_METADATA_URL.length > 0) return;
|
|
process.env.OD_UPDATE_METADATA_URL = updateMetadataUrl;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const config = await readPackagedConfig();
|
|
const headlessRequest = parsePackagedHeadlessRequest(process.argv.slice(1));
|
|
|
|
// Must run BEFORE `app.whenReady()` below, because Chromium consumes
|
|
// `--lang` at session bootstrap. Doing it here lets the packaged
|
|
// renderer's `navigator.language` follow the OS instead of Chromium's
|
|
// en-US default. runDesktopMain (called later) calls the same helper
|
|
// again to recover the resolved locale string for the BrowserWindow.
|
|
applyOsLocaleSwitch(app);
|
|
// Must also land before whenReady — see the helper's docblock for the
|
|
// connection-pool deadlock it prevents (electron/electron#47097).
|
|
applyLoopbackConnectionLimitSwitch(app);
|
|
// Belt-and-braces duplicate of the helper above: the packaged outer
|
|
// shell can outlive auto-updates that only refresh inner resources, so
|
|
// the deadlock fix must not depend on which desktop build the shell
|
|
// happens to bundle. appendSwitch is idempotent for the same key.
|
|
app.commandLine.appendSwitch("ignore-connections-limit", "127.0.0.1,localhost");
|
|
// 关掉内层滚动容器的橡皮筋回弹(产品裁决 2026-09-07:「直接关掉」)。
|
|
//
|
|
// ## 为什么
|
|
//
|
|
// Electron 40 → 41 把 Chromium 从 144 跳到 **146,整个跳过了 145**,而
|
|
// `kOverscrollEffectOnNonRootScrollers` 的默认值正好在 145 从 DISABLED 翻成
|
|
// ENABLED(已拉 branch-heads/7559 与 7680 的 `cc/base/features.cc` 逐字核实)。
|
|
// 它管的是「非根滚动容器撞到滚动边界时怎么表现」——145 之前只有整页会弹,
|
|
// 之后聊天区这类内层容器也会弹。
|
|
//
|
|
// 我们在追的缺陷是:聊天区的滚动范围被**永久冻**在某个早期内容高度上,
|
|
// 布局全对、JS 程序性滚动能到底,但**滚轮和键盘都到不了**(scroll unification
|
|
// 之后两者都走合成器)。位置(滚动边界)、平台(macOS 弹性 overscroll)、
|
|
// 版本窗口三样都对得上。
|
|
//
|
|
// ⚠️ **这是缓解不是根治**:合成页面 89 个用例没能复现,因果链没有建立。
|
|
// 判据仍然是 `client_chat_scroll_frozen` 的事件量 —— 带着这一行还在报,
|
|
// 说明这条线错了,该把这两个 feature 放回去再找别的。
|
|
//
|
|
// ## 代价
|
|
//
|
|
// macOS 上所有内层滚动区失去橡皮筋回弹(整页仍然弹)。产品知情并选择了它 ——
|
|
// 相对「滚不动」这个代价可以接受。
|
|
//
|
|
// 必须在 whenReady 之前:Chromium 在会话初始化时就消费这些开关。
|
|
app.commandLine.appendSwitch(
|
|
"disable-features",
|
|
"OverscrollEffectOnNonRootScrollers,OverscrollBehaviorRespectedOnAllScrollContainers",
|
|
);
|
|
|
|
const afterQuit = parseLauncherAfterQuitArgs(process.argv.slice(1));
|
|
const handoffResume = parseLauncherHandoffResumeArgs(process.argv.slice(1));
|
|
const delegated = parseLauncherDelegatedArgs(process.argv.slice(1));
|
|
const convergedArgvStamp = (() => {
|
|
try { return readCurrentSidecarStamp(); } catch { return null; }
|
|
})();
|
|
const namespace = convergedArgvStamp?.namespace ?? config.namespace;
|
|
const namespaceConfig = namespace === config.namespace ? config : { ...config, namespace };
|
|
const initialPaths = resolvePackagedNamespacePaths(namespaceConfig, namespace, process.env);
|
|
const launchStamp: SidecarStamp = {
|
|
app: APP_KEYS.DESKTOP,
|
|
channel: convergedArgvStamp?.channel
|
|
?? releaseChannelFromVersion(namespaceConfig.appVersion)
|
|
?? releaseChannelFromNamespace(namespace, "default")
|
|
?? "stable",
|
|
mode: headlessRequest.headless ? "headless" : SIDECAR_MODES.RUNTIME,
|
|
namespace,
|
|
source: convergedArgvStamp?.source ?? SIDECAR_SOURCES.PACKAGED,
|
|
};
|
|
if (await runPackagedMcpActionAgainstExistingDaemon(headlessRequest, launchStamp)) {
|
|
app.exit(0);
|
|
return;
|
|
}
|
|
if (headlessRequest.mcpInstallAgent != null) {
|
|
const existingOwner = await findExistingPackagedDesktopOwner(launchStamp, {
|
|
modes: [SIDECAR_MODES.RUNTIME, "headless"],
|
|
});
|
|
if (existingOwner != null) {
|
|
throw new Error(
|
|
`Cannot install MCP while the existing ${existingOwner.stamp.mode} desktop runtime has no healthy daemon. Quit Open Design and retry.`,
|
|
);
|
|
}
|
|
}
|
|
// An updater successor must outlive its predecessor before discovering or
|
|
// bootstrapping a desktop in the same namespace. Otherwise it can focus
|
|
// the quitting predecessor and exit as an ordinary duplicate launch.
|
|
const incomingObservation = { root: initialPaths.installerObservationRoot, namespace,
|
|
channel: launchStamp.channel, version: namespaceConfig.appVersion };
|
|
if (!headlessRequest.headless && afterQuit != null) {
|
|
await recordIncomingUpdateLifecycle(incomingObservation, { stage: "predecessor_wait_started", outcome: "started" });
|
|
}
|
|
if (!headlessRequest.headless && !await waitForLauncherAfterQuit(afterQuit, initialPaths, console, {},
|
|
afterQuit == null ? undefined : (event) => recordIncomingUpdateLifecycle(incomingObservation, event))) {
|
|
app.exit(1);
|
|
return;
|
|
}
|
|
const oppositeDesktop = await inspectExistingDesktopForLauncher(launchStamp, {
|
|
deeplinkUrl: findPackagedDeeplinkArg(process.argv),
|
|
logger: console,
|
|
modes: [
|
|
headlessRequest.headless ? SIDECAR_MODES.RUNTIME : "headless",
|
|
...(
|
|
convergedArgvStamp == null || isCurrentSidecarLauncher()
|
|
? [launchStamp.mode]
|
|
: []
|
|
),
|
|
],
|
|
paths: initialPaths,
|
|
});
|
|
if (exitPackagedLauncherForExistingDesktop(oppositeDesktop, (code) => app.exit(code))) {
|
|
return;
|
|
}
|
|
if (await bootstrapSidecarProcess(launchStamp, {
|
|
dataRoot: initialPaths.dataRoot,
|
|
ownerPid: null,
|
|
port: 0,
|
|
runtimeRoot: initialPaths.runtimeRoot,
|
|
})) {
|
|
app.exit(0);
|
|
return;
|
|
}
|
|
const existingDesktop = await inspectExistingDesktopForLauncher(launchStamp, {
|
|
deeplinkUrl: findPackagedDeeplinkArg(process.argv),
|
|
incomingVersion: namespaceConfig.appVersion,
|
|
logger: console,
|
|
paths: initialPaths,
|
|
});
|
|
if (exitPackagedLauncherForExistingDesktop(existingDesktop, (code) => app.exit(code))) {
|
|
return;
|
|
}
|
|
if (headlessRequest.headless) {
|
|
const { runPackagedHeadless } = await import("./headless-runtime.js");
|
|
await runPackagedHeadless(config, headlessRequest);
|
|
return;
|
|
}
|
|
const launcherRuntime = await resolvePackagedLauncherRuntime(namespaceConfig, initialPaths, {
|
|
delegated,
|
|
resume: handoffResume,
|
|
});
|
|
if (await launchPackagedPayloadDesktop(launcherRuntime)) {
|
|
app.exit(0);
|
|
return;
|
|
}
|
|
const activeConfig = launcherRuntime.config;
|
|
const paths = launcherRuntime.paths;
|
|
const mcpBootstrap = resolvePackagedMcpBootstrapLaunch({
|
|
installedLaunchPath: launcherRuntime.installedLaunchPath,
|
|
});
|
|
|
|
// Arm fatal-exit telemetry now that we know the channel key/version. The
|
|
// startPackagedSidecars call below is THE failure this covers (daemon/web
|
|
// dying before reporting status, e.g. issue #4638's missing better-sqlite3).
|
|
startupTelemetryContext = {
|
|
posthogKey: activeConfig.posthogKey,
|
|
posthogHost: activeConfig.posthogHost,
|
|
appVersion: activeConfig.appVersion,
|
|
namespace,
|
|
source: convergedArgvStamp?.source ?? SIDECAR_SOURCES.PACKAGED,
|
|
// Pass installationRoot explicitly: OD_INSTALLATION_DIR is only set in the
|
|
// daemon child env, not this parent process (see startup-telemetry.ts).
|
|
installationRoot: paths.installationRoot,
|
|
// Absolute path where the daemon's better-sqlite3 binding ships in the
|
|
// packaged bundle (`Contents/Resources/app/node_modules/...` — layout
|
|
// verified against the shipped 0.13.0 DMG). The fatal-exit report probes
|
|
// this to record whether the .node actually exists on the crashing machine.
|
|
nativeModulePath: join(
|
|
app.getAppPath(),
|
|
"node_modules",
|
|
"better-sqlite3",
|
|
"build",
|
|
"Release",
|
|
"better_sqlite3.node",
|
|
),
|
|
};
|
|
|
|
await ensurePackagedNamespacePaths(paths);
|
|
const convergedStamp = launchStamp;
|
|
registerSidecarProcess(convergedStamp, {
|
|
dataRoot: paths.dataRoot,
|
|
ownerPid: null,
|
|
port: 0,
|
|
runtimeRoot: paths.runtimeRoot,
|
|
});
|
|
stabilizePackagedWorkingDirectory(paths);
|
|
const downloadAttribution = await discoverPackagedDownloadAttribution(paths, console).catch((error: unknown) => {
|
|
console.warn("[attribution] failed to discover packaged download attribution", error);
|
|
return null;
|
|
});
|
|
packagedLogger = createPackagedDesktopLogger(paths);
|
|
attachPackagedDesktopProcessLogging({ logger: packagedLogger, paths, stamp: convergedStamp });
|
|
const retireObsoleteInstalledOuter = createObsoleteInstalledOuterRetirement({
|
|
currentExecutablePath: process.execPath,
|
|
currentPid: process.pid,
|
|
installedLaunchPath: launcherRuntime.installedLaunchPath,
|
|
logger: packagedLogger,
|
|
payloadDesktopProcess: launcherRuntime.payloadDesktopProcess,
|
|
payloadExecutablePath: launcherRuntime.desktopExecutablePath,
|
|
platform: process.platform,
|
|
});
|
|
applyPackagedElectronPathOverrides(paths);
|
|
applyPackagedUpdaterEnv(activeConfig.updateMetadataUrl);
|
|
if (!claimPackagedSingleInstanceLock(app, (argv) => {
|
|
secondInstanceHandoff.handle(findPackagedDeeplinkArg(argv));
|
|
})) {
|
|
return;
|
|
}
|
|
await app.whenReady();
|
|
|
|
// Show the brand splash IMMEDIATELY, before we await the daemon/web sidecars
|
|
// below. Cold boot otherwise leaves the user staring at no window at all for
|
|
// the few seconds the sidecars take to come up; putting the animation on
|
|
// screen in parallel masks that gap, and the runtime keeps it up until the
|
|
// real app has mounted (see createDesktopRuntime). The handle carries the
|
|
// creation timestamp so the runtime's minimum-hold timer counts from here —
|
|
// BEFORE the sidecar boot below — rather than re-adding the delay afterwards.
|
|
const splash = createSplashWindow();
|
|
|
|
const runtime = {
|
|
app: APP_KEYS.DESKTOP,
|
|
base: paths.runtimeRoot,
|
|
mode: SIDECAR_MODES.RUNTIME,
|
|
namespace,
|
|
source: convergedStamp.source as SidecarSource,
|
|
} satisfies SidecarRuntimeContext<SidecarStamp>;
|
|
|
|
const sidecars = await startPackagedSidecars(runtime, paths, {
|
|
appVersion: activeConfig.appVersion,
|
|
amrProfile: activeConfig.amrProfile,
|
|
daemonCliEntry: activeConfig.daemonCliEntry,
|
|
daemonSidecarEntry: activeConfig.daemonSidecarEntry,
|
|
electronNodeCommand: launcherRuntime.electronNodeCommand,
|
|
mcpBootstrapArgs: mcpBootstrap.args,
|
|
mcpBootstrapCommand: mcpBootstrap.command,
|
|
nodeCommand: activeConfig.nodeCommand,
|
|
telemetryRelayUrl: activeConfig.telemetryRelayUrl,
|
|
posthogKey: activeConfig.posthogKey,
|
|
posthogHost: activeConfig.posthogHost,
|
|
velaWebUrl: activeConfig.velaWebUrl,
|
|
velaWebUrls: activeConfig.velaWebUrls,
|
|
// PR #974 round-5 (lefarcen P2): the Electron entry runs desktop
|
|
// main alongside the daemon, so the import-folder gate must be
|
|
// pinned ON from request 0. See `apps/packaged/src/headless-runtime.ts`
|
|
// for the windowless counterpart that passes `false`.
|
|
requireDesktopAuth: true,
|
|
webSidecarEntry: activeConfig.webSidecarEntry,
|
|
webStandaloneRoot: activeConfig.webStandaloneRoot,
|
|
webOutputMode: activeConfig.webOutputMode,
|
|
// Surface each sidecar boot phase on the splash status line so a slow
|
|
// cold start (Defender scans, native module loads) never reads as a hang.
|
|
// Both the "spawning" and "ready" edges are mapped so the step counter
|
|
// advances the instant each long native wait clears.
|
|
onPhase(phase) {
|
|
const stage =
|
|
phase === "daemon-spawning"
|
|
? "engine"
|
|
: phase === "daemon-ready"
|
|
? "engineReady"
|
|
: phase === "web-spawning"
|
|
? "interface"
|
|
: "interfaceReady";
|
|
setSplashStage(splash.window, stage);
|
|
},
|
|
});
|
|
if (sidecars.daemon.url) {
|
|
void claimPackagedDownloadAttribution({
|
|
attribution: downloadAttribution,
|
|
daemonUrl: sidecars.daemon.url,
|
|
installerObservationRoot: paths.installerObservationRoot,
|
|
logger: packagedLogger,
|
|
});
|
|
}
|
|
// Sidecars are up; the remaining wait is the hidden main window loading and
|
|
// mounting the web bundle (the runtime re-asserts this stage at its reveal
|
|
// gate, which is a no-op when the label is already current).
|
|
setSplashStage(splash.window, "workspace");
|
|
// Resolve the web sidecar address per request instead of freezing it here.
|
|
// The restart supervisor may bind a fresh ephemeral port, while a temporary
|
|
// lack of a target should surface as the protocol layer's structured 503.
|
|
const odProtocol = registerOdProtocol(() => sidecars.currentWebUrl());
|
|
|
|
const { runDesktopMain } = await import("@open-design/desktop/main");
|
|
let desktopHandle: DesktopMainHandle | null = null;
|
|
const invokeDesktop = async (action: string, input: unknown) => {
|
|
if (desktopHandle == null) throw new Error("packaged desktop sidecar is not running");
|
|
return await desktopHandle.invoke(action, input);
|
|
};
|
|
let client!: SidecarClient<DesktopMainHandle>;
|
|
client = SidecarFactory.create<DesktopMainHandle>({
|
|
handlers: Object.fromEntries([
|
|
SIDECAR_MESSAGES.CLICK,
|
|
SIDECAR_MESSAGES.CONSOLE,
|
|
SIDECAR_MESSAGES.EVAL,
|
|
SIDECAR_MESSAGES.EXPORT_ARTIFACT,
|
|
SIDECAR_MESSAGES.EXPORT_PDF,
|
|
SIDECAR_MESSAGES.RENDER_FRAMES,
|
|
SIDECAR_MESSAGES.RENDER_SLIDES,
|
|
SIDECAR_MESSAGES.SCREENSHOT,
|
|
SIDECAR_MESSAGES.SHOW,
|
|
SIDECAR_MESSAGES.UPDATE,
|
|
].map((action) => [action, (input: unknown) => invokeDesktop(action, input)])),
|
|
lifecycle: {
|
|
async start() {
|
|
const started = await runDesktopMain(runtime, {
|
|
splashWindow: splash.window,
|
|
splashStartedAt: splash.startedAt,
|
|
async beforeShutdown(record) {
|
|
try {
|
|
await retireObsoleteInstalledOuter();
|
|
} finally {
|
|
await sidecars.close(record);
|
|
}
|
|
},
|
|
quiesceRendererTransport() {
|
|
odProtocol.quiesce();
|
|
},
|
|
async discoverWebUrl() {
|
|
return packagedEntryUrl();
|
|
},
|
|
// Round-7 (lefarcen P2 @ runtime.ts:336): packaged main-process
|
|
// fetch targets the daemon sidecar's real http URL — never the
|
|
// od://app/ renderer URL, which Node/undici cannot resolve through
|
|
// Electron's protocol handler.
|
|
async discoverDaemonUrl() {
|
|
return sidecars.daemon.url;
|
|
},
|
|
registerDesktopAuth: async (secret) => {
|
|
try {
|
|
const result = await client.invoke<{ accepted: true }>(
|
|
APP_KEYS.DAEMON,
|
|
"register-desktop-auth",
|
|
{ secret: secret.toString("base64") },
|
|
{ timeoutMs: 800 },
|
|
);
|
|
return result.accepted === true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
windowTitle: resolvePackagedWindowTitle(activeConfig),
|
|
inviteProtocolClientPath:
|
|
process.platform === "win32" ? launcherRuntime.installedLaunchPath : null,
|
|
async onExternalShow() {
|
|
await retireObsoleteInstalledOuter();
|
|
},
|
|
onDesktopReady(controls) {
|
|
void confirmPackagedLauncherRuntime(launcherRuntime).catch((error: unknown) => {
|
|
packagedLogger?.warn("failed to confirm packaged launcher runtime", { error });
|
|
});
|
|
void syncWindowsUninstallDisplayVersion({
|
|
namespace,
|
|
version: launcherRuntime.config.appVersion,
|
|
}).catch((error: unknown) => {
|
|
packagedLogger?.warn("failed to sync Windows uninstall registry version", { error });
|
|
});
|
|
secondInstanceHandoff.attach({
|
|
dispatchDeeplink: controls.dispatchInviteDeeplink,
|
|
show: controls.show,
|
|
});
|
|
},
|
|
preloadPath: join(app.getAppPath(), "preload.cjs"),
|
|
update: {
|
|
currentVersion: activeConfig.appVersion,
|
|
downloadRoot: paths.updateRoot,
|
|
installerObservationRoot: paths.installerObservationRoot,
|
|
launcherLaunchPath: launcherRuntime.installedLaunchPath,
|
|
launcherRoot: launcherRuntime.launcherPaths.root,
|
|
launcherPayloadExtractorPath: activeConfig.resourceRoot == null ? null : join(activeConfig.resourceRoot, "bin", "7z.exe"),
|
|
launcherRuntimePath: launcherRuntime.launcherPaths.runtimePath,
|
|
},
|
|
});
|
|
desktopHandle = started;
|
|
return started;
|
|
},
|
|
status: (started) => started.status(),
|
|
async stop(started) {
|
|
await started.stop();
|
|
desktopHandle = null;
|
|
},
|
|
},
|
|
});
|
|
await client.start();
|
|
}
|
|
|
|
void main().catch(async (error: unknown) => {
|
|
const isPathAccess = error instanceof PackagedPathAccessError;
|
|
if (isPathAccess) {
|
|
try {
|
|
dialog.showErrorBox(error.title, error.message);
|
|
} catch {
|
|
// Fall through to console logging + process exit.
|
|
}
|
|
}
|
|
packagedLogger?.error("packaged runtime failed", { error });
|
|
console.error("packaged runtime failed", error);
|
|
// Best-effort crash telemetry on the way out. This is the ONLY new behavior
|
|
// on the failure path; the happy path never reaches here. reportStartupFailure
|
|
// self-caps its runtime (Promise.race timeout) and swallows all errors, so it
|
|
// can neither block nor crash the exit. No-op when telemetry isn't armed yet
|
|
// or the build has no PostHog key.
|
|
if (startupTelemetryContext) {
|
|
await reportStartupFailure({
|
|
error,
|
|
isPathAccess,
|
|
posthogKey: startupTelemetryContext.posthogKey,
|
|
posthogHost: startupTelemetryContext.posthogHost,
|
|
distinctId: resolveStartupDistinctId(
|
|
startupTelemetryContext.namespace,
|
|
startupTelemetryContext.installationRoot,
|
|
),
|
|
appVersion: startupTelemetryContext.appVersion,
|
|
namespace: startupTelemetryContext.namespace,
|
|
source: startupTelemetryContext.source,
|
|
nativeModulePath: startupTelemetryContext.nativeModulePath,
|
|
});
|
|
}
|
|
process.exit(resolveSidecarLauncherExitCode(error));
|
|
});
|