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