Compare commits

..

5 Commits

Author SHA1 Message Date
b97febc16e move lua handler to new file lua_handler.go 2025-07-30 12:39:18 +03:00
149cfc0a17 sort imports 2025-07-30 12:38:53 +03:00
00276dc817 add hooks/ to fmt and goimports 2025-07-30 12:34:55 +03:00
ec2ef34f23 move initial hooks and Compositor obj to external package 2025-07-30 12:34:30 +03:00
aebc3d2e9b - move run hook to external package
- replace own contains function to slices.Contains
2025-07-30 12:33:51 +03:00
7 changed files with 485 additions and 459 deletions

View File

@@ -50,8 +50,10 @@ test:
fmt: fmt:
@go fmt ./internal/./... @go fmt ./internal/./...
@go fmt ./cmd/./... @go fmt ./cmd/./...
@go fmt ./hooks/./...
@$(GOPATH)/bin/goimports -w ./internal/ @$(GOPATH)/bin/goimports -w ./internal/
@$(GOPATH)/bin/goimports -w ./cmd/ @$(GOPATH)/bin/goimports -w ./cmd/
@$(GOPATH)/bin/goimports -w ./hooks/
vet: vet:
@go vet ./... @go vet ./...

View File

@@ -5,14 +5,12 @@ import (
"log" "log"
"os" "os"
"github.com/akyaiy/GoSally-mvp/hooks"
"github.com/akyaiy/GoSally-mvp/internal/core/corestate" "github.com/akyaiy/GoSally-mvp/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/internal/engine/logs" "github.com/akyaiy/GoSally-mvp/internal/engine/logs"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var compositor *config.Compositor = config.NewCompositor()
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "node", Use: "node",
Short: "Go Sally node", Short: "Go Sally node",
@@ -28,7 +26,7 @@ func Execute() {
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", corestate.StageNotReady))) log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", corestate.StageNotReady)))
log.SetFlags(log.Ldate | log.Ltime) log.SetFlags(log.Ldate | log.Ltime)
compositor.LoadCMDLine(rootCmd) hooks.Compositor.LoadCMDLine(rootCmd)
_ = rootCmd.Execute() _ = rootCmd.Execute()
// if err := rootCmd.Execute(); err != nil { // if err := rootCmd.Execute(); err != nil {
// log.Fatalf("Unexpected error: %s", err.Error()) // log.Fatalf("Unexpected error: %s", err.Error())

View File

@@ -1,349 +1,17 @@
package cmd package cmd
import ( import (
"context" "github.com/akyaiy/GoSally-mvp/hooks"
"errors"
"fmt"
"io"
"io/fs"
"log"
"log/slog"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"syscall"
"time"
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/internal/core/run_manager"
"github.com/akyaiy/GoSally-mvp/internal/core/update"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/internal/engine/logs"
"github.com/akyaiy/GoSally-mvp/internal/server/gateway"
"github.com/akyaiy/GoSally-mvp/internal/server/sv1"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/net/netutil"
"gopkg.in/ini.v1"
) )
func contains(slice []string, item string) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
var runCmd = &cobra.Command{ var runCmd = &cobra.Command{
Use: "run", Use: "run",
Aliases: []string{"r"}, Aliases: []string{"r"},
Short: "Run node normally", Short: "Run node normally",
Long: ` Long: `
"run" starts the node with settings depending on the configuration file`, "run" starts the node with settings depending on the configuration file`,
Run: func(cmd *cobra.Command, args []string) { Run: hooks.Run,
nodeApp := app.New()
nodeApp.InitialHooks(
func(cs *corestate.CoreState, x *app.AppX) {
x.Config = compositor
x.Log.SetOutput(os.Stdout)
x.Log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", cs.Stage)))
x.Log.SetFlags(log.Ldate | log.Ltime)
},
// First stage: pre-init
func(cs *corestate.CoreState, x *app.AppX) {
*cs = *corestate.NewCorestate(&corestate.CoreState{
UUID32DirName: "uuid",
NodeBinName: filepath.Base(os.Args[0]),
NodeVersion: config.NodeVersion,
MetaDir: "./.meta",
Stage: corestate.StagePreInit,
StartTimestampUnix: time.Now().Unix(),
})
},
func(cs *corestate.CoreState, x *app.AppX) {
x.Log.SetPrefix(logs.SetBlue(fmt.Sprintf("(%s) ", cs.Stage)))
if err := x.Config.LoadEnv(); err != nil {
x.Log.Fatalf("env load error: %s", err)
}
cs.NodePath = x.Config.Env.NodePath
if cfgPath := x.Config.CMDLine.Run.ConfigPath; cfgPath != "" {
x.Config.Env.ConfigPath = cfgPath
}
if err := x.Config.LoadConf(x.Config.Env.ConfigPath); err != nil {
x.Log.Fatalf("conf load error: %s", err)
}
},
func(cs *corestate.CoreState, x *app.AppX) {
uuid32, err := corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
if errors.Is(err, fs.ErrNotExist) {
if err := corestate.SetNodeUUID(filepath.Join(cs.NodePath, cs.MetaDir, cs.UUID32DirName)); err != nil {
x.Log.Fatalf("Cannod generate node uuid: %s", err.Error())
}
uuid32, err = corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
if err != nil {
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
}
if err != nil {
x.Log.Fatalf("uuid load error: %s", err)
}
cs.UUID32 = uuid32
},
func(cs *corestate.CoreState, x *app.AppX) {
if x.Config.Env.ParentStagePID != os.Getpid() {
if !contains(x.Config.Conf.DisableWarnings, "--WNonStdTmpDir") && os.TempDir() != "/tmp" {
x.Log.Printf("%s: %s", logs.PrintWarn(), "Non-standard value specified for temporary directory")
}
// still pre-init stage
runDir, err := run_manager.Create(cs.UUID32)
if err != nil {
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
cs.RunDir = runDir
input, err := os.Open(os.Args[0])
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if err := run_manager.Set(cs.NodeBinName); err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
fmgr := run_manager.File(cs.NodeBinName)
output, err := fmgr.Open()
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if _, err := io.Copy(output, input); err != nil {
fmgr.Close()
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if err := os.Chmod(filepath.Join(cs.RunDir, cs.NodeBinName), 0755); err != nil {
fmgr.Close()
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
input.Close()
fmgr.Close()
runArgs := os.Args
runArgs[0] = filepath.Join(cs.RunDir, cs.NodeBinName)
// prepare environ
env := utils.SetEviron(os.Environ(), fmt.Sprintf("GS_PARENT_PID=%d", os.Getpid()))
if err := syscall.Exec(runArgs[0], runArgs, env); err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
}
x.Log.Printf("Node uuid is %s", cs.UUID32)
},
// post-init stage
func(cs *corestate.CoreState, x *app.AppX) {
cs.Stage = corestate.StagePostInit
x.Log.SetPrefix(logs.SetYellow(fmt.Sprintf("(%s) ", cs.Stage)))
cs.RunDir = run_manager.Toggle()
exist, err := utils.ExistsMatchingDirs(filepath.Join(os.TempDir(), fmt.Sprintf("/*-%s-%s", cs.UUID32, "gosally-runtime")), cs.RunDir)
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if exist {
_ = run_manager.Clean()
x.Log.Fatalf("Unable to continue node operation: A node with the same identifier was found in the runtime environment")
}
if err := run_manager.Set("run.lock"); err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
lockPath, err := run_manager.Get("run.lock")
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
lockFile := ini.Empty()
secRun, err := lockFile.NewSection("runtime")
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
secRun.Key("pid").SetValue(fmt.Sprintf("%d/%d", os.Getpid(), x.Config.Env.ParentStagePID))
secRun.Key("version").SetValue(cs.NodeVersion)
secRun.Key("uuid").SetValue(cs.UUID32)
secRun.Key("timestamp").SetValue(time.Unix(cs.StartTimestampUnix, 0).Format("2006-01-02/15:04:05 MST"))
secRun.Key("timestamp-unix").SetValue(fmt.Sprintf("%d", cs.StartTimestampUnix))
err = lockFile.SaveTo(lockPath)
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
},
func(cs *corestate.CoreState, x *app.AppX) {
cs.Stage = corestate.StageReady
x.Log.SetPrefix(logs.SetGreen(fmt.Sprintf("(%s) ", cs.Stage)))
x.SLog = new(slog.Logger)
newSlog, err := logs.SetupLogger(x.Config.Conf.Log)
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
*x.SLog = *newSlog
},
)
nodeApp.Run(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
ctxMain, cancelMain := context.WithCancel(ctx)
runLockFile := run_manager.File("run.lock")
_, err := runLockFile.Open()
if err != nil {
x.Log.Fatalf("cannot open run.lock: %s", err)
}
_, err = runLockFile.Watch(ctxMain, func() {
x.Log.Printf("run.lock was touched")
_ = run_manager.Clean()
cancelMain()
})
if err != nil {
x.Log.Printf("watch error: %s", err)
}
serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{
X: x,
CS: cs,
AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+(>[a-zA-Z0-9]+)*$`),
Ver: "v1",
})
s := gateway.InitGateway(&gateway.GatewayServerInit{
CS: cs,
X: x,
}, serverv1)
r := chi.NewRouter()
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
AllowCredentials: true,
MaxAge: 300,
}))
r.HandleFunc(config.ComDirRoute, s.Handle)
r.Route("/favicon.ico", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
})
srv := &http.Server{
Addr: x.Config.Conf.HTTPServer.Address,
Handler: r,
ErrorLog: log.New(&logs.SlogWriter{
Logger: x.SLog,
Level: logs.GlobalLevel,
}, "", 0),
}
nodeApp.Fallback(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) {
if err := srv.Shutdown(ctxMain); err != nil {
x.Log.Printf("%s: Failed to stop the server gracefully: %s", logs.PrintError(), err.Error())
} else {
x.Log.Printf("Server stopped gracefully")
}
x.Log.Println("Cleaning up...")
if err := run_manager.Clean(); err != nil {
x.Log.Printf("%s: Cleanup error: %s", logs.PrintError(), err.Error())
}
x.Log.Println("bye!")
})
go func() {
defer utils.CatchPanicWithCancel(cancelMain)
if x.Config.Conf.TLS.TlsEnabled {
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
if err != nil {
x.Log.Printf("%s: Failed to start TLS listener: %s", logs.PrintError(), err.Error())
cancelMain()
return
}
x.Log.Printf("Serving on %s port %s with TLS... (https://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
limitedListener := netutil.LimitListener(listener, 100)
if err := srv.ServeTLS(limitedListener, x.Config.Conf.TLS.CertFile, x.Config.Conf.TLS.KeyFile); err != nil && !errors.Is(err, http.ErrServerClosed) {
x.Log.Printf("%s: Failed to start HTTPS server: %s", logs.PrintError(), err.Error())
cancelMain()
}
} else {
x.Log.Printf("Serving on %s port %s... (http://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
if err != nil {
x.Log.Printf("%s: Failed to start listener: %s", logs.PrintError(), err.Error())
cancelMain()
return
}
limitedListener := netutil.LimitListener(listener, 100)
if err := srv.Serve(limitedListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
x.Log.Printf("%s: Failed to start HTTP server: %s", logs.PrintError(), err.Error())
cancelMain()
}
}
}()
if x.Config.Conf.Updates.UpdatesEnabled {
go func() {
defer utils.CatchPanicWithCancel(cancelMain)
updated := update.NewUpdater(&update.UpdaterInit{
X: x,
Ctx: ctxMain,
Cancel: cancelMain,
})
updated.Shutdownfunc(cancelMain)
for {
isNewUpdate, err := updated.CkeckUpdates()
if err != nil {
x.Log.Printf("Failed to check for updates: %s", err.Error())
}
if isNewUpdate {
if err := updated.Update(); err != nil {
x.Log.Printf("Failed to update: %s", err.Error())
} else {
x.Log.Printf("Update completed successfully")
}
}
time.Sleep(x.Config.Conf.Updates.CheckInterval)
}
}()
}
<-ctxMain.Done()
nodeApp.CallFallback(ctx)
return nil
})
},
} }
func init() { func init() {

187
hooks/initial.go Normal file
View File

@@ -0,0 +1,187 @@
package hooks
import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"log/slog"
"os"
"path/filepath"
"slices"
"syscall"
"time"
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/internal/core/run_manager"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/internal/engine/logs"
"gopkg.in/ini.v1"
)
var Compositor *config.Compositor = config.NewCompositor()
func Init0Hook(cs *corestate.CoreState, x *app.AppX) {
x.Config = Compositor
x.Log.SetOutput(os.Stdout)
x.Log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", cs.Stage)))
x.Log.SetFlags(log.Ldate | log.Ltime)
}
// First stage: pre-init
func Init1Hook(cs *corestate.CoreState, x *app.AppX) {
*cs = *corestate.NewCorestate(&corestate.CoreState{
UUID32DirName: "uuid",
NodeBinName: filepath.Base(os.Args[0]),
NodeVersion: config.NodeVersion,
MetaDir: "./.meta",
Stage: corestate.StagePreInit,
StartTimestampUnix: time.Now().Unix(),
})
}
func Init2Hook(cs *corestate.CoreState, x *app.AppX) {
x.Log.SetPrefix(logs.SetBlue(fmt.Sprintf("(%s) ", cs.Stage)))
if err := x.Config.LoadEnv(); err != nil {
x.Log.Fatalf("env load error: %s", err)
}
cs.NodePath = x.Config.Env.NodePath
if cfgPath := x.Config.CMDLine.Run.ConfigPath; cfgPath != "" {
x.Config.Env.ConfigPath = cfgPath
}
if err := x.Config.LoadConf(x.Config.Env.ConfigPath); err != nil {
x.Log.Fatalf("conf load error: %s", err)
}
}
func Init3Hook(cs *corestate.CoreState, x *app.AppX) {
uuid32, err := corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
if errors.Is(err, fs.ErrNotExist) {
if err := corestate.SetNodeUUID(filepath.Join(cs.NodePath, cs.MetaDir, cs.UUID32DirName)); err != nil {
x.Log.Fatalf("Cannod generate node uuid: %s", err.Error())
}
uuid32, err = corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
if err != nil {
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
}
if err != nil {
x.Log.Fatalf("uuid load error: %s", err)
}
cs.UUID32 = uuid32
}
func Init4Hook(cs *corestate.CoreState, x *app.AppX) {
if x.Config.Env.ParentStagePID != os.Getpid() {
if !slices.Contains(x.Config.Conf.DisableWarnings, "--WNonStdTmpDir") && os.TempDir() != "/tmp" {
x.Log.Printf("%s: %s", logs.PrintWarn(), "Non-standard value specified for temporary directory")
}
// still pre-init stage
runDir, err := run_manager.Create(cs.UUID32)
if err != nil {
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
cs.RunDir = runDir
input, err := os.Open(os.Args[0])
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if err := run_manager.Set(cs.NodeBinName); err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
fmgr := run_manager.File(cs.NodeBinName)
output, err := fmgr.Open()
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if _, err := io.Copy(output, input); err != nil {
fmgr.Close()
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if err := os.Chmod(filepath.Join(cs.RunDir, cs.NodeBinName), 0755); err != nil {
fmgr.Close()
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
input.Close()
fmgr.Close()
runArgs := os.Args
runArgs[0] = filepath.Join(cs.RunDir, cs.NodeBinName)
// prepare environ
env := utils.SetEviron(os.Environ(), fmt.Sprintf("GS_PARENT_PID=%d", os.Getpid()))
if err := syscall.Exec(runArgs[0], runArgs, env); err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
}
x.Log.Printf("Node uuid is %s", cs.UUID32)
}
// post-init stage
func Init5Hook(cs *corestate.CoreState, x *app.AppX) {
cs.Stage = corestate.StagePostInit
x.Log.SetPrefix(logs.SetYellow(fmt.Sprintf("(%s) ", cs.Stage)))
cs.RunDir = run_manager.Toggle()
exist, err := utils.ExistsMatchingDirs(filepath.Join(os.TempDir(), fmt.Sprintf("/*-%s-%s", cs.UUID32, "gosally-runtime")), cs.RunDir)
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
if exist {
_ = run_manager.Clean()
x.Log.Fatalf("Unable to continue node operation: A node with the same identifier was found in the runtime environment")
}
if err := run_manager.Set("run.lock"); err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
lockPath, err := run_manager.Get("run.lock")
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
lockFile := ini.Empty()
secRun, err := lockFile.NewSection("runtime")
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
secRun.Key("pid").SetValue(fmt.Sprintf("%d/%d", os.Getpid(), x.Config.Env.ParentStagePID))
secRun.Key("version").SetValue(cs.NodeVersion)
secRun.Key("uuid").SetValue(cs.UUID32)
secRun.Key("timestamp").SetValue(time.Unix(cs.StartTimestampUnix, 0).Format("2006-01-02/15:04:05 MST"))
secRun.Key("timestamp-unix").SetValue(fmt.Sprintf("%d", cs.StartTimestampUnix))
err = lockFile.SaveTo(lockPath)
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
}
func Init6Hook(cs *corestate.CoreState, x *app.AppX) {
cs.Stage = corestate.StageReady
x.Log.SetPrefix(logs.SetGreen(fmt.Sprintf("(%s) ", cs.Stage)))
x.SLog = new(slog.Logger)
newSlog, err := logs.SetupLogger(x.Config.Conf.Log)
if err != nil {
_ = run_manager.Clean()
x.Log.Fatalf("Unexpected failure: %s", err.Error())
}
*x.SLog = *newSlog
}

164
hooks/run.go Normal file
View File

@@ -0,0 +1,164 @@
package hooks
import (
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
"regexp"
"time"
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/internal/core/run_manager"
"github.com/akyaiy/GoSally-mvp/internal/core/update"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/internal/engine/logs"
"github.com/akyaiy/GoSally-mvp/internal/server/gateway"
"github.com/akyaiy/GoSally-mvp/internal/server/sv1"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/spf13/cobra"
"golang.org/x/net/netutil"
)
var nodeApp = app.New()
func Run(cmd *cobra.Command, args []string) {
nodeApp.InitialHooks()
nodeApp.Run(RunHook)
}
func RunHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
ctxMain, cancelMain := context.WithCancel(ctx)
runLockFile := run_manager.File("run.lock")
_, err := runLockFile.Open()
if err != nil {
x.Log.Fatalf("cannot open run.lock: %s", err)
}
_, err = runLockFile.Watch(ctxMain, func() {
x.Log.Printf("run.lock was touched")
_ = run_manager.Clean()
cancelMain()
})
if err != nil {
x.Log.Printf("watch error: %s", err)
}
serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{
X: x,
CS: cs,
AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+(>[a-zA-Z0-9]+)*$`),
Ver: "v1",
})
s := gateway.InitGateway(&gateway.GatewayServerInit{
CS: cs,
X: x,
}, serverv1)
r := chi.NewRouter()
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
AllowCredentials: true,
MaxAge: 300,
}))
r.HandleFunc(config.ComDirRoute, s.Handle)
r.Route("/favicon.ico", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
})
srv := &http.Server{
Addr: x.Config.Conf.HTTPServer.Address,
Handler: r,
ErrorLog: log.New(&logs.SlogWriter{
Logger: x.SLog,
Level: logs.GlobalLevel,
}, "", 0),
}
nodeApp.Fallback(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) {
if err := srv.Shutdown(ctxMain); err != nil {
x.Log.Printf("%s: Failed to stop the server gracefully: %s", logs.PrintError(), err.Error())
} else {
x.Log.Printf("Server stopped gracefully")
}
x.Log.Println("Cleaning up...")
if err := run_manager.Clean(); err != nil {
x.Log.Printf("%s: Cleanup error: %s", logs.PrintError(), err.Error())
}
x.Log.Println("bye!")
})
go func() {
defer utils.CatchPanicWithCancel(cancelMain)
if x.Config.Conf.TLS.TlsEnabled {
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
if err != nil {
x.Log.Printf("%s: Failed to start TLS listener: %s", logs.PrintError(), err.Error())
cancelMain()
return
}
x.Log.Printf("Serving on %s port %s with TLS... (https://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
limitedListener := netutil.LimitListener(listener, 100)
if err := srv.ServeTLS(limitedListener, x.Config.Conf.TLS.CertFile, x.Config.Conf.TLS.KeyFile); err != nil && !errors.Is(err, http.ErrServerClosed) {
x.Log.Printf("%s: Failed to start HTTPS server: %s", logs.PrintError(), err.Error())
cancelMain()
}
} else {
x.Log.Printf("Serving on %s port %s... (http://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
if err != nil {
x.Log.Printf("%s: Failed to start listener: %s", logs.PrintError(), err.Error())
cancelMain()
return
}
limitedListener := netutil.LimitListener(listener, 100)
if err := srv.Serve(limitedListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
x.Log.Printf("%s: Failed to start HTTP server: %s", logs.PrintError(), err.Error())
cancelMain()
}
}
}()
if x.Config.Conf.Updates.UpdatesEnabled {
go func() {
defer utils.CatchPanicWithCancel(cancelMain)
updated := update.NewUpdater(&update.UpdaterInit{
X: x,
Ctx: ctxMain,
Cancel: cancelMain,
})
updated.Shutdownfunc(cancelMain)
for {
isNewUpdate, err := updated.CkeckUpdates()
if err != nil {
x.Log.Printf("Failed to check for updates: %s", err.Error())
}
if isNewUpdate {
if err := updated.Update(); err != nil {
x.Log.Printf("Failed to update: %s", err.Error())
} else {
x.Log.Printf("Update completed successfully")
}
}
time.Sleep(x.Config.Conf.Updates.CheckInterval)
}
}()
}
<-ctxMain.Done()
nodeApp.CallFallback(ctx)
return nil
}

View File

@@ -1,17 +1,10 @@
package sv1 package sv1
import ( import (
"fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"os"
"path/filepath"
"strconv"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/internal/engine/logs"
"github.com/akyaiy/GoSally-mvp/internal/server/rpc" "github.com/akyaiy/GoSally-mvp/internal/server/rpc"
lua "github.com/yuin/gopher-lua"
) )
func (h *HandlerV1) Handle(r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse { func (h *HandlerV1) Handle(r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse {
@@ -33,117 +26,3 @@ func (h *HandlerV1) Handle(r *http.Request, req *rpc.RPCRequest) *rpc.RPCRespons
return h.HandleLUA(method, req) return h.HandleLUA(method, req)
} }
func (h *HandlerV1) HandleLUA(path string, req *rpc.RPCRequest) *rpc.RPCResponse {
L := lua.NewState()
defer L.Close()
inTable := L.NewTable()
paramsTable := L.NewTable()
if fetchedParams, ok := req.Params.(map[string]any); ok {
for k, v := range fetchedParams {
L.SetField(paramsTable, k, utils.ConvertGolangTypesToLua(L, v))
}
}
L.SetField(inTable, "Params", paramsTable)
L.SetGlobal("In", inTable)
outTable := L.NewTable()
resultTable := L.NewTable()
L.SetField(outTable, "Result", resultTable)
L.SetGlobal("Out", outTable)
logTable := L.NewTable()
L.SetField(logTable, "Info", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.SLog.Info(fmt.Sprintf("the script says: %s", msg), slog.String("script", path))
return 0
}))
L.SetField(logTable, "Debug", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.SLog.Debug(fmt.Sprintf("the script says: %s", msg), slog.String("script", path))
return 0
}))
L.SetField(logTable, "Error", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.SLog.Error(fmt.Sprintf("the script says: %s", msg), slog.String("script", path))
return 0
}))
L.SetField(logTable, "Warn", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.SLog.Warn(fmt.Sprintf("the script says: %s", msg), slog.String("script", path))
return 0
}))
L.SetField(logTable, "Event", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.Log.Printf("%s: %s", path, msg)
return 0
}))
L.SetField(logTable, "EventError", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.Log.Printf("%s: %s: %s", logs.PrintError(), path, msg)
return 0
}))
L.SetField(logTable, "EventWarn", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.Log.Printf("%s: %s: %s", logs.PrintWarn(), path, msg)
return 0
}))
L.SetGlobal("Log", logTable)
prep := filepath.Join(h.x.Config.Conf.ComDir, "_prepare.lua")
if _, err := os.Stat(prep); err == nil {
if err := L.DoFile(prep); err != nil {
return rpc.NewError(rpc.ErrInternalError, err.Error(), req.ID)
}
}
if err := L.DoFile(path); err != nil {
return rpc.NewError(rpc.ErrInternalError, err.Error(), req.ID)
}
lv := L.GetGlobal("Out")
outTbl, ok := lv.(*lua.LTable)
if !ok {
return rpc.NewError(rpc.ErrInternalError, "Out is not a table", req.ID)
}
// Check if Out.Error exists
if errVal := outTbl.RawGetString("Error"); errVal != lua.LNil {
if errTbl, ok := errVal.(*lua.LTable); ok {
code := rpc.ErrInternalError
message := "Internal script error"
if c := errTbl.RawGetString("code"); c.Type() == lua.LTNumber {
code = int(c.(lua.LNumber))
}
if msg := errTbl.RawGetString("message"); msg.Type() == lua.LTString {
message = msg.String()
}
h.x.SLog.Error("the script terminated with an error", slog.String("code", strconv.Itoa(code)), slog.String("message", message))
return rpc.NewError(code, message, req.ID)
}
return rpc.NewError(rpc.ErrInternalError, "Out.Error is not a table", req.ID)
}
// Otherwise, parse Out.Result
resultVal := outTbl.RawGetString("Result")
resultTbl, ok := resultVal.(*lua.LTable)
if !ok {
return rpc.NewError(rpc.ErrInternalError, "Out.Result is not a table", req.ID)
}
out := make(map[string]any)
resultTbl.ForEach(func(key lua.LValue, value lua.LValue) {
out[key.String()] = utils.ConvertLuaTypesToGolang(value)
})
out["responsible-node"] = h.cs.UUID32
return rpc.NewResponse(out, req.ID)
}

View File

@@ -0,0 +1,128 @@
package sv1
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"strconv"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/internal/engine/logs"
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
lua "github.com/yuin/gopher-lua"
)
func (h *HandlerV1) HandleLUA(path string, req *rpc.RPCRequest) *rpc.RPCResponse {
L := lua.NewState()
defer L.Close()
inTable := L.NewTable()
paramsTable := L.NewTable()
if fetchedParams, ok := req.Params.(map[string]any); ok {
for k, v := range fetchedParams {
L.SetField(paramsTable, k, utils.ConvertGolangTypesToLua(L, v))
}
}
L.SetField(inTable, "Params", paramsTable)
L.SetGlobal("In", inTable)
outTable := L.NewTable()
resultTable := L.NewTable()
L.SetField(outTable, "Result", resultTable)
L.SetGlobal("Out", outTable)
logTable := L.NewTable()
L.SetField(logTable, "Info", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.SLog.Info(fmt.Sprintf("the script says: %s", msg), slog.String("script", path))
return 0
}))
L.SetField(logTable, "Debug", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.SLog.Debug(fmt.Sprintf("the script says: %s", msg), slog.String("script", path))
return 0
}))
L.SetField(logTable, "Error", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.SLog.Error(fmt.Sprintf("the script says: %s", msg), slog.String("script", path))
return 0
}))
L.SetField(logTable, "Warn", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.SLog.Warn(fmt.Sprintf("the script says: %s", msg), slog.String("script", path))
return 0
}))
L.SetField(logTable, "Event", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.Log.Printf("%s: %s", path, msg)
return 0
}))
L.SetField(logTable, "EventError", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.Log.Printf("%s: %s: %s", logs.PrintError(), path, msg)
return 0
}))
L.SetField(logTable, "EventWarn", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.Log.Printf("%s: %s: %s", logs.PrintWarn(), path, msg)
return 0
}))
L.SetGlobal("Log", logTable)
prep := filepath.Join(h.x.Config.Conf.ComDir, "_prepare.lua")
if _, err := os.Stat(prep); err == nil {
if err := L.DoFile(prep); err != nil {
return rpc.NewError(rpc.ErrInternalError, err.Error(), req.ID)
}
}
if err := L.DoFile(path); err != nil {
return rpc.NewError(rpc.ErrInternalError, err.Error(), req.ID)
}
lv := L.GetGlobal("Out")
outTbl, ok := lv.(*lua.LTable)
if !ok {
return rpc.NewError(rpc.ErrInternalError, "Out is not a table", req.ID)
}
// Check if Out.Error exists
if errVal := outTbl.RawGetString("Error"); errVal != lua.LNil {
if errTbl, ok := errVal.(*lua.LTable); ok {
code := rpc.ErrInternalError
message := "Internal script error"
if c := errTbl.RawGetString("code"); c.Type() == lua.LTNumber {
code = int(c.(lua.LNumber))
}
if msg := errTbl.RawGetString("message"); msg.Type() == lua.LTString {
message = msg.String()
}
h.x.SLog.Error("the script terminated with an error", slog.String("code", strconv.Itoa(code)), slog.String("message", message))
return rpc.NewError(code, message, req.ID)
}
return rpc.NewError(rpc.ErrInternalError, "Out.Error is not a table", req.ID)
}
// Otherwise, parse Out.Result
resultVal := outTbl.RawGetString("Result")
resultTbl, ok := resultVal.(*lua.LTable)
if !ok {
return rpc.NewError(rpc.ErrInternalError, "Out.Result is not a table", req.ID)
}
out := make(map[string]any)
resultTbl.ForEach(func(key lua.LValue, value lua.LValue) {
out[key.String()] = utils.ConvertLuaTypesToGolang(value)
})
out["responsible-node"] = h.cs.UUID32
return rpc.NewResponse(out, req.ID)
}