Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
1961 lines (1741 sloc) 67 KB
-- Copyright 2015 Paul Kulchenko, ZeroBrane LLC; All rights reserved
-- Integration with Redis data store (http://redis.io) from Redis Labs (https://redislabs.com).
-- Repository of the redis-lua Redis client: https://github.com/nrk/redis-lua
local redis = (function() -- redis-lua module start ----------------------------
local redis = {
_VERSION = 'redis-lua 2.0.5-dev',
_DESCRIPTION = 'A Lua client library for the redis key value storage system.',
_COPYRIGHT = 'Copyright (C) 2009-2012 Daniele Alessandri',
}
local unpack = _G.unpack or table.unpack
local network, request, response = {}, {}, {}
local defaults = {
host = '127.0.0.1',
port = 6379,
tcp_nodelay = true,
path = nil,
}
local function merge_defaults(parameters)
if parameters == nil then
parameters = {}
end
for k, v in pairs(defaults) do
if parameters[k] == nil then
parameters[k] = defaults[k]
end
end
return parameters
end
local function parse_boolean(v)
if v == '1' or v == 'true' or v == 'TRUE' then
return true
elseif v == '0' or v == 'false' or v == 'FALSE' then
return false
else
return nil
end
end
local function toboolean(value) return value == 1 end
local function sort_request(client, command, key, params)
--[[ params = {
by = 'weight_*',
get = 'object_*',
limit = { 0, 10 },
sort = 'desc',
alpha = true,
} ]]
local query = { key }
if params then
if params.by then
table.insert(query, 'BY')
table.insert(query, params.by)
end
if type(params.limit) == 'table' then
-- TODO: check for lower and upper limits
table.insert(query, 'LIMIT')
table.insert(query, params.limit[1])
table.insert(query, params.limit[2])
end
if params.get then
if (type(params.get) == 'table') then
for _, getarg in pairs(params.get) do
table.insert(query, 'GET')
table.insert(query, getarg)
end
else
table.insert(query, 'GET')
table.insert(query, params.get)
end
end
if params.sort then
table.insert(query, params.sort)
end
if params.alpha == true then
table.insert(query, 'ALPHA')
end
if params.store then
table.insert(query, 'STORE')
table.insert(query, params.store)
end
end
request.multibulk(client, command, query)
end
local function zset_range_request(client, command, ...)
local args, opts = {...}, { }
if #args >= 1 and type(args[#args]) == 'table' then
local options = table.remove(args, #args)
if options.withscores then
table.insert(opts, 'WITHSCORES')
end
end
for _, v in pairs(opts) do table.insert(args, v) end
request.multibulk(client, command, args)
end
local function zset_range_byscore_request(client, command, ...)
local args, opts = {...}, { }
if #args >= 1 and type(args[#args]) == 'table' then
local options = table.remove(args, #args)
if options.limit then
table.insert(opts, 'LIMIT')
table.insert(opts, options.limit.offset or options.limit[1])
table.insert(opts, options.limit.count or options.limit[2])
end
if options.withscores then
table.insert(opts, 'WITHSCORES')
end
end
for _, v in pairs(opts) do table.insert(args, v) end
request.multibulk(client, command, args)
end
local function zset_range_reply(reply, command, ...)
local args = {...}
local opts = args[4]
if opts and (opts.withscores or string.lower(tostring(opts)) == 'withscores') then
local new_reply = { }
for i = 1, #reply, 2 do
table.insert(new_reply, { reply[i], reply[i + 1] })
end
return new_reply
else
return reply
end
end
local function zset_store_request(client, command, ...)
local args, opts = {...}, { }
if #args >= 1 and type(args[#args]) == 'table' then
local options = table.remove(args, #args)
if options.weights and type(options.weights) == 'table' then
table.insert(opts, 'WEIGHTS')
for _, weight in ipairs(options.weights) do
table.insert(opts, weight)
end
end
if options.aggregate then
table.insert(opts, 'AGGREGATE')
table.insert(opts, options.aggregate)
end
end
for _, v in pairs(opts) do table.insert(args, v) end
request.multibulk(client, command, args)
end
local function mset_filter_args(client, command, ...)
local args, arguments = {...}, {}
if (#args == 1 and type(args[1]) == 'table') then
for k,v in pairs(args[1]) do
table.insert(arguments, k)
table.insert(arguments, v)
end
else
arguments = args
end
request.multibulk(client, command, arguments)
end
local function hash_multi_request_builder(builder_callback)
return function(client, command, ...)
local args, arguments = {...}, { }
if #args == 2 then
table.insert(arguments, args[1])
for k, v in pairs(args[2]) do
builder_callback(arguments, k, v)
end
else
arguments = args
end
request.multibulk(client, command, arguments)
end
end
local function parse_info(response)
local info = {}
local current = info
response:gsub('([^\r\n]*)\r\n', function(kv)
if kv == '' then return end
local section = kv:match('^# (%w+)$')
if section then
current = {}
info[section:lower()] = current
return
end
local k,v = kv:match(('([^:]*):([^:]*)'):rep(1))
if k:match('db%d+') then
current[k] = {}
v:gsub(',', function(dbkv)
local dbk,dbv = dbkv:match('([^:]*)=([^:]*)')
current[k][dbk] = dbv
end)
else
current[k] = v
end
end)
return info
end
local function scan_request(client, command, ...)
local args, req, params = {...}, { }, nil
if command == 'SCAN' then
table.insert(req, args[1])
params = args[2]
else
table.insert(req, args[1])
table.insert(req, args[2])
params = args[3]
end
if params and params.match then
table.insert(req, 'MATCH')
table.insert(req, params.match)
end
if params and params.count then
table.insert(req, 'COUNT')
table.insert(req, params.count)
end
request.multibulk(client, command, req)
end
local zscan_response = function(reply, command, ...)
local original, new = reply[2], { }
for i = 1, #original, 2 do
table.insert(new, { original[i], tonumber(original[i + 1]) })
end
reply[2] = new
return reply
end
local hscan_response = function(reply, command, ...)
local original, new = reply[2], { }
for i = 1, #original, 2 do
new[original[i]] = original[i + 1]
end
reply[2] = new
return reply
end
local function load_methods(proto, commands)
local client = setmetatable ({}, getmetatable(proto))
for cmd, fn in pairs(commands) do
if type(fn) ~= 'function' then
redis.error('invalid type for command ' .. cmd .. '(must be a function)')
end
client[cmd] = fn
end
for i, v in pairs(proto) do
client[i] = v
end
return client
end
local function create_client(proto, client_socket, commands)
local client = load_methods(proto, commands)
client.error = redis.error
client.network = {
socket = client_socket,
read = network.read,
write = network.write,
}
client.requests = {
multibulk = request.multibulk,
}
return client
end
-- ############################################################################
function network.write(client, buffer)
local _, err = client.network.socket:send(buffer)
if type(client.onwrite) == 'function' then client:onwrite(buffer, err) end
if err then return client.error(err) end
end
function network.read(client, len)
if len == nil then len = '*l' end
local line, err = client.network.socket:receive(len)
if type(client.onread) == 'function' then client:onread(line, err) end
if not err then return line else return client.error(err) end
end
-- ############################################################################
function response.read(client)
local payload, err = client.network.read(client)
if not payload then return client.error('reading error: ' .. err) end
local prefix, data = payload:sub(1, -#payload), payload:sub(2)
-- status reply
if prefix == '+' then
if data == 'OK' then
return true
elseif data == 'QUEUED' then
return { queued = true }
else
return data
end
-- error reply
elseif prefix == '-' then
return client.error('redis error: ' .. data)
-- integer reply
elseif prefix == ':' then
local number = tonumber(data)
if not number then
if data == 'nil' then
return nil
end
return client.error('cannot parse ' .. data .. ' as a numeric response.')
end
return number
-- bulk reply
elseif prefix == '$' then
local length = tonumber(data)
if not length then
return client.error('cannot parse ' .. length .. ' as data length')
end
if length == -1 then
return nil
end
local nextchunk = client.network.read(client, length + 2)
return nextchunk:sub(1, -3)
-- multibulk reply
elseif prefix == '*' then
local count = tonumber(data)
if count == -1 then
return nil
end
local list = {}
if count > 0 then
local reader = response.read
for i = 1, count do
list[i] = reader(client)
end
end
return list
-- unknown type of reply
else
return client.error('unknown response prefix: ' .. prefix)
end
end
-- ############################################################################
function request.raw(client, buffer)
local bufferType = type(buffer)
if bufferType == 'table' then
client.network.write(client, table.concat(buffer))
elseif bufferType == 'string' then
client.network.write(client, buffer)
else
client.error('argument error: ' .. bufferType)
end
end
function request.multibulk(client, command, ...)
local args = {...}
local argsn = #args
local buffer = { true, true }
if argsn == 1 and type(args[1]) == 'table' then
argsn, args = #args[1], args[1]
end
buffer[1] = '*' .. tostring(argsn + 1) .. "\r\n"
buffer[2] = '$' .. #command .. "\r\n" .. command .. "\r\n"
local table_insert = table.insert
for i = 1, argsn do
local s_argument = tostring(args[i] or '')
table_insert(buffer, '$' .. #s_argument .. "\r\n" .. s_argument .. "\r\n")
end
client.network.write(client, table.concat(buffer))
end
-- ############################################################################
local function custom(command, send, parse)
command = string.upper(command)
return function(client, ...)
send(client, command, ...)
local reply, err = response.read(client)
if type(reply) == 'table' and reply.queued then
reply.parser = parse
return reply
elseif reply == nil then
return reply, err
else
if parse then
return parse(reply, command, ...)
end
return reply
end
end
end
local function command(command, opts)
if opts == nil or type(opts) == 'function' then
return custom(command, request.multibulk, opts)
else
return custom(command, opts.request or request.multibulk, opts.response)
end
end
local define_command_impl = function(target, name, opts)
local opts = opts or {}
target[string.lower(name)] = custom(
opts.command or string.upper(name),
opts.request or request.multibulk,
opts.response or nil
)
end
local undefine_command_impl = function(target, name)
target[string.lower(name)] = nil
end
-- ############################################################################
local client_prototype = {}
client_prototype.raw_cmd = function(client, buffer)
request.raw(client, buffer .. "\r\n")
return response.read(client)
end
-- obsolete
client_prototype.define_command = function(client, name, opts)
define_command_impl(client, name, opts)
end
-- obsolete
client_prototype.undefine_command = function(client, name)
undefine_command_impl(client, name)
end
client_prototype.quit = function(client)
request.multibulk(client, 'QUIT')
client.network.socket:shutdown()
return true
end
client_prototype.shutdown = function(client)
request.multibulk(client, 'SHUTDOWN')
client.network.socket:shutdown()
end
-- Command pipelining
client_prototype.pipeline = function(client, block)
local requests, replies, parsers = {}, {}, {}
local table_insert = table.insert
local socket_write, socket_read = client.network.write, client.network.read
client.network.write = function(_, buffer)
table_insert(requests, buffer)
end
-- TODO: this hack is necessary to temporarily reuse the current
-- request -> response handling implementation of redis-lua
-- without further changes in the code, but it will surely
-- disappear when the new command-definition infrastructure
-- will finally be in place.
client.network.read = function() return '+QUEUED' end
local pipeline = setmetatable({}, {
__index = function(env, name)
local cmd = client[name]
if not cmd then
client.error('unknown redis command: ' .. name, 2)
end
return function(self, ...)
local reply = cmd(client, ...)
table_insert(parsers, #requests, reply.parser)
return reply
end
end
})
local success, retval = pcall(block, pipeline)
client.network.write, client.network.read = socket_write, socket_read
if not success then return client.error(retval, 0) end
client.network.write(client, table.concat(requests, ''))
for i = 1, #requests do
local reply, parser = response.read(client), parsers[i]
if parser then
reply = parser(reply)
end
table_insert(replies, i, reply)
end
return replies, #requests
end
-- Publish/Subscribe
do
local channels = function(channels)
if type(channels) == 'string' then
channels = { channels }
end
return channels
end
local subscribe = function(client, ...)
request.multibulk(client, 'subscribe', ...)
end
local psubscribe = function(client, ...)
request.multibulk(client, 'psubscribe', ...)
end
local unsubscribe = function(client, ...)
request.multibulk(client, 'unsubscribe')
end
local punsubscribe = function(client, ...)
request.multibulk(client, 'punsubscribe')
end
local consumer_loop = function(client)
local aborting, subscriptions = false, 0
local abort = function()
if not aborting then
unsubscribe(client)
punsubscribe(client)
aborting = true
end
end
return coroutine.wrap(function()
while true do
local message
local response = response.read(client)
if response[1] == 'pmessage' then
message = {
kind = response[1],
pattern = response[2],
channel = response[3],
payload = response[4],
}
else
message = {
kind = response[1],
channel = response[2],
payload = response[3],
}
end
if string.match(message.kind, '^p?subscribe$') then
subscriptions = subscriptions + 1
end
if string.match(message.kind, '^p?unsubscribe$') then
subscriptions = subscriptions - 1
end
if aborting and subscriptions == 0 then
break
end
coroutine.yield(message, abort)
end
end)
end
client_prototype.pubsub = function(client, subscriptions)
if type(subscriptions) == 'table' then
if subscriptions.subscribe then
subscribe(client, channels(subscriptions.subscribe))
end
if subscriptions.psubscribe then
psubscribe(client, channels(subscriptions.psubscribe))
end
end
return consumer_loop(client)
end
end
-- Redis transactions (MULTI/EXEC)
do
local function identity(...) return ... end
local emptytable = {}
local function initialize_transaction(client, options, block, queued_parsers)
local table_insert = table.insert
local coro = coroutine.create(block)
if options.watch then
local watch_keys = {}
for _, key in pairs(options.watch) do
table_insert(watch_keys, key)
end
if #watch_keys > 0 then
client:watch(unpack(watch_keys))
end
end
local transaction_client = setmetatable({}, {__index=client})
transaction_client.exec = function(...)
return client.error('cannot use EXEC inside a transaction block')
end
transaction_client.multi = function(...)
coroutine.yield()
end
transaction_client.commands_queued = function()
return #queued_parsers
end
assert(coroutine.resume(coro, transaction_client))
transaction_client.multi = nil
transaction_client.discard = function(...)
local reply = client:discard()
for i, v in pairs(queued_parsers) do
queued_parsers[i]=nil
end
coro = initialize_transaction(client, options, block, queued_parsers)
return reply
end
transaction_client.watch = function(...)
return client.error('WATCH inside MULTI is not allowed')
end
setmetatable(transaction_client, { __index = function(t, k)
local cmd = client[k]
if type(cmd) == "function" then
local function queuey(self, ...)
local reply = cmd(client, ...)
assert((reply or emptytable).queued == true, 'a QUEUED reply was expected')
table_insert(queued_parsers, reply.parser or identity)
return reply
end
t[k]=queuey
return queuey
else
return cmd
end
end
})
client:multi()
return coro
end
local function transaction(client, options, coroutine_block, attempts)
local queued_parsers, replies = {}, {}
local retry = tonumber(attempts) or tonumber(options.retry) or 2
local coro = initialize_transaction(client, options, coroutine_block, queued_parsers)
local success, retval
if coroutine.status(coro) == 'suspended' then
success, retval = coroutine.resume(coro)
else
-- do not fail if the coroutine has not been resumed (missing t:multi() with CAS)
success, retval = true, 'empty transaction'
end
if #queued_parsers == 0 or not success then
client:discard()
assert(success, retval)
return replies, 0
end
local raw_replies = client:exec()
if not raw_replies then
if (retry or 0) <= 0 then
return client.error("MULTI/EXEC transaction aborted by the server")
else
--we're not quite done yet
return transaction(client, options, coroutine_block, retry - 1)
end
end
local table_insert = table.insert
for i, parser in pairs(queued_parsers) do
table_insert(replies, i, parser(raw_replies[i]))
end
return replies, #queued_parsers
end
client_prototype.transaction = function(client, arg1, arg2)
local options, block
if not arg2 then
options, block = {}, arg1
elseif arg1 then --and arg2, implicitly
options, block = type(arg1)=="table" and arg1 or { arg1 }, arg2
else
return client.error("Invalid parameters for redis transaction.")
end
if not options.watch then
local watch_keys = { }
for i, v in pairs(options) do
if tonumber(i) then
table.insert(watch_keys, v)
options[i] = nil
end
end
options.watch = watch_keys
elseif not (type(options.watch) == 'table') then
options.watch = { options.watch }
end
if not options.cas then
local tx_block = block
block = function(client, ...)
client:multi()
return tx_block(client, ...) --can't wrap this in pcall because we're in a coroutine.
end
end
return transaction(client, options, block)
end
end
-- MONITOR context
do
local monitor_loop = function(client)
local monitoring = true
-- Tricky since the payload format changed starting from Redis 2.6.
local pattern = '^(%d+%.%d+)( ?.- ?) ?"(%a+)" ?(.-)$'
local abort = function()
monitoring = false
end
return coroutine.wrap(function()
client:monitor()
while monitoring do
local message, matched
local response = response.read(client)
local ok = response:gsub(pattern, function(time, info, cmd, args)
message = {
timestamp = tonumber(time),
client = info:match('%d+.%d+.%d+.%d+:%d+'),
database = tonumber(info:match('%d+')) or 0,
command = cmd,
arguments = args:match('.+'),
}
matched = true
end)
if not matched then
return client.error('Unable to match MONITOR payload: '..response)
end
coroutine.yield(message, abort)
end
end)
end
client_prototype.monitor_messages = function(client)
return monitor_loop(client)
end
end
-- ############################################################################
local function connect_tcp(socket, parameters)
local host, port = parameters.host, tonumber(parameters.port)
if parameters.timeout then
socket:settimeout(parameters.timeout, 'b')
end
local ok, err = socket:connect(host, port)
if not ok then
redis.error('could not connect to '..host..':'..port..' ['..err..']')
end
socket:setoption('tcp-nodelay', parameters.tcp_nodelay)
return socket
end
local function connect_unix(socket, parameters)
local ok, err = socket:connect(parameters.path)
if not ok then
redis.error('could not connect to '..parameters.path..' ['..err..']')
end
return socket
end
local function create_connection(parameters)
if parameters.socket then
return parameters.socket
end
local perform_connection, socket
if parameters.scheme == 'unix' then
perform_connection, socket = connect_unix, require('socket.unix')
assert(socket, 'your build of LuaSocket does not support UNIX domain sockets')
else
if parameters.scheme then
local scheme = parameters.scheme
assert(scheme == 'redis' or scheme == 'tcp', 'invalid scheme: '..scheme)
end
perform_connection, socket = connect_tcp, require('socket').tcp
end
return perform_connection(socket(), parameters)
end
-- ############################################################################
function redis.error(message, level)
error(message, (level or 1) + 1)
end
function redis.connect(...)
local args, parameters = {...}, nil
if #args == 1 then
if type(args[1]) == 'table' then
parameters = args[1]
else
local uri = require('socket.url')
parameters = uri.parse(select(1, ...))
if parameters.scheme then
if parameters.query then
for k, v in parameters.query:gmatch('([-_%w]+)=([-_%w]+)') do
if k == 'tcp_nodelay' or k == 'tcp-nodelay' then
parameters.tcp_nodelay = parse_boolean(v)
elseif k == 'timeout' then
parameters.timeout = tonumber(v)
end
end
end
else
parameters.host = parameters.path
end
end
elseif #args > 1 then
local host, port, timeout = unpack(args)
parameters = { host = host, port = port, timeout = tonumber(timeout) }
end
local commands = redis.commands or {}
if type(commands) ~= 'table' then
redis.error('invalid type for the commands table')
end
local socket = create_connection(merge_defaults(parameters))
local client = create_client(client_prototype, socket, commands)
return client
end
function redis.command(cmd, opts)
return command(cmd, opts)
end
-- obsolete
function redis.define_command(name, opts)
define_command_impl(redis.commands, name, opts)
end
-- obsolete
function redis.undefine_command(name)
undefine_command_impl(redis.commands, name)
end
-- ############################################################################
-- Commands defined in this table do not take the precedence over
-- methods defined in the client prototype table.
redis.commands = {
-- commands operating on the key space
exists = command('EXISTS', {
response = toboolean
}),
del = command('DEL'),
type = command('TYPE'),
rename = command('RENAME'),
renamenx = command('RENAMENX', {
response = toboolean
}),
expire = command('EXPIRE', {
response = toboolean
}),
pexpire = command('PEXPIRE', { -- >= 2.6
response = toboolean
}),
expireat = command('EXPIREAT', {
response = toboolean
}),
pexpireat = command('PEXPIREAT', { -- >= 2.6
response = toboolean
}),
ttl = command('TTL'),
pttl = command('PTTL'), -- >= 2.6
move = command('MOVE', {
response = toboolean
}),
dbsize = command('DBSIZE'),
persist = command('PERSIST', { -- >= 2.2
response = toboolean
}),
keys = command('KEYS', {
response = function(response)
if type(response) == 'string' then
-- backwards compatibility path for Redis < 2.0
local keys = {}
response:gsub('[^%s]+', function(key)
table.insert(keys, key)
end)
response = keys
end
return response
end
}),
randomkey = command('RANDOMKEY'),
sort = command('SORT', {
request = sort_request,
}),
scan = command('SCAN', { -- >= 2.8
request = scan_request,
}),
-- commands operating on string values
set = command('SET'),
setnx = command('SETNX', {
response = toboolean
}),
setex = command('SETEX'), -- >= 2.0
psetex = command('PSETEX'), -- >= 2.6
mset = command('MSET', {
request = mset_filter_args
}),
msetnx = command('MSETNX', {
request = mset_filter_args,
response = toboolean
}),
get = command('GET'),
mget = command('MGET'),
getset = command('GETSET'),
incr = command('INCR'),
incrby = command('INCRBY'),
incrbyfloat = command('INCRBYFLOAT', { -- >= 2.6
response = function(reply, command, ...)
return tonumber(reply)
end,
}),
decr = command('DECR'),
decrby = command('DECRBY'),
append = command('APPEND'), -- >= 2.0
substr = command('SUBSTR'), -- >= 2.0
strlen = command('STRLEN'), -- >= 2.2
setrange = command('SETRANGE'), -- >= 2.2
getrange = command('GETRANGE'), -- >= 2.2
setbit = command('SETBIT'), -- >= 2.2
getbit = command('GETBIT'), -- >= 2.2
bitop = command('BITOP'), -- >= 2.6
bitcount = command('BITCOUNT'), -- >= 2.6
-- commands operating on lists
rpush = command('RPUSH'),
lpush = command('LPUSH'),
llen = command('LLEN'),
lrange = command('LRANGE'),
ltrim = command('LTRIM'),
lindex = command('LINDEX'),
lset = command('LSET'),
lrem = command('LREM'),
lpop = command('LPOP'),
rpop = command('RPOP'),
rpoplpush = command('RPOPLPUSH'),
blpop = command('BLPOP'), -- >= 2.0
brpop = command('BRPOP'), -- >= 2.0
rpushx = command('RPUSHX'), -- >= 2.2
lpushx = command('LPUSHX'), -- >= 2.2
linsert = command('LINSERT'), -- >= 2.2
brpoplpush = command('BRPOPLPUSH'), -- >= 2.2
-- commands operating on sets
sadd = command('SADD'),
srem = command('SREM'),
spop = command('SPOP'),
smove = command('SMOVE', {
response = toboolean
}),
scard = command('SCARD'),
sismember = command('SISMEMBER', {
response = toboolean
}),
sinter = command('SINTER'),
sinterstore = command('SINTERSTORE'),
sunion = command('SUNION'),
sunionstore = command('SUNIONSTORE'),
sdiff = command('SDIFF'),
sdiffstore = command('SDIFFSTORE'),
smembers = command('SMEMBERS'),
srandmember = command('SRANDMEMBER'),
sscan = command('SSCAN', { -- >= 2.8
request = scan_request,
}),
-- commands operating on sorted sets
zadd = command('ZADD'),
zincrby = command('ZINCRBY', {
response = function(reply, command, ...)
return tonumber(reply)
end,
}),
zrem = command('ZREM'),
zrange = command('ZRANGE', {
request = zset_range_request,
response = zset_range_reply,
}),
zrevrange = command('ZREVRANGE', {
request = zset_range_request,
response = zset_range_reply,
}),
zrangebyscore = command('ZRANGEBYSCORE', {
request = zset_range_byscore_request,
response = zset_range_reply,
}),
zrevrangebyscore = command('ZREVRANGEBYSCORE', { -- >= 2.2
request = zset_range_byscore_request,
response = zset_range_reply,
}),
zunionstore = command('ZUNIONSTORE', { -- >= 2.0
request = zset_store_request
}),
zinterstore = command('ZINTERSTORE', { -- >= 2.0
request = zset_store_request
}),
zcount = command('ZCOUNT'),
zcard = command('ZCARD'),
zscore = command('ZSCORE'),
zremrangebyscore = command('ZREMRANGEBYSCORE'),
zrank = command('ZRANK'), -- >= 2.0
zrevrank = command('ZREVRANK'), -- >= 2.0
zremrangebyrank = command('ZREMRANGEBYRANK'), -- >= 2.0
zscan = command('ZSCAN', { -- >= 2.8
request = scan_request,
response = zscan_response,
}),
-- commands operating on hashes
hset = command('HSET', { -- >= 2.0
response = toboolean
}),
hsetnx = command('HSETNX', { -- >= 2.0
response = toboolean
}),
hmset = command('HMSET', { -- >= 2.0
request = hash_multi_request_builder(function(args, k, v)
table.insert(args, k)
table.insert(args, v)
end),
}),
hincrby = command('HINCRBY'), -- >= 2.0
hincrbyfloat = command('HINCRBYFLOAT', {-- >= 2.6
response = function(reply, command, ...)
return tonumber(reply)
end,
}),
hget = command('HGET'), -- >= 2.0
hmget = command('HMGET', { -- >= 2.0
request = hash_multi_request_builder(function(args, k, v)
table.insert(args, v)
end),
}),
hdel = command('HDEL'), -- >= 2.0
hexists = command('HEXISTS', { -- >= 2.0
response = toboolean
}),
hlen = command('HLEN'), -- >= 2.0
hkeys = command('HKEYS'), -- >= 2.0
hvals = command('HVALS'), -- >= 2.0
hgetall = command('HGETALL', { -- >= 2.0
response = function(reply, command, ...)
local new_reply = { }
for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end
return new_reply
end
}),
hscan = command('HSCAN', { -- >= 2.8
request = scan_request,
response = hscan_response,
}),
-- connection related commands
ping = command('PING', {
response = function(response) return response == 'PONG' end
}),
echo = command('ECHO'),
auth = command('AUTH'),
select = command('SELECT'),
-- transactions
multi = command('MULTI'), -- >= 2.0
exec = command('EXEC'), -- >= 2.0
discard = command('DISCARD'), -- >= 2.0
watch = command('WATCH'), -- >= 2.2
unwatch = command('UNWATCH'), -- >= 2.2
-- publish - subscribe
subscribe = command('SUBSCRIBE'), -- >= 2.0
unsubscribe = command('UNSUBSCRIBE'), -- >= 2.0
psubscribe = command('PSUBSCRIBE'), -- >= 2.0
punsubscribe = command('PUNSUBSCRIBE'), -- >= 2.0
publish = command('PUBLISH'), -- >= 2.0
-- redis scripting
eval = command('EVAL'), -- >= 2.6
evalsha = command('EVALSHA'), -- >= 2.6
script = command('SCRIPT'), -- >= 2.6
-- remote server control commands
bgrewriteaof = command('BGREWRITEAOF'),
config = command('CONFIG', { -- >= 2.0
response = function(reply, command, ...)
if (type(reply) == 'table') then
local new_reply = { }
for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end
return new_reply
end
return reply
end
}),
client = command('CLIENT'), -- >= 2.4
slaveof = command('SLAVEOF'),
save = command('SAVE'),
bgsave = command('BGSAVE'),
lastsave = command('LASTSAVE'),
flushdb = command('FLUSHDB'),
flushall = command('FLUSHALL'),
monitor = command('MONITOR'),
time = command('TIME'), -- >= 2.6
slowlog = command('SLOWLOG', { -- >= 2.2.13
response = function(reply, command, ...)
if (type(reply) == 'table') then
local structured = { }
for index, entry in ipairs(reply) do
structured[index] = {
id = tonumber(entry[1]),
timestamp = tonumber(entry[2]),
duration = tonumber(entry[3]),
command = entry[4],
}
end
return structured
end
return reply
end
}),
info = command('INFO', {
response = parse_info,
}),
}
-- ############################################################################
return redis -- redis-lua module end -------------------------------------------
end)()
local api = {
KEYS = {
type = "value",
description = "A table with the key names that were explicitly declared for the script to access.\n\nUse the `Command Line Parameters` to provide the script with parameters in `redis-cli --eval`-like format, i.e.: [key1 [key2 [...]]] , [arg1 [arg2 [...]]]"
},
ARGV = {
type = "value",
description = "A table with the arguments that were passed to the script.\n\nUse the `Command Line Parameters` to provide the script with parameters in `redis-cli --eval`-like format, i.e.: [key1 [key2 [...]]] , [arg1 [arg2 [...]]]"
},
redis = {
type = "lib",
description = "A library for inteacting with the Redis server in which the script is EVALed.",
childs = {
-- Values
REPL_ALL = {
type = "value",
description = "Target setting for calling redis.set_repl() that replicates effects both to AOF and slaves (the default).",
},
REPL_AOF = {
type = "value",
description = "Target setting for calling redis.set_repl() that replicates effects only to AOF.",
},
REPL_SLAVE = {
type = "value",
description = "Target setting for calling redis.set_repl() that replicates effects only to slaves.",
},
REPL_NONE = {
type = "value",
description = "Target setting for calling redis.set_repl() that disables replication.",
},
LOG_DEBUG = {
type = "value",
description = "DEBUG loglevel setting for calling redis.log().",
},
LOG_VERBOSE = {
type = "value",
description = "VERBOSE loglevel setting for calling redis.log().",
},
LOG_NOTICE = {
type = "value",
description = "NOTICE loglevel setting for calling redis.log().",
},
LOG_WARNING = {
type = "value",
description = "WARNING loglevel setting for calling redis.log().",
},
-- functions
error_reply = {
type = "function",
description = "Returns a Redis error reply.\nThis helper function returns a single field table with the err field set to the specified string.",
args = "(error: string)",
returns = "(table)",
},
status_reply = {
type = "function",
description = "Returns a Redis status reply.\nThis helper function returns a single field table with the ok field set to the specified string.",
args = "(status: string)",
returns = "(table)",
},
call = {
type = "function",
description = "Calls a Redis command, raising errors if any.\nIf the call to the command returns an error, the error will be raised and returned to the caller. To trap the error consider using pcall instead.\nThe arguments should be a well-formed Redis command. The return value depends on the command called and the dataset.",
args = "(command: string, [arg: string|number, ...])",
returns = "(number|string|table|boolean)",
},
pcall = {
type = "function",
description = "Calls a Redis command, trapping errors if any.\nIf the call to the command returns an error, the error will be trapped and the function will return a table with the error. To raise the error to the caller consider using call instead.\nThe arguments should be a well-formed Redis command. The return value depends on the command called and the dataset.",
args = "(command: string, [arg: string|number, ...])",
returns = "(number|string|table|boolean)",
},
replicate_commands = {
type = "function",
description = "Enables effects replication mode for the script.\nReturns true if successful.\n",
args = "()",
returns = "(boolean)",
},
set_repl = {
type = "function",
description = "Sets the replication target when in effects replication mode.\n\nAccepts one of the following targets:\n* redis.REPL_ALL: Replicate to AOF and slaves (the default).\n* redis.REPL_AOF: Replicate only to AOF.\n* redis.REPL_SLAVE: Replicate only to slaves.\n* redis.REPL_NONE: Don't replicate at all.",
args = "(target: redis.REPL_(ALL|AOF|SLAVE|NONE))", -- TODO: replace with something else?
returns = "()",
},
sha1hex = {
type = "function",
description = "Performs the SHA1 of the input string.",
args = "(input: string)",
returns = "(string)",
},
breakpoint = {
type = "function",
description = "When in SCRIPT DEBUG mode, stops execution as if a breakpoint is encountered at the next line.",
args = "()",
returns = "()",
},
debug = {
type = "function",
description = "When in SCRIPT DEBUG mode, emits a log message to the debug console.",
args = "(message: string)",
returns = "()",
},
log = {
type = "function",
description = "Emits a log message to the Redis log file.\nAccepst one of the standard Redis loglevels and the message to emit.",
args = "(loglevel: redis.LOG_(DEBUG|VERBOSE|NOTICE|WARNING), message: string)",
returns = "()",
},
},
},
struct = {
type = "lib",
description = "A library for packing/unpacking structures within Lua.",
childs = {
pack = {
type = "function",
description = "Returns an encoded representation of the arguments according to the specified format.\n\nValid formats:\n> - big endian\n< - little endian\n![num] - alignment\nx - padding\nb/B - signed/unsigned byte\nh/H - signed/unsigned short\nl/L - signed/unsigned long\nT - size_t\ni/In - signed/unsigned integer with size `n' (default is size of int)\ncn - sequence of `n' chars (from/to a string); when packing, n==0 means\n the whole string; when unpacking, n==0 means use the previous\n read number as the string length\ns - zero-terminated string\nf - float\nd - double\n' ' - ignored",
args = "(format: string, ...)",
returns = "(string)",
},
unpack = {
type = "function",
description = "Returns the values from an encoded representation according to the specified format.\n\nValid formats:\n> - big endian\n< - little endian\n![num] - alignment\nx - padding\nb/B - signed/unsigned byte\nh/H - signed/unsigned short\nl/L - signed/unsigned long\nT - size_t\ni/In - signed/unsigned integer with size `n' (default is size of int)\ncn - sequence of `n' chars (from/to a string); when packing, n==0 means\n the whole string; when unpacking, n==0 means use the previous\n read number as the string length\ns - zero-terminated string\nf - float\nd - double\n' ' - ignored",
args = "(format: string, struct: string)",
returns = "(table)", -- TODO: the table also has an additional element???
},
size = {
type = "function",
description = "Returns the size of the encoded representation according to the specified format.\n\nValid formats:\n> - big endian\n< - little endian\n![num] - alignment\nx - padding\nb/B - signed/unsigned byte\nh/H - signed/unsigned short\nl/L - signed/unsigned long\nT - size_t\ni/In - signed/unsigned integer with size `n' (default is size of int)\ncn - sequence of `n' chars (from/to a string); when packing, n==0 means\n the whole string; when unpacking, n==0 means use the previous\n read number as the string length\ns - zero-terminated string\nf - float\nd - double\n' ' - ignored",
args = "(format: string)",
returns = "(number)"
},
},
},
cjson = {
type = "lib",
description = "A library for encoding/decoding JSON.",
childs = {
encode = {
type = "function",
description = "Encodes a Lua table as a JSON document.",
args = "(input: table)",
returns = "(string)",
},
decode = {
type = "function",
description = "Converts a JSON document to a Lua table.",
args = "(JSON: string)",
returns = "(table)",
},
},
},
cmsgpack = {
type = "lib",
description = "A library for encoding/decoding MessagePack.",
childs = {
pack = {
type = "function",
description = "Encodes a Lua table as a MessagePack.",
args = "(input: table)",
returns = "(string)",
},
unpack = {
type = "function",
description = "Converts a MessagePack to a Lua table.",
args = "(msgpack: string)",
returns = "(table)",
},
},
},
bit = {
type = "lib",
description = "A library for bitwise operations on numbers.",
childs = {
tobit = {
type = "function",
description = "Normalizes a number to the numeric range for bit operations and returns it.",
args = "(x: number)",
returns = "(number)",
},
tohex = {
type = "function",
description = "Converts its first argument to a hex string. The number of hex digits is given by the absolute value of the optional second argument. Positive numbers between 1 and 8 generate lowercase hex digits. Negative numbers generate uppercase hex digits. Only the least-significant 4*|n| bits are used. The default is to generate 8 lowercase hex digits.",
args = "(x: number [, n: number])",
returns = "(string)",
},
bnot = {
type = "function",
description = "Returns the bitwise not of its argument.",
args = "(x: number)",
returns = "(number)",
},
band = {
type = "function",
description = "Returns the bitwise and of all of its arguments.",
args = "(x1: number [, x2: number, ...])",
returns = "(number)",
},
bor = {
type = "function",
description = "Returns the bitwise or of all of its arguments.",
args = "(x1: number [, x2: number, ...])",
returns = "(number)",
},
bxor = {
type = "function",
description = "Returns the bitwise xor of all of its arguments.",
args = "(x1: number [, x2: number, ...])",
returns = "(number)",
},
lshift = {
type = "function",
description = "Returns the bitwise logical left-shift of its first argument by the number of bits given by the second argument.\nTreats the first argument as an unsigned number and shifts in 0-bits.\nOnly the lower 5 bits of the shift count are used (reduces to the range [0..31]).",
args = "(x: number, n: number)",
returns = "(number)",
},
rshift = {
type = "function",
description = "Returns the bitwise logical right-shift of its first argument by the number of bits given by the second argument.\nTreats the first argument as an unsigned number and shifts in 0-bits.\nOnly the lower 5 bits of the shift count are used (reduces to the range [0..31]).",
args = "(x: number, n: number)",
returns = "(number)",
},
arshift = {
type = "function",
description = "Returns the bitwise arithmetic right-shift of its first argument by the number of bits given by the second argument.\nTreats the most-significant bit as a sign bit and replicates it.\nOnly the lower 5 bits of the shift count are used (reduces to the range [0..31]).",
args = "(x: number, n: number)",
returns = "(number)",
},
rol = {
type = "function",
description = "Returns the bitwise left rotation of its first argument by the number of bits given by the second argument.\nBits shifted out on one side are shifted back in on the other side.\nOnly the lower 5 bits of the rotate count are used (reduces to the range [0..31]).",
args = "(x: number, n: number)",
returns = "(number)",
},
ror = {
type = "function",
description = "Returns the bitwise right rotation of its first argument by the number of bits given by the second argument.\nBits shifted out on one side are shifted back in on the other side.\nOnly the lower 5 bits of the rotate count are used (reduces to the range [0..31]).",
args = "(x: number, n: number)",
returns = "(number)",
},
bswap = {
type = "function",
description = "Swaps the bytes of its argument and returns it.",
args = "(x: number)",
returns = "(number)",
},
},
},
}
local function isinstance(host, port, password)
local ok, res = pcall(redis.connect, {host = host, port = port, timeout = 0.5})
if not ok then return nil, res:match("%[(.+)%]") end
-- check if the instance is password protected
local client = res
ok, res = pcall(client.ping, client)
if not ok and res:find("NOAUTH")
and (not password or not pcall(client.auth, client, password)) then
while true do
password = password or wx.wxGetPasswordFromUser("Enter Redis password", "Redis authentication")
if not password or password == "" then return end
if pcall(client.auth, client, password) then break end
password = nil
end
end
client:quit()
return true, password
end
local pkg
local address, password
local interpreter = {
name = "Redis",
description = "Redis interpreter",
api = {"baselib", "redis"},
frun = function(self,wfilename,rundebug)
if not pkg then
ide:Print("Can't get package configuration.")
return
end
local filepath = wfilename:GetFullPath()
if ide.osname == 'Windows' then
-- if running on Windows and can't open the file, this may mean that
-- the file path includes unicode characters that need special handling
local fh = io.open(filepath, "r")
if fh then fh:close() end
if pcall(require, "winapi") and wfilename:FileExists() and not fh then
winapi.set_encoding(winapi.CP_UTF8)
filepath = winapi.short_path(filepath)
end
end
local defaddress = pkg:GetSettings().address or "redis://localhost:6379"
while true do
defaddress = address or wx.wxGetTextFromUser(
"Database URI ([redis://][username:password@]hostname:port[/db])", "Redis server", defaddress)
-- Redis currently ignores the username, should change with RCP1
if not defaddress or defaddress == "" then return end
local uri = require('socket.url').parse(defaddress)
if uri.scheme ~= "redis" then return end -- need to think about support for sockets, future support for SSL/TLS
if not uri.host or not uri.port then
ide:Print(("Can't get host name or port number from URI '%s'."):format(defaddress))
else
local ok, err = isinstance(uri.host, uri.port, (password or uri.password))
if ok then
address, password = uri.scheme .. "://" .. uri.host .. ":" .. uri.port .. (uri.path or ""), err
pkg:SetSettings({address = address})
break
elseif err then
ide:Print(("Can't connect to Redis instance '%s': %s."):format(defaddress, err))
address = nil
else -- cancelled authentication
return
end
end
end
if rundebug then ide:GetDebugger():SetOptions() end
local pkgcfg = pkg:GetConfig()
local redis = " --instance "..address
local controller = " --controller "..ide:GetDebugger():GetHostName()..":"..ide:GetDebugger():GetPortNumber()
local rundebug = " --debug " .. (rundebug and (pkgcfg.debugmode or "yes") or "no")
local pswd = password and (" --password %q"):format(password) or ""
local verbose = pkgcfg.verbose and " --verbose" or ""
local maxlen = pkgcfg.maxlen and (" --maxlen %s"):format(pkgcfg.maxlen) or ""
local cfg = ide:GetConfig()
local params = cfg.arg.any or cfg.arg.redis
local exe = ide:GetInterpreters().luadeb:GetExePath("")
local cmd = ('"%s" "%s"%s "%s"%s'):format(exe, pkg:GetFilePath(),
table.concat({redis, controller, rundebug, pswd, maxlen, verbose}, ""),
filepath, params and " "..params or "")
-- CommandLineRun(cmd,wdir,tooutput,nohide,stringcallback,uid,endcallback)
return CommandLineRun(cmd,self:fworkdir(wfilename),true,false)
end,
hasdebugger = true,
fattachdebug = function(self) ide:GetDebugger():SetOptions() end,
unhideanywindow = false,
takeparameters = true,
}
local name = "redis"
local package = {
name = "Redis",
description = "Integrates with Redis.",
author = "Paul Kulchenko",
version = 0.31,
dependencies = "1.30",
onRegister = function(self)
pkg = self
ide:AddInterpreter(name, interpreter)
ide:AddAPI("lua", name, api)
end,
onUnRegister = function(self)
ide:RemoveInterpreter(name)
ide:RemoveAPI("lua", name)
end,
}
if pcall(debug.getlocal, 4, 1) then return package end
-- main redis script -----------------------------------------------------------
io.stdout:setvbuf('no')
local unpack = unpack or table.unpack
local controller, instance = "localhost:8172", "redis://localhost:6379"
local verbose, maxlen, debugmode, password, params, file = false
while #arg > 0 do
local a = table.remove(arg, 1)
if a == "--instance" then
instance = table.remove(arg, 1)
elseif a == "--controller" then
controller = table.remove(arg, 1)
elseif a == "--debug" then
debugmode = table.remove(arg, 1)
elseif a == "--password" then
password = table.remove(arg, 1)
elseif a == "--maxlen" then
maxlen = table.remove(arg, 1)
elseif a == "--verbose" then
verbose = true
else
file, params, arg = a, arg, {}
end
end
local function check(cond, msg, ...) -- uses "file" name to inject into the error message
if cond or msg == nil then return cond, msg, ... end
print(type(msg) == 'string' and msg:gsub("(.+): @?user_script:%s*", "%1\n"..file..":") or msg)
os.exit(1)
end
check(file, "Required file name is not provided")
-- read the script
local fh, err = io.open(file, "rb")
check(fh, ("Can't open file '%s' for reading: %s"):format(file, err))
local code = fh:read("*a")
fh:close()
local function getval(response, keyword, format, keep)
if type(response) ~= 'table' then return end
local res = {}
for _, v in ipairs(response) do
if type(v) == "string" and v:find("^"..keyword) then
table.insert(res, (format or "%q"):format(keep and v or v:gsub(keyword.." ","")))
end
end
return res
end
local function getline(response)
local line = getval(response, "* Stopped at")
return line and line[1] and line[1]:match("(%d+)")
end
local function getretval(response)
return "return {"..table.concat(getval(response, "<retval>") or {}, ",").."}"
end
local function getreply(response)
local msg = table.concat(getval(response, "<reply>", "%s") or {}, ",")
-- add proper quoting to those messages that may be truncated because of `maxlen` limit
if msg:find('^"') and not msg:find('"$') then msg = msg..'"' end
return "return {"..(msg == "NULL" and "'nil'" or msg).."}"
end
local function geterror(response)
local err = getval(type(response) == 'table' and response or {response}, "<error>")
return err and #err > 0 and err[1] or nil
end
local function isdone(response)
local endsession = getval(response, "<endsession>")
return endsession and #endsession > 0
end
local function isfilesame(fullname, fname, basedir)
local sep = "/"
fullname = fullname:gsub("[\\/]", sep)
basedir = basedir:gsub("[\\/]", sep):gsub(sep.."+$", "")..sep
return fullname == fname or fullname == basedir .. fname
end
local function removebasedir(fullname, basedir)
local sep = "/"
fullname = fullname:gsub("[\\/]", sep)
basedir = basedir:gsub("[\\/]", sep):gsub(sep.."+$", "")..sep
return (fullname:gsub(basedir, ""))
end
local function getvars(response)
if type(response) ~= 'table' then return end
-- convert reported values to local definitions; skip internal variables;
-- best effort handling of maxlen trimmed values; skip hints
-- <value> (for index) = 1
-- <value> (for limit) = 20
-- <value> i = 1
-- <value> s = "foobar"
-- <value> t = {}
-- <value> trimmed_string = "abc ...
-- <value> trimmed_table = { "abc"; "ef ...
-- <hint> some text
local res = {}
for _, v in ipairs(response) do
if not v:find("<value> %(") and not v:find("<hint>") then
local val = v:gsub("<value> ", "")
if val:sub(-3) == "..." then -- trimmed string or table
local msg = "[[contents trimmed, consider using `maxlen 0`]]"
local var, valtype = val:match("^([%p%w]+)%s+=%s+(.)")
if valtype == "\"" then
val = val .. " " .. msg .. "\""
elseif valtype == "{" then
val = var .. " = {\"" .. msg .. "\"}"
end
end
if loadstring("local "..val) then table.insert(res, val) end
end
end
return res
end
local function getvarsaslocals(response)
local vars = getvars(response)
if not vars or #vars == 0 then return "" end
return "local "..table.concat(vars, "; local ")..";"
end
local function getvarsastable(response)
local vars = getvars(response)
for k, v in pairs(vars) do
local name, val = v:match("^([%p%w]+)%s+=%s+(.+)")
vars[k] = ("%s={%s,%s}"):format(name, val, val:find("^{") and "'table'" or "nil")
end
return "{"..table.concat(vars, "; ").."}"
end
local function reportdebug(response)
local deb = getval(response, "<debug>", "%s", true)
if deb and #deb > 0 then print(table.concat(deb, "\n")) end
local hint = getval(response, "<hint>", "%s", true)
if hint and #hint > 0 then print(table.concat(hint, "\n")) end
end
local function reportresult(client, msg)
local function report(msg)
if type(msg) ~= 'table' then return print(msg) end
for _, v in ipairs(msg) do report(v) end
end
report(msg or check(client:echo('nil'))) -- this will also report any errors
client:quit()
end
-- get redis instance host:port
local uri = require("socket.url").parse(instance)
local host, port, db = uri.host, uri.port, uri.path
check(host and port, ("Unknown Redis URI format '%s'; expected [redis://][username:password@]hostname:port[/db]"):format(instance))
-- register Redis debugger commands
for key, command in pairs({continue = 'C', step = 'S', breakpoint = 'B', maxlen = 'M',
abort = 'A', print = 'P', eval = 'E', trace = 'T', redis = 'R'}) do
redis.commands['ldb'..key] = redis.command(command)
end
-- connect to redis instance
local ok, err = pcall(redis.connect, {host = host, port = port, timeout = nil})
check(ok, ("Can't connect to Redis instance '%s': %s.")
:format(instance, type(err) == "string" and err:match("%[(.+)%]" or "Unknown error")))
local client = err
client.error = function(error) return nil, (error:gsub(".*ERR ","")) end
if verbose then
local function formatmsg(msg)
return (("%q"):format(msg)
:gsub("\010","n"):gsub("\\0?13","\\r"):gsub("\026","\\026"):gsub('^"',""):gsub('"$',""))
end
client.onread = function(self, line, err) print("<redis rcvd> "..(line and formatmsg(line) or "error: "..err)) end
client.onwrite = function(self, line, err) print("<redis sent> "..(line and formatmsg(line) or "error: "..err)) end
end
-- authenticate if password is provided
local msg, err = client:ping()
if not msg and err:find("NOAUTH") then
check(password, "Authentication is required, but no password is provided to authenticate.")
check(client:auth(password), "Can't autheticate with the provided password.")
end
-- select database, if any
if db then
msg, err = check(client:select(db:sub(2)))
end
local isdebugging = debugmode ~= "no"
msg, err = check(client:info("SERVER"))
local version = msg and tonumber(((msg.server or {}).redis_version or ""):match("%d+.%d+"))
local needversion = 3.2
if isdebugging and version and version < needversion then
print(("Detected Redis server v%s, but need v%s+ for the debugging to work.")
:format(version, needversion))
end
-- start debugging
if isdebugging then
msg, err = check(client:script("DEBUG", debugmode))
end
-- split passed parameters into KEYS and ARGV (separated by ',')
local keys = #params
for n, v in ipairs(params) do
if v == ',' then
keys = n - 1
table.remove(params, n)
break
end
end
-- load the script to debug; check for any reported errors
msg, err = check(client:eval(code, keys, unpack(params)))
-- if no debugging is requested, nothing else is needed to be done
if not isdebugging then
reportresult(client, msg)
os.exit(0)
end
-- set maxlen if provided
if maxlen then msg, err = check(client:ldbmaxlen(maxlen)) end
-- connect to the debugger
local server, err = socket.tcp()
check(server, ("Can't open socket: %s"):format(err))
host, port = controller:match("^%s*(.+):(%d+)%s*$")
check(host and port, ("Unknown debugger address format '%s'; expected host:port."):format(controller))
if server.settimeout then server:settimeout(2) end
local ok, err = server:connect(host, port)
if server.settimeout then server:settimeout() end
check(ok, ("Can't connect to the debugger at '%s': %s"):format(controller, err))
-- main debugger loop
local basedir = ""
while true do
local line = check(server:receive())
local command = string.sub(line, string.find(line, "^[A-Z]+"))
if command == "SETB" or command == "DELB" then
local _, _, _, lfile, line = string.find(line, "^([A-Z]+)%s+(.-)%s+(%d+)%s*$")
if lfile and line then
line = line == "0" and 0 or (command == "SETB" and tonumber(line) or -tonumber(line))
-- deleting from file '*' deletes all breakpoints
if lfile == '*' or isfilesame(file, lfile, basedir) then client:ldbbreakpoint(line) end
server:send("200 OK\n")
else
server:send("400 Bad Request\n")
end
elseif command == "BASEDIR" then
local _, _, dir = string.find(line, "^[A-Z]+%s+(.+)%s*$")
if dir then
basedir = dir
server:send("200 OK\n")
else
server:send("400 Bad Request\n")
end
elseif command == "LOAD" then
local _, _, size = string.find(line, "^[A-Z]+%s+(%d+)%s+(%S.-)%s*$")
size = tonumber(size)
if size > 0 then server:receive(size) end
server:send("201 Started " .. file .. " " .. (getline(msg) or 1) .. "\n")
elseif command == "RUN" or command == "STEP" or command == "OVER" or command == "OUT" then
server:send("200 OK\n")
local msg, err
if command == "RUN" then
msg, err = check(client:ldbcontinue())
else
msg, err = check(client:ldbstep())
-- check if this is the last step stopped at "out of range" position; do one more step
local outofrange = msg and getval(msg, "->%s+%d+%s+<out of range")
if outofrange and #outofrange > 0 then
reportdebug(msg)
msg, err = check(client:ldbstep())
end
end
reportdebug(msg)
if isdone(msg) then
reportresult(client)
break
end
server:send("202 Paused " .. file .. " " .. (getline(msg) or 0) .. "\n")
elseif command == "DONE" then
check(client:ldbbreakpoint(0)) -- remove all breakpoints
local msg = check(client:ldbcontinue()) -- continue with the script
reportdebug(msg)
if isdone(msg) then reportresult(client) end
break
elseif command == "EXEC" then
local _, _, chunk = string.find(line, "^[A-Z]+%s+(.+)$")
if chunk then
local func, err = loadstring(chunk)
if not func then
local chunkr = "return "..chunk
func = loadstring(chunkr)
if func then chunk = chunkr end
end
if func then
-- evaluation is done in a different environment, so capture local variables
-- to use in the chunk evaluation to make their values available
local vars = getvarsaslocals(check(client:ldbprint()))
local msg, err = client:ldbeval(vars..chunk)
-- if there is an error, try without preamble, as it has a better error message
if msg and geterror(msg) then msg, err = client:ldbeval(chunk) end
if msg and geterror(msg) then msg, err = nil, geterror(msg) end
if msg then
reportdebug(msg)
-- if the chunk starts from "return" or looks like an expression, then return the result
-- otherwise don't return any values to avoid showing `nil` after statements.
msg = (chunk:find("^return ") or loadstring("return "..chunk)) and getretval(msg) or "return {}"
server:send("200 OK " .. tostring(#msg) .. "\n")
server:send(msg)
else
server:send("401 Error in Expression " .. tostring(#err) .. "\n")
server:send(err)
end
elseif chunk:find("^[A-Z][A-Z]+%s") or chunk:find("^@") then
chunk = chunk:gsub("^@", "")
local cmd = {}
for v in chunk:gmatch("(%S+)") do table.insert(cmd, v) end
local msg, err = client:ldbredis(unpack(cmd))
if msg and geterror(msg) then msg, err = nil, geterror(msg) end
if msg then
reportdebug(msg)
msg = getreply(msg)
server:send("200 OK " .. tostring(#msg) .. "\n")
server:send(msg)
else
server:send("401 Error in Expression " .. tostring(#err) .. "\n")
server:send(err)
end
else
server:send("401 Error in Expression " .. tostring(#err) .. "\n")
server:send(err)
end
else
server:send("400 Bad Request\n")
end
elseif command == "STACK" then
-- get stack information and repackage it in the expected format
local msg = check(client:ldbtrace())
local fname = removebasedir(file, basedir)
local stack = {}
local vars = getvarsastable(check(client:ldbprint()))
while #msg > 0 do
local frame = table.remove(msg, 1)
local line = table.remove(msg, 1)
local top = frame:match("^In (.-):")
local funcname = frame:match("^In (.-):") or frame:match("^From (.-):")
if funcname == "top level" then
funcname = #msg > 0 and "anonymous function" or "main chunk"
end
local _, _, curline = line:find("(%d+)", 4)
table.insert(stack, ("{{%q, %q, -1, %d, 'main chunk'}, %s, {}}")
:format(funcname, fname, curline or 0, top and vars or "{}"))
end
server:send("200 OK " .. "return {"..table.concat(stack, ',').."}" .. "\n")
elseif command == "EXIT" then
client:ldbabort()
server:send("200 OK\n")
break
elseif command == "SETW" or command == "DELW" or command == "SUSPEND" or command == "OUTPUT" then
server:send("200 OK\n")
else
server:send("400 Bad Request\n")
end
end
--[[ configuration example:
redis = {debugmode = "sync"} -- set debug mode to "sync" (the default debug mode is "yes")
redis = {verbose = true} -- set verbose output to show all commands sent to or receivd from Redis
redis = {maxlen = 10000} -- set `maxlen` value during debugging
-- several settings can be combined:
redis = {maxlen = 10000, verbose = true}
--]]