mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
✨ feat(settings): 支持自定义并持久化日志目录
- 将日志目录配置整合到数据目录设置,支持选择、打开、恢复默认与保存 - 持久化日志目录并保留环境变量优先级,目录调整后重启生效 - 通过跨进程锁和原子写入保护数据目录与日志目录配置 - 移除重复标题并互斥目录修改操作,补齐六语言文案 - 补充桌面接口、浏览器模拟及并发回归测试
This commit is contained in:
@@ -424,13 +424,17 @@ func dataRootInfoPayload(activeRoot string) map[string]interface{} {
|
||||
if currentRoot == "" {
|
||||
currentRoot = appdata.MustResolveActiveRoot()
|
||||
}
|
||||
return map[string]interface{}{
|
||||
payload := map[string]interface{}{
|
||||
"path": currentRoot,
|
||||
"defaultPath": defaultRoot,
|
||||
"driverPath": appdata.DriverRoot(currentRoot),
|
||||
"isDefaultPath": filepath.Clean(currentRoot) == filepath.Clean(defaultRoot),
|
||||
"bootstrapPath": appdata.BootstrapPath(),
|
||||
}
|
||||
for key, value := range logDirectoryInfoPayload() {
|
||||
payload[key] = value
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func normalizeCacheKeyConfig(config connection.ConnectionConfig) connection.ConnectionConfig {
|
||||
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
)
|
||||
|
||||
var dataRootMigrationExcludedEntries = map[string]struct{}{
|
||||
"storage_root.json": {},
|
||||
filepath.Base(appdata.BootstrapPath()): {},
|
||||
filepath.Base(appdata.BootstrapLockPath()): {},
|
||||
}
|
||||
|
||||
type dataRootTextFunc func(string, map[string]any) string
|
||||
|
||||
@@ -25,6 +25,20 @@ func methodsDataRootFunctionSource(t *testing.T, source string, signature string
|
||||
return source[start : start+len(signature)+end]
|
||||
}
|
||||
|
||||
func methodsLogDirectoryFunctionSource(t *testing.T, source string, signature string) string {
|
||||
t.Helper()
|
||||
start := strings.Index(source, signature)
|
||||
if start < 0 {
|
||||
t.Fatalf("methods_log_directory.go missing function signature %q", signature)
|
||||
}
|
||||
rest := source[start+len(signature):]
|
||||
end := strings.Index(rest, "\nfunc ")
|
||||
if end < 0 {
|
||||
return source[start:]
|
||||
}
|
||||
return source[start : start+len(signature)+end]
|
||||
}
|
||||
|
||||
func TestMethodsDataRootMessagesUseLocalizedText(t *testing.T) {
|
||||
sourceBytes, err := os.ReadFile("methods_data_root.go")
|
||||
if err != nil {
|
||||
@@ -140,6 +154,78 @@ func TestMethodsDataRootMessagesUseLocalizedText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMethodsLogDirectoryMessagesUseLocalizedText(t *testing.T) {
|
||||
sourceBytes, err := os.ReadFile("methods_log_directory.go")
|
||||
if err != nil {
|
||||
t.Fatalf("read methods_log_directory.go: %v", err)
|
||||
}
|
||||
source := string(sourceBytes)
|
||||
|
||||
checks := map[string]struct {
|
||||
rawMessages []string
|
||||
keys []string
|
||||
}{
|
||||
"func (a *App) SelectLogDirectory": {
|
||||
rawMessages: []string{
|
||||
`Message: "日志目录设置仅可在桌面应用中使用"`,
|
||||
`Message: "日志目录由环境变量 GONAVI_LOG_DIR 管理"`,
|
||||
`Title: "选择 GoNavi 日志目录"`,
|
||||
},
|
||||
keys: []string{
|
||||
"app.data_root.log_directory.backend.error.desktop_only",
|
||||
"app.data_root.log_directory.backend.error.environment_managed",
|
||||
"app.data_root.log_directory.backend.dialog.select_directory",
|
||||
},
|
||||
},
|
||||
"func (a *App) ApplyLogDirectory": {
|
||||
rawMessages: []string{
|
||||
`Message: "日志目录设置仅可在桌面应用中使用"`,
|
||||
`Message: "日志目录由环境变量 GONAVI_LOG_DIR 管理"`,
|
||||
`Message: fmt.Sprintf("保存日志目录失败:%v", err)`,
|
||||
`message := "日志目录已保存,重启应用后生效"`,
|
||||
`message = "日志目录未发生变化"`,
|
||||
},
|
||||
keys: []string{
|
||||
"app.data_root.log_directory.backend.error.desktop_only",
|
||||
"app.data_root.log_directory.backend.error.environment_managed",
|
||||
"app.data_root.log_directory.backend.error.save_failed",
|
||||
"app.data_root.log_directory.backend.message.updated_restart",
|
||||
"app.data_root.log_directory.backend.message.unchanged",
|
||||
},
|
||||
},
|
||||
"func (a *App) OpenLogDirectory": {
|
||||
rawMessages: []string{
|
||||
`Message: "日志目录设置仅可在桌面应用中使用"`,
|
||||
`Message: "当前日志目录不存在或不可访问"`,
|
||||
`Message: fmt.Sprintf("当前平台暂不支持打开日志目录:%s", stdRuntime.GOOS)`,
|
||||
`Message: fmt.Sprintf("打开日志目录失败:%v", err)`,
|
||||
`Message: "已打开日志目录"`,
|
||||
},
|
||||
keys: []string{
|
||||
"app.data_root.log_directory.backend.error.desktop_only",
|
||||
"app.data_root.log_directory.backend.error.directory_unavailable",
|
||||
"app.data_root.log_directory.backend.error.open_directory_unsupported",
|
||||
"app.data_root.log_directory.backend.error.open_directory_failed",
|
||||
"app.data_root.log_directory.backend.message.opened",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for signature, check := range checks {
|
||||
body := methodsLogDirectoryFunctionSource(t, source, signature)
|
||||
for _, raw := range check.rawMessages {
|
||||
if strings.Contains(body, raw) {
|
||||
t.Fatalf("%s still contains raw log-directory text %q", signature, raw)
|
||||
}
|
||||
}
|
||||
for _, key := range check.keys {
|
||||
if !strings.Contains(body, key) {
|
||||
t.Fatalf("%s should reference localized key %q", signature, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMethodsDataRootCatalogKeysExist(t *testing.T) {
|
||||
catalogs, err := i18n.LoadCatalogs()
|
||||
if err != nil {
|
||||
@@ -186,6 +272,35 @@ func TestMethodsDataRootCatalogKeysExist(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMethodsLogDirectoryCatalogKeysExistInAllLanguages(t *testing.T) {
|
||||
catalogs, err := i18n.LoadCatalogs()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadCatalogs() error = %v", err)
|
||||
}
|
||||
|
||||
keys := []string{
|
||||
"app.data_root.log_directory.backend.dialog.select_directory",
|
||||
"app.data_root.log_directory.backend.error.desktop_only",
|
||||
"app.data_root.log_directory.backend.error.directory_unavailable",
|
||||
"app.data_root.log_directory.backend.error.environment_managed",
|
||||
"app.data_root.log_directory.backend.error.open_directory_failed",
|
||||
"app.data_root.log_directory.backend.error.open_directory_unsupported",
|
||||
"app.data_root.log_directory.backend.error.save_failed",
|
||||
"app.data_root.log_directory.backend.message.opened",
|
||||
"app.data_root.log_directory.backend.message.unchanged",
|
||||
"app.data_root.log_directory.backend.message.updated_restart",
|
||||
}
|
||||
|
||||
for _, language := range i18n.SupportedLanguages() {
|
||||
catalog := catalogs[language]
|
||||
for _, key := range keys {
|
||||
if strings.TrimSpace(catalog[key]) == "" {
|
||||
t.Fatalf("%s catalog missing log-directory key %q", language, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDataRootDirectoryUsesEnglishLocalizedMessageWhenUnchanged(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
@@ -50,6 +50,14 @@ func TestMigrateDataRootContentsCopiesKnownFilesAndDirectories(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(sourceRoot, "jvm_diag_audit.jsonl"), []byte("jvm-diag-audit\n"), 0o644); err != nil {
|
||||
t.Fatalf("write jvm_diag_audit.jsonl failed: %v", err)
|
||||
}
|
||||
for _, excludedName := range []string{
|
||||
filepath.Base(appdata.BootstrapPath()),
|
||||
filepath.Base(appdata.BootstrapLockPath()),
|
||||
} {
|
||||
if err := os.WriteFile(filepath.Join(sourceRoot, excludedName), []byte("bootstrap-only"), 0o600); err != nil {
|
||||
t.Fatalf("write excluded bootstrap file %s: %v", excludedName, err)
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(sourceRoot, "sessions"), 0o755); err != nil {
|
||||
t.Fatalf("mkdir sessions failed: %v", err)
|
||||
}
|
||||
@@ -72,6 +80,14 @@ func TestMigrateDataRootContentsCopiesKnownFilesAndDirectories(t *testing.T) {
|
||||
if got, err := os.ReadFile(filepath.Join(targetRoot, "jvm_diag_audit.jsonl")); err != nil || string(got) != "jvm-diag-audit\n" {
|
||||
t.Fatalf("expected jvm_diag_audit.jsonl to migrate, content=%q err=%v", string(got), err)
|
||||
}
|
||||
for _, excludedName := range []string{
|
||||
filepath.Base(appdata.BootstrapPath()),
|
||||
filepath.Base(appdata.BootstrapLockPath()),
|
||||
} {
|
||||
if _, err := os.Stat(filepath.Join(targetRoot, excludedName)); !os.IsNotExist(err) {
|
||||
t.Fatalf("bootstrap file %s should not migrate, err=%v", excludedName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateDataRootContentsReplacesAuditDirectoryWithoutStaleSidecars(t *testing.T) {
|
||||
|
||||
187
internal/app/methods_log_directory.go
Normal file
187
internal/app/methods_log_directory.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
stdRuntime "runtime"
|
||||
"strings"
|
||||
|
||||
"GoNavi-Wails/internal/appdata"
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/logger"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
func directoriesEqual(left string, right string) bool {
|
||||
left = strings.TrimSpace(left)
|
||||
right = strings.TrimSpace(right)
|
||||
if left == "" || right == "" {
|
||||
return left == right
|
||||
}
|
||||
left = filepath.Clean(left)
|
||||
right = filepath.Clean(right)
|
||||
if stdRuntime.GOOS == "windows" {
|
||||
return strings.EqualFold(left, right)
|
||||
}
|
||||
return left == right
|
||||
}
|
||||
|
||||
func logDirectoryInfoPayload() map[string]interface{} {
|
||||
directory, managedByEnvironment := logger.ConfiguredDirectory()
|
||||
return buildLogDirectoryInfoPayload(directory, logger.DefaultDirectory(), logger.Path(), managedByEnvironment)
|
||||
}
|
||||
|
||||
func buildLogDirectoryInfoPayload(directory string, defaultDirectory string, logFilePath string, managedByEnvironment bool) map[string]interface{} {
|
||||
directory = strings.TrimSpace(directory)
|
||||
defaultDirectory = strings.TrimSpace(defaultDirectory)
|
||||
logFilePath = strings.TrimSpace(logFilePath)
|
||||
activeDirectory := ""
|
||||
if logFilePath != "" {
|
||||
activeDirectory = filepath.Dir(logFilePath)
|
||||
}
|
||||
source := "custom"
|
||||
if managedByEnvironment {
|
||||
source = "environment"
|
||||
} else if directoriesEqual(directory, defaultDirectory) {
|
||||
source = "default"
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"logDirectory": directory,
|
||||
"activeLogDirectory": activeDirectory,
|
||||
"logFilePath": logFilePath,
|
||||
"defaultLogDirectory": defaultDirectory,
|
||||
"logDirectorySource": source,
|
||||
"logDirectoryEditable": !managedByEnvironment,
|
||||
"logDirectoryRestartRequired": !directoriesEqual(activeDirectory, directory),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) SelectLogDirectory(currentDirectory string) connection.QueryResult {
|
||||
if a.webRuntime {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("app.data_root.log_directory.backend.error.desktop_only", nil)}
|
||||
}
|
||||
configuredDirectory, managedByEnvironment := logger.ConfiguredDirectory()
|
||||
if managedByEnvironment {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("app.data_root.log_directory.backend.error.environment_managed", nil)}
|
||||
}
|
||||
defaultDirectory := strings.TrimSpace(currentDirectory)
|
||||
if defaultDirectory == "" {
|
||||
defaultDirectory = configuredDirectory
|
||||
}
|
||||
if !filepath.IsAbs(defaultDirectory) {
|
||||
if abs, err := filepath.Abs(defaultDirectory); err == nil {
|
||||
defaultDirectory = abs
|
||||
}
|
||||
}
|
||||
|
||||
selection, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: a.appText("app.data_root.log_directory.backend.dialog.select_directory", nil),
|
||||
DefaultDirectory: defaultDirectory,
|
||||
CanCreateDirectories: true,
|
||||
})
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
if strings.TrimSpace(selection) == "" {
|
||||
return connection.QueryResult{Success: false, Message: "已取消"}
|
||||
}
|
||||
resolved, err := filepath.Abs(strings.TrimSpace(selection))
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
return connection.QueryResult{
|
||||
Success: true,
|
||||
Data: map[string]interface{}{
|
||||
"directory": filepath.Clean(resolved),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) ApplyLogDirectory(directory string) connection.QueryResult {
|
||||
a.dataRootApplyMu.Lock()
|
||||
defer a.dataRootApplyMu.Unlock()
|
||||
|
||||
if a.webRuntime {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("app.data_root.log_directory.backend.error.desktop_only", nil)}
|
||||
}
|
||||
currentDirectory, managedByEnvironment := logger.ConfiguredDirectory()
|
||||
if managedByEnvironment {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("app.data_root.log_directory.backend.error.environment_managed", nil)}
|
||||
}
|
||||
|
||||
target := strings.TrimSpace(directory)
|
||||
if directoriesEqual(target, logger.DefaultDirectory()) {
|
||||
target = ""
|
||||
}
|
||||
savedDirectory, err := appdata.SetConfiguredLogDirectory(target)
|
||||
if err != nil {
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("app.data_root.log_directory.backend.error.save_failed", map[string]any{
|
||||
"detail": err.Error(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
if savedDirectory == "" {
|
||||
savedDirectory = logger.DefaultDirectory()
|
||||
}
|
||||
message := a.appText("app.data_root.log_directory.backend.message.updated_restart", nil)
|
||||
if directoriesEqual(currentDirectory, savedDirectory) {
|
||||
message = a.appText("app.data_root.log_directory.backend.message.unchanged", nil)
|
||||
}
|
||||
return connection.QueryResult{
|
||||
Success: true,
|
||||
Message: message,
|
||||
Data: dataRootInfoPayload(a.configDir),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) OpenLogDirectory() connection.QueryResult {
|
||||
if a.webRuntime {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("app.data_root.log_directory.backend.error.desktop_only", nil)}
|
||||
}
|
||||
logFilePath := strings.TrimSpace(logger.Path())
|
||||
directory := ""
|
||||
if logFilePath != "" {
|
||||
directory = filepath.Dir(logFilePath)
|
||||
}
|
||||
if directory == "" {
|
||||
directory, _ = logger.ConfiguredDirectory()
|
||||
}
|
||||
if stat, err := os.Stat(directory); err != nil || !stat.IsDir() {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("app.data_root.log_directory.backend.error.directory_unavailable", nil)}
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch stdRuntime.GOOS {
|
||||
case "darwin":
|
||||
cmd = exec.Command("open", directory)
|
||||
case "windows":
|
||||
cmd = exec.Command("explorer", directory)
|
||||
case "linux":
|
||||
cmd = exec.Command("xdg-open", directory)
|
||||
default:
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("app.data_root.log_directory.backend.error.open_directory_unsupported", map[string]any{
|
||||
"platform": stdRuntime.GOOS,
|
||||
}),
|
||||
}
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
logger.Error(err, "打开日志目录失败")
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("app.data_root.log_directory.backend.error.open_directory_failed", map[string]any{
|
||||
"detail": err.Error(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
return connection.QueryResult{
|
||||
Success: true,
|
||||
Message: a.appText("app.data_root.log_directory.backend.message.opened", nil),
|
||||
Data: dataRootInfoPayload(a.configDir),
|
||||
}
|
||||
}
|
||||
124
internal/app/methods_log_directory_test.go
Normal file
124
internal/app/methods_log_directory_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/appdata"
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
func TestBuildLogDirectoryInfoPayloadMarksPendingRestart(t *testing.T) {
|
||||
configured := filepath.Join(t.TempDir(), "configured")
|
||||
active := filepath.Join(t.TempDir(), "active", "gonavi.log")
|
||||
defaultDirectory := filepath.Join(t.TempDir(), "default")
|
||||
|
||||
payload := buildLogDirectoryInfoPayload(configured, defaultDirectory, active, false)
|
||||
if payload["logDirectory"] != configured {
|
||||
t.Fatalf("logDirectory = %#v, want %q", payload["logDirectory"], configured)
|
||||
}
|
||||
if payload["activeLogDirectory"] != filepath.Dir(active) {
|
||||
t.Fatalf("activeLogDirectory = %#v, want %q", payload["activeLogDirectory"], filepath.Dir(active))
|
||||
}
|
||||
if payload["logDirectorySource"] != "custom" {
|
||||
t.Fatalf("logDirectorySource = %#v, want custom", payload["logDirectorySource"])
|
||||
}
|
||||
if payload["logDirectoryEditable"] != true {
|
||||
t.Fatalf("logDirectoryEditable = %#v, want true", payload["logDirectoryEditable"])
|
||||
}
|
||||
if payload["logDirectoryRestartRequired"] != true {
|
||||
t.Fatalf("logDirectoryRestartRequired = %#v, want true", payload["logDirectoryRestartRequired"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLogDirectoryInfoPayloadMarksEnvironmentManaged(t *testing.T) {
|
||||
directory := filepath.Join(t.TempDir(), "environment")
|
||||
payload := buildLogDirectoryInfoPayload(directory, filepath.Join(t.TempDir(), "default"), filepath.Join(directory, "gonavi.log"), true)
|
||||
if payload["logDirectorySource"] != "environment" {
|
||||
t.Fatalf("logDirectorySource = %#v, want environment", payload["logDirectorySource"])
|
||||
}
|
||||
if payload["logDirectoryEditable"] != false {
|
||||
t.Fatalf("logDirectoryEditable = %#v, want false", payload["logDirectoryEditable"])
|
||||
}
|
||||
if payload["logDirectoryRestartRequired"] != false {
|
||||
t.Fatalf("logDirectoryRestartRequired = %#v, want false", payload["logDirectoryRestartRequired"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyLogDirectoryRejectsFilePathWithoutChangingSetting(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
t.Setenv("GONAVI_LOG_DIR", "")
|
||||
|
||||
blockingPath := filepath.Join(t.TempDir(), "not-a-directory")
|
||||
if err := os.WriteFile(blockingPath, []byte("blocked"), 0o644); err != nil {
|
||||
t.Fatalf("write blocking path: %v", err)
|
||||
}
|
||||
result := NewApp().ApplyLogDirectory(blockingPath)
|
||||
if result.Success {
|
||||
t.Fatalf("ApplyLogDirectory should reject a file path: %+v", result)
|
||||
}
|
||||
configured, err := appdata.ResolveConfiguredLogDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveConfiguredLogDirectory returned error: %v", err)
|
||||
}
|
||||
if configured != "" {
|
||||
t.Fatalf("failed apply changed log directory to %q", configured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyLogDirectoryRejectsEnvironmentManagedSetting(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
t.Setenv("GONAVI_LOG_DIR", filepath.Join(t.TempDir(), "environment-logs"))
|
||||
|
||||
result := NewApp().ApplyLogDirectory(filepath.Join(t.TempDir(), "custom-logs"))
|
||||
if result.Success {
|
||||
t.Fatalf("ApplyLogDirectory should reject environment-managed setting: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyLogDirectoryRejectsWebRuntime(t *testing.T) {
|
||||
application := NewApp()
|
||||
application.webRuntime = true
|
||||
result := application.ApplyLogDirectory(filepath.Join(t.TempDir(), "custom-logs"))
|
||||
if result.Success {
|
||||
t.Fatalf("ApplyLogDirectory should reject web runtime: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyLogDirectorySerializesWithDataRootRequests(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
t.Setenv("GONAVI_LOG_DIR", "")
|
||||
|
||||
application := NewApp()
|
||||
customLogDirectory := filepath.Join(t.TempDir(), "custom-logs")
|
||||
application.dataRootApplyMu.Lock()
|
||||
done := make(chan connection.QueryResult, 1)
|
||||
go func() {
|
||||
done <- application.ApplyLogDirectory(customLogDirectory)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
application.dataRootApplyMu.Unlock()
|
||||
t.Fatal("ApplyLogDirectory bypassed the shared data-root serialization lock")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
application.dataRootApplyMu.Unlock()
|
||||
|
||||
select {
|
||||
case result := <-done:
|
||||
if !result.Success {
|
||||
t.Fatalf("serialized ApplyLogDirectory returned failure: %s", result.Message)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("serialized ApplyLogDirectory did not resume")
|
||||
}
|
||||
}
|
||||
30
internal/appdata/bootstrap_file_lock_other.go
Normal file
30
internal/appdata/bootstrap_file_lock_other.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build !(darwin || dragonfly || freebsd || linux || netbsd || openbsd || windows)
|
||||
|
||||
package appdata
|
||||
|
||||
import "os"
|
||||
|
||||
type bootstrapFileLock struct {
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func acquireBootstrapFileLock(path string) (*bootstrapFileLock, error) {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &bootstrapFileLock{file: file}, nil
|
||||
}
|
||||
|
||||
func (lock *bootstrapFileLock) Close() error {
|
||||
if lock == nil || lock.file == nil {
|
||||
return nil
|
||||
}
|
||||
err := lock.file.Close()
|
||||
lock.file = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func atomicReplaceBootstrapFile(source string, target string) error {
|
||||
return os.Rename(source, target)
|
||||
}
|
||||
49
internal/appdata/bootstrap_file_lock_unix.go
Normal file
49
internal/appdata/bootstrap_file_lock_unix.go
Normal file
@@ -0,0 +1,49 @@
|
||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd
|
||||
|
||||
package appdata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type bootstrapFileLock struct {
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func acquireBootstrapFileLock(path string) (*bootstrapFileLock, error) {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
_ = file.Chmod(0o600)
|
||||
return &bootstrapFileLock{file: file}, nil
|
||||
}
|
||||
|
||||
func (lock *bootstrapFileLock) Close() error {
|
||||
if lock == nil || lock.file == nil {
|
||||
return nil
|
||||
}
|
||||
unlockErr := unix.Flock(int(lock.file.Fd()), unix.LOCK_UN)
|
||||
closeErr := lock.file.Close()
|
||||
lock.file = nil
|
||||
return errors.Join(unlockErr, closeErr)
|
||||
}
|
||||
|
||||
func atomicReplaceBootstrapFile(source string, target string) error {
|
||||
if err := os.Rename(source, target); err != nil {
|
||||
return err
|
||||
}
|
||||
directory, err := os.Open(filepath.Dir(target))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.Join(directory.Sync(), directory.Close())
|
||||
}
|
||||
68
internal/appdata/bootstrap_file_lock_windows.go
Normal file
68
internal/appdata/bootstrap_file_lock_windows.go
Normal file
@@ -0,0 +1,68 @@
|
||||
//go:build windows
|
||||
|
||||
package appdata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type bootstrapFileLock struct {
|
||||
file *os.File
|
||||
overlapped windows.Overlapped
|
||||
}
|
||||
|
||||
func acquireBootstrapFileLock(path string) (*bootstrapFileLock, error) {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lock := &bootstrapFileLock{file: file}
|
||||
if err := windows.LockFileEx(
|
||||
windows.Handle(file.Fd()),
|
||||
windows.LOCKFILE_EXCLUSIVE_LOCK,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
&lock.overlapped,
|
||||
); err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
_ = file.Chmod(0o600)
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
func (lock *bootstrapFileLock) Close() error {
|
||||
if lock == nil || lock.file == nil {
|
||||
return nil
|
||||
}
|
||||
unlockErr := windows.UnlockFileEx(
|
||||
windows.Handle(lock.file.Fd()),
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
&lock.overlapped,
|
||||
)
|
||||
closeErr := lock.file.Close()
|
||||
lock.file = nil
|
||||
return errors.Join(unlockErr, closeErr)
|
||||
}
|
||||
|
||||
func atomicReplaceBootstrapFile(source string, target string) error {
|
||||
sourcePath, err := windows.UTF16PtrFromString(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetPath, err := windows.UTF16PtrFromString(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.MoveFileEx(
|
||||
sourcePath,
|
||||
targetPath,
|
||||
windows.MOVEFILE_REPLACE_EXISTING|windows.MOVEFILE_WRITE_THROUGH,
|
||||
)
|
||||
}
|
||||
@@ -6,14 +6,20 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const bootstrapFileName = "storage_root.json"
|
||||
const (
|
||||
bootstrapFileName = "storage_root.json"
|
||||
bootstrapLockFileName = bootstrapFileName + ".lock"
|
||||
configuredLogFileName = "gonavi.log"
|
||||
)
|
||||
const dataRootEnvName = "GONAVI_DATA_ROOT"
|
||||
|
||||
var (
|
||||
ErrSetActiveRootCreateDataDirectory = errors.New("create data directory failed")
|
||||
ErrSetActiveRootCreateBootstrapDirectory = errors.New("create bootstrap directory failed")
|
||||
bootstrapConfigMu sync.Mutex
|
||||
)
|
||||
|
||||
type setActiveRootError struct {
|
||||
@@ -57,7 +63,97 @@ func SetActiveRootErrorDetail(err error) error {
|
||||
}
|
||||
|
||||
type bootstrapConfig struct {
|
||||
DataRoot string `json:"dataRoot"`
|
||||
DataRoot string `json:"dataRoot,omitempty"`
|
||||
LogDirectory string `json:"logDirectory,omitempty"`
|
||||
}
|
||||
|
||||
func readBootstrapConfig() (bootstrapConfig, error) {
|
||||
data, err := os.ReadFile(BootstrapPath())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return bootstrapConfig{}, nil
|
||||
}
|
||||
return bootstrapConfig{}, err
|
||||
}
|
||||
var cfg bootstrapConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return bootstrapConfig{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func writeBootstrapConfig(cfg bootstrapConfig) error {
|
||||
cfg.DataRoot = strings.TrimSpace(cfg.DataRoot)
|
||||
cfg.LogDirectory = strings.TrimSpace(cfg.LogDirectory)
|
||||
if cfg.DataRoot == "" && cfg.LogDirectory == "" {
|
||||
if err := os.Remove(BootstrapPath()); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(DefaultRoot(), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeBootstrapConfigAtomic(payload)
|
||||
}
|
||||
|
||||
func writeBootstrapConfigAtomic(payload []byte) error {
|
||||
temporary, err := os.CreateTemp(DefaultRoot(), ".storage_root-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temporaryPath := temporary.Name()
|
||||
cleanup := true
|
||||
defer func() {
|
||||
if cleanup {
|
||||
_ = os.Remove(temporaryPath)
|
||||
}
|
||||
}()
|
||||
if err := temporary.Chmod(0o644); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := temporary.Write(payload); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Sync(); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := atomicReplaceBootstrapFile(temporaryPath, BootstrapPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
cleanup = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateBootstrapConfig(update func(*bootstrapConfig)) (err error) {
|
||||
bootstrapConfigMu.Lock()
|
||||
defer bootstrapConfigMu.Unlock()
|
||||
if err := os.MkdirAll(DefaultRoot(), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
fileLock, err := acquireBootstrapFileLock(BootstrapLockPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, fileLock.Close())
|
||||
}()
|
||||
cfg, err := readBootstrapConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
update(&cfg)
|
||||
return writeBootstrapConfig(cfg)
|
||||
}
|
||||
|
||||
func configuredRootOverride() string {
|
||||
@@ -76,6 +172,10 @@ func BootstrapPath() string {
|
||||
return filepath.Join(DefaultRoot(), bootstrapFileName)
|
||||
}
|
||||
|
||||
func BootstrapLockPath() string {
|
||||
return filepath.Join(DefaultRoot(), bootstrapLockFileName)
|
||||
}
|
||||
|
||||
func normalizeRoot(root string) (string, error) {
|
||||
trimmed := strings.TrimSpace(root)
|
||||
if trimmed == "" {
|
||||
@@ -100,15 +200,10 @@ func ResolveActiveRoot() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := os.ReadFile(BootstrapPath())
|
||||
bootstrapConfigMu.Lock()
|
||||
cfg, err := readBootstrapConfig()
|
||||
bootstrapConfigMu.Unlock()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return defaultRoot, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
var cfg bootstrapConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(cfg.DataRoot) == "" {
|
||||
@@ -145,21 +240,66 @@ func SetActiveRoot(root string) (string, error) {
|
||||
if err := os.MkdirAll(targetRoot, 0o755); err != nil {
|
||||
return "", newSetActiveRootError(ErrSetActiveRootCreateDataDirectory, err)
|
||||
}
|
||||
if targetRoot == defaultRoot {
|
||||
if err := os.Remove(BootstrapPath()); err != nil && !os.IsNotExist(err) {
|
||||
return "", err
|
||||
if targetRoot != defaultRoot {
|
||||
if err := os.MkdirAll(defaultRoot, 0o755); err != nil {
|
||||
return "", newSetActiveRootError(ErrSetActiveRootCreateBootstrapDirectory, err)
|
||||
}
|
||||
return defaultRoot, nil
|
||||
}
|
||||
if err := os.MkdirAll(defaultRoot, 0o755); err != nil {
|
||||
return "", newSetActiveRootError(ErrSetActiveRootCreateBootstrapDirectory, err)
|
||||
}
|
||||
payload, err := json.MarshalIndent(bootstrapConfig{DataRoot: targetRoot}, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(BootstrapPath(), payload, 0o644); err != nil {
|
||||
|
||||
if err := updateBootstrapConfig(func(cfg *bootstrapConfig) {
|
||||
if targetRoot == defaultRoot {
|
||||
cfg.DataRoot = ""
|
||||
} else {
|
||||
cfg.DataRoot = targetRoot
|
||||
}
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return targetRoot, nil
|
||||
}
|
||||
|
||||
func ResolveConfiguredLogDirectory() (string, error) {
|
||||
bootstrapConfigMu.Lock()
|
||||
cfg, err := readBootstrapConfig()
|
||||
bootstrapConfigMu.Unlock()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
directory := strings.TrimSpace(cfg.LogDirectory)
|
||||
if directory == "" {
|
||||
return "", nil
|
||||
}
|
||||
abs, err := filepath.Abs(directory)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Clean(abs), nil
|
||||
}
|
||||
|
||||
func SetConfiguredLogDirectory(directory string) (string, error) {
|
||||
target := strings.TrimSpace(directory)
|
||||
if target != "" {
|
||||
abs, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
target = filepath.Clean(abs)
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
probe, err := os.OpenFile(filepath.Join(target, configuredLogFileName), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := probe.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if err := updateBootstrapConfig(func(cfg *bootstrapConfig) {
|
||||
cfg.LogDirectory = target
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestResolveActiveRootDefaultsToLegacyGonaviDir(t *testing.T) {
|
||||
@@ -89,3 +90,143 @@ func TestResolveActiveRootPrefersEnvOverride(t *testing.T) {
|
||||
t.Fatalf("expected env override root %q, got %q", overrideRoot, resolvedRoot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataRootAndLogDirectoryPreserveEachOtherInBootstrap(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
|
||||
customDataRoot := filepath.Join(t.TempDir(), "gonavi-data")
|
||||
customLogDirectory := filepath.Join(t.TempDir(), "gonavi-logs")
|
||||
if _, err := SetConfiguredLogDirectory(customLogDirectory); err != nil {
|
||||
t.Fatalf("SetConfiguredLogDirectory returned error: %v", err)
|
||||
}
|
||||
if _, err := SetActiveRoot(customDataRoot); err != nil {
|
||||
t.Fatalf("SetActiveRoot returned error: %v", err)
|
||||
}
|
||||
|
||||
resolvedLogDirectory, err := ResolveConfiguredLogDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveConfiguredLogDirectory returned error: %v", err)
|
||||
}
|
||||
if resolvedLogDirectory != customLogDirectory {
|
||||
t.Fatalf("expected custom log directory %q, got %q", customLogDirectory, resolvedLogDirectory)
|
||||
}
|
||||
|
||||
if _, err := SetActiveRoot(""); err != nil {
|
||||
t.Fatalf("reset SetActiveRoot returned error: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(BootstrapPath()); err != nil {
|
||||
t.Fatalf("bootstrap should remain while log directory is customized: %v", err)
|
||||
}
|
||||
resolvedLogDirectory, err = ResolveConfiguredLogDirectory()
|
||||
if err != nil || resolvedLogDirectory != customLogDirectory {
|
||||
t.Fatalf("log directory after data-root reset = %q, %v", resolvedLogDirectory, err)
|
||||
}
|
||||
|
||||
if _, err := SetActiveRoot(customDataRoot); err != nil {
|
||||
t.Fatalf("restore custom data root returned error: %v", err)
|
||||
}
|
||||
if _, err := SetConfiguredLogDirectory(""); err != nil {
|
||||
t.Fatalf("reset SetConfiguredLogDirectory returned error: %v", err)
|
||||
}
|
||||
resolvedRoot, err := ResolveActiveRoot()
|
||||
if err != nil || resolvedRoot != customDataRoot {
|
||||
t.Fatalf("data root after log reset = %q, %v", resolvedRoot, err)
|
||||
}
|
||||
if _, err := os.Stat(BootstrapPath()); err != nil {
|
||||
t.Fatalf("bootstrap should remain while data root is customized: %v", err)
|
||||
}
|
||||
|
||||
if _, err := SetActiveRoot(""); err != nil {
|
||||
t.Fatalf("final SetActiveRoot reset returned error: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(BootstrapPath()); !os.IsNotExist(err) {
|
||||
t.Fatalf("bootstrap should be removed when both settings use defaults, got err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetConfiguredLogDirectoryRejectsFilePathWithoutChangingConfig(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
|
||||
blockingPath := filepath.Join(t.TempDir(), "not-a-directory")
|
||||
if err := os.WriteFile(blockingPath, []byte("blocked"), 0o644); err != nil {
|
||||
t.Fatalf("write blocking file: %v", err)
|
||||
}
|
||||
if _, err := SetConfiguredLogDirectory(blockingPath); err == nil {
|
||||
t.Fatal("expected file path to be rejected as a log directory")
|
||||
}
|
||||
configured, err := ResolveConfiguredLogDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveConfiguredLogDirectory returned error: %v", err)
|
||||
}
|
||||
if configured != "" {
|
||||
t.Fatalf("failed update changed configured log directory to %q", configured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetConfiguredLogDirectoryRejectsDirectoryAtLogFilePathWithoutChangingConfig(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
|
||||
targetDirectory := filepath.Join(t.TempDir(), "logs")
|
||||
if err := os.MkdirAll(filepath.Join(targetDirectory, configuredLogFileName), 0o755); err != nil {
|
||||
t.Fatalf("create blocking log-file directory: %v", err)
|
||||
}
|
||||
if _, err := SetConfiguredLogDirectory(targetDirectory); err == nil {
|
||||
t.Fatal("expected a directory at the log file path to be rejected")
|
||||
}
|
||||
configured, err := ResolveConfiguredLogDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveConfiguredLogDirectory returned error: %v", err)
|
||||
}
|
||||
if configured != "" {
|
||||
t.Fatalf("failed update changed configured log directory to %q", configured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapFileLockSerializesAccess(t *testing.T) {
|
||||
lockPath := filepath.Join(t.TempDir(), "storage_root.json.lock")
|
||||
first, err := acquireBootstrapFileLock(lockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("acquire first bootstrap lock: %v", err)
|
||||
}
|
||||
defer first.Close()
|
||||
|
||||
acquired := make(chan *bootstrapFileLock, 1)
|
||||
errs := make(chan error, 1)
|
||||
go func() {
|
||||
second, err := acquireBootstrapFileLock(lockPath)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
acquired <- second
|
||||
}()
|
||||
|
||||
select {
|
||||
case second := <-acquired:
|
||||
_ = second.Close()
|
||||
t.Fatal("second bootstrap lock acquired before the first was released")
|
||||
case err := <-errs:
|
||||
t.Fatalf("acquire second bootstrap lock: %v", err)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
if err := first.Close(); err != nil {
|
||||
t.Fatalf("release first bootstrap lock: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case second := <-acquired:
|
||||
if err := second.Close(); err != nil {
|
||||
t.Fatalf("release second bootstrap lock: %v", err)
|
||||
}
|
||||
case err := <-errs:
|
||||
t.Fatalf("acquire second bootstrap lock after release: %v", err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("second bootstrap lock did not acquire after the first was released")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/appdata"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -147,6 +149,23 @@ func Path() string {
|
||||
return logPath
|
||||
}
|
||||
|
||||
func DefaultDirectory() string {
|
||||
return defaultLogDir()
|
||||
}
|
||||
|
||||
func ConfiguredDirectory() (string, bool) {
|
||||
if directory := strings.TrimSpace(os.Getenv(envLogDir)); directory != "" {
|
||||
if abs, err := filepath.Abs(directory); err == nil {
|
||||
directory = filepath.Clean(abs)
|
||||
}
|
||||
return directory, true
|
||||
}
|
||||
if directory, err := appdata.ResolveConfiguredLogDirectory(); err == nil && directory != "" {
|
||||
return directory, false
|
||||
}
|
||||
return defaultLogDir(), false
|
||||
}
|
||||
|
||||
func Close() {
|
||||
Init()
|
||||
logMu.Lock()
|
||||
@@ -263,14 +282,17 @@ func printf(level string, format string, args ...any) {
|
||||
}
|
||||
|
||||
func initOutput() (string, io.Writer) {
|
||||
dir := strings.TrimSpace(os.Getenv(envLogDir))
|
||||
if dir == "" {
|
||||
dir = defaultLogDir()
|
||||
}
|
||||
dir, _ := ConfiguredDirectory()
|
||||
|
||||
if path, writer, ok := openLogFile(dir); ok {
|
||||
return path, writer
|
||||
}
|
||||
defaultDir := defaultLogDir()
|
||||
if filepath.Clean(dir) != filepath.Clean(defaultDir) {
|
||||
if path, writer, ok := openLogFile(defaultDir); ok {
|
||||
return path, writer
|
||||
}
|
||||
}
|
||||
|
||||
fallbackDir := filepath.Join(os.TempDir(), appHiddenDir, appLogDirName)
|
||||
if path, writer, ok := openLogFile(fallbackDir); ok {
|
||||
|
||||
@@ -6,10 +6,13 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/appdata"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -34,6 +37,62 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestConfiguredDirectoryUsesPersistedSetting(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
t.Setenv(envLogDir, "")
|
||||
|
||||
customDirectory := filepath.Join(t.TempDir(), "custom-logs")
|
||||
if _, err := appdata.SetConfiguredLogDirectory(customDirectory); err != nil {
|
||||
t.Fatalf("SetConfiguredLogDirectory returned error: %v", err)
|
||||
}
|
||||
directory, managedByEnvironment := ConfiguredDirectory()
|
||||
if managedByEnvironment {
|
||||
t.Fatal("persisted log directory should not be marked as environment-managed")
|
||||
}
|
||||
if directory != customDirectory {
|
||||
t.Fatalf("configured directory = %q, want %q", directory, customDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredDirectoryPrefersEnvironmentOverride(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
|
||||
persistedDirectory := filepath.Join(t.TempDir(), "persisted-logs")
|
||||
if _, err := appdata.SetConfiguredLogDirectory(persistedDirectory); err != nil {
|
||||
t.Fatalf("SetConfiguredLogDirectory returned error: %v", err)
|
||||
}
|
||||
environmentDirectory := filepath.Join(t.TempDir(), "environment-logs")
|
||||
t.Setenv(envLogDir, environmentDirectory)
|
||||
|
||||
directory, managedByEnvironment := ConfiguredDirectory()
|
||||
if !managedByEnvironment {
|
||||
t.Fatal("environment log directory should be marked as environment-managed")
|
||||
}
|
||||
if directory != environmentDirectory {
|
||||
t.Fatalf("configured directory = %q, want environment override %q", directory, environmentDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredDirectoryFallsBackToDefault(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
t.Setenv("USERPROFILE", homeDir)
|
||||
t.Setenv(envLogDir, "")
|
||||
|
||||
directory, managedByEnvironment := ConfiguredDirectory()
|
||||
if managedByEnvironment {
|
||||
t.Fatal("default log directory should not be marked as environment-managed")
|
||||
}
|
||||
want := filepath.Join(homeDir, appHiddenDir, appLogDirName)
|
||||
if directory != want {
|
||||
t.Fatalf("configured directory = %q, want default %q", directory, want)
|
||||
}
|
||||
}
|
||||
|
||||
type slowSyncSink struct {
|
||||
mu sync.Mutex
|
||||
contents bytes.Buffer
|
||||
|
||||
@@ -43,6 +43,9 @@ var desktopOnlyAppMethods = map[string]struct{}{
|
||||
"GetDataRootDirectoryInfo": {},
|
||||
"ApplyDataRootDirectory": {},
|
||||
"OpenDataRootDirectory": {},
|
||||
"SelectLogDirectory": {},
|
||||
"ApplyLogDirectory": {},
|
||||
"OpenLogDirectory": {},
|
||||
"SelectDriverDownloadDirectory": {},
|
||||
"SelectDriverPackageFile": {},
|
||||
"SelectDriverPackageDirectory": {},
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestMethodInvokerRejectsDesktopOnlyAppMethodsBeforeReflection(t *testing.T)
|
||||
"Shutdown", "ExportSQLAuditFile", "OpenSQLFile", "ExecuteSQLFile", "ReadSQLFile",
|
||||
"PreviewImportFile", "ImportDataWithProgress", "ImportDataWithProgressOptions", "GetDataRootDirectoryInfo",
|
||||
"ExportDatabaseSQLWithOptions", "ExportSchemaSQLWithOptions",
|
||||
"ApplyDataRootDirectory", "OpenDataRootDirectory", "SetApplicationBrandIcon",
|
||||
"ApplyDataRootDirectory", "OpenDataRootDirectory", "SelectLogDirectory", "ApplyLogDirectory", "OpenLogDirectory", "SetApplicationBrandIcon",
|
||||
} {
|
||||
_, err := invoker.Invoke(invokeRequest{Namespace: "app", Receiver: "app", Method: method})
|
||||
if err == nil || !strings.Contains(err.Error(), "unavailable in web runtime") {
|
||||
|
||||
Reference in New Issue
Block a user