fifo logs

This commit is contained in:
Mike Schwörer 2025-08-20 14:19:12 +02:00
parent 83d6aa10cb
commit fb5d408a01
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
8 changed files with 202 additions and 53 deletions

28
TODO.md
View File

@ -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 X - tray menu
- force sync X - force sync
- ~ current Etag X - ~ current Etag
- ~ current SHA X - ~ current SHA
- ~ last sync ts X - ~ last sync ts
- check sync X - check sync
- quit X - quit
- show log (open terminal /w log) X - show log (open terminal /w log)
- config via json + params override X - config via json + params override
- colorful log X - colorful log
- download/upload progress log X - download/upload progress log
- logfile in workdir X - logfile in workdir

View File

@ -22,7 +22,8 @@ type Application struct {
logLock sync.Mutex logLock sync.Mutex
logFile *os.File // file to write logs to, if set logFile *os.File // file to write logs to, if set
logList []dataext.Triple[string, string, func(string) string] logList []LogMessage
logBroadcaster *dataext.PubSub[string, LogMessage]
config Config config Config
@ -55,7 +56,8 @@ func NewApplication() *Application {
app := &Application{ app := &Application{
masterLock: sync.Mutex{}, masterLock: sync.Mutex{},
logLock: 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), uploadRunning: syncext.NewAtomicBool(false),
trayReady: syncext.NewAtomicBool(false), trayReady: syncext.NewAtomicBool(false),
syncLoopRunning: syncext.NewAtomicBool(false), syncLoopRunning: syncext.NewAtomicBool(false),

View File

@ -21,6 +21,7 @@ type Config struct {
WorkDir string `json:"work_dir"` WorkDir string `json:"work_dir"`
ForceColors bool `json:"force_colors"` ForceColors bool `json:"force_colors"`
TerminalEmulator string `json:"terminal_emulator"`
Debounce int `json:"debounce"` Debounce int `json:"debounce"`
} }
@ -44,12 +45,15 @@ func (app *Application) loadConfig() (Config, string) {
var workDir string var workDir string
flag.StringVar(&workDir, "work_dir", "", "Temporary working directory") flag.StringVar(&workDir, "work_dir", "", "Temporary working directory")
var debounce int
flag.IntVar(&debounce, "debounce", 0, "Debounce before sync (in seconds)")
var forceColors bool var forceColors bool
flag.BoolVar(&forceColors, "color", false, "Force color-output (default: auto-detect)") 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() flag.Parse()
if strings.HasPrefix(configPath, "~") { if strings.HasPrefix(configPath, "~") {
@ -63,6 +67,20 @@ func (app *Application) loadConfig() (Config, string) {
} }
if _, err := os.Stat(configPath); os.IsNotExist(err) && configPath != "" { 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{ _ = os.WriteFile(configPath, langext.Must(json.MarshalIndent(Config{
WebDAVURL: "https://your-nextcloud-domain.example/remote.php/dav/files/keepass.kdbx", WebDAVURL: "https://your-nextcloud-domain.example/remote.php/dav/files/keepass.kdbx",
WebDAVUser: "", WebDAVUser: "",
@ -71,6 +89,7 @@ func (app *Application) loadConfig() (Config, string) {
WorkDir: "/tmp/kpsync", WorkDir: "/tmp/kpsync",
Debounce: 3500, Debounce: 3500,
ForceColors: false, ForceColors: false,
TerminalEmulator: te,
}, "", " ")), 0644) }, "", " ")), 0644)
} }
@ -106,6 +125,9 @@ func (app *Application) loadConfig() (Config, string) {
if forceColors { if forceColors {
cfg.ForceColors = forceColors cfg.ForceColors = forceColors
} }
if terminalEmulator != "" {
cfg.TerminalEmulator = terminalEmulator
}
return cfg, configPath return cfg, configPath
} }

View File

@ -1,14 +1,26 @@
package app package app
import ( import (
"fmt"
"os"
"os/exec"
"path"
"strings" "strings"
"syscall"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/termext" "git.blackforestbytes.com/BlackForestBytes/goext/termext"
) )
type LogMessage struct {
Line int
Prefix string
Message string
ColorFunc func(string) string
}
func colDefault(v string) string { func colDefault(v string) string {
return v return v
} }
@ -58,23 +70,25 @@ func (app *Application) logInternal(pf string, msg string, c func(_ string) stri
for i, s := range strings.Split(msg, "\n") { for i, s := range strings.Split(msg, "\n") {
if i == 0 { if i == 0 {
println(c(pf + s)) println(c(pf + " " + s))
app.logList = append(app.logList, dataext.NewTriple(strings.TrimSpace(pf), s, c)) app.logList = append(app.logList, LogMessage{i, pf, s, c})
if app.logFile != nil { if app.logFile != nil {
_, err := app.logFile.WriteString(pf + s + "\n") _, err := app.logFile.WriteString(pf + " " + s + "\n")
if err != nil { if err != nil {
app.fallbackLog("[!] Failed to write logfile: " + err.Error()) app.fallbackLog("[!] Failed to write logfile: " + err.Error())
} }
} }
app.logBroadcaster.Publish("", LogMessage{i, pf, s, c})
} else { } else {
println(c(langext.StrRepeat(" ", len(pf)) + s)) println(c(langext.StrRepeat(" ", len(pf)+1) + s))
app.logList = append(app.logList, dataext.NewTriple(strings.TrimSpace(pf), s, c)) app.logList = append(app.logList, LogMessage{i, pf, s, c})
if app.logFile != nil { 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 { if err != nil {
app.fallbackLog("[!] Failed to write logfile: " + err.Error()) 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) { func (app *Application) fallbackLog(s string) {
@ -119,7 +135,7 @@ func (app *Application) writeOutStartupLogs() {
defer app.logLock.Unlock() defer app.logLock.Unlock()
for _, v := range app.logList { 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 { if err != nil {
app.fallbackLog("[!] Failed to write logfile: " + err.Error()) app.fallbackLog("[!] Failed to write logfile: " + err.Error())
} }
@ -130,3 +146,91 @@ func (app *Application) writeOutStartupLogs() {
app.fallbackLog("[!] Failed to flush logfile: " + err.Error()) 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
}

View File

@ -40,7 +40,7 @@ func (app *Application) showSuccessNotification(msg string, body string) {
res, err := cmdext. res, err := cmdext.
Runner("notify-send"). Runner("notify-send").
Arg("--urgency=critical"). Arg("--urgency=normal").
Arg("--app-name=kpsync"). Arg("--app-name=kpsync").
Arg("--print-id"). Arg("--print-id").
Arg(msg). Arg(msg).

View File

@ -63,6 +63,10 @@ func (app *Application) initSync() (InitSyncResponse, error) {
app.LogLine() app.LogLine()
needsDownload = false 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 (local) := %s", state.ETag))
app.LogDebug(fmt.Sprintf("ETag (remote) := %s", remoteETag)) 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 return
} }

View File

@ -21,7 +21,8 @@ func (app *Application) initTray() {
miSync := systray.AddMenuItem("Sync Now (checked)", "") miSync := systray.AddMenuItem("Sync Now (checked)", "")
miSyncForce := systray.AddMenuItem("Sync Now (forced)", "") 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("", "") systray.AddMenuItem("", "")
app.trayItemChecksum = systray.AddMenuItem("Checksum: {...}", "") app.trayItemChecksum = systray.AddMenuItem("Checksum: {...}", "")
app.trayItemETag = systray.AddMenuItem("ETag: {...}", "") app.trayItemETag = systray.AddMenuItem("ETag: {...}", "")
@ -37,18 +38,27 @@ func (app *Application) initTray() {
select { select {
case <-miSync.ClickedCh: case <-miSync.ClickedCh:
app.LogDebug("SysTray: [Sync Now (checked)] clicked") app.LogDebug("SysTray: [Sync Now (checked)] clicked")
app.LogLine()
go func() { app.runExplicitSync(false) }() go func() { app.runExplicitSync(false) }()
case <-miSyncForce.ClickedCh: case <-miSyncForce.ClickedCh:
app.LogDebug("SysTray: [Sync Now (forced)] clicked") app.LogDebug("SysTray: [Sync Now (forced)] clicked")
app.LogLine()
go func() { app.runExplicitSync(true) }() go func() { app.runExplicitSync(true) }()
case <-miShowLog.ClickedCh: case <-miShowLogFifo.ClickedCh:
app.LogDebug("SysTray: [Show Log] clicked") app.LogDebug("SysTray: [Show Log Fifo] clicked")
//TODO 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: case <-miQuit.ClickedCh:
app.LogDebug("SysTray: [Quit] clicked") app.LogDebug("SysTray: [Quit] clicked")
app.LogLine()
app.sigManualStopChan <- true app.sigManualStopChan <- true
case <-sigBGStop: case <-sigBGStop:
app.LogDebug("SysTray: Click-Listener goroutine stopped") app.LogDebug("SysTray: Click-Listener goroutine stopped")
app.LogLine()
return return
} }

View File

@ -4,11 +4,13 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"strings" "strings"
"time" "time"
"git.blackforestbytes.com/BlackForestBytes/goext/cryptext" "git.blackforestbytes.com/BlackForestBytes/goext/cryptext"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/timeext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"github.com/shirou/gopsutil/v3/process" "github.com/shirou/gopsutil/v3/process"
) )
@ -68,7 +70,7 @@ func (app *Application) saveState(eTag string, lastModified time.Time, checksum
} }
if app.trayItemChecksum != nil { 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 { if app.trayItemETag != nil {
app.trayItemETag.SetTitle(fmt.Sprintf("ETag: %s", eTag)) app.trayItemETag.SetTitle(fmt.Sprintf("ETag: %s", eTag))
@ -109,3 +111,8 @@ func (app *Application) isKeepassRunning() bool {
return false return false
} }
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}