Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/desktop/src/app/DesktopApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import * as DesktopWslEnvironment from "../wsl/DesktopWslEnvironment.ts";
import * as ElectronMenu from "../electron/ElectronMenu.ts";
import * as ElectronShell from "../electron/ElectronShell.ts";
import * as ElectronTheme from "../electron/ElectronTheme.ts";
import * as ElectronUpdater from "../electron/ElectronUpdater.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as MacPermissions from "../permissions/MacPermissions.ts";
import * as PreviewManager from "../preview/Manager.ts";
Expand Down Expand Up @@ -204,6 +205,7 @@ it.effect(
Layer.mock(ElectronMenu.ElectronMenu, {}),
Layer.mock(ElectronShell.ElectronShell, {}),
Layer.mock(ElectronTheme.ElectronTheme, {}),
Layer.mock(ElectronUpdater.ElectronUpdater, {}),
Layer.mock(ElectronWindow.ElectronWindow, {}),
Layer.mock(MacPermissions.MacPermissions, {}),
Layer.mock(PreviewManager.PreviewManager, {
Expand Down
17 changes: 15 additions & 2 deletions apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type * as Electron from "electron";

import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronTheme from "../electron/ElectronTheme.ts";
import * as ElectronUpdater from "../electron/ElectronUpdater.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
Expand Down Expand Up @@ -226,9 +227,10 @@ describe("DesktopLifecycle", () => {
}),
);

it.effect("relaunches without the app link that launched it", () =>
it.effect("disarms updates and drops startup app links on relaunch", () =>
Effect.gen(function* () {
const relaunched = yield* Deferred.make<Electron.RelaunchOptions>();
let armed = true;
const layerEnvironment = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
platform: "linux",
isDevelopment: false,
Expand All @@ -239,9 +241,20 @@ describe("DesktopLifecycle", () => {
new Map(),
Effect.void,
() => Effect.void,
(options) => Deferred.succeed(relaunched, options).pipe(Effect.asVoid),
(options) =>
Effect.sync(() => {
assert.isFalse(armed);
}).pipe(Effect.andThen(Deferred.succeed(relaunched, options)), Effect.asVoid),
),
),
Layer.provideMerge(
Layer.mock(ElectronUpdater.ElectronUpdater, {
setAutoInstallOnAppQuit: (value) =>
Effect.sync(() => {
armed = value;
}),
}),
),
Layer.provideMerge(layerElectronTheme),
Layer.provideMerge(layerElectronWindow()),
Layer.provideMerge(layerDesktopWindow()),
Expand Down
13 changes: 11 additions & 2 deletions apps/desktop/src/app/DesktopLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as DesktopShutdown from "./DesktopShutdown.ts";
import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
import * as ElectronTheme from "../electron/ElectronTheme.ts";
import * as ElectronUpdater from "../electron/ElectronUpdater.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as DesktopState from "./DesktopState.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
Expand Down Expand Up @@ -46,14 +47,18 @@ type DesktopLifecycleRegistrationServices =
| ElectronWindow.ElectronWindow;

/**
* @effect-expect-leaking DesktopEnvironment | DesktopShutdown | DesktopState | DesktopWindow | ElectronApp | ElectronTheme | ElectronWindow
* @effect-expect-leaking DesktopEnvironment | DesktopShutdown | DesktopState | DesktopWindow | ElectronApp | ElectronTheme | ElectronUpdater | ElectronWindow
*/
export class DesktopLifecycle extends Context.Service<
DesktopLifecycle,
{
readonly relaunch: (
reason: string,
) => Effect.Effect<void, never, DesktopLifecycleRuntimeServices>;
) => Effect.Effect<
void,
never,
DesktopLifecycleRuntimeServices | ElectronUpdater.ElectronUpdater
>;
readonly register: Effect.Effect<
void,
never,
Expand Down Expand Up @@ -217,6 +222,7 @@ export const make = DesktopLifecycle.of({
const electronApp = yield* ElectronApp.ElectronApp;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const state = yield* DesktopState.DesktopState;
const electronUpdater = yield* ElectronUpdater.ElectronUpdater;
yield* logLifecycleInfo("desktop relaunch requested", { reason });
yield* Effect.gen(function* () {
yield* Effect.yieldNow;
Expand All @@ -226,6 +232,9 @@ export const make = DesktopLifecycle.of({
yield* electronApp.exit(75);
return;
}
// app.exit still emits "quit", where electron-updater would install a
// downloaded update under the relaunched app on Windows and Linux.
yield* electronUpdater.setAutoInstallOnAppQuit(false);
// Startup already acted on the app link that launched it; passing it on replays it.
const appLinkPrefix = `${ElectronProtocol.getDesktopScheme(environment.isDevelopment)}:`;
const args = yield* HostProcess.Arguments;
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop/src/electron/ElectronApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,27 @@ const { getSystemLocaleMock, getUserDefaultMock } = vi.hoisted(() => ({
getUserDefaultMock: vi.fn((_key: string) => false),
}));

const { appEvents } = await vi.hoisted(async () => {
const { EventEmitter } = await import("node:events");
return { appEvents: new EventEmitter() };
});
const { autoUpdaterMock } = vi.hoisted(() => ({
autoUpdaterMock: { autoInstallOnAppQuit: false },
}));
vi.mock("electron-updater", () => ({ autoUpdater: autoUpdaterMock }));

vi.mock("electron", () => ({
app: {
getSystemLocale: getSystemLocaleMock,
once: appEvents.once.bind(appEvents),
},
systemPreferences: {
getUserDefault: getUserDefaultMock,
},
}));

import * as ElectronApp from "./ElectronApp.ts";
import * as ElectronUpdater from "./ElectronUpdater.ts";

describe("ElectronApp", () => {
beforeEach(() => {
Expand Down Expand Up @@ -87,3 +98,21 @@ it.effect("reads both macOS hour preferences without overriding explicit formats
}
}),
);

it.effect("records only an armed normal quit after the updater scope closes", () =>
Effect.gen(function* () {
const updater = yield* ElectronUpdater.ElectronUpdater;
const recordInstall = vi.fn();
for (const [exitCode, armed, expectedRecords] of [
[1, true, 0],
[0, false, 0],
[0, true, 1],
] as const) {
yield* updater.setAutoInstallOnAppQuit(true);
yield* Effect.scoped(updater.onInstallOnAppQuit(recordInstall));
yield* updater.setAutoInstallOnAppQuit(armed);
appEvents.emit("quit", {}, exitCode);
assert.equal(recordInstall.mock.calls.length, expectedRecords);
}
}).pipe(Effect.provide(ElectronUpdater.layer)),
);
10 changes: 10 additions & 0 deletions apps/desktop/src/electron/ElectronUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as Scope from "effect/Scope";

import * as Electron from "electron";
import { autoUpdater } from "electron-updater";

type AutoUpdater = typeof autoUpdater;
Expand Down Expand Up @@ -61,6 +62,7 @@ export class ElectronUpdater extends Context.Service<
readonly setFeedURL: (options: ElectronUpdaterFeedUrl) => Effect.Effect<void>;
readonly setAutoDownload: (value: boolean) => Effect.Effect<void>;
readonly setAutoInstallOnAppQuit: (value: boolean) => Effect.Effect<void>;
readonly onInstallOnAppQuit: (listener: () => void) => Effect.Effect<void>;
readonly setChannel: (channel: string) => Effect.Effect<void>;
readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>;
readonly allowDowngrade: Effect.Effect<boolean>;
Expand Down Expand Up @@ -100,6 +102,14 @@ export const make = ElectronUpdater.of({
return Effect.void;
});
}),
onInstallOnAppQuit: Effect.fn("ElectronUpdater.onInstallOnAppQuit")(function (listener) {
return Effect.sync(() => {
// Shutdown closes the service scope before electron-updater's final quit listener runs.
Electron.app.once("quit", (_event, exitCode) => {
if (exitCode === 0 && autoUpdater.autoInstallOnAppQuit) listener();
});
});
}),
setChannel: Effect.fn("ElectronUpdater.setChannel")(function (channel) {
return Effect.suspend(() => {
autoUpdater.channel = channel;
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/methods/localEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as DesktopShutdown from "../../app/DesktopShutdown.ts";
import * as DesktopState from "../../app/DesktopState.ts";
import * as ElectronApp from "../../electron/ElectronApp.ts";
import * as ElectronTheme from "../../electron/ElectronTheme.ts";
import * as ElectronUpdater from "../../electron/ElectronUpdater.ts";
import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
import * as DesktopWindow from "../../window/DesktopWindow.ts";
import { getLocalEnvironmentEnabled, setLocalEnvironmentEnabled } from "./localEnvironment.ts";
Expand All @@ -24,6 +25,7 @@ const layerUnusedLifecycleRuntime = Layer.mergeAll(
Layer.mock(DesktopWindow.DesktopWindow, {}),
Layer.mock(ElectronApp.ElectronApp, {}),
Layer.mock(ElectronTheme.ElectronTheme, {}),
Layer.mock(ElectronUpdater.ElectronUpdater, {}),
);

describe("local environment IPC", () => {
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/ipc/methods/wsl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as DesktopState from "../../app/DesktopState.ts";
import * as ElectronApp from "../../electron/ElectronApp.ts";
import * as ElectronDialog from "../../electron/ElectronDialog.ts";
import * as ElectronTheme from "../../electron/ElectronTheme.ts";
import * as ElectronUpdater from "../../electron/ElectronUpdater.ts";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
import * as DesktopClientSettings from "../../settings/DesktopClientSettings.ts";
Expand Down Expand Up @@ -72,6 +73,10 @@ const layerUnusedLifecycleRuntime = Layer.mergeAll(
ElectronTheme.ElectronTheme,
ElectronTheme.ElectronTheme.of({} as ElectronTheme.ElectronTheme["Service"]),
),
Layer.succeed(
ElectronUpdater.ElectronUpdater,
ElectronUpdater.ElectronUpdater.of({} as ElectronUpdater.ElectronUpdater["Service"]),
),
Layer.succeed(
ElectronDialog.ElectronDialog,
ElectronDialog.ElectronDialog.of({} as ElectronDialog.ElectronDialog["Service"]),
Expand Down
Loading
Loading