Phase 2b: server/client timeouts, atomic state writes, log mutex, panic and shutdown fixes

- http.Server with ReadHeaderTimeout/IdleTimeout (no read/write timeouts: /stream/ is long-lived)
- shared outbound clients: providerHTTPClient (5m), apiHTTPClient (30s), imgcache client (30s)
- imgcache: download outside the lock, per-item helper, cache URL uses the file name not the fs path
- writeFileAtomic (temp + fsync + rename) for settings/xepg/pms/urls/authentication JSON
- one package-level logMu for WebScreenLog and notifications; ring buffer keeps the newest lines
- notifications evict the oldest instead of random map entries
- xepg XMLTV file removal rebuilt after the loop; data.go range-mutation removed
- API handler returns after error body; WS request/response fresh per command
- checked type assertions in data/backup/provider/screen
- SIGINT/SIGTERM handled in main via src.Shutdown(); fatal paths exit 1
This commit is contained in:
2026-09-26 13:17:07 +10:00
parent 36303fecea
commit 3bbea8e952
17 changed files with 759 additions and 170 deletions
+144 -39
View File
@@ -10,6 +10,82 @@ import (
"time"
)
// logMu guards WebScreenLog (its slice and counters) and System.Notification.
// Log lines are appended from every goroutine (buffer, maintenance, web
// handlers) and read by the websocket handler.
var logMu sync.Mutex
// logAppend : Appends a timestamped line to the in-memory log and trims it
// to Settings.LogEntriesRAM.
func logAppend(logMsg string) {
logMu.Lock()
defer logMu.Unlock()
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
logCleanUpLocked()
}
// logAppendCounted : Appends a warning / error line and bumps the matching
// counter. Like the original code this does not trim the log; the next
// info / debug line does.
func logAppendCounted(logMsg string, isError bool) {
logMu.Lock()
defer logMu.Unlock()
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
if isError {
WebScreenLog.Errors++
} else {
WebScreenLog.Warnings++
}
}
// resetWebScreenLog : Clears the in-memory log and its counters.
func resetWebScreenLog() {
logMu.Lock()
defer logMu.Unlock()
WebScreenLog.Log = make([]string, 0)
WebScreenLog.Errors = 0
WebScreenLog.Warnings = 0
}
// webScreenLogSnapshot : Copy of the log for the web client. The slice is
// copied so the JSON encoder never races with a concurrent append.
func webScreenLogSnapshot() (snapshot WebScreenLogStruct) {
logMu.Lock()
defer logMu.Unlock()
snapshot = WebScreenLog
snapshot.Log = make([]string, len(WebScreenLog.Log))
copy(snapshot.Log, WebScreenLog.Log)
return
}
// notificationsSnapshot : Copy of the notifications map for the web client.
func notificationsSnapshot() (snapshot map[string]Notification) {
logMu.Lock()
defer logMu.Unlock()
if System.Notification == nil {
return nil
}
snapshot = make(map[string]Notification, len(System.Notification))
for k, v := range System.Notification {
snapshot[k] = v
}
return
}
func showInfo(str string) {
if System.Flag.Info {
@@ -33,9 +109,7 @@ func showInfo(str string) {
printLogOnScreen(logMsg, "info")
logMsg = strings.Replace(logMsg, " ", " ", -1)
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
logCleanUp()
logAppend(strings.Replace(logMsg, " ", " ", -1))
}
@@ -51,7 +125,6 @@ func showDebug(str string, level int) {
var msg = strings.SplitN(str, ":", 2)
var length = len(msg[0])
var space string
var mutex = sync.RWMutex{}
if len(msg) == 2 {
@@ -64,11 +137,7 @@ func showDebug(str string, level int) {
printLogOnScreen(logMsg, "debug")
mutex.Lock()
logMsg = strings.Replace(logMsg, " ", " ", -1)
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
logCleanUp()
mutex.Unlock()
logAppend(strings.Replace(logMsg, " ", " ", -1))
}
@@ -99,7 +168,8 @@ func showHighlight(str string) {
}
notification.Type = "info"
notification.Message = msg[1]
// Messages without a "key:value" prefix are shown as they are.
notification.Message = msg[len(msg)-1]
addNotification(notification)
@@ -109,31 +179,22 @@ func showWarning(errCode int) {
var errMsg = getErrMsg(errCode)
var logMsg = fmt.Sprintf("[%s] [WARNING] %s", System.Name, errMsg)
var mutex = sync.RWMutex{}
printLogOnScreen(logMsg, "warning")
mutex.Lock()
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
WebScreenLog.Warnings++
mutex.Unlock()
logAppendCounted(logMsg, false)
}
// ShowError : Zeigt die Fehlermeldungen in der Konsole
func ShowError(err error, errCode int) {
var mutex = sync.RWMutex{}
var errMsg = getErrMsg(errCode)
var logMsg = fmt.Sprintf("[%s] [ERROR] %s (%s) - EC: %d", System.Name, err, errMsg, errCode)
printLogOnScreen(logMsg, "error")
mutex.Lock()
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
WebScreenLog.Errors++
mutex.Unlock()
logAppendCounted(logMsg, true)
}
@@ -174,24 +235,17 @@ func printLogOnScreen(logMsg string, logType string) {
}
func logCleanUp() {
// logCleanUpLocked : Trims the log to the newest Settings.LogEntriesRAM
// lines and recounts warnings / errors. The caller must hold logMu.
func logCleanUpLocked() {
WebScreenLog.Log = keepLastEntries(WebScreenLog.Log, Settings.LogEntriesRAM)
var logEntriesRAM = Settings.LogEntriesRAM
var logs = WebScreenLog.Log
WebScreenLog.Warnings = 0
WebScreenLog.Errors = 0
if len(logs) > logEntriesRAM {
var tmp = make([]string, 0)
for i := len(logs) - logEntriesRAM; i < logEntriesRAM; i++ {
tmp = append(tmp, logs[i])
}
logs = tmp
}
for _, log := range logs {
if strings.Contains(log, "WARNING") {
@@ -204,8 +258,25 @@ func logCleanUp() {
}
WebScreenLog.Log = logs
}
// keepLastEntries : Returns the last max entries of logs (a copy, so the
// old backing array does not keep growing). A max <= 0 keeps nothing, which
// matches the original behaviour for an unset LogEntriesRAM.
func keepLastEntries(logs []string, max int) []string {
if max < 0 {
max = 0
}
if len(logs) <= max {
return logs
}
var tmp = make([]string, max)
copy(tmp, logs[len(logs)-max:])
return tmp
}
// Fehlercodes
@@ -386,30 +457,64 @@ func getErrMsg(errCode int) (errMsg string) {
return errMsg
}
// maxNotifications : Number of notifications kept for the web interface.
const maxNotifications = 10
func addNotification(notification Notification) (err error) {
var i int
var t = time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
notification.Time = strconv.FormatInt(t, 10)
insertNotification(notification)
return
}
// insertNotification : Stores the notification under its Time key and evicts
// the oldest ones once more than maxNotifications are kept.
func insertNotification(notification Notification) {
notification.New = true
if len(notification.Headline) == 0 {
notification.Headline = strings.ToUpper(notification.Type)
}
logMu.Lock()
defer logMu.Unlock()
if len(System.Notification) == 0 {
System.Notification = make(map[string]Notification)
}
System.Notification[notification.Time] = notification
for key := range System.Notification {
for len(System.Notification) > maxNotifications {
delete(System.Notification, oldestNotificationKey(System.Notification))
}
if i < len(System.Notification)-10 {
delete(System.Notification, key)
}
// oldestNotificationKey : Key of the notification with the smallest
// timestamp. Keys are millisecond timestamps as decimal strings; anything
// that does not parse sorts before everything that does.
func oldestNotificationKey(notifications map[string]Notification) (oldest string) {
var oldestTime int64
var found bool
for key, n := range notifications {
t, err := strconv.ParseInt(n.Time, 10, 64)
if err != nil {
return key
}
i++
if !found || t < oldestTime {
oldest = key
oldestTime = t
found = true
}
}