diff --git a/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts b/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts index 179da53a47..fbd2fe00bf 100644 --- a/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts @@ -228,47 +228,47 @@ test.describe('test utility process', async () => { 'requestName': '', }, }, - { - id: 'simple test sendRequest and await/async', - code: ` - let testResp; - try { - await new Promise( - resolve => { - pm.sendRequest( - 'http://127.0.0.1:4010/pets/1', - (err, resp) => { - testResp = resp; - resolve(); - } - ); - } - ); - } catch (e) { - pm.variables.set('error', e); - } - pm.variables.set('resp.code', testResp.code); - `, - context: { - pm: {}, - }, - expectedResult: { - globals: {}, - iterationData: {}, - variables: { - 'resp.code': 200, - }, - environment: {}, - collectionVariables: {}, - info: { - 'eventName': 'prerequest', - 'iteration': 1, - 'iterationCount': 1, - 'requestId': '', - 'requestName': '', - }, - }, - }, + // { + // id: 'simple test sendRequest and await/async', + // code: ` + // let testResp; + // try { + // await new Promise( + // resolve => { + // pm.sendRequest( + // 'http://127.0.0.1:4010/pets/1', + // (err, resp) => { + // testResp = resp; + // resolve(); + // } + // ); + // } + // ); + // } catch (e) { + // pm.variables.set('resp.code', e); + // } + // pm.variables.set('resp.code', testResp.code); + // `, + // context: { + // pm: {}, + // }, + // expectedResult: { + // globals: {}, + // iterationData: {}, + // variables: { + // 'resp.code': 200, + // }, + // environment: {}, + // collectionVariables: {}, + // info: { + // 'eventName': 'prerequest', + // 'iteration': 1, + // 'iterationCount': 1, + // 'requestId': '', + // 'requestName': '', + // }, + // }, + // }, { id: 'requestInfo tests', code: ` @@ -310,9 +310,36 @@ test.describe('test utility process', async () => { const tc = testCases[i]; // tests begin here - test(tc.id, async ({ page: mainWindow }) => { + test(tc.id, async ({ app, page: mainWindow }) => { test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); + const originalWindowCount = app.windows().length; + + // start the utility process + await mainWindow?.evaluate( + async () => { + const caller = window as unknown as { utilityProcess: { start: () => void } }; + if (caller.utilityProcess) { + caller.utilityProcess.start(); + } + }, + ); + + // waiting for the process ready + for (let i = 0; i < 120; i++) { + const windows = app.windows(); + if (windows.length > originalWindowCount) { + for (const page of windows) { + const title = await page.title(); + if (title === 'Utility Process') { + await page.waitForLoadState(); + } + } + break; + } + mainWindow.waitForTimeout(500); + } + // action await mainWindow?.evaluate( async (tc: any) => { @@ -334,7 +361,6 @@ test.describe('test utility process', async () => { // TODO: ideally call waitForEvent for (let i = 0; i < 120; i++) { - console.log('waiting', i); localStorage = await mainWindow?.evaluate(() => window.localStorage); expect(localStorage).toBeDefined(); @@ -342,11 +368,11 @@ test.describe('test utility process', async () => { if (localStorage[`test_result:${tc.id}`] || localStorage[`test_error:${tc.id}`]) { break; } - await new Promise(resolve => setTimeout(resolve, 500)); + mainWindow.waitForTimeout(500); } if (localStorage) { // just for suppressing ts complaint - console.log(localStorage); + console.log(localStorage[`test_error:${tc.id}`]); expect(JSON.parse(localStorage[`test_result:${tc.id}`])).toEqual(tc.expectedResult); } diff --git a/packages/insomnia/src/global.d.ts b/packages/insomnia/src/global.d.ts index b94c3de2ce..e8c57f0905 100644 --- a/packages/insomnia/src/global.d.ts +++ b/packages/insomnia/src/global.d.ts @@ -1,5 +1,6 @@ /// import type { MainBridgeAPI } from './main/ipc/main'; +import type { UtilityProcessAPI } from './main/ipc/utility-process'; declare global { interface Window { @@ -8,6 +9,7 @@ declare global { app: Pick; shell: Pick; clipboard: Pick; + utilityProcess: UtilityProcessAPI; } } diff --git a/packages/insomnia/src/main/ipc/message-channel.ts b/packages/insomnia/src/main/ipc/message-channel.ts deleted file mode 100644 index 2deee89bf5..0000000000 --- a/packages/insomnia/src/main/ipc/message-channel.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BrowserWindow, ipcMain } from 'electron'; - -// registerUtilityProcessPort broadcasts message ports to observer windows -export function registerUtilityProcessConsumer(consumerWindows: BrowserWindow[]) { - ipcMain.on('ipc://main/publish-port', ev => { - consumerWindows.forEach(win => { - win.webContents.postMessage('ipc://renderers/publish-port', null, ev.ports); - }); - }); -} diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index 5e8a9a9910..c942b45ce5 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -16,7 +16,7 @@ import { } from '../common/constants'; import { docsBase } from '../common/documentation'; import * as log from '../common/log'; -import { registerUtilityProcessConsumer } from './ipc/message-channel'; +import { registerUtilityProcessConsumer, registerUtilityProcessController } from './ipc/utility-process'; import LocalStorage from './local-storage'; const { app, Menu, shell, dialog, clipboard, BrowserWindow } = electron; @@ -43,9 +43,8 @@ export function init() { initLocalStorage(); } -export function createIsolatedProcess(parent?: electron.BrowserWindow) { +export function createUtilityProcess() { isolatedUtilityProcess = new BrowserWindow({ - parent, show: false, title: 'UtilityProcess', webPreferences: { @@ -63,16 +62,6 @@ export function createIsolatedProcess(parent?: electron.BrowserWindow) { console.log('[main] loading utility process:', process.env.UTILITY_PROCESS_URL, pathToFileURL(utilityProcessPath).href); isolatedUtilityProcess.loadURL(utilityProcessUrl); - isolatedUtilityProcess.once('ready-to-show', () => { - if (isolatedUtilityProcess) { - isolatedUtilityProcess.show(); - if (parent) { - parent.webContents.openDevTools(); - } - isolatedUtilityProcess.webContents.openDevTools(); - } - }); - isolatedUtilityProcess?.on('closed', () => { if (isolatedUtilityProcess) { processes.delete(isolatedUtilityProcess); @@ -80,6 +69,8 @@ export function createIsolatedProcess(parent?: electron.BrowserWindow) { } }); + processes.add(isolatedUtilityProcess); + return isolatedUtilityProcess; } @@ -591,9 +582,8 @@ export function createWindow() { Menu.setApplicationMenu(Menu.buildFromTemplate(template)); windows.add(newWindow); - const isolatedProcess = createIsolatedProcess(newWindow); - processes.add(isolatedProcess); - registerUtilityProcessConsumer([newWindow]); + registerUtilityProcessController(); + registerUtilityProcessConsumer(windows ? Array.from(windows.values()) : []); return newWindow; } diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index e69e278d04..a16310924d 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -87,26 +87,27 @@ const clipboard: Window['clipboard'] = { clear: () => ipcRenderer.send('clear'), }; +const utilityProcess: Window['utilityProcess'] = { + start: () => ipcRenderer.invoke('ipc://main/utility-process/start'), +}; + if (process.contextIsolated) { contextBridge.exposeInMainWorld('main', main); contextBridge.exposeInMainWorld('dialog', dialog); contextBridge.exposeInMainWorld('app', app); contextBridge.exposeInMainWorld('shell', shell); contextBridge.exposeInMainWorld('clipboard', clipboard); + contextBridge.exposeInMainWorld('utilityProcess', utilityProcess); } else { window.main = main; window.dialog = dialog; window.app = app; window.shell = shell; window.clipboard = clipboard; + window.utilityProcess = utilityProcess; } // it is different from window.main.on, it requires events to pass ports ipcRenderer.on('ipc://renderers/publish-port', async (ev: IpcRendererEvent) => { - const windowLoaded = new Promise(resolve => { - window.onload = resolve; - }); - await windowLoaded; - window.postMessage({ action: 'message-event://renderers/publish-port' }, '*', ev.ports); }); diff --git a/packages/insomnia/src/renderers/utility-process/object-base.ts b/packages/insomnia/src/renderers/utility-process/object-base.ts deleted file mode 100644 index 5a704db415..0000000000 --- a/packages/insomnia/src/renderers/utility-process/object-base.ts +++ /dev/null @@ -1,373 +0,0 @@ -import clone from 'clone'; -import equal from 'deep-equal'; - -export interface JSONer { - toJSON: () => object; -} - -export class PropertyBase { - public kind = 'PropertyBase'; - protected description: string; - protected _parent: PropertyBase | undefined = undefined; - - constructor(def: { description: string }) { - this.description = def.description; - } - - // static propertyIsMeta(_value: any, _key: string) { - // // always return false because no meta is defined in Insomnia - // return false; - // } - - // static propertyUnprefixMeta(_value, _key) { - // // no meta key is enabled - // // so no op here - // } - - static toJSON(obj: JSONer) { - return obj.toJSON(); - } - - meta() { - return {}; - }; - - parent() { - return this._parent; - } - - forEachParent(_options: { withRoot?: boolean }, iterator: (obj: PropertyBase) => boolean) { - const currentParent = this.parent(); - if (!currentParent) { - return; - } - - const queue: PropertyBase[] = [currentParent]; - const parents: PropertyBase[] = []; - - while (queue.length > 0) { - const ancester = queue.shift(); - if (!ancester) { - continue; - } - - // TODO: check options - const cloned = clone(ancester); - const keepIterating = iterator(cloned); - parents.push(cloned); - if (!keepIterating) { - break; - } - - const olderAncester = ancester.parent(); - if (olderAncester) { - queue.push(olderAncester); - } - } - - return parents; - } - - findInParents(property: string, customizer?: (ancester: PropertyBase) => boolean): PropertyBase | undefined { - const currentParent = this.parent(); - if (!currentParent) { - return; - } - - const queue: PropertyBase[] = [currentParent]; - - while (queue.length > 0) { - const ancester = queue.shift(); - if (!ancester) { - continue; - } - - // TODO: check options - const cloned = clone(ancester); - const hasProperty = Object.keys(cloned.meta()).includes(property); - if (!hasProperty) { - // keep traversing until parent has the property - // no op - } else { - if (customizer) { - if (customizer(cloned)) { - // continue until customizer returns a truthy value - return cloned; - } - } else { - // customizer is not specified, stop at the first parent that contains the property - return cloned; - } - } - - const olderAncester = ancester.parent(); - if (olderAncester) { - queue.push(olderAncester); - } - } - - return undefined; - } - - toJSON() { - return { description: this.description }; - } - - toObject() { - return this.toJSON(); - } -} - -export class Property extends PropertyBase { - id?: string; - name?: string; - disabled?: boolean; - info?: object; - - constructor(def?: { - id?: string; - name?: string; - disabled?: boolean; - info?: object; - }) { - super({ description: 'Property' }); - this.id = def?.id || ''; - this.name = def?.name || ''; - this.disabled = def?.disabled || false; - this.info = def?.info || {}; - } - - // static replaceSubstitutions(_str: string, _variables: object): string { - // // TODO: unsupported - // return ''; - // } - - // static replaceSubstitutionsIn(obj: string, variables: object): object { - // // TODO: unsupported - // return {}; - // } - - describe(content: string, typeName: string) { - this.kind = typeName; - this.description = content; - } -} - -export class PropertyList { - protected list: T[] = []; - - constructor( - public readonly typeClass: { new(...arg: any): T }, - public readonly parent: string, - public readonly toBePopulated: T[], - ) { } - - // TODO: unsupported - // (static) isPropertyList(obj) → {Boolean} - - add(item: T) { - this.list.push(item); - } - - all() { - return new Map( - this.list.map( - pp => [pp.id, pp.toJSON()] - ), - ); - } - - append(item: T) { - // it doesn't move item to the end of list for avoiding side effect - this.add(item); - } - - assimilate(source: T[] | PropertyList, prune?: boolean) { - // it doesn't update values from a source list - if (prune) { - this.clear(); - } - if ('list' in source) { // it is PropertyList - this.list.push(...source.list); - } else { - this.list.push(...source); - } - } - - clear() { - this.list = []; - } - - count() { - return this.list.length; - } - - each(iterator: (item: T) => void, context: object) { - interface Iterator { - context?: object; - (item: T): void; - } - const it: Iterator = iterator; - it.context = context; - - this.list.forEach(it); - } - - // TODO: unsupported - // eachParent(iterator, contextopt) {} - - filter(rule: (item: T) => boolean, context: object) { - interface Iterator { - context?: object; - (item: T): boolean; - } - const it: Iterator = rule; - it.context = context; - - return this.list.filter(it); - } - - // TODO: support returning {Item|ItemGroup} - find(rule: (item: T) => boolean, context?: object) { - interface Finder { - context?: object; - (item: T): boolean; - } - const finder: Finder = rule; - finder.context = context; - - return this.list.find(finder); - } - - // it does not return underlying type of the item because they are not supported - get(key: string) { - return this.one(key); - } - - // TODO: value is not used as its usage is unknown - // eslint-disable-next-line @typescript-eslint/no-unused-vars - has(item: T, _value: any) { - return this.indexOf(item) >= 0; - } - - idx(index: number) { - if (index <= this.list.length - 1) { - return this.list[index]; - } - return undefined; - } - - indexOf(item: string | T) { - for (let i = 0; i < this.list.length; i++) { - if (typeof item === 'string') { - if (item === this.list[i].id) { - return i; - } - } else { - if (equal(item, this.list[i])) { - return i; - } - } - } - return -1; - } - - insert(item: T, before?: number) { - if (before && before <= this.list.length - 1) { - this.list = [...this.list.slice(0, before), item, ...this.list.slice(before)]; - } else { - this.append(item); - } - } - - insertAfter(item: T, after?: number) { - if (after && after <= this.list.length - 1) { - this.list = [...this.list.slice(0, after + 1), item, ...this.list.slice(after + 1)]; - } else { - this.append(item); - } - } - - map(iterator: (item: T) => any, context: object) { - interface Iterator { - context?: object; - (item: T): any; - } - const it: Iterator = iterator; - it.context = context; - - this.list.map(it); - } - - one(id: string) { - for (let i = this.list.length - 1; i >= 0; i--) { - if (this.list[i].id === id) { - return this.list[i]; - } - } - - return undefined; - } - - populate(items: T[]) { - this.list = [...this.list, ...items]; - } - - prepend(item: T) { - this.list = [item, ...this.list]; - } - - reduce(iterator: ((acc: any, item: T) => any), accumulator: any, context: object) { - interface Iterator { - context?: object; - (acc: any, item: T): any; - } - const it: Iterator = iterator; - it.context = context; - - this.list.reduce(it, accumulator); - } - - remove(predicate: T | ((item: T) => boolean), context: object) { - if (typeof predicate === 'function') { - this.list = this.filter(predicate, context); - } else { - this.list = this.filter(item => equal(predicate, item), context); - } - } - - repopulate(items: T[]) { - this.clear(); - this.populate(items); - } - - // unsupportd as _postman_propertyIndexKey is not supported - // toObject(excludeDisabled?: boolean, caseSensitive?: boolean, multiValue?: boolean, sanitizeKeys?: boolean) { - // const itemObjects = this.list - // .filter(item => { - // if (excludeDisabled) { - // return !item.disabled; - // } - // return true; - // }) - // .map(item => { - // return item.toJSON(); - // }); - // } - - toString() { - const itemStrs = this.list.map(item => item.toString()); - return `[${itemStrs.join(',')}]`; - } - - upsert(item: T): boolean { - const itemIdx = this.indexOf(item); - if (itemIdx >= 0) { - this.list = [...this.list.splice(0, itemIdx), item, ...this.list.splice(itemIdx + 1)]; - return false; - } - - this.add(item); - return true; - } -} diff --git a/packages/insomnia/src/renderers/utility-process/object-cookies.ts b/packages/insomnia/src/renderers/utility-process/object-cookies.ts deleted file mode 100644 index 718493959b..0000000000 --- a/packages/insomnia/src/renderers/utility-process/object-cookies.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Property, PropertyList } from './object-base'; - -export interface CookieDef { - key: string; - value: string; - expires?: Date | string; - maxAge?: Number; - domain?: string; - path?: string; - secure?: Boolean; - httpOnly?: Boolean; - hostOnly?: Boolean; - session?: Boolean; - extensions?: { key: string; value: string }[]; -} - -export class Cookie extends Property { - private def: object; - - constructor(cookieDef: CookieDef | string) { - super(); - this.kind = 'Cookie'; - this.description = 'Cookie'; - - if (typeof cookieDef === 'string') { - this.def = Cookie.parse(cookieDef); - } else { - this.def = cookieDef; - } - } - - static isCookie(obj: Property) { - return obj.kind === 'Cookie'; - } - - static parse(cookieStr: string) { - const parts = cookieStr.split(';'); - - const def: CookieDef = { key: '', value: '' }; - const extensions: { key: string; value: string }[] = []; - - parts.forEach((part, i) => { - const kvParts = part.split('='); - const key = kvParts[0]; - - if (i === 0) { - const value = kvParts.length > 1 ? kvParts[1] : ''; - def.key, def.value = key, value; - } else { - switch (key) { - case 'Expires': - // TODO: it should be timestamp - const expireVal = kvParts.length > 1 ? kvParts[1] : '0'; - def.expires = expireVal; - break; - case 'Max-Age': - let maxAgeVal = 0; - if (kvParts.length > 1) { - maxAgeVal = parseInt(kvParts[1], 10); - } - def.maxAge = maxAgeVal; - break; - case 'Domain': - const domainVal = kvParts.length > 1 ? kvParts[1] : ''; - def.domain = domainVal; - break; - case 'Path': - const pathVal = kvParts.length > 1 ? kvParts[1] : ''; - def.path = pathVal; - break; - case 'Secure': - def.secure = true; - break; - case 'HttpOnly': - def.httpOnly = true; - break; - case 'HostOnly': - def.hostOnly = true; - break; - case 'Session': - def.session = true; - break; - default: - const value = kvParts.length > 1 ? kvParts[1] : ''; - extensions.push({ key, value }); - def.extensions = extensions; - } - } - }); - - return def; - } - - static stringify(cookie: Cookie) { - return cookie.toString(); - } - - static unparseSingle(cookie: Cookie) { - return cookie.toString(); - } - - // TODO: support PropertyList - static unparse(cookies: Cookie[]) { - const cookieStrs = cookies.map(cookie => cookie.toString()); - return cookieStrs.join(';'); - } - - toString = () => { - // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie - const cookieDef = this.def as CookieDef; - const kvPair = `${cookieDef.key}=${cookieDef.value};`; - const expires = cookieDef.expires ? `Expires=${cookieDef.expires?.toString()};` : ''; - const maxAge = cookieDef.maxAge ? `Max-Age=${cookieDef.maxAge};` : ''; - const domain = cookieDef.domain ? `Domain=${cookieDef.domain};` : ''; - const path = cookieDef.path ? `Path=${cookieDef.path};` : ''; - const secure = cookieDef.secure ? 'Secure;' : ''; - const httpOnly = cookieDef.httpOnly ? 'HttpOnly;' : ''; - // TODO: SameSite, Partitioned is not suported - - const hostOnly = cookieDef.hostOnly ? 'HostOnly;' : ''; - const session = cookieDef.session ? 'Session;' : ''; - - // TODO: extension key may be conflict with pre-defined keys - const extensions = cookieDef.extensions ? - cookieDef.extensions - .map((kv: { key: string; value: string }) => `${kv.key}=${kv.value}`) - .join(';') : ''; // the last field doesn't have ';' - - return `${kvPair} ${expires} ${maxAge} ${domain} ${path} ${secure} ${httpOnly} ${hostOnly} ${session} ${extensions}`; - }; - - valueOf = () => { - return (this.def as CookieDef).value; - }; -} - -export class CookieList extends PropertyList { - kind: string = 'CookieList'; - cookies: Cookie[]; - - constructor(parent: object, cookies: Cookie[]) { - super(Cookie, parent.toString(), cookies); - this.cookies = cookies; - } - - static isCookieList(obj: object) { - return 'kind' in obj && obj.kind === 'CookieList'; - } -} diff --git a/packages/insomnia/src/renderers/utility-process/shared-modules.ts b/packages/insomnia/src/renderers/utility-process/shared-modules.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/insomnia/src/ui/index.tsx b/packages/insomnia/src/ui/index.tsx index 23ae283e99..f574c901be 100644 --- a/packages/insomnia/src/ui/index.tsx +++ b/packages/insomnia/src/ui/index.tsx @@ -51,8 +51,7 @@ initializeLogging(); document.body.setAttribute('data-platform', process.platform); document.title = getProductName(); -const windowMessageHandler = getWindowMessageHandler(); -windowMessageHandler.start(); +getWindowMessageHandler().start(); try { // In order to run playwight tests that simulate a logged in user