mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-03 09:35:40 -05:00
Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898 (#15681)
* Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898 * Fixes and cleanup * Distro * Fix hygiene yarn * delete no yarn lock changes file * Fix hygiene * Fix layer check * Fix CI * Skip lib checks * Remove tests deleted in vs code * Fix tests * Distro * Fix tests and add removed extension point * Skip failing notebook tests for now * Disable broken tests and cleanup build folder * Update yarn.lock and fix smoke tests * Bump sqlite * fix contributed actions and file spacing * Fix user data path * Update yarn.locks Co-authored-by: ADS Merger <karlb@microsoft.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import { generateUuid } from 'vs/base/common/uuid';
|
||||
import * as semver from 'vs/base/common/semver/semver';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { Promises } from 'vs/base/common/async';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/;
|
||||
|
||||
@@ -45,13 +46,26 @@ export class ExtensionsDownloader extends Disposable {
|
||||
// Download only if vsix does not exist
|
||||
if (!await this.fileService.exists(location)) {
|
||||
// Download to temporary location first only if vsix does not exist
|
||||
const tempLocation = joinPath(this.extensionsDownloadDir, `.${vsixName}`);
|
||||
const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`);
|
||||
if (!await this.fileService.exists(tempLocation)) {
|
||||
await this.extensionGalleryService.download(extension, tempLocation, operation);
|
||||
}
|
||||
|
||||
// Rename temp location to original
|
||||
await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
|
||||
try {
|
||||
// Rename temp location to original
|
||||
await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
|
||||
} catch (error) {
|
||||
try {
|
||||
await this.fileService.del(tempLocation);
|
||||
} catch (e) { /* ignore */ }
|
||||
if (error.code === 'ENOTEMPTY') {
|
||||
this.logService.info(`Rename failed because vsix was downloaded by another source. So ignoring renaming.`, extension.identifier.id);
|
||||
} else {
|
||||
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the vsix from downloaded location`, tempLocation.path);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return location;
|
||||
|
||||
@@ -47,6 +47,8 @@ import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common
|
||||
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
|
||||
import { ExtensionsScanner, ILocalExtensionManifest, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner';
|
||||
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
|
||||
import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
|
||||
const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled';
|
||||
const INSTALL_ERROR_DOWNLOADING = 'downloading';
|
||||
@@ -91,12 +93,19 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
@optional(IDownloadService) private downloadService: IDownloadService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IFileService fileService: IFileService,
|
||||
) {
|
||||
super();
|
||||
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
|
||||
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension)));
|
||||
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
|
||||
this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader));
|
||||
const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService));
|
||||
|
||||
this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(({ added, removed }) => {
|
||||
added.forEach(extension => this._onDidInstallExtension.fire({ identifier: extension.identifier, operation: InstallOperation.None, local: extension }));
|
||||
removed.forEach(extension => this._onDidUninstallExtension.fire({ identifier: extension }));
|
||||
}));
|
||||
|
||||
this._register(toDisposable(() => {
|
||||
this.installingExtensions.forEach(promise => promise.cancel());
|
||||
@@ -327,7 +336,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
}
|
||||
|
||||
if (existingExtension && semver.neq(existingExtension.manifest.version, extension.version)) {
|
||||
await this.setUninstalled(existingExtension);
|
||||
await this.extensionsScanner.setUninstalled(existingExtension);
|
||||
}
|
||||
|
||||
this.logService.info(`Extensions installed successfully:`, extension.identifier.id);
|
||||
@@ -371,7 +380,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
throw new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled"));
|
||||
}
|
||||
|
||||
await this.setUninstalled(extension);
|
||||
await this.extensionsScanner.setUninstalled(extension);
|
||||
try {
|
||||
await this.extensionsScanner.removeUninstalledExtension(extension);
|
||||
} catch (e) {
|
||||
@@ -397,12 +406,11 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
publisherDisplayName: extension.publisherDisplayName,
|
||||
};
|
||||
|
||||
let zipPath;
|
||||
let zipPath: string | undefined;
|
||||
try {
|
||||
this.logService.trace('Started downloading extension:', extension.identifier.id);
|
||||
const zip = await this.extensionsDownloader.downloadExtension(extension, operation);
|
||||
zipPath = (await this.extensionsDownloader.downloadExtension(extension, operation)).fsPath;
|
||||
this.logService.info('Downloaded extension:', extension.identifier.id, zipPath);
|
||||
zipPath = zip.fsPath;
|
||||
} catch (error) {
|
||||
throw new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING);
|
||||
}
|
||||
@@ -439,11 +447,10 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
|
||||
this.logService.trace('Removing the extension from uninstalled list:', identifierWithVersion.id);
|
||||
// If the same version of extension is marked as uninstalled, remove it from there and return the local.
|
||||
await this.unsetUninstalled(identifierWithVersion);
|
||||
const local = await this.extensionsScanner.setInstalled(identifierWithVersion);
|
||||
this.logService.info('Removed the extension from uninstalled list:', identifierWithVersion.id);
|
||||
|
||||
const installed = await this.getInstalled(ExtensionType.User);
|
||||
return installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null;
|
||||
return local;
|
||||
}
|
||||
|
||||
private async extractAndInstall({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
|
||||
@@ -664,7 +671,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
// Set all versions of the extension as uninstalled
|
||||
promise = createCancelablePromise(async () => {
|
||||
const userExtensions = await this.extensionsScanner.scanUserExtensions(false);
|
||||
await this.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier)));
|
||||
await this.extensionsScanner.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier)));
|
||||
});
|
||||
this.uninstallingExtensions.set(local.identifier.id, promise);
|
||||
promise.finally(() => this.uninstallingExtensions.delete(local.identifier.id));
|
||||
@@ -702,28 +709,15 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
return uninstalled.length === 1;
|
||||
}
|
||||
|
||||
private filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise<string[]> {
|
||||
return this.extensionsScanner.withUninstalledExtensions(allUninstalled => {
|
||||
const uninstalled: string[] = [];
|
||||
for (const identifier of identifiers) {
|
||||
if (!!allUninstalled[identifier.key()]) {
|
||||
uninstalled.push(identifier.key());
|
||||
}
|
||||
private async filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise<string[]> {
|
||||
const uninstalled: string[] = [];
|
||||
const allUninstalled = await this.extensionsScanner.getUninstalledExtensions();
|
||||
for (const identifier of identifiers) {
|
||||
if (!!allUninstalled[identifier.key()]) {
|
||||
uninstalled.push(identifier.key());
|
||||
}
|
||||
return uninstalled;
|
||||
});
|
||||
}
|
||||
|
||||
private setUninstalled(...extensions: ILocalExtension[]): Promise<{ [id: string]: boolean }> {
|
||||
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
|
||||
return this.extensionsScanner.withUninstalledExtensions(uninstalled => {
|
||||
ids.forEach(id => uninstalled[id.key()] = true);
|
||||
return uninstalled;
|
||||
});
|
||||
}
|
||||
|
||||
private unsetUninstalled(extensionIdentifier: ExtensionIdentifierWithVersion): Promise<void> {
|
||||
return this.extensionsScanner.withUninstalledExtensions<void>(uninstalled => delete uninstalled[extensionIdentifier.key()]);
|
||||
}
|
||||
return uninstalled;
|
||||
}
|
||||
|
||||
getExtensionsReport(): Promise<IReportedExtension[]> {
|
||||
|
||||
@@ -25,8 +25,6 @@ type IExeBasedExtensionTips = {
|
||||
|
||||
export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly allImportantExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
|
||||
private readonly allOtherExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
|
||||
|
||||
@@ -59,11 +57,11 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
}
|
||||
}
|
||||
|
||||
getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
|
||||
override getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
|
||||
return this.getValidExecutableBasedExtensionTips(this.allImportantExecutableTips);
|
||||
}
|
||||
|
||||
getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
|
||||
override getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
|
||||
return this.getValidExecutableBasedExtensionTips(this.allOtherExecutableTips);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ import { isWindows } from 'vs/base/common/platform';
|
||||
import { flatten } from 'vs/base/common/arrays';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
|
||||
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
|
||||
@@ -31,7 +35,8 @@ const INSTALL_ERROR_EXTRACTING = 'extracting';
|
||||
const INSTALL_ERROR_DELETING = 'deleting';
|
||||
const INSTALL_ERROR_RENAMING = 'renaming';
|
||||
|
||||
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean }>;
|
||||
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; }>;
|
||||
type IStoredMetadata = IMetadata & { installedTimestamp: number | undefined };
|
||||
export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata };
|
||||
type IRelaxedLocalExtension = Omit<ILocalExtension, 'isBuiltin'> & { isBuiltin: boolean };
|
||||
|
||||
@@ -44,6 +49,7 @@ export class ExtensionsScanner extends Disposable {
|
||||
|
||||
constructor(
|
||||
private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@@ -97,7 +103,7 @@ export class ExtensionsScanner extends Disposable {
|
||||
|
||||
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise<ILocalExtension> {
|
||||
const folderName = identifierWithVersion.key();
|
||||
const tempPath = path.join(this.extensionsPath, `.${folderName}`);
|
||||
const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`);
|
||||
const extensionPath = path.join(this.extensionsPath, folderName);
|
||||
|
||||
try {
|
||||
@@ -110,20 +116,29 @@ export class ExtensionsScanner extends Disposable {
|
||||
}
|
||||
|
||||
await this.extractAtLocation(identifierWithVersion, zipPath, tempPath, token);
|
||||
let local = await this.scanExtension(URI.file(tempPath), ExtensionType.User);
|
||||
if (!local) {
|
||||
throw new Error(localize('cannot read', "Cannot read the extension from {0}", tempPath));
|
||||
}
|
||||
await this.storeMetadata(local, { installedTimestamp: Date.now() });
|
||||
|
||||
try {
|
||||
await this.rename(identifierWithVersion, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
|
||||
this.logService.info('Renamed to', extensionPath);
|
||||
} catch (error) {
|
||||
this.logService.info('Rename failed. Deleting from extracted location', tempPath);
|
||||
try {
|
||||
pfs.rimraf(tempPath);
|
||||
await pfs.rimraf(tempPath);
|
||||
} catch (e) { /* ignore */ }
|
||||
throw error;
|
||||
if (error.code === 'ENOTEMPTY') {
|
||||
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, identifierWithVersion.id);
|
||||
} else {
|
||||
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let local: ILocalExtension | null = null;
|
||||
try {
|
||||
local = await this.scanExtension(folderName, this.extensionsPath, ExtensionType.User);
|
||||
local = await this.scanExtension(URI.file(extensionPath), ExtensionType.User);
|
||||
} catch (e) { /*ignore */ }
|
||||
|
||||
if (local) {
|
||||
@@ -134,23 +149,46 @@ export class ExtensionsScanner extends Disposable {
|
||||
|
||||
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: IMetadata): Promise<ILocalExtension> {
|
||||
this.setMetadata(local, metadata);
|
||||
await this.storeMetadata(local, { ...metadata, installedTimestamp: local.installedTimestamp });
|
||||
return local;
|
||||
}
|
||||
|
||||
private async storeMetadata(local: ILocalExtension, storedMetadata: IStoredMetadata): Promise<ILocalExtension> {
|
||||
// unset if false
|
||||
metadata.isMachineScoped = metadata.isMachineScoped || undefined;
|
||||
metadata.isBuiltin = metadata.isBuiltin || undefined;
|
||||
storedMetadata.isMachineScoped = storedMetadata.isMachineScoped || undefined;
|
||||
storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined;
|
||||
storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined;
|
||||
const manifestPath = path.join(local.location.fsPath, 'package.json');
|
||||
const raw = await fs.promises.readFile(manifestPath, 'utf8');
|
||||
const { manifest } = await this.parseManifest(raw);
|
||||
(manifest as ILocalExtensionManifest).__metadata = metadata;
|
||||
(manifest as ILocalExtensionManifest).__metadata = storedMetadata;
|
||||
await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
|
||||
return local;
|
||||
}
|
||||
|
||||
getUninstalledExtensions(): Promise<{ [id: string]: boolean; }> {
|
||||
return this.withUninstalledExtensions(uninstalled => uninstalled);
|
||||
getUninstalledExtensions(): Promise<IStringDictionary<boolean>> {
|
||||
return this.withUninstalledExtensions();
|
||||
}
|
||||
|
||||
async withUninstalledExtensions<T>(fn: (uninstalled: IStringDictionary<boolean>) => T): Promise<T> {
|
||||
async setUninstalled(...extensions: ILocalExtension[]): Promise<void> {
|
||||
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
|
||||
await this.withUninstalledExtensions(uninstalled => {
|
||||
ids.forEach(id => uninstalled[id.key()] = true);
|
||||
});
|
||||
}
|
||||
|
||||
async setInstalled(identifierWithVersion: ExtensionIdentifierWithVersion): Promise<ILocalExtension | null> {
|
||||
await this.withUninstalledExtensions(uninstalled => delete uninstalled[identifierWithVersion.key()]);
|
||||
const installed = await this.scanExtensions(ExtensionType.User);
|
||||
const localExtension = installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null;
|
||||
if (!localExtension) {
|
||||
return null;
|
||||
}
|
||||
await this.storeMetadata(localExtension, { installedTimestamp: Date.now() });
|
||||
return this.scanExtension(localExtension.location, ExtensionType.User);
|
||||
}
|
||||
|
||||
private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> {
|
||||
return this.uninstalledFileLimiter.queue(async () => {
|
||||
let raw: string | undefined;
|
||||
try {
|
||||
@@ -168,15 +206,16 @@ export class ExtensionsScanner extends Disposable {
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
const result = fn(uninstalled);
|
||||
|
||||
if (Object.keys(uninstalled).length) {
|
||||
await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
|
||||
} else {
|
||||
await pfs.rimraf(this.uninstalledPath);
|
||||
if (updateFn) {
|
||||
updateFn(uninstalled);
|
||||
if (Object.keys(uninstalled).length) {
|
||||
await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
|
||||
} else {
|
||||
await pfs.rimraf(this.uninstalledPath);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return uninstalled;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,33 +276,41 @@ export class ExtensionsScanner extends Disposable {
|
||||
|
||||
private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise<ILocalExtension[]> {
|
||||
const limiter = new Limiter<any>(10);
|
||||
const extensionsFolders = await pfs.readdir(dir);
|
||||
const extensions = await Promise.all<ILocalExtension>(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type))));
|
||||
return extensions.filter(e => e && e.identifier);
|
||||
const stat = await this.fileService.resolve(URI.file(dir));
|
||||
if (stat.children) {
|
||||
const extensions = await Promise.all<ILocalExtension>(stat.children.filter(c => c.isDirectory)
|
||||
.map(c => limiter.queue(async () => {
|
||||
if (type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
|
||||
return null;
|
||||
}
|
||||
return this.scanExtension(c.resource, type);
|
||||
})));
|
||||
return extensions.filter(e => e && e.identifier);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private async scanExtension(folderName: string, root: string, type: ExtensionType): Promise<ILocalExtension | null> {
|
||||
if (type === ExtensionType.User && folderName.indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
|
||||
return null;
|
||||
}
|
||||
const extensionPath = path.join(root, folderName);
|
||||
private async scanExtension(extensionLocation: URI, type: ExtensionType): Promise<ILocalExtension | null> {
|
||||
try {
|
||||
const children = await pfs.readdir(extensionPath);
|
||||
const { manifest, metadata } = await this.readManifest(extensionPath);
|
||||
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
|
||||
const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : undefined;
|
||||
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
|
||||
const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : undefined;
|
||||
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
|
||||
const local = <ILocalExtension>{ type, identifier, manifest, location: URI.file(extensionPath), readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
|
||||
if (metadata) {
|
||||
this.setMetadata(local, metadata);
|
||||
const stat = await this.fileService.resolve(extensionLocation);
|
||||
if (stat.children) {
|
||||
const { manifest, metadata } = await this.readManifest(extensionLocation.fsPath);
|
||||
const readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource;
|
||||
const changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource;
|
||||
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
|
||||
const local = <ILocalExtension>{ type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
|
||||
if (metadata) {
|
||||
this.setMetadata(local, metadata);
|
||||
local.installedTimestamp = metadata.installedTimestamp;
|
||||
}
|
||||
return local;
|
||||
}
|
||||
return local;
|
||||
} catch (e) {
|
||||
this.logService.trace(e);
|
||||
return null;
|
||||
if (type !== ExtensionType.System) {
|
||||
this.logService.trace(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async scanDefaultSystemExtensions(): Promise<ILocalExtension[]> {
|
||||
@@ -344,7 +391,7 @@ export class ExtensionsScanner extends Disposable {
|
||||
return this._devSystemExtensionsPath;
|
||||
}
|
||||
|
||||
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
|
||||
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> {
|
||||
const promises = [
|
||||
fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
|
||||
.then(raw => this.parseManifest(raw)),
|
||||
|
||||
143
src/vs/platform/extensionManagement/node/extensionsWatcher.ts
Normal file
143
src/vs/platform/extensionManagement/node/extensionsWatcher.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionManagementService, ILocalExtension, InstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { FileChangeType, FileSystemProviderCapabilities, IFileChange, IFileService } from 'vs/platform/files/common/files';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtUri } from 'vs/base/common/resources';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export class ExtensionsWatcher extends Disposable {
|
||||
|
||||
private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: ILocalExtension[], removed: IExtensionIdentifier[] }>());
|
||||
readonly onDidChangeExtensionsByAnotherSource = this._onDidChangeExtensionsByAnotherSource.event;
|
||||
|
||||
private startTimestamp = 0;
|
||||
private installingExtensions: IExtensionIdentifier[] = [];
|
||||
private installedExtensions: IExtensionIdentifier[] | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly extensionsManagementService: IExtensionManagementService,
|
||||
@IFileService fileService: IFileService,
|
||||
@INativeEnvironmentService environmentService: INativeEnvironmentService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
this.extensionsManagementService.getInstalled(ExtensionType.User).then(extensions => {
|
||||
this.installedExtensions = extensions.map(e => e.identifier);
|
||||
this.startTimestamp = Date.now();
|
||||
});
|
||||
this._register(extensionsManagementService.onInstallExtension(e => this.onInstallExtension(e)));
|
||||
this._register(extensionsManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e)));
|
||||
this._register(extensionsManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e)));
|
||||
|
||||
const extensionsResource = URI.file(environmentService.extensionsPath);
|
||||
const extUri = new ExtUri(resource => !fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive));
|
||||
this._register(fileService.watch(extensionsResource));
|
||||
this._register(Event.filter(fileService.onDidFilesChange, e => e.changes.some(change => this.doesChangeAffects(change, extensionsResource, extUri)))(() => this.onDidChange()));
|
||||
}
|
||||
|
||||
private doesChangeAffects(change: IFileChange, extensionsResource: URI, extUri: ExtUri): boolean {
|
||||
// Is not immediate child of extensions resource
|
||||
if (!extUri.isEqual(extUri.dirname(change.resource), extensionsResource)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// .obsolete file changed
|
||||
if (extUri.isEqual(change.resource, extUri.joinPath(extensionsResource, '.obsolete'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only interested in added/deleted changes
|
||||
if (change.type !== FileChangeType.ADDED && change.type !== FileChangeType.DELETED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ingore changes to files starting with `.`
|
||||
if (extUri.basename(change.resource).startsWith('.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private onInstallExtension(e: InstallExtensionEvent): void {
|
||||
this.addInstallingExtension(e.identifier);
|
||||
}
|
||||
|
||||
private onDidInstallExtension(e: DidInstallExtensionEvent): void {
|
||||
this.removeInstallingExtension(e.identifier);
|
||||
if (!e.error) {
|
||||
this.addInstalledExtension(e.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private onDidUninstallExtension(e: DidUninstallExtensionEvent): void {
|
||||
if (!e.error) {
|
||||
this.removeInstalledExtension(e.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private addInstallingExtension(extension: IExtensionIdentifier) {
|
||||
this.removeInstallingExtension(extension);
|
||||
this.installingExtensions.push(extension);
|
||||
}
|
||||
|
||||
private removeInstallingExtension(identifier: IExtensionIdentifier) {
|
||||
this.installingExtensions = this.installingExtensions.filter(e => !areSameExtensions(e, identifier));
|
||||
}
|
||||
|
||||
private addInstalledExtension(extension: IExtensionIdentifier): void {
|
||||
if (this.installedExtensions) {
|
||||
this.removeInstalledExtension(extension);
|
||||
this.installedExtensions.push(extension);
|
||||
}
|
||||
}
|
||||
|
||||
private removeInstalledExtension(identifier: IExtensionIdentifier): void {
|
||||
if (this.installedExtensions) {
|
||||
this.installedExtensions = this.installedExtensions.filter(e => !areSameExtensions(e, identifier));
|
||||
}
|
||||
}
|
||||
|
||||
private async onDidChange(): Promise<void> {
|
||||
if (this.installedExtensions) {
|
||||
const extensions = await this.extensionsManagementService.getInstalled(ExtensionType.User);
|
||||
const added = extensions.filter(e => {
|
||||
if ([...this.installingExtensions, ...this.installedExtensions!].some(identifier => areSameExtensions(identifier, e.identifier))) {
|
||||
return false;
|
||||
}
|
||||
if (e.installedTimestamp && e.installedTimestamp > this.startTimestamp) {
|
||||
this.logService.info('Detected extension installed from another source', e.identifier.id);
|
||||
return true;
|
||||
} else {
|
||||
this.logService.info('Ignored extension installed by another source because of invalid timestamp', e.identifier.id);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const removed = this.installedExtensions.filter(identifier => {
|
||||
// Extension being installed
|
||||
if (this.installingExtensions.some(installingExtension => areSameExtensions(installingExtension, identifier))) {
|
||||
return false;
|
||||
}
|
||||
if (extensions.every(e => !areSameExtensions(e.identifier, identifier))) {
|
||||
this.logService.info('Detected extension removed from another source', identifier.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this.installedExtensions = extensions.map(e => e.identifier);
|
||||
if (added.length || removed.length) {
|
||||
this._onDidChangeExtensionsByAnotherSource.fire({ added, removed });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user