#!/usr/bin/env lua
-- ***********************************************************************
--
-- Copyright 2016 by Sean Conner.
--
-- This program is free software: you can redistribute it and/or modify it
-- under the terms of the GNU General Public License as published by the
-- Free Software Foundation, either version 3 of the License, or (at your
-- option) any later version.
--
-- This program is distributed in the hope that it will be useful, but
-- WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
-- Public License for more details.
--
-- You should have received a copy of the GNU General Public License along
-- with this program.  If not, see <http://www.gnu.org/licenses/>.
--
-- Comments, questions and criticisms can be sent to: [email protected]
--
-- =======================================================================
--
-- Main entry point to the Gopher Daemon.  This daemon creates N number of
-- server processes, where N is the number of cores in the system.  Each
-- server process will then accept connections, and fork a handler processor
-- (the 90s called---they want their forking daemons back) and while this is
-- frowned upon these days, I don't think the traffic bears a more modern
-- event driven architecture (and if it does---well, that's a nice problem
-- to have).
--
-- The default values for configuration is below.  All of these can be
-- overridden in the configuration file.
--
-- ***********************************************************************
-- luacheck: globals config blog handler
-- luacheck: ignore 611

config =
{
 interface =
 {
   address  = '0.0.0.0',
   hostname = 'lucy.roswell.area51',
   port     = 'gopher',
 },

 syslog =
 {
   id       = 'gopher',
   facility = 'daemon',
 },

 user =
 {
   uid = 'gopher',
   gid = 'gopher',
 },

 bible =
 {
   books  = "thebooks",
   verses = "books",
 },

 movie = "/home/spc/LINUS/source/play/plotdriver/plotdriver.cnf",

 files  = "/home/spc/source/gopher-blog/share",
 blog   = "/home/spc/web/boston/journal/blog.conf",
 quotes = "/home/spc/LINUS/quotes/quote -r",
}

local exit      = require "org.conman.const.exit"
local process   = require "org.conman.process"
local signal    = require "org.conman.signal"
local getopt    = require "org.conman.getopt"
local syslog    = require "org.conman.syslog"
local errno     = require "org.conman.errno"
local fsys      = require "org.conman.fsys"
local net       = require "org.conman.net"
local sys       = require "org.conman.sys"

local CHILDREN = {}
local SOCKET

-- ***********************************************************************
--
-- usage:       okay,child = create_server_process(socket)
--
-- desc:        Create a server process.
-- input:       socket (userdata/socket)
-- return:      okay (boolean) true if success, false othersise
--              child (integer) child pid, nil on error
-- ***********************************************************************

local function create_server_process(socket)
 -- -----------------------------------------------------------
 -- usage:     wait_for_it()
 -- desc:      accepts connections and forks a handler process
 -- notes:     infinite loop, never returns
 -- -----------------------------------------------------------

 local function wait_for_it()
   local connection,remote,err = socket:accept()
   if not connection then
     syslog('error',"failed connection: %s",errno[err])
     return wait_for_it()
   end

   local child,err = process.fork() -- luacheck: ignore
   if not child then
     syslog('error',"cannot create handler process: %s",errno[err])
     connection:close()
     return wait_for_it()
   end

   if child == 0 then
     socket:close()
     fsys.redirect(connection,io.stdin)
     fsys.redirect(connection,io.stdout)
     io.stdin:setvbuf('no')
     io.stdout:setvbuf('no')
     connection:close()
     signal.default('child')
     local _,msg = pcall(handler.main,remote)
     syslog('error',"handle_request = %s",msg)
     process.exit(exit.SOFTWARE) -- handle_request() should exit
   end

   connection:close()
   return wait_for_it()
 end

 -- ----------------------------------------------------------------

 local child,err = process.fork()
 if not child then
   syslog('critical',"cannot create server process: %s",errno[err])
   return false
 end

 if child > 0 then
   syslog('info',"created server process %d",child)
   return true,child
 end

 if not require "reapchild" then
   syslog('error',"unable to reap children")
   process.exit(exit.SOFTWARE)
 end

 signal.default('int')
 signal.default('term')
 wait_for_it()
end

-- ***********************************************************************
-- usage:       shut_down_server_processes()
-- desc:        Pretty much what it says on the box
-- ***********************************************************************

local function shut_down_server_processes()
 for pid in pairs(CHILDREN) do
   signal.raise('term',pid)
   local info,err = process.wait(pid)
   if not info then
     syslog('error',"process.wait(%d) = %s",pid,errno[err])
   else
     syslog('info',"server process %d stopped: %s",pid,info.description)
   end
 end
 process.exit(0)
end

-- ***********************************************************************
--
-- Main entry point---parse the command line, read the configuration file,
-- create the listening socket and set up signal handling.
--
-- ***********************************************************************

do
 local cfile = "gopher-config.lua"
 local usage = [[
usage: %s [options]
       -c | --config file      Configuration file (%s)
       -h | --help             This very text
]]

 local opts =
 {
   { 'c' , 'config' , true  , function(c) cfile = c end },
   { 'h' , 'help'   , false , function()
       io.stderr:write(string.format(usage,arg[0],cfile))
       os.exit(exit.USAGE)
     end
   }
 }

 getopt.getopt(arg,opts)

 do
   local f,err = loadfile(cfile,"t",config)
   if not f then
     syslog('critical',"%s: %s",cfile,err)
     os.exit(exit.CONFIG)
   end

   if _VERSION == "Lua 5.1" then
     setfenv(f,config)
   end

   f()
   package.loaded['CONFIG'] = config
 end

 syslog.open(config.syslog.id,config.syslog.facility)

 local addr = net.address(config.interface.address,'tcp',config.interface.port)
 config.interface.port = addr.port

 SOCKET = net.socket(addr.family,'tcp')
 SOCKET.reuseaddr = true
 local err = SOCKET:bind(addr)
 if err ~= 0 then
   syslog('critical',"cannot bind to interface: %s",errno[err])
   os.exit(exit.CANTCREATE)
 end

 if process.getuid() == 0 then
   local unix = require "org.conman.unix"
   local gid  = unix.groups[config.user.gid].gid
   local uid  = unix.users[config.user.uid].uid

   process.setgid(gid,gid,gid)
   process.setuid(uid,uid,uid)
   package.loaded['org.conman.unix'] = nil
   unix                              = nil -- luacheck: ignore
 end

 -- ----------------------------------------------------------------
 -- XXX - Unfortunately, due to the way the code is current written, the
 -- following two modules need to be globally visible.  I really need to
 -- fix this some day.
 --
 -- Also, because I know make the config a loaded modules, these need to be
 -- after that happens, not before.
 -- ----------------------------------------------------------------

 blog    = require "blog"
 handler = require "handler"

 blog.init()
 handler.init()

 signal.catch('int')
 signal.catch('term')
 signal.catch('child')
 SOCKET:listen()
end

-- ***********************************************************************
--
-- Main processing loop.  Create the server processes, then monitor them and
-- restart if required.
--
-- ***********************************************************************

for _ = 1 , sys.CORES do
 local okay,child = create_server_process(SOCKET)
 if okay then
   CHILDREN[child] = true
 end
end

while true do
 process.pause()

 if signal.caught('int') or signal.caught('term') then
   shut_down_server_processes()
   os.exit(exit.SUCCESS)

 elseif signal.caught('child') then
   local info,err = process.wait()

   if not info then
     if err ~= errno.ECHILD then
       syslog('error',"process.wait() = %s",errno[err])
     else
       syslog('error',"say what?")
     end

   else
     syslog('error',"server process: status=%s description=%s",info.status,info.description)
     syslog('notice',"restarting server process")
     CHILDREN[info.pid] = nil

     local okay,child = create_server_process(SOCKET)
     if okay then
       CHILDREN[child] = true
     end
   end
 end
end

-- ***********************************************************************