Skip to content

Commit

Permalink
perf(agent): ⚡ don't use a "fat context" for agent options
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuar committed Dec 26, 2024
1 parent 768cc40 commit 1e9d3c9
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 141 deletions.
131 changes: 76 additions & 55 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,79 +38,98 @@ type Agent struct {
ui ui
}

// CtxOption is a functional parameter that will add a value to the agent's
// context.
type CtxOption func(context.Context) context.Context
// agentOptions are the options that can be used to change the behavior of Go
// Hass Agent.
type agentOptions struct {
headless bool
forceRegister bool
ignoreHassURLs bool
registrationServer string
registrationToken string
}

// LoadCtx will "load" a context.Context with the given options (i.e. add values
// to it to be used by the agent).
func newAgentCtx(options ...CtxOption) (context.Context, context.CancelFunc) {
ctx, cancelFunc := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
// options represents the set of run-time options for the current instance of Go
// Hass Agent.
var options agentOptions

// Option represents a run-time option for Go Hass Agent.
type Option func(agentOptions) agentOptions

for _, option := range options {
ctx = option(ctx) //nolint:fatcontext
// setup will establish the options for this run of Go Hass Agent.
func setup(givenOptions ...Option) {
for _, option := range givenOptions {
options = option(options)
}
}

return ctx, cancelFunc
// SetHeadless indicates Go Hass Agent should run without a UI.
func SetHeadless(value bool) Option {
return func(ao agentOptions) agentOptions {
ao.headless = value
return ao
}
}

// SetLogger sets the given logger in the context.
func SetLogger(logger *slog.Logger) CtxOption {
return func(ctx context.Context) context.Context {
ctx = logging.ToContext(ctx, logger)
return ctx
// SetRegistrationInfo contains the information Go Hass Agent should use to
// register with Home Assistant.
func SetRegistrationInfo(server, token string, ignoreURLs bool) Option {
return func(ao agentOptions) agentOptions {
ao.ignoreHassURLs = ignoreURLs
ao.registrationServer = server
ao.registrationToken = token

return ao
}
}

// SetHeadless sets the headless flag in the context.
func SetHeadless(value bool) CtxOption {
return func(ctx context.Context) context.Context {
ctx = addToContext(ctx, headlessCtxKey, value)
return ctx
// ForceRegister indicates that Go Hass Agent should ignore its current
// registration status and re-register with Home Assistant.
func SetForceRegister(value bool) Option {
return func(ao agentOptions) agentOptions {
ao.forceRegister = value
return ao
}
}

// SetRegistrationInfo sets registration details in the context to be used for
// registering the agent.
func SetRegistrationInfo(server, token string, ignoreURLs bool) CtxOption {
return func(ctx context.Context) context.Context {
ctx = addToContext(ctx, serverCtxKey, server)
ctx = addToContext(ctx, tokenCtxKey, token)
ctx = addToContext(ctx, ignoreURLsCtxKey, ignoreURLs)
// newCtx creates a new context for this run of Go Hass Agent.
func newCtx(logger *slog.Logger) (context.Context, context.CancelFunc) {
ctx, cancelFunc := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
ctx = logging.ToContext(ctx, logger)

return ctx
}
return ctx, cancelFunc
}

// ForceRegister sets the forceregister flag in the context.
func SetForceRegister(value bool) CtxOption {
return func(ctx context.Context) context.Context {
ctx = addToContext(ctx, forceRegisterCtxKey, value)
return ctx
// newAgent creates the Agent struct.
func newAgent(ctx context.Context) *Agent {
agent := &Agent{}

// If not running headless, set up the UI.
if !options.headless {
agent.ui = fyneui.NewFyneUI(ctx)
}

return agent
}

// Run is invoked when Go Hass Agent is run with the `run` command-line option
// (i.e., `go-hass-agent run`).
//
//nolint:funlen
//revive:disable:function-length
func Run(options ...CtxOption) error {
func Run(logger *slog.Logger, givenOptions ...Option) error {
var (
wg sync.WaitGroup
regWait sync.WaitGroup
err error
)

ctx, cancelFunc := newAgentCtx(options...)
// Establish run-time options.
setup(givenOptions...)
// Setup context.
ctx, cancelFunc := newCtx(logger)
defer cancelFunc()

agent := &Agent{}

// If running headless, do not set up the UI.
if !Headless(ctx) {
agent.ui = fyneui.NewFyneUI(ctx)
}
// Create struct.
agent := newAgent(ctx)

// Load the preferences from file. Ignore the case where there are no
// existing preferences.
Expand Down Expand Up @@ -204,7 +223,7 @@ func Run(options ...CtxOption) error {
}()

// Do not run the UI loop if the agent is running in headless mode.
if !Headless(ctx) {
if !options.headless {
agent.ui.DisplayTrayIcon(ctx, cancelFunc)
agent.ui.Run(ctx)
}
Expand All @@ -217,20 +236,19 @@ func Run(options ...CtxOption) error {
// Register is run when Go Hass Agent is invoked with the `register`
// command-line option (i.e., `go-hass-agent register`). It will attempt to
// register Go Hass Agent with Home Assistant.
func Register(options ...CtxOption) error {
func Register(logger *slog.Logger, givenOptions ...Option) error {
var (
wg sync.WaitGroup
err error
)

ctx, cancelFunc := newAgentCtx(options...)
// Establish run-time options.
setup(givenOptions...)
// Setup context.
ctx, cancelFunc := newCtx(logger)
defer cancelFunc()

agent := &Agent{}
// If running headless, do not set up the UI.
if !Headless(ctx) {
agent.ui = fyneui.NewFyneUI(ctx)
}
// Create struct.
agent := newAgent(ctx)

if err = preferences.Load(); err != nil && !errors.Is(err, preferences.ErrLoadPreferences) {
return fmt.Errorf("%w: %w", ErrAgentStart, err)
Expand All @@ -254,7 +272,7 @@ func Register(options ...CtxOption) error {
cancelReg()
}()

if !Headless(ctx) {
if !options.headless {
agent.ui.Run(regCtx)
}

Expand All @@ -265,8 +283,11 @@ func Register(options ...CtxOption) error {

// Reset is invoked when Go Hass Agent is run with the `reset` command-line
// option (i.e., `go-hass-agent reset`).
func Reset(options ...CtxOption) error {
ctx, cancelFunc := newAgentCtx(options...)
func Reset(logger *slog.Logger, givenOptions ...Option) error {
// Establish run-time options.
setup(givenOptions...)
// Setup context.
ctx, cancelFunc := newCtx(logger)
defer cancelFunc()

// Load the preferences so we know what we need to reset.
Expand Down
73 changes: 0 additions & 73 deletions internal/agent/context.go

This file was deleted.

2 changes: 1 addition & 1 deletion internal/agent/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
// received notifications will be dipslayed on the device running the agent.
func runNotificationsWorker(ctx context.Context, agentUI ui) {
// Don't run if agent is running headless.\
if Headless(ctx) {
if options.headless {
return
}

Expand Down
12 changes: 6 additions & 6 deletions internal/agent/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ import (
var ErrUserCancelledRegistration = errors.New("user canceled registration")

func checkRegistration(ctx context.Context, agentUI ui) error {
if preferences.Registered() && !ForceRegister(ctx) {
if preferences.Registered() && !options.forceRegister {
return nil
}

// Set the registration options as passed in from command-line.
registrationOptions := &preferences.Registration{
Server: Server(ctx),
Token: Token(ctx),
IgnoreHassURLs: IgnoreURLs(ctx),
Server: options.registrationServer,
Token: options.registrationToken,
IgnoreHassURLs: options.ignoreHassURLs,
}

// If not headless, present a UI for the user to configure options.
if !Headless(ctx) {
if options.headless {
userInputDoneCh := agentUI.DisplayRegistrationWindow(ctx, registrationOptions)
if canceled := <-userInputDoneCh; canceled {
return ErrUserCancelledRegistration
Expand All @@ -54,7 +54,7 @@ func checkRegistration(ctx context.Context, agentUI ui) error {
}

// If the registration was forced, reset the sensor registry.
if ForceRegister(ctx) {
if options.forceRegister {
if err := registry.Reset(); err != nil {
logging.FromContext(ctx).Warn("Problem resetting registry.", slog.Any("error", err))
}
Expand Down
3 changes: 1 addition & 2 deletions internal/cli/registerCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ func (r *RegisterCmd) Help() string {
}

func (r *RegisterCmd) Run(opts *CmdOpts) error {
if err := agent.Register(
if err := agent.Register(opts.Logger,
agent.SetHeadless(opts.Headless),
agent.SetRegistrationInfo(r.Server, r.Token, r.IgnoreURLs),
agent.SetForceRegister(r.Force),
agent.SetLogger(opts.Logger),
); err != nil {
return fmt.Errorf("failed to run: %w", err)
}
Expand Down
3 changes: 1 addition & 2 deletions internal/cli/resetCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ func (r *ResetCmd) Run(opts *CmdOpts) error {
var errs error

// Reset agent.
if err := agent.Reset(
if err := agent.Reset(opts.Logger,
agent.SetHeadless(opts.Headless),
agent.SetLogger(opts.Logger),
); err != nil {
errs = errors.Join(fmt.Errorf("agent reset failed: %w", err))
}
Expand Down
3 changes: 1 addition & 2 deletions internal/cli/runCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ func (r *RunCmd) Help() string {
}

func (r *RunCmd) Run(opts *CmdOpts) error {
if err := agent.Run(
if err := agent.Run(opts.Logger,
agent.SetHeadless(opts.Headless),
agent.SetLogger(opts.Logger),
); err != nil {
return fmt.Errorf("failed to run: %w", err)
}
Expand Down

0 comments on commit 1e9d3c9

Please sign in to comment.