UNPKG

@hitarth-gg/devtron

Version:

Electron DevTools Extension to track IPC events

558 lines (537 loc) 23 kB
import * as __WEBPACK_EXTERNAL_MODULE_electron__ from "electron"; import { createRequire as __WEBPACK_EXTERNAL_createRequire } from "node:module"; /******/ var __webpack_modules__ = ([ /* 0 */, /* 1 */ /***/ ((module) => { module.exports = __WEBPACK_EXTERNAL_MODULE_electron__; /***/ }), /* 2 */ /***/ ((module) => { module.exports = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:path"); /***/ }), /* 3 */ /***/ ((module) => { module.exports = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:module"); /***/ }), /* 4 */ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ MSG_TYPE: () => (/* binding */ MSG_TYPE), /* harmony export */ PORT_NAME: () => (/* binding */ PORT_NAME), /* harmony export */ excludedIpcChannels: () => (/* binding */ excludedIpcChannels) /* harmony export */ }); const PORT_NAME = { PANEL: 'devt-panel', CONTENT_SCRIPT: 'devt-content-script', }; const MSG_TYPE = { PING: 'ping', PONG: 'pong', GET_ALL_EVENTS: 'get-all-events', RENDER_EVENT: 'render-event', CLEAR_EVENTS: 'clear-events', EVENTS_CLEARED_ACK: 'events-cleared-ack', ADD_IPC_EVENT: 'add-ipc-event', SEND_TO_PANEL: 'send-to-panel', }; /** * These channels are used internally by Devtron, and tracking them may lead to unnecessary noise. * Hence, they are ignored by Devtron. */ const excludedIpcChannels = ['devtron-ipc-events']; /***/ }), /* 5 */ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ logger: () => (/* binding */ logger) /* harmony export */ }); class Logger { logLevelMap = { debug: 1, info: 2, warn: 3, error: 4, none: 5, }; currentLogLevel = 'debug'; currentLogLevelIndex = this.logLevelMap[this.currentLogLevel]; setLogLevel(level) { if (this.currentLogLevel === level) return; // no change if (this.logLevelMap[level] === undefined) { console.error(`Invalid log level: ${level}`); return; } this.currentLogLevel = level; this.currentLogLevelIndex = this.logLevelMap[level]; } log(level, ...args) { if (this.currentLogLevel === 'none') return; if (this.logLevelMap[level] < this.currentLogLevelIndex) return; switch (level) { case 'debug': console.debug(...args); break; case 'info': console.log(...args); break; case 'warn': console.warn(...args); break; case 'error': console.error(...args); break; } } debug(...args) { this.log('debug', ...args); } info(...args) { this.log('info', ...args); } warn(...args) { this.log('warn', ...args); } error(...args) { this.log('error', ...args); } } const logger = new Logger(); /***/ }) /******/ ]); /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ var cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = __webpack_module_cache__[moduleId] = { /******/ // no module.id needed /******/ // no module.loaded needed /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /************************************************************************/ /******/ /* webpack/runtime/compat get default export */ /******/ (() => { /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = (module) => { /******/ var getter = module && module.__esModule ? /******/ () => (module['default']) : /******/ () => (module); /******/ __webpack_require__.d(getter, { a: getter }); /******/ return getter; /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports /******/ __webpack_require__.r = (exports) => { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ })(); /******/ /************************************************************************/ var __webpack_exports__ = {}; // This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. (() => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ devtron: () => (/* binding */ devtron) /* harmony export */ }); /* harmony import */ var electron__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(node_path__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var node_module__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3); /* harmony import */ var node_module__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(node_module__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _common_constants__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); /* harmony import */ var _utils_Logger__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5); let isInstalled = false; let isInstalledToDefaultSession = false; let devtronSW; const isPayloadWithUuid = (payload) => { // If the first argument is an object with __uuid__devtron then it is a custom payload return (payload[0] && typeof payload[0] === 'object' && payload[0].__uuid__devtron && Array.isArray(payload[0].args)); }; const getArgsFromPayload = (payload) => { if (isPayloadWithUuid(payload)) { // If the payload is a custom payload, return the args array return payload[0].args || []; } // Otherwise, return the payload as is return payload; }; const getUuidFromPayload = (payload) => { if (isPayloadWithUuid(payload)) { return payload[0].__uuid__devtron; } return ''; }; /** * sends captured IPC events to the service-worker preload script */ function trackIpcEvent({ direction, channel, args, devtronSW, serviceWorkerDetails, method, }) { if (_common_constants__WEBPACK_IMPORTED_MODULE_3__.excludedIpcChannels.includes(channel)) return; if (!devtronSW) { _utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.warn('The service-worker for Devtron is not registered yet. Cannot track IPC event.'); return; } const uuid = getUuidFromPayload(args); const newArgs = getArgsFromPayload(args); const eventData = { direction, channel, args: newArgs, timestamp: Date.now(), serviceWorkerDetails, }; if (method) eventData.method = method; if (uuid) eventData.uuid = uuid; devtronSW.send('devtron-render-event', eventData); } function registerIpcListeners(ses, devtronSW) { ses.on( // @ts-expect-error: '-ipc-message' is an internal event '-ipc-message', (event, channel, args) => { if (event.type === 'frame') trackIpcEvent({ direction: 'renderer-to-main', channel, args, devtronSW }); else if (event.type === 'service-worker') trackIpcEvent({ direction: 'service-worker-to-main', channel, args, devtronSW }); }); ses.on( // @ts-expect-error: '-ipc-invoke' is an internal event '-ipc-invoke', (event, channel, args) => { if (event.type === 'frame') trackIpcEvent({ direction: 'renderer-to-main', channel, args, devtronSW }); else if (event.type === 'service-worker') trackIpcEvent({ direction: 'service-worker-to-main', channel, args, devtronSW }); }); ses.on( // @ts-expect-error: '-ipc-message-sync' is an internal event '-ipc-message-sync', (event, channel, args) => { if (event.type === 'frame') trackIpcEvent({ direction: 'renderer-to-main', channel, args, devtronSW }); else if (event.type === 'service-worker') trackIpcEvent({ direction: 'service-worker-to-main', channel, args, devtronSW }); }); } /** * Registers a listener for the service worker's send method to track IPC events * sent from the main process to the service worker. */ function registerServiceWorkerSendListener(ses, devtronSW) { const isInstalledSet = new Set(); // stores version IDs of patched service workers // register listener for existing service workers const allRunning = ses.serviceWorkers.getAllRunning(); for (const vid in allRunning) { const swInfo = allRunning[vid]; const sw = ses.serviceWorkers.getWorkerFromVersionID(Number(vid)); if (typeof sw === 'undefined' || sw.scope === devtronSW.scope) continue; isInstalledSet.add(swInfo.versionId); const originalSend = sw.send; sw.send = function (...args) { trackIpcEvent({ direction: 'main-to-service-worker', channel: args[0], args: args.slice(1), devtronSW, serviceWorkerDetails: { serviceWorkerScope: sw.scope, serviceWorkerVersionId: sw.versionId, }, }); return originalSend.apply(this, args); }; } // register listener for new service workers ses.serviceWorkers.on('running-status-changed', (details) => { if (details.runningStatus === 'running' || details.runningStatus === 'starting') { const sw = ses.serviceWorkers.getWorkerFromVersionID(details.versionId); if (typeof sw === 'undefined' || sw.scope === devtronSW.scope || isInstalledSet.has(sw.versionId)) return; isInstalledSet.add(details.versionId); const originalSend = sw.send; sw.send = function (...args) { trackIpcEvent({ direction: 'main-to-service-worker', channel: args[0], args: args.slice(1), devtronSW, serviceWorkerDetails: { serviceWorkerScope: sw.scope, serviceWorkerVersionId: sw.versionId, }, }); return originalSend.apply(this, args); }; } }); } async function startServiceWorker(ses, extension) { try { const sw = await ses.serviceWorkers.startWorkerForScope(extension.url); sw.startTask(); devtronSW = sw; registerIpcListeners(ses, sw); registerServiceWorkerSendListener(ses, sw); } catch (error) { _utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.warn(`Failed to start Devtron service-worker (${error}), trying again...`); /** * This is a workaround for the issue where the Devtron service-worker fails to start * when the Electron app is launched for the first time, or when the service worker * hasn't been cached yet. */ try { const handleDetails = async (event, details) => { if (details.scope === extension.url) { const sw = await ses.serviceWorkers.startWorkerForScope(extension.url); sw.startTask(); devtronSW = sw; registerIpcListeners(ses, sw); registerServiceWorkerSendListener(ses, sw); ses.serviceWorkers.removeListener('registration-completed', handleDetails); _utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.info(`Devtron service-worker started successfully`); } }; ses.serviceWorkers.on('registration-completed', handleDetails); } catch (error) { _utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.error('Failed to start Devtron service-worker:', error); } } } function patchIpcMain() { const listenerMap = new Map(); // channel -> (originalListener -> tracked/cleaned Listener) const storeTrackedListener = (channel, original, tracked) => { if (!listenerMap.has(channel)) { listenerMap.set(channel, new Map()); } listenerMap.get(channel).set(original, tracked); }; const originalOn = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.on.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain); const originalOff = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.off.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain); const originalOnce = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.once.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain); const originalAddListener = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.addListener.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain); const originalRemoveListener = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeListener.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain); const originalRemoveAllListeners = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeAllListeners.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain); const originalHandle = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.handle.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain); const originalHandleOnce = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.handleOnce.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain); const originalRemoveHandler = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeHandler.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain); electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.on = (channel, listener) => { const cleanedListener = (event, ...args) => { const newArgs = getArgsFromPayload(args); listener(event, ...newArgs); }; storeTrackedListener(channel, listener, cleanedListener); return originalOn(channel, cleanedListener); }; electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.off = (channel, listener) => { const channelMap = listenerMap.get(channel); const cleanedListener = channelMap?.get(listener); if (!cleanedListener) return electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain; channelMap?.delete(listener); if (channelMap && channelMap.size === 0) { listenerMap.delete(channel); } trackIpcEvent({ direction: 'main', channel, args: [], devtronSW, method: 'off' }); return originalOff(channel, cleanedListener); }; electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.once = (channel, listener) => { const cleanedListener = (event, ...args) => { const newArgs = getArgsFromPayload(args); listener(event, ...newArgs); }; return originalOnce(channel, cleanedListener); }; electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.addListener = (channel, listener) => { const cleanedListener = (event, ...args) => { const newArgs = getArgsFromPayload(args); listener(event, ...newArgs); }; storeTrackedListener(channel, listener, cleanedListener); return originalAddListener(channel, cleanedListener); }; electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeListener = (channel, listener) => { const channelMap = listenerMap.get(channel); const cleanedListener = channelMap?.get(listener); if (!cleanedListener) return electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain; // Remove the listener from the map channelMap?.delete(listener); // If no listeners left for this channel, remove the channel from the map if (channelMap && channelMap.size === 0) { listenerMap.delete(channel); } trackIpcEvent({ direction: 'main', channel, args: [], devtronSW, method: 'removeListener' }); return originalRemoveListener(channel, cleanedListener); }; electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeAllListeners = (channel) => { if (channel) { listenerMap.delete(channel); trackIpcEvent({ direction: 'main', channel, args: [], devtronSW, method: 'removeAllListeners', }); return originalRemoveAllListeners(channel); } else { listenerMap.clear(); trackIpcEvent({ direction: 'main', channel: '', args: [], devtronSW, method: 'removeAllListeners', }); listenerMap.clear(); return originalRemoveAllListeners(); } }; electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.handle = (channel, listener) => { const cleanedListener = async (event, ...args) => { const newArgs = getArgsFromPayload(args); const result = await listener(event, ...newArgs); return result; }; return originalHandle(channel, cleanedListener); }; electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.handleOnce = (channel, listener) => { const cleanedListener = async (event, ...args) => { const newArgs = getArgsFromPayload(args); const result = await listener(event, ...newArgs); return result; }; return originalHandleOnce(channel, cleanedListener); }; electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeHandler = (channel) => { listenerMap.delete(channel); trackIpcEvent({ direction: 'main', channel, args: [], devtronSW, method: 'removeHandler' }); return originalRemoveHandler(channel); }; } async function install(options = {}) { if (isInstalled) return; isInstalled = true; // set log level if (options.logLevel) _utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.setLogLevel(options.logLevel); patchIpcMain(); const installToSession = async (ses) => { if (ses === electron__WEBPACK_IMPORTED_MODULE_0__.session.defaultSession && isInstalledToDefaultSession) return; if (ses === electron__WEBPACK_IMPORTED_MODULE_0__.session.defaultSession) isInstalledToDefaultSession = true; let devtron; try { // register service worker preload script const dirname = import.meta.url; // __dirname is replaced with import.meta.url in ESM builds using webpack const serviceWorkerPreloadPath = (0,node_module__WEBPACK_IMPORTED_MODULE_2__.createRequire)(dirname).resolve('@hitarth-gg/devtron/service-worker-preload'); const rendererPreloadPath = (0,node_module__WEBPACK_IMPORTED_MODULE_2__.createRequire)(dirname).resolve('@hitarth-gg/devtron/renderer-preload'); ses.registerPreloadScript({ filePath: serviceWorkerPreloadPath, type: 'service-worker', id: 'devtron-sw-preload', }); ses.registerPreloadScript({ filePath: rendererPreloadPath, type: 'frame', id: 'devtron-renderer-preload', }); // load extension const extensionPath = node_path__WEBPACK_IMPORTED_MODULE_1___default().resolve(serviceWorkerPreloadPath, '..', '..', 'extension'); devtron = await ses.extensions.loadExtension(extensionPath, { allowFileAccess: true }); await startServiceWorker(ses, devtron); _utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.info('Devtron loaded successfully'); } catch (error) { _utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.error('Failed to load Devtron:', error); } }; electron__WEBPACK_IMPORTED_MODULE_0__.app.on('session-created', installToSession); // explicitly install Devtron to the defaultSession in case the app is already ready if (!isInstalledToDefaultSession && electron__WEBPACK_IMPORTED_MODULE_0__.app.isReady()) await installToSession(electron__WEBPACK_IMPORTED_MODULE_0__.session.defaultSession); } /** * Retrieves the list of IPC events tracked by Devtron. * * - If called before installation or before the Devtron service worker is ready, * an empty array will be returned. */ async function getEvents() { if (!isInstalled) { _utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.warn('You are trying to get IPC events before Devtron is installed.'); return []; } if (!devtronSW) { _utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.warn('Devtron service-worker is not registered yet. Cannot get IPC events.'); return []; } devtronSW.send('devtron-get-ipc-events'); return new Promise((resolve) => { devtronSW.ipc.once('devtron-ipc-events', (event, ipcEvents) => { resolve(ipcEvents); }); }); } const devtron = { install, getEvents, }; })(); const __webpack_exports__devtron = __webpack_exports__.devtron; export { __webpack_exports__devtron as devtron };