Compare commits

40 Commits

Author SHA1 Message Date
Aleksey
a6c9e5102f Merge branch 'main' into dev 2025-08-04 15:13:55 +03:00
a72627d87c delete table creation 2025-08-04 13:38:27 +03:00
4a9719cdfb sqlite usage example 2025-08-04 13:36:38 +03:00
7de5ec5248 add sqlite support 2025-08-04 13:36:30 +03:00
e5f9105364 change cors 2025-08-04 13:36:21 +03:00
ce2a23f9e6 make internal modules in "internal" 2025-08-02 16:22:05 +03:00
d56b022bf5 add com/test.lua to .gitignore 2025-08-02 16:13:22 +03:00
ca38c10ec4 made creation of private field before pushing 2025-08-02 16:12:44 +03:00
13dbd00bb7 update config exaple 2025-08-02 16:12:08 +03:00
e7289dc9be update post example 2025-08-02 11:50:19 +03:00
5394178abc update lua logging 2025-08-02 11:50:09 +03:00
981551e944 change default log output to stderr 2025-08-02 10:14:35 +03:00
27446adf3f add debug information to lua_handler and route 2025-08-02 10:04:00 +03:00
2f071c25b2 update annotations 2025-08-02 01:02:59 +03:00
d23fd32e84 update annotations 2025-08-02 01:01:05 +03:00
86d35a9ede update annotations 2025-08-02 00:55:59 +03:00
c77d51a95c some changes with config.log.output 2025-08-02 00:53:19 +03:00
3cbea14e84 fmt 2025-08-02 00:00:43 +03:00
6e59af1662 add json_format option for structure logging 2025-08-02 00:00:35 +03:00
8684d178e0 update Get example 2025-08-01 23:54:44 +03:00
945ab6c9cf update annotations 2025-08-01 23:53:40 +03:00
520901c331 update script error handling 2025-08-01 23:53:30 +03:00
9a274250cd update annotations 2025-08-01 23:25:46 +03:00
6d49d83ea7 update List.lua 2025-08-01 23:25:36 +03:00
fb04b3bc46 change in/out to request/response 2025-08-01 23:25:24 +03:00
a60b75a4c0 make lua deps modular 2025-08-01 23:18:45 +03:00
041fda8522 change ErrSessionIsTaken to EssSessionIsBusy 2025-08-01 20:07:15 +03:00
6508f03d08 fmt 2025-08-01 13:12:57 +03:00
93cf53025c rename init hooks names 2025-08-01 13:12:27 +03:00
83912b6c28 add context TODO 2025-08-01 12:56:09 +03:00
6ed5a7f9e0 add context to Handle method 2025-08-01 12:55:58 +03:00
2f78e9367c update logger initialization 2025-08-01 12:55:35 +03:00
ac074ce0ff change mapstructure to output 2025-08-01 12:55:17 +03:00
8bdf9197d6 set default config value to stdout 2025-08-01 12:54:58 +03:00
4db8fa2360 change fallback os exit to 0 2025-08-01 12:54:41 +03:00
2a48927a08 update hooks 2025-08-01 12:54:28 +03:00
58027bb988 add config processing 2025-08-01 12:54:18 +03:00
30a87fdb4c update List.lua 2025-08-01 12:53:25 +03:00
f3c4b9e9b1 update config example 2025-07-30 12:02:12 +03:00
Aleksey
81359c036c Merge pull request #1 from akyaiy/dev
Dev
2025-07-29 16:51:18 +03:00
19 changed files with 983 additions and 318 deletions

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@ tmp/
.meta/ .meta/
db/ db/
com/test.lua
.vscode .vscode
Taskfile.yml Taskfile.yml
config.yaml config.yaml

66
com/DB/Put.lua Normal file
View File

@@ -0,0 +1,66 @@
---@diagnostic disable: redefined-local
local db = require("internal.database-sqlite").connect("db/test.db", {log = true})
local log = require("internal.log")
local session = require("internal.session")
if not (session.request.params.name and session.request.params.email) then
session.response.error = {
code = -32602,
message = "Name and email are required"
}
return
end
local existing, err = db:query("SELECT 1 FROM users WHERE email = ? LIMIT 1", {
session.request.params.email
})
if err ~= nil then
session.response.error = {
code = -32603,
message = "Database check failed: "..tostring(err)
}
log.error("Email check failed: "..tostring(err))
return
end
if existing and #existing > 0 then
session.response.error = {
code = -32604,
message = "Email already exists"
}
return
end
local ctx, err = db:exec(
"INSERT INTO users (name, email) VALUES (?, ?)",
{
session.request.params.name,
session.request.params.email
}
)
if err ~= nil then
session.response.error = {
code = -32605,
message = "Insert failed: "..tostring(err)
}
log.error("Insert failed: "..tostring(err))
return
end
local res, err = ctx:wait()
if err ~= nil then
session.response.error = {
code = -32606,
message = "Insert confirmation failed: "..tostring(err)
}
log.error("Insert confirmation failed: "..tostring(err))
return
end
session.response.result = {
success = true,
rows_affected = res,
message = "User created successfully"
}
db:close()

View File

@@ -1,7 +1,9 @@
-- com/List.lua -- com/List.lua
if In.Params and In.Params.about then local session = require("internal.session")
Out.Result = {
if session.request and session.request.params and session.request.params.about then
session.response.result = {
description = "Returns a list of available methods", description = "Returns a list of available methods",
params = { params = {
layer = "select which layer list to display" layer = "select which layer list to display"
@@ -10,7 +12,7 @@ if In.Params and In.Params.about then
return return
end end
local function isValidCommand(name) local function isValidName(name)
return name:match("^[%w]+$") ~= nil return name:match("^[%w]+$") ~= nil
end end
@@ -21,8 +23,20 @@ local function scanDirectory(basePath, targetPath)
if handle then if handle then
for filePath in handle:lines() do for filePath in handle:lines() do
local fileName = filePath:match("([^/]+)%.lua$") local parts = {}
if fileName and isValidCommand(fileName) then for part in filePath:gsub(".lua$", ""):gmatch("[^/]+") do
table.insert(parts, part)
end
local allValid = true
for _, part in ipairs(parts) do
if not isValidName(part) then
allValid = false
break
end
end
if allValid then
local relPath = filePath:gsub("^"..basePath.."/", ""):gsub(".lua$", ""):gsub("/", ">") local relPath = filePath:gsub("^"..basePath.."/", ""):gsub(".lua$", ""):gsub("/", ">")
table.insert(res, relPath) table.insert(res, relPath)
end end
@@ -34,8 +48,8 @@ local function scanDirectory(basePath, targetPath)
end end
local basePath = "com" local basePath = "com"
local layer = In.Params and In.Params.layer and In.Params.layer:gsub(">", "/") or nil local layer = session.request and session.request.params.layer and session.request.params.layer:gsub(">", "/") or nil
Out.Result = { session.response.result = {
answer = layer and scanDirectory(basePath, layer) or scanDirectory(basePath, "") answer = layer and scanDirectory(basePath, layer) or scanDirectory(basePath, "")
} }

View File

@@ -1,26 +1,29 @@
local session = require("internal.session")
local net = require("internal.net")
local reqAddr local reqAddr
local logReq = true local logReq = true
if In.Params and In.Params.url then if session.request.params and session.request.params.url then
reqAddr = In.Params.url reqAddr = session.request.params.url
else else
Out.Error = { session.response.error = {
code = -32602, code = -32602,
message = "no url provided" message = "no url provided"
} }
return return
end end
local resp = Net.Http.Get(logReq, reqAddr) local resp = net.http.get_request(logReq, reqAddr)
if resp then if resp then
Out.Result.answer = { session.response.result.answer = {
status = resp.status, status = resp.status,
body = resp.body body = resp.body
} }
return return
end end
Out.Result.answer = { session.response.error = {
status = resp.status data = "error while requesting"
} }

View File

@@ -1,27 +1,35 @@
local session = require("internal.session")
local net = require("internal.net")
local log = require("internal.log")
local reqAddr local reqAddr
local logReq = true local logReq = true
local payload local payload
if not In.Params and In.Params.url or not In.Params.payload then log.debug(session.request.params)
Out.Error = {
if not (session.request.params and session.request.params.url) then
session.response.error = {
code = -32602, code = -32602,
message = "no url or payload provided" message = "no url or payload provided"
} }
return return
end end
reqAddr = In.Params.url
payload = In.Params.payload
local resp = Net.Http.Post(logReq, reqAddr, "application/json", payload)
reqAddr = session.request.params.url
payload = session.request.params.payload
local resp = net.http.post_request(logReq, reqAddr, "application/json", payload)
if resp then if resp then
Out.Result.answer = { session.response.result.answer = {
status = resp.status, status = resp.status,
body = resp.body body = resp.body
} }
return return
end end
Out.Result.answer = { session.response.error = {
status = resp.status data = "error while requesting"
} }

View File

@@ -1,59 +1,54 @@
---@diagnostic disable: missing-fields, missing-return --@diagnostic disable: missing-fields, missing-return
---@alias AnyTable table<string, any>
---@type AnyTable ---@alias Any any
In = { ---@alias AnyTable table<string, Any>
Params = {},
}
---@type AnyTable --- Global session module interface
Out = { ---@class SessionIn
Result = {}, ---@field params AnyTable Request parameters
}
---@class Log ---@class SessionOut
---@field Info fun(msg: string) ---@field result Any|string? Result payload (table or primitive)
---@field Debug fun(msg: string) ---@field error { code: integer, message: string, data: Any }? Optional error info
---@field Error fun(msg: string)
---@field Warn fun(msg: string)
---@field Event fun(msg: string)
---@field EventError fun(msg: string)
---@field EventWarn fun(msg: string)
---@type Log ---@class SessionModule
Log = {} ---@field request SessionIn Input context (read-only)
---@field response SessionOut Output context (write results/errors)
--- Global log module interface
---@class LogModule
---@field info fun(msg: string) Log informational message
---@field debug fun(msg: string) Log debug message
---@field error fun(msg: string) Log error message
---@field warn fun(msg: string) Log warning message
---@field event fun(msg: string) Log event (generic)
---@field event_error fun(msg: string) Log event error
---@field event_warn fun(msg: string) Log event warning
--- Global net module interface
---@class HttpResponse ---@class HttpResponse
---@field status integer HTTP status code ---@field status integer HTTP status code
---@field status_text string HTTP status text ---@field status_text string HTTP status text
---@field body string Response body ---@field body string Response body
---@field content_length integer Content length ---@field content_length integer Content length
---@field headers table<string, string|string[]> Response headers ---@field headers AnyTable Map of headers
---@class Http ---@class HttpModule
---@field Get fun(log: boolean, url: string): HttpResponse, string? Makes HTTP GET request ---@field get fun(log: boolean, url: string): HttpResponse, string? Perform GET
---@field Post fun(log: boolean, url: string, content_type: string, payload: string): HttpResponse, string? Makes HTTP POST request ---@field post fun(log: boolean, url: string, content_type: string, payload: string): HttpResponse, string? Perform POST
---@class Net ---@class NetModule
---@field Http Http HTTP client methods ---@field http HttpModule HTTP client functions
---@type Net --- Global variables declaration
Net = { ---@global
Http = { ---@type SessionModule
---Makes HTTP GET request _G.session = session or {}
---@param log boolean Whether to log the request
---@param url string URL to request
---@return HttpResponse response
---@return string? error
Get = function(log, url) end,
---Makes HTTP POST request ---@global
---@param log boolean Whether to log the request ---@type LogModule
---@param url string URL to request _G.log = log or {}
---@param content_type string Content-Type header
---@param payload string Request body ---@global
---@return HttpResponse response ---@type NetModule
---@return string? error _G.net = net or {}
Post = function(log, url, content_type, payload) end
}
}

View File

@@ -1,21 +1,30 @@
mode: "prod" node:
mode: dev
name: "My gosally node"
show_config: true
com_dir: "%path%/com"
http_server: http_server:
address: "0.0.0.0:8080" address: "0.0.0.0"
api: port: "8080"
latest-version: v1 session_ttl: 5s
layers: timeout: 3s
- b1 idle_timeout: 30s
- s2
tls: tls:
enabled: false enabled: true
cert_file: "./cert/fullchain.pem" cert_file: "%path%/cert/fullchain.pem"
key_file: "./cert/privkey.pem" key_file: "%path%/cert/privkey.pem"
com_dir: "com/"
updates: updates:
enabled: true enabled: false
check-interval: 1h check-interval: 1h
repository_url: "https://repo.serve.lv/raw/go-sally" repository_url: "https://repo.serve.lv/raw/go-sally"
log:
json_format: false
level: "debug"
disable_warnings:
- --WNonStdTmpDir
- --WUndefLogLevel

View File

@@ -12,6 +12,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"reflect"
"slices" "slices"
"strings" "strings"
"syscall" "syscall"
@@ -29,7 +30,7 @@ import (
var Compositor *config.Compositor = config.NewCompositor() var Compositor *config.Compositor = config.NewCompositor()
func Init0Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) { func InitGlobalLoggerHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
x.Config = Compositor x.Config = Compositor
x.Log.SetOutput(os.Stdout) x.Log.SetOutput(os.Stdout)
x.Log.SetPrefix(colors.SetBrightBlack(fmt.Sprintf("(%s) ", cs.Stage))) x.Log.SetPrefix(colors.SetBrightBlack(fmt.Sprintf("(%s) ", cs.Stage)))
@@ -37,7 +38,7 @@ func Init0Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
} }
// First stage: pre-init // First stage: pre-init
func Init1Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) { func InitCorestateHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
*cs = *corestate.NewCorestate(&corestate.CoreState{ *cs = *corestate.NewCorestate(&corestate.CoreState{
UUID32DirName: "uuid", UUID32DirName: "uuid",
NodeBinName: filepath.Base(os.Args[0]), NodeBinName: filepath.Base(os.Args[0]),
@@ -48,7 +49,7 @@ func Init1Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
}) })
} }
func Init2Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) { func InitConfigLoadHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
x.Log.SetPrefix(colors.SetYellow(fmt.Sprintf("(%s) ", cs.Stage))) x.Log.SetPrefix(colors.SetYellow(fmt.Sprintf("(%s) ", cs.Stage)))
if err := x.Config.LoadEnv(); err != nil { if err := x.Config.LoadEnv(); err != nil {
@@ -64,7 +65,7 @@ func Init2Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
} }
} }
func Init3Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) { func InitUUUDHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
uuid32, err := corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid")) uuid32, err := corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
if err := corestate.SetNodeUUID(filepath.Join(cs.NodePath, cs.MetaDir, cs.UUID32DirName)); err != nil { if err := corestate.SetNodeUUID(filepath.Join(cs.NodePath, cs.MetaDir, cs.UUID32DirName)); err != nil {
@@ -81,7 +82,7 @@ func Init3Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
cs.UUID32 = uuid32 cs.UUID32 = uuid32
} }
func Init4Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) { func InitRuntimeHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
if *x.Config.Env.ParentStagePID != os.Getpid() { if *x.Config.Env.ParentStagePID != os.Getpid() {
// still pre-init stage // still pre-init stage
runDir, err := run_manager.Create(cs.UUID32) runDir, err := run_manager.Create(cs.UUID32)
@@ -132,8 +133,8 @@ func Init4Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
} }
// post-init stage // post-init stage
func Init5Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) { func InitRunlockHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
nodeApp.Fallback(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) { NodeApp.Fallback(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) {
x.Log.Println("Cleaning up...") x.Log.Println("Cleaning up...")
if err := run_manager.Clean(); err != nil { if err := run_manager.Clean(); err != nil {
@@ -184,21 +185,31 @@ func Init5Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
} }
} }
func Init6Hook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) { func InitConfigReplHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
if !slices.Contains(*x.Config.Conf.DisableWarnings, "--WNonStdTmpDir") && os.TempDir() != "/tmp" { if !slices.Contains(*x.Config.Conf.DisableWarnings, "--WNonStdTmpDir") && os.TempDir() != "/tmp" {
x.Log.Printf("%s: %s", colors.PrintWarn(), "Non-standard value specified for temporary directory") x.Log.Printf("%s: %s", colors.PrintWarn(), "Non-standard value specified for temporary directory")
} }
if strings.Contains(*x.Config.Conf.Log.OutPath, `%tmp%`) {
replaced := strings.ReplaceAll(*x.Config.Conf.Log.OutPath, "%tmp%", filepath.Clean(run_manager.RuntimeDir())) replacements := map[string]any{
x.Config.Conf.Log.OutPath = &replaced "%tmp%": filepath.Clean(run_manager.RuntimeDir()),
"%path%": *x.Config.Env.NodePath,
"%stdout%": "_1STDout",
"%stderr%": "_2STDerr",
"%1%": "_1STDout",
"%2%": "_2STDerr",
} }
processConfig(&x.Config.Conf, replacements)
if !slices.Contains(logs.Levels.Available, *x.Config.Conf.Log.Level) { if !slices.Contains(logs.Levels.Available, *x.Config.Conf.Log.Level) {
if !slices.Contains(*x.Config.Conf.DisableWarnings, "--WUndefLogLevel") { if !slices.Contains(*x.Config.Conf.DisableWarnings, "--WUndefLogLevel") {
x.Log.Printf("%s: %s", colors.PrintWarn(), fmt.Sprintf("Unknown logging level %s, fallback level: %s", *x.Config.Conf.Log.Level, logs.Levels.Fallback)) x.Log.Printf("%s: %s", colors.PrintWarn(), fmt.Sprintf("Unknown logging level %s, fallback level: %s", *x.Config.Conf.Log.Level, logs.Levels.Fallback))
} }
x.Config.Conf.Log.Level = &logs.Levels.Fallback x.Config.Conf.Log.Level = &logs.Levels.Fallback
} }
}
func InitConfigPrintHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) {
if *x.Config.Conf.Node.ShowConfig { if *x.Config.Conf.Node.ShowConfig {
fmt.Printf("Configuration from %s:\n", x.Config.CMDLine.Run.ConfigPath) fmt.Printf("Configuration from %s:\n", x.Config.CMDLine.Run.ConfigPath)
x.Config.Print(x.Config.Conf) x.Config.Print(x.Config.Conf)
@@ -206,16 +217,16 @@ func Init6Hook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) {
fmt.Printf("Environment:\n") fmt.Printf("Environment:\n")
x.Config.Print(x.Config.Env) x.Config.Print(x.Config.Env)
if !askConfirm("Is that ok?", true) { if cs.UUID32 != "" && !askConfirm("Is that ok?", true) {
x.Log.Printf("Cancel launch") x.Log.Printf("Cancel launch")
nodeApp.CallFallback(ctx) NodeApp.CallFallback(ctx)
} }
} }
x.Log.Printf("Starting \"%s\" node", *x.Config.Conf.Node.Name) x.Log.Printf("Starting \"%s\" node", *x.Config.Conf.Node.Name)
} }
func Init7Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) { func InitSLogHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
cs.Stage = corestate.StageReady cs.Stage = corestate.StageReady
x.Log.SetPrefix(colors.SetGreen(fmt.Sprintf("(%s) ", cs.Stage))) x.Log.SetPrefix(colors.SetGreen(fmt.Sprintf("(%s) ", cs.Stage)))
@@ -228,6 +239,133 @@ func Init7Hook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
*x.SLog = *newSlog *x.SLog = *newSlog
} }
func processConfig(conf any, replacements map[string]any) error {
val := reflect.ValueOf(conf)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
switch val.Kind() {
case reflect.Struct:
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
if field.CanAddr() && field.CanSet() {
if err := processConfig(field.Addr().Interface(), replacements); err != nil {
return err
}
}
}
case reflect.Slice:
for i := 0; i < val.Len(); i++ {
elem := val.Index(i)
if elem.CanAddr() && elem.CanSet() {
if err := processConfig(elem.Addr().Interface(), replacements); err != nil {
return err
}
}
}
case reflect.Map:
for _, key := range val.MapKeys() {
elem := val.MapIndex(key)
if elem.CanInterface() {
newVal := reflect.New(elem.Type()).Elem()
newVal.Set(elem)
if err := processConfig(newVal.Addr().Interface(), replacements); err != nil {
return err
}
val.SetMapIndex(key, newVal)
}
}
case reflect.String:
str := val.String()
if replacement, exists := replacements[str]; exists {
if err := setValue(val, replacement); err != nil {
return fmt.Errorf("failed to set %q: %v", str, err)
}
} else {
for placeholder, replacement := range replacements {
if strings.Contains(str, placeholder) {
replacementStr, err := toString(replacement)
if err != nil {
return fmt.Errorf("invalid replacement for %q: %v", placeholder, err)
}
newStr := strings.ReplaceAll(str, placeholder, replacementStr)
val.SetString(newStr)
}
}
}
case reflect.Ptr:
if !val.IsNil() {
elem := val.Elem()
if elem.Kind() == reflect.String {
str := elem.String()
if replacement, exists := replacements[str]; exists {
strVal, err := toString(replacement)
if err != nil {
return fmt.Errorf("cannot convert replacement to string: %v", err)
}
elem.SetString(strVal)
} else {
for placeholder, replacement := range replacements {
if strings.Contains(str, placeholder) {
replacementStr, err := toString(replacement)
if err != nil {
return fmt.Errorf("invalid replacement for %q: %v", placeholder, err)
}
newStr := strings.ReplaceAll(str, placeholder, replacementStr)
elem.SetString(newStr)
}
}
}
} else {
return processConfig(elem.Addr().Interface(), replacements)
}
}
}
return nil
}
func setValue(val reflect.Value, replacement any) error {
if !val.CanSet() {
return fmt.Errorf("value is not settable")
}
replacementVal := reflect.ValueOf(replacement)
if replacementVal.Type().AssignableTo(val.Type()) {
val.Set(replacementVal)
return nil
}
if val.Kind() == reflect.String {
str, err := toString(replacement)
if err != nil {
return fmt.Errorf("cannot convert replacement to string: %v", err)
}
val.SetString(str)
return nil
}
return fmt.Errorf("type mismatch: cannot assign %T to %v", replacement, val.Type())
}
func toString(v any) (string, error) {
switch s := v.(type) {
case string:
return s, nil
case fmt.Stringer:
return s.String(), nil
default:
return fmt.Sprint(v), nil
}
}
func askConfirm(prompt string, defaultYes bool) bool { func askConfirm(prompt string, defaultYes bool) bool {
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
@@ -249,7 +387,7 @@ func askConfirm(prompt string, defaultYes bool) bool {
select { select {
case <-ctx.Done(): case <-ctx.Done():
fmt.Println("") fmt.Println("")
nodeApp.CallFallback(ctx) NodeApp.CallFallback(ctx)
os.Exit(3) os.Exit(3)
case text := <-inputChan: case text := <-inputChan:
text = strings.TrimSpace(strings.ToLower(text)) text = strings.TrimSpace(strings.ToLower(text))

View File

@@ -28,16 +28,16 @@ import (
"golang.org/x/net/netutil" "golang.org/x/net/netutil"
) )
var nodeApp = app.New() var NodeApp = app.New()
func Run(cmd *cobra.Command, args []string) { func Run(cmd *cobra.Command, args []string) {
nodeApp.InitialHooks( NodeApp.InitialHooks(
Init0Hook, Init1Hook, Init2Hook, InitGlobalLoggerHook, InitCorestateHook, InitConfigLoadHook,
Init3Hook, Init4Hook, Init5Hook, InitUUUDHook, InitRuntimeHook, InitRunlockHook,
Init6Hook, Init7Hook, InitConfigReplHook, InitConfigPrintHook, InitSLogHook,
) )
nodeApp.Run(RunHook) NodeApp.Run(RunHook)
} }
func RunHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error { func RunHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
@@ -75,8 +75,8 @@ func RunHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
r := chi.NewRouter() r := chi.NewRouter()
r.Use(cors.Handler(cors.Options{ r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"}, AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"}, AllowedMethods: []string{"POST"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Session-UUID"},
AllowCredentials: true, AllowCredentials: true,
MaxAge: 300, MaxAge: 300,
})) }))
@@ -96,7 +96,7 @@ func RunHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
}, "", 0), }, "", 0),
} }
nodeApp.Fallback(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) { NodeApp.Fallback(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) {
if err := srv.Shutdown(ctxMain); err != nil { if err := srv.Shutdown(ctxMain); err != nil {
x.Log.Printf("%s: Failed to stop the server gracefully: %s", colors.PrintError(), err.Error()) x.Log.Printf("%s: Failed to stop the server gracefully: %s", colors.PrintError(), err.Error())
} else { } else {
@@ -171,6 +171,6 @@ func RunHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
} }
<-ctxMain.Done() <-ctxMain.Done()
nodeApp.CallFallback(ctx) NodeApp.CallFallback(ctx)
return nil return nil
} }

View File

@@ -90,6 +90,6 @@ func (a *App) CallFallback(ctx context.Context) {
if a.fallback != nil { if a.fallback != nil {
a.fallback(ctx, a.Corestate, a.AppX) a.fallback(ctx, a.Corestate, a.AppX)
} }
os.Exit(3) os.Exit(0)
}) })
} }

View File

@@ -58,8 +58,9 @@ func (c *Compositor) LoadConf(path string) error {
v.SetDefault("updates.enabled", false) v.SetDefault("updates.enabled", false)
v.SetDefault("updates.check_interval", "2h") v.SetDefault("updates.check_interval", "2h")
v.SetDefault("updates.wanted_version", "latest-stable") v.SetDefault("updates.wanted_version", "latest-stable")
v.SetDefault("log.json_format", "false")
v.SetDefault("log.level", "info") v.SetDefault("log.level", "info")
v.SetDefault("log.out_path", "") v.SetDefault("log.output", "%2%")
if err := v.ReadInConfig(); err != nil { if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("error reading config: %w", err) return fmt.Errorf("error reading config: %w", err)

View File

@@ -55,8 +55,9 @@ type Updates struct {
} }
type Log struct { type Log struct {
JSON *bool `mapstructure:"json_format"`
Level *string `mapstructure:"level"` Level *string `mapstructure:"level"`
OutPath *string `mapstructure:"out_path"` OutPath *string `mapstructure:"output"`
} }
// ConfigEnv structure for environment variables // ConfigEnv structure for environment variables

View File

@@ -57,7 +57,12 @@ func SetupLogger(o *config.Log) (*slog.Logger, error) {
handlerOpts.Level = slog.LevelInfo handlerOpts.Level = slog.LevelInfo
} }
if *o.OutPath != "" { switch *o.OutPath {
case "_1STDout":
writer = os.Stdout
case "_2STDerr":
writer = os.Stderr
default:
logFile := &lumberjack.Logger{ logFile := &lumberjack.Logger{
Filename: filepath.Join(*o.OutPath, "event.log"), Filename: filepath.Join(*o.OutPath, "event.log"),
MaxSize: 10, MaxSize: 10,
@@ -68,6 +73,13 @@ func SetupLogger(o *config.Log) (*slog.Logger, error) {
writer = logFile writer = logFile
} }
log := slog.New(slog.NewJSONHandler(writer, &handlerOpts)) var handler slog.Handler
if *o.JSON {
handler = slog.NewJSONHandler(writer, &handlerOpts)
} else {
handler = slog.NewTextHandler(writer, &handlerOpts)
}
log := slog.New(handler)
return log, nil return log, nil
} }

View File

@@ -1,6 +1,7 @@
package gateway package gateway
import ( import (
"context"
"net/http" "net/http"
"github.com/akyaiy/GoSally-mvp/internal/core/corestate" "github.com/akyaiy/GoSally-mvp/internal/core/corestate"
@@ -14,7 +15,7 @@ type serversApiVer string
type ServerApiContract interface { type ServerApiContract interface {
GetVersion() string GetVersion() string
Handle(sid string, r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse Handle(ctx context.Context, sid string, r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse
} }
// GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions. // GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions.

View File

@@ -1,6 +1,7 @@
package gateway package gateway
import ( import (
"context"
"encoding/json" "encoding/json"
"io" "io"
"log/slog" "log/slog"
@@ -13,18 +14,22 @@ import (
) )
func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) { func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // TODO
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
sessionUUID := r.Header.Get("X-Session-UUID") sessionUUID := r.Header.Get("X-Session-UUID")
if sessionUUID == "" { if sessionUUID == "" {
sessionUUID = uuid.New().String() sessionUUID = uuid.New().String()
} }
gs.x.SLog.Debug("new request", slog.String("session-uuid", sessionUUID), slog.Group("connection", slog.String("ip", r.RemoteAddr)))
w.Header().Set("X-Session-UUID", sessionUUID) w.Header().Set("X-Session-UUID", sessionUUID)
if !gs.sm.Add(sessionUUID) { if !gs.sm.Add(sessionUUID) {
gs.x.SLog.Debug("session is busy", slog.String("session-uuid", sessionUUID))
rpc.WriteError(w, &rpc.RPCResponse{ rpc.WriteError(w, &rpc.RPCResponse{
Error: map[string]any{ Error: map[string]any{
"code": rpc.ErrSessionIsTaken, "code": rpc.ErrSessionIsBusy,
"message": rpc.ErrSessionIsTakenS, "message": rpc.ErrSessionIsBusyS,
}, },
}) })
return return
@@ -33,6 +38,7 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
gs.x.SLog.Debug("failed to read body", slog.String("err", err.Error()))
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
rpc.WriteError(w, &rpc.RPCResponse{ rpc.WriteError(w, &rpc.RPCResponse{
JSONRPC: rpc.JSONRPCVersion, JSONRPC: rpc.JSONRPCVersion,
@@ -52,6 +58,7 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
var single rpc.RPCRequest var single rpc.RPCRequest
if batch == nil { if batch == nil {
if err := json.Unmarshal(body, &single); err != nil { if err := json.Unmarshal(body, &single); err != nil {
gs.x.SLog.Debug("failed to parse json", slog.String("err", err.Error()))
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
rpc.WriteError(w, &rpc.RPCResponse{ rpc.WriteError(w, &rpc.RPCResponse{
JSONRPC: rpc.JSONRPCVersion, JSONRPC: rpc.JSONRPCVersion,
@@ -64,7 +71,11 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrParseErrorS)) gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrParseErrorS))
return return
} }
resp := gs.Route(sessionUUID, r, &single) resp := gs.Route(ctx, sessionUUID, r, &single)
if resp == nil {
w.Write([]byte(""))
return
}
rpc.WriteResponse(w, resp) rpc.WriteResponse(w, resp)
return return
} }
@@ -76,7 +87,7 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
wg.Add(1) wg.Add(1)
go func(req rpc.RPCRequest) { go func(req rpc.RPCRequest) {
defer wg.Done() defer wg.Done()
res := gs.Route(sessionUUID, r, &req) res := gs.Route(ctx, sessionUUID, r, &req)
if res != nil { if res != nil {
responses <- *res responses <- *res
} }
@@ -91,29 +102,31 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
} }
if len(result) > 0 { if len(result) > 0 {
json.NewEncoder(w).Encode(result) json.NewEncoder(w).Encode(result)
} else {
w.Write([]byte("[]"))
} }
} }
func (gs *GatewayServer) Route(sid string, r *http.Request, req *rpc.RPCRequest) (resp *rpc.RPCResponse) { func (gs *GatewayServer) Route(ctx context.Context, sid string, r *http.Request, req *rpc.RPCRequest) (resp *rpc.RPCResponse) {
defer utils.CatchPanicWithFallback(func(rec any) { defer utils.CatchPanicWithFallback(func(rec any) {
gs.x.SLog.Error("panic caught in handler", slog.Any("error", rec)) gs.x.SLog.Error("panic caught in handler", slog.Any("error", rec))
resp = rpc.NewError(rpc.ErrInternalError, "Internal server error (panic)", req.ID) resp = rpc.NewError(rpc.ErrInternalError, "Internal server error (panic)", nil, req.ID)
}) })
if req.JSONRPC != rpc.JSONRPCVersion { if req.JSONRPC != rpc.JSONRPCVersion {
gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrInvalidRequestS), slog.String("requested-version", req.JSONRPC)) gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrInvalidRequestS), slog.String("requested-version", req.JSONRPC))
return rpc.NewError(rpc.ErrInvalidRequest, rpc.ErrInvalidRequestS, req.ID) return rpc.NewError(rpc.ErrInvalidRequest, rpc.ErrInvalidRequestS, nil, req.ID)
} }
server, ok := gs.servers[serversApiVer(req.ContextVersion)] server, ok := gs.servers[serversApiVer(req.ContextVersion)]
if !ok { if !ok {
gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrContextVersionS), slog.String("requested-version", req.ContextVersion)) gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrContextVersionS), slog.String("requested-version", req.ContextVersion))
return rpc.NewError(rpc.ErrContextVersion, rpc.ErrContextVersionS, req.ID) return rpc.NewError(rpc.ErrContextVersion, rpc.ErrContextVersionS, nil, req.ID)
} }
resp = server.Handle(sid, r, req)
// checks if request is notification // checks if request is notification
if req.ID == nil { if req.ID == nil {
go server.Handle(ctx, sid, r, req)
return nil return nil
} }
return resp return server.Handle(ctx, sid, r, req)
} }

View File

@@ -25,6 +25,6 @@ const (
ErrMethodIsMissing = -32020 ErrMethodIsMissing = -32020
ErrMethodIsMissingS = "Method is missing" ErrMethodIsMissingS = "Method is missing"
ErrSessionIsTaken = -32030 ErrSessionIsBusy = -32030
ErrSessionIsTakenS = "The session is already taken" ErrSessionIsBusyS = "The session is busy"
) )

View File

@@ -2,7 +2,18 @@ package rpc
import "encoding/json" import "encoding/json"
func NewError(code int, message string, id *json.RawMessage) *RPCResponse { func NewError(code int, message string, data any, id *json.RawMessage) *RPCResponse {
if data != nil {
return &RPCResponse{
JSONRPC: JSONRPCVersion,
ID: id,
Error: map[string]any{
"code": code,
"message": message,
"data": data,
},
}
}
return &RPCResponse{ return &RPCResponse{
JSONRPC: JSONRPCVersion, JSONRPC: JSONRPCVersion,
ID: id, ID: id,

View File

@@ -1,26 +1,27 @@
package sv1 package sv1
import ( import (
"context"
"log/slog" "log/slog"
"net/http" "net/http"
"github.com/akyaiy/GoSally-mvp/internal/server/rpc" "github.com/akyaiy/GoSally-mvp/internal/server/rpc"
) )
func (h *HandlerV1) Handle(sid string, r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse { func (h *HandlerV1) Handle(_ context.Context, sid string, r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse {
if req.Method == "" { if req.Method == "" {
h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrMethodNotFoundS), slog.String("requested-method", req.Method)) h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrMethodNotFoundS), slog.String("requested-method", req.Method))
return rpc.NewError(rpc.ErrMethodIsMissing, rpc.ErrMethodIsMissingS, req.ID) return rpc.NewError(rpc.ErrMethodIsMissing, rpc.ErrMethodIsMissingS, nil, req.ID)
} }
method, err := h.resolveMethodPath(req.Method) method, err := h.resolveMethodPath(req.Method)
if err != nil { if err != nil {
if err.Error() == rpc.ErrInvalidMethodFormatS { if err.Error() == rpc.ErrInvalidMethodFormatS {
h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrInvalidMethodFormatS), slog.String("requested-method", req.Method)) h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrInvalidMethodFormatS), slog.String("requested-method", req.Method))
return rpc.NewError(rpc.ErrInvalidMethodFormat, rpc.ErrInvalidMethodFormatS, req.ID) return rpc.NewError(rpc.ErrInvalidMethodFormat, rpc.ErrInvalidMethodFormatS, nil, req.ID)
} else if err.Error() == rpc.ErrMethodNotFoundS { } else if err.Error() == rpc.ErrMethodNotFoundS {
h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrMethodNotFoundS), slog.String("requested-method", req.Method)) h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrMethodNotFoundS), slog.String("requested-method", req.Method))
return rpc.NewError(rpc.ErrMethodNotFound, rpc.ErrMethodNotFoundS, req.ID) return rpc.NewError(rpc.ErrMethodNotFound, rpc.ErrMethodNotFoundS, nil, req.ID)
} }
} }

View File

@@ -1,20 +1,334 @@
package sv1 package sv1
// TODO: make a lua state pool using sync.Pool
import ( import (
"database/sql"
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
"math/rand/v2"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/akyaiy/GoSally-mvp/internal/colors" "github.com/akyaiy/GoSally-mvp/internal/colors"
"github.com/akyaiy/GoSally-mvp/internal/server/rpc" "github.com/akyaiy/GoSally-mvp/internal/server/rpc"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
_ "modernc.org/sqlite"
) )
type DBConnection struct {
dbPath string
log bool
logger *slog.Logger
writeChan chan *dbWriteRequest
closeChan chan struct{}
}
type dbWriteRequest struct {
query string
args []interface{}
resCh chan *dbWriteResult
}
type dbWriteResult struct {
rowsAffected int64
err error
}
var dbMutexMap = make(map[string]*sync.RWMutex)
var dbGlobalMutex sync.Mutex
func getDBMutex(dbPath string) *sync.RWMutex {
dbGlobalMutex.Lock()
defer dbGlobalMutex.Unlock()
if mtx, ok := dbMutexMap[dbPath]; ok {
return mtx
}
mtx := &sync.RWMutex{}
dbMutexMap[dbPath] = mtx
return mtx
}
func loadDBMod(llog *slog.Logger) func(*lua.LState) int {
llog.Debug("import module db-sqlite")
return func(L *lua.LState) int {
dbMod := L.NewTable()
L.SetField(dbMod, "connect", L.NewFunction(func(L *lua.LState) int {
dbPath := L.CheckString(1)
logQueries := false
if L.GetTop() >= 2 {
opts := L.CheckTable(2)
if val := opts.RawGetString("log"); val != lua.LNil {
logQueries = lua.LVAsBool(val)
}
}
conn := &DBConnection{
dbPath: dbPath,
log: logQueries,
logger: llog,
writeChan: make(chan *dbWriteRequest, 100),
closeChan: make(chan struct{}),
}
go conn.processWrites()
ud := L.NewUserData()
ud.Value = conn
L.SetMetatable(ud, L.GetTypeMetatable("gosally_db"))
L.Push(ud)
return 1
}))
mt := L.NewTypeMetatable("gosally_db")
L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
"exec": dbExec,
"query": dbQuery,
"close": dbClose,
}))
L.SetField(dbMod, "__gosally_internal", lua.LString("0"))
L.Push(dbMod)
return 1
}
}
func (conn *DBConnection) processWrites() {
for {
select {
case req := <-conn.writeChan:
mtx := getDBMutex(conn.dbPath)
mtx.Lock()
db, err := sql.Open("sqlite", conn.dbPath+"?_busy_timeout=5000&_journal_mode=WAL&_sync=NORMAL&_cache_size=-10000")
if err == nil {
_, err = db.Exec("PRAGMA journal_mode=WAL;")
if err == nil {
res, execErr := db.Exec(req.query, req.args...)
if execErr == nil {
rows, _ := res.RowsAffected()
req.resCh <- &dbWriteResult{rowsAffected: rows}
} else {
req.resCh <- &dbWriteResult{err: execErr}
}
}
db.Close()
}
if err != nil {
req.resCh <- &dbWriteResult{err: err}
}
mtx.Unlock()
case <-conn.closeChan:
return
}
}
}
func dbExec(L *lua.LState) int {
ud := L.CheckUserData(1)
conn, ok := ud.Value.(*DBConnection)
if !ok {
L.Push(lua.LNil)
L.Push(lua.LString("invalid database connection"))
return 2
}
query := L.CheckString(2)
var args []any
if L.GetTop() >= 3 {
params := L.CheckTable(3)
params.ForEach(func(k lua.LValue, v lua.LValue) {
args = append(args, ConvertLuaTypesToGolang(v))
})
}
if conn.log {
conn.logger.Info("DB Exec",
slog.String("query", query),
slog.Any("params", args))
}
resCh := make(chan *dbWriteResult, 1)
conn.writeChan <- &dbWriteRequest{
query: query,
args: args,
resCh: resCh,
}
ctx := L.NewTable()
L.SetField(ctx, "done", lua.LBool(false))
var result lua.LValue = lua.LNil
var errorMsg lua.LValue = lua.LNil
L.SetField(ctx, "wait", L.NewFunction(func(lL *lua.LState) int {
res := <-resCh
L.SetField(ctx, "done", lua.LBool(true))
if res.err != nil {
errorMsg = lua.LString(res.err.Error())
result = lua.LNil
} else {
result = lua.LNumber(res.rowsAffected)
errorMsg = lua.LNil
}
if res.err != nil {
lL.Push(lua.LNil)
lL.Push(lua.LString(res.err.Error()))
return 2
}
lL.Push(lua.LNumber(res.rowsAffected))
lL.Push(lua.LNil)
return 2
}))
L.SetField(ctx, "check", L.NewFunction(func(lL *lua.LState) int {
select {
case res := <-resCh:
lL.SetField(ctx, "done", lua.LBool(true))
if res.err != nil {
errorMsg = lua.LString(res.err.Error())
result = lua.LNil
lL.Push(lua.LNil)
lL.Push(lua.LString(res.err.Error()))
return 2
} else {
result = lua.LNumber(res.rowsAffected)
errorMsg = lua.LNil
lL.Push(lua.LNumber(res.rowsAffected))
lL.Push(lua.LNil)
return 2
}
default:
lL.Push(result)
lL.Push(errorMsg)
return 2
}
}))
L.Push(ctx)
L.Push(lua.LNil)
return 2
}
func dbQuery(L *lua.LState) int {
ud := L.CheckUserData(1)
conn, ok := ud.Value.(*DBConnection)
if !ok {
L.Push(lua.LNil)
L.Push(lua.LString("invalid database connection"))
return 2
}
query := L.CheckString(2)
var args []any
if L.GetTop() >= 3 {
params := L.CheckTable(3)
params.ForEach(func(k lua.LValue, v lua.LValue) {
args = append(args, ConvertLuaTypesToGolang(v))
})
}
if conn.log {
conn.logger.Info("DB Query",
slog.String("query", query),
slog.Any("params", args))
}
mtx := getDBMutex(conn.dbPath)
mtx.RLock()
defer mtx.RUnlock()
db, err := sql.Open("sqlite", conn.dbPath+"?_busy_timeout=5000&_journal_mode=WAL&_sync=NORMAL&_cache_size=-10000")
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
defer db.Close()
rows, err := db.Query(query, args...)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(fmt.Sprintf("query failed: %v", err)))
return 2
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(fmt.Sprintf("get columns failed: %v", err)))
return 2
}
result := L.NewTable()
colCount := len(columns)
values := make([]any, colCount)
valuePtrs := make([]any, colCount)
for rows.Next() {
for i := range columns {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(fmt.Sprintf("scan failed: %v", err)))
return 2
}
rowTable := L.NewTable()
for i, col := range columns {
val := values[i]
if val == nil {
L.SetField(rowTable, col, lua.LNil)
} else {
L.SetField(rowTable, col, ConvertGolangTypesToLua(L, val))
}
}
result.Append(rowTable)
}
if err := rows.Err(); err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(fmt.Sprintf("rows iteration failed: %v", err)))
return 2
}
L.Push(result)
return 1
}
func dbClose(L *lua.LState) int {
ud := L.CheckUserData(1)
conn, ok := ud.Value.(*DBConnection)
if !ok {
L.Push(lua.LFalse)
L.Push(lua.LString("invalid database connection"))
return 2
}
close(conn.closeChan)
L.Push(lua.LTrue)
return 1
}
func addInitiatorHeaders(sid string, req *http.Request, headers http.Header) { func addInitiatorHeaders(sid string, req *http.Request, headers http.Header) {
clientIP := req.RemoteAddr clientIP := req.RemoteAddr
if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" { if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" {
@@ -35,229 +349,305 @@ func addInitiatorHeaders(sid string, req *http.Request, headers http.Header) {
// I will be only glad. // I will be only glad.
// TODO: make this huge function more harmonious by dividing responsibilities // TODO: make this huge function more harmonious by dividing responsibilities
func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest, path string) *rpc.RPCResponse { func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest, path string) *rpc.RPCResponse {
llog := h.x.SLog.With(slog.String("session-id", sid))
llog.Debug("handling LUA")
L := lua.NewState() L := lua.NewState()
defer L.Close() defer L.Close()
inTable := L.NewTable() seed := rand.Int()
paramsTable := L.NewTable()
if fetchedParams, ok := req.Params.(map[string]any); ok { loadSessionMod := func(lL *lua.LState) int {
for k, v := range fetchedParams { llog.Debug("import module session", slog.String("script", path))
L.SetField(paramsTable, k, ConvertGolangTypesToLua(L, v)) sessionMod := lL.NewTable()
inTable := lL.NewTable()
paramsTable := lL.NewTable()
if fetchedParams, ok := req.Params.(map[string]any); ok {
for k, v := range fetchedParams {
lL.SetField(paramsTable, k, ConvertGolangTypesToLua(lL, v))
}
} }
} lL.SetField(inTable, "params", paramsTable)
L.SetField(inTable, "Params", paramsTable)
L.SetGlobal("In", inTable)
outTable := L.NewTable() outTable := lL.NewTable()
resultTable := L.NewTable() resultTable := lL.NewTable()
L.SetField(outTable, "Result", resultTable) lL.SetField(outTable, "result", resultTable)
L.SetGlobal("Out", outTable)
logTable := L.NewTable() lL.SetField(inTable, "address", lua.LString(r.RemoteAddr))
lL.SetField(sessionMod, "request", inTable)
lL.SetField(sessionMod, "response", outTable)
logFuncs := map[string]func(string, ...any){ lL.SetField(sessionMod, "id", lua.LString(sid))
"Info": h.x.SLog.Info,
"Debug": h.x.SLog.Debug, lL.SetField(sessionMod, "__gosally_internal", lua.LString(fmt.Sprint(seed)))
"Error": h.x.SLog.Error, lL.Push(sessionMod)
"Warn": h.x.SLog.Warn, return 1
} }
for name, logFunc := range logFuncs { loadLogMod := func(lL *lua.LState) int {
L.SetField(logTable, name, L.NewFunction(func(L *lua.LState) int { llog.Debug("import module log", slog.String("script", path))
msg := L.ToString(1) logMod := lL.NewTable()
logFunc(fmt.Sprintf("the script says: %s", msg), slog.String("script", path))
return 0 logFuncs := map[string]func(string, ...any){
"info": llog.Info,
"debug": llog.Debug,
"error": llog.Error,
"warn": llog.Warn,
}
for name, logFunc := range logFuncs {
fun := logFunc
lL.SetField(logMod, name, lL.NewFunction(func(lL *lua.LState) int {
msg := lL.Get(1)
converted := ConvertLuaTypesToGolang(msg)
fun(fmt.Sprintf("the script says: %s", converted), slog.String("script", path))
return 0
}))
}
for _, fn := range []struct {
field string
pfunc func(string, ...any)
color func() string
}{
{"event", h.x.Log.Printf, nil},
{"event_error", h.x.Log.Printf, colors.PrintError},
{"event_warn", h.x.Log.Printf, colors.PrintWarn},
} {
lL.SetField(logMod, fn.field, lL.NewFunction(func(lL *lua.LState) int {
msg := lL.Get(1)
converted := ConvertLuaTypesToGolang(msg)
if fn.color != nil {
h.x.Log.Printf("%s: %s: %s", fn.color(), path, converted)
} else {
h.x.Log.Printf("%s: %s", path, converted)
}
return 0
}))
}
lL.SetField(logMod, "__gosally_internal", lua.LString(fmt.Sprint(seed)))
lL.Push(logMod)
return 1
}
loadNetMod := func(lL *lua.LState) int {
llog.Debug("import module net", slog.String("script", path))
netMod := lL.NewTable()
netModhttp := lL.NewTable()
lL.SetField(netModhttp, "get_request", lL.NewFunction(func(lL *lua.LState) int {
logRequest := lL.ToBool(1)
url := lL.ToString(2)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
lL.Push(lua.LNil)
lL.Push(lua.LString(err.Error()))
return 2
}
addInitiatorHeaders(sid, r, req.Header)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
lL.Push(lua.LNil)
lL.Push(lua.LString(err.Error()))
return 2
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
lL.Push(lua.LNil)
lL.Push(lua.LString(err.Error()))
return 2
}
if logRequest {
llog.Info("HTTP GET request",
slog.String("script", path),
slog.String("url", url),
slog.Int("status", resp.StatusCode),
slog.String("status_text", resp.Status),
slog.String("initiator_ip", req.Header.Get("X-Initiator-IP")),
)
}
result := lL.NewTable()
lL.SetField(result, "status", lua.LNumber(resp.StatusCode))
lL.SetField(result, "status_text", lua.LString(resp.Status))
lL.SetField(result, "body", lua.LString(body))
lL.SetField(result, "content_length", lua.LNumber(resp.ContentLength))
headers := lL.NewTable()
for k, v := range resp.Header {
lL.SetField(headers, k, ConvertGolangTypesToLua(lL, v))
}
lL.SetField(result, "headers", headers)
lL.Push(result)
return 1
})) }))
lL.SetField(netModhttp, "post_request", lL.NewFunction(func(lL *lua.LState) int {
logRequest := lL.ToBool(1)
url := lL.ToString(2)
contentType := lL.ToString(3)
payload := lL.ToString(4)
body := strings.NewReader(payload)
req, err := http.NewRequest("POST", url, body)
if err != nil {
lL.Push(lua.LNil)
lL.Push(lua.LString(err.Error()))
return 2
}
req.Header.Set("Content-Type", contentType)
addInitiatorHeaders(sid, r, req.Header)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
lL.Push(lua.LNil)
lL.Push(lua.LString(err.Error()))
return 2
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
lL.Push(lua.LNil)
lL.Push(lua.LString(err.Error()))
return 2
}
if logRequest {
llog.Info("HTTP POST request",
slog.String("script", path),
slog.String("url", url),
slog.String("content_type", contentType),
slog.Int("status", resp.StatusCode),
slog.String("status_text", resp.Status),
slog.String("initiator_ip", req.Header.Get("X-Initiator-IP")),
)
}
result := lL.NewTable()
lL.SetField(result, "status", lua.LNumber(resp.StatusCode))
lL.SetField(result, "status_text", lua.LString(resp.Status))
lL.SetField(result, "body", lua.LString(respBody))
lL.SetField(result, "content_length", lua.LNumber(resp.ContentLength))
headers := lL.NewTable()
for k, v := range resp.Header {
lL.SetField(headers, k, ConvertGolangTypesToLua(lL, v))
}
lL.SetField(result, "headers", headers)
lL.Push(result)
return 1
}))
lL.SetField(netMod, "http", netModhttp)
lL.SetField(netMod, "__gosally_internal", lua.LString(fmt.Sprint(seed)))
lL.Push(netMod)
return 1
} }
L.SetField(logTable, "Event", L.NewFunction(func(L *lua.LState) int { L.PreloadModule("internal.session", loadSessionMod)
msg := L.ToString(1) L.PreloadModule("internal.log", loadLogMod)
h.x.Log.Printf("%s: %s", path, msg) L.PreloadModule("internal.net", loadNetMod)
return 0 L.PreloadModule("internal.database-sqlite", loadDBMod(llog))
}))
L.SetField(logTable, "EventError", L.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
h.x.Log.Printf("%s: %s: %s", colors.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", colors.PrintWarn(), path, msg)
return 0
}))
L.SetGlobal("Log", logTable)
net := L.NewTable()
netHttp := L.NewTable()
L.SetField(netHttp, "Get", L.NewFunction(func(L *lua.LState) int {
logRequest := L.ToBool(1)
url := L.ToString(2)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
addInitiatorHeaders(sid, r, req.Header)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
if logRequest {
h.x.SLog.Info("HTTP GET request",
slog.String("script", path),
slog.String("url", url),
slog.Int("status", resp.StatusCode),
slog.String("status_text", resp.Status),
slog.String("initiator_ip", req.Header.Get("X-Initiator-IP")),
)
}
result := L.NewTable()
L.SetField(result, "status", lua.LNumber(resp.StatusCode))
L.SetField(result, "status_text", lua.LString(resp.Status))
L.SetField(result, "body", lua.LString(body))
L.SetField(result, "content_length", lua.LNumber(resp.ContentLength))
headers := L.NewTable()
for k, v := range resp.Header {
L.SetField(headers, k, ConvertGolangTypesToLua(L, v))
}
L.SetField(result, "headers", headers)
L.Push(result)
return 1
}))
L.SetField(netHttp, "Post", L.NewFunction(func(L *lua.LState) int {
logRequest := L.ToBool(1)
url := L.ToString(2)
contentType := L.ToString(3)
payload := L.ToString(4)
body := strings.NewReader(payload)
req, err := http.NewRequest("POST", url, body)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
req.Header.Set("Content-Type", contentType)
addInitiatorHeaders(sid, r, req.Header)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
if logRequest {
h.x.SLog.Info("HTTP POST request",
slog.String("script", path),
slog.String("url", url),
slog.String("content_type", contentType),
slog.Int("status", resp.StatusCode),
slog.String("status_text", resp.Status),
slog.String("initiator_ip", req.Header.Get("X-Initiator-IP")),
)
}
result := L.NewTable()
L.SetField(result, "status", lua.LNumber(resp.StatusCode))
L.SetField(result, "status_text", lua.LString(resp.Status))
L.SetField(result, "body", lua.LString(respBody))
L.SetField(result, "content_length", lua.LNumber(resp.ContentLength))
headers := L.NewTable()
for k, v := range resp.Header {
L.SetField(headers, k, ConvertGolangTypesToLua(L, v))
}
L.SetField(result, "headers", headers)
L.Push(result)
return 1
}))
L.SetField(net, "Http", netHttp)
L.SetGlobal("Net", net)
llog.Debug("preparing environment")
prep := filepath.Join(*h.x.Config.Conf.Node.ComDir, "_prepare.lua") prep := filepath.Join(*h.x.Config.Conf.Node.ComDir, "_prepare.lua")
if _, err := os.Stat(prep); err == nil { if _, err := os.Stat(prep); err == nil {
if err := L.DoFile(prep); err != nil { if err := L.DoFile(prep); err != nil {
return rpc.NewError(rpc.ErrInternalError, err.Error(), req.ID) llog.Error("script error", slog.String("script", path), slog.String("error", err.Error()))
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
} }
} }
llog.Debug("executing script", slog.String("script", path))
if err := L.DoFile(path); err != nil { if err := L.DoFile(path); err != nil {
return rpc.NewError(rpc.ErrInternalError, err.Error(), req.ID) llog.Error("script error", slog.String("script", path), slog.String("error", err.Error()))
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
} }
lv := L.GetGlobal("Out") pkg := L.GetGlobal("package")
outTbl, ok := lv.(*lua.LTable) pkgTbl, ok := pkg.(*lua.LTable)
if !ok { if !ok {
return rpc.NewError(rpc.ErrInternalError, "Out is not a table", req.ID) llog.Error("script error", slog.String("script", path), slog.String("error", "package not found"))
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
} }
// Check if Out.Error exists loaded := pkgTbl.RawGetString("loaded")
if errVal := outTbl.RawGetString("Error"); errVal != lua.LNil { loadedTbl, ok := loaded.(*lua.LTable)
if !ok {
llog.Error("script error", slog.String("script", path), slog.String("error", "package.loaded not found"))
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
}
sessionVal := loadedTbl.RawGetString("internal.session")
sessionTbl, ok := sessionVal.(*lua.LTable)
if !ok {
return rpc.NewResponse(map[string]any{
"responsible-node": h.cs.UUID32,
}, req.ID)
}
tag := sessionTbl.RawGetString("__gosally_internal")
if tag.Type() != lua.LTString || tag.String() != fmt.Sprint(seed) {
llog.Debug("stock session module is not imported: wrong seed", slog.String("script", path))
return rpc.NewResponse(map[string]any{
"responsible-node": h.cs.UUID32,
}, req.ID)
}
outVal := sessionTbl.RawGetString("response")
outTbl, ok := outVal.(*lua.LTable)
if !ok {
llog.Error("script error", slog.String("script", path), slog.String("error", "response is not a table"))
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
}
if errVal := outTbl.RawGetString("error"); errVal != lua.LNil {
llog.Debug("catch error table", slog.String("script", path))
if errTbl, ok := errVal.(*lua.LTable); ok { if errTbl, ok := errVal.(*lua.LTable); ok {
code := rpc.ErrInternalError code := rpc.ErrInternalError
message := "Internal script error" message := rpc.ErrInternalErrorS
data := make(map[string]any)
if c := errTbl.RawGetString("code"); c.Type() == lua.LTNumber { if c := errTbl.RawGetString("code"); c.Type() == lua.LTNumber {
code = int(c.(lua.LNumber)) code = int(c.(lua.LNumber))
} }
if msg := errTbl.RawGetString("message"); msg.Type() == lua.LTString { if msg := errTbl.RawGetString("message"); msg.Type() == lua.LTString {
message = msg.String() message = msg.String()
} }
h.x.SLog.Error("the script terminated with an error", slog.String("code", strconv.Itoa(code)), slog.String("message", message)) rawData := errTbl.RawGetString("data")
return rpc.NewError(code, message, req.ID)
if tbl, ok := rawData.(*lua.LTable); ok {
tbl.ForEach(func(k, v lua.LValue) { data[k.String()] = ConvertLuaTypesToGolang(v) })
} else {
llog.Error("the script terminated with an error", slog.String("code", strconv.Itoa(code)), slog.String("message", message))
return rpc.NewError(code, message, ConvertLuaTypesToGolang(rawData), req.ID)
}
llog.Error("the script terminated with an error", slog.String("code", strconv.Itoa(code)), slog.String("message", message))
return rpc.NewError(code, message, data, req.ID)
} }
return rpc.NewError(rpc.ErrInternalError, "Out.Error is not a table", req.ID) return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
} }
// Otherwise, parse Out.Result resultVal := outTbl.RawGetString("result")
resultVal := outTbl.RawGetString("Result") payload := make(map[string]any)
resultTbl, ok := resultVal.(*lua.LTable) if tbl, ok := resultVal.(*lua.LTable); ok {
if !ok { tbl.ForEach(func(k, v lua.LValue) { payload[k.String()] = ConvertLuaTypesToGolang(v) })
return rpc.NewError(rpc.ErrInternalError, "Out.Result is not a table", req.ID) } else {
payload["message"] = ConvertLuaTypesToGolang(resultVal)
} }
payload["responsible-node"] = h.cs.UUID32
out := make(map[string]any) return rpc.NewResponse(payload, req.ID)
resultTbl.ForEach(func(key lua.LValue, value lua.LValue) {
out[key.String()] = ConvertLuaTypesToGolang(value)
})
out["responsible-node"] = h.cs.UUID32
return rpc.NewResponse(out, req.ID)
} }