SMOLNET PORTAL home about changes
#!/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: 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

-- ***********************************************************************
.
Response: text/plain
Original URLgopher://gopher.conman.org/0Obsolete%3AGopher%3ASrc%3Agopher.lua
Content-Typetext/plain; charset=utf-8