a9eea8cbe7
Replace the two _ = discards in executeRun (SaveJobs + CleanupLogs after an async run) and the _ = in RunDue (SaveJobs before a scheduled run) with captured errors emitted as ErrorOccurred events after the state lock is released. The UI subscriber in mainwindow.go handles the new event by appending an "Error" record to History so failed saves are visible to the user instead of silently dropped. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
4.0 KiB
Go
101 lines
4.0 KiB
Go
package app
|
|
|
|
import "gitea.mixdep.ru/mix/gosentry/src/domain"
|
|
|
|
// Event is something the Service did to its state that observers may want to
|
|
// react to. It is a sealed interface: the concrete types in this file are the
|
|
// only implementations (enforced by the unexported isEvent marker), so a UI
|
|
// listener can exhaustively type-switch over them and the compiler will flag a
|
|
// new event type that a switch forgot to handle.
|
|
//
|
|
// Events replace the old single onChange callback. Instead of the scheduler
|
|
// reaching into the GUI, the Service emits typed events and the UI subscribes —
|
|
// the UI's listener becomes the one place that touches widgets.
|
|
type Event interface {
|
|
isEvent()
|
|
}
|
|
|
|
// JobChanged signals that a job's durable config or transient runtime changed:
|
|
// created, edited, deleted, enabled/disabled, or a status transition such as a
|
|
// run starting. Observers should re-read the affected state through the Service
|
|
// (Jobs/Runtime) rather than expect a payload snapshot — that keeps the event
|
|
// small and avoids handing out stale copies.
|
|
//
|
|
// JobID identifies the affected job. A zero JobID means a broad change (for
|
|
// example a delete, or a global pause that touched every job) and observers
|
|
// should refresh their whole view.
|
|
type JobChanged struct {
|
|
JobID int
|
|
}
|
|
|
|
// RunRecorded signals that a job run finished and produced a RunRecord. It
|
|
// carries the record by value because the record is an immutable result that
|
|
// observers append to history; there is nothing for them to re-read.
|
|
type RunRecorded struct {
|
|
Record domain.RunRecord
|
|
}
|
|
|
|
// SchedulerStateChanged signals that the global scheduler pause state flipped.
|
|
// The UI uses it to update the pause/resume control and status text.
|
|
type SchedulerStateChanged struct {
|
|
Paused bool
|
|
}
|
|
|
|
// ErrorOccurred signals a background error that could not be returned to a
|
|
// caller — typically a failed save or cleanup after an async run. The UI
|
|
// surfaces it in the History tab so the user is not silently left with
|
|
// un-persisted state.
|
|
type ErrorOccurred struct {
|
|
Err error
|
|
}
|
|
|
|
func (JobChanged) isEvent() {}
|
|
func (RunRecorded) isEvent() {}
|
|
func (SchedulerStateChanged) isEvent() {}
|
|
func (ErrorOccurred) isEvent() {}
|
|
|
|
// Observer receives events emitted by the Service. OnEvent is the single
|
|
// reaction point; the UI implements it and marshals any widget work onto the
|
|
// main thread (fyne.Do) itself — the Service knows nothing about Fyne.
|
|
type Observer interface {
|
|
OnEvent(Event)
|
|
}
|
|
|
|
// ObserverFunc adapts a plain function to the Observer interface, so callers can
|
|
// subscribe a closure without declaring a type.
|
|
type ObserverFunc func(Event)
|
|
|
|
// OnEvent calls the wrapped function.
|
|
func (f ObserverFunc) OnEvent(event Event) { f(event) }
|
|
|
|
// Subscribe registers an observer to receive every subsequently emitted event.
|
|
// Registration is expected during setup, before the scheduler starts, but is
|
|
// guarded so it is safe at any time.
|
|
func (s *Service) Subscribe(observer Observer) {
|
|
s.dispatchMu.Lock()
|
|
defer s.dispatchMu.Unlock()
|
|
s.observers = append(s.observers, observer)
|
|
}
|
|
|
|
// emit delivers an event to every registered observer.
|
|
//
|
|
// Single-threaded dispatch contract:
|
|
// - emit holds dispatchMu for the whole dispatch, so observers are never
|
|
// invoked concurrently and never overlap with each other or with Subscribe.
|
|
// Each observer sees events one at a time, in emit order.
|
|
// - emit must be called WITHOUT holding s.mu. The Service computes a state
|
|
// change under mu, releases it, then emits — so an observer is free to call
|
|
// back into read methods (Jobs/Runtime) without deadlocking on the state
|
|
// lock.
|
|
// - An observer must NOT call back into a Service method that emits (directly
|
|
// or indirectly): dispatchMu is non-reentrant, so re-entrant emission would
|
|
// deadlock. Observers react and return quickly; long or UI work is the
|
|
// observer's own responsibility to defer (e.g. fyne.Do).
|
|
func (s *Service) emit(event Event) {
|
|
s.dispatchMu.Lock()
|
|
defer s.dispatchMu.Unlock()
|
|
for _, observer := range s.observers {
|
|
observer.OnEvent(event)
|
|
}
|
|
}
|