From fb5d408a0166727a607114e92d2994bed7c84819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Wed, 20 Aug 2025 14:19:12 +0200 Subject: [PATCH] fifo logs --- TODO.md | 28 ++++----- app/application.go | 10 ++-- app/config.go | 44 ++++++++++---- app/logger.go | 138 +++++++++++++++++++++++++++++++++++++------ app/notifications.go | 2 +- app/sync.go | 6 +- app/tray.go | 18 ++++-- app/utils.go | 9 ++- 8 files changed, 202 insertions(+), 53 deletions(-) diff --git a/TODO.md b/TODO.md index c465cfc..5d60b5d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,21 +1,21 @@ - - log to linux fd (pipe) to open terminal +X - log to linux fd (pipe) to open terminal - - show state in tray +X - show state in tray - - tray menu - - force sync - - ~ current Etag - - ~ current SHA - - ~ last sync ts - - check sync - - quit - - show log (open terminal /w log) +X - tray menu +X - force sync +X - ~ current Etag +X - ~ current SHA +X - ~ last sync ts +X - check sync +X - quit +X - show log (open terminal /w log) - - config via json + params override +X - config via json + params override - - colorful log - - download/upload progress log +X - colorful log +X - download/upload progress log - - logfile in workdir \ No newline at end of file +X - logfile in workdir \ No newline at end of file diff --git a/app/application.go b/app/application.go index 6c84d41..c2e803e 100644 --- a/app/application.go +++ b/app/application.go @@ -20,9 +20,10 @@ import ( type Application struct { masterLock sync.Mutex - logLock sync.Mutex - logFile *os.File // file to write logs to, if set - logList []dataext.Triple[string, string, func(string) string] + logLock sync.Mutex + logFile *os.File // file to write logs to, if set + logList []LogMessage + logBroadcaster *dataext.PubSub[string, LogMessage] config Config @@ -55,7 +56,8 @@ func NewApplication() *Application { app := &Application{ masterLock: sync.Mutex{}, logLock: sync.Mutex{}, - logList: make([]dataext.Triple[string, string, func(string) string], 0, 1024), + logList: make([]LogMessage, 0, 1024), + logBroadcaster: dataext.NewPubSub[string, LogMessage](128), uploadRunning: syncext.NewAtomicBool(false), trayReady: syncext.NewAtomicBool(false), syncLoopRunning: syncext.NewAtomicBool(false), diff --git a/app/config.go b/app/config.go index d6305e5..4d35e73 100644 --- a/app/config.go +++ b/app/config.go @@ -20,7 +20,8 @@ type Config struct { WorkDir string `json:"work_dir"` - ForceColors bool `json:"force_colors"` + ForceColors bool `json:"force_colors"` + TerminalEmulator string `json:"terminal_emulator"` Debounce int `json:"debounce"` } @@ -44,12 +45,15 @@ func (app *Application) loadConfig() (Config, string) { var workDir string flag.StringVar(&workDir, "work_dir", "", "Temporary working directory") - var debounce int - flag.IntVar(&debounce, "debounce", 0, "Debounce before sync (in seconds)") - var forceColors bool flag.BoolVar(&forceColors, "color", false, "Force color-output (default: auto-detect)") + var terminalEmulator string + flag.StringVar(&terminalEmulator, "terminal_emulator", "", "Command to start terminal-emulator, e.g. 'konsole -e'") + + var debounce int + flag.IntVar(&debounce, "debounce", 0, "Debounce before sync (in seconds)") + flag.Parse() if strings.HasPrefix(configPath, "~") { @@ -63,14 +67,29 @@ func (app *Application) loadConfig() (Config, string) { } if _, err := os.Stat(configPath); os.IsNotExist(err) && configPath != "" { + + te := "" + if commandExists("konsole") { + te = "konsole -e" + } else if commandExists("gnome-terminal") { + te = "gnome-terminal --" + } else if commandExists("xterm") { + te = "xterm -e" + } else if commandExists("x-terminal-emulator") { + te = "x-terminal-emulator -e" + } else { + app.LogError("Failed to determine terminal-emulator", nil) + } + _ = os.WriteFile(configPath, langext.Must(json.MarshalIndent(Config{ - WebDAVURL: "https://your-nextcloud-domain.example/remote.php/dav/files/keepass.kdbx", - WebDAVUser: "", - WebDAVPass: "", - LocalFallback: "", - WorkDir: "/tmp/kpsync", - Debounce: 3500, - ForceColors: false, + WebDAVURL: "https://your-nextcloud-domain.example/remote.php/dav/files/keepass.kdbx", + WebDAVUser: "", + WebDAVPass: "", + LocalFallback: "", + WorkDir: "/tmp/kpsync", + Debounce: 3500, + ForceColors: false, + TerminalEmulator: te, }, "", " ")), 0644) } @@ -106,6 +125,9 @@ func (app *Application) loadConfig() (Config, string) { if forceColors { cfg.ForceColors = forceColors } + if terminalEmulator != "" { + cfg.TerminalEmulator = terminalEmulator + } return cfg, configPath } diff --git a/app/logger.go b/app/logger.go index 994a34e..79c0d3c 100644 --- a/app/logger.go +++ b/app/logger.go @@ -1,28 +1,40 @@ package app import ( + "fmt" + "os" + "os/exec" + "path" "strings" + "syscall" + "time" - "git.blackforestbytes.com/BlackForestBytes/goext/dataext" "git.blackforestbytes.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/termext" ) +type LogMessage struct { + Line int + Prefix string + Message string + ColorFunc func(string) string +} + func colDefault(v string) string { return v } func (app *Application) LogFatal(msg string) { - app.logInternal("[F] ", msg, termext.Red) + app.logInternal("[F]", msg, termext.Red) panic(0) } func (app *Application) LogFatalErr(msg string, err error) { if err != nil { - app.logInternal("[F] ", msg+"\n"+err.Error()+"\n"+exerr.FromError(err).FormatLog(exerr.LogPrintOverview), termext.Red) + app.logInternal("[F]", msg+"\n"+err.Error()+"\n"+exerr.FromError(err).FormatLog(exerr.LogPrintOverview), termext.Red) } else { - app.logInternal("[F] ", msg, termext.Red) + app.logInternal("[F]", msg, termext.Red) } panic(0) @@ -30,22 +42,22 @@ func (app *Application) LogFatalErr(msg string, err error) { func (app *Application) LogError(msg string, err error) { if err != nil { - app.logInternal("[E] ", msg+"\n"+err.Error()+"\n"+exerr.FromError(err).FormatLog(exerr.LogPrintOverview), termext.Red) + app.logInternal("[E]", msg+"\n"+err.Error()+"\n"+exerr.FromError(err).FormatLog(exerr.LogPrintOverview), termext.Red) } else { - app.logInternal("[E] ", msg, termext.Red) + app.logInternal("[E]", msg, termext.Red) } } func (app *Application) LogWarn(msg string) { - app.logInternal("[W] ", msg, termext.Red) + app.logInternal("[W]", msg, termext.Red) } func (app *Application) LogInfo(msg string) { - app.logInternal("[I] ", msg, colDefault) + app.logInternal("[I]", msg, colDefault) } func (app *Application) LogDebug(msg string) { - app.logInternal("[D] ", msg, termext.Gray) + app.logInternal("[D]", msg, termext.Gray) } func (app *Application) logInternal(pf string, msg string, c func(_ string) string) { @@ -58,23 +70,25 @@ func (app *Application) logInternal(pf string, msg string, c func(_ string) stri for i, s := range strings.Split(msg, "\n") { if i == 0 { - println(c(pf + s)) - app.logList = append(app.logList, dataext.NewTriple(strings.TrimSpace(pf), s, c)) + println(c(pf + " " + s)) + app.logList = append(app.logList, LogMessage{i, pf, s, c}) if app.logFile != nil { - _, err := app.logFile.WriteString(pf + s + "\n") + _, err := app.logFile.WriteString(pf + " " + s + "\n") if err != nil { app.fallbackLog("[!] Failed to write logfile: " + err.Error()) } } + app.logBroadcaster.Publish("", LogMessage{i, pf, s, c}) } else { - println(c(langext.StrRepeat(" ", len(pf)) + s)) - app.logList = append(app.logList, dataext.NewTriple(strings.TrimSpace(pf), s, c)) + println(c(langext.StrRepeat(" ", len(pf)+1) + s)) + app.logList = append(app.logList, LogMessage{i, pf, s, c}) if app.logFile != nil { - _, err := app.logFile.WriteString(langext.StrRepeat(" ", len(pf)) + s + "\n") + _, err := app.logFile.WriteString(langext.StrRepeat(" ", len(pf)+1) + s + "\n") if err != nil { app.fallbackLog("[!] Failed to write logfile: " + err.Error()) } } + app.logBroadcaster.Publish("", LogMessage{i, pf, s, c}) } } @@ -103,7 +117,9 @@ func (app *Application) LogLine() { } } - app.logList = append(app.logList, dataext.NewTriple("", "", func(v string) string { return v })) + app.logList = append(app.logList, LogMessage{0, "", "", func(v string) string { return v }}) + + app.logBroadcaster.Publish("", LogMessage{0, "", "", func(v string) string { return v }}) } func (app *Application) fallbackLog(s string) { @@ -119,7 +135,7 @@ func (app *Application) writeOutStartupLogs() { defer app.logLock.Unlock() for _, v := range app.logList { - _, err := app.logFile.WriteString(v.V1 + " " + v.V2 + "\n") + _, err := app.logFile.WriteString(v.Prefix + " " + v.Message + "\n") if err != nil { app.fallbackLog("[!] Failed to write logfile: " + err.Error()) } @@ -130,3 +146,91 @@ func (app *Application) writeOutStartupLogs() { app.fallbackLog("[!] Failed to flush logfile: " + err.Error()) } } + +func (app *Application) openLogFile() { + err := exec.Command("xdg-open", path.Join(app.config.WorkDir, "kpsync.log")).Start() + if err != nil { + app.LogError("Failed to open log file with xdg-open", err) + return + } +} + +func (app *Application) openLogFifo() { + filePath := path.Join(app.config.WorkDir, fmt.Sprintf("kpsync.%s.fifo", langext.RandBase62(8))) + + app.LogDebug(fmt.Sprintf("Creating fifo file at '%s'", filePath)) + err := syscall.Mkfifo(filePath, 0640) + if err != nil { + app.LogError("Failed to create fifo file", err) + return + } + + defer func() { _ = syscall.Unlink(filePath) }() + + listernerStopSig := make(chan bool, 8) + + go func() { + + app.LogDebug(fmt.Sprintf("Opening fifo file '%s'", filePath)) + + f, err := os.OpenFile(filePath, os.O_WRONLY, 0600) + if err != nil { + app.LogError("Failed to open fifo file", err) + return + } + defer func() { _ = f.Close() }() + + app.LogDebug(fmt.Sprintf("Initializing fifo with %d past entries", len(app.logList))) + app.logLock.Lock() + for _, item := range app.logList { + if item.Line == 0 { + _, _ = f.WriteString(item.ColorFunc(item.Prefix+" "+item.Message) + "\n") + } else { + _, _ = f.WriteString(item.ColorFunc(langext.StrRepeat(" ", len(item.Prefix)+1)+item.Message) + "\n") + } + } + app.logLock.Unlock() + + sub := app.logBroadcaster.SubscribeByCallback("", func(msg LogMessage) { + if msg.Line == 0 { + _, _ = f.WriteString(msg.ColorFunc(msg.Prefix+" "+msg.Message) + "\n") + } else { + _, _ = f.WriteString(msg.ColorFunc(langext.StrRepeat(" ", len(msg.Prefix)+1)+msg.Message) + "\n") + } + }) + defer sub.Unsubscribe() + + app.LogDebug(fmt.Sprintf("Starting fifo log listener")) + + <-listernerStopSig + + app.LogDebug(fmt.Sprintf("Finished fifo log listener")) + app.LogLine() + + }() + + time.Sleep(100 * time.Millisecond) + + te := strings.Split(app.config.TerminalEmulator, " ")[0] + tc := strings.Split(app.config.TerminalEmulator, " ")[1:] + tc = append(tc, fmt.Sprintf("cat \"%s\"", filePath)) + //tc = append(tc, "bash") + + proc := exec.Command(te, tc...) + + app.LogDebug(fmt.Sprintf("Starting terminal-emulator '%s' [%v]", te, tc)) + err = proc.Start() + if err != nil { + app.LogError("Failed to start terminal emulator", err) + return + } + + app.LogDebug("Terminal-emulator started - waiting for exit") + app.LogLine() + + _ = proc.Wait() + + app.LogDebug("Terminal-emulator exited - stopping fifo pipe") + + listernerStopSig <- true +} diff --git a/app/notifications.go b/app/notifications.go index 9341c09..55e90dc 100644 --- a/app/notifications.go +++ b/app/notifications.go @@ -40,7 +40,7 @@ func (app *Application) showSuccessNotification(msg string, body string) { res, err := cmdext. Runner("notify-send"). - Arg("--urgency=critical"). + Arg("--urgency=normal"). Arg("--app-name=kpsync"). Arg("--print-id"). Arg(msg). diff --git a/app/sync.go b/app/sync.go index bf6bb5d..d1a173a 100644 --- a/app/sync.go +++ b/app/sync.go @@ -63,6 +63,10 @@ func (app *Application) initSync() (InitSyncResponse, error) { app.LogLine() needsDownload = false + err = app.saveState(state.ETag, state.LastModified, state.Checksum, state.Size) + if err != nil { + app.LogError("Failed to save state", err) + } } } } @@ -412,7 +416,7 @@ func (app *Application) runExplicitSync(force bool) { app.LogDebug(fmt.Sprintf("ETag (local) := %s", state.ETag)) app.LogDebug(fmt.Sprintf("ETag (remote) := %s", remoteETag)) - app.showErrorNotification("KeePassSync", "No sync necessary - file is up-to-date with remote") + app.showSuccessNotification("KeePassSync", "No sync necessary - file is up-to-date with remote") return } diff --git a/app/tray.go b/app/tray.go index 2d55855..7e45f33 100644 --- a/app/tray.go +++ b/app/tray.go @@ -21,7 +21,8 @@ func (app *Application) initTray() { miSync := systray.AddMenuItem("Sync Now (checked)", "") miSyncForce := systray.AddMenuItem("Sync Now (forced)", "") - miShowLog := systray.AddMenuItem("Show Log", "") + miShowLogFifo := systray.AddMenuItem("Show Log (fifo)", "") + miShowLogFile := systray.AddMenuItem("Show Log (file)", "") systray.AddMenuItem("", "") app.trayItemChecksum = systray.AddMenuItem("Checksum: {...}", "") app.trayItemETag = systray.AddMenuItem("ETag: {...}", "") @@ -37,18 +38,27 @@ func (app *Application) initTray() { select { case <-miSync.ClickedCh: app.LogDebug("SysTray: [Sync Now (checked)] clicked") + app.LogLine() go func() { app.runExplicitSync(false) }() case <-miSyncForce.ClickedCh: app.LogDebug("SysTray: [Sync Now (forced)] clicked") + app.LogLine() go func() { app.runExplicitSync(true) }() - case <-miShowLog.ClickedCh: - app.LogDebug("SysTray: [Show Log] clicked") - //TODO + case <-miShowLogFifo.ClickedCh: + app.LogDebug("SysTray: [Show Log Fifo] clicked") + app.LogLine() + go func() { app.openLogFifo() }() + case <-miShowLogFile.ClickedCh: + app.LogDebug("SysTray: [Show Log File] clicked") + app.LogLine() + go func() { app.openLogFile() }() case <-miQuit.ClickedCh: app.LogDebug("SysTray: [Quit] clicked") + app.LogLine() app.sigManualStopChan <- true case <-sigBGStop: app.LogDebug("SysTray: Click-Listener goroutine stopped") + app.LogLine() return } diff --git a/app/utils.go b/app/utils.go index 86b31fd..0983d78 100644 --- a/app/utils.go +++ b/app/utils.go @@ -4,11 +4,13 @@ import ( "encoding/json" "fmt" "os" + "os/exec" "strings" "time" "git.blackforestbytes.com/BlackForestBytes/goext/cryptext" "git.blackforestbytes.com/BlackForestBytes/goext/exerr" + "git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext" "github.com/shirou/gopsutil/v3/process" ) @@ -68,7 +70,7 @@ func (app *Application) saveState(eTag string, lastModified time.Time, checksum } if app.trayItemChecksum != nil { - app.trayItemChecksum.SetTitle(fmt.Sprintf("Checksum: %s", checksum)) + app.trayItemChecksum.SetTitle(fmt.Sprintf("Checksum: %s", langext.StrLimit(checksum, 16, ""))) } if app.trayItemETag != nil { app.trayItemETag.SetTitle(fmt.Sprintf("ETag: %s", eTag)) @@ -109,3 +111,8 @@ func (app *Application) isKeepassRunning() bool { return false } + +func commandExists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +}