blob: cbc2326a552638ae0965ac18a9c5b4a8598f758c [file] [log] [blame]
/****************************************************************************
* apps/netutils/chat/chat.c
*
* Copyright (C) 2016 Vladimir Komendantskiy. All rights reserved.
* Author: Vladimir Komendantskiy <vladimir@moixaenergy.com>
* Partly based on code by Max Nekludov <macscomp@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name NuttX nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <debug.h>
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "chat.h"
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
#define CHAT_TOKEN_SIZE 128
/****************************************************************************
* Pivate types
****************************************************************************/
/* Type of singly-linked list of tokens */
struct chat_token
{
FAR char *string;
bool no_termin;
FAR struct chat_token *next;
};
/****************************************************************************
* Private Functions
****************************************************************************/
void chat_init(FAR struct chat *priv, FAR struct chat_ctl *ctl)
{
DEBUGASSERT(priv != NULL && ctl != NULL && ctl->timeout >= 0);
memcpy(&priv->ctl, ctl, sizeof(struct chat_ctl));
priv->script = NULL;
}
/* Linear one-pass tokenizer. */
static int chat_tokenise(FAR struct chat *priv,
FAR const char *script,
FAR struct chat_token **first_tok)
{
FAR const char *cursor = script; /* pointer to the current character */
unsigned int quoted = 0; /* two-valued:
* 1: quoted (expecting a closing quote)
* 0: not quoted */
unsigned int escaped = 0; /* two-valued */
char tok_str[CHAT_TOKEN_SIZE]; /* current token buffer */
int tok_pos = 0; /* current length of the current token */
FAR struct chat_token *tok = NULL; /* the last complete token */
bool no_termin; /* line termination property:
* true if line terminator is deasserted */
int ret = 0;
/* Delimiter handler */
int tok_on_delimiter(void)
{
if (!tok_pos && !quoted && !no_termin)
{
/* a) the first character in the script is a delimiter or
* b) the previous character was a delimiter,
* and in both cases it is not the empty string token,
* hence skipping.
*/
return 0;
}
/* Terminate the temporary */
tok_str[tok_pos] = '\0';
if (tok)
{
tok->next = malloc(sizeof(struct chat_token));
/* The terminated token becomes previous */
tok = tok->next;
}
else
{
/* There was no previous token */
*first_tok = malloc(sizeof(struct chat_token));
tok = *first_tok;
}
if (!tok)
{
/* out of memory */
return -ENOMEM;
}
/* Copy the temporary */
tok->string = strdup(tok_str);
tok->no_termin = no_termin;
/* Initialize the next token */
tok->next = NULL;
/* Reset the buffer position */
tok_pos = 0;
ninfo("%s (%d)\n", tok->string, tok->no_termin);
return 0;
}
/* Tokenizer start */
DEBUGASSERT(script != NULL);
ninfo("%s\n", script);
while (!ret && *cursor != '\0')
{
/* Assert line terminator */
no_termin = false;
switch (*cursor)
{
case '\\':
if (!quoted)
{
if (escaped)
{
tok_str[tok_pos++] = '\\';
}
escaped ^= 1;
}
else
{
/* Quoted */
tok_str[tok_pos++] = '\\';
}
break;
case '"':
if (escaped)
{
tok_str[tok_pos++] = '"';
escaped = 0;
}
else
{
/* Not escaped */
if (quoted && !tok_pos)
{
/* Empty string token */
ret = tok_on_delimiter();
}
quoted ^= 1;
/* No effect on the position in the temporary */
}
break;
case ' ':
case '\n':
case '\r':
case '\t':
if (quoted)
{
/* Append the quoted character to the temporary */
tok_str[tok_pos++] = *cursor;
}
else
{
ret = tok_on_delimiter();
}
break;
default:
if (escaped)
{
switch (*cursor)
{
case 'n':
tok_str[tok_pos++] = '\n';
break;
case 'r':
tok_str[tok_pos++] = '\r';
break;
case 'c':
/* Deassert line terminator */
no_termin = true;
break;
default:
ret = -EINVAL;
break;
}
escaped = 0;
}
else
{
/* Append the regular character to the temporary */
tok_str[tok_pos++] = *cursor;
}
break;
}
/* Shift the cursor right through the input string */
cursor++;
}
if (!ret && *cursor == '\0')
{
/* Treat the null terminator as an EOF delimiter */
ret = tok_on_delimiter();
}
ninfo("result %d\n", ret);
return ret;
}
/* Creates the internal representation of a tokenised chat script. */
static int chat_internalise(FAR struct chat *priv,
FAR struct chat_token *tok)
{
unsigned int rhs = 0; /* 1 if processing the right-hand side,
* 0 otherwise */
FAR struct chat_line *line = NULL;
int len; /* length of the destination string when variable */
int ret = 0;
while (tok && !ret)
{
DEBUGASSERT(tok->string);
ninfo("(%c) %s\n", rhs ? 'R' : 'L', tok->string);
if (!rhs)
{
/* Create a new line */
if (!line)
{
/* First line */
line = malloc(sizeof(struct chat_line));
priv->script = line;
}
else
{
/* Subsequent line */
line->next = malloc(sizeof(struct chat_line));
line = line->next;
}
if (!line)
{
ret = -ENOMEM;
break;
}
line->next = NULL;
}
if (rhs)
{
len = strlen(tok->string) + 1;
if (!tok->no_termin)
{
/* Add space for the line terminator */
len += 2;
}
line->rhs = malloc(len);
if (line->rhs)
{
/* Copy the token and add the line terminator as appropriate */
snprintf(line->rhs, len,
tok->no_termin ? "%s" : "%s\r\n", tok->string);
}
else
{
ret = -ENOMEM;
}
}
else
{
if (!strcmp(tok->string, "ABORT"))
{
line->type = CHAT_LINE_TYPE_COMMAND;
line->lhs.command = CHAT_COMMAND_ABORT;
}
else if (!strcmp(tok->string, "ECHO"))
{
line->type = CHAT_LINE_TYPE_COMMAND;
line->lhs.command = CHAT_COMMAND_ECHO;
}
else if (!strcmp(tok->string, "PAUSE"))
{
line->type = CHAT_LINE_TYPE_COMMAND;
line->lhs.command = CHAT_COMMAND_PAUSE;
}
else if (!strcmp(tok->string, "SAY"))
{
line->type = CHAT_LINE_TYPE_COMMAND;
line->lhs.command = CHAT_COMMAND_SAY;
}
else if (!strcmp(tok->string, "TIMEOUT"))
{
line->type = CHAT_LINE_TYPE_COMMAND;
line->lhs.command = CHAT_COMMAND_TIMEOUT;
}
else
{
/* Not a command, hence an expectation */
line->type = CHAT_LINE_TYPE_EXPECT_SEND;
line->lhs.expect = strdup(tok->string);
if (!line->lhs.expect)
{
ret = -ENOMEM;
}
}
/* Initialise the rhs - for 'free' in case of error */
line->rhs = NULL;
}
/* Alternate between left-hand side and right-hand side */
rhs ^= 1;
tok = tok->next;
}
if (!ret && rhs)
{
/* The right-hand side of a line is missing */
ret = -ENODATA;
}
ninfo("result %d, rhs %d\n", ret, rhs);
return ret;
}
/* Chat token list deallocator */
static void chat_tokens_free(FAR struct chat_token *first_tok)
{
FAR struct chat_token *next_tok;
while (first_tok)
{
next_tok = first_tok->next;
DEBUGASSERT(first_tok->string != NULL);
free(first_tok->string);
free(first_tok);
first_tok = next_tok;
}
ninfo("tokens freed\n");
}
/* Main parsing function. */
static int chat_script_parse(FAR struct chat *priv, FAR const char *script)
{
FAR struct chat_token *first_tok;
int ret;
ret = chat_tokenise(priv, script, &first_tok);
if (!ret)
{
ret = chat_internalise(priv, first_tok);
}
chat_tokens_free(first_tok);
return ret;
}
/* Polls the file descriptor for a specified number of milliseconds and, on
* successful read, stores 1 read byte at location pointed by 'c'. Otherwise
* returns a negative error code.
*/
static int chat_readb(FAR struct chat *priv, FAR char *c, int timeout_ms)
{
struct pollfd fds;
int ret;
fds.fd = priv->ctl.fd;
fds.events = POLLIN;
fds.revents = 0;
ret = poll(&fds, 1, timeout_ms);
if (ret <= 0)
{
ninfo("poll timed out\n");
return -ETIMEDOUT;
}
ret = read(priv->ctl.fd, c, 1);
if (ret != 1)
{
ninfo("read failed\n");
return -EPERM;
}
if (priv->ctl.echo)
{
fputc(*c, stderr);
}
ninfo("read \'%c\' (0x%02X)\n", *c, *c);
return 0;
}
static void chat_flush(FAR struct chat *priv)
{
char c;
ninfo("starting\n");
while (chat_readb(priv, &c, 0) == 0);
ninfo("done\n");
}
static int chat_expect(FAR struct chat *priv, FAR const char *s)
{
char c;
struct timespec abstime;
struct timespec endtime;
int timeout_ms;
int s_len = strlen(s);
int i_match = 0; /* index of the next character to be matched in s */
int ret = 0;
/* Get initial time and set the end time */
clock_gettime(CLOCK_REALTIME, (FAR struct timespec *)&abstime);
endtime.tv_sec = abstime.tv_sec + priv->ctl.timeout;
endtime.tv_nsec = abstime.tv_nsec;
while (!ret && i_match < s_len &&
(abstime.tv_sec < endtime.tv_sec ||
(abstime.tv_sec == endtime.tv_sec &&
abstime.tv_nsec < endtime.tv_nsec)))
{
timeout_ms =
(endtime.tv_sec - abstime.tv_sec) * 1000 +
(endtime.tv_nsec - abstime.tv_nsec) / 1000000;
DEBUGASSERT(timeout_ms >= 0);
ret = chat_readb(priv, &c, timeout_ms);
if (!ret)
{
if (c == s[i_match])
{
/* Continue matching the next character */
i_match++;
}
else
{
/* Match failed; start anew */
i_match = 0;
}
/* Update current time */
clock_gettime(CLOCK_REALTIME, (FAR struct timespec *)&abstime);
}
}
ninfo("result %d\n", ret);
return ret;
}
static int chat_send(FAR struct chat *priv, FAR const char *s)
{
int ret = 0;
int len = strlen(s);
/* 'write' returns the number of successfully written characters */
ret = write(priv->ctl.fd, s, len);
ninfo("wrote %d out of %d bytes of \'%s\'\n", ret, len, s);
if (ret > 0)
{
/* Just SUCCESS */
ret = 0;
}
return ret;
}
static int chat_line_run(FAR struct chat *priv,
FAR const struct chat_line *line)
{
int ret = 0;
int numarg;
ninfo("type %d, rhs %s\n", line->type, line->rhs);
switch (line->type)
{
case CHAT_LINE_TYPE_COMMAND:
if (priv->ctl.verbose)
{
fprintf(stderr, "chat: cmd %d, arg %s\n",
line->lhs.command, line->rhs);
}
switch (line->lhs.command)
{
case CHAT_COMMAND_ABORT:
/* TODO */
break;
case CHAT_COMMAND_ECHO:
if (strcmp(line->rhs, "ON"))
{
priv->ctl.echo = true;
}
else
{
priv->ctl.echo = false;
}
break;
case CHAT_COMMAND_PAUSE:
numarg = atoi(line->rhs);
if (numarg < 0)
{
numarg = 0;
}
sleep(numarg);
break;
case CHAT_COMMAND_SAY:
fprintf(stderr, "%s\n", line->rhs);
break;
case CHAT_COMMAND_TIMEOUT:
numarg = atoi(line->rhs);
if (numarg < 0)
{
ninfo("invalid timeout string %s\n", line->rhs);
}
else
{
ninfo("timeout is %d s\n", numarg);
priv->ctl.timeout = numarg;
}
break;
default:
break;
}
break;
case CHAT_LINE_TYPE_EXPECT_SEND:
if (priv->ctl.verbose)
{
fprintf(stderr, "chat: %s %s\n", line->lhs.expect, line->rhs);
}
ret = chat_expect(priv, line->lhs.expect);
if (!ret)
{
/* Discard anything after the confirmed expectation */
chat_flush(priv);
ret = chat_send(priv, line->rhs);
}
break;
default:
ret = -EINVAL;
break;
}
return ret;
}
static int chat_script_run(FAR struct chat *priv)
{
FAR struct chat_line *line = priv->script;
int ret = 0;
#ifdef CONFIG_DEBUG_INFO
int line_num = 0;
#endif
while (!ret && line)
{
ret = chat_line_run(priv, line);
if (!ret)
{
line = line->next;
#ifdef CONFIG_DEBUG_INFO
line_num++;
#endif
}
}
#ifdef CONFIG_DEBUG_INFO
ninfo("Script result %d, exited on line %d\n", ret, line_num);
#endif
return ret;
}
static int chat_script_free(FAR struct chat *priv)
{
FAR struct chat_line *line = priv->script;
FAR struct chat_line *next_line;
int ret = 0;
while (line)
{
next_line = line->next;
if (line->type == CHAT_LINE_TYPE_EXPECT_SEND)
{
free(line->lhs.expect);
}
free(line->rhs);
free(line);
line = next_line;
}
priv->script = NULL;
return ret;
}
/****************************************************************************
* Public Functions
****************************************************************************/
int chat(FAR struct chat_ctl *ctl, FAR const char *script)
{
int ret = 0;
struct chat priv;
DEBUGASSERT(script != NULL);
chat_init(&priv, ctl);
ret = chat_script_parse(&priv, script);
if (!ret)
{
/* TODO: optionally, free 'script' here */
ret = chat_script_run(&priv);
}
chat_script_free(&priv);
return ret;
}