302 lines
9.9 KiB
JavaScript
302 lines
9.9 KiB
JavaScript
import _ from "lodash";
|
|
import utils from "utils";
|
|
import CommonMixin from "background/common_mixin";
|
|
import TTYCommandsMixin from "background/tty_commands_mixin";
|
|
import Tab from "background/tab";
|
|
import Dimensions from "background/dimensions";
|
|
|
|
// Boots the background process. Mainly involves connecting to the websocket server
|
|
// launched by the Browsh CLI client and setting up listeners for new tabs that
|
|
// have our webextension content script inside them.
|
|
export default class extends utils.mixins(CommonMixin, TTYCommandsMixin) {
|
|
constructor() {
|
|
super();
|
|
this.dimensions = new Dimensions();
|
|
// All of the tabs open in the real browser
|
|
this.tabs = {};
|
|
// The ID of the tab currently opened tab
|
|
this.active_tab_id = null;
|
|
// When the real GUI browser first launches it's sized to the same size as the desktop
|
|
this._is_initial_window_size_pending = true;
|
|
// Used so that reconnections to the terminal don't also attempt to reconnect to the
|
|
// browser DOM.
|
|
this._is_connected_to_browser_dom = false;
|
|
// Raw text mode is for when Browsh is running as an HTTP server that serves single
|
|
// pages as entire DOMs, in plain text.
|
|
this._is_raw_text_mode = false;
|
|
// Toggle user agent
|
|
this._is_using_mobile_user_agent = false;
|
|
this._addUserAgentListener();
|
|
// Listen to HTTP requests. This allows us to display some helpful status messages at the
|
|
// bottom of the page, eg; "Loading https://coolwebsite.com..."
|
|
this._addWebRequestListener();
|
|
// The manager is the hub between tabs and the terminal. First we connect to the
|
|
// terminal, as that is the process that would have initially booted the browser and
|
|
// this very code that now runs.
|
|
this._connectToTerminal();
|
|
}
|
|
|
|
_connectToTerminal() {
|
|
// This is the websocket server run by the CLI client
|
|
this.terminal = new WebSocket("ws://localhost:3334");
|
|
this.terminal.addEventListener("open", (_event) => {
|
|
this.log("Webextension connected to the terminal's websocket server");
|
|
this.dimensions.terminal = this.terminal;
|
|
this._listenForTerminalMessages();
|
|
this._connectToBrowserDOM();
|
|
});
|
|
this.terminal.addEventListener("close", (_event) => {
|
|
this._reconnectToTerminal();
|
|
});
|
|
}
|
|
|
|
// If we've disconnected from the terminal, but we're still running, then that likely
|
|
// means the terminal crashed, so we wait to see if the user restarts the terminal.
|
|
_reconnectToTerminal() {
|
|
try {
|
|
this._connectToTerminal();
|
|
} catch (_e) {
|
|
_.debounce(() => this._reconnectToTerminal(), 50);
|
|
}
|
|
}
|
|
|
|
// Mostly listening for forwarded STDIN from the terminal. Therefore, the user
|
|
// pressing the arrow keys, typing, moving the mouse, etc, etc. But we also listen
|
|
// to TTY resize events too.
|
|
_listenForTerminalMessages() {
|
|
this.log("Starting to listen to TTY");
|
|
this.terminal.addEventListener("message", (event) => {
|
|
this.handleTerminalMessage(event.data);
|
|
});
|
|
}
|
|
|
|
_connectToBrowserDOM() {
|
|
if (!this._is_connected_to_browser_dom) {
|
|
this._initialDOMConnection();
|
|
} else {
|
|
this._reconnectToDOM();
|
|
}
|
|
}
|
|
|
|
_initialDOMConnection() {
|
|
this._listenForNewTab();
|
|
this._listenForTabUpdates();
|
|
this._listenForTabChannelOpen();
|
|
this._listenForFocussedTab();
|
|
}
|
|
|
|
_reconnectToDOM() {
|
|
this.log("Attempting to resend browser state to terminal...");
|
|
this.currentTab().sendStateToTerminal();
|
|
if (!this._is_raw_text_mode) {
|
|
this.sendToCurrentTab("/rebuild_text");
|
|
}
|
|
}
|
|
|
|
// For when a tab's content script, triggered by `onDOMContentLoaded`,
|
|
// phone's home.
|
|
// Curiously `browser.runtime.onMessage` receives the tab's ID, whereas
|
|
// `browser.runtime.onConnect` doesn't. So we have to have 2 tab listeners:
|
|
// 1. to get the tab ID so we can talk to it later with 2.
|
|
// 2. to maintain a long-lived connection to continuously pass messages
|
|
// back and forth.
|
|
_listenForNewTab() {
|
|
browser.runtime.onMessage.addListener(this._newTabHandler.bind(this));
|
|
}
|
|
|
|
// There's what seems to be a bug: tabs can exist and be processed without
|
|
// triggering any `browser.tabs.onUpdated` events. Therefore we need to
|
|
// manually poll :/
|
|
// TODO: Detect deleted tabs to remove the key from `this.tabs[]`
|
|
_listenForTabUpdates() {
|
|
setInterval(() => {
|
|
this._pollAllTabs((native_tab_object) => {
|
|
let tab = this._applyUpdates(native_tab_object);
|
|
tab.ensureConnectionToBackground();
|
|
});
|
|
}, 100);
|
|
}
|
|
|
|
_maybeNewTab(tabish_object) {
|
|
const tab_id = parseInt(tabish_object.id);
|
|
if (this.tabs[tab_id] === undefined) {
|
|
let new_tab = new Tab(tabish_object);
|
|
this.tabs[tab_id] = new_tab;
|
|
}
|
|
return this.tabs[tab_id];
|
|
}
|
|
|
|
_handleTabUpdate(_tab_id, changes, native_tab_object) {
|
|
this.log(
|
|
`Tab ${native_tab_object.id} detected chages: ${JSON.stringify(changes)}`
|
|
);
|
|
let tab = this.tabs[native_tab_object.id];
|
|
tab.native_last_change = changes;
|
|
tab.ensureConnectionToBackground();
|
|
tab.sendGlobalConfig(this.config);
|
|
}
|
|
|
|
// Note that although this callback signifies that the tab now exists, it is not fully
|
|
// booted and functional until it has opened a communication channel. It can't do that
|
|
// until it knows its internally represented ID.
|
|
_newTabHandler(_request, sender, sendResponse) {
|
|
this.log(
|
|
`Tab ${sender.tab.id} (${sender.tab.title}) registered with background process`
|
|
);
|
|
// Send the tab back to itself, such that it can be enlightened unto its own nature
|
|
sendResponse(sender.tab);
|
|
this._acknowledgeNewTab(sender.tab);
|
|
}
|
|
|
|
_acknowledgeNewTab(native_tab_object) {
|
|
let tab = this._applyUpdates(native_tab_object);
|
|
tab._is_raw_text_mode = this._is_raw_text_mode;
|
|
tab.postDOMLoadInit(this.terminal, this.dimensions);
|
|
}
|
|
|
|
_applyUpdates(tabish_object) {
|
|
let tab = this._maybeNewTab({
|
|
id: tabish_object.id,
|
|
});
|
|
[
|
|
"id",
|
|
"title",
|
|
"url",
|
|
"active",
|
|
"request_id",
|
|
"raw_text_mode_type",
|
|
"start_time",
|
|
].map((key) => {
|
|
if (tabish_object.hasOwnProperty(key)) {
|
|
tab[key] = tabish_object[key];
|
|
}
|
|
});
|
|
if (tabish_object.active) {
|
|
this.active_tab_id = tab.id;
|
|
}
|
|
return tab;
|
|
}
|
|
|
|
// This is the main communication channel for all back and forth messages to tabs
|
|
_listenForTabChannelOpen() {
|
|
browser.runtime.onConnect.addListener(
|
|
this._tabChannelOpenHandler.bind(this)
|
|
);
|
|
}
|
|
|
|
_tabChannelOpenHandler(channel) {
|
|
this.log(
|
|
`Tab ${channel.name} connected for communication with background process`
|
|
);
|
|
let tab = this.tabs[parseInt(channel.name)];
|
|
tab.postConnectionInit(channel, this.config);
|
|
if (!this._is_connected_to_browser_dom) {
|
|
this._startFrameRequestLoop();
|
|
}
|
|
this._is_connected_to_browser_dom = true;
|
|
}
|
|
|
|
_listenForFocussedTab() {
|
|
browser.tabs.onActivated.addListener(this._focussedTabHandler.bind(this));
|
|
}
|
|
|
|
_focussedTabHandler(tab) {
|
|
this.log(`Tab ${tab.id} received new focus`);
|
|
this.active_tab_id = tab.id;
|
|
}
|
|
|
|
_getTabsOnSuccess(windowInfoArray, callback) {
|
|
for (let windowInfo of windowInfoArray) {
|
|
windowInfo.tabs.map((tab) => {
|
|
callback(tab);
|
|
});
|
|
}
|
|
}
|
|
|
|
_getTabsOnError(error) {
|
|
this.log(`Error: ${error}`);
|
|
}
|
|
|
|
_pollAllTabs(callback) {
|
|
var getting = browser.windows.getAll({
|
|
populate: true,
|
|
windowTypes: ["normal"],
|
|
});
|
|
getting.then(
|
|
(windowInfoArray) => this._getTabsOnSuccess(windowInfoArray, callback),
|
|
() => this._getTabsOnError(callback)
|
|
);
|
|
}
|
|
|
|
// The browser window can only be resized once we have both the character dimensions from
|
|
// the browser tab _and the TTY dimensions from the terminal. There's probably a more
|
|
// efficient way of triggering this initial window resize, than just waiting for the data
|
|
// on every frame tick.
|
|
_initialWindowResize() {
|
|
if (!this._is_initial_window_size_pending) return;
|
|
this.dimensions.resizeBrowserWindow();
|
|
this._is_initial_window_size_pending = false;
|
|
}
|
|
|
|
// Instead of having each tab manage its own frame rate, just keep this single, centralised
|
|
// heartbeat in the background process that switches automatically to the current active
|
|
// tab.
|
|
//
|
|
// Note that by "frame rate" here we justs mean the rate at which a TTY-sized frame of
|
|
// graphics pixles are sent. Larger frames are sent in response to scroll events and
|
|
// TTY-sized text frames are sent in response to DOM mutation events.
|
|
_startFrameRequestLoop() {
|
|
this.log(
|
|
"BACKGROUND: Frame loop starting at " +
|
|
this.config.tty.small_pixel_frame_rate +
|
|
"ms intervals"
|
|
);
|
|
setInterval(() => {
|
|
if (this._is_initial_window_size_pending) this._initialWindowResize();
|
|
if (this._isAbleToRequestFrame()) {
|
|
this.sendToCurrentTab("/request_frame");
|
|
}
|
|
}, this.config.tty.small_pixel_frame_rate);
|
|
}
|
|
|
|
_isAbleToRequestFrame() {
|
|
if (this._is_raw_text_mode) {
|
|
return false;
|
|
}
|
|
if (!this.dimensions.tty.width || !this.dimensions.tty.height) {
|
|
this.log("Not sending frame to TTY without TTY size");
|
|
return false;
|
|
}
|
|
if (!this.tabs.hasOwnProperty(this.active_tab_id)) {
|
|
this.log("No active tab, so not requesting a frame");
|
|
return false;
|
|
}
|
|
if (this.currentTab().channel === undefined) {
|
|
this.log(
|
|
`Active tab ${this.active_tab_id} does not have a channel, so not requesting a frame`
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Listen for HTTP activity so we can notify the user that something is loading in the background
|
|
_addWebRequestListener() {
|
|
browser.webRequest.onBeforeRequest.addListener(
|
|
(e) => {
|
|
let message;
|
|
if (e.type == "main_frame") {
|
|
message = `Loading ${e.url}`;
|
|
if (this.currentTab() !== undefined) {
|
|
this.currentTab().updateStatus("info", message);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
urls: ["*://*/*"],
|
|
},
|
|
["blocking"]
|
|
);
|
|
}
|
|
}
|