#!/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 . -- -- Comments, questions and criticisms can be sent to: sean@conman.org -- -- ======================================================================= -- -- 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 -- *********************************************************************** .