Skip to content

Commit

Permalink
A very basic RESP2 CLI to simplify manual testing (#162)
Browse files Browse the repository at this point in the history
* skeleton redis-cli

* basic redis-cli in place

* fix bugs

* rename cli to resp instead of redis

* add simple reconnect & exit logic

* Update README

* fix bug in main.c
  • Loading branch information
Yao Yue committed May 12, 2018
1 parent 5edcecd commit 1af761e
Show file tree
Hide file tree
Showing 17 changed files with 548 additions and 6 deletions.
1 change: 1 addition & 0 deletions .syntastic_c_config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-Ideps/ccommon/include
6 changes: 5 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ option(HAVE_ASSERT_LOG "assert_log enabled by default" ON)
option(HAVE_ASSERT_PANIC "assert_panic disabled by default" OFF)
option(HAVE_LOGGING "logging enabled by default" ON)
option(HAVE_STATS "stats enabled by default" ON)

option(TARGET_PINGSERVER "build pingserver binary" ON)
option(TARGET_SLIMREDIS "build slimredis binary" ON)
option(TARGET_SLIMCACHE "build slimcache binary" ON)
option(TARGET_TWEMCACHE "build twemcache binary" ON)

option(TARGET_RESPCLI "build resp-cli binary" ON)

option(COVERAGE "code coverage" OFF)

# Note: duplicate custom targets only works with Makefile generators, will break XCode & VS
Expand Down Expand Up @@ -113,7 +117,7 @@ include_directories(${include_directories}
"${CCOMMON_SOURCE_DIR}/include"
"${PROJECT_SOURCE_DIR}/src")

# server
# server & (cli) client
add_subdirectory(src)

# tests: always build last
Expand Down
4 changes: 0 additions & 4 deletions config/client.conf

This file was deleted.

2 changes: 2 additions & 0 deletions config/resp-cli.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
debug_log_level: 2
debug_log_file: resp-cli.log
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ add_subdirectory(util ${PROJECT_BINARY_DIR}/util)

# executables
add_custom_target(service)
add_subdirectory(client ${PROJECT_BINARY_DIR}/client)
add_subdirectory(server ${PROJECT_BINARY_DIR}/server)
9 changes: 9 additions & 0 deletions src/client/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
add_subdirectory(network)

if(TARGET_RESPCLI)
add_subdirectory(resp_cli)
endif()

if(TARGET_MEMCACHECLI)
add_subdirectory(memcache_cli)
endif()
12 changes: 12 additions & 0 deletions src/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
The idea of a CLI client comes from Redis, which builds `redis-cli` for easy
testing and interactive play. This is particularly useful for Redis because
the RESP protocol is more verbose than Memcached's ASCII protocol.

The command line prompt and CLI command format are both modeled after Redis.
We may also borrow code from `redis-cli.c` in the future. We want to
acknowledge the fact that redis-cli is an ongoing inspiration.

Since we only shallowly support the syntax portion of the Redis protocol, which
is RESP, the binary and related files are named accordingly to reflect that.
Actual command supported by the server may differ from one binary to another,
and may change over time as well.
1 change: 1 addition & 0 deletions src/client/network/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add_library(client-network cli_network.c)
62 changes: 62 additions & 0 deletions src/client/network/cli_network.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#include "cli_network.h"

#include "core/data/server.h"

#include <channel/cc_channel.h>
#include <channel/cc_tcp.h>
#include <stream/cc_sockio.h>

#include <netdb.h>

struct addrinfo hints;
struct addrinfo *ai = NULL;

struct network_config network_config = {LOCAL, NULL, SERVER_PORT};

channel_handler_st tcp_handler = {
.accept = NULL,
.reject = NULL,
.open = (channel_open_fn)tcp_connect,
.term = (channel_term_fn)tcp_close,
.recv = (channel_recv_fn)tcp_recv,
.send = (channel_send_fn)tcp_send,
.rid = (channel_id_fn)tcp_read_id,
.wid = (channel_id_fn)tcp_write_id
};

bool
cli_connect(struct buf_sock *client)
{
hints.ai_flags = AI_NUMERICSERV;
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

getaddrinfo(network_config.host, network_config.port, &hints, &ai);
if (client->hdl->open(ai, client->ch)) {
/* TODO: make socket blocking */
return true;
} else {
return false;
}
}


void
cli_disconnect(struct buf_sock *client)
{
client->hdl->term(client->ch);
}

bool
cli_reconnect(struct buf_sock *client)
{
cli_disconnect(client);
fwrite(DISCONNECT_MSG, sizeof(DISCONNECT_MSG), 1, stdout);
if (!cli_connect(client)) {
network_config.mode = OFFLINE;
return false;
} else {
fwrite(RECONNECT_MSG, sizeof(RECONNECT_MSG), 1, stdout);
return true;
}
}
46 changes: 46 additions & 0 deletions src/client/network/cli_network.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once

/* for CLI, a few things are simplified:
* - we only need one connection, so we keep it as a static global variable in
* the network module.
* - retry and timeout policy are coded into the network module as well, since
* we don't expect many edge cases (mostly used on localhost for testing or
* debuggin)
* - network IO should block
*/

#include <stream/cc_sockio.h>

#include <stdbool.h>
#include <stdint.h>

/* string argument in order: protocol, host, port */
#define PROMPT_FMT_OFFLINE "%s %s:%s (not connected) > "
#define PROMPT_FMT_LOCAL "%s :%s > " /* show protocol & port */
#define PROMPT_FMT_REMOTE "%s %s: > " /* show protocol & host */

#define SEND_ERROR "ERROR SENDING REQUEST\r\n"
#define RECV_ERROR "ERROR RECEIVING RESPONSE\r\n"
#define RECV_HUP "SERVER HUNG UP (e.g. due to syntax error)\r\n"
#define DISCONNECT_MSG "CLIENT DISCONNECTED\r\n"
#define RECONNECT_MSG "CLIENT RECONNECTED\r\n"

typedef enum cli_network {
LOCAL = 0,
REMOTE = 1,
OFFLINE = 2,
} cli_network_e;

struct network_config {
cli_network_e mode;
char * host;
char * port;
};

extern channel_handler_st tcp_handler;
extern struct network_config network_config;

/* network_config is used for cli_connect */
bool cli_connect(struct buf_sock *client);
void cli_disconnect(struct buf_sock *client);
bool cli_reconnect(struct buf_sock *client);
23 changes: 23 additions & 0 deletions src/client/resp_cli/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
set(SOURCE
${SOURCE}
cli.c
main.c
setting.c)

set(MODULES
client-network
protocol_redis
util)

set(LIBS
ccommon-static
${CMAKE_THREAD_LIBS_INIT})

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/_bin)
set(TARGET_NAME ${PROJECT_NAME}_resp-cli)

add_executable(${TARGET_NAME} ${SOURCE})
target_link_libraries(${TARGET_NAME} ${MODULES} ${LIBS})

install(TARGETS ${TARGET_NAME} RUNTIME DESTINATION bin)
add_dependencies(service ${TARGET_NAME})
183 changes: 183 additions & 0 deletions src/client/resp_cli/cli.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#include "cli.h"

#include "../network/cli_network.h"

#include <cc_debug.h>
#include <cc_mm.h>
#include <cc_print.h>

#include <ctype.h>
#include <sys/param.h>

#define PROTOCOL "resp"
#define IO_BUF_MAX 1024

struct iobuf {
char *input;
char *output;
size_t ilen;
size_t olen;
};

bool quit = false;
struct iobuf buf;

struct request *req;
struct response *rsp;
struct buf_sock *client;

void
cli_setup(respcli_options_st *options)
{
if (options != NULL) {
network_config.host = options->server_host.val.vstr;
network_config.port = options->data_port.val.vstr;
if (network_config.host == NULL) { /* if host is not provided it's local */
network_config.mode = LOCAL;
} else {
network_config.mode = REMOTE;
}
}

/* slacking on NULL check here because this is very unlikely to fail */
req = request_create();
rsp = response_create();
client = buf_sock_create();
client->hdl = &tcp_handler;

}

void
cli_teardown(void)
{
request_destroy(&req);
response_destroy(&rsp);
buf_sock_destroy(&client);
}

static void
_cli_prompt(void)
{
size_t len;

if (buf.output == NULL) {
buf.output = cc_alloc(IO_BUF_MAX);
}

switch (network_config.mode) {
case LOCAL:
len = cc_snprintf(buf.output, IO_BUF_MAX, PROMPT_FMT_LOCAL,
PROTOCOL, network_config.port);
buf.olen = MIN(len, IO_BUF_MAX - 1);
break;

case REMOTE:
len = cc_snprintf(buf.output, IO_BUF_MAX, PROMPT_FMT_REMOTE,
PROTOCOL, network_config.host);
buf.olen = MIN(len, IO_BUF_MAX - 1);
break;

case OFFLINE:
len = cc_snprintf(buf.output, IO_BUF_MAX, PROMPT_FMT_OFFLINE,
PROTOCOL, (network_config.host == NULL) ? "localhost" :
network_config.host, network_config.port);
buf.olen = MIN(len, IO_BUF_MAX - 1);
break;

default:
NOT_REACHED();
}
}


static void
_cli_parse_req(void)
{
char *p, *token;
struct element *el;

p = buf.input;
/* do not parse fully, just breaking down fields/tokens by delimiter */
while ((token = strsep(&p, " \t\r\n")) != NULL) {
if (isspace(*token) || *token == '\0') {
continue;
}
el = array_push(req->token);
el->type = ELEM_BULK;
el->bstr.len = strlen(token);
el->bstr.data = token;
}
}

static bool
_cli_onerun(void)
{
int status;

buf_reset(client->rbuf);
buf_reset(client->wbuf);
request_reset(req);
response_reset(rsp);

/* print prompt */
_cli_prompt();
fwrite(buf.output, buf.olen, 1, stdout);

/* wait for input, quit to exit the loop */
getline(&buf.input, &buf.ilen, stdin);
if (cc_strncmp(buf.input, "quit", 4) == 0) {
quit = true;
return true;
}

/* parse input buffer into request object, translate */
_cli_parse_req();
status = compose_req(&client->wbuf, req);
if (status < 0) {
/* TODO: handle OOM error */
}

/* issue command */
do {
status = buf_tcp_write(client);
} while (status == CC_ERETRY || status == CC_EAGAIN); /* retry write */
if (status != CC_OK) {
fwrite(SEND_ERROR, sizeof(SEND_ERROR), 1, stdout);
return false;
}

/* wait for complete response */
do {
status = buf_tcp_read(client);
if (status != CC_OK && status != CC_ERETRY) {
if (status == CC_ERDHUP) {
fwrite(RECV_HUP, sizeof(RECV_HUP), 1, stdout);
} else {
fwrite(RECV_ERROR, sizeof(RECV_ERROR), 1, stdout);
}
return false;
}
status = parse_rsp(rsp, client->rbuf);
} while (status == PARSE_EUNFIN);
client->rbuf->rpos = client->rbuf->begin;
fwrite(client->rbuf->begin, buf_rsize(client->rbuf), 1, stdout);

return true;
}


void
cli_run(void)
{
if (!cli_connect(client)) {
network_config.mode = OFFLINE;
}

while (!quit) {
if (!_cli_onerun() && !cli_reconnect(client)) {
/* should reconnect but it failed */
quit = true;
}
}

}
6 changes: 6 additions & 0 deletions src/client/resp_cli/cli.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include "setting.h"

void cli_run(void);

void cli_setup(respcli_options_st *options);
void cli_teardown(void);
Loading

0 comments on commit 1af761e

Please sign in to comment.