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
This commit is contained in:
CanadaHonk 2022-12-10 00:39:23 +00:00
parent 1fee7caab5
commit fde8dd1bae
3 changed files with 141 additions and 42 deletions

View File

@ -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] |

View File

@ -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'));

View File

@ -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
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()})();
})()`;
})();`;
run(toRun);
Chromium.window.eval(toRun);
await Page.enable();
await Page.addScriptToEvaluateOnNewDocument({ source: toRun });
await Chromium.CDP.send(`Page.enable`);
await Chromium.CDP.send(`Page.addScriptToEvaluateOnNewDocument`, {
source: toRun
});
return Chromium;
};