Files
hammer/tools/portal-probe/portal-probe.c
T
sergioandClaude Opus 5 8182e459b0 🔭 portal-probe: el log quedaba vacío justo mientras medía
Redirigida a fichero, stdout se bufferiza por bloques: mientras la sonda espera un
Response el log muestra sólo la línea de la llamada. Eso se lee como «se colgó al
llamar» cuando en realidad estaba esperando como debe. Nos pasó depurando ScreenCast en
metal — dos minutos mirando un fichero que no avanzaba, con la sonda perfectamente viva.

setvbuf(_IOLBF) incondicional, no sólo cuando la salida es una terminal: el caso que
importa es justamente el redirigido.

Una sonda que no se puede leer MIENTRAS mide es media sonda.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 12:51:46 -04:00

485 lines
24 KiB
C

/* 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/<SENDER>/<TOKEN>, 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] [--wait N]
* portal-probe filechooser [--save]
* portal-probe screenshot [--wait N]
* portal-probe version (qué interfaces sirve el portal, y en qué versión)
*
* `--wait N` (por defecto 60s) es el plazo de los pasos que ABREN UN SELECTOR y por lo tanto
* esperan a una PERSONA: el `Start` de ScreenCast y el `Screenshot` interactivo. Medido en metal
* el 2026-08-06: con 60s clavados la sonda se rinde antes de que nadie llegue a elegir pantalla, y
* el informe sale «el portal aceptó la llamada y no contestó» — que se lee como un fallo del portal
* cuando en realidad la cadena está esperando un clic. Es el mismo error de método que ya nos costó
* tres diagnósticos falsos: medir contra el reloj en vez de contra el hecho. Aquí el hecho depende
* de un humano, así que el reloj tiene que ser AJUSTABLE — no adivinado.
* Los otros plazos (CreateSession/SelectSources) siguen en 15s a propósito: ésos no piden a nadie.
*/
#include <dbus/dbus.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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("<fd %d>\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("<tipo '%c'>\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);
}
/* Plazo de los pasos que esperan a una PERSONA (ver cabecera). Ajustable con --wait N. */
static int g_wait_s = 60;
/* ── 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, g_wait_s, 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, g_wait_s, NULL, 0) == 0 ? 0 : 1;
}
int main(int argc, char **argv)
{
/* Línea a línea, SIEMPRE — no sólo cuando la salida es una terminal. Redirigida a fichero,
* stdout se bufferiza por bloques y el log queda vacío mientras la sonda espera: justo el
* tramo que interesa mirar. Medido el 2026-08-06 depurando ScreenCast en metal — la sonda
* llevaba minutos esperando un Response y el fichero mostraba sólo la línea de la llamada,
* que se lee como «se colgó al llamar» cuando en realidad estaba esperando como debía. Una
* sonda que no se puede leer MIENTRAS mide es media sonda. */
setvbuf(stdout, NULL, _IOLBF, 0);
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;
else if (!strcmp(argv[i], "--wait") && i + 1 < argc) g_wait_s = atoi(argv[++i]);
}
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);
/* Los portales son servicios ACTIVABLES por D-Bus, no demonios siempre presentes: el bus los
* arranca cuando alguien los pide, leyendo su fichero `.service`. `NameHasOwner` NO dispara esa
* activación — sólo pregunta— así que rechazar ahí es un falso negativo cada vez que el portal
* está simplemente dormido. Costó una corrida entera de diagnóstico: la sonda dijo «no hay
* frontend de portal en el bus» sobre un sistema donde el portal andaba perfecto.
*
* Lo correcto es pedir la activación y dejar que el bus decida. Si de verdad no hay `.service`,
* StartServiceByName falla con ServiceUnknown y ahí sí el diagnóstico es real. */
if (!dbus_bus_name_has_owner(bus, PORTAL_BUS, &err)) {
printf("%s dormido — pidiendo activación por D-Bus…\n", PORTAL_BUS);
dbus_error_free(&err);
dbus_error_init(&err);
dbus_uint32_t res = 0;
if (!dbus_bus_start_service_by_name(bus, PORTAL_BUS, 0, &res, &err)) {
printf("!! no se pudo activar %s: %s: %s\n", PORTAL_BUS,
err.name ? err.name : "?", err.message ? err.message : "?");
return 3;
}
printf(" activado (%s)\n",
res == DBUS_START_REPLY_SUCCESS ? "arrancado ahora" : "ya estaba corriendo");
}
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] [--wait N]\n");
rc = 2;
}
printf("\n== portal-probe %s: %s ==\n", cmd, rc == 0 ? "OK" : "FALLÓ");
return rc;
}