返回 oh-my-ppt
lifecycle.ts
根目录 / src / main / app / lifecycle.ts
1 import { app, BrowserWindow } from 'electron'
2 import { is } from '@electron-toolkit/utils'
3 import log from 'electron-log/main.js'
4 import dayjs from 'dayjs'
5 import { join } from 'path'
6 import { mkdirSync } from 'fs'
7 import type { UpdateAvailablePayload } from '@shared/app-update'
8 import { isRepeatedRendererCrash, shouldRecoverRenderer } from './renderer-recovery'
9
10 const APP_NAME = 'OhMyPPT'
11 const UPDATE_MANIFEST_URL = 'https://www.ohmyppt.cc/version.json'
12
13 export function configureLogging(): void {
14 log.transports.file.level = 'info'
15 log.transports.file.maxSize = 20 * 1024 * 1024
16 log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
17
18 if (is.dev) {
19 const logDir = join(process.cwd(), 'logs')
20 mkdirSync(logDir, { recursive: true })
21 log.transports.file.resolvePathFn = () => join(logDir, 'main.log')
22 } else {
23 log.transports.file.resolvePathFn = () => {
24 const now = dayjs()
25 const yearMonth = now.format('YYYY-MM')
26 const yearMonthDay = now.format('YYYY-MM-DD')
27 return join(
28 app.getPath('userData'),
29 'ohmyppt_logs',
30 yearMonth,
31 `${yearMonthDay}-v${app.getVersion()}.log`
32 )
33 }
34 }
35
36 log.initialize()
37 log.info('[app] logger initialized', {
38 env: is.dev ? 'dev' : 'prod',
39 version: app.getVersion(),
40 file: log.transports.file.getFile().path
41 })
42 }
43
44 const parseVersion = (version: string): number[] =>
45 version
46 .trim()
47 .replace(/^v/i, '')
48 .split(/[.-]/)
49 .slice(0, 3)
50 .map((part) => {
51 const value = Number.parseInt(part, 10)
52 return Number.isFinite(value) ? value : 0
53 })
54
55 const isNewerVersion = (latestVersion: string, currentVersion: string): boolean => {
56 const latest = parseVersion(latestVersion)
57 const current = parseVersion(currentVersion)
58 for (let index = 0; index < Math.max(latest.length, current.length, 3); index += 1) {
59 const latestPart = latest[index] ?? 0
60 const currentPart = current[index] ?? 0
61 if (latestPart > currentPart) return true
62 if (latestPart < currentPart) return false
63 }
64 return false
65 }
66
67 const fetchLatestRelease = async (): Promise<UpdateAvailablePayload | null> => {
68 const controller = new AbortController()
69 const timeout = setTimeout(() => controller.abort(), 8000)
70 try {
71 const response = await fetch(UPDATE_MANIFEST_URL, {
72 headers: {
73 Accept: 'application/json',
74 'User-Agent': `${APP_NAME}/${app.getVersion()}`
75 },
76 signal: controller.signal
77 })
78 if (!response.ok) {
79 log.warn('[update] latest release request failed', {
80 status: response.status,
81 statusText: response.statusText
82 })
83 return null
84 }
85 const manifest = (await response.json()) as {
86 version?: unknown
87 downloadhome?: unknown
88 changeLog?: unknown
89 }
90 const latestVersion = String(manifest.version || '').trim()
91 const currentVersion = app.getVersion()
92 const rawDownloadhome = typeof manifest.downloadhome === 'string' ? manifest.downloadhome.trim() : ''
93 const downloadhome = rawDownloadhome && !/^https?:\/\//i.test(rawDownloadhome)
94 ? `http://${rawDownloadhome}`
95 : rawDownloadhome
96 const changeLog = typeof manifest.changeLog === 'string' ? manifest.changeLog.trim() : ''
97
98 if (!latestVersion || !isNewerVersion(latestVersion, currentVersion)) return null
99 return {
100 currentVersion,
101 latestVersion,
102 downloadUrl: downloadhome || undefined,
103 changeLog: changeLog || undefined
104 }
105 } catch (error) {
106 log.warn('[update] latest release check failed', {
107 message: error instanceof Error ? error.message : String(error)
108 })
109 return null
110 } finally {
111 clearTimeout(timeout)
112 }
113 }
114
115 export function scheduleUpdateNotification(window: BrowserWindow): void {
116 window.webContents.once('did-finish-load', () => {
117 setTimeout(() => {
118 void fetchLatestRelease().then((update) => {
119 if (!update || window.isDestroyed() || window.webContents.isDestroyed()) return
120 log.info('[update] new release available', update)
121 window.webContents.send('app:update-available', update)
122 })
123 }, 2500)
124 })
125 }
126
127 export function attachRendererCrashRecovery(
128 window: BrowserWindow,
129 options: {
130 isShuttingDown(): boolean
131 loadHome(): void
132 }
133 ): void {
134 let lastRendererCrashAt = 0
135 window.webContents.on('render-process-gone', (_event, details) => {
136 log.error('[renderer] process gone', details)
137 if (options.isShuttingDown() || !shouldRecoverRenderer(details.reason)) return
138
139 const now = Date.now()
140 const repeatedCrash = isRepeatedRendererCrash(lastRendererCrashAt, now)
141 lastRendererCrashAt = now
142
143 setTimeout(() => {
144 if (options.isShuttingDown() || window.isDestroyed() || window.webContents.isDestroyed()) return
145 if (!repeatedCrash) {
146 window.webContents.reload()
147 return
148 }
149
150 log.warn('[renderer] repeated crash; recovering at home route')
151 options.loadHome()
152 }, 250)
153 })
154 }
155
155 lines TYPESCRIPT