From 57bb7302b1a1585cd18e74931c8dba6adf694ebf Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Fri, 21 Jan 2022 18:04:40 -0600 Subject: [PATCH] Reorganize code into submodules. --- cliutils.nim | 181 ++------------------------------------ cliutilspkg/config.nim | 64 ++++++++++++++ cliutilspkg/daemonize.nim | 68 ++++++++++++++ cliutilspkg/procutil.nim | 72 +++++++++++++++ 4 files changed, 212 insertions(+), 173 deletions(-) create mode 100644 cliutilspkg/config.nim create mode 100644 cliutilspkg/daemonize.nim create mode 100644 cliutilspkg/procutil.nim diff --git a/cliutils.nim b/cliutils.nim index 362a5db..6f8784a 100644 --- a/cliutils.nim +++ b/cliutils.nim @@ -1,182 +1,17 @@ -import docopt, json, osproc, posix, nre, streams, strtabs, tables, terminal, unicode -import os except sleep +import nre, terminal, strtabs, tables, unicode import strutils except toUpper, toLower +import ./cliutilspkg/config +import ./cliutilspkg/daemonize +import ./cliutilspkg/procutil + +export config +export daemonize +export procutil type - CombinedConfig* = object - docopt*: Table[string, Value] - json*: JsonNode - TermColor = ForegroundColor or BackgroundColor -proc getVal*(cfg: CombinedConfig, key: string): string = - 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 $cfg.docopt[argKey] - elif existsEnv(envKey): return getEnv(envKey) - elif cfg.json.hasKey(jsonKey): - let node = cfg.json[jsonKey] - case node.kind - of JString: return node.getStr - of JInt: return $node.getInt - of JFloat: return $node.getFloat - of JBool: return $node.getBool - of JNull: return "" - of JObject: return $node - of JArray: return $node - else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"") - -proc getVal*(cfg: CombinedConfig, key, default: string): string = - try: return getVal(cfg, key) - except: return default - -proc loadEnv*(): StringTableRef = - result = newStringTable() - - for k, v in envPairs(): - result[k] = v - -## Process execution -type HandleProcMsgCB* = proc (outMsg: TaintedString, - errMsg: TaintedString, cmd: string): void - -proc sendMsg*(h: HandleProcMsgCB, outMsg: TaintedString, - errMsg: TaintedString = "", cmd: string = ""): void = - if h != nil: h(outMsg, errMsg, cmd) - -proc makeProcMsgHandler*(outSink, errSink: File, prefixCmd: bool = true): HandleProcMsgCB = - result = proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} = - let prefix = if cmd.len == 0 or not prefixCmd: "" else: cmd & ": " - if outMsg.len > 0: outSink.writeLine(prefix & outMsg) - if errMsg.len > 0: errSink.writeLine(prefix & errMsg) - -proc makeProcMsgHandler*(outSink, errSink: Stream, prefixCmd: bool = true): HandleProcMsgCB = - result = proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} = - let prefix = if cmd.len == 0 or not prefixCmd: "" else: cmd & ": " - if outMsg.len > 0: outSink.writeLine(prefix & outMsg) - if errMsg.len > 0: errSink.writeLine(prefix & errMsg) - -proc combineProcMsgHandlers*(a, b: HandleProcMsgCB): HandleProcMsgCB = - if a == nil: result = b - elif b == nil: result = a - else: - result = proc(cmd: string, outMsg, errMsg: TaintedString): void = - a(cmd, outMsg, errMsg) - b(cmd, outMsg, errMsg) - -proc waitFor*(p: Process, msgCB: HandleProcMsgCB, procCmd: string = ""): int = - - var pout = outputStream(p) - var perr = errorStream(p) - - var line = newStringOfCap(120).TaintedString - while true: - if pout.readLine(line): - msgCB.sendMsg(line, "", procCmd) - elif perr.readLine(line): - msgCB.sendMsg("", line, procCmd) - else: - result = peekExitCode(p) - if result != -1: break - close(p) - -proc exec*(command: string, workingDir: string = "", - args: openArray[string] = [], env: StringTableRef = nil, - options: set[ProcessOption] = {poUsePath}, - msgCB: HandleProcMsgCB = nil): int - {.tags: [ExecIOEffect, ReadIOEffect, RootEffect], gcsafe.} = - - var p = startProcess(command, workingDir, args, env, options) - result = waitFor(p, msgCB, command) - -proc execWithOutput*(command: string, workingDir:string = "", - args: openArray[string] = [], env: StringTableRef = nil, - options: set[ProcessOption] = {poUsePath}, - msgCB: HandleProcMsgCB = nil): - tuple[output, error: TaintedString, exitCode: int] = - - result = (TaintedString"", TaintedString"", -1) - var outSeq, errSeq: seq[TaintedString] - outSeq = @[]; errSeq = @[] - let outputCollector = combineProcMsgHandlers(msgCB, - proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} = - if outMsg.len > 0: outSeq.add(outMsg) - if errMsg.len > 0: errSeq.add(errMsg)) - - result[2] = exec(command, workingDir, args, env, options, outputCollector) - result[0] = outSeq.join("\n") - result[1] = errSeq.join("\n") - -## Daemonize - -var - pidFileInner: string - fi, fo, fe: File - -proc onStop(sig: cint) {.noconv.} = - close(fi) - close(fo) - close(fe) - removeFile(pidFileInner) - - quit(QuitSuccess) - -proc daemonize*(pidfile, si, so, se: string, daemonMain: proc(): void): Pid = - - if fileExists(pidfile): - raise newException(IOError, "pidfile " & pidfile & " already exists, daemon already running?") - - 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. - discard chdir("/") - discard setsid() - discard umask(0) - - # Fork again... but I'm not sure why. - let pid2 = fork() - - # We don't need the intermediate process, so if we are not the child this - # time, lets just quit - if pid2 > 0: quit(QuitSuccess) - - # If we are the grandchild let's set up our environment. - flushFile(stdout) - flushFile(stderr) - - if si.len > 0: - fi = open(si, fmRead) - discard dup2(getFileHandle(fi), getFileHandle(stdin)) - - if so.len > 0: - fo = open(so, fmAppend) - discard dup2(getFileHandle(fo), getFileHandle(stdout)) - - if se.len > 0: - fe = open(se, fmAppend) - discard dup2(getFileHandle(fe), getFileHandle(stderr)) - - 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 - let childPid = getpid() - writeFile(pidfile, $childPid) - - # Finally, execute our main - daemonMain() - - return pid1 - const termReset* = "\e[0;m" proc termColor*(color: TermColor, bright, bold = false): string = diff --git a/cliutilspkg/config.nim b/cliutilspkg/config.nim new file mode 100644 index 0000000..bc3a279 --- /dev/null +++ b/cliutilspkg/config.nim @@ -0,0 +1,64 @@ +import docopt, json, nre, os, strtabs, strutils + +type + CombinedConfig* = object + docopt*: Table[string, Value] + json*: JsonNode + +template walkFieldDefs*(t: NimNode, body: untyped) = + let tTypeImpl = t.getTypeImpl + + var nodeToItr: NimNode + if tTypeImpl.typeKind == ntyObject: nodeToItr = tTypeImpl[2] + elif tTypeImpl.typeKind == ntyTypeDesc: nodeToItr = tTypeImpl.getType[1].getType[2] + else: error $t & " is not an object or type desc (it's a " & $tTypeImpl.typeKind & ")." + + for fieldDef {.inject.} in nodeToItr.children: + # ignore AST nodes that are not field definitions + if fieldDef.kind == nnkIdentDefs: + let fieldIdent {.inject.} = fieldDef[0] + let fieldType {.inject.} = fieldDef[1] + body + + elif fieldDef.kind == nnkSym: + let fieldIdent {.inject.} = fieldDef + let fieldType {.inject.} = fieldDef.getType + body + + +# TODO: Generic config loader +# macro loadConfig*(filePath: string, cfgType: typed, args: Table[string, docopt.Value): untyped = +# result = newNimNode(nnkObjConstr).(cfgType) +# +# var idx = 0 +# cfgType.walkFieldDefs: +# let valLookup = quote do: `getVal`( + +proc getVal*(cfg: CombinedConfig, key: string): string = + 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 $cfg.docopt[argKey] + elif existsEnv(envKey): return getEnv(envKey) + elif cfg.json.hasKey(jsonKey): + let node = cfg.json[jsonKey] + case node.kind + of JString: return node.getStr + of JInt: return $node.getInt + of JFloat: return $node.getFloat + of JBool: return $node.getBool + of JNull: return "" + of JObject: return $node + of JArray: return $node + else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"") + +proc getVal*(cfg: CombinedConfig, key, default: string): string = + try: return getVal(cfg, key) + except: return default + +proc loadEnv*(): StringTableRef = + result = newStringTable() + + for k, v in envPairs(): + result[k] = v diff --git a/cliutilspkg/daemonize.nim b/cliutilspkg/daemonize.nim new file mode 100644 index 0000000..21cd1f0 --- /dev/null +++ b/cliutilspkg/daemonize.nim @@ -0,0 +1,68 @@ +import posix +import os except sleep + +## Daemonize +var + pidFileInner: string + fi, fo, fe: File + +proc onStop(sig: cint) {.noconv.} = + close(fi) + close(fo) + close(fe) + removeFile(pidFileInner) + + quit(QuitSuccess) + +proc daemonize*(pidfile, si, so, se: string, daemonMain: proc(): void): Pid = + + if fileExists(pidfile): + raise newException(IOError, "pidfile " & pidfile & " already exists, daemon already running?") + + 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. + discard chdir("/") + discard setsid() + discard umask(0) + + # Fork again... but I'm not sure why. + let pid2 = fork() + + # We don't need the intermediate process, so if we are not the child this + # time, lets just quit + if pid2 > 0: quit(QuitSuccess) + + # If we are the grandchild let's set up our environment. + flushFile(stdout) + flushFile(stderr) + + if si.len > 0: + fi = open(si, fmRead) + discard dup2(getFileHandle(fi), getFileHandle(stdin)) + + if so.len > 0: + fo = open(so, fmAppend) + discard dup2(getFileHandle(fo), getFileHandle(stdout)) + + if se.len > 0: + fe = open(se, fmAppend) + discard dup2(getFileHandle(fe), getFileHandle(stderr)) + + 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 + let childPid = getpid() + writeFile(pidfile, $childPid) + + # Finally, execute our main + daemonMain() + + return pid1 diff --git a/cliutilspkg/procutil.nim b/cliutilspkg/procutil.nim new file mode 100644 index 0000000..12664b0 --- /dev/null +++ b/cliutilspkg/procutil.nim @@ -0,0 +1,72 @@ +import osproc, sequtils, streams, strtabs + +## Process execution +type HandleProcMsgCB* = proc (outMsg: TaintedString, + errMsg: TaintedString, cmd: string): void + +proc sendMsg*(h: HandleProcMsgCB, outMsg: TaintedString, + errMsg: TaintedString = "", cmd: string = ""): void = + if h != nil: h(outMsg, errMsg, cmd) + +proc makeProcMsgHandler*(outSink, errSink: File, prefixCmd: bool = true): HandleProcMsgCB = + result = proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} = + let prefix = if cmd.len == 0 or not prefixCmd: "" else: cmd & ": " + if outMsg.len > 0: outSink.writeLine(prefix & outMsg) + if errMsg.len > 0: errSink.writeLine(prefix & errMsg) + +proc makeProcMsgHandler*(outSink, errSink: Stream, prefixCmd: bool = true): HandleProcMsgCB = + result = proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} = + let prefix = if cmd.len == 0 or not prefixCmd: "" else: cmd & ": " + if outMsg.len > 0: outSink.writeLine(prefix & outMsg) + if errMsg.len > 0: errSink.writeLine(prefix & errMsg) + +proc combineProcMsgHandlers*(a, b: HandleProcMsgCB): HandleProcMsgCB = + if a == nil: result = b + elif b == nil: result = a + else: + result = proc(cmd: string, outMsg, errMsg: TaintedString): void = + a(cmd, outMsg, errMsg) + b(cmd, outMsg, errMsg) + +proc waitFor*(p: Process, msgCB: HandleProcMsgCB, procCmd: string = ""): int = + + var pout = outputStream(p) + var perr = errorStream(p) + + var line = newStringOfCap(120).TaintedString + while true: + if pout.readLine(line): + msgCB.sendMsg(line, "", procCmd) + elif perr.readLine(line): + msgCB.sendMsg("", line, procCmd) + else: + result = peekExitCode(p) + if result != -1: break + close(p) + +proc exec*(command: string, workingDir: string = "", + args: openArray[string] = [], env: StringTableRef = nil, + options: set[ProcessOption] = {poUsePath}, + msgCB: HandleProcMsgCB = nil): int + {.tags: [ExecIOEffect, ReadIOEffect, RootEffect], gcsafe.} = + + var p = startProcess(command, workingDir, args, env, options) + result = waitFor(p, msgCB, command) + +proc execWithOutput*(command: string, workingDir:string = "", + args: openArray[string] = [], env: StringTableRef = nil, + options: set[ProcessOption] = {poUsePath}, + msgCB: HandleProcMsgCB = nil): + tuple[output, error: TaintedString, exitCode: int] = + + result = (TaintedString"", TaintedString"", -1) + var outSeq, errSeq: seq[TaintedString] + outSeq = @[]; errSeq = @[] + let outputCollector = combineProcMsgHandlers(msgCB, + proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} = + if outMsg.len > 0: outSeq.add(outMsg) + if errMsg.len > 0: errSeq.add(errMsg)) + + result[2] = exec(command, workingDir, args, env, options, outputCollector) + result[0] = outSeq.join("\n") + result[1] = errSeq.join("\n")