feat(stage-tamagotchi): add electron main process file logger hook (#1247)
* feat(stage-tamagotchi): add electron main process file logger hook * chore: code update Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: update comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * [autofix.ci] apply automated fixes * chore: code style update Co-authored-by: RainbowBird <rbxin2003@outlook.com> * chore: code style update Co-authored-by: RainbowBird <rbxin2003@outlook.com> * [autofix.ci] apply automated fixes --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: RainbowBird <rbxin2003@outlook.com>
This commit is contained in:
co-authored by
Copilot
RainbowBird
autofix-ci[bot]
parent
38d41cca84
commit
92e75d86fb
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* File Logger for Electron Main Process
|
||||
*
|
||||
* Sets up file-based logging by creating timestamped log files in the userData directory.
|
||||
*
|
||||
* Log file naming: airi-tamagotchi-{timestamp}.log
|
||||
* - No rotation needed due to unique timestamp per session
|
||||
* - Unique timestamp per session avoids cross-process log file sharing
|
||||
* - Easy to identify and debug specific sessions
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fileLogger = await setupFileLogger()
|
||||
* setGlobalFormat(Format.Pretty)
|
||||
* setGlobalLogLevel(LogLevel.Log)
|
||||
*
|
||||
* setGlobalHookPostLog((log, formatted) => {
|
||||
* if (fileLogger.logFileFd !== null) {
|
||||
* void fileLogger.appendLog(formatted)
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { mkdir, open, stat } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { app } from 'electron'
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
const LOG_FILE_PREFIX = 'airi-tamagotchi'
|
||||
|
||||
// ============================================================================
|
||||
// Public Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle for the file logger, providing access to the log file and append operations.
|
||||
*/
|
||||
export interface FileLoggerHandle {
|
||||
/** Path to the current session's log file, or null if initialization failed */
|
||||
logFilePath: string | null
|
||||
/** File descriptor for the current session's log file, or null if initialization failed */
|
||||
logFileFd: number | null
|
||||
/**
|
||||
* Appends a log entry to the file.
|
||||
* @param content - The formatted log content to append
|
||||
*/
|
||||
appendLog: (content: string) => Promise<void>
|
||||
/**
|
||||
* Closes the log file and releases resources.
|
||||
*/
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
export const nullFileLoggerHandle: FileLoggerHandle = {
|
||||
logFilePath: null,
|
||||
logFileFd: null,
|
||||
appendLog: async () => {},
|
||||
close: async () => {},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Extracts a human-readable error message from an unknown error object.
|
||||
*/
|
||||
function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the log file path for the current session.
|
||||
* Format: {userData}/logs/airi-tamagotchi-{timestamp}.log
|
||||
*/
|
||||
function createLogFilePath(logsDir: string, timestamp: number): string {
|
||||
return join(logsDir, `${LOG_FILE_PREFIX}-${timestamp}.log`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the logs directory exists.
|
||||
* Returns the logs directory path if successful, null otherwise.
|
||||
*/
|
||||
async function ensureLogsDirectory(): Promise<string | null> {
|
||||
try {
|
||||
const logsDir = join(app.getPath('userData'), 'logs')
|
||||
await mkdir(logsDir, { recursive: true })
|
||||
return logsDir
|
||||
}
|
||||
catch (error) {
|
||||
const message = getErrorMessage(error)
|
||||
console.error(`[FileLogger] Failed to create logs directory: ${message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current log file exists and returns its size.
|
||||
*/
|
||||
async function getLogFileSize(filePath: string): Promise<number | null> {
|
||||
try {
|
||||
const stats = await stat(filePath)
|
||||
return stats.size
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Public API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sets up the file logger by creating a timestamped log file.
|
||||
*
|
||||
* Returns a {@link FileLoggerHandle} that provides:
|
||||
* - `logFilePath`: Path to the log file (null if failed)
|
||||
* - `logFileFd`: File descriptor (null if failed)
|
||||
* - `appendLog(content)`: Async function to append logs to the file
|
||||
* - `close()`: Async function to close the file
|
||||
*
|
||||
* Note: This function only creates the file. The caller is responsible for
|
||||
* registering the `setGlobalHookPostLog` hook from @guiiai/logg.
|
||||
*/
|
||||
export async function setupFileLogger(): Promise<FileLoggerHandle> {
|
||||
const timestamp = Date.now()
|
||||
|
||||
const logsDir = await ensureLogsDirectory()
|
||||
if (!logsDir) {
|
||||
return nullFileLoggerHandle
|
||||
}
|
||||
|
||||
const logFilePath = createLogFilePath(logsDir, timestamp)
|
||||
|
||||
try {
|
||||
const fileHandle = await open(logFilePath, 'a')
|
||||
const logFileFd = fileHandle.fd
|
||||
let isFileClosed = false
|
||||
|
||||
// Write initialization message
|
||||
const sessionStartMessage = `[FileLogger] Initialized - logging to: ${logFilePath}\n`
|
||||
await fileHandle.appendFile(sessionStartMessage)
|
||||
|
||||
console.info(`[FileLogger] Session logs: ${logFilePath}`)
|
||||
|
||||
async function appendLog(content: string) {
|
||||
if (isFileClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedContent = content.endsWith('\n') ? content : `${content}\n`
|
||||
|
||||
try {
|
||||
await fileHandle.appendFile(normalizedContent)
|
||||
}
|
||||
catch (error) {
|
||||
const message = getErrorMessage(error)
|
||||
console.error(`[FileLogger] Failed to write log: ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function close() {
|
||||
if (isFileClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await fileHandle.close()
|
||||
isFileClosed = true
|
||||
console.info('[FileLogger] File closed successfully')
|
||||
}
|
||||
catch (error) {
|
||||
const message = getErrorMessage(error)
|
||||
console.error(`[FileLogger] Failed to close log file: ${message}`)
|
||||
}
|
||||
|
||||
const size = await getLogFileSize(logFilePath)
|
||||
const sizeInfo = size !== null ? ` (${(size / 1024).toFixed(2)} KB)` : ''
|
||||
console.info(`[FileLogger] Session log file: ${logFilePath}${sizeInfo}`)
|
||||
}
|
||||
|
||||
return { logFilePath, logFileFd, appendLog, close }
|
||||
}
|
||||
catch (error) {
|
||||
const message = getErrorMessage(error)
|
||||
console.error(`[FileLogger] Failed to create log file - logging to console only: ${message}`)
|
||||
return nullFileLoggerHandle
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { FileLoggerHandle } from './app/file-logger'
|
||||
|
||||
import { dirname } from 'node:path'
|
||||
import { env, platform } from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -5,7 +7,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import messages from '@proj-airi/i18n/locales'
|
||||
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg'
|
||||
import { Format, LogLevel, setGlobalFormat, setGlobalHookPostLog, setGlobalLogLevel, useLogg } from '@guiiai/logg'
|
||||
import { initScreenCaptureForMain } from '@proj-airi/electron-screen-capture/main'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { noop } from 'es-toolkit'
|
||||
@@ -15,6 +17,7 @@ import { isLinux } from 'std-env'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
|
||||
import { openDebugger, setupDebugger } from './app/debugger'
|
||||
import { nullFileLoggerHandle, setupFileLogger } from './app/file-logger'
|
||||
import { createGlobalAppConfig } from './configs/global'
|
||||
import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed } from './libs/bootkit/lifecycle'
|
||||
import { setElectronMainDirname } from './libs/electron/location'
|
||||
@@ -78,7 +81,19 @@ electronApp.setAppUserModelId('ai.moeru.airi')
|
||||
|
||||
initScreenCaptureForMain()
|
||||
|
||||
let fileLogger: FileLoggerHandle = nullFileLoggerHandle
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Initialize file logger and register the hook
|
||||
fileLogger = await setupFileLogger()
|
||||
|
||||
// Register the global hook for file logging
|
||||
setGlobalHookPostLog((_, formatted) => {
|
||||
if (fileLogger.logFileFd !== null) {
|
||||
void fileLogger.appendLog(formatted)
|
||||
}
|
||||
})
|
||||
|
||||
injeca.setLogger(createLoggLogger(useLogg('injeca').useGlobalConfig()))
|
||||
|
||||
const appConfig = injeca.provide('configs:app', () => createGlobalAppConfig())
|
||||
@@ -190,4 +205,5 @@ app.on('window-all-closed', () => {
|
||||
app.on('before-quit', async () => {
|
||||
emitAppBeforeQuit()
|
||||
injeca.stop()
|
||||
await fileLogger.close() // Ensure all logs are flushed
|
||||
})
|
||||
|
||||
Generated
+4
-9
@@ -3229,7 +3229,7 @@ importers:
|
||||
version: 14.1.0(vue@3.5.29(typescript@5.9.3))
|
||||
'@wxt-dev/module-vue':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
nanoid:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
@@ -6222,9 +6222,6 @@ packages:
|
||||
'@moeru/std@0.1.0-beta.1':
|
||||
resolution: {integrity: sha512-ImfoMBchZP+F4aos2nFmlcipcgIhT+ZkUUVWB/bBHEqyB/w/nbI/xgA8a1ibJUtG7RcQnQH+SKom0r4/1xCWqA==}
|
||||
|
||||
'@moeru/std@0.1.0-beta.15':
|
||||
resolution: {integrity: sha512-lTi/b2ysilOSE/yo9uY9JIaNOE32kHpemWMxA4ZDIMxWeHZAEXK+jLi8EiXHtdBltjDlBYfNWeWcfH0KKiEJQA==}
|
||||
|
||||
'@moeru/std@0.1.0-beta.17':
|
||||
resolution: {integrity: sha512-GyLDCKHiqXyO60Wn0+Hc7Tfo/GJQA3pV5FZGkzGhvJM7LQPZDB0tksuJV92LuRF1lG8x78B+8UfVFhVf8xXVfg==}
|
||||
|
||||
@@ -20173,7 +20170,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@antfu/eslint-config': 7.6.1(@typescript-eslint/rule-tester@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3))(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(@unocss/eslint-plugin@66.6.2(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.29)(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18)
|
||||
'@masknet/eslint-plugin': 0.4.1(eslint@9.39.3(jiti@2.6.1))
|
||||
'@moeru/std': 0.1.0-beta.15
|
||||
'@moeru/std': 0.1.0-beta.17
|
||||
eslint: 9.39.3(jiti@2.6.1)
|
||||
eslint-flat-config-utils: 2.1.4
|
||||
eslint-plugin-de-morgan: 2.0.0(eslint@9.39.3(jiti@2.6.1))
|
||||
@@ -20220,8 +20217,6 @@ snapshots:
|
||||
|
||||
'@moeru/std@0.1.0-beta.1': {}
|
||||
|
||||
'@moeru/std@0.1.0-beta.15': {}
|
||||
|
||||
'@moeru/std@0.1.0-beta.17': {}
|
||||
|
||||
'@mrleebo/prisma-ast@0.13.1':
|
||||
@@ -24105,9 +24100,9 @@ snapshots:
|
||||
'@types/filesystem': 0.0.36
|
||||
'@types/har-format': 1.2.16
|
||||
|
||||
'@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@wxt-dev/module-vue@1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitejs/plugin-vue': 6.0.4(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue': 6.0.4(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))
|
||||
wxt: 0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
|
||||
Reference in New Issue
Block a user