heartbeat

Simple server monitor system using encrypted messages over udp
git clone https://noulin.net/git/heartbeat.git
Log | Files | Refs | README

commit a9044730ec921acc1e7086674c37cf97b5338863
Author: Remy Noulin <loader2x@gmail.com>
Date:   Fri,  7 Jul 2023 13:25:31 +0200

heartbeat initial version

.gitignore                    |  63 ++++
README.md                     |  11 +
heartbeat.c                   | 730 ++++++++++++++++++++++++++++++++++++++++++
package.yml                   |  18 ++
shpPackages/short/main.c      |  29 ++
shpPackages/short/package.yml |  15 +
shpPackages/short/short.h     |  15 +
7 files changed, 881 insertions(+)

Diffstat:
A.gitignore | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 11+++++++++++
Aheartbeat.c | 730+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackage.yml | 18++++++++++++++++++
AshpPackages/short/main.c | 29+++++++++++++++++++++++++++++
AshpPackages/short/package.yml | 15+++++++++++++++
AshpPackages/short/short.h | 15+++++++++++++++
7 files changed, 881 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,63 @@ +# Vim +*.sw* + +# Debug +.gdb_history + +# Coverage +*.gcov +*.gcda +*.gcno + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf diff --git a/README.md b/README.md @@ -0,0 +1,11 @@ +# Heartbeat + +`heartbeat` is a simple monitoring system which uses udp to send logs and sends event by mail. + +# Install + +# Configuration + +# Setup + +agents diff --git a/heartbeat.c b/heartbeat.c @@ -0,0 +1,730 @@ +#! /usr/bin/env sheepy +/* or direct path to sheepy: #! /usr/local/bin/sheepy */ + +/* Libsheepy documentation: https://spartatek.se/libsheepy/ */ +#include "libsheepyObject.h" +#include "shpPackages/short/short.h" + +/* +Commands: +no command: load config.yml + probe and send messages to bridge, forward messages to bridge + if logger, store messages, forward messages to monitor +config: generate configuration yml files for each machine + copy configuration to all machines + copy heartbeat executable +daemon: run as daemon +monitor: display state +*/ + + +// cfgFile is the main configuration file written by the user +// it used in the config command to generate agentCfgFile for each machine +#define cfgFile "heartbeatConfig.yml" + +#define home ".heartbeat" + +// agentCfgFile is the default configuration file +#define agentCfgPath ".heartbeat/config.yml" +#define agentCfgFile "~/" agentCfgPath +#define binPath "bin/" +#define bin "~/" binPath + +#define defaultPeriod 1 +// TODO #define defaultPeriod 120 + +// 10mn timeout +#define agentTimeOut 5 +//TODO #define agentTimeOut 600 + +int argc; char **argv; + +/* enable/disable logging */ +/* #undef pLog */ +/* #define pLog(...) */ + +void printHelp(void); +void config(smallJsont *cfg); +void probe(char *cfgfile); + +/* process arguments on command line + */ +int main(int ARGC, char** ARGV) { + + argc = ARGC; argv = ARGV; + + initLibsheepy(ARGV[0]); + //setLogMode(LOG_VERBOSE); + setLogMode(LOG_DATE); + //openProgLogFile(); + setLogSymbols(LOG_UTF8); + disableLibsheepyErrorLogs; + + if (argc == 1) { + // no arguments + cleanCharP(p) = expandHome(agentCfgFile); + if (!isPath(p)) { + logE("Missing configuration: %s", p); + ret 1; + } + probe(p); + ret 0; + } + elif (argc != 2) { + logE("Too many arguments"); + printHelp(); + ret 1; + } + elif (eqG(argv[1], "daemon")) { + TODO("daemon"); + ret 1; + } + elif (eqG(argv[1], "monitor")) { + TODO("monitor"); + ret 1; + } + elif (eqG(argv[1], "config")) { + cleanAllocateSmallJson(cfg); + smallJsont *r = readFileG(cfg, cfgFile); + + if (!r) { + logE("Missing "cfgFile); + ret 1; + } + config(cfg); + } + elif ( eqG(argv[1], "help") + or eqG(argv[1], "-h") + or eqG(argv[1], "--help") + or eqG(argv[1], "-?") + or eqG(argv[1], "?")) { + printHelp(); + } + else { + logE("Invalid command: %s", argv[1]); + printHelp(); + ret 1; + } + + ret 0; +} + +void printHelp(void) { + puts(BLD GRN"Help"RST); + TODO("write help"); +} + +/* generate setup for all agents + */ +void config(smallJsont *cfg) { + + //lv(cfg); + + // generate agent configs: agentNameConfig.yml + // add shell commands to copyScript + + // generate agent configs: agentNameConfig.yml + u16 port = u$(cfg, "port"); + if (!port) { + logE("Missing port."); + XFailure; + } + delElemG(cfg, "port"); + + // add shell commands to copyScript + cleanAllocateSmallArray(copyScript); + + // id is stored in the agent config and sent in the messages + // it is the index of the machine in the list + u32 id = 0; + iter(cfg, D) { + cast(smallDictt*, d, D); + //logD("key %s", iK(cfg)); + cleanCharP(thisCfgFile) = catS(iK(cfg), "Config.yml"); + //lv(thisCfgFile); + + cleanAllocateSmallJson(thisCfg); + cleanFinishSmallJsonP(agentCfg) = allocG(rtSmallJsont); + setG(thisCfg, "port", (u32)port); + + // populate agent config + setG(agentCfg, "id", id); + + if (hasG(d, "logger")) { + setG(agentCfg, "logger", TRUE); + // TODO("add monitor ip and port in logger agent"); + } + + // set bridge address + if (hasG(d, "bridge")) { + cleanCharP(key) = formatS("\"%s\".\"address\"", $(d, "bridge")); + char *address = $(cfg, key); + //lv(key); + //lv(address); + setG(agentCfg, "bridge", address); + } + + if (hasG(d, "probes")) { + setNFreeG(agentCfg, "probes", getNDupG(d, rtSmallArrayt, "probes")); + } + + setG(thisCfg, iK(cfg), agentCfg); + //lv(thisCfg); + writeFileG(thisCfg, thisCfgFile); + + pushNFreeG(copyScript, + formatS("# %s", iK(cfg))); + if (!$(d, "transfers")) { + // ssh/scp + pushNFreeG(copyScript, + formatS("ssh %s mkdir "home, iK(cfg))); + if (hasG(d, "logger")) { + pushNFreeG(copyScript, + formatS("scp "cfgFile" %s:" home "/" cfgFile, iK(cfg))); + } + pushNFreeG(copyScript, + formatS("scp %s %s:" agentCfgPath, thisCfgFile, iK(cfg))); + pushNFreeG(copyScript, + formatS("scp heartbeat %s:" binPath, iK(cfg))); + } + elif (eqG($(d, "transfers"), "copy")) { + pushG(copyScript, + "mkdir ~/"home); + if (hasG(d, "logger")) { + pushG(copyScript, + "cp "cfgFile" ~/" home "/" cfgFile); + } + pushNFreeG(copyScript, + formatS("cp %s " agentCfgFile, thisCfgFile)); + pushNFreeG(copyScript, + formatS("cp heartbeat " bin)); + } + + inc id; + } + + logG(copyScript); +} + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netdb.h> +#include <sys/stat.h> +#include <fcntl.h> + + +void sendMail(char *cmd); + +void setTimeout(int bridgesock, u32 period, u32 usec) { + struct timeval timeout = {.tv_sec = period, .tv_usec = usec}; + if (setsockopt(bridgesock, SOL_SOCKET, SO_RCVTIMEO, (char *) &timeout, sizeof(timeout)) < 0) { + logE("Could not set receive socket time out: %s", strerror(errno)); + close(bridgesock); + XFailure; + } + if (setsockopt(bridgesock, SOL_SOCKET, SO_SNDTIMEO, (char *) &timeout, sizeof(timeout)) < 0) { + logE("Could not set send socket time out: %s", strerror(errno)); + close(bridgesock); + XFailure; + } +} + +/* agent, bridge and logger + */ +void probe(char *cfgfile) { + + // load agent config + // get port and agent name + // setup agent + // setup logger + // create socket for brigde (only if not logger) + // start server or bridge for the agents + // message structures + // inifinite loop + // check probes + // send message + // this is the logger + // write messages directly + // update logger state and send mail if changed + // set remote agents down if there hasn't been a message in a while + // send mail + // bridge messages from others + // when agent is logger, store message + // update state for this agent + // send mail when agent rebooted + // send mail when service is down + // send mail + // when agent is not logger, forward message + + // load agent config + cleanAllocateSmallJson(cfg); + readFileG(cfg, cfgfile); + //lv(cfg); + + // get port and agent name + u16 port = u$(cfg, "port"); + if (!port) { + logE("Missing port."); + XFailure; + } + delElemG(cfg, "port"); + cleanListP(names) = keysG(cfg); + //lv(names); + if (lenG(names) != 1) { + logE("There should only and at least one agent configured: %d", lenG(names)); + XFailure; + } + + char *name = names[0]; + + // setup agent + cleanFinishSmallDictP(agent) = getG(cfg, rtSmallDictt, name); + //lv(agent); + if (!agent) { + logE("Agent object has wrong type, should be smallDict."); + XFailure; + } + + u32 id = u$(agent, "id"); + bool logger = hasG(agent, "logger"); + // when dst is null, it is the logger + char *dst = $(agent, "bridge"); + if (!dst and not logger) { + logE("Missing bridge address."); + XFailure; + } + cleanFinishSmallArrayP(probes) = getG(agent, rtSmallArrayt, "probes"); + if (probes and !lenG(probes)) { + finishG(probes); + probes = null; + } + u32 period = hasG(agent, "period") ? u$(agent, "period") : defaultPeriod; + /* lv(id); */ + /* lv(logger); */ + /* lv(dst); */ + //lv(probes); + //lv(period); + + // setup logger + cleanSmallJsonP(completeCfg) = null; + // agent names + cleanListP(agents) = null; + // agent db filenames + cleanListP(agentdbs) = null; + // when logger was started + u64 startTime = 0; + cleanCharP(mailSubject) = null; + + if (logger) { + startTime = getCurrentUnixTime(); + + completeCfg = allocG(rtSmallJsont); + cleanCharP(p) = expandHome("~/" home "/" cfgFile); + smallJsont *r = readFileG(completeCfg, p); + if (!r) { + logE("Missing %s", p); + XFailure; + } + delElemG(completeCfg, "port"); + + // create filename with path in ~/.heartbeat/ + agents = keysG(completeCfg); + agentdbs = keysG(completeCfg); + cleanCharP(h) = expandHome("~/" home "/"); + forEachCharP(agentdbs, ag) { + pErrorNULL(iPrependS(ag, h)); + pErrorNULL(iAppendS(ag, ".bin")); + } + //lv(agentdbs); + + // mail setup + pError0(chDir(h)); + mailSubject = catS("Heatbeat on ", name); + } + // open log files + int agentf[lenG(agents)]; + if (logger) { + arange(i, agentf) { + agentf[i] = open(agentdbs[i], O_APPEND|O_CREAT|O_WRONLY, S_IRUSR|S_IWUSR); + if (agentf[i] < 0) { + logE("Failed to open %s: %s", agentdbs[i], strerror(errno)); + XFailure; + } + } + // setup objects for monitor + setG(completeCfg, "startTime", startTime); + iter(completeCfg, D) { + if (!isOSmallDict(D)) continue; + cast(smallDictt*,d,D); + setG(d, "state", "down"); + setG(d, "last", startTime); + setG(d, "mId", 0); + setG(d, "rebooted", FALSE); + setG(d, "lastBoot", startTime); + cleanFinishSmallArrayP(probes) = getG(d, rtSmallArrayt, "probes"); + iter(probes, P) { + cast(smallDictt*,p,P); + setG(p, "state", FALSE); + setG(p, "last", startTime); + setPG(probes, iI(probes), p); + } + setPG(completeCfg, iK(completeCfg), d); + } + } + + + // create socket for brigde (only if not logger) + int sock = -1; + struct sockaddr_in server; + struct hostent *hp; + + if (not logger) { + sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + logE("Failed to create socket: %s", strerror(errno)); + XFailure; + } + + server.sin_family = AF_INET; + + hp = gethostbyname(dst); + if (hp==0) { + logE("gethostbyname failed: %s", strerror(errno)); + close(sock); + XFailure; + } + + memcpy(&server.sin_addr, hp->h_addr, hp->h_length); + server.sin_port = htons(port); + } + + // start server or bridge for the agents + int bridgesock; + struct sockaddr_in bridge; + struct sockaddr client; + char buf[1024]; + + bridgesock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (bridgesock < 0){ + logE("Failed to create bridge socket: %s", strerror(errno)); + XFailure; + } + + bridge.sin_family = AF_INET; + bridge.sin_addr.s_addr = INADDR_ANY; + bridge.sin_port = htons(port); + + if (bind(bridgesock, (struct sockaddr *) &bridge, sizeof(bridge))){ + logE("bind failed: %s", strerror(errno)); + XFailure; + } + + // message structures + typ struct PACKED { + u32 id; + u64 messageId; + u64 status; + u32 info; + } msgt; + typ struct PACKED { + u64 time; + msgt m; + u32 levels[32]; + } dbt; + dbt record = init0Var; + + msgt msg = {.id = id}; + //lv(sizeof(msg)); + + msg.id = id; + + // inifinite loop + rangeInf(messageId) { + //lv(messageId); + + u64 time = getMonotonicTime(); + u64 nextTime = time + (u64)period * 1000000000UL; + + msg.messageId = messageId; + msg.status = 0; + msg.info = 0; + + // check probes + if (probes) { + iter(probes, P) { + cast(smallDictt*,p,P); + if (not isOSmallDict(p)) { + logE("Invalid probe at index %d", iI(probes)); + XFailure; + } + if (hasG(p, "port")) { + // try to bind + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0){ + msg.status |= 1UL << iI(probes); + } + else { + struct sockaddr_in server; + server.sin_family = AF_INET; + server.sin_addr.s_addr = INADDR_ANY; + server.sin_port = htons((u16)u$(p, "port")); + if (bind(sock, (struct sockaddr *) &server, sizeof(server)) == 0) { + // bind successful + msg.status |= 1UL << iI(probes); + } + close(sock); + } + } + elif (hasG(p, "if")) { + // TODO("check if network interface is down"); + } + } + } // if probes + + //logD("status %x", msg.status); + + if (not logger) { + // send message + if (sendto(sock, &msg, sizeof(msg), 0, (const struct sockaddr *)&server, sizeof(server)) < 0) { + logE("send failed: %s", strerror(errno)); + close(sock); + XFailure; + } + } + else { + // this is the logger + // write messages directly + record.m = msg; + record.time = getCurrentUnixTime(); + write(agentf[id], &record, sizeof(record)); + cleanFinishSmallDictP(logAgent) = getG(completeCfg, rtSmallDictt, name); + setG(logAgent, "time", getCurrentUnixTime()); + char *newstate = messageId ? "alive" : "init"; + if (!eqG($(logAgent, "state"), newstate)) { + /* logD("%s is %s, previous state was %s", */ + /* name#<{(|agent name|)}>#, */ + /* newstate, */ + /* $(logAgent,"state")); */ + setG(logAgent, "state", newstate); + setG(logAgent, "last", getCurrentUnixTime()); + } + + cleanAllocateSmallArray(mailMsg); + + // update logger state and send mail if changed + if (probes) { + iter(probes, P) { + cast(smallDictt*,p,P); + bool newstate = msg.status & (1UL << iI(probes)); + if (getG(p, rtBool, "state") != newstate) { + setG(p, "state", newstate); + setG(p, "last", getCurrentUnixTime()); + // send mail when service is down + char *state = newstate ? "down" : "up"; + char *result = newstate ? "failed" : "ok"; + if (hasG(p, "port")) { + cleanCharP(s) = formatS("Service on port %d running in %s is %s", u$(p, "port"), name, state); + pushG(mailMsg, s); + logW("%s", s); + } + elif (hasG(p, "if")) { + cleanCharP(s) = formatS("Network interface %s in %s is down", u$(p, "if"), name); + pushG(mailMsg, s); + logW("%s", s); + } + elif (hasG(p, "cmd")) { + cleanCharP(s) = formatS("'%s' running in %s is %s", u$(p, "cmd"), name, result); + pushG(mailMsg, s); + logW("%s", s); + } + } + } + } // if probes + + // set remote agents down if there hasn't been a message in a while + iter(completeCfg, D) { + if (!isOSmallDict(D)) continue; + cast(smallDictt*,d,D); + if (eqG($(d, "state"), "down")) continue; + if (getCurrentUnixTime() - u$(d, "time") < agentTimeOut) continue; + // send mail when agent is down + cleanCharP(s) = formatS("%s is down, previous state was %s", iK(completeCfg)/*agent name*/, $(d,"state")); + pushG(mailMsg, s); + logW("%s", s); + setG(d, "state", "down"); + setG(d, "last", getCurrentUnixTime()); + } + + // send mail + if (lenG(mailMsg)) { + pError0(writeFileG(mailMsg, "mail.txt")); + cleanFinishSmallArrayP(mails) = getG(logAgent, rtSmallArrayt, "mails"); + char *email = $(mails, 0); + cleanCharP(mutt) = formatS("mutt -s \"%s\" %s < mail.txt", mailSubject, email); + sendMail(mutt); + } + + // TODO send to monitor + //lv(completeCfg); + } + + // bridge messages from others + // set timeout to period time, to sleep enough time + setTimeout(bridgesock, period, 0); + listenToOthers:; + socklen_t addr_size = sizeof(client); + ssize_t r = recvfrom(bridgesock, buf, sizeof(buf), 0, (struct sockaddr *) &client, &addr_size); + if (r == -1) { + // timeout + goto cont; + } + else { + // got a message + //lv(r); + cast(msgt *, m, buf); + /* logD("got message from id %d", m->id); */ + /* logD("with message id %d", m->messageId); */ + /* logD("and status %x", m->status); */ + if (logger) { + // when agent is logger, store message + //logD("store message"); + if (m->id >= ARRAY_SIZE(agentf)) { + logE("Invalid id"); + } + elif (m->id == id) { + logE("Invalid id, the logger doesn't send packets"); + } + else { + record.m = *m; + record.time = getCurrentUnixTime(); + write(agentf[m->id], &record, sizeof(record)); + + cleanAllocateSmallArray(mailMsg); + + // update state for this agent + cleanFinishSmallDictP(agent) = getG(completeCfg, rtSmallDictt, agents[m->id]); + setG(agent, "time", getCurrentUnixTime()); + char *newstate = m->messageId ? "alive" : "init"; + if (!eqG($(agent, "state"), newstate)) { + if (eqG($(agent, "state"), "alive")) { + setG(agent, "rebooted", TRUE); + setG(agent, "lastBoot", getCurrentUnixTime()); + // send mail when agent rebooted + cleanCharP(s) = formatS("%s rebooted", agents[m->id]); + pushG(mailMsg, s); + logW("%s", s); + } + setG(agent, "state", newstate); + setG(agent, "last", getCurrentUnixTime()); + } + if (m->messageId < u$(agent,"mId")) { + setG(agent, "rebooted", TRUE); + setG(agent, "lastBoot", getCurrentUnixTime()); + // send mail when agent rebooted + cleanCharP(s) = formatS("%s rebooted", agents[m->id]); + pushG(mailMsg, s); + logW("%s", s); + } + setG(agent, "mId", m->messageId); + cleanFinishSmallArrayP(probes) = getG(agent, rtSmallArrayt, "probes"); + if (probes) { + iter(probes, P) { + cast(smallDictt*,p,P); + bool newstate = m->status & (1UL << iI(probes)); + if (getG(p, rtBool, "state") != newstate) { + setG(p, "state", newstate); + setG(p, "last", getCurrentUnixTime()); + // send mail when service is down + char *state = newstate ? "down" : "up"; + char *result = newstate ? "failed" : "ok"; + if (hasG(p, "port")) { + cleanCharP(s) = formatS("Service on port %d running in %s is %s", u$(p, "port"), agents[m->id], state); + pushG(mailMsg, s); + logW("%s", s); + } + elif (hasG(p, "if")) { + cleanCharP(s) = formatS("Network interface %s in %s is down", u$(p, "if"), agents[m->id]); + pushG(mailMsg, s); + logW("%s", s); + } + elif (hasG(p, "cmd")) { + cleanCharP(s) = formatS("'%s' running in %s is %s", u$(p, "cmd"), agents[m->id], result); + pushG(mailMsg, s); + logW("%s", s); + } + } + } + } // if probes + + // send mail + if (lenG(mailMsg)) { + pError0(writeFileG(mailMsg, "mail.txt")); + cleanFinishSmallArrayP(mails) = getG(agent, rtSmallArrayt, "mails"); + char *email = $(mails, 0); + cleanCharP(mutt) = formatS("mutt -s \"%s\" %s < mail.txt", mailSubject, email); + sendMail(mutt); + } + + // TODO send to monitor + //lv(completeCfg); + } + } + else { + // when agent is not logger, forward message + //logD("forward message"); + if (sendto(sock, &buf, r, 0, (const struct sockaddr *)&server, sizeof(server)) < 0) { + logE("send failed: %s", strerror(errno)); + close(sock); + XFailure; + } + } + time = getMonotonicTime(); + if (time < nextTime) { + // adjust timeout to have period delay between the sent messages from this agent + u64 delta = nextTime - time; + u64 sec = delta / 1000000000UL; + u64 usec = (delta - sec * 1000000000UL) / 1000; + /* lv(sec); */ + /* lv(usec); */ + setTimeout(bridgesock, sec, usec); + goto listenToOthers; + } + } + + cont:; + } + + // close files + if (logger) { + arange(i, agentf) { + close(agentf[i]); + } + } + close(sock); +} + +/* send mail in another process + */ +void sendMail(char *cmd) { + pid_t pid; + + pid = fork(); + switch (pid) { + case -1: + logE("spawnProc error: %s", strerror(errno)); + XFAILURE + case 0:; + loop(5) { + var r = logSystem(cmd); + if (!r) break; + sleep(5); + } + XSUCCESS; + default: + break; + } + + return; +} +// vim: set expandtab ts=2 sw=2: diff --git a/package.yml b/package.yml @@ -0,0 +1,18 @@ +--- + name: heartbeat + version: 0.0.1 + description: explanation + bin: ./heartbeat.c + repository: + type: git + url: "git+https://github.com/USER/heartbeat.git" + keywords: + - command + author: Anonymous + license: MIT + bugs: + url: "https://github.com/USER/heartbeat/issues" + homepage: "https://github.com/USER/heartbeat#readme" + private: false + dependencies: + short: "" diff --git a/shpPackages/short/main.c b/shpPackages/short/main.c @@ -0,0 +1,29 @@ +#! /usr/bin/env sheepy +/* or direct path to sheepy: #! /usr/local/bin/sheepy */ + +/* Libsheepy documentation: http://spartatek.se/libsheepy/ */ +#include "libsheepyObject.h" +#include "short.h" + +int argc; char **argv; + +/* enable/disable logging */ +/* #undef pLog */ +/* #define pLog(...) */ + +int main(int ARGC, char** ARGV) { + + argc = ARGC; argv = ARGV; + + initLibsheepy(ARGV[0]); + + createSmallDict(d); + + $(&d, "wer"); + + i_("de"); + + lv(&d); + +} +// vim: set expandtab ts=2 sw=2: diff --git a/shpPackages/short/package.yml b/shpPackages/short/package.yml @@ -0,0 +1,15 @@ +--- + name: short + version: 0.0.3 + description: "convenient defines for often used libsheepy function calls making the lines shorter and more readable" + bin: ./short.h + repository: + type: git + url: git+https://github.com/USER/short.git + keywords: + - utility + author: Remy + license: MIT + bugs: + url: https://github.com/USER/short/issues + homepage: https://github.com/USER/short#readme diff --git a/shpPackages/short/short.h b/shpPackages/short/short.h @@ -0,0 +1,15 @@ +#include "libsheepyObject.h" + +#define lv logTVarG +#define i_ logI + +#define iI iterIndexG +#define iK iterKeyG + +// get string from array or dict +#define $(object, key) getG(object, rtChar, key) + +// get unsigned int +#define u$(object, key) getG(object, rtU64, key) + +// vim: set expandtab ts=2 sw=2: