mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-15 20:12:59 -07:00
fix: guard native updater against unsupported builds to prevent crashes
- Skip electron-updater on ad-hoc signed macOS, package-managed Linux AppImages, non-writable AppImages, and Windows - Preserve GitHub metadata checks and direct AppImage updates for supported installs - Add isNativeUpdaterSupported with full platform/signing/writability test coverage
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
area: updates
|
||||||
|
---
|
||||||
|
|
||||||
|
- Avoided native `electron-updater` checks on unsupported builds while preserving direct writable AppImage updates, so tray and background update checks continue through GitHub release metadata without crashing the app.
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
## Highlights
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **Character Dictionary:** Added AniList-based selection to resolve character dictionary mismatches, with series-scoped overrides that replace stale entries. Available via `subminer dictionary --candidates` / `--select` and a default `Ctrl+Alt+A` in-app shortcut.
|
|
||||||
- **Subtitle Bar Toggle:** Added a `V` shortcut and mpv binding to toggle the primary subtitle bar independently of mpv's native subtitle display.
|
|
||||||
- **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- **mpv Plugin Setup:** Managed launches now inject the bundled plugin automatically. The setup flow can trash detected legacy global plugin files before launch, and legacy global install entrypoints have been removed so regular mpv playback is unaffected.
|
|
||||||
- **Tray Menu:** Replaced "Open Overlay" with "Open Help," which opens the session help modal.
|
|
||||||
- **Stats Exclusions:** Vocabulary exclusions now persist in the immersion database and migrate existing browser-local exclusions on first load.
|
|
||||||
- **Config Defaults:** Disabled texthooker startup, subtitle, and annotation websocket servers by default. Fresh installs now use a Japanese font stack, transparent subtitle backgrounds, stronger text shadows, and teal N4/fourth-band coloring for primary subtitles. Yomitan popup auto-pause remains enabled.
|
|
||||||
- **Config Example:** The generated example config now lists every built-in keybinding default.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- **Subtitle Annotations — Grammar Filtering:** Suppressed N+1, JLPT, frequency, and name styling on grammar-only tokens: standalone interjections (`あ`, katakana variants), kana grammar helpers (`ことに`), auxiliary inflection fragments (`れる`, `れた`), polite copula tails (`です`, `じゃないですか`), standalone particles matched by known-word decks, and existence verbs (`ある`/`有る`). Known-word highlighting is preserved where applicable.
|
|
||||||
- **Subtitle Annotations — Color Priority:** Fixed token color priority so typography settings are preserved, JLPT colors no longer override higher-priority known-word or frequency colors, JLPT underlines persist at their correct color after dictionary lookups and when a token also carries known-word or frequency annotations, and frequency highlighting works correctly for ordinal prefix-noun tokens like `第二`.
|
|
||||||
- **Subtitle Annotations — Other:** Stopped kana-only tokens from being selected as N+1 targets; preserved Yomitan compound tokens so known component words no longer color a larger unknown word green; kept annotation prefetch running after immediate cache-hit renders; added a brightness lift for annotated token hover states when hover backgrounds are transparent; accepted `subtitleStyle.hoverBackground` as an alias for `subtitleStyle.hoverTokenBackgroundColor`; refreshed the current subtitle after successful card mining so newly known words recolor immediately.
|
|
||||||
- **Subtitle Bar:** Changed `v` to cycle the primary subtitle bar through visible, hover, and hidden modes with OSD feedback. Added `subtitleStyle.primaryDefaultMode` to set the startup visibility default independently from secondary subtitles.
|
|
||||||
- **Tokenizer:** Now uses Yomitan `wordClasses` metadata for part-of-speech filtering, and backfills blank MeCab POS fields during parser enrichment.
|
|
||||||
- **Overlay (Linux):** Fixed multi-line subtitle copy timing out after the prompt; follow-up number-row digits are now accepted for multi-line mining even when the original shortcut modifiers are still held.
|
|
||||||
- **Overlay (Hyprland):** Fixed fullscreen transitions so overlay geometry refreshes on mpv fullscreen changes, topmost stacking is reasserted, and hover pause works correctly after resize/toggle cycles. Overlay windows now align precisely to mpv bounds with floating decoration disabled; the stats overlay is opaque to prevent mpv bleed-through at the top edge; overlay windows no longer pin across workspaces.
|
|
||||||
- **Overlay (macOS):** Kept the overlay visible and interactive during transient tracker refreshes while mpv is the active tracked window, and kept it behind unrelated foreground windows while remaining above mpv.
|
|
||||||
- **Overlay:** Keyboard-only Yomitan popup shortcuts now take precedence over overlay keybindings like `j`; the browser focus outline is hidden so focused overlays no longer show a yellow/orange viewport border.
|
|
||||||
- **Default Keybindings:** Fixed replay/next subtitle keybindings — session help moved to `Ctrl/Cmd+/`, freeing `Ctrl+Shift+H` and `Ctrl+Shift+L` for subtitle playback controls. `Ctrl+Shift+L` now correctly reaches play-next-subtitle, and play-next resumes from a paused state before pausing again at the subtitle end.
|
|
||||||
- **Anki:** Manual clipboard subtitle updates preserve existing word audio while replacing sentence audio, animated-image media, and expression fields — even when audio overwrite is configured off.
|
|
||||||
- **AniList:** Post-watch progress checks now run on time-position updates using the fresh mpv position; manual mark-watched forces a progress sync; missing episode metadata is filled from the filename parser. Duplicate writes during concurrent checks are prevented, and manual watched marks are preserved when sync fails.
|
|
||||||
- **AniList (Linux):** Retried safeStorage availability after transient keyring failures so tokens can load and save once the keyring becomes available. Prevented config reload from opening the setup window during playback when token storage cannot be resolved, and stopped the setup flow from reporting success when token persistence fails.
|
|
||||||
- **mpv:** Stopped mpv from holding SubMiner subprocesses during shutdown, preventing desktop crash notifications on video close. Kept the overlay alive across same-media buffering reloads to avoid duplicate startup gates and AniSkip lookups; playlist navigation now reuses the running overlay without repeating the pause-until-ready warmup gate.
|
|
||||||
- **Launcher:** Managed playback now exits the background SubMiner app when the video closes; explicit background launches remain persistent.
|
|
||||||
- **Stats:** Background mode routes through the isolated stats daemon; app startup defers to an already-running daemon instead of failing when the port is already in use. Fixed recent session detail pages showing "Media not found" before lifetime media summaries are available.
|
|
||||||
- **Jellyfin:** Improved setup with recent server selection and inline authentication feedback. Added a tray toggle for runtime-only cast discovery.
|
|
||||||
|
|
||||||
### Docs
|
|
||||||
|
|
||||||
- Improved the docs homepage with canonical URLs and a cleaner sitemap.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
See the README and docs/installation guide for full setup steps.
|
|
||||||
|
|
||||||
## Assets
|
|
||||||
|
|
||||||
- Linux: `SubMiner.AppImage`
|
|
||||||
- macOS: `SubMiner-*.dmg` and `SubMiner-*.zip`
|
|
||||||
- Optional extras: `subminer-assets.tar.gz` and the `subminer` launcher
|
|
||||||
|
|
||||||
Note: the `subminer` wrapper script uses Bun (`#!/usr/bin/env bun`), so `bun` must be installed and on `PATH`.
|
|
||||||
+12
-1
@@ -508,7 +508,10 @@ import { handleCharacterDictionaryAutoSyncComplete } from './main/runtime/charac
|
|||||||
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
||||||
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
||||||
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
||||||
import { createElectronAppUpdater } from './main/runtime/update/app-updater';
|
import {
|
||||||
|
createElectronAppUpdater,
|
||||||
|
isNativeUpdaterSupported,
|
||||||
|
} from './main/runtime/update/app-updater';
|
||||||
import {
|
import {
|
||||||
fetchLatestStableRelease,
|
fetchLatestStableRelease,
|
||||||
fetchReleaseAssetBuffer,
|
fetchReleaseAssetBuffer,
|
||||||
@@ -4658,6 +4661,14 @@ function getUpdateService() {
|
|||||||
isPackaged: app.isPackaged,
|
isPackaged: app.isPackaged,
|
||||||
log: (message) => logger.info(message),
|
log: (message) => logger.info(message),
|
||||||
getChannel: () => getResolvedConfig().updates.channel,
|
getChannel: () => getResolvedConfig().updates.channel,
|
||||||
|
isNativeUpdaterSupported: () =>
|
||||||
|
isNativeUpdaterSupported({
|
||||||
|
platform: process.platform,
|
||||||
|
isPackaged: app.isPackaged,
|
||||||
|
execPath: process.execPath,
|
||||||
|
env: process.env,
|
||||||
|
log: (message) => logger.warn(message),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
updateService = createUpdateService({
|
updateService = createUpdateService({
|
||||||
getConfig: () => getResolvedConfig().updates,
|
getConfig: () => getResolvedConfig().updates,
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { configureAutoUpdater, type ElectronAutoUpdaterLike } from './app-updater';
|
import {
|
||||||
|
configureAutoUpdater,
|
||||||
|
createElectronAppUpdater,
|
||||||
|
isKnownLinuxPackageManagedAppImage,
|
||||||
|
isNativeUpdaterSupported,
|
||||||
|
resolveMacAppBundlePath,
|
||||||
|
type ElectronAutoUpdaterLike,
|
||||||
|
} from './app-updater';
|
||||||
|
|
||||||
type UpdaterLogger = {
|
type UpdaterLogger = {
|
||||||
info: (message: string) => void;
|
info: (message: string) => void;
|
||||||
@@ -53,3 +60,192 @@ test('configureAutoUpdater allows prereleases only for the prerelease channel',
|
|||||||
configureAutoUpdater(updater, () => {}, 'stable');
|
configureAutoUpdater(updater, () => {}, 'stable');
|
||||||
assert.equal(updater.allowPrerelease, false);
|
assert.equal(updater.allowPrerelease, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('app updater skips native update checks when native updater is unsupported', async () => {
|
||||||
|
let checked = false;
|
||||||
|
const updater: ElectronAutoUpdaterLike = {
|
||||||
|
autoDownload: true,
|
||||||
|
allowPrerelease: false,
|
||||||
|
allowDowngrade: true,
|
||||||
|
logger: null,
|
||||||
|
checkForUpdates: async () => {
|
||||||
|
checked = true;
|
||||||
|
return {
|
||||||
|
updateInfo: {
|
||||||
|
version: '0.15.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
downloadUpdate: async () => [],
|
||||||
|
quitAndInstall: () => {},
|
||||||
|
};
|
||||||
|
const logged: string[] = [];
|
||||||
|
const appUpdater = createElectronAppUpdater({
|
||||||
|
currentVersion: '0.14.0',
|
||||||
|
isPackaged: true,
|
||||||
|
updater,
|
||||||
|
log: (message) => logged.push(message),
|
||||||
|
isNativeUpdaterSupported: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await appUpdater.checkForUpdates('stable');
|
||||||
|
|
||||||
|
assert.equal(checked, false);
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
available: false,
|
||||||
|
version: '0.14.0',
|
||||||
|
canUpdate: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(logged, [
|
||||||
|
'Skipping native app update check because native updater is unsupported.',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('app updater skips native downloads when native updater is unsupported', async () => {
|
||||||
|
let downloaded = false;
|
||||||
|
const updater: ElectronAutoUpdaterLike = {
|
||||||
|
autoDownload: true,
|
||||||
|
allowPrerelease: false,
|
||||||
|
allowDowngrade: true,
|
||||||
|
logger: null,
|
||||||
|
checkForUpdates: async () => null,
|
||||||
|
downloadUpdate: async () => {
|
||||||
|
downloaded = true;
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
quitAndInstall: () => {},
|
||||||
|
};
|
||||||
|
const logged: string[] = [];
|
||||||
|
const appUpdater = createElectronAppUpdater({
|
||||||
|
currentVersion: '0.14.0',
|
||||||
|
isPackaged: true,
|
||||||
|
updater,
|
||||||
|
log: (message) => logged.push(message),
|
||||||
|
isNativeUpdaterSupported: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await appUpdater.downloadUpdate();
|
||||||
|
|
||||||
|
assert.equal(downloaded, false);
|
||||||
|
assert.deepEqual(logged, ['Skipping app update download because native updater is unsupported.']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveMacAppBundlePath resolves packaged macOS executable path', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveMacAppBundlePath('/Applications/SubMiner.app/Contents/MacOS/SubMiner'),
|
||||||
|
'/Applications/SubMiner.app',
|
||||||
|
);
|
||||||
|
assert.equal(resolveMacAppBundlePath('/usr/local/bin/SubMiner'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mac native updater is unsupported for ad-hoc signed app bundles', () => {
|
||||||
|
const logged: string[] = [];
|
||||||
|
const supported = isNativeUpdaterSupported({
|
||||||
|
platform: 'darwin',
|
||||||
|
isPackaged: true,
|
||||||
|
execPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner',
|
||||||
|
readCodeSignature: () =>
|
||||||
|
['Signature=adhoc', 'TeamIdentifier=not set', 'Runtime Version=26.0.0'].join('\n'),
|
||||||
|
log: (message) => logged.push(message),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(supported, false);
|
||||||
|
assert.deepEqual(logged, ['Skipping native macOS updater because this build is ad-hoc signed.']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mac native updater is supported for Developer ID signed app bundles', () => {
|
||||||
|
const supported = isNativeUpdaterSupported({
|
||||||
|
platform: 'darwin',
|
||||||
|
isPackaged: true,
|
||||||
|
execPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner',
|
||||||
|
readCodeSignature: () =>
|
||||||
|
['Authority=Developer ID Application: Example', 'TeamIdentifier=ABCDE12345'].join('\n'),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(supported, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('linux native updater is supported for writable direct AppImage installs', () => {
|
||||||
|
const supported = isNativeUpdaterSupported({
|
||||||
|
platform: 'linux',
|
||||||
|
isPackaged: true,
|
||||||
|
execPath: '/tmp/.mount_SubMiner/SubMiner',
|
||||||
|
env: {
|
||||||
|
APPIMAGE: '/home/tester/.local/bin/SubMiner.AppImage',
|
||||||
|
},
|
||||||
|
canWriteAppImage: (appImagePath) =>
|
||||||
|
appImagePath === '/home/tester/.local/bin/SubMiner.AppImage',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(supported, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('linux native updater is unsupported when APPIMAGE is missing', () => {
|
||||||
|
const logged: string[] = [];
|
||||||
|
const supported = isNativeUpdaterSupported({
|
||||||
|
platform: 'linux',
|
||||||
|
isPackaged: true,
|
||||||
|
execPath: '/tmp/.mount_SubMiner/SubMiner',
|
||||||
|
env: {},
|
||||||
|
log: (message) => logged.push(message),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(supported, false);
|
||||||
|
assert.deepEqual(logged, ['Skipping native Linux updater because APPIMAGE is not set.']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('linux native updater is unsupported for non-writable AppImage installs', () => {
|
||||||
|
const logged: string[] = [];
|
||||||
|
const supported = isNativeUpdaterSupported({
|
||||||
|
platform: 'linux',
|
||||||
|
isPackaged: true,
|
||||||
|
execPath: '/tmp/.mount_SubMiner/SubMiner',
|
||||||
|
env: {
|
||||||
|
APPIMAGE: '/home/tester/.local/bin/SubMiner.AppImage',
|
||||||
|
},
|
||||||
|
canWriteAppImage: () => false,
|
||||||
|
log: (message) => logged.push(message),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(supported, false);
|
||||||
|
assert.deepEqual(logged, [
|
||||||
|
'Skipping native Linux updater because the running AppImage is not writable.',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('linux native updater is unsupported for package-managed AppImage installs', () => {
|
||||||
|
const logged: string[] = [];
|
||||||
|
const supported = isNativeUpdaterSupported({
|
||||||
|
platform: 'linux',
|
||||||
|
isPackaged: true,
|
||||||
|
execPath: '/tmp/.mount_SubMiner/SubMiner',
|
||||||
|
env: {
|
||||||
|
APPIMAGE: '/opt/SubMiner/SubMiner.AppImage',
|
||||||
|
},
|
||||||
|
canWriteAppImage: () => true,
|
||||||
|
log: (message) => logged.push(message),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(supported, false);
|
||||||
|
assert.deepEqual(logged, [
|
||||||
|
'Skipping native Linux updater because this AppImage is managed by the system package manager.',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('known Linux package-managed AppImage detection follows the canonical AUR path', () => {
|
||||||
|
assert.equal(isKnownLinuxPackageManagedAppImage('/opt/SubMiner/SubMiner.AppImage'), true);
|
||||||
|
assert.equal(
|
||||||
|
isKnownLinuxPackageManagedAppImage('/home/tester/.local/bin/SubMiner.AppImage'),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('native updater is unsupported on Windows by default', () => {
|
||||||
|
const supported = isNativeUpdaterSupported({
|
||||||
|
platform: 'win32',
|
||||||
|
isPackaged: true,
|
||||||
|
execPath: 'C:\\Program Files\\SubMiner\\SubMiner.exe',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(supported, false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { constants, accessSync, realpathSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
import { autoUpdater as electronAutoUpdater } from 'electron-updater';
|
import { autoUpdater as electronAutoUpdater } from 'electron-updater';
|
||||||
import type { UpdateChannel } from '../../../types/config';
|
import type { UpdateChannel } from '../../../types/config';
|
||||||
import { compareSemverLike } from './release-assets';
|
import { compareSemverLike } from './release-assets';
|
||||||
@@ -29,6 +32,103 @@ export interface ElectronAutoUpdaterLike {
|
|||||||
quitAndInstall: (isSilent?: boolean, isForceRunAfter?: boolean) => void;
|
quitAndInstall: (isSilent?: boolean, isForceRunAfter?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveMacAppBundlePath(execPath: string): string | null {
|
||||||
|
const marker = '.app/Contents/MacOS/';
|
||||||
|
const markerIndex = execPath.indexOf(marker);
|
||||||
|
if (markerIndex < 0) return null;
|
||||||
|
return execPath.slice(0, markerIndex + '.app'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMacCodeSignature(appBundlePath: string): string | null {
|
||||||
|
const result = spawnSync('/usr/bin/codesign', ['-dv', '--verbose=4', appBundlePath], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
if (result.error || result.status !== 0) return null;
|
||||||
|
return `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canWriteLinuxAppImage(appImagePath: string): boolean {
|
||||||
|
try {
|
||||||
|
accessSync(appImagePath, constants.W_OK);
|
||||||
|
accessSync(path.dirname(appImagePath), constants.W_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function realpathOrOriginal(filePath: string): string {
|
||||||
|
try {
|
||||||
|
return realpathSync(filePath);
|
||||||
|
} catch {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isKnownLinuxPackageManagedAppImage(appImagePath: string): boolean {
|
||||||
|
return realpathOrOriginal(appImagePath) === '/opt/SubMiner/SubMiner.AppImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNativeUpdaterSupported(options: {
|
||||||
|
platform: NodeJS.Platform;
|
||||||
|
isPackaged: boolean;
|
||||||
|
execPath: string;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
canWriteAppImage?: (appImagePath: string) => boolean;
|
||||||
|
readCodeSignature?: (appBundlePath: string) => string | null;
|
||||||
|
log?: (message: string) => void;
|
||||||
|
}): boolean {
|
||||||
|
if (!options.isPackaged) {
|
||||||
|
options.log?.('Skipping native updater because this build is not packaged.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (options.platform === 'linux') {
|
||||||
|
const appImagePath = options.env?.APPIMAGE?.trim();
|
||||||
|
if (!appImagePath) {
|
||||||
|
options.log?.('Skipping native Linux updater because APPIMAGE is not set.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isKnownLinuxPackageManagedAppImage(appImagePath)) {
|
||||||
|
options.log?.(
|
||||||
|
'Skipping native Linux updater because this AppImage is managed by the system package manager.',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!(options.canWriteAppImage ?? canWriteLinuxAppImage)(appImagePath)) {
|
||||||
|
options.log?.('Skipping native Linux updater because the running AppImage is not writable.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (options.platform !== 'darwin') {
|
||||||
|
options.log?.('Skipping native updater because this platform uses GitHub metadata checks.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appBundlePath = resolveMacAppBundlePath(options.execPath);
|
||||||
|
if (!appBundlePath) {
|
||||||
|
options.log?.(
|
||||||
|
'Skipping native macOS updater because the app bundle path could not be resolved.',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = (options.readCodeSignature ?? readMacCodeSignature)(appBundlePath);
|
||||||
|
if (!signature) {
|
||||||
|
options.log?.(
|
||||||
|
'Skipping native macOS updater because the app code signature could not be read.',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/Signature=adhoc\b/.test(signature) || /TeamIdentifier=not set\b/.test(signature)) {
|
||||||
|
options.log?.('Skipping native macOS updater because this build is ad-hoc signed.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function configureAutoUpdater(
|
export function configureAutoUpdater(
|
||||||
updater: ElectronAutoUpdaterLike,
|
updater: ElectronAutoUpdaterLike,
|
||||||
log: (message: string) => void = () => {},
|
log: (message: string) => void = () => {},
|
||||||
@@ -52,6 +152,7 @@ export function createElectronAppUpdater(options: {
|
|||||||
updater?: ElectronAutoUpdaterLike;
|
updater?: ElectronAutoUpdaterLike;
|
||||||
log: (message: string) => void;
|
log: (message: string) => void;
|
||||||
getChannel?: () => UpdateChannel;
|
getChannel?: () => UpdateChannel;
|
||||||
|
isNativeUpdaterSupported?: () => boolean;
|
||||||
}) {
|
}) {
|
||||||
const getChannel = options.getChannel ?? (() => 'stable' as const);
|
const getChannel = options.getChannel ?? (() => 'stable' as const);
|
||||||
const updater = configureAutoUpdater(
|
const updater = configureAutoUpdater(
|
||||||
@@ -59,6 +160,15 @@ export function createElectronAppUpdater(options: {
|
|||||||
options.log,
|
options.log,
|
||||||
getChannel(),
|
getChannel(),
|
||||||
);
|
);
|
||||||
|
let nativeUpdaterSupported: boolean | null = null;
|
||||||
|
|
||||||
|
function isNativeUpdaterSupported(): boolean {
|
||||||
|
if (!options.isNativeUpdaterSupported) return true;
|
||||||
|
if (nativeUpdaterSupported === null) {
|
||||||
|
nativeUpdaterSupported = options.isNativeUpdaterSupported();
|
||||||
|
}
|
||||||
|
return nativeUpdaterSupported;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async checkForUpdates(channel?: UpdateChannel): Promise<AppUpdateCheckResult> {
|
async checkForUpdates(channel?: UpdateChannel): Promise<AppUpdateCheckResult> {
|
||||||
@@ -69,6 +179,14 @@ export function createElectronAppUpdater(options: {
|
|||||||
canUpdate: false,
|
canUpdate: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!isNativeUpdaterSupported()) {
|
||||||
|
options.log('Skipping native app update check because native updater is unsupported.');
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
version: options.currentVersion,
|
||||||
|
canUpdate: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
configureAutoUpdater(updater, options.log, channel ?? getChannel());
|
configureAutoUpdater(updater, options.log, channel ?? getChannel());
|
||||||
const result = await updater.checkForUpdates();
|
const result = await updater.checkForUpdates();
|
||||||
const version = result?.updateInfo?.version ?? options.currentVersion;
|
const version = result?.updateInfo?.version ?? options.currentVersion;
|
||||||
@@ -83,9 +201,21 @@ export function createElectronAppUpdater(options: {
|
|||||||
options.log('Skipping app update download because this build is not packaged.');
|
options.log('Skipping app update download because this build is not packaged.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isNativeUpdaterSupported()) {
|
||||||
|
options.log('Skipping app update download because native updater is unsupported.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await updater.downloadUpdate();
|
await updater.downloadUpdate();
|
||||||
},
|
},
|
||||||
quitAndInstall(): void {
|
quitAndInstall(): void {
|
||||||
|
if (!options.isPackaged) {
|
||||||
|
options.log('Skipping app update install because this build is not packaged.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isNativeUpdaterSupported()) {
|
||||||
|
options.log('Skipping app update install because native updater is unsupported.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
updater.quitAndInstall(false, true);
|
updater.quitAndInstall(false, true);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,6 +90,32 @@ test('manual update check falls back to GitHub release when app metadata is unav
|
|||||||
assert.deepEqual(calls, ['available-dialog:0.15.0']);
|
assert.deepEqual(calls, ['available-dialog:0.15.0']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('manual update check updates launcher without native download when native updater is unavailable', async () => {
|
||||||
|
const { deps, calls } = createDeps({
|
||||||
|
checkAppUpdate: async () => ({ available: false, version: '0.14.0', canUpdate: false }),
|
||||||
|
fetchLatestStableRelease: async () => ({
|
||||||
|
tag_name: 'v0.15.0',
|
||||||
|
prerelease: false,
|
||||||
|
draft: false,
|
||||||
|
assets: [],
|
||||||
|
}),
|
||||||
|
showUpdateAvailableDialog: async (version) => {
|
||||||
|
calls.push(`available-dialog:${version}`);
|
||||||
|
return 'update';
|
||||||
|
},
|
||||||
|
updateLauncher: async (_launcherPath, channel) => {
|
||||||
|
calls.push(`launcher:${channel}`);
|
||||||
|
return { status: 'skipped' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const service = createUpdateService(deps);
|
||||||
|
|
||||||
|
const result = await service.checkForUpdates({ source: 'manual' });
|
||||||
|
|
||||||
|
assert.equal(result.status, 'updated');
|
||||||
|
assert.deepEqual(calls, ['available-dialog:0.15.0', 'launcher:stable', 'restart-dialog']);
|
||||||
|
});
|
||||||
|
|
||||||
test('automatic update check skips inside configured interval', async () => {
|
test('automatic update check skips inside configured interval', async () => {
|
||||||
const { deps, calls, setState } = createDeps();
|
const { deps, calls, setState } = createDeps();
|
||||||
setState({ lastAutomaticCheckAt: 1_000_000 - 60 * 60 * 1000 });
|
setState({ lastAutomaticCheckAt: 1_000_000 - 60 * 60 * 1000 });
|
||||||
|
|||||||
Reference in New Issue
Block a user