89b9924b9016a4c2c33922fe352c97aa7913e699
[vpp.git] / vpp-api / lua / examples / lute / lute.lua
1 --[[
2 version = 1
3 /*
4  * Copyright (c) 2016 Cisco and/or its affiliates.
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at:
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 ]]
18
19 -- LUTE: Lua Unit Test Environment
20 -- AKA what happens when screen tries to marry with lua and expect,
21 -- but escapes mid-ceremony.
22 --
23 -- comments: @ayourtch
24
25 ffi = require("ffi")
26
27 vpp = {}
28 function vpp.dump(o)
29    if type(o) == 'table' then
30       local s = '{ '
31       for k,v in pairs(o) do
32          if type(k) ~= 'number' then k = '"'..k..'"' end
33          s = s .. '['..k..'] = ' .. vpp.dump(v) .. ','
34       end
35       return s .. '} '
36    else
37       return tostring(o)
38    end
39 end
40
41
42 ffi.cdef([[
43
44 int posix_openpt(int flags);
45 int grantpt(int fd);
46 int unlockpt(int fd);
47 char *ptsname(int fd);
48
49 typedef long pid_t;
50 typedef long ssize_t;
51 typedef long size_t;
52 typedef int nfds_t;
53 typedef long time_t;
54 typedef long suseconds_t;
55
56 pid_t fork(void);
57 pid_t setsid(void);
58
59 int close(int fd);
60 int open(char *pathname, int flags);
61
62 int dup2(int oldfd, int newfd);
63
64 ssize_t read(int fd, void *buf, size_t count);
65 ssize_t write(int fd, const void *buf, size_t count);
66
67 struct pollfd {
68                int   fd;         /* file descriptor */
69                short events;     /* requested events */
70                short revents;    /* returned events */
71            };
72
73 int poll(struct pollfd *fds, nfds_t nfds, int timeout);
74
75 struct timeval {
76                time_t      tv_sec;     /* seconds */
77                suseconds_t tv_usec;    /* microseconds */
78            };
79
80 int gettimeofday(struct timeval *tv, struct timezone *tz);
81
82 int inet_pton(int af, const char *src, void *dst);
83
84 ]])
85
86 ffi.cdef([[
87 void *memset(void *s, int c, size_t n);
88 void *memcpy(void *dest, void *src, size_t n);
89 void *memmove(void *dest, const void *src, size_t n);
90 void *memmem(const void *haystack, size_t haystacklen,
91         const void *needle, size_t needlelen);
92 ]])
93
94
95
96 local O_RDWR = 2
97
98
99 function os_time()
100   local tv = ffi.new("struct timeval[1]")
101   local ret = ffi.C.gettimeofday(tv, nil)
102   return tonumber(tv[0].tv_sec) + (tonumber(tv[0].tv_usec)/1000000.0)
103 end
104
105 function sleep(n)
106   local when_wakeup = os_time() + n
107   while os_time() <= when_wakeup do
108     ffi.C.poll(nil, 0, 10)
109   end
110 end
111
112
113 function c_str(text_in)
114   local text = text_in 
115   local c_str = ffi.new("char[?]", #text+1)
116   ffi.copy(c_str, text)
117   return c_str
118 end
119
120 function ip46(addr_text)
121   local out = ffi.new("char [200]")
122   local AF_INET6 = 10
123   local AF_INET = 2
124   local is_ip6 = ffi.C.inet_pton(AF_INET6, c_str(addr_text), out)
125   if is_ip6 == 1 then
126     return ffi.string(out, 16), true
127   end
128   local is_ip4 = ffi.C.inet_pton(AF_INET, c_str(addr_text), out)
129   if is_ip4 then
130     return (string.rep("4", 12).. ffi.string(out, 4)), false
131   end
132 end
133
134 function pty_master_open()
135   local fd = ffi.C.posix_openpt(O_RDWR)
136   ffi.C.grantpt(fd)
137   ffi.C.unlockpt(fd)
138   local p = ffi.C.ptsname(fd)
139   print("PTS:" .. ffi.string(p))
140   return fd, ffi.string(p)
141 end
142
143 function pty_run(cmd)
144   local master_fd, pts_name = pty_master_open()
145   local child_pid = ffi.C.fork()
146   if (child_pid == -1) then
147     print("Error fork()ing")
148     return -1
149   end 
150  
151   if child_pid ~= 0 then
152     -- print("Parent")
153     return master_fd, child_pid
154   end
155
156   -- print("Child")
157   if (ffi.C.setsid() == -1) then
158     print("Child error setsid")
159     os.exit(-1)
160   end
161
162   ffi.C.close(master_fd)
163
164   local slave_fd = ffi.C.open(c_str(pts_name), O_RDWR)
165   if slave_fd == -1 then
166     print("Child can not open slave fd")
167     os.exit(-2)
168   end
169
170   ffi.C.dup2(slave_fd, 0)
171   ffi.C.dup2(slave_fd, 1)
172   ffi.C.dup2(slave_fd, 2)
173   os.execute(cmd)
174 end
175
176 function readch()
177   local buf = ffi.new("char[1]")
178   local nread= ffi.C.read(0, buf, 1)
179   -- print("\nREADCH : " .. string.char(buf[0]))
180   return string.char(buf[0])
181 end
182
183 function stdout_write(str)
184   ffi.C.write(1, c_str(str), #str)
185 end
186
187
188 readln = {
189 split = function(str, pat)
190   local t = {}  -- NOTE: use {n = 0} in Lua-5.0
191   local fpat = "(.-)" .. pat
192   local last_end = 1
193   if str then
194     local s, e, cap = str:find(fpat, 1)
195     while s do
196       if s ~= 1 or cap ~= "" then
197         table.insert(t,cap)
198       end
199       last_end = e+1
200       s, e, cap = str:find(fpat, last_end)
201     end
202     if last_end <= #str then
203       cap = str:sub(last_end)
204       table.insert(t, cap)
205     end
206   end
207   return t
208 end,
209
210 reader = function()
211   local rl = {}
212
213   rl.init = function()
214     os.execute("stty -icanon min 1 -echo")
215     rl.rawmode = true
216   end
217
218   rl.done = function()
219     os.execute("stty icanon echo")
220     rl.rawmode = false
221   end
222
223   rl.prompt = ">"
224   rl.history = { "" }
225   rl.history_index = 1
226   rl.history_length = 1
227
228   rl.hide_cmd = function()
229     local bs = string.char(8) .. " " .. string.char(8)
230     for i = 1, #rl.command do
231       stdout_write(bs)
232     end
233   end
234
235   rl.show_cmd = function()
236     if rl.command then
237       stdout_write(rl.command)
238     end
239   end
240
241   rl.store_history = function(cmd)
242     if cmd == "" then
243       return
244     end
245     rl.history[rl.history_length] = cmd
246     rl.history_length = rl.history_length + 1
247     rl.history_index = rl.history_length
248     rl.history[rl.history_length] = ""
249   end
250
251   rl.readln = function(stdin_select_fn, batch_cmd, batch_when, batch_expect)
252     local done = false
253     local need_prompt = true
254     rl.command = ""
255
256     if not rl.rawmode then
257       rl.init()
258     end
259
260     while not done do
261       local indent_value = #rl.prompt + #rl.command
262       if need_prompt then
263         stdout_write(rl.prompt)
264         stdout_write(rl.command)
265         need_prompt = false
266       end
267       if type(stdin_select_fn) == "function" then
268         while not stdin_select_fn(indent_value, batch_cmd, batch_when, batch_expect) do
269           stdout_write(rl.prompt)
270           stdout_write(rl.command)
271           indent_value = #rl.prompt + #rl.command
272         end
273         if batch_cmd and ((os_time() > batch_when) or (batch_expect and expect_success(batch_expect, buf, 0))) then
274           stdout_write("\n" .. rl.prompt .. batch_cmd .. "\n")
275           if batch_expect then
276             expect_done(batch_expect)
277           end
278           return batch_cmd, batch_expect
279         end
280       end
281       local ch = readch()
282       if ch:byte(1) == 27 then
283         -- CONTROL
284         local ch2 = readch()
285         -- arrows
286         if ch2:byte(1) == 91 then
287           local ch3 = readch()
288           local b = ch3:byte(1)
289           if b == 65 then
290             ch = "UP"
291           elseif b == 66 then
292             ch = "DOWN"
293           elseif b == 67 then
294             ch = "RIGHT"
295           elseif b == 68 then
296             ch = "LEFT"
297           end
298           -- print("Byte: " .. ch3:byte(1))
299           -- if ch3:byte(1)
300         end
301       end
302
303       if ch == "?" then
304         stdout_write(ch)
305         stdout_write("\n")
306         if rl.help then
307           rl.help(rl)
308         end
309         need_prompt = true
310       elseif ch == "\t" then
311         if rl.tab_complete then
312           rl.tab_complete(rl)
313         end
314         stdout_write("\n")
315         need_prompt = true
316       elseif ch == "\n" then
317         stdout_write(ch)
318         done = true
319       elseif ch == "\004" then
320         stdout_write("\n")
321         rl.command = nil
322         done = true
323       elseif ch == string.char(127) then
324         if rl.command ~= "" then
325           stdout_write(string.char(8) .. " " .. string.char(8))
326           rl.command = string.sub(rl.command, 1, -2)
327         end
328       elseif #ch > 1 then
329         -- control char
330         if ch == "UP" then
331           rl.hide_cmd()
332           if rl.history_index == #rl.history then
333             rl.history[rl.history_index] = rl.command
334           end
335           if rl.history_index > 1 then
336             rl.history_index = rl.history_index - 1
337             rl.command = rl.history[rl.history_index]
338           end
339           rl.show_cmd()
340         elseif ch == "DOWN" then
341           rl.hide_cmd()
342           if rl.history_index < rl.history_length then
343             rl.history_index = rl.history_index + 1
344             rl.command = rl.history[rl.history_index]
345           end
346           rl.show_cmd()
347         end
348       else
349         stdout_write(ch)
350         rl.command = rl.command .. ch
351       end
352     end
353     if rl.command then
354       rl.store_history(rl.command)
355     end
356     return rl.command
357   end
358   return rl
359 end
360
361 }
362
363 local select_fds = {}
364 local sessions = {}
365
366 local line_erased = false
367
368 function erase_line(indent)
369   if not line_erased then
370     line_erased = true
371     stdout_write(string.rep(string.char(8), indent)..string.rep(" ", indent)..string.rep(string.char(8), indent))
372   end
373 end
374
375 function do_select_stdin(indent, batch_cmd, batch_when, batch_expect)
376   while true do
377     local nfds = 1+#select_fds
378     local pfds = ffi.new("struct pollfd[?]", nfds)
379     pfds[0].fd = 0;
380     pfds[0].events = 1;
381     pfds[0].revents = 0;
382     for i = 1,#select_fds do
383       pfds[i].fd = select_fds[i].fd
384       pfds[i].events = 1
385       pfds[i].revents = 0
386     end
387     if batch_cmd and ((os_time() > batch_when) or (batch_expect and expect_success(batch_expect, buf, 0))) then
388       return true
389     end
390     while ffi.C.poll(pfds, nfds, 10) == 0 do
391       if batch_cmd and ((os_time() > batch_when) or (batch_expect and expect_success(batch_expect, buf, 0))) then
392         return true
393       end
394       if line_erased then
395         line_erased = false
396         return false
397       end
398     end
399     if pfds[0].revents == 1 then
400       return true
401     end
402     for i = 1,#select_fds do
403       if(pfds[i].revents > 0) then
404         if pfds[i].fd ~= select_fds[i].fd then
405           print("File descriptors unequal", pfds[i].fd, select_fds[i].fd)
406         end
407         select_fds[i].cb(select_fds[i], pfds[i].revents, indent)
408       end
409     end
410   end
411 end
412
413 local buf = ffi.new("char [32768]")
414
415 function session_stdout_write(prefix, data)
416   data = prefix .. data:gsub("\n", "\n"..prefix):gsub("\n"..prefix.."$", "\n")
417   
418   stdout_write(data)
419 end
420
421 function expect_success(sok, buf, nread)
422   local expect_buf_sz = ffi.sizeof(sok.expect_buf) - 128
423   local expect_buf_avail = expect_buf_sz - sok.expect_buf_idx
424   -- print("EXPECT_SUCCESS: nread ".. tostring(nread).. " expect_buf_idx: " .. tostring(sok.expect_buf_idx) .. " expect_buf_avail: " .. tostring(expect_buf_avail) )
425   if expect_buf_avail < 0 then
426     print "EXPECT BUFFER OVERRUN ALREADY"
427     os.exit(1)
428   end
429   if expect_buf_avail < nread then
430     if (nread >= ffi.sizeof(sok.expect_buf)) then
431       print("Read too large of a chunk to fit into expect buffer")
432       return nil
433     end
434     local delta = nread - expect_buf_avail
435
436     ffi.C.memmove(sok.expect_buf, sok.expect_buf + delta, expect_buf_sz - delta)
437     sok.expect_buf_idx = sok.expect_buf_idx - delta
438     expect_buf_avail = nread 
439   end
440   if sok.expect_buf_idx + nread > expect_buf_sz then
441     print("ERROR, I have just overrun the buffer !")
442     os.exit(1)
443   end
444   ffi.C.memcpy(sok.expect_buf + sok.expect_buf_idx, buf, nread)
445   sok.expect_buf_idx = sok.expect_buf_idx + nread
446   if sok.expect_str == nil then
447     return true
448   end
449   local match_p = ffi.C.memmem(sok.expect_buf, sok.expect_buf_idx, sok.expect_str, sok.expect_str_len)
450   if match_p ~= nil then
451     return true
452   end
453   return false
454 end
455
456 function expect_done(sok)
457   local expect_buf_sz = ffi.sizeof(sok.expect_buf) - 128
458   if not sok.expect_str then 
459     return false
460   end
461   local match_p = ffi.C.memmem(sok.expect_buf, sok.expect_buf_idx, sok.expect_str, sok.expect_str_len)
462   if match_p ~= nil then
463     if sok.expect_cb then
464       sok.expect_cb(sok)
465     end
466     local match_idx = ffi.cast("char *", match_p) - ffi.cast("char *", sok.expect_buf)
467     ffi.C.memmove(sok.expect_buf, ffi.cast("char *", match_p) + sok.expect_str_len, expect_buf_sz - match_idx - sok.expect_str_len)
468     sok.expect_buf_idx = match_idx + sok.expect_str_len
469     sok.expect_success = true
470
471     sok.expect_str = nil
472     sok.expect_str_len = 0
473     return true
474   end
475 end
476
477 function slave_events(sok, revents, indent)
478   local fd = sok.fd
479   local nread = ffi.C.read(fd, buf, ffi.sizeof(buf)-128)
480   local idx = nread - 1
481   while idx >= 0 and buf[idx] ~= 10 do
482     idx = idx - 1
483   end
484   if idx >= 0 then
485     erase_line(indent)
486     session_stdout_write(sok.prefix, sok.buf .. ffi.string(buf, idx+1))
487     sok.buf = ""
488   end
489   sok.buf = sok.buf .. ffi.string(buf+idx+1, nread-idx-1)
490   -- print("\nRead: " .. tostring(nread))
491   -- stdout_write(ffi.string(buf, nread))
492   if expect_success(sok, buf, nread) then
493     return true
494   end
495   return false
496 end
497
498
499 function start_session(name)
500   local mfd, cpid = pty_run("/bin/bash")
501   local sok =  { ["fd"] = mfd, ["cb"] = slave_events, ["buf"] = "", ["prefix"] = name .. ":", ["expect_buf"] = ffi.new("char [165536]"), ["expect_buf_idx"] = 0, ["expect_str"] = nil }
502   table.insert(select_fds, sok)
503   sessions[name] = sok
504 end
505
506 function command_transform(exe)
507   if exe == "break" then
508     exe = string.char(3)
509   end
510   return exe
511 end
512
513 function session_write(a_session, a_str)
514   if has_session(a_session) then
515     return tonumber(ffi.C.write(sessions[a_session].fd, c_str(a_str), #a_str))
516   else
517     return 0
518   end
519 end
520
521 function session_exec(a_session, a_cmd)
522   local exe = command_transform(a_cmd) .. "\n"
523   session_write(a_session, exe)
524 end
525
526 function session_cmd(ui, a_session, a_cmd)
527   if not has_session(a_session) then
528     stdout_write("ERR: No such session '" .. tostring(a_session) .. "'\n")
529     return nil
530   end
531   if a_session == "lua" then
532     local func, msg = loadstring(ui.lua_acc .. a_cmd)
533     -- stdout_write("LOADSTR: " .. vpp.dump({ ret, msg }) .. "\n")
534     if not func and string.match(msg, "<eof>") then
535       if a_session ~= ui.in_session then
536          stdout_write("ERR LOADSTR: " .. tostring(msg) .. "\n")
537          return nil
538       end
539       ui.lua_acc = ui.lua_acc .. a_cmd  .. "\n"
540       return true
541     end
542     ui.lua_acc = ""
543     local ret, msg = pcall(func)
544     if ret then
545       return true
546     else
547       stdout_write("ERR: " .. msg .. "\n") 
548       return nil
549     end
550   else
551     session_exec(a_session, a_cmd)
552     if ui.session_cmd_delay then
553       return { "delay", ui.session_cmd_delay }
554     end
555     return true
556   end
557 end
558
559 function has_session(a_session)
560   if a_session == "lua" then
561     return true
562   end
563   return (sessions[a_session] ~= nil)
564 end
565
566 function command_match(list, input, output)
567   for i, v in ipairs(list) do
568     local m = {}
569     m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9] = string.match(input, v[1])
570     -- print("MATCH: ", vpp.dump(m))
571     if m[1] then
572        output["result"] = m
573        output["result_index"] = i
574        return m
575     end 
576   end
577   return nil
578 end
579
580 function cmd_spawn_shell(ui, a_arg)
581   start_session(a_arg[1])
582   return true
583 end
584
585 function cmd_run_cmd(ui, a_arg)
586   local a_sess = a_arg[1]
587   local a_cmd = a_arg[2]
588   return session_cmd(ui, a_sess, a_cmd)
589 end
590
591 function cmd_cd(ui, a_arg)
592   local a_sess = a_arg[1]
593   if has_session(a_sess) then
594     ui.in_session = a_sess
595     return true
596   else
597     stdout_write("ERR: Unknown session '".. tostring(a_sess) .. "'\n")
598     return nil
599   end
600 end
601
602 function cmd_sleep(ui, a_arg)
603   return { "delay", tonumber(a_arg[1]) }
604 end
605
606 function cmd_expect(ui, a_arg)
607   local a_sess = a_arg[1]
608   local a_expect = a_arg[2]
609   local sok = sessions[a_sess]
610   if not sok then
611     stdout_write("ERR: unknown session '" .. tostring(a_sess) .. "'\n")
612     return nil
613   end
614   sok.expect_str = c_str(a_expect)
615   sok.expect_str_len = #a_expect
616   return { "expect", a_sess }
617 end
618
619 function cmd_info(ui, a_arg)
620   local a_sess = a_arg[1]
621   local sok = sessions[a_sess]
622   if not sok then
623     stdout_write("ERR: unknown session '" .. tostring(a_sess) .. "'\n")
624     return nil
625   end
626   print("Info for session " .. tostring(a_sess) .. "\n")
627   print("Expect buffer index: " .. tostring(sok.expect_buf_idx))
628   print("Expect buffer: '" .. tostring(ffi.string(sok.expect_buf, sok.expect_buf_idx)) .. "'\n")
629   if sok.expect_str then
630     print("Expect string: '" .. tostring(ffi.string(sok.expect_str, sok.expect_str_len)) .. "'\n")
631   else
632     print("Expect string not set\n")
633   end
634 end
635
636 function cmd_echo(ui, a_arg)
637   local a_data = a_arg[1]
638   print("ECHO: " .. tostring(a_data))
639 end
640
641 main_command_table = {
642   { "^shell ([a-zA-Z0-9_]+)$", cmd_spawn_shell },
643   { "^run ([a-zA-Z0-9_]+) (.+)$", cmd_run_cmd },
644   { "^cd ([a-zA-Z0-9_]+)$", cmd_cd  },
645   { "^sleep ([0-9]+)$", cmd_sleep },
646   { "^expect ([a-zA-Z0-9_]+) (.-)$", cmd_expect },
647   { "^info ([a-zA-Z0-9_]+)$", cmd_info },
648   { "^echo (.-)$", cmd_echo }
649 }
650
651
652
653 function ui_set_prompt(ui)
654   if ui.in_session then 
655     if ui.in_session == "lua" then
656       if #ui.lua_acc > 0 then
657         ui.r.prompt = ui.in_session .. ">>"
658       else
659         ui.r.prompt = ui.in_session .. ">"
660       end 
661     else
662       ui.r.prompt = ui.in_session .. "> "
663     end
664   else
665     ui.r.prompt = "> "
666   end
667   return ui.r.prompt
668 end
669
670 function ui_run_command(ui, cmd)
671   -- stdout_write("Command: " .. tostring(cmd) .. "\n")
672   local ret = false
673   if ui.in_session then
674     if cmd then
675       if cmd == "^D^D^D" then
676         ui.in_session = nil
677         ret = true
678       else
679         ret = session_cmd(ui, ui.in_session, cmd)
680       end
681     else
682       ui.in_session = nil
683       ret = true
684     end
685   else  
686     if cmd then
687       local out = {}
688       if cmd == "" then
689         ret = true
690       end
691       if command_match(main_command_table, cmd, out) then
692         local i = out.result_index
693         local m = out.result
694         if main_command_table[i][2] then
695           ret = main_command_table[i][2](ui, m)
696         end
697       end
698     end
699     if not cmd or cmd == "quit" then
700       return "quit"
701     end
702   end
703   return ret
704 end
705
706 local ui = {}
707 ui.in_session = nil
708 ui.r = readln.reader() 
709 ui.lua_acc = ""
710 ui.session_cmd_delay = 0.3
711
712 local lines = ""
713
714 local done = false
715 -- a helper function which always returns nil
716 local no_next_line = function() return nil end
717
718 -- a function which returns the next batch line
719 local next_line = no_next_line
720
721 local batchfile = arg[1]
722
723 if batchfile then
724   local f = io.lines(batchfile)
725   next_line = function() 
726     local line = f()
727     if line then 
728       return line
729     else 
730       next_line = no_next_line
731       session_stdout_write(batchfile .. ":", "End of batch\n")
732       return nil
733     end
734   end
735 end
736
737
738 local batch_when = 0
739 local batch_expect = nil
740 while not done do
741   local prompt = ui_set_prompt(ui)
742   local batch_cmd = next_line() 
743   local cmd, expect_sok = ui.r.readln(do_select_stdin, batch_cmd, batch_when, batch_expect)
744   if expect_sok and not expect_success(expect_sok, buf, 0) then
745     if not cmd_ret and next_line ~= no_next_line then
746       print("ERR: expect timeout\n")
747       next_line = no_next_line
748     end
749   else 
750     local cmd_ret = ui_run_command(ui, cmd)
751     if not cmd_ret and next_line ~= no_next_line then
752       print("ERR: Error during batch execution\n")
753       next_line = no_next_line
754     end
755
756     if cmd_ret  == "quit" then
757       done = true
758     end
759     batch_expect = nil
760     batch_when = 0
761     if type(cmd_ret) == "table" then
762       if cmd_ret[1] == "delay" then
763         batch_when = os_time() + tonumber(cmd_ret[2])
764       end
765       if cmd_ret[1] == "expect" then
766         batch_expect = sessions[cmd_ret[2]]
767         batch_when = os_time() + 15
768       end
769     end
770   end
771 end
772 ui.r.done()
773
774 os.exit(1)
775
776
777