Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions shared/constants/remote-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const pinentryOnCancel = 'remote:pinentryOnCancel'
export const pinentryOnSubmit = 'remote:pinentryOnSubmit'
export const powerMonitorEvent = 'remote:powerMonitorEvent'
export const previewConversation = 'remote:previewConversation'
export const remoteWindowWantsProps = 'remote:remoteWindowWantsProps'
export const saltpackFileOpen = 'remote:saltpackFileOpen'
export const setCriticalUpdate = 'remote:setCriticalUpdate'
export const showMain = 'remote:showMain'
Expand Down Expand Up @@ -53,11 +52,6 @@ export const createUpdateWindowState = (payload: {
y: number
}
}) => ({payload, type: updateWindowState}) as const
/**
* remote electron window wants props sent
*/
export const createRemoteWindowWantsProps = (payload: {readonly component: string; readonly param: string}) =>
({payload, type: remoteWindowWantsProps}) as const
export const createCloseUnlockFolders = (payload?: undefined) =>
({payload, type: closeUnlockFolders}) as const
export const createDumpLogs = (payload: {readonly reason: 'quitting through menu'}) =>
Expand Down Expand Up @@ -124,7 +118,6 @@ export type PinentryOnCancelPayload = ReturnType<typeof createPinentryOnCancel>
export type PinentryOnSubmitPayload = ReturnType<typeof createPinentryOnSubmit>
export type PowerMonitorEventPayload = ReturnType<typeof createPowerMonitorEvent>
export type PreviewConversationPayload = ReturnType<typeof createPreviewConversation>
export type RemoteWindowWantsPropsPayload = ReturnType<typeof createRemoteWindowWantsProps>
export type SaltpackFileOpenPayload = ReturnType<typeof createSaltpackFileOpen>
export type SetCriticalUpdatePayload = ReturnType<typeof createSetCriticalUpdate>
export type ShowMainPayload = ReturnType<typeof createShowMain>
Expand Down Expand Up @@ -156,7 +149,6 @@ export type Actions =
| PinentryOnSubmitPayload
| PowerMonitorEventPayload
| PreviewConversationPayload
| RemoteWindowWantsPropsPayload
| SaltpackFileOpenPayload
| SetCriticalUpdatePayload
| ShowMainPayload
Expand Down
44 changes: 17 additions & 27 deletions shared/desktop/app/ipc-handlers.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,20 @@ import {isDarwin, isLinux, isWindows, socketPath, fileUIName, dokanPath, windows
import {ctlQuit} from './ctl.desktop'
import logger from '@/logger'
import {htmlURL, preloadPath} from './html-root.desktop'
import {
cacheRemoteProps,
getCachedRemoteProps,
getRemoteWindow,
registerRemoteWindow,
} from './remote-windows.desktop'
import * as RPCTypes from '@/constants/rpc/rpc-gen'
import {ensureError} from '@/util/errors'
import type {Action} from '../app/ipctypes'
import type {Engine} from '@/engine'
import {showDevTools, skipSecondaryDevtools, allowMultipleInstances} from '@/local-debug'

const remoteURL = (windowComponent: string, windowParam: string) =>
htmlURL(windowComponent, `param=${windowParam}`)

const findRemoteComponent = (windowComponent: string, windowParam: string) => {
const url = remoteURL(windowComponent, windowParam)
return Electron.BrowserWindow.getAllWindows().find(w => {
const wc = w.webContents
return wc.getURL() === url
})
}
htmlURL('remote', `component=${windowComponent}&param=${windowParam}`)

const winCheckRPCOwnership = async () => {
const {env} = KB2.constants
Expand Down Expand Up @@ -172,11 +170,6 @@ const openInDefaultDirectory = async (openPath: string) => {
}
}

const timeoutPromise = async (timeMs: number) =>
new Promise<void>(resolve => {
setTimeout(() => resolve(), timeMs)
})

export const setupIPCHandlers = (deps: {
getMainWindow: () => Electron.BrowserWindow | null
markAppStartedUp: () => void
Expand Down Expand Up @@ -523,22 +516,18 @@ export const setupIPCHandlers = (deps: {
break
}
case 'rendererNewProps': {
// this can be racy so we try a few
let count = 0
while (count < 5) {
const w = findRemoteComponent(action.payload.windowComponent, action.payload.windowParam)
if (w) {
w.webContents.send('KBprops', action.payload.propsStr)
break
} else {
await timeoutPromise(500)
}
++count
}
const {windowComponent, windowParam, propsStr} = action.payload
// cache so a window that hasn't finished loading can pull these when it's ready
cacheRemoteProps(windowComponent, windowParam, propsStr)
getRemoteWindow(windowComponent, windowParam)?.webContents.send('KBprops', propsStr)
break
}
case 'getRemoteProps': {
const {windowComponent, windowParam} = action.payload
return getCachedRemoteProps(windowComponent, windowParam) ?? ''
}
case 'closeRenderer': {
const w = findRemoteComponent(action.payload.windowComponent ?? '', action.payload.windowParam ?? '')
const w = getRemoteWindow(action.payload.windowComponent ?? '', action.payload.windowParam ?? '')
w?.close()
break
}
Expand All @@ -559,6 +548,7 @@ export const setupIPCHandlers = (deps: {
}

const remoteWindow = new Electron.BrowserWindow(opts)
registerRemoteWindow(action.payload.windowComponent, action.payload.windowParam ?? '', remoteWindow)

remoteWindow.on('show', () => {
R.remoteDispatch(RemoteGen.createUpdateWindowShown({component: action.payload.windowComponent}))
Expand Down
7 changes: 7 additions & 0 deletions shared/desktop/app/ipctypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export type Action =
windowParam: string
}
}
| {
type: 'getRemoteProps'
payload: {
windowComponent: string
windowParam: string
}
}
| {type: 'showMainWindow'}
| {type: 'showContextMenu'; payload: {url: string}}
| {type: 'setupPreloadKB2'}
Expand Down
14 changes: 5 additions & 9 deletions shared/desktop/app/menu-bar.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {menubar} from 'menubar'
import {showDevTools, skipSecondaryDevtools} from '@/local-debug'
import {getMainWindow} from './main-window.desktop'
import {htmlURL, preloadPath} from './html-root.desktop'
import {registerRemoteWindow} from './remote-windows.desktop'
import path from 'path'
import type {BadgeType} from '@/stores/notifications'

Expand Down Expand Up @@ -40,7 +41,7 @@ const getIcons = (iconType: BadgeType, badges: number) => {
}
}

const htmlFile = htmlURL('menubar', 'param=menubar')
const htmlFile = htmlURL('remote', 'component=menubar&param=menubar')

let badgeType: BadgeType = 'regular'
let badges = 0
Expand Down Expand Up @@ -130,20 +131,15 @@ const MenuBar = () => {
})

mb.on('ready', () => {
if (mb.window) {
registerRemoteWindow('menubar', 'menubar', mb.window)
}
mb.window?.setVisibleOnAllWorkspaces(true, {visibleOnFullScreen: true, skipTransformProcessType: true})
mb.window
?.loadURL(htmlFile)
.then(() => {})
.catch(() => {})

// ask for an update in case we missed one
R.remoteDispatch(
RemoteGen.createRemoteWindowWantsProps({
component: 'menubar',
param: '',
})
)

mb.tray.setIgnoreDoubleClickEvents(true)

if (showDevTools && !skipSecondaryDevtools) {
Expand Down
35 changes: 35 additions & 0 deletions shared/desktop/app/remote-windows.desktop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Registry of remote BrowserWindows and the last props sent to each.
// The main process is the source of truth: the main window pushes props here,
// and remote windows pull the cached value when they load (or reload), so no
// handshake back to the main window is needed.
import type * as Electron from 'electron'

const windows = new Map<string, Electron.BrowserWindow>()
const propsCache = new Map<string, string>()

const windowKey = (windowComponent: string, windowParam: string) => `${windowComponent}:${windowParam}`

export const registerRemoteWindow = (
windowComponent: string,
windowParam: string,
win: Electron.BrowserWindow
) => {
const key = windowKey(windowComponent, windowParam)
windows.set(key, win)
win.on('closed', () => {
if (windows.get(key) === win) {
windows.delete(key)
propsCache.delete(key)
}
})
}

export const getRemoteWindow = (windowComponent: string, windowParam: string) =>
windows.get(windowKey(windowComponent, windowParam))

export const cacheRemoteProps = (windowComponent: string, windowParam: string, propsStr: string) => {
propsCache.set(windowKey(windowComponent, windowParam), propsStr)
}

export const getCachedRemoteProps = (windowComponent: string, windowParam: string) =>
propsCache.get(windowKey(windowComponent, windowParam))
2 changes: 1 addition & 1 deletion shared/desktop/package.desktop.mts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ function postPack(appPaths: Array<string>, plat: string, arch: string) {
}
const subdir = plat === 'darwin' ? 'Keybase.app/Contents/Resources' : 'resources'
const dir = path.join(appPaths[0]!, subdir, 'app/desktop/dist')
const modules = ['node', 'main', 'tracker', 'menubar', 'unlock-folders', 'pinentry']
const modules = ['node', 'main', 'remote']
const files = [
...modules.map(p => p + '.bundle.js'),
...modules.filter(p => p !== 'node').map(p => p + '.html'),
Expand Down
11 changes: 8 additions & 3 deletions shared/desktop/remote/component-loader.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import ErrorBoundary from '@/common-adapters/error-boundary'
import KB2 from '@/util/electron'
import {setServiceDecoration} from '@/common-adapters/markdown/react'
import ServiceDecoration from '@/common-adapters/markdown/service-decoration'
import {type RemoteComponentName, useRemotePropsReceiver} from './remote-component.desktop'
import {
getRemoteComponentParam,
type RemoteComponentName,
useRemoteDarkModeSync,
useRemotePropsReceiver,
} from './remote-component.desktop'

setServiceDecoration(ServiceDecoration)

Expand All @@ -29,6 +34,7 @@ type Props<P> = {
function RemoteComponentLoader<P>(p: Props<P>) {
const {Component, component, param, showOnProps} = p
const value = useRemotePropsReceiver<P>({component, param, showOnProps})
useRemoteDarkModeSync(value?.darkMode)

if (!value) return null

Expand Down Expand Up @@ -66,7 +72,6 @@ const styles = Kb.Styles.styleSheetCreate(
export default function loadRemoteComponent<P>(options: {
Component: React.ComponentType<P>
component: RemoteComponentName
param?: string
style?: Kb.Styles.StylesCrossPlatform
showOnProps?: boolean
}) {
Expand All @@ -77,7 +82,7 @@ export default function loadRemoteComponent<P>(options: {
<RemoteComponentLoader<P>
Component={options.Component}
component={options.component}
param={options.param ?? ''}
param={getRemoteComponentParam()}
style={options.style}
showOnProps={options.showOnProps ?? true}
/>
Expand Down
32 changes: 32 additions & 0 deletions shared/desktop/remote/main.desktop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Entry point for all remote render windows. The window's URL says which
// component to load: remote.html?component=<name>&param=<param>
import '../renderer/globals.desktop'
import {waitOnKB2Loaded} from '@/util/electron'

waitOnKB2Loaded(() => {
const component = new URLSearchParams(window.location.search).get('component')
switch (component) {
case 'menubar':
import('@/menubar/main2.desktop')
.then(() => {})
.catch(() => {})
break
case 'pinentry':
import('@/pinentry/main2.desktop')
.then(() => {})
.catch(() => {})
break
case 'tracker':
import('@/tracker/main2.desktop')
.then(() => {})
.catch(() => {})
break
case 'unlock-folders':
import('@/unlock-folders/main2.desktop')
.then(() => {})
.catch(() => {})
break
default:
console.error('remote window loaded with unknown component', component)
}
})
6 changes: 2 additions & 4 deletions shared/desktop/remote/proxies.desktop.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import * as Kb from '@/common-adapters'
import RemoteMenubar from '@/menubar/remote-proxy.desktop'
import RemoteProfile from '@/tracker/remote-proxy.desktop'
import RemotePinentry from '@/pinentry/remote-proxy.desktop'
import RemoteUnlockFolders from '@/unlock-folders/remote-proxy.desktop'

const RemoteProxies = () => (
<Kb.Box2 direction="vertical" style={style}>
<>
<RemoteMenubar />
<RemoteProfile />
<RemotePinentry />
<RemoteUnlockFolders />
</Kb.Box2>
</>
)

const style = {display: 'none' as const}
export default RemoteProxies
53 changes: 18 additions & 35 deletions shared/desktop/remote/remote-component.desktop.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import * as React from 'react'
import logger from '@/logger'
import * as R from '@/constants/remote'
import * as RemoteGen from '@/constants/remote-actions'
import {useDarkModeState} from '@/stores/darkmode'
import KB2 from '@/util/electron'

const {ipcRendererOn, showInactive} = KB2.functions
const {getRemoteProps, ipcRendererOn, showInactive} = KB2.functions

export const remoteComponentNames = ['unlock-folders', 'menubar', 'pinentry', 'tracker'] as const
export type RemoteComponentName = (typeof remoteComponentNames)[number]
Expand All @@ -16,56 +14,41 @@ type UseRemotePropsReceiverOptions = {
showOnProps?: boolean
}

type RemotePropsReceiverState<P> = {
component: RemoteComponentName
param: string
value: P | null
}

export const getRemoteComponentParam = () => new URLSearchParams(window!.location.search).get('param') ?? ''

export const useRemoteDarkModeSync = (darkMode: boolean) => {
// darkMode rides along in the serialized props envelope; undefined until props arrive
export const useRemoteDarkModeSync = (darkMode?: boolean) => {
const setSystemDarkMode = useDarkModeState(s => s.dispatch.setSystemDarkMode)

React.useEffect(() => {
setSystemDarkMode(darkMode)
if (darkMode !== undefined) {
setSystemDarkMode(darkMode)
}
}, [darkMode, setSystemDarkMode])
}

export const RemoteDarkModeSync = (p: {children: React.ReactNode; darkMode: boolean}) => {
useRemoteDarkModeSync(p.darkMode)
return <>{p.children}</>
}

export const useRemotePropsReceiver = <P,>(options: UseRemotePropsReceiverOptions) => {
const {component, param, showOnProps = true} = options
const [propsState, setPropsState] = React.useState<RemotePropsReceiverState<P>>(() => ({
component,
param,
value: null,
}))
const currentPropsState =
propsState.component === component && propsState.param === param
? propsState
: {component, param, value: null}
if (currentPropsState !== propsState) {
setPropsState(currentPropsState)
}
const value = currentPropsState.value
const [value, setValue] = React.useState<(P & {darkMode: boolean}) | null>(null)
const hasShownWindow = React.useRef(false)

React.useEffect(() => {
hasShownWindow.current = false

const unsubscribe = ipcRendererOn?.('KBprops', (_event: unknown, raw: unknown) => {
const onProps = (raw: string) => {
if (!raw) return
try {
setPropsState({component, param, value: JSON.parse(raw as string) as P})
setValue(JSON.parse(raw) as P & {darkMode: boolean})
} catch (error) {
logger.error('remote props parse failed', component, param, error)
}
})
}

R.remoteDispatch(RemoteGen.createRemoteWindowWantsProps({component, param}))
// subscribe before pulling so an update can't slip between the two
const unsubscribe = ipcRendererOn?.('KBprops', (_event: unknown, raw: unknown) => {
onProps(raw as string)
})
getRemoteProps?.(component, param)
.then(onProps)
.catch(() => {})

return () => unsubscribe?.()
}, [component, param])
Expand Down
Loading