3 Commits
0.7.1 ... 0.8.1

3 changed files with 66 additions and 9 deletions

View File

@ -1,6 +1,6 @@
# Package
version = "0.7.1"
version = "0.8.1"
author = "Jonathan Bernard"
description = "Helper functions for writing command line interfaces."
license = "MIT"

View File

@ -1,4 +1,5 @@
import docopt, json, nre, os, strtabs, strutils
import std/[json, nre, os, sequtils, strtabs, strutils]
import docopt
type
CombinedConfig* = object
@ -34,6 +35,22 @@ template walkFieldDefs*(t: NimNode, body: untyped) =
# cfgType.walkFieldDefs:
# let valLookup = quote do: `getVal`(
proc initCombinedConfig*(
filename: string,
docopt: Table[string, Value] = initTable[string, Value]()
): CombinedConfig =
result = CombinedConfig(docopt: docopt, json: parseFile(filename))
proc findConfigFile*(name: string, otherLocations: seq[string] = @[]): string =
let cfgLocations = @[
$getEnv(name.strip(chars = {'.'}).toUpper()),
$getEnv("HOME") / name ] & otherLocations
result = cfgLocations.foldl(if fileExists(b): b else: a, "")
if result == "" or not fileExists(result):
raise newException(ValueError, "could not find configuration file")
proc getVal*(cfg: CombinedConfig, key: string): string =
let argKey = "--" & key
let envKey = key.replace('-', '_').toUpper
@ -57,6 +74,20 @@ proc getVal*(cfg: CombinedConfig, key, default: string): string =
try: return getVal(cfg, key)
except: return default
proc getJson*(cfg: CombinedConfig, key: string): JsonNode =
let argKey = "--" & key
let envKey = key.replace('-', '_').toUpper
let jsonKey = key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper)
if cfg.docopt.contains(argKey) and cfg.docopt[argKey]: return parseJson($cfg.docopt[argKey])
elif existsEnv(envKey): return parseJson(getEnv(envKey))
elif cfg.json.hasKey(jsonKey): return cfg.json[jsonKey]
else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"")
proc getJson*(cfg: CombinedConfig, key: string, default: JsonNode): JsonNode =
try: return getJson(cfg, key)
except: return default
proc loadEnv*(): StringTableRef =
result = newStringTable()

View File

@ -19,27 +19,50 @@ proc daemonize*(pidfile, si, so, se: string, daemonMain: proc(): void): Pid =
if fileExists(pidfile):
raise newException(IOError, "pidfile " & pidfile & " already exists, daemon already running?")
# We're about to do a little dance to end up with a process that is
# disconnected from the calling program's process group and is not a session
# leader (a daemon process).
#
# Remember, after a call to fork the code is now running in two processes,
# the parent process and the child process. So the returned PID will be a
# positive value if we are still on the parent process, and represents the
# PID of the newly created process. If we are on the newly created process,
# the PID returned with be 0.
let pid1 = fork()
# Are we the child process?
if pid1 == 0:
# Yes, so let's get ready to execute the main code given to us. Note that
# this child process will not actually live long. As part of our setup,
# we're going to create a new process session, resulting in this child
# process being the process group owner and session leader. We don't our
# daemon process to be a session leader because that allows it to be
# connected to a terminal. So after we've setup the new process group we
# will fork again and actually run the logic we're daemonizing in the
# grandchild process.
# Yes, so let's get ready to execute the main code given to us.
# Ignore whatever the original working directory was.
discard chdir("/")
# Create a new session to decouple us from the original terminal.
discard setsid()
# Set the process file mode creation mask to 0000
discard umask(0)
# Fork again... but I'm not sure why.
# Fork again so the grandchild process will not be the session leader.
let pid2 = fork()
# We don't need the intermediate process, so if we are not the child this
# time, lets just quit
# If we're still on the child process, we're done.
if pid2 > 0: quit(QuitSuccess)
# If we are the grandchild let's set up our environment.
# Otherwise, if we are the grandchild let's finish setting up our
# environment.
flushFile(stdout)
flushFile(stderr)
# Setup our STDIN, STDERR, and STDOUT pipes (if given)
if si.len > 0:
fi = open(si, fmRead)
discard dup2(getFileHandle(fi), getFileHandle(stdin))
@ -52,17 +75,20 @@ proc daemonize*(pidfile, si, so, se: string, daemonMain: proc(): void): Pid =
fe = open(se, fmAppend)
discard dup2(getFileHandle(fe), getFileHandle(stderr))
# Keep a record of the PID filename in our process memory so we can
# reference it in our cleanup hooks.
pidFileInner = pidfile
# Add hooks to cleanup after ourselves when we're asked to die.
signal(SIGINT, onStop)
signal(SIGTERM, onStop)
# Find out what our actual PID is and save it
# Find out what our actual PID is and save it to the PID file.
let childPid = getpid()
writeFile(pidfile, $childPid)
# Finally, execute our main
# Finally, execute the logic given to us to daemonize
daemonMain()
# If we're not the child proces, return the new PID of the daemonized process
return pid1