Created
September 4, 2025 07:40
-
-
Save sogaiu/86ab04cb443bd06436ec8ed712bc97c3 to your computer and use it in GitHub Desktop.
from llmII
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --[[ | |
| cqueues-servicekit - A daemonization API based upon ServiceKit | |
| Original ServiceKit can be found - | |
| https://github.com/LuaDist2/servicekit-posix | |
| License: | |
| Copyright (c) 2012 Aaron B. | |
| Copyright (c) 2019 LLMII <[email protected]> | |
| Permission is hereby granted, free of charge, to any person obtaining a | |
| copy of this software and associated documentation files (the | |
| "Software"), to deal in the Software without restriction, including | |
| without limitation the rights to use, copy, modify, merge, publish, | |
| distribute, sublicense, and/or sell copies of the Software, and to | |
| permit persons to whom the Software is furnished to do so, subject to | |
| the following conditions: | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
| IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
| CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
| TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
| SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| ]] | |
| --[[ | |
| Why: | |
| The old project was languishing, and the stuff from | |
| http://25thandclement.com/~william/projects/ is top notch. | |
| Since most things I've written utilize cqueues, its pretty much widely | |
| available with all my lua installations. Lunix has some benefits over | |
| the other lua posix modules in that it focuses on thread safety. | |
| As the need arose for a way to daemonize some software I wrote, I decided | |
| to look into how it was done in various languages, and what existed for | |
| lua and ServiceKit almost fit the bill but feeling it could be simplified | |
| a bit, and that some functionality could be added to it, I realized I'd | |
| come close to rewriting it. At that point, made sense to base it off of | |
| code I felt more confident in due to experience with that author's code in | |
| other various situations. This, cqueues-servicekit, is what is arrived | |
| at after having stripped away other dependencies, combined things into a | |
| single file (makes more sense that way so far ), and changed things to use | |
| cqueues and lunix. Hopefully others find this code as useful as I do, | |
| and find the improvements to be nice as well. | |
| ]] | |
| --[[ | |
| Dependencies: | |
| lunix, cqueues | |
| ]] | |
| --[[ | |
| Usage: | |
| (require 'servicekit') | |
| { | |
| settings = { | |
| -- all below is optional, if not used, do settings = {} | |
| change_priv = nil, -- if is a table, then privileges will be | |
| -- dropped and used for directory ownership if | |
| -- one is created for chroot purposes | |
| -- Table should be like: | |
| -- { user = 'nobody', group = 'nobody' } | |
| -- and if group is omitted, user will be used | |
| -- for group | |
| daemonize = true, -- default true, for foreground, set to false | |
| umask = nil, -- umask to change to | |
| singleton = false, -- keep from running multiple processes | |
| lock_file = 'proc.lf', -- name of lock_file to use for singleton | |
| directory = '/', -- change to default, otherwise, as specified | |
| chroot = false -- chroots to directory specified by this, or | |
| -- if true, to directory, otherwise, no chroot | |
| }, | |
| privileged_init = function() end, -- optional | |
| init = function() end, -- optional | |
| logging_init = function() end, -- optional | |
| start = function() end, -- mandatory | |
| stop = function() end, -- mandatory | |
| destroy = function() end, -- optional | |
| reload = function() end -- optional | |
| } | |
| TODO: Document event sequence! | |
| ]] | |
| --[[ | |
| Changelog: | |
| 5/24/2019 - | |
| Reworked a lot of the code to utilize cqueues and lunix. | |
| ]] | |
| --[[ | |
| BUGS/FEATURES: | |
| Need to double fork: see - http://code.activestate.com/recipes/278731/ - | |
| for why. | |
| Add umask capability, lock file (singleton process) capability. | |
| Clean up how the events work, make sure to check PID stuff in grandparent. | |
| Update useage | |
| Allow for setting different group? | |
| ]] | |
| local cqueues = require 'cqueues' | |
| local signal = require 'cqueues.signal' | |
| local unix = require 'unix' | |
| local sk = {} | |
| do -- settings def | |
| sk.settings = { | |
| change_priv = false, | |
| daemonize = true, | |
| umask = false, | |
| singleton = false, | |
| directory = '/', | |
| chroot = false, | |
| lock_file = 'proc.lf', | |
| } | |
| end -- end settings def | |
| do -- event def | |
| local function tnop() return true end | |
| sk.events = { | |
| privileged_init = tnop, -- optional | |
| init = tnop, -- optional | |
| logging_init = tnop, -- optional | |
| start = tnop, -- mandatory | |
| stop = tnop, -- mandatory | |
| destroy = tnop, -- optional | |
| reload = tnop, -- optional | |
| settings = sk.settings | |
| } | |
| end -- end event def | |
| -- the log basically wraps print and friends, or other provided | |
| -- log functions, wherein, until such is provided, it'll store the | |
| -- log data and output later, and if a fatal error occurs, will | |
| -- dump the errors via print anyway and halt waiting for logging functions | |
| local log = {} | |
| do | |
| local errt, inft, reset, _errf, _inff = {}, {}, false | |
| local function dump_log(fun, t) | |
| local f = type(fun) == 'function' and fun or print | |
| for i, v in pairs(t) do | |
| f(v) | |
| end | |
| end | |
| local function exit(ret) | |
| dump_log(_errf, errt) | |
| os.exit(ret) | |
| end | |
| function log.info(str) | |
| if not (_inff and reset) then | |
| table.insert(inft, str) | |
| else | |
| _inff(str) | |
| end | |
| end | |
| function log.error(str) | |
| if not (_errf and reset) then | |
| table.insert(errt, str) | |
| else | |
| _errt(str) | |
| end | |
| end | |
| function log.fatal(str, ret) | |
| log.error('FATAL: ' .. str) | |
| exit(ret or 1) | |
| end | |
| function log.setup(errf, inff) | |
| _errf, _inff = errf, inff | |
| dump_log(_errf, errt) | |
| dump_log(_inff, inft) | |
| end | |
| function log.reset() | |
| _errf, _inff, reset = nil, nil, true | |
| end | |
| end | |
| -- TODO: Document | |
| local utils = {} | |
| do -- utils defs | |
| function utils.assert(success, str) | |
| if not success then | |
| log.fatal(str) | |
| end | |
| end | |
| function utils.handle_fatal(success, es, ec, str) | |
| utils.assert(success, (str .. ' - errno <%s>, err: <%s>'):format(ec, es)) | |
| end | |
| -- change directory | |
| function utils.chdir(directory) | |
| local success, es, ec = unix.chdir(directory) | |
| utils.handle_fatal( | |
| success, es, ec, | |
| ('Failed to change to directory: %s'):format(directory) | |
| ) | |
| end | |
| utils.finished = false | |
| end -- end utils def | |
| local privileges = {} | |
| do -- privileges def | |
| local user, group | |
| function privileges.init(priv) | |
| if priv then | |
| local uinfo = unix.getpwnam(priv.user) | |
| user, group = uinfo.uid, uinfo.gid | |
| group = priv.group and unix.getpwnam(priv.group).gid or group | |
| end | |
| end | |
| function privileges.setup(priv) | |
| if priv then | |
| local success, es, ec = unix.setegid(group) | |
| utils.handle_fatal( | |
| success, es, ec, | |
| ('Failed to drop group privileges to %s'):format | |
| ( | |
| priv.group or priv.user | |
| ) | |
| ) | |
| local success, es, ec = unix.seteuid(user) | |
| utils.handle_fatal( | |
| success, es, ec, | |
| ('Failed to drop user privileges to %s'):format(priv.user) | |
| ) | |
| log.info( | |
| ('Dropped priviledges to <%s:%s>.'):format | |
| ( | |
| priv.user, priv.group or priv.user | |
| ) | |
| ) | |
| end | |
| end | |
| end -- end privileges def | |
| -- almost all functions here are designed to exit the process in the | |
| -- case of failure as it wouldn't result in a properly setup daemon if done | |
| -- incorrectly | |
| local daemonize = {} | |
| do -- daemonize def | |
| local null | |
| -- fork a child, exit the parent | |
| local function fork_and_suicide() | |
| local pid, es, ec = unix.fork() | |
| utils.handle_fatal(pid, es, ec, 'Failed to fork daemon') | |
| if pid > 0 then | |
| os.exit() | |
| end | |
| return pid | |
| end | |
| -- sets umask for the process | |
| local function umask(mask) | |
| success, es, ec = unix.umask(mask or unix.umask()) | |
| utils.handle_fatal( | |
| success, es, ec, | |
| ('Failed to set umask: %s'):format(mask or unix.umask()) | |
| ) | |
| end | |
| -- opens /dev/null | |
| local function open_null() | |
| local null, es, ec = unix.open('/dev/null') | |
| utils.handle_fatal(null, es, ec, 'Failed to open /dev/null') | |
| return null | |
| end | |
| -- assign the FD in file1 to the FD in file2 | |
| local function redirect(file1, file2) | |
| local success, es, ec = unix.dup2(file1, file2) | |
| utils.handle_fatal( | |
| success, es, ec, | |
| ('Cannot dup %s to %s'):format(file1, file2) | |
| ) | |
| end | |
| -- reassign stdio to null for daemonization purposes explained in _daemonize | |
| local function reset_stdio() | |
| redirect(null, 0) | |
| redirect(null, 1) | |
| redirect(null, 2) | |
| end | |
| -- detach the process from the controlling terminal and run it in the | |
| -- background as a daemon | |
| local function full_detach() | |
| -- Fork a child process returning control to the parent (likely a | |
| -- shell or terminal). Required to ensure a call to setsid is | |
| -- successful | |
| fork_and_suicide() | |
| -- Setsid to make this process the session leader and process group | |
| -- leader ensuring there is no controlling terminal. | |
| local success, es, ec = unix.setsid() | |
| utils.handle_fatal(success, es, ec, 'Failed to daemonize.') | |
| -- Fork to create grand-child, the parent of which exits immediately | |
| -- preventing zombie processes. With the grand-child orphaned, the process | |
| -- becomes owned by init, making it responsible for cleanup. The parent | |
| -- was capable of acquiring a controlling terminal by opening one, but | |
| -- this final fork guarantees this process is no longer a session leader, | |
| -- and is incapable of acquiring a controlling terminal. | |
| fork_and_suicide() | |
| end | |
| local function _daemonize(directory, mask) | |
| -- become a daemon | |
| -- Notes: maybe we should close all other file descriptors? probably | |
| -- would ruin the point of acquiring a socket with low port number or | |
| -- other root privileged resources however. | |
| full_detach() | |
| -- change directory, avoid disallowing dismounts by holding open | |
| -- a working directory outside of '/', unless directory is otherwise | |
| -- specified, in which case it could be a preperation for chroot or | |
| -- orchestrated differently and hopefully correctly by the caller | |
| utils.chdir(directory) | |
| -- change to the umask specified, or set the current umask as the umask, | |
| -- giving the daemon complete control over file permissions at file | |
| -- creation. | |
| umask(mask or unix.umask()) | |
| -- prevent stray resource molestation and odd side effects caused by | |
| -- writing/reading stdio by remapping all of stdio to /dev/null | |
| reset_stdio() | |
| end | |
| -- use a servicekit events object to get daemonization settings, will | |
| -- only daemonize if the settings call for daemonization, otherwise silently | |
| -- do nothing | |
| function daemonize.init(events) | |
| if events.settings.daemonize == true then | |
| null = open_null() | |
| end | |
| end | |
| function daemonize.setup(events) | |
| if events.settings.daemonize == true then | |
| log.info 'Daemonization in progress.' | |
| _daemonize(events.settings.directory, events.settings.umask) | |
| log.info 'Daemonized.' | |
| end | |
| end | |
| end -- end daemonize def | |
| -- TODO: Document | |
| local signals = {} | |
| do -- signals def | |
| local handlers = { | |
| [signal.SIGINT] = 'stop', | |
| [signal.SIGTERM] = 'stop', | |
| [signal.SIGHUP] = 'reload', | |
| default = function() end | |
| } | |
| -- block and handle signals relevant to a daemon. | |
| function signals.setup(cq, events) | |
| log.info 'Setting up signal handlers.' | |
| local cq = cqueues.new() | |
| -- block all signals we care about | |
| signal.block(signal.SIGINT, signal.SIGTERM, signal.SIGHUP) | |
| -- listen for the signals | |
| local sl = signal.listen(signal.SIGINT, signal.SIGTERM, signal.SIGHUP) | |
| -- install a function to handle the signals | |
| cq:wrap( | |
| function() | |
| -- if we're finished, signals don't matter any more | |
| while not utils.finished do | |
| -- dispatch signal to the correct handler | |
| local call = handlers[sl:wait()] or handlers.default | |
| events[call]() | |
| end | |
| end | |
| ) | |
| return cq | |
| end | |
| end -- end signals def | |
| local chroot = {} | |
| do -- chroot def | |
| -- check settings and determine if to chroot, and where to chroot to | |
| local function init(events) | |
| local directory | |
| if events.settings.chroot then | |
| if events.settings.chroot == true then | |
| directory = events.settings.directory | |
| else | |
| directory = events.settings.chroot | |
| end | |
| end | |
| if directory then | |
| local success, es, ec = unix.mkdir(directory) | |
| if not success and es ~= 'File exists' then | |
| utils.handle_fatal( | |
| success, es, ec, | |
| ('Failed to setup chroot directory - %s'):format(directory) | |
| ) | |
| end | |
| success, es, ec = unix.chown( | |
| directory, | |
| events.settings.change_priv.user, | |
| events.settings.change_priv.group | |
| ) | |
| utils.handle_fatal( | |
| success, es, ec, | |
| ('Failed to change owner to %s:%s'):format | |
| ( | |
| events.settings.change_priv.user, | |
| events.settings.change_priv.group | |
| ) | |
| ) | |
| end | |
| return directory | |
| end | |
| -- if specified to chroot, create directory if it doesn't exist, and chroot | |
| -- to it | |
| function chroot.setup(events) | |
| local directory = init(events) | |
| if directory then | |
| events.settings.directory = '/' | |
| log.info(('Chrooting to <%s>.'):format(directory)) | |
| local success, es, ec = unix.chroot(directory) | |
| utils.handle_fatal( | |
| success, es, ec, | |
| ('Failed to chroot to directory - %s'):format(directory) | |
| ) | |
| utils.chdir('/') | |
| end | |
| end | |
| end -- end chroot def | |
| local singleton = {} | |
| do -- singleton def | |
| -- try to get a lock on the lock file, otherwise commit suicide after | |
| -- informing the user that there can't be 2 of this process and they were | |
| -- there first | |
| function singleton.setup(events) | |
| if events.settings.singleton == true then | |
| local lf, es, ec = io.open(events.settings.lock_file, 'w') | |
| local success, es, ec = unix.fcntl(lf, unix.F_SETLK) | |
| if not success then | |
| local pid = unix.fcntl(lf, unix.F_GETLK).pid | |
| utils.handle_fatal( | |
| success, es, ec, | |
| ('Failed to start service, service already running, PID: %s'):format | |
| ( | |
| pid | |
| ) | |
| ) | |
| end | |
| end | |
| end | |
| end -- end singleton def | |
| do -- servicekit internals | |
| -- checks the settings and the events provided by the user to make sure | |
| -- that only the settings or events specified are provisioned | |
| local function setup(config) | |
| assert(type(config) == 'table') | |
| for k, v in pairs(config.settings) do | |
| assert(sk.settings[k] ~= nil, ('Unknown setting: %s'):format(k)) | |
| sk.settings[k] = v | |
| end | |
| config.settings = sk.settings | |
| for k, v in pairs(config) do | |
| assert(sk.events[k] ~= nil, ('Unknown event: %s'):format(k)) | |
| sk.events[k] = v | |
| end | |
| return sk.events | |
| end | |
| function sk.run(settings) | |
| local events, cq = setup(settings) | |
| utils.assert(events, 'Service is undefined.') | |
| -- pre-init, all stuff before dropping privileges | |
| events.privileged_init() | |
| -- these need some things figured out before a chroot takes place | |
| privileges.init(events.settings.change_priv) | |
| daemonize.init(events) | |
| chroot.setup(events) -- chroot, must be done as root | |
| -- drop privileges if we're going to! | |
| privileges.setup(events.settings.change_priv) | |
| -- make sure we're the only one if we're supposed to be the only one | |
| -- first check here, then after daemonization grab the actual lock, | |
| -- this allows for a printing of a message that its already running | |
| -- the grandparent will only hold the lock for a moment until the | |
| -- grandchild exists as it will exit | |
| singleton.setup(events) | |
| -- daemonize | |
| -- past here, failures can be silent! | |
| daemonize.setup(events) | |
| -- grab the lock if needed | |
| singleton.setup(events) | |
| -- afix signal handlers for the daemon | |
| cq = signals.setup(cq, events) | |
| -- pass control over to daemon code | |
| events.init() | |
| local le, li = events.logging_init() | |
| log.setup(le, li) | |
| -- rest is async, so signal handlers can get called | |
| cq:wrap( | |
| function() | |
| events.start() | |
| events.destroy() | |
| utils.finished = true | |
| end | |
| ) | |
| -- error string, error code, error thread, error object, error fd | |
| -- in the case of error, we doubt that the main loop deinitialized logging | |
| -- and assume we can use the logging functions it provided before exiting. | |
| local success, es, ec, et, eo, ef = cq:loop() | |
| if not success then | |
| log.fatal( | |
| ('error: %s\ncode: %s\nthread: %s\nobject: %s\nfd: %s\n'):format | |
| ( | |
| success, es, ec, et, eo, ef | |
| ) | |
| ) | |
| end | |
| end | |
| end -- end servicekit internals | |
| return sk.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment