perf(clipboard): isolate remote staging cleanup

This commit is contained in:
OrcaWin
2026-08-09 22:03:19 -07:00
parent d9e84d88ef
commit 4ea51965e3
8 changed files with 761 additions and 216 deletions
@@ -39,6 +39,7 @@ vi.mock('electron', () => ({
vi.mock('./dashboard-popout-window', () => ({ isDashboardPopoutRenderer }))
vi.mock('./clipboard-remote-file-copy', () => ({
cleanupExpiredRemoteClipboardFiles: vi.fn(async () => undefined),
scheduleLegacyRemoteClipboardFileCleanup: vi.fn(),
writeRemoteFileToClipboard: vi.fn()
}))
+33 -6
View File
@@ -12,6 +12,8 @@ const {
spawnMock,
childStdinEndMock,
resolveAuthorizedPathMock,
fsAccessMock,
fsLstatMock,
fsMkdirMock,
fsOpendirMock,
fsRmMock,
@@ -45,6 +47,8 @@ const {
return child
}),
resolveAuthorizedPathMock: vi.fn(),
fsAccessMock: vi.fn(),
fsLstatMock: vi.fn(),
fsMkdirMock: vi.fn(),
fsOpendirMock: vi.fn(),
fsRmMock: vi.fn(),
@@ -68,11 +72,14 @@ vi.mock('node:child_process', () => ({
}))
vi.mock('node:fs/promises', () => ({
access: fsAccessMock,
lstat: fsLstatMock,
mkdir: fsMkdirMock,
opendir: fsOpendirMock,
rm: fsRmMock,
open: fsOpenMock,
stat: fsStatMock,
writeFile: fsWriteFileMock,
default: {
writeFile: fsWriteFileMock
}
@@ -134,6 +141,11 @@ import {
setTrustedClipboardRendererWebContentsId
} from './clipboard-ipc-handlers'
const REMOTE_CLIPBOARD_STAGING_ROOT = join(
'/tmp',
`orca-clipboard-files${typeof process.getuid === 'function' ? `-${process.getuid()}` : ''}`
)
function getRegisteredHandlers(): Map<string, (...args: unknown[]) => unknown> {
const handlers = new Map<string, (...args: unknown[]) => unknown>()
for (const [channel, handler] of handleMock.mock.calls as [
@@ -187,6 +199,13 @@ describe('registerClipboardHandlers', () => {
childStdinEndMock.mockClear()
resolveAuthorizedPathMock.mockReset()
resolveAuthorizedPathMock.mockImplementation(async (path: string) => path)
fsLstatMock.mockReset()
fsLstatMock.mockResolvedValue({
isDirectory: () => true,
isSymbolicLink: () => false,
mode: 0o700,
uid: typeof process.getuid === 'function' ? process.getuid() : 0
})
fsMkdirMock.mockReset()
fsMkdirMock.mockResolvedValue(undefined)
fsOpendirMock.mockReset()
@@ -316,8 +335,8 @@ describe('registerClipboardHandlers', () => {
const handlers = getRegisteredHandlers()
const tempDir = join(
'/tmp',
'orca-clipboard-file-1760000000000-00000000-0000-4000-8000-000000000000'
REMOTE_CLIPBOARD_STAGING_ROOT,
'1760000000000-00000000-0000-4000-8000-000000000000'
)
const tempPath = join(tempDir, 'report.pdf')
@@ -329,6 +348,10 @@ describe('registerClipboardHandlers', () => {
).resolves.toEqual({ ok: true })
expect(provider.stat).toHaveBeenCalledWith('/remote/report.pdf')
expect(fsMkdirMock).toHaveBeenCalledWith(REMOTE_CLIPBOARD_STAGING_ROOT, {
recursive: true,
mode: 0o700
})
expect(fsMkdirMock).toHaveBeenCalledWith(tempDir, { mode: 0o700 })
expect(provider.downloadFile).toHaveBeenCalledWith('/remote/report.pdf', tempPath)
expect(fsStatMock).toHaveBeenCalledWith(tempPath)
@@ -353,7 +376,6 @@ describe('registerClipboardHandlers', () => {
).resolves.toEqual({ ok: false, reason: 'is-directory' })
expect(provider.downloadFile).not.toHaveBeenCalled()
expect(fsMkdirMock).not.toHaveBeenCalled()
expect(clipboardWriteBufferMock).not.toHaveBeenCalled()
})
@@ -367,8 +389,8 @@ describe('registerClipboardHandlers', () => {
const handlers = getRegisteredHandlers()
const tempDir = join(
'/tmp',
'orca-clipboard-file-1760000000000-00000000-0000-4000-8000-000000000000'
REMOTE_CLIPBOARD_STAGING_ROOT,
'1760000000000-00000000-0000-4000-8000-000000000000'
)
const tempPath = join(tempDir, 'report.pdf')
@@ -380,7 +402,12 @@ describe('registerClipboardHandlers', () => {
).rejects.toThrow('transfer failed')
expect(provider.downloadFile).toHaveBeenCalledWith('/remote/report.pdf', tempPath)
expect(fsRmMock).toHaveBeenCalledWith(tempDir, { recursive: true, force: true })
expect(fsRmMock).toHaveBeenCalledWith(tempDir, {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 100
})
expect(clipboardWriteBufferMock).not.toHaveBeenCalled()
})
@@ -31,6 +31,7 @@ import {
} from './clipboard-file-copy'
import {
cleanupExpiredRemoteClipboardFiles,
scheduleLegacyRemoteClipboardFileCleanup,
writeRemoteFileToClipboard
} from './clipboard-remote-file-copy'
import { saveClipboardImageBufferInRuntime } from './clipboard-runtime-image-upload'
@@ -85,6 +86,7 @@ export function registerClipboardHandlers(store: Store): void {
ipcMain.removeHandler('clipboard:saveImageAsTempFile')
void cleanupExpiredRemoteClipboardFiles()
scheduleLegacyRemoteClipboardFileCleanup()
ipcMain.handle('clipboard:readText', async (event, options?: ReadClipboardTextOptions) => {
assertTrustedClipboardTextSender(event)
@@ -0,0 +1,123 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { access, mkdir, mkdtemp, opendir, readdir, rm, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import {
cleanupExpiredRemoteClipboardStaging,
createRemoteClipboardTransferDirectory
} from './clipboard-remote-file-staging'
const BENCH_ENABLED = process.env.ORCA_CLIPBOARD_CLEANUP_BENCH === '1'
const FOREIGN_ENTRY_COUNT = Number(process.env.ORCA_CLIPBOARD_CLEANUP_BENCH_ENTRIES ?? 100_000)
const WARMUP_RUNS = 3
const SAMPLE_RUNS = 25
const FIXTURE_PREFIX = 'orca-clipboard-cleanup-bench-'
const NOW_MS = 1_760_000_000_000
describe.skipIf(!BENCH_ENABLED)('remote clipboard cleanup benchmark', () => {
let fixtureRoot = ''
let freshTransfer = ''
beforeAll(async () => {
fixtureRoot = await mkdtemp(join(tmpdir(), FIXTURE_PREFIX))
await createForeignEntries(fixtureRoot, FOREIGN_ENTRY_COUNT)
const expiredTransfer = await createRemoteClipboardTransferDirectory(
fixtureRoot,
NOW_MS - 2 * 60 * 60 * 1000,
'00000000-0000-4000-8000-000000000000'
)
freshTransfer = await createRemoteClipboardTransferDirectory(
fixtureRoot,
NOW_MS,
'00000000-0000-4000-8000-000000000001'
)
const expiredAt = new Date(NOW_MS - 2 * 60 * 60 * 1000)
await utimes(expiredTransfer, expiredAt, expiredAt)
await cleanupExpiredRemoteClipboardStaging(fixtureRoot, NOW_MS)
await expect(access(expiredTransfer)).rejects.toMatchObject({ code: 'ENOENT' })
await expect(access(freshTransfer)).resolves.toBeUndefined()
expect(await countForeignEntries(fixtureRoot)).toBe(FOREIGN_ENTRY_COUNT)
}, 900_000)
afterAll(async () => {
if (
fixtureRoot &&
dirname(fixtureRoot) === tmpdir() &&
basename(fixtureRoot).startsWith(FIXTURE_PREFIX)
) {
await rm(fixtureRoot, {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 100
})
}
}, 900_000)
it('reports shared-root and owned-parent median and p95', async () => {
for (let index = 0; index < WARMUP_RUNS; index += 1) {
await scanSharedTempRoot(fixtureRoot)
await cleanupExpiredRemoteClipboardStaging(fixtureRoot, NOW_MS)
}
const sharedRootSamples: number[] = []
const ownedParentSamples: number[] = []
for (let index = 0; index < SAMPLE_RUNS; index += 1) {
sharedRootSamples.push(await measure(() => scanSharedTempRoot(fixtureRoot)))
ownedParentSamples.push(
await measure(() => cleanupExpiredRemoteClipboardStaging(fixtureRoot, NOW_MS))
)
}
expect(await countForeignEntries(fixtureRoot)).toBe(FOREIGN_ENTRY_COUNT)
await expect(access(freshTransfer)).resolves.toBeUndefined()
const sharedRoot = summarize(sharedRootSamples)
const ownedParent = summarize(ownedParentSamples)
console.log('correctness=pass')
console.log(`post12917-shared-root median=${sharedRoot.median}ms p95=${sharedRoot.p95}ms`)
console.log(`owned-parent median=${ownedParent.median}ms p95=${ownedParent.p95}ms`)
}, 900_000)
})
async function createForeignEntries(root: string, count: number): Promise<void> {
const batchSize = 128
for (let start = 0; start < count; start += batchSize) {
const tasks: Promise<unknown>[] = []
for (let index = start; index < Math.min(start + batchSize, count); index += 1) {
const target = join(root, `foreign-${String(index).padStart(8, '0')}`)
tasks.push(index % 2 === 0 ? writeFile(target, '') : mkdir(target))
}
await Promise.all(tasks)
}
}
async function countForeignEntries(root: string): Promise<number> {
return (await readdir(root)).filter((name) => name.startsWith('foreign-')).length
}
async function scanSharedTempRoot(root: string): Promise<void> {
const directory = await opendir(root)
try {
for await (const entry of directory) {
if (entry.isDirectory() && entry.name.startsWith('orca-clipboard-file-')) {
// Reproduces the remaining shared-root iteration after #12917.
}
}
} finally {
await directory.close().catch(() => undefined)
}
}
async function measure(operation: () => Promise<void>): Promise<number> {
const startedAt = process.hrtime.bigint()
await operation()
return Number(process.hrtime.bigint() - startedAt) / 1_000_000
}
function summarize(samples: number[]): { median: string; p95: string } {
const sorted = [...samples].sort((left, right) => left - right)
const median = sorted[Math.ceil(sorted.length * 0.5) - 1]
const p95 = sorted[Math.ceil(sorted.length * 0.95) - 1]
return { median: median.toFixed(3), p95: p95.toFixed(3) }
}
@@ -1,153 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { join } from 'node:path'
const { opendirMock, rmMock, statMock } = vi.hoisted(() => ({
opendirMock: vi.fn(),
rmMock: vi.fn(),
statMock: vi.fn()
}))
vi.mock('node:fs/promises', () => ({
mkdir: vi.fn(),
opendir: opendirMock,
rm: rmMock,
stat: statMock
}))
vi.mock('electron', () => ({ app: { getPath: () => '/tmp' } }))
vi.mock('../providers/ssh-filesystem-dispatch', () => ({
requireSshFilesystemProvider: vi.fn()
}))
vi.mock('./clipboard-file-copy', () => ({ writeFileToClipboard: vi.fn() }))
import { cleanupExpiredRemoteClipboardFiles } from './clipboard-remote-file-copy'
const TTL_MS = 60 * 60 * 1000
const NOW_MS = 1_760_000_000_000
function mockTempRoot(entries: Iterable<{ name: string; isDirectory: () => boolean }>): void {
opendirMock.mockResolvedValue({
async *[Symbol.asyncIterator]() {
yield* entries
},
close: vi.fn().mockResolvedValue(undefined)
})
}
function* orcaStagingDirs(count: number): Generator<{ name: string; isDirectory: () => boolean }> {
for (let index = 0; index < count; index += 1) {
yield { name: `orca-clipboard-file-expired-${index}`, isDirectory: () => true }
}
}
function* unrelatedTempEntries(
count: number
): Generator<{ name: string; isDirectory: () => boolean }> {
for (let index = 0; index < count; index += 1) {
// Mirrors a real temp root: mostly foreign entries, both files and directories.
yield { name: `unrelated-${index}`, isDirectory: () => index % 2 === 0 }
}
}
describe('cleanupExpiredRemoteClipboardFiles', () => {
beforeEach(() => {
vi.clearAllMocks()
rmMock.mockResolvedValue(undefined)
statMock.mockResolvedValue({ mtimeMs: NOW_MS - TTL_MS - 1 })
})
it('streams all entries with at most eight cleanups in flight', async () => {
mockTempRoot(orcaStagingDirs(257))
let active = 0
let peak = 0
statMock.mockImplementation(async () => {
active += 1
peak = Math.max(peak, active)
await new Promise<void>((resolve) => setImmediate(resolve))
active -= 1
return { mtimeMs: NOW_MS - TTL_MS - 1 }
})
await cleanupExpiredRemoteClipboardFiles(NOW_MS)
expect(rmMock).toHaveBeenCalledTimes(257)
expect(peak).toBe(8)
})
it('does no per-entry work for foreign temp-root entries', async () => {
mockTempRoot(unrelatedTempEntries(200_000))
await cleanupExpiredRemoteClipboardFiles(NOW_MS)
expect(statMock).not.toHaveBeenCalled()
expect(rmMock).not.toHaveBeenCalled()
})
it('acts on owned directories while the temp root is still being enumerated', async () => {
// Regression: the sweep materialized the whole temp root and built one promise
// per entry before doing any work, so a %TEMP% with ~1.2M unrelated entries
// froze the main process at startup (#12835). Streaming means an owned
// directory is swept before enumeration reaches the end.
let sweptDuringEnumeration = false
function* enumeration(): Generator<{ name: string; isDirectory: () => boolean }> {
yield { name: 'orca-clipboard-file-expired', isDirectory: () => true }
for (const entry of unrelatedTempEntries(1_000)) {
sweptDuringEnumeration ||= rmMock.mock.calls.length > 0
yield entry
}
}
mockTempRoot(enumeration())
await cleanupExpiredRemoteClipboardFiles(NOW_MS)
expect(sweptDuringEnumeration).toBe(true)
expect(rmMock).toHaveBeenCalledTimes(1)
})
it('removes only expired staging directories it owns', async () => {
function* interleaved(): Generator<{ name: string; isDirectory: () => boolean }> {
yield* unrelatedTempEntries(50_000)
yield { name: 'orca-clipboard-file-expired', isDirectory: () => true }
yield* unrelatedTempEntries(50_000)
yield { name: 'orca-clipboard-file-fresh', isDirectory: () => true }
yield { name: 'orca-clipboard-file-plain-file', isDirectory: () => false }
}
mockTempRoot(interleaved())
statMock.mockImplementation(async (targetPath: string) => ({
mtimeMs: targetPath.endsWith('expired') ? NOW_MS - TTL_MS - 1 : NOW_MS - 1000
}))
await cleanupExpiredRemoteClipboardFiles(NOW_MS)
expect(statMock).toHaveBeenCalledTimes(2)
expect(rmMock).toHaveBeenCalledTimes(1)
expect(rmMock).toHaveBeenCalledWith(join('/tmp', 'orca-clipboard-file-expired'), {
recursive: true,
force: true
})
})
it('closes the temp-root handle and still sweeps when enumeration fails midway', async () => {
const close = vi.fn().mockResolvedValue(undefined)
opendirMock.mockResolvedValue({
async *[Symbol.asyncIterator]() {
yield { name: 'orca-clipboard-file-expired', isDirectory: () => true }
throw new Error('EIO')
},
close
})
await expect(cleanupExpiredRemoteClipboardFiles(NOW_MS)).resolves.toBeUndefined()
expect(rmMock).toHaveBeenCalledTimes(1)
expect(close).toHaveBeenCalledTimes(1)
})
it('returns quietly when the temp root cannot be opened', async () => {
opendirMock.mockRejectedValue(new Error('EACCES'))
await expect(cleanupExpiredRemoteClipboardFiles(NOW_MS)).resolves.toBeUndefined()
expect(statMock).not.toHaveBeenCalled()
expect(rmMock).not.toHaveBeenCalled()
})
})
+43 -57
View File
@@ -1,6 +1,5 @@
import { randomUUID } from 'node:crypto'
import type { Dir } from 'node:fs'
import { mkdir, opendir, rm, stat } from 'node:fs/promises'
import { stat } from 'node:fs/promises'
import { join } from 'node:path'
import { app } from 'electron'
@@ -12,14 +11,21 @@ import {
type ClipboardFileDeps,
type ClipboardFileResult
} from './clipboard-file-copy'
import {
cleanupExpiredRemoteClipboardStaging,
cleanupLegacyRemoteClipboardStaging,
createRemoteClipboardTransferDirectory,
removeRemoteClipboardTransferDirectory
} from './clipboard-remote-file-staging'
type RemoteClipboardFileDeps = Omit<ClipboardFileDeps, 'resolveFilePath'>
const REMOTE_CLIPBOARD_FILE_TTL_MS = 60 * 60 * 1000
const REMOTE_CLIPBOARD_FILE_PREFIX = 'orca-clipboard-file-'
const REMOTE_CLIPBOARD_CLEANUP_CONCURRENCY = 8
const REMOTE_CLIPBOARD_CLEANUP_RETRY_MS = 60 * 1000
const REMOTE_CLIPBOARD_LEGACY_CLEANUP_DELAY_MS = 30 * 1000
const WINDOWS_RESERVED_LOCAL_BASENAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i
const LOCAL_FILENAME_REPLACEMENT_CHARS = new Set(['<', '>', ':', '"', '/', '\\', '|', '?', '*'])
let legacyCleanupScheduled = false
export async function writeRemoteFileToClipboard({
remotePath,
@@ -39,11 +45,8 @@ export async function writeRemoteFileToClipboard({
throw new Error('Remote file download is unavailable. Reconnect the SSH target and retry.')
}
const tempDir = join(
app.getPath('temp'),
`${REMOTE_CLIPBOARD_FILE_PREFIX}${Date.now()}-${randomUUID()}`
)
await mkdir(tempDir, { mode: 0o700 })
const tempRoot = app.getPath('temp')
const tempDir = await createRemoteClipboardTransferDirectory(tempRoot, Date.now(), randomUUID())
const localPath = join(
tempDir,
sanitizeLocalClipboardFilename(getRuntimePathBasename(remotePath))
@@ -70,62 +73,33 @@ export async function writeRemoteFileToClipboard({
// Why: OS file clipboards keep a path reference, so the staged copy must
// survive after this IPC call long enough for the user to paste it.
keepTempFile = true
scheduleRemoteClipboardFileCleanup(tempDir)
scheduleRemoteClipboardFileCleanup(tempRoot, tempDir)
}
return result
} finally {
if (!keepTempFile) {
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined)
await removeRemoteClipboardTransferDirectory(tempRoot, tempDir)
}
}
}
// Why: the OS temp root is shared with every other program and routinely holds
// millions of unrelated entries on long-lived machines, so this startup sweep
// streams it and only ever retains work for entries it actually owns.
export async function cleanupExpiredRemoteClipboardFiles(nowMs = Date.now()): Promise<void> {
const tempRoot = app.getPath('temp')
let tempRootDir: Dir
try {
tempRootDir = await opendir(tempRoot)
} catch {
await cleanupExpiredRemoteClipboardStaging(app.getPath('temp'), nowMs)
}
export async function cleanupLegacyRemoteClipboardFiles(nowMs = Date.now()): Promise<void> {
await cleanupLegacyRemoteClipboardStaging(app.getPath('temp'), nowMs)
}
export function scheduleLegacyRemoteClipboardFileCleanup(): void {
if (legacyCleanupScheduled) {
return
}
const pending = new Set<Promise<void>>()
try {
for await (const entry of tempRootDir) {
if (!entry.isDirectory() || !entry.name.startsWith(REMOTE_CLIPBOARD_FILE_PREFIX)) {
continue
}
const cleanup = cleanupExpiredRemoteClipboardDirectory(join(tempRoot, entry.name), nowMs)
pending.add(cleanup)
void cleanup.finally(() => pending.delete(cleanup))
if (pending.size >= REMOTE_CLIPBOARD_CLEANUP_CONCURRENCY) {
await Promise.race(pending)
}
}
} catch {
// Why: a partial best-effort sweep beats failing startup on a temp-root read error.
} finally {
// Why: exhausting the iterator already closes the handle; closing again is harmless.
await tempRootDir.close().catch(() => undefined)
}
await Promise.all(pending)
}
async function cleanupExpiredRemoteClipboardDirectory(
tempDir: string,
nowMs: number
): Promise<void> {
try {
const tempStats = await stat(tempDir)
if (nowMs - tempStats.mtimeMs >= REMOTE_CLIPBOARD_FILE_TTL_MS) {
await rm(tempDir, { recursive: true, force: true })
}
} catch {
// Why: stale staged SSH files should not make startup cleanup noisy.
}
legacyCleanupScheduled = true
const timer = setTimeout(() => {
void cleanupLegacyRemoteClipboardFiles()
}, REMOTE_CLIPBOARD_LEGACY_CLEANUP_DELAY_MS)
unrefTimer(timer)
}
function sanitizeLocalClipboardFilename(remoteBasename: string): string {
@@ -140,10 +114,22 @@ function sanitizeLocalClipboardFilename(remoteBasename: string): string {
return sanitized
}
function scheduleRemoteClipboardFileCleanup(tempDir: string): void {
function scheduleRemoteClipboardFileCleanup(
tempRoot: string,
tempDir: string,
delayMs = REMOTE_CLIPBOARD_FILE_TTL_MS
): void {
const timer = setTimeout(() => {
void rm(tempDir, { recursive: true, force: true }).catch(() => undefined)
}, REMOTE_CLIPBOARD_FILE_TTL_MS)
void removeRemoteClipboardTransferDirectory(tempRoot, tempDir).then((removed) => {
if (!removed) {
scheduleRemoteClipboardFileCleanup(tempRoot, tempDir, REMOTE_CLIPBOARD_CLEANUP_RETRY_MS)
}
})
}, delayMs)
unrefTimer(timer)
}
function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
if (typeof timer === 'object' && 'unref' in timer) {
timer.unref()
}
@@ -0,0 +1,337 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { join, resolve } from 'node:path'
const { accessMock, lstatMock, mkdirMock, opendirMock, rmMock, writeFileMock } = vi.hoisted(() => ({
accessMock: vi.fn(),
lstatMock: vi.fn(),
mkdirMock: vi.fn(),
opendirMock: vi.fn(),
rmMock: vi.fn(),
writeFileMock: vi.fn()
}))
vi.mock('node:fs/promises', () => ({
access: accessMock,
lstat: lstatMock,
mkdir: mkdirMock,
opendir: opendirMock,
rm: rmMock,
writeFile: writeFileMock
}))
import {
cleanupExpiredRemoteClipboardStaging,
cleanupLegacyRemoteClipboardStaging,
createRemoteClipboardTransferDirectory,
getRemoteClipboardStagingRoot,
removeRemoteClipboardTransferDirectory
} from './clipboard-remote-file-staging'
const TTL_MS = 60 * 60 * 1000
const NOW_MS = 1_760_000_000_000
const TEMP_ROOT = resolve('fixture-temp')
const UID_SUFFIX = typeof process.getuid === 'function' ? `-${process.getuid()}` : ''
const STAGING_ROOT = join(TEMP_ROOT, `orca-clipboard-files${UID_SUFFIX}`)
const MARKER_PATH = join(STAGING_ROOT, '.legacy-cleanup-complete')
const UUID_A = '00000000-0000-4000-8000-000000000000'
const UUID_B = '00000000-0000-4000-8000-000000000001'
const EXPIRED_TRANSFER = `1759990000000-${UUID_A}`
const FRESH_TRANSFER = `1760000000000-${UUID_B}`
const LEGACY_EXPIRED = `orca-clipboard-file-${EXPIRED_TRANSFER}`
const LEGACY_FRESH = `orca-clipboard-file-${FRESH_TRANSFER}`
type MockDirent = { name: string; isDirectory: () => boolean }
function directoryEntry(name: string, isDirectory = true): MockDirent {
return { name, isDirectory: () => isDirectory }
}
function openedDirectory(entries: Iterable<MockDirent>): {
[Symbol.asyncIterator]: () => AsyncGenerator<MockDirent>
close: ReturnType<typeof vi.fn>
} {
return {
async *[Symbol.asyncIterator]() {
yield* entries
},
close: vi.fn().mockResolvedValue(undefined)
}
}
function safeDirectoryStats(mtimeMs = NOW_MS - TTL_MS - 1) {
return {
isDirectory: () => true,
isSymbolicLink: () => false,
mode: 0o700,
mtimeMs,
uid: typeof process.getuid === 'function' ? process.getuid() : 0
}
}
function missingError(): NodeJS.ErrnoException {
return Object.assign(new Error('missing'), { code: 'ENOENT' })
}
describe('remote clipboard staging ownership', () => {
beforeEach(() => {
vi.clearAllMocks()
accessMock.mockRejectedValue(missingError())
lstatMock.mockResolvedValue(safeDirectoryStats())
mkdirMock.mockResolvedValue(undefined)
opendirMock.mockResolvedValue(openedDirectory([]))
rmMock.mockResolvedValue(undefined)
writeFileMock.mockResolvedValue(undefined)
})
it('creates a private owned parent and a per-transfer child', async () => {
const result = await createRemoteClipboardTransferDirectory(TEMP_ROOT, NOW_MS, UUID_A)
expect(result).toBe(join(STAGING_ROOT, `${NOW_MS}-${UUID_A}`))
expect(mkdirMock).toHaveBeenNthCalledWith(1, STAGING_ROOT, {
recursive: true,
mode: 0o700
})
expect(mkdirMock).toHaveBeenNthCalledWith(2, result, { mode: 0o700 })
})
it('scans only the owned parent during normal crash cleanup', async () => {
await cleanupExpiredRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(opendirMock).toHaveBeenCalledOnce()
expect(opendirMock).toHaveBeenCalledWith(STAGING_ROOT)
expect(opendirMock).not.toHaveBeenCalledWith(TEMP_ROOT)
})
it('removes expired crash leftovers and preserves fresh or unknown children', async () => {
opendirMock.mockResolvedValue(
openedDirectory([
directoryEntry(EXPIRED_TRANSFER),
directoryEntry(FRESH_TRANSFER),
directoryEntry('unknown-child'),
directoryEntry('.legacy-cleanup-complete', false)
])
)
lstatMock.mockImplementation(async (targetPath: string) =>
safeDirectoryStats(targetPath.endsWith(FRESH_TRANSFER) ? NOW_MS - 1_000 : undefined)
)
await cleanupExpiredRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(rmMock).toHaveBeenCalledOnce()
expect(rmMock).toHaveBeenCalledWith(join(STAGING_ROOT, EXPIRED_TRANSFER), {
force: true,
maxRetries: 3,
recursive: true,
retryDelay: 100
})
})
it('rejects a symlinked owned parent before opening it', async () => {
lstatMock.mockResolvedValue({
...safeDirectoryStats(),
isDirectory: () => false,
isSymbolicLink: () => true
})
await expect(cleanupExpiredRemoteClipboardStaging(TEMP_ROOT, NOW_MS)).resolves.toBeUndefined()
expect(opendirMock).not.toHaveBeenCalled()
expect(rmMock).not.toHaveBeenCalled()
})
it('rejects wrong-owner and insecure POSIX parents', async () => {
if (typeof process.getuid !== 'function') {
return
}
lstatMock
.mockResolvedValueOnce({ ...safeDirectoryStats(), uid: process.getuid() + 1 })
.mockResolvedValueOnce({ ...safeDirectoryStats(), mode: 0o755 })
await cleanupExpiredRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
await cleanupExpiredRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(opendirMock).not.toHaveBeenCalled()
})
it('rejects child symlinks and names outside the owned parent', async () => {
opendirMock.mockResolvedValue(
openedDirectory([directoryEntry(EXPIRED_TRANSFER), directoryEntry('..')])
)
lstatMock.mockImplementation(async (targetPath: string) =>
targetPath === STAGING_ROOT
? safeDirectoryStats()
: {
...safeDirectoryStats(),
isDirectory: () => false,
isSymbolicLink: () => true
}
)
await cleanupExpiredRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(lstatMock).not.toHaveBeenCalledWith(TEMP_ROOT)
expect(rmMock).not.toHaveBeenCalled()
})
it('contains explicit removals to direct owned children', async () => {
expect(
await removeRemoteClipboardTransferDirectory(TEMP_ROOT, join(STAGING_ROOT, '..', 'victim'))
).toBe(false)
expect(await removeRemoteClipboardTransferDirectory(TEMP_ROOT, STAGING_ROOT)).toBe(false)
expect(rmMock).not.toHaveBeenCalled()
})
it('treats an already-removed transfer as cleanup success', async () => {
lstatMock.mockImplementation(async (targetPath: string) => {
if (targetPath === STAGING_ROOT) {
return safeDirectoryStats()
}
throw missingError()
})
await expect(
removeRemoteClipboardTransferDirectory(TEMP_ROOT, join(STAGING_ROOT, EXPIRED_TRANSFER))
).resolves.toBe(true)
expect(rmMock).not.toHaveBeenCalled()
})
it('continues past Windows-style lock failures and retries on a later sweep', async () => {
opendirMock.mockResolvedValue(
openedDirectory([directoryEntry(EXPIRED_TRANSFER), directoryEntry(`1759990000000-${UUID_B}`)])
)
rmMock.mockImplementation(async (targetPath: string) => {
if (targetPath.endsWith(EXPIRED_TRANSFER)) {
throw Object.assign(new Error('locked'), { code: 'EPERM' })
}
})
await expect(cleanupExpiredRemoteClipboardStaging(TEMP_ROOT, NOW_MS)).resolves.toBeUndefined()
expect(rmMock).toHaveBeenCalledTimes(2)
rmMock.mockResolvedValue(undefined)
await cleanupExpiredRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(rmMock).toHaveBeenCalledTimes(4)
})
it('keeps owned-child cleanup concurrency at eight', async () => {
const entries = Array.from({ length: 257 }, (_, index) =>
directoryEntry(`1759990000000-00000000-0000-4000-8000-${String(index).padStart(12, '0')}`)
)
opendirMock.mockResolvedValue(openedDirectory(entries))
let active = 0
let peak = 0
lstatMock.mockImplementation(async () => {
active += 1
peak = Math.max(peak, active)
await new Promise<void>((resolveImmediate) => setImmediate(resolveImmediate))
active -= 1
return safeDirectoryStats()
})
await cleanupExpiredRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(rmMock).toHaveBeenCalledTimes(257)
expect(peak).toBe(8)
})
})
describe('legacy remote clipboard staging compatibility', () => {
beforeEach(() => {
vi.clearAllMocks()
accessMock.mockRejectedValue(missingError())
lstatMock.mockResolvedValue(safeDirectoryStats())
mkdirMock.mockResolvedValue(undefined)
opendirMock.mockResolvedValue(openedDirectory([]))
rmMock.mockResolvedValue(undefined)
writeFileMock.mockResolvedValue(undefined)
})
it('caps a large foreign-root scan at 4096 total entries', async () => {
let visited = 0
function* foreignEntries(): Generator<MockDirent> {
for (let index = 0; index < 200_000; index += 1) {
visited += 1
yield directoryEntry(`foreign-${index}`, index % 2 === 0)
}
}
opendirMock.mockResolvedValue(openedDirectory(foreignEntries()))
await cleanupLegacyRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(visited).toBe(4_096)
expect(lstatMock).toHaveBeenCalledOnce()
expect(rmMock).not.toHaveBeenCalled()
expect(writeFileMock).toHaveBeenCalledWith(MARKER_PATH, '', {
flag: 'wx',
mode: 0o600
})
})
it('removes only exact expired legacy children encountered inside the bound', async () => {
opendirMock.mockResolvedValue(
openedDirectory([
directoryEntry('orca-clipboard-file-lookalike'),
directoryEntry(LEGACY_EXPIRED),
directoryEntry(LEGACY_EXPIRED, false)
])
)
await cleanupLegacyRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(rmMock).toHaveBeenCalledOnce()
expect(rmMock).toHaveBeenCalledWith(join(TEMP_ROOT, LEGACY_EXPIRED), {
force: true,
maxRetries: 3,
recursive: true,
retryDelay: 100
})
expect(writeFileMock).toHaveBeenCalledOnce()
})
it('does not mark migration complete while a fresh or locked legacy child remains', async () => {
opendirMock.mockResolvedValue(
openedDirectory([directoryEntry(LEGACY_FRESH), directoryEntry(LEGACY_EXPIRED)])
)
lstatMock.mockImplementation(async (targetPath: string) =>
safeDirectoryStats(targetPath.endsWith(LEGACY_FRESH) ? NOW_MS - 1_000 : undefined)
)
rmMock.mockRejectedValue(Object.assign(new Error('locked'), { code: 'EBUSY' }))
await cleanupLegacyRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(writeFileMock).not.toHaveBeenCalled()
})
it('never follows legacy symlinks outside the shared temp root', async () => {
opendirMock.mockResolvedValue(openedDirectory([directoryEntry(LEGACY_EXPIRED)]))
lstatMock.mockImplementation(async (targetPath: string) =>
targetPath === STAGING_ROOT
? safeDirectoryStats()
: {
...safeDirectoryStats(),
isDirectory: () => false,
isSymbolicLink: () => true
}
)
await cleanupLegacyRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(rmMock).not.toHaveBeenCalled()
expect(writeFileMock).toHaveBeenCalledOnce()
})
it('skips the shared root after the one-time marker exists', async () => {
accessMock.mockResolvedValue(undefined)
await cleanupLegacyRemoteClipboardStaging(TEMP_ROOT, NOW_MS)
expect(accessMock).toHaveBeenCalledWith(MARKER_PATH)
expect(opendirMock).not.toHaveBeenCalled()
})
it('uses a platform-safe owned parent name', () => {
expect(getRemoteClipboardStagingRoot(TEMP_ROOT)).toBe(STAGING_ROOT)
})
})
@@ -0,0 +1,222 @@
import type { Dir, Stats } from 'node:fs'
import { access, lstat, mkdir, opendir, rm, writeFile } from 'node:fs/promises'
import { basename, dirname, join, resolve } from 'node:path'
const REMOTE_CLIPBOARD_STAGING_ROOT_NAME = 'orca-clipboard-files'
const REMOTE_CLIPBOARD_LEGACY_PREFIX = 'orca-clipboard-file-'
const REMOTE_CLIPBOARD_MIGRATION_MARKER = '.legacy-cleanup-complete'
const REMOTE_CLIPBOARD_FILE_TTL_MS = 60 * 60 * 1000
const REMOTE_CLIPBOARD_CLEANUP_CONCURRENCY = 8
// Why: compatibility cleanup must never restore O(shared temp root) work.
const REMOTE_CLIPBOARD_LEGACY_ENTRY_LIMIT = 4_096
const UUID_PATTERN = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
const TRANSFER_DIRECTORY_PATTERN = new RegExp(`^\\d{1,16}-${UUID_PATTERN}$`, 'i')
const LEGACY_DIRECTORY_PATTERN = new RegExp(
`^${REMOTE_CLIPBOARD_LEGACY_PREFIX}\\d{1,16}-${UUID_PATTERN}$`,
'i'
)
const REMOVE_OPTIONS = {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 100
} as const
type CleanupResult = 'failed' | 'fresh' | 'ignored' | 'removed'
export function getRemoteClipboardStagingRoot(tempRoot: string): string {
const uidSuffix = typeof process.getuid === 'function' ? `-${process.getuid()}` : ''
return join(tempRoot, `${REMOTE_CLIPBOARD_STAGING_ROOT_NAME}${uidSuffix}`)
}
export async function createRemoteClipboardTransferDirectory(
tempRoot: string,
createdAtMs: number,
transferId: string
): Promise<string> {
const stagingRoot = await ensureRemoteClipboardStagingRoot(tempRoot)
const transferDirectory = join(stagingRoot, `${createdAtMs}-${transferId}`)
if (
!isTransferDirectoryName(basename(transferDirectory)) ||
!isDirectChild(stagingRoot, transferDirectory)
) {
throw new Error('Remote clipboard transfer path escapes its staging root')
}
await mkdir(transferDirectory, { mode: 0o700 })
const transferStats = await lstat(transferDirectory)
if (!isSafeOwnedDirectory(transferStats)) {
throw new Error('Remote clipboard transfer directory is unsafe')
}
return transferDirectory
}
export async function cleanupExpiredRemoteClipboardStaging(
tempRoot: string,
nowMs = Date.now()
): Promise<void> {
let stagingRoot: string
try {
stagingRoot = await ensureRemoteClipboardStagingRoot(tempRoot)
} catch {
return
}
await sweepDirectories(stagingRoot, nowMs, isTransferDirectoryName)
}
export async function cleanupLegacyRemoteClipboardStaging(
tempRoot: string,
nowMs = Date.now()
): Promise<void> {
let stagingRoot: string
try {
stagingRoot = await ensureRemoteClipboardStagingRoot(tempRoot)
} catch {
return
}
const markerPath = join(stagingRoot, REMOTE_CLIPBOARD_MIGRATION_MARKER)
try {
await access(markerPath)
return
} catch (error) {
if (!isMissingPathError(error)) {
return
}
}
const result = await sweepDirectories(
tempRoot,
nowMs,
isLegacyTransferDirectoryName,
REMOTE_CLIPBOARD_LEGACY_ENTRY_LIMIT
)
if (result.complete && !result.hasFreshDirectories && !result.hasFailures) {
await writeFile(markerPath, '', { flag: 'wx', mode: 0o600 }).catch(() => undefined)
}
}
export async function removeRemoteClipboardTransferDirectory(
tempRoot: string,
transferDirectory: string
): Promise<boolean> {
const stagingRoot = getRemoteClipboardStagingRoot(tempRoot)
if (
!isDirectChild(stagingRoot, transferDirectory) ||
!isTransferDirectoryName(basename(resolve(transferDirectory)))
) {
return false
}
try {
await ensureRemoteClipboardStagingRoot(tempRoot)
const transferStats = await lstat(transferDirectory)
if (!isSafeOwnedDirectory(transferStats)) {
return false
}
await rm(transferDirectory, REMOVE_OPTIONS)
return true
} catch (error) {
return isMissingPathError(error)
}
}
async function ensureRemoteClipboardStagingRoot(tempRoot: string): Promise<string> {
const stagingRoot = getRemoteClipboardStagingRoot(tempRoot)
await mkdir(stagingRoot, { recursive: true, mode: 0o700 })
const rootStats = await lstat(stagingRoot)
if (!isSafeOwnedDirectory(rootStats)) {
throw new Error('Remote clipboard staging root is unsafe')
}
return stagingRoot
}
async function sweepDirectories(
root: string,
nowMs: number,
ownsEntry: (name: string) => boolean,
entryLimit = Number.POSITIVE_INFINITY
): Promise<{ complete: boolean; hasFailures: boolean; hasFreshDirectories: boolean }> {
let rootDir: Dir
try {
rootDir = await opendir(root)
} catch {
return { complete: false, hasFailures: false, hasFreshDirectories: false }
}
let complete = true
let entriesVisited = 0
let hasFailures = false
let hasFreshDirectories = false
const pending = new Set<Promise<void>>()
try {
for await (const entry of rootDir) {
entriesVisited += 1
if (entry.isDirectory() && ownsEntry(entry.name)) {
const candidate = join(root, entry.name)
if (isDirectChild(root, candidate)) {
const cleanup = cleanupDirectory(candidate, nowMs).then((result) => {
hasFailures ||= result === 'failed'
hasFreshDirectories ||= result === 'fresh'
})
pending.add(cleanup)
void cleanup.finally(() => pending.delete(cleanup))
if (pending.size >= REMOTE_CLIPBOARD_CLEANUP_CONCURRENCY) {
await Promise.race(pending)
}
}
}
if (entriesVisited >= entryLimit) {
break
}
}
} catch {
complete = false
} finally {
await rootDir.close().catch(() => undefined)
}
await Promise.all(pending)
return { complete, hasFailures, hasFreshDirectories }
}
async function cleanupDirectory(directory: string, nowMs: number): Promise<CleanupResult> {
try {
const directoryStats = await lstat(directory)
if (!isSafeOwnedDirectory(directoryStats)) {
return 'ignored'
}
if (nowMs - directoryStats.mtimeMs < REMOTE_CLIPBOARD_FILE_TTL_MS) {
return 'fresh'
}
await rm(directory, REMOVE_OPTIONS)
return 'removed'
} catch (error) {
return isMissingPathError(error) ? 'ignored' : 'failed'
}
}
function isSafeOwnedDirectory(stats: Stats): boolean {
if (!stats.isDirectory() || stats.isSymbolicLink()) {
return false
}
if (typeof process.getuid !== 'function') {
return true
}
return stats.uid === process.getuid() && (stats.mode & 0o777) === 0o700
}
function isDirectChild(parent: string, candidate: string): boolean {
return dirname(resolve(candidate)) === resolve(parent)
}
function isTransferDirectoryName(name: string): boolean {
return TRANSFER_DIRECTORY_PATTERN.test(name)
}
function isLegacyTransferDirectoryName(name: string): boolean {
return LEGACY_DIRECTORY_PATTERN.test(name)
}
function isMissingPathError(error: unknown): boolean {
return (
error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT'
)
}