516 lines
21 KiB
TypeScript
516 lines
21 KiB
TypeScript
import { computed, ref, watch, onScopeDispose } from "vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
|
import * as api from "@/lib/backend/api";
|
|
import { useSettingsStore } from "@/stores/settingsStore";
|
|
import type { UpdateDownloadSource as SettingsUpdateDownloadSource } from "@/stores/settingsStore";
|
|
import type { UpdateDownloadProgress } from "@/lib/backend/tauri";
|
|
import { currentLocale } from "@/i18n";
|
|
import { shouldBlockAppUpdate } from "@/lib/app/appUpdateTaskGuard";
|
|
import { uuid } from "@/lib/common/utils";
|
|
import { isUpdatePreviewMockEnabled, previewAppUpdateInfo } from "@/lib/updates/updatePreviewMock";
|
|
|
|
interface UseAppUpdaterOptions {
|
|
getActiveTaskCount?: () => number;
|
|
prepareForUpdate?: () => Promise<() => void>;
|
|
}
|
|
|
|
export function shouldOpenUpdateDialog(options: { silent?: boolean }) {
|
|
return options.silent !== true;
|
|
}
|
|
|
|
export function canDownloadAndInstallUpdate(info: api.UpdateInfo | null, isDesktop: boolean) {
|
|
return isDesktop && info?.update_available === true && info.manual_update_only !== true;
|
|
}
|
|
|
|
interface ParsedUpdateVersion {
|
|
core: [string, string, string];
|
|
prerelease: string[];
|
|
}
|
|
|
|
function normalizeUpdateVersion(version: string): string {
|
|
return version.trim().replace(/^[vV]/, "");
|
|
}
|
|
|
|
function parseUpdateVersion(version: string): ParsedUpdateVersion | null {
|
|
const match = normalizeUpdateVersion(version).match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
|
|
if (!match) return null;
|
|
return {
|
|
core: [match[1], match[2], match[3]],
|
|
prerelease: match[4]?.split(".") ?? [],
|
|
};
|
|
}
|
|
|
|
function compareNumericVersionIdentifier(left: string, right: string): number {
|
|
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
|
return left === right ? 0 : left < right ? -1 : 1;
|
|
}
|
|
|
|
function compareParsedUpdateVersions(left: ParsedUpdateVersion, right: ParsedUpdateVersion): number {
|
|
for (let index = 0; index < left.core.length; index += 1) {
|
|
const comparison = compareNumericVersionIdentifier(left.core[index], right.core[index]);
|
|
if (comparison !== 0) return comparison;
|
|
}
|
|
if (!left.prerelease.length || !right.prerelease.length) {
|
|
if (left.prerelease.length === right.prerelease.length) return 0;
|
|
return left.prerelease.length ? -1 : 1;
|
|
}
|
|
const length = Math.max(left.prerelease.length, right.prerelease.length);
|
|
for (let index = 0; index < length; index += 1) {
|
|
const leftIdentifier = left.prerelease[index];
|
|
const rightIdentifier = right.prerelease[index];
|
|
if (leftIdentifier === undefined || rightIdentifier === undefined) return leftIdentifier === undefined ? -1 : 1;
|
|
if (leftIdentifier === rightIdentifier) continue;
|
|
const leftNumeric = /^\d+$/.test(leftIdentifier);
|
|
const rightNumeric = /^\d+$/.test(rightIdentifier);
|
|
if (leftNumeric && rightNumeric) return compareNumericVersionIdentifier(leftIdentifier, rightIdentifier);
|
|
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
return leftIdentifier < rightIdentifier ? -1 : 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
export function isUpdateIgnored(info: api.UpdateInfo | null, ignoredVersion: string | undefined): boolean {
|
|
const latest = info?.latest_version;
|
|
if (!latest || !ignoredVersion) return false;
|
|
const parsedLatest = parseUpdateVersion(latest);
|
|
const parsedIgnored = parseUpdateVersion(ignoredVersion);
|
|
if (!parsedLatest || !parsedIgnored) return normalizeUpdateVersion(latest) === normalizeUpdateVersion(ignoredVersion);
|
|
return compareParsedUpdateVersions(parsedLatest, parsedIgnored) <= 0;
|
|
}
|
|
|
|
export function isNewerRemoteVersion(latest: string, cached: string): boolean {
|
|
const parsedLatest = parseUpdateVersion(latest);
|
|
const parsedCached = parseUpdateVersion(cached);
|
|
if (!parsedLatest || !parsedCached) return normalizeUpdateVersion(latest) !== normalizeUpdateVersion(cached);
|
|
return compareParsedUpdateVersions(parsedLatest, parsedCached) > 0;
|
|
}
|
|
|
|
export function normalizeUpdateDownloadSource(value: unknown): SettingsUpdateDownloadSource {
|
|
// Old persisted AtomGit preferences should retain their mainland mirror behavior.
|
|
if (value === "atomgit") return "cnb";
|
|
return value === "cnb" ? "cnb" : "official";
|
|
}
|
|
|
|
export function tagVersion(version: string): string {
|
|
const trimmed = version.trim();
|
|
return trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
|
|
}
|
|
|
|
export function resolveUpdateReleaseUrl(info: api.UpdateInfo | null, source: unknown, fallbackUrl: string): string {
|
|
const normalizedSource = normalizeUpdateDownloadSource(source);
|
|
if (normalizedSource === "cnb" && info?.latest_version) {
|
|
return `https://cnb.cool/dbxio.com/dbx/-/releases/tag/${tagVersion(info.latest_version)}`;
|
|
}
|
|
if (normalizedSource === "cnb") return "https://cnb.cool/dbxio.com/dbx/-/releases";
|
|
return info?.release_url || fallbackUrl;
|
|
}
|
|
|
|
export async function resolveUpdaterProxy(): Promise<string | undefined> {
|
|
if (!isTauriRuntime()) return undefined;
|
|
try {
|
|
const proxy = await api.getSystemProxyUrl();
|
|
return proxy || undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
|
const { t } = useI18n();
|
|
const settingsStore = useSettingsStore();
|
|
type Phase = "idle" | "checking" | "downloading" | "ready" | "preparing" | "installing" | "restart";
|
|
const phase = ref<Phase>("idle");
|
|
const downloaded = ref<api.DownloadedUpdate | null>(null);
|
|
const updateInfo = ref<api.UpdateInfo | null>(null);
|
|
const updateCheckMessage = ref("");
|
|
const updateCheckFailed = ref(false);
|
|
const showUpdateDialog = ref(false);
|
|
const downloadProgress = ref<number | null>(null);
|
|
const isIgnoringUpdate = ref(false);
|
|
const checkingUpdates = computed(() => phase.value === "checking");
|
|
const isDownloadingUpdate = computed(() => phase.value === "downloading");
|
|
const updateDownloaded = computed(() => downloaded.value !== null);
|
|
const isPreparingUpdate = computed(() => phase.value === "preparing");
|
|
const isInstallingUpdate = computed(() => phase.value === "installing" || isPreparingUpdate.value);
|
|
const updateReady = computed(() => phase.value === "restart");
|
|
const activeTaskCount = computed(() => Math.max(0, Math.trunc(options.getActiveTaskCount?.() ?? 0)));
|
|
const autoUpdateEnabled = computed(() => settingsStore.editorSettings.autoUpdateApp !== false);
|
|
const hasUpdateAvailable = computed(() => (updateDownloaded.value || updateReady.value || updateInfo.value?.update_available === true) && !isUpdateIgnored(updateInfo.value, settingsStore.editorSettings.ignoredUpdateVersion));
|
|
const latestReleaseUrl = "https://github.com/t8y2/dbx/releases/latest";
|
|
let generation = 0;
|
|
let activeDownload: Promise<void> | undefined;
|
|
let automaticDownload = false;
|
|
let cancellation: Promise<void> | undefined;
|
|
let cancelOperation: Promise<void> | undefined;
|
|
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
let hourlyTimer: ReturnType<typeof setInterval> | undefined;
|
|
let retries = 0;
|
|
let initialized = false;
|
|
let disposed = false;
|
|
let versionRecheckPending = false;
|
|
let updatePreparationRelease: (() => void) | undefined;
|
|
|
|
function clearRetry() {
|
|
clearTimeout(retryTimer);
|
|
retryTimer = undefined;
|
|
}
|
|
function fail(error: unknown) {
|
|
updateCheckFailed.value = true;
|
|
updateCheckMessage.value = formatUpdateError(error instanceof Error ? error.message : String(error));
|
|
}
|
|
function clearError() {
|
|
updateCheckFailed.value = false;
|
|
updateCheckMessage.value = "";
|
|
}
|
|
function scheduleRetry() {
|
|
clearRetry();
|
|
if (!initialized || disposed || retries >= 3) return;
|
|
const delay = [60_000, 300_000, 900_000][retries++];
|
|
retryTimer = setTimeout(() => {
|
|
retryTimer = undefined;
|
|
void checkUpdates({ silent: true });
|
|
}, delay);
|
|
}
|
|
function setDownloaded(cache: api.DownloadedUpdate) {
|
|
downloaded.value = cache;
|
|
updateInfo.value = {
|
|
current_version: updateInfo.value?.current_version ?? "",
|
|
latest_version: cache.version,
|
|
update_available: true,
|
|
portable_mode: cache.portable_mode,
|
|
manual_update_only: false,
|
|
release_name: tagVersion(cache.version),
|
|
release_url: cache.release_url,
|
|
release_notes: cache.release_notes,
|
|
};
|
|
phase.value = "ready";
|
|
downloadProgress.value = 100;
|
|
retries = 0;
|
|
clearRetry();
|
|
clearError();
|
|
}
|
|
function openUrl(url: string) {
|
|
if (isTauriRuntime()) void import("@tauri-apps/plugin-shell").then(({ open }) => open(url));
|
|
else window.open(url, "_blank", "noopener,noreferrer");
|
|
}
|
|
function openLatestRelease() {
|
|
openUrl(resolveUpdateReleaseUrl(updateInfo.value, settingsStore.editorSettings.updateDownloadSource, latestReleaseUrl));
|
|
}
|
|
function formatUpdateError(message: string): string {
|
|
const lower = message.toLowerCase();
|
|
if (lower.includes("cancel")) return t("updates.downloadCanceled");
|
|
if (lower.includes("403") || lower.includes("rate limit")) return t("updates.rateLimited");
|
|
// A blocked update cache is a local write problem, not a download problem.
|
|
if (lower.includes("update cache")) return t("updates.cacheWriteFailed", { error: message });
|
|
if (["stalled", "timeout", "all available mirrors", "error sending request", "failed to download"].some((part) => lower.includes(part))) return t("updates.networkOrMirrorFailed");
|
|
return t("updates.downloadFailed", { error: message });
|
|
}
|
|
function formatInstallError(message: string): string {
|
|
if (message.toLowerCase().includes("powershell")) return t("updates.portableHelperFailed", { error: message });
|
|
return t("updates.installFailed", { error: message });
|
|
}
|
|
async function checkUpdates(checkOptions: { silent?: boolean } = {}) {
|
|
if (disposed || isIgnoringUpdate.value) return;
|
|
if (!checkOptions.silent) showUpdateDialog.value = true;
|
|
// A downloaded-but-uninstalled update keeps the app in the ready phase; checks
|
|
// continue so a newer release can replace the cached package.
|
|
if (phase.value !== "idle" && phase.value !== "ready") return;
|
|
clearRetry();
|
|
const token = ++generation;
|
|
phase.value = "checking";
|
|
clearError();
|
|
try {
|
|
if (isUpdatePreviewMockEnabled()) {
|
|
if (token !== generation || disposed) return;
|
|
updateInfo.value = previewAppUpdateInfo(updateInfo.value?.current_version || "");
|
|
phase.value = "idle";
|
|
return;
|
|
}
|
|
const info = await api.checkForUpdates(currentLocale(), normalizeUpdateDownloadSource(settingsStore.editorSettings.updateDownloadSource));
|
|
if (token !== generation || disposed) return;
|
|
updateInfo.value = info;
|
|
// Installation may have started while the check was in flight; only restore
|
|
// the phase this check itself owns.
|
|
if (phase.value === "checking") phase.value = downloaded.value ? "ready" : "idle";
|
|
if (!info.update_available) updateCheckMessage.value = t("updates.upToDate", { version: info.current_version });
|
|
if (canDownloadAndInstallUpdate(info, isTauriRuntime()) && !isUpdateIgnored(info, settingsStore.editorSettings.ignoredUpdateVersion)) {
|
|
const cached = downloaded.value;
|
|
if (cached && !isNewerRemoteVersion(info.latest_version, cached.version)) {
|
|
// Keep a prepared package installable without replacing it automatically.
|
|
if (!autoUpdateEnabled.value) setDownloaded(cached);
|
|
return;
|
|
}
|
|
// A cached package older than the remote release must not mask the newer version.
|
|
if (cached && !(await discardSupersededUpdate(cached))) return;
|
|
if (!autoUpdateEnabled.value) return;
|
|
await downloadUpdateInBackground(true);
|
|
}
|
|
} catch (error) {
|
|
if (token !== generation || disposed) return;
|
|
if (phase.value === "checking") phase.value = downloaded.value ? "ready" : "idle";
|
|
fail(error);
|
|
scheduleRetry();
|
|
}
|
|
}
|
|
async function discardSupersededUpdate(cache: api.DownloadedUpdate): Promise<boolean> {
|
|
if (isInstallingUpdate.value || isIgnoringUpdate.value) return false;
|
|
try {
|
|
await api.discardDownloadedUpdate(cache.cache_id);
|
|
if (downloaded.value?.cache_id === cache.cache_id) {
|
|
downloaded.value = null;
|
|
downloadProgress.value = null;
|
|
if (phase.value === "ready" || phase.value === "checking") phase.value = "idle";
|
|
}
|
|
return true;
|
|
} catch (error) {
|
|
fail(error);
|
|
return false;
|
|
}
|
|
}
|
|
async function downloadUpdateInBackground(automatic = false) {
|
|
if (disposed || isIgnoringUpdate.value || phase.value !== "idle" || downloaded.value || !canDownloadAndInstallUpdate(updateInfo.value, isTauriRuntime())) return;
|
|
if (automatic && !autoUpdateEnabled.value) return;
|
|
automaticDownload = automatic;
|
|
const version = updateInfo.value!.latest_version;
|
|
const token = ++generation;
|
|
const attemptId = uuid();
|
|
phase.value = "downloading";
|
|
downloadProgress.value = null;
|
|
clearError();
|
|
clearRetry();
|
|
const run = async () => {
|
|
let unlisten: (() => void) | undefined;
|
|
try {
|
|
await cancellation;
|
|
if (token !== generation || disposed) return;
|
|
const { listen } = await import("@tauri-apps/api/event");
|
|
unlisten = await listen<UpdateDownloadProgress>("update-download-progress", ({ payload }) => {
|
|
if (token !== generation || payload.attempt_id !== attemptId || payload.version !== version) return;
|
|
downloadProgress.value = payload.total && payload.total > 0 ? Math.min(100, Math.round((payload.downloaded / payload.total) * 100)) : null;
|
|
});
|
|
if (token !== generation || disposed) return;
|
|
const cache = await api.downloadUpdate(normalizeUpdateDownloadSource(settingsStore.editorSettings.updateDownloadSource), version, attemptId, updateInfo.value?.release_notes);
|
|
if (token !== generation || disposed) return;
|
|
if (cache.version !== version) throw new Error("Downloaded update version changed; check for updates again.");
|
|
setDownloaded(cache);
|
|
} catch (error) {
|
|
if (token !== generation || disposed) return;
|
|
phase.value = "idle";
|
|
fail(error);
|
|
if ((String(error).includes("VERSION_CHANGED") || String(error).toLowerCase().includes("version changed")) && !versionRecheckPending) {
|
|
versionRecheckPending = true;
|
|
setTimeout(() => {
|
|
void checkUpdates({ silent: true }).finally(() => {
|
|
versionRecheckPending = false;
|
|
});
|
|
});
|
|
} else scheduleRetry();
|
|
} finally {
|
|
unlisten?.();
|
|
}
|
|
};
|
|
activeDownload = run();
|
|
await activeDownload;
|
|
activeDownload = undefined;
|
|
automaticDownload = false;
|
|
}
|
|
function cancelDownload(): Promise<void> {
|
|
if (cancelOperation) return cancelOperation;
|
|
cancelOperation = cancelDownloadImpl().finally(() => {
|
|
cancelOperation = undefined;
|
|
});
|
|
return cancelOperation;
|
|
}
|
|
async function cancelDownloadImpl() {
|
|
clearRetry();
|
|
generation++;
|
|
const running = activeDownload;
|
|
if (phase.value === "checking") phase.value = "idle";
|
|
if (!running) return;
|
|
const pending = api.cancelUpdateDownload();
|
|
cancellation = pending;
|
|
try {
|
|
await pending;
|
|
await running;
|
|
if (!disposed) {
|
|
const cache = await api.getDownloadedUpdate();
|
|
if (cache && !isUpdateIgnored({ latest_version: cache.version } as api.UpdateInfo, settingsStore.editorSettings.ignoredUpdateVersion)) setDownloaded(cache);
|
|
}
|
|
} finally {
|
|
if (cancellation === pending) cancellation = undefined;
|
|
if (phase.value === "downloading") phase.value = "idle";
|
|
}
|
|
}
|
|
async function changeUpdateDownloadSource(source: SettingsUpdateDownloadSource) {
|
|
if (isIgnoringUpdate.value || isInstallingUpdate.value) return;
|
|
try {
|
|
await settingsStore.updateEditorSettingsAndPersist({ updateDownloadSource: source });
|
|
if (downloaded.value || updateReady.value || isInstallingUpdate.value) return;
|
|
await cancelDownload();
|
|
retries = 0;
|
|
await checkUpdates();
|
|
} catch (error) {
|
|
fail(error);
|
|
}
|
|
}
|
|
async function ignoreCurrentVersion() {
|
|
const version = updateInfo.value?.latest_version;
|
|
if (!version || isIgnoringUpdate.value || isInstallingUpdate.value || updateReady.value) return;
|
|
isIgnoringUpdate.value = true;
|
|
try {
|
|
await cancelDownload();
|
|
await settingsStore.updateEditorSettingsAndPersist({ ignoredUpdateVersion: version });
|
|
const cache = downloaded.value;
|
|
downloaded.value = null;
|
|
phase.value = "idle";
|
|
if (cache) await api.discardDownloadedUpdate(cache.cache_id);
|
|
showUpdateDialog.value = false;
|
|
} catch (error) {
|
|
fail(error);
|
|
} finally {
|
|
isIgnoringUpdate.value = false;
|
|
}
|
|
}
|
|
async function performInstall(restartOnly: boolean) {
|
|
if (!isTauriRuntime() || isInstallingUpdate.value || isIgnoringUpdate.value || (!restartOnly && !downloaded.value)) return;
|
|
const cache = downloaded.value;
|
|
let release: (() => void) | undefined;
|
|
phase.value = "preparing";
|
|
clearError();
|
|
let installed = restartOnly;
|
|
let failed = false;
|
|
try {
|
|
release = await options.prepareForUpdate?.();
|
|
updatePreparationRelease = release;
|
|
if (shouldBlockAppUpdate(activeTaskCount.value)) throw new Error(t("updates.activeTasksBlockUpdate", { count: activeTaskCount.value }));
|
|
phase.value = "installing";
|
|
if (!restartOnly && cache) {
|
|
await api.installDownloadedUpdate(cache.cache_id, cache.version);
|
|
installed = true;
|
|
downloaded.value = null;
|
|
}
|
|
// Windows installers and the portable helper own process replacement/relaunch.
|
|
if (!cache?.portable_mode && !navigator.userAgent.includes("Windows")) {
|
|
phase.value = "restart";
|
|
const { relaunch } = await import("@tauri-apps/plugin-process");
|
|
await relaunch();
|
|
}
|
|
} catch (error) {
|
|
failed = true;
|
|
phase.value = installed ? "restart" : "ready";
|
|
updateCheckFailed.value = true;
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
updateCheckMessage.value = installed ? t("updates.restartFailed", { error: message }) : formatInstallError(message);
|
|
} finally {
|
|
// Successful handoff must keep every window frozen until the process exits.
|
|
if (failed) {
|
|
release?.();
|
|
updatePreparationRelease = undefined;
|
|
}
|
|
if (!installed && cache) {
|
|
try {
|
|
const restored = await api.getDownloadedUpdate();
|
|
if (!restored || restored.cache_id !== cache.cache_id) {
|
|
downloaded.value = null;
|
|
phase.value = "idle";
|
|
}
|
|
} catch {
|
|
/* Keep the original installation error available for retry. */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
async function installDownloadedUpdate() {
|
|
await performInstall(false);
|
|
}
|
|
async function restartApp() {
|
|
if (updateReady.value) await performInstall(true);
|
|
}
|
|
async function initialize() {
|
|
if (initialized || disposed) return;
|
|
initialized = true;
|
|
if (isTauriRuntime()) {
|
|
phase.value = "checking";
|
|
try {
|
|
const cache = await api.getDownloadedUpdate();
|
|
if (disposed) return;
|
|
if (cache) {
|
|
const ignored = isUpdateIgnored({ latest_version: cache.version } as api.UpdateInfo, settingsStore.editorSettings.ignoredUpdateVersion);
|
|
if (ignored) await api.discardDownloadedUpdate(cache.cache_id);
|
|
else {
|
|
setDownloaded(cache);
|
|
updateInfo.value!.current_version = await api.getAppVersion();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
fail(error);
|
|
} finally {
|
|
if (phase.value === "checking") phase.value = "idle";
|
|
}
|
|
}
|
|
if (disposed) return;
|
|
hourlyTimer = setInterval(() => {
|
|
if (!retryTimer) {
|
|
retries = 0;
|
|
void checkUpdates({ silent: true });
|
|
}
|
|
}, 3_600_000);
|
|
void checkUpdates({ silent: true });
|
|
}
|
|
const stopSettingsWatch = watch(autoUpdateEnabled, (enabled) => {
|
|
if (disposed) return;
|
|
if (!enabled) {
|
|
clearRetry();
|
|
if (automaticDownload) void cancelDownload().catch(fail);
|
|
return;
|
|
}
|
|
if (!initialized) return;
|
|
void (cancelOperation ?? Promise.resolve())
|
|
.then(() => {
|
|
if (autoUpdateEnabled.value && !disposed) void checkUpdates({ silent: true });
|
|
})
|
|
.catch(fail);
|
|
});
|
|
function dispose() {
|
|
disposed = true;
|
|
clearRetry();
|
|
clearInterval(hourlyTimer);
|
|
stopSettingsWatch();
|
|
updatePreparationRelease?.();
|
|
updatePreparationRelease = undefined;
|
|
void cancelDownload().catch(() => {});
|
|
}
|
|
onScopeDispose(dispose);
|
|
return {
|
|
phase,
|
|
checkingUpdates,
|
|
updateInfo,
|
|
updateCheckMessage,
|
|
updateCheckFailed,
|
|
showUpdateDialog,
|
|
isDownloadingUpdate,
|
|
downloadProgress,
|
|
updateDownloaded,
|
|
isInstallingUpdate,
|
|
isPreparingUpdate,
|
|
updateReady,
|
|
isIgnoringUpdate,
|
|
activeTaskCount,
|
|
hasUpdateAvailable,
|
|
latestReleaseUrl,
|
|
openUrl,
|
|
checkUpdates,
|
|
formatUpdateError,
|
|
openLatestRelease,
|
|
changeUpdateDownloadSource,
|
|
ignoreCurrentVersion,
|
|
downloadUpdateInBackground,
|
|
cancelDownload,
|
|
installDownloadedUpdate,
|
|
restartApp,
|
|
initialize,
|
|
dispose,
|
|
};
|
|
}
|