env capabilities

This commit is contained in:
Sky Johnson 2025-05-30 17:45:58 -05:00
parent 266da9fd23
commit 31a3625873
10 changed files with 527 additions and 33 deletions

View File

@ -186,6 +186,10 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
luaCtx := runner.NewHTTPContext(ctx) luaCtx := runner.NewHTTPContext(ctx)
defer luaCtx.Release() defer luaCtx.Release()
if runner.GetGlobalEnvManager() != nil {
luaCtx.Set("env", runner.GetGlobalEnvManager().GetAll())
}
sessionMap := s.ctxPool.Get().(map[string]any) sessionMap := s.ctxPool.Get().(map[string]any)
defer func() { defer func() {
for k := range sessionMap { for k := range sessionMap {

View File

@ -252,6 +252,10 @@ func (s *Moonshark) Shutdown() error {
s.LuaRunner.Close() s.LuaRunner.Close()
} }
if err := runner.CleanupEnv(); err != nil {
logger.Warning("Environment cleanup failed: %v", err)
}
logger.Info("Shutdown complete") logger.Info("Shutdown complete")
return nil return nil
} }
@ -302,6 +306,11 @@ func initRunner(s *Moonshark) error {
} }
} }
// Initialize environment manager
if err := runner.InitEnv(s.Config.Dirs.Data); err != nil {
logger.Warning("Environment initialization failed: %v", err)
}
sessionManager := sessions.GlobalSessionManager sessionManager := sessions.GlobalSessionManager
sessionManager.SetCookieOptions( sessionManager.SetCookieOptions(
"MoonsharkSID", "MoonsharkSID",

View File

@ -30,7 +30,8 @@ var contextPool = sync.Pool{
// NewContext creates a new context, potentially reusing one from the pool // NewContext creates a new context, potentially reusing one from the pool
func NewContext() *Context { func NewContext() *Context {
return contextPool.Get().(*Context) ctx := contextPool.Get().(*Context)
return ctx
} }
// NewHTTPContext creates a new context from a fasthttp RequestCtx // NewHTTPContext creates a new context from a fasthttp RequestCtx

View File

@ -40,6 +40,9 @@ var timeLuaCode string
//go:embed lua/math.lua //go:embed lua/math.lua
var mathLuaCode string var mathLuaCode string
//go:embed lua/env.lua
var envLuaCode string
// ModuleInfo holds information about an embeddable Lua module // ModuleInfo holds information about an embeddable Lua module
type ModuleInfo struct { type ModuleInfo struct {
Name string // Module name Name string // Module name
@ -61,6 +64,7 @@ var (
{Name: "crypto", Code: cryptoLuaCode, DefinesGlobal: true}, {Name: "crypto", Code: cryptoLuaCode, DefinesGlobal: true},
{Name: "time", Code: timeLuaCode}, {Name: "time", Code: timeLuaCode},
{Name: "math", Code: mathLuaCode}, {Name: "math", Code: mathLuaCode},
{Name: "env", Code: envLuaCode, DefinesGlobal: true},
} }
) )

278
runner/env.go Normal file
View File

@ -0,0 +1,278 @@
package runner
import (
"bufio"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"Moonshark/utils/color"
"Moonshark/utils/logger"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
// EnvManager handles loading, storing, and saving environment variables
type EnvManager struct {
envPath string // Path to .env file
vars map[string]any // Environment variables in memory
mu sync.RWMutex // Thread-safe access
}
// Global environment manager instance
var globalEnvManager *EnvManager
// InitEnv initializes the environment manager with the given data directory
func InitEnv(dataDir string) error {
if dataDir == "" {
return fmt.Errorf("data directory cannot be empty")
}
// Create data directory if it doesn't exist
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
envPath := filepath.Join(dataDir, ".env")
globalEnvManager = &EnvManager{
envPath: envPath,
vars: make(map[string]any),
}
// Load existing .env file if it exists
if err := globalEnvManager.load(); err != nil {
logger.Warning("Failed to load .env file: %v", err)
}
count := len(globalEnvManager.vars)
if count > 0 {
logger.Info("Environment loaded: %s vars from %s",
color.Apply(fmt.Sprintf("%d", count), color.Yellow),
color.Apply(envPath, color.Yellow))
} else {
logger.Info("Environment initialized: %s", color.Apply(envPath, color.Yellow))
}
return nil
}
// GetGlobalEnvManager returns the global environment manager instance
func GetGlobalEnvManager() *EnvManager {
return globalEnvManager
}
// load reads the .env file and populates the vars map
func (e *EnvManager) load() error {
file, err := os.Open(e.envPath)
if os.IsNotExist(err) {
// File doesn't exist, start with empty env
return nil
}
if err != nil {
return fmt.Errorf("failed to open .env file: %w", err)
}
defer file.Close()
e.mu.Lock()
defer e.mu.Unlock()
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
line := strings.TrimSpace(scanner.Text())
// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Parse key=value
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
logger.Warning("Invalid .env line %d: %s", lineNum, line)
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Remove quotes if present
if len(value) >= 2 {
if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) ||
(strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) {
value = value[1 : len(value)-1]
}
}
e.vars[key] = value
}
return scanner.Err()
}
// Save writes the current environment variables to the .env file
func (e *EnvManager) Save() error {
if e == nil {
return nil // No env manager initialized
}
e.mu.RLock()
defer e.mu.RUnlock()
file, err := os.Create(e.envPath)
if err != nil {
return fmt.Errorf("failed to create .env file: %w", err)
}
defer file.Close()
// Sort keys for consistent output
keys := make([]string, 0, len(e.vars))
for key := range e.vars {
keys = append(keys, key)
}
sort.Strings(keys)
// Write header comment
fmt.Fprintln(file, "# Environment variables for Moonshark")
fmt.Fprintln(file, "# Generated automatically - you can edit this file")
fmt.Fprintln(file)
// Write each variable
for _, key := range keys {
value := e.vars[key]
// Convert value to string
var strValue string
switch v := value.(type) {
case string:
strValue = v
case nil:
continue // Skip nil values
default:
strValue = fmt.Sprintf("%v", v)
}
// Quote values that contain spaces or special characters
if strings.ContainsAny(strValue, " \t\n\r\"'\\") {
strValue = fmt.Sprintf("\"%s\"", strings.ReplaceAll(strValue, "\"", "\\\""))
}
fmt.Fprintf(file, "%s=%s\n", key, strValue)
}
logger.Debug("Environment saved: %d vars to %s", len(e.vars), e.envPath)
return nil
}
// Get retrieves an environment variable
func (e *EnvManager) Get(key string) (any, bool) {
if e == nil {
return nil, false
}
e.mu.RLock()
defer e.mu.RUnlock()
value, exists := e.vars[key]
return value, exists
}
// Set stores an environment variable
func (e *EnvManager) Set(key string, value any) {
if e == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.vars[key] = value
}
// GetAll returns a copy of all environment variables
func (e *EnvManager) GetAll() map[string]any {
if e == nil {
return make(map[string]any)
}
e.mu.RLock()
defer e.mu.RUnlock()
result := make(map[string]any, len(e.vars))
for k, v := range e.vars {
result[k] = v
}
return result
}
// CleanupEnv saves the environment and cleans up resources
func CleanupEnv() error {
if globalEnvManager != nil {
return globalEnvManager.Save()
}
return nil
}
// envGet Lua function to get an environment variable
func envGet(state *luajit.State) int {
if !state.IsString(1) {
state.PushNil()
return 1
}
key := state.ToString(1)
if value, exists := globalEnvManager.Get(key); exists {
if err := state.PushValue(value); err != nil {
state.PushNil()
}
} else {
state.PushNil()
}
return 1
}
// envSet Lua function to set an environment variable
func envSet(state *luajit.State) int {
if !state.IsString(1) || !state.IsString(2) {
state.PushBoolean(false)
return 1
}
key := state.ToString(1)
value := state.ToString(2)
globalEnvManager.Set(key, value)
state.PushBoolean(true)
return 1
}
// envGetAll Lua function to get all environment variables
func envGetAll(state *luajit.State) int {
vars := globalEnvManager.GetAll()
if err := state.PushTable(vars); err != nil {
state.PushNil()
}
return 1
}
// RegisterEnvFunctions registers environment functions with the Lua state
func RegisterEnvFunctions(state *luajit.State) error {
if err := state.RegisterGoFunction("__env_get", envGet); err != nil {
return err
}
if err := state.RegisterGoFunction("__env_set", envSet); err != nil {
return err
}
if err := state.RegisterGoFunction("__env_get_all", envGetAll); err != nil {
return err
}
return nil
}

93
runner/lua/env.lua Normal file
View File

@ -0,0 +1,93 @@
-- Environment variable module for Moonshark
-- Provides access to persistent environment variables stored in .env file
-- Get an environment variable with a default value
-- Returns the value if it exists, default_value otherwise
function env_get(key, default_value)
if type(key) ~= "string" then
error("env_get: key must be a string")
end
-- First check context for environment variables (no Go call needed)
if _env and _env[key] ~= nil then
return _env[key]
end
return default_value
end
-- Set an environment variable
-- Returns true on success, false on failure
function env_set(key, value)
if type(key) ~= "string" then
error("env_set: key must be a string")
end
-- Update context immediately for future reads
if not _env then
_env = {}
end
_env[key] = value
-- Persist to Go backend
return __env_set(key, value)
end
-- Get all environment variables as a table
-- Returns a table with all key-value pairs
function env_get_all()
-- Return context table directly if available
if _env then
local copy = {}
for k, v in pairs(_env) do
copy[k] = v
end
return copy
end
-- Fallback to Go call
return __env_get_all()
end
-- Check if an environment variable exists
-- Returns true if the variable exists, false otherwise
function env_exists(key)
if type(key) ~= "string" then
error("env_exists: key must be a string")
end
-- Check context first
if _env then
return _env[key] ~= nil
end
return false
end
-- Set multiple environment variables from a table
-- Returns true on success, false if any setting failed
function env_set_many(vars)
if type(vars) ~= "table" then
error("env_set_many: vars must be a table")
end
if not _env then
_env = {}
end
local success = true
for key, value in pairs(vars) do
if type(key) == "string" and type(value) == "string" then
-- Update context
_env[key] = value
-- Persist to Go
if not __env_set(key, value) then
success = false
end
else
error("env_set_many: all keys and values must be strings")
end
end
return success
end

View File

@ -22,6 +22,10 @@ function __create_env(ctx)
if ctx then if ctx then
env.ctx = ctx env.ctx = ctx
if ctx._env then
env._env = ctx._env
end
end end
if __setup_require then if __setup_require then
@ -457,10 +461,10 @@ _G.render = function(template_str, env)
end end
local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$") local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$")
-- Check if it's a simple variable name for escaped output -- Check if it's a simple variable name for escaped output
local is_simple_var = tag_type == "=" and code:match("^[%w_]+$") local is_simple_var = tag_type == "=" and code:match("^[%w_]+$")
table.insert(chunks, {tag_type, code, pos, is_simple_var}) table.insert(chunks, {tag_type, code, pos, is_simple_var})
pos = close_stop + 1 pos = close_stop + 1
end end
@ -660,4 +664,4 @@ end
function send_binary(content, mime_type) function send_binary(content, mime_type)
http_set_content_type(mime_type or "application/octet-stream") http_set_content_type(mime_type or "application/octet-stream")
return content return content
end end

View File

@ -1,6 +1,7 @@
package runner package runner
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -71,15 +72,11 @@ func (l *ModuleLoader) SetupRequire(state *luajit.State) error {
err := state.DoString(` err := state.DoString(`
-- Initialize global module registry -- Initialize global module registry
__module_paths = {} __module_paths = {}
-- Setup fast module loading system
__module_bytecode = {} __module_bytecode = {}
__ready_modules = {}
-- Create module preload table -- Create module preload table
package.preload = package.preload or {} package.preload = package.preload or {}
-- Setup module state registry
__ready_modules = {}
`) `)
if err != nil { if err != nil {
@ -209,7 +206,7 @@ func (l *ModuleLoader) PreloadModules(state *luajit.State) error {
// Cache bytecode // Cache bytecode
l.bytecodeCache[modName] = bytecode l.bytecodeCache[modName] = bytecode
// Register in Lua // Register in Lua - store path info
escapedPath := escapeLuaString(path) escapedPath := escapeLuaString(path)
escapedName := escapeLuaString(modName) escapedName := escapeLuaString(modName)
@ -217,25 +214,26 @@ func (l *ModuleLoader) PreloadModules(state *luajit.State) error {
return nil return nil
} }
// Load bytecode into Lua state // Load bytecode and register in package.preload properly
if err := state.LoadBytecode(bytecode, path); err != nil { if err := state.LoadBytecode(bytecode, path); err != nil {
return nil return nil
} }
// Add to package.preload // Store the function in package.preload - the function is on the stack
luaCode := ` state.GetGlobal("package")
local modname = "` + escapedName + `" state.GetField(-1, "preload")
local chunk = ... state.PushString(modName)
package.preload[modname] = chunk state.PushCopy(-4) // Copy the compiled function
__ready_modules[modname] = true state.SetTable(-3) // preload[modName] = function
` state.Pop(2) // Pop package and preload tables
if err := state.DoString(luaCode); err != nil { // Mark as ready
state.Pop(1) // Remove chunk from stack if err := state.DoString(`__ready_modules["` + escapedName + `"] = true`); err != nil {
state.Pop(1) // Remove the function from stack
return nil return nil
} }
state.Pop(1) // Remove chunk from stack state.Pop(1) // Remove the function from stack
return nil return nil
}) })
@ -318,24 +316,28 @@ func (l *ModuleLoader) GetModuleByPath(path string) (string, bool) {
l.mu.RLock() l.mu.RLock()
defer l.mu.RUnlock() defer l.mu.RUnlock()
// Clean path for proper comparison // Convert to absolute path for consistent comparison
path = filepath.Clean(path) absPath, err := filepath.Abs(path)
if err != nil {
absPath = filepath.Clean(path)
}
// Try direct lookup from cache // Try direct lookup from cache with absolute path
for modName, modPath := range l.pathCache { for modName, modPath := range l.pathCache {
if modPath == path { if modPath == absPath {
return modName, true return modName, true
} }
} }
// Try to find by relative path from lib dirs // Try to construct module name from lib dirs
for _, dir := range l.config.LibDirs { for _, dir := range l.config.LibDirs {
absDir, err := filepath.Abs(dir) absDir, err := filepath.Abs(dir)
if err != nil { if err != nil {
continue continue
} }
relPath, err := filepath.Rel(absDir, path) // Check if the file is under this lib directory
relPath, err := filepath.Rel(absDir, absPath)
if err != nil || strings.HasPrefix(relPath, "..") { if err != nil || strings.HasPrefix(relPath, "..") {
continue continue
} }
@ -343,13 +345,78 @@ func (l *ModuleLoader) GetModuleByPath(path string) (string, bool) {
if strings.HasSuffix(relPath, ".lua") { if strings.HasSuffix(relPath, ".lua") {
modName := strings.TrimSuffix(relPath, ".lua") modName := strings.TrimSuffix(relPath, ".lua")
modName = strings.ReplaceAll(modName, string(filepath.Separator), ".") modName = strings.ReplaceAll(modName, string(filepath.Separator), ".")
l.debugLog("Found module %s for path %s", modName, path)
return modName, true return modName, true
} }
} }
l.debugLog("No module found for path %s", path)
return "", false return "", false
} }
// RefreshModule recompiles and updates a specific module
func (l *ModuleLoader) RefreshModule(state *luajit.State, moduleName string) error {
l.mu.Lock()
defer l.mu.Unlock()
// Get module path
path, exists := l.pathCache[moduleName]
if !exists {
l.debugLog("Module not found in cache: %s", moduleName)
return fmt.Errorf("module %s not found", moduleName)
}
l.debugLog("Refreshing module: %s at %s", moduleName, path)
// Read updated file content
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read module file: %w", err)
}
// Recompile to bytecode
bytecode, err := state.CompileBytecode(string(content), path)
if err != nil {
return fmt.Errorf("failed to compile module: %w", err)
}
// Update bytecode cache
l.bytecodeCache[moduleName] = bytecode
// Load new bytecode
if err := state.LoadBytecode(bytecode, path); err != nil {
return fmt.Errorf("failed to load bytecode: %w", err)
}
// Update package.preload with new function (function is on stack)
state.GetGlobal("package")
state.GetField(-1, "preload")
state.PushString(moduleName)
state.PushCopy(-4) // Copy the new compiled function
state.SetTable(-3) // preload[moduleName] = new_function
state.Pop(2) // Pop package and preload tables
state.Pop(1) // Pop the function
// Clear from package.loaded so it gets reloaded
escapedName := escapeLuaString(moduleName)
if err := state.DoString(`package.loaded["` + escapedName + `"] = nil`); err != nil {
return fmt.Errorf("failed to clear loaded module: %w", err)
}
l.debugLog("Successfully refreshed module: %s", moduleName)
return nil
}
// RefreshModuleByPath refreshes a module by its file path
func (l *ModuleLoader) RefreshModuleByPath(state *luajit.State, filePath string) error {
moduleName, exists := l.GetModuleByPath(filePath)
if !exists {
return fmt.Errorf("no module found for path: %s", filePath)
}
return l.RefreshModule(state, moduleName)
}
// escapeLuaString escapes special characters in a string for Lua // escapeLuaString escapes special characters in a string for Lua
func escapeLuaString(s string) string { func escapeLuaString(s string) string {
replacer := strings.NewReplacer( replacer := strings.NewReplacer(

View File

@ -384,8 +384,7 @@ waitForInUse:
// NotifyFileChanged alerts the runner about file changes // NotifyFileChanged alerts the runner about file changes
func (r *Runner) NotifyFileChanged(filePath string) bool { func (r *Runner) NotifyFileChanged(filePath string) bool {
logger.Debug("Runner has been notified of a file change...") logger.Debug("Runner notified of file change: %s", filePath)
logger.Debug("%s", filePath)
module, isModule := r.moduleLoader.GetModuleByPath(filePath) module, isModule := r.moduleLoader.GetModuleByPath(filePath)
if isModule { if isModule {
@ -393,7 +392,7 @@ func (r *Runner) NotifyFileChanged(filePath string) bool {
return r.RefreshModule(module) return r.RefreshModule(module)
} }
logger.Debug("File change noted but no state refresh needed: %s", filePath) logger.Debug("File change noted but no refresh needed: %s", filePath)
return true return true
} }
@ -414,10 +413,41 @@ func (r *Runner) RefreshModule(moduleName string) bool {
continue continue
} }
// Invalidate module in Lua // Use the enhanced module loader refresh
if err := state.L.DoString(`package.loaded["` + moduleName + `"] = nil`); err != nil { if err := r.moduleLoader.RefreshModule(state.L, moduleName); err != nil {
success = false success = false
logger.Debug("Failed to invalidate module %s: %v", moduleName, err) logger.Debug("Failed to refresh module %s in state %d: %v", moduleName, state.index, err)
}
}
if success {
logger.Debug("Successfully refreshed module: %s", moduleName)
}
return success
}
// RefreshModuleByPath refreshes a module by its file path
func (r *Runner) RefreshModuleByPath(filePath string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
if !r.isRunning.Load() {
return false
}
logger.Debug("Refreshing module by path: %s", filePath)
success := true
for _, state := range r.states {
if state == nil || state.inUse.Load() {
continue
}
// Use the enhanced module loader refresh by path
if err := r.moduleLoader.RefreshModuleByPath(state.L, filePath); err != nil {
success = false
logger.Debug("Failed to refresh module at %s in state %d: %v", filePath, state.index, err)
} }
} }

View File

@ -121,6 +121,10 @@ func (s *Sandbox) registerCoreFunctions(state *luajit.State) error {
return err return err
} }
if err := RegisterEnvFunctions(state); err != nil {
return err
}
return nil return nil
} }