#!/usr/bin/env tarantool

--[[

=head1 NAME

tarantoolctl - an utility to control tarantool instances

=head1 SYNOPSIS

    vim /etc/tarantool/instances.enabled/my_instance.lua
    tarantoolctl start my_instance
    tarantoolctl stop my_instance
    tarantoolctl logrotate my_instance

=head1 DESCRIPTION

The script is read C</etc/sysconfig/tarantool> or C</etc/default/tarantool>.
The file contains common default instances options:

    $ cat /etc/default/tarantool

    -- Options for Tarantool
    default_cfg = {
        -- will become pid_file .. instance .. '.pid'
        pid_file    =   "/var/run/tarantool",
        -- will become wal_dir/instance/
        wal_dir     =   "/var/lib/tarantool",
        -- snap_dir/instance/
        snap_dir    =   "/var/lib/tarantool",
        -- vinyl_dir/instance/
        vinyl_dir  =   "/var/lib/tarantool/vinyl",
        -- log/instance .. '.log'
        log      =   "/var/log/tarantool",
        username    =   "tarantool",
    }

    instance_dir = "/etc/tarantool/instances.enabled"


The file defines C<instance_dir> where user can place his
applications (instances).

Each instance can be controlled by C<tarantoolctl>:

=head2 Starting instance

    tarantoolctl start instance_name

=head2 Stopping instance

    tarantoolctl stop instance_name

=head2 Logrotate instance's log

    tarantoolctl logrotate instance_name

=head2 Enter instance admin console

    tarantoolctl enter instance_name

=head2 status

    tarantoolctl status instance_name

Check if instance is up.

If pid file exists and control socket exists and control socket is alive
returns code C<0>.

Return code != 0 in other cases. Can complain in log (stderr) if pid file
exists and socket doesn't, etc.


=head2 separate instances control

If You use SysV init, You can use symlink from
C<tarantoolctl> to C</etc/init.d/instance_name[.lua]>.
C<tarantoolctl> detects if it is started by symlink and uses
instance_name as C<`basename $0 .lua`>.

=head1 COPYRIGHT

Copyright (C) 2010-2013 Tarantool AUTHORS:
please see AUTHORS file.

=cut

]]


local io = require('io')
local os = require('os')
local ffi = require('ffi')
local fio = require('fio')
local fun = require('fun')
local log = require('log')
local uri = require('uri')
local json = require('json')
local xlog = require('xlog')
local yaml  = require('yaml')
local errno = require('errno')
local fiber = require('fiber')
local netbox = require('net.box')
local socket = require('socket')
local console = require('console')
local argparse = require('internal.argparse').parse

ffi.cdef[[
struct passwd {
  char *pw_name;   /* username */
  char *pw_passwd; /* user password */
  int   pw_uid;    /* user ID */
  int   pw_gid;    /* group ID */
  char *pw_gecos;  /* user information */
  char *pw_dir;    /* home directory */
  char *pw_shell;  /* shell program */
};

struct group{
  char *gr_name;
  char *gr_passwd;
  int   gr_gid;
  char **gr_mem;
};

int kill(int pid, int sig);
struct passwd *getpwnam(const char *name);
struct group *getgrgid(int gid);
int isatty(int fd);
]]

local TIMEOUT_INFINITY = 100 * 365 * 86400

-- command, that we're executing
local command_name = arg[1]
-- true if we're running in HOME directory of a user
local usermode = false
-- true if we're tarantoolctl is symlink and name != tarantoolctl
local linkmode = false
-- a file with system-wide settings
local default_file
-- current instance settings
local instance_name
local instance_path
local console_sock
local group_name
-- overrides for defaults files
local instance_dir
local default_cfg
local positional_arguments
local keyword_arguments

-- usage printer function
local usage

-- shift argv to remove 'tarantoolctl' from arg[0]
local function shift_argv(arg, argno, argcount)
    for i = argno, 128 do
        arg[i] = arg[i + argcount]
        if arg[i] == nil then
            break
        end
    end
end

local function check_user_level()
    local uid = os.getenv('UID')
    local udir = nil
    if uid == 0 then
        return nil
    end
    -- local dir configuration
    local pwd = os.getenv('PWD')
    udir = pwd and pwd .. '/.tarantoolctl'
    udir = udir and fio.stat(udir) and udir or nil
    -- or home dir configuration
    local homedir = os.getenv('HOME')
    udir = udir or homedir and homedir .. '/.config/tarantool/tarantool'
    udir = udir and fio.stat(udir) and udir or nil
    -- if one of previous is not nil
    if udir ~= nil then
        usermode = true
        return udir
    end

    return nil
end

--
-- Find if we're running under a user, and this user has a default file in his
-- home directory. If present, use it. Otherwise assume a system-wide default.
-- If it's missing, it's OK as well.
--
local function find_default_file()
    -- try to find local dir or user config
    local user_level = check_user_level()
    if user_level ~= nil then
        return user_level
    end

    -- no user-level defaults, use a system-wide one
    local cfg = '/etc/default/tarantool'
    if fio.stat(cfg) then
        return cfg
    end
    -- It's OK if there is no default file - load_default_file() will assume
    -- some defaults
    return nil
end

local function check_file(path)
    local rv, err = loadfile(path)
    if rv == nil then
        log.error("%s", debug.traceback())
        log.error("Failed to check instance file '%s'", err)
        return err
    end
    return nil
end

--
-- System-wide default file may be missing, this is OK, we'll assume built-in
-- defaults
-- It uses sandboxing for isolation. It's not completely safe, but it won't
-- allow a pollution of global variables
--
local function load_default_file(default_file)
    if default_file then
        local env = setmetatable({}, { __index = _G })
        local ufunc, msg = loadfile(default_file)
        -- if load fails - show last 10 lines of the log file
        if not ufunc then
            log.error("Failed to load defaults file: %s", msg)
        end
        debug.setfenv(ufunc, env)
        local state, msg = pcall(ufunc)
        if not state then
            log.error('Failed to execute defaults file: %s', msg)
        end
        default_cfg = env.default_cfg
        instance_dir = env.instance_dir
    end
    local d = default_cfg or {}

    d.pid_file  = d.pid_file  or "/var/run/tarantool"
    d.wal_dir   = d.wal_dir   or "/var/lib/tarantool"
    d.memtx_dir = d.memtx_dir or d.snap_dir or "/var/lib/tarantool"
    d.snap_dir  = nil
    d.log       = d.log       or d.logger or "/var/log/tarantool"
    d.logger    = nil
    d.vinyl_dir = d.vinyl_dir or "/var/lib/tarantool"

    d.pid_file  = fio.pathjoin(d.pid_file,  instance_name .. '.pid')
    d.wal_dir   = fio.pathjoin(d.wal_dir,   instance_name)
    d.memtx_dir = fio.pathjoin(d.memtx_dir, instance_name)
    d.vinyl_dir = fio.pathjoin(d.vinyl_dir, instance_name)
    d.log       = fio.pathjoin(d.log,       instance_name .. '.log')

    default_cfg = d

    if not usermode then
        -- change user name only if not running locally
        d.username = d.username or "tarantool"
        -- instance_dir must be set in the defaults file, but don't try to set
        -- it to the  global instance dir if the user-local defaults file is in
        -- use
        instance_dir = instance_dir or '/etc/tarantool/instances.enabled'
        -- get user data
        local user_data = ffi.C.getpwnam(ffi.cast('const char*', d.username))
        if user_data == nil then
            log.error('Unknown user: %s', d.username)
            os.exit(1)
        end

        -- get group data
        local group = ffi.C.getgrgid(user_data.pw_gid)
        if group == nil then
            log.error('Group lookup by gid failed: %d', user_data.pw_gid)
            os.exit(1)
        end
        group_name = ffi.string(group.gr_name)
    end

    if instance_dir == nil then
        log.error('Instance directory (instance_dir) is not set in %s', default_file)
        os.exit(1)
    end

    if not fio.stat(instance_dir) then
        log.error('Instance directory %s does not exist', instance_dir)
        os.exit(1)
    end
end

--
-- In case there is no explicit instance name, check whether arg[0] is a
-- symlink. In that case, the name of the symlink is the instance name.
--
local function find_instance_name(arg0, arg2)
    if arg2 ~= nil then
        return fio.basename(arg2, '.lua')
    end
    local istat = fio.lstat(arg0)
    if istat == nil then
        log.error("Can't stat %s: %s", arg0, errno.strerror())
        os.exit(1)
    end
    if not istat:is_link() then usage(command_name) end
    arg[2] = arg0
    linkmode = true
    return fio.basename(arg0, '.lua')
end

local function mkdir(dirname)
    log.info("mkdir %s", dirname)
    if not fio.mkdir(dirname, tonumber('0750', 8)) then
        log.error("Can't mkdir %s: %s", dirname, errno.strerror())
        os.exit(1)
    end

    if not usermode and
       not fio.chown(dirname, default_cfg.username, group_name) then
        log.error("Can't chown(%s, %s, %s): %s", default_cfg.username,
                  group_name, dirname, errno.strerror())
    end
end

local function read_file(filename)
    local file = fio.open(filename, {'O_RDONLY'})
    if file == nil then
        return nil, errno.strerror()
    end

    local buf = {}
    local i = 1
    while true do
        buf[i] = file:read(1024)
        if buf[i] == nil then
            return nil, errno.strerror()
        elseif buf[i] == '' then
            break
        end
        i = i + 1
    end
    return table.concat(buf)
end

-- Removes leading and trailing whitespaces
local function string_trim(str)
    return str:gsub("^%s*(.-)%s*$", "%1")
end

local function logger_parse(logger)
    -- syslog
    if logger:find("syslog:") then
        logger = string_trim(logger:sub(8))
        local args = {}
        logger:gsub("([^,]+)", function(keyval)
            keyval:gsub("([^=]+)=([^=]+)", function(key, val)
                args[key] = val
            end)
        end)
        return 'syslog', args
    -- pipes
    elseif logger:find("pipe:")   then
        logger = string_trim(logger:sub(6))
        return 'pipe', logger
    elseif logger:find("|")       then
        logger = string_trim(logger:sub(2))
        return 'pipe', logger
    -- files
    elseif logger:find("file:")   then
        logger = string_trim(logger:sub(6))
        return 'file', logger
    else
        logger = string_trim(logger)
        return 'file', logger
    end
end

local function mk_default_dirs(cfg)
    local init_dirs = {
        fio.dirname(cfg.pid_file),
        cfg.wal_dir,
        cfg.snap_dir,
        cfg.vinyl_dir,
    }
    local log_type, log_args = logger_parse(cfg.log)
    if log_type == 'file' then
        table.insert(init_dirs, fio.dirname(log_args))
    end
    for _, dir in ipairs(init_dirs) do
        if fio.stat(dir) == nil then
            mkdir(dir)
        end
    end
end

-- -------------------------------------------------------------------------- --
--                            CAT command helpers                             --
-- -------------------------------------------------------------------------- --

local function find_space(sid, spaces)
    if type(spaces) == 'number' then
        return sid == spaces
    end
    local shown = false
    for _, v in ipairs(spaces) do
        if v == sid then
            shown = true
            break
        end
    end
    return shown
end

local write_lua_table = nil

-- escaped string will be written
local function write_lua_string(string)
    io.stdout:write("'")
    local pos, byte = 1, string:byte(1)
    while byte ~= nil do
        io.stdout:write(("\\x%x"):format(byte))
        pos = pos + 1
        byte = string:byte(pos)
    end
    io.stdout:write("'")
end

local function write_lua_value(value)
    if type(value) == 'string' then
        write_lua_string(value)
    elseif type(value) == 'table' then
        write_lua_table(value)
    else
        io.stdout:write(tostring(value))
    end
end

local function write_lua_fieldpair(key, val)
    io.stdout:write("[")
    write_lua_value(key)
    io.stdout:write("] = ")
    write_lua_value(val)
end

write_lua_table = function(tuple)
    io.stdout:write('{')
    local is_begin = true
    for key, val in pairs(tuple) do
        if is_begin == false then
            io.stdout:write(', ')
        else
            is_begin = false
        end
        write_lua_fieldpair(key, val)
    end
    io.stdout:write('}')
end

local function cat_lua_cb(record)
    io.stdout:write(('box.space[%d]'):format(record.BODY.space_id))
    local op = record.HEADER.type:lower()
    io.stdout:write((':%s('):format(op))
    if op == 'insert' or op == 'replace' then
        write_lua_table(record.BODY.tuple)
    elseif op == 'delete' then
        write_lua_table(record.BODY.key)
    elseif op == 'update' then
        write_lua_table(record.BODY.key)
        io.stdout:write(', ')
        write_lua_table(record.BODY.tuple)
    elseif op == 'upsert' then
        write_lua_table(record.BODY.tuple)
        io.stdout:write(', ')
        write_lua_table(record.BODY.operations)
    end
    io.stdout:write(')\n')
end

local function cat_yaml_cb(record)
    print(yaml.encode(record):sub(1, -6))
end

local function cat_json_cb(record)
    print(json.encode(record))
end

local cat_formats = setmetatable({
    yaml = cat_yaml_cb,
    json = cat_json_cb,
    lua  = cat_lua_cb,
}, {
    __index = function(self, cmd)
        error(("Unknown formatter '%s'"):format(cmd))
    end
})

-- -------------------------------------------------------------------------- --
--                               Commands                                     --
-- -------------------------------------------------------------------------- --
local orig_cfg = box.cfg

local function wrapper_cfg(cfg)
    for i, v in pairs(default_cfg) do
        if cfg[i] == nil then
            cfg[i] = v
        end
    end
    -- force these startup options
    cfg.pid_file = default_cfg.pid_file
    if os.getenv('USER') ~= default_cfg.username then
        cfg.username = default_cfg.username
    else
        cfg.username = nil
    end
    if cfg.background == nil then
        cfg.background = true
    end

    mk_default_dirs(cfg)
    local success, data = pcall(orig_cfg, cfg)
    if not success then
        log.error("Configuration failed: %s", data)
        if type(cfg) ~= 'function' then
            local log_type, log_args = logger_parse(cfg.log)
            if log_type == 'file' and fio.stat(log_args) then
                os.execute('tail -n 10 ' .. log_args)
            end
        end
        os.exit(1)
    end

    fiber.name(instance_name)
    log.info('Run console at %s', console_sock)
    console.listen(console_sock)
    -- gh-1293: members of `tarantool` group should be able to do `enter`
    local console_sock = uri.parse(console_sock).service
    local mode = '0664'
    if not fio.chmod(console_sock, tonumber(mode, 8)) then
        log.error("Can't chmod(%s, %s) [%d]: %s", console_sock, mode, errno(),
                  errno.strerror())
    end

    return data
end

-- It's not 100% result guaranteed function, but it's ok for most cases
-- Won't help in multiple race-conditions
-- Returns nil if tarantool already started, PID otherwise
local function start_check()
    local pid_file = default_cfg.pid_file

    local fh = fio.open(pid_file, 'O_RDONLY')
    if fh == nil then
        return nil
    end

    local pid = tonumber(fh:read(64))
    fh:close()

    if pid == nil or (ffi.C.kill(pid, 0) < 0 and errno() == errno.ESRCH) then
        return nil
    end
    return pid
end

local function start()
    log.info("Starting instance...")
    local stat = check_file(instance_path)
    if stat ~= nil then
        log.error("Error, while checking syntax: halting")
        os.exit(1)
    end
    local pid = start_check()
    if pid then
        log.error("The daemon is already running: PID %s", pid)
        os.exit(1)
    end
    box.cfg = wrapper_cfg
    require('title').update{
        script_name = instance_path,
        __defer_update = true
    }
    shift_argv(arg, 0, 2)
    local success, data = pcall(dofile, instance_path)
    -- if load fails - show last 10 lines of the log file and exit
    if not success then
        log.error("Start failed: %s", data)
        if type(box.cfg) ~= 'function' then
            local log_type, log_args = logger_parse(box.cfg.log)
            if log_type == 'file' and fio.stat(log_args) then
                os.execute('tail -n 10 ' .. log_args)
            end
        end
        os.exit(1)
    end
    return 0
end

local function stop()
    local pid_file = default_cfg.pid_file

    local function base_stop()
        log.info("Stopping instance...")
        if fio.stat(pid_file) == nil then
            log.error("Process is not running (pid: %s)", pid_file)
            return 0
        end

        local f = fio.open(pid_file, 'O_RDONLY')
        if f == nil then
            log.error("Can't read pid file %s: %s", pid_file, errno.strerror())
            return 1
        end

        local pid = tonumber(f:read(64))
        f:close()

        if pid == nil or pid <= 0 then
            log.error("Broken pid file %s", pid_file)
            return 1
        end

        if ffi.C.kill(pid, 15) < 0 then
            log.error("Can't kill process %d: %s", pid, errno.strerror())
            return 1
        end

        return 0
    end

    local rv = base_stop()
    if fio.stat(pid_file) then
        fio.unlink(pid_file)
    end
    local console_sock = uri.parse(console_sock).service
    if fio.stat(console_sock) then
        fio.unlink(console_sock)
    end
    return rv
end

local function check()
    local rv = check_file(instance_path)
    if rv ~= nil then
        return 1
    end
    log.info("File '%s' is OK", instance_path)
    return 0
end

local function restart()
    local stat = check_file(instance_path)
    if stat ~= nil then
        log.error("Error, while checking syntax: halting")
        return 1
    end
    stop()
    fiber.sleep(1)
    start()
    return 0
end

local function logrotate()
    local console_sock = uri.parse(console_sock).service
    if fio.stat(console_sock) == nil then
        -- process is not running, do nothing
        return 0
    end

    local s = socket.tcp_connect('unix/', console_sock)
    if s == nil then
        -- socket is not opened, do nothing
        return 0
    end

    s:write[[
        require('log'):rotate()
        require('log').info("Rotate log file")
    ]]

    s:read({ '[.][.][.]' }, 2)

    return 0
end

local function enter()
    local console_sock_path = uri.parse(console_sock).service
    if fio.stat(console_sock_path) == nil then
        log.error("Can't connect to %s (%s)", console_sock_path, errno.strerror())
        if not usermode and errno() == errno.EACCES then
            log.error("Please add $USER to group '%s': usermod -a -G %s $USER",
                      group_name, group_name)
        end
        return 1
    end

    local cmd = string.format(
        "require('console').connect('%s', { connect_timeout = %s })",
        console_sock, TIMEOUT_INFINITY
    )

    console.on_start(function(self) self:eval(cmd) end)
    console.on_client_disconnect(function(self) self.running = false end)
    console.start()
    return 0
end

local function stdin_isatty()
    return ffi.C.isatty(0) == 1
end

local function execute_remote(uri, code)
    local remote = netbox.connect(uri, {
        console = true, connect_timeout = TIMEOUT_INFINITY
    })
    if remote == nil then
        return nil
    end
    return true, remote:eval(code)
end

local function connect()
    if not stdin_isatty() then
        local code = io.stdin:read("*a")
        if code == nil then
            usage(command_name)
            return 1
        end
        local status, full_response = execute_remote(arg[2], code)
        if not status then
            log.error("failed to connect to tarantool")
            return 2
        end
        local error_response = yaml.decode(full_response)[1]
        if type(error_response) == 'table' and error_response.error then
            log.error("Error, while executing remote command:")
            log.error(error_response.error)
            return 3
        end
        print(full_response)
        return 0
    end
    -- Otherwise we're starting console
    console.on_start(function(self)
        local status, reason
        status, reason = pcall(function()
            require('console').connect(arg[2], {
                connect_timeout = TIMEOUT_INFINITY
            })
        end)
        if not status then
            self:print(reason)
            self.running = false
        end
    end)
    console.on_client_disconnect(function(self) self.running = false end)
    console.start()
    return 0
end

local function status()
    local pid_file = default_cfg.pid_file
    local console_sock = uri.parse(console_sock).service

    if fio.stat(pid_file) == nil then
        if errno() == errno.ENOENT then
            log.info('%s is stopped (pid file does not exist)', instance_name)
            return 1
        end
        log.error("Can't access pidfile %s: %s", pid_file, errno.strerror())
    end

    if fio.stat(console_sock) == nil and errno() == errno.ENOENT then
        log.error("Pid file exists, but the control socket (%s) doesn't",
                  console_sock)
        return 2
    end

    local s = socket.tcp_connect('unix/', console_sock)
    if s == nil then
        if errno() ~= errno.EACCES then
            log.warn("Can't access control socket '%s' [%d]: %s", console_sock,
                errno(), errno.strerror())
            return 2
        end
        return 0
    end

    s:close()
    log.info('%s is running (pid: %s)', instance_name, default_cfg.pid_file)
    return 0
end

local function eval()
    local console_sock_path = uri.parse(console_sock).service
    local filename = arg[3]
    local code = nil
    if filename == nil then
        if stdin_isatty() then
            log.error("Usage:")
            log.error(" - tarantoolctl eval instance_name file.lua")
            log.error(" - <command> | tarantoolctl eval instance_name")
            return 1
        end
        code = io.stdin:read("*a")
    else
        local err
        code, err = read_file(filename)
        if code == nil then
            log.error("%s: %s", filename, err)
            return 2
        end
    end

    assert(code ~= nil, "Check that we've successfully loaded file")

    if fio.stat(console_sock_path) == nil then
        log.warn("pid file exists, but the control socket (%s) doesn't",
                 console_sock_path)
        return 2
    end

    local status, full_response = execute_remote(console_sock, code)
    if status == false then
        log.error("control socket exists, but tarantool doesn't listen on it")
        return 2
    end
    local error_response = yaml.decode(full_response)[1]
    if type(error_response) == 'table' and error_response.error then
        log.error("Error, while reloading config:")
        log.error(error_response.error)
        return 3
    end

    print(full_response)
    return 0
end

local function cat()
    local options = keyword_arguments
    local from, to, spaces = options.from, options.to, options.space
    local show_system, cat_format = options['show-system'], options.format

    local format_cb   = cat_formats[cat_format]
    local is_printed  = false
    for id, file in ipairs(positional_arguments) do
        log.error("Processing file '%s'", file)
        for lsn, record in xlog.pairs(file) do
            local sid = record.BODY.space_id
            local is_filtered    = spaces ~= nil
            local is_system      = sid < 512 and show_system == false
            local isnt_specified = not (is_filtered and find_space(sid, spaces))
            if (lsn < from) or
               (is_filtered and is_system and isnt_specified) or
               (is_system and isnt_specified) then
                -- pass this tuple
            elseif lsn >= to then
                -- stop, as we've had finished reading tuple with lsn == to
                -- and next lsn's will be bigger
                break
            else
                is_printed = true
                format_cb(record)
                io.stdout:flush()
            end
        end
        if options.format == 'yaml' and is_printed then
            is_printed = false
            print('...\n')
        end
    end
end

local function play()
    local options = keyword_arguments
    local from, to, spaces = options.from, options.to, options.space
    local show_system = options['show-system']
    local uri = table.remove(positional_arguments, 1)

    if uri == nil then
        error("Empty URI is provided")
    end
    local remote = netbox.new(uri)
    if not remote:wait_connected() then
        error(("Error, while connecting to host '%s'"):format(uri))
    end
    for id, file in ipairs(positional_arguments) do
        log.info(("Processing file '%s'"):format(file))
        for lsn, record in xlog.pairs(file) do
            local sid = record.BODY.space_id
            local is_filtered    = spaces ~= nil
            local is_system      = sid < 512 and show_system == false
            local isnt_specified = not (is_filtered and find_space(sid, spaces))
            if (lsn < from) or
               (is_filtered and is_system and isnt_specified) or
               (is_system and isnt_specified) then
                -- pass this tuple
            elseif lsn >= to then
                -- stop, as we've had finished reading tuple with lsn == to
                -- and next lsn's will be bigger
                break
            else
                local args, so = {}, remote.space[sid]
                if so == nil then
                    error(("No space #%s, stopping"):format(sid))
                end
                table.insert(args, so)
                table.insert(args, record.BODY.key)
                table.insert(args, record.BODY.tuple)
                table.insert(args, record.BODY.operations)
                so[record.HEADER.type:lower()](unpack(args))
            end
        end
    end
    remote:close()
end

local function exit_wrapper(func)
    return function() os.exit(func()) end
end

local function process_remote(cmd_function)
    cmd_function()
end

local function process_local(cmd_function)
    instance_name = find_instance_name(arg[0], arg[2])

    default_file = find_default_file()
    load_default_file(default_file)

    if #arg < 2 then
        log.error("Not enough arguments for '%s' command", command_name)
        usage(command_name)
    end

    instance_path = fio.pathjoin(instance_dir, instance_name .. '.lua')

    if not fio.stat(instance_path) then
        log.error('Instance %s is not found in %s', instance_name, instance_dir)
        os.exit(1)
    end

    -- create a path to the control socket (admin console)
    console_sock = instance_name .. '.control'
    console_sock = fio.pathjoin(fio.dirname(default_cfg.pid_file), console_sock)
    console_sock = 'unix/:' .. console_sock

    cmd_function()
end

local commands = setmetatable({
    start = {
        func = start, process = process_local, help = {
            header = "%s start <instance_name>",
            linkmode = "%s start",
            description =
[=[
        [cluster command] start Tarantool instance if it's not already started.
        Tarantool instance should be maintained using tarantoolctl only.
]=],
            weight = 10,
            deprecated = false,
        }
    }, stop = {
        func = exit_wrapper(stop), process = process_local, help = {
            header = "%s stop <instance_name>",
            linkmode = "%s stop",
            description =
[=[
        [cluster command] stop Tarantool instance if it's not already stopped.
]=],
            weight = 20,
            deprecated = false,
        }
    }, logrotate = {
        func = exit_wrapper(logrotate), process = process_local, help = {
            header = "%s logrotate <instance_name>",
            linkmode = "%s logrotate",
            description =
[=[
        [cluster command] rotate log of started Tarantool instance. Works only
        if logging is set into file. Pipe/Syslog aren't supported.
]=],
            weight = 50,
            deprecated = false,
        }
    }, status = {
        func = exit_wrapper(status), process = process_local, help = {
            header = "%s status <instance_name>",
            linkmode = "%s status",
            description =
[=[
        [cluster command] show status of Tarantool instance. (started/stopped)
]=],
            weight = 30,
            deprecated = false,
        }
    }, enter = {
        func = exit_wrapper(enter), process = process_local, help = {
            header = "%s enter <instance_name>",
            linkmode = "%s enter",
            description =
[=[
        [cluster command] enter interactive Lua console of instance.
]=],
            weight = 65,
            deprecated = false,
        }
    }, restart = {
        func = restart, process = process_local, help = {
            header = "%s restart <instance_name>",
            linkmode = "%s restart",
            description =
[=[
        [cluster command] stop and start Tarantool instance (if it's already
        started, fail otherwise)
]=],
            weight = 40,
            deprecated = false,
        }
    }, reload = {
        func = exit_wrapper(eval), process = process_local, help = {
            header = "%s reload <instance_name> <lua_file>",
            linkmode = "%s reload <lua_file>",
            description =
[=[
        DEPRECATED in favor of "eval"
]=],
            weight = 0,
            deprecated = true,
        }
    }, eval = {
        func = exit_wrapper(eval), process = process_local, help = {
            header = {
                "%s eval <instance_name> <lua_file>",
                "<command> | %s eval <instance_name>"
            },
            linkmode = {
                "%s eval <lua_file>",
                "<command> | %s eval"
            },
            description =
[=[
        [cluster command] evaluate local file on Tarantool instance (if it's
        already started, fail otherwise)
]=],
            weight = 70,
            deprecated = false,
        }
    }, check = {
        func = exit_wrapper(check), process = process_local, help = {
            header = "%s check <instance_name>",
            linkmode = "%s check",
            description =
[=[
        [cluster command] Check instance script for syntax errors
]=],
            weight = 60,
            deprecated = false,
        }
    }, connect = {
        func = exit_wrapper(connect), process = process_remote, help = {
            header = {
                "%s connect <instance_uri>",
                "<command> | %s connect <instance_uri>"
            },
            description =
[=[
        Connect to Tarantool instance on admin/console port.
        Supports both TCP/Unix sockets.
]=],
            weight = 80,
            deprecated = false,
        }
    }, cat = {
        func = exit_wrapper(cat), process = process_remote, help = {
            header =
                "%s cat <filename>.. [--space=space_no ..] [--show-system]" ..
                " [--from=from_lsn] [--to=to_lsn]",
            description =
[=[
        Show contents of snapshot/xlog files.

        * If --space=space_no is passed, then output'll be filtered by space
          number. May be passed more than once.
        * If --show-system is passed, then contents of system space'll be showed.
        * If --from=from_lsn is present, then show operations starting from
          given lsn.
        * If --to=to_lsn is present, then show operations ending with given lsn.

        Result is printed to stdout
]=],
            weight = 90,
            deprecated = false,
        }
    }, play = {
        func = exit_wrapper(play), process = process_remote, help = {
            header =
                "%s play <instance_uri> <filename>.. [--space=space_no ..]" ..
                " [--show-system] [--from=from_lsn] [--to=to_lsn]",
            description =
[=[
        Play contents of snapshot/xlog files to another Tarantool service.

        * If --space=space_no is passed, then output'll be filtered by space
          number. May be passed more than once.
        * If --show-system is passed, then contents of system space'll be showed.
        * If --from=from_lsn is present, then show operations starting from
          given lsn.
        * If --to=to_lsn is present, then show operations ending with given lsn.
]=],
            weight = 100,
            deprecated = false,
        }
    }
}, {
    __index = function()
        log.error("Unknown command '%s'", command_name)
        usage()
    end
})

local function usage_command(name, cmd)
    local header = cmd.help.header
    if linkmode then
        header = cmd.help.linkmode
    end
    if type(header) == 'string' then
        header = { header }
    end
    for no, line in ipairs(header) do
        log.error("    " .. line, name)
    end
end

local function usage_header()
    log.error("Tarantool client utility (%s)", _TARANTOOL)
end

usage = function(command, verbose)
    local name = fio.basename(arg[0])
    do -- in case of command is passed and is valid command
        local command_struct = rawget(commands, command)
        if command ~= nil and command_struct then
            log.error("Usage:\n")
            usage_command(name, command_struct, true)
            log.error("")
            log.error(command_struct.help.description)
            os.exit(1)
        end
    end -- do this otherwise
    usage_header()
    if default_file ~= nil then
        log.error("Config file: %s", default_file)
    end
    log.error("")
    log.error("Usage:")
    local names = fun.iter(commands):map(
        function(name, cmd) return {name, cmd.help.weight or 0} end
    ):totable()
    table.sort(names, function(left, right) return left[2] < right[2] end)
    for _, cmd_name in ipairs(names) do
        local cmd = commands[cmd_name[1]]
        if cmd.help.deprecated ~= true then
            usage_command(name, cmd, false)
            if verbose then
                log.error("")
                log.error(cmd.help.description)
            end
        end
    end
    os.exit(1)
end
-- parse parameters and put result into positional/keyword_arguments
do
    local function keyword_arguments_populate(ka)
        ka                = ka                or {}
        ka.from           = ka.from           or 0
        ka.to             = ka.to             or -1ULL
        ka['show-system'] = ka['show-system'] or false
        ka.format         = ka.format         or 'yaml'
        return ka
    end

    -- returns command name, file list and named parameters
    local function parameters_parse(parameters)
        local command_name = table.remove(parameters, 1)
        local positional_arguments, keyword_arguments = {}, {}
        for k, v in pairs(parameters) do
            if type(k) == 'number' then
                positional_arguments[k] = v
            else
                keyword_arguments[k] = v
            end
        end
        return command_name, positional_arguments, keyword_arguments
    end

    local parameters = argparse(arg, {
        { 'space',       'number+' },
        { 'show-system', 'boolean' },
        { 'from',        'number'  },
        { 'to',          'number'  },
        { 'help',        'boolean' },
        { 'format',      'string'  }
    })

    local cmd_name
    cmd_name, positional_arguments, keyword_arguments = parameters_parse(parameters)
    if cmd_name == 'help' or parameters.help == true or #arg < 1 then
        usage(cmd_name, true)
    end
    keyword_arguments = keyword_arguments_populate(parameters)
end

local cmd_pair = commands[command_name]
if #arg < 2 then
    log.error("Error: not enough arguments for '%s' command\n", command_name)
    usage(command_name)
end
cmd_pair.process(cmd_pair.func)

-- vim: syntax=lua
