// Copyright (C) 2021 Russell King. // Licensed under GPL version 2. See COPYING. #include #include "event-httpd.h" #include "resource.h" struct resource_object { GList *client_list; GString *string; }; static struct resource_object position_object; static struct resource_object signal_object; static void resource_obj_send_one(gpointer data, gpointer user_data) { GString *string = user_data; struct client *c = data; respond_chunk(c, string); } static void resource_obj_send_all(struct resource_object *obj, GString *str) { g_list_foreach(obj->client_list, resource_obj_send_one, str); } static void resource_obj_set_string(struct resource_object *obj, GString *str) { GString *old; old = obj->string; obj->string = str; g_string_free(old, TRUE); resource_obj_send_all(obj, str); } static void resource_obj_add_client(struct resource_object *obj, struct client *c) { obj->client_list = g_list_append(obj->client_list, c); } static void resource_obj_del_client(struct resource_object *obj, struct client *c) { obj->client_list = g_list_remove(obj->client_list, c); } static GString *object_backend_status(struct resource *r) { GString *str; str = g_string_new("event: backend\n"); g_string_append_printf(str, "data: {\"status\":\"%s\",\"timestamp\":%ld}\n\n", r->updater ? "connected" : "disconnected", r->updater_time); return str; } static int object_v1_get(struct client *c, struct resource *r) { struct resource_object *obj = r->data; GString *str; respond_header(c, 200, "OK", "Cache-Control: no-cache\r\n" "Connection: keep-alive\r\n" "Content-Type: text/event-stream; charset=UTF-8\r\n" "Transfer-Encoding: chunked\r\n"); str = object_backend_status(r); g_string_append(str, "retry: 10000\n"); if (obj->string) g_string_append_len(str, obj->string->str, obj->string->len); respond_chunk(c, str); g_string_free(str, TRUE); // Add this client to the object list so it receives updates resource_obj_add_client(obj, c); return 1; } static void object_v1_close(struct client *c, struct resource *r) { struct resource_object *obj = r->data; // Remove this client from the object list resource_obj_del_client(obj, c); } static void object_v1_update_open(struct client *c, struct resource *r) { GString *str; // Keep track of the latest updater r->updater = c; time(&r->updater_time); str = object_backend_status(r); resource_obj_send_all(r->data, str); g_string_free(str, TRUE); } // Update all attached clients with the new resource object data static int object_v1_update(struct client *c, struct resource *r, const char *m) { struct resource_object *obj = r->data; GString *string; char *n; // Only allow the latest updater to provide updates, in case we // end up with multiple connections. This also allows us to quickly // release the resources of a stale connection. if (r->updater != c) return -1; // Remove any trailing whitespace. n = g_strchomp(g_strdup(m)); // Format the text/event-stream response // We prefix the string with the "data:" tag in preparation for // sending to via the text/event-stream connection. // The double newline terminates this event // See https://www.html5rocks.com/en/tutorials/eventsource/basics/ string = g_string_new(NULL); g_string_printf(string, "data:%s\n\n", n); g_free(n); resource_obj_set_string(obj, string); return 0; } static void object_v1_update_close(struct client *c, struct resource *r) { GString *str; // If the updater goes away, clear the data so we don't serve // stale data. if (r->updater == c) { r->updater = NULL; time(&r->updater_time); str = object_backend_status(r); resource_obj_send_all(r->data, str); g_string_free(str, TRUE); } } static const struct resource_ops resource_v1_ops = { .get = object_v1_get, .close = object_v1_close, .update_open = object_v1_update_open, .update = object_v1_update, .update_close = object_v1_update_close, }; static struct resource resource_position1 = { .ops = &resource_v1_ops, .data = &position_object, }; static struct resource resource_signal1 = { .ops = &resource_v1_ops, .data = &signal_object, }; void resource_init(GHashTable *hash) { g_hash_table_insert(hash, "/api/1/position", &resource_position1); g_hash_table_insert(hash, "/api/1/signal", &resource_signal1); }