#include "service.h" #include "capture/capture.h" #include "common/json.h" #include "common/log.h" #include "config.h" #include "engine.h" #include "sinks/sink.h" #include #include #include #include #include #define STATUS_SUBSCRIPTION_KEY "status" struct service { LSHandle* handle; GMainLoop* loop; config_t* config; engine_t* engine; // Set from the engine thread, cleared by the idle handler on the main // loop. Coalesces a burst of updates into a single subscription push. gint status_pending; }; // --------------------------------------------------------------------------- // Reply helpers // --------------------------------------------------------------------------- static void reply_json(LSHandle* sh, LSMessage* msg, char* payload) { if (!payload) return; LSError lserror; LSErrorInit(&lserror); if (!LSMessageReply(sh, msg, payload, &lserror)) { ERR("Luna reply failed: %s", lserror.message); LSErrorFree(&lserror); } free(payload); } static void reply_error(LSHandle* sh, LSMessage* msg, const char* fmt, ...) __attribute__((format(printf, 3, 4))); static void reply_error(LSHandle* sh, LSMessage* msg, const char* fmt, ...) { char text[320]; va_list ap; va_start(ap, fmt); vsnprintf(text, sizeof(text), fmt, ap); va_end(ap); json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", false); jw_str(&w, "errorText", text); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); } static void reply_ok(LSHandle* sh, LSMessage* msg) { json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); } // Parses the incoming payload. Returns NULL for an empty or malformed body, // which every handler treats as "no arguments". static json_value_t* message_payload(LSMessage* msg) { const char* text = LSMessageGetPayload(msg); if (!text || !*text) return NULL; return json_parse(text); } // --------------------------------------------------------------------------- // Status // --------------------------------------------------------------------------- static char* build_status(service_t* s, bool subscribed) { json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); if (subscribed) jw_bool(&w, "subscribed", true); engine_write_status(s->engine, &w); jw_str(&w, "configPath", config_path(s->config)); jw_bool(&w, "configPersistent", config_is_persistent(s->config)); jw_obj_close(&w); return jw_take(&w); } static void push_status(service_t* s) { char* payload = build_status(s, true); if (!payload) return; LSError lserror; LSErrorInit(&lserror); if (!LSSubscriptionReply(s->handle, STATUS_SUBSCRIPTION_KEY, payload, &lserror)) { // Not fatal: it usually just means nobody is listening any more. DBG("Status push failed: %s", lserror.message); LSErrorFree(&lserror); } free(payload); } static gboolean status_idle(gpointer user) { service_t* s = user; g_atomic_int_set(&s->status_pending, 0); push_status(s); return G_SOURCE_REMOVE; } // Called on the engine thread; must not touch Luna directly. static void on_engine_notify(void* user) { service_t* s = user; if (g_atomic_int_compare_and_exchange(&s->status_pending, 0, 1)) g_idle_add(status_idle, s); } // --------------------------------------------------------------------------- // Methods // --------------------------------------------------------------------------- static bool method_start(LSHandle* sh, LSMessage* msg, void* ctx) { service_t* s = ctx; // An optional settings patch can be sent with start, so the UI can hit // "apply and start" in one call. json_value_t* payload = message_payload(msg); if (payload && payload->type == JSON_OBJECT && payload->u.object.count > 0) { char err[256]; config_apply(s->config, payload, err, sizeof(err)); } json_free(payload); char err[256] = { 0 }; if (!engine_start(s->engine, config_root(s->config), err, sizeof(err))) { reply_error(sh, msg, "%s", err); return true; } reply_json(sh, msg, build_status(s, false)); return true; } static bool method_stop(LSHandle* sh, LSMessage* msg, void* ctx) { service_t* s = ctx; engine_stop(s->engine); reply_json(sh, msg, build_status(s, false)); return true; } static bool method_get_status(LSHandle* sh, LSMessage* msg, void* ctx) { service_t* s = ctx; bool subscribed = false; if (LSMessageIsSubscription(msg)) { LSError lserror; LSErrorInit(&lserror); if (LSSubscriptionAdd(sh, STATUS_SUBSCRIPTION_KEY, msg, &lserror)) { subscribed = true; } else { WARN("Cannot add subscriber: %s", lserror.message); LSErrorFree(&lserror); } } reply_json(sh, msg, build_status(s, subscribed)); return true; } // The autostart script calls this: the act of calling it is what launches the // service, and the reply tells the caller what happened. static bool method_is_running(LSHandle* sh, LSMessage* msg, void* ctx) { service_t* s = ctx; json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); jw_bool(&w, "isRunning", engine_state(s->engine) == ENGINE_RUNNING); jw_str(&w, "state", engine_state_name(engine_state(s->engine))); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); return true; } static bool method_get_config(LSHandle* sh, LSMessage* msg, void* ctx) { service_t* s = ctx; json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); jw_str(&w, "path", config_path(s->config)); jw_bool(&w, "persistent", config_is_persistent(s->config)); jw_value(&w, "settings", config_root(s->config)); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); return true; } static bool method_set_config(LSHandle* sh, LSMessage* msg, void* ctx) { service_t* s = ctx; json_value_t* payload = message_payload(msg); if (!payload || payload->type != JSON_OBJECT) { json_free(payload); reply_error(sh, msg, "expected an object of settings to change"); return true; } // Accept either the settings directly or wrapped in "settings", so the // frontend can send whichever reads better at the call site. const json_value_t* patch = json_get(payload, "settings"); if (!patch) patch = payload; char err[256] = { 0 }; bool saved = config_apply(s->config, patch, err, sizeof(err)); const char* level = json_str(config_root(s->config), "logLevel", "info"); if (strcmp(level, "debug") == 0) log_set_level(LOG_DEBUG); else if (strcmp(level, "warn") == 0) log_set_level(LOG_WARN); else if (strcmp(level, "error") == 0) log_set_level(LOG_ERROR); else log_set_level(LOG_INFO); json_free(payload); json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); jw_bool(&w, "saved", saved); if (!saved) jw_str(&w, "warning", err); // Changing settings while capturing does nothing until the next start; // say so rather than silently ignoring half of them. jw_bool(&w, "restartRequired", engine_is_active(s->engine)); jw_value(&w, "settings", config_root(s->config)); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); on_engine_notify(s); return true; } static bool method_reset_config(LSHandle* sh, LSMessage* msg, void* ctx) { service_t* s = ctx; json_value_t* defaults = config_defaults(); if (!defaults) { reply_error(sh, msg, "cannot build default settings"); return true; } char err[256] = { 0 }; bool saved = config_apply(s->config, defaults, err, sizeof(err)); json_free(defaults); json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); jw_bool(&w, "saved", saved); jw_value(&w, "settings", config_root(s->config)); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); return true; } static bool method_list_backends(LSHandle* sh, LSMessage* msg, void* ctx) { (void)ctx; size_t count = 0; const capture_driver_t* const* drivers = capture_drivers(&count); json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); jw_arr_open(&w, "backends"); for (size_t i = 0; i < count; i++) { jw_obj_open(&w, NULL); jw_str(&w, "id", drivers[i]->id); jw_str(&w, "name", drivers[i]->name); jw_str(&w, "description", drivers[i]->description); jw_bool(&w, "available", drivers[i]->available()); jw_obj_close(&w); } jw_arr_close(&w); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); return true; } static bool method_list_sinks(LSHandle* sh, LSMessage* msg, void* ctx) { (void)ctx; size_t count = 0; const sink_driver_t* const* drivers = sink_drivers(&count); json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); jw_arr_open(&w, "sinks"); for (size_t i = 0; i < count; i++) { jw_obj_open(&w, NULL); jw_str(&w, "id", drivers[i]->id); jw_str(&w, "name", drivers[i]->name); jw_str(&w, "description", drivers[i]->description); jw_obj_close(&w); } jw_arr_close(&w); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); return true; } static bool method_get_diagnostics(LSHandle* sh, LSMessage* msg, void* ctx) { (void)ctx; json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); capture_write_diagnostics(&w); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); return true; } static bool method_get_logs(LSHandle* sh, LSMessage* msg, void* ctx) { (void)ctx; json_value_t* payload = message_payload(msg); bool clear = json_bool(payload, "clear", false); json_free(payload); char* text = log_dump_recent(); json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); jw_bool(&w, "returnValue", true); jw_str(&w, "logs", text); jw_obj_close(&w); reply_json(sh, msg, jw_take(&w)); free(text); if (clear) log_clear_recent(); return true; } // Deliberately last: stopping the service is how the UI gets the TV back to a // clean state without a reboot. static bool method_quit(LSHandle* sh, LSMessage* msg, void* ctx) { service_t* s = ctx; reply_ok(sh, msg); INFO("Quit requested over Luna"); engine_stop(s->engine); g_main_loop_quit(s->loop); return true; } static LSMethod s_methods[] = { { "start", method_start, LUNA_METHOD_FLAGS_NONE }, { "stop", method_stop, LUNA_METHOD_FLAGS_NONE }, { "getStatus", method_get_status, LUNA_METHOD_FLAGS_NONE }, { "isRunning", method_is_running, LUNA_METHOD_FLAGS_NONE }, { "getConfig", method_get_config, LUNA_METHOD_FLAGS_NONE }, { "setConfig", method_set_config, LUNA_METHOD_FLAGS_NONE }, { "resetConfig", method_reset_config, LUNA_METHOD_FLAGS_NONE }, { "listBackends", method_list_backends, LUNA_METHOD_FLAGS_NONE }, { "listSinks", method_list_sinks, LUNA_METHOD_FLAGS_NONE }, { "getDiagnostics", method_get_diagnostics, LUNA_METHOD_FLAGS_NONE }, { "getLogs", method_get_logs, LUNA_METHOD_FLAGS_NONE }, { "quit", method_quit, LUNA_METHOD_FLAGS_NONE }, { NULL, NULL, 0 }, }; // --------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------- service_t* service_create(LSHandle* handle, GMainLoop* loop) { service_t* s = calloc(1, sizeof(*s)); if (!s) return NULL; s->handle = handle; s->loop = loop; s->config = config_load(); if (!s->config) { free(s); return NULL; } const char* level = json_str(config_root(s->config), "logLevel", "info"); if (strcmp(level, "debug") == 0) log_set_level(LOG_DEBUG); else if (strcmp(level, "warn") == 0) log_set_level(LOG_WARN); else if (strcmp(level, "error") == 0) log_set_level(LOG_ERROR); s->engine = engine_create(on_engine_notify, s); if (!s->engine) { config_free(s->config); free(s); return NULL; } return s; } void service_destroy(service_t* s) { if (!s) return; engine_destroy(s->engine); config_free(s->config); free(s); } bool service_register(service_t* s, char* err, size_t errlen) { LSError lserror; LSErrorInit(&lserror); if (!LSRegisterCategory(s->handle, "/", s_methods, NULL, NULL, &lserror)) { snprintf(err, errlen, "cannot register methods: %s", lserror.message); LSErrorFree(&lserror); return false; } if (!LSCategorySetData(s->handle, "/", s, &lserror)) { snprintf(err, errlen, "cannot attach service data: %s", lserror.message); LSErrorFree(&lserror); return false; } return true; } void service_autostart(service_t* s) { if (!json_bool(config_root(s->config), "autoStart", false)) return; char err[256] = { 0 }; INFO("Autostart is enabled; starting capture"); if (!engine_start(s->engine, config_root(s->config), err, sizeof(err))) ERR("Autostart failed: %s", err); }