Luajit API and some examples

Change-Id: Ia140c4750f06870c40b7058c4afb2e20ca633a49
Signed-off-by: Andrew Yourtchenko <ayourtch@gmail.com>
diff --git a/vpp-api/lua/examples/lute/lute.lua b/vpp-api/lua/examples/lute/lute.lua
new file mode 100644
index 0000000..89b9924
--- /dev/null
+++ b/vpp-api/lua/examples/lute/lute.lua
@@ -0,0 +1,777 @@
+--[[
+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)
+
+
+