// +build lua package server import ( "encoding/hex" "fmt" "io" "sync" "github.com/siddontang/go/hack" "github.com/siddontang/go/num" "github.com/siddontang/ledisdb/ledis" "github.com/siddontang/ledisdb/vendor/lua" ) //ledis <-> lua type conversion, same as http://redis.io/commands/eval type luaWriter struct { l *lua.State } func (w *luaWriter) writeError(err error) { panic(err) } func (w *luaWriter) writeStatus(status string) { w.l.NewTable() top := w.l.GetTop() w.l.PushString("ok") w.l.PushString(status) w.l.SetTable(top) } func (w *luaWriter) writeInteger(n int64) { w.l.PushInteger(n) } func (w *luaWriter) writeBulk(b []byte) { if b == nil { w.l.PushBoolean(false) } else { w.l.PushString(hack.String(b)) } } func (w *luaWriter) writeArray(lst []interface{}) { if lst == nil { w.l.PushBoolean(false) return } w.l.CreateTable(len(lst), 0) top := w.l.GetTop() for i, _ := range lst { w.l.PushInteger(int64(i) + 1) switch v := lst[i].(type) { case []interface{}: w.writeArray(v) case [][]byte: w.writeSliceArray(v) case []byte: w.writeBulk(v) case nil: w.writeBulk(nil) case int64: w.writeInteger(v) default: panic("invalid array type") } w.l.SetTable(top) } } func (w *luaWriter) writeSliceArray(lst [][]byte) { if lst == nil { w.l.PushBoolean(false) return } w.l.CreateTable(len(lst), 0) for i, v := range lst { if v == nil { w.l.PushBoolean(false) //TNL: not sure what is "expected" behaviour here. //Redis returns "false", but may be "nil" is more appropriate? //For me both variants seems better, then empty string. // //w.l.PushNil() } else { w.l.PushString(hack.String(v)) } w.l.RawSeti(-2, i+1) /* //TNL: and instead of push nil we can just skip it if v != nil { w.l.PushString(hack.String(v)) w.l.RawSeti(-2, i+1) } */ } } func (w *luaWriter) writeFVPairArray(lst []ledis.FVPair) { if lst == nil { w.l.PushBoolean(false) return } w.l.CreateTable(len(lst)*2, 0) for i, v := range lst { w.l.PushString(hack.String(v.Field)) w.l.RawSeti(-2, 2*i+1) w.l.PushString(hack.String(v.Value)) w.l.RawSeti(-2, 2*i+2) } } func (w *luaWriter) writeScorePairArray(lst []ledis.ScorePair, withScores bool) { if lst == nil { w.l.PushBoolean(false) return } if withScores { w.l.CreateTable(len(lst)*2, 0) for i, v := range lst { w.l.PushString(hack.String(v.Member)) w.l.RawSeti(-2, 2*i+1) w.l.PushString(hack.String(num.FormatInt64ToSlice(v.Score))) w.l.RawSeti(-2, 2*i+2) } } else { w.l.CreateTable(len(lst), 0) for i, v := range lst { w.l.PushString(hack.String(v.Member)) w.l.RawSeti(-2, i+1) } } } func (w *luaWriter) writeBulkFrom(n int64, rb io.Reader) { w.writeError(fmt.Errorf("unsupport")) } func (w *luaWriter) flush() { } type script struct { sync.Mutex app *App l *lua.State c *client chunks map[string]struct{} } func (app *App) openScript() { s := new(script) s.app = app s.chunks = make(map[string]struct{}) app.script = s l := lua.NewState() l.OpenBase() l.OpenLibs() l.OpenMath() l.OpenString() l.OpenTable() l.OpenPackage() l.OpenCJson() l.OpenCMsgpack() l.OpenStruct() l.Register("error", luaErrorHandler) s.l = l s.c = newClient(app) s.c.db = nil w := new(luaWriter) w.l = l s.c.resp = w l.NewTable() l.PushString("call") l.PushGoFunction(luaCall) l.SetTable(-3) l.PushString("pcall") l.PushGoFunction(luaPCall) l.SetTable(-3) l.PushString("sha1hex") l.PushGoFunction(luaSha1Hex) l.SetTable(-3) l.PushString("error_reply") l.PushGoFunction(luaErrorReply) l.SetTable(-3) l.PushString("status_reply") l.PushGoFunction(luaStatusReply) l.SetTable(-3) l.SetGlobal("ledis") setMapState(l, s) } func (app *App) closeScript() { app.script.l.Close() delMapState(app.script.l) app.script = nil } var mapState = map[*lua.State]*script{} var stateLock sync.Mutex func setMapState(l *lua.State, s *script) { stateLock.Lock() defer stateLock.Unlock() mapState[l] = s } func getMapState(l *lua.State) *script { stateLock.Lock() defer stateLock.Unlock() return mapState[l] } func delMapState(l *lua.State) { stateLock.Lock() defer stateLock.Unlock() delete(mapState, l) } func luaErrorHandler(l *lua.State) int { msg := l.ToString(1) panic(fmt.Errorf(msg)) } func luaCall(l *lua.State) int { return luaCallGenericCommand(l) } func luaPCall(l *lua.State) (n int) { defer func() { if e := recover(); e != nil { luaPushError(l, fmt.Sprintf("%v", e)) n = 1 } return }() return luaCallGenericCommand(l) } func luaErrorReply(l *lua.State) int { return luaReturnSingleFieldTable(l, "err") } func luaStatusReply(l *lua.State) int { return luaReturnSingleFieldTable(l, "ok") } func luaReturnSingleFieldTable(l *lua.State, filed string) int { if l.GetTop() != 1 || l.Type(-1) != lua.LUA_TSTRING { luaPushError(l, "wrong number or type of arguments") return 1 } l.NewTable() l.PushString(filed) l.PushValue(-3) l.SetTable(-3) return 1 } func luaSha1Hex(l *lua.State) int { argc := l.GetTop() if argc != 1 { luaPushError(l, "wrong number of arguments") return 1 } s := l.ToString(1) s = hex.EncodeToString(hack.Slice(s)) l.PushString(s) return 1 } func luaPushError(l *lua.State, msg string) { l.NewTable() l.PushString("err") err := l.NewError(msg) l.PushString(err.Error()) l.SetTable(-3) } func luaCallGenericCommand(l *lua.State) int { s := getMapState(l) if s == nil { panic("Invalid lua call") } else if s.c.db == nil { panic("Invalid lua call, not prepared") } c := s.c argc := l.GetTop() if argc < 1 { panic("Please specify at least one argument for ledis.call()") } c.cmd = l.ToString(1) c.args = make([][]byte, argc-1) for i := 2; i <= argc; i++ { switch l.Type(i) { case lua.LUA_TNUMBER: c.args[i-2] = []byte(fmt.Sprintf("%.17g", l.ToNumber(i))) case lua.LUA_TSTRING: c.args[i-2] = []byte(l.ToString(i)) default: panic("Lua ledis() command arguments must be strings or integers") } } c.perform() return 1 } func luaSetGlobalArray(l *lua.State, name string, ay [][]byte) { l.NewTable() for i := 0; i < len(ay); i++ { l.PushString(hack.String(ay[i])) l.RawSeti(-2, i+1) } l.SetGlobal(name) } func luaReplyToLedisReply(l *lua.State) interface{} { base := l.GetTop() defer func() { l.SetTop(base - 1) }() switch l.Type(-1) { case lua.LUA_TSTRING: return hack.Slice(l.ToString(-1)) case lua.LUA_TBOOLEAN: if l.ToBoolean(-1) { return int64(1) } else { return nil } case lua.LUA_TNUMBER: return int64(l.ToInteger(-1)) case lua.LUA_TTABLE: l.PushString("err") l.GetTable(-2) if l.Type(-1) == lua.LUA_TSTRING { return fmt.Errorf("%s", l.ToString(-1)) } l.Pop(1) l.PushString("ok") l.GetTable(-2) if l.Type(-1) == lua.LUA_TSTRING { return l.ToString(-1) } else { l.Pop(1) ay := make([]interface{}, 0) for i := 1; ; i++ { l.PushInteger(int64(i)) l.GetTable(-2) if l.Type(-1) == lua.LUA_TNIL { l.Pop(1) break } ay = append(ay, luaReplyToLedisReply(l)) } return ay } default: return nil } }