Skip to content

Commit 0c80028

Browse files
Merge pull request #2 from danielsobrado/copilot/stabilize-startup-error-handling
Startup/config/error handling stability
2 parents dde40f8 + b9d8e77 commit 0c80028

9 files changed

Lines changed: 208 additions & 67 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
node_modules/
33
data/postgres/
44
frontend/node_modules/
5-
dist/
5+
/dist/
66
.cache/
77
build/bin/
88
data/keys/

app.go

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -64,62 +64,56 @@ type ActiveTransfer struct {
6464
Speed float64 `json:"speed"`
6565
}
6666

67-
// NewApp creates a new application instance
68-
func NewApp() *App {
69-
logger, _ := zap.NewProduction()
67+
// NewApp creates a new application instance using the provided configuration.
68+
// cfg must not be nil; callers should load it via config.Load or config.LoadDefaults.
69+
// Returns an error if logger creation or directory setup fails.
70+
func NewApp(cfg *config.Config) (*App, error) {
71+
if cfg == nil {
72+
return nil, errors.New("config must not be nil")
73+
}
74+
75+
// Build logger from configuration.
76+
var zapLogger *zap.Logger
77+
var err error
78+
if cfg.IsDevelopment() {
79+
zapLogger, err = zap.NewDevelopment()
80+
} else {
81+
zapLogger, err = zap.NewProduction()
82+
}
83+
if err != nil {
84+
return nil, fmt.Errorf("creating logger: %w", err)
85+
}
86+
7087
ctx, cancel := context.WithCancel(context.Background())
7188

72-
// Create data directories
89+
// Create data directories needed by the embedded Postgres runtime and scripts.
7390
dirs := []string{
7491
"./data",
7592
"./data/postgres",
7693
"./data/scripts",
7794
"./data/keys",
7895
}
79-
8096
for _, dir := range dirs {
8197
if err := os.MkdirAll(dir, 0755); err != nil {
82-
logger.Fatal("Failed to create directory",
83-
zap.String("dir", dir),
84-
zap.Error(err))
98+
cancel()
99+
_ = zapLogger.Sync()
100+
return nil, fmt.Errorf("creating directory %q: %w", dir, err)
85101
}
86102
}
87103

88-
// Create default configuration
89-
cfg := &config.Config{
90-
Database: config.DatabaseConfig{
91-
Type: "postgres",
92-
Port: 5433,
93-
MaxConnections: 10,
94-
MinConnections: 2,
95-
SSLMode: "disable",
96-
},
97-
P2P: config.P2PConfig{
98-
Port: 4001,
99-
MaxPeers: 50,
100-
MinPeers: 5,
101-
PeerTimeout: 30 * time.Second,
102-
BootstrapPeers: []string{},
103-
},
104-
Scripts: config.ScriptConfig{
105-
ScriptDir: "./data/scripts",
106-
PythonPath: "python3",
107-
MaxExecTime: 5 * time.Minute,
108-
MaxMemoryMB: 512,
109-
AllowedPkgs: []string{"pandas", "numpy", "requests"},
110-
},
111-
Security: config.SecurityConfig{
112-
KeyFile: "./data/keys/host.key",
113-
},
104+
// Ensure the script directory points at the embedded data layout when the
105+
// config is still using the generic default value.
106+
if cfg.Scripts.ScriptDir == "scripts" {
107+
cfg.Scripts.ScriptDir = "./data/scripts"
114108
}
115109

116110
return &App{
117111
ctx: ctx,
118112
cancel: cancel,
119-
logger: logger,
113+
logger: zapLogger,
120114
config: cfg,
121115
cleanup: make([]func() error, 0),
122-
}
116+
}, nil
123117
}
124118

125119
func (a *App) startup(ctx context.Context) {
@@ -134,39 +128,62 @@ func (a *App) startup(ctx context.Context) {
134128

135129
// Initialize embedded database
136130
if err := a.initEmbeddedDB(); err != nil {
137-
a.logger.Fatal("Failed to initialize embedded database", zap.Error(err))
131+
a.logger.Error("Failed to initialize embedded database", zap.Error(err))
138132
return
139133
}
140134

141135
// Initialize database connection
142136
if err := a.initDatabase(ctx); err != nil {
143-
a.logger.Fatal("Failed to initialize database", zap.Error(err))
137+
a.logger.Error("Failed to initialize database", zap.Error(err))
138+
a.cleanupEmbeddedDB()
144139
return
145140
}
146141

147142
// Initialize database schema for a fresh embedded database instance
148143
if err := data.NewSchemaManager(a.conn).InitializeSchema(ctx); err != nil {
149-
a.logger.Fatal("Failed to initialize database schema", zap.Error(err))
144+
a.logger.Error("Failed to initialize database schema", zap.Error(err))
145+
a.cleanupDBResources(ctx)
150146
return
151147
}
152148

153149
// Initialize repository
154150
a.repo, err = data.NewPostgresRepository(ctx, a.conn, a.logger)
155151
if err != nil {
156-
a.logger.Fatal("Failed to create repository", zap.Error(err))
152+
a.logger.Error("Failed to create repository", zap.Error(err))
153+
a.cleanupDBResources(ctx)
157154
return
158155
}
159156

160157
// Initialize and start services
161158
if err := a.initServices(ctx); err != nil {
162-
a.logger.Fatal("Failed to initialize services", zap.Error(err))
159+
a.logger.Error("Failed to initialize services", zap.Error(err))
160+
a.cleanupDBResources(ctx)
163161
return
164162
}
165163

166164
a.running = true
167165
a.logger.Info("Application started successfully")
168166
}
169167

168+
// cleanupEmbeddedDB stops the embedded Postgres instance if it was started.
169+
func (a *App) cleanupEmbeddedDB() {
170+
if a.embedded != nil {
171+
if err := a.embedded.Stop(); err != nil {
172+
a.logger.Error("Failed to stop embedded database during cleanup", zap.Error(err))
173+
}
174+
}
175+
}
176+
177+
// cleanupDBResources closes the database connection and stops the embedded
178+
// Postgres instance. Called on partial startup failure.
179+
func (a *App) cleanupDBResources(ctx context.Context) {
180+
if a.conn != nil {
181+
a.conn.Close(ctx)
182+
a.conn = nil
183+
}
184+
a.cleanupEmbeddedDB()
185+
}
186+
170187
func (a *App) initEmbeddedDB() error {
171188
pg := postgres.NewDatabase(
172189
postgres.DefaultConfig().

cmd/app/main.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,17 @@ func initializeApp(ctx context.Context, cfg *config.Config, logger *zap.Logger)
8686
return nil, fmt.Errorf("initializing database service: %w", err)
8787
}
8888

89-
// Initialize script manager
90-
scriptConfig := &config.ScriptConfig{
91-
ScriptDir: filepath.Join(*dataDir, "scripts"),
92-
// Add other required config fields
89+
// Override the script directory when --data-dir is set so the layout is
90+
// consistent with the rest of the application. Only override if the caller
91+
// explicitly specified a non-default data directory.
92+
scriptsCfg := cfg.Scripts
93+
if *dataDir != "./data" {
94+
scriptsCfg.ScriptDir = filepath.Join(*dataDir, "scripts")
9395
}
9496

95-
scriptManager, err := scripts.NewScriptManager(scriptConfig, logger)
97+
scriptManager, err := scripts.NewScriptManager(&scriptsCfg, logger)
9698
if err != nil {
97-
logger.Fatal("Failed to initialize script manager", zap.Error(err))
99+
return nil, fmt.Errorf("initializing script manager: %w", err)
98100
}
99101

100102
app := &App{
@@ -181,15 +183,6 @@ func loadConfig(path string) (*config.Config, error) {
181183
if err != nil {
182184
return nil, fmt.Errorf("loading config: %w", err)
183185
}
184-
185-
// Set default values
186-
if cfg.Database.Port == 0 {
187-
cfg.Database.Port = 5433
188-
}
189-
if cfg.P2P.Port == 0 {
190-
cfg.P2P.Port = 4001
191-
}
192-
193186
return cfg, nil
194187
}
195188

frontend/.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ pnpm-debug.log*
88
lerna-debug.log*
99

1010
node_modules
11-
dist
11+
dist/*
12+
!dist/.gitkeep
1213
dist-ssr
1314
*.local
1415

@@ -22,3 +23,4 @@ dist-ssr
2223
*.njsproj
2324
*.sln
2425
*.sw?
26+
!dist/index.html

frontend/dist/.gitkeep

Whitespace-only changes.

frontend/dist/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!DOCTYPE html><html><body>P2P Market Data</body></html>

main.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ package main
44
import (
55
"embed"
66
"log"
7+
"os"
8+
"strings"
79

810
"github.com/wailsapp/wails/v2"
911
"github.com/wailsapp/wails/v2/pkg/logger"
1012
"github.com/wailsapp/wails/v2/pkg/options"
1113
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
1214
"github.com/wailsapp/wails/v2/pkg/options/mac"
1315
"github.com/wailsapp/wails/v2/pkg/options/windows"
16+
17+
"p2p_market_data/pkg/config"
1418
)
1519

1620
//go:embed frontend/dist
@@ -20,14 +24,24 @@ var assets embed.FS
2024
var icon []byte
2125

2226
func main() {
23-
// Create configuration with defaults
24-
// cfg := config.DefaultConfig()
27+
// Load configuration; both Wails options and the App struct share this instance.
28+
cfg, err := loadWailsConfig()
29+
if err != nil {
30+
log.Printf("Warning: could not load config (%v), falling back to defaults", err)
31+
cfg, err = config.LoadDefaults()
32+
if err != nil {
33+
log.Fatalf("Failed to load default configuration: %v", err)
34+
}
35+
}
2536

2637
// Create an instance of the app structure
27-
app := NewApp()
38+
app, err := NewApp(cfg)
39+
if err != nil {
40+
log.Fatalf("Failed to create application: %v", err)
41+
}
2842

2943
// Create application with options
30-
err := wails.Run(&options.App{
44+
err = wails.Run(&options.App{
3145
Title: "P2P Market Data",
3246
Width: 1280,
3347
Height: 930,
@@ -50,8 +64,8 @@ func main() {
5064
OnBeforeClose: app.beforeClose,
5165
OnShutdown: app.shutdown,
5266

53-
// Enable dev tools in debug mode
54-
LogLevel: determineLogLevel(),
67+
// Log level driven by configuration, not hardcoded.
68+
LogLevel: wailsLogLevel(cfg),
5569

5670
// Window configuration
5771
Windows: &windows.Options{
@@ -85,7 +99,36 @@ func main() {
8599
}
86100
}
87101

88-
func determineLogLevel() logger.LogLevel {
89-
// You could make this configurable via environment variables
90-
return logger.DEBUG
102+
// loadWailsConfig tries to load configuration from well-known paths relative to
103+
// the working directory. If no config file is found it falls back to defaults.
104+
func loadWailsConfig() (*config.Config, error) {
105+
candidates := []string{
106+
"./config.yaml",
107+
"./config/config.yaml",
108+
"./config/db_config.yaml",
109+
}
110+
for _, path := range candidates {
111+
if _, err := os.Stat(path); err == nil {
112+
return config.Load(path)
113+
}
114+
}
115+
return config.LoadDefaults()
116+
}
117+
118+
// wailsLogLevel maps the config log-level string to the Wails logger.LogLevel
119+
// type. Defaults to INFO when the level is unrecognised or the config is nil.
120+
func wailsLogLevel(cfg *config.Config) logger.LogLevel {
121+
if cfg == nil {
122+
return logger.INFO
123+
}
124+
switch strings.ToLower(cfg.LogLevel) {
125+
case "debug":
126+
return logger.DEBUG
127+
case "warn", "warning":
128+
return logger.WARNING
129+
case "error":
130+
return logger.ERROR
131+
default:
132+
return logger.INFO
133+
}
91134
}

pkg/config/config.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ func setDefaults(v *viper.Viper) {
152152
// Database defaults
153153
v.SetDefault("database.type", "postgres")
154154
v.SetDefault("database.url", "postgres://postgres:postgres@localhost:5433/market_data?sslmode=disable")
155+
v.SetDefault("database.port", 5433)
155156
v.SetDefault("database.max_conns", 10)
156157
v.SetDefault("database.min_conns", 2)
157158
v.SetDefault("database.timeout", "30s")
@@ -279,6 +280,26 @@ func (c *Config) validateSecurity() error {
279280
return nil
280281
}
281282

283+
// LoadDefaults returns a Config populated from defaults and environment variables
284+
// only, without reading any config file. Useful as a fallback when no config
285+
// file is available.
286+
func LoadDefaults() (*Config, error) {
287+
v := viper.New()
288+
setDefaults(v)
289+
v.SetEnvPrefix("P2P")
290+
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
291+
v.AutomaticEnv()
292+
293+
cfg := &Config{}
294+
if err := v.Unmarshal(cfg); err != nil {
295+
return nil, fmt.Errorf("failed to parse default config: %w", err)
296+
}
297+
if err := cfg.Validate(); err != nil {
298+
return nil, fmt.Errorf("invalid default configuration: %w", err)
299+
}
300+
return cfg, nil
301+
}
302+
282303
// GetLogLevel returns a zap log level based on the configured string
283304
func (c *Config) GetLogLevel() zap.AtomicLevel {
284305
level := zap.NewAtomicLevel()

0 commit comments

Comments
 (0)