From 0a1241ca885c7e37f2694525069426d3bd5c4e8c Mon Sep 17 00:00:00 2001 From: CanadaHonk Date: Mon, 12 Dec 2022 19:30:53 +0000 Subject: [PATCH] gluon: push current 5.0 wip gluon 5.0! firefox support brought to you by an internal rewrite. still a work in progress but mostly finished. --- gluon/browser/chromium.js | 141 +++++++++++++++ gluon/browser/firefox.js | 140 +++++++++++++++ gluon/browser/ipc.js | 144 +++++++++++++++ gluon/index.js | 361 +++++--------------------------------- gluon/package.json | 24 ++- 5 files changed, 494 insertions(+), 316 deletions(-) create mode 100644 gluon/browser/chromium.js create mode 100644 gluon/browser/firefox.js create mode 100644 gluon/browser/ipc.js diff --git a/gluon/browser/chromium.js b/gluon/browser/chromium.js new file mode 100644 index 0000000..3a5ae36 --- /dev/null +++ b/gluon/browser/chromium.js @@ -0,0 +1,141 @@ +import { spawn } from 'child_process'; + +import makeIPCApi from './ipc.js'; + +const presets = { // Presets from OpenAsar + 'base': '--autoplay-policy=no-user-gesture-required --disable-features=WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService', // Base Discord + 'perf': '--enable-gpu-rasterization --enable-zero-copy --ignore-gpu-blocklist --enable-hardware-overlays=single-fullscreen,single-on-top,underlay --enable-features=EnableDrDc,CanvasOopRasterization,BackForwardCache:TimeToLiveInBackForwardCacheInSeconds/300/should_ignore_blocklists/true/enable_same_site/true,ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes,UseSkiaRenderer,WebAssemblyLazyCompilation --disable-features=Vulkan --force_high_performance_gpu', // Performance + 'battery': '--enable-features=TurnOffStreamingMediaCachingOnBattery --force_low_power_gpu', // Known to have better battery life for Chromium? + 'memory': '--in-process-gpu --js-flags="--lite-mode --optimize_for_size --wasm_opt --wasm_lazy_compilation --wasm_lazy_validation --always_compact" --renderer-process-limit=2 --enable-features=QuickIntensiveWakeUpThrottlingAfterLoading' // Less (?) memory usage +}; + +export default async ({ browserName, browserPath, dataPath }, { url, windowSize }) => { + const proc = spawn(browserPath, [ + `--app=${url}`, + `--remote-debugging-pipe`, + `--user-data-dir=${dataPath}`, + windowSize ? `--window-size=${windowSize.join(',')}` : '', + ...`--new-window --disable-extensions --disable-default-apps --disable-breakpad --disable-crashpad --disable-background-networking --disable-domain-reliability --disable-component-update --disable-sync --disable-features=AutofillServerCommunication ${presets.perf}`.split(' ') + ].filter(x => x), { + detached: false, + stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] + }); + + proc.stdout.pipe(proc.stdout); + proc.stderr.pipe(proc.stderr); + + // todo: move this to it's own library + const { 3: pipeWrite, 4: pipeRead } = proc.stdio; + + let onReply = {}, pageLoadCallback = () => {}, onWindowMessage = () => {}; + const onMessage = msg => { + msg = JSON.parse(msg); + + log('received', msg); + if (onReply[msg.id]) { + onReply[msg.id](msg); + delete onReply[msg.id]; + } + + if (msg.method === 'Runtime.bindingCalled' && msg.name === 'gluonSend') onWindowMessage(JSON.parse(msg.payload)); + if (msg.method === 'Page.frameStoppedLoading') pageLoadCallback(msg.params); + }; + + let msgId = 0; + const sendMessage = async (method, params = {}, sessionId = undefined) => { + const id = msgId++; + + const msg = { + id, + method, + params + }; + + if (sessionId) msg.sessionId = sessionId; + + pipeWrite.write(JSON.stringify(msg)); + pipeWrite.write('\0'); + + log('sent', msg); + + const reply = await new Promise(res => { + onReply[id] = msg => res(msg); + }); + + return reply.result; + }; + + let pending = ''; + pipeRead.on('data', buf => { + let end = buf.indexOf('\0'); // messages are null separated + + if (end === -1) { // no complete message yet + pending += buf.toString(); + return; + } + + let start = 0; + while (end !== -1) { // while we have pending complete messages, dispatch them + const message = pending + buf.toString(undefined, start, end); // get next whole message + onMessage(message); + + start = end + 1; // find next ending + end = buf.indexOf('\0', start); + pending = ''; + } + + pending = buf.toString(undefined, start); // update pending with current pending + }); + + pipeRead.on('close', () => log('pipe read closed')); + + // await new Promise(res => setTimeout(res, 1000)); + + let browserInfo; + sendMessage('Browser.getVersion').then(x => { // get browser info async as not important + browserInfo = x; + log('browser:', x.product); + }); + + const target = (await sendMessage('Target.getTargets')).targetInfos[0]; + + const { sessionId } = await sendMessage('Target.attachToTarget', { + targetId: target.targetId, + flatten: true + }); + + sendMessage('Runtime.enable', {}, sessionId); // enable runtime API + + sendMessage('Runtime.addBinding', { // setup sending from window to Node via Binding + name: '_gluonSend' + }, sessionId); + + const evalInWindow = async func => { + return await sendMessage(`Runtime.evaluate`, { + expression: typeof func === 'string' ? func : `(${func.toString()})()` + }, sessionId); + }; + + + const [ ipcMessageCallback, IPCApi ] = await makeIPCApi({ + browserName, + browserInfo + }, { + evaluate: params => sendMessage(`Runtime.evaluate`, params, sessionId), + addScriptToEvaluateOnNewDocument: params => sendMessage('Page.addScriptToEvaluateOnNewDocument', params, sessionId), + pageLoadPromise: new Promise(res => pageLoadCallback = res) + }); + onWindowMessage = ipcMessageCallback; + + return { + window: { + eval: evalInWindow, + }, + + ipc: IPCApi, + + cdp: { + send: (method, params) => sendMessage(method, params, sessionId) + } + }; +}; \ No newline at end of file diff --git a/gluon/browser/firefox.js b/gluon/browser/firefox.js new file mode 100644 index 0000000..09b72d1 --- /dev/null +++ b/gluon/browser/firefox.js @@ -0,0 +1,140 @@ +import { mkdir, writeFile } from 'fs/promises'; +import { join } from 'path'; +import { spawn } from 'child_process'; +import CDP from 'chrome-remote-interface'; + +import makeIPCApi from './ipc.js'; + +const portRange = [ 10000, 60000 ]; + +export default async ({ browserName, browserPath, dataPath }, { url, windowSize }) => { + const debugPort = Math.floor(Math.random() * (portRange[1] - portRange[0] + 1)) + portRange[0]; + + await mkdir(dataPath, { recursive: true }); + await writeFile(join(dataPath, 'user.js'), ` +user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true); +user_pref('devtools.chrome.enabled', true); +user_pref('devtools.debugger.prompt-connection', false); +user_pref('devtools.debugger.remote-enabled', true); +user_pref('toolkit.telemetry.reportingpolicy.firstRun', false); +user_pref('browser.shell.checkDefaultBrowser', false); +user_pref('privacy.window.maxInnerWidth', ${windowSize[0]}); +user_pref('privacy.window.maxInnerHeight', ${windowSize[1]}); +user_pref('privacy.resistFingerprinting', true); +user_pref('fission.bfcacheInParent', false); +user_pref('fission.webContentIsolationStrategy', 0); +`); + +// user_pref('privacy.resistFingerprinting', false); +/* user_pref('privacy.window.maxInnerWidth', ${windowSize[0]}); +user_pref('privacy.window.maxInnerHeight', ${windowSize[1]}); */ + + await mkdir(join(dataPath, 'chrome'), { recursive: true }); + await writeFile(join(dataPath, 'chrome', 'userChrome.css'), ` +.titlebar-spacer, #firefox-view-button, #alltabs-button, #tabbrowser-arrowscrollbox-periphery, .tab-close-button { + display: none; +} + +#nav-bar, #urlbar-container, #searchbar { visibility: collapse !important; } + +.tab-background, .tab-content, #tabbrowser-tabs { + background: none !important; + margin: 0 !important; + padding: 0 !important; + border: none !important; + box-shadow: none !important; +} + +#tabbrowser-tabs { + margin: 0 6px !important; +} + +.tab-icon-image { + width: 16px; + height: 16px; +} + +#titlebar, .tabbrowser-tab { + height: 20px; +} + +.tab-content { + height: 42px; +} +`); + + const proc = spawn(browserPath, [ + // `--window-size=${windowSize.join(',')}`, + // `--remote-debugging-port`, '9228', + `--remote-debugging-port=${debugPort}`, + `-window-size`, windowSize.join(','), + // `--width=${windowSize[0]}`, + // `--height=${windowSize[1]}` + // '-width', windowSize[0], + // '-height', windowSize[1], + `-profile`, dataPath, + // `-P`, `gluon`, + `-new-window`, url, + // `-ssb`, url, + `-new-instance`, + `-no-remote`, + `-shell` + ].filter(x => x), { + detached: false, + stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] + }); + + proc.stdout.pipe(proc.stdout); + proc.stderr.pipe(proc.stderr); + + let CDPInstance; + const connect = async () => { + try { + CDPInstance = await CDP({ + port: debugPort + }); + } catch { + await new Promise(res => setTimeout(res)); + await connect(); + } + }; + + await connect(); + + const { Browser, Runtime, Page } = CDPInstance; + + const browserInfo = await Browser.getVersion(); + + await Runtime.enable(); + + /* Runtime.addBinding({ + name: '_gluonSend' + }); */ + + const [ ipcMessageCallback, IPCApi ] = await makeIPCApi({ + browserName, + browserInfo + }, { + evaluate: Runtime.evaluate, + addScriptToEvaluateOnNewDocument: Page.addScriptToEvaluateOnNewDocument, + pageLoadPromise: new Promise(res => Page.frameStoppedLoading(res)) + }); + + // todo: IPC Node -> Web for Firefox + + return { + window: { + eval: async func => { + return await Runtime.evaluate({ + expression: typeof func === 'string' ? func : `(${func.toString()})()` + }); + } + }, + + ipc: IPCApi, + + cdp: { // todo: public CDP API for Firefox + send: () => {} + } + }; +}; \ No newline at end of file diff --git a/gluon/browser/ipc.js b/gluon/browser/ipc.js new file mode 100644 index 0000000..fd3726e --- /dev/null +++ b/gluon/browser/ipc.js @@ -0,0 +1,144 @@ +export default ({ browserName, browserInfo }, { evaluate, addScriptToEvaluateOnNewDocument, pageLoadPromise }) => { + const injection = `(() => { +let onIPCReply = {}, ipcListeners = {}; +window.Gluon = { + versions: { + gluon: '${process.versions.gluon}', + builder: '${'GLUGUN_VERSION' === 'G\LUGUN_VERSION' ? 'nothing' : 'Glugun GLUGUN_VERSION'}', + node: '${process.versions.node}', + browser: '${browserInfo.product.split('/')[1]}', + product: '${browserName}', + + js: { + node: '${process.versions.v8}', + browser: '${browserInfo.jsVersion}' + }, + + embedded: { + node: ${'EMBEDDED_NODE' === 'true' ? 'true' : 'false'}, + browser: false + } + }, + + ipc: { + send: async (type, data, id = undefined) => { + id = id ?? Math.random().toString().split('.')[1]; + + window.Gluon.ipc._send(JSON.stringify({ + id, + type, + data + })); + + if (id) return; + + const reply = await new Promise(res => { + onIPCReply[id] = msg => res(msg); + }); + + return reply; + }, + + on: (type, cb) => { + if (!ipcListeners[type]) ipcListeners[type] = []; + ipcListeners[type].push(cb); + }, + + removeListener: (type, cb) => { + if (!ipcListeners[type]) return false; + ipcListeners[type].splice(ipcListeners[type].indexOf(cb), 1); + }, + + _recieve: msg => { + const { id, type, data } = msg; + + if (onIPCReply[id]) { + onIPCReply[id]({ type, data }); + delete onIPCReply[id]; + return; + } + + if (ipcListeners[type]) { + let reply; + + for (const cb of ipcListeners[type]) { + const ret = cb(data); + if (!reply) reply = ret; // use first returned value as reply + } + + if (reply) return Gluon.ipc.send('reply', reply, id); // reply with wanted reply + } + + Gluon.ipc.send('pong', {}, id); + }, + + _send: window._gluonSend + }, +}; + +delete window._gluonSend; +})();`; + + evaluate({ + expression: injection + }); + + addScriptToEvaluateOnNewDocument({ + source: injection + }); + + let onIPCReply = {}, ipcListeners = {}; + const sendToWindow = async (type, data, id = undefined) => { + id = id ?? Math.random().toString().split('.')[1]; + + await pageLoadPromise; // wait for page to load before sending, otherwise messages won't be heard + evalInWindow(`window.Gluon.ipc._recieve(${JSON.stringify({ + id, + type, + data + })})`); + + if (id) return; // we are replying, don't expect reply back + + const reply = await new Promise(res => { + onIPCReply[id] = msg => res(msg); + }); + + return reply; + }; + + const onWindowMessage = ({ id, type, data }) => { + if (onIPCReply[id]) { + onIPCReply[id]({ type, data }); + delete onIPCReply[id]; + return; + } + + if (ipcListeners[type]) { + let reply; + + for (const cb of ipcListeners[type]) { + const ret = cb(data); + if (!reply) reply = ret; // use first returned value as reply + } + + if (reply) return sendToWindow('reply', reply, id); // reply with wanted reply + } + + sendToWindow('pong', {}, id); // send simple pong to confirm + }; + + return [ onWindowMessage, { + on: (type, cb) => { + if (!ipcListeners[type]) ipcListeners[type] = []; + ipcListeners[type].push(cb); + }, + + removeListener: (type, cb) => { + if (!ipcListeners[type]) return false; + ipcListeners[type].splice(ipcListeners[type].indexOf(cb), 1); + }, + + send: sendToWindow, + } ]; +}; \ No newline at end of file diff --git a/gluon/index.js b/gluon/index.js index b73516d..e9beb07 100644 --- a/gluon/index.js +++ b/gluon/index.js @@ -1,351 +1,86 @@ const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`; -const log = (...args) => console.log(`[${rgb(88, 101, 242, 'Gluon')}]`, ...args); +global.log = (...args) => console.log(`[${rgb(88, 101, 242, 'Gluon')}]`, ...args); -process.versions.gluon = '4.1'; +process.versions.gluon = '5.0-dev'; -const presets = { // Presets from OpenAsar - 'base': '--autoplay-policy=no-user-gesture-required --disable-features=WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService', // Base Discord - 'perf': '--enable-gpu-rasterization --enable-zero-copy --ignore-gpu-blocklist --enable-hardware-overlays=single-fullscreen,single-on-top,underlay --enable-features=EnableDrDc,CanvasOopRasterization,BackForwardCache:TimeToLiveInBackForwardCacheInSeconds/300/should_ignore_blocklists/true/enable_same_site/true,ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes,UseSkiaRenderer,WebAssemblyLazyCompilation --disable-features=Vulkan --force_high_performance_gpu', // Performance - 'battery': '--enable-features=TurnOffStreamingMediaCachingOnBattery --force_low_power_gpu', // Known to have better battery life for Chromium? - 'memory': '--in-process-gpu --js-flags="--lite-mode --optimize_for_size --wasm_opt --wasm_lazy_compilation --wasm_lazy_validation --always_compact" --renderer-process-limit=2 --enable-features=QuickIntensiveWakeUpThrottlingAfterLoading' // Less (?) memory usage -}; - -import { spawn } from 'child_process'; import { join, dirname } from 'path'; import { access } from 'fs/promises'; import { fileURLToPath } from 'url'; +import Chromium from './browser/chromium.js'; +import Firefox from './browser/firefox.js'; + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const chromiumPathsWin = { - stable: join(process.env.PROGRAMFILES, 'Google', 'Chrome', 'Application', 'chrome.exe'), - canary: join(process.env.LOCALAPPDATA, 'Google', 'Chrome SxS', 'Application', 'chrome.exe'), +const browserPathsWin = { + chrome_stable: join(process.env.PROGRAMFILES, 'Google', 'Chrome', 'Application', 'chrome.exe'), + chrome_canary: join(process.env.LOCALAPPDATA, 'Google', 'Chrome SxS', 'Application', 'chrome.exe'), edge: join(process.env['PROGRAMFILES(x86)'], 'Microsoft', 'Edge', 'Application', 'msedge.exe'), + + firefox: join(process.env.PROGRAMFILES, 'Mozilla Firefox', 'firefox.exe'), + firefox_nightly: join(process.env.PROGRAMFILES, 'Firefox Nightly', 'firefox.exe'), // todo: add more common good paths/browsers here }; const exists = path => access(path).then(() => true).catch(() => false); -const findChromiumPath = async () => { - let whichChromium = ''; +const findBrowserPath = async (forceBrowser) => { + if (forceBrowser) return [ browserPathsWin[forceBrowser], forceBrowser ]; - for (const x of [ 'stable', 'canary', 'edge' ]) { - if (process.argv.includes('--' + x)) whichChromium = x; + let whichBrowser = ''; + + for (const x of Object.keys(browserPathsWin)) { + if (process.argv.includes('--' + x)) whichBrowser = x; } - if (!whichChromium) { - for (const x in chromiumPathsWin) { - log('checking if ' + x + ' exists:', chromiumPathsWin[x], await exists(chromiumPathsWin[x])); + if (!whichBrowser) { + for (const x in browserPathsWin) { + log('checking if ' + x + ' exists:', browserPathsWin[x], await exists(browserPathsWin[x])); - if (await exists(chromiumPathsWin[x])) { - whichChromium = x; + if (await exists(browserPathsWin[x])) { + whichBrowser = x; break; } } } - if (!whichChromium) return null; + if (!whichBrowser) return null; - return [ chromiumPathsWin[whichChromium], whichChromium ]; -}; - -const getFriendlyName = whichChromium => { - switch (whichChromium) { - case 'stable': return 'Chrome'; - case 'canary': return 'Chrome Canary'; - case 'edge': return 'Edge'; - } + return [ browserPathsWin[whichBrowser], whichBrowser ]; }; +const getFriendlyName = whichBrowser => whichBrowser[0].toUpperCase() + whichBrowser.slice(1).replace(/[a-z]_[a-z]/g, _ => _[0] + ' ' + _[2].toUpperCase()); const getDataPath = () => join(__dirname, '..', 'chrome_data'); -const startChromium = async (url, { windowSize }) => { +const startBrowser = async (url, { windowSize, forceBrowser }) => { const dataPath = getDataPath(); - const [ chromiumPath, chromiumName ] = await findChromiumPath(); - const friendlyProductName = getFriendlyName(chromiumName); + const [ browserPath, browserName ] = await findBrowserPath(forceBrowser); - log('chromium path:', chromiumPath); + const browserFriendlyName = getFriendlyName(browserName); + + log('browser path:', browserPath); log('data path:', dataPath); - if (!chromiumPath) return log('failed to find a good chromium install'); + if (!browserPath) return log('failed to find a good browser install'); - const proc = spawn(chromiumPath, [ - `--app=${url}`, - `--remote-debugging-pipe`, - `--user-data-dir=${dataPath}`, - windowSize ? `--window-size=${windowSize.join(',')}` : '', - ...`--new-window --disable-extensions --disable-default-apps --disable-breakpad --disable-crashpad --disable-background-networking --disable-domain-reliability --disable-component-update --disable-sync --disable-features=AutofillServerCommunication ${presets.perf}`.split(' ') - ].filter(x => x), { - detached: false, - stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] + const browserType = browserName.startsWith('firefox') ? 'firefox' : 'chromium'; + + return await (browserType === 'firefox' ? Firefox : Chromium)({ + browserName: browserFriendlyName, + dataPath, + browserPath + }, { + url, + windowSize }); - - proc.stdout.pipe(proc.stdout); - proc.stderr.pipe(proc.stderr); - - // todo: move this to it's own library - const { 3: pipeWrite, 4: pipeRead } = proc.stdio; - - let onReply = {}, pageLoadCallback = () => {}; - const onMessage = msg => { - msg = JSON.parse(msg); - - // log('received', msg); - if (onReply[msg.id]) { - onReply[msg.id](msg); - delete onReply[msg.id]; - } - - if (msg.method === 'Runtime.bindingCalled' && msg.name === 'gluonSend') onWindowMessage(JSON.parse(msg.payload)); - if (msg.method === 'Page.frameStoppedLoading') pageLoadCallback(msg.params); - }; - - let msgId = 0; - const sendMessage = async (method, params = {}, sessionId = undefined) => { - const id = msgId++; - - const msg = { - id, - method, - params - }; - - if (sessionId) msg.sessionId = sessionId; - - pipeWrite.write(JSON.stringify(msg)); - pipeWrite.write('\0'); - - // log('sent', msg); - - const reply = await new Promise(res => { - onReply[id] = msg => res(msg); - }); - - return reply.result; - }; - - let pending = ''; - pipeRead.on('data', buf => { - let end = buf.indexOf('\0'); // messages are null separated - - if (end === -1) { // no complete message yet - pending += buf.toString(); - return; - } - - let start = 0; - while (end !== -1) { // while we have pending complete messages, dispatch them - const message = pending + buf.toString(undefined, start, end); // get next whole message - onMessage(message); - - start = end + 1; // find next ending - end = buf.indexOf('\0', start); - pending = ''; - } - - pending = buf.toString(undefined, start); // update pending with current pending - }); - - pipeRead.on('close', () => log('pipe read closed')); - - // await new Promise(res => setTimeout(res, 1000)); - - let browserInfo; - sendMessage('Browser.getVersion').then(x => { // get browser info async as not important - browserInfo = x; - log('browser:', x.product); - }); - - const target = (await sendMessage('Target.getTargets')).targetInfos[0]; - - const { sessionId } = await sendMessage('Target.attachToTarget', { - targetId: target.targetId, - flatten: true - }); - - // sendMessage('Page.enable', {}, sessionId); // pause page execution until we inject - // sendMessage('Page.waitForDebugger', {}, sessionId); - - // (await sendMessage('Page.enable', {}, sessionId)); - // (await sendMessage('Page.stopLoading', {}, sessionId)); - - sendMessage('Runtime.enable', {}, sessionId); // enable runtime API - - sendMessage('Runtime.addBinding', { // setup sending from window to Node via Binding - name: '_gluonSend' - }, sessionId); - - const evalInWindow = async func => { - return await sendMessage(`Runtime.evaluate`, { - expression: typeof func === 'string' ? func : `(${func.toString()})()` - }, sessionId); - }; - - const windowInjectionSource = `(() => { -let onIPCReply = {}, ipcListeners = {}; -window.Gluon = { - versions: { - gluon: '${process.versions.gluon}', - builder: '${'GLUGUN_VERSION' === 'G\LUGUN_VERSION' ? 'nothing' : 'Glugun GLUGUN_VERSION'}', - node: '${process.versions.node}', - chromium: '${browserInfo.product.split('/')[1]}' ?? navigator.userAgentData.brands.find(x => x.brand === "Chromium").version, - product: '${friendlyProductName}', - - v8: { - node: '${process.versions.v8}', - chromium: '${browserInfo.jsVersion}' - }, - - embedded: { - node: ${'EMBEDDED_NODE' === 'true' ? 'true' : 'false'}, - chromium: false - } - }, - - ipc: { - send: async (type, data, id = undefined) => { - id = id ?? Math.random().toString().split('.')[1]; - - window.Gluon.ipc._send(JSON.stringify({ - id, - type, - data - })); - - if (id) return; - - const reply = await new Promise(res => { - onIPCReply[id] = msg => res(msg); - }); - - return reply; - }, - - on: (type, cb) => { - if (!ipcListeners[type]) ipcListeners[type] = []; - ipcListeners[type].push(cb); - }, - - removeListener: (type, cb) => { - if (!ipcListeners[type]) return false; - ipcListeners[type].splice(ipcListeners[type].indexOf(cb), 1); - }, - - _recieve: msg => { - const { id, type, data } = msg; - - if (onIPCReply[id]) { - onIPCReply[id]({ type, data }); - delete onIPCReply[id]; - return; - } - - if (ipcListeners[type]) { - let reply; - - for (const cb of ipcListeners[type]) { - const ret = cb(data); - if (!reply) reply = ret; // use first returned value as reply - } - - if (reply) return Gluon.ipc.send('reply', reply, id); // reply with wanted reply - } - - Gluon.ipc.send('pong', {}, id); - }, - - _send: window._gluonSend - }, }; -delete window._gluonSend; -})();`; - evalInWindow(windowInjectionSource); // inject nice reciever and sender wrappers +export const open = async (url, { windowSize, onLoad, forceBrowser } = {}) => { + log('starting browser...'); - // sendMessage('Runtime.runIfWaitingForDebugger', {}, sessionId); - - - sendMessage(`Page.enable`, {}, sessionId); - sendMessage(`Page.addScriptToEvaluateOnNewDocument`, { - source: windowInjectionSource - }, sessionId); - - const pageLoadPromise = new Promise(res => { - pageLoadCallback = res; - }); - - let onIPCReply = {}, ipcListeners = {}; - const sendToWindow = async (type, data, id = undefined) => { - id = id ?? Math.random().toString().split('.')[1]; - - await pageLoadPromise; // wait for page to load before sending, otherwise messages won't be heard - evalInWindow(`window.Gluon.ipc._recieve(${JSON.stringify({ - id, - type, - data - })})`); - - if (id) return; // we are replying, don't expect reply back - - const reply = await new Promise(res => { - onIPCReply[id] = msg => res(msg); - }); - - return reply; - }; - - const onWindowMessage = ({ id, type, data }) => { - if (onIPCReply[id]) { - onIPCReply[id]({ type, data }); - delete onIPCReply[id]; - return; - } - - if (ipcListeners[type]) { - let reply; - - for (const cb of ipcListeners[type]) { - const ret = cb(data); - if (!reply) reply = ret; // use first returned value as reply - } - - if (reply) return sendToWindow('reply', reply, id); // reply with wanted reply - } - - sendToWindow('pong', {}, id); // send simple pong to confirm - }; - - return { - window: { - eval: evalInWindow, - }, - - ipc: { - on: (type, cb) => { - if (!ipcListeners[type]) ipcListeners[type] = []; - ipcListeners[type].push(cb); - }, - - removeListener: (type, cb) => { - if (!ipcListeners[type]) return false; - ipcListeners[type].splice(ipcListeners[type].indexOf(cb), 1); - }, - - send: sendToWindow, - }, - - cdp: { - send: (method, params) => sendMessage(method, params, sessionId) - } - }; -}; - -export const open = async (url, { windowSize, onLoad } = {}) => { - log('starting chromium...'); - - const Chromium = await startChromium(url, { windowSize }); + const Browser = await startBrowser(url, { windowSize, forceBrowser }); if (onLoad) { const toRun = `(() => { @@ -354,13 +89,13 @@ export const open = async (url, { windowSize, onLoad } = {}) => { (${onLoad.toString()})(); })();`; - Chromium.window.eval(toRun); + Browser.window.eval(toRun); - await Chromium.cdp.send(`Page.enable`); - await Chromium.cdp.send(`Page.addScriptToEvaluateOnNewDocument`, { + await Browser.cdp.send(`Page.enable`); + await Browser.cdp.send(`Page.addScriptToEvaluateOnNewDocument`, { source: toRun }); } - return Chromium; -}; + return Browser; +}; \ No newline at end of file diff --git a/gluon/package.json b/gluon/package.json index f344768..ff81c51 100644 --- a/gluon/package.json +++ b/gluon/package.json @@ -1,3 +1,21 @@ -{ - "type": "module" -} \ No newline at end of file +{ + "name": "gluon", + "version": "4.1.0", + "description": "Framework for making desktop-ish apps from websites easily using system-installed Chromium", + "main": "index.js", + "scripts": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/OpenAsar/gluon.git" + }, + "author": "OpenAsar", + "license": "MIT", + "bugs": { + "url": "https://github.com/OpenAsar/gluon/issues" + }, + "homepage": "https://github.com/OpenAsar/gluon#readme", + "type": "module", + "dependencies": { + "chrome-remote-interface": "^0.31.3" + } +}