739 lines
19 KiB
TypeScript
739 lines
19 KiB
TypeScript
import { app, net } from 'electron';
|
|
import { autoUpdater } from 'electron-updater';
|
|
import { readDesktopSettings, type AutoUpdateMode } from '../desktop-settings';
|
|
import { getMainWindow } from '../window/create-window';
|
|
import {
|
|
compareSemanticVersions,
|
|
normalizeSemanticVersion,
|
|
sortSemanticVersionsDescending
|
|
} from './version';
|
|
|
|
const DEFAULT_POLL_INTERVAL_MINUTES = 30;
|
|
const MINIMUM_POLL_INTERVAL_MINUTES = 5;
|
|
|
|
interface ReleaseManifestEntry {
|
|
feedUrl: string;
|
|
notes?: string;
|
|
publishedAt?: string;
|
|
version: string;
|
|
}
|
|
|
|
interface UpdateVersionInfo {
|
|
version: string;
|
|
}
|
|
|
|
interface ReleaseManifest {
|
|
minimumServerVersion: string | null;
|
|
pollIntervalMinutes: number;
|
|
versions: ReleaseManifestEntry[];
|
|
}
|
|
|
|
export type DesktopUpdateStatus =
|
|
| 'idle'
|
|
| 'disabled'
|
|
| 'checking'
|
|
| 'downloading'
|
|
| 'up-to-date'
|
|
| 'restart-required'
|
|
| 'unsupported'
|
|
| 'no-manifest'
|
|
| 'target-unavailable'
|
|
| 'target-older-than-installed'
|
|
| 'error';
|
|
|
|
export type DesktopUpdateServerVersionStatus =
|
|
| 'unknown'
|
|
| 'reported'
|
|
| 'missing'
|
|
| 'unavailable';
|
|
|
|
export interface DesktopUpdateServerContext {
|
|
manifestUrls: string[];
|
|
serverVersion: string | null;
|
|
serverVersionStatus: DesktopUpdateServerVersionStatus;
|
|
}
|
|
|
|
export interface DesktopUpdateState {
|
|
autoUpdateMode: AutoUpdateMode;
|
|
availableVersions: string[];
|
|
configuredManifestUrls: string[];
|
|
currentVersion: string;
|
|
defaultManifestUrls: string[];
|
|
isSupported: boolean;
|
|
lastCheckedAt: number | null;
|
|
latestVersion: string | null;
|
|
manifestUrl: string | null;
|
|
manifestUrls: string[];
|
|
minimumServerVersion: string | null;
|
|
preferredVersion: string | null;
|
|
restartRequired: boolean;
|
|
serverBlocked: boolean;
|
|
serverBlockMessage: string | null;
|
|
serverVersion: string | null;
|
|
serverVersionStatus: DesktopUpdateServerVersionStatus;
|
|
status: DesktopUpdateStatus;
|
|
statusMessage: string | null;
|
|
targetVersion: string | null;
|
|
}
|
|
|
|
export const AUTO_UPDATE_STATE_CHANGED_CHANNEL = 'auto-update-state-changed';
|
|
|
|
let currentCheckPromise: Promise<void> | null = null;
|
|
let currentContext: DesktopUpdateServerContext = {
|
|
manifestUrls: [],
|
|
serverVersion: null,
|
|
serverVersionStatus: 'unknown'
|
|
};
|
|
let desktopUpdateState: DesktopUpdateState = createInitialState();
|
|
let periodicRefreshTimer: NodeJS.Timeout | null = null;
|
|
let refreshGeneration = 0;
|
|
let updaterInitialized = false;
|
|
|
|
function createInitialState(): DesktopUpdateState {
|
|
const settings = readDesktopSettings();
|
|
|
|
return {
|
|
autoUpdateMode: settings.autoUpdateMode,
|
|
availableVersions: [],
|
|
configuredManifestUrls: settings.manifestUrls,
|
|
currentVersion: app.getVersion(),
|
|
defaultManifestUrls: [],
|
|
isSupported: app.isPackaged,
|
|
lastCheckedAt: null,
|
|
latestVersion: null,
|
|
manifestUrl: null,
|
|
manifestUrls: settings.manifestUrls,
|
|
minimumServerVersion: null,
|
|
preferredVersion: settings.preferredVersion,
|
|
restartRequired: false,
|
|
serverBlocked: false,
|
|
serverBlockMessage: null,
|
|
serverVersion: null,
|
|
serverVersionStatus: 'unknown',
|
|
status: 'idle',
|
|
statusMessage: null,
|
|
targetVersion: null
|
|
};
|
|
}
|
|
|
|
function clearPeriodicRefreshTimer(): void {
|
|
if (!periodicRefreshTimer) {
|
|
return;
|
|
}
|
|
|
|
clearInterval(periodicRefreshTimer);
|
|
periodicRefreshTimer = null;
|
|
}
|
|
|
|
function emitState(): void {
|
|
const mainWindow = getMainWindow();
|
|
|
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
return;
|
|
}
|
|
|
|
mainWindow.webContents.send(AUTO_UPDATE_STATE_CHANGED_CHANNEL, desktopUpdateState);
|
|
}
|
|
|
|
function setDesktopUpdateState(patch: Partial<DesktopUpdateState>): void {
|
|
desktopUpdateState = {
|
|
...desktopUpdateState,
|
|
...patch,
|
|
currentVersion: app.getVersion()
|
|
};
|
|
|
|
emitState();
|
|
}
|
|
|
|
function sanitizeHttpUrl(rawValue: unknown): string | null {
|
|
if (typeof rawValue !== 'string') {
|
|
return null;
|
|
}
|
|
|
|
const trimmedValue = rawValue.trim();
|
|
|
|
if (!trimmedValue) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const parsedUrl = new URL(trimmedValue);
|
|
|
|
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'
|
|
? parsedUrl.toString().replace(/\/$/, '')
|
|
: null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function sanitizeHttpUrls(rawValue: unknown): string[] {
|
|
if (!Array.isArray(rawValue)) {
|
|
return [];
|
|
}
|
|
|
|
const manifestUrls: string[] = [];
|
|
|
|
for (const entry of rawValue) {
|
|
const manifestUrl = sanitizeHttpUrl(entry);
|
|
|
|
if (!manifestUrl || manifestUrls.includes(manifestUrl)) {
|
|
continue;
|
|
}
|
|
|
|
manifestUrls.push(manifestUrl);
|
|
}
|
|
|
|
return manifestUrls;
|
|
}
|
|
|
|
function getEffectiveManifestUrls(configuredManifestUrls: string[]): string[] {
|
|
return configuredManifestUrls.length > 0
|
|
? [...configuredManifestUrls]
|
|
: [...currentContext.manifestUrls];
|
|
}
|
|
|
|
function coercePollIntervalMinutes(value: unknown): number {
|
|
return typeof value === 'number' && Number.isFinite(value)
|
|
? Math.max(MINIMUM_POLL_INTERVAL_MINUTES, Math.round(value))
|
|
: DEFAULT_POLL_INTERVAL_MINUTES;
|
|
}
|
|
|
|
function parseReleaseManifest(payload: unknown): ReleaseManifest {
|
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
throw new Error('Release manifest must be a JSON object.');
|
|
}
|
|
|
|
const rawManifest = payload as Record<string, unknown>;
|
|
const rawVersions = Array.isArray(rawManifest.versions) ? rawManifest.versions : [];
|
|
const parsedVersions: ReleaseManifestEntry[] = [];
|
|
|
|
for (const entry of rawVersions) {
|
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
continue;
|
|
}
|
|
|
|
const rawEntry = entry as Record<string, unknown>;
|
|
const version = normalizeSemanticVersion(rawEntry.version as string | undefined);
|
|
const feedUrl = sanitizeHttpUrl(rawEntry.feedUrl);
|
|
|
|
if (!version || !feedUrl) {
|
|
continue;
|
|
}
|
|
|
|
const notes = typeof rawEntry.notes === 'string' && rawEntry.notes.trim().length > 0
|
|
? rawEntry.notes.trim()
|
|
: undefined;
|
|
const publishedAt = typeof rawEntry.publishedAt === 'string'
|
|
&& rawEntry.publishedAt.trim().length > 0
|
|
? rawEntry.publishedAt.trim()
|
|
: undefined;
|
|
|
|
parsedVersions.push({
|
|
feedUrl,
|
|
...(notes ? { notes } : {}),
|
|
...(publishedAt ? { publishedAt } : {}),
|
|
version
|
|
});
|
|
}
|
|
|
|
parsedVersions.sort((left, right) => compareSemanticVersions(right.version, left.version));
|
|
|
|
const deduplicatedVersions: ReleaseManifestEntry[] = [];
|
|
const seenVersions = new Set<string>();
|
|
|
|
for (const entry of parsedVersions) {
|
|
if (seenVersions.has(entry.version)) {
|
|
continue;
|
|
}
|
|
|
|
seenVersions.add(entry.version);
|
|
deduplicatedVersions.push(entry);
|
|
}
|
|
|
|
if (deduplicatedVersions.length === 0) {
|
|
throw new Error('Release manifest does not contain any valid versions.');
|
|
}
|
|
|
|
return {
|
|
minimumServerVersion: normalizeSemanticVersion(
|
|
rawManifest.minimumServerVersion as string | undefined
|
|
),
|
|
pollIntervalMinutes: coercePollIntervalMinutes(rawManifest.pollIntervalMinutes),
|
|
versions: deduplicatedVersions
|
|
};
|
|
}
|
|
|
|
function resolveServerCompatibility(minimumServerVersion: string | null): {
|
|
blocked: boolean;
|
|
message: string | null;
|
|
} {
|
|
if (currentContext.serverVersionStatus === 'missing') {
|
|
return {
|
|
blocked: true,
|
|
message: 'The connected server is too old. Update the server project to a version that reports its release metadata.'
|
|
};
|
|
}
|
|
|
|
if (
|
|
currentContext.serverVersionStatus !== 'reported'
|
|
|| !minimumServerVersion
|
|
|| !currentContext.serverVersion
|
|
) {
|
|
return { blocked: false,
|
|
message: null };
|
|
}
|
|
|
|
if (compareSemanticVersions(currentContext.serverVersion, minimumServerVersion) >= 0) {
|
|
return { blocked: false,
|
|
message: null };
|
|
}
|
|
|
|
return {
|
|
blocked: true,
|
|
message: `This desktop app requires server version ${minimumServerVersion} or newer. The connected server is ${currentContext.serverVersion}.`
|
|
};
|
|
}
|
|
|
|
function getManifestMissingMessage(autoUpdateMode: AutoUpdateMode): string {
|
|
return autoUpdateMode === 'off'
|
|
? 'Automatic updates are turned off.'
|
|
: 'No release manifest URLs are configured.';
|
|
}
|
|
|
|
function getTargetUnavailableMessage(
|
|
autoUpdateMode: AutoUpdateMode,
|
|
preferredVersion: string | null
|
|
): string {
|
|
if (autoUpdateMode === 'version') {
|
|
return preferredVersion
|
|
? `The selected version ${preferredVersion} is not present in the release manifest.`
|
|
: 'Select a version before enabling pinned updates.';
|
|
}
|
|
|
|
return 'No compatible release was found in the release manifest.';
|
|
}
|
|
|
|
function getFriendlyErrorMessage(error: unknown): string {
|
|
const rawMessage = error instanceof Error
|
|
? error.message
|
|
: typeof error === 'string'
|
|
? error
|
|
: 'Unknown error';
|
|
|
|
if (rawMessage.includes('APPIMAGE')) {
|
|
return 'Automatic updates on Linux require the packaged AppImage or DEB build.';
|
|
}
|
|
|
|
return rawMessage;
|
|
}
|
|
|
|
function getSelectedRelease(manifest: ReleaseManifest): ReleaseManifestEntry | null {
|
|
const settings = readDesktopSettings();
|
|
|
|
if (settings.autoUpdateMode === 'version') {
|
|
const preferredVersion = normalizeSemanticVersion(settings.preferredVersion);
|
|
|
|
if (!preferredVersion) {
|
|
return null;
|
|
}
|
|
|
|
return manifest.versions.find((entry) => entry.version === preferredVersion) ?? null;
|
|
}
|
|
|
|
return manifest.versions[0] ?? null;
|
|
}
|
|
|
|
function refreshSettingsSnapshot(): void {
|
|
const settings = readDesktopSettings();
|
|
const defaultManifestUrls = [...currentContext.manifestUrls];
|
|
const manifestUrls = getEffectiveManifestUrls(settings.manifestUrls);
|
|
|
|
setDesktopUpdateState({
|
|
autoUpdateMode: settings.autoUpdateMode,
|
|
configuredManifestUrls: settings.manifestUrls,
|
|
currentVersion: app.getVersion(),
|
|
defaultManifestUrls,
|
|
isSupported: app.isPackaged,
|
|
manifestUrls,
|
|
preferredVersion: settings.preferredVersion
|
|
});
|
|
}
|
|
|
|
function schedulePeriodicRefresh(pollIntervalMinutes: number): void {
|
|
clearPeriodicRefreshTimer();
|
|
|
|
if (!app.isPackaged || desktopUpdateState.autoUpdateMode === 'off' || desktopUpdateState.restartRequired) {
|
|
return;
|
|
}
|
|
|
|
periodicRefreshTimer = setInterval(() => {
|
|
void refreshDesktopUpdater('scheduled');
|
|
}, pollIntervalMinutes * 60_000);
|
|
}
|
|
|
|
async function loadReleaseManifest(manifestUrl: string): Promise<ReleaseManifest> {
|
|
const response = await net.fetch(manifestUrl, {
|
|
headers: {
|
|
accept: 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Release manifest request failed with status ${response.status}.`);
|
|
}
|
|
|
|
const payload = await response.json();
|
|
|
|
return parseReleaseManifest(payload);
|
|
}
|
|
|
|
function formatManifestLoadErrors(errors: string[]): string {
|
|
if (errors.length === 0) {
|
|
return 'No valid release manifest could be loaded.';
|
|
}
|
|
|
|
if (errors.length === 1) {
|
|
return errors[0];
|
|
}
|
|
|
|
const remainingCount = errors.length - 1;
|
|
|
|
return `${errors[0]} (${remainingCount} more manifest URL${remainingCount === 1 ? '' : 's'} failed.)`;
|
|
}
|
|
|
|
async function loadReleaseManifestFromCandidates(manifestUrls: string[]): Promise<{
|
|
manifest: ReleaseManifest;
|
|
manifestUrl: string;
|
|
}> {
|
|
const errors: string[] = [];
|
|
|
|
for (const manifestUrl of manifestUrls) {
|
|
try {
|
|
const manifest = await loadReleaseManifest(manifestUrl);
|
|
|
|
return {
|
|
manifest,
|
|
manifestUrl
|
|
};
|
|
} catch (error) {
|
|
errors.push(`${manifestUrl}: ${getFriendlyErrorMessage(error)}`);
|
|
}
|
|
}
|
|
|
|
throw new Error(formatManifestLoadErrors(errors));
|
|
}
|
|
|
|
async function performUpdateCheck(
|
|
targetRelease: ReleaseManifestEntry,
|
|
generation: number
|
|
): Promise<void> {
|
|
if (generation !== refreshGeneration || desktopUpdateState.restartRequired) {
|
|
return;
|
|
}
|
|
|
|
if (currentCheckPromise) {
|
|
await currentCheckPromise;
|
|
return;
|
|
}
|
|
|
|
currentCheckPromise = (async () => {
|
|
autoUpdater.autoDownload = true;
|
|
autoUpdater.autoInstallOnAppQuit = true;
|
|
autoUpdater.allowDowngrade = false;
|
|
autoUpdater.setFeedURL({ provider: 'generic', url: targetRelease.feedUrl });
|
|
|
|
setDesktopUpdateState({
|
|
lastCheckedAt: Date.now(),
|
|
status: 'checking',
|
|
statusMessage: `Checking for MetoYou ${targetRelease.version}…`,
|
|
targetVersion: targetRelease.version
|
|
});
|
|
|
|
await autoUpdater.checkForUpdates();
|
|
})();
|
|
|
|
try {
|
|
await currentCheckPromise;
|
|
} finally {
|
|
currentCheckPromise = null;
|
|
}
|
|
}
|
|
|
|
async function refreshDesktopUpdater(
|
|
_reason: 'context-changed' | 'manual' | 'scheduled' | 'settings-changed'
|
|
): Promise<void> {
|
|
const generation = ++refreshGeneration;
|
|
|
|
refreshSettingsSnapshot();
|
|
|
|
const manifestUrls = [...desktopUpdateState.manifestUrls];
|
|
const baseServerCompatibility = resolveServerCompatibility(null);
|
|
|
|
setDesktopUpdateState({
|
|
manifestUrl: null,
|
|
serverBlocked: baseServerCompatibility.blocked,
|
|
serverBlockMessage: baseServerCompatibility.message,
|
|
serverVersion: currentContext.serverVersion,
|
|
serverVersionStatus: currentContext.serverVersionStatus
|
|
});
|
|
|
|
if (manifestUrls.length === 0) {
|
|
clearPeriodicRefreshTimer();
|
|
|
|
if (desktopUpdateState.restartRequired) {
|
|
setDesktopUpdateState({
|
|
availableVersions: [],
|
|
latestVersion: null,
|
|
manifestUrl: null,
|
|
minimumServerVersion: null
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
setDesktopUpdateState({
|
|
availableVersions: [],
|
|
latestVersion: null,
|
|
manifestUrl: null,
|
|
minimumServerVersion: null,
|
|
status: app.isPackaged && desktopUpdateState.autoUpdateMode !== 'off'
|
|
? 'no-manifest'
|
|
: 'disabled',
|
|
statusMessage: getManifestMissingMessage(desktopUpdateState.autoUpdateMode),
|
|
targetVersion: null
|
|
});
|
|
|
|
if (!app.isPackaged && desktopUpdateState.autoUpdateMode !== 'off') {
|
|
setDesktopUpdateState({
|
|
status: 'unsupported',
|
|
statusMessage: 'Automatic updates only work in packaged desktop builds.'
|
|
});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const manifestResult = await loadReleaseManifestFromCandidates(manifestUrls);
|
|
|
|
if (generation !== refreshGeneration) {
|
|
return;
|
|
}
|
|
|
|
const { manifest, manifestUrl } = manifestResult;
|
|
const availableVersions = sortSemanticVersionsDescending(
|
|
manifest.versions.map((entry) => entry.version)
|
|
);
|
|
const latestVersion = availableVersions[0] ?? null;
|
|
const selectedRelease = getSelectedRelease(manifest);
|
|
const serverCompatibility = resolveServerCompatibility(
|
|
manifest.minimumServerVersion
|
|
);
|
|
|
|
setDesktopUpdateState({
|
|
availableVersions,
|
|
latestVersion,
|
|
manifestUrl,
|
|
minimumServerVersion: manifest.minimumServerVersion,
|
|
serverBlocked: serverCompatibility.blocked,
|
|
serverBlockMessage: serverCompatibility.message,
|
|
targetVersion: selectedRelease?.version ?? null
|
|
});
|
|
|
|
if (desktopUpdateState.restartRequired) {
|
|
clearPeriodicRefreshTimer();
|
|
return;
|
|
}
|
|
|
|
if (!app.isPackaged) {
|
|
clearPeriodicRefreshTimer();
|
|
|
|
setDesktopUpdateState({
|
|
status: 'unsupported',
|
|
statusMessage: 'Automatic updates only work in packaged desktop builds.'
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (desktopUpdateState.autoUpdateMode === 'off') {
|
|
clearPeriodicRefreshTimer();
|
|
|
|
setDesktopUpdateState({
|
|
status: 'disabled',
|
|
statusMessage: 'Automatic updates are turned off.'
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (!selectedRelease) {
|
|
clearPeriodicRefreshTimer();
|
|
|
|
setDesktopUpdateState({
|
|
status: 'target-unavailable',
|
|
statusMessage: getTargetUnavailableMessage(
|
|
desktopUpdateState.autoUpdateMode,
|
|
desktopUpdateState.preferredVersion
|
|
)
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (compareSemanticVersions(selectedRelease.version, app.getVersion()) < 0) {
|
|
clearPeriodicRefreshTimer();
|
|
|
|
setDesktopUpdateState({
|
|
status: 'target-older-than-installed',
|
|
statusMessage: `MetoYou ${app.getVersion()} is newer than ${selectedRelease.version}. Downgrades are not applied automatically.`,
|
|
targetVersion: selectedRelease.version
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
schedulePeriodicRefresh(manifest.pollIntervalMinutes);
|
|
await performUpdateCheck(selectedRelease, generation);
|
|
} catch (error) {
|
|
if (generation !== refreshGeneration) {
|
|
return;
|
|
}
|
|
|
|
clearPeriodicRefreshTimer();
|
|
|
|
setDesktopUpdateState({
|
|
availableVersions: [],
|
|
latestVersion: null,
|
|
manifestUrl: null,
|
|
minimumServerVersion: null,
|
|
status: 'error',
|
|
statusMessage: getFriendlyErrorMessage(error),
|
|
targetVersion: null
|
|
});
|
|
}
|
|
}
|
|
|
|
export function getDesktopUpdateState(): DesktopUpdateState {
|
|
return desktopUpdateState;
|
|
}
|
|
|
|
export function initializeDesktopUpdater(): void {
|
|
if (updaterInitialized) {
|
|
return;
|
|
}
|
|
|
|
updaterInitialized = true;
|
|
|
|
autoUpdater.on('checking-for-update', () => {
|
|
if (desktopUpdateState.restartRequired) {
|
|
return;
|
|
}
|
|
|
|
setDesktopUpdateState({
|
|
status: 'checking',
|
|
statusMessage: 'Checking for desktop updates…'
|
|
});
|
|
});
|
|
|
|
autoUpdater.on('update-available', (updateInfo: UpdateVersionInfo) => {
|
|
const nextVersion = normalizeSemanticVersion(updateInfo.version)
|
|
?? desktopUpdateState.targetVersion;
|
|
|
|
setDesktopUpdateState({
|
|
lastCheckedAt: Date.now(),
|
|
status: 'downloading',
|
|
statusMessage: `Downloading MetoYou ${nextVersion ?? 'update'}…`,
|
|
targetVersion: nextVersion
|
|
});
|
|
});
|
|
|
|
autoUpdater.on('update-not-available', () => {
|
|
if (desktopUpdateState.restartRequired) {
|
|
return;
|
|
}
|
|
|
|
const isPinnedVersion = desktopUpdateState.autoUpdateMode === 'version'
|
|
&& !!desktopUpdateState.targetVersion;
|
|
|
|
setDesktopUpdateState({
|
|
lastCheckedAt: Date.now(),
|
|
status: 'up-to-date',
|
|
statusMessage: isPinnedVersion
|
|
? `MetoYou ${desktopUpdateState.targetVersion} is already installed.`
|
|
: 'MetoYou is up to date.'
|
|
});
|
|
});
|
|
|
|
autoUpdater.on('update-downloaded', (updateInfo: UpdateVersionInfo) => {
|
|
clearPeriodicRefreshTimer();
|
|
|
|
const nextVersion = normalizeSemanticVersion(updateInfo.version)
|
|
?? desktopUpdateState.targetVersion;
|
|
|
|
setDesktopUpdateState({
|
|
lastCheckedAt: Date.now(),
|
|
restartRequired: true,
|
|
status: 'restart-required',
|
|
statusMessage: `MetoYou ${nextVersion ?? 'update'} is ready. Restart the app to finish installing it.`,
|
|
targetVersion: nextVersion
|
|
});
|
|
});
|
|
|
|
autoUpdater.on('error', (error: unknown) => {
|
|
if (desktopUpdateState.restartRequired) {
|
|
return;
|
|
}
|
|
|
|
setDesktopUpdateState({
|
|
lastCheckedAt: Date.now(),
|
|
status: 'error',
|
|
statusMessage: getFriendlyErrorMessage(error)
|
|
});
|
|
});
|
|
|
|
refreshSettingsSnapshot();
|
|
}
|
|
|
|
export async function configureDesktopUpdaterContext(
|
|
context: Partial<DesktopUpdateServerContext>
|
|
): Promise<DesktopUpdateState> {
|
|
initializeDesktopUpdater();
|
|
|
|
const manifestUrls = sanitizeHttpUrls(context.manifestUrls);
|
|
|
|
currentContext = {
|
|
manifestUrls,
|
|
serverVersion: normalizeSemanticVersion(context.serverVersion),
|
|
serverVersionStatus: context.serverVersionStatus ?? 'unknown'
|
|
};
|
|
|
|
await refreshDesktopUpdater('context-changed');
|
|
return desktopUpdateState;
|
|
}
|
|
|
|
export async function handleDesktopSettingsChanged(): Promise<void> {
|
|
initializeDesktopUpdater();
|
|
await refreshDesktopUpdater('settings-changed');
|
|
}
|
|
|
|
export async function checkForDesktopUpdates(): Promise<DesktopUpdateState> {
|
|
initializeDesktopUpdater();
|
|
await refreshDesktopUpdater('manual');
|
|
return desktopUpdateState;
|
|
}
|
|
|
|
export function restartToApplyUpdate(): boolean {
|
|
if (!desktopUpdateState.restartRequired) {
|
|
return false;
|
|
}
|
|
|
|
autoUpdater.quitAndInstall(true, true);
|
|
return true;
|
|
}
|
|
|
|
export function shutdownDesktopUpdater(): void {
|
|
clearPeriodicRefreshTimer();
|
|
}
|