From 5e6c0b80aa7ae6a9ed0cd6fee5b4ac2ab7f106b8 Mon Sep 17 00:00:00 2001 From: mleku Date: Thu, 30 Oct 2025 17:57:57 +0000 Subject: [PATCH] Add Relay functionality for managing startup and shutdown processes - Introduced a new package `run` with a `Relay` struct to manage the lifecycle of a relay instance. - Implemented `Start` and `Stop` methods for initializing and gracefully shutting down the relay, including options for log capturing and data directory cleanup. - Added methods to retrieve captured stdout and stderr logs. - Enhanced configuration handling for data directory and logging based on user-defined options. --- pkg/run/run.go | 200 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 pkg/run/run.go diff --git a/pkg/run/run.go b/pkg/run/run.go new file mode 100644 index 0000000..699239b --- /dev/null +++ b/pkg/run/run.go @@ -0,0 +1,200 @@ +package run + +import ( + "bytes" + "context" + "io" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/adrg/xdg" + "lol.mleku.dev/chk" + lol "lol.mleku.dev" + "next.orly.dev/app" + "next.orly.dev/app/config" + "next.orly.dev/pkg/acl" + "next.orly.dev/pkg/database" +) + +// Options configures relay startup behavior. +type Options struct { + // CleanupDataDir controls whether the data directory is deleted on Stop(). + // Defaults to true. Set to false to preserve the data directory. + CleanupDataDir *bool + + // StdoutWriter is an optional writer to receive stdout logs. + // If nil, stdout will be captured to a buffer accessible via Relay.Stdout(). + StdoutWriter io.Writer + + // StderrWriter is an optional writer to receive stderr logs. + // If nil, stderr will be captured to a buffer accessible via Relay.Stderr(). + StderrWriter io.Writer +} + +// Relay represents a running relay instance that can be started and stopped. +type Relay struct { + ctx context.Context + cancel context.CancelFunc + db *database.D + quit chan struct{} + dataDir string + cleanupDataDir bool + + // Log capture + stdoutBuf *bytes.Buffer + stderrBuf *bytes.Buffer + stdoutWriter io.Writer + stderrWriter io.Writer + logMu sync.RWMutex +} + +// Start initializes and starts a relay with the given configuration. +// It bypasses the configuration loading step and uses the provided config directly. +// +// Parameters: +// - cfg: The configuration to use for the relay +// - opts: Optional configuration for relay behavior. If nil, defaults are used. +// +// Returns: +// - relay: A Relay instance that can be used to stop the relay +// - err: An error if initialization or startup fails +func Start(cfg *config.C, opts *Options) (relay *Relay, err error) { + relay = &Relay{ + cleanupDataDir: true, + } + + // Apply options + var userStdoutWriter, userStderrWriter io.Writer + if opts != nil { + if opts.CleanupDataDir != nil { + relay.cleanupDataDir = *opts.CleanupDataDir + } + userStdoutWriter = opts.StdoutWriter + userStderrWriter = opts.StderrWriter + } + + // Set up log capture buffers + relay.stdoutBuf = &bytes.Buffer{} + relay.stderrBuf = &bytes.Buffer{} + + // Build writers list for stdout + stdoutWriters := []io.Writer{relay.stdoutBuf} + if userStdoutWriter != nil { + stdoutWriters = append(stdoutWriters, userStdoutWriter) + } + stdoutWriters = append(stdoutWriters, os.Stdout) + relay.stdoutWriter = io.MultiWriter(stdoutWriters...) + + // Build writers list for stderr + stderrWriters := []io.Writer{relay.stderrBuf} + if userStderrWriter != nil { + stderrWriters = append(stderrWriters, userStderrWriter) + } + stderrWriters = append(stderrWriters, os.Stderr) + relay.stderrWriter = io.MultiWriter(stderrWriters...) + + // Set up logging - write to appropriate destination and capture + if cfg.LogToStdout { + lol.Writer = relay.stdoutWriter + } else { + lol.Writer = relay.stderrWriter + } + lol.SetLogLevel(cfg.LogLevel) + + // Expand DataDir if needed + if cfg.DataDir == "" || strings.Contains(cfg.DataDir, "~") { + cfg.DataDir = filepath.Join(xdg.DataHome, cfg.AppName) + } + relay.dataDir = cfg.DataDir + + // Create context + relay.ctx, relay.cancel = context.WithCancel(context.Background()) + + // Initialize database + if relay.db, err = database.New( + relay.ctx, relay.cancel, cfg.DataDir, cfg.DBLogLevel, + ); chk.E(err) { + return + } + + // Configure ACL + acl.Registry.Active.Store(cfg.ACLMode) + if err = acl.Registry.Configure(cfg, relay.db, relay.ctx); chk.E(err) { + return + } + acl.Registry.Syncer() + + // Start the relay + relay.quit = app.Run(relay.ctx, cfg, relay.db) + + return +} + +// Stop gracefully stops the relay by canceling the context and closing the database. +// If CleanupDataDir is enabled (default), it also removes the data directory. +// +// Returns: +// - err: An error if shutdown fails +func (r *Relay) Stop() (err error) { + if r.cancel != nil { + r.cancel() + } + if r.quit != nil { + <-r.quit + } + if r.db != nil { + err = r.db.Close() + } + // Clean up data directory if enabled + if r.cleanupDataDir && r.dataDir != "" { + if rmErr := os.RemoveAll(r.dataDir); rmErr != nil { + if err == nil { + err = rmErr + } + } + } + return +} + +// Stdout returns the complete stdout log buffer contents. +func (r *Relay) Stdout() string { + r.logMu.RLock() + defer r.logMu.RUnlock() + if r.stdoutBuf == nil { + return "" + } + return r.stdoutBuf.String() +} + +// Stderr returns the complete stderr log buffer contents. +func (r *Relay) Stderr() string { + r.logMu.RLock() + defer r.logMu.RUnlock() + if r.stderrBuf == nil { + return "" + } + return r.stderrBuf.String() +} + +// StdoutBytes returns the complete stdout log buffer as bytes. +func (r *Relay) StdoutBytes() []byte { + r.logMu.RLock() + defer r.logMu.RUnlock() + if r.stdoutBuf == nil { + return nil + } + return r.stdoutBuf.Bytes() +} + +// StderrBytes returns the complete stderr log buffer as bytes. +func (r *Relay) StderrBytes() []byte { + r.logMu.RLock() + defer r.logMu.RUnlock() + if r.stderrBuf == nil { + return nil + } + return r.stderrBuf.Bytes() +} +