diff --git a/Dockerfile b/Dockerfile index 15fd7a9..1e4b28b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /code # Removes any already existing binary so that when compilation fails the container stops RUN rm -f /code/nimd RUN rm -f /code/main -RUN nimble install syscall -y +RUN nimble install syscall glob shlex -y RUN nim -d:release --opt:size --passL:"-static" --gc:orc -d:useMalloc c -o:nimd src/main RUN cp /code/nimd /sbin/nimd diff --git a/src/core/fs.nim b/src/core/fs.nim new file mode 100644 index 0000000..85f2751 --- /dev/null +++ b/src/core/fs.nim @@ -0,0 +1,359 @@ +# Copyright 2021 Mattia Giambirtone & All Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import strutils +import sequtils +import strformat +import posix +import os + +import ../util/[logging, misc] +import shutdown + + +# Nim wrappers around C functionality in sys/mount.h on Linux +proc mount*(source: cstring, target: cstring, fstype: cstring, + mountflags: culong, data: pointer): cint {.header: "sys/mount.h", importc.} +# Since cstrings are weak references, we need to convert nim strings to cstrings only +# when we're ready to use them and only when we're sure the underlying nim string is +# in scope, otherwise garbage collection madness happens +proc mount*(source, target, fstype: string, mountflags: uint64, data: string): int = int(mount(cstring(source), cstring(target), cstring(fstype), culong(mountflags), cstring(data))) + +proc umount*(target: cstring): cint {.header: "sys/mount.h", importc.} +proc umount2*(target: cstring, flags: cint): cint {.header: "sys/mount.h", importc.} +# These 2 wrappers silent the CStringConv warning +# (implicit conversion to 'cstring' from a non-const location) +proc umount*(target: string): int = int(umount(cstring(target))) +proc umount2*(target: string, flags: int): int = int(umount2(cstring(target), cint(flags))) + + +## Our Nim API +type + Directory* = ref object + path: string + permissions: uint64 + Symlink* = ref object + ## A symbolic link + source: string + dest: string + Filesystem* = ref object + ## A filesystem + ## (real or virtual) + source: string + target: string + fstype: string + mountflags: uint64 + data: string + dump: uint8 + pass: uint8 + + +proc newFilesystem*(source, target, fstype: string, mountflags: uint64 = 0, data: string = "", dump: uint8 = 0, pass: uint8 = 0): Filesystem = + ## Initializes a new filesystem object + result = Filesystem(source: source, target: target, fstype: fstype, mountflags: mountflags, data: data, dump: dump, pass: pass) + + +proc newSymlink*(source, dest: string): Symlink = + ## Initializes a new symlink object + result = Symlink(source: source, dest: dest) + + +proc newDirectory*(path: string, permissions: uint64): Directory = + ## Initializes a new directory object + result = Directory(path: path, permissions: permissions) + + +# Stores VFS entries to be mounted upon boot (usually /proc, /sys, etc). You could +# do this with a oneshot service, but it's a simple enough feature to have it built-in +# into the init itself (especially since it makes error handling a heck of a lot easier) +var virtualFileSystems: seq[Filesystem] = @[] +# Since creating symlinks is a pretty typical operation for an init, NimD +# provides a straightforward way to create them on boot without creating +# full fledged oneshot services +var symlinks: seq[Symlink ] = @[] +# Stores directories to be created on boot. Again, this is achievable trough oneshots, +# but having a builtin API is a nice option IMHO +var directories: seq[Directory] = @[] + + +proc addVFS*(filesystem: FileSystem) = + ## Adds a virtual filesystem to be mounted upon boot + virtualFileSystems.add(filesystem) + + +proc removeVFS*(filesystem: Filesystem) = + ## Removes a virtual filesystem. Note + ## this has no effect if executed after + ## the VFSs have been mounted (i.e. after + ## a call to mountVirtualDisks) + for i, f in virtualFileSystems: + if f == filesystem: + virtualFileSystems.del(i) + + +iterator getAllVFSPaths: string = + ## Yields all of the mount points of + ## the currently registered virtual + ## filesystems + for vfs in virtualFileSystems: + yield vfs.target + + +iterator getAllVFSNames: string = + ## This is similar to what + ## getAllVFSPaths does, except + ## it yields the VFS' source + ## instead of the mount point + ## (which in this case is just + ## an alias, hence the "names" part) + for vfs in virtualFileSystems: + yield vfs.source + + +proc addSymlink*(symlink: Symlink) = + ## Adds a symlink to be created + ## upon boot (check createSymlinks) + symlinks.add(symlink) + + +proc removeSymlink*(symlink: Symlink) = + ## Removes a symlink. This has no + ## effect after createSymlinks has + ## been executed + for i, sym in symlinks: + if sym == symlink: + symlinks.del(i) + + +proc addDirectory*(directory: Directory) = + ## Adds a directory to be created upon + ## boot (check createDirectories) + directories.add(directory) + + +proc removeDirectory*(directory: Directory) = + ## Removes a directory. This has no + ## effect after createDirectories has + ## been executed + for i, dir in directories: + if dir == directory: + directories.del(i) + + +proc parseFileSystemTable*(fstab: string): seq[Filesystem] = + ## Parses the contents of the given filesystem table and returns a Filesystem object. + ## An improperly formatted or semantically invalid fstab will cause this function to + ## error out with a ValueError exception that should be caught by the caller. + ## No other checks other than very basic syntax are performed, as that job + ## is delegated to the operating system. Missing dump/pass entries are interpreted + ## as if they were set to 0, following the way Linux does it. Note that this function + ## automatically converts UUID/LABEL/PARTUUID/ID directives to their corresponding + ## /dev/disk/by-XXX/YYY symlink just like the mount command would do on a Linux system. + var temp: seq[string] = @[] + var dump: int + var pass: int + var line: string = "" + var s: seq[string] = @[] + for l in fstab.splitlines(): + line = l.strip().replace("\t", " ") + if line.startswith("#") or line.isEmptyOrWhitespace(): + continue + # This madness will make sure we only get (hopefully) 6 entries + # in our temporary list + temp = line.split().filterIt(it != "").join(" ").split(maxsplit=6) + if len(temp) < 6: + if len(temp) < 4: + # Not enough columns! + raise newException(ValueError, "improperly formatted filesystem table") + elif len(temp) == 4: + dump = 0 + pass = 0 + elif len(temp) == 5: + dump = 0 + else: + try: + dump = parseInt(temp[4]) + except ValueError: + raise newException(ValueError, &"improperly formatted filesystem table -> invalid value ({dump}) for dump") + try: + pass = parseInt(temp[5]) + except ValueError: + raise newException(ValueError, &"improperly formatted filesystem table -> invalid value ({pass}) for pass") + if dump notin 0..1: + raise newException(ValueError, &"invalid value in filesystem table -> invalid value ({dump}) for dump") + if pass < 0: + raise newException(ValueError, &"invalid value in filesystem table -> invalid value ({pass}) for pass") + s = temp[0].split("=", maxsplit=2) + if temp[0].toLowerAscii().startswith("id="): + if len(s) < 2: + raise newException(ValueError, "improperly formatted filesystem table") + temp[0] = &"""/dev/disk/by-id/{s[1]}""" + if temp[0].toLowerAscii().startswith("label="): + if len(s) < 2: + raise newException(ValueError, "improperly formatted filesystem table") + temp[0] = &"""/dev/disk/by-label/{s[1]}""" + if temp[0].toLowerAscii().startswith("uuid="): + if len(s) < 2: + raise newException(ValueError, "improperly formatted filesystem table") + temp[0] = &"""/dev/disk/by-uuid/{s[1]}""" + if temp[0].toLowerAscii().startswith("partuuid="): + if len(s) < 2: + raise newException(ValueError, "improperly formatted filesystem table") + temp[0] = &"""/dev/disk/by-partuuid/{s[1]}""" + result.add(newFilesystem(source=temp[0], target=temp[1], fstype=temp[2], mountflags=0u64, data=temp[3], dump=uint8(dump), pass=uint8(pass))) + + +proc checkDiskIsMounted(search: Filesystem, expand: bool = false): bool = + ## Returns true if a disk is already mounted. If expand is true, + ## symlinks are expanded and checked instead of doing a simple + ## string comparison of the source entry point. This should be + ## true when mounting real filesystems. Returns false if + ## /proc/mounts does not exist (usually happens when /proc has + ## not been mounted yet) + if not fileExists("/proc/mounts"): + return false + for entry in parseFileSystemTable(readFile("/proc/mounts")): + if expand: + if exists(entry.source) and exists(search.source) and sameFile(entry.source, search.source): + return true + elif entry.source == search.source: + return true + return false + + +proc mountRealDisks*(logger: Logger, fstab: string = "/etc/fstab") = + ## Mounts real disks from /etc/fstab + var retcode = 0 + try: + logger.debug(&"Reading disk entries from {fstab}") + for entry in parseFileSystemTable(readFile(fstab)): + if checkDiskIsMounted(entry, expand=true): + logger.debug(&"Skipping mounting filesystem {entry.source} ({entry.fstype}) at {entry.target}: already mounted") + continue + logger.debug(&"fsck returned status code {retcode}") + logger.debug(&"Mounting filesystem {entry.source} ({entry.fstype}) at {entry.target} with mount option(s) {entry.data}") + logger.trace(&"Calling mount('{entry.source}', '{entry.target}', '{entry.fstype}', {entry.mountflags}, '{entry.data}')") + retcode = mount(entry.source, entry.target, entry.fstype, entry.mountflags, entry.data) + logger.trace(&"mount('{entry.source}', '{entry.target}', '{entry.fstype}', {entry.mountflags}, '{entry.data}') returned {retcode}") + if retcode == -1: + logger.error(&"Mounting {entry.source} at {entry.target} has failed with error {posix.errno}: {posix.strerror(posix.errno)}") + # Resets the error code + posix.errno = cint(0) + else: + logger.debug(&"Mounted {entry.source} at {entry.target}") + except ValueError: # Check parseFileSystemTable for more info on this catch block + logger.fatal("Improperly formatted fstab, exiting") + nimDExit(logger, 131) + + +proc mountVirtualDisks*(logger: Logger) = + ## Mounts POSIX virtual filesystems/partitions, + ## such as /proc and /sys + var retcode = 0 + for entry in virtualFileSystems: + if checkDiskIsMounted(entry): + logger.debug(&"Skipping mounting filesystem {entry.source} ({entry.fstype}) at {entry.target}: already mounted") + continue + logger.debug(&"Mounting filesystem {entry.source} ({entry.fstype}) at {entry.target} with mount option(s) {entry.data}") + logger.trace(&"Calling mount('{entry.source}', '{entry.target}', '{entry.fstype}', {entry.mountflags}, '{entry.data}')") + retcode = mount(entry.source, entry.target, entry.fstype, entry.mountflags, entry.data) + logger.trace(&"mount('{entry.source}', '{entry.target}', '{entry.fstype}', {entry.mountflags}, '{entry.data}') returned {retcode}") + if retcode == -1: + logger.error(&"Mounting disk {entry.source} at {entry.target} has failed with error {posix.errno}: {posix.strerror(posix.errno)}") + # Resets the error code + posix.errno = cint(0) + logger.fatal("Failed mounting vital system disk partition, system is likely corrupted, booting cannot continue") + nimDExit(logger, 131) # ENOTRECOVERABLE - State not recoverable + else: + logger.debug(&"Mounted {entry.source} at {entry.target}") + + +proc unmountAllDisks*(logger: Logger, code: int) = + ## Unmounts all currently mounted disks, including the ones that + ## were not mounted trough fstab but excluding virtual filesystems + var isVFS: bool = false + var retcode = 0 + try: + logger.info("Detaching real filesystems") + logger.debug(&"Reading disk entries from /proc/mounts") + for entry in parseFileSystemTable(readFile("/proc/mounts")): + # We don't detach the virtual filesystems because they are a software-level abstraction + # that exists purely in memory, and unmounting them while keeping the system stable during + # shutdown is a headache I don't wanna deal with. + # All of these checks seem excessive, but they make absolutely sure we don't unmount them, + # as they are critical system components (especially /proc): maybe we should use stat() + # instead and make a generic check, but adding a system call into the mix seems overkill given + # we alredy have all the info we need + if entry in virtualFileSystems: + continue + for source in getAllVFSNames(): + if entry.source == source: + isVFS = true + break + for path in getAllVFSPaths(): + if entry.target.startswith(path): + isVFS = true + break + if isVFS: + isVFS = false + logger.trace(&"Skipping unmounting filesystem {entry.source} ({entry.fstype}) from {entry.target} as it is a virtual filesystem") + continue + if not checkDiskIsMounted(entry): + logger.trace(&"Skipping unmounting filesystem {entry.source} ({entry.fstype}) from {entry.target}: not mounted") + continue + logger.debug(&"Unmounting filesystem {entry.source} ({entry.fstype}) from {entry.target}") + logger.trace(&"Calling umount2('{entry.source}', MNT_DETACH)") + retcode = umount2(entry.source, 2) # 2 = MNT_DETACH - Since we're shutting down, we need the disks to be *gone*! + logger.trace(&"umount2('{entry.source}', MNT_DETACH) returned {retcode}") + if retcode == -1: + logger.error(&"Unmounting disk {entry.source} from {entry.target} has failed with error {posix.errno}: {posix.strerror(posix.errno)}") + # Resets the error code + posix.errno = cint(0) + else: + logger.debug(&"Unmounted {entry.source} from {entry.target}") + except ValueError: # Check parseFileSystemTable for more info on this catch block + logger.fatal(&"A fatal error occurred while unmounting disks: {getCurrentExceptionMsg()}") + nimDExit(logger, 131) + + +proc createSymlinks*(logger: Logger) = + ## Creates a set of symlinks needed + ## by stuff like Linux ports of BSD + ## software. Non-existing directories + ## are created until the path to the + ## symlink is valid. Already existing + ## sources and non-existent destinations + ## cause the symlink creation to be skipped + for sym in symlinks: + try: + if not exists(sym.source): + logger.warning(&"Skipping creation of symbolic link from {sym.dest} to {sym.source}: destination does not exist") + continue + elif exists(sym.dest): + if symlinkExists(sym.dest) and sameFile(expandSymlink(sym.dest), sym.source): + logger.debug(&"Skipping creation of symbolic link from {sym.dest} to {sym.source}: link already exists") + elif symlinkExists(sym.dest) and not sameFile(expandSymlink(sym.dest), sym.source): + logger.warning(&"Attempted to create symbolic link from {sym.dest} to {sym.source}, but link already exists and points to {expandSymlink(sym.dest)}") + else: + logger.warning(&"Attempted to create symbolic link from {sym.dest} to {sym.source}, but destination already exists and is not a symlink") + continue + logger.debug(&"Creating symbolic link from {sym.dest} to {sym.source}") + 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()}") + + +proc createDirectories*(logger: Logger) = + ## Creates standard directories that + ## Linux software expects to be present. \ No newline at end of file diff --git a/src/core/mainloop.nim b/src/core/mainloop.nim index 63b3d48..b427842 100644 --- a/src/core/mainloop.nim +++ b/src/core/mainloop.nim @@ -13,38 +13,17 @@ # limitations under the License. import segfaults # Makes us catch segfaults as NilAccessDefect exceptions! import strformat -import os -import ../util/[logging, disks, misc] +import ../util/[logging, misc] +import services -proc mainLoop*(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab") = + +proc mainLoop*(logger: Logger) = ## NimD's main execution loop - try: - addShutdownHandler(unmountAllDisks, logger) - if mountDisks: - logger.info("Mounting filesystem") - logger.info("Mounting virtual disks") - mountVirtualDisks(logger) - logger.info("Mounting real disks") - mountRealDisks(logger, fstab) - else: - logger.info("Skipping disk mounting, did we restart after a critical error?") - except: - logger.fatal(&"A fatal error has occurred while preparing filesystem, booting cannot continue. Error -> {getCurrentExceptionMsg()}") - nimDExit(logger, 131) - 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("Processing boot runlevel") - # TODO logger.info("Processing default runlevel") - # TODO + startServices(logger, workers=1, level=Default) logger.info("System initialization complete, going idle") while true: try: @@ -53,4 +32,4 @@ proc mainLoop*(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fs except: logger.critical(&"A critical error has occurred while running, restarting the mainloop! Error -> {getCurrentExceptionMsg()}") # We *absolutely* cannot die - mainLoop(logger, mountDisks=false) + mainLoop(logger) diff --git a/src/core/services.nim b/src/core/services.nim new file mode 100644 index 0000000..1b9fbfe --- /dev/null +++ b/src/core/services.nim @@ -0,0 +1,192 @@ +# Copyright 2021 Mattia Giambirtone & All Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import strformat +import cpuinfo +import tables +import osproc +import posix +import shlex + + +import ../util/logging +import ../util/misc + + +type + RunLevel* = enum + ## Enum of possible runlevels + Boot, Default, Shutdown + ServiceKind* = enum + ## Enumerates all service + ## types + Oneshot, Simple + Service* = ref object of RootObj + ## A service object + name: string + description: string + kind: ServiceKind + workDir: string + runlevel: RunLevel + exec: string + supervised: bool + restartOnFailure: bool + restartDelay: int + + +proc newService*(name, description: string, kind: ServiceKind, workDir: string, runlevel: RunLevel, exec: string, supervised, restartOnFailure: bool, 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) + + +var services: seq[Service] = @[] +var processIDs: TableRef[int, Service] = newTable[int, Service]() + + +proc isManagedProcess*(pid: int): bool = + ## Returns true if the given process + ## id is associated to a supervised + ## NimD service + result = pid in processIDs + + +proc getManagedProcess*(pid: int): Service = + ## Returns a managed process by its PID. + ## Returns nil if the given pid doesn't + ## belong to a managed process + + result = if pid.isManagedProcess(): processIDs[pid] else: nil + + +proc removeManagedProcess*(pid: int) = + ## Removes a managed process entry + ## from the table + if pid.isManagedProcess(): + processIDs.del(pid) + + +proc addManagedProcess*(pid: int, service: Service) = + ## Adds a managed process to the + ## table + processIDs[pid] = service + + +proc addService*(service: Service) = + ## Adds a service to be started when + ## its runlevel is processed + services.add(service) + + +proc removeService*(service: Service) = + ## Unregisters a service from being + ## started (has no effect after services + ## have already been started) + for i, serv in services: + if serv == service: + services.del(i) + break + + +proc supervisorWorker(logger: Logger, service: Service, pid: int) = + ## This is the actual worker that supervises the service process + var pid = pid + var status: cint + var returnCode: int + var sig: int + while true: + returnCode = posix.waitPid(Pid(pid), status, WUNTRACED) + if WIFEXITED(status): + sig = 0 + elif WIFSIGNALED(status): + sig = WTERMSIG(status) + else: + sig = -1 + if service.restartOnFailure and sig > 0: + logger.info(&"Service {service.name} has exited with return code {returnCode} (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 starting service {service.name}: invalid exec syntax") + return + var arguments = split.words + let progName = arguments[0] + arguments = arguments[1..^1] + pid = startProcess(progName, workingDir=service.workDir, args=arguments, options={poParentStreams}).processID() + else: + logger.info(&"Service {service.name} has exited with return code {returnCode}), shutting down controlling process") + break + + + +proc startService(logger: Logger, service: Service) = + ## Starts a single service (this is called by + ## startServices below until all services have + ## been started) + + var split = shlex(service.exec) + if split.error: + logger.error(&"Error while starting service {service.name}: invalid exec syntax") + return + var arguments = split.words + let progName = arguments[0] + arguments = arguments[1..^1] + try: + var process = startProcess(progName, workingDir=service.workDir, args=arguments, options={poParentStreams, }) + if service.supervised: + supervisorWorker(logger, service, process.processID) + except OSError: + logger.error(&"Error while starting service {service.name}: {getCurrentExceptionMsg()}") + quit(0) + + +proc startServices*(logger: Logger, workers: int = 1, level: RunLevel) = + ## Starts the services in the given + ## runlevel. The workers parameter + ## configures parallelism and allows + ## for faster boot times by starting + ## services concurrently rather than + ## sequentially (1 to disable parallelism). + ## Note this function immediately returns to + ## the caller and forks in the background + echo posix.getpid() + discard readLine(stdin) + var pid: int = posix.fork() + if pid == -1: + logger.fatal(&"Could not fork(): {posix.strerror(posix.errno)}") + return + elif pid == 0: + quit(0) + var servicesCopy: seq[Service] = @[] + echo servicesCopy.len(), " ", posix.getpid() + for service in services: + if service.runlevel == level: + servicesCopy.add(service) + echo servicesCopy.len(), " ", posix.getpid() + if workers > cpuinfo.countProcessors() * 2 - 1: + logger.warning(&"The configured workers count is beyond the recommended threshold ({workers} > {cpuinfo.countProcessors() * 2 - 1}), performance may degrade") + while servicesCopy.len() > 0: + echo servicesCopy.len(), " ", posix.getpid() + sleepSeconds(5) + for i in 1..workers: + pid = posix.fork() + if pid == -1: + logger.error(&"An error occurred while forking to spawn services, trying again: {posix.strerror(posix.errno)}") + elif pid == 0: + logger.info(&"Starting service {servicesCopy[0].name}") + startService(logger, servicesCopy[0]) + else: + if servicesCopy.len() > 0 and servicesCopy[0].supervised: + addManagedProcess(pid, servicesCopy[0]) + diff --git a/src/core/shutdown.nim b/src/core/shutdown.nim new file mode 100644 index 0000000..b1cf9bb --- /dev/null +++ b/src/core/shutdown.nim @@ -0,0 +1,110 @@ +# Copyright 2021 Mattia Giambirtone & All Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import posix +import glob +import strutils +import strformat +import times + + +import ../util/logging +import ../util/misc + + +type ShutdownHandler* = ref object + ## A shutdown handler (internal to NimD) + body*: proc (logger: Logger, code: int) + + +proc newShutdownHandler*(body: proc (logger: Logger, code: int)): ShutdownHandler = + result = ShutdownHandler(body: body) + + +var shutdownHandlers: seq[ShutdownHandler] = @[] +var sigTermDelay: float = 10.0 + + +proc addShutdownHandler*(handler: ShutdownHandler) = + ## Registers a shutdown handler to be executed + ## upon a call of NimDExit + shutdownHandlers.add(handler) + + +proc removeShutdownHandler*(handler: ShutdownHandler) = + ## Unregisters a shutdown handler + for i, hndlr in shutdownHandlers: + if hndlr == handler: + shutdownHandlers.del(i) + break + + +proc anyUserlandProcessLeft: bool = + ## Returns true if there's any + ## userland processes running. + ## A userland process is one + ## whose pid is higher than 2 + ## or whose /proc//cmdline + ## file is empty. This function + ## assumes /proc is mounted and + ## readable and returns false in + ## the event of any I/O exceptions + try: + for dir in walkGlob("/proc/[0-9]"): + if dir.lastPathPart.parseInt() > 2 or readFile(dir.joinPath("/cmdline")).len() == 0: + # PID > 2 or empty cmdline file means it's a kernel process so we ignore + # it (not that we'd have the right to send those processes a signal anyway) + continue + else: + return true # There is at least one userland process running + except OSError: + return false + except IOError: + return false + return false + + +proc nimDExit*(logger: Logger, code: int, emerg: bool = true) = + ## NimD's exit point. This function tries to shut down + ## as cleanly as possible. When emerg equals true, it will + ## try to spawn a root shell and exit + logger.warning("The system is shutting down") + logger.info("Processing shutdown runlevel") + # TODO + logger.info("Running shutdown handlers") + try: + for handler in shutdownHandlers: + handler.body(logger, code) + except: + logger.error(&"An error has occurred while calling shutdown handlers. Error -> {getCurrentExceptionMsg()}") + # Note: continues calling handlers! + if emerg: + # 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") + discard execShellCmd("/bin/sh") # TODO: Is this fine? maybe use execProcess + else: + logger.info("Terminating child processes with SIGTERM") + logger.debug(&"Waiting {sigTermDelay} seconds for the kernel to deliver signals") + discard posix.kill(SIGTERM, -1) # The kernel handles this for us asynchronously + var t = cpuTime() + # We wait some time for the signals to propagate + while anyUserlandProcessLeft(): + sleepSeconds(0.25) + if cpuTime() - t >= sigTermDelay: + break + if anyUserlandProcessLeft(): + logger.info("Terminating child processes with SIGKILL") + discard posix.kill(SIGKILL, -1) + logger.warning("Shutdown procedure complete, sending final termination signal") + quit(code) \ No newline at end of file diff --git a/src/main.nim b/src/main.nim index bdd969d..8a7a7a3 100644 --- a/src/main.nim +++ b/src/main.nim @@ -18,13 +18,15 @@ import os # NimD's own stuff import util/[logging, constants, misc] -import core/mainloop +import core/[mainloop, fs, shutdown, services] -proc main(logger: Logger) = - ## NimD's entry point + +proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab") = + ## NimD's entry point and setup + ## function 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.info(&"NimD version {NimdVersion.major}.{NimdVersion.minor}.{NimdVersion.patch} is starting up!") logger.trace("Calling getCurrentProcessId()") let pid = getCurrentProcessId() logger.trace(&"getCurrentProcessId() returned {pid}") @@ -35,17 +37,66 @@ proc main(logger: Logger) = logger.trace(&"getuid() returned {uid}") if uid != 0: logger.fatal(&"NimD must run as root, but current user id is {uid}") - quit(EPERM) # EPERM - Operation not permitted - logger.debug("Setting up dummy signal handlers") - onSignal(SIGABRT, SIGALRM, SIGHUP, SIGILL, SIGKILL, SIGQUIT, SIGSTOP, SIGSEGV, SIGTSTP, + nimDExit(logger, EPERM) # EPERM - Operation not permitted + logger.trace("Setting up signal handlers") + onSignal(SIGABRT, SIGALRM, SIGHUP, SIGILL, SIGKILL, SIGQUIT, SIGSTOP, SIGTSTP, SIGTRAP, SIGTERM, SIGPIPE, SIGUSR1, SIGUSR2, 6, SIGFPE, SIGBUS, SIGURG, SIGINT): # 6 is SIGIOT # Can't capture local variables because this implicitly generates - # a noconv procedure + # a noconv procedure, so we use getDefaultLogger() instead. Must find + # a better solution long-term because we need the configuration from + # our own logger object (otherwise we'd always create a new one and + # never switch our logs to file once booting is completed) getDefaultLogger().warning(&"Ignoring signal {sig} ({strsignal(sig)})") # Nim injects the variable "sig" into the scope. Gotta love those macros - logger.debug("Setting up SIGCHLD signal handler") onSignal(SIGCHLD): + # One of the key features of an init system is reaping child + # processes! reapProcess(getDefaultLogger()) - logger.debug("Starting uninterruptible mainloop") + 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)) + try: + if mountDisks: + logger.info("Mounting filesystem") + logger.info("Mounting virtual disks") + mountVirtualDisks(logger) + logger.info("Mounting real disks") + mountRealDisks(logger, fstab) + else: + logger.info("Skipping disk mounting, assuming this has already been done") + 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) + createDirectories(logger) + logger.info("Processing boot runlevel") + addService(newService(name="Test", description="prints owo", exec="/bin/echo owo", + runlevel=Boot, kind=Oneshot, workDir=getCurrentDir(), + supervised=false, restartOnFailure=false, restartDelay=0)) + startServices(logger, workers=1, level=Boot) + logger.debug("Starting main loop") mainLoop(logger) @@ -93,6 +144,7 @@ when isMainModule: try: main(logger) except: + raise 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!", diff --git a/src/util/disks.nim b/src/util/disks.nim deleted file mode 100644 index 88f5036..0000000 --- a/src/util/disks.nim +++ /dev/null @@ -1,215 +0,0 @@ -# Copyright 2021 Mattia Giambirtone & All Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import strutils -import sequtils -import strformat -import posix -import os - -import logging -import misc - - -const virtualFileSystems: array[6, tuple[source: string, target: string, filesystemtype: string, mountflags: uint64, data: string, dump, pass: uint8]] = [ - # ALl the standard POSIX filesystems needed by the OS to work properly are listed here - (source: "proc", target: "/proc", filesystemtype: "proc", mountflags: 0u64, data: "nosuid,noexec,nodev", dump: 0u8, pass: 0u8), - (source: "sys", target: "/sys", filesystemtype: "sysfs", mountflags: 0u64, data: "nosuid,noexec,nodev", dump: 0u8, pass: 0u8), - (source: "run", target: "/run", filesystemtype: "tmpfs", mountflags: 0u64, data: "mode=0755,nosuid,nodev", dump: 0u8, pass: 0u8), - (source: "dev", target: "/dev", filesystemtype: "devtmpfs", mountflags: 0u64, data: "mode=0755,nosuid", dump: 0u8, pass: 0u8), - (source: "devpts", target: "/dev/pts", filesystemtype: "devpts", mountflags: 0u64, data: "mode=0620,gid=5,nosuid,noexec", dump: 0u8, pass: 0u8), - (source: "shm", target: "/dev/shm", filesystemtype: "tmpfs", mountflags: 0u64, data: "mode=1777,nosuid,nodev", dump: 0u8, pass: 0u8), -] - - -proc parseFileSystemTable*(fstab: string): seq[tuple[source, target, filesystemtype: string, mountflags: uint64, data: string, dump, pass: uint8]] = - ## Parses the contents of the given file (the contents of /etc/fstab or /etc/mtab - ## most of the time, but this is not enforced in any way) and returns a sequence - ## of tuples with elements source, target, filesystemtype, mountflags, data, dump - ## and pass as required by mount/umount/umount2 in sys/mount.h which are wrapped below. - ## The types of these arguments are Nim types to make the garbage collector happy - ## and avoid freeing the underlying string object. - ## An improperly formatted fstab will cause this function to error out with a - ## ValueError exception (when an entry is incomplete) that should be caught by - ## the caller. No other checks other than very basic syntax are performed, as - ## that job is delegated to the operating system. Missing dump/pass entries are - ## interpreted as if they were set to 0, following the way Linux does it. - ## Note that this function automatically converts UUID/LABEL/PARTUUID/ID directives - ## to their corresponding /dev/disk symlink just like the mount command would do - ## on a Linux system. - var temp: seq[string] = @[] - var dump: int - var pass: int - var line: string = "" - for l in fstab.splitlines(): - line = l.strip().replace("\t", " ") - if line.startswith("#") or line.isEmptyOrWhitespace(): - continue - # This madness will make sure we only get (hopefully) 6 entries - # in our temporary list - temp = line.split().filterIt(it != "").join(" ").split(maxsplit=6) - if len(temp) < 6: - if len(temp) < 4: - # Not enough columns! - raise newException(ValueError, "improperly formatted filesystem table") - elif len(temp) == 4: - dump = 0 - pass = 0 - elif len(temp) == 5: - dump = 0 - else: - try: - dump = parseInt(temp[4]) - except ValueError: - raise newException(ValueError, &"improperly formatted filesystem table -> invalid value ({dump}) for dump") - try: - pass = parseInt(temp[5]) - except ValueError: - raise newException(ValueError, &"improperly formatted filesystem table -> invalid value ({pass}) for pass") - if dump notin 0..1: - raise newException(ValueError, &"invalid value in filesystem table -> invalid value ({dump}) for dump") - if pass notin 0..2: - raise newException(ValueError, &"invalid value in filesystem table -> invalid value ({pass}) for pass") - if temp[0].toLowerAscii().startswith("id="): - if (let s = temp[0].split("=", maxsplit=2); len(s) < 2): - raise newException(ValueError, "improperly formatted filesystem table") - temp[0] = &"""/dev/disk/by-id/{temp[0].split("=", maxsplit=2)[1]}""" - if temp[0].toLowerAscii().startswith("label="): - if (let s = temp[0].split("=", maxsplit=2); len(s) < 2): - raise newException(ValueError, "improperly formatted filesystem table") - temp[0] = &"""/dev/disk/by-label/{temp[0].split("=", maxsplit=2)[1]}""" - if temp[0].toLowerAscii().startswith("uuid="): - if (let s = temp[0].split("=", maxsplit=2); len(s) < 2): - raise newException(ValueError, "improperly formatted filesystem table") - temp[0] = &"""/dev/disk/by-uuid/{temp[0].split("=", maxsplit=2)[1]}""" - if temp[0].toLowerAscii().startswith("partuuid="): - if (let s = temp[0].split("=", maxsplit=2); len(s) < 2): - raise newException(ValueError, "improperly formatted filesystem table") - temp[0] = &"""/dev/disk/by-partuuid/{temp[0].split("=", maxsplit=2)[1]}""" - result.add((source: temp[0], target: temp[1], filesystemtype: temp[2], mountflags: 0u64, data: temp[3], dump: uint8(dump), pass: uint8(pass))) - - -# Nim wrappers around C functionality in sys/mount.h on Linux -proc mount*(source: cstring, target: cstring, filesystemtype: cstring, - mountflags: culong, data: pointer): cint {.header: "sys/mount.h", importc.} -# Since cstrings are weak references, we need to convert nim strings to cstrings only -# when we're ready to use them and only when we're sure the underlying nim string is -# in scope, otherwise garbage collection madness happens -proc mount*(source, target, filesystemtype: string, mountflags: uint64, data: string): int = int(mount(cstring(source), cstring(target), cstring(filesystemtype), culong(mountflags), cstring(data))) - -proc umount*(target: cstring): cint {.header: "sys/mount.h", importc.} -proc umount2*(target: cstring, flags: cint): cint {.header: "sys/mount.h", importc.} -# These 2 wrappers silent the CStringConv warning -# (implicit conversion to 'cstring' from a non-const location) -proc umount*(target: string): int = int(umount(cstring(target))) -proc umount2*(target: string, flags: int): int = int(umount2(cstring(target), cint(flags))) - - -proc checkDisksIsMounted(search: tuple[source, target, filesystemtype: string, mountflags: uint64, data: string, dump, pass: uint8], expand: bool = false): bool = - ## Returns true if a disk is already mounted. If expand is true, - ## symlinks are expanded and checked instead of doing a simple - ## string comparison of the source entry point. This should be - ## true when mounting real filesystems. This returns false if - ## /proc/mounts does not exist (usually happens when /proc has - ## not been mounted yet) - if not fileExists("/proc/mounts"): - return false - for entry in parseFileSystemTable(readFile("/proc/mounts")): - if expand: - if exists(entry.source) and exists(search.source) and sameFile(entry.source, search.source): - return true - elif entry.source == search.source: - return true - return false - - -proc mountRealDisks*(logger: Logger, fstab: string = "/etc/fstab") = - ## Mounts real disks from /etc/fstab - var retcode = 0 - try: - logger.debug(&"Reading disk entries from {fstab}") - for entry in parseFileSystemTable(readFile(fstab)): - if checkDisksIsMounted(entry, expand=true): - logger.debug(&"Skipping mounting filesystem {entry.source} ({entry.filesystemtype}) at {entry.target}: already mounted") - continue - logger.debug(&"Mounting filesystem {entry.source} ({entry.filesystemtype}) at {entry.target} with mount option(s) {entry.data}") - logger.trace(&"Calling mount('{entry.source}', '{entry.target}', '{entry.filesystemtype}', {entry.mountflags}, '{entry.data}')") - retcode = mount(entry.source, entry.target, entry.filesystemtype, entry.mountflags, entry.data) - logger.trace(&"mount('{entry.source}', '{entry.target}', '{entry.filesystemtype}', {entry.mountflags}, '{entry.data}') returned {retcode}") - if retcode == -1: - logger.error(&"Mounting {entry.source} at {entry.target} has failed with error {posix.errno}: {posix.strerror(posix.errno)}") - # Resets the error code - posix.errno = cint(0) - else: - logger.debug(&"Mounted {entry.source} at {entry.target}") - except ValueError: # Check parseFileSystemTable for more info on this catch block - logger.fatal("Improperly formatted fstab, exiting") - nimDExit(logger, 131) - - -proc mountVirtualDisks*(logger: Logger) = - ## Mounts POSIX virtual filesystems/partitions, - ## such as /proc and /sys - var retcode = 0 - for entry in virtualFileSystems: - if checkDisksIsMounted(entry): - logger.debug(&"Skipping mounting filesystem {entry.source} ({entry.filesystemtype}) at {entry.target}: already mounted") - continue - logger.debug(&"Mounting filesystem {entry.source} ({entry.filesystemtype}) at {entry.target} with mount option(s) {entry.data}") - logger.trace(&"Calling mount('{entry.source}', '{entry.target}', '{entry.filesystemtype}', {entry.mountflags}, '{entry.data}')") - retcode = mount(entry.source, entry.target, entry.filesystemtype, entry.mountflags, entry.data) - logger.trace(&"mount('{entry.source}', '{entry.target}', '{entry.filesystemtype}', {entry.mountflags}, '{entry.data}') returned {retcode}") - if retcode == -1: - logger.error(&"Mounting disk {entry.source} at {entry.target} has failed with error {posix.errno}: {posix.strerror(posix.errno)}") - # Resets the error code - posix.errno = cint(0) - logger.fatal("Failed mounting vital system disk partition, system is likely corrupted, booting cannot continue") - nimDExit(logger, 131) # ENOTRECOVERABLE - State not recoverable - else: - logger.debug(&"Mounted {entry.source} at {entry.target}") - - -proc unmountAllDisks*(logger: Logger, code: int) = - ## Unmounts all currently mounted disks, including the ones that - ## were not mounted trough fstab but excluding virtual filesystems - var flag: bool = false - var retcode = 0 - try: - logger.info("Detaching real filesystems") - logger.debug(&"Reading disk entries from /proc/mounts") - for entry in parseFileSystemTable(readFile("/proc/mounts")): - if entry.source in ["proc", "sys", "run", "dev", "devpts", "shm"]: - flag = true # We don't detach the vfs - for path in ["/proc", "/sys", "/run", "/dev", "/dev/pts", "/dev/shm"]: - if entry.target.startswith(path): - flag = true - if flag: - flag = false - logger.debug(&"Skipping unmounting filesystem {entry.source} ({entry.filesystemtype}) from {entry.target} as it is a virtual filesystem") - continue - if not checkDisksIsMounted(entry): - logger.debug(&"Skipping unmounting filesystem {entry.source} ({entry.filesystemtype}) from {entry.target}: not mounted") - continue - logger.debug(&"Unmounting filesystem {entry.source} ({entry.filesystemtype}) from {entry.target}") - logger.trace(&"Calling umount2('{entry.source}', MNT_DETACH)") - retcode = umount2(entry.source, 2) # 2 = MNT_DETACH - Since we're shutting down, we need the disks to be *gone*! - logger.trace(&"umount2('{entry.source}', MNT_DETACH) returned {retcode}") - if retcode == -1: - logger.error(&"Unmounting disk {entry.source} from {entry.target} has failed with error {posix.errno}: {posix.strerror(posix.errno)}") - # Resets the error code - posix.errno = cint(0) - else: - logger.debug(&"Unmounted {entry.source} from {entry.target}") - except ValueError: # Check parseFileSystemTable for more info on this catch block - logger.fatal(&"A fatal error occurred while unmounting disks: {getCurrentExceptionMsg()}") - nimDExit(logger, 131) diff --git a/src/util/misc.nim b/src/util/misc.nim index 02f51b8..a2d3892 100644 --- a/src/util/misc.nim +++ b/src/util/misc.nim @@ -15,40 +15,41 @@ ## Default signal handlers, exit procedures and helpers ## to allow a clean shutdown of NimD import os -import strformat import strutils import syscall +import strformat +import posix import logging -# Note: This will work reliably only with absolute paths. Use with care -const symlinks: array[7, tuple[dest, source: string]] = [ - (dest: "/dev/fd", source: "/proc/self/fd"), - (dest: "/dev/fd/0", source: "/proc/self/fd/0"), - (dest: "/dev/fd/1", source: "/proc/self/fd/1"), - (dest: "/dev/fd/2", source: "/proc/self/fd/2"), - (dest: "/dev/std/in", source: "/proc/self/fd/0"), - (dest: "/dev/std/out", source: "/proc/self/fd/1"), - (dest: "/dev/std/err", source: "/proc/self/fd/2"), -] - -var shutdownHandlers: seq[proc (logger: Logger, code: int)] = @[] +proc sleepSeconds*(amount: SomeNumber) = sleep(int(amount * 1000)) +proc strsignal*(sig: cint): cstring {.header: "string.h", importc.} proc doSync*(logger: Logger) = + ## Performs a sync() system call logger.debug(&"Calling sync() syscall has returned {syscall(SYNC)}") proc reapProcess*(logger: Logger) = + ## Reaps zombie processes. Note: This does not + ## handle restarting crashed service processes, + ## it simply makes sure that there's no dead + ## process entries in the kernel's ptable. + ## When (supervised) services are started, + ## they are spawned by a controlling subprocess + ## of PID 1 which listens for changes in them + ## and restarts them as needed logger.debug("Handling SIGCHLD") - # TODO + var status: cint + discard posix.waitPid(-1, status, WNOHANG) # This doesn't hang, which is what we want proc exists*(p: string): bool = - # Checks if a path exists. Thanks - # araq :) + ## Returns true if a path exists, + ## false otherwise try: discard getFileInfo(p) result = true @@ -56,42 +57,6 @@ proc exists*(p: string): bool = result = false -proc addShutdownHandler*(handler: proc (logger: Logger, code: int), logger: Logger) = - shutdownHandlers.add(handler) - - -proc removeShutdownHandler*(handler: proc (logger: Logger, code: int)) = - for i, h in shutdownHandlers: - if h == handler: - shutdownHandlers.delete(i) - - -proc nimDExit*(logger: Logger, code: int, emerg: bool = true) = - logger.warning("The system is shutting down") - # TODO - logger.info("Processing shutdown runlevel") - # TODO - logger.info("Running shutdown handlers") - try: - for handler in shutdownHandlers: - handler(logger, code) - except: - logger.error(&"An error has occurred while calling shutdown handlers. Error -> {getCurrentExceptionMsg()}") - # Note: continues calling handlers! - if emerg: - # 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") - discard execShellCmd("/bin/sh") # TODO: Is this fine? maybe use execProcess - else: - logger.info("Terminating child processes with SIGINT") - # TODO - logger.info("Terminating child processes with SIGKILL") - # TODO - logger.warning("Shutdown procedure complete, sending final termination signal") - # TODO - quit(code) - - proc setHostname*(logger: Logger): string = ## Sets the machine's hostname. Returns ## the hostname that has been set or an @@ -110,33 +75,3 @@ proc setHostname*(logger: Logger): string = logger.error(&"An error occurred while setting hostname -> {getCurrentExceptionMsg()}") return "" return hostname - - -proc createSymlinks*(logger: Logger) = - ## Creates a set of symlinks needed - ## by stuff like Linux ports of BSD - ## software - for sym in symlinks: - try: - if not exists(sym.source): - logger.warning(&"Skipping creation of symbolic link from {sym.dest} to {sym.source}: destination does not exist") - continue - elif exists(sym.dest): - if symlinkExists(sym.dest) and sameFile(expandSymlink(sym.dest), sym.source): - logger.debug(&"Skipping creation of symbolic link from {sym.dest} to {sym.source}: link already exists") - elif symlinkExists(sym.dest) and not sameFile(expandSymlink(sym.dest), sym.source): - logger.warning(&"Attempted to create symbolic link from {sym.dest} to {sym.source}, but link already exists and points to {expandSymlink(sym.dest)}") - continue - logger.debug(&"Creating symbolic link from {sym.source} to {sym.dest}") - createSymlink(sym.dest, sym.source) - except: - logger.error(&"Failed to create symbolic link from {sym.dest} to {sym.source}: {getCurrentExceptionMsg()}") - - -proc createDirectories*(logger: Logger) = - ## Creates - -proc sleepSeconds*(amount: SomeInteger) = sleep(amount * 1000) - - -proc strsignal*(sig: cint): cstring {.header: "string.h", importc.}