feat(telemetry): handle application shutdown process
This update introduces a structured shutdown process for the application. It ensures that background tasks are properly terminated and telemetry is notified when the frontend is shutting down, preventing any ongoing operations from continuing during this state. - Added event listeners to manage app shutdown events - Implemented a shutdown handler to clear timers and notify telemetry - Updated background task checks to respect the shutdown state
This commit is contained in:
+34
-9
@@ -1,9 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount, tick } from "svelte";
|
import { onDestroy, onMount, tick } from "svelte";
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||||
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
||||||
import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
|
import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
|
||||||
|
import { beginFrontendShutdown } from "./lib/telemetry";
|
||||||
|
|
||||||
import TitleBar from "./lib/TitleBar.svelte";
|
import TitleBar from "./lib/TitleBar.svelte";
|
||||||
import RepoToolbar from "./lib/RepoToolbar.svelte";
|
import RepoToolbar from "./lib/RepoToolbar.svelte";
|
||||||
@@ -376,6 +378,8 @@
|
|||||||
let backgroundFetchInFlight = false;
|
let backgroundFetchInFlight = false;
|
||||||
let backgroundRepoStatusInFlight = false;
|
let backgroundRepoStatusInFlight = false;
|
||||||
let backgroundRepoStatusIndex = 0;
|
let backgroundRepoStatusIndex = 0;
|
||||||
|
let appShuttingDown = false;
|
||||||
|
let unlistenCloseRequested: (() => void) | undefined;
|
||||||
let lastRepoSwitchAt = 0;
|
let lastRepoSwitchAt = 0;
|
||||||
let updateToastOpen = false;
|
let updateToastOpen = false;
|
||||||
let updateToastState: UpdateToastState = "available";
|
let updateToastState: UpdateToastState = "available";
|
||||||
@@ -476,21 +480,40 @@
|
|||||||
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
|
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
|
||||||
void runStartupSequence();
|
void runStartupSequence();
|
||||||
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
|
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
|
||||||
|
window.addEventListener("beforeunload", handleAppShutdown);
|
||||||
|
window.addEventListener("pagehide", handleAppShutdown);
|
||||||
|
void getCurrentWindow().onCloseRequested(() => handleAppShutdown()).then((unlisten) => {
|
||||||
|
if (appShuttingDown) unlisten();
|
||||||
|
else unlistenCloseRequested = unlisten;
|
||||||
|
}).catch(() => {
|
||||||
|
// Browser preview has no Tauri window; DOM lifecycle events still cover it.
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
handleAppShutdown();
|
||||||
themeMediaQuery?.removeEventListener("change", handleSystemThemeChange);
|
themeMediaQuery?.removeEventListener("change", handleSystemThemeChange);
|
||||||
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
|
window.removeEventListener("beforeunload", handleAppShutdown);
|
||||||
if (backgroundRepoStatusTimer) clearInterval(backgroundRepoStatusTimer);
|
window.removeEventListener("pagehide", handleAppShutdown);
|
||||||
if (backgroundFetchTimer) clearInterval(backgroundFetchTimer);
|
unlistenCloseRequested?.();
|
||||||
if (commitAiPollTimer) clearInterval(commitAiPollTimer);
|
|
||||||
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
|
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
|
||||||
Object.values(errorAutoHideStates).forEach((state) => {
|
Object.values(errorAutoHideStates).forEach((state) => {
|
||||||
if (state?.timer) clearTimeout(state.timer);
|
if (state?.timer) clearTimeout(state.timer);
|
||||||
});
|
});
|
||||||
if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId);
|
// Do not invoke backend cancellation here: Windows may already be shutting
|
||||||
|
// down, and starting another IPC/Git operation is precisely what we avoid.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleAppShutdown() {
|
||||||
|
if (appShuttingDown) return;
|
||||||
|
appShuttingDown = true;
|
||||||
|
beginFrontendShutdown();
|
||||||
|
if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = undefined; }
|
||||||
|
if (backgroundRepoStatusTimer) { clearInterval(backgroundRepoStatusTimer); backgroundRepoStatusTimer = undefined; }
|
||||||
|
if (backgroundFetchTimer) { clearInterval(backgroundFetchTimer); backgroundFetchTimer = undefined; }
|
||||||
|
if (commitAiPollTimer) { clearInterval(commitAiPollTimer); commitAiPollTimer = undefined; }
|
||||||
|
}
|
||||||
|
|
||||||
function wait(ms: number): Promise<void> {
|
function wait(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
@@ -528,6 +551,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startBackgroundTimers() {
|
function startBackgroundTimers() {
|
||||||
|
if (appShuttingDown) return;
|
||||||
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
|
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
|
||||||
backgroundRepoStatusTimer = setInterval(() => { void backgroundRepoStatusTick(false); }, BACKGROUND_REPO_STATUS_INTERVAL);
|
backgroundRepoStatusTimer = setInterval(() => { void backgroundRepoStatusTick(false); }, BACKGROUND_REPO_STATUS_INTERVAL);
|
||||||
backgroundFetchTimer = setInterval(() => { void backgroundFetchTick(); }, BACKGROUND_FETCH_INTERVAL);
|
backgroundFetchTimer = setInterval(() => { void backgroundFetchTick(); }, BACKGROUND_FETCH_INTERVAL);
|
||||||
@@ -630,7 +654,7 @@
|
|||||||
// Errors are swallowed here — auth/network failures surface via the manual Fetch/Pull/
|
// Errors are swallowed here — auth/network failures surface via the manual Fetch/Pull/
|
||||||
// Push buttons instead, not as a background popup.
|
// Push buttons instead, not as a background popup.
|
||||||
async function backgroundFetchTick() {
|
async function backgroundFetchTick() {
|
||||||
if (!autoRefreshEnabled) return;
|
if (appShuttingDown || !autoRefreshEnabled) return;
|
||||||
|
|
||||||
if (activeView === "repository" && activeRepoPath && !backgroundFetchInFlight
|
if (activeView === "repository" && activeRepoPath && !backgroundFetchInFlight
|
||||||
&& !isBusy && Date.now() - lastRepoSwitchAt >= BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS) {
|
&& !isBusy && Date.now() - lastRepoSwitchAt >= BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS) {
|
||||||
@@ -650,7 +674,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function backgroundFetchRepo(path: string) {
|
async function backgroundFetchRepo(path: string) {
|
||||||
if (!autoRefreshEnabled || !path || backgroundFetchInFlight) return;
|
if (appShuttingDown || !autoRefreshEnabled || !path || backgroundFetchInFlight) return;
|
||||||
|
|
||||||
backgroundFetchInFlight = true;
|
backgroundFetchInFlight = true;
|
||||||
try {
|
try {
|
||||||
@@ -700,7 +724,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function backgroundRepoStatusTick(fetchFirst: boolean) {
|
async function backgroundRepoStatusTick(fetchFirst: boolean) {
|
||||||
if (!autoRefreshEnabled || backgroundRepoStatusInFlight) return;
|
if (appShuttingDown || !autoRefreshEnabled || backgroundRepoStatusInFlight) return;
|
||||||
const others = knownRepoPathsForBackground();
|
const others = knownRepoPathsForBackground();
|
||||||
if (others.length === 0) return;
|
if (others.length === 0) return;
|
||||||
|
|
||||||
@@ -711,6 +735,7 @@
|
|||||||
// still never running more than one `git fetch` subprocess at a time.
|
// still never running more than one `git fetch` subprocess at a time.
|
||||||
const batchSize = fetchFirst ? BACKGROUND_FETCH_OTHER_BATCH_SIZE : BACKGROUND_REPO_STATUS_BATCH_SIZE;
|
const batchSize = fetchFirst ? BACKGROUND_FETCH_OTHER_BATCH_SIZE : BACKGROUND_REPO_STATUS_BATCH_SIZE;
|
||||||
for (let step = 0; step < Math.min(batchSize, others.length); step++) {
|
for (let step = 0; step < Math.min(batchSize, others.length); step++) {
|
||||||
|
if (appShuttingDown) break;
|
||||||
if (backgroundRepoStatusIndex >= others.length) backgroundRepoStatusIndex = 0;
|
if (backgroundRepoStatusIndex >= others.length) backgroundRepoStatusIndex = 0;
|
||||||
const path = others[backgroundRepoStatusIndex];
|
const path = others[backgroundRepoStatusIndex];
|
||||||
backgroundRepoStatusIndex += 1;
|
backgroundRepoStatusIndex += 1;
|
||||||
@@ -729,7 +754,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function autoRefreshTick() {
|
async function autoRefreshTick() {
|
||||||
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || newBranchCommit || globalSearchOpen || helpOpen) return;
|
if (appShuttingDown || !autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || newBranchCommit || globalSearchOpen || helpOpen) return;
|
||||||
const path = activeRepoPath;
|
const path = activeRepoPath;
|
||||||
autoRefreshInFlight = true;
|
autoRefreshInFlight = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ type TelemetryLevel = "info" | "warn" | "error";
|
|||||||
const MAX_MESSAGE_LENGTH = 2_048;
|
const MAX_MESSAGE_LENGTH = 2_048;
|
||||||
let telemetryEnabled = false;
|
let telemetryEnabled = false;
|
||||||
let configuration: Promise<unknown> = Promise.resolve();
|
let configuration: Promise<unknown> = Promise.resolve();
|
||||||
|
let frontendShuttingDown = false;
|
||||||
|
|
||||||
|
export function beginFrontendShutdown() {
|
||||||
|
frontendShuttingDown = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Telemetry must never contain repository contents or identifying local data.
|
// Telemetry must never contain repository contents or identifying local data.
|
||||||
function sanitize(message: string): string {
|
function sanitize(message: string): string {
|
||||||
@@ -42,6 +47,9 @@ function randomHex(bytes: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function tracedInvoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
export async function tracedInvoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||||
|
if (frontendShuttingDown) {
|
||||||
|
throw new Error("Application is shutting down");
|
||||||
|
}
|
||||||
if (!telemetryEnabled) return invoke<T>(command, args);
|
if (!telemetryEnabled) return invoke<T>(command, args);
|
||||||
const startedAtMs = Date.now();
|
const startedAtMs = Date.now();
|
||||||
const started = performance.now();
|
const started = performance.now();
|
||||||
|
|||||||
Reference in New Issue
Block a user