diff --git a/src/apps/main/bootstrap/bootstrap-runtime-state.ts b/src/apps/main/bootstrap/bootstrap-runtime-state.ts new file mode 100644 index 000000000..80c0d4d64 --- /dev/null +++ b/src/apps/main/bootstrap/bootstrap-runtime-state.ts @@ -0,0 +1,11 @@ +export type PendingUpdateInfo = { version: string } | null; + +let pendingUpdateInfo: PendingUpdateInfo = null; + +export function getPendingUpdateInfo() { + return pendingUpdateInfo; +} + +export function setPendingUpdateInfo(updateInfo: Exclude) { + pendingUpdateInfo = updateInfo; +} diff --git a/src/apps/main/bootstrap/main-process-bootstrap.test.ts b/src/apps/main/bootstrap/main-process-bootstrap.test.ts new file mode 100644 index 000000000..e3f69909f --- /dev/null +++ b/src/apps/main/bootstrap/main-process-bootstrap.test.ts @@ -0,0 +1,40 @@ +import { bootstrapMainProcess } from './main-process-bootstrap'; +import * as registerAppReadyFlowModule from './register-app-ready-flow'; +import * as registerMainIpcHandlersModule from './register-main-ipc-handlers'; +import * as registerProcessHandlersModule from './register-process-handlers'; +import * as registerSecondInstanceFlowModule from './register-second-instance-flow'; +import * as registerSessionEventHandlersModule from './register-session-event-handlers'; +import * as setupEnvironmentDebugToolsModule from './setup-environment-debug-tools'; +import { partialSpyOn } from 'tests/vitest/utils.helper'; + +describe('main-process-bootstrap', () => { + const setupEnvironmentDebugToolsMock = partialSpyOn(setupEnvironmentDebugToolsModule, 'setupEnvironmentDebugTools'); + const registerMainIpcHandlersMock = partialSpyOn(registerMainIpcHandlersModule, 'registerMainIpcHandlers'); + const registerAppReadyFlowMock = partialSpyOn(registerAppReadyFlowModule, 'registerAppReadyFlow'); + const registerSecondInstanceFlowMock = partialSpyOn(registerSecondInstanceFlowModule, 'registerSecondInstanceFlow'); + const registerSessionEventHandlersMock = partialSpyOn( + registerSessionEventHandlersModule, + 'registerSessionEventHandlers', + ); + const registerProcessHandlersMock = partialSpyOn(registerProcessHandlersModule, 'registerProcessHandlers'); + + beforeEach(() => { + setupEnvironmentDebugToolsMock.mockImplementation(() => undefined); + registerMainIpcHandlersMock.mockImplementation(() => undefined); + registerAppReadyFlowMock.mockImplementation(() => undefined); + registerSecondInstanceFlowMock.mockImplementation(() => undefined); + registerSessionEventHandlersMock.mockImplementation(() => undefined); + registerProcessHandlersMock.mockImplementation(() => undefined); + }); + + it('should register all main process bootstrap flows', () => { + bootstrapMainProcess(); + + expect(setupEnvironmentDebugToolsMock).toBeCalled(); + expect(registerMainIpcHandlersMock).toBeCalled(); + expect(registerAppReadyFlowMock).toBeCalled(); + expect(registerSecondInstanceFlowMock).toBeCalled(); + expect(registerSessionEventHandlersMock).toBeCalled(); + expect(registerProcessHandlersMock).toBeCalled(); + }); +}); diff --git a/src/apps/main/bootstrap/main-process-bootstrap.ts b/src/apps/main/bootstrap/main-process-bootstrap.ts new file mode 100644 index 000000000..edcd5e911 --- /dev/null +++ b/src/apps/main/bootstrap/main-process-bootstrap.ts @@ -0,0 +1,15 @@ +import { registerAppReadyFlow } from './register-app-ready-flow'; +import { setupEnvironmentDebugTools } from './setup-environment-debug-tools'; +import { registerMainIpcHandlers } from './register-main-ipc-handlers'; +import { registerProcessHandlers } from './register-process-handlers'; +import { registerSecondInstanceFlow } from './register-second-instance-flow'; +import { registerSessionEventHandlers } from './register-session-event-handlers'; + +export function bootstrapMainProcess() { + setupEnvironmentDebugTools(); + registerMainIpcHandlers(); + registerAppReadyFlow(); + registerSecondInstanceFlow(); + registerSessionEventHandlers(); + registerProcessHandlers(); +} diff --git a/src/apps/main/bootstrap/register-app-ready-flow.ts b/src/apps/main/bootstrap/register-app-ready-flow.ts new file mode 100644 index 000000000..f85b8d381 --- /dev/null +++ b/src/apps/main/bootstrap/register-app-ready-flow.ts @@ -0,0 +1,55 @@ +import { app } from 'electron'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import eventBus from '../event-bus'; +import { getIsLoggedIn } from '../auth/handlers'; +import { createAuthWindow } from '../windows/auth'; +import { setTrayStatus } from '../tray/tray'; +import { broadcastToWindows } from '../windows'; +import { setupThemeListener } from '../../../core/theme'; +import { registerAvailableUserProductsHandlers } from '../../../backend/features/payments/ipc/register-available-user-products-handlers'; +import { setupAppImageDeeplink } from '../auth/deeplink/setup-appimage-deeplink'; +import { INTERNXT_VERSION } from '../../../core/utils/utils'; +import { checkForUpdates } from '../auto-update/check-for-updates'; +import { setPendingUpdateInfo } from './bootstrap-runtime-state'; + +export function registerAppReadyFlow() { + app + .whenReady() + .then(async () => { + /** + * v.2.5.1 + * Esteban Galvis Triana + * .AppImage users may experience login issues because the deeplink protocol + * is not registered automatically, unlike with .deb packages. + * This function manually registers the protocol handler for .AppImage installations. + */ + await setupAppImageDeeplink(); + /** + * TODO: Nautilus extension disabled temporarily + * v.2.5.4 + * Esteban Galvis Triana + * The Nautilus extension will be temporarily disabled + * while the exact behavior of the context menu options is being determined. + */ + // await installNautilusExtension(); + setupThemeListener(); + + eventBus.emit('APP_IS_READY'); + const isLoggedIn = getIsLoggedIn(); + + if (!isLoggedIn) { + await createAuthWindow(); + setTrayStatus('IDLE'); + } + + await checkForUpdates({ + currentVersion: INTERNXT_VERSION, + onUpdateAvailable: (updateInfo) => { + setPendingUpdateInfo(updateInfo); + broadcastToWindows('update-available', updateInfo); + }, + }); + registerAvailableUserProductsHandlers(); + }) + .catch((exc) => logger.error({ msg: 'Error starting app', exc })); +} diff --git a/src/apps/main/bootstrap/register-main-ipc-handlers.ts b/src/apps/main/bootstrap/register-main-ipc-handlers.ts new file mode 100644 index 000000000..ae8c8b06d --- /dev/null +++ b/src/apps/main/bootstrap/register-main-ipc-handlers.ts @@ -0,0 +1,17 @@ +import dns from 'node:dns'; +import { ipcMain } from 'electron'; +import { getPendingUpdateInfo } from './bootstrap-runtime-state'; + +export function registerMainIpcHandlers() { + ipcMain.handle('get-update-status', () => getPendingUpdateInfo()); + + ipcMain.handle('check-internet-connection', async () => { + return new Promise((resolve) => { + dns.lookup('google.com', (err) => { + resolve(!err); + }); + + setTimeout(() => resolve(false), 3000); + }); + }); +} diff --git a/src/apps/main/bootstrap/register-process-handlers.ts b/src/apps/main/bootstrap/register-process-handlers.ts new file mode 100644 index 000000000..be2c1c73c --- /dev/null +++ b/src/apps/main/bootstrap/register-process-handlers.ts @@ -0,0 +1,25 @@ +import { logger } from '@internxt/drive-desktop-core/build/backend'; + +export function registerProcessHandlers() { + process.on('uncaughtException', (error) => { + /** + * v.2.5.1 + * Esteban Galvis Triana + * EPIPE errors close stdout, so they must be handled specially to avoid infinite logging loops. + */ + if ('code' in error && error.code === 'EPIPE') { + return; + } + + if (error.name === 'AbortError') { + logger.debug({ msg: 'Fetch request was aborted' }); + return; + } + + try { + logger.error({ msg: 'Uncaught exception in main process: ', error }); + } catch { + return; + } + }); +} diff --git a/src/apps/main/bootstrap/register-second-instance-flow.ts b/src/apps/main/bootstrap/register-second-instance-flow.ts new file mode 100644 index 000000000..6ca97a37f --- /dev/null +++ b/src/apps/main/bootstrap/register-second-instance-flow.ts @@ -0,0 +1,19 @@ +import { app } from 'electron'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import { handleDeeplink } from '../auth/deeplink/handle-deeplink'; + +export function registerSecondInstanceFlow() { + app.on('second-instance', async (_, argv) => { + logger.debug({ tag: 'AUTH', msg: 'Deeplink received on second instance, processing...' }); + const deeplinkArg = argv.find((arg) => arg.startsWith('internxt://')); + if (!deeplinkArg) { + return; + } + + try { + await handleDeeplink({ url: deeplinkArg }); + } catch (error) { + logger.error({ tag: 'AUTH', msg: 'Error handling deeplink', error }); + } + }); +} diff --git a/src/apps/main/bootstrap/register-session-event-handlers.ts b/src/apps/main/bootstrap/register-session-event-handlers.ts new file mode 100644 index 000000000..90ab78cc7 --- /dev/null +++ b/src/apps/main/bootstrap/register-session-event-handlers.ts @@ -0,0 +1,87 @@ +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import eventBus from '../event-bus'; +import { AppDataSource, resetAppDataSourceOnLogout } from '../database/data-source'; +import { getOrCreateWidged, getWidget, setBoundsOfWidgetByPath } from '../windows/widget'; +import { createAuthWindow, getAuthWindow } from '../windows/auth'; +import configStore from '../config'; +import { getTray, setTrayStatus } from '../tray/tray'; +import { openOnboardingWindow } from '../windows/onboarding'; +import { getTheme } from '../../../core/theme'; +import { getAntivirusManager } from '../antivirus/antivirusManager'; +import { trySetupAntivirusIpcAndInitialize } from '../background-processes/antivirus/try-setup-antivirus-ipc-and-initialize'; +import { getUserAvailableProductsAndStore } from '../../../backend/features/payments/services/get-user-available-products-and-store'; +import { registerBackupHandlers } from '../../../backend/features/backup/register-backup-handlers'; +import { startBackupsIfAvailable } from '../../../backend/features/backup/start-backups-if-available'; + +function onWidgetIsReady() { + registerBackupHandlers(); + startBackupsIfAvailable(); +} + +async function onUserLoggedIn() { + try { + if (!AppDataSource.isInitialized) { + await AppDataSource.initialize(); + eventBus.emit('APP_DATA_SOURCE_INITIALIZED'); + } + + getAuthWindow()?.hide(); + + getTheme(); + + setTrayStatus('IDLE'); + const widget = await getOrCreateWidged(); + const tray = getTray(); + if (widget && tray) { + setBoundsOfWidgetByPath(widget, tray); + } + + setTimeout(() => { + const authWin = getAuthWindow(); + if (authWin && !authWin.isDestroyed()) { + authWin.destroy(); + } + }, 300); + + const lastOnboardingShown = configStore.get('lastOnboardingShown'); + + if (!lastOnboardingShown) { + openOnboardingWindow(); + } else if (widget) { + widget.show(); + } + await getUserAvailableProductsAndStore(); + await trySetupAntivirusIpcAndInitialize(); + } catch (error) { + logger.error({ + msg: 'Error on main process while handling USER_LOGGED_IN event:', + error, + }); + } +} + +async function onUserLoggedOut() { + setTrayStatus('IDLE'); + const widget = getWidget(); + + if (widget) { + widget.hide(); + + void getAntivirusManager().shutdown(); + } + + await createAuthWindow(); + + if (widget) { + widget.destroy(); + } + await resetAppDataSourceOnLogout(); + + // await uninstallNautilusExtension(); +} + +export function registerSessionEventHandlers() { + eventBus.on('WIDGET_IS_READY', onWidgetIsReady); + eventBus.on('USER_LOGGED_IN', onUserLoggedIn); + eventBus.on('USER_LOGGED_OUT', onUserLoggedOut); +} diff --git a/src/apps/main/bootstrap/setup-environment-debug-tools.ts b/src/apps/main/bootstrap/setup-environment-debug-tools.ts new file mode 100644 index 000000000..03f53b8aa --- /dev/null +++ b/src/apps/main/bootstrap/setup-environment-debug-tools.ts @@ -0,0 +1,12 @@ +export function setupEnvironmentDebugTools() { + if (process.env.NODE_ENV === 'production') { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const sourceMapSupport = require('source-map-support'); + sourceMapSupport.install(); + } + + if (process.env.NODE_ENV === 'development') { + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('electron-debug')({ showDevTools: false }); + } +} diff --git a/src/apps/main/main.ts b/src/apps/main/main.ts index 1bb88a869..d98c80ce8 100644 --- a/src/apps/main/main.ts +++ b/src/apps/main/main.ts @@ -2,12 +2,8 @@ import 'reflect-metadata'; import 'core-js/stable'; import 'regenerator-runtime/runtime'; -// Only effective during development -// the variables are injectedif (process.env.NODE_ENV === 'production') { - -// via webpack in prod import 'dotenv/config'; -// ***** APP BOOTSTRAPPING ****************************************************** // + import { PATHS } from '../../core/electron/paths'; import { setupElectronLog } from '@internxt/drive-desktop-core/build/backend'; @@ -15,6 +11,7 @@ setupElectronLog({ logsPath: PATHS.LOGS, }); +// Side-effect handlers registration. import './virtual-root-folder/handlers'; import './auto-launch/handlers'; import './auth/handlers'; @@ -35,34 +32,13 @@ import './remote-sync/handlers'; import './../../backend/features/cleaner/ipc/handlers'; import './virtual-drive'; -import { app, ipcMain } from 'electron'; -import eventBus from './event-bus'; -import { AppDataSource, resetAppDataSourceOnLogout } from './database/data-source'; -import { getIsLoggedIn } from './auth/handlers'; -import { getOrCreateWidged, getWidget, setBoundsOfWidgetByPath } from './windows/widget'; -import { createAuthWindow, getAuthWindow } from './windows/auth'; -import configStore from './config'; -import { getTray, setTrayStatus } from './tray/tray'; -import { broadcastToWindows } from './windows'; -import { openOnboardingWindow } from './windows/onboarding'; -import { setupThemeListener, getTheme } from '../../core/theme'; -// import { installNautilusExtension } from './nautilus-extension/install'; -// import { uninstallNautilusExtension } from './nautilus-extension/uninstall'; -import dns from 'node:dns'; -import { registerAvailableUserProductsHandlers } from '../../backend/features/payments/ipc/register-available-user-products-handlers'; -import { getAntivirusManager } from './antivirus/antivirusManager'; +import { app } from 'electron'; import { registerAuthIPCHandlers } from '../../infra/ipc/auth-ipc-handlers'; import { registerQuitHandler } from '../../core/quit/quit.handler'; import { logger } from '@internxt/drive-desktop-core/build/backend'; -import { trySetupAntivirusIpcAndInitialize } from './background-processes/antivirus/try-setup-antivirus-ipc-and-initialize'; -import { getUserAvailableProductsAndStore } from '../../backend/features/payments/services/get-user-available-products-and-store'; -import { handleDeeplink } from './auth/deeplink/handle-deeplink'; -import { setupAppImageDeeplink } from './auth/deeplink/setup-appimage-deeplink'; import { version, release } from 'node:os'; import { INTERNXT_VERSION } from '../../core/utils/utils'; -import { registerBackupHandlers } from '../../backend/features/backup/register-backup-handlers'; -import { startBackupsIfAvailable } from '../../backend/features/backup/start-backups-if-available'; -import { checkForUpdates } from './auto-update/check-for-updates'; +import { bootstrapMainProcess } from './bootstrap/main-process-bootstrap'; const gotTheLock = app.requestSingleInstanceLock(); app.setAsDefaultProtocolClient('internxt'); @@ -82,165 +58,4 @@ logger.debug({ osRelease: release(), }); -let pendingUpdateInfo: { version: string } | null = null; - -ipcMain.handle('get-update-status', () => pendingUpdateInfo); - -if (process.env.NODE_ENV === 'production') { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const sourceMapSupport = require('source-map-support'); - sourceMapSupport.install(); -} - -if (process.env.NODE_ENV === 'development') { - // eslint-disable-next-line @typescript-eslint/no-var-requires - require('electron-debug')({ showDevTools: false }); -} - -app - .whenReady() - .then(async () => { - /** - * v.2.5.1 - * Esteban Galvis Triana - * .AppImage users may experience login issues because the deeplink protocol - * is not registered automatically, unlike with .deb packages. - * This function manually registers the protocol handler for .AppImage installations. - */ - await setupAppImageDeeplink(); - /** - * TODO: Nautilus extension disabled temporarily - * v.2.5.4 - * Esteban Galvis Triana - * The Nautilus extension will be temporarily disabled - * while the exact behavior of the context menu options is being determined. - */ - // await installNautilusExtension(); - setupThemeListener(); - - eventBus.emit('APP_IS_READY'); - const isLoggedIn = getIsLoggedIn(); - - if (!isLoggedIn) { - await createAuthWindow(); - setTrayStatus('IDLE'); - } - - await checkForUpdates({ - currentVersion: INTERNXT_VERSION, - onUpdateAvailable: (updateInfo) => { - pendingUpdateInfo = updateInfo; - broadcastToWindows('update-available', updateInfo); - }, - }); - registerAvailableUserProductsHandlers(); - }) - .catch((exc) => logger.error({ msg: 'Error starting app', exc })); - -app.on('second-instance', async (_, argv) => { - logger.debug({ tag: 'AUTH', msg: 'Deeplink received on second instance, processing...' }); - const deeplinkArg = argv.find((arg) => arg.startsWith('internxt://')); - if (!deeplinkArg) return; - - try { - await handleDeeplink({ url: deeplinkArg }); - } catch (error) { - logger.error({ tag: 'AUTH', msg: 'Error handling deeplink', error }); - } -}); - -eventBus.on('WIDGET_IS_READY', () => { - registerBackupHandlers(); - startBackupsIfAvailable(); -}); - -eventBus.on('USER_LOGGED_IN', async () => { - try { - if (!AppDataSource.isInitialized) { - await AppDataSource.initialize(); - eventBus.emit('APP_DATA_SOURCE_INITIALIZED'); - } - - getAuthWindow()?.hide(); - - getTheme(); - - setTrayStatus('IDLE'); - const widget = await getOrCreateWidged(); - const tray = getTray(); - if (widget && tray) { - setBoundsOfWidgetByPath(widget, tray); - } - - setTimeout(() => { - const authWin = getAuthWindow(); - if (authWin && !authWin.isDestroyed()) { - authWin.destroy(); - } - }, 300); - - const lastOnboardingShown = configStore.get('lastOnboardingShown'); - - if (!lastOnboardingShown) { - openOnboardingWindow(); - } else if (widget) { - widget.show(); - } - await getUserAvailableProductsAndStore(); - await trySetupAntivirusIpcAndInitialize(); - } catch (error) { - logger.error({ - msg: 'Error on main process while handling USER_LOGGED_IN event:', - error, - }); - } -}); - -eventBus.on('USER_LOGGED_OUT', async () => { - setTrayStatus('IDLE'); - const widget = getWidget(); - - if (widget) { - widget?.hide(); - - void getAntivirusManager().shutdown(); - } - - await createAuthWindow(); - - if (widget) { - widget.destroy(); - } - await resetAppDataSourceOnLogout(); - - // await uninstallNautilusExtension(); -}); - -process.on('uncaughtException', (error) => { - /** - * v.2.5.1 - * Esteban Galvis Triana - * EPIPE errors close stdout, so they must be handled specially to avoid infinite logging loops. - */ - if ('code' in error && error.code === 'EPIPE') return; - - if (error.name === 'AbortError') { - logger.debug({ msg: 'Fetch request was aborted' }); - } else { - try { - logger.error({ msg: 'Uncaught exception in main process: ', error }); - } catch { - return; - } - } -}); - -ipcMain.handle('check-internet-connection', async () => { - return new Promise((resolve) => { - dns.lookup('google.com', (err) => { - resolve(!err); - }); - - setTimeout(() => resolve(false), 3000); - }); -}); +bootstrapMainProcess();