modules: move zeroconf code to zeroconf-utils

This commit is contained in:
Wim Taymans 2026-02-26 17:09:45 +01:00
parent 4a399172b6
commit fa04146cfb
12 changed files with 15 additions and 16 deletions

View file

@ -1,558 +0,0 @@
/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
/* SPDX-License-Identifier: MIT */
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include "config.h"
#include <spa/utils/result.h>
#include <spa/utils/json.h>
#include <avahi-client/lookup.h>
#include <avahi-client/publish.h>
#include <avahi-common/error.h>
#include <avahi-common/malloc.h>
#include "../module-zeroconf-discover/avahi-poll.h"
#include "zeroconf.h"
#define pw_zeroconf_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_zeroconf_events, m, v, ##__VA_ARGS__)
#define pw_zeroconf_emit_destroy(c) pw_zeroconf_emit(c, destroy, 0)
#define pw_zeroconf_emit_error(c,e,m) pw_zeroconf_emit(c, error, 0, e, m)
#define pw_zeroconf_emit_added(c,id,i) pw_zeroconf_emit(c, added, 0, id, i)
#define pw_zeroconf_emit_removed(c,id,i) pw_zeroconf_emit(c, removed, 0, id, i)
struct service_info {
AvahiIfIndex interface;
AvahiProtocol protocol;
const char *name;
const char *type;
const char *domain;
const char *host_name;
AvahiAddress address;
uint16_t port;
};
#define SERVICE_INFO(...) ((struct service_info){ __VA_ARGS__ })
struct entry {
struct pw_zeroconf *zc;
struct spa_list link;
#define TYPE_ANNOUNCE 0
#define TYPE_BROWSE 1
uint32_t type;
void *user;
struct pw_properties *props;
AvahiEntryGroup *group;
AvahiServiceBrowser *browser;
struct spa_list services;
};
struct service {
struct entry *e;
struct spa_list link;
struct service_info info;
struct pw_properties *props;
};
struct pw_zeroconf {
int refcount;
struct pw_context *context;
struct pw_properties *props;
struct spa_hook_list listener_list;
AvahiPoll *poll;
AvahiClient *client;
AvahiClientState state;
struct spa_list entries;
bool discover_local;
};
static struct service *service_find(struct entry *e, const struct service_info *info)
{
struct service *s;
spa_list_for_each(s, &e->services, link) {
if (s->info.interface == info->interface &&
s->info.protocol == info->protocol &&
spa_streq(s->info.name, info->name) &&
spa_streq(s->info.type, info->type) &&
spa_streq(s->info.domain, info->domain))
return s;
}
return NULL;
}
static void service_free(struct service *s)
{
spa_list_remove(&s->link);
free((void*)s->info.name);
free((void*)s->info.type);
free((void*)s->info.domain);
free((void*)s->info.host_name);
pw_properties_free(s->props);
free(s);
}
struct entry *entry_find(struct pw_zeroconf *zc, uint32_t type, void *user)
{
struct entry *e;
spa_list_for_each(e, &zc->entries, link)
if (e->type == type && e->user == user)
return e;
return NULL;
}
static void entry_free(struct entry *e)
{
struct service *s;
spa_list_remove(&e->link);
if (e->group)
avahi_entry_group_free(e->group);
spa_list_consume(s, &e->services, link)
service_free(s);
pw_properties_free(e->props);
free(e);
}
static void zeroconf_free(struct pw_zeroconf *zc)
{
struct entry *a;
spa_list_consume(a, &zc->entries, link)
entry_free(a);
if (zc->client)
avahi_client_free(zc->client);
if (zc->poll)
pw_avahi_poll_free(zc->poll);
pw_properties_free(zc->props);
free(zc);
}
static void zeroconf_unref(struct pw_zeroconf *zc)
{
if (--zc->refcount == 0)
zeroconf_free(zc);
}
void pw_zeroconf_destroy(struct pw_zeroconf *zc)
{
pw_zeroconf_emit_destroy(zc);
zeroconf_unref(zc);
}
static struct service *service_new(struct entry *e,
const struct service_info *info, AvahiStringList *txt)
{
struct service *s;
struct pw_zeroconf *zc = e->zc;
const AvahiAddress *a = &info->address;
static const char *link_local_range = "169.254.";
AvahiStringList *l;
char at[AVAHI_ADDRESS_STR_MAX], if_suffix[16] = "";
if ((s = calloc(1, sizeof(*s))) == NULL)
goto error;
s->e = e;
spa_list_append(&e->services, &s->link);
s->info.interface = info->interface;
s->info.protocol = info->protocol;
s->info.name = strdup(info->name);
s->info.type = strdup(info->type);
s->info.domain = strdup(info->domain);
s->info.host_name = strdup(info->host_name);
s->info.address = info->address;
s->info.port = info->port;
if ((s->props = pw_properties_new(NULL, NULL)) == NULL)
goto error;
if (a->proto == AVAHI_PROTO_INET6 &&
a->data.ipv6.address[0] == 0xfe &&
(a->data.ipv6.address[1] & 0xc0) == 0x80)
snprintf(if_suffix, sizeof(if_suffix), "%%%d", info->interface);
avahi_address_snprint(at, sizeof(at), a);
if (a->proto == AVAHI_PROTO_INET &&
spa_strstartswith(at, link_local_range))
snprintf(if_suffix, sizeof(if_suffix), "%%%d", info->interface);
pw_properties_setf(s->props, "zeroconf.ifindex", "%d", info->interface);
pw_properties_setf(s->props, "zeroconf.name", "%s", info->name);
pw_properties_setf(s->props, "zeroconf.type", "%s", info->type);
pw_properties_setf(s->props, "zeroconf.domain", "%s", info->domain);
pw_properties_setf(s->props, "zeroconf.hostname", "%s", info->host_name);
pw_properties_setf(s->props, "zeroconf.address", "%s%s", at, if_suffix);
pw_properties_setf(s->props, "zeroconf.port", "%u", info->port);
for (l = txt; l; l = l->next) {
char *key, *value;
if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0)
break;
pw_properties_set(s->props, key, value);
avahi_free(key);
avahi_free(value);
}
pw_log_info("new %s %s %s %s", info->name, info->type, info->domain, info->host_name);
pw_zeroconf_emit_added(zc, e->user, &s->props->dict);
return s;
error:
if (s)
service_free(s);
return NULL;
}
static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface,
AvahiProtocol protocol, AvahiResolverEvent event,
const char *name, const char *type, const char *domain,
const char *host_name, const AvahiAddress *a, uint16_t port,
AvahiStringList *txt, AvahiLookupResultFlags flags,
void *userdata)
{
struct entry *e = userdata;
struct pw_zeroconf *zc = e->zc;
struct service_info info;
if (event != AVAHI_RESOLVER_FOUND) {
pw_log_error("Resolving of '%s' failed: %s", name,
avahi_strerror(avahi_client_errno(zc->client)));
goto done;
}
info = SERVICE_INFO(.interface = interface,
.protocol = protocol,
.name = name,
.type = type,
.domain = domain,
.host_name = host_name,
.address = *a,
.port = port);
service_new(e, &info, txt);
done:
avahi_service_resolver_free(r);
}
static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol,
AvahiBrowserEvent event, const char *name, const char *type, const char *domain,
AvahiLookupResultFlags flags, void *userdata)
{
struct entry *e = userdata;
struct pw_zeroconf *zc = e->zc;
struct service_info info;
struct service *s;
if ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && !zc->discover_local)
return;
info = SERVICE_INFO(.interface = interface,
.protocol = protocol,
.name = name,
.type = type,
.domain = domain);
s = service_find(e, &info);
switch (event) {
case AVAHI_BROWSER_NEW:
if (s != NULL)
return;
if (!(avahi_service_resolver_new(zc->client,
interface, protocol,
name, type, domain,
AVAHI_PROTO_UNSPEC, 0,
resolver_cb, e))) {
int res = avahi_client_errno(zc->client);
pw_log_error("can't make service resolver: %s", avahi_strerror(res));
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
}
break;
case AVAHI_BROWSER_REMOVE:
if (s == NULL)
return;
pw_log_info("removed %s %s %s", name, type, domain);
pw_zeroconf_emit_removed(zc, e->user, &s->props->dict);
service_free(s);
break;
default:
break;
}
}
static int do_browse(struct pw_zeroconf *zc, struct entry *e)
{
const struct spa_dict_item *it;
const char *service_name = NULL;
int res;
if (e->browser == NULL) {
spa_dict_for_each(it, &e->props->dict) {
if (spa_streq(it->key, "zeroconf.service"))
service_name = it->value;
}
if (service_name == NULL) {
res = -EINVAL;
pw_log_error("no service provided");
pw_zeroconf_emit_error(zc, res, spa_strerror(res));
return res;
}
e->browser = avahi_service_browser_new(zc->client,
AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
service_name, NULL, 0,
browser_cb, e);
if (e->browser == NULL) {
res = avahi_client_errno(zc->client);
pw_log_error("can't make browser: %s", avahi_strerror(res));
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
return -EIO;
}
}
return 0;
}
static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata)
{
struct entry *e = userdata;
struct pw_zeroconf *zc = e->zc;
int res;
zc->refcount++;
switch (state) {
case AVAHI_ENTRY_GROUP_ESTABLISHED:
pw_log_info("Service successfully established");
break;
case AVAHI_ENTRY_GROUP_COLLISION:
pw_log_error("Service name collision");
break;
case AVAHI_ENTRY_GROUP_FAILURE:
res = avahi_client_errno(zc->client);
pw_log_error("Entry group failure: %s", avahi_strerror(res));
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
break;
case AVAHI_ENTRY_GROUP_UNCOMMITED:
case AVAHI_ENTRY_GROUP_REGISTERING:
break;
}
zeroconf_unref(zc);
}
static int do_announce(struct pw_zeroconf *zc, struct entry *e)
{
AvahiStringList *txt = NULL;
int res;
const struct spa_dict_item *it;
const char *session_name = "unnamed", *service = NULL;
uint16_t port = 0;
if (e->group == NULL) {
e->group = avahi_entry_group_new(zc->client,
entry_group_callback, e);
if (e->group == NULL) {
res = avahi_client_errno(zc->client);
pw_log_error("can't make group: %s", avahi_strerror(res));
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
return -EIO;
}
}
avahi_entry_group_reset(e->group);
spa_dict_for_each(it, &e->props->dict) {
if (spa_streq(it->key, "zeroconf.session"))
session_name = it->value;
else if (spa_streq(it->key, "zeroconf.port"))
port = atoi(it->value);
else if (spa_streq(it->key, "zeroconf.service"))
service = it->value;
else
txt = avahi_string_list_add_pair(txt, it->key, it->value);
}
if (service == NULL) {
res = -EINVAL;
pw_log_error("no service provided");
pw_zeroconf_emit_error(zc, res, spa_strerror(res));
return res;
}
res = avahi_entry_group_add_service_strlst(e->group,
AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
(AvahiPublishFlags)0, session_name,
service, NULL, NULL, port, txt);
avahi_string_list_free(txt);
if (res < 0) {
res = avahi_client_errno(zc->client);
pw_log_error("can't add service: %s", avahi_strerror(res));
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
return -EIO;
}
if ((res = avahi_entry_group_commit(e->group)) < 0) {
res = avahi_client_errno(zc->client);
pw_log_error("can't commit group: %s", avahi_strerror(res));
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
return -EIO;
}
return 0;
}
static int entry_start(struct pw_zeroconf *zc, struct entry *e)
{
if (zc->state != AVAHI_CLIENT_S_REGISTERING &&
zc->state != AVAHI_CLIENT_S_RUNNING &&
zc->state != AVAHI_CLIENT_S_COLLISION)
return 0;
if (e->type == TYPE_ANNOUNCE)
return do_announce(zc, e);
else
return do_browse(zc, e);
}
static void client_callback(AvahiClient *c, AvahiClientState state, void *d)
{
struct pw_zeroconf *zc = d;
struct entry *e;
zc->client = c;
zc->refcount++;
zc->state = state;
switch (state) {
case AVAHI_CLIENT_S_REGISTERING:
case AVAHI_CLIENT_S_RUNNING:
case AVAHI_CLIENT_S_COLLISION:
spa_list_for_each(e, &zc->entries, link)
entry_start(zc, e);
break;
case AVAHI_CLIENT_FAILURE:
{
int err = avahi_client_errno(c);
pw_zeroconf_emit_error(zc, err, avahi_strerror(err));
break;
}
case AVAHI_CLIENT_CONNECTING:
default:
break;
}
zeroconf_unref(zc);
}
static struct entry *entry_new(struct pw_zeroconf *zc, uint32_t type, void *user, const struct spa_dict *info)
{
struct entry *e;
if ((e = calloc(1, sizeof(*e))) == NULL)
return NULL;
e->zc = zc;
e->type = type;
e->user = user;
e->props = pw_properties_new_dict(info);
spa_list_append(&zc->entries, &e->link);
spa_list_init(&e->services);
pw_log_info("created %s", type == TYPE_ANNOUNCE ? "announce" : "browse");
return e;
}
static int set_entry(struct pw_zeroconf *zc, uint32_t type, void *user, const struct spa_dict *info)
{
struct entry *e;
e = entry_find(zc, type, user);
if (e == NULL) {
if (info == NULL)
return 0;
e = entry_new(zc, type, user, info);
entry_start(zc, e);
} else {
if (info == NULL)
entry_free(e);
else {
pw_properties_update(e->props, info);
entry_start(zc, e);
}
}
return 0;
}
int pw_zeroconf_set_announce(struct pw_zeroconf *zc, void *user, const struct spa_dict *info)
{
return set_entry(zc, TYPE_ANNOUNCE, user, info);
}
int pw_zeroconf_set_browse(struct pw_zeroconf *zc, void *user, const struct spa_dict *info)
{
return set_entry(zc, TYPE_BROWSE, user, info);
}
struct pw_zeroconf * pw_zeroconf_new(struct pw_context *context,
struct spa_dict *props)
{
struct pw_zeroconf *zc;
uint32_t i;
int res;
if ((zc = calloc(1, sizeof(*zc))) == NULL)
return NULL;
zc->refcount = 1;
zc->context = context;
spa_hook_list_init(&zc->listener_list);
spa_list_init(&zc->entries);
zc->props = props ? pw_properties_new_dict(props) : pw_properties_new(NULL, NULL);
zc->discover_local = true;
for (i = 0; props && i < props->n_items; i++) {
const char *k = props->items[i].key;
const char *v = props->items[i].value;
if (spa_streq(k, "zeroconf.disable-local"))
zc->discover_local = spa_atob(v);
}
zc->poll = pw_avahi_poll_new(context);
if (zc->poll == NULL)
goto error;
zc->client = avahi_client_new(zc->poll, AVAHI_CLIENT_NO_FAIL,
client_callback, zc, &res);
if (!zc->client) {
pw_log_error("failed to create avahi client: %s", avahi_strerror(res));
goto error;
}
return zc;
error:
zeroconf_free(zc);
return NULL;
}
void pw_zeroconf_add_listener(struct pw_zeroconf *zc,
struct spa_hook *listener,
const struct pw_zeroconf_events *events, void *data)
{
spa_hook_list_append(&zc->listener_list, listener, events, data);
}

View file

@ -1,45 +0,0 @@
/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */
/* SPDX-License-Identifier: MIT */
#ifndef PIPEWIRE_ZEROCONF_H
#define PIPEWIRE_ZEROCONF_H
#include <stdarg.h>
#include <pipewire/pipewire.h>
#ifdef __cplusplus
extern "C" {
#endif
struct pw_zeroconf;
struct pw_zeroconf_events {
#define PW_VERSION_ZEROCONF_EVENTS 0
uint32_t version;
void (*destroy) (void *data);
void (*error) (void *data, int err, const char *message);
void (*added) (void *data, void *user, const struct spa_dict *info);
void (*removed) (void *data, void *user, const struct spa_dict *info);
};
struct pw_zeroconf * pw_zeroconf_new(struct pw_context *context,
struct spa_dict *props);
void pw_zeroconf_destroy(struct pw_zeroconf *zc);
int pw_zeroconf_set_announce(struct pw_zeroconf *zc, void *user, const struct spa_dict *info);
int pw_zeroconf_set_browse(struct pw_zeroconf *zc, void *user, const struct spa_dict *info);
void pw_zeroconf_add_listener(struct pw_zeroconf *zc,
struct spa_hook *listener,
const struct pw_zeroconf_events *events, void *data);
#ifdef __cplusplus
}
#endif
#endif /* PIPEWIRE_ZEROCONF_H */