diff --git a/src/helpers/electron-ipc-proxy/client.ts b/src/helpers/electron-ipc-proxy/client.ts index 4939b306..9a4db93a 100644 --- a/src/helpers/electron-ipc-proxy/client.ts +++ b/src/helpers/electron-ipc-proxy/client.ts @@ -1,8 +1,8 @@ -import { Subscribable, Observer, TeardownLogic, Observable } from 'rxjs'; +import { Subscribable, Observer, TeardownLogic, Observable, isObservable } from 'rxjs'; import { IpcRenderer, ipcRenderer, Event } from 'electron'; import { v4 as uuid } from 'uuid'; import Errio from 'errio'; -import { IpcProxyError } from './utils'; +import { getSubscriptionKey, IpcProxyError } from './utils'; import { Request, RequestType, Response, ResponseType, ProxyDescriptor, ProxyPropertyType } from './common'; export type ObservableConstructor = new (subscribe: (obs: Observer) => TeardownLogic) => Subscribable; @@ -20,10 +20,23 @@ export function createProxy(descriptor: ProxyDescriptor, ObservableCtor: Obse ); } - Object.defineProperty(result, propertyKey, { - enumerable: true, - get: memoize(() => getProperty(propertyType, propertyKey, descriptor.channel, ObservableCtor, transport)), - }); + // fix https://github.com/electron/electron/issues/28176 + if (propertyType === ProxyPropertyType.Value$) { + Object.defineProperty(result, getSubscriptionKey(propertyKey), { + enumerable: true, + get: memoize(() => (next: (value?: any) => void) => { + const originalObservable = getProperty(propertyType, propertyKey, descriptor.channel, ObservableCtor, transport); + if (isObservable(originalObservable)) { + originalObservable.subscribe((value: any) => next(value)); + } + }), + }); + } else { + Object.defineProperty(result, propertyKey, { + enumerable: true, + get: memoize(() => getProperty(propertyType, propertyKey, descriptor.channel, ObservableCtor, transport)), + }); + } }); return result as T; diff --git a/src/helpers/electron-ipc-proxy/fixContextIsolation.ts b/src/helpers/electron-ipc-proxy/fixContextIsolation.ts new file mode 100644 index 00000000..ea77f92b --- /dev/null +++ b/src/helpers/electron-ipc-proxy/fixContextIsolation.ts @@ -0,0 +1,27 @@ +/** + * fix https://github.com/electron/electron/issues/28176 + * We cannot pass Observable across contextBridge, so we have to add a hidden patch to the object on preload script, and use that patch to regenerate Observable on renderer side + */ +import { Observable } from 'rxjs'; +import { ProxyDescriptor, ProxyPropertyType } from './common'; +import { getSubscriptionKey } from './utils'; + +export function ipcProxyFixContextIsolation>(service: T, descriptor: ProxyDescriptor): void { + for (const key in descriptor.properties) { + if (descriptor.properties[key] === ProxyPropertyType.Value$ && !(key in service) && getSubscriptionKey(key) in service) { + // object is not extensible as contextBridge uses https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions + // but we can still assign property to __proto__ + service.__proto__[key as keyof T] = new Observable((observer) => { + service[getSubscriptionKey(key)]((value: any) => observer.next(value)); + }) as T[keyof T]; + } + } +} + +export function fixContextIsolation(): void { + const { descriptors, ...services } = window.service; + for (const key in services) { + const serviceName = key as Exclude; + ipcProxyFixContextIsolation(services[serviceName], descriptors[serviceName]); + } +} diff --git a/src/helpers/electron-ipc-proxy/server.ts b/src/helpers/electron-ipc-proxy/server.ts index 70ee5f1a..8fb9385a 100644 --- a/src/helpers/electron-ipc-proxy/server.ts +++ b/src/helpers/electron-ipc-proxy/server.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/strict-boolean-expressions */ -import { Observable, Subscription } from 'rxjs'; +import { Observable, Subscription, isObservable } from 'rxjs'; import { ipcMain, IpcMain, WebContents, IpcMainEvent } from 'electron'; import Errio from 'errio'; -import { IpcProxyError, isFunction, isObservable } from './utils'; +import { IpcProxyError, isFunction } from './utils'; import { Request, RequestType, @@ -116,6 +116,9 @@ class ProxyServerHandler { if (!isObservable(obs)) { throw new IpcProxyError(`Remote property [${propKey}] is not an observable`); } + if (typeof subscriptionId !== 'string') { + throw new IpcProxyError(`subscriptionId [${subscriptionId}] is not a string`); + } this.doSubscribe(obs, subscriptionId, sender); } @@ -133,6 +136,9 @@ class ProxyServerHandler { if (!isObservable(obs)) { throw new IpcProxyError(`Remote function [${propKey}] did not return an observable`); } + if (typeof subscriptionId !== 'string') { + throw new IpcProxyError(`subscriptionId [${subscriptionId}] is not a string`); + } this.doSubscribe(obs, subscriptionId, sender); } @@ -148,8 +154,17 @@ class ProxyServerHandler { () => sender.send(subscriptionId, { type: ResponseType.Complete }), ); - /* If the sender does not clean up after itself then we need to do it */ - sender.once('destroyed', () => this.doUnsubscribe(subscriptionId)); + /* + * If the sender does not clean up after itself then we need to do it + * This won't be called when webContent refresh by CMD+R, so beware this kind of memory leak. + * But we will try to detect devtools-reload-page + */ + sender.once('destroyed', () => { + this.doUnsubscribe(subscriptionId); + }); + sender.once('devtools-reload-page', () => { + this.doUnsubscribe(subscriptionId); + }); } private handleUnsubscribe(request: UnsubscribeRequest): void { diff --git a/src/helpers/electron-ipc-proxy/utils.ts b/src/helpers/electron-ipc-proxy/utils.ts index 60daa8da..20d88549 100644 --- a/src/helpers/electron-ipc-proxy/utils.ts +++ b/src/helpers/electron-ipc-proxy/utils.ts @@ -1,4 +1,3 @@ -import { Observable } from 'rxjs'; import Errio from 'errio'; /* Custom Error */ @@ -15,6 +14,11 @@ export function isFunction(value: any): value is Function { return value && typeof value === 'function'; } -export function isObservable(value: any): value is Observable { - return value && typeof value.subscribe === 'function'; +/** + * Fix ContextIsolation + * @param key original key + * @returns + */ +export function getSubscriptionKey(key: string): string { + return `${key}Subscribe`; } diff --git a/src/preload/common/services.ts b/src/preload/common/services.ts index e4866ea3..844b7f64 100644 --- a/src/preload/common/services.ts +++ b/src/preload/common/services.ts @@ -60,3 +60,22 @@ export const wikiGitWorkspace = createProxy>(WindowServiceIPCDescriptor); export const workspace = createProxy>(WorkspaceServiceIPCDescriptor); export const workspaceView = createProxy>(WorkspaceViewServiceIPCDescriptor); + +export const descriptors = { + auth: AuthenticationServiceIPCDescriptor, + context: ContextServiceIPCDescriptor, + git: GitServiceIPCDescriptor, + menu: MenuServiceIPCDescriptor, + native: NativeServiceIPCDescriptor, + notification: NotificationServiceIPCDescriptor, + preference: PreferenceServiceIPCDescriptor, + systemPreference: SystemPreferenceServiceIPCDescriptor, + theme: ThemeServiceIPCDescriptor, + updater: UpdaterServiceIPCDescriptor, + view: ViewServiceIPCDescriptor, + wiki: WikiServiceIPCDescriptor, + wikiGitWorkspace: WikiGitWorkspaceServiceIPCDescriptor, + window: WindowServiceIPCDescriptor, + workspace: WorkspaceServiceIPCDescriptor, + workspaceView: WorkspaceViewServiceIPCDescriptor, +}; diff --git a/src/preload/index.ts b/src/preload/index.ts index b33d9b43..b0e410a4 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -5,7 +5,7 @@ import path from 'path'; import './common/i18n'; import './common/authing-postmessage'; import * as service from './common/services'; -import { MetaDataChannel, ViewChannel, ContextChannel, WindowChannel } from '@/constants/channels'; +import { MetaDataChannel, ViewChannel, WindowChannel } from '@/constants/channels'; import { WindowNames, WindowMeta, IPossibleWindowMeta } from '@services/windows/WindowProperties'; const extraMetaJSONString = process.argv.pop() as string; diff --git a/src/renderer.tsx b/src/renderer.tsx index 764fef61..eaae0ac1 100644 --- a/src/renderer.tsx +++ b/src/renderer.tsx @@ -15,6 +15,9 @@ import { WindowNames, WindowMeta, IPreferenceWindowMeta } from '@services/window import 'typeface-roboto/index.css'; import { initI18N } from './i18n'; +import { fixContextIsolation } from './helpers/electron-ipc-proxy/fixContextIsolation'; + +fixContextIsolation(); const Main = React.lazy(async () => await import('./pages/Main')); const AboutPage = React.lazy(async () => await import('./pages/About'));