fix: harden startup flow and auth re-entry
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { BrowserWindow, app, shell } from 'electron'
|
import { BrowserWindow, app, shell } from 'electron'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { is } from '@electron-toolkit/utils'
|
|
||||||
import icon from '../../../resources/icon.png?asset'
|
import icon from '../../../resources/icon.png?asset'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { dirname } from 'path'
|
import { dirname } from 'path'
|
||||||
@@ -8,6 +7,10 @@ import { dirname } from 'path'
|
|||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = dirname(__filename)
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
|
function isDevelopment(): boolean {
|
||||||
|
return Boolean(process.env['ELECTRON_RENDERER_URL']) || process.env.NODE_ENV === 'development'
|
||||||
|
}
|
||||||
|
|
||||||
export function createMainWindow(): BrowserWindow {
|
export function createMainWindow(): BrowserWindow {
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@@ -32,7 +35,7 @@ export function createMainWindow(): BrowserWindow {
|
|||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
if (isDevelopment() && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { app, dialog } from 'electron'
|
import { app, dialog } from 'electron'
|
||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { ConfigManager } from '../services/config/config-manager'
|
import { ConfigManager } from '../services/config/config-manager'
|
||||||
@@ -12,10 +11,23 @@ export function configurePlaywrightBrowsersPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setupElectronRuntime(): void {
|
export function setupElectronRuntime(): void {
|
||||||
electronApp.setAppUserModelId('com.electron')
|
app.setAppUserModelId('com.electron')
|
||||||
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window)
|
window.webContents.on('before-input-event', (event, input) => {
|
||||||
|
const isReloadShortcut = (input.control || input.meta) && input.key.toLowerCase() === 'r'
|
||||||
|
const isToggleDevTools = input.key === 'F12'
|
||||||
|
|
||||||
|
if (!app.isPackaged && isToggleDevTools && input.type === 'keyDown') {
|
||||||
|
window.webContents.toggleDevTools()
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.isPackaged && isReloadShortcut) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,10 @@ import {
|
|||||||
} from './bootstrap/runtime'
|
} from './bootstrap/runtime'
|
||||||
import { setupProcessGuards } from './bootstrap/process-guards'
|
import { setupProcessGuards } from './bootstrap/process-guards'
|
||||||
|
|
||||||
const playwrightBrowsersPath = configurePlaywrightBrowsersPath()
|
app.whenReady().then(async () => {
|
||||||
|
|
||||||
setupProcessGuards()
|
setupProcessGuards()
|
||||||
registerMainWindowLifecycle()
|
registerMainWindowLifecycle()
|
||||||
|
const playwrightBrowsersPath = configurePlaywrightBrowsersPath()
|
||||||
app.whenReady().then(async () => {
|
|
||||||
ensurePlaywrightRuntime(playwrightBrowsersPath)
|
ensurePlaywrightRuntime(playwrightBrowsersPath)
|
||||||
await initializeMainProcessServices()
|
await initializeMainProcessServices()
|
||||||
setupElectronRuntime()
|
setupElectronRuntime()
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type {
|
|||||||
const log = createLogger('AuthApplicationService')
|
const log = createLogger('AuthApplicationService')
|
||||||
|
|
||||||
export class AuthApplicationService {
|
export class AuthApplicationService {
|
||||||
|
private silentLoginPromise: Promise<SilentLoginResponse> | null = null
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly sessionManager: SessionManager = SessionManager.getInstance(),
|
private readonly sessionManager: SessionManager = SessionManager.getInstance(),
|
||||||
private readonly updateService: UpdateService = UpdateService.getInstance()
|
private readonly updateService: UpdateService = UpdateService.getInstance()
|
||||||
@@ -25,6 +27,20 @@ export class AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async silentLogin(): Promise<SilentLoginResponse> {
|
async silentLogin(): Promise<SilentLoginResponse> {
|
||||||
|
if (this.silentLoginPromise) {
|
||||||
|
log.debug('Reusing in-flight silent login request')
|
||||||
|
return this.silentLoginPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
this.silentLoginPromise = this.performSilentLogin()
|
||||||
|
try {
|
||||||
|
return await this.silentLoginPromise
|
||||||
|
} finally {
|
||||||
|
this.silentLoginPromise = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performSilentLogin(): Promise<SilentLoginResponse> {
|
||||||
log.info('Attempting silent login')
|
log.info('Attempting silent login')
|
||||||
const success = await this.sessionManager.loginByComputerName()
|
const success = await this.sessionManager.loginByComputerName()
|
||||||
const userInfo = this.sessionManager.getUserInfo()
|
const userInfo = this.sessionManager.getUserInfo()
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export class ConfigManager {
|
|||||||
if (this.initialized) return
|
if (this.initialized) return
|
||||||
|
|
||||||
// 检测是否为开发环境
|
// 检测是否为开发环境
|
||||||
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
|
const isDev = process.env.NODE_ENV === 'development' || !(app?.isPackaged ?? false)
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
// 开发环境:配置文件放在项目根目录,方便编辑和调试
|
// 开发环境:配置文件放在项目根目录,方便编辑和调试
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export function setLogLevel(level: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add error-specific file transport in production
|
// Add error-specific file transport in production
|
||||||
if (app.isPackaged) {
|
if (app?.isPackaged) {
|
||||||
logger.add(
|
logger.add(
|
||||||
new DailyRotateFile({
|
new DailyRotateFile({
|
||||||
filename: path.join(getLogDir(), 'error-%DATE%.log'),
|
filename: path.join(getLogDir(), 'error-%DATE%.log'),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function appendPortableLaunchLog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentAppVersion(): string {
|
function getCurrentAppVersion(): string {
|
||||||
return typeof app.getVersion === 'function' ? app.getVersion() : '0.0.0'
|
return typeof app?.getVersion === 'function' ? app.getVersion() : '0.0.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentChannel(): ReleaseChannel {
|
function getCurrentChannel(): ReleaseChannel {
|
||||||
@@ -79,7 +79,7 @@ function getSupportState(config: UpdateConfig | null): {
|
|||||||
return { supported: false, reason: '当前仅支持 Windows 自动更新' }
|
return { supported: false, reason: '当前仅支持 Windows 自动更新' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app?.isPackaged) {
|
||||||
return { supported: true }
|
return { supported: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,10 @@ export class SessionManager {
|
|||||||
* @returns True if login successful, false otherwise
|
* @returns True if login successful, false otherwise
|
||||||
*/
|
*/
|
||||||
public async login(username: string, password: string): Promise<boolean> {
|
public async login(username: string, password: string): Promise<boolean> {
|
||||||
|
let dao: InstanceType<(typeof import('./bip-users-dao'))['BIPUsersDAO']> | null = null
|
||||||
try {
|
try {
|
||||||
const { BIPUsersDAO } = await import('./bip-users-dao')
|
const { BIPUsersDAO } = await import('./bip-users-dao')
|
||||||
const dao = new BIPUsersDAO()
|
dao = new BIPUsersDAO()
|
||||||
const userInfo = await dao.authenticate(username, password)
|
const userInfo = await dao.authenticate(username, password)
|
||||||
|
|
||||||
if (userInfo) {
|
if (userInfo) {
|
||||||
@@ -60,6 +61,12 @@ export class SessionManager {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SessionManager] Login error:', error)
|
console.error('[SessionManager] Login error:', error)
|
||||||
return false
|
return false
|
||||||
|
} finally {
|
||||||
|
if (dao) {
|
||||||
|
await dao.disconnect().catch((error) => {
|
||||||
|
console.error('[SessionManager] Login disconnect error:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +75,11 @@ export class SessionManager {
|
|||||||
* @returns True if login successful, false otherwise
|
* @returns True if login successful, false otherwise
|
||||||
*/
|
*/
|
||||||
public async loginByComputerName(): Promise<boolean> {
|
public async loginByComputerName(): Promise<boolean> {
|
||||||
|
let dao: InstanceType<(typeof import('./bip-users-dao'))['BIPUsersDAO']> | null = null
|
||||||
try {
|
try {
|
||||||
const { BIPUsersDAO } = await import('./bip-users-dao')
|
const { BIPUsersDAO } = await import('./bip-users-dao')
|
||||||
const { hostname } = await import('os')
|
const { hostname } = await import('os')
|
||||||
const dao = new BIPUsersDAO()
|
dao = new BIPUsersDAO()
|
||||||
const computerName = hostname()
|
const computerName = hostname()
|
||||||
const userInfo = await dao.authenticateByComputerName(computerName)
|
const userInfo = await dao.authenticateByComputerName(computerName)
|
||||||
|
|
||||||
@@ -87,6 +95,12 @@ export class SessionManager {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SessionManager] Silent login error:', error)
|
console.error('[SessionManager] Silent login error:', error)
|
||||||
return false
|
return false
|
||||||
|
} finally {
|
||||||
|
if (dao) {
|
||||||
|
await dao.disconnect().catch((error) => {
|
||||||
|
console.error('[SessionManager] Silent login disconnect error:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,13 +188,20 @@ export class SessionManager {
|
|||||||
* Get all users from database (for Admin user selection)
|
* Get all users from database (for Admin user selection)
|
||||||
*/
|
*/
|
||||||
public async getAllUsers(): Promise<UserInfo[]> {
|
public async getAllUsers(): Promise<UserInfo[]> {
|
||||||
|
let dao: InstanceType<(typeof import('./bip-users-dao'))['BIPUsersDAO']> | null = null
|
||||||
try {
|
try {
|
||||||
const { BIPUsersDAO } = await import('./bip-users-dao')
|
const { BIPUsersDAO } = await import('./bip-users-dao')
|
||||||
const dao = new BIPUsersDAO()
|
dao = new BIPUsersDAO()
|
||||||
return await dao.getAllUsers()
|
return await dao.getAllUsers()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SessionManager] Get all users error:', error)
|
console.error('[SessionManager] Get all users error:', error)
|
||||||
return []
|
return []
|
||||||
|
} finally {
|
||||||
|
if (dao) {
|
||||||
|
await dao.disconnect().catch((error) => {
|
||||||
|
console.error('[SessionManager] Get all users disconnect error:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ function App(): React.JSX.Element {
|
|||||||
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null)
|
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null)
|
||||||
const [updateCatalog, setUpdateCatalog] = useState<UpdateDialogCatalog | null>(null)
|
const [updateCatalog, setUpdateCatalog] = useState<UpdateDialogCatalog | null>(null)
|
||||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false)
|
const [showUpdateDialog, setShowUpdateDialog] = useState(false)
|
||||||
|
const authInitializationStartedRef = React.useRef(false)
|
||||||
|
|
||||||
// Load error message from sessionStorage
|
// Load error message from sessionStorage
|
||||||
const showError = (message: string) => {
|
const showError = (message: string) => {
|
||||||
@@ -145,6 +146,12 @@ function App(): React.JSX.Element {
|
|||||||
|
|
||||||
// Initialize authentication on mount
|
// Initialize authentication on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (authInitializationStartedRef.current) {
|
||||||
|
logger.debug('Skipping duplicate auth initialization effect')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authInitializationStartedRef.current = true
|
||||||
logger.info('=== Initializing auth... ===')
|
logger.info('=== Initializing auth... ===')
|
||||||
void initializeAuth()
|
void initializeAuth()
|
||||||
}, [initializeAuth, logger])
|
}, [initializeAuth, logger])
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import type { LogLevel } from '../../../shared/ipc-channels'
|
import type { LogLevel } from '../../../shared/ipc-channels'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,34 +70,25 @@ export function useLogger(context: string): RendererLogger {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Return memoized logger methods
|
// Return memoized logger methods
|
||||||
return {
|
return useMemo(
|
||||||
|
() => ({
|
||||||
log: logger,
|
log: logger,
|
||||||
info: useCallback(
|
info: (message: string, meta?: Record<string, unknown>) => {
|
||||||
(message: string, meta?: Record<string, unknown>) => {
|
|
||||||
logger('info', message, meta)
|
logger('info', message, meta)
|
||||||
},
|
},
|
||||||
[logger]
|
warn: (message: string, meta?: Record<string, unknown>) => {
|
||||||
),
|
|
||||||
warn: useCallback(
|
|
||||||
(message: string, meta?: Record<string, unknown>) => {
|
|
||||||
logger('warn', message, meta)
|
logger('warn', message, meta)
|
||||||
},
|
},
|
||||||
[logger]
|
error: (message: string, meta?: Record<string, unknown>) => {
|
||||||
),
|
|
||||||
error: useCallback(
|
|
||||||
(message: string, meta?: Record<string, unknown>) => {
|
|
||||||
logger('error', message, meta)
|
logger('error', message, meta)
|
||||||
},
|
},
|
||||||
[logger]
|
debug: (message: string, meta?: Record<string, unknown>) => {
|
||||||
),
|
|
||||||
debug: useCallback(
|
|
||||||
(message: string, meta?: Record<string, unknown>) => {
|
|
||||||
logger('debug', message, meta)
|
logger('debug', message, meta)
|
||||||
},
|
}
|
||||||
|
}),
|
||||||
[logger]
|
[logger]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FPS monitoring utility for detecting UI lag from excessive logging
|
* FPS monitoring utility for detecting UI lag from excessive logging
|
||||||
|
|||||||
Reference in New Issue
Block a user