diff options
Diffstat (limited to 'nim')
-rw-r--r-- | nim/file.nim | 35 | ||||
-rw-r--r-- | nim/logger.nim | 92 | ||||
-rw-r--r-- | nim/main.nim | 133 | ||||
-rw-r--r-- | nim/ping.nim | 320 |
4 files changed, 580 insertions, 0 deletions
diff --git a/nim/file.nim b/nim/file.nim new file mode 100644 index 0000000..83c2560 --- /dev/null +++ b/nim/file.nim | |||
@@ -0,0 +1,35 @@ | |||
1 | # tickwatch | ||
2 | # Author: Euxane TRAN-GIRARD | ||
3 | # Licence: EUPL-1.2 | ||
4 | |||
5 | import std/paths | ||
6 | import std/strutils | ||
7 | |||
8 | |||
9 | type | ||
10 | FileValueMonitor = ref object | ||
11 | path: Path | ||
12 | |||
13 | FileChangeMonitor = ref object | ||
14 | path: Path | ||
15 | lastValue: int | ||
16 | |||
17 | proc readValue(path: Path): int = | ||
18 | readFile(path.string).strip.parseInt | ||
19 | |||
20 | proc initFileValueMonitor*(path: Path): FileValueMonitor = | ||
21 | FileValueMonitor(path: path) | ||
22 | |||
23 | proc readValue*(mon: FileValueMonitor): int = | ||
24 | mon.path.readValue() | ||
25 | |||
26 | proc initFileChangeMonitor*(path: Path): FileChangeMonitor = | ||
27 | FileChangeMonitor( | ||
28 | path: path, | ||
29 | lastValue: path.readValue(), | ||
30 | ) | ||
31 | |||
32 | proc readValue*(mon: FileChangeMonitor): int = | ||
33 | let newValue = mon.path.readValue() | ||
34 | result = abs(mon.lastValue - newValue) | ||
35 | mon.lastValue = newValue | ||
diff --git a/nim/logger.nim b/nim/logger.nim new file mode 100644 index 0000000..48aedf2 --- /dev/null +++ b/nim/logger.nim | |||
@@ -0,0 +1,92 @@ | |||
1 | # tickwatch | ||
2 | # Author: Euxane TRAN-GIRARD | ||
3 | # Licence: EUPL-1.2 | ||
4 | |||
5 | import std/sugar | ||
6 | import std/math | ||
7 | import std/unicode | ||
8 | import std/times | ||
9 | import std/os | ||
10 | |||
11 | |||
12 | const | ||
13 | TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mmZZZ " | ||
14 | SKIPPED_SYMBOL = "." | ||
15 | UNKNOWN_SYMBOL = "!" | ||
16 | NUMERIC_SYMBOLS* = "123456789".toRunes | ||
17 | UNICODE_SYMBOLS* = "_▁▂▃▄▅▆▇█".toRunes | ||
18 | |||
19 | |||
20 | type Scale* = proc(val: float): float {.noSideEffect.} | ||
21 | |||
22 | proc indicator(symbols: seq[Rune], min, max, val: float, scale: Scale): Rune = | ||
23 | let division = scale(max - min) / float(symbols.len) | ||
24 | let bucket = int(scale(val - min) / division) | ||
25 | symbols[bucket.clamp(0, symbols.len - 1)] | ||
26 | |||
27 | proc indicator*(symbols: seq[Rune], min, max, val: int, scale: Scale): Rune = | ||
28 | symbols.indicator(min.float, max.float, val.float, scale) | ||
29 | |||
30 | func millisecond(t: DateTime): int = | ||
31 | t.nanosecond div (1_000_000) | ||
32 | |||
33 | func msUntilNextSec(t: DateTime): int = | ||
34 | 1000 - t.millisecond + 1 | ||
35 | |||
36 | proc puts(f: File, s: string) = | ||
37 | f.write s | ||
38 | f.flushFile | ||
39 | |||
40 | proc puts(f: File, r: Rune) = | ||
41 | f.puts r.toUTF8 | ||
42 | |||
43 | func formatTimestampDateTime*(dt: DateTime): string = | ||
44 | dt.format(TIMESTAMP_FORMAT) | ||
45 | |||
46 | func formatTimestampUnix*(dt: DateTime): string = | ||
47 | ($dt.toTime.toUnix) & " " | ||
48 | |||
49 | func formatTimestampNone*(dt: DateTime): string = | ||
50 | "" | ||
51 | |||
52 | proc loop*( | ||
53 | probe: (timeout: Duration) -> int, | ||
54 | getSymbol: (int) -> Rune, | ||
55 | timestampHeader: (DateTime) -> string, | ||
56 | ) = | ||
57 | while true: | ||
58 | stdout.puts timestampHeader(now()) | ||
59 | |||
60 | for tick in 0..<60: | ||
61 | let t = now() | ||
62 | if tick < t.second: | ||
63 | stdout.puts SKIPPED_SYMBOL | ||
64 | continue | ||
65 | |||
66 | let timeout = initDuration(milliseconds = t.msUntilNextSec) | ||
67 | try: | ||
68 | let val = probe(timeout) | ||
69 | stdout.puts getSymbol(val) | ||
70 | except CatchableError: | ||
71 | stdout.puts UNKNOWN_SYMBOL | ||
72 | |||
73 | let tAfter = now() | ||
74 | if tAfter < t + timeout: | ||
75 | sleep(tAfter.msUntilNextSec) | ||
76 | |||
77 | stdout.puts "\n" | ||
78 | |||
79 | proc flushAndQuit*() {.noconv.} = | ||
80 | stdout.puts "\n" | ||
81 | quit(0) | ||
82 | |||
83 | |||
84 | when defined(test): | ||
85 | import std/unittest | ||
86 | import std/sequtils | ||
87 | |||
88 | suite "logger": | ||
89 | test "indicator value": | ||
90 | proc id(val: float): float = val | ||
91 | let indics = (0..30).mapIt(NUMERIC_SYMBOLS.indicator(5, 20, it, id)) | ||
92 | check indics.deduplicate(isSorted = true) == NUMERIC_SYMBOLS | ||
diff --git a/nim/main.nim b/nim/main.nim new file mode 100644 index 0000000..0ea96f4 --- /dev/null +++ b/nim/main.nim | |||
@@ -0,0 +1,133 @@ | |||
1 | # tickwatch | ||
2 | # Author: Euxane TRAN-GIRARD | ||
3 | # Licence: EUPL-1.2 | ||
4 | |||
5 | import std/sugar | ||
6 | import std/math | ||
7 | import std/times | ||
8 | import std/net | ||
9 | import std/posix | ||
10 | import std/paths | ||
11 | import std/strutils | ||
12 | import std/sequtils | ||
13 | import std/parseopt | ||
14 | |||
15 | import file | ||
16 | import ping | ||
17 | import logger | ||
18 | |||
19 | |||
20 | const | ||
21 | NAME = "tickwatch" | ||
22 | VERSION = staticExec(""" | ||
23 | command -v git >/dev/null && git describe --tags --always || echo $VERSION | ||
24 | """).strip() | ||
25 | HELP_TEXT = | ||
26 | staticRead("../readme.md") | ||
27 | .split("```helptext", 1)[1] | ||
28 | .split("```", 1)[0] | ||
29 | .strip() | ||
30 | |||
31 | proc registerTerminationHandlers() = | ||
32 | proc terminationHandler(sig: cint) {.noconv.} = flushAndQuit() | ||
33 | var action = SigAction(sa_handler: terminationHandler) | ||
34 | for signal in [SIGTERM, SIGHUP, SIGQUIT, SIGINT]: | ||
35 | discard sigaction(signal, action) | ||
36 | |||
37 | func getArg( | ||
38 | args: seq[tuple[kind: CmdLineKind, key, val: string]], | ||
39 | index: int, | ||
40 | label: string | ||
41 | ): string = | ||
42 | try: args[index].key | ||
43 | except: raise newException(ValueError, "Missing " & label & " argument") | ||
44 | |||
45 | proc main() = | ||
46 | var | ||
47 | scale: Scale = log2 | ||
48 | symbols = UNICODE_SYMBOLS | ||
49 | timestampHeader = formatTimestampDateTime | ||
50 | (min, max) = (0, 1000) | ||
51 | |||
52 | for optKind, key, val in getopt(): | ||
53 | if optKind notin {cmdLongOption, cmdShortOption}: | ||
54 | continue | ||
55 | |||
56 | case key: | ||
57 | |||
58 | of "help", "h": | ||
59 | echo HELP_TEXT | ||
60 | quit(0) | ||
61 | |||
62 | of "version", "v": | ||
63 | echo NAME & " " & VERSION | ||
64 | quit(0) | ||
65 | |||
66 | of "scale": | ||
67 | case val: | ||
68 | of "logarithmic", "log", "log2": scale = log2 | ||
69 | of "log10": scale = log10 | ||
70 | of "ln": scale = ln | ||
71 | of "linear", "lin": scale = (val: float) => val | ||
72 | else: raise newException(ValueError, "Unrecognised scale choice") | ||
73 | |||
74 | of "symbols": | ||
75 | case val: | ||
76 | of "unicode", "utf8", "utf-8": symbols = UNICODE_SYMBOLS | ||
77 | of "numeric", "ascii": symbols = NUMERIC_SYMBOLS | ||
78 | else: raise newException(ValueError, "Unrecognised symbols choice") | ||
79 | |||
80 | of "timestamp": | ||
81 | case val: | ||
82 | of "datetime", "date-time": timestampHeader = formatTimestampDateTime | ||
83 | of "unix", "epoch": timestampHeader = formatTimestampUnix | ||
84 | of "none", "false": timestampHeader = formatTimestampNone | ||
85 | else: raise newException(ValueError, "Unrecognised timestamp choice") | ||
86 | |||
87 | of "range": | ||
88 | let parts = val.split(':', 1) | ||
89 | if parts.len != 2: raise newException(ValueError, "Invalid range") | ||
90 | (min, max) = (parseInt(parts[0]), parseInt(parts[1])) | ||
91 | |||
92 | else: | ||
93 | raise newException(ValueError, "Unrecognised option") | ||
94 | |||
95 | let | ||
96 | args = getopt().toSeq.filterIt(it.kind == cmdArgument) | ||
97 | monitor = args.getArg(0, "monitor") | ||
98 | target = args.getArg(1, "target") | ||
99 | |||
100 | if args.len > 2: | ||
101 | raise newException(ValueError, "Invalid number of arguments") | ||
102 | |||
103 | let probe = case monitor: | ||
104 | of "ping": | ||
105 | let targetIp = resolve(target) | ||
106 | let mon = initPingMonitor(targetIp, procIdent()) | ||
107 | (timeout: Duration) => mon.ping(timeout).inMilliseconds.int | ||
108 | |||
109 | of "value": | ||
110 | let mon = initFileValueMonitor(Path target) | ||
111 | (timeout: Duration) => mon.readValue() | ||
112 | |||
113 | of "change": | ||
114 | let mon = initFileChangeMonitor(Path target) | ||
115 | (timeout: Duration) => mon.readValue() | ||
116 | |||
117 | else: | ||
118 | raise newException(ValueError, "Unrecognised monitor argument") | ||
119 | |||
120 | loop( | ||
121 | probe, | ||
122 | (val: int) => symbols.indicator(min, max, val, scale), | ||
123 | timestampHeader, | ||
124 | ) | ||
125 | |||
126 | |||
127 | when not defined(test): | ||
128 | try: | ||
129 | registerTerminationHandlers() | ||
130 | main() | ||
131 | except CatchableError as err: | ||
132 | stderr.writeLine err.msg | ||