From fde8dd1bae19f6904bfa180b1b59c1cc6f433ab8 Mon Sep 17 00:00:00 2001 From: CanadaHonk Date: Sat, 10 Dec 2022 00:39:23 +0000 Subject: [PATCH] gluon: 3.0! Gluon 3.0! Biggest update yet and is a massive improvement. Also Glugun 2.2 for fixing now lacking node_modules. - Now uses our own library for Pipe-based CDP instead of NPM dependency and WebSocket - IPC now exists! (experimental) - Now has 0 dependencies! - Inherits Chromium process properly - Faster startup --- README.md | 6 +- glugun/index.js | 11 +--- gluon/index.js | 166 +++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 141 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index f3c1040..943d6f4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Gluon -Minimal integrated ecosystem for making "desktop apps" from websites easily using Chromium and NodeJS. Uses system installed Chromium and NodeJS, with optional bundling if you want that too (soon). ***VERY*** early and probably never finished/production ready. Finds system installed Chromium binary (doesn't use WebView2). +Minimal integrated ecosystem for making "desktop apps" from websites easily using Chromium and NodeJS. Uses system installed Chromium and NodeJS, with optional bundling if you want that too (soon). ***VERY*** early and probably never finished/production ready. Finds system installed Chromium binaries (doesn't use WebView2). ![Gluworld Screenshot](https://user-images.githubusercontent.com/19228318/206796827-5f19addb-a063-4603-b242-6e8f915e8932.png) @@ -26,7 +26,7 @@ Gluon (and it's subprojects) use a `major.patch` version format, with major rele | ---- | ----- | -------- | ------------ | ----- | | Frontend | System installed Chromium | Self-contained Chromium | System installed webview | System installed webview | | Backend | System installed Node.JS | Self-contained Node.JS | Native (Rust) | Native (Any) | -| IPC | None (WIP) | Preload | Window object | Window object | +| IPC | Window object | Preload | Window object | Window object | | Status | Early in development | "Production ready" | Usable | Usable | | Ecosystem | Integrated | Distributed | Integrated | Integrated | @@ -36,7 +36,7 @@ Basic (plain HTML) Hello World demo, measured on up to date Windows 10. Used lat | Stat | Gluon | Electron | Tauri | Neutralinojs | | ---- | ----- | -------- | ------------ | ----- | -| Build Size | ~0.5MB[^system][^gluon][^1] | ~220MB | ~1.8MB[^system] | ~2.6MB[^system] | +| Build Size | ~7KB[^system][^gluon][^1] | ~220MB | ~1.8MB[^system] | ~2.6MB[^system] | | Memory Usage | ~80MB[^gluon] | ~100MB | ~90MB | ~90MB | | Backend[^2] Memory Usage | ~13MB[^gluon] (Node) | ~22MB (Node) | ~3MB (Native) | ~3MB (Native) | | Build Time | ~0.7s[^3] | ~20s[^4] | ~120s[^5] | ~2s[^3][^6] | diff --git a/glugun/index.js b/glugun/index.js index 59c20e4..51fc166 100644 --- a/glugun/index.js +++ b/glugun/index.js @@ -54,20 +54,11 @@ const _buildWin32 = async (name, dir, attrs) => { await cp(dir, join(buildDir, 'src'), { recursive: true }); // copy project src to build await cp(join(__dirname, '..', 'gluon'), join(buildDir, 'src', 'gluon'), { recursive: true }); // copy gluon into build - for (const m of [ 'ws', 'chrome-remote-interface' ]) { - const dest = join(buildDir, 'src', 'node_modules', m); - await cp(join(__dirname, '..', 'node_modules', m), dest, { recursive: true }); // copy gluon deps into build - - for (const x of await readdir(dest)) { - if ([ 'bin', 'README.md', 'webpack.config.json', 'browser.js' ].includes(x)) await rm(join(dest, x), { recursive: true }); - } - } - // await writeFile(join(buildDir, 'gluon_info.txt'), `Gluon 0.1, built with Glugun 0.1 (win32 ${attrs.join(',')})`); let indexContent = await readFile(join(buildDir, 'src', 'index.js'), 'utf8'); indexContent = indexContent.replace('../gluon/', './gluon/') - .replaceAll('GLUGUN_VERSION', '2.1') + .replaceAll('GLUGUN_VERSION', '2.2') .replaceAll('SYSTEM_CHROMIUM', attrs.includes('system-chromium')) .replaceAll('SYSTEM_NODE', attrs.includes('system-node')); diff --git a/gluon/index.js b/gluon/index.js index 3d580e0..06dd538 100644 --- a/gluon/index.js +++ b/gluon/index.js @@ -1,7 +1,7 @@ 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); -process.versions.gluon = '2.1'; +process.versions.gluon = '3.0'; const presets = { // Presets from OpenAsar 'base': '--autoplay-policy=no-user-gesture-required --disable-features=WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService', // Base Discord @@ -10,7 +10,7 @@ const presets = { // Presets from OpenAsar '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 { exec } from 'child_process'; +import { spawn } from 'child_process'; import { join, dirname } from 'path'; import { access } from 'fs/promises'; import { fileURLToPath } from 'url'; @@ -18,12 +18,11 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -import CDP from 'chrome-remote-interface'; - const chromiumPathsWin = { stable: join(process.env.PROGRAMFILES, 'Google', 'Chrome', 'Application', 'chrome.exe'), canary: join(process.env.LOCALAPPDATA, 'Google', 'Chrome SxS', 'Application', 'chrome.exe'), - edge: join(process.env['PROGRAMFILES(x86)'], 'Microsoft', 'Edge', 'Application', 'msedge.exe') + edge: join(process.env['PROGRAMFILES(x86)'], 'Microsoft', 'Edge', 'Application', 'msedge.exe'), + // todo: add more common good paths/browsers here }; const exists = path => access(path).then(() => true).catch(() => false); @@ -37,7 +36,7 @@ const findChromiumPath = async () => { if (!whichChromium) { for (const x in chromiumPathsWin) { - log(x, chromiumPathsWin[x], await exists(chromiumPathsWin[x])); + log('checking if ' + x + ' exists:', chromiumPathsWin[x], await exists(chromiumPathsWin[x])); if (await exists(chromiumPathsWin[x])) { whichChromium = x; @@ -54,7 +53,7 @@ const findChromiumPath = async () => { const getDataPath = () => join(__dirname, '..', 'chrome_data'); -const startChromium = (url, { windowSize }) => new Promise(async res => { +const startChromium = async (url, { windowSize }) => { const dataPath = getDataPath(); const chromiumPath = await findChromiumPath(); @@ -63,38 +62,147 @@ const startChromium = (url, { windowSize }) => new Promise(async res => { if (!chromiumPath) return log('failed to find a good chromium install'); - const debugPort = 9222; - - exec(`"${chromiumPath}" --app=${url} --remote-debugging-port=${debugPort} --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}`, (err, stdout, stderr) => { - log(err, stdout, stderr); + const process = 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'] }); - setTimeout(() => res(debugPort), 500); -}); + process.stdout.pipe(process.stdout); + process.stderr.pipe(process.stderr); + + // todo: move this to it's own library + const { 3: pipeWrite, 4: pipeRead } = process.stdio; + + let onReply = {}; + 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)); + }; + + 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)); + + const target = (await sendMessage('Target.getTargets')).targetInfos[0]; + + const { sessionId } = await sendMessage('Target.attachToTarget', { + targetId: target.targetId, + flatten: true + }); + + (await sendMessage('Runtime.enable', {}, sessionId)); // enable runtime API + + (await 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); + }; + + // evalInWindow(`window.gluonRecieve = msg => console.log('STUB gluonRecieve', msg)`); // make stub reciever + + const sendToWindow = msg => evalInWindow(`window.gluonRecieve(${JSON.stringify(msg)})`); + + let onWindowMessage = () => {}; + + return { + window: { + onMessage: cb => { + onWindowMessage = cb; + }, + send: sendToWindow, + + eval: evalInWindow, + }, + + CDP: { + send: (method, params) => sendMessage(method, params, sessionId) + } + }; +}; export const open = async (url, onLoad = () => {}, { windowSize } = {}) => { log('starting chromium...'); - const debugPort = await startChromium(url, { windowSize }); - log('connecting to CDP...'); - - const { Runtime, Page } = await CDP({ port: debugPort }); - - // const run = async js => (await Runtime.evaluate({ expression: js })).result.value; - const run = async js => log(await Runtime.evaluate({ expression: js })); + const Chromium = await startChromium(url, { windowSize }); const toRun = `(() => { - if (window.self !== window.top) return; // inside frame + if (window.self !== window.top) return; // inside frame + const GLUON_VERSION = '${process.versions.gluon}'; + const NODE_VERSION = '${process.versions.node}'; + const CHROMIUM_VERSION = navigator.userAgentData.brands.find(x => x.brand === "Chromium").version; - const GLUON_VERSION = '${process.versions.gluon}'; - const NODE_VERSION = '${process.versions.node}'; - const CHROMIUM_VERSION = navigator.userAgentData.brands.find(x => x.brand === "Chromium").version; + (${onLoad.toString()})(); +})();`; - (${onLoad.toString()})(); - })()`; + Chromium.window.eval(toRun); - run(toRun); + await Chromium.CDP.send(`Page.enable`); + await Chromium.CDP.send(`Page.addScriptToEvaluateOnNewDocument`, { + source: toRun + }); - await Page.enable(); - await Page.addScriptToEvaluateOnNewDocument({ source: toRun }); + return Chromium; };