mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-28 00:55:16 -07:00
feat(macos): configuration window + curl-backed macOS updater (#71)
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
'use strict';
|
||||
var __importDefault =
|
||||
(this && this.__importDefault) ||
|
||||
function (mod) {
|
||||
return mod && mod.__esModule ? mod : { default: mod };
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.createUpdateService = createUpdateService;
|
||||
exports.createFileUpdateStateStore = createFileUpdateStateStore;
|
||||
const node_fs_1 = __importDefault(require('node:fs'));
|
||||
const node_path_1 = __importDefault(require('node:path'));
|
||||
const release_assets_1 = require('./release-assets');
|
||||
function getBestLatestVersion(currentVersion, appUpdate, release) {
|
||||
const releaseVersion = (0, release_assets_1.parseReleaseVersion)(release);
|
||||
const candidates = [appUpdate.version, releaseVersion].filter(
|
||||
(value) => typeof value === 'string' && value.length > 0,
|
||||
);
|
||||
const latest = candidates.reduce(
|
||||
(best, candidate) =>
|
||||
(0, release_assets_1.compareSemverLike)(candidate, best) > 0 ? candidate : best,
|
||||
currentVersion,
|
||||
);
|
||||
return {
|
||||
available:
|
||||
appUpdate.available || (0, release_assets_1.compareSemverLike)(latest, currentVersion) > 0,
|
||||
version: latest,
|
||||
};
|
||||
}
|
||||
function shouldSkipAutomaticCheck(config, state, now) {
|
||||
if (!config.enabled) return true;
|
||||
if (!state.lastAutomaticCheckAt) return false;
|
||||
const intervalMs = Math.max(1, config.checkIntervalHours) * 60 * 60 * 1000;
|
||||
return now - state.lastAutomaticCheckAt < intervalMs;
|
||||
}
|
||||
function summarizeError(error) {
|
||||
const raw = error instanceof Error ? error.message : String(error);
|
||||
const firstLine = raw
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.length > 0);
|
||||
return firstLine ?? 'unknown error';
|
||||
}
|
||||
function createUpdateService(deps) {
|
||||
const inFlightBySource = new Map();
|
||||
async function runCheck(request) {
|
||||
const now = deps.now();
|
||||
const config = deps.getConfig();
|
||||
const channel = config.channel;
|
||||
const state = await deps.readState();
|
||||
const isAutomatic = request.source === 'automatic';
|
||||
if (isAutomatic && !request.force && shouldSkipAutomaticCheck(config, state, now)) {
|
||||
return { status: 'skipped' };
|
||||
}
|
||||
try {
|
||||
const appUpdate = await deps.checkAppUpdate(channel).catch((error) => {
|
||||
if (isAutomatic) {
|
||||
deps.log(`App update metadata check failed: ${summarizeError(error)}`);
|
||||
}
|
||||
return {
|
||||
available: false,
|
||||
version: deps.getCurrentVersion(),
|
||||
};
|
||||
});
|
||||
const shouldFetchReleaseMetadata =
|
||||
deps.shouldFetchReleaseMetadata?.({ request, channel, appUpdate }) ?? true;
|
||||
const release = shouldFetchReleaseMetadata
|
||||
? await deps.fetchLatestStableRelease(channel).catch((error) => {
|
||||
deps.log(`GitHub release update check failed: ${summarizeError(error)}`);
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
const currentVersion = deps.getCurrentVersion();
|
||||
const latest = getBestLatestVersion(currentVersion, appUpdate, release);
|
||||
if (isAutomatic) {
|
||||
const nextState = {
|
||||
...state,
|
||||
lastAutomaticCheckAt: now,
|
||||
};
|
||||
if (latest.available && state.lastNotifiedVersion !== latest.version) {
|
||||
await deps.notifyUpdateAvailable(latest.version);
|
||||
nextState.lastNotifiedVersion = latest.version;
|
||||
}
|
||||
await deps.writeState(nextState);
|
||||
}
|
||||
if (!latest.available) {
|
||||
if (!isAutomatic) {
|
||||
await deps.showNoUpdateDialog(currentVersion);
|
||||
}
|
||||
return { status: 'up-to-date', version: currentVersion };
|
||||
}
|
||||
if (isAutomatic) {
|
||||
return { status: 'update-available', version: latest.version };
|
||||
}
|
||||
const choice = await deps.showUpdateAvailableDialog(latest.version);
|
||||
if (choice === 'close') {
|
||||
return { status: 'update-available', version: latest.version };
|
||||
}
|
||||
const canInstallAppUpdate = appUpdate.available && appUpdate.canUpdate !== false;
|
||||
let appUpdateApplied = false;
|
||||
if (canInstallAppUpdate) {
|
||||
await deps.downloadAppUpdate();
|
||||
appUpdateApplied = true;
|
||||
}
|
||||
const launcherResult = await deps.updateLauncher(request.launcherPath, channel, release);
|
||||
if (launcherResult.status === 'protected' && launcherResult.command) {
|
||||
deps.log(`Launcher update requires manual command: ${launcherResult.command}`);
|
||||
}
|
||||
if (!appUpdateApplied) {
|
||||
await deps.showManualUpdateRequiredDialog(latest.version);
|
||||
return { status: 'update-available', version: latest.version };
|
||||
}
|
||||
const restartChoice = await deps.showRestartDialog();
|
||||
if (restartChoice === 'restart') {
|
||||
await deps.quitAndInstall();
|
||||
}
|
||||
return { status: 'updated', version: latest.version };
|
||||
} catch (error) {
|
||||
const message = summarizeError(error);
|
||||
if (isAutomatic) {
|
||||
deps.log(`Automatic update check failed: ${message}`);
|
||||
} else {
|
||||
await deps.showUpdateFailedDialog(message);
|
||||
}
|
||||
return { status: 'failed', error: message };
|
||||
}
|
||||
}
|
||||
return {
|
||||
checkForUpdates(request) {
|
||||
const inFlight = inFlightBySource.get(request.source);
|
||||
if (inFlight) return inFlight;
|
||||
const nextInFlight = runCheck(request).finally(() => {
|
||||
inFlightBySource.delete(request.source);
|
||||
});
|
||||
inFlightBySource.set(request.source, nextInFlight);
|
||||
return nextInFlight;
|
||||
},
|
||||
startAutomaticChecks(options = {}) {
|
||||
const setTimeoutFn = deps.setTimeout ?? setTimeout;
|
||||
const setIntervalFn = deps.setInterval ?? setInterval;
|
||||
const startupDelayMs = options.startupDelayMs ?? 15_000;
|
||||
const pollIntervalMs = options.pollIntervalMs ?? 60 * 60 * 1000;
|
||||
setTimeoutFn(() => {
|
||||
void this.checkForUpdates({ source: 'automatic' });
|
||||
}, startupDelayMs);
|
||||
setIntervalFn(() => {
|
||||
void this.checkForUpdates({ source: 'automatic' });
|
||||
}, pollIntervalMs);
|
||||
},
|
||||
};
|
||||
}
|
||||
function createFileUpdateStateStore(statePath) {
|
||||
return {
|
||||
async readState() {
|
||||
try {
|
||||
return JSON.parse(await node_fs_1.default.promises.readFile(statePath, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
async writeState(state) {
|
||||
await node_fs_1.default.promises.mkdir(node_path_1.default.dirname(statePath), {
|
||||
recursive: true,
|
||||
});
|
||||
await node_fs_1.default.promises.writeFile(
|
||||
statePath,
|
||||
`${JSON.stringify(state, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=update-service.js.map
|
||||
Reference in New Issue
Block a user