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.
This commit is contained in:
parent
7df7481369
commit
0a1241ca88
141
gluon/browser/chromium.js
Normal file
141
gluon/browser/chromium.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
140
gluon/browser/firefox.js
Normal file
140
gluon/browser/firefox.js
Normal file
@ -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: () => {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
144
gluon/browser/ipc.js
Normal file
144
gluon/browser/ipc.js
Normal file
@ -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,
|
||||||
|
} ];
|
||||||
|
};
|
361
gluon/index.js
361
gluon/index.js
@ -1,351 +1,86 @@
|
|||||||
const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`;
|
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 { join, dirname } from 'path';
|
||||||
import { access } from 'fs/promises';
|
import { access } from 'fs/promises';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
import Chromium from './browser/chromium.js';
|
||||||
|
import Firefox from './browser/firefox.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
const chromiumPathsWin = {
|
const browserPathsWin = {
|
||||||
stable: join(process.env.PROGRAMFILES, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
chrome_stable: join(process.env.PROGRAMFILES, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
||||||
canary: join(process.env.LOCALAPPDATA, 'Google', 'Chrome SxS', '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'),
|
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
|
// todo: add more common good paths/browsers here
|
||||||
};
|
};
|
||||||
|
|
||||||
const exists = path => access(path).then(() => true).catch(() => false);
|
const exists = path => access(path).then(() => true).catch(() => false);
|
||||||
|
|
||||||
const findChromiumPath = async () => {
|
const findBrowserPath = async (forceBrowser) => {
|
||||||
let whichChromium = '';
|
if (forceBrowser) return [ browserPathsWin[forceBrowser], forceBrowser ];
|
||||||
|
|
||||||
for (const x of [ 'stable', 'canary', 'edge' ]) {
|
let whichBrowser = '';
|
||||||
if (process.argv.includes('--' + x)) whichChromium = x;
|
|
||||||
|
for (const x of Object.keys(browserPathsWin)) {
|
||||||
|
if (process.argv.includes('--' + x)) whichBrowser = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!whichChromium) {
|
if (!whichBrowser) {
|
||||||
for (const x in chromiumPathsWin) {
|
for (const x in browserPathsWin) {
|
||||||
log('checking if ' + x + ' exists:', chromiumPathsWin[x], await exists(chromiumPathsWin[x]));
|
log('checking if ' + x + ' exists:', browserPathsWin[x], await exists(browserPathsWin[x]));
|
||||||
|
|
||||||
if (await exists(chromiumPathsWin[x])) {
|
if (await exists(browserPathsWin[x])) {
|
||||||
whichChromium = x;
|
whichBrowser = x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!whichChromium) return null;
|
if (!whichBrowser) return null;
|
||||||
|
|
||||||
return [ chromiumPathsWin[whichChromium], whichChromium ];
|
return [ browserPathsWin[whichBrowser], whichBrowser ];
|
||||||
};
|
|
||||||
|
|
||||||
const getFriendlyName = whichChromium => {
|
|
||||||
switch (whichChromium) {
|
|
||||||
case 'stable': return 'Chrome';
|
|
||||||
case 'canary': return 'Chrome Canary';
|
|
||||||
case 'edge': return 'Edge';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 getDataPath = () => join(__dirname, '..', 'chrome_data');
|
||||||
|
|
||||||
const startChromium = async (url, { windowSize }) => {
|
const startBrowser = async (url, { windowSize, forceBrowser }) => {
|
||||||
const dataPath = getDataPath();
|
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);
|
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, [
|
const browserType = browserName.startsWith('firefox') ? 'firefox' : 'chromium';
|
||||||
`--app=${url}`,
|
|
||||||
`--remote-debugging-pipe`,
|
return await (browserType === 'firefox' ? Firefox : Chromium)({
|
||||||
`--user-data-dir=${dataPath}`,
|
browserName: browserFriendlyName,
|
||||||
windowSize ? `--window-size=${windowSize.join(',')}` : '',
|
dataPath,
|
||||||
...`--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(' ')
|
browserPath
|
||||||
].filter(x => x), {
|
}, {
|
||||||
detached: false,
|
url,
|
||||||
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe']
|
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;
|
export const open = async (url, { windowSize, onLoad, forceBrowser } = {}) => {
|
||||||
})();`;
|
log('starting browser...');
|
||||||
evalInWindow(windowInjectionSource); // inject nice reciever and sender wrappers
|
|
||||||
|
|
||||||
// sendMessage('Runtime.runIfWaitingForDebugger', {}, sessionId);
|
const Browser = await startBrowser(url, { windowSize, forceBrowser });
|
||||||
|
|
||||||
|
|
||||||
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 });
|
|
||||||
|
|
||||||
if (onLoad) {
|
if (onLoad) {
|
||||||
const toRun = `(() => {
|
const toRun = `(() => {
|
||||||
@ -354,13 +89,13 @@ export const open = async (url, { windowSize, onLoad } = {}) => {
|
|||||||
(${onLoad.toString()})();
|
(${onLoad.toString()})();
|
||||||
})();`;
|
})();`;
|
||||||
|
|
||||||
Chromium.window.eval(toRun);
|
Browser.window.eval(toRun);
|
||||||
|
|
||||||
await Chromium.cdp.send(`Page.enable`);
|
await Browser.cdp.send(`Page.enable`);
|
||||||
await Chromium.cdp.send(`Page.addScriptToEvaluateOnNewDocument`, {
|
await Browser.cdp.send(`Page.addScriptToEvaluateOnNewDocument`, {
|
||||||
source: toRun
|
source: toRun
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Chromium;
|
return Browser;
|
||||||
};
|
};
|
@ -1,3 +1,21 @@
|
|||||||
{
|
{
|
||||||
"type": "module"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user