fix: extension service workers not starting beyond first app launch (#50611)

* fix: extension service worker not starting beyond first app launch

* fix: set preference only for extensions with service workers
This commit is contained in:
Niklas Wenzel
2026-04-02 10:02:06 +02:00
committed by GitHub
parent 81f8fc1880
commit 156a4e610c
2 changed files with 110 additions and 3 deletions

View File

@@ -20,6 +20,7 @@
#include "base/time/time.h"
#include "content/public/browser/browser_context.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_pref_names.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/pref_names.h"
@@ -27,6 +28,7 @@
#include "extensions/common/error_utils.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/background_info.h"
namespace extensions {
@@ -143,6 +145,19 @@ void ElectronExtensionLoader::FinishExtensionLoad(
std::pair<scoped_refptr<const Extension>, std::string> result) {
scoped_refptr<const Extension> extension = result.first;
if (extension) {
ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(browser_context_);
if (BackgroundInfo::IsServiceWorkerBased(extension.get())) {
// Tell Chromium that it needs to start the extension's service worker.
// Chromium usually does this only when an extension is first installed
// because Chrome will restart the service worker when the browser
// relaunches. In Electron, we make a fresh install on every app start,
// so we need to run the fresh install logic again.
extension_prefs->UpdateExtensionPref(
extension.get()->id(), extensions::kPrefHasStartedServiceWorker,
base::Value(false));
}
extension_registrar_->AddExtension(extension);
// Write extension install time to ExtensionPrefs.
@@ -152,7 +167,6 @@ void ElectronExtensionLoader::FinishExtensionLoad(
// Implementation for writing the pref was based on
// PreferenceAPIBase::SetExtensionControlledPref.
{
ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(browser_context_);
ExtensionPrefs::ScopedDictionaryUpdate update(
extension_prefs, extension.get()->id(),
extensions::pref_names::kPrefPreferences);

View File

@@ -1,4 +1,4 @@
import { app, session, webFrameMain, BrowserWindow, ipcMain, WebContents, Extension, Session } from 'electron/main';
import { app, session, webFrameMain, BrowserWindow, ipcMain, WebContents, Extension, Session, ServiceWorkerInfo, ServiceWorkersRunningStatusChangedEventParams } from 'electron/main';
import { expect } from 'chai';
import * as WebSocket from 'ws';
@@ -10,7 +10,7 @@ import * as http from 'node:http';
import * as path from 'node:path';
import { emittedNTimes, emittedUntil } from './lib/events-helpers';
import { ifit, listen, waitUntil } from './lib/spec-helpers';
import { ifit, listen, startRemoteControlApp, waitUntil } from './lib/spec-helpers';
import { expectWarningMessages } from './lib/warning-helpers';
import { closeAllWindows, closeWindow, cleanupWebContents } from './lib/window-helpers';
@@ -816,6 +816,99 @@ describe('chrome extensions', () => {
expect(scope).equals(extension.url);
});
it('launches background service worker', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const launchPromise = new Promise<void>(resolve => {
customSession.serviceWorkers.on('running-status-changed', ({ runningStatus }) => {
if (runningStatus === 'running') resolve();
});
});
const extension = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'mv3-service-worker'));
await launchPromise;
const serviceWorkers = customSession.serviceWorkers.getAllRunning();
expect(Object.values(serviceWorkers).some(worker => worker.scope === extension.url)).equals(true);
});
it('launches background service worker when the extension is loaded again without restarting the app', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const extensionPath = path.join(fixtures, 'extensions', 'mv3-service-worker');
const isWorkerRunning = (extension: Extension) => {
const serviceWorkers = customSession.serviceWorkers.getAllRunning();
return Object.values(serviceWorkers).some(worker => worker.scope === extension.url);
};
const loadAndUnloadExtension = async () => {
const launchPromise = new Promise<void>(resolve => {
customSession.serviceWorkers.on('running-status-changed', ({ runningStatus }) => {
if (runningStatus === 'running') resolve();
});
});
const extension = await customSession.extensions.loadExtension(extensionPath);
await launchPromise;
expect(isWorkerRunning(extension)).equals(true);
const stopPromise = new Promise<void>(resolve => {
customSession.serviceWorkers.on('running-status-changed', ({ runningStatus }) => {
if (runningStatus === 'stopped') resolve();
});
});
customSession.extensions.removeExtension(extension.id);
await stopPromise;
expect(isWorkerRunning(extension)).equals(false);
};
for (let i = 0; i < 3; i++) {
await loadAndUnloadExtension();
}
});
// Regression test for https://github.com/electron/electron/issues/41613
it('launches background service worker after restarting the app', async () => {
const partition = `persist:${uuid.v4()}`;
const extensionPath = path.join(fixtures, 'extensions', 'mv3-service-worker');
const runRemoteControlApp = async () => {
const rc = await startRemoteControlApp();
const exitPromise = once(rc.process, 'exit');
const { workerScopes, extensionUrl } = await rc.remotely(async (partition: string, extensionPath: string) => {
const { session } = require('electron/main');
const { setTimeout } = require('node:timers/promises');
const customSession = session.fromPartition(partition);
const launchPromise = new Promise<void>(resolve => {
customSession.serviceWorkers.on('running-status-changed', ({ runningStatus }: ServiceWorkersRunningStatusChangedEventParams) => {
if (runningStatus === 'running') resolve();
});
});
const extension = await customSession.extensions.loadExtension(extensionPath);
await launchPromise;
const serviceWorkers = customSession.serviceWorkers.getAllRunning();
const workerScopes = Object.values(serviceWorkers).map(worker => (worker as ServiceWorkerInfo).scope);
const extensionUrl = extension.url;
// Give Chromium some time to update extensions::kPrefHasStartedServiceWorker on disk
await setTimeout(500);
global.setTimeout(() => require('electron').app.quit());
return { workerScopes, extensionUrl };
}, partition, extensionPath);
await exitPromise;
expect(workerScopes.includes(extensionUrl)).equals(true);
};
for (let i = 0; i < 3; i++) {
await runRemoteControlApp();
}
});
it('can run chrome extension APIs', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });