Commit 0a77f133 authored by Andrei Mihu's avatar Andrei Mihu
Browse files

New check command for runtime modules.

parent 1a026714
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
- Filtering by group state in user groups listing operations.
- Allow max count to be set when creating groups from client calls.
- Log better startup error message when database schema is not set up at all.
- New `check` command to validate runtime modules without starting the server.

### Changed
- Use Go 1.12.9 on Alpine 3.10 as base Docker container image and native builds.
+18 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ package main
import (
	"context"
	"database/sql"
	"flag"
	"fmt"
	"math/rand"
	"net/http"
@@ -76,6 +77,23 @@ func main() {
			return
		case "migrate":
			migrate.Parse(os.Args[2:], tmpLogger)
		case "check":
			// Parse any command line args to look up runtime path.
			// Use full config structure even if not all of its options are available in this command.
			config := server.NewConfig(tmpLogger)
			var runtimePath string
			flags := flag.NewFlagSet("check", flag.ExitOnError)
			flags.StringVar(&runtimePath, "runtime.path", filepath.Join(config.GetDataDir(), "modules"), "Path for the server to scan for Lua and Go library files.")
			if err := flags.Parse(os.Args[2:]); err != nil {
				tmpLogger.Fatal("Could not parse check flags.")
			}
			config.GetRuntime().Path = runtimePath

			if err := server.CheckRuntime(tmpLogger, config); err != nil {
				// Errors are already logged in the function above.
				os.Exit(1)
			}
			return
		}
	}

+39 −8
Original line number Diff line number Diff line
@@ -378,18 +378,15 @@ type Runtime struct {
	eventFunctions *RuntimeEventFunctions
}

func NewRuntime(logger, startupLogger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, leaderboardRankCache LeaderboardRankCache, leaderboardScheduler LeaderboardScheduler, sessionRegistry SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, streamManager StreamManager, router MessageRouter) (*Runtime, error) {
	runtimeConfig := config.GetRuntime()
	startupLogger.Info("Initialising runtime", zap.String("path", runtimeConfig.Path))

	if err := os.MkdirAll(runtimeConfig.Path, os.ModePerm); err != nil {
func GetRuntimePaths(logger *zap.Logger, rootPath string) ([]string, error) {
	if err := os.MkdirAll(rootPath, os.ModePerm); err != nil {
		return nil, err
	}

	paths := make([]string, 0)
	if err := filepath.Walk(runtimeConfig.Path, func(path string, f os.FileInfo, err error) error {
	if err := filepath.Walk(rootPath, func(path string, f os.FileInfo, err error) error {
		if err != nil {
			startupLogger.Error("Error listing runtime path", zap.String("path", path), zap.Error(err))
			logger.Error("Error listing runtime path", zap.String("path", path), zap.Error(err))
			return err
		}

@@ -399,7 +396,41 @@ func NewRuntime(logger, startupLogger *zap.Logger, db *sql.DB, jsonpbMarshaler *
		}
		return nil
	}); err != nil {
		startupLogger.Error("Failed to list runtime path", zap.Error(err))
		logger.Error("Failed to list runtime path", zap.Error(err))
		return nil, err
	}

	return paths, nil
}

func CheckRuntime(logger *zap.Logger, config Config) error {
	// Get all paths inside the configured runtime.
	paths, err := GetRuntimePaths(logger, config.GetRuntime().Path)
	if err != nil {
		return err
	}

	// Check any Go runtime modules.
	err = CheckRuntimeProviderGo(logger, config.GetRuntime().Path, paths)
	if err != nil {
		return err
	}

	// Check any Lua runtime modules.
	err = CheckRuntimeProviderLua(logger, config, paths)
	if err != nil {
		return err
	}

	return nil
}

func NewRuntime(logger, startupLogger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, leaderboardRankCache LeaderboardRankCache, leaderboardScheduler LeaderboardScheduler, sessionRegistry SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, streamManager StreamManager, router MessageRouter) (*Runtime, error) {
	runtimeConfig := config.GetRuntime()
	startupLogger.Info("Initialising runtime", zap.String("path", runtimeConfig.Path))

	paths, err := GetRuntimePaths(startupLogger, runtimeConfig.Path)
	if err != nil {
		return nil, err
	}

+51 −20
Original line number Diff line number Diff line
@@ -1812,34 +1812,18 @@ func NewRuntimeProviderGo(logger, startupLogger *zap.Logger, db *sql.DB, config

	modulePaths := make([]string, 0)
	for _, path := range paths {
		// Skip everything except shared object files.
		if strings.ToLower(filepath.Ext(path)) != ".so" {
			continue
		}

		relPath, _ := filepath.Rel(rootPath, path)
		name := strings.TrimSuffix(relPath, filepath.Ext(relPath))

		// Open the plugin.
		p, err := plugin.Open(path)
		if err != nil {
			startupLogger.Error("Could not open Go module", zap.String("path", path), zap.Error(err))
			return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
		}

		// Look up the required initialisation function.
		f, err := p.Lookup("InitModule")
		// Open the plugin, and look up the required initialisation function.
		relPath, name, fn, err := openGoModule(startupLogger, rootPath, path)
		if err != nil {
			startupLogger.Fatal("Error looking up InitModule function in Go module", zap.String("name", name))
			// Errors are already logged in the function above.
			return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
		}

		// Ensure the function has the correct signature.
		fn, ok := f.(func(context.Context, runtime.Logger, *sql.DB, runtime.NakamaModule, runtime.Initializer) error)
		if !ok {
			startupLogger.Fatal("Error reading InitModule function in Go module", zap.String("name", name))
			return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, errors.New("error reading InitModule function in Go module")
		}

		// Run the initialisation.
		if err = fn(ctx, runtimeLogger, db, nk, initializer); err != nil {
			startupLogger.Fatal("Error returned by InitModule function in Go module", zap.String("name", name), zap.Error(err))
@@ -1883,3 +1867,50 @@ func NewRuntimeProviderGo(logger, startupLogger *zap.Logger, db *sql.DB, config

	return modulePaths, initializer.rpc, initializer.beforeRt, initializer.afterRt, initializer.beforeReq, initializer.afterReq, initializer.matchmakerMatched, matchCreateFn, initializer.tournamentEnd, initializer.tournamentReset, initializer.leaderboardReset, events, nk.SetMatchCreateFn, matchNamesListFn, nil
}

func CheckRuntimeProviderGo(logger *zap.Logger, rootPath string, paths []string) error {
	for _, path := range paths {
		// Skip everything except shared object files.
		if strings.ToLower(filepath.Ext(path)) != ".so" {
			continue
		}

		// Open the plugin, and look up the required initialisation function.
		// The function isn't used here, all we need is a type/signature check.
		_, _, _, err := openGoModule(logger, rootPath, path)
		if err != nil {
			// Errors are already logged in the function above.
			return err
		}
	}

	return nil
}

func openGoModule(logger *zap.Logger, rootPath, path string) (string, string, func(context.Context, runtime.Logger, *sql.DB, runtime.NakamaModule, runtime.Initializer) error, error) {
	relPath, _ := filepath.Rel(rootPath, path)
	name := strings.TrimSuffix(relPath, filepath.Ext(relPath))

	// Open the plugin.
	p, err := plugin.Open(path)
	if err != nil {
		logger.Error("Could not open Go module", zap.String("path", path), zap.Error(err))
		return "", "", nil, err
	}

	// Look up the required initialisation function.
	f, err := p.Lookup("InitModule")
	if err != nil {
		logger.Fatal("Error looking up InitModule function in Go module", zap.String("name", name))
		return "", "", nil, err
	}

	// Ensure the function has the correct signature.
	fn, ok := f.(func(context.Context, runtime.Logger, *sql.DB, runtime.NakamaModule, runtime.Initializer) error)
	if !ok {
		logger.Fatal("Error reading InitModule function in Go module", zap.String("name", name))
		return "", "", nil, errors.New("error reading InitModule function in Go module")
	}

	return relPath, name, fn, nil
}
+112 −48
Original line number Diff line number Diff line
@@ -108,57 +108,15 @@ type RuntimeProviderLua struct {
}

func NewRuntimeProviderLua(logger, startupLogger *zap.Logger, db *sql.DB, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, leaderboardRankCache LeaderboardRankCache, leaderboardScheduler LeaderboardScheduler, sessionRegistry SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, streamManager StreamManager, router MessageRouter, goMatchCreateFn RuntimeMatchCreateFunction, rootPath string, paths []string) ([]string, map[string]RuntimeRpcFunction, map[string]RuntimeBeforeRtFunction, map[string]RuntimeAfterRtFunction, *RuntimeBeforeReqFunctions, *RuntimeAfterReqFunctions, RuntimeMatchmakerMatchedFunction, RuntimeMatchCreateFunction, RuntimeTournamentEndFunction, RuntimeTournamentResetFunction, RuntimeLeaderboardResetFunction, error) {
	moduleCache := &RuntimeLuaModuleCache{
		Names:   make([]string, 0),
		Modules: make(map[string]*RuntimeLuaModule, 0),
	}
	modulePaths := make([]string, 0)

	// Override before Package library is invoked.
	lua.LuaLDir = rootPath
	lua.LuaPathDefault = lua.LuaLDir + string(os.PathSeparator) + "?.lua;" + lua.LuaLDir + string(os.PathSeparator) + "?" + string(os.PathSeparator) + "init.lua"
	if err := os.Setenv(lua.LuaPath, lua.LuaPathDefault); err != nil {
		startupLogger.Error("Could not set Lua module path", zap.Error(err))
		return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
	}

	startupLogger.Info("Initialising Lua runtime provider", zap.String("path", rootPath))

	for _, path := range paths {
		if strings.ToLower(filepath.Ext(path)) != ".lua" {
			continue
		}

		// Load the file contents into memory.
		var content []byte
		var err error
		if content, err = ioutil.ReadFile(path); err != nil {
			startupLogger.Error("Could not read Lua module", zap.String("path", path), zap.Error(err))
	// Load Lua modules into memory by reading the file contents. No evaluation/execution at this stage.
	moduleCache, modulePaths, stdLibs, err := openLuaModules(startupLogger, rootPath, paths)
	if err != nil {
		// Errors already logged in the function call above.
		return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
	}

		relPath, _ := filepath.Rel(rootPath, path)
		name := strings.TrimSuffix(relPath, filepath.Ext(relPath))
		// Make paths Lua friendly.
		name = strings.Replace(name, string(os.PathSeparator), ".", -1)

		moduleCache.Add(&RuntimeLuaModule{
			Name:    name,
			Path:    path,
			Content: content,
		})
		modulePaths = append(modulePaths, relPath)
	}

	stdLibs := map[string]lua.LGFunction{
		lua.LoadLibName:   OpenPackage(moduleCache),
		lua.BaseLibName:   lua.OpenBase,
		lua.TabLibName:    lua.OpenTable,
		lua.OsLibName:     OpenOs,
		lua.StringLibName: lua.OpenString,
		lua.MathLibName:   lua.OpenMath,
		Bit32LibName:      OpenBit32,
	}
	once := &sync.Once{}
	localCache := NewRuntimeLuaLocalCache()
	rpcFunctions := make(map[string]RuntimeRpcFunction, 0)
@@ -962,6 +920,78 @@ func NewRuntimeProviderLua(logger, startupLogger *zap.Logger, db *sql.DB, jsonpb
	return modulePaths, rpcFunctions, beforeRtFunctions, afterRtFunctions, beforeReqFunctions, afterReqFunctions, matchmakerMatchedFunction, allMatchCreateFn, tournamentEndFunction, tournamentResetFunction, leaderboardResetFunction, nil
}

func CheckRuntimeProviderLua(logger *zap.Logger, config Config, paths []string) error {
	// Load Lua modules into memory by reading the file contents. No evaluation/execution at this stage.
	moduleCache, _, stdLibs, err := openLuaModules(logger, config.GetRuntime().Path, paths)
	if err != nil {
		// Errors already logged in the function call above.
		return err
	}

	// Evaluate (but do not execute) available Lua modules.
	err = checkRuntimeLuaVM(logger, config, stdLibs, moduleCache)
	if err != nil {
		// Errors already logged in the function call above.
		return err
	}

	return nil
}

func openLuaModules(logger *zap.Logger, rootPath string, paths []string) (*RuntimeLuaModuleCache, []string, map[string]lua.LGFunction, error) {
	moduleCache := &RuntimeLuaModuleCache{
		Names:   make([]string, 0),
		Modules: make(map[string]*RuntimeLuaModule, 0),
	}
	modulePaths := make([]string, 0)

	// Override before Package library is invoked.
	lua.LuaLDir = rootPath
	lua.LuaPathDefault = lua.LuaLDir + string(os.PathSeparator) + "?.lua;" + lua.LuaLDir + string(os.PathSeparator) + "?" + string(os.PathSeparator) + "init.lua"
	if err := os.Setenv(lua.LuaPath, lua.LuaPathDefault); err != nil {
		logger.Error("Could not set Lua module path", zap.Error(err))
		return nil, nil, nil, err
	}

	for _, path := range paths {
		if strings.ToLower(filepath.Ext(path)) != ".lua" {
			continue
		}

		// Load the file contents into memory.
		var content []byte
		var err error
		if content, err = ioutil.ReadFile(path); err != nil {
			logger.Error("Could not read Lua module", zap.String("path", path), zap.Error(err))
			return nil, nil, nil, err
		}

		relPath, _ := filepath.Rel(rootPath, path)
		name := strings.TrimSuffix(relPath, filepath.Ext(relPath))
		// Make paths Lua friendly.
		name = strings.Replace(name, string(os.PathSeparator), ".", -1)

		moduleCache.Add(&RuntimeLuaModule{
			Name:    name,
			Path:    path,
			Content: content,
		})
		modulePaths = append(modulePaths, relPath)
	}

	stdLibs := map[string]lua.LGFunction{
		lua.LoadLibName:   OpenPackage(moduleCache),
		lua.BaseLibName:   lua.OpenBase,
		lua.TabLibName:    lua.OpenTable,
		lua.OsLibName:     OpenOs,
		lua.StringLibName: lua.OpenString,
		lua.MathLibName:   lua.OpenMath,
		Bit32LibName:      OpenBit32,
	}

	return moduleCache, modulePaths, stdLibs, nil
}

func (rp *RuntimeProviderLua) Rpc(ctx context.Context, id string, queryParams map[string][]string, userID, username string, expiry int64, sessionID, clientIP, clientPort, payload string) (string, error, codes.Code) {
	r, err := rp.Get(ctx)
	if err != nil {
@@ -1752,8 +1782,42 @@ func (r *RuntimeLua) Stop() {
	r.vm.Close()
}

func checkRuntimeLuaVM(logger *zap.Logger, config Config, stdLibs map[string]lua.LGFunction, moduleCache *RuntimeLuaModuleCache) error {
	vm := lua.NewState(lua.Options{
		CallStackSize:       128,
		RegistrySize:        512,
		SkipOpenLibs:        true,
		IncludeGoStackTrace: true,
	})
	vm.SetContext(context.Background())
	for name, lib := range stdLibs {
		vm.Push(vm.NewFunction(lib))
		vm.Push(lua.LString(name))
		vm.Call(1, 0)
	}
	nakamaModule := NewRuntimeLuaNakamaModule(nil, nil, nil, config, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
	vm.PreloadModule("nakama", nakamaModule.Loader)

	preload := vm.GetField(vm.GetField(vm.Get(lua.EnvironIndex), "package"), "preload")
	for _, name := range moduleCache.Names {
		module, ok := moduleCache.Modules[name]
		if !ok {
			logger.Fatal("Failed to find named module in cache", zap.String("name", name))
		}

		f, err := vm.Load(bytes.NewReader(module.Content), module.Path)
		if err != nil {
			logger.Error("Could not load module", zap.String("name", module.Path), zap.Error(err))
			return err
		} else {
			vm.SetField(preload, module.Name, f)
		}
	}

	return nil
}

func newRuntimeLuaVM(logger *zap.Logger, db *sql.DB, jsonpbUnmarshaler *jsonpb.Unmarshaler, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, rankCache LeaderboardRankCache, leaderboardScheduler LeaderboardScheduler, sessionRegistry SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, streamManager StreamManager, router MessageRouter, stdLibs map[string]lua.LGFunction, moduleCache *RuntimeLuaModuleCache, once *sync.Once, localCache *RuntimeLuaLocalCache, matchCreateFn RuntimeMatchCreateFunction, announceCallbackFn func(RuntimeExecutionMode, string)) (*RuntimeLua, error) {
	// Initialize a one-off runtime to ensure startup code runs and modules are valid.
	vm := lua.NewState(lua.Options{
		CallStackSize:       config.GetRuntime().CallStackSize,
		RegistrySize:        config.GetRuntime().RegistrySize,