diff --git a/src/index.js b/src/index.js index 88d6f73..6d7c4bc 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,8 @@ import { fileURLToPath } from 'url'; import Chromium from './browser/chromium.js'; import Firefox from './browser/firefox.js'; +import IdleAPI from './lib/idle.js'; + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -94,7 +96,7 @@ const startBrowser = async (url, { windowSize, forceBrowser }) => { const browserType = browserName.startsWith('firefox') ? 'firefox' : 'chromium'; - return await (browserType === 'firefox' ? Firefox : Chromium)({ + const Browser = await (browserType === 'firefox' ? Firefox : Chromium)({ browserName: browserFriendlyName, dataPath, browserPath @@ -102,6 +104,10 @@ const startBrowser = async (url, { windowSize, forceBrowser }) => { url, windowSize }); + + Browser.idle = await IdleAPI(Browser.cdp, { browserType, dataPath }); + + return Browser; }; export const open = async (url, { windowSize, onLoad, forceBrowser } = {}) => { diff --git a/src/lib/idle.js b/src/lib/idle.js new file mode 100644 index 0000000..b89dfff --- /dev/null +++ b/src/lib/idle.js @@ -0,0 +1,113 @@ +import { exec } from 'child_process'; + +const getProcesses = async containing => process.platform !== 'win32' ? 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 => 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, 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 = process.argv.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(); + } + }; +}; \ No newline at end of file