改进软件错误处理,添加对软件崩溃的错误日志记录

This commit is contained in:
lyswhut 2021-05-18 15:32:33 +08:00
parent 3a6b889829
commit b1822f4584
11 changed files with 291 additions and 58 deletions

View File

@ -6,6 +6,7 @@
// import '@/utils/log'
import './shim'
import '@/utils/errorHandle'
import { init as initLog } from '@/utils/log'
import '@/config/globalData'
import SplashScreen from 'react-native-splash-screen'
import { init as initNavigation, navigations, showPactModal } from '@/navigation'
@ -25,6 +26,7 @@ console.log('starting app...')
let store
let isInited = false
let isFirstRun = true
initLog()
const init = () => {
if (isInited) return Promise.resolve()

View File

@ -1,5 +1,7 @@
### 优化
- 改进软件错误处理,添加对软件崩溃的错误日志记录,可在设置-其他查看错误日志历史。注:清理缓存时日志也将会被清理
### 修复
- 修复修复协议弹窗可以被绕过的问题
- 修复从在线列表使用稍后播放功能播放歌曲时,歌曲封面不显示的问题
- 修复正在播放“稍后播放”的歌曲时,对“稍后播放”前播放的列表进行添加、删除操作会导致切歌的问题
- 修复显示版本更新弹窗会导致应用崩溃的问题

View File

@ -10,12 +10,14 @@ const styles = StyleSheet.create({
// flexGrow: 0,
flexShrink: 1,
marginTop: 15,
marginLeft: 15,
marginRight: 15,
marginLeft: 5,
marginRight: 5,
marginBottom: 25,
},
content: {
flexGrow: 0,
paddingLeft: 10,
paddingRight: 10,
},
title: {
fontSize: 14,
@ -24,9 +26,15 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'center',
paddingBottom: 15,
paddingLeft: 15,
// paddingRight: 15,
},
btnsDirection: {
paddingLeft: 15,
},
btnsReversedDirection: {
paddingLeft: 15,
flexDirection: 'row-reverse',
},
btn: {
flex: 1,
paddingTop: 10,
@ -35,8 +43,13 @@ const styles = StyleSheet.create({
paddingRight: 10,
alignItems: 'center',
borderRadius: 4,
},
btnDirection: {
marginRight: 15,
},
btnReversedDirection: {
marginLeft: 15,
},
})
@ -51,8 +64,9 @@ export default ({
text = '',
cancelText = '',
confirmText = '',
showCancel = true,
showConfirm = true,
children,
reverseBtn = false,
}) => {
const theme = useGetter('common', 'theme')
const { t } = useTranslation()
@ -64,15 +78,15 @@ export default ({
{children || <Text style={{ ...styles.title, color: theme.normal }}>{text}</Text>}
</ScrollView>
</View>
<View style={styles.btns}>
{showCancel
? <Button style={{ ...styles.btn, backgroundColor: theme.secondary45 }} onPress={onCancel}>
<Text style={{ color: theme.secondary_5 }}>{cancelText || t('cancel')}</Text>
<View style={{ ...styles.btns, ...(reverseBtn ? styles.btnsReversedDirection : styles.btnsDirection) }}>
<Button style={{ ...styles.btn, ...(reverseBtn ? styles.btnReversedDirection : styles.btnDirection), backgroundColor: theme.secondary45 }} onPress={onCancel}>
<Text style={{ color: theme.secondary_5 }}>{cancelText || t('cancel')}</Text>
</Button>
{showConfirm
? <Button style={{ ...styles.btn, ...(reverseBtn ? styles.btnReversedDirection : styles.btnDirection), backgroundColor: theme.secondary45 }} onPress={onConfirm}>
<Text style={{ fontSize: 14, color: theme.secondary_5 }}>{confirmText || t('confirm')}</Text>
</Button>
: null}
<Button style={{ ...styles.btn, backgroundColor: theme.secondary45 }} onPress={onConfirm}>
<Text style={{ fontSize: 14, color: theme.secondary_5 }}>{confirmText || t('confirm')}</Text>
</Button>
</View>
</Dialog>
)

View File

@ -14,7 +14,7 @@ const styles = StyleSheet.create({
},
modalView: {
maxWidth: '90%',
minWidth: '50%',
minWidth: '60%',
maxHeight: '78%',
backgroundColor: 'white',
borderRadius: 4,

View File

@ -168,11 +168,17 @@
"setting_backup_part_import_list_desc": "Select the list of backup files",
"setting_backup_part_export_list_desc": "Select the save location of the playlist backup file",
"setting_other": "Extras",
"setting_other_cache": "Cache management (including the cache of songs, lyrics, etc., it is not recommended to clean up if there is no problem related to song playback)",
"setting_other_cache_size": "Currently used cache size: ",
"setting_other": "Other",
"setting_other_cache": "Cache management (including the cache of songs, lyrics, error logs, etc., it is not recommended to clean up if there is no problem related to song playback)",
"setting_other_cache_size": "Currently used cache size:",
"setting_other_cache_clear_btn": "Clear Cache",
"setting_other_cache_clear_success_tip": "Cache clearing completed",
"setting_other_log": "Error log (error log when the software crashes 💥)",
"setting_other_log_tip_clean_success": "Log cleaning completed",
"setting_other_log_tip_null": "The log is empty~",
"setting_other_log_btn_show": "View log",
"setting_other_log_btn_hide": "Close",
"setting_other_log_btn_clean": "Clear",
"setting_version": "Software Update",
"setting_version_show_ver_modal": "Open the update window 🚀",

View File

@ -169,10 +169,16 @@
"setting_backup_part_export_list_desc": "选择歌单备份文件保存位置",
"setting_other": "其他",
"setting_other_cache": "缓存管理(包括歌曲、歌词等缓存,没有歌曲播放相关的问题不建议清理)",
"setting_other_cache": "缓存管理(包括歌曲、歌词、错误日志等缓存,没有歌曲播放相关的问题不建议清理)",
"setting_other_cache_size": "当前已用缓存大小:",
"setting_other_cache_clear_btn": "清理缓存",
"setting_other_cache_clear_success_tip": "缓存清理完成",
"setting_other_log": "错误日志(软件崩溃💥时的错误日志)",
"setting_other_log_tip_clean_success": "日志清理完成",
"setting_other_log_tip_null": "日志是空的哦~",
"setting_other_log_btn_show": "查看日志",
"setting_other_log_btn_hide": "关闭",
"setting_other_log_btn_clean": "清空",
"setting_version": "软件更新",
"setting_version_show_ver_modal": "打开更新窗口 🚀",

View File

@ -34,6 +34,7 @@ const VersionModal = ({ componentId }) => {
const textStyle = StyleSheet.compose(styles.text, {
color: theme.normal,
marginBottom: 10,
})
const textLinkStyle = StyleSheet.compose(styles.text, {
textDecorationLine: 'underline',
@ -74,46 +75,22 @@ const VersionModal = ({ componentId }) => {
<View style={styles.main}>
<Text style={{ ...styles.title, color: theme.normal }}>许可协议</Text>
<ScrollView style={styles.content} keyboardShouldPersistTaps={'always'}>
<View style={styles.part}>
<Text style={textStyle} >本项目软件基于 <Text onPress={openLicensePage} style={textLinkStyle}>Apache License 2.0</Text> 使使使 Apache License 2.0 </Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} >词语约定本协议中的本软件指洛雪音乐桌面版项目使用者指签署本协议的使用者官方音乐平台指对本软件内置的包括酷我酷狗咪咕等音乐源的官方平台统称版权数据指包括但不限于图像音频名字等在内的他人拥有所属版权的数据</Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} ><Text style={styles.bold}>1.</Text> </Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} ><Text style={styles.bold}>2.</Text> 使使 <Text style={styles.bold}>24</Text> 使</Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} ><Text style={styles.bold}>3.</Text> </Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} ><Text style={styles.bold}>4.</Text> 使</Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} ><Text style={styles.bold}>5.</Text> 使使使使</Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} ><Text style={styles.bold}>6.</Text> <Text onPress={openHomePage} style={textLinkStyle}>GitHub</Text> <Text style={styles.bold}>使</Text>使使使</Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} ><Text style={styles.bold}>*</Text> </Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} ><Text style={styles.bold}>*</Text> 使</Text>
</View>
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={textStyle} ><Text style={styles.bold}>*</Text> </Text>
</View>
<Text selectable style={textStyle} >本项目软件基于 <Text onPress={openLicensePage} style={textLinkStyle}>Apache License 2.0</Text> 使使使 Apache License 2.0 </Text>
<Text selectable style={textStyle} >词语约定本协议中的本软件指洛雪音乐桌面版项目使用者指签署本协议的使用者官方音乐平台指对本软件内置的包括酷我酷狗咪咕等音乐源的官方平台统称版权数据指包括但不限于图像音频名字等在内的他人拥有所属版权的数据</Text>
<Text selectable style={textStyle} ><Text style={styles.bold}>1.</Text> </Text>
<Text selectable style={textStyle} ><Text style={styles.bold}>2.</Text> 使使 <Text style={styles.bold}>24</Text> 使</Text>
<Text selectable style={textStyle} ><Text style={styles.bold}>3.</Text> </Text>
<Text selectable style={textStyle} ><Text style={styles.bold}>4.</Text> 使</Text>
<Text selectable style={textStyle} ><Text style={styles.bold}>5.</Text> 使使使使</Text>
<Text selectable style={textStyle} ><Text style={styles.bold}>6.</Text> <Text onPress={openHomePage} style={textLinkStyle}>GitHub</Text> <Text style={styles.bold}>使</Text>使使使</Text>
<Text selectable style={textStyle} ><Text style={styles.bold}>*</Text> </Text>
<Text selectable style={textStyle} ><Text style={styles.bold}>*</Text> 使</Text>
<Text selectable style={textStyle} ><Text style={styles.bold}>*</Text> </Text>
{
isAgreePact
? null
: (
<View style={{ ...styles.part, flexDirection: 'row', flexWrap: 'wrap' }}>
<Text style={{ ...styles.text, ...styles.bold, color: theme.normal }} >若你使用者接受以上协议请点击下面的接受按钮签署本协议若不接受请点击不接受后退出软件并清除本软件的所有数据</Text>
</View>
<Text selectable style={{ ...styles.text, ...styles.bold, color: theme.normal }} >若你使用者接受以上协议请点击下面的接受按钮签署本协议若不接受请点击不接受后退出软件并清除本软件的所有数据</Text>
)
}
</ScrollView>

View File

@ -0,0 +1,95 @@
import React, { memo, useRef, useState, useEffect } from 'react'
import { StyleSheet, View, Text, InteractionManager } from 'react-native'
import { LOG_TYPE, getLogs, clearLogs } from '@/utils/log'
import { useGetter } from '@/store'
// import { gzip, ungzip } from 'pako'
import SubTitle from '../components/SubTitle'
import Button from '../components/Button'
import { useTranslation } from '@/plugins/i18n'
import { toast } from '@/utils/tools'
import ConfirmAlert from '@/components/common/ConfirmAlert'
export default memo(() => {
const { t } = useTranslation()
const [visibleNewFolder, setVisibleNewFolder] = useState(false)
const [logText, setLogText] = useState('')
const theme = useGetter('common', 'theme')
const isUnmountedRef = useRef(true)
const getErrorLog = () => {
getLogs(LOG_TYPE.error).then(log => {
if (isUnmountedRef.current) return
const logArr = log.split('\n')
logArr.reverse()
setLogText(logArr.join('\n\n').replace(/\n+$/, ''))
})
}
const openLogModal = () => {
getErrorLog()
setVisibleNewFolder(true)
}
const handleHide = () => {
setVisibleNewFolder(false)
}
const handleCleanLog = () => {
clearLogs(LOG_TYPE.error).then(() => {
toast(t('setting_other_log_tip_clean_success'))
getErrorLog()
})
}
useEffect(() => {
isUnmountedRef.current = false
return () => {
isUnmountedRef.current = true
}
// handleGetAppCacheSize()
}, [])
return (
<>
<SubTitle title={t('setting_other_log')}>
<View style={styles.btn}>
<Button onPress={openLogModal}>{t('setting_other_log_btn_show')}</Button>
</View>
</SubTitle>
<ConfirmAlert
cancelText={t('setting_other_log_btn_hide')}
confirmText={t('setting_other_log_btn_clean')}
visible={visibleNewFolder}
onCancel={handleHide}
onConfirm={handleCleanLog}
showConfirm={!!logText}
reverseBtn={true}
>
<View style={styles.newFolderContent} onStartShouldSetResponder={() => true}>
{
logText
? <Text selectable style={{ ...styles.logText, color: theme.normal10 }}>{ logText }</Text>
: <Text style={{ ...styles.tipText, color: theme.normal10 }}>{t('setting_other_log_tip_null')}</Text>
}
</View>
</ConfirmAlert>
</>
)
})
const styles = StyleSheet.create({
cacheSize: {
marginBottom: 5,
},
btn: {
flexDirection: 'row',
},
tipText: {
fontSize: 14,
},
logText: {
fontSize: 12,
},
})

View File

@ -2,6 +2,7 @@ import React, { memo } from 'react'
import Section from '../components/Section'
import Cache from './Cache'
import Log from './Log'
// import MaxCache from './MaxCache'
import { useTranslation } from '@/plugins/i18n'
@ -11,6 +12,7 @@ export default memo(() => {
return (
<Section title={t('setting_other')}>
<Cache />
<Log />
{/* <MaxCache /> */}
</Section>
)

View File

@ -1,7 +1,11 @@
import { Alert } from 'react-native'
import { exitApp } from '@/utils/tools'
import { setJSExceptionHandler, setNativeExceptionHandler } from 'react-native-exception-handler'
import { log } from '@/utils/log'
const errorHandler = (e, isFatal) => {
if (isFatal) {
log.error(e.message)
Alert.alert(
'💥Unexpected error occurred💥',
`
@ -12,15 +16,20 @@ ${isFatal ? 'Fatal:' : ''} ${e.name} ${e.message}
`,
[{
text: '关闭 (Close)',
onPress: () => {
exitApp()
},
}],
)
} else {
log.error(e.message)
console.log(e) // So that we can see it in the ADB logs in case of Android if needed
}
}
setJSExceptionHandler(errorHandler)
setJSExceptionHandler(errorHandler, true)
setNativeExceptionHandler((errorString) => {
log.error(errorString)
console.error('+++++', errorString, '+++++')
})

View File

@ -1,6 +1,125 @@
import { requestStoragePermission } from '@/utils/common'
import { externalDirectoryPath, existsFile, writeFile, appendFile } from '@/utils/fs'
// import { requestStoragePermission } from '@/utils/common'
import { temporaryDirectoryPath, existsFile, writeFile, appendFile, mkdir, readFile, unlink } from '@/utils/fs'
export const LOG_TYPE = {
info: 'INFO',
warn: 'WARN',
error: 'ERROR',
}
const logDir = temporaryDirectoryPath + '/lx_logs'
const logPath = {
info: logDir + '/info.log',
warn: logDir + '/warn.log',
error: logDir + '/error.log',
}
const logTools = {
tempLog: {
info: [],
warn: [],
error: [],
},
writeLog(type, msg) {
switch (type) {
case LOG_TYPE.info:
appendFile(logPath.info, '\n' + msg)
break
case LOG_TYPE.warn:
appendFile(logPath.warn, '\n' + msg)
break
case LOG_TYPE.error:
appendFile(logPath.error, '\n' + msg)
break
default:
break
}
},
async initLogFile(type, filePath) {
try {
let isExists = await existsFile(filePath)
if (!isExists) await writeFile(filePath, '')
if (this.tempLog[type].length) this.writeLog(LOG_TYPE[type], this.tempLog[type].map(m => `${m.time} ${m.type} ${m.text}`).join('\n'))
this.tempLog[type] = null
} catch (err) {
console.error(err)
}
},
}
export const init = () => {
return mkdir(logDir).then(() => {
const tasks = []
for (const [type, path] of Object.entries(logPath)) {
tasks.push(logTools.initLogFile(type, path))
}
console.log('init log tools')
return Promise.all(tasks)
})
}
export const getLogs = (type = LOG_TYPE.error) => {
let path
switch (type) {
case LOG_TYPE.info:
path = logPath.info
break
case LOG_TYPE.warn:
path = logPath.warn
break
case LOG_TYPE.error:
path = logPath.error
break
default:
return Promise.reject(new Error('Unknow log type'))
}
return readFile(path)
}
export const clearLogs = (type = LOG_TYPE.error) => {
let path
switch (type) {
case LOG_TYPE.info:
path = logPath.info
break
case LOG_TYPE.warn:
path = logPath.warn
break
case LOG_TYPE.error:
path = logPath.error
break
default:
return Promise.reject(new Error('Unknow log type'))
}
return unlink(path).then(() => writeFile(path, ''))
}
export const log = {
info(...msgs) {
console.info(...msgs)
let msg = msgs.map(m => typeof m == 'object' ? JSON.stringify(m) : m).join(' ')
if (msg.startsWith('%c')) return
let time = new Date().toLocaleString()
if (logTools.tempLog.info) {
logTools.tempLog.info.push({ type: 'LOG', time, text: msg })
} else logTools.writeLog(LOG_TYPE.info, `${time} LOG ${msg}`)
},
warn(...msgs) {
console.warn(...msgs)
let msg = msgs.map(m => typeof m == 'object' ? JSON.stringify(m) : m).join(' ')
let time = new Date().toLocaleString()
if (logTools.tempLog.warn) {
logTools.tempLog.warn.push({ type: 'WARN', time, text: msg })
} else logTools.writeLog(LOG_TYPE.warn, `${time} WARN ${msg}`)
},
error(...msgs) {
console.error(...msgs)
let msg = msgs.map(m => typeof m == 'object' ? JSON.stringify(m) : m).join(' ')
let time = new Date().toLocaleString()
if (logTools.tempLog.error) {
logTools.tempLog.error.push({ type: 'ERROR', time, text: msg })
} else logTools.writeLog(LOG_TYPE.error, `${time} ERROR ${msg}`)
},
}
/*
if (process.env.NODE_ENV !== 'development') {
const logPath = externalDirectoryPath + '/debug.log'
@ -56,3 +175,4 @@ if (process.env.NODE_ENV !== 'development') {
init()
}
*/