Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65e6323a8c | ||
|
|
0085017635 | ||
|
|
e810c168f8 | ||
|
|
b2e2a54e73 |
No files matched your search
@@ -103,6 +103,7 @@ import { createSourceChangeHandler } from "./source-change-handler";
|
||||
import { SourceDirectoryPicker } from "./source-directory-picker";
|
||||
import {
|
||||
installBundledCliIfNeeded,
|
||||
installServiceWithDisabledFallback,
|
||||
isInstalledApplicationsApp,
|
||||
stableRuntimeErrorCode,
|
||||
startStableRuntimeRepair,
|
||||
@@ -989,6 +990,7 @@ async function initialize(isQuitting: () => boolean): Promise<void> {
|
||||
const startAtLogin = owner?.owner === "app" ? owner.start_at_login : true;
|
||||
const previousOwner = owner;
|
||||
let runtimeChanged = false;
|
||||
let transientRuntime = false;
|
||||
if (owner?.owner !== "cli") {
|
||||
const installed = await installPackagedRuntime({
|
||||
resourcesPath: process.resourcesPath,
|
||||
@@ -1027,25 +1029,48 @@ async function initialize(isQuitting: () => boolean): Promise<void> {
|
||||
});
|
||||
}
|
||||
if (!alreadyRunning) {
|
||||
try {
|
||||
await installUserService(layout, {
|
||||
uid: process.getuid?.(),
|
||||
platform: "darwin",
|
||||
});
|
||||
await waitForRuntimeProcess({ dataDir, attempts: 360 });
|
||||
} catch (error) {
|
||||
if (owner?.owner === "app") {
|
||||
if (previousOwner === null)
|
||||
await removeRuntimeServiceOwner(dataDir);
|
||||
else await writeRuntimeServiceOwner(dataDir, previousOwner);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
// A durable `launchctl disable` override is a state to report, not a
|
||||
// failure to undo: `installUserService` never re-enables a service the
|
||||
// user or the system disabled. An explicit App launch may still own a
|
||||
// transient Runtime for this UI session. Shutdown stops that process.
|
||||
const serviceMode = await installServiceWithDisabledFallback({
|
||||
install: async () => {
|
||||
await installUserService(layout, {
|
||||
uid: process.getuid?.(),
|
||||
platform: "darwin",
|
||||
});
|
||||
await waitForRuntimeProcess({ dataDir, attempts: 360 });
|
||||
},
|
||||
restoreOwner: async () => {
|
||||
if (owner?.owner === "app") {
|
||||
if (previousOwner === null)
|
||||
await removeRuntimeServiceOwner(dataDir);
|
||||
else await writeRuntimeServiceOwner(dataDir, previousOwner);
|
||||
}
|
||||
owner = previousOwner;
|
||||
},
|
||||
reportDisabled: () =>
|
||||
diagnostics.log({
|
||||
level: "warn",
|
||||
component: "desktop.stable_runtime",
|
||||
event: "stable_runtime.service_disabled",
|
||||
outcome: "degraded",
|
||||
error_code: "runtime_service_disabled_by_system",
|
||||
}),
|
||||
startTransient: async () => {
|
||||
await ensureRuntimeProcess({
|
||||
dataDir,
|
||||
launch: runtimeLaunch(),
|
||||
attempts: app.isPackaged ? 360 : 120,
|
||||
});
|
||||
},
|
||||
});
|
||||
transientRuntime = serviceMode === "transient";
|
||||
}
|
||||
if (owner?.owner === "app" && !startAtLogin) {
|
||||
await disableRuntimeService(dataDir);
|
||||
}
|
||||
preserveManagedRuntimeOnQuit = true;
|
||||
preserveManagedRuntimeOnQuit = !transientRuntime;
|
||||
} else if (standaloneRuntime) {
|
||||
await ensureRuntimeProcess({
|
||||
dataDir,
|
||||
|
||||
@@ -12,6 +12,8 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
ensureStableRuntimeThenRepair,
|
||||
installBundledCliIfNeeded,
|
||||
installServiceWithDisabledFallback,
|
||||
isDisabledServiceInstallError,
|
||||
isInstalledApplicationsApp,
|
||||
stableRuntimeErrorCode,
|
||||
startStableRuntimeRepair,
|
||||
@@ -158,3 +160,97 @@ describe("ensureStableRuntimeThenRepair", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDisabledServiceInstallError", () => {
|
||||
it("recognises the durable disabled-service state", () => {
|
||||
expect(
|
||||
isDisabledServiceInstallError(
|
||||
new Error("runtime_service_disabled_by_system"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps every other startup failure fatal", () => {
|
||||
for (const other of [
|
||||
new Error("runtime_service_label_conflict"),
|
||||
new Error("runtime_service_install_root_not_ready"),
|
||||
new Error("runtime_service_command_failed"),
|
||||
"runtime_service_disabled_by_system",
|
||||
null,
|
||||
undefined,
|
||||
]) {
|
||||
expect(isDisabledServiceInstallError(other)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("installServiceWithDisabledFallback", () => {
|
||||
it("keeps a successfully installed service managed", async () => {
|
||||
const restoreOwner = vi.fn(async () => undefined);
|
||||
const reportDisabled = vi.fn(async () => undefined);
|
||||
const startTransient = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
installServiceWithDisabledFallback({
|
||||
install: async () => undefined,
|
||||
restoreOwner,
|
||||
reportDisabled,
|
||||
startTransient,
|
||||
}),
|
||||
).resolves.toBe("managed");
|
||||
expect(restoreOwner).not.toHaveBeenCalled();
|
||||
expect(reportDisabled).not.toHaveBeenCalled();
|
||||
expect(startTransient).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses an app-lifetime Runtime when launchd disabled the service", async () => {
|
||||
const order: string[] = [];
|
||||
|
||||
await expect(
|
||||
installServiceWithDisabledFallback({
|
||||
install: async () => {
|
||||
order.push("install");
|
||||
throw new Error("runtime_service_disabled_by_system");
|
||||
},
|
||||
restoreOwner: async () => {
|
||||
order.push("restore-owner");
|
||||
},
|
||||
reportDisabled: async () => {
|
||||
order.push("report-start");
|
||||
await Promise.resolve();
|
||||
order.push("report-end");
|
||||
},
|
||||
startTransient: async () => {
|
||||
order.push("start-transient");
|
||||
},
|
||||
}),
|
||||
).resolves.toBe("transient");
|
||||
expect(order).toEqual([
|
||||
"install",
|
||||
"restore-owner",
|
||||
"report-start",
|
||||
"report-end",
|
||||
"start-transient",
|
||||
]);
|
||||
});
|
||||
|
||||
it("restores ownership and preserves unrelated startup failures", async () => {
|
||||
const restoreOwner = vi.fn(async () => undefined);
|
||||
const reportDisabled = vi.fn(async () => undefined);
|
||||
const startTransient = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
installServiceWithDisabledFallback({
|
||||
install: async () => {
|
||||
throw new Error("runtime_service_command_failed");
|
||||
},
|
||||
restoreOwner,
|
||||
reportDisabled,
|
||||
startTransient,
|
||||
}),
|
||||
).rejects.toThrow("runtime_service_command_failed");
|
||||
expect(restoreOwner).toHaveBeenCalledOnce();
|
||||
expect(reportDisabled).not.toHaveBeenCalled();
|
||||
expect(startTransient).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -137,3 +137,44 @@ export function stableRuntimeErrorCode(error: unknown): string {
|
||||
}
|
||||
return "desktop_start_failed";
|
||||
}
|
||||
|
||||
/**
|
||||
* `installUserService` reports a durable `launchctl disable` override with this
|
||||
* exact code. It is a state to surface, not a failure to undo: the installer
|
||||
* never re-enables a service the user or the system disabled, and the macOS
|
||||
* platform checks assert that refusal
|
||||
* (`install_refuses_to_re_enable_a_disabled_service`).
|
||||
*
|
||||
* The override outlives a re-install, so treating it as fatal leaves an
|
||||
* affected machine with an app that exits on every launch. Desktop degrades
|
||||
* instead, owning the Runtime as a child process, and reports the durable
|
||||
* state rather than clearing it.
|
||||
*
|
||||
* Only this exact code degrades. Every other install failure keeps failing
|
||||
* closed, so a real misconfiguration still surfaces instead of hiding behind a
|
||||
* child process that may not serve hosts either.
|
||||
*/
|
||||
export function isDisabledServiceInstallError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
error.message === "runtime_service_disabled_by_system"
|
||||
);
|
||||
}
|
||||
|
||||
export async function installServiceWithDisabledFallback(options: {
|
||||
install(): Promise<void>;
|
||||
restoreOwner(): Promise<void>;
|
||||
reportDisabled(): Promise<void>;
|
||||
startTransient(): Promise<void>;
|
||||
}): Promise<"managed" | "transient"> {
|
||||
try {
|
||||
await options.install();
|
||||
return "managed";
|
||||
} catch (error) {
|
||||
await options.restoreOwner();
|
||||
if (!isDisabledServiceInstallError(error)) throw error;
|
||||
await options.reportDisabled();
|
||||
await options.startTransient();
|
||||
return "transient";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user