Moonshark/main.go

385 lines
9.4 KiB
Go

package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
"Moonshark/http"
"Moonshark/router"
"Moonshark/runner"
"Moonshark/sessions"
"Moonshark/utils/color"
"Moonshark/utils/config"
"Moonshark/utils/logger"
"Moonshark/utils/metadata"
"Moonshark/watchers"
)
// Moonshark represents the server and all its dependencies
type Moonshark struct {
Config *config.Config
LuaRouter *router.LuaRouter
LuaRunner *runner.Runner
HTTPServer *http.Server
cleanupFuncs []func() error
scriptMode bool
}
func main() {
configPath := flag.String("config", "config.lua", "Path to configuration file")
debugFlag := flag.Bool("debug", false, "Enable debug mode")
scriptPath := flag.String("script", "", "Path to Lua script to execute once")
flag.Parse()
scriptMode := *scriptPath != ""
banner()
mode := ""
if scriptMode {
mode = "[Script Mode]"
} else {
mode = "[Server Mode]"
}
fmt.Printf("%s %s\n\n", color.Apply(mode, color.Gray), color.Apply("v"+metadata.Version, color.Blue))
// Initialize logger
logger.InitGlobalLogger(true, false)
// Load config
cfg, err := config.Load(*configPath)
if err != nil {
logger.Warning("Config load failed: %v, using defaults", color.Apply(err.Error(), color.Red))
cfg = config.New()
}
// Setup logging with debug mode
if *debugFlag || cfg.Server.Debug {
logger.EnableDebug()
logger.Debug("Debug logging enabled")
}
var moonshark *Moonshark
if scriptMode {
moonshark, err = initScriptMode(cfg)
} else {
moonshark, err = initServerMode(cfg, *debugFlag)
}
if err != nil {
logger.Fatal("Initialization failed: %v", err)
}
defer func() {
if err := moonshark.Shutdown(); err != nil {
logger.Error("Error during shutdown: %v", err)
os.Exit(1)
}
}()
if scriptMode {
// Run the script and exit
if err := moonshark.RunScript(*scriptPath); err != nil {
logger.Fatal("Script execution failed: %v", err)
}
return
}
// Start the server
if err := moonshark.Start(); err != nil {
logger.Fatal("Failed to start server: %v", err)
}
// Wait for shutdown signal
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
fmt.Print("\n")
logger.Info("Shutdown signal received")
}
// initScriptMode initializes minimal components needed for script execution
func initScriptMode(cfg *config.Config) (*Moonshark, error) {
moonshark := &Moonshark{
Config: cfg,
scriptMode: true,
}
// Only initialize the Lua runner with required paths
runnerOpts := []runner.RunnerOption{
runner.WithPoolSize(1), // Only need one state for script mode
runner.WithLibDirs(cfg.Dirs.Libs...),
runner.WithFsDir(cfg.Dirs.FS),
runner.WithDataDir(cfg.Dirs.Data),
}
var err error
moonshark.LuaRunner, err = runner.NewRunner(runnerOpts...)
if err != nil {
return nil, fmt.Errorf("failed to initialize Lua runner: %v", err)
}
logger.Debug("Script mode initialized with minimized components")
return moonshark, nil
}
// initServerMode initializes all components needed for server operation
func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) {
moonshark := &Moonshark{
Config: cfg,
scriptMode: false,
}
if debug {
cfg.Server.Debug = true
}
if err := initLuaRouter(moonshark); err != nil {
return nil, err
}
if err := initRunner(moonshark); err != nil {
return nil, err
}
if err := setupWatchers(moonshark); err != nil {
logger.Warning("Watcher setup failed: %v", err)
}
// Get static directory - empty string if it doesn't exist
staticDir := ""
if dirExists(cfg.Dirs.Static) {
staticDir = cfg.Dirs.Static
logger.Info("Static files enabled: %s", color.Apply(staticDir, color.Yellow))
} else {
logger.Warning("Static directory not found: %s", color.Apply(cfg.Dirs.Static, color.Yellow))
}
moonshark.HTTPServer = http.New(
moonshark.LuaRouter,
staticDir,
moonshark.LuaRunner,
cfg.Server.HTTPLogging,
cfg.Server.Debug,
cfg.Dirs.Override,
cfg,
)
// For development, disable caching. For production, enable it
if cfg.Server.Debug {
moonshark.HTTPServer.SetStaticCaching(0) // No caching in debug mode
} else {
moonshark.HTTPServer.SetStaticCaching(1 * time.Hour) // Cache for 1 hour in production
}
return moonshark, nil
}
// RunScript executes a Lua script in the sandbox environment
func (s *Moonshark) RunScript(scriptPath string) error {
scriptPath, err := filepath.Abs(scriptPath)
if err != nil {
return fmt.Errorf("failed to resolve script path: %v", err)
}
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
return fmt.Errorf("script file not found: %s", scriptPath)
}
logger.Info("Executing: %s", scriptPath)
resp, err := s.LuaRunner.RunScriptFile(scriptPath)
if err != nil {
return fmt.Errorf("execution failed: %v", err)
}
if resp != nil && resp.Body != nil {
logger.Info("Script result: %v", resp.Body)
} else {
logger.Info("Script executed successfully (no return value)")
}
return nil
}
// Start starts the HTTP server
func (s *Moonshark) Start() error {
if s.scriptMode {
return errors.New("cannot start server in script mode")
}
logger.Info("Surf's up on port %s!", color.Apply(strconv.Itoa(s.Config.Server.Port), color.Cyan))
go func() {
if err := s.HTTPServer.ListenAndServe(fmt.Sprintf(":%d", s.Config.Server.Port)); err != nil {
if err.Error() != "http: Server closed" {
logger.Error("Server error: %v", err)
}
}
}()
return nil
}
// Shutdown gracefully shuts down Moonshark
func (s *Moonshark) Shutdown() error {
logger.Info("Shutting down...")
if !s.scriptMode && s.HTTPServer != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.HTTPServer.Shutdown(ctx); err != nil {
logger.Error("HTTP server shutdown error: %v", err)
}
}
for _, cleanup := range s.cleanupFuncs {
if err := cleanup(); err != nil {
logger.Warning("Cleanup error: %v", err)
}
}
if s.LuaRunner != nil {
s.LuaRunner.Close()
}
logger.Info("Shutdown complete")
return nil
}
func dirExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}
func initLuaRouter(s *Moonshark) error {
if !dirExists(s.Config.Dirs.Routes) {
return fmt.Errorf("routes directory doesn't exist: %s", s.Config.Dirs.Routes)
}
var err error
s.LuaRouter, err = router.NewLuaRouter(s.Config.Dirs.Routes)
if err != nil {
if errors.Is(err, router.ErrRoutesCompilationErrors) {
// Non-fatal, some routes failed
logger.Warning("Some routes failed to compile")
if failedRoutes := s.LuaRouter.ReportFailedRoutes(); len(failedRoutes) > 0 {
for _, re := range failedRoutes {
logger.Error("Route %s %s: %v", re.Method, re.Path, re.Err)
}
}
} else {
return fmt.Errorf("lua router init failed: %v", err)
}
}
logger.Info("LuaRouter is g2g! %s", color.Set(s.Config.Dirs.Routes, color.Yellow))
return nil
}
func initRunner(s *Moonshark) error {
if !dirExists(s.Config.Dirs.Override) {
logger.Warning("Override directory not found... %s", color.Apply(s.Config.Dirs.Override, color.Yellow))
s.Config.Dirs.Override = ""
}
for _, dir := range s.Config.Dirs.Libs {
if !dirExists(dir) {
logger.Warning("Lib directory not found... %s", color.Apply(dir, color.Yellow))
}
}
sessionManager := sessions.GlobalSessionManager
sessionManager.SetCookieOptions(
"MoonsharkSID",
"/",
"",
false,
true,
86400,
)
poolSize := s.Config.Runner.PoolSize
if s.scriptMode {
poolSize = 1 // Only need one state for script mode
}
runnerOpts := []runner.RunnerOption{
runner.WithPoolSize(poolSize),
runner.WithLibDirs(s.Config.Dirs.Libs...),
runner.WithFsDir(s.Config.Dirs.FS),
runner.WithDataDir(s.Config.Dirs.Data),
}
var err error
s.LuaRunner, err = runner.NewRunner(runnerOpts...)
if err != nil {
return fmt.Errorf("lua runner init failed: %v", err)
}
logger.Info("LuaRunner is g2g with %s states!", color.Apply(strconv.Itoa(poolSize), color.Yellow))
return nil
}
func setupWatchers(s *Moonshark) error {
manager := watchers.GetWatcherManager()
// Watch routes directory
routeWatcher, err := watchers.WatchLuaRouter(s.LuaRouter, s.LuaRunner, s.Config.Dirs.Routes)
if err != nil {
logger.Warning("Routes directory watch failed: %v", err)
} else {
routesDir := routeWatcher.GetDir()
s.cleanupFuncs = append(s.cleanupFuncs, func() error {
return manager.UnwatchDirectory(routesDir)
})
}
// Watch module directories
moduleWatchers, err := watchers.WatchLuaModules(s.LuaRunner, s.Config.Dirs.Libs)
if err != nil {
logger.Warning("Module directories watch failed: %v", err)
} else {
for _, watcher := range moduleWatchers {
dirPath := watcher.GetDir()
s.cleanupFuncs = append(s.cleanupFuncs, func() error {
return manager.UnwatchDirectory(dirPath)
})
}
plural := ""
if len(moduleWatchers) == 1 {
plural = "directory"
} else {
plural = "directories"
}
logger.Info("Watching %s module %s.", color.Apply(strconv.Itoa(len(moduleWatchers)), color.Yellow), plural)
}
return nil
}
func banner() {
banner := `
_____ _________.__ __
/ \ ____ ____ ____ / _____/| |__ _____ _______| | __
/ \ / \ / _ \ / _ \ / \ \_____ \ | | \\__ \\_ __ \ |/ /
/ Y ( <_> | <_> ) | \/ \| Y \/ __ \| | \/ <
\____|__ /\____/ \____/|___| /_______ /|___| (____ /__| |__|_ \
\/ \/ \/ \/ \/ \/
`
fmt.Println(color.Apply(banner, color.Blue))
}