Improve attachment memory safety, downloads, and high-memory alert UX.
All checks were successful
Queue Release Build / prepare (push) Successful in 20s
Deploy Web Apps / deploy (push) Successful in 9m2s
Queue Release Build / build-windows (push) Successful in 28m8s
Queue Release Build / build-linux (push) Successful in 47m26s
Queue Release Build / build-android (push) Successful in 19m52s
Queue Release Build / finalize (push) Successful in 4m42s
All checks were successful
Queue Release Build / prepare (push) Successful in 20s
Deploy Web Apps / deploy (push) Successful in 9m2s
Queue Release Build / build-windows (push) Successful in 28m8s
Queue Release Build / build-linux (push) Successful in 47m26s
Queue Release Build / build-android (push) Successful in 19m52s
Queue Release Build / finalize (push) Successful in 4m42s
Stream large receives to disk with chunk acks to cap renderer RAM, evict off-screen display blobs, and route exports through a disk-aware download service. Fix the high-memory dialog (backdrop dismiss, copy, log actions), allow diagnostics paths in the path jail, and restore persisted image hydration after reload. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,7 +25,9 @@ import { startIdleMonitor, stopIdleMonitor } from '../idle/idle-monitor';
|
||||
import {
|
||||
attachRendererDiagnosticsHooks,
|
||||
ensurePerfDiagIpcRegistered,
|
||||
shutdownHighMemoryMonitoring,
|
||||
shutdownPerfDiagnostics,
|
||||
startHighMemoryMonitoring,
|
||||
startPerfDiagnostics
|
||||
} from '../diagnostics';
|
||||
|
||||
@@ -39,6 +41,7 @@ function startLocalApiAfterWindowReady(): void {
|
||||
|
||||
export function registerAppLifecycle(): void {
|
||||
ensurePerfDiagIpcRegistered();
|
||||
startHighMemoryMonitoring();
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const dockIconPath = getDockIconPath();
|
||||
@@ -83,6 +86,7 @@ export function registerAppLifecycle(): void {
|
||||
|
||||
app.on('before-quit', async (event) => {
|
||||
prepareWindowForAppQuit();
|
||||
shutdownHighMemoryMonitoring();
|
||||
await shutdownPerfDiagnostics();
|
||||
|
||||
if (getDataSource()?.isInitialized) {
|
||||
|
||||
@@ -10,19 +10,21 @@ import { resolveReadablePath } from '../path-jail';
|
||||
import { sumWorkingSetKb } from './process-metrics.rules';
|
||||
import { isPerfDiagEnabled } from './diagnostics.flags';
|
||||
import { exceedsHighMemoryThreshold } from './high-memory-alert.rules';
|
||||
import { buildHighMemoryDiagnosticPayload } from './high-memory-snapshot.rules';
|
||||
import { collectImmediateRendererSamples } from './immediate-renderer-samples.collector';
|
||||
import { captureHighMemoryDiagnostics } from './high-memory-capture';
|
||||
import { collectSessionContext } from './session-context.collector';
|
||||
import {
|
||||
clearHighMemoryAlert,
|
||||
readHighMemoryAlert,
|
||||
writeHighMemoryAlert
|
||||
writeHighMemoryAlert,
|
||||
type HighMemoryAlertRecord
|
||||
} from './high-memory-alert.store';
|
||||
import type { PerfDiagEntry } from './diagnostics.models';
|
||||
import { PerfDiagWriter } from './diagnostics.writer';
|
||||
|
||||
const PROCESS_POLL_INTERVAL_MS = 5_000;
|
||||
|
||||
export const HIGH_MEMORY_ALERT_PENDING_CHANNEL = 'high-memory-alert-pending';
|
||||
|
||||
let activeWriter: PerfDiagWriter | null = null;
|
||||
let processPollTimer: NodeJS.Timeout | null = null;
|
||||
let diagnosticsEnabled = false;
|
||||
@@ -67,6 +69,24 @@ export function ensurePerfDiagIpcRegistered(): void {
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('export-high-memory-diagnostics', async () => {
|
||||
const metrics = collectAppMetricsSnapshot();
|
||||
const totalKb = sumWorkingSetKb(metrics.processes) ?? 0;
|
||||
const record = await captureHighMemoryDiagnostics({
|
||||
userDataPath: app.getPath('userData'),
|
||||
sessionStartedAt,
|
||||
metrics,
|
||||
totalWorkingSetKb: totalKb,
|
||||
writer: activeWriter,
|
||||
mainWindow: getMainWindow(),
|
||||
reason: 'manual'
|
||||
});
|
||||
|
||||
await persistAndNotifyHighMemoryAlert(record);
|
||||
|
||||
return record;
|
||||
});
|
||||
|
||||
ipcMain.handle('show-log-file-in-folder', async (_event, filePath: string) => {
|
||||
if (typeof filePath !== 'string' || !filePath.trim()) {
|
||||
return {
|
||||
@@ -94,8 +114,48 @@ export function getActivePerfDiagWriter(): PerfDiagWriter | null {
|
||||
return activeWriter;
|
||||
}
|
||||
|
||||
export function startHighMemoryMonitoring(): void {
|
||||
ensurePerfDiagIpcRegistered();
|
||||
|
||||
if (!sessionStartedAt) {
|
||||
sessionStartedAt = Date.now();
|
||||
highMemoryAlertTriggeredThisSession = false;
|
||||
}
|
||||
|
||||
if (processPollTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sample = (): void => {
|
||||
try {
|
||||
const metrics = collectAppMetricsSnapshot();
|
||||
const totalKb = sumWorkingSetKb(metrics.processes);
|
||||
|
||||
if (activeWriter && diagnosticsEnabled) {
|
||||
activeWriter.append({
|
||||
collectedAt: metrics.collectedAt,
|
||||
source: 'main',
|
||||
type: 'process',
|
||||
payload: {
|
||||
totalWorkingSetKb: totalKb,
|
||||
processes: metrics.processes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void maybeTriggerHighMemoryAlert(metrics, totalKb);
|
||||
} catch {
|
||||
// Collector failures must never affect the app.
|
||||
}
|
||||
};
|
||||
|
||||
sample();
|
||||
processPollTimer = setInterval(sample, PROCESS_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
export function startPerfDiagnostics(): PerfDiagWriter | null {
|
||||
ensurePerfDiagIpcRegistered();
|
||||
startHighMemoryMonitoring();
|
||||
diagnosticsEnabled = isPerfDiagEnabled(process.env, app.isPackaged);
|
||||
|
||||
if (!diagnosticsEnabled) {
|
||||
@@ -109,10 +169,7 @@ export function startPerfDiagnostics(): PerfDiagWriter | null {
|
||||
});
|
||||
|
||||
activeWriter = writer;
|
||||
highMemoryAlertTriggeredThisSession = false;
|
||||
sessionStartedAt = Date.now();
|
||||
registerProcessCrashHandlers(writer);
|
||||
startProcessMetricsPolling(writer);
|
||||
|
||||
const userDataPath = app.getPath('userData');
|
||||
|
||||
@@ -188,14 +245,15 @@ export async function shutdownPerfDiagnostics(): Promise<void> {
|
||||
}
|
||||
|
||||
await activeWriter.flushSnapshot('shutdown');
|
||||
activeWriter = null;
|
||||
diagnosticsEnabled = false;
|
||||
}
|
||||
|
||||
export function shutdownHighMemoryMonitoring(): void {
|
||||
if (processPollTimer) {
|
||||
clearInterval(processPollTimer);
|
||||
processPollTimer = null;
|
||||
}
|
||||
|
||||
activeWriter = null;
|
||||
diagnosticsEnabled = false;
|
||||
}
|
||||
|
||||
function registerProcessCrashHandlers(writer: PerfDiagWriter): void {
|
||||
@@ -241,34 +299,7 @@ function registerProcessCrashHandlers(writer: PerfDiagWriter): void {
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
void maybeTriggerHighMemoryAlert(writer, metrics, totalKb);
|
||||
} catch {
|
||||
// Collector failures must never affect the app.
|
||||
}
|
||||
};
|
||||
|
||||
sample();
|
||||
processPollTimer = setInterval(sample, PROCESS_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async function maybeTriggerHighMemoryAlert(
|
||||
writer: PerfDiagWriter,
|
||||
metrics: AppMetricsSnapshot,
|
||||
totalWorkingSetKb: number | null
|
||||
): Promise<void> {
|
||||
@@ -278,51 +309,26 @@ async function maybeTriggerHighMemoryAlert(
|
||||
|
||||
highMemoryAlertTriggeredThisSession = true;
|
||||
|
||||
const detectedAt = Date.now();
|
||||
const userDataPath = app.getPath('userData');
|
||||
const immediateRendererEntries = await collectImmediateRendererSamples(getMainWindow());
|
||||
const environment = collectSessionContext({
|
||||
const record = await captureHighMemoryDiagnostics({
|
||||
userDataPath: app.getPath('userData'),
|
||||
sessionStartedAt,
|
||||
userDataPath
|
||||
metrics,
|
||||
totalWorkingSetKb: totalWorkingSetKb ?? 0,
|
||||
writer: activeWriter,
|
||||
mainWindow: getMainWindow(),
|
||||
reason: 'threshold'
|
||||
});
|
||||
|
||||
for (const entry of immediateRendererEntries) {
|
||||
writer.append(entry);
|
||||
}
|
||||
await persistAndNotifyHighMemoryAlert(record);
|
||||
}
|
||||
|
||||
writer.append({
|
||||
collectedAt: detectedAt,
|
||||
source: 'main',
|
||||
type: 'environment',
|
||||
payload: {
|
||||
...environment
|
||||
}
|
||||
});
|
||||
async function persistAndNotifyHighMemoryAlert(record: HighMemoryAlertRecord): Promise<void> {
|
||||
await writeHighMemoryAlert(app.getPath('userData'), record);
|
||||
notifyHighMemoryAlert(record);
|
||||
}
|
||||
|
||||
writer.append({
|
||||
collectedAt: detectedAt,
|
||||
source: 'main',
|
||||
type: 'high-memory',
|
||||
payload: buildHighMemoryDiagnosticPayload({
|
||||
detectedAt,
|
||||
totalWorkingSetKb: totalWorkingSetKb ?? 0,
|
||||
metrics,
|
||||
environment,
|
||||
mainProcessMemory: process.memoryUsage(),
|
||||
ringEntries: writer.bufferedEntries,
|
||||
immediateRendererEntries,
|
||||
sessionId: writer.sessionId
|
||||
})
|
||||
});
|
||||
|
||||
await writer.flushSnapshot('high-memory-threshold');
|
||||
|
||||
await writeHighMemoryAlert(userDataPath, {
|
||||
logFilePath: writer.snapshotFilePath,
|
||||
detectedAt,
|
||||
peakWorkingSetKb: totalWorkingSetKb ?? 0,
|
||||
sessionId: writer.sessionId
|
||||
});
|
||||
function notifyHighMemoryAlert(record: HighMemoryAlertRecord): void {
|
||||
getMainWindow()?.webContents.send(HIGH_MEMORY_ALERT_PENDING_CHANNEL, record);
|
||||
}
|
||||
|
||||
function normalizeRendererEntry(entry: PerfDiagEntry): PerfDiagEntry {
|
||||
|
||||
@@ -33,7 +33,8 @@ describe('high-memory-alert.store', () => {
|
||||
logFilePath: path.join(userDataPath, 'diagnostics', 'perf-session.jsonl'),
|
||||
detectedAt: 1_700_000_000_000,
|
||||
peakWorkingSetKb: 2_200_000,
|
||||
sessionId: 'session-1'
|
||||
sessionId: 'session-1',
|
||||
reason: 'threshold' as const
|
||||
};
|
||||
|
||||
await writeHighMemoryAlert(userDataPath, record);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as fsp from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
export type HighMemoryAlertReason = 'manual' | 'threshold';
|
||||
|
||||
export interface HighMemoryAlertRecord {
|
||||
logFilePath: string;
|
||||
detectedAt: number;
|
||||
peakWorkingSetKb: number;
|
||||
sessionId: string;
|
||||
reason?: HighMemoryAlertReason;
|
||||
}
|
||||
|
||||
export function resolveHighMemoryAlertPath(userDataPath: string): string {
|
||||
@@ -31,7 +34,10 @@ export async function readHighMemoryAlert(userDataPath: string): Promise<HighMem
|
||||
logFilePath: parsed.logFilePath,
|
||||
detectedAt: parsed.detectedAt,
|
||||
peakWorkingSetKb: parsed.peakWorkingSetKb,
|
||||
sessionId: parsed.sessionId
|
||||
sessionId: parsed.sessionId,
|
||||
...(parsed.reason === 'manual' || parsed.reason === 'threshold'
|
||||
? { reason: parsed.reason }
|
||||
: {})
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
57
electron/diagnostics/high-memory-capture.spec.ts
Normal file
57
electron/diagnostics/high-memory-capture.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fsp from 'fs/promises';
|
||||
|
||||
import { captureHighMemoryDiagnostics } from './high-memory-capture';
|
||||
|
||||
vi.mock('./immediate-renderer-samples.collector', () => ({
|
||||
collectImmediateRendererSamples: vi.fn(async () => [])
|
||||
}));
|
||||
|
||||
vi.mock('./session-context.collector', () => ({
|
||||
collectSessionContext: vi.fn(() => ({
|
||||
platform: 'linux',
|
||||
userDataPath: '/tmp/user-data'
|
||||
}))
|
||||
}));
|
||||
|
||||
describe('captureHighMemoryDiagnostics', () => {
|
||||
let userDataPath = '';
|
||||
|
||||
beforeEach(async () => {
|
||||
userDataPath = await fsp.mkdtemp(path.join(os.tmpdir(), 'metoyou-high-memory-capture-'));
|
||||
});
|
||||
|
||||
it('writes a diagnostics snapshot and returns an alert record', async () => {
|
||||
const record = await captureHighMemoryDiagnostics({
|
||||
userDataPath,
|
||||
sessionStartedAt: Date.now() - 60_000,
|
||||
metrics: {
|
||||
collectedAt: Date.now(),
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
type: 'Browser',
|
||||
workingSetKb: 2_200_000
|
||||
}
|
||||
]
|
||||
},
|
||||
totalWorkingSetKb: 2_200_000,
|
||||
writer: null,
|
||||
mainWindow: null,
|
||||
reason: 'manual'
|
||||
});
|
||||
|
||||
expect(record.peakWorkingSetKb).toBe(2_200_000);
|
||||
expect(record.reason).toBe('manual');
|
||||
expect(record.logFilePath).toContain(userDataPath);
|
||||
await expect(fsp.stat(record.logFilePath)).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
80
electron/diagnostics/high-memory-capture.ts
Normal file
80
electron/diagnostics/high-memory-capture.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { AppMetricsSnapshot } from '../app-metrics';
|
||||
import { buildHighMemoryDiagnosticPayload } from './high-memory-snapshot.rules';
|
||||
import { collectImmediateRendererSamples } from './immediate-renderer-samples.collector';
|
||||
import { collectSessionContext } from './session-context.collector';
|
||||
import type { HighMemoryAlertRecord } from './high-memory-alert.store';
|
||||
import type { PerfDiagEntry } from './diagnostics.models';
|
||||
import { PerfDiagWriter } from './diagnostics.writer';
|
||||
|
||||
export type HighMemoryCaptureReason = 'manual' | 'threshold';
|
||||
|
||||
export interface CaptureHighMemoryDiagnosticsInput {
|
||||
userDataPath: string;
|
||||
sessionStartedAt: number;
|
||||
metrics: AppMetricsSnapshot;
|
||||
totalWorkingSetKb: number;
|
||||
writer: PerfDiagWriter | null;
|
||||
mainWindow: BrowserWindow | null;
|
||||
reason: HighMemoryCaptureReason;
|
||||
}
|
||||
|
||||
export async function captureHighMemoryDiagnostics(
|
||||
input: CaptureHighMemoryDiagnosticsInput
|
||||
): Promise<HighMemoryAlertRecord> {
|
||||
const detectedAt = Date.now();
|
||||
const writer = input.writer ?? new PerfDiagWriter({
|
||||
userDataPath: input.userDataPath,
|
||||
sessionId: `${input.reason}-${detectedAt.toString(36)}-${process.pid}`
|
||||
});
|
||||
const immediateRendererEntries = await collectImmediateRendererSamples(input.mainWindow);
|
||||
const environment = collectSessionContext({
|
||||
sessionStartedAt: input.sessionStartedAt,
|
||||
userDataPath: input.userDataPath
|
||||
});
|
||||
|
||||
appendEntries(writer, immediateRendererEntries);
|
||||
appendEntries(writer, [
|
||||
{
|
||||
collectedAt: detectedAt,
|
||||
source: 'main',
|
||||
type: 'environment',
|
||||
payload: {
|
||||
...environment
|
||||
}
|
||||
},
|
||||
{
|
||||
collectedAt: detectedAt,
|
||||
source: 'main',
|
||||
type: 'high-memory',
|
||||
payload: buildHighMemoryDiagnosticPayload({
|
||||
detectedAt,
|
||||
totalWorkingSetKb: input.totalWorkingSetKb,
|
||||
metrics: input.metrics,
|
||||
environment,
|
||||
mainProcessMemory: process.memoryUsage(),
|
||||
ringEntries: writer.bufferedEntries,
|
||||
immediateRendererEntries,
|
||||
sessionId: writer.sessionId
|
||||
})
|
||||
}
|
||||
]);
|
||||
|
||||
await writer.flushSnapshot(
|
||||
input.reason === 'manual' ? 'manual-export' : 'high-memory-threshold'
|
||||
);
|
||||
|
||||
return {
|
||||
logFilePath: writer.snapshotFilePath,
|
||||
detectedAt,
|
||||
peakWorkingSetKb: input.totalWorkingSetKb,
|
||||
sessionId: writer.sessionId,
|
||||
reason: input.reason
|
||||
};
|
||||
}
|
||||
|
||||
function appendEntries(writer: PerfDiagWriter, entries: readonly PerfDiagEntry[]): void {
|
||||
for (const entry of entries) {
|
||||
writer.append(entry);
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,11 @@ export {
|
||||
attachRendererDiagnosticsHooks,
|
||||
ensurePerfDiagIpcRegistered,
|
||||
getActivePerfDiagWriter,
|
||||
HIGH_MEMORY_ALERT_PENDING_CHANNEL,
|
||||
isPerfDiagActive,
|
||||
shutdownHighMemoryMonitoring,
|
||||
shutdownPerfDiagnostics,
|
||||
startHighMemoryMonitoring,
|
||||
startPerfDiagnostics
|
||||
} from './diagnostics.lifecycle';
|
||||
export type { PerfDiagEntry, PerfDiagEntryType, PerfDiagSource } from './diagnostics.models';
|
||||
|
||||
16
electron/ipc/file-read.rules.spec.ts
Normal file
16
electron/ipc/file-read.rules.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
import { isReadableRegularFile } from './file-read.rules';
|
||||
|
||||
describe('file-read.rules', () => {
|
||||
it('accepts regular files', () => {
|
||||
expect(isReadableRegularFile({ isFile: () => true })).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects directories and other non-file paths', () => {
|
||||
expect(isReadableRegularFile({ isFile: () => false })).toBe(false);
|
||||
});
|
||||
});
|
||||
6
electron/ipc/file-read.rules.ts
Normal file
6
electron/ipc/file-read.rules.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Stats } from 'fs';
|
||||
|
||||
/** Only regular files can be read through the read-file IPC surface. */
|
||||
export function isReadableRegularFile(stats: Pick<Stats, 'isFile'>): boolean {
|
||||
return stats.isFile();
|
||||
}
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
grantPluginReadRoot,
|
||||
resolveReadablePath
|
||||
} from '../path-jail';
|
||||
import { isReadableRegularFile } from './file-read.rules';
|
||||
|
||||
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
||||
const MAX_ACTIVE_DESKTOP_NOTIFICATIONS = 20;
|
||||
@@ -654,9 +655,19 @@ export function setupSystemHandlers(): void {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await fsp.readFile(scopedPath);
|
||||
try {
|
||||
const stats = await fsp.stat(scopedPath);
|
||||
|
||||
return data.toString('base64');
|
||||
if (!isReadableRegularFile(stats)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await fsp.readFile(scopedPath);
|
||||
|
||||
return data.toString('base64');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('read-file-chunk', async (_event, filePath: string, start: number, end: number) => {
|
||||
@@ -666,17 +677,27 @@ export function setupSystemHandlers(): void {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileHandle = await fsp.open(scopedPath, 'r');
|
||||
|
||||
try {
|
||||
const safeStart = Math.max(0, Math.trunc(start));
|
||||
const safeEnd = Math.max(safeStart, Math.trunc(end));
|
||||
const buffer = Buffer.alloc(safeEnd - safeStart);
|
||||
const result = await fileHandle.read(buffer, 0, buffer.length, safeStart);
|
||||
const stats = await fsp.stat(scopedPath);
|
||||
|
||||
return buffer.subarray(0, result.bytesRead).toString('base64');
|
||||
} finally {
|
||||
await fileHandle.close();
|
||||
if (!isReadableRegularFile(stats)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileHandle = await fsp.open(scopedPath, 'r');
|
||||
|
||||
try {
|
||||
const safeStart = Math.max(0, Math.trunc(start));
|
||||
const safeEnd = Math.max(safeStart, Math.trunc(end));
|
||||
const buffer = Buffer.alloc(safeEnd - safeStart);
|
||||
const result = await fileHandle.read(buffer, 0, buffer.length, safeStart);
|
||||
|
||||
return buffer.subarray(0, result.bytesRead).toString('base64');
|
||||
} finally {
|
||||
await fileHandle.close();
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -728,6 +749,17 @@ export function setupSystemHandlers(): void {
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('append-file-bytes', async (_event, filePath: string, bytes: Uint8Array) => {
|
||||
const scopedPath = await resolveWritableUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await fsp.appendFile(scopedPath, Buffer.from(bytes));
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-file', async (_event, filePath: string) => {
|
||||
const scopedPath = await resolveWritableUserDataFilePath(filePath);
|
||||
|
||||
|
||||
@@ -35,6 +35,17 @@ describe('path-jail', () => {
|
||||
await expect(assertPathUnderRoot(tempRoot, allowedPath, ['server'])).resolves.toBe(allowedPath);
|
||||
});
|
||||
|
||||
it('accepts diagnostics log paths under diagnostics', async () => {
|
||||
const diagnosticsDir = path.join(tempRoot, 'diagnostics');
|
||||
|
||||
fs.mkdirSync(diagnosticsDir, { recursive: true });
|
||||
const logPath = path.join(diagnosticsDir, 'perf-session.jsonl');
|
||||
|
||||
fs.writeFileSync(logPath, '{}');
|
||||
|
||||
await expect(assertPathUnderRoot(tempRoot, logPath)).resolves.toBe(logPath);
|
||||
});
|
||||
|
||||
it('accepts cached plugin bundle paths under plugin-bundles', async () => {
|
||||
const bundleDir = path.join(tempRoot, 'plugin-bundles', 'example.plugin', '1.0.0');
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ export const DEFAULT_USER_DATA_SUBDIRS = [
|
||||
'plugin-bundles',
|
||||
'plugin-cache',
|
||||
'themes',
|
||||
'metoyou'
|
||||
'metoyou',
|
||||
'diagnostics'
|
||||
] as const;
|
||||
|
||||
export function isPathInside(parentPath: string, candidatePath: string): boolean {
|
||||
|
||||
@@ -11,6 +11,7 @@ const AUTO_UPDATE_STATE_CHANGED_CHANNEL = 'auto-update-state-changed';
|
||||
const DEEP_LINK_RECEIVED_CHANNEL = 'deep-link-received';
|
||||
const WINDOW_STATE_CHANGED_CHANNEL = 'window-state-changed';
|
||||
const IDLE_STATE_CHANGED_CHANNEL = 'idle-state-changed';
|
||||
const HIGH_MEMORY_ALERT_PENDING_CHANNEL = 'high-memory-alert-pending';
|
||||
|
||||
export interface LinuxScreenShareAudioRoutingInfo {
|
||||
available: boolean;
|
||||
@@ -264,8 +265,23 @@ export interface ElectronAPI {
|
||||
detectedAt: number;
|
||||
peakWorkingSetKb: number;
|
||||
sessionId: string;
|
||||
reason?: 'manual' | 'threshold';
|
||||
} | null>;
|
||||
acknowledgeHighMemoryAlert: () => Promise<boolean>;
|
||||
exportHighMemoryDiagnostics: () => Promise<{
|
||||
logFilePath: string;
|
||||
detectedAt: number;
|
||||
peakWorkingSetKb: number;
|
||||
sessionId: string;
|
||||
reason?: 'manual' | 'threshold';
|
||||
}>;
|
||||
onHighMemoryAlertPending: (listener: (alert: {
|
||||
logFilePath: string;
|
||||
detectedAt: number;
|
||||
peakWorkingSetKb: number;
|
||||
sessionId: string;
|
||||
reason?: 'manual' | 'threshold';
|
||||
}) => void) => () => void;
|
||||
showLogFileInFolder: (filePath: string) => Promise<{ shown: boolean; reason?: string }>;
|
||||
getAppDataPath: () => Promise<string>;
|
||||
openCurrentDataFolder: () => Promise<boolean>;
|
||||
@@ -335,6 +351,7 @@ export interface ElectronAPI {
|
||||
grantPluginReadRoot: (rootPath: string) => Promise<boolean>;
|
||||
writeFile: (filePath: string, data: string) => Promise<boolean>;
|
||||
appendFile: (filePath: string, data: string) => Promise<boolean>;
|
||||
appendFileBytes: (filePath: string, data: Uint8Array) => Promise<boolean>;
|
||||
saveFileAs: (defaultFileName: string, data: string) => Promise<{ saved: boolean; cancelled: boolean }>;
|
||||
saveExistingFileAs: (sourceFilePath: string, defaultFileName: string) => Promise<{ saved: boolean; cancelled: boolean }>;
|
||||
openFilePath: (filePath: string) => Promise<{ opened: boolean; reason?: string }>;
|
||||
@@ -410,6 +427,23 @@ const electronAPI: ElectronAPI = {
|
||||
reportPerfDiagSample: (entry) => ipcRenderer.invoke('perf-diag-report', entry),
|
||||
getPendingHighMemoryAlert: () => ipcRenderer.invoke('get-pending-high-memory-alert'),
|
||||
acknowledgeHighMemoryAlert: () => ipcRenderer.invoke('acknowledge-high-memory-alert'),
|
||||
exportHighMemoryDiagnostics: () => ipcRenderer.invoke('export-high-memory-diagnostics'),
|
||||
onHighMemoryAlertPending: (listener) => {
|
||||
const wrappedListener = (_event: Electron.IpcRendererEvent, alert: {
|
||||
logFilePath: string;
|
||||
detectedAt: number;
|
||||
peakWorkingSetKb: number;
|
||||
sessionId: string;
|
||||
}) => {
|
||||
listener(alert);
|
||||
};
|
||||
|
||||
ipcRenderer.on(HIGH_MEMORY_ALERT_PENDING_CHANNEL, wrappedListener);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener(HIGH_MEMORY_ALERT_PENDING_CHANNEL, wrappedListener);
|
||||
};
|
||||
},
|
||||
showLogFileInFolder: (filePath) => ipcRenderer.invoke('show-log-file-in-folder', filePath),
|
||||
getAppDataPath: () => ipcRenderer.invoke('get-app-data-path'),
|
||||
openCurrentDataFolder: () => ipcRenderer.invoke('open-current-data-folder'),
|
||||
@@ -478,6 +512,7 @@ const electronAPI: ElectronAPI = {
|
||||
grantPluginReadRoot: (rootPath) => ipcRenderer.invoke('grant-plugin-read-root', rootPath),
|
||||
writeFile: (filePath, data) => ipcRenderer.invoke('write-file', filePath, data),
|
||||
appendFile: (filePath, data) => ipcRenderer.invoke('append-file', filePath, data),
|
||||
appendFileBytes: (filePath, data) => ipcRenderer.invoke('append-file-bytes', filePath, data),
|
||||
saveFileAs: (defaultFileName, data) => ipcRenderer.invoke('save-file-as', defaultFileName, data),
|
||||
saveExistingFileAs: (sourceFilePath, defaultFileName) => ipcRenderer.invoke('save-existing-file-as', sourceFilePath, defaultFileName),
|
||||
openFilePath: (filePath) => ipcRenderer.invoke('open-file-path', filePath),
|
||||
|
||||
Reference in New Issue
Block a user