diff --git a/.gitignore b/.gitignore index 3ce2f49..c5540b1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ packervm test initramfs-linux.img vmlinuz-linux +debian* diff --git a/src/core/fs.nim b/src/core/fs.nim index 8fa874c..799a38d 100644 --- a/src/core/fs.nim +++ b/src/core/fs.nim @@ -350,9 +350,44 @@ proc createSymlinks*(logger: Logger) = createDir(sym.dest.splitPath().head) createSymlink(sym.source, sym.dest) except: - logger.warning(&"Failed to create symbolic link from {sym.dest} to {sym.source}: {getCurrentExceptionMsg()}") + logger.error(&"Failed to create symbolic link from {sym.dest} to {sym.source}: {getCurrentExceptionMsg()}") proc createDirectories*(logger: Logger) = ## Creates standard directories that - ## Linux software expects to be present. \ No newline at end of file + ## Linux software expects to be present. + ## Note that this has to run after the + ## filesystem has been initialized. + ## If a chmod binary is found, it is used + ## to set directory permissions as specified + ## in their config. Note that the entire path + ## of the directory is created if it does not + ## exist yet + var hasChmod = false + try: + if findExe("chmod").isEmptyOrWhitespace(): + logger.warning("Could not find chmod binary, directory permissions will default to OS configuration") + hasChmod = true + except: + logger.error(&"Failed to search for chmod binary: {getCurrentExceptionMsg()}") + for dir in directories: + try: + if exists(dir.path): + if dirExists(dir.path): + logger.warning(&"Creation of directory {dir.path} skipped: directory already exists") + elif fileExists(dir.path): + logger.warning(&"Creation of directory {dir.path} skipped: path is a file") + elif symlinkExists(dir.path): + logger.warning(&"Creation of directory {dir.path} skipped: path is a symlink to {expandSymlink(dir.path)}") + else: + # Catch-all + logger.warning(&"Creation of directory {dir.path} skipped: destination already exists") + else: + createDir(dir.path) + logger.debug(&"Created new directory at {dir.path}") + if hasChmod: + logger.debug(&"Setting permissions to {dir.permissions} for {dir.path}") + if (let code = execShellCmd(&"chmod -R {dir.permissions} {dir.path}"); code) != 0: + logger.warning(&"Command 'chmod -R {dir.permissions}' exited non-zero status code {code}") + except: + logger.error(&"Failed to create directory at {dir.path}: {getCurrentExceptionMsg()}") diff --git a/src/core/services.nim b/src/core/services.nim index 1018180..9e1c60f 100644 --- a/src/core/services.nim +++ b/src/core/services.nim @@ -17,10 +17,13 @@ import tables import osproc import posix import shlex +import os + + +proc strsignal(sig: cint): cstring {.header: "string.h", importc.} import ../util/logging -import ../util/misc type @@ -31,6 +34,9 @@ type ## Enumerates all service ## types Oneshot, Simple + RestartKind* = enum + ## Enum of possible restart modes + Always, OnFailure, Never Service* = ref object of RootObj ## A service object name: string @@ -40,14 +46,14 @@ type runlevel: RunLevel exec: string supervised: bool - restartOnFailure: bool + restart: RestartKind restartDelay: int -proc newService*(name, description: string, kind: ServiceKind, workDir: string, runlevel: RunLevel, exec: string, supervised, restartOnFailure: bool, restartDelay: int): Service = +proc newService*(name, description: string, kind: ServiceKind, workDir: string, runlevel: RunLevel, exec: string, supervised: bool, restart: RestartKind, restartDelay: int): Service = ## Creates a new service object result = Service(name: name, description: description, kind: kind, workDir: workDir, runLevel: runLevel, - exec: exec, supervised: supervised, restartOnFailure: restartOnFailure, restartDelay: restartDelay) + exec: exec, supervised: supervised, restart: restart, restartDelay: restartDelay) var services: seq[Service] = @[] @@ -114,25 +120,42 @@ proc supervisorWorker(logger: Logger, service: Service, pid: int) = sig = WTERMSIG(status) else: sig = -1 - if sig > 0 and service.restartOnFailure: - logger.info(&"Service '{service.name}' ({returnCode}) has crashed (terminated by signal {sig}: {strsignal(cint(sig))}), sleeping {service.restartDelay} seconds before restarting it") - removeManagedProcess(pid) - sleepSeconds(service.restartDelay) - var split = shlex(service.exec) - if split.error: - logger.error(&"Error while restarting service '{service.name}': invalid exec syntax") + 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() - elif sig > 0: - logger.info(&"Service '{service.name}' ({returnCode}) has crashed (terminated by signal {sig}: {strsignal(cint(sig))}), shutting down controlling process") - break - else: - 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") + 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() @@ -140,7 +163,8 @@ proc supervisorWorker(logger: Logger, service: Service, pid: int) = proc startService(logger: Logger, service: Service) = ## Starts a single service (this is called by ## startServices below until all services have - ## been started) + ## been started). This function is supposed to + ## be called from a forked process! var process: Process try: var split = shlex(service.exec) @@ -152,7 +176,9 @@ proc startService(logger: Logger, service: Service) = arguments = arguments[1..^1] process = startProcess(progName, workingDir=service.workDir, args=arguments) if service.supervised: - supervisorWorker(logger, service, process.processID) + var pid = posix.fork() + if pid == 0: + supervisorWorker(logger, service, process.processID) # If the service is unsupervised we just exit except: logger.error(&"Error while starting service {service.name}: {getCurrentExceptionMsg()}") diff --git a/src/core/shutdown.nim b/src/core/shutdown.nim index f467874..cb2bd3f 100644 --- a/src/core/shutdown.nim +++ b/src/core/shutdown.nim @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import os +import osproc import posix import glob import strutils @@ -20,6 +21,7 @@ import times import ../util/logging +import services type ShutdownHandler* = ref object @@ -79,17 +81,15 @@ proc nimDExit*(logger: Logger, code: int, emerg: bool = true) = ## as cleanly as possible. When emerg equals true, it will ## try to spawn a root shell and exit if emerg: - var status: cint # 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 posix.kill(SIGKILL, -1) - discard posix.waitPid(-1, status, 0) - discard execShellCmd("/bin/sh") # TODO: Is this fine? maybe use execProcess + discard execCmd("/bin/sh") # TODO: Is this fine? maybe use execProcess + discard posix.kill(-1, SIGKILL) quit(-1) logger.warning("The system is shutting down") logger.info("Processing shutdown runlevel") - # TODO + startServices(logger, Shutdown) logger.info("Running shutdown handlers") try: for handler in shutdownHandlers: @@ -99,13 +99,13 @@ proc nimDExit*(logger: Logger, code: int, emerg: bool = true) = # Note: continues calling handlers! logger.info("Terminating child processes with SIGTERM") logger.debug(&"Waiting up to {sigTermDelay} seconds for the kernel to deliver signals") - discard posix.kill(SIGTERM, -1) # The kernel handles this for us asynchronously + discard posix.kill(-1, SIGTERM) # The kernel handles this for us asynchronously var t = cpuTime() # We wait some time for the signals to propagate while anyUserlandProcessLeft() or cpuTime() - t >= sigTermDelay: sleep(int(0.25 * 1000)) if anyUserlandProcessLeft(): logger.info("Terminating child processes with SIGKILL") - discard posix.kill(SIGKILL, -1) + discard posix.kill(-1, SIGKILL) logger.warning("Shutdown procedure complete, sending final termination signal") quit(code) diff --git a/src/main.nim b/src/main.nim index 6e8498a..bc3e892 100644 --- a/src/main.nim +++ b/src/main.nim @@ -21,6 +21,50 @@ import util/[logging, constants, misc] import core/[mainloop, fs, shutdown, services] +proc addStuff = + ## Adds stuff to test NimD. This is + ## a temporary procedure + + # Adds symlinks + addSymlink(newSymlink(dest="/dev/fd", source="/proc/self/fd")) + addSymlink(newSymlink(dest="/dev/fd/0", source="/proc/self/fd/0")) + addSymlink(newSymlink(dest="/dev/fd/1", source="/proc/self/fd/1")) + addSymlink(newSymlink(dest="/dev/fd/2", source="/proc/self/fd/2")) + addSymlink(newSymlink(dest="/dev/std/in", source="/proc/self/fd/0")) + addSymlink(newSymlink(dest="/dev/std/out", source="/proc/self/fd/1")) + addSymlink(newSymlink(dest="/dev/std/err", source="/proc/self/fd/2")) + # Tests here. Check logging output (debug) to see if + # they work as intended + 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")) # Shuld say destination does not exist + addSymlink(newSymlink(dest="/dev/std/in", source="/proc/self/fd/0")) # Should say link already exists + # Adds virtual filesystems + addVFS(newFilesystem(source="proc", target="/proc", fstype="proc", mountflags=0u64, data="nosuid,noexec,nodev", dump=0u8, pass=0u8)) + addVFS(newFilesystem(source="sys", target="/sys", fstype="sysfs", mountflags=0u64, data="nosuid,noexec,nodev", dump=0u8, pass=0u8)) + addVFS(newFilesystem(source="run", target="/run", fstype="tmpfs", mountflags=0u64, data="mode=0755,nosuid,nodev", dump=0u8, pass=0u8)) + addVFS(newFilesystem(source="dev", target="/dev", fstype="devtmpfs", mountflags=0u64, data="mode=0755,nosuid", dump=0u8, pass=0u8)) + addVFS(newFilesystem(source="devpts", target="/dev/pts", fstype="devpts", mountflags=0u64, data="mode=0620,gid=5,nosuid,noexec", dump=0u8, pass=0u8)) + addVFS(newFilesystem(source="shm", target="/dev/shm", fstype="tmpfs", mountflags=0u64, data="mode=1777,nosuid,nodev", dump=0u8, pass=0u8)) + addDirectory(newDirectory("test", 777)) # Should create a directory + addDirectory(newDirectory("/dev/disk", 123)) # Should say directory already exists + addDirectory(newDirectory("/dev/test/owo", 000)) # Should say path does not exist + # Shutdown handler to unmount disks + addShutdownHandler(newShutdownHandler(unmountAllDisks)) + # Adds test services + addService(newService(name="echoer", description="prints owo", exec="/bin/echo owo", + runlevel=Boot, kind=Oneshot, workDir=getCurrentDir(), + supervised=false, restart=Never, restartDelay=0)) + addService(newService(name="errorer", description="la mamma di gavd", + exec="/bin/false", supervised=true, restart=OnFailure, + restartDelay=5, runlevel=Boot, workDir="/", kind=Simple)) + addService(newService(name="exiter", description="la mamma di licenziat", + exec="/bin/true", supervised=true, restart=Always, + restartDelay=5, runlevel=Boot, workDir="/", kind=Simple)) + addService(newService(name="sleeper", description="la mamma di danieloz", + exec="/usr/bin/sleep", supervised=true, restart=Always, + restartDelay=5, runlevel=Boot, workDir="/", kind=Simple)) + + proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab") = ## NimD's entry point and setup ## function @@ -53,26 +97,7 @@ proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab") onSignal(SIGINT): # Temporary nimDExit(getDefaultLogger(), 131, emerg=true) - addSymlink(newSymlink(dest="/dev/fd", source="/proc/self/fd")) - addSymlink(newSymlink(dest="/dev/fd/0", source="/proc/self/fd/0")) - addSymlink(newSymlink(dest="/dev/fd/1", source="/proc/self/fd/1")) - addSymlink(newSymlink(dest="/dev/fd/2", source="/proc/self/fd/2")) - addSymlink(newSymlink(dest="/dev/std/in", source="/proc/self/fd/0")) - addSymlink(newSymlink(dest="/dev/std/out", source="/proc/self/fd/1")) - addSymlink(newSymlink(dest="/dev/std/err", source="/proc/self/fd/2")) - # Tests here. Check logging output (debug) to see if - # they work as intended - 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")) # Shuld say destination does not exist - addSymlink(newSymlink(dest="/dev/std/in", source="/proc/self/fd/0")) # Should say link already exists - # Adds virtual filesystems - addVFS(newFilesystem(source="proc", target="/proc", fstype="proc", mountflags=0u64, data="nosuid,noexec,nodev", dump=0u8, pass=0u8)) - addVFS(newFilesystem(source="sys", target="/sys", fstype="sysfs", mountflags=0u64, data="nosuid,noexec,nodev", dump=0u8, pass=0u8)) - addVFS(newFilesystem(source="run", target="/run", fstype="tmpfs", mountflags=0u64, data="mode=0755,nosuid,nodev", dump=0u8, pass=0u8)) - addVFS(newFilesystem(source="dev", target="/dev", fstype="devtmpfs", mountflags=0u64, data="mode=0755,nosuid", dump=0u8, pass=0u8)) - addVFS(newFilesystem(source="devpts", target="/dev/pts", fstype="devpts", mountflags=0u64, data="mode=0620,gid=5,nosuid,noexec", dump=0u8, pass=0u8)) - addVFS(newFilesystem(source="shm", target="/dev/shm", fstype="tmpfs", mountflags=0u64, data="mode=1777,nosuid,nodev", dump=0u8, pass=0u8)) - addShutdownHandler(newShutdownHandler(unmountAllDisks)) + addStuff() try: if mountDisks: logger.info("Mounting filesystem") @@ -82,31 +107,22 @@ proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab") mountRealDisks(logger, fstab) else: logger.info("Skipping disk mounting, assuming this has already been done") + logger.info("Creating symlinks") + createSymlinks(logger) + logger.info("Creating directories") + createDirectories(logger) + logger.info("Filesystem preparation complete") + logger.debug("Calling sync() just in case") + doSync(logger) except: logger.fatal(&"A fatal error has occurred while preparing filesystem, booting cannot continue. Error -> {getCurrentExceptionMsg()}") nimDExit(logger, 131, emerg=false) - logger.info("Disks mounted") - logger.debug("Calling sync() just in case") - doSync(logger) logger.info("Setting hostname") logger.debug(&"Hostname was set to '{setHostname(logger)}'") - logger.info("Creating symlinks") - createSymlinks(logger) - logger.info("Creating directories") - createDirectories(logger) logger.debug("Entering critical fork() section: blocking signals") - blockSignals(logger) + blockSignals(logger) # They are later unblocked in mainLoop logger.info("Processing boot runlevel") - addService(newService(name="echoer", description="prints owo", exec="/bin/echo owo", - runlevel=Boot, kind=Oneshot, workDir=getCurrentDir(), - supervised=false, restartOnFailure=false, restartDelay=0)) - addService(newService(name="sleeper", description="la mamma di licenziato", - exec="/usr/bin/sleep 10", supervised=true, restartOnFailure=true, - restartDelay=5, runlevel=Boot, workDir="/home", kind=Simple)) - addService(newService(name="errorer", description="la mamma di gavd", - exec="/bin/false", supervised=true, restartOnFailure=true, - restartDelay=5, runlevel=Boot, workDir="/", kind=Simple)) - startServices(logger, workers=2, level=Boot) + startServices(logger, workers=1, level=Boot) logger.debug("Starting main loop") mainLoop(logger) @@ -117,7 +133,8 @@ when isMainModule: for kind, key, value in optParser.getopt(): case kind: of cmdArgument: - discard + echo "Error: unexpected argument" + quit(EINVAL) of cmdLongOption: case key: of "help": @@ -131,7 +148,7 @@ when isMainModule: of "extra": logger.setLevel(LogLevel.Trace) else: - logger.error(&"Unkown command-line long option '{key}'") + echo &"Unkown command-line long option '{key}'" quit(EINVAL) # EINVAL - Invalid argument of cmdShortOption: case key: @@ -146,7 +163,9 @@ when isMainModule: of "X": logger.setLevel(LogLevel.Trace) else: - logger.error(&"Unkown command-line short option '{key}'") + echo &"Unkown command-line short option '{key}'" + echo "Usage: nimd [options]" + echo "Try nimd --help for more info" quit(EINVAL) # EINVAL - Invalid argument else: echo "Usage: nimd [options]" @@ -158,4 +177,4 @@ when isMainModule: logger.fatal(&"A fatal unrecoverable error has occurred during startup and NimD cannot continue: {getCurrentExceptionMsg()}") nimDExit(logger, 131) # ENOTRECOVERABLE - State not recoverable # This will almost certainly cause the kernel to crash with an error the likes of "Kernel not syncing, attempted to kill init!", - # but, after all, there isn't much we can do if we can't even initialize *ourselves* is there? \ No newline at end of file + # but, after all, there isn't much we can do if we can't even initialize *ourselves* is there? diff --git a/src/util/logging.nim b/src/util/logging.nim index 8db845f..d2f1c29 100644 --- a/src/util/logging.nim +++ b/src/util/logging.nim @@ -75,7 +75,7 @@ proc getDefaultLogger*(): Logger = ## standard error with some basic info like the ## current date and time and the log level - setStdIoUnbuffered() # Just in case + setStdIoUnbuffered() # Doesn't work otherwise! proc logTrace(self: LogHandler, logger: Logger, message: string) = setForegroundColor(fgMagenta) diff --git a/start.sh b/start.sh deleted file mode 100755 index bcff935..0000000 --- a/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t nimd:latest . -docker run --rm -it nimd \ No newline at end of file