fix: Fix multiple bugs with new authentication flow
This commit is contained in:
14
electron/api/auth-store.spec.ts
Normal file
14
electron/api/auth-store.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
import { getLocalApiTokenTtlMs } from './auth-store';
|
||||
|
||||
const TEN_YEARS_MS = 10 * 365 * 24 * 60 * 60 * 1000;
|
||||
|
||||
describe('auth-store', () => {
|
||||
it('defaults local API tokens to a very long lifetime', () => {
|
||||
expect(getLocalApiTokenTtlMs()).toBe(TEN_YEARS_MS);
|
||||
});
|
||||
});
|
||||
@@ -10,9 +10,13 @@ export interface IssuedToken {
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
const TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
const DEFAULT_TOKEN_TTL_MS = 10 * 365 * 24 * 60 * 60 * 1000;
|
||||
const tokens = new Map<string, IssuedToken>();
|
||||
|
||||
export function getLocalApiTokenTtlMs(): number {
|
||||
return DEFAULT_TOKEN_TTL_MS;
|
||||
}
|
||||
|
||||
export function issueToken(params: {
|
||||
userId: string;
|
||||
username: string;
|
||||
@@ -24,7 +28,7 @@ export function issueToken(params: {
|
||||
const issued: IssuedToken = {
|
||||
token,
|
||||
issuedAt,
|
||||
expiresAt: issuedAt + TOKEN_TTL_MS,
|
||||
expiresAt: issuedAt + getLocalApiTokenTtlMs(),
|
||||
userId: params.userId,
|
||||
username: params.username,
|
||||
displayName: params.displayName,
|
||||
|
||||
@@ -22,6 +22,12 @@ import {
|
||||
setupWindowControlHandlers
|
||||
} from '../ipc';
|
||||
import { startIdleMonitor, stopIdleMonitor } from '../idle/idle-monitor';
|
||||
import {
|
||||
attachRendererDiagnosticsHooks,
|
||||
ensurePerfDiagIpcRegistered,
|
||||
shutdownPerfDiagnostics,
|
||||
startPerfDiagnostics
|
||||
} from '../diagnostics';
|
||||
|
||||
function startLocalApiAfterWindowReady(): void {
|
||||
setImmediate(() => {
|
||||
@@ -32,6 +38,8 @@ function startLocalApiAfterWindowReady(): void {
|
||||
}
|
||||
|
||||
export function registerAppLifecycle(): void {
|
||||
ensurePerfDiagIpcRegistered();
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const dockIconPath = getDockIconPath();
|
||||
|
||||
@@ -45,7 +53,15 @@ export function registerAppLifecycle(): void {
|
||||
await migrateLegacyDesktopBranding();
|
||||
await synchronizeAutoStartSetting();
|
||||
initializeDesktopUpdater();
|
||||
startPerfDiagnostics();
|
||||
await createWindow();
|
||||
|
||||
const mainWindow = getMainWindow();
|
||||
|
||||
if (mainWindow) {
|
||||
attachRendererDiagnosticsHooks(mainWindow);
|
||||
}
|
||||
|
||||
startLocalApiAfterWindowReady();
|
||||
startIdleMonitor();
|
||||
|
||||
@@ -67,6 +83,7 @@ export function registerAppLifecycle(): void {
|
||||
|
||||
app.on('before-quit', async (event) => {
|
||||
prepareWindowForAppQuit();
|
||||
await shutdownPerfDiagnostics();
|
||||
|
||||
if (getDataSource()?.isInitialized) {
|
||||
event.preventDefault();
|
||||
|
||||
27
electron/diagnostics/diagnostics.flags.spec.ts
Normal file
27
electron/diagnostics/diagnostics.flags.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect
|
||||
} from 'vitest';
|
||||
import { isPerfDiagEnabled } from './diagnostics.flags';
|
||||
|
||||
describe('isPerfDiagEnabled', () => {
|
||||
it('returns false when the flag is unset', () => {
|
||||
expect(isPerfDiagEnabled({}, false)).toBe(false);
|
||||
expect(isPerfDiagEnabled({}, true)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true in development when METOYOU_PERF_DIAG is truthy', () => {
|
||||
expect(isPerfDiagEnabled({ METOYOU_PERF_DIAG: '1' }, false)).toBe(true);
|
||||
expect(isPerfDiagEnabled({ METOYOU_PERF_DIAG: 'true' }, false)).toBe(true);
|
||||
expect(isPerfDiagEnabled({ METOYOU_PERF_DIAG: 'on' }, false)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false in packaged builds unless force is set', () => {
|
||||
expect(isPerfDiagEnabled({ METOYOU_PERF_DIAG: '1' }, true)).toBe(false);
|
||||
expect(isPerfDiagEnabled({
|
||||
METOYOU_PERF_DIAG: '1',
|
||||
METOYOU_PERF_DIAG_FORCE: '1'
|
||||
}, true)).toBe(true);
|
||||
});
|
||||
});
|
||||
29
electron/diagnostics/diagnostics.flags.ts
Normal file
29
electron/diagnostics/diagnostics.flags.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export const PERF_DIAG_ENV = 'METOYOU_PERF_DIAG';
|
||||
export const PERF_DIAG_FORCE_ENV = 'METOYOU_PERF_DIAG_FORCE';
|
||||
|
||||
const TRUTHY = new Set([
|
||||
'1',
|
||||
'true',
|
||||
'yes',
|
||||
'on'
|
||||
]);
|
||||
|
||||
function isTruthyFlag(value: string | undefined): boolean {
|
||||
return TRUTHY.has(String(value ?? '').trim()
|
||||
.toLowerCase());
|
||||
}
|
||||
|
||||
export function isPerfDiagEnabled(
|
||||
env: NodeJS.ProcessEnv,
|
||||
isPackaged: boolean
|
||||
): boolean {
|
||||
if (!isTruthyFlag(env[PERF_DIAG_ENV])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPackaged && !isTruthyFlag(env[PERF_DIAG_FORCE_ENV])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
214
electron/diagnostics/diagnostics.lifecycle.ts
Normal file
214
electron/diagnostics/diagnostics.lifecycle.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
ipcMain
|
||||
} from 'electron';
|
||||
import { collectAppMetricsSnapshot } from '../app-metrics';
|
||||
import { sumWorkingSetKb } from './process-metrics.rules';
|
||||
import { isPerfDiagEnabled } from './diagnostics.flags';
|
||||
import type { PerfDiagEntry } from './diagnostics.models';
|
||||
import { PerfDiagWriter } from './diagnostics.writer';
|
||||
|
||||
const PROCESS_POLL_INTERVAL_MS = 5_000;
|
||||
|
||||
let activeWriter: PerfDiagWriter | null = null;
|
||||
let processPollTimer: NodeJS.Timeout | null = null;
|
||||
let diagnosticsEnabled = false;
|
||||
let ipcRegistered = false;
|
||||
|
||||
export function isPerfDiagActive(): boolean {
|
||||
return diagnosticsEnabled;
|
||||
}
|
||||
|
||||
export function ensurePerfDiagIpcRegistered(): void {
|
||||
if (ipcRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRegistered = true;
|
||||
|
||||
ipcMain.handle('perf-diag-is-enabled', () => diagnosticsEnabled);
|
||||
|
||||
ipcMain.handle('perf-diag-report', (_event, entry: PerfDiagEntry) => {
|
||||
const writer = activeWriter;
|
||||
|
||||
if (!diagnosticsEnabled || !writer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
writer.append(normalizeRendererEntry(entry));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getActivePerfDiagWriter(): PerfDiagWriter | null {
|
||||
return activeWriter;
|
||||
}
|
||||
|
||||
export function startPerfDiagnostics(): PerfDiagWriter | null {
|
||||
ensurePerfDiagIpcRegistered();
|
||||
diagnosticsEnabled = isPerfDiagEnabled(process.env, app.isPackaged);
|
||||
|
||||
if (!diagnosticsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sessionId = `${Date.now().toString(36)}-${process.pid}`;
|
||||
const writer = new PerfDiagWriter({
|
||||
userDataPath: app.getPath('userData'),
|
||||
sessionId
|
||||
});
|
||||
|
||||
activeWriter = writer;
|
||||
registerProcessCrashHandlers(writer);
|
||||
startProcessMetricsPolling(writer);
|
||||
|
||||
writer.append({
|
||||
collectedAt: Date.now(),
|
||||
source: 'main',
|
||||
type: 'session',
|
||||
payload: {
|
||||
event: 'started',
|
||||
sessionId,
|
||||
filePath: writer.snapshotFilePath
|
||||
}
|
||||
});
|
||||
|
||||
return writer;
|
||||
}
|
||||
|
||||
export function attachRendererDiagnosticsHooks(window: BrowserWindow): void {
|
||||
const writer = activeWriter;
|
||||
|
||||
if (!writer) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.webContents.on('render-process-gone', (_event, details) => {
|
||||
writer.append({
|
||||
collectedAt: Date.now(),
|
||||
source: 'main',
|
||||
type: 'crash',
|
||||
payload: {
|
||||
reason: details.reason,
|
||||
exitCode: details.exitCode
|
||||
}
|
||||
});
|
||||
|
||||
void writer.flushSnapshot('render-process-gone');
|
||||
});
|
||||
|
||||
window.webContents.on('unresponsive', () => {
|
||||
writer.append({
|
||||
collectedAt: Date.now(),
|
||||
source: 'main',
|
||||
type: 'unresponsive',
|
||||
payload: {}
|
||||
});
|
||||
});
|
||||
|
||||
window.webContents.on('responsive', () => {
|
||||
writer.append({
|
||||
collectedAt: Date.now(),
|
||||
source: 'main',
|
||||
type: 'session',
|
||||
payload: { event: 'renderer-responsive' }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function shutdownPerfDiagnostics(): Promise<void> {
|
||||
if (!activeWriter) {
|
||||
return;
|
||||
}
|
||||
|
||||
await activeWriter.flushSnapshot('shutdown');
|
||||
|
||||
if (processPollTimer) {
|
||||
clearInterval(processPollTimer);
|
||||
processPollTimer = null;
|
||||
}
|
||||
|
||||
activeWriter = null;
|
||||
diagnosticsEnabled = false;
|
||||
}
|
||||
|
||||
function registerProcessCrashHandlers(writer: PerfDiagWriter): void {
|
||||
app.on('child-process-gone', (_event, details) => {
|
||||
writer.append({
|
||||
collectedAt: Date.now(),
|
||||
source: 'main',
|
||||
type: 'crash',
|
||||
payload: {
|
||||
type: details.type,
|
||||
reason: details.reason,
|
||||
exitCode: details.exitCode,
|
||||
serviceName: details.serviceName ?? null,
|
||||
name: details.name ?? null
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
writer.append({
|
||||
collectedAt: Date.now(),
|
||||
source: 'main',
|
||||
type: 'crash',
|
||||
payload: {
|
||||
scope: 'main-uncaughtException',
|
||||
message: error.message
|
||||
}
|
||||
});
|
||||
|
||||
void writer.flushSnapshot('uncaughtException');
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
writer.append({
|
||||
collectedAt: Date.now(),
|
||||
source: 'main',
|
||||
type: 'crash',
|
||||
payload: {
|
||||
scope: 'main-unhandledRejection',
|
||||
reason: String(reason)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startProcessMetricsPolling(writer: PerfDiagWriter): void {
|
||||
const sample = (): void => {
|
||||
try {
|
||||
const metrics = collectAppMetricsSnapshot();
|
||||
const totalKb = sumWorkingSetKb(metrics.processes);
|
||||
|
||||
writer.append({
|
||||
collectedAt: metrics.collectedAt,
|
||||
source: 'main',
|
||||
type: 'process',
|
||||
payload: {
|
||||
totalWorkingSetKb: totalKb,
|
||||
processes: metrics.processes
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// Collector failures must never affect the app.
|
||||
}
|
||||
};
|
||||
|
||||
sample();
|
||||
processPollTimer = setInterval(sample, PROCESS_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function normalizeRendererEntry(entry: PerfDiagEntry): PerfDiagEntry {
|
||||
return {
|
||||
collectedAt: Number(entry.collectedAt) || Date.now(),
|
||||
source: 'renderer',
|
||||
type: entry.type,
|
||||
payload: entry.payload ?? {}
|
||||
};
|
||||
}
|
||||
17
electron/diagnostics/diagnostics.models.ts
Normal file
17
electron/diagnostics/diagnostics.models.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type PerfDiagSource = 'main' | 'renderer';
|
||||
|
||||
export type PerfDiagEntryType =
|
||||
| 'session'
|
||||
| 'process'
|
||||
| 'store'
|
||||
| 'components'
|
||||
| 'heap'
|
||||
| 'crash'
|
||||
| 'unresponsive';
|
||||
|
||||
export interface PerfDiagEntry {
|
||||
collectedAt: number;
|
||||
source: PerfDiagSource;
|
||||
type: PerfDiagEntryType;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
53
electron/diagnostics/diagnostics.rules.spec.ts
Normal file
53
electron/diagnostics/diagnostics.rules.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect
|
||||
} from 'vitest';
|
||||
import {
|
||||
formatPerfDiagLine,
|
||||
pushRingBuffer,
|
||||
resolveDiagnosticsFilePath
|
||||
} from './diagnostics.rules';
|
||||
|
||||
describe('pushRingBuffer', () => {
|
||||
it('appends items until capacity is reached', () => {
|
||||
expect(pushRingBuffer([1, 2], 3, 4)).toEqual([
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops the oldest items when capacity is exceeded', () => {
|
||||
expect(pushRingBuffer([
|
||||
1,
|
||||
2,
|
||||
3
|
||||
], 4, 3)).toEqual([
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPerfDiagLine', () => {
|
||||
it('serializes one JSON object per line', () => {
|
||||
const line = formatPerfDiagLine({
|
||||
collectedAt: 1_700_000_000_000,
|
||||
source: 'main',
|
||||
type: 'process',
|
||||
payload: { browserKb: 128 }
|
||||
});
|
||||
|
||||
expect(line).toBe('{"collectedAt":1700000000000,"source":"main","type":"process","payload":{"browserKb":128}}');
|
||||
expect(line.endsWith('\n')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDiagnosticsFilePath', () => {
|
||||
it('places session files under diagnostics/', () => {
|
||||
expect(resolveDiagnosticsFilePath('/tmp/user-data', 'session-1'))
|
||||
.toBe('/tmp/user-data/diagnostics/perf-session-1.jsonl');
|
||||
});
|
||||
});
|
||||
24
electron/diagnostics/diagnostics.rules.ts
Normal file
24
electron/diagnostics/diagnostics.rules.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as path from 'path';
|
||||
import type { PerfDiagEntry } from './diagnostics.models';
|
||||
|
||||
export function pushRingBuffer<T>(items: readonly T[], item: T, capacity: number): T[] {
|
||||
const next = [...items, item];
|
||||
|
||||
if (next.length <= capacity) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return next.slice(next.length - capacity);
|
||||
}
|
||||
|
||||
export function formatPerfDiagLine(entry: PerfDiagEntry): string {
|
||||
return JSON.stringify(entry);
|
||||
}
|
||||
|
||||
export function resolveDiagnosticsFilePath(userDataPath: string, sessionId: string): string {
|
||||
return path.join(userDataPath, 'diagnostics', `perf-${sessionId}.jsonl`);
|
||||
}
|
||||
|
||||
export function resolveDiagnosticsDirectory(userDataPath: string): string {
|
||||
return path.join(userDataPath, 'diagnostics');
|
||||
}
|
||||
108
electron/diagnostics/diagnostics.writer.ts
Normal file
108
electron/diagnostics/diagnostics.writer.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import * as fsp from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import type { PerfDiagEntry } from './diagnostics.models';
|
||||
import {
|
||||
formatPerfDiagLine,
|
||||
pushRingBuffer,
|
||||
resolveDiagnosticsFilePath
|
||||
} from './diagnostics.rules';
|
||||
|
||||
const DEFAULT_RING_CAPACITY = 120;
|
||||
const FLUSH_DEBOUNCE_MS = 250;
|
||||
|
||||
export interface PerfDiagWriterOptions {
|
||||
userDataPath: string;
|
||||
sessionId: string;
|
||||
ringCapacity?: number;
|
||||
}
|
||||
|
||||
export class PerfDiagWriter {
|
||||
private readonly filePath: string;
|
||||
private readonly ringCapacity: number;
|
||||
private readonly pendingLines: string[] = [];
|
||||
private ring: PerfDiagEntry[] = [];
|
||||
private flushTimer: NodeJS.Timeout | null = null;
|
||||
private flushInFlight: Promise<void> | null = null;
|
||||
private disabled = false;
|
||||
|
||||
constructor(options: PerfDiagWriterOptions) {
|
||||
this.filePath = resolveDiagnosticsFilePath(options.userDataPath, options.sessionId);
|
||||
this.ringCapacity = options.ringCapacity ?? DEFAULT_RING_CAPACITY;
|
||||
}
|
||||
|
||||
get snapshotFilePath(): string {
|
||||
return this.filePath;
|
||||
}
|
||||
|
||||
get bufferedEntries(): readonly PerfDiagEntry[] {
|
||||
return this.ring;
|
||||
}
|
||||
|
||||
append(entry: PerfDiagEntry): void {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.ring = pushRingBuffer(this.ring, entry, this.ringCapacity);
|
||||
this.pendingLines.push(`${formatPerfDiagLine(entry)}\n`);
|
||||
this.scheduleFlush();
|
||||
} catch {
|
||||
this.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
if (this.disabled || this.pendingLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.flushInFlight) {
|
||||
await this.flushInFlight;
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = this.pendingLines.splice(0, this.pendingLines.length);
|
||||
|
||||
this.flushInFlight = this.writeLines(lines)
|
||||
.catch(() => {
|
||||
this.disabled = true;
|
||||
})
|
||||
.finally(() => {
|
||||
this.flushInFlight = null;
|
||||
});
|
||||
|
||||
await this.flushInFlight;
|
||||
}
|
||||
|
||||
async flushSnapshot(label: string): Promise<void> {
|
||||
this.append({
|
||||
collectedAt: Date.now(),
|
||||
source: 'main',
|
||||
type: 'session',
|
||||
payload: {
|
||||
event: label,
|
||||
filePath: this.filePath,
|
||||
entries: this.ring
|
||||
}
|
||||
});
|
||||
|
||||
await this.flush();
|
||||
}
|
||||
|
||||
private scheduleFlush(): void {
|
||||
if (this.flushTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.flushTimer = setTimeout(() => {
|
||||
this.flushTimer = null;
|
||||
void this.flush();
|
||||
}, FLUSH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
private async writeLines(lines: string[]): Promise<void> {
|
||||
await fsp.mkdir(path.dirname(this.filePath), { recursive: true });
|
||||
await fsp.appendFile(this.filePath, lines.join(''), 'utf8');
|
||||
}
|
||||
}
|
||||
11
electron/diagnostics/index.ts
Normal file
11
electron/diagnostics/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { isPerfDiagEnabled, PERF_DIAG_ENV, PERF_DIAG_FORCE_ENV } from './diagnostics.flags';
|
||||
export {
|
||||
attachRendererDiagnosticsHooks,
|
||||
ensurePerfDiagIpcRegistered,
|
||||
getActivePerfDiagWriter,
|
||||
isPerfDiagActive,
|
||||
shutdownPerfDiagnostics,
|
||||
startPerfDiagnostics
|
||||
} from './diagnostics.lifecycle';
|
||||
export type { PerfDiagEntry, PerfDiagEntryType, PerfDiagSource } from './diagnostics.models';
|
||||
export { PerfDiagWriter } from './diagnostics.writer';
|
||||
19
electron/diagnostics/process-metrics.rules.ts
Normal file
19
electron/diagnostics/process-metrics.rules.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface ProcessWorkingSetSnapshot {
|
||||
workingSetKb: number | null;
|
||||
}
|
||||
|
||||
export function sumWorkingSetKb(processes: readonly ProcessWorkingSetSnapshot[]): number | null {
|
||||
let total = 0;
|
||||
let hasAny = false;
|
||||
|
||||
for (const process of processes) {
|
||||
if (process.workingSetKb == null || process.workingSetKb < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
total += process.workingSetKb;
|
||||
hasAny = true;
|
||||
}
|
||||
|
||||
return hasAny ? total : null;
|
||||
}
|
||||
@@ -252,6 +252,13 @@ export interface ElectronAPI {
|
||||
workingSetKb: number | null;
|
||||
}[];
|
||||
}>;
|
||||
isPerfDiagEnabled: () => Promise<boolean>;
|
||||
reportPerfDiagSample: (entry: {
|
||||
collectedAt: number;
|
||||
source: 'main' | 'renderer';
|
||||
type: string;
|
||||
payload: Record<string, unknown>;
|
||||
}) => Promise<boolean>;
|
||||
getAppDataPath: () => Promise<string>;
|
||||
openCurrentDataFolder: () => Promise<boolean>;
|
||||
exportUserData: () => Promise<ExportUserDataResult>;
|
||||
@@ -388,6 +395,8 @@ const electronAPI: ElectronAPI = {
|
||||
};
|
||||
},
|
||||
getAppMetrics: () => ipcRenderer.invoke('get-app-metrics'),
|
||||
isPerfDiagEnabled: () => ipcRenderer.invoke('perf-diag-is-enabled'),
|
||||
reportPerfDiagSample: (entry) => ipcRenderer.invoke('perf-diag-report', entry),
|
||||
getAppDataPath: () => ipcRenderer.invoke('get-app-data-path'),
|
||||
openCurrentDataFolder: () => ipcRenderer.invoke('open-current-data-folder'),
|
||||
exportUserData: () => ipcRenderer.invoke('export-user-data'),
|
||||
|
||||
Reference in New Issue
Block a user