logging and fixes

This commit is contained in:
Mike Schwörer 2025-08-20 13:29:12 +02:00
parent 52399acb42
commit 83d6aa10cb
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
7 changed files with 238 additions and 62 deletions

View File

@ -4,19 +4,26 @@ import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"path"
"sync" "sync"
"syscall" "syscall"
"time" "time"
"fyne.io/systray" "fyne.io/systray"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/syncext" "git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"git.blackforestbytes.com/BlackForestBytes/goext/termext" "git.blackforestbytes.com/BlackForestBytes/goext/termext"
"git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"mikescher.com/kpsync/assets" "mikescher.com/kpsync/assets"
) )
type Application struct { type Application struct {
masterLock sync.Mutex masterLock sync.Mutex
logLock sync.Mutex
logFile *os.File // file to write logs to, if set
logList []dataext.Triple[string, string, func(string) string]
config Config config Config
trayReady *syncext.AtomicBool trayReady *syncext.AtomicBool
@ -24,6 +31,8 @@ type Application struct {
syncLoopRunning *syncext.AtomicBool syncLoopRunning *syncext.AtomicBool
keepassRunning *syncext.AtomicBool keepassRunning *syncext.AtomicBool
fileWatcherIgnore []dataext.Tuple[time.Time, string]
sigKPExitChan chan bool // keepass exited sigKPExitChan chan bool // keepass exited
sigManualStopChan chan bool // manual stop sigManualStopChan chan bool // manual stop
sigErrChan chan error // fatal error sigErrChan chan error // fatal error
@ -34,7 +43,7 @@ type Application struct {
dbFile string dbFile string
stateFile string stateFile string
currSysTrayTooltop string currSysTrayTooltip string
trayItemChecksum *systray.MenuItem trayItemChecksum *systray.MenuItem
trayItemETag *systray.MenuItem trayItemETag *systray.MenuItem
@ -45,10 +54,13 @@ func NewApplication() *Application {
app := &Application{ app := &Application{
masterLock: sync.Mutex{}, masterLock: sync.Mutex{},
logLock: sync.Mutex{},
logList: make([]dataext.Triple[string, string, func(string) string], 0, 1024),
uploadRunning: syncext.NewAtomicBool(false), uploadRunning: syncext.NewAtomicBool(false),
trayReady: syncext.NewAtomicBool(false), trayReady: syncext.NewAtomicBool(false),
syncLoopRunning: syncext.NewAtomicBool(false), syncLoopRunning: syncext.NewAtomicBool(false),
keepassRunning: syncext.NewAtomicBool(false), keepassRunning: syncext.NewAtomicBool(false),
fileWatcherIgnore: make([]dataext.Tuple[time.Time, string], 0, 128),
sigKPExitChan: make(chan bool, 128), sigKPExitChan: make(chan bool, 128),
sigManualStopChan: make(chan bool, 128), sigManualStopChan: make(chan bool, 128),
sigErrChan: make(chan error, 128), sigErrChan: make(chan error, 128),
@ -56,7 +68,9 @@ func NewApplication() *Application {
sigTermKeepassChan: make(chan bool, 128), sigTermKeepassChan: make(chan bool, 128),
} }
app.LogInfo("Starting kpsync...") app.LogInfo(fmt.Sprintf("Starting kpsync {%s} ...", time.Now().In(timeext.TimezoneBerlin).Format(time.RFC3339)))
app.LogLine()
app.LogDebug(fmt.Sprintf("SupportsColors := %v", termext.SupportsColors())) app.LogDebug(fmt.Sprintf("SupportsColors := %v", termext.SupportsColors()))
app.LogLine() app.LogLine()
@ -65,6 +79,7 @@ func NewApplication() *Application {
func (app *Application) Run() { func (app *Application) Run() {
var configPath string var configPath string
var err error
app.config, configPath = app.loadConfig() app.config, configPath = app.loadConfig()
@ -78,6 +93,17 @@ func (app *Application) Run() {
app.LogDebug(fmt.Sprintf("ForceColors := %v", app.config.ForceColors)) app.LogDebug(fmt.Sprintf("ForceColors := %v", app.config.ForceColors))
app.LogLine() app.LogLine()
app.logFile, err = os.OpenFile(path.Join(app.config.WorkDir, "kpsync.log"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
app.LogFatalErr("Failed to open log file", err)
}
defer func() {
if err := app.logFile.Close(); err != nil {
app.fallbackLog("Failed to close log file: " + err.Error())
}
}()
app.writeOutStartupLogs()
go func() { app.initTray() }() go func() { app.initTray() }()
go func() { go func() {
@ -106,7 +132,7 @@ func (app *Application) Run() {
app.setTrayStateDirect("Sleeping...", assets.IconDefault) app.setTrayStateDirect("Sleeping...", assets.IconDefault)
err = app.runSyncLoop() err = app.runSyncWatcher()
if err != nil { if err != nil {
app.sigErrChan <- err app.sigErrChan <- err
return return

View File

@ -3,6 +3,7 @@ package app
import ( import (
"strings" "strings"
"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"
@ -20,11 +21,11 @@ func (app *Application) LogFatal(msg string) {
func (app *Application) LogFatalErr(msg string, err error) { func (app *Application) LogFatalErr(msg string, err error) {
if err != nil { 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)
panic(0)
} else { } else {
app.logInternal("[F] ", msg, termext.Red) app.logInternal("[F] ", msg, termext.Red)
panic(0)
} }
panic(0)
} }
func (app *Application) LogError(msg string, err error) { func (app *Application) LogError(msg string, err error) {
@ -48,6 +49,9 @@ func (app *Application) LogDebug(msg string) {
} }
func (app *Application) logInternal(pf string, msg string, c func(_ string) string) { func (app *Application) logInternal(pf string, msg string, c func(_ string) string) {
app.logLock.Lock()
defer app.logLock.Unlock()
if !termext.SupportsColors() && !app.config.ForceColors { if !termext.SupportsColors() && !app.config.ForceColors {
c = func(s string) string { return s } c = func(s string) string { return s }
} }
@ -55,12 +59,74 @@ 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))
if app.logFile != nil {
_, err := app.logFile.WriteString(pf + s + "\n")
if err != nil {
app.fallbackLog("[!] Failed to write logfile: " + err.Error())
}
}
} else { } else {
println(c(langext.StrRepeat(" ", len(pf)) + s)) println(c(langext.StrRepeat(" ", len(pf)) + s))
app.logList = append(app.logList, dataext.NewTriple(strings.TrimSpace(pf), s, c))
if app.logFile != nil {
_, err := app.logFile.WriteString(langext.StrRepeat(" ", len(pf)) + s + "\n")
if err != nil {
app.fallbackLog("[!] Failed to write logfile: " + err.Error())
}
}
}
}
if app.logFile != nil {
err := app.logFile.Sync()
if err != nil {
app.fallbackLog("[!] Failed to flush logfile: " + err.Error())
} }
} }
} }
func (app *Application) LogLine() { func (app *Application) LogLine() {
app.logLock.Lock()
defer app.logLock.Unlock()
println() println()
if app.logFile != nil {
_, err := app.logFile.WriteString("\n")
if err != nil {
app.fallbackLog("[!] Failed to write logfile: " + err.Error())
}
err = app.logFile.Sync()
if err != nil {
app.fallbackLog("[!] Failed to flush logfile: " + err.Error())
}
}
app.logList = append(app.logList, dataext.NewTriple("", "", func(v string) string { return v }))
}
func (app *Application) fallbackLog(s string) {
if termext.SupportsColors() || app.config.ForceColors {
s = termext.Red(s)
}
println(s)
}
func (app *Application) writeOutStartupLogs() {
app.logLock.Lock()
defer app.logLock.Unlock()
for _, v := range app.logList {
_, err := app.logFile.WriteString(v.V1 + " " + v.V2 + "\n")
if err != nil {
app.fallbackLog("[!] Failed to write logfile: " + err.Error())
}
}
err := app.logFile.Sync()
if err != nil {
app.fallbackLog("[!] Failed to flush logfile: " + err.Error())
}
} }

View File

@ -67,6 +67,8 @@ func (app *Application) showChoiceNotification(msg string, body string, options
bldr := cmdext. bldr := cmdext.
Runner("notify-send"). Runner("notify-send").
Arg("--urgency=critical").
Arg("--expire-time=0").
Arg("--wait"). Arg("--wait").
Arg("--app-name=kpsync") Arg("--app-name=kpsync")

View File

@ -12,7 +12,6 @@ import (
"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/timeext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"github.com/fsnotify/fsnotify"
"mikescher.com/kpsync/assets" "mikescher.com/kpsync/assets"
) )
@ -49,7 +48,7 @@ func (app *Application) initSync() (InitSyncResponse, error) {
if err != nil { if err != nil {
app.LogError("Failed to calculate local database checksum", err) app.LogError("Failed to calculate local database checksum", err)
} else if localCS == state.Checksum { } else if localCS == state.Checksum {
remoteETag, remoteLM, err := app.getRemoteETag() remoteETag, remoteLM, err := app.getRemoteState()
if err != nil { if err != nil {
app.LogError("Failed to get remote ETag", err) app.LogError("Failed to get remote ETag", err)
} else if remoteETag == state.ETag { } else if remoteETag == state.ETag {
@ -196,47 +195,6 @@ func (app *Application) runKeepass(fallback bool) {
} }
func (app *Application) runSyncLoop() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return exerr.Wrap(err, "failed to init file-watcher").Build()
}
defer func() { _ = watcher.Close() }()
err = watcher.Add(app.config.WorkDir)
if err != nil {
return exerr.Wrap(err, "").Build()
}
for {
select {
case <-app.sigSyncLoopStopChan:
app.LogInfo("Stopping sync loop (received signal)")
return nil
case event := <-watcher.Events:
app.LogDebug(fmt.Sprintf("Received inotify event: [%s] %s", event.Op.String(), event.Name))
if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
app.LogDebug("Ignoring event - not a write|create event")
app.LogLine()
continue
}
if event.Name != app.dbFile {
app.LogDebug(fmt.Sprintf("Ignoring event - not the database file (%s)", app.dbFile))
app.LogLine()
continue
}
app.onDBFileChanged()
case err := <-watcher.Errors:
app.LogError("Filewatcher reported an error", err)
}
}
}
func (app *Application) onDBFileChanged() { func (app *Application) onDBFileChanged() {
app.masterLock.Lock() app.masterLock.Lock()
app.uploadRunning.Wait(false) app.uploadRunning.Wait(false)
@ -396,6 +354,11 @@ func (app *Application) runFinalSync() {
app.LogInfo("Starting final sync...") app.LogInfo("Starting final sync...")
remoteETag, _, err := app.getRemoteState()
if err != nil {
app.LogError("Failed to get remote ETag", err)
}
state := app.readState() state := app.readState()
localCS, err := app.calcLocalChecksum() localCS, err := app.calcLocalChecksum()
if err != nil { if err != nil {
@ -404,12 +367,56 @@ func (app *Application) runFinalSync() {
return return
} }
if state != nil && localCS == state.Checksum { if state != nil && localCS == state.Checksum && remoteETag == state.ETag {
app.LogInfo("Local database still matches remote (via checksum) - no need to upload") app.LogInfo("Local database still matches remote (via checksum+etag) - no need to upload")
app.LogInfo(fmt.Sprintf("Checksum (remote/cached) := %s", state.Checksum)) app.LogInfo(fmt.Sprintf("Checksum (remote/cached) := %s", state.Checksum))
app.LogInfo(fmt.Sprintf("Checksum (local) := %s", localCS)) app.LogInfo(fmt.Sprintf("Checksum (local) := %s", localCS))
app.LogDebug(fmt.Sprintf("ETag (local) := %s", state.ETag))
app.LogDebug(fmt.Sprintf("ETag (remote) := %s", remoteETag))
return return
} }
app.doDBUpload(state, fin1, false) app.doDBUpload(state, fin1, false)
} }
func (app *Application) runExplicitSync(force bool) {
app.masterLock.Lock()
app.uploadRunning.Wait(false)
app.uploadRunning.Set(true)
app.masterLock.Unlock()
defer app.uploadRunning.Set(false)
state := app.readState()
if !force {
remoteETag, _, err := app.getRemoteState()
if err != nil {
app.LogError("Failed to get remote ETag", err)
app.showErrorNotification("KeePassSync: Error", "Failed to get status from remote")
return
}
localCS, err := app.calcLocalChecksum()
if err != nil {
app.LogError("Failed to calculate local database checksum", err)
app.showErrorNotification("KeePassSync: Error", "Failed to calculate local database checksum")
return
}
if state != nil && localCS == state.Checksum {
app.LogInfo("Local database still matches remote (via checksum+etag) - no need to upload")
app.LogInfo(fmt.Sprintf("Checksum (remote/cached) := %s", state.Checksum))
app.LogInfo(fmt.Sprintf("Checksum (local) := %s", localCS))
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")
return
}
}
app.doDBUpload(state, func() {}, true)
}

View File

@ -16,8 +16,8 @@ func (app *Application) initTray() {
systray.SetIcon(assets.IconInit) systray.SetIcon(assets.IconInit)
systray.SetTitle("KeepassXC Sync") systray.SetTitle("KeepassXC Sync")
app.currSysTrayTooltop = "Initializing..." app.currSysTrayTooltip = "Initializing..."
systray.SetTooltip(app.currSysTrayTooltop) systray.SetTooltip(app.currSysTrayTooltip)
miSync := systray.AddMenuItem("Sync Now (checked)", "") miSync := systray.AddMenuItem("Sync Now (checked)", "")
miSyncForce := systray.AddMenuItem("Sync Now (forced)", "") miSyncForce := systray.AddMenuItem("Sync Now (forced)", "")
@ -37,10 +37,10 @@ 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")
//TODO 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")
//TODO go func() { app.runExplicitSync(true) }()
case <-miShowLog.ClickedCh: case <-miShowLog.ClickedCh:
app.LogDebug("SysTray: [Show Log] clicked") app.LogDebug("SysTray: [Show Log] clicked")
//TODO //TODO
@ -94,8 +94,8 @@ func (app *Application) setTrayState(txt string, icon []byte) func() {
} }
systray.SetIcon(assets.IconDefault) systray.SetIcon(assets.IconDefault)
app.currSysTrayTooltop = "Sleeping..." app.currSysTrayTooltip = "Sleeping..."
systray.SetTooltip(app.currSysTrayTooltop) systray.SetTooltip(app.currSysTrayTooltip)
finDone = true finDone = true
} }
@ -124,6 +124,6 @@ func (app *Application) setTrayTooltip(txt string) {
defer app.masterLock.Unlock() defer app.masterLock.Unlock()
systray.SetTooltip(txt) systray.SetTooltip(txt)
app.currSysTrayTooltop = txt app.currSysTrayTooltip = txt
systray.SetTooltip(app.currSysTrayTooltop) systray.SetTooltip(app.currSysTrayTooltip)
} }

70
app/watcher.go Normal file
View File

@ -0,0 +1,70 @@
package app
import (
"fmt"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"github.com/fsnotify/fsnotify"
)
func (app *Application) runSyncWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return exerr.Wrap(err, "failed to init file-watcher").Build()
}
defer func() { _ = watcher.Close() }()
err = watcher.Add(app.config.WorkDir)
if err != nil {
return exerr.Wrap(err, "").Build()
}
for {
select {
case <-app.sigSyncLoopStopChan:
app.LogInfo("Stopping sync loop (received signal)")
return nil
case event := <-watcher.Events:
if event.Name != app.dbFile {
continue // no log!! otherwise we end in an endless log-loop
}
if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
continue // no log!! otherwise we end in an endless log-loop
}
app.LogDebug(fmt.Sprintf("Received inotify event: [%s] %s", event.Op.String(), event.Name))
localCS, err := app.calcLocalChecksum()
if err != nil {
app.LogError("Failed to calculate local database checksum", err)
app.showErrorNotification("KeePassSync: Error", "Failed to calculate checksum")
continue
}
ignoreFN := func() bool {
app.masterLock.Lock()
defer app.masterLock.Unlock()
for _, ign := range app.fileWatcherIgnore {
if ign.V2 == localCS && time.Since(ign.V1) < 10*time.Second {
return true
}
}
return false
}
if ignoreFN() {
app.LogDebug("Ignoring file-change - event is explicitly ignored")
continue
}
app.onDBFileChanged()
case err := <-watcher.Errors:
app.LogError("Filewatcher reported an error", err)
}
}
}

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"git.blackforestbytes.com/BlackForestBytes/goext/cryptext" "git.blackforestbytes.com/BlackForestBytes/goext/cryptext"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/timeext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
) )
@ -18,7 +19,7 @@ var ETagConflictError = errors.New("ETag conflict")
func (app *Application) downloadDatabase() (string, time.Time, string, int64, error) { func (app *Application) downloadDatabase() (string, time.Time, string, int64, error) {
prevTT := app.currSysTrayTooltop prevTT := app.currSysTrayTooltip
defer app.setTrayTooltip(prevTT) defer app.setTrayTooltip(prevTT)
client := http.Client{Timeout: 90 * time.Second} client := http.Client{Timeout: 90 * time.Second}
@ -67,6 +68,10 @@ func (app *Application) downloadDatabase() (string, time.Time, string, int64, er
sz := int64(len(bin)) sz := int64(len(bin))
app.masterLock.Lock()
app.fileWatcherIgnore = append(app.fileWatcherIgnore, dataext.NewTuple(time.Now(), sha))
app.masterLock.Unlock()
err = os.WriteFile(app.dbFile, bin, 0644) err = os.WriteFile(app.dbFile, bin, 0644)
if err != nil { if err != nil {
return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to write database file").Build() return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to write database file").Build()
@ -75,7 +80,7 @@ func (app *Application) downloadDatabase() (string, time.Time, string, int64, er
return etag, lm, sha, sz, nil return etag, lm, sha, sz, nil
} }
func (app *Application) getRemoteETag() (string, time.Time, error) { func (app *Application) getRemoteState() (string, time.Time, error) {
client := http.Client{Timeout: 90 * time.Second} client := http.Client{Timeout: 90 * time.Second}
req, err := http.NewRequest("HEAD", app.config.WebDAVURL, nil) req, err := http.NewRequest("HEAD", app.config.WebDAVURL, nil)
@ -109,7 +114,7 @@ func (app *Application) getRemoteETag() (string, time.Time, error) {
func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time, string, int64, error) { func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time, string, int64, error) {
prevTT := app.currSysTrayTooltop prevTT := app.currSysTrayTooltip
defer app.setTrayTooltip(prevTT) defer app.setTrayTooltip(prevTT)
client := http.Client{Timeout: 90 * time.Second} client := http.Client{Timeout: 90 * time.Second}
@ -176,7 +181,7 @@ func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time,
func (app *Application) parseHeader(resp *http.Response) (string, time.Time, error) { func (app *Application) parseHeader(resp *http.Response) (string, time.Time, error) {
var err error var err error
etag := resp.Header.Get("ETag") etag := resp.Header.Get("ETag")
if etag == "" { if etag == "" {
return "", time.Time{}, exerr.New(exerr.TypeInternal, "ETag header is missing").Build() return "", time.Time{}, exerr.New(exerr.TypeInternal, "ETag header is missing").Build()