chore(vscode): update to 1.53.2
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
This commit is contained in:
@ -3,8 +3,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || [];
|
||||
globalThis.MonacoPerformanceMarks.push('renderer/started', Date.now());
|
||||
performance.mark('code/didStartRenderer')
|
||||
</script>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@ -56,7 +55,7 @@
|
||||
</script>
|
||||
<script src="./static/out/vs/loader.js"></script>
|
||||
<script>
|
||||
globalThis.MonacoPerformanceMarks.push('willLoadWorkbenchMain', Date.now());
|
||||
performance.mark('code/willLoadWorkbenchMain');
|
||||
</script>
|
||||
<script>
|
||||
require(['vs/code/browser/workbench/workbench'], function() {});
|
||||
|
@ -3,8 +3,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || [];
|
||||
globalThis.MonacoPerformanceMarks.push('renderer/started', Date.now());
|
||||
performance.mark('code/didStartRenderer')
|
||||
</script>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@ -55,7 +54,7 @@
|
||||
</script>
|
||||
<script src="./static/out/vs/loader.js"></script>
|
||||
<script>
|
||||
globalThis.MonacoPerformanceMarks.push('willLoadWorkbenchMain', Date.now());
|
||||
performance.mark('code/willLoadWorkbenchMain');
|
||||
</script>
|
||||
<script src="./static/out/vs/workbench/workbench.web.api.nls.js"></script>
|
||||
<script src="./static/out/vs/workbench/workbench.web.api.js"></script>
|
||||
|
@ -265,7 +265,6 @@ class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvi
|
||||
setTimeout(() => this.periodicFetchCallback(requestId, startTime), PollingURLCallbackProvider.FETCH_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WorkspaceProvider implements IWorkspaceProvider {
|
||||
|
@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
|
||||
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
|
||||
export class DeprecatedExtensionsCleaner extends Disposable {
|
||||
|
||||
constructor(
|
||||
@IExtensionManagementService private readonly extensionManagementService: ExtensionManagementService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(extensionManagementService); // TODO@sandy081 this seems fishy
|
||||
|
||||
this.cleanUpDeprecatedExtensions();
|
||||
}
|
||||
|
||||
private cleanUpDeprecatedExtensions(): void {
|
||||
this.extensionManagementService.removeDeprecatedExtensions();
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
|
||||
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
|
||||
|
||||
export class LocalizationsUpdater extends Disposable {
|
||||
|
||||
constructor(
|
||||
@ILocalizationsService private readonly localizationsService: LocalizationsService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.updateLocalizations();
|
||||
}
|
||||
|
||||
private updateLocalizations(): void {
|
||||
this.localizationsService.update();
|
||||
}
|
||||
}
|
@ -15,10 +15,7 @@
|
||||
|
||||
// Load shared process into window
|
||||
bootstrapWindow.load(['vs/code/electron-browser/sharedProcess/sharedProcessMain'], function (sharedProcess, configuration) {
|
||||
sharedProcess.startup({
|
||||
machineId: configuration.machineId,
|
||||
windowId: configuration.windowId
|
||||
});
|
||||
return sharedProcess.main(configuration);
|
||||
});
|
||||
|
||||
|
||||
|
@ -3,15 +3,16 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { serve, Server, connect } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import * as fs from 'fs';
|
||||
import { release } from 'os';
|
||||
import { gracefulify } from 'graceful-fs';
|
||||
import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp';
|
||||
import { StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
|
||||
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
|
||||
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
@ -23,24 +24,23 @@ import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { RequestService } from 'vs/platform/request/browser/requestService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { combinedAppender, NullTelemetryService, ITelemetryAppender, NullAppender } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
|
||||
import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc';
|
||||
import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties';
|
||||
import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc';
|
||||
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
|
||||
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
import { ILogService, LogLevel, ILoggerService } from 'vs/platform/log/common/log';
|
||||
import { ILogService, ILoggerService, MultiplexLogService, ConsoleLogService } from 'vs/platform/log/common/log';
|
||||
import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc';
|
||||
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
|
||||
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
|
||||
import { combinedDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { combinedDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { DownloadService } from 'vs/platform/download/common/downloadService';
|
||||
import { IDownloadService } from 'vs/platform/download/common/download';
|
||||
import { IChannel, IServerChannel, StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner';
|
||||
import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner';
|
||||
import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner';
|
||||
import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner';
|
||||
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { IMainProcessService, MessagePortMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
|
||||
import { DiagnosticsService, IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
@ -48,7 +48,7 @@ import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration as registerUserDataSyncConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel, UserDataSyncStoreManagementServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc';
|
||||
@ -67,146 +67,184 @@ import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-s
|
||||
import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
|
||||
import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
|
||||
import { ExtensionRecommendationNotificationServiceChannelClient } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc';
|
||||
import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker';
|
||||
import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker';
|
||||
import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender';
|
||||
import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
|
||||
import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions';
|
||||
import { ExtensionsStorageSyncService, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess';
|
||||
import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater';
|
||||
import { DeprecatedExtensionsCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner';
|
||||
import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
|
||||
export interface ISharedProcessConfiguration {
|
||||
readonly machineId: string;
|
||||
readonly windowId: number;
|
||||
}
|
||||
class SharedProcessMain extends Disposable {
|
||||
|
||||
export function startup(configuration: ISharedProcessConfiguration) {
|
||||
handshake(configuration);
|
||||
}
|
||||
private server = this._register(new MessagePortServer());
|
||||
|
||||
interface ISharedProcessInitData {
|
||||
sharedIPCHandle: string;
|
||||
args: NativeParsedArgs;
|
||||
logLevel: LogLevel;
|
||||
nodeCachedDataDir?: string;
|
||||
backupWorkspacesPath: string;
|
||||
}
|
||||
constructor(private configuration: ISharedProcessConfiguration) {
|
||||
super();
|
||||
|
||||
const eventPrefix = 'monacoworkbench';
|
||||
// Enable gracefulFs
|
||||
gracefulify(fs);
|
||||
|
||||
class MainProcessService implements IMainProcessService {
|
||||
|
||||
constructor(
|
||||
private server: Server,
|
||||
private mainRouter: StaticRouter
|
||||
) { }
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
getChannel(channelName: string): IChannel {
|
||||
return this.server.getChannel(channelName, this.mainRouter);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<string>): void {
|
||||
this.server.registerChannel(channelName, channel);
|
||||
private registerListeners(): void {
|
||||
|
||||
// Dispose on exit
|
||||
const onExit = () => this.dispose();
|
||||
process.once('exit', onExit);
|
||||
ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit);
|
||||
}
|
||||
}
|
||||
|
||||
async function main(server: Server, initData: ISharedProcessInitData, configuration: ISharedProcessConfiguration): Promise<void> {
|
||||
const services = new ServiceCollection();
|
||||
async open(): Promise<void> {
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
// Services
|
||||
const instantiationService = await this.initServices();
|
||||
|
||||
const onExit = () => disposables.dispose();
|
||||
process.once('exit', onExit);
|
||||
ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit);
|
||||
// Config
|
||||
registerUserDataSyncConfiguration();
|
||||
|
||||
disposables.add(server);
|
||||
instantiationService.invokeFunction(accessor => {
|
||||
const logService = accessor.get(ILogService);
|
||||
|
||||
const environmentService = new NativeEnvironmentService(initData.args);
|
||||
// Log info
|
||||
logService.trace('sharedProcess configuration', JSON.stringify(this.configuration));
|
||||
|
||||
const mainRouter = new StaticRouter(ctx => ctx === 'main');
|
||||
const loggerClient = new LoggerChannelClient(server.getChannel('logger', mainRouter));
|
||||
const logService = new FollowerLogService(loggerClient, new SpdLogService('sharedprocess', environmentService.logsPath, initData.logLevel));
|
||||
disposables.add(logService);
|
||||
logService.info('main', JSON.stringify(configuration));
|
||||
// Channels
|
||||
this.initChannels(accessor);
|
||||
|
||||
const mainProcessService = new MainProcessService(server, mainRouter);
|
||||
services.set(IMainProcessService, mainProcessService);
|
||||
// Error handler
|
||||
this.registerErrorHandler(logService);
|
||||
});
|
||||
|
||||
// Files
|
||||
const fileService = new FileService(logService);
|
||||
services.set(IFileService, fileService);
|
||||
disposables.add(fileService);
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(logService);
|
||||
disposables.add(diskFileSystemProvider);
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
// Instantiate Contributions
|
||||
this._register(combinedDisposable(
|
||||
new NodeCachedDataCleaner(this.configuration.nodeCachedDataDir),
|
||||
instantiationService.createInstance(LanguagePackCachedDataCleaner),
|
||||
instantiationService.createInstance(StorageDataCleaner, this.configuration.backupWorkspacesPath),
|
||||
instantiationService.createInstance(LogsDataCleaner),
|
||||
instantiationService.createInstance(LocalizationsUpdater),
|
||||
instantiationService.createInstance(DeprecatedExtensionsCleaner)
|
||||
));
|
||||
}
|
||||
|
||||
// Configuration
|
||||
const configurationService = new ConfigurationService(environmentService.settingsResource, fileService);
|
||||
disposables.add(configurationService);
|
||||
await configurationService.initialize();
|
||||
|
||||
// Storage
|
||||
const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService);
|
||||
await storageService.initialize();
|
||||
services.set(IStorageService, storageService);
|
||||
disposables.add(toDisposable(() => storageService.flush()));
|
||||
|
||||
services.set(IEnvironmentService, environmentService);
|
||||
services.set(INativeEnvironmentService, environmentService);
|
||||
|
||||
services.set(IProductService, { _serviceBrand: undefined, ...product });
|
||||
services.set(ILogService, logService);
|
||||
services.set(IConfigurationService, configurationService);
|
||||
services.set(IRequestService, new SyncDescriptor(RequestService));
|
||||
services.set(ILoggerService, new SyncDescriptor(LoggerService));
|
||||
|
||||
const nativeHostService = createChannelSender<INativeHostService>(mainProcessService.getChannel('nativeHost'), { context: configuration.windowId });
|
||||
services.set(INativeHostService, nativeHostService);
|
||||
const activeWindowManager = new ActiveWindowManager(nativeHostService);
|
||||
const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
|
||||
|
||||
services.set(IDownloadService, new SyncDescriptor(DownloadService));
|
||||
services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(server.getChannel('IExtensionRecommendationNotificationService', activeWindowRouter)));
|
||||
|
||||
const instantiationService = new InstantiationService(services);
|
||||
|
||||
let telemetryService: ITelemetryService;
|
||||
instantiationService.invokeFunction(accessor => {
|
||||
private async initServices(): Promise<IInstantiationService> {
|
||||
const services = new ServiceCollection();
|
||||
|
||||
// Environment
|
||||
const environmentService = new NativeEnvironmentService(this.configuration.args);
|
||||
services.set(IEnvironmentService, environmentService);
|
||||
services.set(INativeEnvironmentService, environmentService);
|
||||
|
||||
// Log
|
||||
const mainRouter = new StaticRouter(ctx => ctx === 'main');
|
||||
const loggerClient = new LoggerChannelClient(this.server.getChannel('logger', mainRouter)); // we only use this for log levels
|
||||
const multiplexLogger = this._register(new MultiplexLogService([
|
||||
this._register(new ConsoleLogService(this.configuration.logLevel)),
|
||||
this._register(new SpdLogService('sharedprocess', environmentService.logsPath, this.configuration.logLevel))
|
||||
]));
|
||||
|
||||
const logService = this._register(new FollowerLogService(loggerClient, multiplexLogger));
|
||||
services.set(ILogService, logService);
|
||||
|
||||
// Main Process
|
||||
const mainProcessService = new MessagePortMainProcessService(this.server, mainRouter);
|
||||
services.set(IMainProcessService, mainProcessService);
|
||||
|
||||
// Files
|
||||
const fileService = this._register(new FileService(logService));
|
||||
services.set(IFileService, fileService);
|
||||
|
||||
const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService));
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
|
||||
// Configuration
|
||||
const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService));
|
||||
services.set(IConfigurationService, configurationService);
|
||||
|
||||
await configurationService.initialize();
|
||||
|
||||
// Storage
|
||||
const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService);
|
||||
services.set(IStorageService, storageService);
|
||||
|
||||
await storageService.initialize();
|
||||
this._register(toDisposable(() => storageService.flush()));
|
||||
|
||||
// Product
|
||||
services.set(IProductService, { _serviceBrand: undefined, ...product });
|
||||
|
||||
// Request
|
||||
services.set(IRequestService, new SyncDescriptor(RequestService));
|
||||
|
||||
// Native Host
|
||||
const nativeHostService = createChannelSender<INativeHostService>(mainProcessService.getChannel('nativeHost'), { context: this.configuration.windowId });
|
||||
services.set(INativeHostService, nativeHostService);
|
||||
|
||||
// Download
|
||||
services.set(IDownloadService, new SyncDescriptor(DownloadService));
|
||||
|
||||
// Extension recommendations
|
||||
const activeWindowManager = this._register(new ActiveWindowManager(nativeHostService));
|
||||
const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
|
||||
services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(this.server.getChannel('extensionRecommendationNotification', activeWindowRouter)));
|
||||
|
||||
// Logger
|
||||
const loggerService = this._register(new LoggerService(logService, fileService));
|
||||
services.set(ILoggerService, loggerService);
|
||||
|
||||
// Telemetry
|
||||
const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService;
|
||||
|
||||
let telemetryAppender: ITelemetryAppender = NullAppender;
|
||||
let telemetryService: ITelemetryService;
|
||||
let telemetryAppender: ITelemetryAppender;
|
||||
if (!extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) {
|
||||
telemetryAppender = new TelemetryLogAppender(accessor.get(ILoggerService), environmentService);
|
||||
telemetryAppender = new TelemetryLogAppender(loggerService, environmentService);
|
||||
|
||||
// Application Insights
|
||||
if (product.aiConfig && product.aiConfig.asimovKey && isBuilt) {
|
||||
const appInsightsAppender = new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey);
|
||||
disposables.add(toDisposable(() => appInsightsAppender!.flush())); // Ensure the AI appender is disposed so that it flushes remaining data
|
||||
const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, product.aiConfig.asimovKey);
|
||||
this._register(toDisposable(() => appInsightsAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data
|
||||
telemetryAppender = combinedAppender(appInsightsAppender, telemetryAppender);
|
||||
}
|
||||
const config: ITelemetryServiceConfig = {
|
||||
|
||||
telemetryService = new TelemetryService({
|
||||
appender: telemetryAppender,
|
||||
commonProperties: resolveCommonProperties(product.commit, product.version, configuration.machineId, product.msftInternalDomains, installSourcePath),
|
||||
commonProperties: resolveCommonProperties(fileService, release(), process.arch, product.commit, product.version, this.configuration.machineId, product.msftInternalDomains, installSourcePath),
|
||||
sendErrorTelemetry: true,
|
||||
piiPaths: [appRoot, extensionsPath]
|
||||
};
|
||||
|
||||
telemetryService = new TelemetryService(config, configurationService);
|
||||
services.set(ITelemetryService, telemetryService);
|
||||
}, configurationService);
|
||||
} else {
|
||||
telemetryService = NullTelemetryService;
|
||||
services.set(ITelemetryService, NullTelemetryService);
|
||||
telemetryAppender = NullAppender;
|
||||
}
|
||||
server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender));
|
||||
|
||||
this.server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender));
|
||||
services.set(ITelemetryService, telemetryService);
|
||||
|
||||
// Extension Management
|
||||
services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
|
||||
|
||||
// Extension Gallery
|
||||
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
|
||||
services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService));
|
||||
services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService));
|
||||
|
||||
// Extension Tips
|
||||
services.set(IExtensionTipsService, new SyncDescriptor(ExtensionTipsService));
|
||||
|
||||
// Localizations
|
||||
services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService));
|
||||
|
||||
// Diagnostics
|
||||
services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService));
|
||||
|
||||
// Settings Sync
|
||||
services.set(IUserDataSyncAccountService, new SyncDescriptor(UserDataSyncAccountService));
|
||||
services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService));
|
||||
services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main')));
|
||||
services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(this.server.getChannel('userDataSyncUtil', client => client.ctx !== 'main')));
|
||||
services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService));
|
||||
services.set(IIgnoredExtensionsManagementService, new SyncDescriptor(IgnoredExtensionsManagementService));
|
||||
services.set(IExtensionsStorageSyncService, new SyncDescriptor(ExtensionsStorageSyncService));
|
||||
@ -217,114 +255,86 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat
|
||||
services.set(IUserDataAutoSyncEnablementService, new SyncDescriptor(UserDataAutoSyncEnablementService));
|
||||
services.set(IUserDataSyncResourceEnablementService, new SyncDescriptor(UserDataSyncResourceEnablementService));
|
||||
services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService));
|
||||
registerConfiguration();
|
||||
|
||||
const instantiationService2 = instantiationService.createChild(services);
|
||||
|
||||
instantiationService2.invokeFunction(accessor => {
|
||||
|
||||
const extensionManagementService = accessor.get(IExtensionManagementService);
|
||||
const channel = new ExtensionManagementChannel(extensionManagementService, () => null);
|
||||
server.registerChannel('extensions', channel);
|
||||
|
||||
const localizationsService = accessor.get(ILocalizationsService);
|
||||
const localizationsChannel = createChannelReceiver(localizationsService);
|
||||
server.registerChannel('localizations', localizationsChannel);
|
||||
|
||||
const diagnosticsService = accessor.get(IDiagnosticsService);
|
||||
const diagnosticsChannel = createChannelReceiver(diagnosticsService);
|
||||
server.registerChannel('diagnostics', diagnosticsChannel);
|
||||
|
||||
const extensionTipsService = accessor.get(IExtensionTipsService);
|
||||
const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService);
|
||||
server.registerChannel('extensionTipsService', extensionTipsChannel);
|
||||
|
||||
const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService);
|
||||
const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService);
|
||||
server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel);
|
||||
|
||||
const authTokenService = accessor.get(IUserDataSyncAccountService);
|
||||
const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService);
|
||||
server.registerChannel('userDataSyncAccount', authTokenChannel);
|
||||
|
||||
const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService);
|
||||
const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService);
|
||||
server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel);
|
||||
|
||||
const userDataSyncService = accessor.get(IUserDataSyncService);
|
||||
const userDataSyncChannel = new UserDataSyncChannel(server, userDataSyncService, logService);
|
||||
server.registerChannel('userDataSync', userDataSyncChannel);
|
||||
|
||||
const userDataAutoSync = instantiationService2.createInstance(UserDataAutoSyncService);
|
||||
const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync);
|
||||
server.registerChannel('userDataAutoSync', userDataAutoSyncChannel);
|
||||
|
||||
// clean up deprecated extensions
|
||||
(extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions();
|
||||
// update localizations cache
|
||||
(localizationsService as LocalizationsService).update();
|
||||
// cache clean ups
|
||||
disposables.add(combinedDisposable(
|
||||
new NodeCachedDataCleaner(initData.nodeCachedDataDir),
|
||||
instantiationService2.createInstance(LanguagePackCachedDataCleaner),
|
||||
instantiationService2.createInstance(StorageDataCleaner, initData.backupWorkspacesPath),
|
||||
instantiationService2.createInstance(LogsDataCleaner),
|
||||
userDataAutoSync
|
||||
));
|
||||
disposables.add(extensionManagementService as ExtensionManagementService);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupIPC(hook: string): Promise<Server> {
|
||||
function setup(retry: boolean): Promise<Server> {
|
||||
return serve(hook).then(null, err => {
|
||||
if (!retry || platform.isWindows || err.code !== 'EADDRINUSE') {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
// should retry, not windows and eaddrinuse
|
||||
|
||||
return connect(hook, '').then(
|
||||
client => {
|
||||
// we could connect to a running instance. this is not good, abort
|
||||
client.dispose();
|
||||
return Promise.reject(new Error('There is an instance already running.'));
|
||||
},
|
||||
err => {
|
||||
// it happens on Linux and OS X that the pipe is left behind
|
||||
// let's delete it, since we can't connect to it
|
||||
// and the retry the whole thing
|
||||
try {
|
||||
fs.unlinkSync(hook);
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error('Error deleting the shared ipc hook.'));
|
||||
}
|
||||
|
||||
return setup(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
return new InstantiationService(services);
|
||||
}
|
||||
|
||||
return setup(true);
|
||||
private initChannels(accessor: ServicesAccessor): void {
|
||||
|
||||
// Extensions Management
|
||||
const extensionManagementService = accessor.get(IExtensionManagementService);
|
||||
const channel = new ExtensionManagementChannel(extensionManagementService, () => null);
|
||||
this.server.registerChannel('extensions', channel);
|
||||
|
||||
// Localizations
|
||||
const localizationsService = accessor.get(ILocalizationsService);
|
||||
const localizationsChannel = createChannelReceiver(localizationsService);
|
||||
this.server.registerChannel('localizations', localizationsChannel);
|
||||
|
||||
// Diagnostics
|
||||
const diagnosticsService = accessor.get(IDiagnosticsService);
|
||||
const diagnosticsChannel = createChannelReceiver(diagnosticsService);
|
||||
this.server.registerChannel('diagnostics', diagnosticsChannel);
|
||||
|
||||
// Extension Tips
|
||||
const extensionTipsService = accessor.get(IExtensionTipsService);
|
||||
const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService);
|
||||
this.server.registerChannel('extensionTipsService', extensionTipsChannel);
|
||||
|
||||
// Settings Sync
|
||||
const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService);
|
||||
const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService);
|
||||
this.server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel);
|
||||
|
||||
const authTokenService = accessor.get(IUserDataSyncAccountService);
|
||||
const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService);
|
||||
this.server.registerChannel('userDataSyncAccount', authTokenChannel);
|
||||
|
||||
const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService);
|
||||
const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService);
|
||||
this.server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel);
|
||||
|
||||
const userDataSyncService = accessor.get(IUserDataSyncService);
|
||||
const userDataSyncChannel = new UserDataSyncChannel(this.server, userDataSyncService, accessor.get(ILogService));
|
||||
this.server.registerChannel('userDataSync', userDataSyncChannel);
|
||||
|
||||
const userDataAutoSync = this._register(accessor.get(IInstantiationService).createInstance(UserDataAutoSyncService));
|
||||
const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync);
|
||||
this.server.registerChannel('userDataAutoSync', userDataAutoSyncChannel);
|
||||
}
|
||||
|
||||
private registerErrorHandler(logService: ILogService): void {
|
||||
|
||||
// Listen on unhandled rejection events
|
||||
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
|
||||
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
|
||||
onUnexpectedError(event.reason);
|
||||
|
||||
// Prevent the printing of this event to the console
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Install handler for unexpected errors
|
||||
setUnexpectedErrorHandler(error => {
|
||||
const message = toErrorMessage(error, true);
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
logService.error(`[uncaught exception in sharedProcess]: ${message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handshake(configuration: ISharedProcessConfiguration): Promise<void> {
|
||||
export async function main(configuration: ISharedProcessConfiguration): Promise<void> {
|
||||
|
||||
// receive payload from electron-main to start things
|
||||
const data = await new Promise<ISharedProcessInitData>(c => {
|
||||
ipcRenderer.once('vscode:electron-main->shared-process=payload', (event: unknown, r: ISharedProcessInitData) => c(r));
|
||||
|
||||
// tell electron-main we are ready to receive payload
|
||||
ipcRenderer.send('vscode:shared-process->electron-main=ready-for-payload');
|
||||
});
|
||||
|
||||
// await IPC connection and signal this back to electron-main
|
||||
const server = await setupIPC(data.sharedIPCHandle);
|
||||
// create shared process and signal back to main that we are
|
||||
// ready to accept message ports as client connections
|
||||
const sharedProcess = new SharedProcessMain(configuration);
|
||||
ipcRenderer.send('vscode:shared-process->electron-main=ipc-ready');
|
||||
|
||||
// await initialization and signal this back to electron-main
|
||||
await main(server, data, configuration);
|
||||
await sharedProcess.open();
|
||||
ipcRenderer.send('vscode:shared-process->electron-main=init-done');
|
||||
}
|
||||
|
@ -3,7 +3,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' vscode-webview: https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' vscode-webview: https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https: ws:; font-src 'self' https: vscode-remote-resource:;">
|
||||
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types default TrustedFunctionWorkaround ExtensionScripts amdLoader cellRendererEditorText defaultWorkerFactory diffEditorWidget domLineBreaksComputer editorViewLayer diffReview extensionHostWorker insane notebookOutputRenderer safeInnerHtml standaloneColorizer tokenizeToString webNestedWorkerExtensionHost webWorkerExtensionHost;">
|
||||
</head>
|
||||
<body aria-label="">
|
||||
</body>
|
||||
|
@ -12,8 +12,7 @@
|
||||
const bootstrapWindow = bootstrapWindowLib();
|
||||
|
||||
// Add a perf entry right from the top
|
||||
const perf = bootstrapWindow.perfLib();
|
||||
perf.mark('renderer/started');
|
||||
performance.mark('code/didStartRenderer');
|
||||
|
||||
// Load workbench main JS, CSS and NLS all in parallel. This is an
|
||||
// optimization to prevent a waterfall of loading to happen, because
|
||||
@ -24,10 +23,10 @@
|
||||
'vs/nls!vs/workbench/workbench.desktop.main',
|
||||
'vs/css!vs/workbench/workbench.desktop.main'
|
||||
],
|
||||
async function (workbench, configuration) {
|
||||
function (_, configuration) {
|
||||
|
||||
// Mark start of workbench
|
||||
perf.mark('didLoadWorkbenchMain');
|
||||
performance.mark('code/didLoadWorkbenchMain');
|
||||
|
||||
// @ts-ignore
|
||||
return require('vs/workbench/electron-browser/desktop.main').main(configuration);
|
||||
@ -41,19 +40,34 @@
|
||||
loaderConfig.recordStats = true;
|
||||
},
|
||||
beforeRequire: function () {
|
||||
perf.mark('willLoadWorkbenchMain');
|
||||
performance.mark('code/willLoadWorkbenchMain');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// add default trustedTypes-policy for logging and to workaround
|
||||
// lib/platform limitations
|
||||
window.trustedTypes?.createPolicy('default', {
|
||||
createHTML(value) {
|
||||
// see https://github.com/electron/electron/issues/27211
|
||||
// Electron webviews use a static innerHTML default value and
|
||||
// that isn't trusted. We use a default policy to check for the
|
||||
// exact value of that innerHTML-string and only allow that.
|
||||
if (value === '<!DOCTYPE html><style type="text/css">:host { display: flex; }</style>') {
|
||||
return value;
|
||||
}
|
||||
throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached');
|
||||
// console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached');
|
||||
// return value;
|
||||
}
|
||||
});
|
||||
|
||||
//region Helpers
|
||||
|
||||
/**
|
||||
* @returns {{
|
||||
* load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown,
|
||||
* globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals'),
|
||||
* perfLib: () => { mark: (name: string) => void }
|
||||
* load: (modules: string[], resultCallback: (result, configuration: import('../../../platform/windows/common/windows').INativeWindowConfiguration) => any, options: object) => unknown,
|
||||
* globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals')
|
||||
* }}
|
||||
*/
|
||||
function bootstrapWindowLib() {
|
||||
@ -67,12 +81,11 @@
|
||||
* colorScheme: ('light' | 'dark' | 'hc'),
|
||||
* autoDetectHighContrast?: boolean,
|
||||
* extensionDevelopmentPath?: string[],
|
||||
* folderUri?: object,
|
||||
* workspace?: object
|
||||
* workspace?: import('../../../platform/workspaces/common/workspaces').IWorkspaceIdentifier | import('../../../platform/workspaces/common/workspaces').ISingleFolderWorkspaceIdentifier
|
||||
* }} configuration
|
||||
*/
|
||||
function showPartsSplash(configuration) {
|
||||
perf.mark('willShowPartsSplash');
|
||||
performance.mark('code/willShowPartsSplash');
|
||||
|
||||
let data;
|
||||
if (typeof configuration.partsSplashPath === 'string') {
|
||||
@ -147,7 +160,7 @@
|
||||
splash.appendChild(activityDiv);
|
||||
|
||||
// part: side bar (only when opening workspace/folder)
|
||||
if (configuration.folderUri || configuration.workspace) {
|
||||
if (configuration.workspace) {
|
||||
// folder or workspace -> status bar color, sidebar
|
||||
const sideDiv = document.createElement('div');
|
||||
sideDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: ${layoutInfo.activityBarWidth}px; width: ${layoutInfo.sideBarWidth}px; background-color: ${colorInfo.sideBarBackground};`);
|
||||
@ -156,13 +169,13 @@
|
||||
|
||||
// part: statusbar
|
||||
const statusDiv = document.createElement('div');
|
||||
statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.folderUri || configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`);
|
||||
statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`);
|
||||
splash.appendChild(statusDiv);
|
||||
|
||||
document.body.appendChild(splash);
|
||||
}
|
||||
|
||||
perf.mark('didShowPartsSplash');
|
||||
performance.mark('code/didShowPartsSplash');
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
@ -4,17 +4,18 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { app, ipcMain, systemPreferences, contentTracing, protocol, BrowserWindow, dialog, session } from 'electron';
|
||||
import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform';
|
||||
import { release } from 'os';
|
||||
import { IProcessEnvironment, isWindows, isMacintosh, isLinux, isLinuxSnap } from 'vs/base/common/platform';
|
||||
import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService';
|
||||
import { IWindowOpenable } from 'vs/platform/windows/common/windows';
|
||||
import { OpenContext } from 'vs/platform/windows/node/window';
|
||||
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { resolveShellEnv } from 'vs/code/node/shellEnv';
|
||||
import { IUpdateService } from 'vs/platform/update/common/update';
|
||||
import { UpdateChannel } from 'vs/platform/update/electron-main/updateIpc';
|
||||
import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main';
|
||||
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { Server, connect } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron';
|
||||
import { Server as NodeIPCServer } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Client as MessagePortClient } from 'vs/base/parts/ipc/electron-main/ipc.mp';
|
||||
import { SharedProcess } from 'vs/code/electron-main/sharedProcess';
|
||||
import { LaunchMainService, ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
@ -28,19 +29,17 @@ import { IOpenURLOptions, IURLService } from 'vs/platform/url/common/url';
|
||||
import { URLHandlerChannelClient, URLHandlerRouter } from 'vs/platform/url/common/urlIpc';
|
||||
import { ITelemetryService, machineIdKey } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc';
|
||||
import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc';
|
||||
import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
|
||||
import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { ProxyAuthHandler } from 'vs/code/electron-main/auth';
|
||||
import { ProxyAuthHandler2 } from 'vs/code/electron-main/auth2';
|
||||
import { FileProtocolHandler } from 'vs/code/electron-main/protocol';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
|
||||
import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { hasWorkspaceFileExtension, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { WorkspacesService } from 'vs/platform/workspaces/electron-main/workspacesService';
|
||||
import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { getMachineId } from 'vs/base/node/id';
|
||||
import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32';
|
||||
import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux';
|
||||
@ -64,14 +63,13 @@ import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainSe
|
||||
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
|
||||
import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
|
||||
import { NativeURLService } from 'vs/platform/url/common/urlService';
|
||||
import { WorkspacesMainService, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { WorkspacesManagementMainService, IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
|
||||
import { statSync } from 'fs';
|
||||
import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
|
||||
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
|
||||
import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc';
|
||||
import { INativeHostMainService, NativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
|
||||
import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService';
|
||||
import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
|
||||
import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels';
|
||||
import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService';
|
||||
@ -81,7 +79,7 @@ import { stripComments } from 'vs/base/common/json';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { EncryptionMainService, IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService';
|
||||
import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker';
|
||||
import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker';
|
||||
import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { DisplayMainService, IDisplayMainService } from 'vs/platform/display/electron-main/displayMainService';
|
||||
@ -90,6 +88,7 @@ import { isEqualOrParent } from 'vs/base/common/extpath';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust';
|
||||
import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
|
||||
export class CodeApplication extends Disposable {
|
||||
private windowsMainService: IWindowsMainService | undefined;
|
||||
@ -97,7 +96,7 @@ export class CodeApplication extends Disposable {
|
||||
private nativeHostMainService: INativeHostMainService | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly mainIpcServer: Server,
|
||||
private readonly mainIpcServer: NodeIPCServer,
|
||||
private readonly userEnv: IProcessEnvironment,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@ -150,19 +149,19 @@ export class CodeApplication extends Disposable {
|
||||
event.preventDefault();
|
||||
});
|
||||
app.on('remote-get-global', (event, sender, module) => {
|
||||
this.logService.trace(`App#on(remote-get-global): prevented on ${module}`);
|
||||
this.logService.trace(`app#on(remote-get-global): prevented on ${module}`);
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
app.on('remote-get-builtin', (event, sender, module) => {
|
||||
this.logService.trace(`App#on(remote-get-builtin): prevented on ${module}`);
|
||||
this.logService.trace(`app#on(remote-get-builtin): prevented on ${module}`);
|
||||
|
||||
if (module !== 'clipboard') {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
app.on('remote-get-current-window', event => {
|
||||
this.logService.trace(`App#on(remote-get-current-window): prevented`);
|
||||
this.logService.trace(`app#on(remote-get-current-window): prevented`);
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
@ -171,7 +170,7 @@ export class CodeApplication extends Disposable {
|
||||
return; // the driver needs access to web contents
|
||||
}
|
||||
|
||||
this.logService.trace(`App#on(remote-get-current-web-contents): prevented`);
|
||||
this.logService.trace(`app#on(remote-get-current-web-contents): prevented`);
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
@ -271,9 +270,13 @@ export class CodeApplication extends Disposable {
|
||||
|
||||
//#region Bootstrap IPC Handlers
|
||||
|
||||
let slowShellResolveWarningShown = false;
|
||||
ipcMain.on('vscode:fetchShellEnv', async event => {
|
||||
|
||||
// DO NOT remove: not only usual windows are fetching the
|
||||
// shell environment but also shared process, issue reporter
|
||||
// etc, so we need to reply via `webContents` always
|
||||
const webContents = event.sender;
|
||||
const window = this.windowsMainService?.getWindowByWebContents(event.sender);
|
||||
|
||||
let replied = false;
|
||||
|
||||
@ -291,11 +294,19 @@ export class CodeApplication extends Disposable {
|
||||
}
|
||||
|
||||
// Handle slow shell environment resolve calls:
|
||||
// - a warning after 3s but continue to resolve
|
||||
// - an error after 10s and stop trying to resolve
|
||||
// - a warning after 3s but continue to resolve (only once in active window)
|
||||
// - an error after 10s and stop trying to resolve (in every window where this happens)
|
||||
const cts = new CancellationTokenSource();
|
||||
const shellEnvSlowWarningHandle = setTimeout(() => window?.sendWhenReady('vscode:showShellEnvSlowWarning', cts.token), 3000);
|
||||
const shellEnvTimeoutErrorHandle = setTimeout(function () {
|
||||
|
||||
const shellEnvSlowWarningHandle = setTimeout(() => {
|
||||
if (!slowShellResolveWarningShown) {
|
||||
this.windowsMainService?.sendToFocused('vscode:showShellEnvSlowWarning', cts.token);
|
||||
slowShellResolveWarningShown = true;
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
const window = this.windowsMainService?.getWindowByWebContents(event.sender); // Note: this can be `undefined` for the shared process!!
|
||||
const shellEnvTimeoutErrorHandle = setTimeout(() => {
|
||||
cts.dispose(true);
|
||||
window?.sendWhenReady('vscode:showShellEnvTimeoutError', CancellationToken.None);
|
||||
acceptShellEnv({});
|
||||
@ -304,8 +315,11 @@ export class CodeApplication extends Disposable {
|
||||
// Prefer to use the args and env from the target window
|
||||
// when resolving the shell env. It is possible that
|
||||
// a first window was opened from the UI but a second
|
||||
// from the CLI and that has implications for wether to
|
||||
// from the CLI and that has implications for whether to
|
||||
// resolve the shell environment or not.
|
||||
//
|
||||
// Window can be undefined for e.g. the shared process
|
||||
// that is not part of our windows registry!
|
||||
let args: NativeParsedArgs;
|
||||
let env: NodeJS.ProcessEnv;
|
||||
if (window?.config) {
|
||||
@ -321,10 +335,10 @@ export class CodeApplication extends Disposable {
|
||||
acceptShellEnv(shellEnv);
|
||||
});
|
||||
|
||||
ipcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => {
|
||||
ipcMain.handle('vscode:writeNlsFile', async (event, path: unknown, data: unknown) => {
|
||||
const uri = this.validateNlsPath([path]);
|
||||
if (!uri || typeof data !== 'string') {
|
||||
return Promise.reject('Invalid operation (vscode:writeNlsFile)');
|
||||
throw new Error('Invalid operation (vscode:writeNlsFile)');
|
||||
}
|
||||
|
||||
return this.fileService.writeFile(uri, VSBuffer.fromString(data));
|
||||
@ -333,7 +347,7 @@ export class CodeApplication extends Disposable {
|
||||
ipcMain.handle('vscode:readNlsFile', async (event, ...paths: unknown[]) => {
|
||||
const uri = this.validateNlsPath(paths);
|
||||
if (!uri) {
|
||||
return Promise.reject('Invalid operation (vscode:readNlsFile)');
|
||||
throw new Error('Invalid operation (vscode:readNlsFile)');
|
||||
}
|
||||
|
||||
return (await this.fileService.readFile(uri)).value.toString();
|
||||
@ -427,16 +441,20 @@ export class CodeApplication extends Disposable {
|
||||
|
||||
// Spawn shared process after the first window has opened and 3s have passed
|
||||
const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);
|
||||
const sharedProcessClient = sharedProcess.whenIpcReady().then(() => {
|
||||
this.logService.trace('Shared process: IPC ready');
|
||||
const sharedProcessClient = (async () => {
|
||||
this.logService.trace('Main->SharedProcess#connect');
|
||||
|
||||
return connect(this.environmentService.sharedIPCHandle, 'main');
|
||||
});
|
||||
const sharedProcessReady = sharedProcess.whenReady().then(() => {
|
||||
this.logService.trace('Shared process: init ready');
|
||||
const port = await sharedProcess.connect();
|
||||
|
||||
this.logService.trace('Main->SharedProcess#connect: connection established');
|
||||
|
||||
return new MessagePortClient(port, 'main');
|
||||
})();
|
||||
const sharedProcessReady = (async () => {
|
||||
await sharedProcess.whenReady();
|
||||
|
||||
return sharedProcessClient;
|
||||
});
|
||||
})();
|
||||
this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => {
|
||||
this._register(new RunOnceScheduler(async () => {
|
||||
sharedProcess.spawn(await resolveShellEnv(this.logService, this.environmentService.args, process.env));
|
||||
@ -454,12 +472,8 @@ export class CodeApplication extends Disposable {
|
||||
this._register(server);
|
||||
}
|
||||
|
||||
// Setup Auth Handler (TODO@ben remove old auth handler eventually)
|
||||
if (this.configurationService.getValue('window.enableExperimentalProxyLoginDialog') === false) {
|
||||
this._register(new ProxyAuthHandler());
|
||||
} else {
|
||||
this._register(appInstantiationService.createInstance(ProxyAuthHandler2));
|
||||
}
|
||||
// Setup Auth Handler
|
||||
this._register(appInstantiationService.createInstance(ProxyAuthHandler));
|
||||
|
||||
// Open Windows
|
||||
const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient, fileProtocolHandler));
|
||||
@ -487,7 +501,7 @@ export class CodeApplication extends Disposable {
|
||||
return machineId;
|
||||
}
|
||||
|
||||
private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise<Client<string>>): Promise<IInstantiationService> {
|
||||
private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise<MessagePortClient>): Promise<IInstantiationService> {
|
||||
const services = new ServiceCollection();
|
||||
|
||||
switch (process.platform) {
|
||||
@ -496,8 +510,8 @@ export class CodeApplication extends Disposable {
|
||||
break;
|
||||
|
||||
case 'linux':
|
||||
if (process.env.SNAP && process.env.SNAP_REVISION) {
|
||||
services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env.SNAP, process.env.SNAP_REVISION]));
|
||||
if (isLinuxSnap) {
|
||||
services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env['SNAP'], process.env['SNAP_REVISION']]));
|
||||
} else {
|
||||
services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService));
|
||||
}
|
||||
@ -510,7 +524,6 @@ export class CodeApplication extends Disposable {
|
||||
|
||||
services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, this.userEnv]));
|
||||
services.set(IDialogMainService, new SyncDescriptor(DialogMainService));
|
||||
services.set(ISharedProcessMainService, new SyncDescriptor(SharedProcessMainService, [sharedProcess]));
|
||||
services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService));
|
||||
services.set(IDiagnosticsService, createChannelSender(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics')))));
|
||||
|
||||
@ -518,9 +531,9 @@ export class CodeApplication extends Disposable {
|
||||
services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId]));
|
||||
services.set(IKeyboardLayoutMainService, new SyncDescriptor(KeyboardLayoutMainService));
|
||||
services.set(IDisplayMainService, new SyncDescriptor(DisplayMainService));
|
||||
services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService));
|
||||
services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService, [sharedProcess]));
|
||||
services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService));
|
||||
services.set(IWorkspacesService, new SyncDescriptor(WorkspacesService));
|
||||
services.set(IWorkspacesService, new SyncDescriptor(WorkspacesMainService));
|
||||
services.set(IMenubarMainService, new SyncDescriptor(MenubarMainService));
|
||||
services.set(IExtensionUrlTrustService, new SyncDescriptor(ExtensionUrlTrustService));
|
||||
|
||||
@ -533,13 +546,13 @@ export class CodeApplication extends Disposable {
|
||||
|
||||
services.set(IWorkspacesHistoryMainService, new SyncDescriptor(WorkspacesHistoryMainService));
|
||||
services.set(IURLService, new SyncDescriptor(NativeURLService));
|
||||
services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService));
|
||||
services.set(IWorkspacesManagementMainService, new SyncDescriptor(WorkspacesManagementMainService));
|
||||
|
||||
// Telemetry
|
||||
if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) {
|
||||
const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender')));
|
||||
const appender = new TelemetryAppenderClient(channel);
|
||||
const commonProperties = resolveCommonProperties(product.commit, product.version, machineId, product.msftInternalDomains, this.environmentService.installSourcePath);
|
||||
const commonProperties = resolveCommonProperties(this.fileService, release(), process.arch, product.commit, product.version, machineId, product.msftInternalDomains, this.environmentService.installSourcePath);
|
||||
const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath];
|
||||
const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths, sendErrorTelemetry: true };
|
||||
|
||||
@ -589,7 +602,7 @@ export class CodeApplication extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] {
|
||||
private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<MessagePortClient>, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] {
|
||||
|
||||
// Register more Main IPC services
|
||||
const launchMainService = accessor.get(ILaunchMainService);
|
||||
@ -622,10 +635,6 @@ export class CodeApplication extends Disposable {
|
||||
electronIpcServer.registerChannel('nativeHost', nativeHostChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('nativeHost', nativeHostChannel));
|
||||
|
||||
const sharedProcessMainService = accessor.get(ISharedProcessMainService);
|
||||
const sharedProcessChannel = createChannelReceiver(sharedProcessMainService);
|
||||
electronIpcServer.registerChannel('sharedProcess', sharedProcessChannel);
|
||||
|
||||
const workspacesService = accessor.get(IWorkspacesService);
|
||||
const workspacesChannel = createChannelReceiver(workspacesService);
|
||||
electronIpcServer.registerChannel('workspaces', workspacesChannel);
|
||||
@ -705,6 +714,8 @@ export class CodeApplication extends Disposable {
|
||||
});
|
||||
|
||||
// Create a URL handler to open file URIs in the active window
|
||||
// or open new windows. The URL handler will be invoked from
|
||||
// protocol invocations outside of VSCode.
|
||||
const app = this;
|
||||
const environmentService = this.environmentService;
|
||||
urlService.registerHandler({
|
||||
@ -718,13 +729,15 @@ export class CodeApplication extends Disposable {
|
||||
// Check for URIs to open in window
|
||||
const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri);
|
||||
if (windowOpenableFromProtocolLink) {
|
||||
windowsMainService.open({
|
||||
const [window] = windowsMainService.open({
|
||||
context: OpenContext.API,
|
||||
cli: { ...environmentService.args },
|
||||
urisToOpen: [windowOpenableFromProtocolLink],
|
||||
gotoLineMode: true
|
||||
});
|
||||
|
||||
window.focus(); // this should help ensuring that the right window gets focus when multiple are opened
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -832,7 +845,7 @@ export class CodeApplication extends Disposable {
|
||||
mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&No")),
|
||||
],
|
||||
cancelId: 1,
|
||||
message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath), product.nameShort),
|
||||
message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath, this.environmentService), product.nameShort),
|
||||
detail: localize('confirmOpenDetail', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'"),
|
||||
noLink: true
|
||||
});
|
||||
@ -901,8 +914,25 @@ export class CodeApplication extends Disposable {
|
||||
// Signal phase: after window open
|
||||
this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen;
|
||||
|
||||
// Windows: install mutex
|
||||
const win32MutexName = product.win32MutexName;
|
||||
if (isWindows && win32MutexName) {
|
||||
try {
|
||||
const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex;
|
||||
const mutex = new WindowsMutex(win32MutexName);
|
||||
once(this.lifecycleMainService.onWillShutdown)(() => mutex.release());
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Remote Authorities
|
||||
this.handleRemoteAuthorities();
|
||||
protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => {
|
||||
callback({
|
||||
url: request.url.replace(/^vscode-remote-resource:/, 'http:'),
|
||||
method: request.method
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize update service
|
||||
const updateService = accessor.get(IUpdateService);
|
||||
@ -934,19 +964,11 @@ export class CodeApplication extends Disposable {
|
||||
'}'
|
||||
];
|
||||
const newArgvString = argvString.substring(0, argvString.length - 2).concat(',\n', additionalArgvContent.join('\n'));
|
||||
|
||||
await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(newArgvString));
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleRemoteAuthorities(): void {
|
||||
protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => {
|
||||
callback({
|
||||
url: request.url.replace(/^vscode-remote-resource:/, 'http:'),
|
||||
method: request.method
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,18 +3,28 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, app, AuthInfo, WebContents, Event as ElectronEvent } from 'electron';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
||||
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
|
||||
import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails {
|
||||
firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70
|
||||
}
|
||||
|
||||
type LoginEvent = {
|
||||
event: ElectronEvent;
|
||||
webContents: WebContents;
|
||||
req: Request;
|
||||
authInfo: AuthInfo;
|
||||
cb: (username: string, password: string) => void;
|
||||
req: ElectronAuthenticationResponseDetails;
|
||||
|
||||
callback: (username?: string, password?: string) => void;
|
||||
};
|
||||
|
||||
type Credentials = {
|
||||
@ -22,81 +32,211 @@ type Credentials = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
enum ProxyAuthState {
|
||||
|
||||
/**
|
||||
* Initial state: we will try to use stored credentials
|
||||
* first to reply to the auth challenge.
|
||||
*/
|
||||
Initial = 1,
|
||||
|
||||
/**
|
||||
* We used stored credentials and are still challenged,
|
||||
* so we will show a login dialog next.
|
||||
*/
|
||||
StoredCredentialsUsed,
|
||||
|
||||
/**
|
||||
* Finally, if we showed a login dialog already, we will
|
||||
* not show any more login dialogs until restart to reduce
|
||||
* the UI noise.
|
||||
*/
|
||||
LoginDialogShown
|
||||
}
|
||||
|
||||
export class ProxyAuthHandler extends Disposable {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`;
|
||||
|
||||
private retryCount = 0;
|
||||
private pendingProxyResolve: Promise<Credentials | undefined> | undefined = undefined;
|
||||
|
||||
constructor() {
|
||||
private state = ProxyAuthState.Initial;
|
||||
|
||||
private sessionCredentials: Credentials | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
|
||||
@IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
const onLogin = Event.fromNodeEventEmitter<LoginEvent>(app, 'login', (event, webContents, req, authInfo, cb) => ({ event, webContents, req, authInfo, cb }));
|
||||
const onLogin = Event.fromNodeEventEmitter<LoginEvent>(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback }));
|
||||
this._register(onLogin(this.onLogin, this));
|
||||
}
|
||||
|
||||
private onLogin({ event, authInfo, cb }: LoginEvent): void {
|
||||
private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise<void> {
|
||||
if (!authInfo.isProxy) {
|
||||
return;
|
||||
return; // only for proxy
|
||||
}
|
||||
|
||||
if (this.retryCount++ > 1) {
|
||||
return;
|
||||
if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) {
|
||||
this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown');
|
||||
|
||||
return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem)
|
||||
}
|
||||
|
||||
// Signal we handle this event on our own, otherwise
|
||||
// Electron will ignore our provided credentials.
|
||||
event.preventDefault();
|
||||
|
||||
const opts: BrowserWindowConstructorOptions = {
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
resizable: false,
|
||||
width: 450,
|
||||
height: 225,
|
||||
show: true,
|
||||
title: 'VS Code',
|
||||
webPreferences: {
|
||||
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
|
||||
sandbox: true,
|
||||
contextIsolation: true,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
spellcheck: false,
|
||||
devTools: false
|
||||
}
|
||||
};
|
||||
let credentials: Credentials | undefined = undefined;
|
||||
if (!this.pendingProxyResolve) {
|
||||
this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new');
|
||||
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
opts.parent = focusedWindow;
|
||||
opts.modal = true;
|
||||
this.pendingProxyResolve = this.resolveProxyCredentials(authInfo);
|
||||
try {
|
||||
credentials = await this.pendingProxyResolve;
|
||||
} finally {
|
||||
this.pendingProxyResolve = undefined;
|
||||
}
|
||||
} else {
|
||||
this.logService.trace('auth#onLogin (proxy) - pending proxy handling found');
|
||||
|
||||
credentials = await this.pendingProxyResolve;
|
||||
}
|
||||
|
||||
const win = new BrowserWindow(opts);
|
||||
const windowUrl = FileAccess.asBrowserUri('vs/code/electron-sandbox/proxy/auth.html', require);
|
||||
const proxyUrl = `${authInfo.host}:${authInfo.port}`;
|
||||
const title = localize('authRequire', "Proxy Authentication Required");
|
||||
const message = localize('proxyauth', "The proxy {0} requires authentication.", proxyUrl);
|
||||
// According to Electron docs, it is fine to call back without
|
||||
// username or password to signal that the authentication was handled
|
||||
// by us, even though without having credentials received:
|
||||
//
|
||||
// > If `callback` is called without a username or password, the authentication
|
||||
// > request will be cancelled and the authentication error will be returned to the
|
||||
// > page.
|
||||
callback(credentials?.username, credentials?.password);
|
||||
}
|
||||
|
||||
const onWindowClose = () => cb('', '');
|
||||
win.on('close', onWindowClose);
|
||||
private async resolveProxyCredentials(authInfo: AuthInfo): Promise<Credentials | undefined> {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - enter');
|
||||
|
||||
win.setMenu(null);
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
const data = { title, message };
|
||||
win.webContents.send('vscode:openProxyAuthDialog', data);
|
||||
});
|
||||
win.webContents.on('ipc-message', (event, channel, credentials: Credentials) => {
|
||||
if (channel === 'vscode:proxyAuthResponse') {
|
||||
const { username, password } = credentials;
|
||||
cb(username, password);
|
||||
win.removeListener('close', onWindowClose);
|
||||
win.close();
|
||||
try {
|
||||
const credentials = await this.doResolveProxyCredentials(authInfo);
|
||||
if (credentials) {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials');
|
||||
|
||||
return credentials;
|
||||
} else {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials');
|
||||
}
|
||||
} finally {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - exit');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async doResolveProxyCredentials(authInfo: AuthInfo): Promise<Credentials | undefined> {
|
||||
this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo);
|
||||
|
||||
// Compute a hash over the authentication info to be used
|
||||
// with the credentials store to return the right credentials
|
||||
// given the properties of the auth request
|
||||
// (see https://github.com/microsoft/vscode/issues/109497)
|
||||
const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port }));
|
||||
|
||||
// Find any previously stored credentials
|
||||
let storedUsername: string | undefined = undefined;
|
||||
let storedPassword: string | undefined = undefined;
|
||||
try {
|
||||
const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash);
|
||||
if (encryptedSerializedProxyCredentials) {
|
||||
const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials));
|
||||
|
||||
storedUsername = credentials.username;
|
||||
storedPassword = credentials.password;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error); // handle errors by asking user for login via dialog
|
||||
}
|
||||
|
||||
// Reply with stored credentials unless we used them already.
|
||||
// In that case we need to show a login dialog again because
|
||||
// they seem invalid.
|
||||
if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') {
|
||||
this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use');
|
||||
this.state = ProxyAuthState.StoredCredentialsUsed;
|
||||
|
||||
return { username: storedUsername, password: storedPassword };
|
||||
}
|
||||
|
||||
// Find suitable window to show dialog: prefer to show it in the
|
||||
// active window because any other network request will wait on
|
||||
// the credentials and we want the user to present the dialog.
|
||||
const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow();
|
||||
if (!window) {
|
||||
this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in');
|
||||
|
||||
return undefined; // unexpected
|
||||
}
|
||||
|
||||
this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`);
|
||||
|
||||
// Open proxy dialog
|
||||
const payload = {
|
||||
authInfo,
|
||||
username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored
|
||||
password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored
|
||||
replyChannel: `vscode:proxyAuthResponse:${generateUuid()}`
|
||||
};
|
||||
window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload);
|
||||
this.state = ProxyAuthState.LoginDialogShown;
|
||||
|
||||
// Handle reply
|
||||
const loginDialogCredentials = await new Promise<Credentials | undefined>(resolve => {
|
||||
const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => {
|
||||
if (channel === payload.replyChannel) {
|
||||
this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`);
|
||||
window.win.webContents.off('ipc-message', proxyAuthResponseHandler);
|
||||
|
||||
// We got credentials from the window
|
||||
if (reply) {
|
||||
const credentials: Credentials = { username: reply.username, password: reply.password };
|
||||
|
||||
// Update stored credentials based on `remember` flag
|
||||
try {
|
||||
if (reply.remember) {
|
||||
const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials));
|
||||
await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials);
|
||||
} else {
|
||||
await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error); // handle gracefully
|
||||
}
|
||||
|
||||
resolve({ username: credentials.username, password: credentials.password });
|
||||
}
|
||||
|
||||
// We did not get any credentials from the window (e.g. cancelled)
|
||||
else {
|
||||
resolve(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.win.webContents.on('ipc-message', proxyAuthResponseHandler);
|
||||
});
|
||||
win.loadURL(windowUrl.toString(true));
|
||||
|
||||
// Remember credentials for the session in case
|
||||
// the credentials are wrong and we show the dialog
|
||||
// again
|
||||
this.sessionCredentials = loginDialogCredentials;
|
||||
|
||||
return loginDialogCredentials;
|
||||
}
|
||||
}
|
||||
|
@ -1,242 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Event } from 'vs/base/common/event';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
||||
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
|
||||
import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails {
|
||||
firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70
|
||||
}
|
||||
|
||||
type LoginEvent = {
|
||||
event: ElectronEvent;
|
||||
authInfo: AuthInfo;
|
||||
req: ElectronAuthenticationResponseDetails;
|
||||
|
||||
callback: (username?: string, password?: string) => void;
|
||||
};
|
||||
|
||||
type Credentials = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
enum ProxyAuthState {
|
||||
|
||||
/**
|
||||
* Initial state: we will try to use stored credentials
|
||||
* first to reply to the auth challenge.
|
||||
*/
|
||||
Initial = 1,
|
||||
|
||||
/**
|
||||
* We used stored credentials and are still challenged,
|
||||
* so we will show a login dialog next.
|
||||
*/
|
||||
StoredCredentialsUsed,
|
||||
|
||||
/**
|
||||
* Finally, if we showed a login dialog already, we will
|
||||
* not show any more login dialogs until restart to reduce
|
||||
* the UI noise.
|
||||
*/
|
||||
LoginDialogShown
|
||||
}
|
||||
|
||||
export class ProxyAuthHandler2 extends Disposable {
|
||||
|
||||
private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`;
|
||||
|
||||
private pendingProxyResolve: Promise<Credentials | undefined> | undefined = undefined;
|
||||
|
||||
private state = ProxyAuthState.Initial;
|
||||
|
||||
private sessionCredentials: Credentials | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
|
||||
@IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
const onLogin = Event.fromNodeEventEmitter<LoginEvent>(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback }));
|
||||
this._register(onLogin(this.onLogin, this));
|
||||
}
|
||||
|
||||
private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise<void> {
|
||||
if (!authInfo.isProxy) {
|
||||
return; // only for proxy
|
||||
}
|
||||
|
||||
if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) {
|
||||
this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown');
|
||||
|
||||
return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem)
|
||||
}
|
||||
|
||||
// Signal we handle this event on our own, otherwise
|
||||
// Electron will ignore our provided credentials.
|
||||
event.preventDefault();
|
||||
|
||||
let credentials: Credentials | undefined = undefined;
|
||||
if (!this.pendingProxyResolve) {
|
||||
this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new');
|
||||
|
||||
this.pendingProxyResolve = this.resolveProxyCredentials(authInfo);
|
||||
try {
|
||||
credentials = await this.pendingProxyResolve;
|
||||
} finally {
|
||||
this.pendingProxyResolve = undefined;
|
||||
}
|
||||
} else {
|
||||
this.logService.trace('auth#onLogin (proxy) - pending proxy handling found');
|
||||
|
||||
credentials = await this.pendingProxyResolve;
|
||||
}
|
||||
|
||||
// According to Electron docs, it is fine to call back without
|
||||
// username or password to signal that the authentication was handled
|
||||
// by us, even though without having credentials received:
|
||||
//
|
||||
// > If `callback` is called without a username or password, the authentication
|
||||
// > request will be cancelled and the authentication error will be returned to the
|
||||
// > page.
|
||||
callback(credentials?.username, credentials?.password);
|
||||
}
|
||||
|
||||
private async resolveProxyCredentials(authInfo: AuthInfo): Promise<Credentials | undefined> {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - enter');
|
||||
|
||||
try {
|
||||
const credentials = await this.doResolveProxyCredentials(authInfo);
|
||||
if (credentials) {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials');
|
||||
|
||||
return credentials;
|
||||
} else {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials');
|
||||
}
|
||||
} finally {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - exit');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async doResolveProxyCredentials(authInfo: AuthInfo): Promise<Credentials | undefined> {
|
||||
this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo);
|
||||
|
||||
// Compute a hash over the authentication info to be used
|
||||
// with the credentials store to return the right credentials
|
||||
// given the properties of the auth request
|
||||
// (see https://github.com/microsoft/vscode/issues/109497)
|
||||
const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port }));
|
||||
|
||||
// Find any previously stored credentials
|
||||
let storedUsername: string | undefined = undefined;
|
||||
let storedPassword: string | undefined = undefined;
|
||||
try {
|
||||
const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash);
|
||||
if (encryptedSerializedProxyCredentials) {
|
||||
const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials));
|
||||
|
||||
storedUsername = credentials.username;
|
||||
storedPassword = credentials.password;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error); // handle errors by asking user for login via dialog
|
||||
}
|
||||
|
||||
// Reply with stored credentials unless we used them already.
|
||||
// In that case we need to show a login dialog again because
|
||||
// they seem invalid.
|
||||
if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') {
|
||||
this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use');
|
||||
this.state = ProxyAuthState.StoredCredentialsUsed;
|
||||
|
||||
return { username: storedUsername, password: storedPassword };
|
||||
}
|
||||
|
||||
// Find suitable window to show dialog: prefer to show it in the
|
||||
// active window because any other network request will wait on
|
||||
// the credentials and we want the user to present the dialog.
|
||||
const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow();
|
||||
if (!window) {
|
||||
this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in');
|
||||
|
||||
return undefined; // unexpected
|
||||
}
|
||||
|
||||
this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`);
|
||||
|
||||
// Open proxy dialog
|
||||
const payload = {
|
||||
authInfo,
|
||||
username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored
|
||||
password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored
|
||||
replyChannel: `vscode:proxyAuthResponse:${generateUuid()}`
|
||||
};
|
||||
window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload);
|
||||
this.state = ProxyAuthState.LoginDialogShown;
|
||||
|
||||
// Handle reply
|
||||
const loginDialogCredentials = await new Promise<Credentials | undefined>(resolve => {
|
||||
const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => {
|
||||
if (channel === payload.replyChannel) {
|
||||
this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`);
|
||||
window.win.webContents.off('ipc-message', proxyAuthResponseHandler);
|
||||
|
||||
// We got credentials from the window
|
||||
if (reply) {
|
||||
const credentials: Credentials = { username: reply.username, password: reply.password };
|
||||
|
||||
// Update stored credentials based on `remember` flag
|
||||
try {
|
||||
if (reply.remember) {
|
||||
const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials));
|
||||
await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials);
|
||||
} else {
|
||||
await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error); // handle gracefully
|
||||
}
|
||||
|
||||
resolve({ username: credentials.username, password: credentials.password });
|
||||
}
|
||||
|
||||
// We did not get any credentials from the window (e.g. cancelled)
|
||||
else {
|
||||
resolve(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.win.webContents.on('ipc-message', proxyAuthResponseHandler);
|
||||
});
|
||||
|
||||
// Remember credentials for the session in case
|
||||
// the credentials are wrong and we show the dialog
|
||||
// again
|
||||
this.sessionCredentials = loginDialogCredentials;
|
||||
|
||||
return loginDialogCredentials;
|
||||
}
|
||||
}
|
@ -5,15 +5,17 @@
|
||||
|
||||
import 'vs/platform/update/common/update.config.contribution';
|
||||
import { app, dialog } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import { unlinkSync } from 'fs';
|
||||
import { localize } from 'vs/nls';
|
||||
import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper';
|
||||
import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile';
|
||||
import { mkdirp } from 'vs/base/node/pfs';
|
||||
import { LifecycleMainService, ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { Server, serve, connect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Server as NodeIPCServer, serve as nodeIPCServe, connect as nodeIPCConnect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Client as NodeIPCClient } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
|
||||
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
|
||||
@ -29,17 +31,15 @@ import { ConfigurationService } from 'vs/platform/configuration/common/configura
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService';
|
||||
import { CodeApplication } from 'vs/code/electron-main/app';
|
||||
import { localize } from 'vs/nls';
|
||||
import { mnemonicButtonLabel } from 'vs/base/common/labels';
|
||||
import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels';
|
||||
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
|
||||
import { BufferLogService } from 'vs/platform/log/common/bufferLog';
|
||||
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
|
||||
import { IThemeMainService, ThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
|
||||
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
import { SignService } from 'vs/platform/sign/node/signService';
|
||||
import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
|
||||
import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
@ -53,6 +53,8 @@ import { rtrim, trim } from 'vs/base/common/strings';
|
||||
import { basename, resolve } from 'vs/base/common/path';
|
||||
import { coalesce, distinct } from 'vs/base/common/arrays';
|
||||
import { EnvironmentMainService, IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
|
||||
class ExpectedError extends Error {
|
||||
readonly isExpected = true;
|
||||
@ -212,14 +214,14 @@ class CodeMain {
|
||||
return instanceEnvironment;
|
||||
}
|
||||
|
||||
private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise<Server> {
|
||||
private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise<NodeIPCServer> {
|
||||
|
||||
// Try to setup a server for running. If that succeeds it means
|
||||
// we are the first instance to startup. Otherwise it is likely
|
||||
// that another instance is already running.
|
||||
let server: Server;
|
||||
let server: NodeIPCServer;
|
||||
try {
|
||||
server = await serve(environmentService.mainIPCHandle);
|
||||
server = await nodeIPCServe(environmentService.mainIPCHandle);
|
||||
once(lifecycleMainService.onWillShutdown)(() => server.dispose());
|
||||
} catch (error) {
|
||||
|
||||
@ -235,9 +237,9 @@ class CodeMain {
|
||||
}
|
||||
|
||||
// there's a running instance, let's connect to it
|
||||
let client: Client<string>;
|
||||
let client: NodeIPCClient<string>;
|
||||
try {
|
||||
client = await connect(environmentService.mainIPCHandle, 'main');
|
||||
client = await nodeIPCConnect(environmentService.mainIPCHandle, 'main');
|
||||
} catch (error) {
|
||||
|
||||
// Handle unexpected connection errors by showing a dialog to the user
|
||||
@ -256,7 +258,7 @@ class CodeMain {
|
||||
// let's delete it, since we can't connect to it and then
|
||||
// retry the whole thing
|
||||
try {
|
||||
fs.unlinkSync(environmentService.mainIPCHandle);
|
||||
unlinkSync(environmentService.mainIPCHandle);
|
||||
} catch (error) {
|
||||
logService.warn('Could not delete obsolete instance handle', error);
|
||||
|
||||
@ -293,11 +295,7 @@ class CodeMain {
|
||||
// Process Info
|
||||
if (args.status) {
|
||||
return instantiationService.invokeFunction(async () => {
|
||||
|
||||
// Create a diagnostic service connected to the existing shared process
|
||||
const sharedProcessClient = await connect(environmentService.sharedIPCHandle, 'main');
|
||||
const diagnosticsChannel = sharedProcessClient.getChannel('diagnostics');
|
||||
const diagnosticsService = createChannelSender<IDiagnosticsService>(diagnosticsChannel);
|
||||
const diagnosticsService = new DiagnosticsService(NullTelemetryService);
|
||||
const mainProcessInfo = await launchService.getMainProcessInfo();
|
||||
const remoteDiagnostics = await launchService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true });
|
||||
const diagnostics = await diagnosticsService.getDiagnostics(mainProcessInfo, remoteDiagnostics);
|
||||
@ -343,11 +341,11 @@ class CodeMain {
|
||||
|
||||
private handleStartupDataDirError(environmentService: IEnvironmentMainService, error: NodeJS.ErrnoException): void {
|
||||
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
||||
const directories = coalesce([environmentService.userDataPath, environmentService.extensionsPath, XDG_RUNTIME_DIR]);
|
||||
const directories = coalesce([environmentService.userDataPath, environmentService.extensionsPath, XDG_RUNTIME_DIR]).map(folder => getPathLabel(folder, environmentService));
|
||||
|
||||
this.showStartupWarningDialog(
|
||||
localize('startupDataDirError', "Unable to write program user data."),
|
||||
localize('startupUserDataAndExtensionsDirErrorDetail', "Please make sure the following directories are writeable:\n\n{0}", directories.join('\n'))
|
||||
localize('startupUserDataAndExtensionsDirErrorDetail', "{0}\n\nPlease make sure the following directories are writeable:\n\n{1}", toErrorMessage(error), directories.join('\n'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro
|
||||
import { session } from 'electron';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { TernarySearchTree } from 'vs/base/common/map';
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
import { isLinux, isPreferringBrowserCodeLoad } from 'vs/base/common/platform';
|
||||
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
||||
|
||||
type ProtocolCallback = { (result: string | Electron.FilePathWithHeaders | { error: number }): void };
|
||||
@ -37,15 +37,17 @@ export class FileProtocolHandler extends Disposable {
|
||||
// Register vscode-file:// handler
|
||||
defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback as unknown as ProtocolCallback));
|
||||
|
||||
// Block any file:// access (sandbox only)
|
||||
if (environmentService.args.__sandbox) {
|
||||
// Block any file:// access (explicitly enabled only)
|
||||
if (isPreferringBrowserCodeLoad) {
|
||||
this.logService.info(`Intercepting ${Schemas.file}: protocol and blocking it`);
|
||||
|
||||
defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback as unknown as ProtocolCallback));
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
this._register(toDisposable(() => {
|
||||
defaultSession.protocol.unregisterProtocol(Schemas.vscodeFileResource);
|
||||
if (environmentService.args.__sandbox) {
|
||||
if (isPreferringBrowserCodeLoad) {
|
||||
defaultSession.protocol.uninterceptProtocol(Schemas.file);
|
||||
}
|
||||
}));
|
||||
|
@ -3,25 +3,25 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { BrowserWindow, ipcMain, Event, MessagePortMain } from 'electron';
|
||||
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
import { BrowserWindow, ipcMain, WebContents, Event as ElectronEvent } from 'electron';
|
||||
import { ISharedProcess } from 'vs/platform/ipc/electron-main/sharedProcessMainService';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
|
||||
import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { browserCodeLoadingCacheStrategy } from 'vs/base/common/platform';
|
||||
import { ISharedProcess, ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/ipc.mp';
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
|
||||
export class SharedProcess implements ISharedProcess {
|
||||
export class SharedProcess extends Disposable implements ISharedProcess {
|
||||
|
||||
private barrier = new Barrier();
|
||||
private readonly whenSpawnedBarrier = new Barrier();
|
||||
|
||||
private window: BrowserWindow | null = null;
|
||||
|
||||
private readonly _whenReady: Promise<void>;
|
||||
private window: BrowserWindow | undefined = undefined;
|
||||
private windowCloseListener: ((event: Event) => void) | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
private readonly machineId: string,
|
||||
@ -31,17 +31,125 @@ export class SharedProcess implements ISharedProcess {
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IThemeMainService private readonly themeMainService: IThemeMainService
|
||||
) {
|
||||
// overall ready promise when shared process signals initialization is done
|
||||
this._whenReady = new Promise<void>(c => ipcMain.once('vscode:shared-process->electron-main=init-done', () => c(undefined)));
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get _whenIpcReady(): Promise<void> {
|
||||
private registerListeners(): void {
|
||||
|
||||
// Lifecycle
|
||||
this._register(this.lifecycleMainService.onWillShutdown(() => this.onWillShutdown()));
|
||||
|
||||
// Shared process connections from workbench windows
|
||||
ipcMain.on('vscode:createSharedProcessMessageChannel', async (e, nonce: string) => {
|
||||
this.logService.trace('SharedProcess: on vscode:createSharedProcessMessageChannel');
|
||||
|
||||
// await the shared process to be overall ready
|
||||
// we do not just wait for IPC ready because the
|
||||
// workbench window will communicate directly
|
||||
await this.whenReady();
|
||||
|
||||
// connect to the shared process window
|
||||
const port = await this.connect();
|
||||
|
||||
// Check back if the requesting window meanwhile closed
|
||||
// Since shared process is delayed on startup there is
|
||||
// a chance that the window close before the shared process
|
||||
// was ready for a connection.
|
||||
if (e.sender.isDestroyed()) {
|
||||
return port.close();
|
||||
}
|
||||
|
||||
// send the port back to the requesting window
|
||||
e.sender.postMessage('vscode:createSharedProcessMessageChannelResult', nonce, [port]);
|
||||
});
|
||||
}
|
||||
|
||||
private onWillShutdown(): void {
|
||||
const window = this.window;
|
||||
if (!window) {
|
||||
return; // possibly too early before created
|
||||
}
|
||||
|
||||
// Signal exit to shared process when shutting down
|
||||
if (!window.isDestroyed() && !window.webContents.isDestroyed()) {
|
||||
window.webContents.send('vscode:electron-main->shared-process=exit');
|
||||
}
|
||||
|
||||
// Shut the shared process down when we are quitting
|
||||
//
|
||||
// Note: because we veto the window close, we must first remove our veto.
|
||||
// Otherwise the application would never quit because the shared process
|
||||
// window is refusing to close!
|
||||
//
|
||||
if (this.windowCloseListener) {
|
||||
window.removeListener('close', this.windowCloseListener);
|
||||
this.windowCloseListener = undefined;
|
||||
}
|
||||
|
||||
// Electron seems to crash on Windows without this setTimeout :|
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.close();
|
||||
} catch (err) {
|
||||
// ignore, as electron is already shutting down
|
||||
}
|
||||
|
||||
this.window = undefined;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private _whenReady: Promise<void> | undefined = undefined;
|
||||
whenReady(): Promise<void> {
|
||||
if (!this._whenReady) {
|
||||
// Overall signal that the shared process window was loaded and
|
||||
// all services within have been created.
|
||||
this._whenReady = new Promise<void>(resolve => ipcMain.once('vscode:shared-process->electron-main=init-done', () => {
|
||||
this.logService.trace('SharedProcess: Overall ready');
|
||||
|
||||
resolve();
|
||||
}));
|
||||
}
|
||||
|
||||
return this._whenReady;
|
||||
}
|
||||
|
||||
private _whenIpcReady: Promise<void> | undefined = undefined;
|
||||
private get whenIpcReady() {
|
||||
if (!this._whenIpcReady) {
|
||||
this._whenIpcReady = (async () => {
|
||||
|
||||
// Always wait for `spawn()`
|
||||
await this.whenSpawnedBarrier.wait();
|
||||
|
||||
// Create window for shared process
|
||||
this.createWindow();
|
||||
|
||||
// Listeners
|
||||
this.registerWindowListeners();
|
||||
|
||||
// Wait for window indicating that IPC connections are accepted
|
||||
await new Promise<void>(resolve => ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => {
|
||||
this.logService.trace('SharedProcess: IPC ready');
|
||||
|
||||
resolve();
|
||||
}));
|
||||
})();
|
||||
}
|
||||
|
||||
return this._whenIpcReady;
|
||||
}
|
||||
|
||||
private createWindow(): void {
|
||||
|
||||
// shared process is a hidden window by default
|
||||
this.window = new BrowserWindow({
|
||||
show: false,
|
||||
backgroundColor: this.themeMainService.getBackgroundColor(),
|
||||
webPreferences: {
|
||||
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
|
||||
v8CacheOptions: browserCodeLoadingCacheStrategy,
|
||||
nodeIntegration: true,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
@ -52,118 +160,85 @@ export class SharedProcess implements ISharedProcess {
|
||||
disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer
|
||||
}
|
||||
});
|
||||
const config = {
|
||||
appRoot: this.environmentService.appRoot,
|
||||
|
||||
const config: ISharedProcessConfiguration = {
|
||||
machineId: this.machineId,
|
||||
windowId: this.window.id,
|
||||
appRoot: this.environmentService.appRoot,
|
||||
nodeCachedDataDir: this.environmentService.nodeCachedDataDir,
|
||||
backupWorkspacesPath: this.environmentService.backupWorkspacesPath,
|
||||
userEnv: this.userEnv,
|
||||
windowId: this.window.id
|
||||
sharedIPCHandle: this.environmentService.sharedIPCHandle,
|
||||
args: this.environmentService.args,
|
||||
logLevel: this.logService.getLevel()
|
||||
};
|
||||
|
||||
const windowUrl = (this.environmentService.sandbox ?
|
||||
FileAccess._asCodeFileUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require) :
|
||||
FileAccess.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require))
|
||||
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` });
|
||||
this.window.loadURL(windowUrl.toString(true));
|
||||
// Load with config
|
||||
this.window.loadURL(FileAccess
|
||||
.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require)
|
||||
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` })
|
||||
.toString(true)
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent the window from dying
|
||||
const onClose = (e: ElectronEvent) => {
|
||||
private registerWindowListeners(): void {
|
||||
if (!this.window) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent the window from closing
|
||||
this.windowCloseListener = (e: Event) => {
|
||||
this.logService.trace('SharedProcess#close prevented');
|
||||
|
||||
// We never allow to close the shared process unless we get explicitly disposed()
|
||||
e.preventDefault();
|
||||
|
||||
// Still hide the window though if visible
|
||||
if (this.window && this.window.isVisible()) {
|
||||
if (this.window?.isVisible()) {
|
||||
this.window.hide();
|
||||
}
|
||||
};
|
||||
|
||||
this.window.on('close', onClose);
|
||||
this.window.on('close', this.windowCloseListener);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
this.lifecycleMainService.onWillShutdown(() => {
|
||||
disposables.dispose();
|
||||
|
||||
// Shut the shared process down when we are quitting
|
||||
//
|
||||
// Note: because we veto the window close, we must first remove our veto.
|
||||
// Otherwise the application would never quit because the shared process
|
||||
// window is refusing to close!
|
||||
//
|
||||
if (this.window) {
|
||||
this.window.removeListener('close', onClose);
|
||||
}
|
||||
|
||||
// Electron seems to crash on Windows without this setTimeout :|
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (this.window) {
|
||||
this.window.close();
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore, as electron is already shutting down
|
||||
}
|
||||
|
||||
this.window = null;
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return new Promise<void>(c => {
|
||||
// send payload once shared process is ready to receive it
|
||||
disposables.add(Event.once(Event.fromNodeEventEmitter(ipcMain, 'vscode:shared-process->electron-main=ready-for-payload', ({ sender }: { sender: WebContents }) => sender))(sender => {
|
||||
sender.send('vscode:electron-main->shared-process=payload', {
|
||||
sharedIPCHandle: this.environmentService.sharedIPCHandle,
|
||||
args: this.environmentService.args,
|
||||
logLevel: this.logService.getLevel(),
|
||||
backupWorkspacesPath: this.environmentService.backupWorkspacesPath,
|
||||
nodeCachedDataDir: this.environmentService.nodeCachedDataDir
|
||||
});
|
||||
|
||||
// signal exit to shared process when we get disposed
|
||||
disposables.add(toDisposable(() => sender.send('vscode:electron-main->shared-process=exit')));
|
||||
|
||||
// complete IPC-ready promise when shared process signals this to us
|
||||
ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => c(undefined));
|
||||
}));
|
||||
});
|
||||
// Crashes & Unrsponsive & Failed to load
|
||||
this.window.webContents.on('render-process-gone', (event, details) => this.logService.error(`SharedProcess: crashed (detail: ${details?.reason})`));
|
||||
this.window.on('unresponsive', () => this.logService.error('SharedProcess: detected unresponsive window'));
|
||||
this.window.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('SharedProcess: failed to load window, ', errorDescription));
|
||||
}
|
||||
|
||||
spawn(userEnv: NodeJS.ProcessEnv): void {
|
||||
this.userEnv = { ...this.userEnv, ...userEnv };
|
||||
this.barrier.open();
|
||||
|
||||
// Release barrier
|
||||
this.whenSpawnedBarrier.open();
|
||||
}
|
||||
|
||||
async whenReady(): Promise<void> {
|
||||
await this.barrier.wait();
|
||||
await this._whenReady;
|
||||
async connect(): Promise<MessagePortMain> {
|
||||
|
||||
// Wait for shared process being ready to accept connection
|
||||
await this.whenIpcReady;
|
||||
|
||||
// Connect and return message port
|
||||
const window = assertIsDefined(this.window);
|
||||
return connectMessagePort(window);
|
||||
}
|
||||
|
||||
async whenIpcReady(): Promise<void> {
|
||||
await this.barrier.wait();
|
||||
await this._whenIpcReady;
|
||||
}
|
||||
async toggle(): Promise<void> {
|
||||
|
||||
toggle(): void {
|
||||
if (!this.window || this.window.isVisible()) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
// wait for window to be created
|
||||
await this.whenIpcReady;
|
||||
|
||||
if (!this.window) {
|
||||
return; // possibly disposed already
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
if (this.window) {
|
||||
if (this.window.isVisible()) {
|
||||
this.window.webContents.closeDevTools();
|
||||
this.window.hide();
|
||||
} else {
|
||||
this.window.show();
|
||||
this.window.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
if (this.window) {
|
||||
this.window.webContents.closeDevTools();
|
||||
this.window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,13 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as perf from 'vs/base/common/performance';
|
||||
import { release } from 'os';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { localize } from 'vs/nls';
|
||||
import { getMarks, mark } from 'vs/base/common/performance';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, Details } from 'electron';
|
||||
import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, RenderProcessGoneDetails } from 'electron';
|
||||
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@ -18,17 +18,17 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { WindowMinimumSize, IWindowSettings, MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, zoomLevelToZoomFactor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
import { browserCodeLoadingCacheStrategy, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
|
||||
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
|
||||
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
|
||||
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
|
||||
import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService';
|
||||
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
|
||||
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
|
||||
import { mnemonicButtonLabel } from 'vs/base/common/labels';
|
||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
@ -102,32 +102,32 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
|
||||
private hiddenTitleBarStyle: boolean | undefined;
|
||||
private showTimeoutHandle: NodeJS.Timeout | undefined;
|
||||
private _lastFocusTime: number;
|
||||
private _readyState: ReadyState;
|
||||
private _lastFocusTime = -1;
|
||||
private _readyState = ReadyState.NONE;
|
||||
private windowState: IWindowState;
|
||||
private currentMenuBarVisibility: MenuBarVisibility | undefined;
|
||||
|
||||
private representedFilename: string | undefined;
|
||||
private documentEdited: boolean | undefined;
|
||||
|
||||
private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[];
|
||||
private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[] = [];
|
||||
|
||||
private marketplaceHeadersPromise: Promise<object>;
|
||||
|
||||
private readonly touchBarGroups: TouchBarSegmentedControl[];
|
||||
private readonly touchBarGroups: TouchBarSegmentedControl[] = [];
|
||||
|
||||
private currentHttpProxy?: string;
|
||||
private currentNoProxy?: string;
|
||||
private currentHttpProxy: string | undefined = undefined;
|
||||
private currentNoProxy: string | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
config: IWindowCreationOptions,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IStorageMainService private readonly storageService: IStorageMainService,
|
||||
@IStorageMainService storageService: IStorageMainService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IThemeMainService private readonly themeMainService: IThemeMainService,
|
||||
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
|
||||
@IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService,
|
||||
@IBackupMainService private readonly backupMainService: IBackupMainService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IDialogMainService private readonly dialogMainService: IDialogMainService,
|
||||
@ -135,11 +135,6 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
) {
|
||||
super();
|
||||
|
||||
this.touchBarGroups = [];
|
||||
this._lastFocusTime = -1;
|
||||
this._readyState = ReadyState.NONE;
|
||||
this.whenReadyCallbacks = [];
|
||||
|
||||
//#region create browser window
|
||||
{
|
||||
// Load window state
|
||||
@ -164,6 +159,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
title: product.nameLong,
|
||||
webPreferences: {
|
||||
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
|
||||
v8CacheOptions: browserCodeLoadingCacheStrategy,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
spellcheck: false,
|
||||
@ -185,13 +181,17 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
}
|
||||
};
|
||||
|
||||
if (browserCodeLoadingCacheStrategy) {
|
||||
this.logService.info(`window#ctor: using vscode-file protocol and V8 cache options: ${browserCodeLoadingCacheStrategy}`);
|
||||
}
|
||||
|
||||
// Apply icon to window
|
||||
// Linux: always
|
||||
// Windows: only when running out of sources, otherwise an icon is set by us on the executable
|
||||
if (isLinux) {
|
||||
options.icon = path.join(this.environmentService.appRoot, 'resources/linux/code.png');
|
||||
options.icon = join(this.environmentService.appRoot, 'resources/linux/code.png');
|
||||
} else if (isWindows && !this.environmentService.isBuilt) {
|
||||
options.icon = path.join(this.environmentService.appRoot, 'resources/win32/code_150x150.png');
|
||||
options.icon = join(this.environmentService.appRoot, 'resources/win32/code_150x150.png');
|
||||
}
|
||||
|
||||
if (isMacintosh && !this.useNativeFullScreen()) {
|
||||
@ -233,7 +233,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any
|
||||
}
|
||||
|
||||
// TODO@Ben (Electron 4 regression): when running on multiple displays where the target display
|
||||
// TODO@bpasero (Electron 4 regression): when running on multiple displays where the target display
|
||||
// to open the window has a larger resolution than the primary display, the window will not size
|
||||
// correctly unless we set the bounds again (https://github.com/microsoft/vscode/issues/74872)
|
||||
//
|
||||
@ -275,10 +275,9 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
this.createTouchBar();
|
||||
|
||||
// Request handling
|
||||
const that = this;
|
||||
this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentService, this.fileService, {
|
||||
get(key) { return that.storageService.get(key); },
|
||||
store(key, value) { that.storageService.store(key, value); }
|
||||
get(key) { return storageService.get(key); },
|
||||
store(key, value) { storageService.store(key, value); }
|
||||
});
|
||||
|
||||
// Eventing
|
||||
@ -361,13 +360,11 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
|
||||
get lastFocusTime(): number { return this._lastFocusTime; }
|
||||
|
||||
get backupPath(): string | undefined { return this.currentConfig ? this.currentConfig.backupPath : undefined; }
|
||||
get backupPath(): string | undefined { return this.currentConfig?.backupPath; }
|
||||
|
||||
get openedWorkspace(): IWorkspaceIdentifier | undefined { return this.currentConfig ? this.currentConfig.workspace : undefined; }
|
||||
get openedWorkspace(): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { return this.currentConfig?.workspace; }
|
||||
|
||||
get openedFolderUri(): URI | undefined { return this.currentConfig ? this.currentConfig.folderUri : undefined; }
|
||||
|
||||
get remoteAuthority(): string | undefined { return this.currentConfig ? this.currentConfig.remoteAuthority : undefined; }
|
||||
get remoteAuthority(): string | undefined { return this.currentConfig?.remoteAuthority; }
|
||||
|
||||
setReady(): void {
|
||||
this._readyState = ReadyState.READY;
|
||||
@ -413,9 +410,10 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Crashes & Unrsponsive
|
||||
// Crashes & Unrsponsive & Failed to load
|
||||
this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(WindowError.CRASHED, details));
|
||||
this._win.on('unresponsive', () => this.onWindowError(WindowError.UNRESPONSIVE));
|
||||
this._win.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('Main: failed to load workbench window, ', errorDescription));
|
||||
|
||||
// Window close
|
||||
this._win.on('closed', () => {
|
||||
@ -424,24 +422,42 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
this.dispose();
|
||||
});
|
||||
|
||||
// Prevent loading of svgs
|
||||
this._win.webContents.session.webRequest.onBeforeRequest(null!, (details, callback) => {
|
||||
if (details.url.indexOf('.svg') > 0) {
|
||||
const uri = URI.parse(details.url);
|
||||
if (uri && !uri.scheme.match(/file/i) && uri.path.endsWith('.svg')) {
|
||||
const svgFileSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']);
|
||||
this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => {
|
||||
const uri = URI.parse(details.url);
|
||||
// Prevent loading of remote svgs
|
||||
if (uri && uri.path.endsWith('.svg')) {
|
||||
const safeScheme = svgFileSchemes.has(uri.scheme) ||
|
||||
uri.path.includes(Schemas.vscodeRemoteResource);
|
||||
if (!safeScheme) {
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
}
|
||||
|
||||
return callback({});
|
||||
return callback({ cancel: false });
|
||||
});
|
||||
|
||||
this._win.webContents.session.webRequest.onHeadersReceived(null!, (details, callback) => {
|
||||
this._win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
const responseHeaders = details.responseHeaders as Record<string, (string) | (string[])>;
|
||||
|
||||
const contentType = (responseHeaders['content-type'] || responseHeaders['Content-Type']);
|
||||
if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) {
|
||||
return callback({ cancel: true });
|
||||
|
||||
if (contentType && Array.isArray(contentType)) {
|
||||
const uri = URI.parse(details.url);
|
||||
// https://github.com/microsoft/vscode/issues/97564
|
||||
// ensure local svg files have Content-Type image/svg+xml
|
||||
if (uri && uri.path.endsWith('.svg')) {
|
||||
if (svgFileSchemes.has(uri.scheme)) {
|
||||
responseHeaders['Content-Type'] = ['image/svg+xml'];
|
||||
return callback({ cancel: false, responseHeaders });
|
||||
}
|
||||
}
|
||||
|
||||
// remote extension schemes have the following format
|
||||
// http://127.0.0.1:<port>/vscode-remote-resource?path=
|
||||
if (!uri.path.includes(Schemas.vscodeRemoteResource) &&
|
||||
contentType.some(x => x.toLowerCase().includes('image/svg'))) {
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
}
|
||||
|
||||
return callback({ cancel: false });
|
||||
@ -526,16 +542,11 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
this.sendWhenReady('vscode:leaveFullScreen', CancellationToken.None);
|
||||
});
|
||||
|
||||
// Window Failed to load
|
||||
this._win.webContents.on('did-fail-load', (event: Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => {
|
||||
this.logService.warn('[electron event]: fail to load, ', errorDescription);
|
||||
});
|
||||
|
||||
// Handle configuration changes
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated()));
|
||||
|
||||
// Handle Workspace events
|
||||
this._register(this.workspacesMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e)));
|
||||
this._register(this.workspacesManagementMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e)));
|
||||
|
||||
// Inject headers when requests are incoming
|
||||
const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*'];
|
||||
@ -544,9 +555,9 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
}
|
||||
|
||||
private onWindowError(error: WindowError.UNRESPONSIVE): void;
|
||||
private onWindowError(error: WindowError.CRASHED, details: Details): void;
|
||||
private onWindowError(error: WindowError, details?: Details): void {
|
||||
this.logService.error(error === WindowError.CRASHED ? `[VS Code]: renderer process crashed (detail: ${details?.reason})` : '[VS Code]: detected unresponsive');
|
||||
private onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): void;
|
||||
private onWindowError(error: WindowError, details?: RenderProcessGoneDetails): void {
|
||||
this.logService.error(error === WindowError.CRASHED ? `Main: renderer process crashed (detail: ${details?.reason})` : 'Main: detected unresponsive');
|
||||
|
||||
// If we run extension tests from CLI, showing a dialog is not
|
||||
// very helpful in this case. Rather, we bring down the test run
|
||||
@ -568,7 +579,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
// Unresponsive
|
||||
if (error === WindowError.UNRESPONSIVE) {
|
||||
if (this.isExtensionDevelopmentHost || this.isExtensionTestHost || (this._win && this._win.webContents && this._win.webContents.isDevToolsOpened())) {
|
||||
// TODO@Ben Workaround for https://github.com/microsoft/vscode/issues/56994
|
||||
// TODO@bpasero Workaround for https://github.com/microsoft/vscode/issues/56994
|
||||
// In certain cases the window can report unresponsiveness because a breakpoint was hit
|
||||
// and the process is stopped executing. The most typical cases are:
|
||||
// - devtools are opened and debugging happens
|
||||
@ -581,9 +592,9 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
this.dialogMainService.showMessageBox({
|
||||
title: product.nameLong,
|
||||
type: 'warning',
|
||||
buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
|
||||
message: nls.localize('appStalled', "The window is no longer responding"),
|
||||
detail: nls.localize('appStalledDetail', "You can reopen or close the window or keep waiting."),
|
||||
buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
|
||||
message: localize('appStalled', "The window is no longer responding"),
|
||||
detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."),
|
||||
noLink: true
|
||||
}, this._win).then(result => {
|
||||
if (!this._win) {
|
||||
@ -591,6 +602,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
}
|
||||
|
||||
if (result.response === 0) {
|
||||
this._win.webContents.forcefullyCrashRenderer(); // Calling reload() immediately after calling this method will force the reload to occur in a new process
|
||||
this.reload();
|
||||
} else if (result.response === 2) {
|
||||
this.destroyWindow();
|
||||
@ -602,17 +614,17 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
else {
|
||||
let message: string;
|
||||
if (details && details.reason !== 'crashed') {
|
||||
message = nls.localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason);
|
||||
message = localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason);
|
||||
} else {
|
||||
message = nls.localize('appCrashed', "The window has crashed", details?.reason);
|
||||
message = localize('appCrashed', "The window has crashed", details?.reason);
|
||||
}
|
||||
|
||||
this.dialogMainService.showMessageBox({
|
||||
title: product.nameLong,
|
||||
type: 'warning',
|
||||
buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
|
||||
buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
|
||||
message,
|
||||
detail: nls.localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."),
|
||||
detail: localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."),
|
||||
noLink: true
|
||||
}, this._win).then(result => {
|
||||
if (!this._win) {
|
||||
@ -643,31 +655,32 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
}
|
||||
|
||||
private onConfigurationUpdated(): void {
|
||||
|
||||
// Menubar
|
||||
const newMenuBarVisibility = this.getMenuBarVisibility();
|
||||
if (newMenuBarVisibility !== this.currentMenuBarVisibility) {
|
||||
this.currentMenuBarVisibility = newMenuBarVisibility;
|
||||
this.setMenuBarVisibility(newMenuBarVisibility);
|
||||
}
|
||||
// Do not set to empty configuration at startup if setting is empty to not override configuration through CLI options:
|
||||
const env = process.env;
|
||||
|
||||
// Proxy
|
||||
let newHttpProxy = (this.configurationService.getValue<string>('http.proxy') || '').trim()
|
||||
|| (env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || '').trim() // Not standardized.
|
||||
|| (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized.
|
||||
|| undefined;
|
||||
|
||||
if (newHttpProxy?.endsWith('/')) {
|
||||
newHttpProxy = newHttpProxy.substr(0, newHttpProxy.length - 1);
|
||||
}
|
||||
const newNoProxy = (env.no_proxy || env.NO_PROXY || '').trim() || undefined; // Not standardized.
|
||||
|
||||
const newNoProxy = (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized.
|
||||
if ((newHttpProxy || '').indexOf('@') === -1 && (newHttpProxy !== this.currentHttpProxy || newNoProxy !== this.currentNoProxy)) {
|
||||
this.currentHttpProxy = newHttpProxy;
|
||||
this.currentNoProxy = newNoProxy;
|
||||
|
||||
const proxyRules = newHttpProxy || '';
|
||||
const proxyBypassRules = newNoProxy ? `${newNoProxy},<local>` : '<local>';
|
||||
this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`);
|
||||
this._win.webContents.session.setProxy({
|
||||
proxyRules,
|
||||
proxyBypassRules,
|
||||
pacScript: '',
|
||||
});
|
||||
this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -677,7 +690,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
}
|
||||
}
|
||||
|
||||
load(config: INativeWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void {
|
||||
load(config: INativeWindowConfiguration, { isReload, disableExtensions }: { isReload?: boolean, disableExtensions?: boolean } = Object.create(null)): void {
|
||||
|
||||
// If this window was loaded before from the command line
|
||||
// (as indicated by VSCODE_CLI environment), make sure to
|
||||
@ -689,6 +702,15 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
config.userEnv = { ...currentUserEnv, ...config.userEnv }; // still allow to override certain environment as passed in
|
||||
}
|
||||
|
||||
// If named pipe was instantiated for the crashpad_handler process, reuse the same
|
||||
// pipe for new app instances connecting to the original app instance.
|
||||
// Ref: https://github.com/microsoft/vscode/issues/115874
|
||||
if (process.env['CHROME_CRASHPAD_PIPE_NAME']) {
|
||||
Object.assign(config.userEnv, {
|
||||
CHROME_CRASHPAD_PIPE_NAME: process.env['CHROME_CRASHPAD_PIPE_NAME']
|
||||
});
|
||||
}
|
||||
|
||||
// If this is the first time the window is loaded, we associate the paths
|
||||
// directly with the window because we assume the loading will just work
|
||||
if (this._readyState === ReadyState.NONE) {
|
||||
@ -728,7 +750,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
}
|
||||
|
||||
// Load URL
|
||||
perf.mark('main:loadWindow');
|
||||
mark('code/willOpenNewWindow');
|
||||
this._win.loadURL(this.getUrl(configuration));
|
||||
|
||||
// Make window visible if it did not open in N seconds because this indicates an error
|
||||
@ -747,11 +769,14 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
this._onLoad.fire();
|
||||
}
|
||||
|
||||
reload(cli?: NativeParsedArgs): void {
|
||||
async reload(cli?: NativeParsedArgs): Promise<void> {
|
||||
|
||||
// Copy our current config for reuse
|
||||
const configuration = Object.assign({}, this.currentConfig);
|
||||
|
||||
// Validate workspace
|
||||
configuration.workspace = await this.validateWorkspace(configuration);
|
||||
|
||||
// Delete some properties we do not want during reload
|
||||
delete configuration.filesToOpenOrCreate;
|
||||
delete configuration.filesToDiff;
|
||||
@ -761,17 +786,44 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
// in extension development mode. These options are all development related.
|
||||
if (this.isExtensionDevelopmentHost && cli) {
|
||||
configuration.verbose = cli.verbose;
|
||||
configuration.debugId = cli.debugId;
|
||||
configuration['inspect-extensions'] = cli['inspect-extensions'];
|
||||
configuration['inspect-brk-extensions'] = cli['inspect-brk-extensions'];
|
||||
configuration.debugId = cli.debugId;
|
||||
configuration['extensions-dir'] = cli['extensions-dir'];
|
||||
}
|
||||
|
||||
configuration.isInitialStartup = false; // since this is a reload
|
||||
|
||||
// Load config
|
||||
const disableExtensions = cli ? cli['disable-extensions'] : undefined;
|
||||
this.load(configuration, true, disableExtensions);
|
||||
this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] });
|
||||
}
|
||||
|
||||
private async validateWorkspace(configuration: INativeWindowConfiguration): Promise<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined> {
|
||||
|
||||
// Multi folder
|
||||
if (isWorkspaceIdentifier(configuration.workspace)) {
|
||||
const configPath = configuration.workspace.configPath;
|
||||
if (configPath.scheme === Schemas.file) {
|
||||
const workspaceExists = await this.fileService.exists(configPath);
|
||||
if (!workspaceExists) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Single folder
|
||||
else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) {
|
||||
const uri = configuration.workspace.uri;
|
||||
if (uri.scheme === Schemas.file) {
|
||||
const folderExists = await this.fileService.exists(uri);
|
||||
if (!folderExists) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace is valid
|
||||
return configuration.workspace;
|
||||
}
|
||||
|
||||
private getUrl(windowConfiguration: INativeWindowConfiguration): string {
|
||||
@ -780,6 +832,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
windowConfiguration.windowId = this._win.id;
|
||||
windowConfiguration.sessionId = `window:${this._win.id}`;
|
||||
windowConfiguration.logLevel = this.logService.getLevel();
|
||||
windowConfiguration.logsPath = this.environmentService.logsPath;
|
||||
|
||||
// Set zoomlevel
|
||||
const windowConfig = this.configurationService.getValue<IWindowSettings | undefined>('window');
|
||||
@ -803,14 +856,14 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
windowConfiguration.maximized = this._win.isMaximized();
|
||||
|
||||
// Dump Perf Counters
|
||||
windowConfiguration.perfEntries = perf.exportEntries();
|
||||
windowConfiguration.perfMarks = getMarks();
|
||||
|
||||
// Parts splash
|
||||
windowConfiguration.partsSplashPath = path.join(this.environmentService.userDataPath, 'rapid_render.json');
|
||||
windowConfiguration.partsSplashPath = join(this.environmentService.userDataPath, 'rapid_render.json');
|
||||
|
||||
// OS Info
|
||||
windowConfiguration.os = {
|
||||
release: os.release()
|
||||
release: release()
|
||||
};
|
||||
|
||||
// Config (combination of process.argv and window configuration)
|
||||
@ -848,10 +901,10 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
workbench = 'vs/code/electron-browser/workbench/workbench.html';
|
||||
}
|
||||
|
||||
return (this.environmentService.sandbox ?
|
||||
FileAccess._asCodeFileUri(workbench, require) :
|
||||
FileAccess.asBrowserUri(workbench, require))
|
||||
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }).toString(true);
|
||||
return FileAccess
|
||||
.asBrowserUri(workbench, require)
|
||||
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` })
|
||||
.toString(true);
|
||||
}
|
||||
|
||||
serializeWindowState(): IWindowState {
|
||||
@ -1161,7 +1214,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
|
||||
if (visibility === 'toggle') {
|
||||
if (notify) {
|
||||
this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key."));
|
||||
this.send('vscode:showInfoMessage', localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key."));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1256,6 +1309,11 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
|
||||
send(channel: string, ...args: any[]): void {
|
||||
if (this._win) {
|
||||
if (this._win.isDestroyed() || this._win.webContents.isDestroyed()) {
|
||||
this.logService.warn(`Sending IPC message to channel ${channel} for window that is destroyed`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._win.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,10 @@ import 'vs/css!./media/issueReporter';
|
||||
import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded
|
||||
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
|
||||
import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService';
|
||||
import { ipcRenderer, process } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window';
|
||||
import { $, reset, safeInnerHtml, windowOpenNoOpener } from 'vs/base/browser/dom';
|
||||
import { Button } from 'vs/base/browser/ui/button/button';
|
||||
import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel';
|
||||
import * as collections from 'vs/base/common/collections';
|
||||
import { debounce } from 'vs/base/common/decorators';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
@ -23,9 +22,11 @@ import BaseHtml from 'vs/code/electron-sandbox/issue/issueReporterPage';
|
||||
import { localize } from 'vs/nls';
|
||||
import { isRemoteDiagnosticError, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { IMainProcessService, ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { IssueReporterData, IssueReporterExtensionData, IssueReporterFeatures, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue';
|
||||
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels';
|
||||
|
||||
const MAX_URL_LENGTH = 2045;
|
||||
|
||||
@ -82,14 +83,12 @@ export class IssueReporter extends Disposable {
|
||||
|
||||
this.initServices(configuration);
|
||||
|
||||
const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION;
|
||||
|
||||
const targetExtension = configuration.data.extensionId ? configuration.data.enabledExtensions.find(extension => extension.id === configuration.data.extensionId) : undefined;
|
||||
this.issueReporterModel = new IssueReporterModel({
|
||||
issueType: configuration.data.issueType || IssueType.Bug,
|
||||
versionInfo: {
|
||||
vscodeVersion: `${configuration.product.nameShort} ${configuration.product.version} (${configuration.product.commit || 'Commit unknown'}, ${configuration.product.date || 'Date unknown'})`,
|
||||
os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${isSnap ? ' snap' : ''}`
|
||||
os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${platform.isLinuxSnap ? ' snap' : ''}`
|
||||
},
|
||||
extensionsDisabled: !!configuration.disableExtensions,
|
||||
fileOnExtension: configuration.data.extensionId ? !targetExtension?.isBuiltin : undefined,
|
||||
@ -267,7 +266,7 @@ export class IssueReporter extends Disposable {
|
||||
|
||||
private initServices(configuration: IssueReporterConfiguration): void {
|
||||
const serviceCollection = new ServiceCollection();
|
||||
const mainProcessService = new MainProcessService(configuration.windowId);
|
||||
const mainProcessService = new ElectronIPCMainProcessService(configuration.windowId);
|
||||
serviceCollection.set(IMainProcessService, mainProcessService);
|
||||
|
||||
this.nativeHostService = new NativeHostService(configuration.windowId, mainProcessService) as INativeHostService;
|
||||
@ -442,7 +441,11 @@ export class IssueReporter extends Disposable {
|
||||
|
||||
private updatePreviewButtonState() {
|
||||
if (this.isPreviewEnabled()) {
|
||||
this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub");
|
||||
if (this.configuration.data.githubAccessToken) {
|
||||
this.previewButton.label = localize('createOnGitHub', "Create on GitHub");
|
||||
} else {
|
||||
this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub");
|
||||
}
|
||||
this.previewButton.enabled = true;
|
||||
} else {
|
||||
this.previewButton.enabled = false;
|
||||
@ -598,8 +601,7 @@ export class IssueReporter extends Disposable {
|
||||
issueState = $('span.issue-state');
|
||||
|
||||
const issueIcon = $('span.issue-icon');
|
||||
const codicon = new CodiconLabel(issueIcon);
|
||||
codicon.text = issue.state === 'open' ? '$(issue-opened)' : '$(issue-closed)';
|
||||
issueIcon.appendChild(renderIcon(issue.state === 'open' ? Codicon.issueOpened : Codicon.issueClosed));
|
||||
|
||||
const issueStateLabel = $('span.issue-state.label');
|
||||
issueStateLabel.textContent = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed");
|
||||
@ -778,6 +780,35 @@ export class IssueReporter extends Disposable {
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string, repositoryName: string }): Promise<boolean> {
|
||||
const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`;
|
||||
const init = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: issueTitle,
|
||||
body: issueBody
|
||||
}),
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.configuration.data.githubAccessToken}`
|
||||
})
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
window.fetch(url, init).then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then(result => {
|
||||
ipcRenderer.send('vscode:openExternal', result.html_url);
|
||||
ipcRenderer.send('vscode:closeIssueReporter');
|
||||
resolve(true);
|
||||
});
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async createIssue(): Promise<boolean> {
|
||||
if (!this.validateInputs()) {
|
||||
// If inputs are invalid, set focus to the first one and add listeners on them
|
||||
@ -810,8 +841,16 @@ export class IssueReporter extends Disposable {
|
||||
|
||||
this.hasBeenSubmitted = true;
|
||||
|
||||
const baseUrl = this.getIssueUrlWithTitle((<HTMLInputElement>this.getElementById('issue-title')).value);
|
||||
const issueTitle = (<HTMLInputElement>this.getElementById('issue-title')).value;
|
||||
const issueBody = this.issueReporterModel.serialize();
|
||||
|
||||
const issueUrl = this.issueReporterModel.fileOnExtension() ? this.getExtensionGitHubUrl() : this.configuration.product.reportIssueUrl!;
|
||||
const gitHubDetails = this.parseGitHubUrl(issueUrl);
|
||||
if (this.configuration.data.githubAccessToken && gitHubDetails) {
|
||||
return this.submitToGitHub(issueTitle, issueBody, gitHubDetails);
|
||||
}
|
||||
|
||||
const baseUrl = this.getIssueUrlWithTitle((<HTMLInputElement>this.getElementById('issue-title')).value);
|
||||
let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`;
|
||||
|
||||
if (url.length > MAX_URL_LENGTH) {
|
||||
@ -841,6 +880,20 @@ export class IssueReporter extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
private parseGitHubUrl(url: string): undefined | { repositoryName: string, owner: string } {
|
||||
// Assumes a GitHub url to a particular repo, https://github.com/repositoryName/owner.
|
||||
// Repository name and owner cannot contain '/'
|
||||
const match = /^https?:\/\/github\.com\/([^\/]*)\/([^\/]*).*/.exec(url);
|
||||
if (match && match.length) {
|
||||
return {
|
||||
owner: match[1],
|
||||
repositoryName: match[2]
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getExtensionGitHubUrl(): string {
|
||||
let repositoryUrl = '';
|
||||
const bugsUrl = this.getExtensionBugsUrl();
|
||||
|
@ -49,28 +49,12 @@ body {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.process-item {
|
||||
line-height: 22px;
|
||||
.monaco-list-row:first-of-type {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th[scope='col'] {
|
||||
vertical-align: bottom;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
padding: .5rem;
|
||||
border-top: 1px solid #cccccc;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: .25rem;
|
||||
vertical-align: top;
|
||||
cursor: default;
|
||||
.row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.centered {
|
||||
@ -79,6 +63,9 @@ td {
|
||||
|
||||
.nameLabel{
|
||||
text-align: left;
|
||||
width: calc(100% - 185px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.data {
|
||||
@ -93,15 +80,3 @@ td {
|
||||
padding-left: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody > tr:hover {
|
||||
background-color: #2A2D2E;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data:; media-src 'none'; child-src 'self'; object-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https:;">
|
||||
</head>
|
||||
<body aria-label="">
|
||||
<table id="process-list"></table>
|
||||
<div id="process-list"></div>
|
||||
</body>
|
||||
|
||||
<!-- Init Bootstrap Helpers -->
|
||||
|
@ -14,38 +14,226 @@ import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox
|
||||
import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu';
|
||||
import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu';
|
||||
import { ProcessItem } from 'vs/base/common/processes';
|
||||
import { addDisposableListener, $ } from 'vs/base/browser/dom';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics';
|
||||
import { MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel';
|
||||
import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { ByteSize } from 'vs/platform/files/common/files';
|
||||
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
|
||||
import { DataTree } from 'vs/base/browser/ui/tree/dataTree';
|
||||
|
||||
const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/;
|
||||
const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/;
|
||||
|
||||
interface FormattedProcessItem {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
pid: string;
|
||||
class ProcessListDelegate implements IListVirtualDelegate<MachineProcessInformation | ProcessItem | IRemoteDiagnosticError> {
|
||||
getHeight(element: MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) {
|
||||
return 22;
|
||||
}
|
||||
|
||||
getTemplateId(element: ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) {
|
||||
if (isProcessItem(element)) {
|
||||
return 'process';
|
||||
}
|
||||
|
||||
if (isMachineProcessInformation(element)) {
|
||||
return 'machine';
|
||||
}
|
||||
|
||||
if (isRemoteDiagnosticError(element)) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (isProcessInformation(element)) {
|
||||
return 'header';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
interface IProcessItemTemplateData extends IProcessRowTemplateData {
|
||||
CPU: HTMLElement;
|
||||
memory: HTMLElement;
|
||||
PID: HTMLElement;
|
||||
}
|
||||
|
||||
interface IProcessRowTemplateData {
|
||||
name: HTMLElement;
|
||||
}
|
||||
|
||||
class ProcessTreeDataSource implements IDataSource<ProcessTree, ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError> {
|
||||
hasChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError): boolean {
|
||||
if (isRemoteDiagnosticError(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isProcessItem(element)) {
|
||||
return !!element.children?.length;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
getChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) {
|
||||
if (isProcessItem(element)) {
|
||||
return element.children ? element.children : [];
|
||||
}
|
||||
|
||||
if (isRemoteDiagnosticError(element)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isProcessInformation(element)) {
|
||||
// If there are multiple process roots, return these, otherwise go directly to the root process
|
||||
if (element.processRoots.length > 1) {
|
||||
return element.processRoots;
|
||||
} else {
|
||||
return [element.processRoots[0].rootProcess];
|
||||
}
|
||||
}
|
||||
|
||||
if (isMachineProcessInformation(element)) {
|
||||
return [element.rootProcess];
|
||||
}
|
||||
|
||||
return [element.processes];
|
||||
}
|
||||
}
|
||||
|
||||
class ProcessHeaderTreeRenderer implements ITreeRenderer<ProcessInformation, void, IProcessItemTemplateData> {
|
||||
templateId: string = 'header';
|
||||
renderTemplate(container: HTMLElement): IProcessItemTemplateData {
|
||||
const data = Object.create(null);
|
||||
const row = dom.append(container, dom.$('.row'));
|
||||
data.name = dom.append(row, dom.$('.nameLabel'));
|
||||
data.CPU = dom.append(row, dom.$('.cpu'));
|
||||
data.memory = dom.append(row, dom.$('.memory'));
|
||||
data.PID = dom.append(row, dom.$('.pid'));
|
||||
return data;
|
||||
}
|
||||
renderElement(node: ITreeNode<ProcessInformation, void>, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void {
|
||||
templateData.name.textContent = localize('name', "Process Name");
|
||||
templateData.CPU.textContent = localize('cpu', "CPU %");
|
||||
templateData.PID.textContent = localize('pid', "PID");
|
||||
templateData.memory.textContent = localize('memory', "Memory (MB)");
|
||||
|
||||
}
|
||||
disposeTemplate(templateData: any): void {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
class MachineRenderer implements ITreeRenderer<MachineProcessInformation, void, IProcessRowTemplateData> {
|
||||
templateId: string = 'machine';
|
||||
renderTemplate(container: HTMLElement): IProcessRowTemplateData {
|
||||
const data = Object.create(null);
|
||||
const row = dom.append(container, dom.$('.row'));
|
||||
data.name = dom.append(row, dom.$('.nameLabel'));
|
||||
return data;
|
||||
}
|
||||
renderElement(node: ITreeNode<MachineProcessInformation, void>, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void {
|
||||
templateData.name.textContent = node.element.name;
|
||||
}
|
||||
disposeTemplate(templateData: IProcessRowTemplateData): void {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorRenderer implements ITreeRenderer<IRemoteDiagnosticError, void, IProcessRowTemplateData> {
|
||||
templateId: string = 'error';
|
||||
renderTemplate(container: HTMLElement): IProcessRowTemplateData {
|
||||
const data = Object.create(null);
|
||||
const row = dom.append(container, dom.$('.row'));
|
||||
data.name = dom.append(row, dom.$('.nameLabel'));
|
||||
return data;
|
||||
}
|
||||
renderElement(node: ITreeNode<IRemoteDiagnosticError, void>, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void {
|
||||
templateData.name.textContent = node.element.errorMessage;
|
||||
}
|
||||
disposeTemplate(templateData: IProcessRowTemplateData): void {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProcessRenderer implements ITreeRenderer<ProcessItem, void, IProcessItemTemplateData> {
|
||||
constructor(private platform: string, private totalMem: number, private mapPidToWindowTitle: Map<number, string>) { }
|
||||
|
||||
templateId: string = 'process';
|
||||
renderTemplate(container: HTMLElement): IProcessItemTemplateData {
|
||||
const data = <IProcessItemTemplateData>Object.create(null);
|
||||
const row = dom.append(container, dom.$('.row'));
|
||||
|
||||
data.name = dom.append(row, dom.$('.nameLabel'));
|
||||
data.CPU = dom.append(row, dom.$('.cpu'));
|
||||
data.memory = dom.append(row, dom.$('.memory'));
|
||||
data.PID = dom.append(row, dom.$('.pid'));
|
||||
|
||||
return data;
|
||||
}
|
||||
renderElement(node: ITreeNode<ProcessItem, void>, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void {
|
||||
const { element } = node;
|
||||
|
||||
let name = element.name;
|
||||
if (name === 'window') {
|
||||
const windowTitle = this.mapPidToWindowTitle.get(element.pid);
|
||||
name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(element.pid)})` : name;
|
||||
}
|
||||
|
||||
templateData.name.textContent = name;
|
||||
templateData.name.title = element.cmd;
|
||||
|
||||
templateData.CPU.textContent = element.load.toFixed(0);
|
||||
templateData.PID.textContent = element.pid.toFixed(0);
|
||||
|
||||
const memory = this.platform === 'win32' ? element.mem : (this.totalMem * (element.mem / 100));
|
||||
templateData.memory.textContent = (memory / ByteSize.MB).toFixed(0);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IProcessItemTemplateData): void {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
interface MachineProcessInformation {
|
||||
name: string;
|
||||
formattedName: string;
|
||||
cmd: string;
|
||||
rootProcess: ProcessItem | IRemoteDiagnosticError
|
||||
}
|
||||
|
||||
interface ProcessInformation {
|
||||
processRoots: MachineProcessInformation[];
|
||||
}
|
||||
|
||||
interface ProcessTree {
|
||||
processes: ProcessInformation;
|
||||
}
|
||||
|
||||
function isMachineProcessInformation(item: any): item is MachineProcessInformation {
|
||||
return !!item.name && !!item.rootProcess;
|
||||
}
|
||||
|
||||
function isProcessInformation(item: any): item is ProcessInformation {
|
||||
return !!item.processRoots;
|
||||
}
|
||||
|
||||
function isProcessItem(item: any): item is ProcessItem {
|
||||
return !!item.pid;
|
||||
}
|
||||
|
||||
class ProcessExplorer {
|
||||
private lastRequestTime: number;
|
||||
|
||||
private collapsedStateCache: Map<string, boolean> = new Map<string, boolean>();
|
||||
|
||||
private mapPidToWindowTitle = new Map<number, string>();
|
||||
|
||||
private listeners = new DisposableStore();
|
||||
|
||||
private nativeHostService: INativeHostService;
|
||||
|
||||
private tree: DataTree<any, ProcessTree | MachineProcessInformation | ProcessItem | ProcessInformation | IRemoteDiagnosticError, any> | undefined;
|
||||
|
||||
constructor(windowId: number, private data: ProcessExplorerData) {
|
||||
const mainProcessService = new MainProcessService(windowId);
|
||||
const mainProcessService = new ElectronIPCMainProcessService(windowId);
|
||||
this.nativeHostService = new NativeHostService(windowId, mainProcessService) as INativeHostService;
|
||||
|
||||
this.applyStyles(data.styles);
|
||||
@ -56,8 +244,19 @@ class ProcessExplorer {
|
||||
windows.forEach(window => this.mapPidToWindowTitle.set(window.pid, window.title));
|
||||
});
|
||||
|
||||
ipcRenderer.on('vscode:listProcessesResponse', (event: unknown, processRoots: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]) => {
|
||||
this.updateProcessInfo(processRoots);
|
||||
ipcRenderer.on('vscode:listProcessesResponse', async (event: unknown, processRoots: MachineProcessInformation[]) => {
|
||||
processRoots.forEach((info, index) => {
|
||||
if (isProcessItem(info.rootProcess)) {
|
||||
info.rootProcess.name = index === 0 ? `${this.data.applicationName} main` : 'remote agent';
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.tree) {
|
||||
await this.createProcessTree(processRoots);
|
||||
} else {
|
||||
this.tree.setInput({ processes: { processRoots } });
|
||||
}
|
||||
|
||||
this.requestProcessList(0);
|
||||
});
|
||||
|
||||
@ -66,54 +265,58 @@ class ProcessExplorer {
|
||||
ipcRenderer.send('vscode:listProcesses');
|
||||
}
|
||||
|
||||
private getProcessList(rootProcess: ProcessItem, isLocal: boolean, totalMem: number): FormattedProcessItem[] {
|
||||
const processes: FormattedProcessItem[] = [];
|
||||
const handledProcesses = new Set<number>();
|
||||
|
||||
if (rootProcess) {
|
||||
this.getProcessItem(processes, rootProcess, 0, isLocal, totalMem, handledProcesses);
|
||||
private async createProcessTree(processRoots: MachineProcessInformation[]): Promise<void> {
|
||||
const container = document.getElementById('process-list');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
return processes;
|
||||
}
|
||||
const { totalmem } = await this.nativeHostService.getOSStatistics();
|
||||
|
||||
private getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, indent: number, isLocal: boolean, totalMem: number, handledProcesses: Set<number>): void {
|
||||
const isRoot = (indent === 0);
|
||||
const renderers = [
|
||||
new ProcessRenderer(this.data.platform, totalmem, this.mapPidToWindowTitle),
|
||||
new ProcessHeaderTreeRenderer(),
|
||||
new MachineRenderer(),
|
||||
new ErrorRenderer()
|
||||
];
|
||||
|
||||
handledProcesses.add(item.pid);
|
||||
this.tree = new DataTree('processExplorer',
|
||||
container,
|
||||
new ProcessListDelegate(),
|
||||
renderers,
|
||||
new ProcessTreeDataSource(),
|
||||
{
|
||||
identityProvider:
|
||||
{
|
||||
getId: (element: ProcessTree | ProcessItem | MachineProcessInformation | ProcessInformation | IRemoteDiagnosticError) => {
|
||||
if (isProcessItem(element)) {
|
||||
return element.pid.toString();
|
||||
}
|
||||
|
||||
let name = item.name;
|
||||
if (isRoot) {
|
||||
name = isLocal ? `${this.data.applicationName} main` : 'remote agent';
|
||||
}
|
||||
if (isRemoteDiagnosticError(element)) {
|
||||
return element.hostName;
|
||||
}
|
||||
|
||||
if (name === 'window') {
|
||||
const windowTitle = this.mapPidToWindowTitle.get(item.pid);
|
||||
name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(item.pid)})` : name;
|
||||
}
|
||||
if (isProcessInformation(element)) {
|
||||
return 'processes';
|
||||
}
|
||||
|
||||
// Format name with indent
|
||||
const formattedName = isRoot ? name : `${' '.repeat(indent)} ${name}`;
|
||||
const memory = this.data.platform === 'win32' ? item.mem : (totalMem * (item.mem / 100));
|
||||
processes.push({
|
||||
cpu: item.load,
|
||||
memory: (memory / ByteSize.MB),
|
||||
pid: item.pid.toFixed(0),
|
||||
name,
|
||||
formattedName,
|
||||
cmd: item.cmd
|
||||
});
|
||||
if (isMachineProcessInformation(element)) {
|
||||
return element.name;
|
||||
}
|
||||
|
||||
// Recurse into children if any
|
||||
if (Array.isArray(item.children)) {
|
||||
item.children.forEach(child => {
|
||||
if (!child || handledProcesses.has(child.pid)) {
|
||||
return; // prevent loops
|
||||
return 'header';
|
||||
}
|
||||
}
|
||||
|
||||
this.getProcessItem(processes, child, indent + 1, isLocal, totalMem, handledProcesses);
|
||||
});
|
||||
}
|
||||
|
||||
this.tree.setInput({ processes: { processRoots } });
|
||||
this.tree.layout(window.innerHeight, window.innerWidth);
|
||||
this.tree.onContextMenu(e => {
|
||||
if (isProcessItem(e.element)) {
|
||||
this.showContextMenu(e.element, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isDebuggable(cmd: string): boolean {
|
||||
@ -121,7 +324,7 @@ class ProcessExplorer {
|
||||
return (matches && matches.length >= 2) || cmd.indexOf('node ') >= 0 || cmd.indexOf('node.exe') >= 0;
|
||||
}
|
||||
|
||||
private attachTo(item: FormattedProcessItem) {
|
||||
private attachTo(item: ProcessItem) {
|
||||
const config: any = {
|
||||
type: 'node',
|
||||
request: 'attach',
|
||||
@ -150,179 +353,16 @@ class ProcessExplorer {
|
||||
ipcRenderer.send('vscode:workbenchCommand', { id: 'debug.startFromConfig', from: 'processExplorer', args: [config] });
|
||||
}
|
||||
|
||||
private getProcessIdWithHighestProperty(processList: any[], propertyName: string) {
|
||||
let max = 0;
|
||||
let maxProcessId;
|
||||
processList.forEach(process => {
|
||||
if (process[propertyName] > max) {
|
||||
max = process[propertyName];
|
||||
maxProcessId = process.pid;
|
||||
}
|
||||
});
|
||||
|
||||
return maxProcessId;
|
||||
}
|
||||
|
||||
private updateSectionCollapsedState(shouldExpand: boolean, body: HTMLElement, twistie: CodiconLabel, sectionName: string) {
|
||||
if (shouldExpand) {
|
||||
body.classList.remove('hidden');
|
||||
this.collapsedStateCache.set(sectionName, false);
|
||||
twistie.text = '$(chevron-down)';
|
||||
} else {
|
||||
body.classList.add('hidden');
|
||||
this.collapsedStateCache.set(sectionName, true);
|
||||
twistie.text = '$(chevron-right)';
|
||||
}
|
||||
}
|
||||
|
||||
private renderProcessFetchError(sectionName: string, errorMessage: string) {
|
||||
const container = document.getElementById('process-list');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = document.createElement('tbody');
|
||||
|
||||
this.renderProcessGroupHeader(sectionName, body, container);
|
||||
|
||||
const errorRow = document.createElement('tr');
|
||||
const data = document.createElement('td');
|
||||
data.textContent = errorMessage;
|
||||
data.className = 'error';
|
||||
data.colSpan = 4;
|
||||
errorRow.appendChild(data);
|
||||
|
||||
body.appendChild(errorRow);
|
||||
container.appendChild(body);
|
||||
}
|
||||
|
||||
private renderProcessGroupHeader(sectionName: string, body: HTMLElement, container: HTMLElement) {
|
||||
const headerRow = document.createElement('tr');
|
||||
|
||||
const headerData = document.createElement('td');
|
||||
headerData.colSpan = 4;
|
||||
headerRow.appendChild(headerData);
|
||||
|
||||
const headerContainer = document.createElement('div');
|
||||
headerContainer.className = 'header';
|
||||
headerData.appendChild(headerContainer);
|
||||
|
||||
const twistieContainer = document.createElement('div');
|
||||
const twistieCodicon = new CodiconLabel(twistieContainer);
|
||||
this.updateSectionCollapsedState(!this.collapsedStateCache.get(sectionName), body, twistieCodicon, sectionName);
|
||||
headerContainer.appendChild(twistieContainer);
|
||||
|
||||
const headerLabel = document.createElement('span');
|
||||
headerLabel.textContent = sectionName;
|
||||
headerContainer.appendChild(headerLabel);
|
||||
|
||||
this.listeners.add(addDisposableListener(headerData, 'click', (e) => {
|
||||
const isHidden = body.classList.contains('hidden');
|
||||
this.updateSectionCollapsedState(isHidden, body, twistieCodicon, sectionName);
|
||||
}));
|
||||
|
||||
container.appendChild(headerRow);
|
||||
}
|
||||
|
||||
private renderTableSection(sectionName: string, processList: FormattedProcessItem[], renderManySections: boolean, sectionIsLocal: boolean): void {
|
||||
const container = document.getElementById('process-list');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highestCPUProcess = this.getProcessIdWithHighestProperty(processList, 'cpu');
|
||||
const highestMemoryProcess = this.getProcessIdWithHighestProperty(processList, 'memory');
|
||||
|
||||
const body = document.createElement('tbody');
|
||||
|
||||
if (renderManySections) {
|
||||
this.renderProcessGroupHeader(sectionName, body, container);
|
||||
}
|
||||
|
||||
processList.forEach(p => {
|
||||
const row = document.createElement('tr');
|
||||
row.id = p.pid.toString();
|
||||
|
||||
const cpu = document.createElement('td');
|
||||
p.pid === highestCPUProcess
|
||||
? cpu.classList.add('centered', 'highest')
|
||||
: cpu.classList.add('centered');
|
||||
cpu.textContent = p.cpu.toFixed(0);
|
||||
|
||||
const memory = document.createElement('td');
|
||||
p.pid === highestMemoryProcess
|
||||
? memory.classList.add('centered', 'highest')
|
||||
: memory.classList.add('centered');
|
||||
memory.textContent = p.memory.toFixed(0);
|
||||
|
||||
const pid = document.createElement('td');
|
||||
pid.classList.add('centered');
|
||||
pid.textContent = p.pid;
|
||||
|
||||
const name = document.createElement('th');
|
||||
name.scope = 'row';
|
||||
name.classList.add('data');
|
||||
name.title = p.cmd;
|
||||
name.textContent = p.formattedName;
|
||||
|
||||
row.append(cpu, memory, pid, name);
|
||||
|
||||
this.listeners.add(addDisposableListener(row, 'contextmenu', (e) => {
|
||||
this.showContextMenu(e, p, sectionIsLocal);
|
||||
}));
|
||||
|
||||
body.appendChild(row);
|
||||
});
|
||||
|
||||
container.appendChild(body);
|
||||
}
|
||||
|
||||
private async updateProcessInfo(processLists: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]): Promise<void> {
|
||||
const container = document.getElementById('process-list');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerText = '';
|
||||
this.listeners.clear();
|
||||
|
||||
const tableHead = $('thead', undefined);
|
||||
const row = $('tr');
|
||||
tableHead.append(row);
|
||||
|
||||
row.append($('th.cpu', { scope: 'col' }, localize('cpu', "CPU %")));
|
||||
row.append($('th.memory', { scope: 'col' }, localize('memory', "Memory (MB)")));
|
||||
row.append($('th.pid', { scope: 'col' }, localize('pid', "PID")));
|
||||
row.append($('th.nameLabel', { scope: 'col' }, localize('name', "Name")));
|
||||
|
||||
container.append(tableHead);
|
||||
|
||||
const hasMultipleMachines = Object.keys(processLists).length > 1;
|
||||
const { totalmem } = await this.nativeHostService.getOSStatistics();
|
||||
processLists.forEach((remote, i) => {
|
||||
const isLocal = i === 0;
|
||||
if (isRemoteDiagnosticError(remote.rootProcess)) {
|
||||
this.renderProcessFetchError(remote.name, remote.rootProcess.errorMessage);
|
||||
} else {
|
||||
this.renderTableSection(remote.name, this.getProcessList(remote.rootProcess, isLocal, totalmem), hasMultipleMachines, isLocal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private applyStyles(styles: ProcessExplorerStyles): void {
|
||||
const styleTag = document.createElement('style');
|
||||
const content: string[] = [];
|
||||
|
||||
if (styles.hoverBackground) {
|
||||
content.push(`tbody > tr:hover, table > tr:hover { background-color: ${styles.hoverBackground}; }`);
|
||||
content.push(`.monaco-list-row:hover { background-color: ${styles.hoverBackground}; }`);
|
||||
}
|
||||
|
||||
if (styles.hoverForeground) {
|
||||
content.push(`tbody > tr:hover, table > tr:hover { color: ${styles.hoverForeground}; }`);
|
||||
}
|
||||
|
||||
if (styles.highlightForeground) {
|
||||
content.push(`.highest { color: ${styles.highlightForeground}; }`);
|
||||
content.push(`.monaco-list-row:hover { color: ${styles.hoverForeground}; }`);
|
||||
}
|
||||
|
||||
styleTag.textContent = content.join('\n');
|
||||
@ -334,9 +374,7 @@ class ProcessExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
private showContextMenu(e: MouseEvent, item: FormattedProcessItem, isLocal: boolean) {
|
||||
e.preventDefault();
|
||||
|
||||
private showContextMenu(item: ProcessItem, isLocal: boolean) {
|
||||
const items: IContextMenuItem[] = [];
|
||||
const pid = Number(item.pid);
|
||||
|
||||
@ -417,8 +455,6 @@ class ProcessExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function startup(windowId: number, data: ProcessExplorerData): void {
|
||||
const platformClass = data.platform === 'win32' ? 'windows' : data.platform === 'linux' ? 'linux' : 'mac';
|
||||
document.body.classList.add(platformClass); // used by our fonts
|
||||
|
@ -1,83 +0,0 @@
|
||||
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; img-src 'self' https: data:; media-src 'none'; child-src 'self'; object-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https:;">
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", system-ui, "Ubuntu", "Droid Sans", sans-serif;
|
||||
font-size: 10pt;
|
||||
background-color: #F3F3F3;
|
||||
}
|
||||
|
||||
#main {
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
font-size: 16px;
|
||||
background-color: #555;
|
||||
color: #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#username,
|
||||
#password {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#buttons {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", system-ui, "Ubuntu", "Droid Sans", sans-serif !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1 id="title"></h1>
|
||||
<section id="main">
|
||||
<p id="message"></p>
|
||||
<form id="form">
|
||||
<p><input type="text" id="username" placeholder="Username" required /></p>
|
||||
<p><input type="password" id="password" placeholder="Password" required /></p>
|
||||
<p id="buttons">
|
||||
<input id="ok" type="submit" value="OK" />
|
||||
<input id="cancel" type="button" value="Cancel" />
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
<script src="auth.js"></script>
|
||||
|
||||
</html>
|
@ -1,48 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer } = window.vscode;
|
||||
|
||||
function promptForCredentials(data) {
|
||||
return new Promise((c, e) => {
|
||||
const $title = document.getElementById('title');
|
||||
const $username = document.getElementById('username');
|
||||
const $password = document.getElementById('password');
|
||||
const $form = document.getElementById('form');
|
||||
const $cancel = document.getElementById('cancel');
|
||||
const $message = document.getElementById('message');
|
||||
|
||||
function submit() {
|
||||
c({ username: $username.value, password: $password.value });
|
||||
return false;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
c({ username: '', password: '' });
|
||||
return false;
|
||||
}
|
||||
|
||||
$form.addEventListener('submit', submit);
|
||||
$cancel.addEventListener('click', cancel);
|
||||
|
||||
document.body.addEventListener('keydown', function (e) {
|
||||
switch (e.keyCode) {
|
||||
case 27: e.preventDefault(); e.stopPropagation(); return cancel();
|
||||
case 13: e.preventDefault(); e.stopPropagation(); return submit();
|
||||
}
|
||||
});
|
||||
|
||||
$title.textContent = data.title;
|
||||
$message.textContent = data.message;
|
||||
$username.focus();
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on('vscode:openProxyAuthDialog', async (event, data) => {
|
||||
const response = await promptForCredentials(data);
|
||||
ipcRenderer.send('vscode:proxyAuthResponse', response);
|
||||
});
|
@ -3,7 +3,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' vscode-webview: https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' vscode-webview: https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https: ws:; font-src 'self' https: vscode-remote-resource:;">
|
||||
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types default TrustedFunctionWorkaround ExtensionScripts amdLoader cellRendererEditorText defaultWorkerFactory diffEditorWidget domLineBreaksComputer editorViewLayer diffReview extensionHostWorker insane notebookOutputRenderer safeInnerHtml standaloneColorizer tokenizeToString webNestedWorkerExtensionHost webWorkerExtensionHost;">
|
||||
</head>
|
||||
<body aria-label="">
|
||||
</body>
|
||||
|
@ -12,8 +12,7 @@
|
||||
const bootstrapWindow = bootstrapWindowLib();
|
||||
|
||||
// Add a perf entry right from the top
|
||||
const perf = bootstrapWindow.perfLib();
|
||||
perf.mark('renderer/started');
|
||||
performance.mark('code/didStartRenderer');
|
||||
|
||||
// Load workbench main JS, CSS and NLS all in parallel. This is an
|
||||
// optimization to prevent a waterfall of loading to happen, because
|
||||
@ -24,10 +23,10 @@
|
||||
'vs/nls!vs/workbench/workbench.desktop.main',
|
||||
'vs/css!vs/workbench/workbench.desktop.main'
|
||||
],
|
||||
async function (workbench, configuration) {
|
||||
function (_, configuration) {
|
||||
|
||||
// Mark start of workbench
|
||||
perf.mark('didLoadWorkbenchMain');
|
||||
performance.mark('code/didLoadWorkbenchMain');
|
||||
|
||||
// @ts-ignore
|
||||
return require('vs/workbench/electron-sandbox/desktop.main').main(configuration);
|
||||
@ -41,19 +40,34 @@
|
||||
loaderConfig.recordStats = true;
|
||||
},
|
||||
beforeRequire: function () {
|
||||
perf.mark('willLoadWorkbenchMain');
|
||||
performance.mark('code/willLoadWorkbenchMain');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// add default trustedTypes-policy for logging and to workaround
|
||||
// lib/platform limitations
|
||||
window.trustedTypes?.createPolicy('default', {
|
||||
createHTML(value) {
|
||||
// see https://github.com/electron/electron/issues/27211
|
||||
// Electron webviews use a static innerHTML default value and
|
||||
// that isn't trusted. We use a default policy to check for the
|
||||
// exact value of that innerHTML-string and only allow that.
|
||||
if (value === '<!DOCTYPE html><style type="text/css">:host { display: flex; }</style>') {
|
||||
return value;
|
||||
}
|
||||
throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached');
|
||||
// console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached');
|
||||
// return value;
|
||||
}
|
||||
});
|
||||
|
||||
//region Helpers
|
||||
|
||||
/**
|
||||
* @returns {{
|
||||
* load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown,
|
||||
* globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals'),
|
||||
* perfLib: () => { mark: (name: string) => void }
|
||||
* load: (modules: string[], resultCallback: (result, configuration: import('../../../platform/windows/common/windows').INativeWindowConfiguration) => any, options: object) => unknown,
|
||||
* globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals')
|
||||
* }}
|
||||
*/
|
||||
function bootstrapWindowLib() {
|
||||
|
@ -3,15 +3,15 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { constants, existsSync, statSync, unlinkSync, chmodSync, truncateSync, readFileSync } from 'fs';
|
||||
import { spawn, ChildProcess, SpawnOptions } from 'child_process';
|
||||
import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { parseCLIProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper';
|
||||
import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import * as paths from 'vs/base/common/path';
|
||||
import { isAbsolute, join } from 'vs/base/common/path';
|
||||
import { whenDeleted, writeFileSync } from 'vs/base/node/pfs';
|
||||
import { findFreePort, randomPort } from 'vs/base/node/ports';
|
||||
import { isWindows, isLinux } from 'vs/base/common/platform';
|
||||
@ -69,10 +69,10 @@ export async function main(argv: string[]): Promise<any> {
|
||||
|
||||
// Validate
|
||||
if (
|
||||
!source || !target || source === target || // make sure source and target are provided and are not the same
|
||||
!paths.isAbsolute(source) || !paths.isAbsolute(target) || // make sure both source and target are absolute paths
|
||||
!fs.existsSync(source) || !fs.statSync(source).isFile() || // make sure source exists as file
|
||||
!fs.existsSync(target) || !fs.statSync(target).isFile() // make sure target exists as file
|
||||
!source || !target || source === target || // make sure source and target are provided and are not the same
|
||||
!isAbsolute(source) || !isAbsolute(target) || // make sure both source and target are absolute paths
|
||||
!existsSync(source) || !statSync(source).isFile() || // make sure source exists as file
|
||||
!existsSync(target) || !statSync(target).isFile() // make sure target exists as file
|
||||
) {
|
||||
throw new Error('Using --file-write with invalid arguments.');
|
||||
}
|
||||
@ -83,15 +83,15 @@ export async function main(argv: string[]): Promise<any> {
|
||||
let targetMode: number = 0;
|
||||
let restoreMode = false;
|
||||
if (!!args['file-chmod']) {
|
||||
targetMode = fs.statSync(target).mode;
|
||||
if (!(targetMode & 128) /* readonly */) {
|
||||
fs.chmodSync(target, targetMode | 128);
|
||||
targetMode = statSync(target).mode;
|
||||
if (!(targetMode & constants.S_IWUSR)) {
|
||||
chmodSync(target, targetMode | constants.S_IWUSR);
|
||||
restoreMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Write source to target
|
||||
const data = fs.readFileSync(source);
|
||||
const data = readFileSync(source);
|
||||
if (isWindows) {
|
||||
// On Windows we use a different strategy of saving the file
|
||||
// by first truncating the file and then writing with r+ mode.
|
||||
@ -99,7 +99,7 @@ export async function main(argv: string[]): Promise<any> {
|
||||
// (see https://github.com/microsoft/vscode/issues/931) and
|
||||
// prevent removing alternate data streams
|
||||
// (see https://github.com/microsoft/vscode/issues/6363)
|
||||
fs.truncateSync(target, 0);
|
||||
truncateSync(target, 0);
|
||||
writeFileSync(target, data, { flag: 'r+' });
|
||||
} else {
|
||||
writeFileSync(target, data);
|
||||
@ -107,7 +107,7 @@ export async function main(argv: string[]): Promise<any> {
|
||||
|
||||
// Restore previous mode as needed
|
||||
if (restoreMode) {
|
||||
fs.chmodSync(target, targetMode);
|
||||
chmodSync(target, targetMode);
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `Error using --file-write: ${error.message}`;
|
||||
@ -215,7 +215,7 @@ export async function main(argv: string[]): Promise<any> {
|
||||
throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.');
|
||||
}
|
||||
|
||||
const filenamePrefix = paths.join(os.homedir(), 'prof-' + Math.random().toString(16).slice(-4));
|
||||
const filenamePrefix = join(homedir(), 'prof-' + Math.random().toString(16).slice(-4));
|
||||
|
||||
addArg(argv, `--inspect-brk=${portMain}`);
|
||||
addArg(argv, `--remote-debugging-port=${portRenderer}`);
|
||||
@ -338,7 +338,7 @@ export async function main(argv: string[]): Promise<any> {
|
||||
|
||||
// Make sure to delete the tmp stdin file if we have any
|
||||
if (stdinFilePath) {
|
||||
fs.unlinkSync(stdinFilePath);
|
||||
unlinkSync(stdinFilePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -3,11 +3,12 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { release } from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { gracefulify } from 'graceful-fs';
|
||||
import { isAbsolute, join } from 'vs/base/common/path';
|
||||
import { raceTimeout } from 'vs/base/common/async';
|
||||
import * as semver from 'vs/base/common/semver/semver';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
@ -15,416 +16,147 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia
|
||||
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { IExtensionManagementService, IExtensionGalleryService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IExtensionManagementService, IExtensionGalleryService, IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
|
||||
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
|
||||
import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { RequestService } from 'vs/platform/request/node/requestService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
|
||||
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
|
||||
import { mkdirp, writeFile } from 'vs/base/node/pfs';
|
||||
import { getBaseLabel } from 'vs/base/common/labels';
|
||||
import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { StateService } from 'vs/platform/state/node/stateService';
|
||||
import { ILogService, getLogLevel, LogLevel, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { areSameExtensions, adoptToGalleryExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
|
||||
import { IExtensionManifest, ExtensionType, isLanguagePackExtension, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
|
||||
import { buildTelemetryMessage } from 'vs/platform/telemetry/node/telemetry';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
|
||||
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
|
||||
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
|
||||
const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id);
|
||||
const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id);
|
||||
const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp');
|
||||
|
||||
function getId(manifest: IExtensionManifest, withVersion?: boolean): string {
|
||||
if (withVersion) {
|
||||
return `${manifest.publisher}.${manifest.name}@${manifest.version}`;
|
||||
} else {
|
||||
return `${manifest.publisher}.${manifest.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
|
||||
|
||||
export function getIdAndVersion(id: string): [string, string | undefined] {
|
||||
const matches = EXTENSION_ID_REGEX.exec(id);
|
||||
if (matches && matches[1]) {
|
||||
return [adoptToGalleryExtensionId(matches[1]), matches[2]];
|
||||
}
|
||||
return [adoptToGalleryExtensionId(id), undefined];
|
||||
}
|
||||
|
||||
type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions };
|
||||
|
||||
export class Main {
|
||||
class CliMain extends Disposable {
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
|
||||
) { }
|
||||
private argv: NativeParsedArgs
|
||||
) {
|
||||
super();
|
||||
|
||||
async run(argv: NativeParsedArgs): Promise<void> {
|
||||
if (argv['install-source']) {
|
||||
await this.setInstallSource(argv['install-source']);
|
||||
} else if (argv['list-extensions']) {
|
||||
await this.listExtensions(!!argv['show-versions'], argv['category']);
|
||||
} else if (argv['install-extension'] || argv['install-builtin-extension']) {
|
||||
await this.installExtensions(argv['install-extension'] || [], argv['install-builtin-extension'] || [], !!argv['do-not-sync'], !!argv['force']);
|
||||
} else if (argv['uninstall-extension']) {
|
||||
await this.uninstallExtension(argv['uninstall-extension'], !!argv['force']);
|
||||
} else if (argv['locate-extension']) {
|
||||
await this.locateExtension(argv['locate-extension']);
|
||||
} else if (argv['telemetry']) {
|
||||
console.log(buildTelemetryMessage(this.environmentService.appRoot, this.environmentService.extensionsPath));
|
||||
}
|
||||
// Enable gracefulFs
|
||||
gracefulify(fs);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private setInstallSource(installSource: string): Promise<void> {
|
||||
return writeFile(this.environmentService.installSourcePath, installSource.slice(0, 30));
|
||||
private registerListeners(): void {
|
||||
|
||||
// Dispose on exit
|
||||
process.once('exit', () => this.dispose());
|
||||
}
|
||||
|
||||
private async listExtensions(showVersions: boolean, category?: string): Promise<void> {
|
||||
let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase());
|
||||
if (category && category !== '') {
|
||||
if (categories.indexOf(category.toLowerCase()) < 0) {
|
||||
console.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified');
|
||||
return;
|
||||
}
|
||||
extensions = extensions.filter(e => {
|
||||
if (e.manifest.categories) {
|
||||
const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase());
|
||||
return lowerCaseCategories.indexOf(category.toLowerCase()) > -1;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
} else if (category === '') {
|
||||
console.log('Possible Categories: ');
|
||||
categories.forEach(category => {
|
||||
console.log(category);
|
||||
});
|
||||
return;
|
||||
}
|
||||
extensions.forEach(e => console.log(getId(e.manifest, showVersions)));
|
||||
}
|
||||
async run(): Promise<void> {
|
||||
|
||||
async installExtensions(extensions: string[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean): Promise<void> {
|
||||
const failed: string[] = [];
|
||||
const installedExtensionsManifests: IExtensionManifest[] = [];
|
||||
if (extensions.length) {
|
||||
console.log(localize('installingExtensions', "Installing extensions..."));
|
||||
}
|
||||
// Services
|
||||
const [instantiationService, appenders] = await this.initServices();
|
||||
|
||||
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const checkIfNotInstalled = (id: string, version?: string): boolean => {
|
||||
const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id }));
|
||||
if (installedExtension) {
|
||||
if (!version && !force) {
|
||||
console.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@<version>' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id));
|
||||
return false;
|
||||
}
|
||||
if (version && installedExtension.manifest.version === version) {
|
||||
console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const vsixs: string[] = [];
|
||||
const installExtensionInfos: InstallExtensionInfo[] = [];
|
||||
for (const extension of extensions) {
|
||||
if (/\.vsix$/i.test(extension)) {
|
||||
vsixs.push(extension);
|
||||
} else {
|
||||
const [id, version] = getIdAndVersion(extension);
|
||||
if (checkIfNotInstalled(id, version)) {
|
||||
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const extension of builtinExtensionIds) {
|
||||
const [id, version] = getIdAndVersion(extension);
|
||||
if (checkIfNotInstalled(id, version)) {
|
||||
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } });
|
||||
}
|
||||
}
|
||||
return instantiationService.invokeFunction(async accessor => {
|
||||
const logService = accessor.get(ILogService);
|
||||
const environmentService = accessor.get(INativeEnvironmentService);
|
||||
const extensionManagementCLIService = accessor.get(IExtensionManagementCLIService);
|
||||
|
||||
if (vsixs.length) {
|
||||
await Promise.all(vsixs.map(async vsix => {
|
||||
try {
|
||||
const manifest = await this.installVSIX(vsix, force);
|
||||
if (manifest) {
|
||||
installedExtensionsManifests.push(manifest);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err.message || err.stack || err);
|
||||
failed.push(vsix);
|
||||
}
|
||||
}));
|
||||
}
|
||||
// Log info
|
||||
logService.info('CLI main', this.argv);
|
||||
|
||||
if (installExtensionInfos.length) {
|
||||
// Error handler
|
||||
this.registerErrorHandler(logService);
|
||||
|
||||
const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos);
|
||||
// Run based on argv
|
||||
await this.doRun(environmentService, extensionManagementCLIService);
|
||||
|
||||
await Promise.all(installExtensionInfos.map(async extensionInfo => {
|
||||
const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase());
|
||||
if (gallery) {
|
||||
try {
|
||||
const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force);
|
||||
if (manifest) {
|
||||
installedExtensionsManifests.push(manifest);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err.message || err.stack || err);
|
||||
failed.push(extensionInfo.id);
|
||||
}
|
||||
} else {
|
||||
console.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`);
|
||||
failed.push(extensionInfo.id);
|
||||
}
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) {
|
||||
await this.updateLocalizationsCache();
|
||||
}
|
||||
|
||||
if (failed.length) {
|
||||
throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', ')));
|
||||
}
|
||||
}
|
||||
|
||||
private async installVSIX(vsix: string, force: boolean): Promise<IExtensionManifest | null> {
|
||||
vsix = path.isAbsolute(vsix) ? vsix : path.join(process.cwd(), vsix);
|
||||
const manifest = await getManifest(vsix);
|
||||
const valid = await this.validate(manifest, force);
|
||||
if (valid) {
|
||||
try {
|
||||
await this.extensionManagementService.install(URI.file(vsix));
|
||||
console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix)));
|
||||
return manifest;
|
||||
} catch (error) {
|
||||
if (isPromiseCanceledError(error)) {
|
||||
console.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix)));
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise<Map<string, IGalleryExtension>> {
|
||||
const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id);
|
||||
const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined);
|
||||
|
||||
const galleryExtensions = new Map<string, IGalleryExtension>();
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None);
|
||||
result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension));
|
||||
})(),
|
||||
Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => {
|
||||
const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version);
|
||||
if (extension) {
|
||||
galleryExtensions.set(extension.identifier.id.toLowerCase(), extension);
|
||||
}
|
||||
}))
|
||||
]);
|
||||
|
||||
return galleryExtensions;
|
||||
}
|
||||
|
||||
private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean): Promise<IExtensionManifest | null> {
|
||||
const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None);
|
||||
const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier));
|
||||
if (installedExtension) {
|
||||
if (galleryExtension.version === installedExtension.manifest.version) {
|
||||
console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id));
|
||||
return null;
|
||||
}
|
||||
console.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version));
|
||||
}
|
||||
|
||||
try {
|
||||
if (installOptions.isBuiltin) {
|
||||
console.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version));
|
||||
} else {
|
||||
console.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version));
|
||||
}
|
||||
await this.extensionManagementService.installFromGallery(galleryExtension, installOptions);
|
||||
console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version));
|
||||
return manifest;
|
||||
} catch (error) {
|
||||
if (isPromiseCanceledError(error)) {
|
||||
console.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id));
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async validate(manifest: IExtensionManifest, force: boolean): Promise<boolean> {
|
||||
if (!manifest) {
|
||||
throw new Error('Invalid vsix');
|
||||
}
|
||||
|
||||
const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && semver.gt(local.manifest.version, manifest.version));
|
||||
|
||||
if (newer && !force) {
|
||||
console.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async uninstallExtension(extensions: string[], force: boolean): Promise<void> {
|
||||
async function getExtensionId(extensionDescription: string): Promise<string> {
|
||||
if (!/\.vsix$/i.test(extensionDescription)) {
|
||||
return extensionDescription;
|
||||
}
|
||||
|
||||
const zipPath = path.isAbsolute(extensionDescription) ? extensionDescription : path.join(process.cwd(), extensionDescription);
|
||||
const manifest = await getManifest(zipPath);
|
||||
return getId(manifest);
|
||||
}
|
||||
|
||||
const uninstalledExtensions: ILocalExtension[] = [];
|
||||
for (const extension of extensions) {
|
||||
const id = await getExtensionId(extension);
|
||||
const installed = await this.extensionManagementService.getInstalled();
|
||||
const extensionToUninstall = installed.find(e => areSameExtensions(e.identifier, { id }));
|
||||
if (!extensionToUninstall) {
|
||||
throw new Error(`${notInstalled(id)}\n${useId}`);
|
||||
}
|
||||
if (extensionToUninstall.type === ExtensionType.System) {
|
||||
console.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be installed", id));
|
||||
return;
|
||||
}
|
||||
if (extensionToUninstall.isBuiltin && !force) {
|
||||
console.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id));
|
||||
return;
|
||||
}
|
||||
console.log(localize('uninstalling', "Uninstalling {0}...", id));
|
||||
await this.extensionManagementService.uninstall(extensionToUninstall);
|
||||
uninstalledExtensions.push(extensionToUninstall);
|
||||
console.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id));
|
||||
}
|
||||
|
||||
if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) {
|
||||
await this.updateLocalizationsCache();
|
||||
}
|
||||
}
|
||||
|
||||
private async locateExtension(extensions: string[]): Promise<void> {
|
||||
const installed = await this.extensionManagementService.getInstalled();
|
||||
extensions.forEach(e => {
|
||||
installed.forEach(i => {
|
||||
if (i.identifier.id === e) {
|
||||
if (i.location.scheme === Schemas.file) {
|
||||
console.log(i.location.fsPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Flush the remaining data in AI adapter (with 1s timeout)
|
||||
return raceTimeout(combinedAppender(...appenders).flush(), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
private async updateLocalizationsCache(): Promise<void> {
|
||||
const localizationService = this.instantiationService.createInstance(LocalizationsService);
|
||||
await localizationService.update();
|
||||
localizationService.dispose();
|
||||
}
|
||||
}
|
||||
private async initServices(): Promise<[IInstantiationService, AppInsightsAppender[]]> {
|
||||
const services = new ServiceCollection();
|
||||
|
||||
const eventPrefix = 'monacoworkbench';
|
||||
// Environment
|
||||
const environmentService = new NativeEnvironmentService(this.argv);
|
||||
services.set(IEnvironmentService, environmentService);
|
||||
services.set(INativeEnvironmentService, environmentService);
|
||||
|
||||
export async function main(argv: NativeParsedArgs): Promise<void> {
|
||||
const services = new ServiceCollection();
|
||||
const disposables = new DisposableStore();
|
||||
// Init folders
|
||||
await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? mkdirp(path) : undefined));
|
||||
|
||||
const environmentService = new NativeEnvironmentService(argv);
|
||||
const logLevel = getLogLevel(environmentService);
|
||||
const loggers: ILogService[] = [];
|
||||
loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel));
|
||||
if (logLevel === LogLevel.Trace) {
|
||||
loggers.push(new ConsoleLogService(logLevel));
|
||||
}
|
||||
const logService = new MultiplexLogService(loggers);
|
||||
process.once('exit', () => logService.dispose());
|
||||
logService.info('main', argv);
|
||||
// Log
|
||||
const logLevel = getLogLevel(environmentService);
|
||||
const loggers: ILogService[] = [];
|
||||
loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel));
|
||||
if (logLevel === LogLevel.Trace) {
|
||||
loggers.push(new ConsoleLogService(logLevel));
|
||||
}
|
||||
|
||||
await Promise.all<void | undefined>([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath]
|
||||
.map((path): undefined | Promise<void> => path ? mkdirp(path) : undefined));
|
||||
const logService = this._register(new MultiplexLogService(loggers));
|
||||
services.set(ILogService, logService);
|
||||
|
||||
// Files
|
||||
const fileService = new FileService(logService);
|
||||
disposables.add(fileService);
|
||||
services.set(IFileService, fileService);
|
||||
// Files
|
||||
const fileService = this._register(new FileService(logService));
|
||||
services.set(IFileService, fileService);
|
||||
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(logService);
|
||||
disposables.add(diskFileSystemProvider);
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService));
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
|
||||
const configurationService = new ConfigurationService(environmentService.settingsResource, fileService);
|
||||
disposables.add(configurationService);
|
||||
await configurationService.initialize();
|
||||
// Configuration
|
||||
const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService));
|
||||
services.set(IConfigurationService, configurationService);
|
||||
|
||||
services.set(IEnvironmentService, environmentService);
|
||||
services.set(INativeEnvironmentService, environmentService);
|
||||
// Init config
|
||||
await configurationService.initialize();
|
||||
|
||||
services.set(ILogService, logService);
|
||||
services.set(IConfigurationService, configurationService);
|
||||
services.set(IStateService, new SyncDescriptor(StateService));
|
||||
services.set(IProductService, { _serviceBrand: undefined, ...product });
|
||||
// State
|
||||
const stateService = new StateService(environmentService, logService);
|
||||
services.set(IStateService, stateService);
|
||||
|
||||
const instantiationService: IInstantiationService = new InstantiationService(services);
|
||||
|
||||
return instantiationService.invokeFunction(async accessor => {
|
||||
const stateService = accessor.get(IStateService);
|
||||
// Product
|
||||
services.set(IProductService, { _serviceBrand: undefined, ...product });
|
||||
|
||||
const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService;
|
||||
|
||||
const services = new ServiceCollection();
|
||||
// Request
|
||||
services.set(IRequestService, new SyncDescriptor(RequestService));
|
||||
|
||||
// Extensions
|
||||
services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
|
||||
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
|
||||
services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService));
|
||||
|
||||
// Localizations
|
||||
services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService));
|
||||
|
||||
// Telemetry
|
||||
const appenders: AppInsightsAppender[] = [];
|
||||
if (isBuilt && !extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) {
|
||||
if (product.aiConfig && product.aiConfig.asimovKey) {
|
||||
appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey));
|
||||
appenders.push(new AppInsightsAppender('monacoworkbench', null, product.aiConfig.asimovKey));
|
||||
}
|
||||
|
||||
const config: ITelemetryServiceConfig = {
|
||||
appender: combinedAppender(...appenders),
|
||||
sendErrorTelemetry: false,
|
||||
commonProperties: resolveCommonProperties(product.commit, product.version, stateService.getItem('telemetry.machineId'), product.msftInternalDomains, installSourcePath),
|
||||
commonProperties: resolveCommonProperties(fileService, release(), process.arch, product.commit, product.version, stateService.getItem('telemetry.machineId'), product.msftInternalDomains, installSourcePath),
|
||||
piiPaths: [appRoot, extensionsPath]
|
||||
};
|
||||
|
||||
@ -434,17 +166,70 @@ export async function main(argv: NativeParsedArgs): Promise<void> {
|
||||
services.set(ITelemetryService, NullTelemetryService);
|
||||
}
|
||||
|
||||
const instantiationService2 = instantiationService.createChild(services);
|
||||
const main = instantiationService2.createInstance(Main);
|
||||
return [new InstantiationService(services), appenders];
|
||||
}
|
||||
|
||||
try {
|
||||
await main.run(argv);
|
||||
private registerErrorHandler(logService: ILogService): void {
|
||||
|
||||
// Flush the remaining data in AI adapter.
|
||||
// If it does not complete in 1 second, exit the process.
|
||||
await raceTimeout(combinedAppender(...appenders).flush(), 1000);
|
||||
} finally {
|
||||
disposables.dispose();
|
||||
// Install handler for unexpected errors
|
||||
setUnexpectedErrorHandler(error => {
|
||||
const message = toErrorMessage(error, true);
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
logService.error(`[uncaught exception in CLI]: ${message}`);
|
||||
});
|
||||
}
|
||||
|
||||
private async doRun(environmentService: INativeEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService): Promise<void> {
|
||||
|
||||
// Install Source
|
||||
if (this.argv['install-source']) {
|
||||
return this.setInstallSource(environmentService, this.argv['install-source']);
|
||||
}
|
||||
});
|
||||
|
||||
// List Extensions
|
||||
if (this.argv['list-extensions']) {
|
||||
return extensionManagementCLIService.listExtensions(!!this.argv['show-versions'], this.argv['category']);
|
||||
}
|
||||
|
||||
// Install Extension
|
||||
else if (this.argv['install-extension'] || this.argv['install-builtin-extension']) {
|
||||
return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.argv['install-builtin-extension'] || [], !!this.argv['do-not-sync'], !!this.argv['force']);
|
||||
}
|
||||
|
||||
// Uninstall Extension
|
||||
else if (this.argv['uninstall-extension']) {
|
||||
return extensionManagementCLIService.uninstallExtensions(this.asExtensionIdOrVSIX(this.argv['uninstall-extension']), !!this.argv['force']);
|
||||
}
|
||||
|
||||
// Locate Extension
|
||||
else if (this.argv['locate-extension']) {
|
||||
return extensionManagementCLIService.locateExtension(this.argv['locate-extension']);
|
||||
}
|
||||
|
||||
// Telemetry
|
||||
else if (this.argv['telemetry']) {
|
||||
console.log(buildTelemetryMessage(environmentService.appRoot, environmentService.extensionsPath));
|
||||
}
|
||||
}
|
||||
|
||||
private asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] {
|
||||
return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(process.cwd(), input)) : input);
|
||||
}
|
||||
|
||||
private setInstallSource(environmentService: INativeEnvironmentService, installSource: string): Promise<void> {
|
||||
return writeFile(environmentService.installSourcePath, installSource.slice(0, 30));
|
||||
}
|
||||
}
|
||||
|
||||
export async function main(argv: NativeParsedArgs): Promise<void> {
|
||||
const cliMain = new CliMain(argv);
|
||||
|
||||
try {
|
||||
await cliMain.run();
|
||||
} finally {
|
||||
cliMain.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,12 @@
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { isWindows, platform } from 'vs/base/common/platform';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { getSystemShell } from 'vs/base/node/shell';
|
||||
|
||||
/**
|
||||
* We need to get the environment from a user's shell.
|
||||
@ -58,7 +59,7 @@ export async function resolveShellEnv(logService: ILogService, args: NativeParse
|
||||
let unixShellEnvPromise: Promise<typeof process.env> | undefined = undefined;
|
||||
|
||||
async function doResolveUnixShellEnv(logService: ILogService): Promise<typeof process.env> {
|
||||
const promise = new Promise<typeof process.env>((resolve, reject) => {
|
||||
const promise = new Promise<typeof process.env>(async (resolve, reject) => {
|
||||
const runAsNode = process.env['ELECTRON_RUN_AS_NODE'];
|
||||
logService.trace('getUnixShellEnvironment#runAsNode', runAsNode);
|
||||
|
||||
@ -78,7 +79,8 @@ async function doResolveUnixShellEnv(logService: ILogService): Promise<typeof pr
|
||||
logService.trace('getUnixShellEnvironment#env', env);
|
||||
logService.trace('getUnixShellEnvironment#spawn', command);
|
||||
|
||||
const child = spawn(process.env.SHELL!, ['-ilc', command], {
|
||||
const systemShellUnix = await getSystemShell(platform);
|
||||
const child = spawn(systemShellUnix, ['-ilc', command], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'pipe', process.stderr],
|
||||
env
|
||||
|
Reference in New Issue
Block a user