mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 23:35:54 +01:00
Bootstrap Playwright service (#295261)
* Bootstrap Playwright service * feedback
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -50,6 +50,7 @@
|
||||
"native-keymap": "^3.3.5",
|
||||
"node-pty": "^1.2.0-beta.10",
|
||||
"open": "^10.1.2",
|
||||
"playwright-core": "^1.58.2",
|
||||
"tas-client": "0.3.1",
|
||||
"undici": "^7.18.2",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
@@ -2044,6 +2045,19 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/browser-chromium/node_modules/playwright-core": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
||||
@@ -13958,6 +13972,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/playwright-core": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
"native-keymap": "^3.3.5",
|
||||
"node-pty": "^1.2.0-beta.10",
|
||||
"open": "^10.1.2",
|
||||
"playwright-core": "^1.58.2",
|
||||
"tas-client": "0.3.1",
|
||||
"undici": "^7.18.2",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
|
||||
@@ -38,6 +38,7 @@ import { EncryptionMainService } from '../../platform/encryption/electron-main/e
|
||||
import { NativeBrowserElementsMainService, INativeBrowserElementsMainService } from '../../platform/browserElements/electron-main/nativeBrowserElementsMainService.js';
|
||||
import { ipcBrowserViewChannelName } from '../../platform/browserView/common/browserView.js';
|
||||
import { BrowserViewMainService, IBrowserViewMainService } from '../../platform/browserView/electron-main/browserViewMainService.js';
|
||||
import { BrowserViewCDPProxyServer, IBrowserViewCDPProxyServer } from '../../platform/browserView/electron-main/browserViewCDPProxyServer.js';
|
||||
import { NativeParsedArgs } from '../../platform/environment/common/argv.js';
|
||||
import { IEnvironmentMainService } from '../../platform/environment/electron-main/environmentMainService.js';
|
||||
import { isLaunchedFromCli } from '../../platform/environment/node/argvHelper.js';
|
||||
@@ -1040,6 +1041,7 @@ export class CodeApplication extends Disposable {
|
||||
services.set(INativeBrowserElementsMainService, new SyncDescriptor(NativeBrowserElementsMainService, undefined, false /* proxied to other processes */));
|
||||
|
||||
// Browser View
|
||||
services.set(IBrowserViewCDPProxyServer, new SyncDescriptor(BrowserViewCDPProxyServer, undefined, true));
|
||||
services.set(IBrowserViewMainService, new SyncDescriptor(BrowserViewMainService, undefined, false /* proxied to other processes */));
|
||||
|
||||
// Keyboard Layout
|
||||
@@ -1202,6 +1204,7 @@ export class CodeApplication extends Disposable {
|
||||
// Browser View
|
||||
const browserViewChannel = ProxyChannel.fromService(accessor.get(IBrowserViewMainService), disposables);
|
||||
mainProcessElectronServer.registerChannel(ipcBrowserViewChannelName, browserViewChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel(ipcBrowserViewChannelName, browserViewChannel));
|
||||
|
||||
// Signing
|
||||
const signChannel = ProxyChannel.fromService(accessor.get(ISignService), disposables);
|
||||
|
||||
@@ -134,6 +134,8 @@ import { IMcpGalleryManifestService } from '../../../platform/mcp/common/mcpGall
|
||||
import { McpGalleryManifestIPCService } from '../../../platform/mcp/common/mcpGalleryManifestServiceIpc.js';
|
||||
import { IMeteredConnectionService } from '../../../platform/meteredConnection/common/meteredConnection.js';
|
||||
import { MeteredConnectionChannelClient, METERED_CONNECTION_CHANNEL } from '../../../platform/meteredConnection/common/meteredConnectionIpc.js';
|
||||
import { IPlaywrightService } from '../../../platform/browserView/common/playwrightService.js';
|
||||
import { PlaywrightService } from '../../../platform/browserView/node/playwrightService.js';
|
||||
|
||||
class SharedProcessMain extends Disposable implements IClientConnectionFilter {
|
||||
|
||||
@@ -401,6 +403,9 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
|
||||
// Web Content Extractor
|
||||
services.set(ISharedWebContentExtractorService, new SyncDescriptor(SharedWebContentExtractorService));
|
||||
|
||||
// Playwright
|
||||
services.set(IPlaywrightService, new SyncDescriptor(PlaywrightService));
|
||||
|
||||
return new InstantiationService(services);
|
||||
}
|
||||
|
||||
@@ -467,6 +472,10 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
|
||||
// Web Content Extractor
|
||||
const webContentExtractorChannel = ProxyChannel.fromService(accessor.get(ISharedWebContentExtractorService), this._store);
|
||||
this.server.registerChannel('sharedWebContentExtractor', webContentExtractorChannel);
|
||||
|
||||
// Playwright
|
||||
const playwrightChannel = ProxyChannel.fromService(accessor.get(IPlaywrightService), this._store);
|
||||
this.server.registerChannel('playwright', playwrightChannel);
|
||||
}
|
||||
|
||||
private registerErrorHandler(logService: ILogService): void {
|
||||
|
||||
@@ -275,4 +275,9 @@ export interface IBrowserViewService {
|
||||
* @param id The browser view identifier
|
||||
*/
|
||||
clearStorage(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get a CDP WebSocket endpoint URL.
|
||||
*/
|
||||
getDebugWebSocketEndpoint(): Promise<string>;
|
||||
}
|
||||
|
||||
18
src/vs/platform/browserView/common/playwrightService.ts
Normal file
18
src/vs/platform/browserView/common/playwrightService.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
|
||||
export const IPlaywrightService = createDecorator<IPlaywrightService>('playwrightService');
|
||||
|
||||
/**
|
||||
* A service for using Playwright to connect to and automate the integrated browser.
|
||||
*/
|
||||
export interface IPlaywrightService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
// TODO@kycutler: define a more specific API.
|
||||
initialize(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import type * as http from 'http';
|
||||
import { AddressInfo, Socket } from 'net';
|
||||
import { upgradeToISocket } from '../../../base/parts/ipc/node/ipc.net.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { VSBuffer } from '../../../base/common/buffer.js';
|
||||
import { CDPBrowserProxy } from '../common/cdp/proxy.js';
|
||||
import { CDPEvent, CDPRequest, CDPError, CDPErrorCode, ICDPBrowserTarget, ICDPConnection } from '../common/cdp/types.js';
|
||||
import { disposableTimeout } from '../../../base/common/async.js';
|
||||
import { ISocket } from '../../../base/parts/ipc/common/ipc.net.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
|
||||
export const IBrowserViewCDPProxyServer = createDecorator<IBrowserViewCDPProxyServer>('browserViewCDPProxyServer');
|
||||
|
||||
export interface IBrowserViewCDPProxyServer {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Returns a debug endpoint with a short-lived, single-use token.
|
||||
*/
|
||||
getWebSocketEndpoint(): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket server that provides CDP debugging for browser views.
|
||||
*/
|
||||
export class BrowserViewCDPProxyServer extends Disposable implements IBrowserViewCDPProxyServer {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private server: http.Server | undefined;
|
||||
private port: number | undefined;
|
||||
private readonly tokens: TokenManager;
|
||||
|
||||
constructor(
|
||||
private readonly browserTarget: ICDPBrowserTarget,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tokens = this._register(new TokenManager());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a debug endpoint with a short-lived, single-use token in the
|
||||
* WebSocket URL. The token is revoked once a WebSocket connection is made
|
||||
* or after 30 seconds, whichever comes first.
|
||||
*/
|
||||
async getWebSocketEndpoint(): Promise<string> {
|
||||
await this.ensureServerStarted();
|
||||
|
||||
const token = await this.tokens.issueToken();
|
||||
return this.getWebSocketUrl(token);
|
||||
}
|
||||
|
||||
private getWebSocketUrl(token: string): string {
|
||||
return `ws://localhost:${this.port}/devtools/browser?token=${token}`;
|
||||
}
|
||||
|
||||
private async ensureServerStarted(): Promise<void> {
|
||||
if (this.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
const http = await import('http');
|
||||
this.server = http.createServer();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// Only listen on localhost to prevent external access
|
||||
this.server!.listen(0, '127.0.0.1', () => resolve());
|
||||
this.server!.once('error', reject);
|
||||
});
|
||||
|
||||
const address = this.server.address() as AddressInfo;
|
||||
this.port = address.port;
|
||||
|
||||
this.server.on('request', (req, res) => this.handleHttpRequest(req, res));
|
||||
this.server.on('upgrade', (req: http.IncomingMessage, socket: Socket) => this.handleWebSocketUpgrade(req, socket));
|
||||
}
|
||||
|
||||
private async handleHttpRequest(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
this.logService.debug(`[BrowserViewDebugProxy] HTTP request at ${_req.url}`);
|
||||
// No support for HTTP endpoints for now.
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
|
||||
private handleWebSocketUpgrade(req: http.IncomingMessage, socket: Socket): void {
|
||||
const [pathname, params] = (req.url || '').split('?');
|
||||
|
||||
const token = new URLSearchParams(params).get('token');
|
||||
if (!token || !this.tokens.consumeToken(token)) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const browserMatch = pathname.match(/^\/devtools\/browser(\/.*)?$/);
|
||||
|
||||
this.logService.debug(`[BrowserViewDebugProxy] WebSocket upgrade requested: ${pathname}`);
|
||||
|
||||
if (!browserMatch) {
|
||||
this.logService.warn(`[BrowserViewDebugProxy] Rejecting WebSocket on unknown path: ${pathname}`);
|
||||
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.debug(`[BrowserViewDebugProxy] WebSocket connected: ${pathname}`);
|
||||
|
||||
const upgraded = upgradeToISocket(req, socket, {
|
||||
debugLabel: 'browser-view-cdp-' + generateUuid(),
|
||||
enableMessageSplitting: false,
|
||||
});
|
||||
|
||||
if (!upgraded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const proxy = new CDPBrowserProxy(this.browserTarget);
|
||||
const disposables = this.wireWebSocket(upgraded, proxy);
|
||||
this._register(disposables);
|
||||
this._register(upgraded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire a WebSocket (ISocket) to an ICDPConnection bidirectionally.
|
||||
* Returns a DisposableStore that cleans up all subscriptions.
|
||||
*/
|
||||
private wireWebSocket(upgraded: ISocket, connection: ICDPConnection): DisposableStore {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Socket -> Connection: parse JSON, call sendMessage, write response/error
|
||||
disposables.add(upgraded.onData((rawData: VSBuffer) => {
|
||||
try {
|
||||
const message = rawData.toString();
|
||||
const { id, method, params, sessionId } = JSON.parse(message) as CDPRequest;
|
||||
this.logService.debug(`[BrowserViewDebugProxy] <- ${message}`);
|
||||
connection.sendMessage(method, params, sessionId)
|
||||
.then((result: unknown) => {
|
||||
const response = { id, result, sessionId };
|
||||
const responseStr = JSON.stringify(response);
|
||||
this.logService.debug(`[BrowserViewDebugProxy] -> ${responseStr}`);
|
||||
upgraded.write(VSBuffer.fromString(responseStr));
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
const response = {
|
||||
id,
|
||||
error: {
|
||||
code: error instanceof CDPError ? error.code : CDPErrorCode.ServerError,
|
||||
message: error.message || 'Unknown error'
|
||||
},
|
||||
sessionId
|
||||
};
|
||||
const responseStr = JSON.stringify(response);
|
||||
this.logService.debug(`[BrowserViewDebugProxy] -> ${responseStr}`);
|
||||
upgraded.write(VSBuffer.fromString(responseStr));
|
||||
});
|
||||
} catch (error) {
|
||||
this.logService.error('[BrowserViewDebugProxy] Error parsing message:', error);
|
||||
upgraded.end();
|
||||
}
|
||||
}));
|
||||
|
||||
// Connection -> Socket: serialize events and write
|
||||
disposables.add(connection.onEvent((event: CDPEvent) => {
|
||||
const eventStr = JSON.stringify(event);
|
||||
this.logService.debug(`[BrowserViewDebugProxy] -> ${eventStr}`);
|
||||
upgraded.write(VSBuffer.fromString(eventStr));
|
||||
}));
|
||||
|
||||
// Connection close -> close socket
|
||||
disposables.add(connection.onClose(() => {
|
||||
this.logService.debug(`[BrowserViewDebugProxy] WebSocket closing`);
|
||||
upgraded.end();
|
||||
}));
|
||||
|
||||
// Socket closed -> cleanup
|
||||
disposables.add(upgraded.onClose(() => {
|
||||
this.logService.debug(`[BrowserViewDebugProxy] WebSocket closed`);
|
||||
connection.dispose();
|
||||
disposables.dispose();
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.server = undefined;
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class TokenManager extends Disposable {
|
||||
/** Map of currently valid single-use tokens. Each expires after 30 seconds. */
|
||||
private readonly tokens = new Map<string, { expiresAt: number }>();
|
||||
|
||||
/**
|
||||
* Creates a short-lived, single-use token.
|
||||
* The token is revoked once consumed or after 30 seconds.
|
||||
*/
|
||||
async issueToken(): Promise<string> {
|
||||
const token = this.makeToken();
|
||||
this.tokens.set(token, { expiresAt: Date.now() + 30_000 });
|
||||
this._register(disposableTimeout(() => this.tokens.delete(token), 30_000));
|
||||
return token;
|
||||
}
|
||||
|
||||
consumeToken(token: string): boolean {
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
const info = this.tokens.get(token);
|
||||
if (!info) {
|
||||
return false;
|
||||
}
|
||||
this.tokens.delete(token);
|
||||
return Date.now() <= info.expiresAt;
|
||||
}
|
||||
|
||||
private makeToken(): string {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
||||
const binary = Array.from(bytes).map(b => String.fromCharCode(b)).join('');
|
||||
const base64 = btoa(binary);
|
||||
const urlSafeToken = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
|
||||
return urlSafeToken;
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,15 @@ import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { BrowserViewUri } from '../common/browserViewUri.js';
|
||||
import { IWindowsMainService } from '../../windows/electron-main/windows.js';
|
||||
import { BrowserSession } from './browserSession.js';
|
||||
import { IBrowserViewCDPProxyServer } from './browserViewCDPProxyServer.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { CDPBrowserProxy } from '../common/cdp/proxy.js';
|
||||
|
||||
export const IBrowserViewMainService = createDecorator<IBrowserViewMainService>('browserViewMainService');
|
||||
|
||||
export interface IBrowserViewMainService extends IBrowserViewService, ICDPBrowserTarget {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
tryGetBrowserView(id: string): BrowserView | undefined;
|
||||
}
|
||||
|
||||
@@ -48,7 +51,8 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
|
||||
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
@IProductService private readonly productService: IProductService
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IBrowserViewCDPProxyServer private readonly cdpProxyServer: IBrowserViewCDPProxyServer,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -359,4 +363,8 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
|
||||
);
|
||||
await browserSession.electronSession.clearData();
|
||||
}
|
||||
|
||||
async getDebugWebSocketEndpoint(): Promise<string> {
|
||||
return this.cdpProxyServer.getWebSocketEndpoint();
|
||||
}
|
||||
}
|
||||
|
||||
85
src/vs/platform/browserView/node/playwrightService.ts
Normal file
85
src/vs/platform/browserView/node/playwrightService.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 '../../../base/common/lifecycle.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IBrowserViewService, ipcBrowserViewChannelName } from '../common/browserView.js';
|
||||
import { IPlaywrightService } from '../common/playwrightService.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import type { Browser } from 'playwright-core';
|
||||
|
||||
/**
|
||||
* Shared-process implementation of {@link IPlaywrightService}.
|
||||
*/
|
||||
export class PlaywrightService extends Disposable implements IPlaywrightService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly browserViewService: IBrowserViewService;
|
||||
private _browser: Browser | undefined;
|
||||
private _initPromise: Promise<void> | undefined;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const channel = mainProcessService.getChannel(ipcBrowserViewChannelName);
|
||||
this.browserViewService = ProxyChannel.toService<IBrowserViewService>(channel);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this._browser?.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._initPromise) {
|
||||
return this._initPromise;
|
||||
}
|
||||
|
||||
this._initPromise = (async () => {
|
||||
try {
|
||||
this.logService.debug('[PlaywrightService] Connecting to browser via CDP');
|
||||
|
||||
const playwright = await import('playwright-core');
|
||||
const endpoint = await this.browserViewService.getDebugWebSocketEndpoint();
|
||||
const browser = await playwright.chromium.connectOverCDP(endpoint);
|
||||
|
||||
this.logService.debug('[PlaywrightService] Connected to browser');
|
||||
|
||||
browser.on('disconnected', () => {
|
||||
this.logService.debug('[PlaywrightService] Browser disconnected');
|
||||
if (this._browser === browser) {
|
||||
this._browser = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// This can happen if the service was disposed while we were waiting for the connection. In that case, clean up immediately.
|
||||
if (this._initPromise === undefined) {
|
||||
browser.close().catch(() => { /* ignore */ });
|
||||
throw new Error('PlaywrightService was disposed during initialization');
|
||||
}
|
||||
|
||||
this._browser = browser;
|
||||
} finally {
|
||||
this._initPromise = undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
return this._initPromise;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
if (this._browser) {
|
||||
this._browser.close().catch(() => { /* ignore */ });
|
||||
this._browser = undefined;
|
||||
}
|
||||
this._initPromise = undefined;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,11 @@ export interface IBrowserViewWorkbenchService {
|
||||
* Clear all storage data for the current workspace browser session
|
||||
*/
|
||||
clearWorkspaceStorage(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the endpoint for connecting to a browser view's CDP proxy server
|
||||
*/
|
||||
getDebugWebSocketEndpoint(): Promise<string>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -54,4 +54,8 @@ export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService
|
||||
const workspaceId = this.workspaceContextService.getWorkspace().id;
|
||||
return this._browserViewService.clearWorkspaceStorage(workspaceId);
|
||||
}
|
||||
|
||||
async getDebugWebSocketEndpoint() {
|
||||
return this._browserViewService.getDebugWebSocketEndpoint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IPlaywrightService } from '../../../../platform/browserView/common/playwrightService.js';
|
||||
import { registerSharedProcessRemoteService } from '../../../../platform/ipc/electron-browser/services.js';
|
||||
|
||||
registerSharedProcessRemoteService(IPlaywrightService, 'playwright');
|
||||
@@ -91,6 +91,7 @@ import '../platform/userDataProfile/electron-browser/userDataProfileStorageServi
|
||||
import './services/auxiliaryWindow/electron-browser/auxiliaryWindowService.js';
|
||||
import '../platform/extensionManagement/electron-browser/extensionsProfileScannerService.js';
|
||||
import '../platform/webContentExtractor/electron-browser/webContentExtractorService.js';
|
||||
import './services/browserView/electron-browser/playwrightWorkbenchService.js';
|
||||
import './services/process/electron-browser/processService.js';
|
||||
import './services/power/electron-browser/powerService.js';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user