From 49094a39da94eeb2c917440be612f60b188e60a2 Mon Sep 17 00:00:00 2001
From: "Dmitry E. Oboukhov" <unera@debian.org>
Date: Tue, 30 Sep 2014 18:14:13 +0400
Subject: [PATCH] console can connect to remote admin port. closes #538

 + Add tarantoolctl enter instance_name command
---
 extra/dist/dist.lua           |  19 +++++
 src/box/lua/load_cfg.lua      |   6 +-
 src/lua/box_net_box.lua       | 136 ++++++++++++++++++++++++++++------
 src/lua/console.lua           |  35 +++++++++
 test/app/console.result       |   3 +-
 test/app/console.test.lua     |   4 +-
 test/box/box.net.box.result   |  57 +++++++++++++-
 test/box/box.net.box.test.lua |  28 +++++++
 test/box/cfg.result           |   2 +-
 test/lib/admin_connection.py  |   6 ++
 10 files changed, 269 insertions(+), 27 deletions(-)

diff --git a/extra/dist/dist.lua b/extra/dist/dist.lua
index 8a27a76e8c..b4d9e381ce 100755
--- a/extra/dist/dist.lua
+++ b/extra/dist/dist.lua
@@ -61,6 +61,9 @@ Each instance can be controlled by C<dist.lua>:
 
     dist.lua logrotate instance_name
 
+=head2 Enter instance admin console
+
+    dist.lua enter instance_name
 
 =head1 COPYRIGHT
 
@@ -253,6 +256,22 @@ elseif cmd == 'logrotate' then
     s:read({ '[.][.][.]' }, 2)
 
     os.exit(0)
+
+elseif cmd == 'enter' then
+    if fio.stat(console_sock) == nil then
+        log.error("Can't connect to %s (socket not found)", console_sock)
+        os.exit(-1)
+    end
+    
+    log.info('Connecting to %s', console_sock)
+    
+    local cmd = string.format(
+        "require('console').connect('%s')", console_sock)
+
+    console.on_start( function(self) self:eval(cmd) end )
+    console.on_client_disconnect( function(self) self.running = false end )
+    console.start()
+    os.exit(0)
 else
     log.error("Unknown command '%s'", cmd)
     os.exit(-1)
diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua
index 47496a3e64..019e873dbd 100644
--- a/src/box/lua/load_cfg.lua
+++ b/src/box/lua/load_cfg.lua
@@ -163,11 +163,15 @@ local box = require('box')
 local box_configured = {}
 for k, v in pairs(box) do
     box_configured[k] = v
-    box[k] = nil
+    -- box.net.box uses box.error and box.internal
+    if k ~= 'error' and k ~= 'internal' then
+        box[k] = nil
+    end
 end
 
 setmetatable(box, {
     __index = function(table, index)
+        error(debug.traceback("Please call box.cfg{} first"))
         error("Please call box.cfg{} first")
      end
 })
diff --git a/src/lua/box_net_box.lua b/src/lua/box_net_box.lua
index 9642ce907c..256cf11e02 100644
--- a/src/lua/box_net_box.lua
+++ b/src/lua/box_net_box.lua
@@ -42,6 +42,8 @@ local TIMEOUT_INFINITY  = 500 * 365 * 86400
 local sequence_mt = { __serialize = 'sequence'}
 local mapping_mt = { __serialize = 'mapping'}
 
+local CONSOLE_FAKESYNC  = 15121974
+
 local function request(header, body)
 
     -- hint msgpack to always encode header and body as a map
@@ -399,7 +401,32 @@ local remote_methods = {
 
     call    = function(self, proc_name, ...)
         proc_name = tostring(proc_name)
-        local res = self:_request('call', true, proc_name, {...})
+
+        if not self.console then
+            local res = self:_request('call', true, proc_name, {...})
+            return res.body[DATA]
+        end
+        
+        local eval_str = proc_name .. '('
+        for i = 1, select('#', ...) do
+            if i > 1 then
+                eval_str = eval_str .. ', '
+            end
+            local arg = select(i, ...)
+
+            if arg == nil then
+                eval_str = eval_str .. 'nil'
+            elseif type(arg) == 'number' then
+                eval_str = eval_str .. tostring(arg)
+            else
+                arg = tostring(arg)
+                arg = string.gsub(arg, '"', '\\"')
+                eval_str = eval_str .. '"' .. arg .. '"'
+            end
+        end
+        eval_str = eval_str .. ")\n"
+
+        local res = self:_console_request(eval_str, true)
         return res.body[DATA]
     end,
 
@@ -497,7 +524,49 @@ local remote_methods = {
         end
     end,
 
+
+    _check_console_response = function(self)
+        while true do
+            local resp = string.match(self.rbuf, '.-\n[.][.][.]\r?\n')
+            if resp == nil then
+                break
+            end
+            self.rbuf = string.sub(self.rbuf, #resp + 1)
+
+            local result = yaml.decode(resp)
+            if result ~= nil then
+                result = result[1]
+            end
+            
+            local hdr = { [SYNC] = CONSOLE_FAKESYNC, [TYPE] = 0 }
+            local body = {}
+
+            if type(result) ~= 'table' then
+                result = { result }
+            end
+
+
+            if result.error ~= nil then
+                hdr[TYPE] = bit.bor(ERROR_TYPE, box.error.PROC_LUA)
+                body[ERROR] = result.error
+            else
+                body[DATA] = { result }
+            end
+
+            if self.ch.sync[CONSOLE_FAKESYNC] ~= nil then
+                self.ch.sync[CONSOLE_FAKESYNC]:put({hdr = hdr, body = body })
+                self.ch.sync[CONSOLE_FAKESYNC] = nil
+            else
+                log.warn("Unexpected console response: %s", resp)
+            end
+        end
+    end,
+
     _check_response = function(self)
+        if self.console then
+            return self:_check_console_response(self)
+        end
+    
         while true do
             if #self.rbuf < 5 then
                 break
@@ -599,6 +668,7 @@ local remote_methods = {
         end
     end,
 
+
     _connect_worker = function(self)
         fiber.name('net.box.connector')
         while true do
@@ -630,24 +700,30 @@ local remote_methods = {
                 elseif string.len(self.handshake) ~= 128 then
                     self:_fatal("Can't read handshake")
                 else
-
                     self.wbuf = ''
                     self.rbuf = ''
 
-                    local s, e = pcall(function()
-                        self:_auth()
-                    end)
-                    if not s then
-                        self:_fatal(e)
-                    end
-                        
-                    xpcall(function() self:_load_schema() end,
-                        function(e)
-                            log.info("Can't load schema: %s", tostring(e))
-                        end)
-                   
-                    if self.state ~= 'error' and self.state ~= 'closed' then
+                    if string.match(self.handshake, '^Tarantool console') then
+                        log.info('Remote host is tarantool console')
+                        self.console = true
                         self:_switch_state('active')
+                    else
+                        self.console = false
+                        local s, e = pcall(function()
+                            self:_auth()
+                        end)
+                        if not s then
+                            self:_fatal(e)
+                        end
+                            
+                        xpcall(function() self:_load_schema() end,
+                            function(e)
+                                log.info("Can't load schema: %s", tostring(e))
+                            end)
+                       
+                        if self.state ~= 'error' and self.state ~= 'closed' then
+                            self:_switch_state('active')
+                        end
                     end
                 end
             end
@@ -886,16 +962,20 @@ local remote_methods = {
         return self:_request_internal(name, raise, ...)
     end,
 
-    _request_internal = function(self, name, raise, ...)
+    _console_request = function(self, request_body, raise)
+        if raise == nil then
+            raise = true
+        end
+        return self:_request_raw(CONSOLE_FAKESYNC, request_body, raise)
+    end,
+
+    _request_raw = function(self, sync, request, raise)
         
         local fid = fiber.id()
         if self.timeouts[fid] == nil then
             self.timeouts[fid] = TIMEOUT_INFINITY
         end
 
-        local sync = self.proto:sync()
-        local request = self.proto[name](sync, ...)
-
         self.wbuf = self.wbuf .. request
 
         local wstate = self._to_wstate[self.state]
@@ -931,8 +1011,11 @@ local remote_methods = {
         end
 
         if response.body[DATA] ~= nil then
-            for i, v in pairs(response.body[DATA]) do
-                response.body[DATA][i] = box.tuple.new(response.body[DATA][i])
+            if rawget(box, 'tuple') ~= nil then
+                for i, v in pairs(response.body[DATA]) do
+                    response.body[DATA][i] =
+                        box.tuple.new(response.body[DATA][i])
+                end
             end
             -- disable YAML flow output (useful for admin console)
             setmetatable(response.body[DATA], sequence_mt)
@@ -941,6 +1024,14 @@ local remote_methods = {
         return response
     end,
 
+    _request_internal = function(self, name, raise, ...)
+
+        local sync = self.proto:sync()
+        local request = self.proto[name](sync, ...)
+        return self:_request_raw(sync, request, raise)
+        
+    end,
+
     -- private (low level) methods
     _select = function(self, spaceno, indexno, key, opts)
         local res = self:_request('select', true, spaceno, indexno, key, opts)
@@ -994,7 +1085,8 @@ remote.self = {
             result[i] = box.tuple.new(v)
         end
         return result
-    end
+    end,
+    console = false
 }
 
 
diff --git a/src/lua/console.lua b/src/lua/console.lua
index fe04952f5b..04a5eea269 100644
--- a/src/lua/console.lua
+++ b/src/lua/console.lua
@@ -70,6 +70,9 @@ end
 --
 local function remote_eval(self, line)
     if not line then
+        if type(self.on_client_disconnect) == 'function' then
+            self:on_client_disconnect()
+        end
         pcall(self.remote.close, self.remote)
         self.remote = nil
         self.eval = nil
@@ -179,7 +182,12 @@ local repl_mt = {
 -- REPL = read-eval-print-loop
 --
 local function repl(self)
+    
     fiber.self().storage.console = self
+    if type(self.on_start) == 'function' then
+        self:on_start()
+    end
+
     while self.running do
         local command = self:read()
         local output = self:eval(command)
@@ -188,6 +196,22 @@ local function repl(self)
     fiber.self().storage.console = nil
 end
 
+local function on_start(foo)
+    if foo == nil or type(foo) == 'function' then
+        repl_mt.__index.on_start = foo 
+        return
+    end
+    error('Wrong type of on_start hook: ' .. type(foo))
+end
+
+local function on_client_disconnect(foo)
+    if foo == nil or type(foo) == 'function' then
+        repl_mt.__index.on_client_disconnect = foo 
+        return
+    end
+    error('Wrong type of on_client_disconnect hook: ' .. type(foo))
+end
+
 --
 -- Set delimiter
 --
@@ -239,6 +263,14 @@ local function connect(uri)
     -- connect to remote host
     local remote = require('net.box'):new(u.host, u.service,
         { user = u.login, password = u.password })
+
+    -- run disconnect trigger
+    if remote.state == 'closed' then
+        if type(self.on_client_disconnect) == 'function' then
+            self:on_client_disconnect()
+        end
+    end
+
     -- check permissions
     remote:call('dostring', 'return true')
     -- override methods
@@ -256,6 +288,7 @@ local function client_handler(client, peer)
         print = client_print;
         client = client;
     }, repl_mt)
+    state:print(string.format("%-63s\n%-63s\n", "Tarantool console port", ""))
     repl(state)
     log.info("console: client %s:%s disconnected", peer.host, peer.port)
 end
@@ -292,4 +325,6 @@ return {
     delimiter = delimiter;
     connect = connect;
     listen = listen;
+    on_start = on_start;
+    on_client_disconnect = on_client_disconnect;
 }
diff --git a/test/app/console.result b/test/app/console.result
index c36eef7c72..40596ba7e4 100644
--- a/test/app/console.result
+++ b/test/app/console.result
@@ -1,6 +1,7 @@
 TAP version 13
-1..25
+1..26
 ok - console.listen started
+ok - Handshake
 ok - connect to console
 ok - eval
 ok - state.socker:peer().host
diff --git a/test/app/console.test.lua b/test/app/console.test.lua
index 8ee97cbaf1..d78df26eac 100755
--- a/test/app/console.test.lua
+++ b/test/app/console.test.lua
@@ -22,12 +22,14 @@ local EOL = "\n%.%.%.\n"
 
 test = tap.test("console")
 
-test:plan(25)
+test:plan(26)
 
 -- Start console and connect to it
 local server = console.listen(CONSOLE_SOCKET)
 test:ok(server ~= nil, "console.listen started")
 local client = socket.tcp_connect("unix/", CONSOLE_SOCKET)
+local handshake = client:read{chunk = 128}
+test:ok(string.match(handshake, '^Tarantool console') ~= nil, 'Handshake')
 test:ok(client ~= nil, "connect to console")
 
 -- Execute some command
diff --git a/test/box/box.net.box.result b/test/box/box.net.box.result
index eff5cf4775..c4f8a3adf0 100644
--- a/test/box/box.net.box.result
+++ b/test/box/box.net.box.result
@@ -141,7 +141,7 @@ cn.space.net_box_test_space:insert{234, 1,2,3}
 ...
 cn.space.net_box_test_space.insert{234, 1,2,3}
 ---
-- error: 'builtin/net.box.lua:226: Use space:method(...) instead space.method(...)'
+- error: 'builtin/net.box.lua:228: Use space:method(...) instead space.method(...)'
 ...
 cn.space.net_box_test_space:replace{354, 1,2,3}
 ---
@@ -518,3 +518,58 @@ remote.self:timeout(123).space.net_box_test_space:select{234}
 space:drop()
 ---
 ...
+-- admin console tests
+function console_test(...) return { ... } end
+---
+...
+function console_test_error(...) error(string.format(...)) end
+---
+...
+function console_unpack_test(...) return ... end
+---
+...
+ADMIN = require('uri').parse(os.getenv('ADMIN'))
+---
+...
+cn = remote:new(LISTEN.host, LISTEN.service)
+---
+...
+cnc = remote:new(ADMIN.host, ADMIN.service)
+---
+...
+cnc.console
+---
+- true
+...
+cn:call('console_test', 1, 2, 3, 'string', nil)
+---
+- - [1, 2, 3, 'string']
+...
+cnc:call('console_test', 1, 2, 3, 'string', nil)
+---
+- - [1, 2, 3, 'string']
+...
+cn:call('console_test_error', 'error %d', 123)
+---
+- error: '[string "function console_test_error(...) error(string..."]:1: error 123'
+...
+cnc:call('console_test_error', 'error %d', 123)
+---
+- error: '[string "function console_test_error(...) error(string..."]:1: error 123'
+...
+cn:call('console_unpack_test', 1)
+---
+- - [1]
+...
+cnc:call('console_unpack_test', 1)
+---
+- - [1]
+...
+cn:call('123')
+---
+- error: Procedure '123' is not defined
+...
+cnc:call('123')
+---
+- error: '[string "123()"]:1: unexpected symbol near ''123'''
+...
diff --git a/test/box/box.net.box.test.lua b/test/box/box.net.box.test.lua
index 6dc3448aaf..b3de500c8a 100644
--- a/test/box/box.net.box.test.lua
+++ b/test/box/box.net.box.test.lua
@@ -195,3 +195,31 @@ remote.self:timeout(123).space.net_box_test_space:select{234}
 -- cleanup database after tests
 space:drop()
 
+
+-- admin console tests
+function console_test(...) return { ... } end
+function console_test_error(...) error(string.format(...)) end
+function console_unpack_test(...) return ... end
+
+
+ADMIN = require('uri').parse(os.getenv('ADMIN'))
+
+cn = remote:new(LISTEN.host, LISTEN.service)
+cnc = remote:new(ADMIN.host, ADMIN.service)
+cnc.console
+
+cn:call('console_test', 1, 2, 3, 'string', nil)
+cnc:call('console_test', 1, 2, 3, 'string', nil)
+
+cn:call('console_test_error', 'error %d', 123)
+cnc:call('console_test_error', 'error %d', 123)
+
+
+cn:call('console_unpack_test', 1)
+cnc:call('console_unpack_test', 1)
+
+
+
+
+cn:call('123')
+cnc:call('123')
diff --git a/test/box/cfg.result b/test/box/cfg.result
index 60f359c31f..6255ecf21b 100644
--- a/test/box/cfg.result
+++ b/test/box/cfg.result
@@ -2,7 +2,7 @@
 --# push filter 'admin: .*' to 'admin: <uri>'
 box.cfg.nosuchoption = 1
 ---
-- error: '[string "-- load_cfg.lua - internal file..."]:191: Attempt to modify a read-only
+- error: '[string "-- load_cfg.lua - internal file..."]:195: Attempt to modify a read-only
     table'
 ...
 t = {} for k,v in pairs(box.cfg) do if type(v) ~= 'table' and type(v) ~= 'function' then table.insert(t, k..': '..tostring(v)) end end
diff --git a/test/lib/admin_connection.py b/test/lib/admin_connection.py
index 14d2e90802..544be0b950 100644
--- a/test/lib/admin_connection.py
+++ b/test/lib/admin_connection.py
@@ -56,3 +56,9 @@ class AdminConnection(TarantoolConnection):
             if not silent:
                 sys.stdout.write(res.replace("\r\n", "\n"))
         return res
+
+    def connect(self):
+        super(AdminConnection, self).connect()
+        handshake = self.socket.recv(128)
+        if not re.search(r'^Tarantool console', str(handshake)):
+            raise RuntimeError('Broken tarantool console handshake')
-- 
GitLab