#!/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",

        -- sophia_dir/instance/
        sophia_dir  =   "/var/lib/tarantool/sophia",

        -- logger/instance .. '.log'
        logger      =   "/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 fio = require 'fio'
local log = require 'log'
local errno = require 'errno'
local yaml = require 'yaml'
local console = require 'console'
local socket = require 'socket'
local ffi = require 'ffi'
local os = require 'os'
local fiber = require 'fiber'

ffi.cdef[[ int kill(int pid, int sig); ]]


local available_commands = {
    'start',
    'stop',
    'logrotate',
    'status',
    'enter',
    'restart'
}

local function usage()
    log.error("Usage: %s {%s} instance_name",
        arg[0], table.concat(available_commands, '|'))
    os.exit(1)
end

-- 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 cmd = arg[1]

local valid_cmd = false
for _, vcmd in pairs(available_commands) do
    if cmd == vcmd then
        valid_cmd = true
        break
    end
end

if not valid_cmd then
    usage()
end

local instance
if arg[2] == nil then
    local istat = fio.lstat(arg[0])
    if istat == nil then
        log.error("Can't stat %s: %s", arg[0], errno.strerror())
        os.exit(1)
    end
    if not istat:is_link() then
        usage()
    end
    instance = fio.basename(arg[0], '.lua')
    arg[2] = instance
else
    instance = fio.basename(arg[2], '.lua')
end

shift_argv(arg, 0, 2)

if fio.stat('/etc/sysconfig/tarantool') then
    dofile('/etc/sysconfig/tarantool')
elseif fio.stat('/etc/default/tarantool') then
    dofile('/etc/default/tarantool')
end

if default_cfg == nil then
    default_cfg = {}
end

if instance_dir == nil then
    instance_dir = '/etc/tarantool/instances.enabled'
end

default_cfg.pid_file   = default_cfg.pid_file and default_cfg.pid_file or "/var/run/tarantool"
default_cfg.wal_dir    = default_cfg.wal_dir and default_cfg.wal_dir or "/var/lib/tarantool"
default_cfg.snap_dir   = default_cfg.snap_dir and default_cfg.snap_dir or "/var/lib/tarantool"
default_cfg.sophia_dir = default_cfg.sophia_dir and default_cfg.sophia_dir or "/var/lib/tarantool"
default_cfg.logger     = default_cfg.logger and default_cfg.logger or "/var/log/tarantool"
default_cfg.username   = default_cfg.username and default_cfg.username or "tarantool"

-- create  a path to the control socket (admin console)
local console_sock = fio.pathjoin(default_cfg.pid_file, instance .. '.control')

default_cfg.pid_file   = fio.pathjoin(default_cfg.pid_file, instance .. '.pid')
default_cfg.wal_dir    = fio.pathjoin(default_cfg.wal_dir, instance)
default_cfg.snap_dir   = fio.pathjoin(default_cfg.snap_dir, instance)
default_cfg.sophia_dir = fio.pathjoin(default_cfg.sophia_dir, instance, 'sophia')
default_cfg.logger     = fio.pathjoin(default_cfg.logger, instance .. '.log')

local instance_lua = fio.pathjoin(instance_dir, instance .. '.lua')

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

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

function mk_default_dirs(cfg)
    -- create pid_dir
    pid_dir = fio.dirname(cfg.pid_file)
    if fio.stat(pid_dir) == nil then
        mkdir(pid_dir)
    end
    -- create wal_dir
    if fio.stat(cfg.wal_dir) == nil then
        mkdir(cfg.wal_dir)
    end
    -- create snap_dir
    if fio.stat(cfg.snap_dir) == nil then
        mkdir(cfg.snap_dir)
    end
    -- create sophia_dir
    if fio.stat(cfg.sophia_dir) == nil then
        mkdir(cfg.sophia_dir)
    end
    -- create log_dir
    log_dir = fio.dirname(cfg.logger)
    if log_dir:find('|') == nil and fio.stat(log_dir) == nil then
        mkdir(log_dir)
    end
end

local force_cfg = {
    pid_file    = default_cfg.pid_file,
    username    = default_cfg.username,
    background  = true,
    custom_proc_title = instance
}

local orig_cfg = box.cfg
wrapper_cfg = function(cfg)

    for i, v in pairs(force_cfg) do
        cfg[i] = v
    end

    for i, v in pairs(default_cfg) do
        if cfg[i] == nil then
            cfg[i] = v
        end
    end

    mk_default_dirs(cfg)
    local res = orig_cfg(cfg)

    require('fiber').name(instance)
    log.info('Run console at %s', console_sock)
    console.listen(console_sock)

    return res
end

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

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

    local str = f:read(64)
    f:close()

    local pid = tonumber(str)

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

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

function start()
    log.info("Starting instance...")
    box.cfg = wrapper_cfg
    dofile(instance_lua)
end


if cmd == 'start' then
    start()

elseif cmd == 'stop' then
    os.exit(stop())

elseif cmd == 'restart' then
    stop()
    fiber.sleep(1)
    start()

elseif cmd == 'logrotate' then
    if fio.stat(console_sock) == nil then
        -- process is not running, do nothing
        os.exit(0)
    end

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

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

    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)
elseif cmd == 'status' then
    if fio.stat(force_cfg.pid_file) == nil then
        if errno() == errno.ENOENT then
            os.exit(1)
        end
        log.error("Cant access pidfile %s: %s",
            force_cfg.pid_file, errno.strerror())
    end

    if fio.stat(console_sock) == nil then
        if errno() == errno.ENOENT then
            log.warn("pidfile is exists, but control socket (%s) isn't",
                console_sock)
            os.exit(2)
        end
    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: %s", console_sock,
                errno.strerror())
            os.exit(3)
        else
            os.exit(0)
        end
    end

    s:close()
    os.exit(0)
else
    log.error("Unknown command '%s'", cmd)
    os.exit(-1)
end

-- vim: syntax=lua
