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