blacktraffic/shared_resources/AgentBrowserPuppeteer.js

if (!global.Blacktraffic) global.Blacktraffic = {};

/**
 * Refer to <span color = "yellow">{@link Blacktraffic.AgentBrowser}</span> for methods or fields inherited from automatic destructuring.
 * 
 * Creates a new Puppeteer browser agent (Chrome) used for scraping tasks and purposes. Requires npm puppeteer.
 * 
 * ##### Constructor:
 * - `arg0_key=Class.generateRandomID(Blacktraffic.AgentBrowserPuppeteer)`: {@link string} - The key to use for the given browser agent. Used for ID.
 * - `arg1_options`: {@link Object}
 *   - `.chrome_binary_path`: {@link string}
 *   - `.debug_console=false`: {@link boolean}
 *   - `.debugging_port=0`: {@link number}
 *   - `.headless=false`: {@link boolean}
 *   - `.onload`: {@link function}
 *   - `.user_data_folder`: {@link string} - Refers to a Chrome profile necessary for spoofing.
 *   - 
 *   - `.connection_attempts_threshold=3`: {@link number} - The number of connection attempts to use when opening the browser.
 *   - `.log_channel="console"`: {@link string}
 *   - `.unique_ports=false`: {@link boolean}
 *   
 * ##### Instance:
 * - `.key`: {@link string}
 * - `.options`: {@link Object}
 *   - `.onload`: {@link function}
 * - `.tab_obj`: {@link Object}<{@link Object}>
 * 
 * ##### Methods:
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.captureConsoleToChannel|captureConsoleToChannel}</span>(arg0_tab_key:{@link Object}|{@link string}, arg1_channel_key:{@link string})
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.close|close}</span>() | {@link Blacktraffic.AgentBrowserPuppeteer}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.closeTab|closeTab}</span>(arg0_tab_key:{@link Object}|{@link string}) | {@link Blacktraffic.AgentBrowserPuppeteer}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.closeUserTabs|closeUserTabs}</span>() | {@link Blacktraffic.AgentBrowserPuppeteer}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.focusTab|focusTab}</span>(arg0_tab_key:{@link Object}|{@link string}) | {@link Object}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.getElement|getElement}</span>(arg0_tab_key:{@link Object}|{@link string}, arg1_selector:{@link string}, arg2_options:{@link Object}) | {@link HTMLElement}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.getTab|getTab}</span>(arg0_tab_key:{@link Object}|{@link string}) | {@link Object}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.getTabs|getTabs}</span>() | {@link Array}<{@link Object}>
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.injectScript|injectScript}</span>(arg0_tab_key:{@link Object}|{@link string}, arg1_function:{@link function}, arg2_options:{@link Object}) | {@link Object}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.injectScriptOnload|injectScriptOnload}</span>(arg0_tab_key:{@link Object}|{@link string}, arg1_function:{@link function}, arg2_options:{@link Object}) | {@link Object}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.open|open}</span>() | {@link Blacktraffic.AgentBrowserPuppeteer}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.openTab|openTab}</span>(arg0_tab_key:{@link Object}|{@link string}, arg1_url:{@link string}) | {@link Object}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.reloadTab|reloadTab}</span>(arg0_tab_key:{@link Object}|{@link string}) | {@link Object}
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.remove|remove}</span>()
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.screenshotHTML|screenshotHTML}</span>(arg0_tab_key:{@link Object}|{@link string}, arg1_path:{@link string}, arg2_options:{@link Object})
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.tabExists|tabExists}</span>(arg0_tab_key:{@link Object}|{@link string}, arg1_options:{@link Object})
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.updateLogChannel|updateLogChannel}</span>(arg0_channel_key:{@link string})
 * - <span color=00ffff>{@link Blacktraffic.AgentBrowserPuppeteer.waitForStableContent|waitForStableContent}</span>(arg0_tab_key:{@link Object}|{@link string}, arg1_selector:{@link string}, arg2_interval=3000:{@link number}) | {@link boolean}
 * 
 * ##### Static Fields:
 * - `.instances`: {@link Array}<{@link Blacktraffic.AgentBrowserPuppeteer}>
 * 
 * @augments Blacktraffic.AgentBrowser
 * @memberof Blacktraffic.AgentBrowser
 * @type {Blacktraffic.AgentBrowserPuppeteer}
 */
Blacktraffic.AgentBrowserPuppeteer = class extends Blacktraffic.AgentBrowser { //[WIP] - Finish documentation
	static instances = [];
	
	constructor (arg0_key, arg1_options) {
		//Convert from parameters
		let key = (arg0_key) ? arg0_key : Class.generateRandomID(Blacktraffic.AgentBrowserPuppeteer);
		let options = (arg1_options) ? arg1_options : {};
			super(key, options);
		
		//Initialise options
		if (options.debug_console === undefined) options.debug_console = false;
		if (options.headless === undefined) options.headless = false;
		if (options.unique_ports === undefined) options.unique_ports = false;
		
		options.connection_attempts_threshold = Math.returnSafeNumber(options.connection_attempts_threshold, 3);
		
		//Declare local instance variables
		this.updateLogChannel(options.log_channel);
		this.key = key;
		this.options = options;
		this.tab_obj = {};
		
		//Initialise and push to instances
		this.open().then(() => {
			if (this.options.onload)
				this.options.onload.call(this);
		});
		Blacktraffic.AgentBrowserPuppeteer.instances.push(this);
	}
	
	/**
	 * Captures the current tab's console and feeds it into a {@link log.Channel}.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 * 
	 * @alias captureConsoleToChannel
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @param {Object|string} arg0_tab_key
	 * @param {string} arg1_channel_key
	 * 
	 * @returns {Promise<void>}
	 */
	async captureConsoleToChannel (arg0_tab_key, arg1_channel_key) {
		//Convert from parameters
		let tab = this.getTab(arg0_tab_key);
		let channel_key = arg1_channel_key;
		
		//Declare local instance variables
		let log_obj = log.getLoggingFunctions(channel_key);
		
		//Set listener on tab page if possible
		tab.on("console", async (message) => {
			let args = await Promise.all(message.args().map((local_arg) => local_arg.jsonValue()));
			let type = message.type();
			
			log_obj.log_fn(`${tab.url()} [${type.toUpperCase()}]:`, ...args);
		});
	}
	
	/**
	 * Closes the browser currently mounted to the AgentBrowser.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 * 
	 * @returns {Promise<Blacktraffic.AgentBrowserPuppeteer>}
	 */
	async close () {
		//Close browser first
		if (this.browser) await this.browser.close();
		this.browser = undefined;
		
		//Return statement
		return this;
	}
	
	/**
	 * Closes the tab specified.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 * 
	 * @alias closeTab
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @param {Object|string} arg0_tab_key
	 * 
	 * @returns {Promise<Blacktraffic.AgentBrowserPuppeteer>}
	 */
	async closeTab (arg0_tab_key) {
		//Convert from parameters
		let tab_obj = this.getTab(arg0_tab_key);
		
		//Attempt to close the tab if found
		if (tab_obj) {
			await tab_obj.close();
			if (this.browser && !this.browser.isConnected())
				this.browser = undefined;
		}
		
		//Return statement
		return this;
	}
	
	/**
	 * Closes all user tabs from the current browser.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 * 
	 * @alias closeUserTabs
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @returns {Promise<Blacktraffic.AgentBrowserPuppeteer>}
	 */
	async closeUserTabs () {
		//Declare local instance variables
		let all_tab_keys = Object.keys(this.tab_obj);
		
		//Iterate over all_tab_keys and determine which tabs do not have a _blacktraffic_key, then close them
		for (let i = 0; i < all_tab_keys.length; i++) {
			let local_tab = this.tab_obj[all_tab_keys[i]];
			
			if (!local_tab._blacktraffic_key)
				await this.closeTab(local_tab);
		}
		
		//Return statement
		return this;
	}
	
	/**
	 * Focuses the specified tab.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias focusTab
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 *
	 * @param {Object|string} arg0_tab_key
	 *
	 * @returns {Promise<Object|undefined>}
	 */
	async focusTab (arg0_tab_key) {
		//Convert from parameters
		let tab_key = arg0_tab_key;
		
		//Declare local instance variables
		let tab_obj = this.getTab(tab_key);
		
		//Focus the current tab
		if (tab_obj) {
			await tab_obj.bringToFront();
		} else {
			this.warn_fn(`Blacktraffic.AgentBrowserPuppeteer: Could not focus ${tab_key}, as it doesn't exist.`)
		}
		
		//Return statement
		return tab_obj;
	}
	
	/**
	 * Fetches a specific element handle using CSS selectors.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias getElement
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @param {Object|string} arg0_tab_key
	 * @param {string} arg1_selector
	 * @param {Object} [arg2_options]
	 *  @param {number} [arg2_options.timeout=10000]
	 * 
	 * @returns {Promise<HTMLElement>}
	 */
	async getElement (arg0_tab_key, arg1_selector, arg2_options) {
		//Convert from parameters
		let tab_obj = this.getTab(arg0_tab_key);
		let selector = arg1_selector;
		let options = (arg2_options) ? arg2_options : {};
		
		//Initialise options
		options.timeout = Math.returnSafeNumber(options.timeout, 10000);
		
		//Return statement
		return await tab_obj.waitForSelector(selector, {
			visible: true,
			timeout: options.timeout,
		});
	}
	
	/**
	 * Returns a tab object based on its key.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias getTab
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @param {Object|string} arg0_tab_key
	 * 
	 * @returns {Object}
	 */
	getTab (arg0_tab_key) {
		//Convert from parameters
		let tab_key = arg0_tab_key;
		
		if (typeof tab_key === "object") return tab_key; //Internal guard clause if tab_key is already a tab object
		
		//Return statement
		return this.tab_obj[tab_key];
	}
	
	/**
	 * Returns all tabs in the current browser.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias getTabs
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @returns {Promise<Object[]>}
	 */
	async getTabs () {
		//Return statement
		if (this.browser)
			return await this.browser.pages();
	}
	
	/**
	 * Injects a script within the current tab. 
	 * **Note.** Contexts are fully isolated when passing a function.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias injectScript
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @param {string} arg0_tab_key
	 * @param {function} arg1_function
	 *  @param {Object} [arg2_options]
	 *  	@param {Object} [arg2_options.options] - Any options to pass down to the local function.
	 * 
	 * @returns {Promise<Object>}
	 */
	async injectScript (arg0_tab_key, arg1_function, arg2_options) {
		//Convert from parameters
		let tab_obj = this.getTab(arg0_tab_key);
		let local_function = arg1_function;
		let options = (arg2_options) ? arg2_options : {};
		
		//Inject script if possible
		if (tab_obj && local_function)
			await tab_obj.evaluate(local_function, (options.options) ? options.options : {});
		
		//Return statement
		return tab_obj;
	}
	
	/**
	 * Registers an onload script for future page visits using the mounted tab. 
	 * **Note.** Contexts are fully isolated when passing a function.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias injectScriptOnload
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @param {Object|string} arg0_tab_key
	 * @param {function} arg1_function
	 * @param {Object} [arg2_options]
	 *  @param {Object} [arg2_options.options] - Any options to pass down to the local function.
	 *  @param {string} [arg2_options.url]
	 * 
	 * @returns {Promise<Object>}
	 */
	async injectScriptOnload (arg0_tab_key, arg1_function, arg2_options) {
		//Convert from parameters
		let tab_obj = this.getTab(arg0_tab_key);
		let local_function = arg1_function;
		let options = (arg2_options) ? arg2_options : {};
		
		//Inject script at document start if possible
		if (tab_obj && local_function) {
			await tab_obj.evaluateOnNewDocument(local_function, (options.options) ? options.options : {});
			if (options.url) await tab_obj.goto(options.url, { waitUntil: "networkidle2" });
		}
		
		//Return statement
		return tab_obj;
	}
	
	/**
	 * Initialises a Chrome instance and connects Puppeteer.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias open
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @returns {Promise<Blacktraffic.AgentBrowserPuppeteer>}
	 */
	async open () {
		//Declare local instance variables
		let attempts = 0;
		
		//Iterate over all attempts until threshold or the for loop exits
		for (let i = 0; i < this.options.connection_attempts_threshold; i++)
			try {
				let target_port = (!this.options.unique_ports) ? 
					9222 : await Blacktraffic.getFreePort();
					
				let flags = [
					`--remote-debugging-port=${Math.returnSafeNumber(this.options.debugging_port, target_port)}`,
					`--no-first-run`,
					`--no-default-browser-check`
				];
				
				if (this.options.user_data_folder) {
					flags.push(`--user-data-dir="${this.options.user_data_folder}"`);
					flags.push(`--remote-allow-origins=*`);
				}
				
				//1. Run launch command
				this.launch_cmd = `"${Blacktraffic.getChromeBinaryPath()}" ${flags.join(" ")}`;
				exec(this.launch_cmd);
				
				//2. Connect to browser instance
				await Blacktraffic.sleep(3500);
				this.browser = await puppeteer.connect({
					browserURL: `http://127.0.0.1:${target_port}`,
					defaultViewport: null
				});
				this.log_fn(`Blacktraffic.AgentBrowserPuppeteer: ${this.key} connected to port ${target_port}.`);
				break;
			} catch (e) {
				attempts++;
				this.warn_fn(`Port collision or launch failure, retrying .. ${attempts}/${this.options.connection_attempts_threshold}`);
				await Blacktraffic.sleep(500);
			}
		
		if (!this.browser) this.error_fn(`Blacktraffic.AgentBrowserPuppeteer: ${this.key} failed to connect to a browser.`);
		
		//Return statement
		return this;
	}
	
	/**
	 * Opens a tab at the corresponding URL. Corresponding URLs are optional.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias openTab
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @param {string} [arg0_tab_key=Object.generateRandomID(this.tab_obj)]
	 * @param {string} arg1_url
	 * 
	 * @returns {Promise<Object>}
	 */
	async openTab (arg0_tab_key, arg1_url) {
		//Convert from parameters
		let tab_key = (arg0_tab_key) ? arg0_tab_key : Object.generateRandomID(this.tab_obj);
		let url = arg1_url;
		
		//Open tab first
		if (!this.browser) await this.open();
		this.tab_obj[tab_key] = await this.browser.newPage();
			this.tab_obj[tab_key][`_blacktraffic_key`] = tab_key;
		let tab_obj = this.tab_obj[tab_key];
		
		//Load WebAPI in tab
		let serialised_api = Object.serialise(Blacktraffic.AgentBrowser.webapi);
		
		await tab_obj.evaluateOnNewDocument((encoded_api) => {
			//HELPER: Recursive Rehydrator
			const rehydrate = (obj) => {
				if (obj === null || typeof obj !== "object") return obj;
				
				if (obj.__type === "function") {
					// Reconstruct the function/class using indirect eval
					return (0, eval)(`(${obj.source})`);
				}
				
				const result = Array.isArray(obj) ? [] : {};
				for (const key in obj) {
					result[key] = rehydrate(obj[key]);
				}
				return result;
			};
			
			window.webapi = rehydrate(encoded_api);
		}, serialised_api);
		if (url) await tab_obj.goto(url, { waitUntil: "networkidle2" });
		
		//Return statement
		return this.tab_obj[tab_key];
	}
	
	/**
	 * Reloads the given tab.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias reloadTab
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @param {string} arg0_tab_key
	 * 
	 * @returns {Promise<Object>}
	 */
	async reloadTab (arg0_tab_key) {
		//Convert from parameters
		let tab_obj = this.getTab(arg0_tab_key);
		
		//Reload the current tab
		await tab.reload({ waitUntil: "networkidle2" });
		
		//Return statement
		return tab_obj;
	}
	
	/**
	 * Removes the current {@link Blacktraffic.AgentBrowser}.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias remove
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 */
	async remove () {
		await this.close(); //Close browser first
		super.remove();
		
		//Iterate over all Blacktraffic.AgentBrowserPuppeteer.instances and remove the current instance
		for (let i = 0; i < Blacktraffic.AgentBrowserPuppeteer.instances.length; i++) {
			let local_browser = Blacktraffic.AgentBrowserPuppeteer.instances[i];
			
			if (local_browser.key === this.key) {
				Blacktraffic.AgentBrowserPuppeteer.instances.splice(i, 1);
				break;
			}
		}
	}
	
	/**
	 * Takes multi-page or full-page screenshots from a Puppeteer instance. A4 page format by default, hence strange px values.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias screenshotHTML
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 *
	 * @param {Object} arg0_tab_key
	 * @param {string} arg1_path
	 * @param {Object} [arg2_options]
	 *  @param {boolean} [arg2_options.full_page=false]
	 *  @param {number} [arg2_options.height=794]
	 *  @param {number} [arg2_options.width=1531]
	 *  @param {number} [arg2_options.margin_bottom=20]
	 */
	async screenshotHTML (arg0_tab_key, arg1_path, arg2_options) {
		//Convert from parameters
		let tab_obj = this.getTab(arg0_tab_key);
		let path = arg1_path;
		let options = (arg2_options) ? arg2_options : {};
		
		//Initialise options
		if (options.height === undefined) options.height = 794;
		if (options.width === undefined) options.width = 1531;
		
		if (options.margin_bottom === undefined) options.margin_bottom = 20;
		if (options.margin_left === undefined) options.margin_left = 20;
		if (options.margin_top === undefined) options.margin_top = 20;
		if (options.margin_right === undefined) options.margin_right = 20;
		
		//Function body
		try {
			await tab_obj.setViewport({ width: options.width, height: options.height });
			
			if (!options.full_page) {
				let body_handle = await tab_obj.$("body");
				let { height } = await body_handle.boundingBox();
				await body_handle.dispose();
				
				let line_positions = await tab_obj.evaluate(() => {
					let lines = [];
					let node;
					let walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
					
					while (node = walker.nextNode()) {
						let range = document.createRange();
						range.selectNodeContents(node);
						let rects = range.getClientRects();
						for (let rect of rects) lines.push(rect.top);
					}
					return lines;
				});
				
				let split_points = [0];
				for (let i = 1; i < line_positions.length; i++)
					if (line_positions[i] - split_points[split_points.length - 1] > options.height)
						split_points.push(line_positions[i - 1]);
				split_points.push(height);
				
				for (let i = 0; i < split_points.length - 1; i++) {
					let clip_height = split_points[i + 1] - split_points[i];
					let screenshot_buffer = await tab_obj.screenshot({
						clip: {
							x: 0,
							y: split_points[i],
							width: await (tab_obj.evaluate(() => document.body.clientWidth)),
							height: clip_height
						}
					});
					
					let padded_screenshot_buffer = await CURL.addMarginsToScreenshot(screenshot_buffer, {
						height: clip_height + options.margin_top + options.margin_bottom,
						width: options.width,
						margin_bottom: options.margin_bottom,
						margin_left: options.margin_left,
						margin_right: options.margin_right,
						margin_top: options.margin_top
					});
					
					fs.writeFileSync(`${path}_${i + 1}.png`, padded_screenshot_buffer);
				}
			} else {
				let height = await tab_obj.evaluate(() => document.documentElement.scrollHeight);
				await tab_obj.setViewport({ width: options.width, height: Math.ceil(height) + 48 });
				await tab_obj.screenshot({ path: `${path}_full.png` });
			}
		} catch (e) {
			console.error(`Error taking A4 screenshots: ${e}`);
		}
	}
	
	/**
	 * Checks if a given tab exists.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias tabExists
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 *
	 * @param {Object|string} arg0_tab_key
	 * @param {Object} [arg1_options]
	 *  @param {boolean} [arg1_options.strict=false] - Whether to also ensure the current tab is connected.
	 *
	 * @returns {Promise<boolean>}
	 */
	async tabExists (arg0_tab_key, arg1_options) {
		//Convert from parameters
		let tab_obj = this.getTab(arg0_tab_key);
		let options = (arg1_options) ? arg1_options : {};
		
		//Declare local instance variables
		let is_connected = false;
		
		//options.strict handler
		if (tab_obj && options.strict)
			try {
				await tab_obj.title();
				is_connected = true;
			} catch (e) {}
		
		//Return statement
		if (!options.strict && tab_obj) return true;
		if (options.strict && is_connected) return true;
	}
	
	/**
	 * Updates the default logging channel for the current agent.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias updateLogChannel
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 * 
	 * @param {string} arg0_channel_key
	 */
	updateLogChannel (arg0_channel_key) {
		//Convert from parameters
		let channel_key = arg0_channel_key;
		
		this.log_obj = log.getLoggingFunctions(channel_key);
			this.error_fn = this.log_obj.error_fn;
			this.log_fn = this.log_obj.log_fn;
			this.warn_fn = this.log_obj.warn_fn;
	}
	
	/**
	 * Waits for content to stop changing within a selector.
	 * - Method of: {@link Blacktraffic.AgentBrowserPuppeteer}
	 *
	 * @alias waitForStableContent
	 * @memberof Blacktraffic.AgentBrowser.Blacktraffic.AgentBrowserPuppeteer
	 *
	 * @param {Object|string} arg0_tab_key - The given tab key.
	 * @param {string} arg1_selector
	 * @param {number} [arg2_interval=3000]
	 */
	async waitForStableContent (arg0_tab_key, arg1_selector, arg2_interval) {
		//Convert from parameters
		let tab_obj = this.getTab(arg0_tab_key);
		let selector = arg1_selector;
		let interval = Math.returnSafeNumber(arg2_interval, 3000);
		
		//Wait for function inside of tab should it exist
		if (tab_obj) {
			let state_key = `_Blacktraffic_stable_${selector}`;
			
			await tab_obj.waitForFunction((selector, state_key) => {
				let local_els = document.querySelectorAll(selector);
				if (local_els.length === 0) return false;
				
				let last_el = local_els[local_els.length - 1];
				
				//Check if HTML has changed
				if (!window[state_key]) {
					window[state_key] = last_el.innerHTML;
					return false;
				}
				
				let current_html = last_el.innerHTML;
				if (current_html === window[state_key]) {
					return true;
				} else {
					window[state_key] = current_html;
					return false;
				}
			}, { polling: interval, timeout: 0 }, selector, state_key);
		}
	}
};

/**
 * Attempts to return the Chrome binary path.
 * 
 * @returns {string}
 */
Blacktraffic.getChromeBinaryPath = function () {
	//Declare local instance variables
	let os_platform = Blacktraffic.getOS();
	
	//Handle Windows
	if (os_platform === "win") {
		let suffix = "/Google/Chrome/Application/chrome.exe";
		let prefixes = [process.env.LOCALAPPDATA, process.env.ProgramFiles, process.env["PROGRAMFILES(X86)"]];
		
		for (let local_prefix of prefixes)
			if (local_prefix) {
				let chrome_path = path.join(local_prefix, suffix);
				if (fs.existsSync(chrome_path)) return chrome_path;
			}
	} else if (os_platform === "lin") {
		let chrome_path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
		if (fs.existsSync(chrome_path)) return chrome_path;
	} else {
		let binaries = ["google-chrome", "google-chrome-stable", "chromium"];
		
		for (let local_binary of binaries)
			try {
				let chrome_path = child_process.execSync(`which ${local_binary}`, { stdio: "pipe" })
					.toString().trim();
				if (chrome_path && fs.existsSync(chrome_path)) return chrome_path;
			} catch (e) {} //Which returns non-zero exit code if not found
	}
};

/**
 * Attempts to return the default Chrome profile path.
 * 
 * @param {string} [arg0_profile]
 * 
 * @returns {string}
 */
Blacktraffic.getChromeDefaultProfilePath = function (arg0_profile) {
	//Convert from parameters
	let profile = arg0_profile;
	
	//Declare local instance variables
	let os_platform = Blacktraffic.getOS();
	let profile_name = (profile) ? profile : "Profile 1";
	let user_home = process.env.HOME || process.env.USERPROFILE;
	
	//Handle Windows
	if (os_platform === "win") {
		if (process.env.LOCALAPPDATA) {
			let profile_path = path.join(
				process.env.LOCALAPPDATA,
				"Google/Chrome/User Data",
				profile_name
			);
			if (fs.existsSync(profile_path)) return profile_path;
		}
	} else if (os_platform === "lin") {
		if (user_home) {
			let profile_path = path.join(user_home, "Library/Application Support/Google/Chrome", profile_name);
			if (fs.existsSync(profile_path)) return profile_path;
		}
	} else {
		if (user_home) {
			//Check for both official Google Chrome and Chromium paths
			let potential_paths = [
				path.join(user_home, ".config/google-chrome", profile_name),
				path.join(user_home, ".config/chromium", profile_name),
			];
			
			//Iterate over all potential_paths
			for (let local_path of potential_paths)
				if (fs.existsSync(local_path)) return local_path;
		}
	}
	
	//Fallback after Profile 1 to Default
	if (profile !== "Default") {
		let default_profile_path = Blacktraffic.getChromeDefaultProfilePath("Default");
		if (default_profile_path) return default_profile_path;
	}
	
	//Return statement; fallback return if no path is found
	console.error(`Blacktraffic.getChromeDefaultProfile() called, but path could not be found.`);
	return undefined;
};