mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
* Move portbase into monorepo * Add new simple module mgr * [WIP] Switch to new simple module mgr * Add StateMgr and more worker variants * [WIP] Switch more modules * [WIP] Switch more modules * [WIP] swtich more modules * [WIP] switch all SPN modules * [WIP] switch all service modules * [WIP] Convert all workers to the new module system * [WIP] add new task system to module manager * [WIP] Add second take for scheduling workers * [WIP] Add FIXME for bugs in new scheduler * [WIP] Add minor improvements to scheduler * [WIP] Add new worker scheduler * [WIP] Fix more bug related to new module system * [WIP] Fix start handing of the new module system * [WIP] Improve startup process * [WIP] Fix minor issues * [WIP] Fix missing subsystem in settings * [WIP] Initialize managers in constructor * [WIP] Move module event initialization to constrictors * [WIP] Fix setting for enabling and disabling the SPN module * [WIP] Move API registeration into module construction * [WIP] Update states mgr for all modules * [WIP] Add CmdLine operation support * Add state helper methods to module group and instance * Add notification and module status handling to status package * Fix starting issues * Remove pilot widget and update security lock to new status data * Remove debug logs * Improve http server shutdown * Add workaround for cleanly shutting down firewall+netquery * Improve logging * Add syncing states with notifications for new module system * Improve starting, stopping, shutdown; resolve FIXMEs/TODOs * [WIP] Fix most unit tests * Review new module system and fix minor issues * Push shutdown and restart events again via API * Set sleep mode via interface * Update example/template module * [WIP] Fix spn/cabin unit test * Remove deprecated UI elements * Make log output more similar for the logging transition phase * Switch spn hub and observer cmds to new module system * Fix log sources * Make worker mgr less error prone * Fix tests and minor issues * Fix observation hub * Improve shutdown and restart handling * Split up big connection.go source file * Move varint and dsd packages to structures repo * Improve expansion test * Fix linter warnings * Fix interception module on windows * Fix linter errors --------- Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
351 lines
9.1 KiB
Go
351 lines
9.1 KiB
Go
package mgr
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// workerContextKey is a key used for the context key/value storage.
|
|
type workerContextKey struct{}
|
|
|
|
// WorkerCtxContextKey is the key used to add the WorkerCtx to a context.
|
|
var WorkerCtxContextKey = workerContextKey{}
|
|
|
|
// WorkerCtx provides workers with the necessary environment for flow control
|
|
// and logging.
|
|
type WorkerCtx struct {
|
|
ctx context.Context
|
|
cancelCtx context.CancelFunc
|
|
|
|
workerMgr *WorkerMgr // TODO: Attach to context instead?
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// AddToCtx adds the WorkerCtx to the given context.
|
|
func (w *WorkerCtx) AddToCtx(ctx context.Context) context.Context {
|
|
return context.WithValue(ctx, WorkerCtxContextKey, w)
|
|
}
|
|
|
|
// WorkerFromCtx returns the WorkerCtx from the given context.
|
|
func WorkerFromCtx(ctx context.Context) *WorkerCtx {
|
|
v := ctx.Value(WorkerCtxContextKey)
|
|
if w, ok := v.(*WorkerCtx); ok {
|
|
return w
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Ctx returns the worker context.
|
|
// Is automatically canceled after the worker stops/returns, regardless of error.
|
|
func (w *WorkerCtx) Ctx() context.Context {
|
|
return w.ctx
|
|
}
|
|
|
|
// Cancel cancels the worker context.
|
|
// Is automatically called after the worker stops/returns, regardless of error.
|
|
func (w *WorkerCtx) Cancel() {
|
|
w.cancelCtx()
|
|
}
|
|
|
|
// WorkerMgr returns the worker manager the worker was started from.
|
|
// Returns nil if the worker is not associated with a scheduler.
|
|
func (w *WorkerCtx) WorkerMgr() *WorkerMgr {
|
|
return w.workerMgr
|
|
}
|
|
|
|
// Done returns the context Done channel.
|
|
func (w *WorkerCtx) Done() <-chan struct{} {
|
|
return w.ctx.Done()
|
|
}
|
|
|
|
// IsDone checks whether the worker context is done.
|
|
func (w *WorkerCtx) IsDone() bool {
|
|
return w.ctx.Err() != nil
|
|
}
|
|
|
|
// Logger returns the logger used by the worker context.
|
|
func (w *WorkerCtx) Logger() *slog.Logger {
|
|
return w.logger
|
|
}
|
|
|
|
// LogEnabled reports whether the logger emits log records at the given level.
|
|
// The worker context is automatically supplied.
|
|
func (w *WorkerCtx) LogEnabled(level slog.Level) bool {
|
|
return w.logger.Enabled(w.ctx, level)
|
|
}
|
|
|
|
// Debug logs at LevelDebug.
|
|
// The worker context is automatically supplied.
|
|
func (w *WorkerCtx) Debug(msg string, args ...any) {
|
|
if !w.logger.Enabled(w.ctx, slog.LevelDebug) {
|
|
return
|
|
}
|
|
w.writeLog(slog.LevelDebug, msg, args...)
|
|
}
|
|
|
|
// Info logs at LevelInfo.
|
|
// The worker context is automatically supplied.
|
|
func (w *WorkerCtx) Info(msg string, args ...any) {
|
|
if !w.logger.Enabled(w.ctx, slog.LevelInfo) {
|
|
return
|
|
}
|
|
w.writeLog(slog.LevelInfo, msg, args...)
|
|
}
|
|
|
|
// Warn logs at LevelWarn.
|
|
// The worker context is automatically supplied.
|
|
func (w *WorkerCtx) Warn(msg string, args ...any) {
|
|
if !w.logger.Enabled(w.ctx, slog.LevelWarn) {
|
|
return
|
|
}
|
|
w.writeLog(slog.LevelWarn, msg, args...)
|
|
}
|
|
|
|
// Error logs at LevelError.
|
|
// The worker context is automatically supplied.
|
|
func (w *WorkerCtx) Error(msg string, args ...any) {
|
|
if !w.logger.Enabled(w.ctx, slog.LevelError) {
|
|
return
|
|
}
|
|
w.writeLog(slog.LevelError, msg, args...)
|
|
}
|
|
|
|
// Log emits a log record with the current time and the given level and message.
|
|
// The worker context is automatically supplied.
|
|
func (w *WorkerCtx) Log(level slog.Level, msg string, args ...any) {
|
|
if !w.logger.Enabled(w.ctx, level) {
|
|
return
|
|
}
|
|
w.writeLog(level, msg, args...)
|
|
}
|
|
|
|
// LogAttrs is a more efficient version of Log() that accepts only Attrs.
|
|
// The worker context is automatically supplied.
|
|
func (w *WorkerCtx) LogAttrs(level slog.Level, msg string, attrs ...slog.Attr) {
|
|
if !w.logger.Enabled(w.ctx, level) {
|
|
return
|
|
}
|
|
|
|
var pcs [1]uintptr
|
|
runtime.Callers(2, pcs[:]) // skip "Callers" and "LogAttrs".
|
|
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
|
r.AddAttrs(attrs...)
|
|
_ = w.logger.Handler().Handle(w.ctx, r)
|
|
}
|
|
|
|
func (w *WorkerCtx) writeLog(level slog.Level, msg string, args ...any) {
|
|
var pcs [1]uintptr
|
|
runtime.Callers(3, pcs[:]) // skip "Callers", "writeLog" and the calling function.
|
|
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
|
r.Add(args...)
|
|
_ = w.logger.Handler().Handle(w.ctx, r)
|
|
}
|
|
|
|
// Go starts the given function in a goroutine (as a "worker").
|
|
// The worker context has
|
|
// - A separate context which is canceled when the functions returns.
|
|
// - Access to named structure logging.
|
|
// - Given function is re-run after failure (with backoff).
|
|
// - Panic catching.
|
|
// - Flow control helpers.
|
|
func (m *Manager) Go(name string, fn func(w *WorkerCtx) error) {
|
|
// m.logger.Log(m.ctx, slog.LevelInfo, "worker started", "name", name)
|
|
go m.manageWorker(name, fn)
|
|
}
|
|
|
|
func (m *Manager) manageWorker(name string, fn func(w *WorkerCtx) error) {
|
|
m.workerStart()
|
|
defer m.workerDone()
|
|
|
|
w := &WorkerCtx{
|
|
logger: m.logger.With("worker", name),
|
|
}
|
|
w.ctx = m.ctx
|
|
|
|
backoff := time.Second
|
|
failCnt := 0
|
|
|
|
for {
|
|
panicInfo, err := m.runWorker(w, fn)
|
|
switch {
|
|
case err == nil:
|
|
// No error means that the worker is finished.
|
|
return
|
|
|
|
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
|
|
// A canceled context or exceeded deadline also means that the worker is finished.
|
|
return
|
|
|
|
default:
|
|
// Any other errors triggers a restart with backoff.
|
|
|
|
// If manager is stopping, just log error and return.
|
|
if m.IsDone() {
|
|
if panicInfo != "" {
|
|
w.Error(
|
|
"worker failed",
|
|
"err", err,
|
|
"file", panicInfo,
|
|
)
|
|
} else {
|
|
w.Error(
|
|
"worker failed",
|
|
"err", err,
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Count failure and increase backoff (up to limit),
|
|
failCnt++
|
|
backoff *= 2
|
|
if backoff > time.Minute {
|
|
backoff = time.Minute
|
|
}
|
|
|
|
// Log error and retry after backoff duration.
|
|
if panicInfo != "" {
|
|
w.Error(
|
|
"worker failed",
|
|
"failCnt", failCnt,
|
|
"backoff", backoff,
|
|
"err", err,
|
|
"file", panicInfo,
|
|
)
|
|
} else {
|
|
w.Error(
|
|
"worker failed",
|
|
"failCnt", failCnt,
|
|
"backoff", backoff,
|
|
"err", err,
|
|
)
|
|
}
|
|
select {
|
|
case <-time.After(backoff):
|
|
case <-m.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Do directly executes the given function (as a "worker").
|
|
// The worker context has
|
|
// - A separate context which is canceled when the functions returns.
|
|
// - Access to named structure logging.
|
|
// - Given function is re-run after failure (with backoff).
|
|
// - Panic catching.
|
|
// - Flow control helpers.
|
|
func (m *Manager) Do(name string, fn func(w *WorkerCtx) error) error {
|
|
m.workerStart()
|
|
defer m.workerDone()
|
|
|
|
// Create context.
|
|
w := &WorkerCtx{
|
|
ctx: m.Ctx(),
|
|
logger: m.logger.With("worker", name),
|
|
}
|
|
|
|
// Run worker.
|
|
panicInfo, err := m.runWorker(w, fn)
|
|
switch {
|
|
case err == nil:
|
|
// No error means that the worker is finished.
|
|
return nil
|
|
|
|
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
|
|
// A canceled context or exceeded deadline also means that the worker is finished.
|
|
return err
|
|
|
|
default:
|
|
// Log error and return.
|
|
if panicInfo != "" {
|
|
w.Error(
|
|
"worker failed",
|
|
"err", err,
|
|
"file", panicInfo,
|
|
)
|
|
} else {
|
|
w.Error(
|
|
"worker failed",
|
|
"err", err,
|
|
)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (m *Manager) runWorker(w *WorkerCtx, fn func(w *WorkerCtx) error) (panicInfo string, err error) {
|
|
// Create worker context that is canceled when worker finished or dies.
|
|
w.ctx, w.cancelCtx = context.WithCancel(w.ctx)
|
|
defer w.Cancel()
|
|
|
|
// Recover from panic.
|
|
defer func() {
|
|
panicVal := recover()
|
|
if panicVal != nil {
|
|
err = fmt.Errorf("panic: %s", panicVal)
|
|
|
|
// Print panic to stderr.
|
|
stackTrace := string(debug.Stack())
|
|
fmt.Fprintf(
|
|
os.Stderr,
|
|
"===== PANIC =====\n%s\n\n%s===== END =====\n",
|
|
panicVal,
|
|
stackTrace,
|
|
)
|
|
|
|
// Find the line in the stack trace that refers to where the panic occurred.
|
|
stackLines := strings.Split(stackTrace, "\n")
|
|
foundPanic := false
|
|
for i, line := range stackLines {
|
|
if !foundPanic {
|
|
if strings.Contains(line, "panic(") {
|
|
foundPanic = true
|
|
}
|
|
} else {
|
|
if strings.Contains(line, "portmaster") {
|
|
if i+1 < len(stackLines) {
|
|
panicInfo = strings.SplitN(strings.TrimSpace(stackLines[i+1]), " ", 2)[0]
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = fn(w)
|
|
return //nolint
|
|
}
|
|
|
|
// Repeat executes the given function periodically in a goroutine (as a "worker").
|
|
// The worker context has
|
|
// - A separate context which is canceled when the functions returns.
|
|
// - Access to named structure logging.
|
|
// - By default error/panic will be logged. For custom behavior supply errorFn, the argument is optional.
|
|
// - Flow control helpers.
|
|
// - Repeat is intended for long running tasks that are mostly idle.
|
|
func (m *Manager) Repeat(name string, period time.Duration, fn func(w *WorkerCtx) error) *WorkerMgr {
|
|
t := m.NewWorkerMgr(name, fn, nil)
|
|
return t.Repeat(period)
|
|
}
|
|
|
|
// Delay starts the given function delayed in a goroutine (as a "worker").
|
|
// The worker context has
|
|
// - A separate context which is canceled when the functions returns.
|
|
// - Access to named structure logging.
|
|
// - By default error/panic will be logged. For custom behavior supply errorFn, the argument is optional.
|
|
// - Panic catching.
|
|
// - Flow control helpers.
|
|
func (m *Manager) Delay(name string, period time.Duration, fn func(w *WorkerCtx) error) *WorkerMgr {
|
|
t := m.NewWorkerMgr(name, fn, nil)
|
|
return t.Delay(period)
|
|
}
|