tracks/node_modules/playwright/lib/mcp/browser/browserContextFactory.js

296 lines
12 KiB
JavaScript

"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var browserContextFactory_exports = {};
__export(browserContextFactory_exports, {
SharedContextFactory: () => SharedContextFactory,
contextFactory: () => contextFactory
});
module.exports = __toCommonJS(browserContextFactory_exports);
var import_crypto = __toESM(require("crypto"));
var import_fs = __toESM(require("fs"));
var import_net = __toESM(require("net"));
var import_path = __toESM(require("path"));
var playwright = __toESM(require("playwright-core"));
var import_registry = require("playwright-core/lib/server/registry/index");
var import_server = require("playwright-core/lib/server");
var import_log = require("../log");
var import_config = require("./config");
var import_server2 = require("../sdk/server");
function contextFactory(config) {
if (config.sharedBrowserContext)
return SharedContextFactory.create(config);
if (config.browser.remoteEndpoint)
return new RemoteContextFactory(config);
if (config.browser.cdpEndpoint)
return new CdpContextFactory(config);
if (config.browser.isolated)
return new IsolatedContextFactory(config);
return new PersistentContextFactory(config);
}
class BaseContextFactory {
constructor(name, config) {
this._logName = name;
this.config = config;
}
async _obtainBrowser(clientInfo) {
if (this._browserPromise)
return this._browserPromise;
(0, import_log.testDebug)(`obtain browser (${this._logName})`);
this._browserPromise = this._doObtainBrowser(clientInfo);
void this._browserPromise.then((browser) => {
browser.on("disconnected", () => {
this._browserPromise = void 0;
});
}).catch(() => {
this._browserPromise = void 0;
});
return this._browserPromise;
}
async _doObtainBrowser(clientInfo) {
throw new Error("Not implemented");
}
async createContext(clientInfo) {
(0, import_log.testDebug)(`create browser context (${this._logName})`);
const browser = await this._obtainBrowser(clientInfo);
const browserContext = await this._doCreateContext(browser);
await addInitScript(browserContext, this.config.browser.initScript);
return {
browserContext,
close: (afterClose) => this._closeBrowserContext(browserContext, browser, afterClose)
};
}
async _doCreateContext(browser) {
throw new Error("Not implemented");
}
async _closeBrowserContext(browserContext, browser, afterClose) {
(0, import_log.testDebug)(`close browser context (${this._logName})`);
if (browser.contexts().length === 1)
this._browserPromise = void 0;
await browserContext.close().catch(import_log.logUnhandledError);
await afterClose();
if (browser.contexts().length === 0) {
(0, import_log.testDebug)(`close browser (${this._logName})`);
await browser.close().catch(import_log.logUnhandledError);
}
}
}
class IsolatedContextFactory extends BaseContextFactory {
constructor(config) {
super("isolated", config);
}
async _doObtainBrowser(clientInfo) {
await injectCdpPort(this.config.browser);
const browserType = playwright[this.config.browser.browserName];
const tracesDir = await computeTracesDir(this.config, clientInfo);
if (tracesDir && this.config.saveTrace)
await startTraceServer(this.config, tracesDir);
return browserType.launch({
tracesDir,
...this.config.browser.launchOptions,
handleSIGINT: false,
handleSIGTERM: false
}).catch((error) => {
if (error.message.includes("Executable doesn't exist"))
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
throw error;
});
}
async _doCreateContext(browser) {
return browser.newContext(this.config.browser.contextOptions);
}
}
class CdpContextFactory extends BaseContextFactory {
constructor(config) {
super("cdp", config);
}
async _doObtainBrowser() {
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint, { headers: this.config.browser.cdpHeaders });
}
async _doCreateContext(browser) {
return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
}
}
class RemoteContextFactory extends BaseContextFactory {
constructor(config) {
super("remote", config);
}
async _doObtainBrowser() {
const url = new URL(this.config.browser.remoteEndpoint);
url.searchParams.set("browser", this.config.browser.browserName);
if (this.config.browser.launchOptions)
url.searchParams.set("launch-options", JSON.stringify(this.config.browser.launchOptions));
return playwright[this.config.browser.browserName].connect(String(url));
}
async _doCreateContext(browser) {
return browser.newContext();
}
}
class PersistentContextFactory {
constructor(config) {
this.name = "persistent";
this.description = "Create a new persistent browser context";
this._userDataDirs = /* @__PURE__ */ new Set();
this.config = config;
}
async createContext(clientInfo) {
await injectCdpPort(this.config.browser);
(0, import_log.testDebug)("create browser context (persistent)");
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo);
const tracesDir = await computeTracesDir(this.config, clientInfo);
if (tracesDir && this.config.saveTrace)
await startTraceServer(this.config, tracesDir);
this._userDataDirs.add(userDataDir);
(0, import_log.testDebug)("lock user data dir", userDataDir);
const browserType = playwright[this.config.browser.browserName];
for (let i = 0; i < 5; i++) {
const launchOptions = {
tracesDir,
...this.config.browser.launchOptions,
...this.config.browser.contextOptions,
handleSIGINT: false,
handleSIGTERM: false,
ignoreDefaultArgs: [
"--disable-extensions"
],
assistantMode: true
};
try {
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
await addInitScript(browserContext, this.config.browser.initScript);
const close = (afterClose) => this._closeBrowserContext(browserContext, userDataDir, afterClose);
return { browserContext, close };
} catch (error) {
if (error.message.includes("Executable doesn't exist"))
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
if (error.message.includes("ProcessSingleton") || error.message.includes("Invalid URL")) {
await new Promise((resolve) => setTimeout(resolve, 1e3));
continue;
}
throw error;
}
}
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
}
async _closeBrowserContext(browserContext, userDataDir, afterClose) {
(0, import_log.testDebug)("close browser context (persistent)");
(0, import_log.testDebug)("release user data dir", userDataDir);
await browserContext.close().catch(() => {
});
await afterClose();
this._userDataDirs.delete(userDataDir);
(0, import_log.testDebug)("close browser context complete (persistent)");
}
async _createUserDataDir(clientInfo) {
const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? import_registry.registryDirectory;
const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName;
const rootPath = (0, import_server2.firstRootPath)(clientInfo);
const rootPathToken = rootPath ? `-${createHash(rootPath)}` : "";
const result = import_path.default.join(dir, `mcp-${browserToken}${rootPathToken}`);
await import_fs.default.promises.mkdir(result, { recursive: true });
return result;
}
}
async function injectCdpPort(browserConfig) {
if (browserConfig.browserName === "chromium")
browserConfig.launchOptions.cdpPort = await findFreePort();
}
async function findFreePort() {
return new Promise((resolve, reject) => {
const server = import_net.default.createServer();
server.listen(0, () => {
const { port } = server.address();
server.close(() => resolve(port));
});
server.on("error", reject);
});
}
async function startTraceServer(config, tracesDir) {
if (!config.saveTrace)
return;
const server = await (0, import_server.startTraceViewerServer)();
const urlPrefix = server.urlPrefix("human-readable");
const url = urlPrefix + "/trace/index.html?trace=" + tracesDir + "/trace.json";
console.error("\nTrace viewer listening on " + url);
}
function createHash(data) {
return import_crypto.default.createHash("sha256").update(data).digest("hex").slice(0, 7);
}
async function addInitScript(browserContext, initScript) {
for (const scriptPath of initScript ?? [])
await browserContext.addInitScript({ path: import_path.default.resolve(scriptPath) });
}
class SharedContextFactory {
static create(config) {
if (SharedContextFactory._instance)
throw new Error("SharedContextFactory already exists");
const baseConfig = { ...config, sharedBrowserContext: false };
const baseFactory = contextFactory(baseConfig);
SharedContextFactory._instance = new SharedContextFactory(baseFactory);
return SharedContextFactory._instance;
}
constructor(baseFactory) {
this._baseFactory = baseFactory;
}
async createContext(clientInfo, abortSignal, toolName) {
if (!this._contextPromise) {
(0, import_log.testDebug)("create shared browser context");
this._contextPromise = this._baseFactory.createContext(clientInfo, abortSignal, toolName);
}
const { browserContext } = await this._contextPromise;
(0, import_log.testDebug)(`shared context client connected`);
return {
browserContext,
close: async () => {
(0, import_log.testDebug)(`shared context client disconnected`);
}
};
}
static async dispose() {
await SharedContextFactory._instance?._dispose();
}
async _dispose() {
const contextPromise = this._contextPromise;
this._contextPromise = void 0;
if (!contextPromise)
return;
const { close } = await contextPromise;
await close(async () => {
});
}
}
async function computeTracesDir(config, clientInfo) {
if (!config.saveTrace && !config.capabilities?.includes("tracing"))
return;
return await (0, import_config.outputFile)(config, clientInfo, `traces`, { origin: "code", reason: "Collecting trace" });
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SharedContextFactory,
contextFactory
});