Skip to content

Commit

Permalink
Fix: ensure the service worker is awake before every port message (#1433
Browse files Browse the repository at this point in the history
)

* fix: ensure there is an open port connection before sending a port message

* chore: remove unnecessary console.log

* fix: prevent Error: Invalid url messages when setting a non http active tab

* fix test
  • Loading branch information
F-OBrien authored Aug 6, 2024
1 parent c88dfe4 commit 07605ca
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 47 deletions.
65 changes: 65 additions & 0 deletions packages/extension-base/src/utils/portUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2019-2024 @polkadot/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Message } from '@polkadot/extension-base/types';

import { chrome } from '@polkadot/extension-inject/chrome';

export function setupPort (portName: string, onMessageHandler: (data: Message['data']) => void, onDisconnectHandler: () => void): chrome.runtime.Port {
const port = chrome.runtime.connect({ name: portName });

port.onMessage.addListener(onMessageHandler);

port.onDisconnect.addListener(() => {
console.log(`Disconnected from ${portName}`);
onDisconnectHandler();
});

return port;
}

export async function wakeUpServiceWorker (): Promise<{ status: string }> {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type: 'wakeup' }, (response: { status: string }) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(response);
}
});
});
}

// This object is required to allow jest.spyOn to be used to create a mock Implementation for testing
export const wakeUpServiceWorkerWrapper = { wakeUpServiceWorker };

export async function ensurePortConnection (
portRef: chrome.runtime.Port | undefined,
portConfig: {
portName: string,
onPortMessageHandler: (data: Message['data']) => void,
onPortDisconnectHandler: () => void
}
): Promise<chrome.runtime.Port> {
const maxAttempts = 5;
const delayMs = 1000;

for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const response = await wakeUpServiceWorkerWrapper.wakeUpServiceWorker();

if (response?.status === 'awake') {
if (!portRef) {
return setupPort(portConfig.portName, portConfig.onPortMessageHandler, portConfig.onPortDisconnectHandler);
}

return portRef;
}
} catch (error) {
console.error(`Attempt ${attempt + 1} failed: ${(error as Error).message}`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}

throw new Error('Failed to wake up the service worker and setup the port after multiple attempts');
}
2 changes: 0 additions & 2 deletions packages/extension-ui/src/Popup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ export default function Popup (): React.ReactElement {
const [settingsCtx, setSettingsCtx] = useState<SettingsStruct>(startSettings);
const history = useHistory();

console.log('WINDOW; ', window);

const _onAction = useCallback(
(to?: string): void => {
setWelcomeDone(window.localStorage.getItem('welcome_read') === 'ok');
Expand Down
18 changes: 16 additions & 2 deletions packages/extension-ui/src/messaging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,31 @@ import type * as _ from '@polkadot/dev-test/globals.d.ts';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import enzyme from 'enzyme';

import { wakeUpServiceWorkerWrapper } from '../../extension-base/src/utils/portUtils.js';
import { exportAccount } from './messaging.js';

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
enzyme.configure({ adapter: new Adapter() });

describe('messaging sends message to background via extension port for', () => {
it('exportAccount', () => {
beforeEach(() => {
jest.spyOn(wakeUpServiceWorkerWrapper, 'wakeUpServiceWorker').mockImplementation(() => Promise.resolve({ status: 'awake' }));
});

afterEach(() => {
jest.restoreAllMocks();
});

it('exportAccount', async () => {
const callback = jest.fn();

chrome.runtime.connect().onMessage.addListener(callback);
exportAccount('HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5', 'passw0rd').catch(console.error);

try {
await exportAccount('HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5', 'passw0rd');
} catch (error) {
console.error(error);
}

expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down
57 changes: 22 additions & 35 deletions packages/extension-ui/src/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { KeypairType } from '@polkadot/util-crypto/types';

import { PORT_EXTENSION } from '@polkadot/extension-base/defaults';
import { getId } from '@polkadot/extension-base/utils/getId';
import { ensurePortConnection } from '@polkadot/extension-base/utils/portUtils';
import { metadataExpand } from '@polkadot/extension-chains';

import allChains from './util/chains.js';
Expand All @@ -30,41 +31,11 @@ interface Handler {

type Handlers = Record<string, Handler>;

async function wakeupBackground (): Promise<Error | null> {
try {
await chrome.runtime.sendMessage({ type: 'wakeup' });

return null;
} catch (cause) {
return cause instanceof Error ? cause : new Error(String(cause));
}
}

async function createPort (name: string, maxAttempts: number, delayMs: number): Promise<chrome.runtime.Port> {
let lastError: Error | null = null;

for (let attempt = 0; attempt < maxAttempts; attempt++) {
const error = await wakeupBackground();

if (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}

const port = chrome.runtime.connect({ name });

return port;
}

throw new Error('Failed to create port after multiple attempts', { cause: lastError });
}

const port = await createPort(PORT_EXTENSION, 5, 1000);
const handlers: Handlers = {};

// setup a listener for messages, any incoming resolves the promise
port.onMessage.addListener((data: Message['data']): void => {
let port: chrome.runtime.Port | undefined;

function onPortMessageHandler (data: Message['data']): void {
const handler = handlers[data.id];

if (!handler) {
Expand All @@ -85,7 +56,17 @@ port.onMessage.addListener((data: Message['data']): void => {
} else {
handler.resolve(data.response);
}
});
}

function onPortDisconnectHandler (): void {
port = undefined;
}

const portConfig = {
onPortDisconnectHandler,
onPortMessageHandler,
portName: PORT_EXTENSION
};

function sendMessage<TMessageType extends MessageTypesWithNullRequest>(message: TMessageType): Promise<ResponseTypes[TMessageType]>;
function sendMessage<TMessageType extends MessageTypesWithNoSubscriptions>(message: TMessageType, request: RequestTypes[TMessageType]): Promise<ResponseTypes[TMessageType]>;
Expand All @@ -96,7 +77,13 @@ function sendMessage<TMessageType extends MessageTypes> (message: TMessageType,

handlers[id] = { reject, resolve, subscriber };

port.postMessage({ id, message, request: request || {} });
ensurePortConnection(port, portConfig).then((connectedPort) => {
connectedPort.postMessage({ id, message, request: request || {} });
port = connectedPort;
}).catch((error) => {
console.error(`Failed to send message: ${(error as Error).message}`);
reject(error);
});
});
}

Expand Down
15 changes: 13 additions & 2 deletions packages/extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,26 @@ chrome.runtime.onConnect.addListener((port): void => {
port.onDisconnect.addListener(() => console.log(`Disconnected from ${port.name}`));
});

function isValidUrl (url: string) {
try {
const urlObj = new URL(url);

return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
} catch (_e) {
return false;
}
}

function getActiveTabs () {
// queriing the current active tab in the current window should only ever return 1 tab
// although an array is specified here
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
// get the urls of the active tabs. In the case of new tab the url may be empty or undefined
// get the urls of the active tabs. Only http or https urls are supported. Other urls will be filtered out.
// e.g. browser tabs like chrome://newtab/, chrome://extensions/, about:addons etc will be filtered out
// we filter these out
const urls: string[] = tabs
.map(({ url }) => url)
.filter((url) => !!url) as string[];
.filter((url) => !!url && isValidUrl(url)) as string[];

const request: TransportRequestMessage<'pri(activeTabsUrl.update)'> = {
id: 'background',
Expand Down
24 changes: 18 additions & 6 deletions packages/extension/src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@
import type { Message } from '@polkadot/extension-base/types';

import { MESSAGE_ORIGIN_CONTENT, MESSAGE_ORIGIN_PAGE, PORT_CONTENT } from '@polkadot/extension-base/defaults';
import { ensurePortConnection } from '@polkadot/extension-base/utils/portUtils';
import { chrome } from '@polkadot/extension-inject/chrome';

// connect to the extension
const port = chrome.runtime.connect({ name: PORT_CONTENT });
let port: chrome.runtime.Port | undefined;

// send any messages from the extension back to the page
port.onMessage.addListener((data): void => {
function onPortMessageHandler (data: Message['data']): void {
window.postMessage({ ...data, origin: MESSAGE_ORIGIN_CONTENT }, '*');
});
}

function onPortDisconnectHandler (): void {
port = undefined;
}

const portConfig = {
onPortDisconnectHandler,
onPortMessageHandler,
portName: PORT_CONTENT
};

// all messages from the page, pass them to the extension
window.addEventListener('message', ({ data, source }: Message): void => {
Expand All @@ -21,7 +30,10 @@ window.addEventListener('message', ({ data, source }: Message): void => {
return;
}

port.postMessage(data);
ensurePortConnection(port, portConfig).then((connectedPort) => {
connectedPort.postMessage(data);
port = connectedPort;
}).catch((error) => console.error(`Failed to send message: ${(error as Error).message}`));
});

// inject our data injector
Expand Down

0 comments on commit 07605ca

Please sign in to comment.