From fd35755da5fc5a0d7130962997c158fbbac1d228 Mon Sep 17 00:00:00 2001 From: Nocturn9x Date: Thu, 9 Dec 2021 17:09:00 +0100 Subject: [PATCH] Hopefully fixed dependency resolution. Added file locking on logs. Improved README, fixed some minor issues --- README.md | 25 ++++-- src/core/fs.nim | 9 +- src/core/services.nim | 196 +++++++++++++++++++++++++++++------------- src/main.nim | 22 ++--- src/util/logging.nim | 103 ++++++++++++++-------- 5 files changed, 236 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 191674b..55c8432 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,21 @@ stdout = /var/log/sshd # Path of the stdout log for the service stdin = /dev/null # Path of the stdin fd for the service ``` -A dependency name can either be the name of a unit file (without the `.conf` extension), or one of the following placeholders: +__Note__: Unsupervised services cannot be restarted, as NimD has no control over them once they're spawned. + + +A dependency name can either be the name of a unit file (case sensitive, but without the `.conf` extension), or one of the following placeholders: - `net` -> Stands for network connection. Services like NetworkManager and dhcpcd should be set as providers for this - `fs` -> If you mount your disks using a oneshot service (recommended for the best experience), your service should provide this +- `ssh` -> The service provides some sort of SSH functionality +- `ftp` -> The service provides an FTP server +- `http` -> The service is an HTTP webserver +Note that NimD resolves placeholders before service names: this means that if you have a service named `ssh.conf`, using `ssh` as +a dependency will __not__ set that service as a dependency and will __not__ override the default behavior unless said unit file also has +`provides=ssh` in it. Also note that multiple providers for the same service raise a warning by default and cause NimD to let the alphabet decide +which dependency is started (i.e. they are sorted lexicographically by their filename, without the extension, and the first is picked), but this +behavior can be changed (e.g. raising an error instead) ## Configuring NimD @@ -94,12 +105,14 @@ logFile = /var/log/nimd # Path to log file [Filesystem] -autoMount = true # Automatically parses /etc/fstab and mounts disks -fstabPath = /etc/fstab # Path to your system's fstab (defaults to /etc/fstab) -createDirs = /path/to/dir1, /path/to/dir2 # Creates these directories on boot. Empty to disable -createSymlinks = /path/to/dir1, /path/to/dir2 # Creates these symlinks on boot. Empty to disable +autoMount = true # Automatically parses /etc/fstab and mounts disks +autoUnmount = true # Automatically parses /proc/mounts and unmounts everything on shutdown +fstabPath = /etc/fstab # Path to your system's fstab (defaults to /etc/fstab) +createDirs = /path/to/dir1, /path/to/dir2 # Creates these directories on boot. Empty to disable +createSymlinks = /path/to/symlink:/path/to/dest, ... # Creates these symlinks on boot. Empty to disable [Misc] -controlSocket = /var/run/nimd.sock # Path to the Unix domain socket to create for IPC +controlSocket = /var/run/nimd.sock # Path to the Unix domain socket to create for IPC +onDependencyConflict = skip # Other option: warn, error ``` \ No newline at end of file diff --git a/src/core/fs.nim b/src/core/fs.nim index df32e2e..d202f57 100644 --- a/src/core/fs.nim +++ b/src/core/fs.nim @@ -349,13 +349,14 @@ proc createDirectories*(logger: Logger) = ## in their config. Note that the entire path ## of the directory is created if it does not ## exist yet - var hasChmod = false + var hasChmod = true try: if findExe("chmod").isEmptyOrWhitespace(): logger.warning("Could not find chmod binary, directory permissions will default to OS configuration") - hasChmod = true + hasChmod = false except: - logger.error(&"Failed to search for chmod binary: {getCurrentExceptionMsg()}") + logger.error(&"Failed to search for chmod binary, directory permissions will default to OS configuration: {getCurrentExceptionMsg()}") + hasChmod = false for dir in directories: try: if exists(dir.path): @@ -374,6 +375,6 @@ proc createDirectories*(logger: Logger) = if hasChmod: logger.debug(&"Setting permissions to {dir.permissions} for {dir.path}") if (let code = execShellCmd(&"chmod -R {dir.permissions} {dir.path}"); code) != 0: - logger.warning(&"Command 'chmod -R {dir.permissions}' exited non-zero status code {code}") + logger.warning(&"Command 'chmod -R {dir.permissions} {dir.path}' exited non-zero status code {code}") except: logger.error(&"Failed to create directory at {dir.path}: {getCurrentExceptionMsg()}") diff --git a/src/core/services.nim b/src/core/services.nim index d58ecf2..8d836bb 100644 --- a/src/core/services.nim +++ b/src/core/services.nim @@ -30,7 +30,9 @@ import ../util/logging type RunLevel* = enum ## Enum of possible runlevels - Boot, Default, Shutdown + Boot = 0, + Default, + Shutdown ServiceKind* = enum ## Enumerates all service ## types @@ -38,6 +40,14 @@ type RestartKind* = enum ## Enum of possible restart modes Always, OnFailure, Never + DependencyKind* = enum + ## Enum of possible dependencies + Network, Filesystem, + Ssh, Ftp, Http, Other + Dependency* = ref object + ## A dependency + kind*: DependencyKind + provider*: Service Service* = ref object of RootObj ## A service object name: string @@ -49,17 +59,28 @@ type supervised: bool restart: RestartKind restartDelay: int - depends*: seq[Service] - provides*: seq[Service] + depends: seq[Dependency] + provides: seq[Dependency] + ## These two fields are + ## used by the dependency + ## resolver + isMarked: bool + isResolved: bool + + + +proc newDependency*(kind: DependencyKind, provider: Service): Dependency = + ## Creates a new dependency object + result = Dependency(kind: kind, provider: provider) proc newService*(name, description: string, kind: ServiceKind, workDir: string, runlevel: RunLevel, exec: string, supervised: bool, restart: RestartKind, - restartDelay: int, depends, provides: seq[Service]): Service = + restartDelay: int, depends, provides: seq[Dependency]): Service = ## Creates a new service object result = Service(name: name, description: description, kind: kind, workDir: workDir, runLevel: runLevel, exec: exec, supervised: supervised, restart: restart, restartDelay: restartDelay, - depends: depends, provides: provides) - result.provides.add(result) + depends: depends, provides: provides, isMarked: false, isResolved: false) + result.provides.add(newDependency(Other, result)) proc extend[T](self: var seq[T], other: seq[T]) = @@ -72,34 +93,83 @@ var services: seq[Service] = @[] var processIDs: TableRef[int, Service] = newTable[int, Service]() -proc resolveDependencies(logger: Logger, node: Service, resolved, unresolved: var seq[Service]) = - ## Resolves dependencies and modifies the resolved - ## parameter in place to a list that satisfies the - ## dependency tree. This is basically traversing - ## a directed cyclic graph, although note that cycles - ## in our graph are errors and cause the dependants and - ## the providers to be skipped and an error to be logged - - # Note: It turns out this is an NP-hard problem (see https://stackoverflow.com/a/28102139/12159081), - # so hopefully this doesn't blow up. No wonder runit doesn't do any dependency resolution, lol. - # The algorithm comes from https://www.electricmonk.nl/log/2008/08/07/dependency-resolving-algorithm/ - # and has been extended to support the dependent-provider paradigm - var ok = true - unresolved.add(node) - for dependency in node.depends: - if dependency notin resolved: - if dependency in unresolved: - logger.error(&"Could not resolve dependencies for '{node.name}' -> '{dependency.name}': cyclic dependency detected") +proc resolve(logger: Logger, node: Service): seq[Service] = + ## Returns a sorted list of services according + ## to their dependency and provider requirements. + ## This function recursively iterates over the + ## list of services, treating it as a DAG + ## (Directed Acyclic Graph) and builds a topologically + ## sorted list such that a service appears in it only + ## after all of its dependencies and only + ## before all of its dependents. + ## This function also automatically handles + ## detached subgraphs, which can occurr if + ## one or more dependencies have common + ## dependencies/dependents between each other, + ## but not with the rest of the graph. Nodes + ## that have no dependencies nor provide any + ## service may be located anywhere in the list, + ## as that does not invalidate the invariants + ## described above. The algorithm comes from + ## https://www.electricmonk.nl/log/2008/08/07/dependency-resolving-algorithm/ + ## and has been extended to support the dependent-provider paradigm. + ## Note that it is not an error for a service in a given runlevel to depend + ## on services in other runlevels: when that occurs a warning is raised and + ## the service in the lower runlevel is promoted to the higher one (runlevels start from 0), + ## which means adding a module in a given runlevel implicitly adds all of its dependencies + ## to said runlevel as well, regardless of what was specified in their unit file + if node.isResolved: + logger.debug(&"Dependency '{node.name}' has already been satisfied, skipping it") + return @[] + var ok: bool = true + result = @[] + node.isMarked = true + for service in node.provides: + if service.provider == node: + continue # Services implicitly provide themselves + if node.runlevel < service.provider.runlevel: + logger.warning(&"Service '{node.name}' in runlevel {node.runlevel} depends on '{service.provider.name}' in runlevel {service.provider.runlevel}, loading dependency regardless") + if not service.provider.isResolved: + if service.provider.isMarked: + logger.warning(&"Cyclic dependency from '{node.name}' to '{service.provider.name}' detected while building dependency graph: skipping both") ok = false - continue - resolveDependencies(logger, dependency, resolved, unresolved) - for dependency in node.provides: - if dependency == node: - continue - resolveDependencies(logger, dependency, resolved, unresolved) + break + service.provider.isMarked = true + result.extend(resolve(logger, service.provider)) + for service in node.depends: + if service.provider == node: + logger.warning(&"Cyclic dependency from '{node.name}' to itself detected while building dependency graph: skipping it") + ok = false + break + if node.runlevel > service.provider.runlevel: + logger.warning(&"Service '{node.name}' in runlevel {node.runlevel} depends on '{service.provider.name}' in runlevel {service.provider.runlevel}, loading both") + if not service.provider.isResolved: + if service.provider.isMarked: + logger.warning(&"Cyclic dependency from '{node.name}' to '{service.provider.name}' detected while building dependency graph: skipping both") + ok = false + break + service.provider.isMarked = true + result.extend(resolve(logger, service.provider)) if ok: - resolved.add(node) - unresolved.del(unresolved.find(node)) + result.add(node) + node.isResolved = true + node.isMarked = false + + +proc resolveDependencies(logger: Logger, services: seq[Service], level: RunLevel): seq[Service] = + ## Iteratively calls resolve() until all services + ## have been processed + result = @[] + var node: Service + var i = 1 + var s: seq[Service] = @[] + for service in services: + if service.runlevel == level: + s.add(service) + while i <= len(s): + node = s[^i] + result.extend(resolve(logger, node)) + inc(i) proc isManagedProcess*(pid: int): bool = @@ -239,47 +309,49 @@ proc startService(logger: Logger, service: Service) = proc startServices*(logger: Logger, level: RunLevel, workers: int = 1) = ## Starts the registered services in the ## given runlevel - var resolved: seq[Service] = @[] - var unresolved: seq[Service] = @[] - resolveDependencies(logger, services[0], resolved, unresolved) + if len(services) == 0: + return + var dependencies = resolveDependencies(logger, services, level) if workers > cpuinfo.countProcessors(): logger.warning(&"The configured number of workers ({workers}) is greater than the number of CPU cores ({cpuinfo.countProcessors()}), performance may degrade") - var workerCount: int = 0 var status: cint var pid: int = posix.fork() + var pids: seq[int] = @[] if pid == -1: logger.error(&"Error, cannot fork: {posix.strerror(posix.errno)}") elif pid == 0: + var service: Service logger.debug("Started service spawner process") - var servicesCopy: seq[Service] = @[] - for service in services: - if service.runlevel == level: - servicesCopy.add(service) - while servicesCopy.len() > 0: - if workerCount == workers: - logger.debug(&"Worker queue full, waiting for some worker to exit...") - logger.trace(&"Calling waitpid() on {pid}") - var returnCode = waitPid(cint(pid), status, WUNTRACED) - logger.trace(&"Call to waitpid() set status to {status} and returned {returnCode}") - dec(workerCount) - 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.trace(&"New child has been spawned") - if not servicesCopy[0].supervised or servicesCopy[0].kind == Oneshot: - logger.info(&"""Starting {(if servicesCopy[0].kind != Oneshot: "unsupervised" else: "oneshot")} service '{servicesCopy[0].name}'""") + while dependencies.len() > 0: + for _ in 0.. 0: - workerCount += 1 - if servicesCopy[0].supervised: - addManagedProcess(pid, servicesCopy[0]) - servicesCopy.del(0) + pids.add(pid) + if service.supervised: + addManagedProcess(pid, service) + if len(pids) == workers: + logger.debug(&"""Worker queue full, waiting for some worker{(if workers > 1: "s" else: "")} to exit...""") + for i, pid in pids: + logger.trace(&"Calling waitpid() on {pid}") + var returnCode = waitPid(cint(pid), status, WUNTRACED) + logger.trace(&"Call to waitpid() on {pid} set status to {status} and returned {returnCode}") + pids = @[] quit(0) else: logger.debug(&"Waiting for completion of service spawning in runlevel {($level).toLowerAscii()}") logger.trace(&"Calling waitpid() on {pid}") var returnCode = waitPid(cint(pid), status, WUNTRACED) - logger.trace(&"Call to waitpid() set status to {status} and returned {returnCode}") \ No newline at end of file + logger.trace(&"Call to waitpid() on {pid} set status to {status} and returned {returnCode}") \ No newline at end of file diff --git a/src/main.nim b/src/main.nim index fc4db89..3f9448f 100644 --- a/src/main.nim +++ b/src/main.nim @@ -36,11 +36,10 @@ proc addStuff = # 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="/does/not/exist")) # Should say destination does not exist addSymlink(newSymlink(dest="/dev/std/in", source="/proc/self/fd/0")) # Should say link already exists addDirectory(newDirectory("test", 777)) # Should create a directory addDirectory(newDirectory("/dev/disk", 123)) # Should say directory already exists - addDirectory(newDirectory("/dev/test/owo", 000)) # Should say path does not exist # Shutdown handler to unmount disks addShutdownHandler(newShutdownHandler(unmountAllDisks)) # Adds test services @@ -51,25 +50,25 @@ proc addStuff = var errorer = newService(name="errorer", description="la mamma di gavd", exec="/bin/false", supervised=true, restart=OnFailure, restartDelay=5, runlevel=Boot, workDir="/", kind=Simple, - depends=(@[echoer]), provides=(@[])) + depends=(@[newDependency(Other, echoer)]), provides=(@[])) var test = newService(name="broken", description="", exec="/bin/echo owo", runlevel=Boot, kind=Oneshot, workDir=getCurrentDir(), supervised=false, restart=Never, restartDelay=0, - depends=(@[echoer]), provides=(@[])) + depends=(@[newDependency(Other, echoer)]), provides=(@[])) var exiter = newService(name="exiter", description="la mamma di licenziat", exec="/bin/true", supervised=true, restart=Always, restartDelay=5, runlevel=Boot, workDir="/", kind=Simple, - depends=(@[errorer]), provides=(@[])) + depends=(@[newDependency(Other, errorer)]), provides=(@[])) addService(errorer) addService(echoer) addService(exiter) addService(test) - echoer.depends.add(test) -proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab") = +proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab", setHostname: bool = true, workerCount: int = 1) = ## NimD's entry point and setup ## function + setStdIoUnbuffered() # Colors and output synchronization don't work otherwise 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.trace("Calling getCurrentProcessId()") @@ -110,12 +109,15 @@ proc main(logger: Logger, mountDisks: bool = true, fstab: string = "/etc/fstab") except: logger.fatal(&"A fatal error has occurred while preparing filesystem, booting cannot continue. Error -> {getCurrentExceptionMsg()}") nimDExit(logger, 131, emerg=false) - logger.info("Setting hostname") - logger.debug(&"Hostname was set to '{setHostname(logger)}'") + if setHostname: + logger.info("Setting hostname") + logger.debug(&"Hostname was set to '{misc.setHostname(logger)}'") + else: + logger.info("Skipping setting hostname") logger.debug("Entering critical fork() section: blocking signals") blockSignals(logger) # They are later unblocked in mainLoop logger.info("Processing boot runlevel") - startServices(logger, workers=1, level=Boot) + startServices(logger, workers=workerCount, level=Boot) logger.debug("Starting main loop") mainLoop(logger) diff --git a/src/util/logging.nim b/src/util/logging.nim index 6874c10..75e9905 100644 --- a/src/util/logging.nim +++ b/src/util/logging.nim @@ -40,7 +40,7 @@ type var defaultLevel = LogLevel.Info var logFile = "/var/log/nimd" -var logToFile: bool = false +var logToFileOnly: bool = false proc log(self: Logger, level: LogLevel = defaultLevel, message: string) # Forward declaration @@ -79,52 +79,71 @@ proc log(self: Logger, level: LogLevel = defaultLevel, message: string) = # Do NOT touch the alignment offsets or your console output and logs will look like trash +proc lockFile(logger: Logger, handle: File) = + ## Locks the given file across the whole system for writing using fcntl() + if fcntl(handle.getFileHandle(), F_WRLCK) == -1: + stderr.writeLine(&"Error while locking handle (code {posix.errno}, {posix.strerror(posix.errno)}): output may be mangled") + + +proc unlockFile(logger: Logger, handle: File) = + ## Unlocks the given file across the whole system for writing using fcntl() + if fcntl(handle.getFileHandle(), F_UNLCK) == -1: + stderr.writeLine(&"Error while locking stderr (code {posix.errno}, {posix.strerror(posix.errno)}): output may be mangled") + + proc logTraceStderr(self: LogHandler, logger: Logger, message: string) = + logger.lockFile(stderr) setForegroundColor(fgMagenta) stderr.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} TRACE {"-":>3} ({posix.getpid():03})] {message}""") - stderr.flushFile() setForegroundColor(fgDefault) + logger.unlockFile(stderr) proc logDebugStderr(self: LogHandler, logger: Logger, message: string) = + logger.lockFile(stderr) setForegroundColor(fgCyan) stderr.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} DEBUG {"-":>3} ({posix.getpid():03})] {message}""") - stderr.flushFile() setForegroundColor(fgDefault) + logger.unlockFile(stderr) proc logInfoStderr(self: LogHandler, logger: Logger, message: string) = + logger.lockFile(stderr) setForegroundColor(fgGreen) stderr.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} INFO {"-":>4} ({posix.getpid():03})] {message}""") - stderr.flushFile() setForegroundColor(fgDefault) + logger.unlockFile(stderr) proc logWarningStderr(self: LogHandler, logger: Logger, message: string) = + logger.lockFile(stderr) setForegroundColor(fgYellow) stderr.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} WARNING {"-":>1} ({posix.getpid():03})] {message}""") - stderr.flushFile() setForegroundColor(fgDefault) + logger.unlockFile(stderr) proc logErrorStderr(self: LogHandler, logger: Logger, message: string) = + logger.lockFile(stderr) setForegroundColor(fgRed) stderr.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} ERROR {"-":>3} ({posix.getpid():03})] {message}""") - stderr.flushFile() setForegroundColor(fgDefault) + logger.unlockFile(stderr) proc logCriticalStderr(self: LogHandler, logger: Logger, message: string) = + logger.lockFile(stderr) setForegroundColor(fgYellow) setBackgroundColor(bgRed) stderr.write(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<4} {"-":>1} CRITICAL {"-":>2} ({posix.getpid():03})]""") setBackgroundColor(bgDefault) stderr.writeLine(&""" {message}""") setForegroundColor(fgDefault) - stderr.flushFile() + logger.unlockFile(stderr) proc logFatalStderr(self: LogHandler, logger: Logger, message: string) = + logger.lockFile(stderr) setForegroundColor(fgBlack) setBackgroundColor(bgRed) stderr.write(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<5} {"-":>1} {"":>1} FATAL {"-":>3} ({posix.getpid():03})]""") @@ -132,51 +151,64 @@ proc logFatalStderr(self: LogHandler, logger: Logger, message: string) = setBackgroundColor(bgDefault) stderr.writeline(&""" {message}""") setForegroundColor(fgDefault) - stderr.flushFile() - + logger.unlockFile(stderr) + proc logTraceFile(self: LogHandler, logger: Logger, message: string) = - StreamHandler(self).file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} TRACE {"-":>3} ({posix.getpid():03})] {message}""") - StreamHandler(self).file.flushFile() + var self = StreamHandler(self) + logger.lockFile(self.file) + self.file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} TRACE {"-":>3} ({posix.getpid():03})] {message}""") + logger.unlockFile(self.file) proc logDebugFile(self: LogHandler, logger: Logger, message: string) = - StreamHandler(self).file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} DEBUG {"-":>3} ({posix.getpid():03})] {message}""") - StreamHandler(self).file.flushFile() + var self = StreamHandler(self) + logger.lockFile(self.file) + self.file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} DEBUG {"-":>3} ({posix.getpid():03})] {message}""") + logger.unlockFile(self.file) proc logInfoFile(self: LogHandler, logger: Logger, message: string) = - StreamHandler(self).file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} INFO {"-":>4} ({posix.getpid():03})] {message}""") - StreamHandler(self).file.flushFile() + var self = StreamHandler(self) + logger.lockFile(self.file) + self.file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} INFO {"-":>4} ({posix.getpid():03})] {message}""") + logger.unlockFile(self.file) proc logWarningFile(self: LogHandler, logger: Logger, message: string) = - StreamHandler(self).file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} WARNING {"-":>1} ({posix.getpid():03})] {message}""") - StreamHandler(self).file.flushFile() + var self = StreamHandler(self) + logger.lockFile(self.file) + self.file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} WARNING {"-":>1} ({posix.getpid():03})] {message}""") + logger.unlockFile(self.file) proc logErrorFile(self: LogHandler, logger: Logger, message: string) = - StreamHandler(self).file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} ERROR {"-":>3} ({posix.getpid():03})] {message}""") - StreamHandler(self).file.flushFile() + var self = StreamHandler(self) + logger.lockFile(self.file) + self.file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<10} {"-":>1} {"":>1} ERROR {"-":>3} ({posix.getpid():03})] {message}""") + logger.unlockFile(self.file) proc logCriticalFile(self: LogHandler, logger: Logger, message: string) = - StreamHandler(self).file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<4} {"-":>1} CRITICAL {"-":>2} ({posix.getpid():03})] {message}""") - StreamHandler(self).file.flushFile() + var self = StreamHandler(self) + logger.lockFile(self.file) + self.file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<4} {"-":>1} CRITICAL {"-":>2} ({posix.getpid():03})] {message}""") + logger.unlockFile(self.file) proc logFatalFile(self: LogHandler, logger: Logger, message: string) = - StreamHandler(self).file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<5} {"-":>1} {"":>1} FATAL {"-":>3} ({posix.getpid():03})] {message}""") - StreamHandler(self).file.flushFile() + var self = StreamHandler(self) + logger.lockFile(self.file) + self.file.writeLine(&"""[{fromUnix(getTime().toUnixFloat().int).format("d/M/yyyy HH:mm:ss"):<5} {"-":>1} {"":>1} FATAL {"-":>3} ({posix.getpid():03})] {message}""") + logger.unlockFile(self.file) proc switchToFile*(self: Logger) = ## Switches logging to file and ## changes the behavior of getDefaultLogger ## accordingly - if logToFile: + if logToFileOnly: return - logToFile = true self.handlers = @[] # Don't you love it when you can just let the GC manage memory for you? self.addHandler(createStreamHandler(logTraceFile, LogLevel.Trace, logFile)) self.addHandler(createStreamHandler(logDebugFile, LogLevel.Debug, logFile)) @@ -191,9 +223,8 @@ proc switchToConsole*(self: Logger) = ## Switches logging to the console and ## changes the behavior of getDefaultLogger ## accordingly - if not logToFile: + if not logToFileOnly: return - logToFile = false self.handlers = @[] self.addHandler(createHandler(logTraceStderr, LogLevel.Trace)) self.addHandler(createHandler(logDebugStderr, LogLevel.Debug)) @@ -211,8 +242,7 @@ proc getDefaultLogger*(): Logger = ## standard error with some basic info like the ## current date and time and the log level result = newLogger() - if not logToFile: - setStdIoUnbuffered() # Colors don't work otherwise! + if not logToFileOnly: result.addHandler(createHandler(logTraceStderr, LogLevel.Trace)) result.addHandler(createHandler(logDebugStderr, LogLevel.Debug)) result.addHandler(createHandler(logInfoStderr, LogLevel.Info)) @@ -220,11 +250,10 @@ proc getDefaultLogger*(): Logger = result.addHandler(createHandler(logErrorStderr, LogLevel.Error)) result.addHandler(createHandler(logCriticalStderr, LogLevel.Critical)) result.addHandler(createHandler(logFatalStderr, LogLevel.Fatal)) - else: - result.addHandler(createStreamHandler(logTraceFile, LogLevel.Trace, logFile)) - result.addHandler(createStreamHandler(logDebugFile, LogLevel.Debug, logFile)) - result.addHandler(createStreamHandler(logInfoFile, LogLevel.Info, logFile)) - result.addHandler(createStreamHandler(logWarningFile, LogLevel.Warning, logFile)) - result.addHandler(createStreamHandler(logErrorFile, LogLevel.Error, logFile)) - result.addHandler(createStreamHandler(logCriticalFile, LogLevel.Critical, logFile)) - result.addHandler(createStreamHandler(logFatalFile, LogLevel.Fatal, logFile)) + result.addHandler(createStreamHandler(logTraceFile, LogLevel.Trace, logFile)) + result.addHandler(createStreamHandler(logDebugFile, LogLevel.Debug, logFile)) + result.addHandler(createStreamHandler(logInfoFile, LogLevel.Info, logFile)) + result.addHandler(createStreamHandler(logWarningFile, LogLevel.Warning, logFile)) + result.addHandler(createStreamHandler(logErrorFile, LogLevel.Error, logFile)) + result.addHandler(createStreamHandler(logCriticalFile, LogLevel.Critical, logFile)) + result.addHandler(createStreamHandler(logFatalFile, LogLevel.Fatal, logFile))