diff --git a/README.md b/README.md index 55c8432..f7d8063 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,6 @@ want to make sure your disks are mounted and that your network has been set up. ``` [Service] - name = ssh # The name of the service description = Secure Shell Server # A short description type = simple # Other option: oneshot (i.e. runs only once, implies supervised=false) @@ -70,7 +69,6 @@ restartDelay = 10 # NimD will wait this many seconds before supervised = true # This is the default. Disable it if you don't need NimD to watch for it [Logging] - stderr = /var/log/sshd # Path of the stderr log for the service stdout = /var/log/sshd # Path of the stdout log for the service stdin = /dev/null # Path of the stdin fd for the service @@ -99,20 +97,22 @@ INI-like structure), but the options are obviously different. An example config ``` [Logging] - level = info # Levels are: trace, debug, info, warning, error, critical, fatal logFile = /var/log/nimd # Path to log file [Filesystem] - -autoMount = true # Automatically parses /etc/fstab and mounts disks -autoUnmount = true # Automatically parses /proc/mounts and unmounts everything on shutdown -fstabPath = /etc/fstab # Path to your system's fstab (defaults to /etc/fstab) -createDirs = /path/to/dir1, /path/to/dir2 # Creates these directories on boot. Empty to disable -createSymlinks = /path/to/symlink:/path/to/dest, ... # Creates these symlinks on boot. Empty to disable +autoMount = true # Automatically parses /etc/fstab and mounts disks +autoUnmount = true # Automatically parses /proc/mounts and unmounts everything on shutdown +fstabPath = /etc/fstab # Path to your system's fstab (defaults to /etc/fstab) +createDirs = "/path/to/dir:perms," # Creates these directories with the given permissions on boot. Empty to disable +createSymlinks = "/path/to/symlink:/path/to/dest," # Creates these symlinks on boot. Empty to disable [Misc] - controlSocket = /var/run/nimd.sock # Path to the Unix domain socket to create for IPC -onDependencyConflict = skip # Other option: warn, error +setHostname = true # Set to true to set the machine's hostname automatically from /etc/hostname +onDependencyConflict = skip # Other option: warn, error +workers = 1 # Number of worker processes to use when spawning services +restartDelay = 10 # Delay (seconds) that nimd will wait before restarting itself after crashes +sigtermDelay = 90 # Delay (seconds) that nimd will wait before terminating child processes with + # SIGKILL after sending a more gentle SIGTERM upon shutdown or exit ``` \ No newline at end of file diff --git a/config.nims b/config.nims index bdc1869..e29ba61 100644 --- a/config.nims +++ b/config.nims @@ -1,12 +1,11 @@ from macros import error -from ospaths import splitFile, `/` +from os import splitFile, `/` # -d:musl when defined(musl): var muslGccPath: string echo " [-d:musl] Building a static binary using musl .." muslGccPath = findExe("musl-gcc") - # echo "debug: " & muslGccPath if muslGccPath == "": error("'musl-gcc' binary was not found in PATH.") switch("gcc.exe", muslGccPath) @@ -15,7 +14,7 @@ when defined(musl): proc binOptimize(binFile: string) = - ## Optimize size of the ``binFile`` binary. + ## Optimize size of the binFile binary. echo "" if findExe("strip") != "": echo "Running 'strip -s' .." @@ -36,11 +35,10 @@ task musl, "Builds an optimized static binary using musl": "\n Usage Example: nim musl FILE.nim.") let - nimFile = paramStr(numParams) ## The nim file name *must* be the last. + nimFile = paramStr(numParams) ## The nim file name *must* be the last (dirName, baseName, _) = splitFile(nimFile) binFile = dirName / baseName # Save the binary in the same dir as the nim file - nimArgs = "c -d:musl -d:release --opt:size " & nimFile - # echo "[debug] nimFile = " & nimFile & ", binFile = " & binFile + nimArgs = "c -d:musl -d:release --opt:size --gc:orc" & nimFile # Build binary echo "\nRunning 'nim " & nimArgs & "' .." diff --git a/nimd.conf b/nimd.conf new file mode 100644 index 0000000..3627f6c --- /dev/null +++ b/nimd.conf @@ -0,0 +1,21 @@ +[Misc] +controlSocket = /var/run/nimd.sock +setHostname = true +onDependencyConflict = skip +workers = 1 +restartDelay = 10 +sigtermDelay = 90 + +[Logging] +level = info +logFile = /var/log/nimd + +[Filesystem] +autoMount = true +autoUnmount = true +fstabPath = /etc/fstab +createDirs = "test:764,/dev/disk:123" +createSymlinks = """/dev/fd:/proc/self/fd,/dev/fd/0:/proc/self/fd/0,/dev/fd/1:/proc/self/fd/1,/dev/fd/2:/proc/self/fd/2, + /dev/std/in:/proc/self/fd/0,/dev/std/out:/proc/self/fd/1,/dev/std/err:/proc/self/fd/2, + /dev/std/in:/,/dev/std/out:/does/not/exist,/dev/std/err:/proc/self/fd/2 + """ diff --git a/src/core/mainloop.nim b/src/core/mainloop.nim index fa4b861..381961b 100644 --- a/src/core/mainloop.nim +++ b/src/core/mainloop.nim @@ -16,18 +16,18 @@ import os import net -import ../util/[logging, misc] +import ../util/[logging, misc, config] import services import control import shutdown -proc mainLoop*(logger: Logger, workers: int = 1, startServices: bool = true) = +proc mainLoop*(logger: Logger, config: NimDConfig, startServices: bool = true) = ## NimD's main execution loop if startServices: logger.info("Processing default runlevel") - startServices(logger, workers=workers, level=Default) + startServices(logger, workers=config.workers, level=Default) logger.debug(&"Unblocking signals") unblockSignals(logger) logger.info("System initialization complete, idling on control socket") @@ -68,7 +68,7 @@ proc mainLoop*(logger: Logger, workers: int = 1, startServices: bool = true) = discard clientSocket.close() except: - logger.critical(&"A critical error has occurred while running, restarting the mainloop in 30 seconds! Error -> {getCurrentExceptionMsg()}") - sleepSeconds(30) + logger.critical(&"A critical error has occurred while running, restarting the mainloop in {config.restartDelay} seconds! Error -> {getCurrentExceptionMsg()}") + sleepSeconds(config.restartDelay) # We *absolutely* cannot die - mainLoop(logger, startServices=false) + mainLoop(logger, config, startServices=false) diff --git a/src/core/shutdown.nim b/src/core/shutdown.nim index fa8e08a..4f5134e 100644 --- a/src/core/shutdown.nim +++ b/src/core/shutdown.nim @@ -39,7 +39,12 @@ proc newShutdownHandler*(body: proc (logger: Logger, code: int)): ShutdownHandle var shutdownHandlers: seq[ShutdownHandler] = @[] -var sigTermDelay: float = 10.0 +var sigTermDelay: float = 90 + + +proc setSigTermDelay*(delay: int = 90) = + # Sets the sigtermDelay variable + sigTermDelay = float(delay) proc addShutdownHandler*(handler: ShutdownHandler) = @@ -90,7 +95,7 @@ proc nimDExit*(logger: Logger, code: int, emerg: bool = true) = # We're in emergency mode: do not crash the kernel, spawn a shell and exit logger.fatal("NimD has entered emergency mode and cannot continue. You will be now (hopefully) dropped in a root shell: you're on your own. May the force be with you") logger.info("Terminating child processes with SIGKILL") - discard execCmd("/bin/sh") # TODO: Is this fine? maybe use execProcess + discard execCmd(os.getEnv("SHELL", "/bin/sh")) # TODO: Is this fine? maybe use execProcess discard posix.kill(-1, SIGKILL) quit(-1) logger.warning("The system is shutting down") @@ -113,7 +118,7 @@ proc nimDExit*(logger: Logger, code: int, emerg: bool = true) = if anyUserlandProcessLeft(): logger.info("Terminating child processes with SIGKILL") discard posix.kill(-1, SIGKILL) - logger.warning("Shutdown procedure complete, sending final termination signal") + logger.warning("Shutdown procedure complete, NimD is exiting") proc reboot*(logger: Logger) = diff --git a/src/main.nim b/src/main.nim index 4c9d980..d38e0d1 100644 --- a/src/main.nim +++ b/src/main.nim @@ -17,7 +17,7 @@ import posix import os # NimD's own stuff -import util/[logging, constants, misc] +import util/[logging, constants, misc, config] import core/[mainloop, fs, shutdown, services] @@ -40,8 +40,7 @@ proc addStuff = addSymlink(newSymlink(dest="/dev/std/in", source="/proc/self/fd/0")) # Should say link already exists 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 owoooooooooo", runlevel=Boot, kind=Simple, workDir=getCurrentDir(), @@ -67,10 +66,11 @@ proc addStuff = -proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab", setHostname: bool = true, workerCount: int = 1) = +proc main(logger: Logger, config: NimDConfig) = ## NimD's entry point and setup ## function - setStdIoUnbuffered() # Colors and output synchronization don't work otherwise + logger.debug(&"Setting log file to '{config.logFile}'") + setLogFile(file=config.logFile) logger.debug("Starting NimD: A minimal, self-contained, dependency-based Linux init system written in Nim") logger.info(&"NimD version {NimdVersion.major}.{NimdVersion.minor}.{NimdVersion.patch} is starting up!") logger.trace("Calling getCurrentProcessId()") @@ -86,7 +86,7 @@ proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab", nimDExit(logger, EPERM, emerg=false) # EPERM - Operation not permitted logger.trace("Setting up signal handlers") onSignal(SIGABRT, SIGALRM, SIGHUP, SIGILL, SIGKILL, SIGQUIT, SIGSTOP, SIGSEGV, SIGTSTP, - SIGTRAP, SIGPIPE, SIGUSR1, SIGUSR2, 6, SIGFPE, SIGBUS, SIGURG, SIGTERM, SIGINT): # 6 is SIGIOT + SIGTRAP, SIGPIPE, SIGUSR1, SIGUSR2, 6, SIGFPE, SIGBUS, SIGURG, SIGTERM, SIGINT): # 6 is SIGIOT # Can't capture local variables because this implicitly generates # a noconv procedure, so we use getDefaultLogger() instead getDefaultLogger().warning(&"Ignoring signal {sig} ({strsignal(sig)})") # Nim injects the variable "sig" into the scope. Gotta love those macros @@ -94,11 +94,15 @@ proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab", # One of the key features of an init system is reaping child # processes! reapProcess(getDefaultLogger()) + if config.autoUnmount: + logger.debug("Setting up shutdown handler for automatic disk unmounting") + # Shutdown handler to unmount disks + addShutdownHandler(newShutdownHandler(unmountAllDisks)) addStuff() try: - if mountDisks: + if config.autoMount: logger.info("Mounting filesystem") - mountDisks(logger, fstab) + mountDisks(logger, config.fstab) else: logger.info("Skipping disk mounting, assuming this has already been done") logger.info("Creating symlinks") @@ -111,7 +115,7 @@ proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab", except: logger.fatal(&"A fatal error has occurred while preparing filesystem, booting cannot continue. Error -> {getCurrentExceptionMsg()}") nimDExit(logger, 131, emerg=false) - if setHostname: + if config.setHostname: logger.info("Setting hostname") logger.debug(&"Hostname was set to '{misc.setHostname(logger)}'") else: @@ -119,9 +123,9 @@ proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab", logger.debug("Entering critical fork() section: blocking signals") blockSignals(logger) # They are later unblocked in mainLoop logger.info("Processing boot runlevel") - startServices(logger, workers=workerCount, level=Boot) + startServices(logger, workers=config.workers, level=Boot) logger.debug("Starting main loop") - mainLoop(logger, workers=workerCount) + mainLoop(logger, config) when isMainModule: @@ -167,9 +171,10 @@ when isMainModule: else: echo "Usage: nimd [options]" quit(EINVAL) # EINVAL - Invalid argument - logger.debug("Calling NimD entry point") + + setStdIoUnbuffered() # Colors don't work otherwise! try: - main(logger) + main(logger, parseConfig(logger, "/etc/nimd/nimd.conf")) except: logger.fatal(&"A fatal unrecoverable error has occurred during startup and NimD cannot continue: {getCurrentExceptionMsg()}") nimDExit(logger, 131) # ENOTRECOVERABLE - State not recoverable diff --git a/src/core/config.nim b/src/util/config.nim similarity index 62% rename from src/core/config.nim rename to src/util/config.nim index fc3bf72..7561fbc 100644 --- a/src/core/config.nim +++ b/src/util/config.nim @@ -18,37 +18,42 @@ import strutils import strformat -import ../util/logging -import services -import fs +import logging +import ../core/fs type NimDConfig* = ref object ## Configuration object logLevel*: LogLevel - logDir*: Directory - services*: tuple[runlevel: RunLevel, services: seq[Service]] + logFile*: string + workers*: int directories*: seq[Directory] symlinks*: seq[Symlink] filesystems*: seq[Filesystem] restartDelay*: int + sigtermDelay*: int autoMount*: bool autoUnmount*: bool fstab*: string sock*: string + onDependencyConflict*: string + setHostname*: bool -const defaultSections = ["Misc", "Logging", "Filesystem"] -const defaultConfig = { +const defaultSections* = ["Misc", "Logging", "Filesystem"] +const defaultConfig* = { "Logging": { - "stderr": "/var/log/nimd", - "stdout": "/var/log/nimd", + "logFile": "/var/log/nimd", "level": "info" }.toTable(), "Misc": { "controlSocket": "/var/run/nimd.sock", - "onDependencyConflict": "skip" + "onDependencyConflict": "skip", + "setHostname": "true", + "workers": "1", + "sigtermDelay": "90", + "restartDelay": "30" }.toTable(), "Filesystem": { "autoMount": "true", @@ -59,9 +64,16 @@ const defaultConfig = { "virtualDisks": "" }.toTable() }.toTable() -const levels = {"trace": Trace, "debug": Debug, "info": Info, - "warning": Warning, "error": Error, "critical": Critical, - "fatal": Fatal}.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 = @@ -89,16 +101,31 @@ proc parseConfig*(logger: Logger, file: string = "/etc/nimd/nimd.conf"): NimDCon elif section notin defaultSections: logger.warning(&"Unknown section '{section}' found in config file, skipping it") else: + if not data.hasKey(section): + data[section] = newTable[string, string]() 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])))) + directories.add(newDirectory(temp[0].strip(), uint64(parseInt(temp[1].strip())))) 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"]}""") + 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) \ No newline at end of file + logFile: data["Logging"]["logFile"], + filesystems: filesystems, + symlinks: symlinks, + directories: directories, + autoMount: parseBool(data["Filesystem"]["autoMount"].toLowerAscii()), + autoUnmount: parseBool(data["Filesystem"]["autoUnmount"].toLowerAscii()), + fstab: data["Filesystem"]["fstabPath"], + sock: data["Misc"]["controlSocket"], + onDependencyConflict: data["Misc"]["onDependencyConflict"].toLowerAscii(), + restartDelay: parseInt(data["Misc"]["restartDelay"]), + sigtermDelay: parseInt(data["Misc"]["sigtermDelay"]), + workers: parseInt(data["Misc"]["workers"]), + setHostname: parseBool(data["Misc"]["setHostname"]) + ) diff --git a/src/util/logging.nim b/src/util/logging.nim index d1d3788..a53667b 100644 --- a/src/util/logging.nim +++ b/src/util/logging.nim @@ -46,7 +46,13 @@ var defaultLevel = LogLevel.Info var logFile = "/var/log/nimd" var logToFileOnly: bool = false -## This mess is needed to make sure stderr writes are mostly atomic. Sort of + +proc setLogFile*(file: string) = + # Sets the log file + logFile = file + + +## This mess is needed to make sure stderr writes are mostly atomic. Sort of. ## No error handling yet. Deal with it var customStderrFd = dup(stderr.getFileHandle()) discard dup3(stderr.getFileHandle(), customStderrFd, O_APPEND)