UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

437 lines (436 loc) 16.6 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { isNonEmptyArray } from '@sussudio/base/common/arrays.mjs'; import { disposableTimeout, timeout } from '@sussudio/base/common/async.mjs'; import { Event } from '@sussudio/base/common/event.mjs'; import { join } from '@sussudio/base/common/path.mjs'; import { isWindows } from '@sussudio/base/common/platform.mjs'; import { env } from '@sussudio/base/common/process.mjs'; import { URI } from '@sussudio/base/common/uri.mjs'; import { localize } from 'vscode-nls.mjs'; import { INativeEnvironmentService } from '../../environment/common/environment.mjs'; import { IExtensionManagementService } from '../common/extensionManagement.mjs'; import { areSameExtensions } from '../common/extensionManagementUtil.mjs'; import { ExtensionTipsService as BaseExtensionTipsService } from '../common/extensionTipsService.mjs'; import { IExtensionRecommendationNotificationService } from '../../extensionRecommendations/common/extensionRecommendations.mjs'; import { IFileService } from '../../files/common/files.mjs'; import { INativeHostService } from '../../native/electron-sandbox/native.mjs'; import { IProductService } from '../../product/common/productService.mjs'; import { IStorageService } from '../../storage/common/storage.mjs'; import { ITelemetryService } from '../../telemetry/common/telemetry.mjs'; const promptedExecutableTipsStorageKey = 'extensionTips/promptedExecutableTips'; const lastPromptedMediumImpExeTimeStorageKey = 'extensionTips/lastPromptedMediumImpExeTime'; let ExtensionTipsService = class ExtensionTipsService extends BaseExtensionTipsService { environmentService; telemetryService; extensionManagementService; storageService; nativeHostService; extensionRecommendationNotificationService; highImportanceExecutableTips = new Map(); mediumImportanceExecutableTips = new Map(); allOtherExecutableTips = new Map(); highImportanceTipsByExe = new Map(); mediumImportanceTipsByExe = new Map(); constructor( environmentService, telemetryService, extensionManagementService, storageService, nativeHostService, extensionRecommendationNotificationService, fileService, productService, ) { super(fileService, productService); this.environmentService = environmentService; this.telemetryService = telemetryService; this.extensionManagementService = extensionManagementService; this.storageService = storageService; this.nativeHostService = nativeHostService; this.extensionRecommendationNotificationService = extensionRecommendationNotificationService; if (productService.exeBasedExtensionTips) { Object.entries(productService.exeBasedExtensionTips).forEach(([key, exeBasedExtensionTip]) => { const highImportanceRecommendations = []; const mediumImportanceRecommendations = []; const otherRecommendations = []; Object.entries(exeBasedExtensionTip.recommendations).forEach(([extensionId, value]) => { if (value.important) { if (exeBasedExtensionTip.important) { highImportanceRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack, }); } else { mediumImportanceRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack, }); } } else { otherRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack, }); } }); if (highImportanceRecommendations.length) { this.highImportanceExecutableTips.set(key, { exeFriendlyName: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: highImportanceRecommendations, }); } if (mediumImportanceRecommendations.length) { this.mediumImportanceExecutableTips.set(key, { exeFriendlyName: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: mediumImportanceRecommendations, }); } if (otherRecommendations.length) { this.allOtherExecutableTips.set(key, { exeFriendlyName: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: otherRecommendations, }); } }); } /* 3s has come out to be the good number to fetch and prompt important exe based recommendations Also fetch important exe based recommendations for reporting telemetry */ timeout(3000).then(async () => { await this.collectTips(); this.promptHighImportanceExeBasedTip(); this.promptMediumImportanceExeBasedTip(); }); } async getImportantExecutableBasedTips() { const highImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.highImportanceExecutableTips); const mediumImportanceExeTips = await this.getValidExecutableBasedExtensionTips( this.mediumImportanceExecutableTips, ); return [...highImportanceExeTips, ...mediumImportanceExeTips]; } getOtherExecutableBasedTips() { return this.getValidExecutableBasedExtensionTips(this.allOtherExecutableTips); } async collectTips() { const highImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.highImportanceExecutableTips); const mediumImportanceExeTips = await this.getValidExecutableBasedExtensionTips( this.mediumImportanceExecutableTips, ); const local = await this.extensionManagementService.getInstalled(); this.highImportanceTipsByExe = this.groupImportantTipsByExe(highImportanceExeTips, local); this.mediumImportanceTipsByExe = this.groupImportantTipsByExe(mediumImportanceExeTips, local); } groupImportantTipsByExe(importantExeBasedTips, local) { const importantExeBasedRecommendations = new Map(); importantExeBasedTips.forEach((tip) => importantExeBasedRecommendations.set(tip.extensionId.toLowerCase(), tip)); const { installed, uninstalled: recommendations } = this.groupByInstalled( [...importantExeBasedRecommendations.keys()], local, ); /* Log installed and uninstalled exe based recommendations */ for (const extensionId of installed) { const tip = importantExeBasedRecommendations.get(extensionId); if (tip) { this.telemetryService.publicLog2('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: tip.exeName, }); } } for (const extensionId of recommendations) { const tip = importantExeBasedRecommendations.get(extensionId); if (tip) { this.telemetryService.publicLog2('exeExtensionRecommendations:notInstalled', { extensionId, exeName: tip.exeName, }); } } const promptedExecutableTips = this.getPromptedExecutableTips(); const tipsByExe = new Map(); for (const extensionId of recommendations) { const tip = importantExeBasedRecommendations.get(extensionId); if ( tip && (!promptedExecutableTips[tip.exeName] || !promptedExecutableTips[tip.exeName].includes(tip.extensionId)) ) { let tips = tipsByExe.get(tip.exeName); if (!tips) { tips = []; tipsByExe.set(tip.exeName, tips); } tips.push(tip); } } return tipsByExe; } /** * High importance tips are prompted once per restart session */ promptHighImportanceExeBasedTip() { if (this.highImportanceTipsByExe.size === 0) { return; } const [exeName, tips] = [...this.highImportanceTipsByExe.entries()][0]; this.promptExeRecommendations(tips).then((result) => { switch (result) { case 'reacted' /* RecommendationsNotificationResult.Accepted */: this.addToRecommendedExecutables(tips[0].exeName, tips); break; case 'ignored' /* RecommendationsNotificationResult.Ignored */: this.highImportanceTipsByExe.delete(exeName); break; case 'incompatibleWindow' /* RecommendationsNotificationResult.IncompatibleWindow */: { // Recommended in incompatible window. Schedule the prompt after active window change const onActiveWindowChange = Event.once( Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)), ); this._register(onActiveWindowChange(() => this.promptHighImportanceExeBasedTip())); break; } case 'toomany' /* RecommendationsNotificationResult.TooMany */: { // Too many notifications. Schedule the prompt after one hour const disposable = this._register( disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */), ); break; } } }); } /** * Medium importance tips are prompted once per 7 days */ promptMediumImportanceExeBasedTip() { if (this.mediumImportanceTipsByExe.size === 0) { return; } const lastPromptedMediumExeTime = this.getLastPromptedMediumExeTime(); const timeSinceLastPrompt = Date.now() - lastPromptedMediumExeTime; const promptInterval = 7 * 24 * 60 * 60 * 1000; // 7 Days if (timeSinceLastPrompt < promptInterval) { // Wait until interval and prompt const disposable = this._register( disposableTimeout(() => { disposable.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval - timeSinceLastPrompt), ); return; } const [exeName, tips] = [...this.mediumImportanceTipsByExe.entries()][0]; this.promptExeRecommendations(tips).then((result) => { switch (result) { case 'reacted' /* RecommendationsNotificationResult.Accepted */: { // Accepted: Update the last prompted time and caches. this.updateLastPromptedMediumExeTime(Date.now()); this.mediumImportanceTipsByExe.delete(exeName); this.addToRecommendedExecutables(tips[0].exeName, tips); // Schedule the next recommendation for next internval const disposable1 = this._register( disposableTimeout(() => { disposable1.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval), ); break; } case 'ignored' /* RecommendationsNotificationResult.Ignored */: // Ignored: Remove from the cache and prompt next recommendation this.mediumImportanceTipsByExe.delete(exeName); this.promptMediumImportanceExeBasedTip(); break; case 'incompatibleWindow' /* RecommendationsNotificationResult.IncompatibleWindow */: { // Recommended in incompatible window. Schedule the prompt after active window change const onActiveWindowChange = Event.once( Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)), ); this._register(onActiveWindowChange(() => this.promptMediumImportanceExeBasedTip())); break; } case 'toomany' /* RecommendationsNotificationResult.TooMany */: { // Too many notifications. Schedule the prompt after one hour const disposable2 = this._register( disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */), ); break; } } }); } async promptExeRecommendations(tips) { const installed = await this.extensionManagementService.getInstalled(1 /* ExtensionType.User */); const extensionIds = tips .filter( (tip) => !tip.whenNotInstalled || tip.whenNotInstalled.every((id) => installed.every((local) => !areSameExtensions(local.identifier, { id }))), ) .map(({ extensionId }) => extensionId.toLowerCase()); const message = localize( { key: 'exeRecommended', comment: ['Placeholder string is the name of the software that is installed.'] }, 'You have {0} installed on your system. Do you want to install the recommended extensions for it?', tips[0].exeFriendlyName, ); return this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification( extensionIds, message, `@exe:"${tips[0].exeName}"`, 3 /* RecommendationSource.EXE */, ); } getLastPromptedMediumExeTime() { let value = this.storageService.getNumber( lastPromptedMediumImpExeTimeStorageKey, -1 /* StorageScope.APPLICATION */, ); if (!value) { value = Date.now(); this.updateLastPromptedMediumExeTime(value); } return value; } updateLastPromptedMediumExeTime(value) { this.storageService.store( lastPromptedMediumImpExeTimeStorageKey, value, -1 /* StorageScope.APPLICATION */, 1 /* StorageTarget.MACHINE */, ); } getPromptedExecutableTips() { return JSON.parse( this.storageService.get(promptedExecutableTipsStorageKey, -1 /* StorageScope.APPLICATION */, '{}'), ); } addToRecommendedExecutables(exeName, tips) { const promptedExecutableTips = this.getPromptedExecutableTips(); promptedExecutableTips[exeName] = tips.map(({ extensionId }) => extensionId.toLowerCase()); this.storageService.store( promptedExecutableTipsStorageKey, JSON.stringify(promptedExecutableTips), -1 /* StorageScope.APPLICATION */, 0 /* StorageTarget.USER */, ); } groupByInstalled(recommendationsToSuggest, local) { const installed = [], uninstalled = []; const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set()); recommendationsToSuggest.forEach((id) => { if (installedExtensionsIds.has(id.toLowerCase())) { installed.push(id); } else { uninstalled.push(id); } }); return { installed, uninstalled }; } async getValidExecutableBasedExtensionTips(executableTips) { const result = []; const checkedExecutables = new Map(); for (const exeName of executableTips.keys()) { const extensionTip = executableTips.get(exeName); if (!extensionTip || !isNonEmptyArray(extensionTip.recommendations)) { continue; } const exePaths = []; if (isWindows) { if (extensionTip.windowsPath) { exePaths.push( extensionTip.windowsPath .replace('%USERPROFILE%', () => env['USERPROFILE']) .replace('%ProgramFiles(x86)%', () => env['ProgramFiles(x86)']) .replace('%ProgramFiles%', () => env['ProgramFiles']) .replace('%APPDATA%', () => env['APPDATA']) .replace('%WINDIR%', () => env['WINDIR']), ); } } else { exePaths.push(join('/usr/local/bin', exeName)); exePaths.push(join('/usr/bin', exeName)); exePaths.push(join(this.environmentService.userHome.fsPath, exeName)); } for (const exePath of exePaths) { let exists = checkedExecutables.get(exePath); if (exists === undefined) { exists = await this.fileService.exists(URI.file(exePath)); checkedExecutables.set(exePath, exists); } if (exists) { for (const { extensionId, extensionName, isExtensionPack, whenNotInstalled, } of extensionTip.recommendations) { result.push({ extensionId, extensionName, isExtensionPack, exeName, exeFriendlyName: extensionTip.exeFriendlyName, windowsPath: extensionTip.windowsPath, whenNotInstalled: whenNotInstalled, }); } } } } return result; } }; ExtensionTipsService = __decorate( [ __param(0, INativeEnvironmentService), __param(1, ITelemetryService), __param(2, IExtensionManagementService), __param(3, IStorageService), __param(4, INativeHostService), __param(5, IExtensionRecommendationNotificationService), __param(6, IFileService), __param(7, IProductService), ], ExtensionTipsService, ); export { ExtensionTipsService };