tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ main tools/portal-probe/portal-probe.c C · 496 líneas · 25.4 KB Historia Quién tocó qué crudo

🎯 portal-probe: omitir `persist_mode` MATA al portal — por eso Start no contestaba nunca a5c959b61 · sergio ·

1/* portal-probe — sonda del XDG Desktop Portal: ejerce el handshake COMPLETO y lo IMPRIME.
2 *
3 * ── POR QUÉ EXISTE, Y POR QUÉ NO ES UN `dbus-send` ──────────────────────────────────────────────
4 * Los portales no son llamadas RPC: son un protocolo ASÍNCRONO en dos tiempos. Cada método devuelve
5 * al instante un object path de `Request`, y la respuesta de verdad llega DESPUÉS como señal
6 * `org.freedesktop.portal.Request.Response` sobre ese path. `dbus-send` ya salió para entonces, así
7 * que devuelve el path y nunca el resultado — por eso la URI del FileChooser no se pudo capturar
8 * nunca desde el shell.
9 *
10 * Y hay una segunda razón, más dura: el portal ata la sesión al **sender**. `CreateSession` con un
11 * `dbus-send` y `SelectSources` con otro llegan desde nombres únicos DISTINTOS y el portal rechaza
12 * el segundo. Hace falta UNA conexión que viva durante todo el intercambio. Eso es este programa.
13 *
14 * ── POR QUÉ EN C Y NO CON `ashpd` ───────────────────────────────────────────────────────────────
15 * `ashpd` es el cliente de portales del ecosistema Rust y es lo que usan cosmic-screenshot y
16 * cosmic-files. Meterlo acá significaría sumar ~150 crates al `Cargo.lock` del propio hammer (el
17 * árbol fuente de esta receta es el repo de la herramienta de build) para una sonda de diagnóstico.
18 * `libdbus` ya está en la imagen y en el corpus, cuesta cero deps nuevas… y sobre todo: acá el
19 * protocolo se ve. Una sonda cuyo valor es DOCUMENTAR un handshake no debería esconderlo tras una
20 * biblioteca que lo abstrae.
21 *
22 * ── EL PATH DEL REQUEST SE PUEDE PREDECIR, Y HAY QUE ESCUCHAR ANTES DE LLAMAR ────────────────────
23 * La especificación fija el path: /org/freedesktop/portal/desktop/request/<SENDER>/<TOKEN>, donde
24 * SENDER es el nombre único propio sin el ':' inicial y con los '.' pasados a '_'. Existe a
25 * propósito: permite suscribirse ANTES de emitir la llamada. Si uno espera a que el método devuelva
26 * el path para recién ahí hacer `AddMatch`, el portal puede haber emitido ya la señal y se pierde
27 * para siempre. Es una carrera real, no teórica.
28 *
29 * Uso:  portal-probe screencast [--multiple] [--window] [--wait N] [--persist 0|1|2]
30 *       portal-probe filechooser [--save]
31 *       portal-probe screenshot [--wait N]
32 *       portal-probe version           (qué interfaces sirve el portal, y en qué versión)
33 *
34 * `--wait N` (por defecto 60s) es el plazo de los pasos que ABREN UN SELECTOR y por lo tanto
35 * esperan a una PERSONA: el `Start` de ScreenCast y el `Screenshot` interactivo. Medido en metal
36 * el 2026-08-06: con 60s clavados la sonda se rinde antes de que nadie llegue a elegir pantalla, y
37 * el informe sale «el portal aceptó la llamada y no contestó» — que se lee como un fallo del portal
38 * cuando en realidad la cadena está esperando un clic. Es el mismo error de método que ya nos costó
39 * tres diagnósticos falsos: medir contra el reloj en vez de contra el hecho. Aquí el hecho depende
40 * de un humano, así que el reloj tiene que ser AJUSTABLE — no adivinado.
41 * Los otros plazos (CreateSession/SelectSources) siguen en 15s a propósito: ésos no piden a nadie.
42 */
43
44#include <dbus/dbus.h>
45#include <stdio.h>
46#include <stdlib.h>
47#include <string.h>
48#include <unistd.h>
49
50#define PORTAL_BUS   "org.freedesktop.portal.Desktop"
51#define PORTAL_PATH  "/org/freedesktop/portal/desktop"
52#define IFACE_REQ    "org.freedesktop.portal.Request"
53
54static int  g_token = 0;
55static char g_sender[256];
56
57/* ── helpers de escritura: a{sv} a mano, que es lo que todo método de portal pide ─────────────── */
58
59static void dict_entry(DBusMessageIter *arr, const char *key, int type,
60                       const char *sig, const void *val)
61{
62    DBusMessageIter ent, var;
63    dbus_message_iter_open_container(arr, DBUS_TYPE_DICT_ENTRY, NULL, &ent);
64    dbus_message_iter_append_basic(&ent, DBUS_TYPE_STRING, &key);
65    dbus_message_iter_open_container(&ent, DBUS_TYPE_VARIANT, sig, &var);
66    dbus_message_iter_append_basic(&var, type, val);
67    dbus_message_iter_close_container(&ent, &var);
68    dbus_message_iter_close_container(arr, &ent);
69}
70
71static void opt_str(DBusMessageIter *a, const char *k, const char *v)
72{ dict_entry(a, k, DBUS_TYPE_STRING, "s", &v); }
73
74static void opt_u32(DBusMessageIter *a, const char *k, dbus_uint32_t v)
75{ dict_entry(a, k, DBUS_TYPE_UINT32, "u", &v); }
76
77static void opt_bool(DBusMessageIter *a, const char *k, dbus_bool_t v)
78{ dict_entry(a, k, DBUS_TYPE_BOOLEAN, "b", &v); }
79
80/* Token único por llamada. El portal lo usa para construir el path del Request, así que dos
81 * llamadas con el mismo token colisionarían sus señales. */
82static void next_token(char *out, size_t n) { snprintf(out, n, "hammer%d", ++g_token); }
83
84static void request_path(char *out, size_t n, const char *token)
85{ snprintf(out, n, "%s/request/%s/%s", PORTAL_PATH, g_sender, token); }
86
87/* ── impresión recursiva de cualquier valor D-Bus ─────────────────────────────────────────────── */
88/* El valor de la sonda está acá: no filtra lo que muestra. Si el portal devuelve un campo que este
89 * programa no conoce, igual sale por pantalla — que es exactamente lo que uno quiere de una sonda. */
90
91static void print_iter(DBusMessageIter *it, int depth)
92{
93    for (;;) {
94        int t = dbus_message_iter_get_arg_type(it);
95        if (t == DBUS_TYPE_INVALID) return;
96        for (int i = 0; i < depth; i++) fputs("  ", stdout);
97
98        switch (t) {
99        case DBUS_TYPE_STRING: case DBUS_TYPE_OBJECT_PATH: case DBUS_TYPE_SIGNATURE: {
100            const char *s; dbus_message_iter_get_basic(it, &s);
101            printf("\"%s\"\n", s); break;
102        }
103        case DBUS_TYPE_UINT32: { dbus_uint32_t v; dbus_message_iter_get_basic(it, &v); printf("%u\n", v); break; }
104        case DBUS_TYPE_INT32:  { dbus_int32_t  v; dbus_message_iter_get_basic(it, &v); printf("%d\n", v); break; }
105        case DBUS_TYPE_UINT64: { dbus_uint64_t v; dbus_message_iter_get_basic(it, &v); printf("%llu\n", (unsigned long long)v); break; }
106        case DBUS_TYPE_BOOLEAN:{ dbus_bool_t   v; dbus_message_iter_get_basic(it, &v); printf("%s\n", v ? "true" : "false"); break; }
107        case DBUS_TYPE_DOUBLE: { double        v; dbus_message_iter_get_basic(it, &v); printf("%f\n", v); break; }
108        case DBUS_TYPE_UNIX_FD:{ dbus_int32_t  v; dbus_message_iter_get_basic(it, &v); printf("<fd %d>\n", v); break; }
109        case DBUS_TYPE_VARIANT: {
110            DBusMessageIter sub; dbus_message_iter_recurse(it, &sub);
111            printf("variant:\n"); print_iter(&sub, depth + 1); break;
112        }
113        case DBUS_TYPE_ARRAY: case DBUS_TYPE_STRUCT: case DBUS_TYPE_DICT_ENTRY: {
114            DBusMessageIter sub; dbus_message_iter_recurse(it, &sub);
115            printf("%s\n", t == DBUS_TYPE_ARRAY ? "[" : "(");
116            print_iter(&sub, depth + 1);
117            for (int i = 0; i < depth; i++) fputs("  ", stdout);
118            printf("%s\n", t == DBUS_TYPE_ARRAY ? "]" : ")");
119            break;
120        }
121        default: printf("<tipo '%c'>\n", (char)t);
122        }
123        if (!dbus_message_iter_next(it)) return;
124    }
125}
126
127/* ── esperar el Response de un Request ────────────────────────────────────────────────────────── */
128/* Devuelve el código de respuesta (0 ok, 1 cancelado, 2 otro) o -1 si venció el plazo. Si
129 * `session_out` no es NULL, copia ahí el `session_handle` de los results cuando aparece. */
130
131static int wait_response(DBusConnection *bus, const char *path, int timeout_s,
132                         char *session_out, size_t session_n)
133{
134    printf("   ← esperando Response en %s (hasta %ds)\n", path, timeout_s);
135    for (int elapsed = 0; elapsed < timeout_s * 10; elapsed++) {
136        dbus_connection_read_write(bus, 100);
137        DBusMessage *msg;
138        while ((msg = dbus_connection_pop_message(bus))) {
139            if (dbus_message_is_signal(msg, IFACE_REQ, "Response") &&
140                !strcmp(dbus_message_get_path(msg), path)) {
141                DBusMessageIter it;
142                dbus_message_iter_init(msg, &it);
143                dbus_uint32_t code = 2;
144                dbus_message_iter_get_basic(&it, &code);
145                printf("   ← Response: %u (%s)\n", code,
146                       code == 0 ? "OK" : code == 1 ? "CANCELADO por el usuario" : "otro error");
147                dbus_message_iter_next(&it);
148
149                /* Buscar session_handle antes de imprimir, si nos lo pidieron. */
150                if (session_out) {
151                    DBusMessageIter arr;
152                    dbus_message_iter_recurse(&it, &arr);
153                    while (dbus_message_iter_get_arg_type(&arr) == DBUS_TYPE_DICT_ENTRY) {
154                        DBusMessageIter ent, var; const char *k;
155                        dbus_message_iter_recurse(&arr, &ent);
156                        dbus_message_iter_get_basic(&ent, &k);
157                        dbus_message_iter_next(&ent);
158                        dbus_message_iter_recurse(&ent, &var);
159                        if (!strcmp(k, "session_handle") &&
160                            dbus_message_iter_get_arg_type(&var) == DBUS_TYPE_STRING) {
161                            const char *s; dbus_message_iter_get_basic(&var, &s);
162                            snprintf(session_out, session_n, "%s", s);
163                        }
164                        dbus_message_iter_next(&arr);
165                    }
166                }
167                printf("   ← results:\n");
168                print_iter(&it, 3);
169                dbus_message_unref(msg);
170                return (int)code;
171            }
172            dbus_message_unref(msg);
173        }
174    }
175    printf("   !! sin Response en %ds — el portal aceptó la llamada y no contestó\n", timeout_s);
176    return -1;
177}
178
179/* ── llamada bloqueante que devuelve el object path del Request ───────────────────────────────── */
180
181static int call_request(DBusConnection *bus, DBusMessage *m, const char *what)
182{
183    DBusError err; dbus_error_init(&err);
184    DBusMessage *r = dbus_connection_send_with_reply_and_block(bus, m, 10000, &err);
185    dbus_message_unref(m);
186    if (!r) {
187        printf("   !! %s FALLÓ: %s: %s\n", what, err.name ? err.name : "?",
188               err.message ? err.message : "?");
189        dbus_error_free(&err);
190        return 0;
191    }
192    const char *p = NULL;
193    if (dbus_message_get_args(r, &err, DBUS_TYPE_OBJECT_PATH, &p, DBUS_TYPE_INVALID))
194        printf("   → %s aceptado, Request = %s\n", what, p);
195    else
196        printf("   → %s aceptado (sin object path en la respuesta)\n", what);
197    dbus_message_unref(r);
198    return 1;
199}
200
201static DBusMessage *new_call(const char *iface, const char *method)
202{ return dbus_message_new_method_call(PORTAL_BUS, PORTAL_PATH, iface, method); }
203
204static void subscribe(DBusConnection *bus, const char *path)
205{
206    char rule[512];
207    snprintf(rule, sizeof rule,
208             "type='signal',interface='%s',member='Response',path='%s'", IFACE_REQ, path);
209    DBusError err; dbus_error_init(&err);
210    dbus_bus_add_match(bus, rule, &err);
211    dbus_connection_flush(bus);
212    if (dbus_error_is_set(&err)) {
213        printf("   !! AddMatch falló: %s\n", err.message);
214        dbus_error_free(&err);
215    }
216}
217
218/* ── la propiedad `version` de una interfaz del portal ────────────────────────────────────────── */
219
220static void probe_version(DBusConnection *bus, const char *iface)
221{
222    DBusMessage *m = dbus_message_new_method_call(PORTAL_BUS, PORTAL_PATH,
223                                                  "org.freedesktop.DBus.Properties", "Get");
224    const char *prop = "version";
225    dbus_message_append_args(m, DBUS_TYPE_STRING, &iface, DBUS_TYPE_STRING, &prop, DBUS_TYPE_INVALID);
226    DBusError err; dbus_error_init(&err);
227    DBusMessage *r = dbus_connection_send_with_reply_and_block(bus, m, 5000, &err);
228    dbus_message_unref(m);
229    if (!r) {
230        printf("  %-46s AUSENTE (%s)\n", iface, err.name ? err.name : "?");
231        dbus_error_free(&err);
232        return;
233    }
234    DBusMessageIter it, var; dbus_uint32_t v = 0;
235    if (dbus_message_iter_init(r, &it) &&
236        dbus_message_iter_get_arg_type(&it) == DBUS_TYPE_VARIANT) {
237        dbus_message_iter_recurse(&it, &var);
238        dbus_message_iter_get_basic(&var, &v);
239    }
240    printf("  %-46s v%u\n", iface, v);
241    dbus_message_unref(r);
242}
243
244/* Plazo de los pasos que esperan a una PERSONA (ver cabecera). Ajustable con --wait N. */
245static int g_wait_s = 60;
246
247/* ── ScreenCast: el handshake de CUATRO pasos ─────────────────────────────────────────────────── */
248
249static int do_screencast(DBusConnection *bus, int multiple, int window, int persist)
250{
251    const char *IF = "org.freedesktop.portal.ScreenCast";
252    char tok[64], path[512], session[256] = {0};
253
254    /* 1. CreateSession — el único paso que además crea un objeto Session que sobrevive a la Request */
255    puts("\n[1/4] CreateSession");
256    next_token(tok, sizeof tok);
257    request_path(path, sizeof path, tok);
258    subscribe(bus, path);
259    {
260        DBusMessage *m = new_call(IF, "CreateSession");
261        DBusMessageIter it, arr;
262        dbus_message_iter_init_append(m, &it);
263        dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr);
264        opt_str(&arr, "handle_token", tok);
265        opt_str(&arr, "session_handle_token", "hammersess");
266        dbus_message_iter_close_container(&it, &arr);
267        if (!call_request(bus, m, "CreateSession")) return 1;
268    }
269    if (wait_response(bus, path, 15, session, sizeof session) != 0) return 1;
270    if (!session[0]) { puts("   !! sin session_handle en los results"); return 1; }
271    printf("   ✓ session_handle = %s\n", session);
272
273    /* 2. SelectSources — qué se va a capturar. `types` es un bitfield: 1=monitor, 2=ventana. */
274    puts("\n[2/4] SelectSources");
275    next_token(tok, sizeof tok);
276    request_path(path, sizeof path, tok);
277    subscribe(bus, path);
278    {
279        DBusMessage *m = new_call(IF, "SelectSources");
280        DBusMessageIter it, arr;
281        dbus_message_iter_init_append(m, &it);
282        const char *sp = session;
283        dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &sp);
284        dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr);
285        opt_str(&arr, "handle_token", tok);
286        opt_u32(&arr, "types", window ? 2 : 1);
287        opt_bool(&arr, "multiple", multiple ? TRUE : FALSE);
288        opt_u32(&arr, "cursor_mode", 2);   /* 1=oculto 2=embebido 4=metadatos */
289        /* persist_mode NO es un lujo: OMITIRLO CUELGA EL PORTAL. Medido en metal el 2026-08-06.
290         * Sin esta opción el portal asume PERSIST_MODE_NONE; el backend de COSMIC devuelve
291         * `restore_data` igual, y xdg-desktop-portal 1.18.4 entra por la rama NONE de
292         * xdp_session_persistence_generate_and_save_restore_token, que hace
293         * `g_clear_pointer(in_out_restore_token, g_free)` — deja el token en NULL — y a la vuelta
294         * el llamador hace `g_variant_new_string(*in_out_restore_token)` SIN comprobarlo.
295         * glib llama strlen(NULL) y el portal muere con SIGSEGV justo antes de emitir el Response.
296         * Desde el cliente se ve como «Start aceptado y nunca contesta»: el proceso que debía
297         * responder ya no existe. Con persist_mode >= 1 el token se genera (uuid) y no hay nulo.
298         * Es un fallo de aguas arriba; esto lo esquiva sin tocar el portal. */
299        opt_u32(&arr, "persist_mode", (dbus_uint32_t)persist);
300        dbus_message_iter_close_container(&it, &arr);
301        if (!call_request(bus, m, "SelectSources")) return 1;
302    }
303    if (wait_response(bus, path, 15, NULL, 0) != 0) return 1;
304
305    /* 3. Start — acá es donde el backend abre su UI de elección y, al aceptar, NEGOCIA EL NODO
306     *    contra el demonio de pipewire. Es el paso que no podía funcionar sin demonio corriendo. */
307    puts("\n[3/4] Start  (el backend abre su selector; los `streams` traen el node id)");
308    next_token(tok, sizeof tok);
309    request_path(path, sizeof path, tok);
310    subscribe(bus, path);
311    {
312        DBusMessage *m = new_call(IF, "Start");
313        DBusMessageIter it, arr;
314        dbus_message_iter_init_append(m, &it);
315        const char *sp = session, *parent = "";
316        dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &sp);
317        dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &parent);
318        dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr);
319        opt_str(&arr, "handle_token", tok);
320        dbus_message_iter_close_container(&it, &arr);
321        if (!call_request(bus, m, "Start")) return 1;
322    }
323    if (wait_response(bus, path, g_wait_s, NULL, 0) != 0) return 1;
324
325    /* 4. OpenPipeWireRemote — devuelve un FD, no un path. Es la prueba final de que hay demonio:
326     *    sin pipewire corriendo, el backend no tiene remoto que abrir. */
327    puts("\n[4/4] OpenPipeWireRemote  (devuelve un fd al demonio de pipewire)");
328    {
329        DBusMessage *m = new_call(IF, "OpenPipeWireRemote");
330        DBusMessageIter it, arr;
331        dbus_message_iter_init_append(m, &it);
332        const char *sp = session;
333        dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &sp);
334        dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr);
335        dbus_message_iter_close_container(&it, &arr);
336        DBusError err; dbus_error_init(&err);
337        DBusMessage *r = dbus_connection_send_with_reply_and_block(bus, m, 15000, &err);
338        dbus_message_unref(m);
339        if (!r) {
340            printf("   !! OpenPipeWireRemote FALLÓ: %s: %s\n",
341                   err.name ? err.name : "?", err.message ? err.message : "?");
342            dbus_error_free(&err);
343            return 1;
344        }
345        int fd = -1;
346        if (dbus_message_get_args(r, &err, DBUS_TYPE_UNIX_FD, &fd, DBUS_TYPE_INVALID)) {
347            /* Que el fd sea >= 0 no basta: el kernel siempre da un número. Lo que prueba que es un
348             * remoto vivo es que sea un SOCKET abierto, así que se le pregunta al propio fd. */
349            char link[256] = {0}, proc[64];
350            snprintf(proc, sizeof proc, "/proc/self/fd/%d", fd);
351            ssize_t n = readlink(proc, link, sizeof link - 1);
352            printf("   ✓ fd = %d  →  %s\n", fd, n > 0 ? link : "(sin readlink)");
353            puts("   ✓✓ HAY REMOTO DE PIPEWIRE: el stream se puede consumir desde este fd");
354            close(fd);
355        } else {
356            printf("   !! la respuesta no traía un fd: %s\n", err.message ? err.message : "?");
357            dbus_error_free(&err);
358            dbus_message_unref(r);
359            return 1;
360        }
361        dbus_message_unref(r);
362    }
363    return 0;
364}
365
366/* ── FileChooser: un solo paso, pero el resultado SÓLO llega por la señal ─────────────────────── */
367
368static int do_filechooser(DBusConnection *bus, int save)
369{
370    const char *IF = "org.freedesktop.portal.FileChooser";
371    char tok[64], path[512];
372    next_token(tok, sizeof tok);
373    request_path(path, sizeof path, tok);
374    subscribe(bus, path);
375
376    printf("\n[1/1] %s  (elegí algo en el diálogo; las `uris` llegan por la señal)\n",
377           save ? "SaveFile" : "OpenFile");
378    DBusMessage *m = new_call(IF, save ? "SaveFile" : "OpenFile");
379    DBusMessageIter it, arr;
380    dbus_message_iter_init_append(m, &it);
381    const char *parent = "", *title = "portal-probe";
382    dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &parent);
383    dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &title);
384    dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr);
385    opt_str(&arr, "handle_token", tok);
386    if (save) opt_str(&arr, "current_name", "portal-probe.txt");
387    dbus_message_iter_close_container(&it, &arr);
388    if (!call_request(bus, m, save ? "SaveFile" : "OpenFile")) return 1;
389
390    /* 120s: hay un humano (o un click por QMP) del otro lado. */
391    return wait_response(bus, path, 120, NULL, 0) == 0 ? 0 : 1;
392}
393
394static int do_screenshot(DBusConnection *bus)
395{
396    const char *IF = "org.freedesktop.portal.Screenshot";
397    char tok[64], path[512];
398    next_token(tok, sizeof tok);
399    request_path(path, sizeof path, tok);
400    subscribe(bus, path);
401
402    puts("\n[1/1] Screenshot");
403    DBusMessage *m = new_call(IF, "Screenshot");
404    DBusMessageIter it, arr;
405    dbus_message_iter_init_append(m, &it);
406    const char *parent = "";
407    dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &parent);
408    dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &arr);
409    opt_str(&arr, "handle_token", tok);
410    opt_bool(&arr, "interactive", FALSE);
411    dbus_message_iter_close_container(&it, &arr);
412    if (!call_request(bus, m, "Screenshot")) return 1;
413    return wait_response(bus, path, g_wait_s, NULL, 0) == 0 ? 0 : 1;
414}
415
416int main(int argc, char **argv)
417{
418    /* Línea a línea, SIEMPRE — no sólo cuando la salida es una terminal. Redirigida a fichero,
419     * stdout se bufferiza por bloques y el log queda vacío mientras la sonda espera: justo el
420     * tramo que interesa mirar. Medido el 2026-08-06 depurando ScreenCast en metal — la sonda
421     * llevaba minutos esperando un Response y el fichero mostraba sólo la línea de la llamada,
422     * que se lee como «se colgó al llamar» cuando en realidad estaba esperando como debía. Una
423     * sonda que no se puede leer MIENTRAS mide es media sonda. */
424    setvbuf(stdout, NULL, _IOLBF, 0);
425
426    const char *cmd = argc > 1 ? argv[1] : "version";
427    int multiple = 0, window = 0, save = 0, persist = 1;  /* 0=none 1=transitorio 2=persistente */
428    for (int i = 2; i < argc; i++) {
429        if (!strcmp(argv[i], "--multiple")) multiple = 1;
430        else if (!strcmp(argv[i], "--window")) window = 1;
431        else if (!strcmp(argv[i], "--save")) save = 1;
432        else if (!strcmp(argv[i], "--wait") && i + 1 < argc) g_wait_s = atoi(argv[++i]);
433        else if (!strcmp(argv[i], "--persist") && i + 1 < argc) persist = atoi(argv[++i]);
434    }
435
436    DBusError err; dbus_error_init(&err);
437    DBusConnection *bus = dbus_bus_get(DBUS_BUS_SESSION, &err);
438    if (!bus) {
439        fprintf(stderr, "sin bus de sesión: %s\n", err.message ? err.message : "?");
440        return 2;
441    }
442    /* Que este proceso NO muera cuando el bus se desconecte: queremos ver el error, no un exit(1)
443     * silencioso de libdbus. */
444    dbus_connection_set_exit_on_disconnect(bus, FALSE);
445
446    /* El sender sanitizado: nombre único sin ':' y con '.' → '_'. Es lo que el portal usa para
447     * construir el path del Request, y calcularlo nosotros es lo que permite escuchar ANTES. */
448    const char *uniq = dbus_bus_get_unique_name(bus);
449    snprintf(g_sender, sizeof g_sender, "%s", uniq && uniq[0] == ':' ? uniq + 1 : uniq);
450    for (char *p = g_sender; *p; p++) if (*p == '.') *p = '_';
451    printf("bus de sesión OK · nombre único %s · sender para el path del Request: %s\n",
452           uniq ? uniq : "?", g_sender);
453
454    /* Los portales son servicios ACTIVABLES por D-Bus, no demonios siempre presentes: el bus los
455     * arranca cuando alguien los pide, leyendo su fichero `.service`. `NameHasOwner` NO dispara esa
456     * activación — sólo pregunta— así que rechazar ahí es un falso negativo cada vez que el portal
457     * está simplemente dormido. Costó una corrida entera de diagnóstico: la sonda dijo «no hay
458     * frontend de portal en el bus» sobre un sistema donde el portal andaba perfecto.
459     *
460     * Lo correcto es pedir la activación y dejar que el bus decida. Si de verdad no hay `.service`,
461     * StartServiceByName falla con ServiceUnknown y ahí sí el diagnóstico es real. */
462    if (!dbus_bus_name_has_owner(bus, PORTAL_BUS, &err)) {
463        printf("%s dormido — pidiendo activación por D-Bus…\n", PORTAL_BUS);
464        dbus_error_free(&err);
465        dbus_error_init(&err);
466        dbus_uint32_t res = 0;
467        if (!dbus_bus_start_service_by_name(bus, PORTAL_BUS, 0, &res, &err)) {
468            printf("!! no se pudo activar %s: %s: %s\n", PORTAL_BUS,
469                   err.name ? err.name : "?", err.message ? err.message : "?");
470            return 3;
471        }
472        printf("   activado (%s)\n",
473               res == DBUS_START_REPLY_SUCCESS ? "arrancado ahora" : "ya estaba corriendo");
474    }
475    puts("portal presente en el bus\n");
476
477    int rc;
478    if (!strcmp(cmd, "version")) {
479        puts("interfaces del portal:");
480        probe_version(bus, "org.freedesktop.portal.ScreenCast");
481        probe_version(bus, "org.freedesktop.portal.Screenshot");
482        probe_version(bus, "org.freedesktop.portal.FileChooser");
483        probe_version(bus, "org.freedesktop.portal.Settings");
484        probe_version(bus, "org.freedesktop.portal.Access");
485        probe_version(bus, "org.freedesktop.portal.RemoteDesktop");
486        rc = 0;
487    } else if (!strcmp(cmd, "screencast"))   rc = do_screencast(bus, multiple, window, persist);
488    else if (!strcmp(cmd, "filechooser"))    rc = do_filechooser(bus, save);
489    else if (!strcmp(cmd, "screenshot"))     rc = do_screenshot(bus);
490    else {
491        fprintf(stderr, "uso: portal-probe {version|screencast|filechooser|screenshot} [--multiple|--window|--save] [--wait N] [--persist 0|1|2]\n");
492        rc = 2;
493    }
494    printf("\n== portal-probe %s: %s ==\n", cmd, rc == 0 ? "OK" : "FALLÓ");
495    return rc;
496}