Added log redirection and cgroups support via setsid()

This commit is contained in:
Nocturn9x 2021-12-16 11:24:00 +01:00
parent 9289c455e3
commit fea5e625e2
5 changed files with 182 additions and 79 deletions

View File

@ -12,7 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import tables
import parsecfg
import strutils
import strformat
import ../util/logging
@ -21,10 +24,81 @@ import fs
type
Config* = ref object
NimDConfig* = ref object
## Configuration object
# The desired log verbosity of nimD
logLevel*: LogLevel
logDir*: Directory
services*: tuple[runlevel: RunLevel, services: seq[Service]]
filesystems*: seq[Filesystem]
directories*: seq[Directory]
symlinks*: seq[Symlink]
filesystems*: seq[Filesystem]
restartDelay*: int
autoMount*: bool
autoUnmount*: bool
fstab*: string
sock*: string
const defaultSections = ["Misc", "Logging", "Filesystem"]
const defaultConfig = {
"Logging": {
"stderr": "/var/log/nimd",
"stdout": "/var/log/nimd",
"level": "info"
}.toTable(),
"Misc": {
"controlSocket": "/var/run/nimd.sock",
"onDependencyConflict": "skip"
}.toTable(),
"Filesystem": {
"autoMount": "true",
"autoUnmount": "true",
"fstabPath": "/etc/fstab",
"createDirs": "",
"createSymlinks": "",
"virtualDisks": ""
}.toTable()
}.toTable()
const levels = {"trace": Trace, "debug": Debug, "info": Info,
"warning": Warning, "error": Error, "critical": Critical,
"fatal": Fatal}.toTable()
proc parseConfig*(logger: Logger, file: string = "/etc/nimd/nimd.conf"): NimDConfig =
## Parses NimD's configuration file
## and returns a configuration object
# Yes this code is far from perfect, but I hate paesing
# config files :(
logger.debug(&"Reading config file at {file}")
let cfgObject = loadConfig(file)
var data = newTable[string, TableRef[string, string]]()
var existingSections: seq[string] = @[]
var directories: seq[Directory] = @[]
var filesystems: seq[Filesystem] = @[]
var symlinks: seq[Symlink] = @[]
var temp: seq[string] = @[]
for section in cfgObject.sections():
existingSections.add(section)
for section in defaultSections:
logger.debug(&"Parsing section '{section}'")
if section notin existingSections:
logger.warning(&"Missing section '{section}' from config file, falling back to defaults")
for (value, key) in defaultConfig[section].pairs():
data[section][value] = key
elif section notin defaultSections:
logger.warning(&"Unknown section '{section}' found in config file, skipping it")
else:
for key in defaultConfig[section].keys():
data[section][key] = cfgObject.getSectionValue(section, key, defaultConfig[section][key])
for dirInfo in data["Filesystem"]["createDirs"].split(","):
temp = dirInfo.split(":")
directories.add(newDirectory(temp[0], uint64(parseInt(temp[1]))))
for symInfo in data["Filesystem"]["createSymlinks"].split(","):
temp = symInfo.split(":")
symlinks.add(newSymlink(temp[0], temp[1]))
if data["Logging"]["level"] notin levels:
logger.warning(&"""Unknown logging level '{data["Logging"]["level"]}', defaulting to {defaultConfig["Logging"]["level"]}""")
data["Logging"]["level"] = defaultConfig["Logging"]["level"]
result = NimDConfig(logLevel: levels[data["Logging"]["level"]],
filesystems: filesystems)

View File

@ -15,6 +15,7 @@ import strformat
import strutils
import cpuinfo
import tables
import streams
import osproc
import posix
import shlex
@ -61,6 +62,7 @@ type
restartDelay: int
depends: seq[Dependency]
provides: seq[Dependency]
useParentStreams: bool
## These two fields are
## used by the dependency
## resolver
@ -75,11 +77,11 @@ proc newDependency*(kind: DependencyKind, provider: Service): Dependency =
proc newService*(name, description: string, kind: ServiceKind, workDir: string, runlevel: RunLevel, exec: string, supervised: bool, restart: RestartKind,
restartDelay: int, depends, provides: seq[Dependency]): Service =
restartDelay: int, depends, provides: seq[Dependency], useParentStreams: bool = false): Service =
## Creates a new service object
result = Service(name: name, description: description, kind: kind, workDir: workDir, runLevel: runLevel,
exec: exec, supervised: supervised, restart: restart, restartDelay: restartDelay,
depends: depends, provides: provides, isMarked: false, isResolved: false)
depends: depends, provides: provides, isMarked: false, isResolved: false, useParentStreams: useParentStreams)
result.provides.add(newDependency(Other, result))
@ -197,6 +199,7 @@ proc removeManagedProcess*(pid: int) =
proc addManagedProcess*(pid: int, service: Service) =
## Adds a managed process to the
## table
discard posix.setsid() # For cgroups support
processIDs[pid] = service
@ -216,64 +219,88 @@ proc removeService*(service: Service) =
break
proc supervisorWorker(logger: Logger, service: Service, pid: int) =
proc loggerWorker(logger: Logger, service: Service, process: Process) =
## Captures the output of a given process and relays it
## in a formatted manner into our logging system
try:
logger.debug("Switching logs to file")
logger.switchToFile()
var line: string = ""
var stream = process.outputStream
while stream.readLine(line):
logger.info(&"{service.name}: {line}")
except:
logger.error(&"An error occurred in loggerWorker: {getCurrentExceptionMsg()}")
quit(-1)
proc supervisorWorker(logger: Logger, service: Service, process: Process) =
## This is the actual worker that supervises the service process
logger.trace(&"New supervisor for service '{service.name}' has been spawned")
var pid = pid
var status: cint
var returnCode: int
var sig: int
var process: Process
logger.debug("Switching logs to file")
logger.switchToFile()
while true:
logger.trace(&"Calling waitpid() on {pid}")
returnCode = posix.waitPid(cint(pid), status, WUNTRACED)
if WIFEXITED(status):
sig = 0
elif WIFSIGNALED(status):
sig = WTERMSIG(status)
else:
sig = -1
logger.trace(&"Call to waitpid() set status to {status} and returned {returnCode}, setting sig to {sig}")
case service.restart:
of Never:
logger.info(&"Service '{service.name}' ({returnCode}) has exited, shutting down controlling process")
break
of Always:
if sig > 0:
logger.info(&"Service '{service.name}' ({returnCode}) has crashed (terminated by signal {sig}: {strsignal(cint(sig))}), sleeping {service.restartDelay} seconds before restarting it")
elif sig == 0:
logger.info(&"Service '{service.name}' has exited gracefully, sleeping {service.restartDelay} seconds before restarting it")
else:
logger.info(&"Service '{service.name}' has exited, sleeping {service.restartDelay} seconds before restarting it")
removeManagedProcess(pid)
sleep(service.restartDelay * 1000)
var split = shlex(service.exec)
if split.error:
logger.error(&"Error while restarting service '{service.name}': invalid exec syntax")
if not service.useParentStreams:
logger.trace(&"Spawning log watcher for '{service.name}'")
var p = posix.fork()
if p == -1:
logger.error(&"Error, cannot fork: {posix.strerror(posix.errno)}")
elif p == 0:
logger.trace(&"New child has been spawned")
loggerWorker(logger, service, process)
else:
var pid = process.processID
var status: cint
var returnCode: int
var sig: int
var process: Process
logger.debug("Switching logs to file")
logger.switchToFile()
while true:
logger.trace(&"Calling waitpid() on {pid}")
returnCode = posix.waitPid(cint(pid), status, WUNTRACED)
if WIFEXITED(status):
sig = 0
elif WIFSIGNALED(status):
sig = WTERMSIG(status)
else:
sig = -1
logger.trace(&"Call to waitpid() set status to {status} and returned {returnCode}, setting sig to {sig}")
case service.restart:
of Never:
logger.info(&"Service '{service.name}' ({returnCode}) has exited, shutting down controlling process")
break
var arguments = split.words
let progName = arguments[0]
arguments = arguments[1..^1]
process = startProcess(progName, workingDir=service.workDir, args=arguments)
pid = process.processID()
of OnFailure:
if sig > 0:
logger.info(&"Service '{service.name}' ({returnCode}) has crashed (terminated by signal {sig}: {strsignal(cint(sig))}), sleeping {service.restartDelay} seconds before restarting it")
removeManagedProcess(pid)
sleep(service.restartDelay * 1000)
var split = shlex(service.exec)
if split.error:
logger.error(&"Error while restarting service '{service.name}': invalid exec syntax")
break
var arguments = split.words
let progName = arguments[0]
arguments = arguments[1..^1]
process = startProcess(progName, workingDir=service.workDir, args=arguments)
pid = process.processID()
if process != nil:
process.close()
of Always:
if sig > 0:
logger.info(&"Service '{service.name}' ({returnCode}) has crashed (terminated by signal {sig}: {strsignal(cint(sig))}), sleeping {service.restartDelay} seconds before restarting it")
elif sig == 0:
logger.info(&"Service '{service.name}' has exited gracefully, sleeping {service.restartDelay} seconds before restarting it")
else:
logger.info(&"Service '{service.name}' has exited, sleeping {service.restartDelay} seconds before restarting it")
removeManagedProcess(pid)
sleep(service.restartDelay * 1000)
var split = shlex(service.exec)
if split.error:
logger.error(&"Error while restarting service '{service.name}': invalid exec syntax")
break
var arguments = split.words
let progName = arguments[0]
arguments = arguments[1..^1]
process = startProcess(progName, workingDir=service.workDir, args=arguments)
pid = process.processID()
of OnFailure:
if sig > 0:
logger.info(&"Service '{service.name}' ({returnCode}) has crashed (terminated by signal {sig}: {strsignal(cint(sig))}), sleeping {service.restartDelay} seconds before restarting it")
removeManagedProcess(pid)
sleep(service.restartDelay * 1000)
var split = shlex(service.exec)
if split.error:
logger.error(&"Error while restarting service '{service.name}': invalid exec syntax")
break
var arguments = split.words
let progName = arguments[0]
arguments = arguments[1..^1]
process = startProcess(progName, workingDir=service.workDir, args=arguments)
pid = process.processID()
if process != nil:
process.close()
proc startService(logger: Logger, service: Service) =
@ -292,18 +319,18 @@ proc startService(logger: Logger, service: Service) =
var arguments = split.words
let progName = arguments[0]
arguments = arguments[1..^1]
process = startProcess(progName, workingDir=service.workDir, args=arguments, options={poParentStreams})
if service.supervised and service.kind != Oneshot:
process = startProcess(progName, workingDir=service.workDir, args=arguments, options=if service.useParentStreams: {poParentStreams} else: {poUsePath, poDaemon, poStdErrToStdOut})
if service.supervised or service.kind != Oneshot:
var pid = posix.fork()
if pid == 0:
if pid == -1:
logger.error(&"Error, cannot fork: {posix.strerror(posix.errno)}")
elif pid == 0:
logger.trace(&"New child has been spawned")
supervisorWorker(logger, service, process.processID)
# If the service is unsupervised we just exit
supervisorWorker(logger, service, process)
# If the service is unsupervised we just spawn the logger worker
loggerWorker(logger, service, process)
except:
logger.error(&"Error while starting service {service.name}: {getCurrentExceptionMsg()}")
if process != nil:
process.close()
quit(0)
logger.error(&"Error while starting service '{service.name}': {getCurrentExceptionMsg()}")
proc startServices*(logger: Logger, level: RunLevel, workers: int = 1) =

View File

@ -38,12 +38,12 @@ proc addStuff =
addSymlink(newSymlink(dest="/dev/std/err", source="/")) # Should say link already exists and points to /proc/self/fd/2
addSymlink(newSymlink(dest="/dev/std/in", source="/does/not/exist")) # Should say destination does not exist
addSymlink(newSymlink(dest="/dev/std/in", source="/proc/self/fd/0")) # Should say link already exists
addDirectory(newDirectory("test", 777)) # Should create a directory
addDirectory(newDirectory("test", 764)) # Should create a directory
addDirectory(newDirectory("/dev/disk", 123)) # Should say directory already exists
# Shutdown handler to unmount disks
addShutdownHandler(newShutdownHandler(unmountAllDisks))
# Adds test services
var echoer = newService(name="echoer", description="prints owo", exec="/bin/echo owo",
var echoer = newService(name="echoer", description="prints owo", exec="/bin/echo owoooooooooo",
runlevel=Boot, kind=Oneshot, workDir=getCurrentDir(),
supervised=false, restart=Never, restartDelay=0,
depends=(@[]), provides=(@[]))
@ -61,7 +61,8 @@ proc addStuff =
depends=(@[newDependency(Other, errorer)]), provides=(@[]))
var shell = newService(name="login", description="A simple login shell", kind=Simple,
getCurrentDir(), runlevel=Boot, exec="/bin/login -f root",
supervised=true, restart=Always, restartDelay=0, depends=(@[]), provides=(@[])
supervised=true, restart=Always, restartDelay=0, depends=(@[]), provides=(@[]),
useParentStreams=true
)
addService(errorer)
addService(echoer)

View File

@ -82,13 +82,17 @@ proc log(self: Logger, level: LogLevel = defaultLevel, message: string) =
proc lockFile(logger: Logger, handle: File) =
## Locks the given file across the whole system for writing using fcntl()
if fcntl(handle.getFileHandle(), F_WRLCK) == -1:
stderr.writeLine(&"Error while locking handle (code {posix.errno}, {posix.strerror(posix.errno)}): output may be mangled")
setForegroundColor(fgRed)
stderr.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} ERROR {"-":>3} ({posix.getpid():03})] Error while locking handle (code {posix.errno}, {posix.strerror(posix.errno)}): output may be mangled""")
setForegroundColor(fgDefault)
proc unlockFile(logger: Logger, handle: File) =
## Unlocks the given file across the whole system for writing using fcntl()
if fcntl(handle.getFileHandle(), F_UNLCK) == -1:
stderr.writeLine(&"Error while locking stderr (code {posix.errno}, {posix.strerror(posix.errno)}): output may be mangled")
setForegroundColor(fgRed)
stderr.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} ERROR {"-":>3} ({posix.getpid():03})] Error while unlocking handle (code {posix.errno}, {posix.strerror(posix.errno)}): output may be missing""")
setForegroundColor(fgDefault)
proc logTraceStderr(self: LogHandler, logger: Logger, message: string) =

View File

@ -27,6 +27,8 @@ import ../core/shutdown
proc sleepSeconds*(amount: SomeNumber) = sleep(int(amount * 1000))
proc strsignal*(sig: cint): cstring {.header: "string.h", importc.}
proc dummySigHandler(x: cint) {.noconv.} = discard
proc doSync*(logger: Logger) =
@ -34,13 +36,9 @@ proc doSync*(logger: Logger) =
logger.debug(&"Calling sync() syscall has returned {syscall(SYNC)}")
proc dummySigHandler(x: cint) {.noconv.} = discard
proc blockSignals*(logger: Logger) =
## Temporarily blocks all signals
## for critical sections of code
var tmp: Sigset
var sigaction: Sigaction
sigaction.sa_handler = dummySigHandler
@ -57,7 +55,6 @@ proc unblockSignals*(logger: Logger) =
## Unblocks all signals
var tmp: Sigset
var sigaction: Sigaction
sigaction.sa_handler = dummySigHandler
sigaction.sa_flags = SA_RESTART
if posix.sigemptyset(sigaction.sa_mask) == -1:
logger.fatal(&"Could not initialize signal unlock (code {posix.errno}, {posix.strerror(posix.errno)}): environment is not safe, exiting now!")