保存目录不存在时给一句人话再退出,不再 panic 崩进程
为什么: checkConfig() 里检查 directory / mergeOutputDir 用的是 os.Stat + checkErr(err)。 目录不在时(盘没插、路径拼错)前者失败会直接 panic,打印一屏 goroutine 堆栈后崩掉 整个进程,而不是像同函数里其它检查那样给一句提示再 os.Exit(1)。服务被 nssm 之类托管 时崩了还会被反复拉起来,用户看到的只是一堆堆栈。 处理方式是复用原本只给 web 用的 checkWebDir,把它提升成通用的 checkDirField 并搬到 config.go:启动读配置、web 改设置、转码排队从此共用同一份实现,不会有两份各自漂移的 写法。文案沿用原来 IsDir 分支的措辞(「X 必须是存在的文件夹:路径」),只是不再依赖 os.Stat 先成功,且空串仍然表示「不指定」而不是错误。 验证: - 实跑对比。修前 directory 指向不存在的目录: panic: GetFileAttributesEx ...: The system cannot find the file specified. 修后:[ERROR] config.json里的directory 必须是存在的文件夹:… 退出码 1, 日志里 panic 出现 0 次。 - 同一个 exe 另验两件事:directory 指向存在的目录能正常启动并建出 record/merged; web /api/config 传不存在的目录仍返回 400 + 同一套文案(改名没走样)。 - 新增 2 条测试,其中 TestCheckConfigNeverPanicsOnMissingDir 是静态检查——这条失败 路径是 os.Exit(1),真跑一遍会把整个测试进程带走,所以改为读 config.go 源码断言 checkConfig 函数体里不出现 checkErr(,与项目里既有的静态一致性检查同一套路。 - 反向验证新增一项:把两处换回老的 checkErr 写法后,该测试确实变红。
This commit is contained in:
@@ -404,6 +404,20 @@ func currentAuthToken() string {
|
|||||||
return config.AuthToken
|
return config.AuthToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查配置里的文件夹字段,返回错误信息,空字符串表示通过。
|
||||||
|
// 启动读配置、web UI 改设置、转码排队都用这一份 —— 以前各自实现,
|
||||||
|
// 启动那份直接 os.Stat + checkErr,目录不存在时会 panic 崩掉整个进程。
|
||||||
|
func checkDirField(path, name string) string {
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
return name + " 必须是存在的文件夹:" + path
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// 检查 config.json 里的配置
|
// 检查 config.json 里的配置
|
||||||
func checkConfig() {
|
func checkConfig() {
|
||||||
for _, u := range config.UIDs {
|
for _, u := range config.UIDs {
|
||||||
@@ -416,21 +430,15 @@ func checkConfig() {
|
|||||||
lPrintErr(configFile + "里的 webPort 必须为 0(不启动 web UI)或者大于 1023 且少于 65536")
|
lPrintErr(configFile + "里的 webPort 必须为 0(不启动 web UI)或者大于 1023 且少于 65536")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if config.Directory != "" {
|
// 目录不在(盘没插、路径拼错)要给人话提示然后退出,不能直接 panic:
|
||||||
info, err := os.Stat(config.Directory)
|
// 崩溃时文件系统风险未知,而且服务被托管时会被反复拉起来
|
||||||
checkErr(err)
|
if msg := checkDirField(config.Directory, configFile+"里的directory"); msg != "" {
|
||||||
if !info.IsDir() {
|
lPrintErr(msg)
|
||||||
lPrintErrf("%s里的directory必须是存在的文件夹:%s", configFile, config.Directory)
|
os.Exit(1)
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if config.MergeOutputDir != "" {
|
if msg := checkDirField(config.MergeOutputDir, configFile+"里的mergeOutputDir"); msg != "" {
|
||||||
info, err := os.Stat(config.MergeOutputDir)
|
lPrintErr(msg)
|
||||||
checkErr(err)
|
os.Exit(1)
|
||||||
if !info.IsDir() {
|
|
||||||
lPrintErrf("%s里的mergeOutputDir必须是存在的文件夹:%s", configFile, config.MergeOutputDir)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if config.MergeBitrate < 0 {
|
if config.MergeBitrate < 0 {
|
||||||
lPrintErrf("%s里的mergeBitrate不能小于 0", configFile)
|
lPrintErrf("%s里的mergeBitrate不能小于 0", configFile)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -145,3 +146,48 @@ func TestReloadConfigKeepsMergeTempFiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存目录填错(盘没插、路径拼错)时启动必须给一句人话再退出,
|
||||||
|
// 不能 panic 崩掉整个进程 —— 服务被托管时崩了还会被反复拉起来
|
||||||
|
func TestCheckDirFieldReportsMissingDir(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
if msg := checkDirField("", "directory"); msg != "" {
|
||||||
|
t.Errorf("留空表示不指定,不该报错:%s", msg)
|
||||||
|
}
|
||||||
|
if msg := checkDirField(root, "directory"); msg != "" {
|
||||||
|
t.Errorf("存在的文件夹不该报错:%s", msg)
|
||||||
|
}
|
||||||
|
if msg := checkDirField(filepath.Join(root, "没有这个目录"), "directory"); !strings.Contains(msg, "必须是存在的文件夹") {
|
||||||
|
t.Errorf("目录不存在时要给出提示,实际返回 %q", msg)
|
||||||
|
}
|
||||||
|
file := filepath.Join(root, "afile")
|
||||||
|
if err := os.WriteFile(file, []byte("x"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if msg := checkDirField(file, "directory"); msg == "" {
|
||||||
|
t.Error("指向文件而不是文件夹时应当报错")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConfig 里曾经直接 os.Stat + checkErr:目录不在时会 panic 崩掉进程。
|
||||||
|
// 这条路径没法在测试里真跑一遍(失败时是 os.Exit(1),会把整个测试进程带走),
|
||||||
|
// 所以退一步把「只能给提示再退出」钉在源码上。
|
||||||
|
func TestCheckConfigNeverPanicsOnMissingDir(t *testing.T) {
|
||||||
|
src, err := os.ReadFile("config.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("读不到 config.go:" + err.Error())
|
||||||
|
}
|
||||||
|
text := string(src)
|
||||||
|
start := strings.Index(text, "func checkConfig()")
|
||||||
|
if start < 0 {
|
||||||
|
t.Fatal("config.go 里找不到 checkConfig")
|
||||||
|
}
|
||||||
|
rest := text[start:]
|
||||||
|
end := strings.Index(rest, "\n}\n")
|
||||||
|
if end < 0 {
|
||||||
|
t.Fatal("checkConfig 的函数体没有正常收尾")
|
||||||
|
}
|
||||||
|
if strings.Contains(rest[:end], "checkErr(") {
|
||||||
|
t.Error("checkConfig 里不能用 checkErr:出错会变成 panic 崩进程,改成 lPrintErr + os.Exit(1)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -956,7 +956,7 @@ func transcodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if outDir == "" {
|
if outDir == "" {
|
||||||
outDir = mergeSaveDir()
|
outDir = mergeSaveDir()
|
||||||
} else {
|
} else {
|
||||||
if msg := checkWebDir(outDir, "输出目录"); msg != "" {
|
if msg := checkDirField(outDir, "输出目录"); msg != "" {
|
||||||
http.Error(w, msg, http.StatusBadRequest)
|
http.Error(w, msg, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2431,18 +2431,6 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查配置里的文件夹字段,返回错误信息,空字符串表示通过
|
|
||||||
func checkWebDir(path, name string) string {
|
|
||||||
if path == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
info, err := os.Stat(path)
|
|
||||||
if err != nil || !info.IsDir() {
|
|
||||||
return name + " 必须是存在的文件夹:" + path
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改设置(保存目录和自动合并相关)
|
// 修改设置(保存目录和自动合并相关)
|
||||||
func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
@@ -2481,11 +2469,11 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "请求不是合法的 json:"+err.Error(), http.StatusBadRequest)
|
http.Error(w, "请求不是合法的 json:"+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if msg := checkWebDir(req.Directory, "directory"); msg != "" {
|
if msg := checkDirField(req.Directory, "directory"); msg != "" {
|
||||||
http.Error(w, msg, http.StatusBadRequest)
|
http.Error(w, msg, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if msg := checkWebDir(req.MergeOutputDir, "mergeOutputDir"); msg != "" {
|
if msg := checkDirField(req.MergeOutputDir, "mergeOutputDir"); msg != "" {
|
||||||
http.Error(w, msg, http.StatusBadRequest)
|
http.Error(w, msg, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user