diff --git a/tools/portal-probe/portal-probe.c b/tools/portal-probe/portal-probe.c new file mode 100644 index 00000000..2b843525 --- /dev/null +++ b/tools/portal-probe/portal-probe.c @@ -0,0 +1,446 @@ +/* portal-probe — sonda del XDG Desktop Portal: ejerce el handshake COMPLETO y lo IMPRIME. + * + * ── POR QUÉ EXISTE, Y POR QUÉ NO ES UN `dbus-send` ────────────────────────────────────────────── + * Los portales no son llamadas RPC: son un protocolo ASÍNCRONO en dos tiempos. Cada método devuelve + * al instante un object path de `Request`, y la respuesta de verdad llega DESPUÉS como señal + * `org.freedesktop.portal.Request.Response` sobre ese path. `dbus-send` ya salió para entonces, así + * que devuelve el path y nunca el resultado — por eso la URI del FileChooser no se pudo capturar + * nunca desde el shell. + * + * Y hay una segunda razón, más dura: el portal ata la sesión al **sender**. `CreateSession` con un + * `dbus-send` y `SelectSources` con otro llegan desde nombres únicos DISTINTOS y el portal rechaza + * el segundo. Hace falta UNA conexión que viva durante todo el intercambio. Eso es este programa. + * + * ── POR QUÉ EN C Y NO CON `ashpd` ─────────────────────────────────────────────────────────────── + * `ashpd` es el cliente de portales del ecosistema Rust y es lo que usan cosmic-screenshot y + * cosmic-files. Meterlo acá significaría sumar ~150 crates al `Cargo.lock` del propio hammer (el + * árbol fuente de esta receta es el repo de la herramienta de build) para una sonda de diagnóstico. + * `libdbus` ya está en la imagen y en el corpus, cuesta cero deps nuevas… y sobre todo: acá el + * protocolo se ve. Una sonda cuyo valor es DOCUMENTAR un handshake no debería esconderlo tras una + * biblioteca que lo abstrae. + * + * ── EL PATH DEL REQUEST SE PUEDE PREDECIR, Y HAY QUE ESCUCHAR ANTES DE LLAMAR ──────────────────── + * La especificación fija el path: /org/freedesktop/portal/desktop/request//, donde + * SENDER es el nombre único propio sin el ':' inicial y con los '.' pasados a '_'. Existe a + * propósito: permite suscribirse ANTES de emitir la llamada. Si uno espera a que el método devuelva + * el path para recién ahí hacer `AddMatch`, el portal puede haber emitido ya la señal y se pierde + * para siempre. Es una carrera real, no teórica. + * + * Uso: portal-probe screencast [--multiple] [--window] + * portal-probe filechooser [--save] + * portal-probe screenshot + * portal-probe version (qué interfaces sirve el portal, y en qué versión) + */ + +#include +#include +#include +#include +#include + +#define PORTAL_BUS "org.freedesktop.portal.Desktop" +#define PORTAL_PATH "/org/freedesktop/portal/desktop" +#define IFACE_REQ "org.freedesktop.portal.Request" + +static int g_token = 0; +static char g_sender[256]; + +/* ── helpers de escritura: a{sv} a mano, que es lo que todo método de portal pide ─────────────── */ + +static void dict_entry(DBusMessageIter *arr, const char *key, int type, + const char *sig, const void *val) +{ + DBusMessageIter ent, var; + dbus_message_iter_open_container(arr, DBUS_TYPE_DICT_ENTRY, NULL, &ent); + dbus_message_iter_append_basic(&ent, DBUS_TYPE_STRING, &key); + dbus_message_iter_open_container(&ent, DBUS_TYPE_VARIANT, sig, &var); + dbus_message_iter_append_basic(&var, type, val); + dbus_message_iter_close_container(&ent, &var); + dbus_message_iter_close_container(arr, &ent); +} + +static void opt_str(DBusMessageIter *a, const char *k, const char *v) +{ dict_entry(a, k, DBUS_TYPE_STRING, "s", &v); } + +static void opt_u32(DBusMessageIter *a, const char *k, dbus_uint32_t v) +{ dict_entry(a, k, DBUS_TYPE_UINT32, "u", &v); } + +static void opt_bool(DBusMessageIter *a, const char *k, dbus_bool_t v) +{ dict_entry(a, k, DBUS_TYPE_BOOLEAN, "b", &v); } + +/* Token único por llamada. El portal lo usa para construir el path del Request, así que dos + * llamadas con el mismo token colisionarían sus señales. */ +static void next_token(char *out, size_t n) { snprintf(out, n, "hammer%d", ++g_token); } + +static void request_path(char *out, size_t n, const char *token) +{ snprintf(out, n, "%s/request/%s/%s", PORTAL_PATH, g_sender, token); } + +/* ── impresión recursiva de cualquier valor D-Bus ─────────────────────────────────────────────── */ +/* El valor de la sonda está acá: no filtra lo que muestra. Si el portal devuelve un campo que este + * programa no conoce, igual sale por pantalla — que es exactamente lo que uno quiere de una sonda. */ + +static void print_iter(DBusMessageIter *it, int depth) +{ + for (;;) { + int t = dbus_message_iter_get_arg_type(it); + if (t == DBUS_TYPE_INVALID) return; + for (int i = 0; i < depth; i++) fputs(" ", stdout); + + switch (t) { + case DBUS_TYPE_STRING: case DBUS_TYPE_OBJECT_PATH: case DBUS_TYPE_SIGNATURE: { + const char *s; dbus_message_iter_get_basic(it, &s); + printf("\"%s\"\n", s); break; + } + case DBUS_TYPE_UINT32: { dbus_uint32_t v; dbus_message_iter_get_basic(it, &v); printf("%u\n", v); break; } + case DBUS_TYPE_INT32: { dbus_int32_t v; dbus_message_iter_get_basic(it, &v); printf("%d\n", v); break; } + case DBUS_TYPE_UINT64: { dbus_uint64_t v; dbus_message_iter_get_basic(it, &v); printf("%llu\n", (unsigned long long)v); break; } + case DBUS_TYPE_BOOLEAN:{ dbus_bool_t v; dbus_message_iter_get_basic(it, &v); printf("%s\n", v ? "true" : "false"); break; } + case DBUS_TYPE_DOUBLE: { double v; dbus_message_iter_get_basic(it, &v); printf("%f\n", v); break; } + case DBUS_TYPE_UNIX_FD:{ dbus_int32_t v; dbus_message_iter_get_basic(it, &v); printf("\n", v); break; } + case DBUS_TYPE_VARIANT: { + DBusMessageIter sub; dbus_message_iter_recurse(it, &sub); + printf("variant:\n"); print_iter(&sub, depth + 1); break; + } + case DBUS_TYPE_ARRAY: case DBUS_TYPE_STRUCT: case DBUS_TYPE_DICT_ENTRY: { + DBusMessageIter sub; dbus_message_iter_recurse(it, &sub); + printf("%s\n", t == DBUS_TYPE_ARRAY ? "[" : "("); + print_iter(&sub, depth + 1); + for (int i = 0; i < depth; i++) fputs(" ", stdout); + printf("%s\n", t == DBUS_TYPE_ARRAY ? "]" : ")"); + break; + } + default: printf("\n", (char)t); + } + if (!dbus_message_iter_next(it)) return; + } +} + +/* ── esperar el Response de un Request ────────────────────────────────────────────────────────── */ +/* Devuelve el código de respuesta (0 ok, 1 cancelado, 2 otro) o -1 si venció el plazo. Si + * `session_out` no es NULL, copia ahí el `session_handle` de los results cuando aparece. */ + +static int wait_response(DBusConnection *bus, const char *path, int timeout_s, + char *session_out, size_t session_n) +{ + printf(" ← esperando Response en %s (hasta %ds)\n", path, timeout_s); + for (int elapsed = 0; elapsed < timeout_s * 10; elapsed++) { + dbus_connection_read_write(bus, 100); + DBusMessage *msg; + while ((msg = dbus_connection_pop_message(bus))) { + if (dbus_message_is_signal(msg, IFACE_REQ, "Response") && + !strcmp(dbus_message_get_path(msg), path)) { + DBusMessageIter it; + dbus_message_iter_init(msg, &it); + dbus_uint32_t code = 2; + dbus_message_iter_get_basic(&it, &code); + printf(" ← Response: %u (%s)\n", code, + code == 0 ? "OK" : code == 1 ? "CANCELADO por el usuario" : "otro error"); + dbus_message_iter_next(&it); + + /* Buscar session_handle antes de imprimir, si nos lo pidieron. */ + if (session_out) { + DBusMessageIter arr; + dbus_message_iter_recurse(&it, &arr); + while (dbus_message_iter_get_arg_type(&arr) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter ent, var; const char *k; + dbus_message_iter_recurse(&arr, &ent); + dbus_message_iter_get_basic(&ent, &k); + dbus_message_iter_next(&ent); + dbus_message_iter_recurse(&ent, &var); + if (!strcmp(k, "session_handle") && + dbus_message_iter_get_arg_type(&var) == DBUS_TYPE_STRING) { + const char *s; dbus_message_iter_get_basic(&var, &s); + snprintf(session_out, session_n, "%s", s); + } + dbus_message_iter_next(&arr); + } + } + printf(" ← results:\n"); + print_iter(&it, 3); + dbus_message_unref(msg); + return (int)code; + } + dbus_message_unref(msg); + } + } + printf(" !! sin Response en %ds — el portal aceptó la llamada y no contestó\n", timeout_s); + return -1; +} + +/* ── llamada bloqueante que devuelve el object path del Request ───────────────────────────────── */ + +static int call_request(DBusConnection *bus, DBusMessage *m, const char *what) +{ + DBusError err; dbus_error_init(&err); + DBusMessage *r = dbus_connection_send_with_reply_and_block(bus, m, 10000, &err); + dbus_message_unref(m); + if (!r) { + printf(" !! %s FALLÓ: %s: %s\n", what, err.name ? err.name : "?", + err.message ? err.message : "?"); + dbus_error_free(&err); + return 0; + } + const char *p = NULL; + if (dbus_message_get_args(r, &err, DBUS_TYPE_OBJECT_PATH, &p, DBUS_TYPE_INVALID)) + printf(" → %s aceptado, Request = %s\n", what, p); + else + printf(" → %s aceptado (sin object path en la respuesta)\n", what); + dbus_message_unref(r); + return 1; +} + +static DBusMessage *new_call(const char *iface, const char *method) +{ return dbus_message_new_method_call(PORTAL_BUS, PORTAL_PATH, iface, method); } + +static void subscribe(DBusConnection *bus, const char *path) +{ + char rule[512]; + snprintf(rule, sizeof rule, + "type='signal',interface='%s',member='Response',path='%s'", IFACE_REQ, path); + DBusError err; dbus_error_init(&err); + dbus_bus_add_match(bus, rule, &err); + dbus_connection_flush(bus); + if (dbus_error_is_set(&err)) { + printf(" !! AddMatch falló: %s\n", err.message); + dbus_error_free(&err); + } +} + +/* ── la propiedad `version` de una interfaz del portal ────────────────────────────────────────── */ + +static void probe_version(DBusConnection *bus, const char *iface) +{ + DBusMessage *m = dbus_message_new_method_call(PORTAL_BUS, PORTAL_PATH, + "org.freedesktop.DBus.Properties", "Get"); + const char *prop = "version"; + dbus_message_append_args(m, DBUS_TYPE_STRING, &iface, DBUS_TYPE_STRING, &prop, DBUS_TYPE_INVALID); + DBusError err; dbus_error_init(&err); + DBusMessage *r = dbus_connection_send_with_reply_and_block(bus, m, 5000, &err); + dbus_message_unref(m); + if (!r) { + printf(" %-46s AUSENTE (%s)\n", iface, err.name ? err.name : "?"); + dbus_error_free(&err); + return; + } + DBusMessageIter it, var; dbus_uint32_t v = 0; + if (dbus_message_iter_init(r, &it) && + dbus_message_iter_get_arg_type(&it) == DBUS_TYPE_VARIANT) { + dbus_message_iter_recurse(&it, &var); + dbus_message_iter_get_basic(&var, &v); + } + printf(" %-46s v%u\n", iface, v); + dbus_message_unref(r); +} + +/* ── ScreenCast: el handshake de CUATRO pasos ─────────────────────────────────────────────────── */ + +static int do_screencast(DBusConnection *bus, int multiple, int window) +{ + const char *IF = "org.freedesktop.portal.ScreenCast"; + char tok[64], path[512], session[256] = {0}; + + /* 1. CreateSession — el único paso que además crea un objeto Session que sobrevive a la Request */ + puts("\n[1/4] CreateSession"); + next_token(tok, sizeof tok); + request_path(path, sizeof path, tok); + subscribe(bus, path); + { + DBusMessage *m = new_call(IF, "CreateSession"); + DBusMessageIter it, arr; + dbus_message_iter_init_append(m, &it); + dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr); + opt_str(&arr, "handle_token", tok); + opt_str(&arr, "session_handle_token", "hammersess"); + dbus_message_iter_close_container(&it, &arr); + if (!call_request(bus, m, "CreateSession")) return 1; + } + if (wait_response(bus, path, 15, session, sizeof session) != 0) return 1; + if (!session[0]) { puts(" !! sin session_handle en los results"); return 1; } + printf(" ✓ session_handle = %s\n", session); + + /* 2. SelectSources — qué se va a capturar. `types` es un bitfield: 1=monitor, 2=ventana. */ + puts("\n[2/4] SelectSources"); + next_token(tok, sizeof tok); + request_path(path, sizeof path, tok); + subscribe(bus, path); + { + DBusMessage *m = new_call(IF, "SelectSources"); + DBusMessageIter it, arr; + dbus_message_iter_init_append(m, &it); + const char *sp = session; + dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &sp); + dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr); + opt_str(&arr, "handle_token", tok); + opt_u32(&arr, "types", window ? 2 : 1); + opt_bool(&arr, "multiple", multiple ? TRUE : FALSE); + opt_u32(&arr, "cursor_mode", 2); /* 1=oculto 2=embebido 4=metadatos */ + dbus_message_iter_close_container(&it, &arr); + if (!call_request(bus, m, "SelectSources")) return 1; + } + if (wait_response(bus, path, 15, NULL, 0) != 0) return 1; + + /* 3. Start — acá es donde el backend abre su UI de elección y, al aceptar, NEGOCIA EL NODO + * contra el demonio de pipewire. Es el paso que no podía funcionar sin demonio corriendo. */ + puts("\n[3/4] Start (el backend abre su selector; los `streams` traen el node id)"); + next_token(tok, sizeof tok); + request_path(path, sizeof path, tok); + subscribe(bus, path); + { + DBusMessage *m = new_call(IF, "Start"); + DBusMessageIter it, arr; + dbus_message_iter_init_append(m, &it); + const char *sp = session, *parent = ""; + dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &sp); + dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &parent); + dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr); + opt_str(&arr, "handle_token", tok); + dbus_message_iter_close_container(&it, &arr); + if (!call_request(bus, m, "Start")) return 1; + } + if (wait_response(bus, path, 60, NULL, 0) != 0) return 1; + + /* 4. OpenPipeWireRemote — devuelve un FD, no un path. Es la prueba final de que hay demonio: + * sin pipewire corriendo, el backend no tiene remoto que abrir. */ + puts("\n[4/4] OpenPipeWireRemote (devuelve un fd al demonio de pipewire)"); + { + DBusMessage *m = new_call(IF, "OpenPipeWireRemote"); + DBusMessageIter it, arr; + dbus_message_iter_init_append(m, &it); + const char *sp = session; + dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &sp); + dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr); + dbus_message_iter_close_container(&it, &arr); + DBusError err; dbus_error_init(&err); + DBusMessage *r = dbus_connection_send_with_reply_and_block(bus, m, 15000, &err); + dbus_message_unref(m); + if (!r) { + printf(" !! OpenPipeWireRemote FALLÓ: %s: %s\n", + err.name ? err.name : "?", err.message ? err.message : "?"); + dbus_error_free(&err); + return 1; + } + int fd = -1; + if (dbus_message_get_args(r, &err, DBUS_TYPE_UNIX_FD, &fd, DBUS_TYPE_INVALID)) { + /* Que el fd sea >= 0 no basta: el kernel siempre da un número. Lo que prueba que es un + * remoto vivo es que sea un SOCKET abierto, así que se le pregunta al propio fd. */ + char link[256] = {0}, proc[64]; + snprintf(proc, sizeof proc, "/proc/self/fd/%d", fd); + ssize_t n = readlink(proc, link, sizeof link - 1); + printf(" ✓ fd = %d → %s\n", fd, n > 0 ? link : "(sin readlink)"); + puts(" ✓✓ HAY REMOTO DE PIPEWIRE: el stream se puede consumir desde este fd"); + close(fd); + } else { + printf(" !! la respuesta no traía un fd: %s\n", err.message ? err.message : "?"); + dbus_error_free(&err); + dbus_message_unref(r); + return 1; + } + dbus_message_unref(r); + } + return 0; +} + +/* ── FileChooser: un solo paso, pero el resultado SÓLO llega por la señal ─────────────────────── */ + +static int do_filechooser(DBusConnection *bus, int save) +{ + const char *IF = "org.freedesktop.portal.FileChooser"; + char tok[64], path[512]; + next_token(tok, sizeof tok); + request_path(path, sizeof path, tok); + subscribe(bus, path); + + printf("\n[1/1] %s (elegí algo en el diálogo; las `uris` llegan por la señal)\n", + save ? "SaveFile" : "OpenFile"); + DBusMessage *m = new_call(IF, save ? "SaveFile" : "OpenFile"); + DBusMessageIter it, arr; + dbus_message_iter_init_append(m, &it); + const char *parent = "", *title = "portal-probe"; + dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &parent); + dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &title); + dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr); + opt_str(&arr, "handle_token", tok); + if (save) opt_str(&arr, "current_name", "portal-probe.txt"); + dbus_message_iter_close_container(&it, &arr); + if (!call_request(bus, m, save ? "SaveFile" : "OpenFile")) return 1; + + /* 120s: hay un humano (o un click por QMP) del otro lado. */ + return wait_response(bus, path, 120, NULL, 0) == 0 ? 0 : 1; +} + +static int do_screenshot(DBusConnection *bus) +{ + const char *IF = "org.freedesktop.portal.Screenshot"; + char tok[64], path[512]; + next_token(tok, sizeof tok); + request_path(path, sizeof path, tok); + subscribe(bus, path); + + puts("\n[1/1] Screenshot"); + DBusMessage *m = new_call(IF, "Screenshot"); + DBusMessageIter it, arr; + dbus_message_iter_init_append(m, &it); + const char *parent = ""; + dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &parent); + dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr); + opt_str(&arr, "handle_token", tok); + opt_bool(&arr, "interactive", FALSE); + dbus_message_iter_close_container(&it, &arr); + if (!call_request(bus, m, "Screenshot")) return 1; + return wait_response(bus, path, 60, NULL, 0) == 0 ? 0 : 1; +} + +int main(int argc, char **argv) +{ + const char *cmd = argc > 1 ? argv[1] : "version"; + int multiple = 0, window = 0, save = 0; + for (int i = 2; i < argc; i++) { + if (!strcmp(argv[i], "--multiple")) multiple = 1; + else if (!strcmp(argv[i], "--window")) window = 1; + else if (!strcmp(argv[i], "--save")) save = 1; + } + + DBusError err; dbus_error_init(&err); + DBusConnection *bus = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (!bus) { + fprintf(stderr, "sin bus de sesión: %s\n", err.message ? err.message : "?"); + return 2; + } + /* Que este proceso NO muera cuando el bus se desconecte: queremos ver el error, no un exit(1) + * silencioso de libdbus. */ + dbus_connection_set_exit_on_disconnect(bus, FALSE); + + /* El sender sanitizado: nombre único sin ':' y con '.' → '_'. Es lo que el portal usa para + * construir el path del Request, y calcularlo nosotros es lo que permite escuchar ANTES. */ + const char *uniq = dbus_bus_get_unique_name(bus); + snprintf(g_sender, sizeof g_sender, "%s", uniq && uniq[0] == ':' ? uniq + 1 : uniq); + for (char *p = g_sender; *p; p++) if (*p == '.') *p = '_'; + printf("bus de sesión OK · nombre único %s · sender para el path del Request: %s\n", + uniq ? uniq : "?", g_sender); + + if (!dbus_bus_name_has_owner(bus, PORTAL_BUS, &err)) { + printf("!! nadie posee %s — no hay frontend de portal en el bus\n", PORTAL_BUS); + return 3; + } + puts("portal presente en el bus\n"); + + int rc; + if (!strcmp(cmd, "version")) { + puts("interfaces del portal:"); + probe_version(bus, "org.freedesktop.portal.ScreenCast"); + probe_version(bus, "org.freedesktop.portal.Screenshot"); + probe_version(bus, "org.freedesktop.portal.FileChooser"); + probe_version(bus, "org.freedesktop.portal.Settings"); + probe_version(bus, "org.freedesktop.portal.Access"); + probe_version(bus, "org.freedesktop.portal.RemoteDesktop"); + rc = 0; + } else if (!strcmp(cmd, "screencast")) rc = do_screencast(bus, multiple, window); + else if (!strcmp(cmd, "filechooser")) rc = do_filechooser(bus, save); + else if (!strcmp(cmd, "screenshot")) rc = do_screenshot(bus); + else { + fprintf(stderr, "uso: portal-probe {version|screencast|filechooser|screenshot} [--multiple|--window|--save]\n"); + rc = 2; + } + printf("\n== portal-probe %s: %s ==\n", cmd, rc == 0 ? "OK" : "FALLÓ"); + return rc; +}