| /**************************************************************************** |
| * 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; |
| } |