#!/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
-- ***********************************************************************