Path: blob/main/src/vs/platform/agentHost/node/sessionDataService.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IReference, ReferenceCollection } from '../../../base/common/lifecycle.js';6import { URI } from '../../../base/common/uri.js';7import { IFileService } from '../../files/common/files.js';8import { ILogService } from '../../log/common/log.js';9import { AgentSession } from '../common/agentService.js';10import { ISessionDatabase, ISessionDataService, SESSION_DB_FILENAME } from '../common/sessionDataService.js';11import { SessionDatabase } from './sessionDatabase.js';1213class SessionDatabaseCollection extends ReferenceCollection<ISessionDatabase> {1415/**16* The set of currently-open databases. Mirrors what's held by the17* underlying ref-counted map, but exposed so {@link SessionDataService.whenIdle}18* can iterate without reaching into private state.19*/20readonly liveDatabases = new Set<ISessionDatabase>();2122constructor(23private readonly _getDbPath: (key: string) => string,24private readonly _logService: ILogService,25) {26super();27}2829protected createReferencedObject(key: string): ISessionDatabase {30const dbPath = this._getDbPath(key);31this._logService.trace(`[SessionDataService] Opening database: ${dbPath}`);32const db = new SessionDatabase(dbPath);33this.liveDatabases.add(db);34return db;35}3637protected destroyReferencedObject(_key: string, object: ISessionDatabase): void {38this.liveDatabases.delete(object);39object.dispose();40}41}4243/**44* Implementation of {@link ISessionDataService} that stores per-session data45* under `{userDataPath}/agentSessionData/{sessionId}/`.46*/47export class SessionDataService implements ISessionDataService {48declare readonly _serviceBrand: undefined;4950private readonly _basePath: URI;51private readonly _databases: SessionDatabaseCollection;5253constructor(54userDataPath: URI,55@IFileService private readonly _fileService: IFileService,56@ILogService private readonly _logService: ILogService,57getDbPath?: (key: string) => string, // for testing58) {59this._basePath = URI.joinPath(userDataPath, 'agentSessionData');60this._databases = new SessionDatabaseCollection(61getDbPath ?? (key => URI.joinPath(this._basePath, key, SESSION_DB_FILENAME).fsPath),62this._logService,63);64}6566getSessionDataDir(session: URI): URI {67return this.getSessionDataDirById(AgentSession.id(session));68}6970getSessionDataDirById(sessionId: string): URI {71const sanitized = sessionId.replace(/[^a-zA-Z0-9_.-]/g, '-');72return URI.joinPath(this._basePath, sanitized);73}7475private _sanitizedSessionKey(session: URI): string {76return AgentSession.id(session).replace(/[^a-zA-Z0-9_.-]/g, '-');77}7879openDatabase(session: URI): IReference<ISessionDatabase> {80return this._databases.acquire(this._sanitizedSessionKey(session));81}8283async tryOpenDatabase(session: URI): Promise<IReference<ISessionDatabase> | undefined> {84const key = this._sanitizedSessionKey(session);85const dbPath = URI.joinPath(this._basePath, key, SESSION_DB_FILENAME);86if (!await this._fileService.exists(dbPath)) {87return undefined;88}89return this._databases.acquire(key);90}9192async deleteSessionData(session: URI): Promise<void> {93const dir = this.getSessionDataDir(session);94try {95if (await this._fileService.exists(dir)) {96await this._fileService.del(dir, { recursive: true });97this._logService.trace(`[SessionDataService] Deleted session data: ${dir.toString()}`);98}99} catch (err) {100this._logService.warn(`[SessionDataService] Failed to delete session data: ${dir.toString()}`, err);101}102}103104async cleanupOrphanedData(knownSessionIds: Set<string>): Promise<void> {105try {106const exists = await this._fileService.exists(this._basePath);107if (!exists) {108return;109}110111const stat = await this._fileService.resolve(this._basePath);112if (!stat.children) {113return;114}115116const deletions: Promise<void>[] = [];117for (const child of stat.children) {118if (!child.isDirectory) {119continue;120}121const name = child.name;122if (!knownSessionIds.has(name)) {123this._logService.trace(`[SessionDataService] Cleaning up orphaned session data: ${name}`);124deletions.push(125this._fileService.del(child.resource, { recursive: true }).catch(err => {126this._logService.warn(`[SessionDataService] Failed to clean up orphaned data: ${name}`, err);127})128);129}130}131132await Promise.all(deletions);133} catch (err) {134this._logService.warn('[SessionDataService] Failed to run orphan cleanup', err);135}136}137138async whenIdle(): Promise<void> {139// Each `SessionDatabase.whenIdle()` already loops internally until140// that DB is quiescent, so the outer loop only needs to handle the141// case where a new DB was opened (and writes queued against it)142// while we were awaiting an earlier pass.143while (true) {144const dbs = [...this._databases.liveDatabases];145if (dbs.length === 0) {146return;147}148await Promise.all(dbs.map(db => db.whenIdle()));149const newOnes = [...this._databases.liveDatabases].filter(db => !dbs.includes(db));150if (newOnes.length === 0) {151return;152}153}154}155}156157158