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
|
||||
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
|
||||
|
||||
|
|
|
@ -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.
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
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!",
|
||||
|
|
|
@ -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
|
||||
## 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.}
|
||||
|
|
Loading…
Reference in New Issue