Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1949707
shared application storage
sandy081 Apr 17, 2026
dc929e7
add tests
sandy081 Apr 17, 2026
2383633
Merge branch 'main' into sandy081/main-scorpion
sandy081 Apr 19, 2026
049e0ed
fix tests
sandy081 Apr 20, 2026
117c46c
add logging and address feedback
sandy081 Apr 20, 2026
efa4eab
Add application shared storage scope
sandy081 Apr 20, 2026
a09dae1
Add fallback migration for APPLICATION_SHARED storage
sandy081 Apr 20, 2026
4c02e81
Move storage fallback into Storage class with auto-migration
sandy081 Apr 20, 2026
d3145c7
Make fallbackStorage an implementation detail of Storage
sandy081 Apr 20, 2026
6dff5f3
Pass fallback storage via ApplicationSharedStorageMain constructor
sandy081 Apr 20, 2026
a671ca7
Remove fallbackDatabasePath - use fallbackStorage only
sandy081 Apr 20, 2026
11b42ee
Add MigratingStorage with persisted migration tracking
sandy081 Apr 21, 2026
c9eee0a
Merge branch 'main' into sandy081/main-scorpion
sandy081 Apr 21, 2026
497ff3d
fix compilation
sandy081 Apr 21, 2026
47972dc
minimise changes
sandy081 Apr 21, 2026
e9b7d5a
some fixes
sandy081 Apr 21, 2026
c4d4be3
fixes
sandy081 Apr 21, 2026
6ee1aed
fix
sandy081 Apr 21, 2026
21c358b
delete migrated key
sandy081 Apr 21, 2026
9841a76
fix removing migrated key
sandy081 Apr 21, 2026
0c61bfb
update distro
sandy081 Apr 21, 2026
799f168
feedback
sandy081 Apr 21, 2026
7d8524a
Fix MigratingStorage: persist marker only on actual migration
sandy081 Apr 21, 2026
45a2ad5
feedback
sandy081 Apr 21, 2026
5c74779
fix tests
sandy081 Apr 21, 2026
9aa6815
Merge branch 'main' into sandy081/main-scorpion
sandy081 Apr 21, 2026
52323b0
fix application storage path
sandy081 Apr 21, 2026
e039cc4
fix compilation
sandy081 Apr 21, 2026
bf6640f
Merge branch 'main' into sandy081/main-scorpion
sandy081 Apr 22, 2026
ef0f2c9
Merge branch 'main' into sandy081/main-scorpion
sandy081 Apr 22, 2026
272e324
Add info logging for fallback storage initialization
sandy081 Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions product.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"nameLong": "Code - OSS",
"applicationName": "code-oss",
"dataFolderName": ".vscode-oss",
"sharedDataFolderName": ".vscode-oss-shared",
"win32MutexName": "vscodeoss",
"licenseName": "MIT",
"licenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt",
Expand Down
1 change: 1 addition & 0 deletions src/vs/base/common/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export interface IProductConfiguration {

readonly urlProtocol: string;
readonly dataFolderName: string; // location for extensions (e.g. ~/.vscode-insiders)
readonly sharedDataFolderName: string; // location for shared data (e.g. ~/.vscode-insiders-shared)

readonly builtInExtensions?: IBuiltInExtension[];
readonly walkthroughMetadata?: IProductWalkthrough[];
Expand Down
65 changes: 64 additions & 1 deletion src/vs/base/parts/storage/common/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ThrottledDelayer } from '../../../common/async.js';
import { Event, PauseableEmitter } from '../../../common/event.js';
import { Disposable, IDisposable } from '../../../common/lifecycle.js';
import { parse, stringify } from '../../../common/marshalling.js';
import { isObject, isUndefinedOrNull } from '../../../common/types.js';
import { isObject, isUndefined, isUndefinedOrNull } from '../../../common/types.js';

export enum StorageHint {

Expand Down Expand Up @@ -434,3 +434,66 @@ export class InMemoryStorageDatabase implements IStorageDatabase {
async optimize(): Promise<void> { }
async close(): Promise<void> { }
}


export const MIGRATED_KEY = '__$__migratedStorageMarker';

export class MigratingStorage extends Storage {

private migratedKeys: Set<string> = new Set();
private fallbackStorage: IStorage | undefined = undefined;
private isFallbackStorageReadonly: boolean = false;

override async init(): Promise<void> {
await super.init();

// Load the set of keys already migrated from fallback
this.migratedKeys = this.loadMigratedKeys();
}

public setFallbackStorage(storage: IStorage, isReadonly: boolean): void {
this.fallbackStorage = storage;
this.isFallbackStorageReadonly = isReadonly;
}

private static readonly INTERNAL_KEY_PREFIX = '__$__';

override get(key: string, fallbackValue: string): string;
override get(key: string, fallbackValue?: string): string | undefined;
override get(key: string, fallbackValue?: string): string | undefined {
if (!key.startsWith(MigratingStorage.INTERNAL_KEY_PREFIX) && !this.migratedKeys.has(key) && isUndefined(super.get(key))) {
// Check fallback storage and auto-migrate on hit.
// Mark the key as migrated immediately to prevent
// re-checking the fallback, and to ensure a key
// that was intentionally removed after migration
// is not resurrected from the fallback.
this.migratedKeys.add(key);
const value = this.fallbackStorage?.items.get(key);
if (!isUndefined(value)) {
this.set(key, value);
if (!this.isFallbackStorageReadonly) {
this.fallbackStorage?.delete(key);
}
this.persistMigratedKeys();
}
}
return super.get(key, fallbackValue);
}

private loadMigratedKeys(): Set<string> {
const raw = super.get(MIGRATED_KEY);
if (raw) {
try {
return new Set(JSON.parse(raw));
} catch {
// Fail gracefully
}
}
return new Set();
}

private persistMigratedKeys(): void {
this.set(MIGRATED_KEY, JSON.stringify([...this.migratedKeys]));
}
}

25 changes: 22 additions & 3 deletions src/vs/base/parts/storage/node/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ interface IDatabaseConnection {

export interface ISQLiteStorageDatabaseOptions {
readonly logging?: ISQLiteStorageDatabaseLoggingOptions;
readonly useWAL?: boolean;

/**
* If set, configures SQLite's busy timeout in milliseconds.
* When another process holds a write lock, SQLite will retry
* for this duration before returning SQLITE_BUSY.
*/
readonly busyTimeout?: number;
}

export interface ISQLiteStorageDatabaseLoggingOptions {
Expand All @@ -41,6 +49,8 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
private readonly name: string;

private readonly logger: SQLiteStorageDatabaseLogger;
private readonly useWAL: boolean;
private readonly busyTimeout: number | undefined;

private readonly whenConnected: Promise<IDatabaseConnection>;

Expand All @@ -50,6 +60,8 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
) {
this.name = basename(this.path);
this.logger = new SQLiteStorageDatabaseLogger(options.logging);
this.useWAL = !!options.useWAL;
this.busyTimeout = options.busyTimeout;
this.whenConnected = this.connect(this.path);
}

Expand Down Expand Up @@ -326,10 +338,17 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
// The following exec() statement serves two purposes:
// - create the DB if it does not exist yet
// - validate that the DB is not corrupt (the open() call does not throw otherwise)
return this.exec(connection, [
const pragmas: string[] = [
'PRAGMA user_version = 1;',
'CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)'
].join('')).then(() => {
'CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB);'
];
if (this.useWAL) {
pragmas.push('PRAGMA journal_mode=WAL;');
}
if (this.busyTimeout) {
pragmas.push(`PRAGMA busy_timeout=${this.busyTimeout};`);
}
return this.exec(connection, pragmas.join('')).then(() => {
return resolve(connection);
}, error => {
return connection.db.close(() => reject(error));
Expand Down
4 changes: 3 additions & 1 deletion src/vs/code/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,13 @@ export async function main(argv: string[]): Promise<void> {
const tempParentDir = randomPath(tmpdir(), 'vscode');
const tempUserDataDir = join(tempParentDir, 'data');
const tempExtensionsDir = join(tempParentDir, 'extensions');
const tempSharedDataDir = join(tempParentDir, 'shared');

addArg(argv, '--user-data-dir', tempUserDataDir);
addArg(argv, '--extensions-dir', tempExtensionsDir);
addArg(argv, '--shared-data-dir', tempSharedDataDir);
Comment thread
sandy081 marked this conversation as resolved.

console.log(`State is temporarily stored. Relaunch this state with: ${product.applicationName} --user-data-dir "${tempUserDataDir}" --extensions-dir "${tempExtensionsDir}"`);
console.log(`State is temporarily stored. Relaunch this state with: ${product.applicationName} --user-data-dir "${tempUserDataDir}" --extensions-dir "${tempExtensionsDir}" --shared-data-dir "${tempSharedDataDir}"`);
}

const hasReadStdinArg = args._.some(arg => arg === '-') || args.chat?._.some(arg => arg === '-');
Expand Down
1 change: 1 addition & 0 deletions src/vs/editor/standalone/browser/standaloneServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ class StandaloneEnvironmentService implements IEnvironmentService {
readonly argvResource: URI = URI.from({ scheme: 'monaco', authority: 'argvResource' });
readonly untitledWorkspacesHome: URI = URI.from({ scheme: 'monaco', authority: 'untitledWorkspacesHome' });
readonly workspaceStorageHome: URI = URI.from({ scheme: 'monaco', authority: 'workspaceStorageHome' });
readonly appSharedDataHome: URI = URI.from({ scheme: 'monaco', authority: 'appSharedDataHome' });
readonly localHistoryHome: URI = URI.from({ scheme: 'monaco', authority: 'localHistoryHome' });
readonly cacheHome: URI = URI.from({ scheme: 'monaco', authority: 'cacheHome' });
readonly userDataSyncHome: URI = URI.from({ scheme: 'monaco', authority: 'userDataSyncHome' });
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/environment/common/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface NativeParsedArgs {
'extensions-dir'?: string;
'extensions-download-dir'?: string;
'builtin-extensions-dir'?: string;
'shared-data-dir'?: string;
'agent-plugins-dir'?: string;
extensionDevelopmentPath?: string[]; // undefined or array of 1 or more local paths or URIs
extensionTestsPath?: string; // either a local path or a URI
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/environment/common/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface IEnvironmentService {
workspaceStorageHome: URI;
localHistoryHome: URI;
cacheHome: URI;
appSharedDataHome: URI;

// --- settings sync
userDataSyncHome: URI;
Expand Down
15 changes: 15 additions & 0 deletions src/vs/platform/environment/common/environmentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,21 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
return joinPath(this.userHome, this.productService.dataFolderName, 'extensions').fsPath;
}

@memoize
get appSharedDataHome(): URI {
const cliSharedDataDir = this.args['shared-data-dir'];
Comment thread
sandy081 marked this conversation as resolved.
if (cliSharedDataDir) {
return URI.file(resolve(cliSharedDataDir));
}

const vscodePortable = env['VSCODE_PORTABLE'];
if (vscodePortable) {
return URI.file(join(vscodePortable, 'shared-data'));
}

return joinPath(this.userHome, this.productService.sharedDataFolderName);
}

@memoize
get agentPluginsPath(): string {
const cliAgentPluginsDir = this.args['agent-plugins-dir'];
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/environment/node/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'extensions-dir': { type: 'string', deprecates: ['extensionHomePath'], cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") },
'extensions-download-dir': { type: 'string' },
'builtin-extensions-dir': { type: 'string' },
'shared-data-dir': { type: 'string' },
'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") },
'agent-plugins-dir': { type: 'string' },
'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") },
Expand Down
57 changes: 56 additions & 1 deletion src/vs/platform/storage/common/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export interface IApplicationStorageValueChangeEvent extends IStorageValueChange
readonly scope: StorageScope.APPLICATION;
}

export interface IApplicationSharedStorageValueChangeEvent extends IStorageValueChangeEvent {
readonly scope: StorageScope.APPLICATION_SHARED;
}

export interface IStorageService {

readonly _serviceBrand: undefined;
Expand All @@ -69,6 +73,7 @@ export interface IStorageService {
onDidChangeValue(scope: StorageScope.WORKSPACE, key: string | undefined, disposable: DisposableStore): Event<IWorkspaceStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope.PROFILE, key: string | undefined, disposable: DisposableStore): Event<IProfileStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope.APPLICATION, key: string | undefined, disposable: DisposableStore): Event<IApplicationStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope.APPLICATION_SHARED, key: string | undefined, disposable: DisposableStore): Event<IApplicationSharedStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope, key: string | undefined, disposable: DisposableStore): Event<IStorageValueChangeEvent>;

/**
Expand Down Expand Up @@ -222,6 +227,12 @@ export interface IStorageService {

export const enum StorageScope {

/**
* The stored data will be scoped to all workspaces across all profiles
* and shared across VS Code and Sessions app.
*/
APPLICATION_SHARED = -2,

/**
* The stored data will be scoped to all workspaces across all profiles.
*/
Expand Down Expand Up @@ -340,6 +351,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
onDidChangeValue(scope: StorageScope.WORKSPACE, key: string | undefined, disposable: DisposableStore): Event<IWorkspaceStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope.PROFILE, key: string | undefined, disposable: DisposableStore): Event<IProfileStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope.APPLICATION, key: string | undefined, disposable: DisposableStore): Event<IApplicationStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope.APPLICATION_SHARED, key: string | undefined, disposable: DisposableStore): Event<IApplicationSharedStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope, key: string | undefined, disposable: DisposableStore): Event<IStorageValueChangeEvent> {
return Event.filter(this._onDidChangeValue.event, e => e.scope === scope && (key === undefined || e.key === key), disposable);
}
Expand Down Expand Up @@ -398,6 +410,9 @@ export abstract class AbstractStorageService extends Disposable implements IStor

// Clear our cached version which is now out of date
switch (scope) {
case StorageScope.APPLICATION_SHARED:
this._applicationSharedKeyTargets = undefined;
break;
case StorageScope.APPLICATION:
this._applicationKeyTargets = undefined;
break;
Expand Down Expand Up @@ -564,8 +579,19 @@ export abstract class AbstractStorageService extends Disposable implements IStor
return this._applicationKeyTargets;
}

private _applicationSharedKeyTargets: IKeyTargets | undefined = undefined;
private get applicationSharedKeyTargets(): IKeyTargets {
if (!this._applicationSharedKeyTargets) {
this._applicationSharedKeyTargets = this.loadKeyTargets(StorageScope.APPLICATION_SHARED);
}

return this._applicationSharedKeyTargets;
}

private getKeyTargets(scope: StorageScope): IKeyTargets {
switch (scope) {
case StorageScope.APPLICATION_SHARED:
return this.applicationSharedKeyTargets;
case StorageScope.APPLICATION:
return this.applicationKeyTargets;
case StorageScope.PROFILE:
Expand All @@ -591,6 +617,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
this._onWillSaveState.fire({ reason });

const applicationStorage = this.getStorage(StorageScope.APPLICATION);
const applicationSharedStorage = this.getStorage(StorageScope.APPLICATION_SHARED);
const profileStorage = this.getStorage(StorageScope.PROFILE);
const workspaceStorage = this.getStorage(StorageScope.WORKSPACE);

Expand All @@ -600,6 +627,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
case WillSaveStateReason.NONE:
await Promises.settled([
applicationStorage?.whenFlushed() ?? Promise.resolve(),
applicationSharedStorage?.whenFlushed() ?? Promise.resolve(),
profileStorage?.whenFlushed() ?? Promise.resolve(),
workspaceStorage?.whenFlushed() ?? Promise.resolve()
]);
Expand All @@ -610,6 +638,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
case WillSaveStateReason.SHUTDOWN:
await Promises.settled([
applicationStorage?.flush(0) ?? Promise.resolve(),
applicationSharedStorage?.flush(0) ?? Promise.resolve(),
profileStorage?.flush(0) ?? Promise.resolve(),
workspaceStorage?.flush(0) ?? Promise.resolve()
]);
Expand All @@ -619,14 +648,17 @@ export abstract class AbstractStorageService extends Disposable implements IStor

async log(): Promise<void> {
const applicationItems = this.getStorage(StorageScope.APPLICATION)?.items ?? new Map<string, string>();
const applicationSharedItems = this.getStorage(StorageScope.APPLICATION_SHARED)?.items ?? new Map<string, string>();
const profileItems = this.getStorage(StorageScope.PROFILE)?.items ?? new Map<string, string>();
const workspaceItems = this.getStorage(StorageScope.WORKSPACE)?.items ?? new Map<string, string>();

return logStorage(
applicationItems,
applicationSharedItems,
profileItems,
workspaceItems,
this.getLogDetails(StorageScope.APPLICATION) ?? '',
this.getLogDetails(StorageScope.APPLICATION_SHARED) ?? '',
this.getLogDetails(StorageScope.PROFILE) ?? '',
this.getLogDetails(StorageScope.WORKSPACE) ?? ''
);
Expand Down Expand Up @@ -707,6 +739,7 @@ export function isProfileUsingDefaultStorage(profile: IUserDataProfile): boolean
export class InMemoryStorageService extends AbstractStorageService {

private readonly applicationStorage = this._register(new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }));
private readonly applicationSharedStorage = this._register(new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }));
private readonly profileStorage = this._register(new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }));
private readonly workspaceStorage = this._register(new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }));

Expand All @@ -716,10 +749,13 @@ export class InMemoryStorageService extends AbstractStorageService {
this._register(this.workspaceStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.WORKSPACE, e)));
this._register(this.profileStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.PROFILE, e)));
this._register(this.applicationStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.APPLICATION, e)));
this._register(this.applicationSharedStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.APPLICATION_SHARED, e)));
}

protected getStorage(scope: StorageScope): IStorage {
switch (scope) {
case StorageScope.APPLICATION_SHARED:
return this.applicationSharedStorage;
case StorageScope.APPLICATION:
return this.applicationStorage;
case StorageScope.PROFILE:
Expand All @@ -731,6 +767,8 @@ export class InMemoryStorageService extends AbstractStorageService {

protected getLogDetails(scope: StorageScope): string | undefined {
switch (scope) {
case StorageScope.APPLICATION_SHARED:
return 'inMemory (application-shared)';
case StorageScope.APPLICATION:
return 'inMemory (application)';
case StorageScope.PROFILE:
Expand Down Expand Up @@ -759,7 +797,7 @@ export class InMemoryStorageService extends AbstractStorageService {
}
}

export async function logStorage(application: Map<string, string>, profile: Map<string, string>, workspace: Map<string, string>, applicationPath: string, profilePath: string, workspacePath: string): Promise<void> {
export async function logStorage(application: Map<string, string>, applicationShared: Map<string, string>, profile: Map<string, string>, workspace: Map<string, string>, applicationPath: string, applicationSharedPath: string, profilePath: string, workspacePath: string): Promise<void> {
const safeParse = (value: string) => {
try {
return JSON.parse(value);
Expand All @@ -775,6 +813,13 @@ export async function logStorage(application: Map<string, string>, profile: Map<
applicationItemsParsed.set(key, safeParse(value));
});

const applicationSharedItems = new Map<string, string>();
const applicationSharedItemsParsed = new Map<string, string>();
applicationShared.forEach((value, key) => {
applicationSharedItems.set(key, value);
applicationSharedItemsParsed.set(key, safeParse(value));
});

const profileItems = new Map<string, string>();
const profileItemsParsed = new Map<string, string>();
profile.forEach((value, key) => {
Expand Down Expand Up @@ -803,6 +848,16 @@ export async function logStorage(application: Map<string, string>, profile: Map<

console.log(applicationItemsParsed);

console.group(`Storage: Application Shared (path: ${applicationSharedPath})`);
const applicationSharedValues: { key: string; value: string }[] = [];
applicationSharedItems.forEach((value, key) => {
applicationSharedValues.push({ key, value });
});
console.table(applicationSharedValues);
console.groupEnd();

console.log(applicationSharedItemsParsed);

if (applicationPath !== profilePath) {
console.group(`Storage: Profile (path: ${profilePath}, profile specific)`);
const profileValues: { key: string; value: string }[] = [];
Expand Down
Loading
Loading