ayakashi
Version:
The next generation web scraping framework
199 lines (198 loc) • 8.53 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getInstance = void 0;
const CDP = require("chrome-remote-interface");
const debug_1 = __importDefault(require("debug"));
const launcher_1 = require("./launcher");
const createTarget_1 = require("./createTarget");
const opLog_1 = require("../opLog/opLog");
const retryOnErrorOrTimeout_1 = require("../utils/retryOnErrorOrTimeout");
const d = debug_1.default("ayakashi:engine:browser");
const HOST = "localhost";
const MAX_LAUNCH_ATTEMPTS = 3;
function getInstance() {
const SIGINT = "SIGINT";
let isHeadless = true;
let isPersistentSession = false;
let masterConnection;
const opLog = opLog_1.getOpLog();
return {
chromeInstance: null,
init: function (options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.chromeInstance)
return;
if (!options)
throw new Error("init_options_not_set");
if (!options.chromePath) {
throw new Error("chrome_path_not_set");
}
this.chromeInstance = yield launchChrome({
chromePath: options.chromePath,
headless: options.headless === false ? false : true,
autoOpenDevTools: options.autoOpenDevTools === true ? true : false,
launchAttempts: options.launchAttempts || MAX_LAUNCH_ATTEMPTS,
windowSize: `${options.windowWidth || 1920},${options.windowHeight || 1080}`,
proxyUrl: options.proxyUrl || "",
sessionDir: options.sessionDir || "",
protocolPort: options.protocolPort
});
if (!this.chromeInstance) {
throw new Error("chrome_not_launched");
}
isHeadless = options.headless !== false;
isPersistentSession = !!options.sessionDir;
try {
masterConnection = yield retryOnErrorOrTimeout_1.retryOnErrorOrTimeOut(() => {
return getMasterConnection(HOST, this.chromeInstance.port);
});
const sigintListener = () => __awaiter(this, void 0, void 0, function* () {
d("trap SIGINT, closing chrome");
yield this.close();
process.removeListener(SIGINT, sigintListener);
});
process.on(SIGINT, sigintListener);
}
catch (err) {
d(err);
throw new Error("could not establish master connection");
}
});
},
close: function () {
return __awaiter(this, void 0, void 0, function* () {
let forceKill = false;
try {
if (this.chromeInstance) {
yield masterConnection.Browser.close();
this.chromeInstance = null;
}
}
catch (err) {
d(err);
forceKill = true;
}
try {
if (forceKill && this.chromeInstance) {
d("force killing chrome...");
yield this.chromeInstance.forceKill();
}
}
catch (err) {
d(err);
opLog.error("Failed to close chrome");
}
});
},
createTarget: function () {
return __awaiter(this, void 0, void 0, function* () {
if (this.chromeInstance) {
try {
return createTarget_1.createTarget(HOST, this.chromeInstance.port, masterConnection, isHeadless && !isPersistentSession);
}
catch (err) {
d(err);
throw new Error("could_not_create_target");
}
}
else {
return null;
}
});
},
destroyTarget: function (targetId, browserContextId) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.chromeInstance)
return;
if (browserContextId) {
d("disposing browserContextId", browserContextId);
yield masterConnection.Target.disposeBrowserContext({
browserContextId: browserContextId
});
}
d("closing target", targetId);
yield masterConnection.Target.closeTarget({
targetId: targetId
});
});
}
};
}
exports.getInstance = getInstance;
function launchChrome(options) {
return __awaiter(this, void 0, void 0, function* () {
const opLog = opLog_1.getOpLog();
d(`Launching Chrome instance. Headless: ${options.headless}`);
const flags = [];
if (options.headless) {
flags.push("--headless");
}
if (options.autoOpenDevTools) {
flags.push("--auto-open-devtools-for-tabs");
}
if (options.proxyUrl) {
flags.push(`--proxy-server=${options.proxyUrl}`);
}
flags.push(`--window-size=${options.windowSize}`);
flags.push("--allow-insecure-localhost");
let attempt = 0;
let instance = null;
while (!instance && (attempt += 1) < options.launchAttempts) {
d(`Launching Chrome. Attempt ${attempt}/${options.launchAttempts}...`);
try {
instance = yield launcher_1.launch(flags, {
chromePath: options.chromePath,
sessionDir: options.sessionDir,
protocolPort: options.protocolPort
});
}
catch (err) {
d(`Can not launch Chrome in attempt ${attempt}/${options.launchAttempts}. Error code: ${err.code}`, err);
if (err.code === "EAGAIN" || err.code === "ECONNREFUSED") {
yield sleep(attempt * 1000);
if (attempt >= options.launchAttempts) {
throw err;
}
}
else {
throw err;
}
}
}
if (!instance) {
if (process.platform === "linux") {
opLog.warn("If you are running this on a headless linux server, you might be missing some dependencies.");
opLog.warn("Learn how to fix it here: https://ayakashi-io.github.io/docs/installation#installing-missing-chromium-dependencies-on-a-linux-server");
}
throw new Error(`Can't launch Chrome! (attempts: ${attempt - 1}/${options.launchAttempts})`);
}
return instance;
});
}
function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay));
}
function getMasterConnection(host, port) {
return __awaiter(this, void 0, void 0, function* () {
const { webSocketDebuggerUrl } = yield CDP.Version({ host, port });
const masterConnection = yield CDP({
target: webSocketDebuggerUrl || `ws://${host}:${port}/devtools/browser`
});
if (!masterConnection.Target) {
throw new Error("could not establish master connection");
}
return masterConnection;
});
}