| --[[ |
| version = 1 |
| /* |
| * Copyright (c) 2016 Cisco and/or its affiliates. |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at: |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| ]] |
| |
| -- LUTE: Lua Unit Test Environment |
| -- AKA what happens when screen tries to marry with lua and expect, |
| -- but escapes mid-ceremony. |
| -- |
| -- comments: @ayourtch |
| |
| ffi = require("ffi") |
| |
| vpp = {} |
| function vpp.dump(o) |
| if type(o) == 'table' then |
| local s = '{ ' |
| for k,v in pairs(o) do |
| if type(k) ~= 'number' then k = '"'..k..'"' end |
| s = s .. '['..k..'] = ' .. vpp.dump(v) .. ',' |
| end |
| return s .. '} ' |
| else |
| return tostring(o) |
| end |
| end |
| |
| |
| ffi.cdef([[ |
| |
| int posix_openpt(int flags); |
| int grantpt(int fd); |
| int unlockpt(int fd); |
| char *ptsname(int fd); |
| |
| typedef long pid_t; |
| typedef long ssize_t; |
| typedef long size_t; |
| typedef int nfds_t; |
| typedef long time_t; |
| typedef long suseconds_t; |
| |
| pid_t fork(void); |
| pid_t setsid(void); |
| |
| int close(int fd); |
| int open(char *pathname, int flags); |
| |
| int dup2(int oldfd, int newfd); |
| |
| ssize_t read(int fd, void *buf, size_t count); |
| ssize_t write(int fd, const void *buf, size_t count); |
| |
| struct pollfd { |
| int fd; /* file descriptor */ |
| short events; /* requested events */ |
| short revents; /* returned events */ |
| }; |
| |
| int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
| |
| struct timeval { |
| time_t tv_sec; /* seconds */ |
| suseconds_t tv_usec; /* microseconds */ |
| }; |
| |
| int gettimeofday(struct timeval *tv, struct timezone *tz); |
| |
| int inet_pton(int af, const char *src, void *dst); |
| |
| ]]) |
| |
| ffi.cdef([[ |
| void *memset(void *s, int c, size_t n); |
| void *memcpy(void *dest, void *src, size_t n); |
| void *memmove(void *dest, const void *src, size_t n); |
| void *memmem(const void *haystack, size_t haystacklen, |
| const void *needle, size_t needlelen); |
| ]]) |
| |
| |
| |
| local O_RDWR = 2 |
| |
| |
| function os_time() |
| local tv = ffi.new("struct timeval[1]") |
| local ret = ffi.C.gettimeofday(tv, nil) |
| return tonumber(tv[0].tv_sec) + (tonumber(tv[0].tv_usec)/1000000.0) |
| end |
| |
| function sleep(n) |
| local when_wakeup = os_time() + n |
| while os_time() <= when_wakeup do |
| ffi.C.poll(nil, 0, 10) |
| end |
| end |
| |
| |
| function c_str(text_in) |
| local text = text_in |
| local c_str = ffi.new("char[?]", #text+1) |
| ffi.copy(c_str, text) |
| return c_str |
| end |
| |
| function ip46(addr_text) |
| local out = ffi.new("char [200]") |
| local AF_INET6 = 10 |
| local AF_INET = 2 |
| local is_ip6 = ffi.C.inet_pton(AF_INET6, c_str(addr_text), out) |
| if is_ip6 == 1 then |
| return ffi.string(out, 16), true |
| end |
| local is_ip4 = ffi.C.inet_pton(AF_INET, c_str(addr_text), out) |
| if is_ip4 then |
| return (string.rep("4", 12).. ffi.string(out, 4)), false |
| end |
| end |
| |
| function pty_master_open() |
| local fd = ffi.C.posix_openpt(O_RDWR) |
| ffi.C.grantpt(fd) |
| ffi.C.unlockpt(fd) |
| local p = ffi.C.ptsname(fd) |
| print("PTS:" .. ffi.string(p)) |
| return fd, ffi.string(p) |
| end |
| |
| function pty_run(cmd) |
| local master_fd, pts_name = pty_master_open() |
| local child_pid = ffi.C.fork() |
| if (child_pid == -1) then |
| print("Error fork()ing") |
| return -1 |
| end |
| |
| if child_pid ~= 0 then |
| -- print("Parent") |
| return master_fd, child_pid |
| end |
| |
| -- print("Child") |
| if (ffi.C.setsid() == -1) then |
| print("Child error setsid") |
| os.exit(-1) |
| end |
| |
| ffi.C.close(master_fd) |
| |
| local slave_fd = ffi.C.open(c_str(pts_name), O_RDWR) |
| if slave_fd == -1 then |
| print("Child can not open slave fd") |
| os.exit(-2) |
| end |
| |
| ffi.C.dup2(slave_fd, 0) |
| ffi.C.dup2(slave_fd, 1) |
| ffi.C.dup2(slave_fd, 2) |
| os.execute(cmd) |
| end |
| |
| function readch() |
| local buf = ffi.new("char[1]") |
| local nread= ffi.C.read(0, buf, 1) |
| -- print("\nREADCH : " .. string.char(buf[0])) |
| return string.char(buf[0]) |
| end |
| |
| function stdout_write(str) |
| ffi.C.write(1, c_str(str), #str) |
| end |
| |
| |
| readln = { |
| split = function(str, pat) |
| local t = {} -- NOTE: use {n = 0} in Lua-5.0 |
| local fpat = "(.-)" .. pat |
| local last_end = 1 |
| if str then |
| local s, e, cap = str:find(fpat, 1) |
| while s do |
| if s ~= 1 or cap ~= "" then |
| table.insert(t,cap) |
| end |
| last_end = e+1 |
| s, e, cap = str:find(fpat, last_end) |
| end |
| if last_end <= #str then |
| cap = str:sub(last_end) |
| table.insert(t, cap) |
| end |
| end |
| return t |
| end, |
| |
| reader = function() |
| local rl = {} |
| |
| rl.init = function() |
| os.execute("stty -icanon min 1 -echo") |
| rl.rawmode = true |
| end |
| |
| rl.done = function() |
| os.execute("stty icanon echo") |
| rl.rawmode = false |
| end |
| |
| rl.prompt = ">" |
| rl.history = { "" } |
| rl.history_index = 1 |
| rl.history_length = 1 |
| |
| rl.hide_cmd = function() |
| local bs = string.char(8) .. " " .. string.char(8) |
| for i = 1, #rl.command do |
| stdout_write(bs) |
| end |
| end |
| |
| rl.show_cmd = function() |
| if rl.command then |
| stdout_write(rl.command) |
| end |
| end |
| |
| rl.store_history = function(cmd) |
| if cmd == "" then |
| return |
| end |
| rl.history[rl.history_length] = cmd |
| rl.history_length = rl.history_length + 1 |
| rl.history_index = rl.history_length |
| rl.history[rl.history_length] = "" |
| end |
| |
| rl.readln = function(stdin_select_fn, batch_cmd, batch_when, batch_expect) |
| local done = false |
| local need_prompt = true |
| rl.command = "" |
| |
| if not rl.rawmode then |
| rl.init() |
| end |
| |
| while not done do |
| local indent_value = #rl.prompt + #rl.command |
| if need_prompt then |
| stdout_write(rl.prompt) |
| stdout_write(rl.command) |
| need_prompt = false |
| end |
| if type(stdin_select_fn) == "function" then |
| while not stdin_select_fn(indent_value, batch_cmd, batch_when, batch_expect) do |
| stdout_write(rl.prompt) |
| stdout_write(rl.command) |
| indent_value = #rl.prompt + #rl.command |
| end |
| if batch_cmd and ((os_time() > batch_when) or (batch_expect and expect_success(batch_expect, buf, 0))) then |
| stdout_write("\n" .. rl.prompt .. batch_cmd .. "\n") |
| if batch_expect then |
| expect_done(batch_expect) |
| end |
| return batch_cmd, batch_expect |
| end |
| end |
| local ch = readch() |
| if ch:byte(1) == 27 then |
| -- CONTROL |
| local ch2 = readch() |
| -- arrows |
| if ch2:byte(1) == 91 then |
| local ch3 = readch() |
| local b = ch3:byte(1) |
| if b == 65 then |
| ch = "UP" |
| elseif b == 66 then |
| ch = "DOWN" |
| elseif b == 67 then |
| ch = "RIGHT" |
| elseif b == 68 then |
| ch = "LEFT" |
| end |
| -- print("Byte: " .. ch3:byte(1)) |
| -- if ch3:byte(1) |
| end |
| end |
| |
| if ch == "?" then |
| stdout_write(ch) |
| stdout_write("\n") |
| if rl.help then |
| rl.help(rl) |
| end |
| need_prompt = true |
| elseif ch == "\t" then |
| if rl.tab_complete then |
| rl.tab_complete(rl) |
| end |
| stdout_write("\n") |
| need_prompt = true |
| elseif ch == "\n" then |
| stdout_write(ch) |
| done = true |
| elseif ch == "\004" then |
| stdout_write("\n") |
| rl.command = nil |
| done = true |
| elseif ch == string.char(127) then |
| if rl.command ~= "" then |
| stdout_write(string.char(8) .. " " .. string.char(8)) |
| rl.command = string.sub(rl.command, 1, -2) |
| end |
| elseif #ch > 1 then |
| -- control char |
| if ch == "UP" then |
| rl.hide_cmd() |
| if rl.history_index == #rl.history then |
| rl.history[rl.history_index] = rl.command |
| end |
| if rl.history_index > 1 then |
| rl.history_index = rl.history_index - 1 |
| rl.command = rl.history[rl.history_index] |
| end |
| rl.show_cmd() |
| elseif ch == "DOWN" then |
| rl.hide_cmd() |
| if rl.history_index < rl.history_length then |
| rl.history_index = rl.history_index + 1 |
| rl.command = rl.history[rl.history_index] |
| end |
| rl.show_cmd() |
| end |
| else |
| stdout_write(ch) |
| rl.command = rl.command .. ch |
| end |
| end |
| if rl.command then |
| rl.store_history(rl.command) |
| end |
| return rl.command |
| end |
| return rl |
| end |
| |
| } |
| |
| local select_fds = {} |
| local sessions = {} |
| |
| local line_erased = false |
| |
| function erase_line(indent) |
| if not line_erased then |
| line_erased = true |
| stdout_write(string.rep(string.char(8), indent)..string.rep(" ", indent)..string.rep(string.char(8), indent)) |
| end |
| end |
| |
| function do_select_stdin(indent, batch_cmd, batch_when, batch_expect) |
| while true do |
| local nfds = 1+#select_fds |
| local pfds = ffi.new("struct pollfd[?]", nfds) |
| pfds[0].fd = 0; |
| pfds[0].events = 1; |
| pfds[0].revents = 0; |
| for i = 1,#select_fds do |
| pfds[i].fd = select_fds[i].fd |
| pfds[i].events = 1 |
| pfds[i].revents = 0 |
| end |
| if batch_cmd and ((os_time() > batch_when) or (batch_expect and expect_success(batch_expect, buf, 0))) then |
| return true |
| end |
| while ffi.C.poll(pfds, nfds, 10) == 0 do |
| if batch_cmd and ((os_time() > batch_when) or (batch_expect and expect_success(batch_expect, buf, 0))) then |
| return true |
| end |
| if line_erased then |
| line_erased = false |
| return false |
| end |
| end |
| if pfds[0].revents == 1 then |
| return true |
| end |
| for i = 1,#select_fds do |
| if(pfds[i].revents > 0) then |
| if pfds[i].fd ~= select_fds[i].fd then |
| print("File descriptors unequal", pfds[i].fd, select_fds[i].fd) |
| end |
| select_fds[i].cb(select_fds[i], pfds[i].revents, indent) |
| end |
| end |
| end |
| end |
| |
| local buf = ffi.new("char [32768]") |
| |
| function session_stdout_write(prefix, data) |
| data = prefix .. data:gsub("\n", "\n"..prefix):gsub("\n"..prefix.."$", "\n") |
| |
| stdout_write(data) |
| end |
| |
| function expect_success(sok, buf, nread) |
| local expect_buf_sz = ffi.sizeof(sok.expect_buf) - 128 |
| local expect_buf_avail = expect_buf_sz - sok.expect_buf_idx |
| -- print("EXPECT_SUCCESS: nread ".. tostring(nread).. " expect_buf_idx: " .. tostring(sok.expect_buf_idx) .. " expect_buf_avail: " .. tostring(expect_buf_avail) ) |
| if expect_buf_avail < 0 then |
| print "EXPECT BUFFER OVERRUN ALREADY" |
| os.exit(1) |
| end |
| if expect_buf_avail < nread then |
| if (nread >= ffi.sizeof(sok.expect_buf)) then |
| print("Read too large of a chunk to fit into expect buffer") |
| return nil |
| end |
| local delta = nread - expect_buf_avail |
| |
| ffi.C.memmove(sok.expect_buf, sok.expect_buf + delta, expect_buf_sz - delta) |
| sok.expect_buf_idx = sok.expect_buf_idx - delta |
| expect_buf_avail = nread |
| end |
| if sok.expect_buf_idx + nread > expect_buf_sz then |
| print("ERROR, I have just overrun the buffer !") |
| os.exit(1) |
| end |
| ffi.C.memcpy(sok.expect_buf + sok.expect_buf_idx, buf, nread) |
| sok.expect_buf_idx = sok.expect_buf_idx + nread |
| if sok.expect_str == nil then |
| return true |
| end |
| local match_p = ffi.C.memmem(sok.expect_buf, sok.expect_buf_idx, sok.expect_str, sok.expect_str_len) |
| if match_p ~= nil then |
| return true |
| end |
| return false |
| end |
| |
| function expect_done(sok) |
| local expect_buf_sz = ffi.sizeof(sok.expect_buf) - 128 |
| if not sok.expect_str then |
| return false |
| end |
| local match_p = ffi.C.memmem(sok.expect_buf, sok.expect_buf_idx, sok.expect_str, sok.expect_str_len) |
| if match_p ~= nil then |
| if sok.expect_cb then |
| sok.expect_cb(sok) |
| end |
| local match_idx = ffi.cast("char *", match_p) - ffi.cast("char *", sok.expect_buf) |
| ffi.C.memmove(sok.expect_buf, ffi.cast("char *", match_p) + sok.expect_str_len, expect_buf_sz - match_idx - sok.expect_str_len) |
| sok.expect_buf_idx = match_idx + sok.expect_str_len |
| sok.expect_success = true |
| |
| sok.expect_str = nil |
| sok.expect_str_len = 0 |
| return true |
| end |
| end |
| |
| function slave_events(sok, revents, indent) |
| local fd = sok.fd |
| local nread = ffi.C.read(fd, buf, ffi.sizeof(buf)-128) |
| local idx = nread - 1 |
| while idx >= 0 and buf[idx] ~= 10 do |
| idx = idx - 1 |
| end |
| if idx >= 0 then |
| erase_line(indent) |
| session_stdout_write(sok.prefix, sok.buf .. ffi.string(buf, idx+1)) |
| sok.buf = "" |
| end |
| sok.buf = sok.buf .. ffi.string(buf+idx+1, nread-idx-1) |
| -- print("\nRead: " .. tostring(nread)) |
| -- stdout_write(ffi.string(buf, nread)) |
| if expect_success(sok, buf, nread) then |
| return true |
| end |
| return false |
| end |
| |
| |
| function start_session(name) |
| local mfd, cpid = pty_run("/bin/bash") |
| local sok = { ["fd"] = mfd, ["cb"] = slave_events, ["buf"] = "", ["prefix"] = name .. ":", ["expect_buf"] = ffi.new("char [165536]"), ["expect_buf_idx"] = 0, ["expect_str"] = nil } |
| table.insert(select_fds, sok) |
| sessions[name] = sok |
| end |
| |
| function command_transform(exe) |
| if exe == "break" then |
| exe = string.char(3) |
| end |
| return exe |
| end |
| |
| function session_write(a_session, a_str) |
| if has_session(a_session) then |
| return tonumber(ffi.C.write(sessions[a_session].fd, c_str(a_str), #a_str)) |
| else |
| return 0 |
| end |
| end |
| |
| function session_exec(a_session, a_cmd) |
| local exe = command_transform(a_cmd) .. "\n" |
| session_write(a_session, exe) |
| end |
| |
| function session_cmd(ui, a_session, a_cmd) |
| if not has_session(a_session) then |
| stdout_write("ERR: No such session '" .. tostring(a_session) .. "'\n") |
| return nil |
| end |
| if a_session == "lua" then |
| local func, msg = loadstring(ui.lua_acc .. a_cmd) |
| -- stdout_write("LOADSTR: " .. vpp.dump({ ret, msg }) .. "\n") |
| if not func and string.match(msg, "<eof>") then |
| if a_session ~= ui.in_session then |
| stdout_write("ERR LOADSTR: " .. tostring(msg) .. "\n") |
| return nil |
| end |
| ui.lua_acc = ui.lua_acc .. a_cmd .. "\n" |
| return true |
| end |
| ui.lua_acc = "" |
| local ret, msg = pcall(func) |
| if ret then |
| return true |
| else |
| stdout_write("ERR: " .. msg .. "\n") |
| return nil |
| end |
| else |
| session_exec(a_session, a_cmd) |
| if ui.session_cmd_delay then |
| return { "delay", ui.session_cmd_delay } |
| end |
| return true |
| end |
| end |
| |
| function has_session(a_session) |
| if a_session == "lua" then |
| return true |
| end |
| return (sessions[a_session] ~= nil) |
| end |
| |
| function command_match(list, input, output) |
| for i, v in ipairs(list) do |
| local m = {} |
| m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9] = string.match(input, v[1]) |
| -- print("MATCH: ", vpp.dump(m)) |
| if m[1] then |
| output["result"] = m |
| output["result_index"] = i |
| return m |
| end |
| end |
| return nil |
| end |
| |
| function cmd_spawn_shell(ui, a_arg) |
| start_session(a_arg[1]) |
| return true |
| end |
| |
| function cmd_run_cmd(ui, a_arg) |
| local a_sess = a_arg[1] |
| local a_cmd = a_arg[2] |
| return session_cmd(ui, a_sess, a_cmd) |
| end |
| |
| function cmd_cd(ui, a_arg) |
| local a_sess = a_arg[1] |
| if has_session(a_sess) then |
| ui.in_session = a_sess |
| return true |
| else |
| stdout_write("ERR: Unknown session '".. tostring(a_sess) .. "'\n") |
| return nil |
| end |
| end |
| |
| function cmd_sleep(ui, a_arg) |
| return { "delay", tonumber(a_arg[1]) } |
| end |
| |
| function cmd_expect(ui, a_arg) |
| local a_sess = a_arg[1] |
| local a_expect = a_arg[2] |
| local sok = sessions[a_sess] |
| if not sok then |
| stdout_write("ERR: unknown session '" .. tostring(a_sess) .. "'\n") |
| return nil |
| end |
| sok.expect_str = c_str(a_expect) |
| sok.expect_str_len = #a_expect |
| return { "expect", a_sess } |
| end |
| |
| function cmd_info(ui, a_arg) |
| local a_sess = a_arg[1] |
| local sok = sessions[a_sess] |
| if not sok then |
| stdout_write("ERR: unknown session '" .. tostring(a_sess) .. "'\n") |
| return nil |
| end |
| print("Info for session " .. tostring(a_sess) .. "\n") |
| print("Expect buffer index: " .. tostring(sok.expect_buf_idx)) |
| print("Expect buffer: '" .. tostring(ffi.string(sok.expect_buf, sok.expect_buf_idx)) .. "'\n") |
| if sok.expect_str then |
| print("Expect string: '" .. tostring(ffi.string(sok.expect_str, sok.expect_str_len)) .. "'\n") |
| else |
| print("Expect string not set\n") |
| end |
| end |
| |
| function cmd_echo(ui, a_arg) |
| local a_data = a_arg[1] |
| print("ECHO: " .. tostring(a_data)) |
| end |
| |
| main_command_table = { |
| { "^shell ([a-zA-Z0-9_]+)$", cmd_spawn_shell }, |
| { "^run ([a-zA-Z0-9_]+) (.+)$", cmd_run_cmd }, |
| { "^cd ([a-zA-Z0-9_]+)$", cmd_cd }, |
| { "^sleep ([0-9]+)$", cmd_sleep }, |
| { "^expect ([a-zA-Z0-9_]+) (.-)$", cmd_expect }, |
| { "^info ([a-zA-Z0-9_]+)$", cmd_info }, |
| { "^echo (.-)$", cmd_echo } |
| } |
| |
| |
| |
| function ui_set_prompt(ui) |
| if ui.in_session then |
| if ui.in_session == "lua" then |
| if #ui.lua_acc > 0 then |
| ui.r.prompt = ui.in_session .. ">>" |
| else |
| ui.r.prompt = ui.in_session .. ">" |
| end |
| else |
| ui.r.prompt = ui.in_session .. "> " |
| end |
| else |
| ui.r.prompt = "> " |
| end |
| return ui.r.prompt |
| end |
| |
| function ui_run_command(ui, cmd) |
| -- stdout_write("Command: " .. tostring(cmd) .. "\n") |
| local ret = false |
| if ui.in_session then |
| if cmd then |
| if cmd == "^D^D^D" then |
| ui.in_session = nil |
| ret = true |
| else |
| ret = session_cmd(ui, ui.in_session, cmd) |
| end |
| else |
| ui.in_session = nil |
| ret = true |
| end |
| else |
| if cmd then |
| local out = {} |
| if cmd == "" then |
| ret = true |
| end |
| if command_match(main_command_table, cmd, out) then |
| local i = out.result_index |
| local m = out.result |
| if main_command_table[i][2] then |
| ret = main_command_table[i][2](ui, m) |
| end |
| end |
| end |
| if not cmd or cmd == "quit" then |
| return "quit" |
| end |
| end |
| return ret |
| end |
| |
| local ui = {} |
| ui.in_session = nil |
| ui.r = readln.reader() |
| ui.lua_acc = "" |
| ui.session_cmd_delay = 0.3 |
| |
| local lines = "" |
| |
| local done = false |
| -- a helper function which always returns nil |
| local no_next_line = function() return nil end |
| |
| -- a function which returns the next batch line |
| local next_line = no_next_line |
| |
| local batchfile = arg[1] |
| |
| if batchfile then |
| local f = io.lines(batchfile) |
| next_line = function() |
| local line = f() |
| if line then |
| return line |
| else |
| next_line = no_next_line |
| session_stdout_write(batchfile .. ":", "End of batch\n") |
| return nil |
| end |
| end |
| end |
| |
| |
| local batch_when = 0 |
| local batch_expect = nil |
| while not done do |
| local prompt = ui_set_prompt(ui) |
| local batch_cmd = next_line() |
| local cmd, expect_sok = ui.r.readln(do_select_stdin, batch_cmd, batch_when, batch_expect) |
| if expect_sok and not expect_success(expect_sok, buf, 0) then |
| if not cmd_ret and next_line ~= no_next_line then |
| print("ERR: expect timeout\n") |
| next_line = no_next_line |
| end |
| else |
| local cmd_ret = ui_run_command(ui, cmd) |
| if not cmd_ret and next_line ~= no_next_line then |
| print("ERR: Error during batch execution\n") |
| next_line = no_next_line |
| end |
| |
| if cmd_ret == "quit" then |
| done = true |
| end |
| batch_expect = nil |
| batch_when = 0 |
| if type(cmd_ret) == "table" then |
| if cmd_ret[1] == "delay" then |
| batch_when = os_time() + tonumber(cmd_ret[2]) |
| end |
| if cmd_ret[1] == "expect" then |
| batch_expect = sessions[cmd_ret[2]] |
| batch_when = os_time() + 15 |
| end |
| end |
| end |
| end |
| ui.r.done() |
| |
| os.exit(1) |
| |
| |
| |