Files
sics/ascon.c
zolliker 61341b52f4 various improvements
- use dig for resolving host names
- ascon.c: fix terminator parsing
- property callback: change property before callback
- logger.c:default for logger period must be the old value instead of 1
- add frappy type history writing
- increase max. logreader line length
- HIPNONE returns "null" with json protocol
- encode strings properly in formatNameValue
- fix memory leak in json2tcl
- scriptcontext: do not show debug messages when script starts with underscore or when the "send" property is empty
- scriptcontext: remove args for action timestamp
- scriptcontext: "que" function will replace an already queued action, e.g. for 'halt
- introduced updatestatus script
2021-09-16 12:26:18 +02:00

847 lines
19 KiB
C

#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include "sics.h"
#include "splitter.h"
#include "ascon.i"
#include "uselect.h"
#include "socketaddr.h"
static double lastClose = 0; /* time of last close operation */
static AsconProtocol *protocols = NULL;
void AsconError(Ascon *a, char *msg, int errorno)
{
static char *stateText[]={
"not connected",
"connect start",
"connecting",
"connect done",
"write start",
"writing",
"write done",
"read start",
"reading",
"read done",
"idle",
"failed",
"no response"
};
char *state;
char num[8];
if ( a->state >= AsconMaxState) {
state = "bad state";
} else {
state = stateText[a->state];
}
DynStringCopy(a->errmsg, "ASCERR: ");
if (errorno == 0) {
DynStringConcat(a->errmsg, msg);
DynStringConcat(a->errmsg, " (");
DynStringConcat(a->errmsg, state);
DynStringConcat(a->errmsg, " state)");
DynStringConcat(a->errmsg, " on ");
DynStringConcat(a->errmsg, a->hostport);
} else {
DynStringConcat(a->errmsg, strerror(errorno));
DynStringConcat(a->errmsg, " (");
DynStringConcat(a->errmsg, state);
DynStringConcat(a->errmsg, " state, ");
DynStringConcat(a->errmsg, msg);
DynStringConcat(a->errmsg, ")");
DynStringConcat(a->errmsg, " on ");
DynStringConcat(a->errmsg, a->hostport);
}
a->state = AsconFailed;
}
static void AsconConnect(Ascon * a)
{
/* input state: AsconConnectStart
output state: AsconFailed or AsconConnecting */
int ret;
struct sockaddr adr;
char *colon;
int port;
int oldopts;
char ipaddress[16];
/* wait 0.5 sec before connecting again after a close
2 reasons for that:
- it seems that connecting immediately to a closed port fails.
We will avoid some "Connection refused" error messages.
- a bug in the lantronix terminal: if a channel is closed and reopened
within short time the connect may be succesful, but a message will be
sent on the channel!
In principle we need only to wait when connecting to the same address
and port, but bookkeeping would be too complicated.
*/
if (DoubleTime() < lastClose + 0.5) return;
if (a->fd < 0) {
a->fd = socket(AF_INET, SOCK_STREAM, 0);
if (a->fd < 0) {
AsconError(a, "ASC1", errno);
return;
}
}
if (a->hostport == NULL) {
AsconError(a, "no host:port given", 0);
return;
}
colon = strchr(a->hostport, ':');
if (colon == NULL) {
if (strcmp(a->hostport, "unconnected") == 0) {
AsconError(a, "disconnected", 0);
} else {
AsconError(a, "expected 'host:port' or 'unconnected'", 0);
}
return;
}
port = atoi(colon + 1);
if (port <= 0) {
AsconError(a, "bad port number", 0);
return;
}
*colon = '\0';
ret = MakeSocketAddr(&adr, a->hostport, port, a->ip);
*colon = ':';
if (ret == 0) {
AsconError(a, "bad host specification", 0);
return;
}
oldopts = fcntl(a->fd, F_GETFL, 0);
fcntl(a->fd, F_SETFL, oldopts | O_NONBLOCK);
ret = connect(a->fd, &adr, sizeof(struct sockaddr_in));
if (ret < 0) {
switch (errno) {
case EINPROGRESS:
case EALREADY:
case EISCONN:
a->state = AsconConnecting;
break;
default:
AsconError(a, "ASC2", errno);
return;
}
}
a->state = AsconConnecting;
return;
}
int AsconReadGarbage(int fd)
{
fd_set rmask;
struct timeval tmo = { 0, 0 };
int l, ret, result;
char garbage[100];
FD_ZERO(&rmask);
result = 0;
do {
FD_SET(fd, &rmask);
ret = uselect(fd + 1, &rmask, NULL, NULL, &tmo);
if (ret > 0) {
l = recv(fd, garbage, sizeof garbage - 1, 0);
if (l > 0) {
/* swallow */
garbage[l] = '\0';
/* printf("(((%s)))\n", garbage); */
result += l;
} else if (l == 0) {
errno = ECONNRESET;
return -2;
} else if (l < 0) {
return -2;
}
}
} while (ret > 0);
return result;
}
void PrintChar(char chr)
{
if (chr <= 32 || chr >= 127) {
printf("%2.2x ", chr);
} else {
printf(" %c ", chr);
}
}
int AsconConnectSuccess(int fd)
{
fd_set wmask, rmask;
struct timeval tmo = { 0, 0 };
int oldopts;
int ret;
oldopts = fcntl(fd, F_GETFL, 0);
assert(oldopts | O_NONBLOCK); /* fd must be in non-blocking mode */
FD_ZERO(&wmask);
FD_ZERO(&rmask);
FD_SET(fd, &wmask);
FD_SET(fd, &rmask);
ret = uselect(fd + 1, &rmask, &wmask, NULL, &tmo);
if (ret > 0) {
/**
* MK:
* This assertion triggered for some reason: as writing is only done later
* I moved the test for ISSET(wmask) down there
* assert(FD_ISSET(fd, &wmask));
* MZ:
* I remember having found this logic on the www. Anyway, your way must be o.k. too
*/
if (FD_ISSET(fd, &rmask)) { /* there may already be data for read */
if (recv(fd, NULL, 0, 0) < 0) { /* zero length, check only return value */
ret = ASCON_RECV_ERROR; /* first recv failed */
}
} else {
if (FD_ISSET(fd,&wmask)) {
if (send(fd, NULL, 0, 0) < 0) { /* zero length, check only return value */
ret = ASCON_SEND_ERROR; /* first send failed */
}
}
}
}
fcntl(fd, F_SETFL, oldopts & ~O_NONBLOCK); /* reset to blocking mode */
return ret;
}
int AsconReadChar(int fd, char *chr)
{
fd_set rmask;
struct timeval tmo = { 0, 0 };
int ret;
FD_ZERO(&rmask);
FD_SET(fd, &rmask);
ret = uselect(fd + 1, &rmask, NULL, NULL, &tmo);
if (ret <= 0)
return ret;
ret = recv(fd, chr, 1, 0);
/* PrintChar(*chr); fflush(stdout); */
if (ret > 0)
return 1;
if (ret == 0) {
errno = ECONNRESET;
return ASCON_DISCONNECTED;
}
return ASCON_RECV_ERROR;
}
int AsconWriteChars(int fd, char *data, int length)
{
fd_set wmask;
struct timeval tmo = { 0, 0 };
int ret;
if (length <= 0)
return 0;
/* { int i;
for (i=0; i<length; i++) {
PrintChar(data[i]);
}
}
printf("<written\n");
*/
FD_ZERO(&wmask);
FD_SET(fd, &wmask);
ret = uselect(fd + 1, NULL, &wmask, NULL, &tmo);
if (ret <= 0)
return ASCON_SELECT_ERROR;
ret = send(fd, data, length, 0);
if (ret > 0)
return ret;
if (ret == 0) {
errno = ECONNRESET;
return ASCON_DISCONNECTED;
}
return ASCON_SEND_ERROR;
}
static double lastCall = 0;
int AsconBaseHandler(Ascon * a)
{
int ret;
int l;
char chr;
double now = DoubleTime();
if (now > lastCall + 0.5) { /* AsconBaseHandler was not called since a long time (0.5 sec) */
if (lastCall != 0) { /* extend timeout time (for debugging purposes) */
a->start += now - lastCall - 0.5;
}
}
lastCall = now;
switch (a->state) {
case AsconConnectStart:
AsconConnect(a);
break;
case AsconConnecting:
ret = AsconConnectSuccess(a->fd);
if (ret == 0) {
/* in progress */
} else if (ret > 0) {
DynStringClear(a->errmsg);
a->state = AsconConnectDone; /* success */
} else if (ret < 0) {
AsconError(a, "ASC3", errno);
}
break;
case AsconWriteStart:
if (a->sendTerminator) {
DynStringConcat(a->wrBuffer, a->sendTerminator);
}
a->wrPos = 0;
a->state = AsconWriting;
break;
case AsconWriting:
AsconReadGarbage(a->fd);
l = GetDynStringLength(a->wrBuffer) - a->wrPos;
ret = AsconWriteChars(a->fd, GetCharArray(a->wrBuffer) + a->wrPos, l);
if (ret < 0) {
AsconError(a, "ASC4", errno); /* sets state to AsconFailed */
} else {
a->wrPos += ret;
if (a->wrPos >= GetDynStringLength(a->wrBuffer)) {
a->state = AsconWriteDone;
}
}
break;
case AsconReadStart:
DynStringClear(a->rdBuffer);
a->start = DoubleTime();
a->state = AsconReading;
break;
case AsconReading:
ret = AsconReadChar(a->fd, &chr);
if (ret <= 0) {
if (ret < 0) {
AsconError(a, "ASC5", errno);
return 0;
}
if (a->timeout > 0) {
if (DoubleTime() - a->start > a->timeout) {
AsconError(a, "no response", 0);
a->state = AsconTimeout;
}
}
return 0;
}
a->lastChar = chr;
a->start = DoubleTime();
if (DynStringConcatChar(a->rdBuffer, chr) == 0) {
AsconError(a, "ASC6", errno);
return 0;
}
break;
default:
break;
}
return 1;
}
void AsconInsertProtocol(AsconProtocol *protocol) {
AsconProtocol *p;
for (p = protocols; p != NULL; p = p->next) {
if (p == protocol)
return;
}
protocol->next = protocols;
protocols = protocol;
}
AsconHandler AsconSetHandler(Ascon *a, SConnection *con,
int argc, char *argv[])
{
AsconProtocol *p;
if (argc < 1)
return NULL;
if (strcasecmp(argv[0], "std") == 0) {
if (argc < 2)
return NULL;
if (AsconStdInit(a, con, argc, argv)) {
return AsconStdHandler;
} else {
return NULL;
}
}
for (p = protocols; p!= NULL; p=p->next) {
if (strcasecmp(p->name, argv[0]) == 0) {
if (p->init(a, con, argc, argv)) {
return p->handler;
} else {
return NULL;
}
}
}
return NULL;
}
/* --- standard handler ---- */
int AsconStdHandler(Ascon * a)
{
int result;
char chr;
int ret, l;
char *cmd, *opt, *buf;
switch (a->state) {
case AsconWriteStart:
cmd = GetCharArray(a->wrBuffer);
a->lineCount = 1;
if (a->separator != NULL) { /* multiline mode enabled */
l = strlen(cmd);
if (l> 0 && cmd[l-1] == '}') {
opt = strrchr(cmd, '{');
if (opt != NULL) {
if (sscanf(opt, "{%d}", &a->lineCount) == 1) {
/* remove option */
for (l = strlen(opt); l > 0; l--) {
DynStringBackspace(a->wrBuffer);
}
}
}
}
} else if (strstr(GetCharArray(a->wrBuffer), "@@NOSEND@@") != NULL) {
a->state = AsconWriteDone;
return 1;
}
if(a->lineCount == 0){
a->noResponse = 1;
} else {
a->noResponse = 0;
}
break; /* go to the base handler */
case AsconReading:
if (a->lineCount == 0) {
/* no response expected */
a->state = AsconReadDone;
return 1;
}
result = AsconBaseHandler(a);
if (result == 0)
return 0;
chr = a->lastChar;
if (chr == '\0') { /* skip NUL characters */
DynStringBackspace(a->rdBuffer); /* remove NUL */
return 1;
}
if (a->replyTerminator != NULL) {
if (strchr(a->replyTerminator, chr) != NULL) {
if (a->compositeTerminator) {
/* one character was o.k., but all have to match */
l = strlen(a->replyTerminator);
buf = GetCharArray(a->rdBuffer) + GetDynStringLength(a->rdBuffer) - l;
if (strncmp(buf, a->replyTerminator, l) == 0) {
a->state = AsconReadDone;
}
} else {
a->state = AsconReadDone;
if (chr == '\n' || chr == '\r') {
DynStringBackspace(a->rdBuffer); /* remove LF or CR */
}
}
}
} else {
if (chr == '\n') { /* LF */
DynStringBackspace(a->rdBuffer); /* remove LF */
if (a->readState) { /* last char was CR */
/* LF after CR is not a terminator */
a->readState = 0;
} else {
a->state = AsconReadDone;
}
} else if (chr == '\r') { /* CR */
DynStringBackspace(a->rdBuffer); /* remove CR */
a->readState = 1; /* set 'last char was CR' */
a->state = AsconReadDone;
}
}
if (a->state == AsconReadDone && a->lineCount > 1) {
if (a->separator != NULL) {
DynStringConcat(a->rdBuffer, a->separator);
}
a->lineCount--;
a->state = AsconReading;
}
return 1; /* base handler was already called */
default:
break;
}
return AsconBaseHandler(a);
}
static void checkTerminator(char *term) {
int c, l;
if (term == NULL) return;
l = strlen(term);
if (l > 1 && term[0] == '"' && term[l-1] == '"') {
memmove(term, term+1, l-2);
term[l-2] = 0;
}
if (strncmp(term,"0x",2) == 0) {
sscanf(term,"%x",&c);
term[0] = (char)c;
term[1] = '\0';
}
}
/**
* Treat hex strings as terminators right. Note that this
* is limited to single character terminators.
* M.Z. changed strstr to strncmp (more precise)
*
* M.Z. add new option (composite terminator):
* convert 'term' to term
*/
void AsconCheckTerminators(Ascon *a)
{
int i, l;
checkTerminator(a->sendTerminator);
checkTerminator(a->replyTerminator);
a->compositeTerminator = 0;
if (a->replyTerminator != NULL && a->replyTerminator[0] == '\'') {
l = strlen(a->replyTerminator);
if (l > 2 && a->replyTerminator[l-1] == '\'') {
for (i = 0; i < l - 2; i++) {
a->replyTerminator[i] = a->replyTerminator[i+1];
}
a->replyTerminator[l-2] = '\0';
a->compositeTerminator = 1;
}
}
}
int AsconInterpreteArgs(int argc, char *argv[],
int parc, char *parn[], char *pars[])
{
/* interprete arguments in the form "name=value" or
with a fixed order. The result is 1 in case
of success or 0 in case of an error. */
int ia, ip, l;
for (ip = 0; ip < parc; ip++) {
pars[ip] = NULL;
}
if (argc == 0) return 1;
if (strchr(argv[0], '=') == NULL) {
for (ia = 0; ia < argc; ia++) {
if (ia >= ip) {
return 0;
}
if (strchr(argv[ia], '=') != NULL) {
return 0;
}
pars[ia] = argv[ia];
}
return 1;
}
for (ia = 0; ia < argc; ia++) {
for (ip = 0; ip < parc; ip++) {
l = strlen(parn[ip]);
if (strncasecmp(argv[ia], parn[ip], l) == 0 && argv[ia][l] == '=') {
pars[ip] = argv[ia] + l + 1;
break;
}
}
if (ip >= parc) {
return 0;
}
}
return 1;
}
int AsconStdInit(Ascon *a, SConnection *con, int argc, char *argv[])
{
enum nPars {NA=4};
char *pars[NA];
static char *parn[NA]={
"sendterminator",
"timeout",
"replyterminator",
"lineseparator"
};
char *msg;
assert(argc>1);
a->hostport = strdup(argv[1]);
if (!AsconInterpreteArgs(argc-2, argv+2, NA, parn, pars)) {
return 0;
}
if (pars[0]) {
a->sendTerminator = strdup(pars[0]);
} else {
a->sendTerminator = strdup("\n");
}
if (pars[1] && pars[1][0] != '\0') {
a->timeout = atof(pars[1]);
} else {
a->timeout = 2.0; /* sec */
}
if (pars[2] && pars[2][0] != '\0') {
a->replyTerminator = strdup(pars[2]);
} else {
a->replyTerminator = NULL;
}
if (pars[3] && pars[3][0] != '\0') {
a->separator = strdup(pars[3]);
} else {
a->separator = NULL;
}
AsconCheckTerminators(a);
return 1;
}
/* --- implementation of higher level interface ---- */
char *ConcatArgs(int argc, char *argv[])
{
return Arg2Tcl(argc, argv, NULL, -1);
}
Ascon *AsconMake(SConnection * con, int argc, char *argv[])
{
Ascon *a;
char *args;
a = calloc(1, sizeof(*a));
if (a == NULL) {
SCWrite(con, "ERROR: no memory", eError);
return NULL;
}
a->fd = -1;
a->state = AsconConnectStart;
a->timeout = 2.0; /* 2 sec default */
a->reconnectInterval = 10; /* 10 sec default */
a->replyTerminator = NULL;
a->sendTerminator = NULL;
a->hostport = NULL;
a->responseValid = 0;
a->readState = 0;
a->lineCount = 1;
a->separator = NULL;
a->killPrivate = NULL;
a->private = NULL;
a->handler = AsconSetHandler(a, con, argc, argv);
if (a->handler == NULL) {
args = ConcatArgs(argc, argv);
if (!args)
return NULL;
SCPrintf(con, eError, "ERROR: illegal protocol: %s", args);
free(args);
return NULL;
}
a->rdBuffer = CreateDynString(60, 65536);
a->wrBuffer = CreateDynString(60, 63);
a->errmsg = CreateDynString(60, 63);
return a;
}
void AsconKill(Ascon * a)
{
if (a->fd > 0) {
close(a->fd);
lastClose = DoubleTime();
}
DeleteDynString(a->rdBuffer);
DeleteDynString(a->wrBuffer);
DeleteDynString(a->errmsg);
if (a->hostport) {
free(a->hostport);
}
if (a->sendTerminator) {
free(a->sendTerminator);
}
if (a->replyTerminator) {
free(a->replyTerminator);
}
if (a->separator) {
free(a->separator);
}
if (a->private != NULL && a->killPrivate != NULL) {
a->killPrivate(a->private);
}
free(a);
}
void AsconDisconnect(Ascon * a)
{
if (a->fd > 0) {
close(a->fd);
lastClose = DoubleTime();
}
a->fd = -1;
a->state = AsconNotConnected;
}
void AsconReconnect(Ascon * a, char * hostport)
{
if (a->fd > 0) {
close(a->fd);
lastClose = DoubleTime();
}
if (hostport != NULL && hostport[0] != '\0') {
if (a->hostport) {
free(a->hostport);
}
a->hostport = strdup(hostport);
}
a->fd = -1;
a->state = AsconConnectStart;
}
AsconStatus AsconTask(Ascon * a)
{
int result;
double now;
while (1) {
result = a->handler(a);
switch (a->state) {
case AsconNotConnected:
return AsconOffline;
case AsconConnecting:
return AsconConnectPending;
case AsconConnectDone:
a->state = AsconIdle;
DynStringClear(a->errmsg); /* connection o.k. */
return AsconReady;
case AsconWriteDone:
if (a->noResponse) {
a->state = AsconIdle;
return AsconReady;
}
a->state = AsconReadStart;
break;
case AsconReading:
if (result != 0) {
break; /* char received: try again, probably more characters pending */
}
return AsconPending;
case AsconReadDone:
a->state = AsconIdle;
a->responseValid = 1;
DynStringClear(a->errmsg);
return AsconReady;
case AsconReadDoneReconnect:
a->responseValid = 1;
DynStringClear(a->errmsg);
AsconReconnect(a,NULL);
return AsconReady;
case AsconIdle:
return AsconReady;
case AsconTimeout:
a->state = AsconIdle;
return AsconFailure;
case AsconFailed:
now = DoubleTime();
if (a->fd > 0) {
close(a->fd);
lastClose = now;
a->fd = -1;
}
if (a->reconnectInterval > 0 && now > a->lastReconnect + a->reconnectInterval) {
a->lastReconnect = now;
a->state = AsconConnectStart;
}
return AsconFailure;
default:
return AsconPending;
}
}
}
int AsconWrite(Ascon * a, char *command, int noResponse)
{
if (a->state != AsconIdle) {
/* this might happen if a script is sending after an error */
return 0;
}
DynStringCopy(a->wrBuffer, command);
a->noResponse = noResponse;
a->state = AsconWriteStart;
a->responseValid = 0;
return AsconTask(a);
}
char *AsconRead(Ascon * a)
{
if (a->noResponse) {
a->noResponse = 0;
return NULL;
}
if (a->state != AsconIdle) {
a->state = AsconIdle;
return "programming error in devser.c/ascon.c";
}
if (a->responseValid) {
a->responseValid = 0;
return GetCharArray(a->rdBuffer);
}
return NULL;
}
char *AsconGetError(Ascon *a)
{
return GetCharArray(a->errmsg);
}
int AsconLastState(Ascon *a)
{
return (int)a->state;
}
char *AsconHostport(Ascon *a)
{
if (a==NULL) {
return NULL;
}
return a->hostport;
}
char *AsconIP(Ascon *a)
{
if (a==NULL) {
return NULL;
}
return a->ip;
}
double AsconGetSetTimeout(Ascon *a, double timeout, int setmode) {
if (setmode) {
a->timeout = timeout;
}
return a->timeout;
}
int AsconReconnectInterval(Ascon *a, int interval) {
if (interval >= 0) {
a->reconnectInterval = interval;
}
return a->reconnectInterval;
}