Tarantool development patches archive
 help / color / mirror / Atom feed
From: Vladislav Shpilevoy <v.shpilevoy@tarantool.org>
To: tarantool-patches@freelists.org
Cc: vdavydov.dev@gmail.com, Vladislav Shpilevoy <v.shpilevoy@tarantool.org>
Subject: [PATCH 2/3] console: do not use netbox for console text connections
Date: Thu, 22 Mar 2018 22:17:11 +0300	[thread overview]
Message-ID: <0c219168ea51252e15d683639e09cf95bc1c2b88.1521745741.git.v.shpilevoy@tarantool.org> (raw)
In-Reply-To: <cover.1521745741.git.v.shpilevoy@tarantool.org>
In-Reply-To: <cover.1521745741.git.v.shpilevoy@tarantool.org>

Netbox console support complicates both netbox and console. Lets
use sockets directly for text protocol.

Part of #2677
---
 src/box/lua/console.lua | 178 +++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 146 insertions(+), 32 deletions(-)

diff --git a/src/box/lua/console.lua b/src/box/lua/console.lua
index d49cf42be..19ff12876 100644
--- a/src/box/lua/console.lua
+++ b/src/box/lua/console.lua
@@ -8,6 +8,9 @@ local log = require('log')
 local errno = require('errno')
 local urilib = require('uri')
 local yaml = require('yaml')
+local net_box = require('net.box')
+
+local YAML_TERM = '\n...\n'
 
 -- admin formatter must be able to encode any Lua variable
 local formatter = yaml.new()
@@ -29,7 +32,7 @@ local function format(status, ...)
     if status then
         local count = select('#', ...)
         if count == 0 then
-            return "---\n...\n"
+            return "---"..YAML_TERM
         end
         local res = {}
         for i=1,count,1 do
@@ -76,26 +79,122 @@ local function eval(line)
     return local_eval(nil, line)
 end
 
+local text_connection_mt = {
+    __index = {
+        --
+        -- Close underlying socket.
+        --
+        close = function(self)
+            self._socket:close()
+        end,
+        --
+        -- Write a text into a socket.
+        -- @param test Text to send.
+        -- @retval not nil Bytes sent.
+        -- @retval     nil Error.
+        --
+        write = function(self, text)
+            -- It is the hack to protect from SIGPIPE on send in
+            -- a socket, that is actually closed. If a socket is
+            -- readable and read() returns nothing the the socket
+            -- is closed, and writing into it will raise SIGPIPE.
+            if self._socket:readable(0) then
+                local rc = self._socket:recv(1)
+                if not rc or rc == '' then
+                    return nil
+                else
+                    -- But if it was literally readable, the
+                    -- single read byte can not be put back, and
+                    -- must be stored somewhere, until console
+                    -- do read().
+                    self.buffer = self.buffer..rc
+                end
+            end
+            return self._socket:send(text)
+        end,
+        --
+        -- Read a text from a socket until YAML terminator.
+        -- @retval not nil Well formatted YAML.
+        -- @retval     nil Error.
+        --
+        read = function(self)
+            local ret = self._socket:read(YAML_TERM)
+            if ret and ret ~= '' then
+                ret = (self.buffer or '')..ret
+                self.buffer = nil
+                return ret
+            end
+        end,
+        --
+        -- Write + Read.
+        --
+        eval = function(self, text)
+            text = text..'$EOF$\n'
+            if self:write(text) then
+                local rc = self:read()
+                if rc then
+                    return rc
+                end
+            end
+            error(self:set_error())
+        end,
+        --
+        -- Make the connection be in error state, set error
+        -- message.
+        -- @retval Error message.
+        --
+        set_error = function(self)
+            self.state = 'error'
+            self.error = self._socket:error()
+            if not self.error then
+                self.error = 'Peer closed'
+            end
+            return self.error
+        end,
+    }
+}
+
+--
+-- Wrap an existing socket with text Read-Write API inside a
+-- netbox-like object.
+-- @param connection Socket to wrap.
+-- @param url Parsed destination URL.
+-- @retval nil, err Error, and err contains an error message.
+-- @retval  not nil Netbox-like object.
+--
+local function wrap_text_socket(connection, url)
+    local conn = setmetatable({
+        _socket = connection,
+        state = 'active',
+        host = url.host or 'localhost',
+        port = url.service,
+        buffer = nil,
+    }, text_connection_mt)
+    if not conn:write('require("console").delimiter("$EOF$")\n') or
+       not conn:read() then
+        return nil, conn:set_error()
+    end
+    return conn
+end
+
 --
 -- Evaluate command on remote instance
 --
 local function remote_eval(self, line)
-    if not line or self.remote.state ~= 'active' then
-        local err = self.remote.error
-        self.remote:close()
-        self.remote = nil
-        -- restore local REPL mode
-        self.eval = nil
-        self.prompt = nil
-        self.completion = nil
-        pcall(self.on_client_disconnect, self)
-        return (err and format(false, err)) or ''
+    if line and self.remote.state == 'active' then
+        local ok, res = pcall(self.remote.eval, self.remote, line)
+        if self.remote.state == 'active' then
+            return ok and res or format(false, res)
+        end
     end
-    --
-    -- execute line
-    --
-    local ok, res = pcall(self.remote.eval, self.remote, line)
-    return ok and res or format(false, res)
+    local err = self.remote.error
+    self.remote:close()
+    self.remote = nil
+    self.eval = nil
+    self.prompt = nil
+    self.completion = nil
+    pcall(self.on_client_disconnect, self)
+    return (err and format(false, err)) or ''
 end
 
 --
@@ -285,12 +384,7 @@ end
 --
 -- Connect to remove instance
 --
-local netbox_connect
 local function connect(uri, opts)
-    if not netbox_connect then -- workaround the broken loader
-        netbox_connect = require('net.box').connect
-    end
-
     opts = opts or {}
 
     local self = fiber.self().storage.console
@@ -306,18 +400,38 @@ local function connect(uri, opts)
         error('Usage: console.connect("[login:password@][host:]port")')
     end
 
-    -- connect to remote host
+    local ts = fiber.clock()
+    local connection = socket.tcp_connect(u.host, u.service, opts.timeout)
+    if connection == nil then
+        log.verbose(errno.strerror(errno()))
+        box.error(box.error.NO_CONNECTION)
+    end
+    local left_time
+    if opts.timeout then
+        left_time = opts.timeout - (fiber.clock() - ts)
+    end
+    local greeting = connection:read({chunk = 128}, left_time)
+    if not greeting then
+        log.verbose(errno.strerror(errno()))
+        box.error(box.error.NO_CONNECTION)
+    end
+    greeting = net_box.decode_greeting(greeting)
+    if not greeting then
+        log.verbose("Can't decode handshake")
+        box.error(box.error.NO_CONNECTION)
+    end
     local remote
-    remote = netbox_connect(u.host, u.service, {
-        user = u.login, password = u.password,
-        console = true, connect_timeout = opts.timeout
-    })
-    remote.host, remote.port = u.host or 'localhost', u.service
-
-    -- run disconnect trigger if connection failed
-    if not remote:is_connected() then
-        pcall(self.on_client_disconnect, self)
-        error('Connection is not established: '..remote.error)
+    if greeting.protocol == 'Lua console' then
+        remote = wrap_text_socket(connection, u)
+    else
+        remote = net_box.wrap_socket(connection, greeting, u)
+        if not remote.host then
+            remote.host = 'localhost'
+        end
+        local old_eval = remote.eval
+        remote.eval = function(con, line)
+            return old_eval(con, 'return require("console").eval(...)', {line})
+        end
     end
 
     -- check connection && permissions
-- 
2.14.3 (Apple Git-98)

  parent reply	other threads:[~2018-03-22 19:17 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-03-22 19:17 [PATCH 0/3] " Vladislav Shpilevoy
2018-03-22 19:17 ` [PATCH 1/3] netbox: allow to create a netbox connection from existing socket Vladislav Shpilevoy
2018-03-22 19:33   ` [tarantool-patches] " Konstantin Osipov
2018-03-22 19:50     ` v.shpilevoy
2018-03-22 19:57       ` [tarantool-patches] " v.shpilevoy
2018-03-23 10:01         ` [tarantool-patches] " v.shpilevoy
2018-03-26 21:55       ` Konstantin Osipov
2018-03-22 19:17 ` Vladislav Shpilevoy [this message]
2018-03-22 19:36   ` [tarantool-patches] [PATCH 2/3] console: do not use netbox for console text connections Konstantin Osipov
2018-03-22 19:17 ` [PATCH 3/3] netbox: deprecate console support Vladislav Shpilevoy
2018-03-22 19:37   ` [tarantool-patches] " Konstantin Osipov

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=0c219168ea51252e15d683639e09cf95bc1c2b88.1521745741.git.v.shpilevoy@tarantool.org \
    --to=v.shpilevoy@tarantool.org \
    --cc=tarantool-patches@freelists.org \
    --cc=vdavydov.dev@gmail.com \
    --subject='Re: [PATCH 2/3] console: do not use netbox for console text connections' \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox