Major code cleanup. Added initial (VERY broken) support for services

This commit is contained in:
Nocturn9x 2021-12-04 13:47:06 +01:00
parent bd7d4e1974
commit 5777e1a715
8 changed files with 747 additions and 335 deletions

View File

@ -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

359
src/core/fs.nim Normal file
View File

@ -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.

View File

@ -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)

192
src/core/services.nim Normal file
View File

@ -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])

110
src/core/shutdown.nim Normal file
View File

@ -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/<pid>/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)

View File

@ -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!",

View File

@ -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)

View File

@ -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.}