Inicio / TLS / Guía de módulos

Guía de módulos

Guía paso a paso para crear un module TLS. Para la referencia completa de la API, consulte API del núcleo.

Inicio rápido

1. Crear el archivo fuente:

modules/mod_hello/mod_hello.c

2. Escribir el module:

#include "portal/portal.h"
#include <string.h>

static portal_module_info_t info = {
    .name = "hello", .version = "1.0.0",
    .description = "Hello world module",
    .soft_deps = NULL
};
portal_module_info_t *portal_module_info(void) { return &info; }

int portal_module_load(portal_core_t *core) {
    core->path_register(core, "/hello/resources/greeting", "hello");
    core->path_set_access(core, "/hello/resources/greeting", PORTAL_ACCESS_READ);
    core->log(core, PORTAL_LOG_INFO, "hello", "Module loaded");
    return PORTAL_MODULE_OK;
}

int portal_module_unload(portal_core_t *core) {
    core->path_unregister(core, "/hello/resources/greeting");
    return PORTAL_MODULE_OK;
}

int portal_module_handle(portal_core_t *core, const portal_msg_t *msg,
                          portal_resp_t *resp) {
    (void)core;
    if (strcmp(msg->path, "/hello/resources/greeting") == 0) {
        portal_resp_set_status(resp, PORTAL_OK);
        portal_resp_set_body(resp, "Hello from TLS!\n", 16);
        return 0;
    }
    portal_resp_set_status(resp, PORTAL_NOT_FOUND);
    return -1;
}

3. Compilar:

gcc -shared -fPIC -Wall -Wextra -Werror -std=c11 -D_GNU_SOURCE \
    -Iinclude -Isrc -Ilib/libev \
    -o modules/mod_hello.so \
    modules/mod_hello/mod_hello.c src/core/core_message.c

4. Configurar — crear /etc/portal/<instance>/modules/mod_hello.conf:

# mod_hello — Hello world module
enabled = true

[mod_hello]
greeting = Hello from TLS!

5. Cargar en tiempo de ejecución:

portal:/> module load hello
portal:/> get /hello/resources/greeting
Hello from TLS!
Eso es todo. Cinco pasos desde un archivo vacío hasta un module en ejecución. El resto de esta guía cubre los patrones que utilizará en cada module real.

Anatomía de un module

Cada module es una biblioteca compartida (.so) que exporta exactamente 4 símbolos. Ni más, ni menos. El core los llama en orden: info → load → handle (N veces) → unload.

SímboloPropósito
portal_module_info()Devolver nombre, versión, descripción y soft deps
portal_module_load(core)Inicializar: registrar path, abrir recursos
portal_module_unload(core)Limpieza: desregistrar path, liberar memoria
portal_module_handle(core, msg, resp)Gestionar mensajes entrantes

Nombrado de archivos: mod_<name>.so — la parte <name> debe coincidir con info.name. Si info.name es "hello", el archivo debe ser mod_hello.so.

Gestión de mensajes

El handler recibe cada mensaje enrutado a sus path. Un patrón típico despacha primero por path y luego por método:

int portal_module_handle(portal_core_t *core, const portal_msg_t *msg,
                          portal_resp_t *resp) {
    if (strcmp(msg->path, "/mymod/items") == 0) {
        switch (msg->method) {
        case PORTAL_METHOD_GET:  /* return list */ break;
        case PORTAL_METHOD_SET:  /* create/update */ break;
        default:
            portal_resp_set_status(resp, PORTAL_BAD_REQUEST);
            return -1;
        }
    }
    portal_resp_set_status(resp, PORTAL_NOT_FOUND);
    return -1;
}

Leer la entrada: todo lo que necesita está en la estructura portal_msg_t:

OrigenCómo leerlo
Pathmsg->path
Métodomsg->method
Headersmsg->headers[0..header_count-1]
Bodymsg->body, msg->body_len
Autenticaciónmsg->ctx->auth.user, msg->ctx->auth.labels

Comunicación con otros módulos

Los módulos se comunican enviando mensajes a través del core. Nunca se llama directamente a las funciones de otro module — se envía un mensaje a su path y el core lo enruta:

/* Ask the cache module for a value */
portal_msg_t *req = portal_msg_alloc();
portal_msg_set_path(req, "/cache/functions/get");
portal_msg_set_method(req, PORTAL_METHOD_GET);
portal_msg_add_header(req, "key", "session_token");

portal_resp_t resp;
int rc = core->send(core, req, &resp);

if (rc == 0 && resp.status == PORTAL_OK) {
    /* resp.body contains the cached value */
    core->log(core, PORTAL_LOG_DEBUG, "mymod",
              "Got value: %.*s", (int)resp.body_len, resp.body);
}

portal_msg_free(req);

Este es el mismo mecanismo tanto si el module de destino es local como si está en un nodo remoto. El core gestiona el enrutamiento de forma transparente (Ley 7).

Soft dependencies

Si su module se beneficia de otro module pero puede funcionar sin él, declárelo como soft dependency en info y compruébelo en tiempo de ejecución:

static const char *deps[] = { "cache", NULL };

static portal_module_info_t info = {
    .name = "mymod", .version = "1.0.0",
    .description = "My module with optional caching",
    .soft_deps = deps
};

int portal_module_handle(portal_core_t *core, const portal_msg_t *msg,
                          portal_resp_t *resp) {
    /* Check if cache is available before using it */
    if (core->module_loaded(core, "cache")) {
        /* Use cache — send message to /cache/... */
    } else {
        /* Proceed without cache — degrade gracefully */
    }
    /* ... */
}
Ley 4: Sin dependencias duras. Si el module A necesita al module B y B no está cargado, A no se bloquea. Toda dependencia es soft. Toda ausencia se gestiona con elegancia.

Control de acceso

Cada path que registre debe declarar su modo de acceso (Ley 8). También puede restringir path a etiquetas específicas. El core aplica las ACL automáticamente — su handler solo se ejecuta si el llamante tiene permiso:

int portal_module_load(portal_core_t *core) {
    /* Register a read-only public path */
    core->path_register(core, "/mymod/resources/status", "mymod");
    core->path_set_access(core, "/mymod/resources/status", PORTAL_ACCESS_READ);

    /* Register a read-write path restricted to admins */
    core->path_register(core, "/mymod/resources/config", "mymod");
    core->path_set_access(core, "/mymod/resources/config", PORTAL_ACCESS_RW);
    core->path_add_label(core, "/mymod/resources/config", "admin");

    /* Register a write-only path for operators and admins */
    core->path_register(core, "/mymod/functions/reset", "mymod");
    core->path_set_access(core, "/mymod/functions/reset", PORTAL_ACCESS_WRITE);
    core->path_add_label(core, "/mymod/functions/reset", "admin");
    core->path_add_label(core, "/mymod/functions/reset", "operator");

    return PORTAL_MODULE_OK;
}

Un path sin etiquetas está abierto a todos los usuarios autenticados. Un path con etiquetas requiere que el llamante tenga al menos una etiqueta coincidente. El usuario root ignora todas las comprobaciones de ACL.

E/S asíncrona

Si su module lee de un puerto serie, socket o cualquier descriptor de archivo, debe usar core->fd_add() para registrarlo en el event loop. Nunca bloquee el event loop (Ley 13):

static void serial_read_cb(portal_core_t *core, int fd, void *userdata) {
    char buf[256];
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n > 0) {
        core->log(core, PORTAL_LOG_DEBUG, "serial",
                  "Read %zd bytes from serial port", n);
        /* Process data, emit event */
        core->event_emit(core, "/events/serial/data", buf, n);
    }
}

int portal_module_load(portal_core_t *core) {
    int fd = open("/dev/ttyUSB0", O_RDONLY | O_NONBLOCK);
    if (fd < 0) {
        core->log(core, PORTAL_LOG_WARN, "serial", "Cannot open serial port");
        return PORTAL_MODULE_OK;  /* degrade gracefully */
    }
    core->fd_add(core, fd, serial_read_cb, NULL);
    core->log(core, PORTAL_LOG_INFO, "serial", "Listening on /dev/ttyUSB0");
    return PORTAL_MODULE_OK;
}

int portal_module_unload(portal_core_t *core) {
    core->fd_remove(core, fd);
    close(fd);
    return PORTAL_MODULE_OK;
}

El callback se ejecuta cada vez que hay datos disponibles en el descriptor de archivo. El event loop permanece libre para procesar otros mensajes. Para trabajo intensivo de CPU, use el pool de hilos en su lugar.

Lista de verificación para publicación

Antes de publicar un module, verifique cada elemento:

  • Exporta exactamente 4 símbolos
  • info.name coincide con el nombre del archivo .so
  • Todos los path registrados en load(), desregistrados en unload()
  • Todos los fd registrados en load(), eliminados en unload()
  • Toda la memoria liberada en unload()
  • Usa core->log() para toda salida
  • Usa core->fd_add() para toda E/S
  • Comprueba module_loaded() antes de usar soft deps
  • Establece resp->status en cada path del handler
  • Compila con -Wall -Wextra -Werror
  • Cada path_register seguido de path_set_access (Ley 8)
  • event_emit() en cada cambio de estado (Ley 10)
Si unload tiene fugas, no publique. Un module que no puede descargarse limpiamente es un module que no puede hacer hot-reload. Si no puede hacer hot-reload, viola la Ley 5. Ejecute module load / module unload en bucle bajo valgrind hasta que esté limpio.