mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
203 lines
8.1 KiB
TypeScript
203 lines
8.1 KiB
TypeScript
import { app, dialog } from 'electron';
|
|
import { execFile } from 'node:child_process';
|
|
import path from 'node:path';
|
|
import type { UpdateChannel, UpdatesConfig } from '../../../types/config';
|
|
import type { OverlayNotificationPayload } from '../../../types/notification';
|
|
import { createElectronAppUpdater, isNativeUpdaterSupported } from './app-updater';
|
|
import { createCurlFetch, createGlobalFetch } from './fetch-adapter';
|
|
import { createCurlHttpExecutor } from './curl-http-executor';
|
|
import { createFetchHttpExecutor } from './fetch-http-executor';
|
|
import {
|
|
fetchLatestStableRelease,
|
|
fetchReleaseAssetBuffer,
|
|
fetchReleaseAssetText,
|
|
findReleaseAsset,
|
|
parseSha256Sums,
|
|
type GitHubRelease,
|
|
} from './release-assets';
|
|
import { shouldFetchReleaseMetadataForPlatform } from './release-metadata-policy';
|
|
import { updateLauncherFromRelease } from './launcher-updater';
|
|
import { notifyUpdateAvailable } from './update-notifications';
|
|
import { createUpdateDialogPresenter } from './update-dialogs';
|
|
import {
|
|
createFileUpdateStateStore,
|
|
createUpdateService,
|
|
takePendingLauncherMigrationPath,
|
|
} from './update-service';
|
|
import { updateSupportAssetsFromRelease } from './support-assets';
|
|
import { runSupportAssetUpdatesForLauncherResult } from './update-support-assets-runtime';
|
|
|
|
const SUBMINER_BUNDLE_ID = 'com.sudacode.SubMiner';
|
|
|
|
export interface UpdateServiceRuntimeDeps {
|
|
userDataPath: string;
|
|
getUpdatesConfig: () => Required<UpdatesConfig>;
|
|
logInfo: (message: string) => void;
|
|
logWarn: (message: string, details?: unknown) => void;
|
|
showOverlayNotification: (payload: OverlayNotificationPayload) => void;
|
|
showDesktopNotification: (title: string, options: { body: string }) => void;
|
|
showMpvOsd: (message: string) => void;
|
|
withStatsWindowLayerSuspendedForNativeDialog: <T>(showDialog: () => Promise<T>) => Promise<T>;
|
|
}
|
|
|
|
export function createUpdateServiceRuntime(deps: UpdateServiceRuntimeDeps): {
|
|
getUpdateService: () => ReturnType<typeof createUpdateService>;
|
|
takePendingLauncherMigrationPath: (
|
|
refresh: Parameters<typeof takePendingLauncherMigrationPath>[1],
|
|
) => Promise<string | undefined>;
|
|
} {
|
|
const updateStateStore = createFileUpdateStateStore(
|
|
path.join(deps.userDataPath, 'update-state.json'),
|
|
);
|
|
let updateService: ReturnType<typeof createUpdateService> | null = null;
|
|
const globalFetchForUpdater = createGlobalFetch();
|
|
const curlFetch = createCurlFetch();
|
|
|
|
function createNativeUpdaterHttpExecutor() {
|
|
if (process.platform === 'win32') {
|
|
return createFetchHttpExecutor();
|
|
}
|
|
return createCurlHttpExecutor();
|
|
}
|
|
|
|
function getFetchForUpdater() {
|
|
if (process.platform === 'win32') return globalFetchForUpdater;
|
|
return curlFetch;
|
|
}
|
|
|
|
async function updateLauncherFromSelectedRelease(
|
|
launcherPath?: string,
|
|
channel: UpdateChannel = deps.getUpdatesConfig().channel,
|
|
release: GitHubRelease | null = null,
|
|
) {
|
|
const fetchForUpdater = getFetchForUpdater();
|
|
if (!release) {
|
|
return { status: 'missing-asset', message: `No ${channel} GitHub release found.` };
|
|
}
|
|
const sumsAsset = findReleaseAsset(release, 'SHA256SUMS.txt');
|
|
if (!sumsAsset) {
|
|
return { status: 'missing-asset', message: 'Release has no SHA256SUMS.txt asset.' };
|
|
}
|
|
const sums = parseSha256Sums(
|
|
await fetchReleaseAssetText(fetchForUpdater, sumsAsset.browser_download_url),
|
|
);
|
|
const launcherResult = await updateLauncherFromRelease({
|
|
release,
|
|
sha256Sums: sums,
|
|
launcherPath,
|
|
downloadAsset: (url) => fetchReleaseAssetBuffer(fetchForUpdater, url),
|
|
deferRecognizedLauncherUpdate: true,
|
|
});
|
|
return runSupportAssetUpdatesForLauncherResult({
|
|
launcherResult,
|
|
assetDescription: 'Support asset update',
|
|
updateSupportAssets: () =>
|
|
updateSupportAssetsFromRelease({
|
|
release,
|
|
sha256Sums: sums,
|
|
downloadAsset: (url) => fetchReleaseAssetBuffer(fetchForUpdater, url),
|
|
}),
|
|
logWarn: (message, details) => deps.logWarn(message, details),
|
|
});
|
|
}
|
|
|
|
function getUpdateService() {
|
|
if (updateService) return updateService;
|
|
const appUpdater = createElectronAppUpdater({
|
|
currentVersion: app.getVersion(),
|
|
isPackaged: app.isPackaged,
|
|
log: (message) => deps.logInfo(message),
|
|
getChannel: () => deps.getUpdatesConfig().channel,
|
|
configureHttpExecutor: createNativeUpdaterHttpExecutor,
|
|
disableDifferentialDownload: true,
|
|
isNativeUpdaterSupported: () =>
|
|
isNativeUpdaterSupported({
|
|
platform: process.platform,
|
|
isPackaged: app.isPackaged,
|
|
execPath: process.execPath,
|
|
env: process.env,
|
|
log: (message) => deps.logWarn(message),
|
|
}),
|
|
});
|
|
const updateDialogPresenter = createUpdateDialogPresenter({
|
|
platform: process.platform,
|
|
focusApp: async () => {
|
|
if (process.platform !== 'darwin') {
|
|
app.focus({ steal: true });
|
|
return;
|
|
}
|
|
try {
|
|
await app.dock?.show();
|
|
} catch (error) {
|
|
deps.logWarn('Failed to show macOS dock before update dialog', error);
|
|
}
|
|
// app.focus({ steal: true }) alone does not reliably activate the process
|
|
// when SubMiner was reached via `subminer -u` (single-instance forwarding
|
|
// from a CLI-spawned child). osascript's `activate` uses LaunchServices,
|
|
// which is the only path that reliably brings the running app forward.
|
|
await new Promise<void>((resolve) => {
|
|
execFile(
|
|
'/usr/bin/osascript',
|
|
['-e', `tell application id "${SUBMINER_BUNDLE_ID}" to activate`],
|
|
{ timeout: 2000 },
|
|
(error) => {
|
|
if (error) {
|
|
deps.logWarn(
|
|
`Failed to activate SubMiner via osascript: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
resolve();
|
|
},
|
|
);
|
|
});
|
|
app.focus({ steal: true });
|
|
},
|
|
withStatsWindowLayerSuspended: (showDialog) =>
|
|
deps.withStatsWindowLayerSuspendedForNativeDialog(showDialog),
|
|
showMessageBox: (options) => dialog.showMessageBox(options),
|
|
});
|
|
updateService = createUpdateService({
|
|
getConfig: () => deps.getUpdatesConfig(),
|
|
getCurrentVersion: () => app.getVersion(),
|
|
now: () => Date.now(),
|
|
stateStore: updateStateStore,
|
|
checkAppUpdate: (channel) => appUpdater.checkForUpdates(channel),
|
|
shouldFetchReleaseMetadata: ({ request, appUpdate }) =>
|
|
shouldFetchReleaseMetadataForPlatform(process.platform, appUpdate, request),
|
|
fetchLatestStableRelease: (channel) =>
|
|
fetchLatestStableRelease({ fetch: getFetchForUpdater(), channel }),
|
|
updateLauncher: (launcherPath, channel, release) =>
|
|
updateLauncherFromSelectedRelease(launcherPath, channel, release),
|
|
showNoUpdateDialog: (version) => updateDialogPresenter.showNoUpdateDialog(version),
|
|
showUpdateAvailableDialog: (version) =>
|
|
updateDialogPresenter.showUpdateAvailableDialog(version),
|
|
showUpdateFailedDialog: (message) => updateDialogPresenter.showUpdateFailedDialog(message),
|
|
showManualUpdateRequiredDialog: (version) =>
|
|
updateDialogPresenter.showManualUpdateRequiredDialog(version),
|
|
downloadAppUpdate: () => appUpdater.downloadUpdate(),
|
|
showRestartDialog: () => updateDialogPresenter.showRestartDialog(),
|
|
quitAndInstall: () => appUpdater.quitAndInstall(),
|
|
notifyUpdateAvailable: (version) =>
|
|
notifyUpdateAvailable(
|
|
{ notificationType: deps.getUpdatesConfig().notificationType, version },
|
|
{
|
|
showSystemNotification: (title, body) => deps.showDesktopNotification(title, { body }),
|
|
showOverlayNotification: (payload) => deps.showOverlayNotification(payload),
|
|
showOsdNotification: (message) => {
|
|
deps.showMpvOsd(message);
|
|
},
|
|
log: (message) => deps.logWarn(message),
|
|
},
|
|
),
|
|
log: (message) => deps.logWarn(message),
|
|
});
|
|
return updateService;
|
|
}
|
|
|
|
return {
|
|
getUpdateService,
|
|
takePendingLauncherMigrationPath: (refresh) =>
|
|
takePendingLauncherMigrationPath(updateStateStore, refresh),
|
|
};
|
|
}
|