Files
Recordly/electron/updater.ts
webadderall 8abceaa914 feat: add H.264 stream-copy export, overhaul exporter pipeline, rename native binaries
- IPC: new inputMode 'h264-stream' — browser VideoEncoder output stream-copied
  into MP4 via ffmpeg rather than piping raw RGBA frames, cuts memory usage
- Security: replace flat approvedLocalReadPaths with userApprovedPaths fed from
  dialog pickers and project load; isAllowedLocalReadPath uses it alongside
  dir prefix / existsSync checks
- nativeVideoExport: add buildNativeH264StreamExportArgs for stream-copy path
- frameRenderer / modernFrameRenderer: cursor-follow camera integration,
  annotation blur post-processing, shadow profile, squircle geometry
- audioEncoder / streamingDecoder / gifExporter / muxer: full pipeline
  hardening — drift correction, backpressure, proper teardown
- Rename native helpers openscreen-* → recordly-* on darwin-arm64 and
  darwin-x64; update helpers-manifest.json for win32-x64
- electron/main, windows, updater, rendererServer: startup and window
  lifecycle improvements
- preload: expose inputMode and new IPC surface to renderer
2026-04-16 17:51:41 +10:00

804 lines
22 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import type { MessageBoxOptions, MessageBoxReturnValue } from "electron";
import { app, BrowserWindow, dialog } from "electron";
import { autoUpdater } from "electron-updater";
import { USER_DATA_PATH } from "./appPaths";
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
export const UPDATE_REMINDER_DELAY_MS = 3 * 60 * 60 * 1000;
const DISMISSED_READY_REMINDER_DELAY_MS = 5 * 60 * 1000;
const AUTO_UPDATES_DISABLED = process.env.RECORDLY_DISABLE_AUTO_UPDATES === "1";
const AUTO_UPDATE_ERROR_TOASTS_DISABLED =
process.env.RECORDLY_DISABLE_AUTO_UPDATE_ERROR_TOASTS === "1";
const UPDATE_FEED_URL_OVERRIDE = process.env.RECORDLY_UPDATE_FEED_URL?.trim() ?? "";
const UPDATER_LOG_PATH =
process.env.RECORDLY_UPDATER_LOG_PATH?.trim() || path.join(USER_DATA_PATH, "updater.log");
const DEV_UPDATE_PREVIEW_VERSION = "9.9.9";
const DEV_UPDATE_PREVIEW_PROGRESS_STEP_MS = 300;
const DEV_UPDATE_PREVIEW_PROGRESS_INCREMENT = 20;
export type UpdateToastPhase = "available" | "downloading" | "ready" | "error";
export type UpdateStatusKind =
| "idle"
| "checking"
| "up-to-date"
| "available"
| "downloading"
| "ready"
| "error";
export interface UpdateStatusSummary {
status: UpdateStatusKind;
currentVersion: string;
availableVersion: string | null;
detail?: string;
}
export interface UpdateToastPayload {
version: string;
detail: string;
phase: UpdateToastPhase;
delayMs: number;
isPreview?: boolean;
progressPercent?: number;
primaryAction?: "download-update" | "install-update" | "retry-check";
}
type UpdateToastSender = (
channel: "update-toast-state",
payload: UpdateToastPayload | null,
) => boolean;
let updaterInitialized = false;
let updateCheckInProgress = false;
let manualCheckRequested = false;
let periodicCheckTimer: NodeJS.Timeout | null = null;
let deferredReminderTimer: NodeJS.Timeout | null = null;
let devPreviewProgressTimer: NodeJS.Timeout | null = null;
let currentToastPayload: UpdateToastPayload | null = null;
let availableVersion: string | null = null;
let pendingDownloadedVersion: string | null = null;
let downloadInProgress = false;
let downloadToastDismissed = false;
let skippedVersion: string | null = null;
let updateCheckErrorHandled = false;
let activeUpdateToastSender: UpdateToastSender | undefined;
let updateStatusSummary: UpdateStatusSummary = {
status: "idle",
currentVersion: app.getVersion(),
availableVersion: null,
};
function setUpdateStatusSummary(summary: Partial<UpdateStatusSummary>) {
updateStatusSummary = {
...updateStatusSummary,
currentVersion: app.getVersion(),
...summary,
};
}
function summarizeError(error: unknown) {
if (error instanceof Error) {
return error.stack || `${error.name}: ${error.message}`;
}
return String(error);
}
function writeUpdaterLog(message: string, detail?: unknown) {
try {
fs.mkdirSync(path.dirname(UPDATER_LOG_PATH), { recursive: true });
const suffix = detail === undefined ? "" : ` ${summarizeError(detail)}`;
fs.appendFileSync(
UPDATER_LOG_PATH,
`${new Date().toISOString()} ${message}${suffix}\n`,
"utf8",
);
} catch (logError) {
console.error("Failed to write updater log:", logError);
}
}
function createAutoCheckErrorToastPayload(): UpdateToastPayload {
return {
version: app.getVersion(),
phase: "error",
detail: "Recordly could not check for updates automatically. Retry now, or inspect updater.log in your user data folder.",
delayMs: UPDATE_REMINDER_DELAY_MS,
primaryAction: "retry-check",
};
}
function shouldSurfaceAutomaticCheckErrors() {
return !AUTO_UPDATE_ERROR_TOASTS_DISABLED;
}
function configureUpdateFeed() {
if (!UPDATE_FEED_URL_OVERRIDE) {
writeUpdaterLog("Using published GitHub update feed.");
return;
}
autoUpdater.setFeedURL({
provider: "generic",
url: UPDATE_FEED_URL_OVERRIDE,
channel: "latest",
});
writeUpdaterLog(`Using overridden update feed: ${UPDATE_FEED_URL_OVERRIDE}`);
}
function canUseAutoUpdates() {
return !AUTO_UPDATES_DISABLED && app.isPackaged && !process.mas;
}
export function isAutoUpdateFeatureEnabled() {
return !AUTO_UPDATES_DISABLED;
}
function getDialogWindow(getMainWindow: () => BrowserWindow | null) {
const window = getMainWindow();
return window && !window.isDestroyed() ? window : undefined;
}
function showMessageBox(
getMainWindow: () => BrowserWindow | null,
options: MessageBoxOptions,
): Promise<MessageBoxReturnValue> {
const window = getDialogWindow(getMainWindow);
return window ? dialog.showMessageBox(window, options) : dialog.showMessageBox(options);
}
function clearDeferredReminderTimer() {
if (deferredReminderTimer) {
clearTimeout(deferredReminderTimer);
deferredReminderTimer = null;
}
}
function clearDevPreviewProgressTimer() {
if (devPreviewProgressTimer) {
clearInterval(devPreviewProgressTimer);
devPreviewProgressTimer = null;
}
}
function emitUpdateToastState(
sendToRenderer: UpdateToastSender | undefined,
payload: UpdateToastPayload | null,
) {
currentToastPayload = payload;
if (!sendToRenderer) {
return false;
}
return sendToRenderer("update-toast-state", payload);
}
function createAvailableUpdateToastPayload(version: string): UpdateToastPayload {
return {
version,
phase: "available",
detail: "A new version is available. Download it now, or wait and we will remind you again in 3 hours.",
delayMs: UPDATE_REMINDER_DELAY_MS,
primaryAction: "download-update",
};
}
function createDownloadingUpdateToastPayload(
version: string,
progressPercent = 0,
): UpdateToastPayload {
const normalizedProgress = Math.max(0, Math.min(100, progressPercent));
return {
version,
phase: "downloading",
detail:
normalizedProgress >= 100
? "Finishing the update download. You can keep using Recordly while this completes."
: `Downloading the update in the foreground: ${normalizedProgress.toFixed(0)}% complete.`,
delayMs: UPDATE_REMINDER_DELAY_MS,
progressPercent: normalizedProgress,
};
}
function createDownloadedUpdateToastPayload(version: string): UpdateToastPayload {
return {
version,
phase: "ready",
detail: "Install now to restart into the new version, or wait and we will remind you again in 3 hours.",
delayMs: UPDATE_REMINDER_DELAY_MS,
primaryAction: "install-update",
};
}
function createUpdateErrorToastPayload(version: string, error: unknown): UpdateToastPayload {
return {
version,
phase: "error",
detail: `The update download failed. ${String(error)}`,
delayMs: UPDATE_REMINDER_DELAY_MS,
primaryAction: "download-update",
};
}
function getReminderPayload(): UpdateToastPayload | null {
if (pendingDownloadedVersion) {
return createDownloadedUpdateToastPayload(pendingDownloadedVersion);
}
if (availableVersion && !downloadInProgress) {
return createAvailableUpdateToastPayload(availableVersion);
}
return null;
}
function clearVisibleUpdateToast(sendToRenderer?: UpdateToastSender) {
emitUpdateToastState(sendToRenderer, null);
}
export function getCurrentUpdateToastPayload() {
return currentToastPayload;
}
export function getUpdaterLogPath() {
return UPDATER_LOG_PATH;
}
export function getUpdateStatusSummary() {
return updateStatusSummary;
}
async function showNoUpdatesDialog(getMainWindow: () => BrowserWindow | null) {
await showMessageBox(getMainWindow, {
type: "info",
title: "No Updates Available",
message: "Recordly is up to date.",
detail: `You are running version ${app.getVersion()}.`,
});
}
async function showUpdateErrorDialog(getMainWindow: () => BrowserWindow | null, error: unknown) {
await showMessageBox(getMainWindow, {
type: "error",
title: "Update Check Failed",
message: "Recordly could not check for updates.",
detail: String(error),
});
}
function resetDevPreviewState(sendToRenderer?: UpdateToastSender) {
clearDevPreviewProgressTimer();
availableVersion = null;
pendingDownloadedVersion = null;
downloadInProgress = false;
downloadToastDismissed = false;
skippedVersion = null;
clearVisibleUpdateToast(sendToRenderer);
}
function simulateDevPreviewDownload(sendToRenderer?: UpdateToastSender) {
availableVersion = DEV_UPDATE_PREVIEW_VERSION;
pendingDownloadedVersion = null;
downloadInProgress = true;
downloadToastDismissed = false;
clearDeferredReminderTimer();
clearDevPreviewProgressTimer();
let progressPercent = 0;
emitUpdateToastState(sendToRenderer, {
...createDownloadingUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION, progressPercent),
isPreview: true,
});
devPreviewProgressTimer = setInterval(() => {
progressPercent = Math.min(100, progressPercent + DEV_UPDATE_PREVIEW_PROGRESS_INCREMENT);
if (progressPercent >= 100) {
clearDevPreviewProgressTimer();
downloadInProgress = false;
pendingDownloadedVersion = DEV_UPDATE_PREVIEW_VERSION;
emitUpdateToastState(sendToRenderer, {
...createDownloadedUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION),
isPreview: true,
detail: "Development preview: the update is ready to install. No real update will be installed.",
});
return;
}
if (downloadToastDismissed) {
return;
}
emitUpdateToastState(sendToRenderer, {
...createDownloadingUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION, progressPercent),
isPreview: true,
});
}, DEV_UPDATE_PREVIEW_PROGRESS_STEP_MS);
return { success: true };
}
export function dismissUpdateToast(
getMainWindow: () => BrowserWindow | null,
sendToRenderer?: UpdateToastSender,
) {
if (currentToastPayload?.isPreview) {
resetDevPreviewState(sendToRenderer);
return { success: true };
}
if (downloadInProgress) {
downloadToastDismissed = true;
clearVisibleUpdateToast(sendToRenderer);
return { success: true };
}
if (currentToastPayload?.phase === "ready") {
return deferUpdateReminder(
getMainWindow,
sendToRenderer,
DISMISSED_READY_REMINDER_DELAY_MS,
);
}
if (currentToastPayload?.phase === "available" || currentToastPayload?.phase === "error") {
return deferUpdateReminder(getMainWindow, sendToRenderer, UPDATE_REMINDER_DELAY_MS);
}
clearVisibleUpdateToast(sendToRenderer);
return { success: true };
}
export function installDownloadedUpdateNow(sendToRenderer?: UpdateToastSender) {
if (currentToastPayload?.isPreview) {
resetDevPreviewState(sendToRenderer);
return;
}
clearDeferredReminderTimer();
downloadToastDismissed = false;
clearVisibleUpdateToast(sendToRenderer);
setUpdateStatusSummary({ status: "ready", availableVersion: pendingDownloadedVersion });
writeUpdaterLog("Installing downloaded update.");
autoUpdater.quitAndInstall();
}
export async function downloadAvailableUpdate(sendToRenderer?: UpdateToastSender) {
if (currentToastPayload?.isPreview) {
return simulateDevPreviewDownload(sendToRenderer);
}
if (!availableVersion) {
return { success: false, message: "No update is ready to download." };
}
if (pendingDownloadedVersion === availableVersion) {
return { success: false, message: "This update has already been downloaded." };
}
if (downloadInProgress) {
return { success: false, message: "This update is already downloading." };
}
clearDeferredReminderTimer();
downloadInProgress = true;
downloadToastDismissed = false;
setUpdateStatusSummary({
status: "downloading",
availableVersion,
detail: `Downloading Recordly ${availableVersion}`,
});
emitUpdateToastState(sendToRenderer, createDownloadingUpdateToastPayload(availableVersion, 0));
writeUpdaterLog(`Starting update download for ${availableVersion}.`);
try {
await autoUpdater.downloadUpdate();
writeUpdaterLog(`Update download requested for ${availableVersion}.`);
return { success: true };
} catch (error) {
downloadInProgress = false;
setUpdateStatusSummary({
status: "error",
availableVersion,
detail: String(error),
});
writeUpdaterLog(`Update download failed for ${availableVersion}.`, error);
emitUpdateToastState(
sendToRenderer,
createUpdateErrorToastPayload(availableVersion, error),
);
return { success: false, message: String(error) };
}
}
export function deferUpdateReminder(
getMainWindow: () => BrowserWindow | null,
sendToRenderer?: UpdateToastSender,
delayMs = UPDATE_REMINDER_DELAY_MS,
) {
const payload = getReminderPayload();
if (!payload) {
return { success: false, message: "No update reminder is ready yet." };
}
clearDeferredReminderTimer();
clearVisibleUpdateToast(sendToRenderer);
deferredReminderTimer = setTimeout(() => {
const nextPayload = getReminderPayload();
if (!nextPayload) {
return;
}
if (sendToRenderer && emitUpdateToastState(sendToRenderer, nextPayload)) {
return;
}
if (nextPayload.phase === "ready") {
void showDownloadedUpdateDialog(getMainWindow, nextPayload.version);
return;
}
void showAvailableUpdateDialog(getMainWindow, nextPayload.version, sendToRenderer);
}, delayMs);
return { success: true };
}
export function skipAvailableUpdateVersion(sendToRenderer?: UpdateToastSender) {
const versionToSkip = pendingDownloadedVersion ?? availableVersion;
if (!versionToSkip) {
return { success: false, message: "No update is available to skip." };
}
skippedVersion = versionToSkip;
if (pendingDownloadedVersion === versionToSkip) {
pendingDownloadedVersion = null;
}
if (availableVersion === versionToSkip) {
availableVersion = null;
}
downloadInProgress = false;
downloadToastDismissed = false;
clearDeferredReminderTimer();
clearVisibleUpdateToast(sendToRenderer);
return { success: true };
}
export function previewUpdateToast(sendToRenderer: UpdateToastSender) {
clearDeferredReminderTimer();
clearDevPreviewProgressTimer();
availableVersion = DEV_UPDATE_PREVIEW_VERSION;
pendingDownloadedVersion = null;
downloadInProgress = false;
downloadToastDismissed = false;
return emitUpdateToastState(sendToRenderer, {
version: DEV_UPDATE_PREVIEW_VERSION,
phase: "available",
detail: "This is a development preview of the in-app update toast.",
delayMs: UPDATE_REMINDER_DELAY_MS,
isPreview: true,
});
}
async function showAvailableUpdateDialog(
getMainWindow: () => BrowserWindow | null,
version: string,
sendToRenderer?: UpdateToastSender,
) {
const result = await showMessageBox(getMainWindow, {
type: "info",
title: "Update Available",
message: `Recordly ${version} is available.`,
detail: "Download now, remind me in 3 hours, or skip this version.",
buttons: ["Download Update", "Remind Me in 3 Hours", "Skip This Version"],
defaultId: 0,
cancelId: 1,
noLink: true,
});
if (result.response === 0) {
await downloadAvailableUpdate(sendToRenderer);
return;
}
if (result.response === 1) {
deferUpdateReminder(getMainWindow, sendToRenderer, UPDATE_REMINDER_DELAY_MS);
return;
}
skipAvailableUpdateVersion(sendToRenderer);
}
async function showDownloadedUpdateDialog(
getMainWindow: () => BrowserWindow | null,
version: string,
options?: { isPreview?: boolean },
) {
const isPreview = Boolean(options?.isPreview);
const result = await showMessageBox(getMainWindow, {
type: "info",
title: "Update Ready",
message: isPreview
? `Recordly ${version} is ready to install.`
: `Recordly ${version} has been downloaded.`,
detail: isPreview
? "Development preview of the native update prompt. No real update will be installed."
: "Install now, remind me in 3 hours, or skip this version.",
buttons: ["Install Update", "Remind Me in 3 Hours", "Skip This Version"],
defaultId: 0,
cancelId: 1,
noLink: true,
});
if (result.response === 0) {
if (isPreview) {
await showMessageBox(getMainWindow, {
type: "info",
title: "Preview Only",
message: "No real update was installed.",
detail: "This was only a manual development preview of the update prompt.",
});
return;
}
clearDeferredReminderTimer();
setImmediate(() => {
installDownloadedUpdateNow();
});
return;
}
if (result.response === 1) {
if (isPreview) {
return;
}
deferUpdateReminder(getMainWindow, undefined, UPDATE_REMINDER_DELAY_MS);
return;
}
if (isPreview) {
return;
}
skipAvailableUpdateVersion();
}
export async function checkForAppUpdates(
getMainWindow: () => BrowserWindow | null,
options?: { manual?: boolean },
) {
if (!canUseAutoUpdates()) {
writeUpdaterLog(
`Skipped update check because auto-updates are unavailable. packaged=${app.isPackaged} mas=${process.mas ? "yes" : "no"} disabled=${AUTO_UPDATES_DISABLED ? "yes" : "no"}`,
);
if (options?.manual) {
await showMessageBox(getMainWindow, {
type: "info",
title: "Updates Not Enabled",
message: "Auto-updates are only available in packaged releases.",
detail: AUTO_UPDATES_DISABLED
? "This build disabled auto-updates through RECORDLY_DISABLE_AUTO_UPDATES=1."
: "Development builds do not ship the packaged update metadata required by electron-updater.",
});
}
return;
}
if (updateCheckInProgress) {
writeUpdaterLog("Skipped update check because a previous check is still running.");
if (options?.manual) {
await showMessageBox(getMainWindow, {
type: "info",
title: "Update Check In Progress",
message: "Recordly is already checking for updates.",
});
}
return;
}
manualCheckRequested = Boolean(options?.manual);
updateCheckInProgress = true;
updateCheckErrorHandled = false;
setUpdateStatusSummary({ status: "checking", detail: "Checking for updates..." });
writeUpdaterLog(`Starting ${manualCheckRequested ? "manual" : "automatic"} update check.`);
try {
await autoUpdater.checkForUpdates();
writeUpdaterLog("Update check request completed.");
} catch (error) {
updateCheckInProgress = false;
const shouldReport = manualCheckRequested;
manualCheckRequested = false;
setUpdateStatusSummary({
status: "error",
availableVersion,
detail: String(error),
});
writeUpdaterLog("Update check failed.", error);
console.error("Auto-update check failed:", error);
if (shouldReport && !updateCheckErrorHandled) {
await showUpdateErrorDialog(getMainWindow, error);
} else if (!updateCheckErrorHandled && shouldSurfaceAutomaticCheckErrors()) {
emitUpdateToastState(activeUpdateToastSender, createAutoCheckErrorToastPayload());
}
}
}
export function setupAutoUpdates(
getMainWindow: () => BrowserWindow | null,
sendToRenderer: UpdateToastSender,
) {
if (updaterInitialized) {
return;
}
if (!canUseAutoUpdates()) {
setUpdateStatusSummary({ status: "idle", availableVersion: null, detail: undefined });
return;
}
updaterInitialized = true;
activeUpdateToastSender = sendToRenderer;
configureUpdateFeed();
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
writeUpdaterLog(`Updater initialized. logPath=${UPDATER_LOG_PATH}`);
autoUpdater.on("checking-for-update", () => {
setUpdateStatusSummary({
status: "checking",
availableVersion: null,
detail: "Checking for updates...",
});
writeUpdaterLog("electron-updater emitted checking-for-update.");
});
autoUpdater.on("update-available", (info) => {
writeUpdaterLog(`Update available: version=${info.version}`);
updateCheckInProgress = false;
availableVersion = info.version;
pendingDownloadedVersion = null;
downloadInProgress = false;
downloadToastDismissed = false;
setUpdateStatusSummary({
status: "available",
availableVersion: info.version,
detail: `Recordly ${info.version} is available.`,
});
if (skippedVersion === info.version) {
manualCheckRequested = false;
return;
}
const payload = createAvailableUpdateToastPayload(info.version);
if (emitUpdateToastState(sendToRenderer, payload)) {
manualCheckRequested = false;
return;
}
if (manualCheckRequested) {
void showAvailableUpdateDialog(getMainWindow, info.version, sendToRenderer);
manualCheckRequested = false;
}
});
autoUpdater.on("update-not-available", () => {
writeUpdaterLog("No update available.");
updateCheckInProgress = false;
availableVersion = null;
pendingDownloadedVersion = null;
downloadInProgress = false;
downloadToastDismissed = false;
setUpdateStatusSummary({
status: "up-to-date",
availableVersion: null,
detail: `Recordly ${app.getVersion()} is up to date.`,
});
clearVisibleUpdateToast(sendToRenderer);
const shouldReport = manualCheckRequested;
manualCheckRequested = false;
if (shouldReport) {
void showNoUpdatesDialog(getMainWindow);
}
});
autoUpdater.on("download-progress", (progress) => {
if (!availableVersion) {
return;
}
downloadInProgress = true;
setUpdateStatusSummary({
status: "downloading",
availableVersion,
detail: `Downloading Recordly ${availableVersion}`,
});
writeUpdaterLog(
`Download progress for ${availableVersion}: ${progress.percent.toFixed(1)}%`,
);
if (downloadToastDismissed) {
return;
}
emitUpdateToastState(
sendToRenderer,
createDownloadingUpdateToastPayload(availableVersion, progress.percent),
);
});
autoUpdater.on("error", (error) => {
updateCheckInProgress = false;
const shouldReport = manualCheckRequested;
manualCheckRequested = false;
if (!downloadInProgress) {
updateCheckErrorHandled = true;
}
setUpdateStatusSummary({
status: "error",
availableVersion,
detail: String(error),
});
writeUpdaterLog("electron-updater emitted error.", error);
console.error("Auto-updater error:", error);
if (downloadInProgress && availableVersion) {
downloadInProgress = false;
downloadToastDismissed = false;
emitUpdateToastState(
sendToRenderer,
createUpdateErrorToastPayload(availableVersion, error),
);
}
if (shouldReport) {
void showUpdateErrorDialog(getMainWindow, error);
} else if (shouldSurfaceAutomaticCheckErrors()) {
emitUpdateToastState(sendToRenderer, createAutoCheckErrorToastPayload());
}
});
autoUpdater.on("update-downloaded", (info) => {
writeUpdaterLog(`Update downloaded: version=${info.version}`);
updateCheckInProgress = false;
manualCheckRequested = false;
downloadInProgress = false;
downloadToastDismissed = false;
if (skippedVersion === info.version) {
return;
}
availableVersion = info.version;
pendingDownloadedVersion = info.version;
setUpdateStatusSummary({
status: "ready",
availableVersion: info.version,
detail: `Recordly ${info.version} is ready to install.`,
});
clearDeferredReminderTimer();
if (
emitUpdateToastState(sendToRenderer, createDownloadedUpdateToastPayload(info.version))
) {
return;
}
void showDownloadedUpdateDialog(getMainWindow, info.version);
});
void checkForAppUpdates(getMainWindow);
periodicCheckTimer = setInterval(() => {
void checkForAppUpdates(getMainWindow);
}, UPDATE_CHECK_INTERVAL_MS);
app.on("before-quit", () => {
clearDeferredReminderTimer();
clearDevPreviewProgressTimer();
if (periodicCheckTimer) {
clearInterval(periodicCheckTimer);
periodicCheckTimer = null;
}
});
}