Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(pre-req): several fixes to the current hidden window launching process - INS-3319 #7174

Merged
merged 8 commits into from
Mar 22, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ test('handle hidden browser window getting closed', async ({ app, page }) => {
await page.getByTestId('settings-button').click();
await page.getByLabel('Request timeout (ms)').fill('1');
await page.getByRole('button', { name: '' }).click();
await page.getByRole('button', { name: 'Send' }).click();

await page.getByText('Pre-request Scripts').click();
await page.getByLabel('Request Collection').getByTestId('Long running task').press('Enter');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();

await page.getByText('Timeout: Pre-request script took too long').click();
await page.getByRole('tab', { name: 'Timeline' }).click();
Expand All @@ -57,5 +60,6 @@ test('handle hidden browser window getting closed', async ({ app, page }) => {
const hiddenWindow = windows[1];
hiddenWindow.close();
await page.getByRole('button', { name: 'Send' }).click();
await page.getByText('Timeout: Hidden browser window is not responding').click();
// as the hidden window is restarted, it should not show "Timeout: Hidden browser window is not responding"
await page.getByText('Timeout: Pre-request script took too long').click();
Comment on lines +63 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await page.getByText('Timeout: Pre-request script took too long').click();
await page.getByText('Timeout: An error occurred while running a pre-request script please restart Insomnia').click();

});
5 changes: 5 additions & 0 deletions packages/insomnia/src/hidden-window-preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface HiddenBrowserWindowToMainBridgeAPI {
onmessage: (listener: (data: any, callback: (result: any) => void) => void) => void;
curlRequest: (options: any) => Promise<any>;
readCurlResponse: (options: { bodyPath: string; bodyCompression: Compression }) => Promise<{ body: string; error: string }>;
setBusy: (busy: boolean) => void;
}
const bridge: HiddenBrowserWindowToMainBridgeAPI = {
onmessage: listener => {
Expand All @@ -17,8 +18,11 @@ const bridge: HiddenBrowserWindowToMainBridgeAPI = {
console.log('[preload] opened port to insomnia renderer');
const callback = (result: RequestContext) => port.postMessage(result);
port.onmessage = event => listener(event.data, callback);
ipcRenderer.invoke('hidden-window-received-port');
};

ipcRenderer.on('renderer-listener', rendererListener);
ipcRenderer.invoke('renderer-listener-ready');
return () => ipcRenderer.removeListener('renderer-listener', rendererListener);
},

Expand All @@ -34,6 +38,7 @@ const bridge: HiddenBrowserWindowToMainBridgeAPI = {

curlRequest: options => ipcRenderer.invoke('curlRequest', options),
readCurlResponse: options => ipcRenderer.invoke('readCurlResponse', options),
setBusy: busy => ipcRenderer.send('set-hidden-window-busy-status', busy),
};

if (process.contextIsolated) {
Expand Down
3 changes: 3 additions & 0 deletions packages/insomnia/src/hidden-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface HiddenBrowserWindowBridgeAPI {
};

window.bridge.onmessage(async (data, callback) => {
window.bridge.setBusy(true);
console.log('[hidden-browser-window] recieved message', data);
try {
const timeout = data.context.timeout || 5000;
Expand All @@ -20,6 +21,8 @@ window.bridge.onmessage(async (data, callback) => {
} catch (err) {
console.error('error', err);
callback({ error: err.message });
} finally {
window.bridge.setBusy(false);
}
});

Expand Down
14 changes: 7 additions & 7 deletions packages/insomnia/src/main.development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ const _launchApp = async () => {
ipcMain.once('halfSecondAfterAppStart', () => {
console.log('[main] Window ready, handling command line arguments', process.argv);
const args = process.argv.slice(1).filter(a => a !== '.');
console.log('[main] Check args and create windows', args);
if (args.length) {
window = windowUtils.getOrCreateWindow();
windowUtils.createHiddenBrowserWindow();
window = windowUtils.createWindowsAndReturnMain();
window.webContents.send('shell:open', args.join());
}
});
Expand All @@ -168,7 +168,7 @@ const _launchApp = async () => {
// Called when second instance launched with args (Windows/Linux)
app.on('second-instance', (_1, args) => {
console.log('Second instance listener received:', args.join('||'));
window = windowUtils.getOrCreateWindow();
window = windowUtils.createWindowsAndReturnMain();
if (window) {
if (window.isMinimized()) {
window.restore();
Expand All @@ -179,24 +179,24 @@ const _launchApp = async () => {
console.log('[main] Open Deep Link URL sent from second instance', lastArg);
window.webContents.send('shell:open', lastArg);
});
window = windowUtils.getOrCreateWindow();
window = windowUtils.createWindowsAndReturnMain();

app.on('open-url', (_event, url) => {
console.log('[main] Open Deep Link URL', url);
window = windowUtils.getOrCreateWindow();
window = windowUtils.createWindowsAndReturnMain();
if (window) {
if (window.isMinimized()) {
window.restore();
}
window.focus();
} else {
window = windowUtils.getOrCreateWindow();
window = windowUtils.createWindowsAndReturnMain();
}
window.webContents.send('shell:open', url);
});
}
} else {
window = windowUtils.getOrCreateWindow();
window = windowUtils.createWindowsAndReturnMain();
}

// Don't send origin header from Insomnia because we're not technically using CORS
Expand Down
151 changes: 101 additions & 50 deletions packages/insomnia/src/main/window-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type BrowserWindow as ElectronBrowserWindow,
clipboard,
dialog,
ipcMain,
Menu,
type MenuItemConstructorOptions,
MessageChannelMain,
Expand Down Expand Up @@ -37,6 +38,7 @@ const MINIMUM_HEIGHT = 400;

const browserWindows = new Map<'Insomnia' | 'HiddenBrowserWindow', ElectronBrowserWindow>();
let localStorage: LocalStorage | null = null;
let hiddenWindowIsBusy = false;

interface Bounds {
height?: number;
Expand All @@ -49,64 +51,109 @@ export function init() {
initLocalStorage();
}

export async function createHiddenBrowserWindow(): Promise<ElectronBrowserWindow> {
// if open, close it
if (browserWindows.get('HiddenBrowserWindow')) {
await new Promise<void>(resolve => {
const hiddenBrowserWindow = browserWindows.get('HiddenBrowserWindow');
invariant(hiddenBrowserWindow, 'hiddenBrowserWindow is not defined');

// overwrite the closed handler
hiddenBrowserWindow.on('closed', () => {
if (hiddenBrowserWindow) {
console.log('[main] restarting hidden browser window');
browserWindows.delete('HiddenBrowserWindow');
}
resolve();
export async function createHiddenBrowserWindow() {
const mainWindow = browserWindows.get('Insomnia');
invariant(mainWindow, 'MainWindow is not defined, please restart the app.');

console.log('[main] Registering the hidden window restarting handler');
ipcMain.on('set-hidden-window-busy-status', (_, busyStatus) => {
hiddenWindowIsBusy = busyStatus;
});
// when the main window runs a script
// if the hidden window is down, start it
ipcMain.handle('open-channel-to-hidden-browser-window', async event => {
// sync the hidden window status
const runningHiddenWindow = browserWindows.get('HiddenBrowserWindow');
const isAvailable = !hiddenWindowIsBusy && runningHiddenWindow;
if (isAvailable) {
return;
}
const isOccupied = hiddenWindowIsBusy && runningHiddenWindow;
if (isOccupied) {
// stop and sync the map
await new Promise<void>(resolve => {
invariant(runningHiddenWindow, 'hiddenBrowserWindow is running');
// overwrite the closed handler
runningHiddenWindow.on('closed', () => {
if (runningHiddenWindow) {
console.log('[main] restarting hidden browser window:', runningHiddenWindow.id);
browserWindows.delete('HiddenBrowserWindow');
}
resolve();
});
stopHiddenBrowserWindow();
hiddenWindowIsBusy = false;
});
}

const windowWasClosedUnexpectedly = hiddenWindowIsBusy && !runningHiddenWindow;
if (windowWasClosedUnexpectedly) {
hiddenWindowIsBusy = false;
}

stopHiddenBrowserWindow();
console.log('[main] hidden window is down, restarting');
const hiddenBrowserWindow = new BrowserWindow({
show: false,
title: 'HiddenBrowserWindow',
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
minHeight: MINIMUM_HEIGHT,
minWidth: MINIMUM_WIDTH,
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload: path.join(__dirname, 'hidden-window-preload.js'),
spellcheck: false,
devTools: process.env.NODE_ENV === 'development',
},
});
}
const hiddenBrowserWindow = new BrowserWindow({
show: false,
title: 'HiddenBrowserWindow',
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
minHeight: MINIMUM_HEIGHT,
minWidth: MINIMUM_WIDTH,
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload: path.join(__dirname, 'hidden-window-preload.js'),
spellcheck: false,
devTools: process.env.NODE_ENV === 'development',
},
});
browserWindows.set('HiddenBrowserWindow', hiddenBrowserWindow);

const hiddenBrowserWindowPath = path.resolve(__dirname, 'hidden-window.html');
const hiddenBrowserWindowUrl = process.env.HIDDEN_BROWSER_WINDOW_URL || pathToFileURL(hiddenBrowserWindowPath).href;
hiddenBrowserWindow.loadURL(hiddenBrowserWindowUrl);
console.log(`[main] Loading ${hiddenBrowserWindowUrl}`);
hiddenBrowserWindow.on('closed', () => {
if (browserWindows.get('HiddenBrowserWindow')) {
console.log('[main] closing hidden browser window');
browserWindows.delete('HiddenBrowserWindow');
}
});

hiddenBrowserWindow.on('closed', () => {
if (browserWindows.get('HiddenBrowserWindow')) {
console.log('[main] closing hidden browser window');
browserWindows.delete('HiddenBrowserWindow');
}
});
const mainWindow = browserWindows.get('Insomnia');
const hiddenBrowserWindowPath = path.resolve(__dirname, 'hidden-window.html');
const hiddenBrowserWindowUrl = process.env.HIDDEN_BROWSER_WINDOW_URL || pathToFileURL(hiddenBrowserWindowPath).href;
hiddenBrowserWindow.loadURL(hiddenBrowserWindowUrl);
console.log(`[main] Loading ${hiddenBrowserWindowUrl}`);

ipcMain.removeHandler('renderer-listener-ready');
const hiddenWinListenerReady = new Promise<void>(resolve => {
ipcMain.handleOnce('renderer-listener-ready', () => {
console.log('[main] hidden window listener is ready');
resolve();
});
});
await hiddenWinListenerReady;

ipcMain.removeHandler('hidden-window-received-port');
const hiddenWinPortReady = new Promise<void>(resolve => {
ipcMain.handleOnce('hidden-window-received-port', () => {
console.log('[main] hidden window has received port');
resolve();
});
});

invariant(mainWindow, 'mainWindow is not defined');
mainWindow.webContents.mainFrame.ipc.on('open-channel-to-hidden-browser-window', event => {
const { port1, port2 } = new MessageChannelMain();
hiddenBrowserWindow.webContents.postMessage('renderer-listener', null, [port1]);
await hiddenWinPortReady;

ipcMain.removeHandler('main-window-script-port-ready');
const mainWinPortReady = new Promise<void>(resolve => {
ipcMain.handleOnce('main-window-script-port-ready', () => {
console.log('[main] main window has received hidden window port');
resolve();
});
});

event.senderFrame.postMessage('hidden-browser-window-response-listener', null, [port2]);
port1.close();
port2.close();
await mainWinPortReady;

browserWindows.set('HiddenBrowserWindow', hiddenBrowserWindow);
});
return hiddenBrowserWindow;
}

export function stopHiddenBrowserWindow() {
Expand Down Expand Up @@ -737,6 +784,10 @@ function initLocalStorage() {
localStorage = new LocalStorage(localStoragePath);
}

export function getOrCreateWindow() {
return browserWindows.get('Insomnia') ?? createWindow();
export function createWindowsAndReturnMain() {
const mainWindow = browserWindows.get('Insomnia') ?? createWindow();
if (!browserWindows.get('HiddenBrowserWindow')) {
createHiddenBrowserWindow();
Comment on lines +788 to +790
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it helps to keep createWindow and createHiddenWindow signatures and behaviour aligned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool

}
return mainWindow;
}
38 changes: 25 additions & 13 deletions packages/insomnia/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { contextBridge, ipcRenderer } from 'electron';
import { gRPCBridgeAPI } from './main/ipc/grpc';
import { CurlBridgeAPI } from './main/network/curl';
import type { WebSocketBridgeAPI } from './main/network/websocket';
import { invariant } from './utils/invariant';

const ports = new Map<'hiddenWindowPort', MessagePort>();

const webSocket: WebSocketBridgeAPI = {
open: options => ipcRenderer.invoke('webSocket.open', options),
Expand Down Expand Up @@ -70,22 +73,31 @@ const main: Window['main'] = {
},
},
hiddenBrowserWindow: {
runPreRequestScript: options => new Promise((resolve, reject) => {
ipcRenderer.send('open-channel-to-hidden-browser-window');
ipcRenderer.once('hidden-browser-window-response-listener', event => {
const [port] = event.ports;
port.onmessage = event => {
console.log('received result:', event.data);
if (event.data.error) {
reject(new Error(event.data.error));
}
resolve(event.data);
};
port.postMessage({ ...options, type: 'runPreRequestScript' });
});
runPreRequestScript: options => new Promise(async (resolve, reject) => {
await ipcRenderer.invoke('open-channel-to-hidden-browser-window');

const port = ports.get('hiddenWindowPort');
invariant(port, 'hiddenWindowPort is undefined');

port.onmessage = event => {
console.log('received result:', event.data);
if (event.data.error) {
reject(new Error(event.data.error));
}
resolve(event.data);
};

port.postMessage({ ...options, type: 'runPreRequestScript' });
}),
},
};

ipcRenderer.on('hidden-browser-window-response-listener', event => {
const [port] = event.ports;
ports.set('hiddenWindowPort', port);
ipcRenderer.invoke('main-window-script-port-ready');
});

const dialog: Window['dialog'] = {
showOpenDialog: options => ipcRenderer.invoke('showOpenDialog', options),
showSaveDialog: options => ipcRenderer.invoke('showSaveDialog', options),
Expand Down
Loading