diff --git a/dev/cli/Makefile b/dev/cli/Makefile index 650e3e3..6b09781 100644 --- a/dev/cli/Makefile +++ b/dev/cli/Makefile @@ -109,8 +109,8 @@ $(CLI_BUILD)/stream_recv_cli: $(CLI_BUILD)/stream_recv_cli.o $(COMMON_OBJ) $(SER $(CLI_BUILD)/reconciler_cli: $(CLI_BUILD)/reconciler_cli.o $(RECONCILER_OBJ) $(CC) $(CFLAGS) -o $@ $^ -$(CLI_BUILD)/controller_cli: $(CLI_BUILD)/controller_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(PROTOCOL_OBJ) - $(CC) $(CFLAGS) -o $@ $^ -lpthread +$(CLI_BUILD)/controller_cli: $(CLI_BUILD)/controller_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(DISCOVERY_OBJ) $(PROTOCOL_OBJ) + $(CC) $(CFLAGS) -o $@ $^ -lpthread -lreadline $(CLI_BUILD): mkdir -p $@ diff --git a/dev/cli/controller_cli.c b/dev/cli/controller_cli.c index eab6c5c..8b526da 100644 --- a/dev/cli/controller_cli.c +++ b/dev/cli/controller_cli.c @@ -3,11 +3,82 @@ #include #include #include +#include +#include +#include +#include #include "transport.h" #include "protocol.h" +#include "discovery.h" #include "error.h" +/* ------------------------------------------------------------------------- + * Discovery peer table + * ------------------------------------------------------------------------- */ + +#define MAX_PEERS 16 + +struct Peer_Entry { + char host[64]; + uint16_t port; + char name[DISCOVERY_MAX_NAME_LEN + 1]; +}; + +static struct Peer_Entry peer_table[MAX_PEERS]; +static int peer_count = 0; +static pthread_mutex_t peer_mutex = PTHREAD_MUTEX_INITIALIZER; + +static void on_peer_found(const struct Discovery_Peer *peer, void *ud) +{ + (void)ud; + pthread_mutex_lock(&peer_mutex); + if (peer_count < MAX_PEERS) { + struct in_addr a; + a.s_addr = peer->addr; + inet_ntop(AF_INET, &a, peer_table[peer_count].host, + sizeof(peer_table[0].host)); + peer_table[peer_count].port = peer->tcp_port; + strncpy(peer_table[peer_count].name, peer->name, DISCOVERY_MAX_NAME_LEN); + peer_table[peer_count].name[DISCOVERY_MAX_NAME_LEN] = '\0'; + peer_count++; + /* Print inline — readline will redraw the prompt */ + printf("\n[discovered %d] %s %s:%u\n", + peer_count - 1, + peer_table[peer_count - 1].name, + peer_table[peer_count - 1].host, + peer->tcp_port); + rl_on_new_line(); + rl_redisplay(); + } + pthread_mutex_unlock(&peer_mutex); +} + +static void on_peer_lost(const struct Discovery_Peer *peer, void *ud) +{ + (void)ud; + struct in_addr a; + a.s_addr = peer->addr; + char host[64]; + inet_ntop(AF_INET, &a, host, sizeof(host)); + + pthread_mutex_lock(&peer_mutex); + for (int i = 0; i < peer_count; i++) { + if (strcmp(peer_table[i].host, host) == 0 && + peer_table[i].port == peer->tcp_port) { + printf("\n[lost] %s %s:%u\n", + peer_table[i].name, host, peer->tcp_port); + rl_on_new_line(); + rl_redisplay(); + memmove(&peer_table[i], &peer_table[i + 1], + (size_t)(peer_count - i - 1) * sizeof(peer_table[0])); + peer_count--; + break; + } + } + pthread_mutex_unlock(&peer_mutex); +} + /* ------------------------------------------------------------------------- * Shared state between REPL and transport read thread * ------------------------------------------------------------------------- */ @@ -194,7 +265,9 @@ static void on_frame(struct Transport_Conn *conn, static void on_disconnect(struct Transport_Conn *conn, void *userdata) { (void)conn; (void)userdata; - printf("disconnected from node\n"); + printf("\ndisconnected from node\n"); + rl_on_new_line(); + rl_redisplay(); } /* ------------------------------------------------------------------------- @@ -341,6 +414,8 @@ static void cmd_stop_display(struct Transport_Conn *conn, static void cmd_help(void) { printf("commands:\n" + " peers list discovered nodes\n" + " connect [idx|host:port] connect to peer (no arg = first discovered)\n" " enum-devices\n" " enum-controls \n" " get-control \n" @@ -361,40 +436,76 @@ static void cmd_help(void) static void usage(void) { fprintf(stderr, - "usage: controller_cli --host HOST [--port PORT]\n" + "usage: controller_cli [--host HOST] [--port PORT]\n" "\n" " Interactive controller for a video node.\n" - " --host HOST node hostname or IP (required)\n" - " --port PORT node TCP port (default 8000)\n"); + " --host HOST connect directly on startup\n" + " --port PORT TCP port (default 8000; used with --host)\n" + "\n" + " Without --host: starts discovery and waits for nodes.\n" + " Use 'connect' in the REPL to connect to a discovered node.\n"); +} + +/* Attempt to connect/reconnect; prints result. Returns new conn or NULL. */ +static struct Transport_Conn *do_connect(struct Ctrl_State *cs, + const char *host, uint16_t port, + struct Transport_Conn *old_conn) +{ + if (old_conn) { transport_conn_close(old_conn); } + struct Transport_Conn *conn; + struct App_Error e = transport_connect(&conn, host, port, + TRANSPORT_DEFAULT_MAX_PAYLOAD, on_frame, on_disconnect, cs); + if (!APP_IS_OK(e)) { + app_error_print(&e); + return NULL; + } + printf("connected to %s:%u\n", host, port); + return conn; } int main(int argc, char **argv) { - const char *host = NULL; - uint16_t port = 8000; + const char *init_host = NULL; + uint16_t init_port = 8000; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) { - host = argv[++i]; + init_host = argv[++i]; } else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) { - port = (uint16_t)atoi(argv[++i]); + init_port = (uint16_t)atoi(argv[++i]); } else { usage(); return 1; } } - if (!host) { usage(); return 1; } - /* Connect */ + /* Start discovery (always — useful even when --host given, for 'peers') */ + struct Discovery *disc = NULL; + struct Discovery_Config dcfg = {0}; + dcfg.site_id = 0; + dcfg.tcp_port = 0; + dcfg.function_flags = DISCOVERY_FLAG_CONTROLLER; + dcfg.name = "controller_cli"; + dcfg.on_peer_found = on_peer_found; + dcfg.on_peer_lost = on_peer_lost; + if (!APP_IS_OK(discovery_create(&disc, &dcfg)) || + !APP_IS_OK(discovery_start(disc))) { + fprintf(stderr, "warning: discovery failed to start\n"); + disc = NULL; + } + struct Ctrl_State cs; memset(&cs, 0, sizeof(cs)); sem_init(&cs.sem, 0, 0); - struct Transport_Conn *conn; - struct App_Error e = transport_connect(&conn, host, port, - TRANSPORT_DEFAULT_MAX_PAYLOAD, on_frame, on_disconnect, &cs); - if (!APP_IS_OK(e)) { app_error_print(&e); return 1; } + struct Transport_Conn *conn = NULL; + + if (init_host) { + conn = do_connect(&cs, init_host, init_port, NULL); + if (!conn) { return 1; } + } else { + printf("listening for nodes — type 'peers' to list, 'connect' to connect\n"); + } - printf("connected to %s:%u\n\n", host, port); cmd_help(); printf("\n"); @@ -403,22 +514,17 @@ int main(int argc, char **argv) char line[512]; while (1) { - printf("> "); - fflush(stdout); - - if (fgets(line, sizeof(line), stdin) == NULL) { break; } - - /* Strip trailing newline */ - size_t len = strlen(line); - while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) { - line[--len] = '\0'; - } + char *rl_line = readline(conn ? "> " : "(no node) > "); + if (!rl_line) { break; } + if (*rl_line) { add_history(rl_line); } + strncpy(line, rl_line, sizeof(line) - 1); + line[sizeof(line) - 1] = '\0'; + free(rl_line); /* Tokenise (up to 12 tokens) */ char *tokens[12]; int ntok = 0; char *p = line; - while (*p && ntok < 12) { while (*p == ' ' || *p == '\t') { p++; } if (!*p) { break; } @@ -432,8 +538,71 @@ int main(int argc, char **argv) if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0) { break; + } else if (strcmp(cmd, "help") == 0) { cmd_help(); + + } else if (strcmp(cmd, "peers") == 0) { + pthread_mutex_lock(&peer_mutex); + if (peer_count == 0) { + printf("no peers discovered yet\n"); + } else { + for (int i = 0; i < peer_count; i++) { + printf(" [%d] %s %s:%u\n", i, + peer_table[i].name, + peer_table[i].host, + peer_table[i].port); + } + } + pthread_mutex_unlock(&peer_mutex); + + } else if (strcmp(cmd, "connect") == 0) { + char host[64]; + uint16_t port = 8000; + + if (ntok < 2) { + /* No argument — connect to first discovered peer */ + pthread_mutex_lock(&peer_mutex); + int ok = peer_count > 0; + if (ok) { + strncpy(host, peer_table[0].host, sizeof(host) - 1); + host[sizeof(host) - 1] = '\0'; + port = peer_table[0].port; + } + pthread_mutex_unlock(&peer_mutex); + if (!ok) { + printf("no peers discovered yet — try 'peers'\n"); + continue; + } + } else if (strchr(tokens[1], ':')) { + /* host:port */ + char *colon = strchr(tokens[1], ':'); + size_t hlen = (size_t)(colon - tokens[1]); + if (hlen >= sizeof(host)) { hlen = sizeof(host) - 1; } + memcpy(host, tokens[1], hlen); + host[hlen] = '\0'; + port = (uint16_t)atoi(colon + 1); + } else { + /* numeric index into peer table */ + int idx = atoi(tokens[1]); + pthread_mutex_lock(&peer_mutex); + int ok = idx >= 0 && idx < peer_count; + if (ok) { + strncpy(host, peer_table[idx].host, sizeof(host) - 1); + host[sizeof(host) - 1] = '\0'; + port = peer_table[idx].port; + } + pthread_mutex_unlock(&peer_mutex); + if (!ok) { + printf("index %d out of range — try 'peers'\n", idx); + continue; + } + } + conn = do_connect(&cs, host, port, conn); + + } else if (!conn) { + printf("not connected — use 'connect' to connect to a node\n"); + } else if (strcmp(cmd, "enum-devices") == 0) { cmd_enum_devices(conn, &cs, &req_id); } else if (strcmp(cmd, "enum-controls") == 0) { @@ -460,7 +629,8 @@ int main(int argc, char **argv) } } - transport_conn_close(conn); + if (conn) { transport_conn_close(conn); } + if (disc) { discovery_destroy(disc); } sem_destroy(&cs.sem); return 0; }