Skip to content

Commit 07605ca

Browse files
authored
Fix: ensure the service worker is awake before every port message (#1433)
* 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
1 parent c88dfe4 commit 07605ca

File tree

6 files changed

+134
-47
lines changed

6 files changed

+134
-47
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2019-2024 @polkadot/extension-base authors & contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type { Message } from '@polkadot/extension-base/types';
5+
6+
import { chrome } from '@polkadot/extension-inject/chrome';
7+
8+
export function setupPort (portName: string, onMessageHandler: (data: Message['data']) => void, onDisconnectHandler: () => void): chrome.runtime.Port {
9+
const port = chrome.runtime.connect({ name: portName });
10+
11+
port.onMessage.addListener(onMessageHandler);
12+
13+
port.onDisconnect.addListener(() => {
14+
console.log(`Disconnected from ${portName}`);
15+
onDisconnectHandler();
16+
});
17+
18+
return port;
19+
}
20+
21+
export async function wakeUpServiceWorker (): Promise<{ status: string }> {
22+
return new Promise((resolve, reject) => {
23+
chrome.runtime.sendMessage({ type: 'wakeup' }, (response: { status: string }) => {
24+
if (chrome.runtime.lastError) {
25+
reject(new Error(chrome.runtime.lastError.message));
26+
} else {
27+
resolve(response);
28+
}
29+
});
30+
});
31+
}
32+
33+
// This object is required to allow jest.spyOn to be used to create a mock Implementation for testing
34+
export const wakeUpServiceWorkerWrapper = { wakeUpServiceWorker };
35+
36+
export async function ensurePortConnection (
37+
portRef: chrome.runtime.Port | undefined,
38+
portConfig: {
39+
portName: string,
40+
onPortMessageHandler: (data: Message['data']) => void,
41+
onPortDisconnectHandler: () => void
42+
}
43+
): Promise<chrome.runtime.Port> {
44+
const maxAttempts = 5;
45+
const delayMs = 1000;
46+
47+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
48+
try {
49+
const response = await wakeUpServiceWorkerWrapper.wakeUpServiceWorker();
50+
51+
if (response?.status === 'awake') {
52+
if (!portRef) {
53+
return setupPort(portConfig.portName, portConfig.onPortMessageHandler, portConfig.onPortDisconnectHandler);
54+
}
55+
56+
return portRef;
57+
}
58+
} catch (error) {
59+
console.error(`Attempt ${attempt + 1} failed: ${(error as Error).message}`);
60+
await new Promise((resolve) => setTimeout(resolve, delayMs));
61+
}
62+
}
63+
64+
throw new Error('Failed to wake up the service worker and setup the port after multiple attempts');
65+
}

packages/extension-ui/src/Popup/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ export default function Popup (): React.ReactElement {
7979
const [settingsCtx, setSettingsCtx] = useState<SettingsStruct>(startSettings);
8080
const history = useHistory();
8181

82-
console.log('WINDOW; ', window);
83-
8482
const _onAction = useCallback(
8583
(to?: string): void => {
8684
setWelcomeDone(window.localStorage.getItem('welcome_read') === 'ok');

packages/extension-ui/src/messaging.spec.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,31 @@ import type * as _ from '@polkadot/dev-test/globals.d.ts';
1010
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
1111
import enzyme from 'enzyme';
1212

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

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

1819
describe('messaging sends message to background via extension port for', () => {
19-
it('exportAccount', () => {
20+
beforeEach(() => {
21+
jest.spyOn(wakeUpServiceWorkerWrapper, 'wakeUpServiceWorker').mockImplementation(() => Promise.resolve({ status: 'awake' }));
22+
});
23+
24+
afterEach(() => {
25+
jest.restoreAllMocks();
26+
});
27+
28+
it('exportAccount', async () => {
2029
const callback = jest.fn();
2130

2231
chrome.runtime.connect().onMessage.addListener(callback);
23-
exportAccount('HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5', 'passw0rd').catch(console.error);
32+
33+
try {
34+
await exportAccount('HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5', 'passw0rd');
35+
} catch (error) {
36+
console.error(error);
37+
}
2438

2539
expect(callback).toHaveBeenCalledWith(
2640
expect.objectContaining({

packages/extension-ui/src/messaging.ts

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { KeypairType } from '@polkadot/util-crypto/types';
1515

1616
import { PORT_EXTENSION } from '@polkadot/extension-base/defaults';
1717
import { getId } from '@polkadot/extension-base/utils/getId';
18+
import { ensurePortConnection } from '@polkadot/extension-base/utils/portUtils';
1819
import { metadataExpand } from '@polkadot/extension-chains';
1920

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

3132
type Handlers = Record<string, Handler>;
3233

33-
async function wakeupBackground (): Promise<Error | null> {
34-
try {
35-
await chrome.runtime.sendMessage({ type: 'wakeup' });
36-
37-
return null;
38-
} catch (cause) {
39-
return cause instanceof Error ? cause : new Error(String(cause));
40-
}
41-
}
42-
43-
async function createPort (name: string, maxAttempts: number, delayMs: number): Promise<chrome.runtime.Port> {
44-
let lastError: Error | null = null;
45-
46-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
47-
const error = await wakeupBackground();
48-
49-
if (error) {
50-
lastError = error;
51-
await new Promise((resolve) => setTimeout(resolve, delayMs));
52-
continue;
53-
}
54-
55-
const port = chrome.runtime.connect({ name });
56-
57-
return port;
58-
}
59-
60-
throw new Error('Failed to create port after multiple attempts', { cause: lastError });
61-
}
62-
63-
const port = await createPort(PORT_EXTENSION, 5, 1000);
6434
const handlers: Handlers = {};
6535

66-
// setup a listener for messages, any incoming resolves the promise
67-
port.onMessage.addListener((data: Message['data']): void => {
36+
let port: chrome.runtime.Port | undefined;
37+
38+
function onPortMessageHandler (data: Message['data']): void {
6839
const handler = handlers[data.id];
6940

7041
if (!handler) {
@@ -85,7 +56,17 @@ port.onMessage.addListener((data: Message['data']): void => {
8556
} else {
8657
handler.resolve(data.response);
8758
}
88-
});
59+
}
60+
61+
function onPortDisconnectHandler (): void {
62+
port = undefined;
63+
}
64+
65+
const portConfig = {
66+
onPortDisconnectHandler,
67+
onPortMessageHandler,
68+
portName: PORT_EXTENSION
69+
};
8970

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

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

99-
port.postMessage({ id, message, request: request || {} });
80+
ensurePortConnection(port, portConfig).then((connectedPort) => {
81+
connectedPort.postMessage({ id, message, request: request || {} });
82+
port = connectedPort;
83+
}).catch((error) => {
84+
console.error(`Failed to send message: ${(error as Error).message}`);
85+
reject(error);
86+
});
10087
});
10188
}
10289

packages/extension/src/background.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,26 @@ chrome.runtime.onConnect.addListener((port): void => {
2929
port.onDisconnect.addListener(() => console.log(`Disconnected from ${port.name}`));
3030
});
3131

32+
function isValidUrl (url: string) {
33+
try {
34+
const urlObj = new URL(url);
35+
36+
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
37+
} catch (_e) {
38+
return false;
39+
}
40+
}
41+
3242
function getActiveTabs () {
3343
// queriing the current active tab in the current window should only ever return 1 tab
3444
// although an array is specified here
3545
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
36-
// get the urls of the active tabs. In the case of new tab the url may be empty or undefined
46+
// get the urls of the active tabs. Only http or https urls are supported. Other urls will be filtered out.
47+
// e.g. browser tabs like chrome://newtab/, chrome://extensions/, about:addons etc will be filtered out
3748
// we filter these out
3849
const urls: string[] = tabs
3950
.map(({ url }) => url)
40-
.filter((url) => !!url) as string[];
51+
.filter((url) => !!url && isValidUrl(url)) as string[];
4152

4253
const request: TransportRequestMessage<'pri(activeTabsUrl.update)'> = {
4354
id: 'background',

packages/extension/src/content.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@
44
import type { Message } from '@polkadot/extension-base/types';
55

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

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

12-
// send any messages from the extension back to the page
13-
port.onMessage.addListener((data): void => {
12+
function onPortMessageHandler (data: Message['data']): void {
1413
window.postMessage({ ...data, origin: MESSAGE_ORIGIN_CONTENT }, '*');
15-
});
14+
}
15+
16+
function onPortDisconnectHandler (): void {
17+
port = undefined;
18+
}
19+
20+
const portConfig = {
21+
onPortDisconnectHandler,
22+
onPortMessageHandler,
23+
portName: PORT_CONTENT
24+
};
1625

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

24-
port.postMessage(data);
33+
ensurePortConnection(port, portConfig).then((connectedPort) => {
34+
connectedPort.postMessage(data);
35+
port = connectedPort;
36+
}).catch((error) => console.error(`Failed to send message: ${(error as Error).message}`));
2537
});
2638

2739
// inject our data injector

0 commit comments

Comments
 (0)