mirror of
https://github.com/TracksApp/tracks.git
synced 2025-12-19 16:50:12 +01:00
296 lines
12 KiB
JavaScript
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
|
|
});
|