blob: 89b9924b9016a4c2c33922fe352c97aa7913e699 [file] [log] [blame]
Andrew Yourtchenkofa1456a2016-11-11 16:32:52 +00001--[[
2version = 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
25ffi = require("ffi")
26
27vpp = {}
28function 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
39end
40
41
42ffi.cdef([[
43
44int posix_openpt(int flags);
45int grantpt(int fd);
46int unlockpt(int fd);
47char *ptsname(int fd);
48
49typedef long pid_t;
50typedef long ssize_t;
51typedef long size_t;
52typedef int nfds_t;
53typedef long time_t;
54typedef long suseconds_t;
55
56pid_t fork(void);
57pid_t setsid(void);
58
59int close(int fd);
60int open(char *pathname, int flags);
61
62int dup2(int oldfd, int newfd);
63
64ssize_t read(int fd, void *buf, size_t count);
65ssize_t write(int fd, const void *buf, size_t count);
66
67struct pollfd {
68 int fd; /* file descriptor */
69 short events; /* requested events */
70 short revents; /* returned events */
71 };
72
73int poll(struct pollfd *fds, nfds_t nfds, int timeout);
74
75struct timeval {
76 time_t tv_sec; /* seconds */
77 suseconds_t tv_usec; /* microseconds */
78 };
79
80int gettimeofday(struct timeval *tv, struct timezone *tz);
81
82int inet_pton(int af, const char *src, void *dst);
83
84]])
85
86ffi.cdef([[
87void *memset(void *s, int c, size_t n);
88void *memcpy(void *dest, void *src, size_t n);
89void *memmove(void *dest, const void *src, size_t n);
90void *memmem(const void *haystack, size_t haystacklen,
91 const void *needle, size_t needlelen);
92]])
93
94
95
96local O_RDWR = 2
97
98
99function 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)
103end
104
105function 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
110end
111
112
113function 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
118end
119
120function 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
132end
133
134function 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)
141end
142
143function 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)
174end
175
176function 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])
181end
182
183function stdout_write(str)
184 ffi.C.write(1, c_str(str), #str)
185end
186
187
188readln = {
189split = 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
208end,
209
210reader = 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
359end
360
361}
362
363local select_fds = {}
364local sessions = {}
365
366local line_erased = false
367
368function 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
373end
374
375function 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
411end
412
413local buf = ffi.new("char [32768]")
414
415function session_stdout_write(prefix, data)
416 data = prefix .. data:gsub("\n", "\n"..prefix):gsub("\n"..prefix.."$", "\n")
417
418 stdout_write(data)
419end
420
421function 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
454end
455
456function 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
475end
476
477function 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
496end
497
498
499function 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
504end
505
506function command_transform(exe)
507 if exe == "break" then
508 exe = string.char(3)
509 end
510 return exe
511end
512
513function 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
519end
520
521function session_exec(a_session, a_cmd)
522 local exe = command_transform(a_cmd) .. "\n"
523 session_write(a_session, exe)
524end
525
526function 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
557end
558
559function has_session(a_session)
560 if a_session == "lua" then
561 return true
562 end
563 return (sessions[a_session] ~= nil)
564end
565
566function 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
578end
579
580function cmd_spawn_shell(ui, a_arg)
581 start_session(a_arg[1])
582 return true
583end
584
585function 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)
589end
590
591function 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
600end
601
602function cmd_sleep(ui, a_arg)
603 return { "delay", tonumber(a_arg[1]) }
604end
605
606function 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 }
617end
618
619function 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
634end
635
636function cmd_echo(ui, a_arg)
637 local a_data = a_arg[1]
638 print("ECHO: " .. tostring(a_data))
639end
640
641main_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
653function 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
668end
669
670function 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
704end
705
706local ui = {}
707ui.in_session = nil
708ui.r = readln.reader()
709ui.lua_acc = ""
710ui.session_cmd_delay = 0.3
711
712local lines = ""
713
714local done = false
715-- a helper function which always returns nil
716local no_next_line = function() return nil end
717
718-- a function which returns the next batch line
719local next_line = no_next_line
720
721local batchfile = arg[1]
722
723if 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
735end
736
737
738local batch_when = 0
739local batch_expect = nil
740while 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
771end
772ui.r.done()
773
774os.exit(1)
775
776
777