diff --git a/CHANGELOG.md b/CHANGELOG.md index da01774..a1103b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to the "python-ply-preview" extension will be documented in Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +### Unreleased + + * Preview files left behind when VS Code was closed during a debug session are now removed after a day. + ### 0.1.0 * The PLY file is now written only when you click **View PLY**, not every time the cursor moves. This makes the debugger much more responsive with large point clouds. diff --git a/src/ViewPLYService.ts b/src/ViewPLYService.ts index ef64361..db5233a 100644 --- a/src/ViewPLYService.ts +++ b/src/ViewPLYService.ts @@ -17,6 +17,40 @@ and o.ndim in (2, 3) and (o.ndim == 2 or o.shape[0] == 1) and o.shape[-1] * o.sh and (o.shape[-1] in (3, 4, 6) or o.shape[-2] in (3, 4, 6))))\ (type(o).__module__ or '', type(o).__name__))((${expression}))`; +// --- Stale session cleanup --- + +/** Session folders untouched for this long are considered abandoned. */ +const STALE_SESSION_AGE_MS = 24 * 60 * 60 * 1000; + +/** + * Removes session folders in `baseDir` that were last modified more than `maxAgeMs` ago. + * Folders are normally removed when their debug session terminates, but they are left behind when + * VS Code exits while a session is still running. Returns the removed folders. + */ +export async function removeStaleSessionDirs(baseDir: string, maxAgeMs: number, now = Date.now()): Promise { + let entries; + try { + entries = await fs.readdir(baseDir, { withFileTypes: true }); + } catch { + // Nothing has been previewed in this workspace yet + return []; + } + + const removed: string[] = []; + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + const dir = join(baseDir, entry.name); + const { mtimeMs } = await fs.stat(dir); + if (now - mtimeMs > maxAgeMs) { + await fs.rm(dir, { recursive: true, force: true }); + removed.push(dir); + } + } + return removed; +} + // --- Service Class --- export default class ViewPLYService { @@ -38,6 +72,10 @@ export default class ViewPLYService { this.logInfo(`Service initialized. Storage directory: ${this.baseStorageDir}`); vscode.debug.onDidTerminateDebugSession(this.cleanupSessionFiles.bind(this)); + + removeStaleSessionDirs(this.baseStorageDir, STALE_SESSION_AGE_MS) + .then(removed => removed.forEach(dir => this.logInfo(`Removed stale session directory: ${dir}`))) + .catch(error => this.logError('Error while removing stale session directories.', error)); } /** diff --git a/src/test/cleanup.test.ts b/src/test/cleanup.test.ts new file mode 100644 index 0000000..10113b0 --- /dev/null +++ b/src/test/cleanup.test.ts @@ -0,0 +1,38 @@ +import * as assert from 'assert'; +import { existsSync, mkdirSync, mkdtempSync, utimesSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { removeStaleSessionDirs } from '../ViewPLYService'; + +const DAY_MS = 24 * 60 * 60 * 1000; + +function makeSessionDir(baseDir: string, name: string, ageMs: number): string { + const dir = join(baseDir, name); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, 'cloud.ply'), 'ply\n'); + const time = new Date(Date.now() - ageMs); + utimesSync(dir, time, time); + return dir; +} + +suite('Stale session cleanup', () => { + test('removes only session folders older than the max age', async () => { + const baseDir = mkdtempSync(join(tmpdir(), 'viewply-')); + const stale = makeSessionDir(baseDir, 'stale-session', 2 * DAY_MS); + const fresh = makeSessionDir(baseDir, 'fresh-session', 60 * 1000); + writeFileSync(join(baseDir, 'not-a-session.txt'), ''); + + const removed = await removeStaleSessionDirs(baseDir, DAY_MS); + + assert.deepStrictEqual(removed, [stale]); + assert.ok(!existsSync(stale)); + assert.ok(existsSync(join(fresh, 'cloud.ply'))); + assert.ok(existsSync(join(baseDir, 'not-a-session.txt'))); + }); + + test('does nothing when the preview folder does not exist', async () => { + const removed = await removeStaleSessionDirs(join(tmpdir(), 'viewply-does-not-exist'), DAY_MS); + + assert.deepStrictEqual(removed, []); + }); +});