Merge pull request #4891 from tridactyl/ez-bg-acS-2-fg

Enable easy access to content process from background script
This commit is contained in:
Oliver Blanthorn 2024-01-26 19:32:30 +00:00 committed by GitHub
commit 4164f7d8d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 163 additions and 1 deletions

View file

@ -3,8 +3,8 @@
/* tslint:disable:import-spacing */
import * as proxy_background from "@src/lib/browser_proxy_background"
import * as controller from "@src/lib/controller"
import { omniscient_controller } from "@src/lib/omniscient_controller"
import * as perf from "@src/perf"
import { listenForCounters } from "@src/perf"
import * as messaging from "@src/lib/messaging"
@ -30,6 +30,7 @@ import * as commands from "@src/background/commands"
import * as meta from "@src/background/meta"
import * as Logging from "@src/lib/logging"
import * as Proxy from "@src/lib/proxy"
import { tabsProxy } from "@src/lib/tabs"
// Add various useful modules to the window for debugging
;(window as any).tri = Object.assign(Object.create(null), {
@ -52,6 +53,7 @@ import * as Proxy from "@src/lib/proxy"
R,
perf,
meta,
tabs: tabsProxy,
})
import { HintingCmds } from "@src/background/hinting"
@ -258,6 +260,7 @@ const messages = {
downloadUrlAs: download_background.downloadUrlAs,
},
browser_proxy_background: { shim: proxy_background.shim },
omniscient_background: omniscient_controller,
}
export type Messages = typeof messages

View file

@ -83,6 +83,7 @@ try {
}
const controller = await import("@src/lib/controller")
const { omniscient_controller } = await import("@src/lib/omniscient_controller")
const excmds_content = await import("@src/.excmds_content.generated")
const hinting_content = await import("@src/content/hinting")
// Hook the keyboard up to the controller
@ -95,6 +96,7 @@ const excmds = await import("@src/.excmds_content.generated")
const finding_content = await import("@src/content/finding")
const itertools = await import("@src/lib/itertools")
const messaging = await import("@src/lib/messaging")
const backgroundProxy = await import("@src/lib/tabs")
const State = await import("@src/state")
const webext = await import("@src/lib/webext")
const perf = await import("@src/perf")
@ -124,6 +126,7 @@ messaging.addListener(
"controller_content",
messaging.attributeCaller(controller),
)
messaging.addListener("omniscient_content", messaging.attributeCaller(omniscient_controller))
// eslint-disable-next-line @typescript-eslint/require-await
messaging.addListener("alive", async () => true)
@ -206,6 +209,7 @@ config.getAsync("preventautofocusjackhammer").then(allowautofocus => {
})
;(window as any).tri = Object.assign(Object.create(null), {
browserBg: webext.browserBg,
bg: backgroundProxy.backgroundProxy,
commandline_content,
convert,
config,

View file

@ -5853,6 +5853,7 @@ async function js_helper(str: string[]) {
* - -r load the js source from a Javascript file relative to your RC file. (NB: will throw an error if no RC file exists)
*
* Some of Tridactyl's functions are accessible here via the `tri` object. Just do `console.log(tri)` in the web console on the new tab page to see what's available.
* `tri.bg` is an object enabling access to the background script's context. It works similarly to the `tri.tabs` objects documented in the [[jsb]] documentation.
*
* If you want to pipe an argument to `js`, you need to use the "-p" flag or "-d" flag with an argument and then use the JS_ARG global variable, e.g:
*
@ -5884,6 +5885,29 @@ export async function js(...str: string[]) {
/**
* Lets you execute JavaScript in the background context. All the help from [[js]] applies. Gives you a different `tri` object which has access to more excmds and web-extension APIs.
*
* In `:jsb`, the `tri` object has a special `tabs` property that can be used to access the window object of the corresponding tab by indexing it with the tab ID. Here are a few examples:
*
* - Get the URL of the tab whose id 3:
* `:jsb tri.tabs[3].location.href.then(console.log)`
* - Set the title of the tab whose id is 6:
* `:jsb tri.tabs[6].document.title = "New title!"`
* - Run `alert()` in a tab whose id is 9:
* `:jsb tri.tabs[9].alert()`
*
* You can also directly access the corresonding property in all tabs by using
* the "tabs" object itself, e.g.
*
* - Build a string containing the id of the active element of each tab:
* `:jsb tri.tabs.document.activeElement.id.then(ids => ids.reduce(s, id => s + " " + id))`
* - Scroll all tabs to the tenth pixel:
* `:jsb tri.tabs.document.documentElement.scrollTop = 10`
* - Use tridactyl's JS ex command to perform a complex computation:
* `:jsb tri.tabs.tri.excmds.js("let x = 1; let y = 2; x + y").then(console.log)`
*
* When fetching a value or running a function in a tab through the `tabs` property, the returned value is a Promise and must be awaited.
* Setting values through the `tab` property is asynchronous too and there is no way to await this operation.
* If you need to ensure that the value has been set before performing another action, use tri.tabs[tab.id].tri.excmds.js to set the value instead and await the result.
*/
/* tslint:disable:no-identical-functions */
//#background

View file

@ -9,6 +9,7 @@ export type TabMessageType =
| "controller_content"
| "commandline_content"
| "finding_content"
| "omniscient_content"
| "commandline_cmd"
| "commandline_frame"
| "state"

View file

@ -0,0 +1,38 @@
export const omniscient_controller = {
set: ({ target, value }) => {
let result
try {
const the_last_one = target[target.length - 1]
const everything_except_the_last_one = target.slice(
0,
target.length - 1,
)
const second_to_last = everything_except_the_last_one.reduce(
(acc, prop) => acc[prop],
window,
)
result = second_to_last[the_last_one] = value
} catch (e) {
console.error(e)
}
return result
},
get: ({ target, _ }) => {
let result
try {
result = target.reduce((acc, prop) => acc[prop], window)
} catch (e) {
console.error(e)
}
return result
},
apply: ({ target, value }) => {
let result
try {
result = target.reduce((acc, prop) => acc[prop], window)(...value)
} catch (e) {
console.error(e)
}
return result
},
}

92
src/lib/tabs.ts Normal file
View file

@ -0,0 +1,92 @@
import * as Messaging from "@src/lib/messaging"
const allTabs = -1
const background = -2
const msg = (tabId, ...args) => {
if (tabId === allTabs) {
return Messaging.messageAllTabs("omniscient_content", ...args)
} else if (tabId === background) {
return (Messaging.message as any)("omniscient_background", args[0], args[1][0])
} else {
return Messaging.messageTab(tabId, "omniscient_content", ...args)
}
}
// This function is used to generate proxies. We use a function rather than an
// object created through Object.create(null) in order to make our proxy
// callable. As all proxy calls should be handled through the proxy's "apply"
// function, we make ItWouldBeAMistakeToCallThis throw an error, to make bugs
// as obvious as possible.
const ItWouldBeAMistakeToCallThis = () => {
throw Error("Error, base function ItWouldBeAMistakeToCallThis was called!")
}
// tabProxy accumulates all accesses to a tab's properties and then, when the
// value of said properties are read, set or called, sends a request to the
// corresponding tab to either retrieve or set the value.
const tabProxy = (tabId, props) =>
new Proxy(ItWouldBeAMistakeToCallThis, {
get(target, p) {
if (p === Symbol.toPrimitive) {
// Symbol.toPrimitive will be accessed when the user attempts
// to use a content process value without awaiting it first,
// e.g.:
//
// tri.tabs[3].document.title + "!"
//
// It is a mistake to do this, so we throw an error - see the
// if condition checking for "then" for more details.
throw Error(
`TypeError: tabs[${tabId}].${props.join(".")} is a Promise, you need to await its value.`,
)
}
if (p === "then") {
// Because we can only access content process values by
// messaging a tab, we can only get values as a promise. We
// take advantage of this fact to wait until we get an access
// to a "then" property before fetching anything - this enables
// rapid traversal of objects instead of having to perform slow
// back and forths for every property.
//
// This works with the "await" keyword too as awaits are turned
// into calls to "then" by the javascript engine.
// One drawback of this approach is that properties named
//
// "then" in the content process cannot be directly accessed.
// We consider this an okay trade-off, as there exists an
// alternative: using tri.tabs[3].eval("x.then") instead.
const promise = msg(tabId, "get", [
{ target: props, value: undefined },
])
return promise.then.bind(promise)
}
return tabProxy(tabId, props.concat(p))
},
set(target, p, value) {
msg(tabId, "set", [{ target: props.concat(p), value }])
return true
},
apply(target, thisArg, argArray) {
return msg(tabId, "apply", [{ target: props, value: argArray }])
},
})
export const tabsProxy = new Proxy(Object.create(null), {
get(target, p) {
const id = Number(p)
if (typeof id === "number" && isFinite(id)) {
// We're accessing tabs[i] - meaning that we should return a proxy
// for a single tab
return tabProxy(id, [])
}
if (typeof p === "string") {
// If `p` is a string, then we return a proxy with a sentinel value
// indicating that the request should be sent to all tabs instead.
return tabProxy(allTabs, [])[p]
}
throw new Error(`'tabs' object can only be accessed by a number (e.g. tabs[3]) or a string (e.g. tabs.document or tabs['document']). Type of accessor: "${typeof p}"`)
},
})
export const backgroundProxy = tabProxy(background, [])