Compare commits
No commits in common. "35ce3c39af7dd92cfa60b347851f2a8249573c41" and "bb2b8148c79e4e8d97b38329f540f9a4ce2c7607" have entirely different histories.
35ce3c39af
...
bb2b8148c7
@ -6,9 +6,32 @@ import desktopBridge, {
|
|||||||
isElectrobunDesktop
|
isElectrobunDesktop
|
||||||
} from '../../../electrobun-bridge.js'
|
} from '../../../electrobun-bridge.js'
|
||||||
|
|
||||||
|
const electron = window.require ? window.require('electron') : null
|
||||||
|
const ipcRenderer = electron ? electron.ipcRenderer : null
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export function isElectron() {
|
export function isElectron() {
|
||||||
return isElectrobunBridgeReady() || isElectrobunDesktop()
|
if (isElectrobunDesktop()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
window.process &&
|
||||||
|
window.process.type === 'renderer'
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof navigator === 'object' &&
|
||||||
|
typeof navigator.userAgent === 'string' &&
|
||||||
|
navigator.userAgent.indexOf('Electron') >= 0
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const ElectronContext = createContext()
|
const ElectronContext = createContext()
|
||||||
@ -33,6 +56,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
const [isMaximized, setIsMaximized] = useState(false)
|
const [isMaximized, setIsMaximized] = useState(false)
|
||||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||||
const [electronAvailable] = useState(isElectron())
|
const [electronAvailable] = useState(isElectron())
|
||||||
|
const useElectrobun = isElectrobunBridgeReady() || isElectrobunDesktop()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const lastNavigationAtRef = useRef(0)
|
const lastNavigationAtRef = useRef(0)
|
||||||
|
|
||||||
@ -61,34 +85,74 @@ const ElectronProvider = ({ children }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const openExternalUrl = (url) => {
|
const openExternalUrl = (url) => {
|
||||||
if (!electronAvailable) return false
|
if (useElectrobun) {
|
||||||
void desktopBridge.openExternalUrl(url).catch((error) => {
|
void desktopBridge.openExternalUrl(url).catch((error) => {
|
||||||
console.warn('[ElectronContext] Failed to open external url:', error)
|
console.warn('[ElectronContext] Failed to open external url:', error)
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
|
}
|
||||||
|
if (electronAvailable && ipcRenderer) {
|
||||||
|
ipcRenderer.invoke('open-external-url', url)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const openInternalUrl = (url) => {
|
const openInternalUrl = (url) => {
|
||||||
if (!electronAvailable) return false
|
if (useElectrobun) {
|
||||||
void desktopBridge.openInternalUrl(url).catch((error) => {
|
void desktopBridge.openInternalUrl(url).catch((error) => {
|
||||||
console.warn('[ElectronContext] Failed to open internal url:', error)
|
console.warn('[ElectronContext] Failed to open internal url:', error)
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
|
}
|
||||||
|
if (electronAvailable && ipcRenderer) {
|
||||||
|
ipcRenderer.invoke('open-internal-url', url)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!electronAvailable) return
|
if (!electronAvailable) return
|
||||||
|
|
||||||
document.body.classList.add('electron-body')
|
if (useElectrobun) {
|
||||||
return () => {
|
desktopBridge.getOsInfo().then((info) => {
|
||||||
document.body.classList.remove('electron-body')
|
if (info?.platform) {
|
||||||
|
setPlatform(info.platform)
|
||||||
|
if (info.platform === 'darwin') {
|
||||||
|
document.documentElement.classList.add('macos-vibrancy')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
desktopBridge.getWindowState().then(applyWindowState)
|
||||||
|
|
||||||
|
const unsubWindowState = desktopBridge.onMessage(
|
||||||
|
'windowState',
|
||||||
|
applyWindowState
|
||||||
|
)
|
||||||
|
const unsubNavigate = desktopBridge.onMessage('navigate', (url) => {
|
||||||
|
if (url.toLowerCase() == '/favicon.ico') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('[ElectronContext] Navigating to:', url)
|
||||||
|
navigate(url)
|
||||||
|
})
|
||||||
|
const unsubNavigationGesture = desktopBridge.onMessage(
|
||||||
|
'navigationGesture',
|
||||||
|
navigateHistory
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubWindowState()
|
||||||
|
unsubNavigate()
|
||||||
|
unsubNavigationGesture()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [electronAvailable])
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (!ipcRenderer) return
|
||||||
if (!electronAvailable) return
|
|
||||||
|
|
||||||
desktopBridge.getOsInfo().then((info) => {
|
ipcRenderer.invoke('os-info').then((info) => {
|
||||||
if (info?.platform) {
|
if (info?.platform) {
|
||||||
setPlatform(info.platform)
|
setPlatform(info.platform)
|
||||||
if (info.platform === 'darwin') {
|
if (info.platform === 'darwin') {
|
||||||
@ -97,30 +161,39 @@ const ElectronProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
desktopBridge.getWindowState().then(applyWindowState)
|
ipcRenderer.invoke('window-state').then(applyWindowState)
|
||||||
|
|
||||||
const unsubWindowState = desktopBridge.onMessage(
|
const windowStateHandler = (_event, state) => {
|
||||||
'windowState',
|
applyWindowState(state)
|
||||||
applyWindowState
|
}
|
||||||
)
|
ipcRenderer.on('window-state', windowStateHandler)
|
||||||
const unsubNavigate = desktopBridge.onMessage('navigate', (url) => {
|
|
||||||
|
const navigateHandler = (_event, url) => {
|
||||||
if (url.toLowerCase() == '/favicon.ico') {
|
if (url.toLowerCase() == '/favicon.ico') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('[ElectronContext] Navigating to:', url)
|
console.log('[ElectronContext] Navigating to:', url)
|
||||||
navigate(url)
|
navigate(url)
|
||||||
})
|
}
|
||||||
const unsubNavigationGesture = desktopBridge.onMessage(
|
ipcRenderer.on('navigate', navigateHandler)
|
||||||
'navigationGesture',
|
|
||||||
navigateHistory
|
const navigationGestureHandler = (_event, direction) => {
|
||||||
)
|
navigateHistory(direction)
|
||||||
|
}
|
||||||
|
ipcRenderer.on('navigation-gesture', navigationGestureHandler)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubWindowState()
|
ipcRenderer.removeListener('navigate', navigateHandler)
|
||||||
unsubNavigate()
|
ipcRenderer.removeListener('navigation-gesture', navigationGestureHandler)
|
||||||
unsubNavigationGesture()
|
ipcRenderer.removeListener('window-state', windowStateHandler)
|
||||||
}
|
}
|
||||||
}, [applyWindowState, electronAvailable, navigate, navigateHistory])
|
}, [
|
||||||
|
applyWindowState,
|
||||||
|
electronAvailable,
|
||||||
|
navigate,
|
||||||
|
navigateHistory,
|
||||||
|
useElectrobun
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!electronAvailable || platform !== 'darwin') return
|
if (!electronAvailable || platform !== 'darwin') return
|
||||||
@ -154,74 +227,103 @@ const ElectronProvider = ({ children }) => {
|
|||||||
}, [electronAvailable, navigateHistory, platform])
|
}, [electronAvailable, navigateHistory, platform])
|
||||||
|
|
||||||
const handleWindowControl = (action) => {
|
const handleWindowControl = (action) => {
|
||||||
if (!electronAvailable) return
|
if (useElectrobun) {
|
||||||
void desktopBridge.windowControl(action)
|
void desktopBridge.windowControl(action)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (electronAvailable && ipcRenderer) {
|
||||||
|
ipcRenderer.send('window-control', action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAuthSession = async () => {
|
const getAuthSession = async () => {
|
||||||
if (!electronAvailable) return null
|
if (!electronAvailable) return null
|
||||||
return await desktopBridge.getAuthSession()
|
if (useElectrobun) return await desktopBridge.getAuthSession()
|
||||||
|
if (!ipcRenderer) return null
|
||||||
|
return await ipcRenderer.invoke('auth-session-get')
|
||||||
}
|
}
|
||||||
|
|
||||||
const setAuthSession = async (session) => {
|
const setAuthSession = async (session) => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
const result = await desktopBridge.setAuthSession(session)
|
if (useElectrobun) {
|
||||||
return result?.ok ?? false
|
const result = await desktopBridge.setAuthSession(session)
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
if (!ipcRenderer) return false
|
||||||
|
return await ipcRenderer.invoke('auth-session-set', session)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearAuthSession = async () => {
|
const clearAuthSession = async () => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
const result = await desktopBridge.clearAuthSession()
|
if (useElectrobun) {
|
||||||
return result?.ok ?? false
|
const result = await desktopBridge.clearAuthSession()
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
if (!ipcRenderer) return false
|
||||||
|
return await ipcRenderer.invoke('auth-session-clear')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAppSettings = useCallback(async () => {
|
const getAppSettings = useCallback(async () => {
|
||||||
if (!electronAvailable) return {}
|
if (!electronAvailable) return {}
|
||||||
return await desktopBridge.getAppSettings()
|
if (useElectrobun) return await desktopBridge.getAppSettings()
|
||||||
}, [electronAvailable])
|
if (!ipcRenderer) return {}
|
||||||
|
return await ipcRenderer.invoke('app-settings-get')
|
||||||
|
}, [electronAvailable, useElectrobun])
|
||||||
|
|
||||||
const setAppSettings = useCallback(
|
const setAppSettings = useCallback(
|
||||||
async (settings) => {
|
async (settings) => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
const result = await desktopBridge.setAppSettings(settings)
|
if (useElectrobun) {
|
||||||
return result?.ok ?? false
|
const result = await desktopBridge.setAppSettings(settings)
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
if (!ipcRenderer) return false
|
||||||
|
return await ipcRenderer.invoke('app-settings-set', settings)
|
||||||
},
|
},
|
||||||
[electronAvailable]
|
[electronAvailable, useElectrobun]
|
||||||
)
|
)
|
||||||
|
|
||||||
const startAppUpdate = useCallback(
|
const startAppUpdate = useCallback(
|
||||||
async (update) => {
|
async (update) => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
const result = await desktopBridge.startAppUpdate(update)
|
if (useElectrobun) {
|
||||||
return result?.ok ?? false
|
const result = await desktopBridge.startAppUpdate(update)
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
if (!ipcRenderer) return false
|
||||||
|
return await ipcRenderer.invoke('app-update-start', update)
|
||||||
},
|
},
|
||||||
[electronAvailable]
|
[electronAvailable, useElectrobun]
|
||||||
)
|
)
|
||||||
|
|
||||||
const checkAppUpdateResult = useCallback(async () => {
|
const checkAppUpdateResult = useCallback(async () => {
|
||||||
if (!electronAvailable) return null
|
if (!electronAvailable || !useElectrobun) return null
|
||||||
return await desktopBridge.checkAppUpdateResult()
|
return await desktopBridge.checkAppUpdateResult()
|
||||||
}, [electronAvailable])
|
}, [electronAvailable, useElectrobun])
|
||||||
|
|
||||||
const checkDuplicateInstallations = useCallback(async () => {
|
const checkDuplicateInstallations = useCallback(async () => {
|
||||||
if (!electronAvailable) return null
|
if (!electronAvailable || !useElectrobun) return null
|
||||||
return await desktopBridge.checkDuplicateInstallations()
|
return await desktopBridge.checkDuplicateInstallations()
|
||||||
}, [electronAvailable])
|
}, [electronAvailable, useElectrobun])
|
||||||
|
|
||||||
const removeDuplicateInstallations = useCallback(async () => {
|
const removeDuplicateInstallations = useCallback(async () => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable || !useElectrobun) return false
|
||||||
const result = await desktopBridge.removeDuplicateInstallations()
|
const result = await desktopBridge.removeDuplicateInstallations()
|
||||||
return result?.ok ?? false
|
return result?.ok ?? false
|
||||||
}, [electronAvailable])
|
}, [electronAvailable, useElectrobun])
|
||||||
|
|
||||||
const onDuplicateInstallationsRemoved = useCallback(
|
const onDuplicateInstallationsRemoved = useCallback(
|
||||||
(handler) => {
|
(handler) => {
|
||||||
if (!electronAvailable || typeof handler !== 'function') {
|
if (
|
||||||
|
!electronAvailable ||
|
||||||
|
!useElectrobun ||
|
||||||
|
typeof handler !== 'function'
|
||||||
|
) {
|
||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
return desktopBridge.onMessage('duplicateInstallationsRemoved', handler)
|
return desktopBridge.onMessage('duplicateInstallationsRemoved', handler)
|
||||||
},
|
},
|
||||||
[electronAvailable]
|
[electronAvailable, useElectrobun]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onAppUpdateProgress = useCallback(
|
const onAppUpdateProgress = useCallback(
|
||||||
@ -229,9 +331,24 @@ const ElectronProvider = ({ children }) => {
|
|||||||
if (!electronAvailable || typeof handler !== 'function') {
|
if (!electronAvailable || typeof handler !== 'function') {
|
||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
return desktopBridge.onMessage('appUpdateProgress', handler)
|
|
||||||
|
if (useElectrobun) {
|
||||||
|
return desktopBridge.onMessage('appUpdateProgress', handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ipcRenderer) return () => {}
|
||||||
|
|
||||||
|
const progressHandler = (_event, progress) => {
|
||||||
|
handler(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.on('app-update-progress', progressHandler)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener('app-update-progress', progressHandler)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[electronAvailable]
|
[electronAvailable, useElectrobun]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onCheckForUpdatesRequest = useCallback(
|
const onCheckForUpdatesRequest = useCallback(
|
||||||
@ -239,9 +356,24 @@ const ElectronProvider = ({ children }) => {
|
|||||||
if (!electronAvailable || typeof handler !== 'function') {
|
if (!electronAvailable || typeof handler !== 'function') {
|
||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
return desktopBridge.onMessage('checkForUpdates', handler)
|
|
||||||
|
if (useElectrobun) {
|
||||||
|
return desktopBridge.onMessage('checkForUpdates', handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ipcRenderer) return () => {}
|
||||||
|
|
||||||
|
const checkHandler = () => {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.on('check-for-updates', checkHandler)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener('check-for-updates', checkHandler)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[electronAvailable]
|
[electronAvailable, useElectrobun]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getToken = async () => {
|
const getToken = async () => {
|
||||||
@ -257,8 +389,12 @@ const ElectronProvider = ({ children }) => {
|
|||||||
const resizeSpotlightWindow = async (height) => {
|
const resizeSpotlightWindow = async (height) => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
try {
|
try {
|
||||||
const result = await desktopBridge.resizeSpotlightWindow(height)
|
if (useElectrobun) {
|
||||||
return result?.ok ?? false
|
const result = await desktopBridge.resizeSpotlightWindow(height)
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
if (!ipcRenderer) return false
|
||||||
|
return await ipcRenderer.invoke('spotlight-window-resize', height)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[ElectronContext] Failed to resize spotlight window:',
|
'[ElectronContext] Failed to resize spotlight window:',
|
||||||
@ -271,22 +407,31 @@ const ElectronProvider = ({ children }) => {
|
|||||||
const setSidebarViewMenu = useCallback(
|
const setSidebarViewMenu = useCallback(
|
||||||
async (sections) => {
|
async (sections) => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
const result = await desktopBridge.setSidebarViewMenu(sections)
|
if (useElectrobun) {
|
||||||
return result?.ok ?? false
|
const result = await desktopBridge.setSidebarViewMenu(sections)
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
if (!ipcRenderer) return false
|
||||||
|
return await ipcRenderer.invoke('set-sidebar-view-menu', sections)
|
||||||
},
|
},
|
||||||
[electronAvailable]
|
[electronAvailable, useElectrobun]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getElectronVersion = useCallback(async () => {
|
const getElectronVersion = useCallback(async () => {
|
||||||
if (!electronAvailable) return null
|
if (!electronAvailable) return null
|
||||||
return await desktopBridge.getAppVersion()
|
if (useElectrobun) return await desktopBridge.getAppVersion()
|
||||||
}, [electronAvailable])
|
if (!ipcRenderer) return null
|
||||||
|
return await ipcRenderer.invoke('electron-version')
|
||||||
|
}, [electronAvailable, useElectrobun])
|
||||||
|
|
||||||
const getAppEngine = useCallback(async () => {
|
const getAppEngine = useCallback(async () => {
|
||||||
if (!electronAvailable) return 'native'
|
if (!electronAvailable) return 'native'
|
||||||
const engine = await desktopBridge.getAppEngine()
|
if (useElectrobun) {
|
||||||
return engine === 'chromium' ? 'chromium' : 'native'
|
const engine = await desktopBridge.getAppEngine()
|
||||||
}, [electronAvailable])
|
return engine === 'chromium' ? 'chromium' : 'native'
|
||||||
|
}
|
||||||
|
return 'native'
|
||||||
|
}, [electronAvailable, useElectrobun])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ElectronContext.Provider
|
<ElectronContext.Provider
|
||||||
|
|||||||
@ -126,14 +126,13 @@ export const prepareInstallerPath = async (installerPath) => {
|
|||||||
const startWindowsInstallerProgressWatch = (
|
const startWindowsInstallerProgressWatch = (
|
||||||
logPath,
|
logPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
{ onInstallSuccessful, onInstallFailed, onStalled, stallTimeoutMs }
|
onInstallSuccessful
|
||||||
) => {
|
) => {
|
||||||
let installerOutput = ''
|
let installerOutput = ''
|
||||||
let offset = 0
|
let offset = 0
|
||||||
let lastPercent = null
|
let lastPercent = null
|
||||||
let lastMessage = null
|
let lastMessage = null
|
||||||
let outcomeNotified = false
|
let installSuccessNotified = false
|
||||||
let lastActivityAt = Date.now()
|
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
try {
|
try {
|
||||||
@ -146,7 +145,6 @@ const startWindowsInstallerProgressWatch = (
|
|||||||
await handle.read(buffer, 0, buffer.length, offset)
|
await handle.read(buffer, 0, buffer.length, offset)
|
||||||
offset = stat.size
|
offset = stat.size
|
||||||
installerOutput += buffer.toString('utf8')
|
installerOutput += buffer.toString('utf8')
|
||||||
lastActivityAt = Date.now()
|
|
||||||
|
|
||||||
const { percent, message } =
|
const { percent, message } =
|
||||||
parseWindowsInstallerProgress(installerOutput)
|
parseWindowsInstallerProgress(installerOutput)
|
||||||
@ -162,14 +160,13 @@ const startWindowsInstallerProgressWatch = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!outcomeNotified) {
|
if (
|
||||||
if (isWindowsInstallFailed(installerOutput)) {
|
!installSuccessNotified &&
|
||||||
outcomeNotified = true
|
isWindowsInstallSuccessful(installerOutput) &&
|
||||||
onInstallFailed?.(installerOutput)
|
!isWindowsInstallFailed(installerOutput)
|
||||||
} else if (isWindowsInstallSuccessful(installerOutput)) {
|
) {
|
||||||
outcomeNotified = true
|
installSuccessNotified = true
|
||||||
onInstallSuccessful?.(installerOutput)
|
onInstallSuccessful?.(installerOutput)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await handle.close()
|
await handle.close()
|
||||||
@ -182,18 +179,6 @@ const startWindowsInstallerProgressWatch = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
// The installer runs detached (outside our job object), so a dead
|
|
||||||
// installer only shows up as the log going quiet before any outcome.
|
|
||||||
if (
|
|
||||||
!outcomeNotified &&
|
|
||||||
stallTimeoutMs > 0 &&
|
|
||||||
Date.now() - lastActivityAt > stallTimeoutMs
|
|
||||||
) {
|
|
||||||
outcomeNotified = true
|
|
||||||
onStalled?.(installerOutput)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
poll().catch((error) => {
|
poll().catch((error) => {
|
||||||
console.error('[app-update] installer log poll error:', error)
|
console.error('[app-update] installer log poll error:', error)
|
||||||
})
|
})
|
||||||
@ -264,22 +249,8 @@ export const launchWindowsInstaller = async (
|
|||||||
const stopProgressWatch = startWindowsInstallerProgressWatch(
|
const stopProgressWatch = startWindowsInstallerProgressWatch(
|
||||||
logPath,
|
logPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
{
|
(output) => {
|
||||||
stallTimeoutMs: 3 * 60 * 1000,
|
settleSuccess(output)
|
||||||
onInstallSuccessful: (output) => {
|
|
||||||
settleSuccess(output)
|
|
||||||
},
|
|
||||||
onInstallFailed: (output) => {
|
|
||||||
settleFailure(getInstallErrorMessage(null, output))
|
|
||||||
},
|
|
||||||
onStalled: (output) => {
|
|
||||||
settleFailure(
|
|
||||||
getInstallErrorMessage(
|
|
||||||
new Error('The update installer stopped responding.'),
|
|
||||||
output
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -287,63 +258,65 @@ export const launchWindowsInstaller = async (
|
|||||||
return await stopProgressWatch()
|
return await stopProgressWatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Silent NSIS install.
|
// Silent NSIS install in a detached child process (not a batch file).
|
||||||
// /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
|
// /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
|
||||||
// /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
|
// /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
|
||||||
// /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
|
// /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
|
||||||
// /PARENTPID = exact process to wait for (CEF may outlive the launcher)
|
// /PARENTPID = exact process to wait for (CEF may outlive the launcher)
|
||||||
// /LOG= = progress log (installer:% / PHASE / STATUS / COPY_TOTAL / COPY_FILE)
|
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log
|
||||||
const installerCommandLine = [
|
// (installer:% / PHASE / STATUS / COPY_TOTAL / COPY_FILE)
|
||||||
`"${resolvedPath}"`,
|
const installerArgs = [
|
||||||
'/S',
|
'/S',
|
||||||
'/UPDATE',
|
'/UPDATE',
|
||||||
'/RESTARTFC',
|
'/RESTARTFC',
|
||||||
`/PARENTPID=${process.pid}`,
|
`/PARENTPID=${process.pid}`,
|
||||||
`/LOG="${logPath}"`
|
`/LOG=${logPath}`
|
||||||
].join(' ')
|
]
|
||||||
|
|
||||||
// This process runs inside a Windows job object (CEF/launcher) that kills
|
const installerProcess = spawn(resolvedPath, installerArgs, {
|
||||||
// every child process when the app exits — a directly spawned installer
|
detached: true,
|
||||||
// (even with detached: true) inherits the job and dies mid folder-swap the
|
env: {
|
||||||
// moment the app quits. Creating the process via WMI (Win32_Process.Create)
|
...process.env,
|
||||||
// parents it to the WMI provider host, outside our job, so it survives.
|
FARMCONTROL_INSTALL_LOG: logPath
|
||||||
const quoteForPowershell = (value) => `'${value.replace(/'/g, "''")}'`
|
},
|
||||||
|
stdio: 'ignore',
|
||||||
|
windowsHide: true
|
||||||
|
})
|
||||||
|
|
||||||
const spawnerProcess = spawn(
|
installerProcess.unref()
|
||||||
'powershell.exe',
|
|
||||||
[
|
|
||||||
'-NoProfile',
|
|
||||||
'-NonInteractive',
|
|
||||||
'-WindowStyle',
|
|
||||||
'Hidden',
|
|
||||||
'-Command',
|
|
||||||
`$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = ${quoteForPowershell(installerCommandLine)} }; exit $result.ReturnValue`
|
|
||||||
],
|
|
||||||
{
|
|
||||||
stdio: 'ignore',
|
|
||||||
windowsHide: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
spawnerProcess.on('error', async (error) => {
|
installerProcess.on('error', async (error) => {
|
||||||
const output = await refreshInstallerOutput()
|
const output = await refreshInstallerOutput()
|
||||||
console.error('[app-update] installer launch error:', error)
|
console.error('[app-update] installer error:', error)
|
||||||
settleFailure(getInstallErrorMessage(error, output))
|
settleFailure(getInstallErrorMessage(error, output))
|
||||||
})
|
})
|
||||||
|
|
||||||
spawnerProcess.on('exit', async (code) => {
|
installerProcess.on('exit', async (code) => {
|
||||||
// PowerShell exits as soon as the installer is created; from here the
|
|
||||||
// log watcher owns success/failure. Only a non-zero code (WMI create
|
|
||||||
// failed) means the installer never started.
|
|
||||||
if (code === 0 || settled) return
|
|
||||||
|
|
||||||
const output = await refreshInstallerOutput()
|
const output = await refreshInstallerOutput()
|
||||||
settleFailure(
|
|
||||||
getInstallErrorMessage(
|
await fs.unlink(logPath).catch(() => {})
|
||||||
new Error(`Failed to launch the update installer (code ${code}).`),
|
|
||||||
output
|
if (settled) return
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
settleFailure(
|
||||||
|
getInstallErrorMessage(
|
||||||
|
new Error(`Installer exited with code ${code}.`),
|
||||||
|
output
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isWindowsInstallFailed(output) ||
|
||||||
|
!isWindowsInstallSuccessful(output)
|
||||||
|
) {
|
||||||
|
settleFailure(getInstallErrorMessage(null, output))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settleSuccess(output)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user