run.go raw

   1  package run
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"io"
   7  	"os"
   8  	"path/filepath"
   9  	"strings"
  10  	"sync"
  11  
  12  	"github.com/adrg/xdg"
  13  	"next.orly.dev/pkg/lol/chk"
  14  	lol "next.orly.dev/pkg/lol"
  15  	"next.orly.dev/app"
  16  	"next.orly.dev/app/config"
  17  	"next.orly.dev/pkg/acl"
  18  	"next.orly.dev/pkg/database"
  19  	"next.orly.dev/pkg/ratelimit"
  20  )
  21  
  22  // Options configures relay startup behavior.
  23  type Options struct {
  24  	// CleanupDataDir controls whether the data directory is deleted on Stop().
  25  	// Defaults to true. Set to false to preserve the data directory.
  26  	CleanupDataDir *bool
  27  
  28  	// StdoutWriter is an optional writer to receive stdout logs.
  29  	// If nil, stdout will be captured to a buffer accessible via Relay.Stdout().
  30  	StdoutWriter io.Writer
  31  
  32  	// StderrWriter is an optional writer to receive stderr logs.
  33  	// If nil, stderr will be captured to a buffer accessible via Relay.Stderr().
  34  	StderrWriter io.Writer
  35  }
  36  
  37  // Relay represents a running relay instance that can be started and stopped.
  38  type Relay struct {
  39  	ctx           context.Context
  40  	cancel        context.CancelFunc
  41  	db            *database.D
  42  	quit          chan struct{}
  43  	dataDir       string
  44  	cleanupDataDir bool
  45  
  46  	// Log capture
  47  	stdoutBuf     *bytes.Buffer
  48  	stderrBuf     *bytes.Buffer
  49  	stdoutWriter  io.Writer
  50  	stderrWriter  io.Writer
  51  	logMu         sync.RWMutex
  52  }
  53  
  54  // Start initializes and starts a relay with the given configuration.
  55  // It bypasses the configuration loading step and uses the provided config directly.
  56  //
  57  // Parameters:
  58  //   - cfg: The configuration to use for the relay
  59  //   - opts: Optional configuration for relay behavior. If nil, defaults are used.
  60  //
  61  // Returns:
  62  //   - relay: A Relay instance that can be used to stop the relay
  63  //   - err: An error if initialization or startup fails
  64  func Start(cfg *config.C, opts *Options) (relay *Relay, err error) {
  65  	relay = &Relay{
  66  		cleanupDataDir: true,
  67  	}
  68  
  69  	// Apply options
  70  	var userStdoutWriter, userStderrWriter io.Writer
  71  	if opts != nil {
  72  		if opts.CleanupDataDir != nil {
  73  			relay.cleanupDataDir = *opts.CleanupDataDir
  74  		}
  75  		userStdoutWriter = opts.StdoutWriter
  76  		userStderrWriter = opts.StderrWriter
  77  	}
  78  
  79  	// Set up log capture buffers
  80  	relay.stdoutBuf = &bytes.Buffer{}
  81  	relay.stderrBuf = &bytes.Buffer{}
  82  
  83  	// Build writers list for stdout
  84  	stdoutWriters := []io.Writer{relay.stdoutBuf}
  85  	if userStdoutWriter != nil {
  86  		stdoutWriters = append(stdoutWriters, userStdoutWriter)
  87  	}
  88  	stdoutWriters = append(stdoutWriters, os.Stdout)
  89  	relay.stdoutWriter = io.MultiWriter(stdoutWriters...)
  90  
  91  	// Build writers list for stderr
  92  	stderrWriters := []io.Writer{relay.stderrBuf}
  93  	if userStderrWriter != nil {
  94  		stderrWriters = append(stderrWriters, userStderrWriter)
  95  	}
  96  	stderrWriters = append(stderrWriters, os.Stderr)
  97  	relay.stderrWriter = io.MultiWriter(stderrWriters...)
  98  
  99  	// Set up logging - write to appropriate destination and capture
 100  	if cfg.LogToStdout {
 101  		lol.Writer = relay.stdoutWriter
 102  	} else {
 103  		lol.Writer = relay.stderrWriter
 104  	}
 105  	lol.SetLogLevel(cfg.LogLevel)
 106  
 107  	// Expand DataDir if needed
 108  	if cfg.DataDir == "" || strings.Contains(cfg.DataDir, "~") {
 109  		cfg.DataDir = filepath.Join(xdg.DataHome, cfg.AppName)
 110  	}
 111  	relay.dataDir = cfg.DataDir
 112  
 113  	// Create context
 114  	relay.ctx, relay.cancel = context.WithCancel(context.Background())
 115  
 116  	// Initialize database
 117  	if relay.db, err = database.New(
 118  		relay.ctx, relay.cancel, cfg.DataDir, cfg.DBLogLevel,
 119  	); chk.E(err) {
 120  		return
 121  	}
 122  
 123  	// Configure ACL
 124  	acl.Registry.SetMode(cfg.ACLMode)
 125  	if err = acl.Registry.Configure(cfg, relay.db, relay.ctx); chk.E(err) {
 126  		return
 127  	}
 128  	acl.Registry.Syncer()
 129  
 130  	// Create rate limiter (disabled for test relay instances)
 131  	limiter := ratelimit.NewDisabledLimiter()
 132  
 133  	// Start the relay
 134  	relay.quit = app.Run(relay.ctx, cfg, relay.db, limiter)
 135  
 136  	return
 137  }
 138  
 139  // Stop gracefully stops the relay by canceling the context and closing the database.
 140  // If CleanupDataDir is enabled (default), it also removes the data directory.
 141  //
 142  // Returns:
 143  //   - err: An error if shutdown fails
 144  func (r *Relay) Stop() (err error) {
 145  	if r.cancel != nil {
 146  		r.cancel()
 147  	}
 148  	if r.quit != nil {
 149  		<-r.quit
 150  	}
 151  	if r.db != nil {
 152  		err = r.db.Close()
 153  	}
 154  	// Clean up data directory if enabled
 155  	if r.cleanupDataDir && r.dataDir != "" {
 156  		if rmErr := os.RemoveAll(r.dataDir); rmErr != nil {
 157  			if err == nil {
 158  				err = rmErr
 159  			}
 160  		}
 161  	}
 162  	return
 163  }
 164  
 165  // Stdout returns the complete stdout log buffer contents.
 166  func (r *Relay) Stdout() string {
 167  	r.logMu.RLock()
 168  	defer r.logMu.RUnlock()
 169  	if r.stdoutBuf == nil {
 170  		return ""
 171  	}
 172  	return r.stdoutBuf.String()
 173  }
 174  
 175  // Stderr returns the complete stderr log buffer contents.
 176  func (r *Relay) Stderr() string {
 177  	r.logMu.RLock()
 178  	defer r.logMu.RUnlock()
 179  	if r.stderrBuf == nil {
 180  		return ""
 181  	}
 182  	return r.stderrBuf.String()
 183  }
 184  
 185  // StdoutBytes returns the complete stdout log buffer as bytes.
 186  func (r *Relay) StdoutBytes() []byte {
 187  	r.logMu.RLock()
 188  	defer r.logMu.RUnlock()
 189  	if r.stdoutBuf == nil {
 190  		return nil
 191  	}
 192  	return r.stdoutBuf.Bytes()
 193  }
 194  
 195  // StderrBytes returns the complete stderr log buffer as bytes.
 196  func (r *Relay) StderrBytes() []byte {
 197  	r.logMu.RLock()
 198  	defer r.logMu.RUnlock()
 199  	if r.stderrBuf == nil {
 200  		return nil
 201  	}
 202  	return r.stderrBuf.Bytes()
 203  }
 204  
 205