Skip to content
Merged
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
7 changes: 7 additions & 0 deletions demo/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ kit.ipc.handle(IPC.OPEN_ISOLATED_WINDOW, () => {
});
kit.ipc.handle(IPC.GET_ELECTRON_VERSION, () => process.versions.electron);

// For the update tests: every update request through the proxy KIT_DEMO_PROXY
// names (Electron's proxyRules). The demo's update server is on this machine,
// which Chromium never sends through a proxy unless told: <-loopback>.
if (process.env.KIT_DEMO_PROXY) kit.updater.setProxy({ proxyRules: process.env.KIT_DEMO_PROXY, proxyBypassRules: "<-loopback>" });
// For the tests, which change it from the main process (Playwright's app.evaluate()).
globalThis.demoKit = kit;

kit.ready.then(() => {
kit.ipc.handle(IPC.GET_ISOLATED_VERSION, () => process.versions.electron, { session: kit.sessions.isolated(ISOLATED_PARTITION) });
kit.log.info("Opened with", JSON.stringify(process.argv));
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ and links here, so these can be corrected without a new release.
| [The Shell, Settings and Credits](shell.md) | `kit.ui.mountShell()`: the nav rail, the header, the bottom bar and its panels, and the Settings view with its Update and Credits tabs |
| [Page Components](page-components.md) | Toasts, prompts, the parts of a section of files (drop zone, progress, action bar), cards, the status line and busy buttons, the switch and the stepped slider, the key guard, `kit.format`, and the copy rules |
| [The Main Process](main-process.md) | The main window, isolated sessions, spell checking, one instance and the files an app is opened with, the settings and their migration, the redacted log, and the menu |
| [Updates](updates.md) | The updater: when it checks, Stable, Beta and Alpha, automatic downloads, and its state |
| [Updates](updates.md) | The updater: when it checks, Stable, Beta and Alpha, automatic downloads, its state, and a proxy |
| [Building](building.md) | `config()`, the electron-builder config every app builds with; the files an app opens, on Linux; the update files every release carries; and naming an app's files |
| [Theme, Accent and Tokens](theming.md) | The OS theme, an app's accent and its WCAG 2.2 AA check, the house tokens, focus rings and controls |
| [Testing an App](testing.md) | The helpers for an app's own tests: nothing reached beyond the machine, the preload, the menu, the accent and the build config |
Expand Down
26 changes: 25 additions & 1 deletion docs/updates.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Updates

The updater behind Settings > Update: when it checks, the channels, automatic downloads, and its state.
The updater behind Settings > Update: when it checks, the channels, automatic downloads, a proxy, and its state.

[All the docs](README.md)

Expand Down Expand Up @@ -63,3 +63,27 @@ The state, from `getUpdateStatus()` and `onUpdateStatus()`:

The version rules are `require("@diamonddigitaldev/electron-kit/main").version` (`parse`, `compare`,
`channelOf`, `isOfferableUpdate` and the rest), for an app's own use.

## Through a Proxy

`kit.updater.setProxy(config)`, from the app's main process, sends every update check and download through a
proxy: an app whose own requests go through one (Dropgate's chosen network adapter) sends its updates the same way.

```js
await kit.updater.setProxy({ proxyRules: "socks5://127.0.0.1:1080", proxyBypassRules: "<-loopback>" });
await kit.updater.setProxy(null); // back to how the app was launched
```

- **`proxyRules` and `proxyBypassRules`** are Electron's own (`session.setProxy()`), strings; nothing else is taken,
and anything else throws. `null` goes back to how the app was launched: the system's proxy settings, or none
when it was launched with `--no-proxy-server`, as its tests are (`netLogSwitches()`, [Testing](testing.md)),
since the system's settings can search the network for a proxy.
- **It's electron-updater's own session** (its `netSession`), which the kit never makes itself. A proxy set
before electron-updater loads (before `kit.ready`) is kept, and applied as it loads, before any check.
- **It resolves once applied**, and at once when there's no updater to apply it to (run from source, or without
`updates`). Calls apply in order, and a check or download waits for the one being applied.
- **A change cuts what's under way:** the session's connections are closed, and a download under way is
cancelled, so it fails and the Update tab says so ("Version x couldn't be downloaded."), as any network failure.
Nothing starts again by itself: Check for Updates or Download Update tries again, through the new proxy.
- **Chromium never sends loopback through a proxy** unless `proxyBypassRules` says `<-loopback>`: a proxy that
must carry everything, an update server on this machine included, needs it.
118 changes: 117 additions & 1 deletion e2e/update-server.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
// loads electron-updater from app.asar, and every request goes through
// Electron's own network stack. test/updater.test.js covers the rules in
// detail against the same library under Node; this checks they hold in a
// packaged app, from the Update tab.
// packaged app, from the Update tab, and that kit.updater.setProxy() sends
// them through a proxy (a local one, test/helpers/proxy.js).
//
// Run from source, the updater never checks, so these run against a packaged
// build only (KIT_DEMO_EXECUTABLE). The server's port is fixed by the build,
Expand All @@ -21,6 +22,7 @@ const os = require("os");
const path = require("path");
const { test, expect } = require("./helpers/demo");
const { startUpdateServer } = require("../test/helpers/update-server");
const { startProxy, THROUGH } = require("../test/helpers/proxy");
const { STAGING_ID } = require("../main/updater");
const demoBuild = require("../demo/electron-builder.cjs");

Expand Down Expand Up @@ -138,3 +140,117 @@ test("with no update server, the check says it couldn't, and nothing else is ask
expect(printed).toContain("Update check failed");
expect(printed).not.toMatch(/Checking for update|^\s+at /m);
});

// -- Through a proxy ---------------------------------------------------------
//
// kit.updater.setProxy(), from the demo's KIT_DEMO_PROXY (set before the
// updater loads) and from its main process (globalThis.demoKit), with a local
// test proxy that marks what it forwards. The demo adds <-loopback>, so the
// update server on this machine goes through the proxy too. The proxies'
// ports are fixed, as the launch names them.

const PROXY_PORT = PORT + 1;
const SECOND_PROXY_PORT = PORT + 2;
/** A port nothing listens on. */
const CLOSED_PORT = PORT + 3;

/** Give the demo's updater a proxy from its main process, or null for the launch's settings. */
const setProxy = (demo, proxyRules) => demo.app.evaluate((_electron, proxyRules) => globalThis.demoKit.updater.setProxy(proxyRules === null ? null : { proxyRules, proxyBypassRules: "<-loopback>" }), proxyRules);

/** The requests the server had that came through the test proxy, and those that didn't. */
const throughProxy = (server) => server.requests.filter((r) => r.headers[THROUGH] === "1").map((r) => r.file);
const straight = (server) => server.requests.filter((r) => r.headers[THROUGH] === undefined).map((r) => r.file);

test.describe("through a proxy", () => {
test.use({ demoEnv: { KIT_DEMO_PROXY: `http://127.0.0.1:${PROXY_PORT}` } });

test("every request of a check and a download goes through the proxy the app set before the updater loaded", async ({ demo }) => {
const proxy = await startProxy({ port: PROXY_PORT });
const server = await serve({ latest: "0.0.1" });
try {
const main = await demo.mainWindow();
const pane = await openUpdate(main);
await pane.getByRole("button", { name: "Check for Updates" }).click();
await expect(pane.getByRole("status")).toHaveText("Version 0.0.1 has downloaded and will be installed when you close electron-kit Demo.", { timeout: 30_000 });
expect(server.channelFiles()).toEqual(["latest.yml"]);
expect(server.installers()).toEqual([`Kit-Test-Setup-0.0.1.${EXT}`]);
expect(straight(server), "requests that went straight to the server").toEqual([]);
expect(proxy.requests.map((r) => decodeURIComponent(new URL(r.url).pathname.slice(1)))).toEqual(throughProxy(server));
await keepFromInstalling(demo);
} finally {
await server.close();
await proxy.close();
}
});

test("null goes back to the launch's settings (the tests' --no-proxy-server, so no search for one): the check goes straight to the server", async ({ demo }) => {
const proxy = await startProxy({ port: PROXY_PORT });
const server = await serve({ latest: "0.0.0" });
try {
const main = await demo.mainWindow();
await setProxy(demo, null);
const pane = await openUpdate(main);
await pane.getByRole("button", { name: "Check for Updates" }).click();
await expect(pane.getByRole("status")).toHaveText("You're up to date.");
expect(server.channelFiles()).toEqual(["latest.yml"]);
expect(throughProxy(server), "requests that went through the proxy").toEqual([]);
expect(proxy.requests).toEqual([]);
} finally {
await server.close();
await proxy.close();
}
});

test("a change mid-download fails it, saying so, and Download Update tries again through the new proxy", async ({ demo }) => {
const proxy = await startProxy({ port: PROXY_PORT });
const second = await startProxy({ port: SECOND_PROXY_PORT });
const server = await serve({ latest: "0.0.1" });
try {
const main = await demo.mainWindow();
const pane = await openUpdate(main);
await pane.getByRole("switch", { name: "Download updates automatically" }).click();
await expect.poll(() => main.evaluate(() => window.kitAPI.getSettings().then((s) => s.autoDownloadUpdates))).toBe(false);
await pane.getByRole("button", { name: "Check for Updates" }).click();
await expect(pane.getByRole("status")).toHaveText("Version 0.0.1 is available.");

// Half the installer, then the proxy changes under it.
server.hold(true);
await pane.getByRole("button", { name: "Download Update" }).click();
await expect.poll(() => server.installers()).toEqual([`Kit-Test-Setup-0.0.1.${EXT}`]);
await setProxy(demo, second.rules);
await expect(pane.getByRole("status")).toHaveText("Version 0.0.1 couldn't be downloaded.", { timeout: 30_000 });
await expect(main.locator(".update-dot:visible")).toHaveCount(2);
expect(second.requests, "nothing starts again by itself").toEqual([]);

server.hold(false);
await pane.getByRole("button", { name: "Download Update" }).click();
await expect(pane.getByRole("status")).toHaveText("Version 0.0.1 has downloaded and will be installed when you close electron-kit Demo.", { timeout: 30_000 });
// Through the new proxy (with the blockmap, beside the part already downloaded), and nothing straight.
expect(second.requests.map((r) => new URL(r.url).pathname)).toContain(`/Kit-Test-Setup-0.0.1.${EXT}`);
expect(straight(server)).toEqual([]);
await keepFromInstalling(demo);
} finally {
await server.close();
await second.close();
await proxy.close();
}
});
});

test.describe("through a proxy that isn't there", () => {
test.use({ demoEnv: { KIT_DEMO_PROXY: `http://127.0.0.1:${CLOSED_PORT}` } });

test("the check fails and says so, and nothing reaches the server", async ({ demo }) => {
const server = await serve({ latest: "0.0.1" });
try {
const main = await demo.mainWindow();
const pane = await openUpdate(main);
await pane.getByRole("button", { name: "Check for Updates" }).click();
await expect(pane.getByRole("status")).toHaveText("Couldn't check for updates. Try again later.", { timeout: 30_000 });
await expect(pane.getByRole("button", { name: "Check for Updates" })).toBeEnabled();
expect(server.requests, "Chromium went around the proxy").toEqual([]);
} finally {
await server.close();
}
});
});
6 changes: 5 additions & 1 deletion main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ function registerPreload(ses) {
* log: { error(...args: unknown[]): void, warn(...args: unknown[]): void, info(...args: unknown[]): void, debug(...args: unknown[]): void, lines(): string[] },
* windows: { createMain(options: object): Electron.BrowserWindow, main(): Electron.BrowserWindow | null },
* files: { open(paths: string[]): void },
* updater: { setProxy(config: { proxyRules: string, proxyBypassRules?: string } | null): Promise<void> },
* primary: boolean,
* }}
* ready resolves once the app is ready, the shared preload is registered and
Expand All @@ -113,7 +114,9 @@ function registerPreload(ses) {
* kit's bridge (sessions.js). log is the app's log, redacted, and its
* lines() the banner and the run's last lines (log.js).
* windows.createMain() makes the main window (windows.js).
* files.open() hands the page files from the app's own menu. primary is
* files.open() hands the page files from the app's own menu.
* updater.setProxy() sends every update request through a proxy, or as
* the app was launched again with null (updater.js). primary is
* false in a second launch, which is quitting.
*/
function start(config = {}) {
Expand Down Expand Up @@ -214,6 +217,7 @@ function start(config = {}) {
log: { error: log.error, warn: log.warn, info: log.info, debug: log.debug, lines: log.lines },
windows: { createMain: windows.createMain, main: windows.main },
files: { open: files.open },
updater: { setProxy: updates.setProxy },
primary,
};
}
Expand Down
Loading
Loading