diff --git a/src/Makefile b/src/Makefile index 20fbe49..1913765 100644 --- a/src/Makefile +++ b/src/Makefile @@ -34,7 +34,7 @@ frontend.out: LDLIBS +=-lncurses -lm frontend.out: cursor.o fe_modes.o canvas.o view.o network.o lib/argtable3.o server.out: LDLIBS +=-lpthread -server.out: canvas.o +server.out: canvas.o network.o ## PATTERNS diff --git a/src/frontend.c b/src/frontend.c index 52b5bfb..03c05eb 100644 --- a/src/frontend.c +++ b/src/frontend.c @@ -95,6 +95,12 @@ FILE *logfile = NULL; #define VERSION "unknown" #endif +// colors of collaborator cursors, drawn in order based on uid +// see `setup_colors` and `draw_collab_cursors` +const short cursor_colors[] = { + COLOR_CYAN, COLOR_YELLOW, COLOR_MAGENTA, COLOR_GREEN, COLOR_RED, COLOR_BLUE, +}; + // cli pieces const char *program_name = "collascii"; const char *program_version = VERSION; @@ -307,6 +313,7 @@ void init_state(State *state, const arguments_t *const arguments) { .view = view, .last_cursor = cursor_newyx(arguments->y, arguments->x), .filepath = arguments->filename, + .collab_list = collab_list_create(NUM_COLLAB), }; *state = new_state; } @@ -442,15 +449,18 @@ int main(int argc, char *argv[]) { fd == net_cfg->sockfd) { // Accept data from open socket logd("recv network\n"); // If server disconnects - if (net_handler(view) != 0) { + if (net_handler(state) != 0) { networked = false; print_msg_win("Server Disconnect!"); }; redraw_canvas_win(); // TODO: draw single char update + draw_collab_cursors(state->collab_list); refresh_screen(); } else if (fd == 0) { // process keyboard activity master_handler(state, canvas_win, status_interface->info_win); + draw_collab_cursors(state->collab_list); refresh_screen(); + net_update_pos(state); } } } @@ -470,18 +480,22 @@ int main(int argc, char *argv[]) { finish(0); } +/* Initialize ncurses color configurations + * + * Before using color features elsewhere make sure to check has_colors() first. + */ void setup_colors() { start_color(); - - // TODO: Use #define to get colors for standard uses - // Assign color codes - init_pair(1, COLOR_RED, COLOR_BLACK); - init_pair(2, COLOR_GREEN, COLOR_BLACK); - init_pair(3, COLOR_BLUE, COLOR_BLACK); - init_pair(4, COLOR_CYAN, COLOR_BLACK); - init_pair(5, COLOR_MAGENTA, COLOR_BLACK); - init_pair(6, COLOR_YELLOW, COLOR_BLACK); - init_pair(7, COLOR_BLACK, COLOR_WHITE); + // Use the terminal's prefered color scheme if it supports it + // -1 can be used to refer to the prefered background/foreground + use_default_colors(); + + // initialize cursor colors - prefered foreground with colored background + // start at index 1 (0 is already default colors) + const int start = 1; + for (int i = 0; i < sizeof(cursor_colors) / sizeof(short); i++) { + init_pair(i + start, -1, cursor_colors[i]); + } } /* Update canvas with character at cursor current position. @@ -531,6 +545,40 @@ void redraw_canvas_win() { } } +/* Draw all visible collaborator cursors on the canvas. + * + * Collaborator cursor colors are from `cursors_colors` and set by uid. + */ +void draw_collab_cursors(collab_list_t *collab_list) { + collab_t *c = NULL; + // calculate visible bounds (in canvas coordinates) + const int min_x = view->x; + const int min_y = view->y; + const int max_x = min(view->canvas->num_cols, view->x + view_max_x) - 1; + const int max_y = min(view->canvas->num_rows, view->y + view_max_y) - 1; + for (int i = 0; i < collab_list->len; i++) { + c = collab_list->list[i]; + // only draw cursors that exist and are visible on the screen + if (c != NULL && (c->x >= min_x && c->x <= max_x) && + (c->y >= min_y && c->y <= max_y)) { + logd("Drawing collab %i\n", c->uid); + + // pick color based on uid, which starts at 1 + // if color isn't supported, use normal terminal colors and reverse video + const int uid_start = 0; + const int color_start = 1; + const size_t colors_len = sizeof(cursor_colors) / sizeof(short); + const int color = + has_colors() ? ((c->uid - uid_start) % colors_len) + color_start : 0; + const int attr = has_colors() ? 0 : A_REVERSE; + // TODO: blink cursor with A_BLINK attribute (needs to pause between + // updates/only move on changes?) + mvwchgat(canvas_win, c->y - view->y + 1, c->x - view->x + 1, 1, attr, + color, NULL); + } + } +} + void refresh_screen() { update_screen_size(); wmove(canvas_win, cursor_y_to_canvas(cursor), cursor_x_to_canvas(cursor)); diff --git a/src/frontend.h b/src/frontend.h index c61a46f..bbe0610 100644 --- a/src/frontend.h +++ b/src/frontend.h @@ -4,6 +4,7 @@ #include #include "cursor.h" #include "mode_id.h" +#include "state.h" #include "view.h" #define KEY_TAB '\t' @@ -34,4 +35,5 @@ void highlight_mode_text(int x, int num_ch); int print_mode_win(char *format, ...); void update_info_win(const Mode_ID current_mode, const int x, const int y, const int w, const int h); +void draw_collab_cursors(collab_list_t *collab_list); #endif diff --git a/src/network.c b/src/network.c index 154da44..063b720 100644 --- a/src/network.c +++ b/src/network.c @@ -12,9 +12,12 @@ #include "canvas.h" #include "network.h" +#include "state.h" #include "util.h" #include "view.h" +#define LOG_TRAFFIC + /* Network Client Variables */ fd_set testfds, clientfds; char *msg_buf; @@ -29,6 +32,166 @@ struct hostent *hostinfo; struct sockaddr_in address; struct addrinfo hints, *servinfo; +const char *PROTOCOL_VERSION = "1.0"; + +int write_fd(int fd, const char *s) { + const int len = strlen(s); + const int res = write(fd, s, strlen(s)); +#ifdef LOG_TRAFFIC + const int errnum = errno; + logd("Wrote %d of %d bytes to descriptor %d: '%s'\n", res, len, fd, s); + errno = errnum; +#endif + return res; +} + +int build_set_msg(char *buff, int buff_len, const int y, const int x, + const char val) { + return snprintf(buff, buff_len, "s %i %i %c\n", y, x, val); +} + +int build_pos_msg(char *buff, int buff_len, const int y, const int x, + const int uid) { + return snprintf(buff, buff_len, "p %i %i %i\n", y, x, uid); +} + +int parse_pos_msg(const char *buff, int *y, int *x, int *uid) { + return sscanf(buff, "p %i %i %i", y, x, uid) == 3; +} + +int net_send_pos(int y, int x) { + char send_buf[32]; + snprintf(send_buf, 32, "p %d %d 0\n", y, x); + if (write_fd(sockfd, send_buf) < 0) { + perrorf("net_send_pos: write_fd"); + return -1; + } + return 0; +} + +int net_update_pos(State *state) { + logd("Hello\n"); + static int last_x = 0; + static int last_y = 0; + bool updated = false; + const int cur_x = state->view->x + state->cursor->x; + const int cur_y = state->view->y + state->cursor->y; + if (cur_x != last_x) { + last_x = cur_x; + updated = true; + } + if (cur_y != last_y) { + last_y = cur_y; + updated = true; + } + if (updated) { + logd("Sending updated pos\n"); + net_send_pos(cur_y, cur_x); + return 1; + } + return 0; +} + +collab_t *collab_create(int uid, int y, int x) { + collab_t *c = malloc(sizeof(collab_t)); + c->uid = uid; + c->y = y; + c->x = x; + return c; +} + +void collab_free(collab_t *collab) { + free(collab); +} + +/* Sends a set char command to the server + * + */ +int net_send_char(int y, int x, char ch) { + char send_buf[50]; + snprintf(send_buf, 50, "s %d %d %c\n", y, x, ch); + + if (write_fd(sockfd, send_buf) < 0) { + perrorf("net_send_char: write_fd"); + return -1; + } + + // DON"T TRUST FPRINTF!!! It has failed me! + // fprintf(sockstream, "s %d %d %c\n", y, x, ch); + + return 0; +} + +collab_list_t *collab_list_create(int len) { + collab_list_t *l = malloc(sizeof(collab_list_t)); + l->len = len; + l->list = malloc(sizeof(collab_t) * len); + l->num = 0; + for (int i = 0; i < len; i++) { + l->list[i] = NULL; + } + return l; +} + +void collab_list_free(collab_list_t *l) { + for (int i = 0; i < l->len; i++) { + collab_free(l->list[i]); + } + free(l->list); + free(l); +} + +int collab_list_add(collab_list_t *l, int uid, int y, int x) { + int i; + for (i = 0; i < l->len; i++) { + if (l->list[i] == NULL) { + l->list[i] = collab_create(uid, y, x); + l->num++; + logd("Added new collaborator %i at (%i, %i)\n", uid, y, x); + break; + } + } + if (i == l->len) { + logd("Collaborator list full\n"); + return -1; + } + return 0; +} + +int collab_list_del(collab_list_t *l, int uid) { + int i; + for (i = 0; i < l->len; i++) { + if (l->list[i]->uid == uid) { + collab_t *c = l->list[i]; + l->list[i] = NULL; + collab_free(c); + l->num--; + break; + } + } + if (i == l->len) { + logd("Couldn't find uid %i in list\n", uid); + return -1; + } + return 0; +} + +int collab_list_upd(collab_list_t *l, int uid, int y, int x) { + int i; + for (i = 0; i < l->len; i++) { + if (l->list[i] != NULL && l->list[i]->uid == uid) { + l->list[i]->x = x; + l->list[i]->y = y; + logd("Updated collaborator %i to (%i, %i)\n", uid, x, y); + break; + } + } + if (i == l->len) { + return collab_list_add(l, uid, y, x); + } + return 0; +} + /* Connects to server and returns its canvas * */ @@ -61,9 +224,28 @@ Canvas *net_init(char *in_hostname, char *in_port) { sockstream = fdopen(sockfd, "r+"); + // "negotiate" protocol version + char version_request_msg[16]; + snprintf(version_request_msg, 16, "v %s\n", PROTOCOL_VERSION); + if (write(sockfd, version_request_msg, strlen(version_request_msg)) < 0) { + perror("version negotiation: write error"); + exit(1); + } + + if (getline(&msg_buf, &msg_size, sockstream) == -1) { + perror("version negotiation: read error"); + exit(1); + } + if (!(msg_buf[0] == 'v' && msg_buf[1] == 'o' && msg_buf[2] == 'k')) { + eprintf("Failed to negotiate protocol version: the server says '%s'\n", + msg_buf); + exit(1); + } + + // receive canvas from server getline(&msg_buf, &msg_size, sockstream); char *command = strtok(msg_buf, " "); - if (!strcmp(command, "cs")) { + if (strcmp(command, "cs") == 0) { int row = atoi(strtok(NULL, " ")); int col = atoi(strtok(NULL, " ")); @@ -98,45 +280,37 @@ Net_cfg *net_getcfg() { /* Reads incoming packets and updates canvas. * Need to run redraw_canvas_win() after calling! */ -int net_handler(View *view) { - logd("receiving: "); +int net_handler(State *state) { + View *view = state->view; + getline(&msg_buf, &msg_size, sockstream); - logd("[%li]", msg_size); - logd("rec buffer: '%s'", msg_buf); +#ifdef LOG_TRAFFIC + logd("received %li bytes: '%s'\n", msg_size, msg_buf); +#endif char ch = msg_buf[strlen(msg_buf) - 2]; // -2 for '\n' + char *msg = strndup(msg_buf, msg_size); char *command = strtok(msg_buf, " \n"); logd("\"%s\"", command); - if (!strcmp(command, "s")) { - int y = atoi(strtok(NULL, " ")); - int x = atoi(strtok(NULL, " ")); - - canvas_scharyx(view->canvas, y, x, ch); - } - if (!strcmp(command, "q")) { + if (strcmp(command, "q") == 0) { logd("closing socket\n"); close(sockfd); return 1; } - return 0; -} + if (strcmp(command, "s") == 0) { + int y = atoi(strtok(NULL, " ")); + int x = atoi(strtok(NULL, " ")); -/* Sends a set char command to the server - * - */ -int net_send_char(int y, int x, char ch) { - char send_buf[50]; - snprintf(send_buf, 50, "s %d %d %c\n", y, x, ch); - logd("send buffer: '%s'\n", send_buf); - if (write(sockfd, send_buf, strlen(send_buf)) < 0) { - logd("write error"); - return -1; + canvas_scharyx(view->canvas, y, x, ch); + } else if (strcmp(command, "p") == 0) { + int y, x, uid; + // logd("msg_buf: '%s'", msg_buf); + if (!parse_pos_msg(msg, &y, &x, &uid)) { + perrorf("net_handler: parse_pos_msg"); + } + collab_list_upd(state->collab_list, uid, y, x); } - - logd("sending: s %d %d %c\n", y, x, ch); - // DON"T TRUST FPRINTF!!! It has failed me! - // fprintf(sockstream, "s %d %d %c\n", y, x, ch); - + free(msg); return 0; } diff --git a/src/network.h b/src/network.h index f793a5f..dd4acf7 100644 --- a/src/network.h +++ b/src/network.h @@ -7,16 +7,30 @@ #include #include "canvas.h" +#include "state.h" #include "view.h" +#define NUM_COLLAB 20 + +const char *PROTOCOL_VERSION; + typedef struct NET_CFG { fd_set clientfds; int sockfd; } Net_cfg; +int write_fd(int fd, const char *s); + Canvas *net_init(char *hostname, char *port); Net_cfg *net_getcfg(); -int net_handler(View *view); +int net_handler(State *state); int net_send_char(int y, int x, char ch); +int net_update_pos(State *state); +int parse_pos_msg(const char *buff, int *y, int *x, int *uid); +int build_pos_msg(char *buff, int buff_len, const int y, const int x, + const int uid); + +collab_list_t *collab_list_create(int len); +void collab_list_free(collab_list_t *l); #endif diff --git a/src/server.c b/src/server.c index ce10c42..ef2544a 100644 --- a/src/server.c +++ b/src/server.c @@ -21,13 +21,17 @@ #include #include "canvas.h" +#include "network.h" +#include "util.h" static _Atomic unsigned int cli_count = 0; -static int uid = 10; +static int uid = 1; // start uids at 1 #define MAX_CLIENTS 100 #define BUFFER_SZ 2048 +#define LOG_TRAFFIC + /* Client structure */ typedef struct { struct sockaddr_in addr; /* Client remote address */ @@ -43,42 +47,73 @@ pthread_mutex_t clients_mutex = PTHREAD_MUTEX_INITIALIZER; Canvas *canvas; char *canvas_buf; +bool client_eq(client_t *a, client_t *b) { + return a != NULL && b != NULL && a->uid == b->uid; +} + /* Add client to queue */ void queue_add(client_t *cl) { pthread_mutex_lock(&clients_mutex); - for (int i = 0; i < MAX_CLIENTS; ++i) { + int i; + for (i = 0; i < MAX_CLIENTS; ++i) { if (!clients[i]) { clients[i] = cl; + logd("Stored client %d (%s) at index %d\n", cl->uid, cl->name, i); break; } } + if (i == MAX_CLIENTS) { + logd("No room for additional clients!"); + } pthread_mutex_unlock(&clients_mutex); } +// int write_fd(int fd, const char *s) { +// const int len = strlen(s); +// const int res = write(fd, s, strlen(s)); +// #ifdef LOG_TRAFFIC +// const int errnum = errno; +// logd("Wrote %d of %d bytes to descriptor %d: '%s'\n", res, len, fd, s); +// errno = errnum; +// #endif +// return res; +// } + +int write_client(client_t *client, const char *s) { + int res = write_fd(client->connfd, s); + if (res < 0) { + perrorf("Write to client %i (%s) failed", client->uid, client->name); + } + return res; +} + /* Delete client from queue */ -void queue_delete(int uid) { +void queue_delete(client_t *client) { pthread_mutex_lock(&clients_mutex); - for (int i = 0; i < MAX_CLIENTS; ++i) { + int i; + for (i = 0; i < MAX_CLIENTS; ++i) { if (clients[i]) { - if (clients[i]->uid == uid) { + if (client_eq(clients[i], client)) { + logd("queue_delete: removed client %d (%s) from queue\n", + clients[i]->uid, clients[i]->name); clients[i] = NULL; break; } } } + if (i == MAX_CLIENTS) { + logd("queue_delete: couldn't find client %i in queue\n", uid); + } pthread_mutex_unlock(&clients_mutex); } /* Send message to all clients but the sender */ -void send_message(char *s, int uid) { +void send_message(char *s, client_t *client) { pthread_mutex_lock(&clients_mutex); for (int i = 0; i < MAX_CLIENTS; ++i) { if (clients[i]) { - if (clients[i]->uid != uid) { - if (write(clients[i]->connfd, s, strlen(s)) < 0) { - perror("Write to descriptor failed"); - break; - } + if (!client_eq(clients[i], client)) { + write_client(clients[i], s); } } } @@ -90,33 +125,25 @@ void broadcast_message(char *s) { pthread_mutex_lock(&clients_mutex); for (int i = 0; i < MAX_CLIENTS; ++i) { if (clients[i]) { - if (write(clients[i]->connfd, s, strlen(s)) < 0) { - perror("Write to descriptor failed"); - break; - } + write_client(clients[i], s); } } pthread_mutex_unlock(&clients_mutex); } /* Send message to sender */ -void send_message_self(const char *s, int connfd) { - if (write(connfd, s, strlen(s)) < 0) { - perror("Write to descriptor failed"); - exit(-1); - } +void send_message_self(const char *s, client_t *client) { + write_client(client, s); } /* Send message to client */ -void send_message_client(char *s, int uid) { +void send_message_client(char *s, client_t *client) { pthread_mutex_lock(&clients_mutex); for (int i = 0; i < MAX_CLIENTS; ++i) { - if (clients[i]) { - if (clients[i]->uid == uid) { - if (write(clients[i]->connfd, s, strlen(s)) < 0) { - perror("Write to descriptor failed"); - break; - } + if (client_eq(clients[i], client)) { + if (write(clients[i]->connfd, s, strlen(s)) < 0) { + perror("Write to descriptor failed"); + break; } } } @@ -144,7 +171,8 @@ void print_client_addr(struct sockaddr_in addr) { /* Handle all communication with the client */ void *handle_client(void *arg) { char buff_out[BUFFER_SZ]; - char buff_in[BUFFER_SZ / 2]; + char *buff_in; + size_t buff_in_size; int rlen; cli_count++; @@ -154,18 +182,48 @@ void *handle_client(void *arg) { print_client_addr(cli->addr); printf(" referenced by %d\n", cli->uid); + FILE *input_stream = fdopen(cli->connfd, "r"); + + // protocol negotiation + if ((rlen = getline(&buff_in, &buff_in_size, input_stream)) < 0) { + printf("version negotation: error reading from socket\n"); + goto CLIENT_CLOSE; + } + strip_newline(buff_in); + char *cmd = strtok(buff_in, " "); + if (cmd == NULL || cmd[0] != 'v') { + printf("version negotiation: client command not 'v'\n"); + + goto CLIENT_CLOSE; + } + char *client_version = strtok(NULL, " "); + if (client_version == NULL) { + printf("version negotiation: unable to parse client version\n"); + send_message_self("can't parse version\n", cli); + goto CLIENT_CLOSE; + } + if (strcmp(client_version, PROTOCOL_VERSION) != 0) { + printf("version negotiation: unknown client protocol version: '%s'\n", + client_version); + send_message_self("unknown protocol - supported protocol versions: ", cli); + send_message_self(PROTOCOL_VERSION, cli); + send_message_self("\n", cli); + goto CLIENT_CLOSE; + } + send_message_self("vok\n", cli); + + // send canvas sprintf(buff_out, "cs %d %d\n", canvas->num_rows, canvas->num_cols); - send_message_self(buff_out, cli->connfd); + send_message_self(buff_out, cli); printf("sent canvas size\n"); canvas_serialize(canvas, canvas_buf); - send_message_self(canvas_buf, cli->connfd); + send_message_self(canvas_buf, cli); sprintf(buff_out, "\n"); - send_message_self(buff_out, cli->connfd); + send_message_self(buff_out, cli); printf("sent serialized canvas\n"); /* Receive input from client */ - while ((rlen = read(cli->connfd, buff_in, sizeof(buff_in) - 1)) > 0) { - buff_in[rlen] = '\0'; + while ((rlen = getline(&buff_in, &buff_in_size, input_stream)) > 0) { buff_out[0] = '\0'; strip_newline(buff_in); @@ -174,14 +232,21 @@ void *handle_client(void *arg) { continue; } +#ifdef LOG_TRAFFIC + logd("Read %d bytes from client %d (%s): '%s'\n", rlen, cli->uid, cli->name, + buff_in); +#endif + /* Process Command */ char c = buff_in[strlen(buff_in) - 1]; char *command; + char *msg = strndup(buff_in, BUFFER_SZ); command = strtok(buff_in, " "); - if (!strcmp(command, "q")) { + if (strcmp(command, "q") == 0) { break; } - if (!strcmp(command, "s")) { + + if (strcmp(command, "s") == 0) { int y = atoi(strtok(NULL, " ")); int x = atoi(strtok(NULL, " ")); @@ -192,19 +257,34 @@ void *handle_client(void *arg) { canvas_scharyx(canvas, y, x, c); sprintf(buff_out, "s %d %d %c\n", y, x, c); - send_message(buff_out, cli->uid); + send_message(buff_out, cli); } - } else if (!strcmp(command, "c")) { + } else if (strcmp(command, "c") == 0) { canvas_serialize(canvas, canvas_buf); - send_message_self(canvas_buf, cli->connfd); + send_message_self(canvas_buf, cli); + } else if (strcmp(command, "p") == 0) { + // copy pos message and fill in client id + char send_buff[32]; + int y, x, uid; + if (!parse_pos_msg(msg, &y, &x, &uid)) { + logd("Warning: parse_pos_msg didn't get all of them\n"); + } + logd("Got (%i, %i) from '%s'\n", x, y, msg); + if (build_pos_msg(send_buff, 32, y, x, cli->uid) >= 32) { + logd("handle_client: build_pos_msg buffer too small\n"); + } + // send to other clients + send_message(send_buff, cli); } + free(msg); } +CLIENT_CLOSE: /* Close connection */ close(cli->connfd); /* Delete client from queue and yield thread */ - queue_delete(cli->uid); + queue_delete(cli); printf("<< quit "); print_client_addr(cli->addr); printf(" referenced by %d\n", cli->uid); @@ -275,7 +355,7 @@ int main(int argc, char *argv[]) { perror("Socket binding failed"); serv_addr.sin_port = htons(++port); } - printf("connected to port %d", port); + printf("Connected to port %d\n", port); /* Listen */ if (listen(listenfd, 10) < 0) { diff --git a/src/state.h b/src/state.h index b4c9d25..cc8584f 100644 --- a/src/state.h +++ b/src/state.h @@ -5,6 +5,18 @@ #include "mode_id.h" #include "view.h" +typedef struct { + int uid; + int y; + int x; +} collab_t; + +typedef struct { + int num; + int len; + collab_t **list; +} collab_list_t; + /* State keeps track of changing variables for mode functions. * If you add something, don't forget to also add an init before the main * loop. @@ -22,6 +34,10 @@ typedef struct { int last_arrow_direction; Cursor *last_cursor; char *filepath; // path of savefile + + // network-related + collab_list_t *collab_list; + char *name; } State; #endif diff --git a/src/util.h b/src/util.h index bc64d1e..b6452d8 100644 --- a/src/util.h +++ b/src/util.h @@ -1,4 +1,8 @@ #ifndef util_h +/* Utility macros and functions. + * + * TODO: use fmt argument in macros instead of anything + */ #define util_h #include @@ -8,16 +12,23 @@ // printf to stderr #define eprintf(...) fprintf(stderr, __VA_ARGS__) +// print an error message with formatting that correctly gets the errno +#define perrorf(...) \ + do { \ + const int errnum = errno; \ + eprintf(__VA_ARGS__); \ + eprintf(": %s\n", strerror(errnum)); \ + } while (0) + // LOGGING -// techniques for preventing unused variables/function warnings based on zf_log: -// https://github.com/wonder-mice/zf_log/ +// techniques for preventing unused variables/function warnings based on +// zf_log: https://github.com/wonder-mice/zf_log/ /* Dummy function that does nothing with variadic args. * * Static b/c it shouldn't be used directly anywhere outside of this header. * Inline b/c it fixes the "defined but not used" warning, and it will be called - * many times to do nothing. - * https://stackoverflow.com/a/2765211 + * many times to do nothing. https://stackoverflow.com/a/2765211 * https://stackoverflow.com/q/2845748 * https://stackoverflow.com/a/1932371 * https://stackoverflow.com/q/7762731