bump Deno branch to 0.9.0 (#27)
* cdp: add internal close api * inject: add Window.close() api * typedef: add Window.close() * roadmap: add close api for 0.8.0 * release: 0.8.0 * readme: update project age * paths: add more linux browsers * readme: update screenshot * readme: move trying gluon to earlier up * inject: add useSessionId to Window.cdp.send() * typedef: add useSessionId to CDPApi.send() * idle: rewrite to use CDP to get processes instead of exec * index: remove now unneeded parameters for idle creation * chore: bump version to 0.9.0-alpha.0 * inject: pass IPC browser engine * api: add Window.versions * typedef: add future BrowserEngine and update Browser values * typedef: add Window.versions * index: rename internal variable * idle: v2 - added sleep, utils, documenting, and more * typedef: update for IdleApi v2 * roadmap: update * chore: bump version to 0.9.0-alpha.2 * readme: add more feature specific statuses * readme: make specific feature statuses into a table * cdp: return protocol errors * idle: move to new api dir * controls: new Window.controls API * darwin: full support (#4) * meta: add pnpm-lock.yaml to .gitignore for my own sanity * darwin: preliminary support * chore: disable menubar key in firefox * Add support new browsers Mac OS (#20) Co-authored-by: a.artamonov <a.artamonov@sftpro.ru> * roadmap: tweak * roadmap: update done * roadmap: remove overall * typedef: change some voids to Promise<void>s * typedef: add Window.controls * release: 0.9.0 Co-authored-by: CanadaHonk <oj@oojmed.com> Co-authored-by: Beef <beefers@riseup.net> Co-authored-by: Alexander Artamonov <47431914+artamonovtech@users.noreply.github.com> Co-authored-by: a.artamonov <a.artamonov@sftpro.ru>
This commit is contained in:
parent
11ef8cc79b
commit
a86305eb41
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,6 +2,9 @@
|
|||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
# gluon
|
# gluon
|
||||||
build
|
build
|
||||||
chrome_data
|
chrome_data
|
@ -9,7 +9,7 @@
|
|||||||
- **Chromium *and Firefox* supported as browser engine**, unlike any other active framework
|
- **Chromium *and Firefox* supported as browser engine**, unlike any other active framework
|
||||||
- **Minimal and easy to use** - Gluon has a simple yet powerful API to make apps with a Deno backend
|
- **Minimal and easy to use** - Gluon has a simple yet powerful API to make apps with a Deno backend
|
||||||
- **Fast build times** - Gluon has build times under 1 second on most machines for small projects
|
- **Fast build times** - Gluon has build times under 1 second on most machines for small projects
|
||||||
- **Actively developed** and **listening to feedback** - already on 5th major revision in ~1 week of development, quickly adding unplanned requested features if liked by the community (like Firefox support!)
|
- **Actively developed** and **listening to feedback** - new updates are coming around weekly, quickly adding unplanned requested features if liked by the community (like Firefox support!)
|
||||||
- **Lower memory usage** - compared to most other frameworks Gluon should have a slightly lower average memory usage by using browser flags to squeeze out more performance
|
- **Lower memory usage** - compared to most other frameworks Gluon should have a slightly lower average memory usage by using browser flags to squeeze out more performance
|
||||||
<!-- - **No forks needed** - Gluon doesn't need forks of Deno or Chromium/etc to use them, it just uses normal versions -->
|
<!-- - **No forks needed** - Gluon doesn't need forks of Deno or Chromium/etc to use them, it just uses normal versions -->
|
||||||
|
|
||||||
@ -106,4 +106,4 @@ Basic (plain HTML) Hello World demo, measured on up to date Windows 10, on my ma
|
|||||||
[^3]: Includes Node.JS spinup time.
|
[^3]: Includes Node.JS spinup time.
|
||||||
[^4]: Built for win32 zip (not Squirrel) as a fairer comparison.
|
[^4]: Built for win32 zip (not Squirrel) as a fairer comparison.
|
||||||
[^5]: Cold build (includes deps compiling) in release mode.
|
[^5]: Cold build (includes deps compiling) in release mode.
|
||||||
[^6]: Using `neu build -r`.
|
[^6]: Using `neu build -r`.
|
13
changelog.md
13
changelog.md
@ -1,5 +1,18 @@
|
|||||||
# Gluon Changelog
|
# Gluon Changelog
|
||||||
|
|
||||||
|
## v0.9.0 [2023-01-03]
|
||||||
|
- New `Window.versions` API with browser version info
|
||||||
|
- New `Window.controls` API to manage window state (minimize/maximize/etc)
|
||||||
|
- New additions and improvements to `Window.idle`:
|
||||||
|
- `Window.idle.sleep()` now performs a light version of hibernation
|
||||||
|
- Now uses CDP commands instead of native to detect processes
|
||||||
|
- Added new `useSessionId` option to `Window.cdp.send()`, allowing to send browser-level CDP commands instead of just to target
|
||||||
|
- Added initial Mac support
|
||||||
|
|
||||||
|
## v0.8.0 [2022-12-30]
|
||||||
|
- Rewrote browser detection to support more setups
|
||||||
|
- Added `Window.close()` API to close Gluon windows gracefully
|
||||||
|
|
||||||
## 0.7.0 [2022-12-20]
|
## 0.7.0 [2022-12-20]
|
||||||
- Added typedef
|
- Added typedef
|
||||||
- Added async IPC listener support
|
- Added async IPC listener support
|
||||||
|
77
gluon.d.ts
vendored
77
gluon.d.ts
vendored
@ -48,7 +48,10 @@ type CDPApi = {
|
|||||||
method: string,
|
method: string,
|
||||||
|
|
||||||
/** Parameters of CDP command. */
|
/** Parameters of CDP command. */
|
||||||
params?: Object
|
params?: Object,
|
||||||
|
|
||||||
|
/** Send session ID with the command (default true). */
|
||||||
|
useSessionId?: Boolean = true
|
||||||
): Promise<any>
|
): Promise<any>
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,16 +65,15 @@ type IdleAutoOptions = {
|
|||||||
|
|
||||||
type IdleApi = {
|
type IdleApi = {
|
||||||
/** Put the window into hibernation. */
|
/** Put the window into hibernation. */
|
||||||
hibernate(): void,
|
hibernate(): Promise<void>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Put the window to sleep.
|
* Put the window to sleep.
|
||||||
* @todo Unimplemented (for Idle v2).
|
|
||||||
*/
|
*/
|
||||||
sleep(): void,
|
sleep(): Promise<void>,
|
||||||
|
|
||||||
/** Wake up the window from hibernation or sleep. */
|
/** Wake up the window from hibernation or sleep. */
|
||||||
wake(): void,
|
wake(): Promise<void>,
|
||||||
|
|
||||||
/** Enable/disable automatic idle management, and set its options. */
|
/** Enable/disable automatic idle management, and set its options. */
|
||||||
auto(
|
auto(
|
||||||
@ -83,6 +85,54 @@ type IdleApi = {
|
|||||||
): void
|
): void
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type VersionInfo = {
|
||||||
|
/** Name of component. */
|
||||||
|
name: string,
|
||||||
|
|
||||||
|
/** Full version of component. */
|
||||||
|
version: string,
|
||||||
|
|
||||||
|
/** Major version of component as a number. */
|
||||||
|
major: number
|
||||||
|
};
|
||||||
|
|
||||||
|
type BrowserVersions = {
|
||||||
|
/**
|
||||||
|
* Product (browser) version and name.
|
||||||
|
* @example
|
||||||
|
* Window.versions.product // { name: 'Chrome Canary', version: '111.0.5513.0', major: 111 }
|
||||||
|
*/
|
||||||
|
product: VersionInfo,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browser engine (Chromium/Firefox) version and name.
|
||||||
|
* @example
|
||||||
|
* Window.versions.engine // { name: 'chromium', version: '111.0.5513.0', major: 111 }
|
||||||
|
*/
|
||||||
|
engine: VersionInfo,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS engine (V8/SpiderMonkey) version and name.
|
||||||
|
* @example
|
||||||
|
* Window.versions.jsEngine // { name: 'v8', version: '11.1.86', major: 11 }
|
||||||
|
*/
|
||||||
|
jsEngine: VersionInfo
|
||||||
|
};
|
||||||
|
|
||||||
|
type ControlsApi = {
|
||||||
|
/** Minimize the browser window. */
|
||||||
|
minimize(): Promise<void>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximize the browser window.
|
||||||
|
* Doesn't make the window appear (use show() before as well).
|
||||||
|
*/
|
||||||
|
maximize(): Promise<void>,
|
||||||
|
|
||||||
|
/** Show (unminimize) the browser window. */
|
||||||
|
show(): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
type Window = {
|
type Window = {
|
||||||
/** API for accessing the window itself. */
|
/** API for accessing the window itself. */
|
||||||
window: WindowApi,
|
window: WindowApi,
|
||||||
@ -97,12 +147,25 @@ type Window = {
|
|||||||
* API for Gluon idle management (like hibernation).
|
* API for Gluon idle management (like hibernation).
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
idle: IdleApi
|
idle: IdleApi,
|
||||||
|
|
||||||
|
/** Browser version info of the window: product (browser), engine (Chromium/Firefox), and JS engine (V8/SpiderMonkey). */
|
||||||
|
versions: BrowserVersions,
|
||||||
|
|
||||||
|
/** Control (minimize, maximize, etc) the browser window. */
|
||||||
|
controls: ControlsApi,
|
||||||
|
|
||||||
|
/** Close the Gluon window. */
|
||||||
|
close(): void
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/** A browser that Gluon supports. */
|
/** A browser that Gluon supports. */
|
||||||
type Browser = 'chrome'|'chrome_canary'|'chromium'|'edge'|'firefox'|'firefox_nightly';
|
type Browser = 'chrome'|'chrome_canary'|'chromium'|'chromium_snapshot'|'edge'|'firefox'|'firefox_nightly';
|
||||||
|
|
||||||
|
/** A browser engine that Gluon supports. */
|
||||||
|
type BrowserEngine = 'chromium'|'firefox';
|
||||||
|
|
||||||
|
|
||||||
/** Additional options for opening */
|
/** Additional options for opening */
|
||||||
type OpenOptions = {
|
type OpenOptions = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@gluon-framework/gluon",
|
"name": "@gluon-framework/gluon",
|
||||||
"version": "0.8.0-dev",
|
"version": "0.9.0",
|
||||||
"description": "Make websites into desktop apps with system installed browsers and NodeJS.",
|
"description": "Make websites into desktop apps with system installed browsers and NodeJS.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"types": "gluon.d.ts",
|
"types": "gluon.d.ts",
|
||||||
|
25
roadmap.md
25
roadmap.md
@ -3,20 +3,19 @@
|
|||||||
> **Note** |
|
> **Note** |
|
||||||
> Want more info on what some of these mean/are? Ask in [our Discord](https://discord.gg/RFtUCA8fST)!
|
> Want more info on what some of these mean/are? Ask in [our Discord](https://discord.gg/RFtUCA8fST)!
|
||||||
|
|
||||||
## v0.8.0
|
## v0.10.0
|
||||||
- [X] Rewrite browser detection to support more setups
|
|
||||||
- [ ] Resources Window API
|
- [ ] Resources Window API
|
||||||
- [ ] On/await load Window API
|
- [ ] On/await load Window API
|
||||||
|
|
||||||
## v0.7.0
|
## v0.9.0
|
||||||
- [X] Early Idle Window API
|
- [X] Browser version info API
|
||||||
|
- [X] Idle API v2
|
||||||
|
- [X] Mac support
|
||||||
|
- [X] Window controls API
|
||||||
|
|
||||||
## Overall - December 2022 - January 2023
|
## v0.8.0
|
||||||
- [X] Total internal rewrite
|
- [X] Rewrite browser detection to support more setups
|
||||||
- [X] Type definitions
|
- [X] Close API
|
||||||
- [X] "Hibernation" feasibility study
|
|
||||||
- [X] Early hibernation API
|
## v0.7.0
|
||||||
- [ ] Automated PR/commit CI testing
|
- [X] Early Idle Window API
|
||||||
- [ ] System Tray API
|
|
||||||
- [ ] Electron "compatibility layer" for basic/simple apps as a demo of versatile API / pros?
|
|
||||||
- More? Need to know what's wanted
|
|
20
src/api/controls.js
vendored
Normal file
20
src/api/controls.js
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export default async (CDP) => {
|
||||||
|
const { windowId } = await CDP.send('Browser.getWindowForTarget');
|
||||||
|
|
||||||
|
const setWindowState = async state => await CDP.send('Browser.setWindowBounds', { windowId, bounds: { windowState: state }});
|
||||||
|
|
||||||
|
return {
|
||||||
|
minimize: async () => {
|
||||||
|
await setWindowState('minimized');
|
||||||
|
},
|
||||||
|
|
||||||
|
maximize: async () => {
|
||||||
|
await setWindowState('maximized');
|
||||||
|
},
|
||||||
|
|
||||||
|
show: async () => {
|
||||||
|
await setWindowState('minimized');
|
||||||
|
await setWindowState('normal');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
166
src/api/idle.js
Normal file
166
src/api/idle.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { exec } from 'https://deno.land/std@0.170.0/node/child_process.ts';
|
||||||
|
|
||||||
|
const killProcesses = async pids => process.platform !== 'win32' ? Promise.resolve('') : new Promise(resolve => exec(`taskkill /F ${pids.map(x => `/PID ${x}`).join(' ')}`, (e, out) => resolve(out)));
|
||||||
|
|
||||||
|
export default async (CDP, { browserType }) => {
|
||||||
|
if (browserType !== 'chromium') { // current implementation is for chromium-based only
|
||||||
|
const warning = () => log(`Warning: Idle API is currently only for Chromium (running on ${browserType})`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hibernate: warning,
|
||||||
|
sleep: warning,
|
||||||
|
wake: warning,
|
||||||
|
auto: warning
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const killNonCrit = async () => { // kill non-critical processes to save memory - crashes chromium internally but not fully
|
||||||
|
const procs = (await CDP.send('SystemInfo.getProcessInfo', {}, false)).processInfo;
|
||||||
|
const nonCriticalProcs = procs.filter(x => x.type !== 'browser'); // browser = the actual main chromium binary
|
||||||
|
|
||||||
|
await killProcesses(nonCriticalProcs.map(x => x.id));
|
||||||
|
log(`killed ${nonCriticalProcs.length} processes`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const purgeMemory = async () => { // purge most memory we can
|
||||||
|
await CDP.send('Memory.forciblyPurgeJavaScriptMemory');
|
||||||
|
await CDP.send('HeapProfiler.collectGarbage');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScreenshot = async () => { // get a screenshot a webm base64 data url
|
||||||
|
const { data } = await CDP.send(`Page.captureScreenshot`, {
|
||||||
|
format: 'webp'
|
||||||
|
});
|
||||||
|
|
||||||
|
return `data:image/webp;base64,${data}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLastUrl = async () => {
|
||||||
|
const history = await CDP.send('Page.getNavigationHistory');
|
||||||
|
return history.entries[history.currentIndex].url;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let wakeUrl, hibernating = false;
|
||||||
|
const hibernate = async () => { // hibernate - crashing chromium internally to save max memory. users will see a crash/gone wrong page but we hopefully "reload" quick enough once visible again for not much notice.
|
||||||
|
if (hibernating) return;
|
||||||
|
if (process.platform !== 'win32') return sleep(); // sleep instead - full hibernation is windows only for now due to needing to do native things
|
||||||
|
|
||||||
|
hibernating = true;
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
wakeUrl = await getLastUrl();
|
||||||
|
|
||||||
|
purgeMemory();
|
||||||
|
await killNonCrit();
|
||||||
|
purgeMemory();
|
||||||
|
|
||||||
|
log(`hibernated in ${(performance.now() - startTime).toFixed(2)}ms`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sleep = async () => { // light hibernate - instead of killing chromium processes we just navigate to a screenshot of the current page.
|
||||||
|
if (hibernating) return;
|
||||||
|
hibernating = true;
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
wakeUrl = await getLastUrl();
|
||||||
|
|
||||||
|
purgeMemory();
|
||||||
|
|
||||||
|
await CDP.send(`Page.navigate`, {
|
||||||
|
url: lastScreenshot
|
||||||
|
});
|
||||||
|
|
||||||
|
purgeMemory();
|
||||||
|
|
||||||
|
log(`slept in ${(performance.now() - startTime).toFixed(2)}ms`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const wake = async () => { // wake up from hibernation/sleep by navigating to the original page
|
||||||
|
if (!hibernating) return;
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
await CDP.send('Page.navigate', {
|
||||||
|
url: wakeUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`began wake in ${(performance.now() - startTime).toFixed(2)}ms`);
|
||||||
|
|
||||||
|
hibernating = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const { windowId } = await CDP.send('Browser.getWindowForTarget');
|
||||||
|
|
||||||
|
let autoEnabled = Deno.args.includes('--force-auto-idle'), autoOptions = {
|
||||||
|
timeMinimizedToHibernate: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
let autoInterval;
|
||||||
|
const startAuto = () => {
|
||||||
|
if (autoInterval) return; // already started
|
||||||
|
|
||||||
|
let lastState = '', lastStateWhen = performance.now();
|
||||||
|
autoInterval = setInterval(async () => {
|
||||||
|
const { bounds: { windowState } } = await CDP.send('Browser.getWindowBounds', { windowId });
|
||||||
|
|
||||||
|
if (windowState !== lastState) {
|
||||||
|
lastState = windowState;
|
||||||
|
lastStateWhen = performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hibernating && windowState === 'minimized' && performance.now() - lastStateWhen > autoOptions.timeMinimizedToHibernate * 1000) await hibernate();
|
||||||
|
else if (hibernating && windowState !== 'minimized') await wake();
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
log('started auto idle');
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopAuto = () => {
|
||||||
|
if (!autoInterval) return; // already stopped
|
||||||
|
|
||||||
|
clearInterval(autoInterval);
|
||||||
|
autoInterval = null;
|
||||||
|
|
||||||
|
log('stopped auto idle');
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastScreenshot, takingScreenshot = false;
|
||||||
|
const screenshotInterval = setInterval(async () => {
|
||||||
|
if (takingScreenshot) return;
|
||||||
|
|
||||||
|
takingScreenshot = true;
|
||||||
|
lastScreenshot = await getScreenshot();
|
||||||
|
takingScreenshot = false;
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
getScreenshot().then(x => lastScreenshot = x);
|
||||||
|
|
||||||
|
log(`idle API active (window id: ${windowId})`);
|
||||||
|
if (autoEnabled) startAuto();
|
||||||
|
|
||||||
|
const setWindowState = async state => await CDP.send('Browser.setWindowBounds', { windowId, bounds: { windowState: state }});
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
hibernate,
|
||||||
|
sleep,
|
||||||
|
wake,
|
||||||
|
|
||||||
|
auto: (enabled, options) => {
|
||||||
|
autoEnabled = enabled;
|
||||||
|
|
||||||
|
autoOptions = {
|
||||||
|
...options,
|
||||||
|
...autoOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
if (enabled) startAuto();
|
||||||
|
else stopAuto();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@ -18,6 +18,8 @@ user_pref('privacy.window.maxInnerHeight', ${windowSize[1]});`}
|
|||||||
user_pref('privacy.resistFingerprinting', true);
|
user_pref('privacy.resistFingerprinting', true);
|
||||||
user_pref('fission.bfcacheInParent', false);
|
user_pref('fission.bfcacheInParent', false);
|
||||||
user_pref('fission.webContentIsolationStrategy', 0);
|
user_pref('fission.webContentIsolationStrategy', 0);
|
||||||
|
user_pref('ui.key.menuAccessKeyFocuses', false);
|
||||||
|
${process.platform === 'darwin' ? `user_pref('browser.tabs.inTitlebar', 0);` : `` }
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// user_pref('privacy.resistFingerprinting', false);
|
// user_pref('privacy.resistFingerprinting', false);
|
||||||
|
30
src/index.js
30
src/index.js
@ -3,10 +3,9 @@ window.log = (...args) => console.log(`[${rgb(88, 101, 242, 'Gluon')}]`, ...args
|
|||||||
|
|
||||||
Deno.version = { // have to do this because... Deno
|
Deno.version = { // have to do this because... Deno
|
||||||
...Deno.version,
|
...Deno.version,
|
||||||
gluon: '0.8.0-deno-dev'
|
gluon: '0.9.0-deno-dev'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
import { join, dirname, delimiter, sep } from 'https://deno.land/std@0.170.0/node/path.ts';
|
import { join, dirname, delimiter, sep } from 'https://deno.land/std@0.170.0/node/path.ts';
|
||||||
import { access, readdir } from 'https://deno.land/std@0.170.0/node/fs/promises.ts';
|
import { access, readdir } from 'https://deno.land/std@0.170.0/node/fs/promises.ts';
|
||||||
import { fileURLToPath } from 'https://deno.land/std@0.170.0/node/url.ts';
|
import { fileURLToPath } from 'https://deno.land/std@0.170.0/node/url.ts';
|
||||||
@ -14,7 +13,8 @@ import { fileURLToPath } from 'https://deno.land/std@0.170.0/node/url.ts';
|
|||||||
import Chromium from './browser/chromium.js';
|
import Chromium from './browser/chromium.js';
|
||||||
import Firefox from './browser/firefox.js';
|
import Firefox from './browser/firefox.js';
|
||||||
|
|
||||||
import IdleAPI from './lib/idle.js';
|
import IdleAPI from './api/idle.js';
|
||||||
|
import ControlsAPI from './api/controls.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@ -32,8 +32,23 @@ const browserPaths = ({
|
|||||||
linux: { // these should be in path so just use the name of the binary
|
linux: { // these should be in path so just use the name of the binary
|
||||||
chrome: [ 'chrome', 'google-chrome', 'chrome-browser', 'google-chrome-stable' ],
|
chrome: [ 'chrome', 'google-chrome', 'chrome-browser', 'google-chrome-stable' ],
|
||||||
chrome_canary: [ 'chrome-canary', 'google-chrome-canary', 'google-chrome-unstable', 'chrome-unstable' ],
|
chrome_canary: [ 'chrome-canary', 'google-chrome-canary', 'google-chrome-unstable', 'chrome-unstable' ],
|
||||||
|
|
||||||
chromium: [ 'chromium', 'chromium-browser' ],
|
chromium: [ 'chromium', 'chromium-browser' ],
|
||||||
|
chromium_snapshot: [ 'chromium-snapshot', 'chromium-snapshot-bin' ],
|
||||||
|
|
||||||
firefox: 'firefox',
|
firefox: 'firefox',
|
||||||
|
firefox_nightly: 'firefox-nightly'
|
||||||
|
},
|
||||||
|
|
||||||
|
darwin: {
|
||||||
|
chrome: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
chrome_canary: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
||||||
|
edge: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
||||||
|
|
||||||
|
chromium: '/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||||
|
|
||||||
|
firefox: '/Applications/Firefox.app/Contents/MacOS/firefox',
|
||||||
|
firefox_nightly: '/Applications/Firefox Nightly.app/Contents/MacOS/firefox'
|
||||||
}
|
}
|
||||||
})[Deno.build.os];
|
})[Deno.build.os];
|
||||||
|
|
||||||
@ -98,7 +113,7 @@ const startBrowser = async (url, { windowSize, forceBrowser }) => {
|
|||||||
|
|
||||||
const browserType = browserName.startsWith('firefox') ? 'firefox' : 'chromium';
|
const browserType = browserName.startsWith('firefox') ? 'firefox' : 'chromium';
|
||||||
|
|
||||||
const Browser = await (browserType === 'firefox' ? Firefox : Chromium)({
|
const Window = await (browserType === 'firefox' ? Firefox : Chromium)({
|
||||||
browserName: browserFriendlyName,
|
browserName: browserFriendlyName,
|
||||||
dataPath,
|
dataPath,
|
||||||
browserPath
|
browserPath
|
||||||
@ -107,9 +122,10 @@ const startBrowser = async (url, { windowSize, forceBrowser }) => {
|
|||||||
windowSize
|
windowSize
|
||||||
});
|
});
|
||||||
|
|
||||||
Browser.idle = await IdleAPI(Browser.cdp, { browserType, dataPath });
|
Window.idle = await IdleAPI(Window.cdp, { browserType });
|
||||||
|
Window.controls = await ControlsAPI(Window.cdp);
|
||||||
|
|
||||||
return Browser;
|
return Window;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const open = async (url, { windowSize, onLoad, forceBrowser } = {}) => {
|
export const open = async (url, { windowSize, onLoad, forceBrowser } = {}) => {
|
||||||
@ -133,4 +149,4 @@ export const open = async (url, { windowSize, onLoad, forceBrowser } = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Browser;
|
return Browser;
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import IPCApi from '../lib/ipc.js';
|
import IPCApi from '../lib/ipc.js';
|
||||||
|
|
||||||
export default async (CDP, injectionType = 'browser', { browserName }) => {
|
export default async (CDP, proc, injectionType = 'browser', { browserName } = { browserName: 'unknown' }) => {
|
||||||
let pageLoadCallback = () => {}, onWindowMessage = () => {};
|
let pageLoadCallback = () => {}, onWindowMessage = () => {};
|
||||||
CDP.onMessage(msg => {
|
CDP.onMessage(msg => {
|
||||||
if (msg.method === 'Runtime.bindingCalled' && msg.params.name === '_gluonSend') onWindowMessage(JSON.parse(msg.params.payload));
|
if (msg.method === 'Runtime.bindingCalled' && msg.params.name === '_gluonSend') onWindowMessage(JSON.parse(msg.params.payload));
|
||||||
@ -27,6 +27,9 @@ export default async (CDP, injectionType = 'browser', { browserName }) => {
|
|||||||
log('browser:', browserInfo.product);
|
log('browser:', browserInfo.product);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const browserEngine = browserInfo.jsVersion.startsWith('1.') ? 'firefox' : 'chromium';
|
||||||
|
|
||||||
|
|
||||||
CDP.sendMessage('Runtime.enable', {}, sessionId); // enable runtime API
|
CDP.sendMessage('Runtime.enable', {}, sessionId); // enable runtime API
|
||||||
|
|
||||||
CDP.sendMessage('Runtime.addBinding', { // setup sending from window to Node via Binding
|
CDP.sendMessage('Runtime.addBinding', { // setup sending from window to Node via Binding
|
||||||
@ -42,17 +45,24 @@ export default async (CDP, injectionType = 'browser', { browserName }) => {
|
|||||||
|
|
||||||
const [ ipcMessageCallback, injectIPC, IPC ] = await IPCApi({
|
const [ ipcMessageCallback, injectIPC, IPC ] = await IPCApi({
|
||||||
browserName,
|
browserName,
|
||||||
browserInfo
|
browserInfo,
|
||||||
|
browserEngine
|
||||||
}, {
|
}, {
|
||||||
evalInWindow,
|
evalInWindow,
|
||||||
evalOnNewDocument: source => CDP.sendMessage('Page.addScriptToEvaluateOnNewDocument', { source }, sessionId),
|
evalOnNewDocument: source => CDP.sendMessage('Page.addScriptToEvaluateOnNewDocument', { source }, sessionId),
|
||||||
pageLoadPromise: new Promise(res => pageLoadCallback = res)
|
pageLoadPromise: new Promise(res => pageLoadCallback = res)
|
||||||
});
|
});
|
||||||
|
|
||||||
onWindowMessage = ipcMessageCallback;
|
onWindowMessage = ipcMessageCallback;
|
||||||
|
|
||||||
|
|
||||||
log('finished setup');
|
log('finished setup');
|
||||||
|
|
||||||
|
const generateVersionInfo = (name, version) => ({
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
major: parseInt(version.split('.')[0])
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
window: {
|
window: {
|
||||||
eval: evalInWindow,
|
eval: evalInWindow,
|
||||||
@ -61,7 +71,18 @@ export default async (CDP, injectionType = 'browser', { browserName }) => {
|
|||||||
ipc: IPC,
|
ipc: IPC,
|
||||||
|
|
||||||
cdp: {
|
cdp: {
|
||||||
send: (method, params) => CDP.sendMessage(method, params, sessionId)
|
send: (method, params, useSessionId = true) => CDP.sendMessage(method, params, useSessionId ? sessionId : undefined)
|
||||||
|
},
|
||||||
|
|
||||||
|
close: () => {
|
||||||
|
CDP.close();
|
||||||
|
proc.kill();
|
||||||
|
},
|
||||||
|
|
||||||
|
versions: {
|
||||||
|
product: generateVersionInfo(browserName, browserInfo.product.split('/')[1]),
|
||||||
|
engine: generateVersionInfo(browserEngine, browserInfo.product.split('/')[1]),
|
||||||
|
jsEngine: generateVersionInfo(browserEngine === 'chromium' ? 'v8' : 'spidermonkey', browserInfo.jsVersion)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -30,5 +30,5 @@ export default async (browserPath, args, transport, extra) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await InjectInto(CDP, transport === 'stdio' ? 'browser' : 'target', extra);
|
return await InjectInto(CDP, proc, transport === 'stdio' ? 'browser' : 'target', extra);
|
||||||
};
|
};
|
@ -3,6 +3,8 @@ import { get } from 'https://deno.land/std@0.170.0/node/http.ts';
|
|||||||
export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
|
export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
|
||||||
let messageCallbacks = [], onReply = {};
|
let messageCallbacks = [], onReply = {};
|
||||||
const onMessage = msg => {
|
const onMessage = msg => {
|
||||||
|
if (closed) return; // closed, ignore
|
||||||
|
|
||||||
msg = JSON.parse(msg);
|
msg = JSON.parse(msg);
|
||||||
|
|
||||||
// log('received', msg);
|
// log('received', msg);
|
||||||
@ -16,10 +18,13 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
|
|||||||
for (const callback of messageCallbacks) callback(msg);
|
for (const callback of messageCallbacks) callback(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
let _send;
|
let closed = false;
|
||||||
|
let _send, _close;
|
||||||
|
|
||||||
let msgId = 0;
|
let msgId = 0;
|
||||||
const sendMessage = async (method, params = {}, sessionId = undefined) => {
|
const sendMessage = async (method, params = {}, sessionId = undefined) => {
|
||||||
|
if (closed) throw new Error('CDP connection closed');
|
||||||
|
|
||||||
const id = msgId++;
|
const id = msgId++;
|
||||||
|
|
||||||
const msg = {
|
const msg = {
|
||||||
@ -38,6 +43,8 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
|
|||||||
onReply[id] = msg => res(msg);
|
onReply[id] = msg => res(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (reply.error) return new Error(reply.error.message);
|
||||||
|
|
||||||
return reply.result;
|
return reply.result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -79,11 +86,15 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
|
|||||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||||
await new Promise(resolve => ws.onopen = resolve);
|
await new Promise(resolve => ws.onopen = resolve);
|
||||||
|
|
||||||
_send = data => ws.send(data);
|
|
||||||
ws.onmessage = ({ data }) => onMessage(data);
|
ws.onmessage = ({ data }) => onMessage(data);
|
||||||
|
|
||||||
|
_send = data => ws.send(data);
|
||||||
|
_close = () => ws.close();
|
||||||
} else {
|
} else {
|
||||||
let pending = '';
|
let pending = '';
|
||||||
pipeRead.on('data', buf => {
|
pipeRead.on('data', buf => {
|
||||||
|
if (closed) return; // closed, ignore
|
||||||
|
|
||||||
let end = buf.indexOf('\0'); // messages are null separated
|
let end = buf.indexOf('\0'); // messages are null separated
|
||||||
|
|
||||||
if (end === -1) { // no complete message yet
|
if (end === -1) { // no complete message yet
|
||||||
@ -110,6 +121,8 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
|
|||||||
pipeWrite.write(data);
|
pipeWrite.write(data);
|
||||||
pipeWrite.write('\0');
|
pipeWrite.write('\0');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_close = () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -121,6 +134,12 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
|
|||||||
|
|
||||||
messageCallbacks.push(callback);
|
messageCallbacks.push(callback);
|
||||||
},
|
},
|
||||||
sendMessage
|
|
||||||
|
sendMessage,
|
||||||
|
|
||||||
|
close: () => {
|
||||||
|
closed = true;
|
||||||
|
_close();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
113
src/lib/idle.js
113
src/lib/idle.js
@ -1,113 +0,0 @@
|
|||||||
import { exec } from 'https://deno.land/std@0.170.0/node/child_process.ts';
|
|
||||||
|
|
||||||
const getProcesses = async containing => Deno.build.os !== 'windows' ? Promise.resolve([]) : new Promise(resolve => exec(`wmic process get Commandline,ProcessID /format:csv`, (e, out) => {
|
|
||||||
resolve(out.toString().split('\r\n').slice(2).map(x => {
|
|
||||||
const parsed = x.trim().split(',').slice(1).reverse();
|
|
||||||
|
|
||||||
return [
|
|
||||||
parseInt(parsed[0]) || parsed[0], // pid to int
|
|
||||||
parsed.slice(1).join(',')
|
|
||||||
];
|
|
||||||
}).filter(x => x[1] && x[1].includes(containing)));
|
|
||||||
}));
|
|
||||||
|
|
||||||
const killProcesses = async pids => Deno.build.os !== 'windows' ? Promise.resolve('') : new Promise(resolve => exec(`taskkill /F ${pids.map(x => `/PID ${x}`).join(' ')}`, (e, out) => resolve(out)));
|
|
||||||
|
|
||||||
export default async (CDP, { browserType, dataPath }) => {
|
|
||||||
if (browserType !== 'chromium') { // current implementation is for chromium-based only
|
|
||||||
const warning = () => log(`Warning: Idle API is currently only for Chromium (running on ${browserType})`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
hibernate: warning,
|
|
||||||
sleep: warning,
|
|
||||||
wake: warning,
|
|
||||||
auto: warning
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const killNonCrit = async () => {
|
|
||||||
const procs = await getProcesses(dataPath);
|
|
||||||
const nonCriticalProcs = procs.filter(x => x[1].includes('type='));
|
|
||||||
|
|
||||||
await killProcesses(nonCriticalProcs.map(x => x[0]));
|
|
||||||
log(`killed ${nonCriticalProcs.length} processes`);
|
|
||||||
};
|
|
||||||
|
|
||||||
let hibernating = false;
|
|
||||||
const hibernate = async () => {
|
|
||||||
hibernating = true;
|
|
||||||
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
await killNonCrit();
|
|
||||||
// await killNonCrit();
|
|
||||||
|
|
||||||
log(`hibernated in ${(performance.now() - startTime).toFixed(2)}ms`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const wake = async () => {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
await CDP.send('Page.reload');
|
|
||||||
|
|
||||||
log(`began wake in ${(performance.now() - startTime).toFixed(2)}ms`);
|
|
||||||
|
|
||||||
hibernating = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { windowId } = await CDP.send('Browser.getWindowForTarget');
|
|
||||||
|
|
||||||
let autoEnabled = Deno.args.includes('--force-auto-idle'), autoOptions = {
|
|
||||||
timeMinimizedToHibernate: 5
|
|
||||||
};
|
|
||||||
|
|
||||||
let autoInterval;
|
|
||||||
const startAuto = () => {
|
|
||||||
if (autoInterval) return; // already started
|
|
||||||
|
|
||||||
let lastState = '', lastStateWhen = performance.now();
|
|
||||||
autoInterval = setInterval(async () => {
|
|
||||||
const { bounds: { windowState } } = await CDP.send('Browser.getWindowBounds', { windowId });
|
|
||||||
|
|
||||||
if (windowState !== lastState) {
|
|
||||||
lastState = windowState;
|
|
||||||
lastStateWhen = performance.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hibernating && windowState === 'minimized' && performance.now() - lastStateWhen > autoOptions.timeMinimizedToHibernate * 1000) await hibernate();
|
|
||||||
else if (hibernating && windowState !== 'minimized') await wake();
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
log('started auto idle');
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopAuto = () => {
|
|
||||||
if (!autoInterval) return; // already stopped
|
|
||||||
|
|
||||||
clearInterval(autoInterval);
|
|
||||||
autoInterval = null;
|
|
||||||
|
|
||||||
log('stopped auto idle');
|
|
||||||
};
|
|
||||||
|
|
||||||
log(`idle API active (window id: ${windowId})`);
|
|
||||||
if (autoEnabled) startAuto();
|
|
||||||
|
|
||||||
return {
|
|
||||||
hibernate,
|
|
||||||
sleep: () => {},
|
|
||||||
wake,
|
|
||||||
|
|
||||||
auto: (enabled, options) => {
|
|
||||||
autoEnabled = enabled;
|
|
||||||
|
|
||||||
autoOptions = {
|
|
||||||
...options,
|
|
||||||
...autoOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
if (enabled) startAuto();
|
|
||||||
else stopAuto();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
export default ({ browserName, browserInfo }, { evalInWindow, evalOnNewDocument, pageLoadPromise }) => {
|
export default ({ browserName, browserInfo, browserEngine }, { evalInWindow, evalOnNewDocument, pageLoadPromise }) => {
|
||||||
const injection = `(() => {
|
const injection = `(() => {
|
||||||
if (window.Gluon) return;
|
if (window.Gluon) return;
|
||||||
let onIPCReply = {}, ipcListeners = {};
|
let onIPCReply = {}, ipcListeners = {};
|
||||||
@ -8,7 +8,7 @@ window.Gluon = {
|
|||||||
builder: '${'GLUGUN_VERSION' === 'G\LUGUN_VERSION' ? 'nothing' : 'Glugun GLUGUN_VERSION'}',
|
builder: '${'GLUGUN_VERSION' === 'G\LUGUN_VERSION' ? 'nothing' : 'Glugun GLUGUN_VERSION'}',
|
||||||
deno: '${Deno.version.deno}',
|
deno: '${Deno.version.deno}',
|
||||||
browser: '${browserInfo?.product.split('/')[1]}',
|
browser: '${browserInfo?.product.split('/')[1]}',
|
||||||
browserType: '${browserName.startsWith('Firefox') ? 'firefox' : 'chromium'}',
|
browserType: '${browserEngine}',
|
||||||
product: '${browserName}',
|
product: '${browserName}',
|
||||||
|
|
||||||
js: {
|
js: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user