mirror of https://github.com/nocturn9x/nimd.git
Added log redirection and cgroups support via setsid()
This commit is contained in:
parent
9289c455e3
commit
fea5e625e2
|
@ -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)
|
|
@ -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) =
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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!")
|
||||
|
|
Loading…
Reference in New Issue