mirror of https://github.com/nocturn9x/nimd.git
Major code cleanup. Added initial (VERY broken) support for services
This commit is contained in:
parent
bd7d4e1974
commit
5777e1a715
|
@ -6,7 +6,7 @@ WORKDIR /code
|
||||||
# Removes any already existing binary so that when compilation fails the container stops
|
# Removes any already existing binary so that when compilation fails the container stops
|
||||||
RUN rm -f /code/nimd
|
RUN rm -f /code/nimd
|
||||||
RUN rm -f /code/main
|
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 nim -d:release --opt:size --passL:"-static" --gc:orc -d:useMalloc c -o:nimd src/main
|
||||||
RUN cp /code/nimd /sbin/nimd
|
RUN cp /code/nimd /sbin/nimd
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
@ -13,38 +13,17 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import segfaults # Makes us catch segfaults as NilAccessDefect exceptions!
|
import segfaults # Makes us catch segfaults as NilAccessDefect exceptions!
|
||||||
import strformat
|
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
|
## 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")
|
logger.info("Processing default runlevel")
|
||||||
# TODO
|
startServices(logger, workers=1, level=Default)
|
||||||
logger.info("System initialization complete, going idle")
|
logger.info("System initialization complete, going idle")
|
||||||
while true:
|
while true:
|
||||||
try:
|
try:
|
||||||
|
@ -53,4 +32,4 @@ proc mainLoop*(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fs
|
||||||
except:
|
except:
|
||||||
logger.critical(&"A critical error has occurred while running, restarting the mainloop! Error -> {getCurrentExceptionMsg()}")
|
logger.critical(&"A critical error has occurred while running, restarting the mainloop! Error -> {getCurrentExceptionMsg()}")
|
||||||
# We *absolutely* cannot die
|
# We *absolutely* cannot die
|
||||||
mainLoop(logger, mountDisks=false)
|
mainLoop(logger)
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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)
|
72
src/main.nim
72
src/main.nim
|
@ -18,13 +18,15 @@ import os
|
||||||
|
|
||||||
# NimD's own stuff
|
# NimD's own stuff
|
||||||
import util/[logging, constants, misc]
|
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.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()")
|
logger.trace("Calling getCurrentProcessId()")
|
||||||
let pid = getCurrentProcessId()
|
let pid = getCurrentProcessId()
|
||||||
logger.trace(&"getCurrentProcessId() returned {pid}")
|
logger.trace(&"getCurrentProcessId() returned {pid}")
|
||||||
|
@ -35,17 +37,66 @@ proc main(logger: Logger) =
|
||||||
logger.trace(&"getuid() returned {uid}")
|
logger.trace(&"getuid() returned {uid}")
|
||||||
if uid != 0:
|
if uid != 0:
|
||||||
logger.fatal(&"NimD must run as root, but current user id is {uid}")
|
logger.fatal(&"NimD must run as root, but current user id is {uid}")
|
||||||
quit(EPERM) # EPERM - Operation not permitted
|
nimDExit(logger, EPERM) # EPERM - Operation not permitted
|
||||||
logger.debug("Setting up dummy signal handlers")
|
logger.trace("Setting up signal handlers")
|
||||||
onSignal(SIGABRT, SIGALRM, SIGHUP, SIGILL, SIGKILL, SIGQUIT, SIGSTOP, SIGSEGV, SIGTSTP,
|
onSignal(SIGABRT, SIGALRM, SIGHUP, SIGILL, SIGKILL, SIGQUIT, SIGSTOP, SIGTSTP,
|
||||||
SIGTRAP, SIGTERM, SIGPIPE, SIGUSR1, SIGUSR2, 6, SIGFPE, SIGBUS, SIGURG, SIGINT): # 6 is SIGIOT
|
SIGTRAP, SIGTERM, SIGPIPE, SIGUSR1, SIGUSR2, 6, SIGFPE, SIGBUS, SIGURG, SIGINT): # 6 is SIGIOT
|
||||||
# Can't capture local variables because this implicitly generates
|
# 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
|
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):
|
onSignal(SIGCHLD):
|
||||||
|
# One of the key features of an init system is reaping child
|
||||||
|
# processes!
|
||||||
reapProcess(getDefaultLogger())
|
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)
|
mainLoop(logger)
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,6 +144,7 @@ when isMainModule:
|
||||||
try:
|
try:
|
||||||
main(logger)
|
main(logger)
|
||||||
except:
|
except:
|
||||||
|
raise
|
||||||
logger.fatal(&"A fatal unrecoverable error has occurred during startup and NimD cannot continue: {getCurrentExceptionMsg()}")
|
logger.fatal(&"A fatal unrecoverable error has occurred during startup and NimD cannot continue: {getCurrentExceptionMsg()}")
|
||||||
nimDExit(logger, 131) # ENOTRECOVERABLE - State not recoverable
|
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!",
|
# This will almost certainly cause the kernel to crash with an error the likes of "Kernel not syncing, attempted to kill init!",
|
||||||
|
|
|
@ -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)
|
|
|
@ -15,40 +15,41 @@
|
||||||
## Default signal handlers, exit procedures and helpers
|
## Default signal handlers, exit procedures and helpers
|
||||||
## to allow a clean shutdown of NimD
|
## to allow a clean shutdown of NimD
|
||||||
import os
|
import os
|
||||||
import strformat
|
|
||||||
import strutils
|
import strutils
|
||||||
import syscall
|
import syscall
|
||||||
|
import strformat
|
||||||
|
import posix
|
||||||
|
|
||||||
|
|
||||||
import logging
|
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"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
proc sleepSeconds*(amount: SomeNumber) = sleep(int(amount * 1000))
|
||||||
var shutdownHandlers: seq[proc (logger: Logger, code: int)] = @[]
|
proc strsignal*(sig: cint): cstring {.header: "string.h", importc.}
|
||||||
|
|
||||||
|
|
||||||
proc doSync*(logger: Logger) =
|
proc doSync*(logger: Logger) =
|
||||||
|
## Performs a sync() system call
|
||||||
logger.debug(&"Calling sync() syscall has returned {syscall(SYNC)}")
|
logger.debug(&"Calling sync() syscall has returned {syscall(SYNC)}")
|
||||||
|
|
||||||
|
|
||||||
proc reapProcess*(logger: Logger) =
|
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")
|
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 =
|
proc exists*(p: string): bool =
|
||||||
# Checks if a path exists. Thanks
|
## Returns true if a path exists,
|
||||||
# araq :)
|
## false otherwise
|
||||||
try:
|
try:
|
||||||
discard getFileInfo(p)
|
discard getFileInfo(p)
|
||||||
result = true
|
result = true
|
||||||
|
@ -56,42 +57,6 @@ proc exists*(p: string): bool =
|
||||||
result = false
|
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 =
|
proc setHostname*(logger: Logger): string =
|
||||||
## Sets the machine's hostname. Returns
|
## Sets the machine's hostname. Returns
|
||||||
## the hostname that has been set or an
|
## 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()}")
|
logger.error(&"An error occurred while setting hostname -> {getCurrentExceptionMsg()}")
|
||||||
return ""
|
return ""
|
||||||
return hostname
|
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.}
|
|
||||||
|
|
Loading…
Reference in New Issue