# HG changeset patch # User Matthew Wild # Date 1545562838 0 # Node ID 66bda434d4762a3faadc76f2c169c8b30a97bb5b # Parent 6421c9f05e02deae96cc2e258fdbc75c52d17821 mod_statistics: Update 'top' to (hopefully) work with Prosody 0.11/Lua 5.2 diff -r 6421c9f05e02 -r 66bda434d476 mod_statistics/mod_statistics.lua --- a/mod_statistics/mod_statistics.lua Sat Dec 22 19:02:21 2018 +0100 +++ b/mod_statistics/mod_statistics.lua Sun Dec 23 11:00:38 2018 +0000 @@ -131,7 +131,7 @@ if command == "top" then local dir = module:get_directory(); package.path = dir.."/?.lua;"..dir.."/?.lib.lua;"..package.path; - local prosodytop = require "prosodytop"; + local prosodytop = module:require "prosodytop"; prosodytop.run(); end end diff -r 6421c9f05e02 -r 66bda434d476 mod_statistics/prosodytop.lib.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_statistics/prosodytop.lib.lua Sun Dec 23 11:00:38 2018 +0000 @@ -0,0 +1,138 @@ +local curses = require "curses"; +local server = require "net.server_select"; +local timer = require "util.timer"; + +assert(curses.timeout, "Incorrect version of curses library. Try 'sudo luarocks install luaposix'"); + +local top = module:require "top"; + +function main() + local stdscr = curses.stdscr() -- it's a userdatum + --stdscr:clear(); + local view = top.new({ + stdscr = stdscr; + prosody = { up_since = os.time() }; + conn_list = {}; + }); + + timer.add_task(0.01, function () + local ch = stdscr:getch(); + if ch then + if stdscr:getch() == 410 then + view:resized(); + else + curses.ungetch(ch); + end + end + return 0.2; + end); + + timer.add_task(0, function () + view:draw(); + return 1; + end); + + --[[ + posix.signal(28, function () + table.insert(view.conn_list, { jid = "WINCH" }); + --view:draw(); + end); + ]] + + -- Fake socket object around stdin + local stdin = { + getfd = function () return 0; end; + dirty = function (self) return false; end; + settimeout = function () end; + send = function (_, d) return #d, 0; end; + close = function () end; + receive = function (_, patt) + local ch = stdscr:getch(); + if ch >= 0 and ch <=255 then + return string.char(ch); + elseif ch == 410 then + view:resized(); + else + table.insert(view.conn_list, { jid = tostring(ch) }); --FIXME + end + return ""; + end + }; + local function on_incoming(stdin, text) + -- TODO: Handle keypresses + if text:lower() == "q" then os.exit(); end + end + stdin = server.wrapclient(stdin, "stdin", 0, { + onincoming = on_incoming, ondisconnect = function () end + }, "*a"); + + local function handle_line(line) + local e = { + STAT = function (name) return function (value) + view:update_stat(name, value); + end end; + SESS = function (id) return function (jid) return function (stats) + view:update_session(id, jid, stats); + end end end; + }; + local chunk = assert(loadstring(line)); + setfenv(chunk, e); + chunk(); + end + + local stats_listener = {}; + + function stats_listener.onconnect(conn) + --stdscr:mvaddstr(6, 0, "CONNECTED"); + end + + local partial = ""; + function stats_listener.onincoming(conn, data) + --print("DATA", data) + data = partial..data; + local lastpos = 1; + for line, pos in data:gmatch("([^\n]+)\n()") do + lastpos = pos; + handle_line(line); + end + partial = data:sub(lastpos); + end + + function stats_listener.ondisconnect(conn, err) + stdscr:mvaddstr(6, 0, "DISCONNECTED: "..(err or "unknown")); + end + + local conn = require "socket".tcp(); + assert(conn:connect("localhost", 5782)); + handler = server.wrapclient(conn, "localhost", 5782, stats_listener, "*a"); +end + +return { + run = function () + --os.setlocale("UTF-8", "all") + + curses.initscr() + curses.cbreak() + curses.echo(false) -- not noecho ! + curses.nl(false) -- not nonl ! + curses.timeout(0); + + local ok, err = pcall(main); + + --while true do stdscr:getch() end + --stdscr:endwin() + + if ok then + ok, err = xpcall(server.loop, debug.traceback); + end + + curses.endwin(); + + --stdscr:refresh(); + if not ok then + print(err); + end + + print"DONE" + end; +}; diff -r 6421c9f05e02 -r 66bda434d476 mod_statistics/prosodytop.lua --- a/mod_statistics/prosodytop.lua Sat Dec 22 19:02:21 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,138 +0,0 @@ -local curses = require "curses"; -local server = require "net.server_select"; -local timer = require "util.timer"; - -assert(curses.timeout, "Incorrect version of curses library. Try 'sudo luarocks install luaposix'"); - -local top = require "top"; - -function main() - local stdscr = curses.stdscr() -- it's a userdatum - --stdscr:clear(); - local view = top.new({ - stdscr = stdscr; - prosody = { up_since = os.time() }; - conn_list = {}; - }); - - timer.add_task(0.01, function () - local ch = stdscr:getch(); - if ch then - if stdscr:getch() == 410 then - view:resized(); - else - curses.ungetch(ch); - end - end - return 0.2; - end); - - timer.add_task(0, function () - view:draw(); - return 1; - end); - - --[[ - posix.signal(28, function () - table.insert(view.conn_list, { jid = "WINCH" }); - --view:draw(); - end); - ]] - - -- Fake socket object around stdin - local stdin = { - getfd = function () return 0; end; - dirty = function (self) return false; end; - settimeout = function () end; - send = function (_, d) return #d, 0; end; - close = function () end; - receive = function (_, patt) - local ch = stdscr:getch(); - if ch >= 0 and ch <=255 then - return string.char(ch); - elseif ch == 410 then - view:resized(); - else - table.insert(view.conn_list, { jid = tostring(ch) }); --FIXME - end - return ""; - end - }; - local function on_incoming(stdin, text) - -- TODO: Handle keypresses - if text:lower() == "q" then os.exit(); end - end - stdin = server.wrapclient(stdin, "stdin", 0, { - onincoming = on_incoming, ondisconnect = function () end - }, "*a"); - - local function handle_line(line) - local e = { - STAT = function (name) return function (value) - view:update_stat(name, value); - end end; - SESS = function (id) return function (jid) return function (stats) - view:update_session(id, jid, stats); - end end end; - }; - local chunk = assert(loadstring(line)); - setfenv(chunk, e); - chunk(); - end - - local stats_listener = {}; - - function stats_listener.onconnect(conn) - --stdscr:mvaddstr(6, 0, "CONNECTED"); - end - - local partial = ""; - function stats_listener.onincoming(conn, data) - --print("DATA", data) - data = partial..data; - local lastpos = 1; - for line, pos in data:gmatch("([^\n]+)\n()") do - lastpos = pos; - handle_line(line); - end - partial = data:sub(lastpos); - end - - function stats_listener.ondisconnect(conn, err) - stdscr:mvaddstr(6, 0, "DISCONNECTED: "..(err or "unknown")); - end - - local conn = require "socket".tcp(); - assert(conn:connect("localhost", 5782)); - handler = server.wrapclient(conn, "localhost", 5782, stats_listener, "*a"); -end - -return { - run = function () - --os.setlocale("UTF-8", "all") - - curses.initscr() - curses.cbreak() - curses.echo(false) -- not noecho ! - curses.nl(false) -- not nonl ! - curses.timeout(0); - - local ok, err = pcall(main); - - --while true do stdscr:getch() end - --stdscr:endwin() - - if ok then - ok, err = xpcall(server.loop, debug.traceback); - end - - curses.endwin(); - - --stdscr:refresh(); - if not ok then - print(err); - end - - print"DONE" - end; -}; diff -r 6421c9f05e02 -r 66bda434d476 mod_statistics/top.lib.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_statistics/top.lib.lua Sun Dec 23 11:00:38 2018 +0000 @@ -0,0 +1,215 @@ +local array = require "util.array"; +local it = require "util.iterators"; +local curses = require "curses"; +local stats = module:require "stats".stats; +local time = require "socket".gettime; + +local sessions_idle_after = 60; +local stanza_names = {"message", "presence", "iq"}; + +local top = {}; +top.__index = top; + +local status_lines = { + "Prosody $version - $time up $up_since, $total_users users, $cpu busy"; + "Connections: $total_c2s c2s, $total_s2sout s2sout, $total_s2sin s2sin, $total_component component"; + "Memory: $memory_lua lua, $memory_allocated process ($memory_used in use)"; + "Stanzas in: $message_in_per_second message/s, $presence_in_per_second presence/s, $iq_in_per_second iq/s"; + "Stanzas out: $message_out_per_second message/s, $presence_out_per_second presence/s, $iq_out_per_second iq/s"; +}; + +function top:draw() + self:draw_status(); + self:draw_column_titles(); + self:draw_conn_list(); + self.statuswin:refresh(); + self.listwin:refresh(); + --self.infowin:refresh() + self.stdscr:move(#status_lines,0) +end + +-- Width specified as cols or % of unused space, defaults to +-- title width if not specified +local conn_list_columns = { + { title = "ID", key = "id", width = "8" }; + { title = "JID", key = "jid", width = "100%" }; + { title = "STANZAS IN>", key = "total_stanzas_in", align = "right" }; + { title = "MSG", key = "message_in", align = "right", width = "4" }; + { title = "PRES", key = "presence_in", align = "right", width = "4" }; + { title = "IQ", key = "iq_in", align = "right", width = "4" }; + { title = "STANZAS OUT>", key = "total_stanzas_out", align = "right" }; + { title = "MSG", key = "message_out", align = "right", width = "4" }; + { title = "PRES", key = "presence_out", align = "right", width = "4" }; + { title = "IQ", key = "iq_out", align = "right", width = "4" }; + { title = "BYTES IN", key = "bytes_in", align = "right" }; + { title = "BYTES OUT", key = "bytes_out", align = "right" }; + +}; + +function top:draw_status() + for row, line in ipairs(status_lines) do + self.statuswin:mvaddstr(row-1, 0, (line:gsub("%$([%w_]+)", self.data))); + self.statuswin:clrtoeol(); + end + -- Clear stanza counts + for _, stanza_type in ipairs(stanza_names) do + self.prosody[stanza_type.."_in_per_second"] = 0; + self.prosody[stanza_type.."_out_per_second"] = 0; + end +end + +local function padright(s, width) + return s..string.rep(" ", width-#s); +end + +local function padleft(s, width) + return string.rep(" ", width-#s)..s; +end + +function top:resized() + self:recalc_column_widths(); + --self.stdscr:clear(); + self:draw(); +end + +function top:recalc_column_widths() + local widths = {}; + self.column_widths = widths; + local total_width = curses.cols()-4; + local free_width = total_width; + for i = 1, #conn_list_columns do + local width = conn_list_columns[i].width or "0"; + if not(type(width) == "string" and width:sub(-1) == "%") then + width = math.max(tonumber(width), #conn_list_columns[i].title+1); + widths[i] = width; + free_width = free_width - width; + end + end + for i = 1, #conn_list_columns do + if not widths[i] then + local pc_width = tonumber((conn_list_columns[i].width:gsub("%%$", ""))); + widths[i] = math.floor(free_width*(pc_width/100)); + end + end + return widths; +end + +function top:draw_column_titles() + local widths = self.column_widths; + self.listwin:attron(curses.A_REVERSE); + self.listwin:mvaddstr(0, 0, " "); + for i, column in ipairs(conn_list_columns) do + self.listwin:addstr(padright(column.title, widths[i])); + end + self.listwin:addstr(" "); + self.listwin:attroff(curses.A_REVERSE); +end + +local function session_compare(session1, session2) + local stats1, stats2 = session1.stats, session2.stats; + return (stats1.total_stanzas_in + stats1.total_stanzas_out) > + (stats2.total_stanzas_in + stats2.total_stanzas_out); +end + +function top:draw_conn_list() + local rows = curses.lines()-(#status_lines+2)-5; + local cutoff_time = time() - sessions_idle_after; + local widths = self.column_widths; + local top_sessions = array.collect(it.values(self.active_sessions)):sort(session_compare); + for index = 1, rows do + local session = top_sessions[index]; + if session then + if session.last_update < cutoff_time then + self.active_sessions[session.id] = nil; + else + local row = {}; + for i, column in ipairs(conn_list_columns) do + local width = widths[i]; + local v = tostring(session[column.key] or ""):sub(1, width); + if #v < width then + if column.align == "right" then + v = padleft(v, width-1).." "; + else + v = padright(v, width); + end + end + table.insert(row, v); + end + if session.updated then + self.listwin:attron(curses.A_BOLD); + end + self.listwin:mvaddstr(index, 0, " "..table.concat(row)); + if session.updated then + session.updated = false; + self.listwin:attroff(curses.A_BOLD); + end + end + else + -- FIXME: How to clear a line? It's 5am and I don't feel like reading docs. + self.listwin:move(index, 0); + self.listwin:clrtoeol(); + end + end +end + +function top:update_stat(name, value) + self.prosody[name] = value; +end + +function top:update_session(id, jid, stats) + self.active_sessions[id] = stats; + stats.id, stats.jid, stats.stats = id, jid, stats; + stats.total_bytes = stats.bytes_in + stats.bytes_out; + for _, stanza_type in ipairs(stanza_names) do + self.prosody[stanza_type.."_in_per_second"] = (self.prosody[stanza_type.."_in_per_second"] or 0) + stats[stanza_type.."_in"]; + self.prosody[stanza_type.."_out_per_second"] = (self.prosody[stanza_type.."_out_per_second"] or 0) + stats[stanza_type.."_out"]; + end + stats.total_stanzas_in = stats.message_in + stats.presence_in + stats.iq_in; + stats.total_stanzas_out = stats.message_out + stats.presence_out + stats.iq_out; + stats.last_update = time(); + stats.updated = true; +end + +local function new(base) + setmetatable(base, top); + base.data = setmetatable({}, { + __index = function (t, k) + local stat = stats[k]; + if stat and stat.tostring then + if type(stat.tostring) == "function" then + return stat.tostring(base.prosody[k]); + elseif type(stat.tostring) == "string" then + local v = base.prosody[k]; + if v == nil then + return "?"; + end + return (stat.tostring):format(v); + end + end + return base.prosody[k]; + end; + }); + + base.active_sessions = {}; + + base.statuswin = curses.newwin(#status_lines, 0, 0, 0); + + base.promptwin = curses.newwin(1, 0, #status_lines, 0); + base.promptwin:addstr(""); + base.promptwin:refresh(); + + base.listwin = curses.newwin(curses.lines()-(#status_lines+2)-5, 0, #status_lines+1, 0); + base.listwin:syncok(); + + base.infowin = curses.newwin(5, 0, curses.lines()-5, 0); + base.infowin:mvaddstr(1, 1, "Hello world"); + base.infowin:border(0,0,0,0); + base.infowin:syncok(); + base.infowin:refresh(); + + base:resized(); + + return base; +end + +return { new = new }; diff -r 6421c9f05e02 -r 66bda434d476 mod_statistics/top.lua --- a/mod_statistics/top.lua Sat Dec 22 19:02:21 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,217 +0,0 @@ -module("prosodytop", package.seeall); - -local array = require "util.array"; -local it = require "util.iterators"; -local curses = require "curses"; -local stats = require "stats".stats; -local time = require "socket".gettime; - -local sessions_idle_after = 60; -local stanza_names = {"message", "presence", "iq"}; - -local top = {}; -top.__index = top; - -local status_lines = { - "Prosody $version - $time up $up_since, $total_users users, $cpu busy"; - "Connections: $total_c2s c2s, $total_s2sout s2sout, $total_s2sin s2sin, $total_component component"; - "Memory: $memory_lua lua, $memory_allocated process ($memory_used in use)"; - "Stanzas in: $message_in_per_second message/s, $presence_in_per_second presence/s, $iq_in_per_second iq/s"; - "Stanzas out: $message_out_per_second message/s, $presence_out_per_second presence/s, $iq_out_per_second iq/s"; -}; - -function top:draw() - self:draw_status(); - self:draw_column_titles(); - self:draw_conn_list(); - self.statuswin:refresh(); - self.listwin:refresh(); - --self.infowin:refresh() - self.stdscr:move(#status_lines,0) -end - --- Width specified as cols or % of unused space, defaults to --- title width if not specified -local conn_list_columns = { - { title = "ID", key = "id", width = "8" }; - { title = "JID", key = "jid", width = "100%" }; - { title = "STANZAS IN>", key = "total_stanzas_in", align = "right" }; - { title = "MSG", key = "message_in", align = "right", width = "4" }; - { title = "PRES", key = "presence_in", align = "right", width = "4" }; - { title = "IQ", key = "iq_in", align = "right", width = "4" }; - { title = "STANZAS OUT>", key = "total_stanzas_out", align = "right" }; - { title = "MSG", key = "message_out", align = "right", width = "4" }; - { title = "PRES", key = "presence_out", align = "right", width = "4" }; - { title = "IQ", key = "iq_out", align = "right", width = "4" }; - { title = "BYTES IN", key = "bytes_in", align = "right" }; - { title = "BYTES OUT", key = "bytes_out", align = "right" }; - -}; - -function top:draw_status() - for row, line in ipairs(status_lines) do - self.statuswin:mvaddstr(row-1, 0, (line:gsub("%$([%w_]+)", self.data))); - self.statuswin:clrtoeol(); - end - -- Clear stanza counts - for _, stanza_type in ipairs(stanza_names) do - self.prosody[stanza_type.."_in_per_second"] = 0; - self.prosody[stanza_type.."_out_per_second"] = 0; - end -end - -local function padright(s, width) - return s..string.rep(" ", width-#s); -end - -local function padleft(s, width) - return string.rep(" ", width-#s)..s; -end - -function top:resized() - self:recalc_column_widths(); - --self.stdscr:clear(); - self:draw(); -end - -function top:recalc_column_widths() - local widths = {}; - self.column_widths = widths; - local total_width = curses.cols()-4; - local free_width = total_width; - for i = 1, #conn_list_columns do - local width = conn_list_columns[i].width or "0"; - if not(type(width) == "string" and width:sub(-1) == "%") then - width = math.max(tonumber(width), #conn_list_columns[i].title+1); - widths[i] = width; - free_width = free_width - width; - end - end - for i = 1, #conn_list_columns do - if not widths[i] then - local pc_width = tonumber((conn_list_columns[i].width:gsub("%%$", ""))); - widths[i] = math.floor(free_width*(pc_width/100)); - end - end - return widths; -end - -function top:draw_column_titles() - local widths = self.column_widths; - self.listwin:attron(curses.A_REVERSE); - self.listwin:mvaddstr(0, 0, " "); - for i, column in ipairs(conn_list_columns) do - self.listwin:addstr(padright(column.title, widths[i])); - end - self.listwin:addstr(" "); - self.listwin:attroff(curses.A_REVERSE); -end - -local function session_compare(session1, session2) - local stats1, stats2 = session1.stats, session2.stats; - return (stats1.total_stanzas_in + stats1.total_stanzas_out) > - (stats2.total_stanzas_in + stats2.total_stanzas_out); -end - -function top:draw_conn_list() - local rows = curses.lines()-(#status_lines+2)-5; - local cutoff_time = time() - sessions_idle_after; - local widths = self.column_widths; - local top_sessions = array.collect(it.values(self.active_sessions)):sort(session_compare); - for index = 1, rows do - session = top_sessions[index]; - if session then - if session.last_update < cutoff_time then - self.active_sessions[session.id] = nil; - else - local row = {}; - for i, column in ipairs(conn_list_columns) do - local width = widths[i]; - local v = tostring(session[column.key] or ""):sub(1, width); - if #v < width then - if column.align == "right" then - v = padleft(v, width-1).." "; - else - v = padright(v, width); - end - end - table.insert(row, v); - end - if session.updated then - self.listwin:attron(curses.A_BOLD); - end - self.listwin:mvaddstr(index, 0, " "..table.concat(row)); - if session.updated then - session.updated = false; - self.listwin:attroff(curses.A_BOLD); - end - end - else - -- FIXME: How to clear a line? It's 5am and I don't feel like reading docs. - self.listwin:move(index, 0); - self.listwin:clrtoeol(); - end - end -end - -function top:update_stat(name, value) - self.prosody[name] = value; -end - -function top:update_session(id, jid, stats) - self.active_sessions[id] = stats; - stats.id, stats.jid, stats.stats = id, jid, stats; - stats.total_bytes = stats.bytes_in + stats.bytes_out; - for _, stanza_type in ipairs(stanza_names) do - self.prosody[stanza_type.."_in_per_second"] = (self.prosody[stanza_type.."_in_per_second"] or 0) + stats[stanza_type.."_in"]; - self.prosody[stanza_type.."_out_per_second"] = (self.prosody[stanza_type.."_out_per_second"] or 0) + stats[stanza_type.."_out"]; - end - stats.total_stanzas_in = stats.message_in + stats.presence_in + stats.iq_in; - stats.total_stanzas_out = stats.message_out + stats.presence_out + stats.iq_out; - stats.last_update = time(); - stats.updated = true; -end - -function new(base) - setmetatable(base, top); - base.data = setmetatable({}, { - __index = function (t, k) - local stat = stats[k]; - if stat and stat.tostring then - if type(stat.tostring) == "function" then - return stat.tostring(base.prosody[k]); - elseif type(stat.tostring) == "string" then - local v = base.prosody[k]; - if v == nil then - return "?"; - end - return (stat.tostring):format(v); - end - end - return base.prosody[k]; - end; - }); - - base.active_sessions = {}; - - base.statuswin = curses.newwin(#status_lines, 0, 0, 0); - - base.promptwin = curses.newwin(1, 0, #status_lines, 0); - base.promptwin:addstr(""); - base.promptwin:refresh(); - - base.listwin = curses.newwin(curses.lines()-(#status_lines+2)-5, 0, #status_lines+1, 0); - base.listwin:syncok(); - - base.infowin = curses.newwin(5, 0, curses.lines()-5, 0); - base.infowin:mvaddstr(1, 1, "Hello world"); - base.infowin:border(0,0,0,0); - base.infowin:syncok(); - base.infowin:refresh(); - - base:resized(); - - return base; -end - -return _M;