Files
Alexander Pevzner 443bc7b797 WSDD: management of extended discovery time revisited (see also #151e440)
See #151e440 for problem description and workaround method being used

The criterion of applying extended time was simplified. Instead of looking
for unpaired WSDD/MDNS device we simply compare count of WSDD/MDNS devices,
discovered so far, with problematic model names

This change not only makes things conceptually simpler, but also allows to
take in account MDNS-devices early, with not yet completed discovery/resolving
process
2024-02-23 14:43:52 +03:00

1427 lines
38 KiB
C

/* AirScan (a.k.a. eSCL) backend for SANE
*
* Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* MDNS device discovery
*/
#include "airscan.h"
#include <arpa/inet.h>
#include <net/if.h>
#include <avahi-client/client.h>
#include <avahi-client/lookup.h>
#include <avahi-common/domain.h>
#include <avahi-common/error.h>
#include <fnmatch.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
/******************** Constants *********************/
/* If failed, AVAHI client will be automatically
* restarted after the following timeout expires,
* in seconds
*/
#define MDNS_AVAHI_CLIENT_RESTART_TIMEOUT 1
/* Max time to wait until device table is ready, in seconds
*/
#define MDNS_READY_TIMEOUT 5
/* MDNS_SERVICE represents numerical identifiers for
* DNS-SD service types we are interested in
*/
typedef enum {
MDNS_SERVICE_UNKNOWN = -1,
MDNS_SERVICE_IPP_TCP, /* _ipp._tcp */
MDNS_SERVICE_IPPS_TCP, /* _ipps._tcp */
MDNS_SERVICE_USCAN_TCP, /* _uscan._tcp */
MDNS_SERVICE_USCANS_TCP, /* _uscans._tcp */
MDNS_SERVICE_SCANNER_TCP, /* _scanner._tcp */
NUM_MDNS_SERVICE
} MDNS_SERVICE;
/* Action names for mdns_debug/mdns_perror messages
*/
#define MDNS_ACTION_BROWSE "browse"
#define MDNS_ACTION_RESOLVE "resolve"
#define MDNS_ACTION_LOOKUP "lookup"
/******************** Local Types *********************/
/* mdns_finding represents zeroconf_finding for MDNS
* device discovery
*/
typedef struct {
zeroconf_finding finding; /* Base class */
AvahiServiceResolver **resolvers; /* Array of pending resolvers */
eloop_timer *publish_timer; /* ZEROCONF_PUBLISH_DELAY timer */
ll_node node_list; /* In mdns_finding_list */
bool should_publish; /* Should we publish this finding */
bool is_published; /* Finding actually published */
bool initscan; /* Device discovered during initial scan */
} mdns_finding;
/* Static variables
*/
static log_ctx *mdns_log;
static ll_head mdns_finding_list;
static size_t mdns_finding_list_count;
static const AvahiPoll *mdns_avahi_poll;
static AvahiTimeout *mdns_avahi_restart_timer;
static AvahiClient *mdns_avahi_client;
static bool mdns_avahi_browser_running;
static AvahiServiceBrowser *mdns_avahi_browser[NUM_MDNS_SERVICE];
static bool mdns_initscan[NUM_MDNS_SERVICE];
static int mdns_initscan_count[NUM_ZEROCONF_METHOD];
/* Forward declarations
*/
static void
mdns_avahi_browser_stop (void);
static void
mdns_avahi_client_start (void);
static void
mdns_avahi_client_restart_defer (void);
/* Print debug message
*/
static void
mdns_debug (const char *action, AvahiIfIndex interface, AvahiProtocol protocol,
AvahiLookupResultFlags flags, const char *in_type, const char *in_name,
const char *out)
{
const char *af = protocol == AVAHI_PROTO_INET ? "ipv4" : "ipv6";
char ifname[IF_NAMESIZE] = "?";
char buf[512];
char buf2[128] = "";
if (interface == AVAHI_IF_UNSPEC) {
strcpy(ifname, "*");
} else if (if_indextoname(interface, ifname) == NULL) {
sprintf(ifname, "%d", interface);
}
if (in_name == NULL) {
snprintf(buf, sizeof(buf), "\"%s\"", in_type);
} else if (in_type == NULL) {
snprintf(buf, sizeof(buf), "\"%s\"", in_name);
} else {
snprintf(buf, sizeof(buf), "\"%s\", \"%s\"", in_type, in_name);
}
flags &= AVAHI_LOOKUP_RESULT_CACHED |
AVAHI_LOOKUP_RESULT_WIDE_AREA |
AVAHI_LOOKUP_RESULT_MULTICAST |
AVAHI_LOOKUP_RESULT_STATIC;
if (flags != 0) {
char *s = buf2 + 1;
if ((flags & AVAHI_LOOKUP_RESULT_CACHED) != 0) {
strcpy(s, " CACHED");
s += strlen(s);
}
if ((flags & AVAHI_LOOKUP_RESULT_WIDE_AREA) != 0) {
strcpy(s, " WAN");
s += strlen(s);
}
if ((flags & AVAHI_LOOKUP_RESULT_MULTICAST) != 0) {
strcpy(s, " MCAST");
s += strlen(s);
}
if ((flags & AVAHI_LOOKUP_RESULT_STATIC) != 0) {
strcpy(s, " STATIC");
s += strlen(s);
}
strcpy(s, ")");
buf2[0] = ' ';
buf2[1] = '(';
}
log_debug(mdns_log, "%s-%s@%s(%s): %s%s",
action, af, ifname, buf, out, buf2);
}
/* Print error message
*/
static void
mdns_perror (const char *action, AvahiIfIndex interface, AvahiProtocol protocol,
const char *in_type, const char *in_name)
{
mdns_debug(action, interface, protocol, 0, in_type, in_name,
avahi_strerror(avahi_client_errno(mdns_avahi_client)));
}
/* Get MDNS_SERVICE name
*/
static const char*
mdns_service_name (MDNS_SERVICE service)
{
switch (service) {
case MDNS_SERVICE_IPP_TCP: return "_ipp._tcp";
case MDNS_SERVICE_IPPS_TCP: return "_ipps._tcp";
case MDNS_SERVICE_USCAN_TCP: return "_uscan._tcp";
case MDNS_SERVICE_USCANS_TCP: return "_uscans._tcp";
case MDNS_SERVICE_SCANNER_TCP: return "_scanner._tcp";
case MDNS_SERVICE_UNKNOWN:
case NUM_MDNS_SERVICE:
break;
}
log_internal_error(mdns_log);
return NULL;
}
/* Get MDNS_SERVICE by name
*/
static MDNS_SERVICE
mdns_service_by_name (const char *name)
{
int i;
for (i = 0; i < NUM_MDNS_SERVICE; i ++) {
if (!strcasecmp(name, mdns_service_name(i))) {
return i;
}
}
return MDNS_SERVICE_UNKNOWN;
}
/* Map MDNS_SERVICE to ZEROCONF_METHOD
*/
static ZEROCONF_METHOD
mdns_service_to_method (MDNS_SERVICE service)
{
switch (service) {
case MDNS_SERVICE_USCAN_TCP: return ZEROCONF_USCAN_TCP;
case MDNS_SERVICE_USCANS_TCP: return ZEROCONF_USCANS_TCP;
default: return ZEROCONF_MDNS_HINT;
}
}
/* Get AvahiResolverEvent name, for debugging
*/
static const char*
mdns_avahi_resolver_event_name (AvahiResolverEvent e)
{
static char buf[64];
switch (e) {
case AVAHI_RESOLVER_FOUND: return "AVAHI_RESOLVER_FOUND";
case AVAHI_RESOLVER_FAILURE: return "AVAHI_RESOLVER_FAILURE";
}
/* Safe, because used only from the work thread */
sprintf(buf, "AVAHI_RESOLVER_UNKNOWN(%d)", e);
return buf;
}
/* Get AvahiBrowserEvent name, for debugging
*/
static const char*
mdns_avahi_browser_event_name (AvahiBrowserEvent e)
{
static char buf[64];
switch (e) {
case AVAHI_BROWSER_NEW: return "AVAHI_BROWSER_NEW";
case AVAHI_BROWSER_REMOVE: return "AVAHI_BROWSER_REMOVE";
case AVAHI_BROWSER_CACHE_EXHAUSTED: return "AVAHI_BROWSER_CACHE_EXHAUSTED";
case AVAHI_BROWSER_ALL_FOR_NOW: return "AVAHI_BROWSER_ALL_FOR_NOW";
case AVAHI_BROWSER_FAILURE: return "AVAHI_BROWSER_FAILURE";
}
/* Safe, because used only from the work thread */
sprintf(buf, "AVAHI_BROWSER_UNKNOWN(%d)", e);
return buf;
}
/* Get AvahiClientState name, for debugging
*/
static const char*
mdns_avahi_client_state_name (AvahiClientState s)
{
static char buf[64];
switch (s) {
case AVAHI_CLIENT_S_REGISTERING: return "AVAHI_CLIENT_S_REGISTERING";
case AVAHI_CLIENT_S_RUNNING: return "AVAHI_CLIENT_S_RUNNING";
case AVAHI_CLIENT_S_COLLISION: return "AVAHI_CLIENT_S_COLLISION";
case AVAHI_CLIENT_FAILURE: return "AVAHI_CLIENT_FAILURE";
case AVAHI_CLIENT_CONNECTING: return "AVAHI_CLIENT_CONNECTING";
}
/* Safe, because used only from the work thread */
sprintf(buf, "AVAHI_BROWSER_UNKNOWN(%d)", s);
return buf;
}
/* Increment count of initial scan tasks
*/
static void
mdns_initscan_count_inc (ZEROCONF_METHOD method)
{
mdns_initscan_count[method] ++;
}
/* Decrement count if initial scan tasks
*/
static void
mdns_initscan_count_dec (ZEROCONF_METHOD method)
{
log_assert(mdns_log, mdns_initscan_count[method] > 0);
mdns_initscan_count[method] --;
if (mdns_initscan_count[method] == 0) {
zeroconf_finding_done(method);
}
}
/* Create new mdns_finding structure
*/
static mdns_finding*
mdns_finding_new (ZEROCONF_METHOD method, int ifindex, const char *name,
bool initscan)
{
mdns_finding *mdns = mem_new(mdns_finding, 1);
mdns->finding.method = method;
mdns->finding.ifindex = ifindex;
mdns->finding.name = str_dup(name);
mdns->finding.addrs = ip_addrset_new();
mdns->resolvers = ptr_array_new(AvahiServiceResolver*);
mdns->initscan = initscan;
if (mdns->initscan) {
mdns_initscan_count_inc(mdns->finding.method);
}
return mdns;
}
/* Free mdns_finding structure
*/
static void
mdns_finding_free (mdns_finding *mdns)
{
mem_free((char*) mdns->finding.name);
mem_free((char*) mdns->finding.model);
ip_addrset_free(mdns->finding.addrs);
zeroconf_endpoint_list_free(mdns->finding.endpoints);
if (mdns->initscan) {
mdns_initscan_count_dec(mdns->finding.method);
}
mem_free(mdns->resolvers);
mem_free(mdns);
}
/* Find mdns_finding
*/
static mdns_finding*
mdns_finding_find (ZEROCONF_METHOD method, int ifindex, const char *name)
{
ll_node *node;
for (LL_FOR_EACH(node, &mdns_finding_list)) {
mdns_finding *mdns;
mdns = OUTER_STRUCT(node, mdns_finding, node_list);
if (mdns->finding.method == method &&
mdns->finding.ifindex == ifindex &&
!strcasecmp(mdns->finding.name, name)) {
return mdns;
}
}
return NULL;
}
/* Get mdns_finding: find existing or add a new one
*/
static mdns_finding*
mdns_finding_get (ZEROCONF_METHOD method, int ifindex, const char *name,
bool initscan)
{
mdns_finding *mdns;
/* Check for duplicated device */
mdns = mdns_finding_find(method, ifindex, name);
if (mdns != NULL) {
return mdns;
}
/* Add new mdns_finding state */
mdns = mdns_finding_new(method, ifindex, name, initscan);
ll_push_end(&mdns_finding_list, &mdns->node_list);
mdns_finding_list_count ++;
return mdns;
}
/* Kill pending resolvers
*
* It also cancels mdns->publish_timer, if it is active
*/
static void
mdns_finding_kill_resolvers (mdns_finding *mdns)
{
size_t i, len = mem_len(mdns->resolvers);
for (i = 0; i < len; i ++) {
avahi_service_resolver_free(mdns->resolvers[i]);
}
ptr_array_trunc(mdns->resolvers);
if (mdns->publish_timer != NULL) {
eloop_timer_cancel(mdns->publish_timer);
mdns->publish_timer = NULL;
}
}
/* Del the mdns_finding
*/
static void
mdns_finding_del (mdns_finding *mdns)
{
if (mdns->is_published) {
zeroconf_finding_withdraw(&mdns->finding);
}
ll_del(&mdns->node_list);
mdns_finding_list_count --;
mdns_finding_kill_resolvers(mdns);
mdns_finding_free(mdns);
}
/* Delete all mdns_finding
*/
static void
mdns_finding_del_all (void)
{
ll_node *node;
while ((node = ll_first(&mdns_finding_list)) != NULL) {
mdns_finding *mdns;
mdns = OUTER_STRUCT(node, mdns_finding, node_list);
mdns_finding_del(mdns);
}
}
/* Publish mdns_finding
*/
static void
mdns_finding_publish (mdns_finding *mdns)
{
/* Cancel any activity, if still pending */
mdns_finding_kill_resolvers(mdns);
/* Fixup endpoints */
mdns->finding.endpoints = zeroconf_endpoint_list_sort_dedup(
mdns->finding.endpoints);
/* Fixup model and UUID */
if (mdns->finding.model == NULL) {
/* Very unlikely, just paranoia */
mdns->finding.model = str_dup(mdns->finding.name);
}
if (!uuid_valid(mdns->finding.uuid)) {
/* Paranoia too
*
* If device UUID is not available from DNS-SD (which
* is very unlikely), we generate a synthetic UUID,
* based on device name hash
*/
mdns->finding.uuid = uuid_hash(mdns->finding.name);
}
/* Update initscan count */
if (mdns->initscan) {
mdns->initscan = false;
mdns_initscan_count_dec(mdns->finding.method);
}
/* Publish the finding */
if (mdns->should_publish && !mdns->is_published) {
mdns->is_published = true;
zeroconf_finding_publish(&mdns->finding);
}
}
/* ZEROCONF_PUBLISH_DELAY timer callback
*/
static void
mdns_finding_publish_delay_timer_callback (void *data)
{
mdns_finding *mdns = data;
log_debug(mdns_log, "\"%s\": publish-delay timer expired",
mdns->finding.name);
mdns->publish_timer = NULL;
mdns_finding_publish(mdns);
}
/* Publish mdns_finding with optional delay
*/
static void
mdns_finding_publish_delay (mdns_finding *mdns)
{
if (mdns->resolvers[0] == NULL) {
mdns_finding_publish(mdns);
} else if (mdns->publish_timer == NULL) {
mdns->publish_timer = eloop_timer_new(ZEROCONF_PUBLISH_DELAY,
mdns_finding_publish_delay_timer_callback, mdns);
}
}
/* Make zeroconf_endpoint for eSCL
*/
static zeroconf_endpoint*
mdns_make_escl_endpoint (ZEROCONF_METHOD method, const AvahiAddress *addr,
uint16_t port, const char *rs, AvahiIfIndex interface)
{
char str_addr[128];
int rs_len;
char *u;
http_uri *uri;
const char *scheme;
if (method == ZEROCONF_USCAN_TCP) {
scheme = "http";
} else {
scheme = "https";
}
if (addr->proto == AVAHI_PROTO_INET) {
avahi_address_snprint(str_addr, sizeof(str_addr), addr);
} else {
size_t len;
str_addr[0] = '[';
avahi_address_snprint(str_addr + 1, sizeof(str_addr) - 2, addr);
len = strlen(str_addr);
/* Connect to link-local address requires explicit scope */
if (ip_is_linklocal(AF_INET6, addr->data.data)) {
/* Percent character in the IPv6 address literal
* needs to be properly escaped, so it becomes %25
* See RFC6874 for details
*/
len += sprintf(str_addr + len, "%%25%d", interface);
}
str_addr[len++] = ']';
str_addr[len] = '\0';
}
/* Normalize rs */
rs_len = 0;
if (rs != NULL) {
while (*rs == '/') {
rs ++;
}
rs_len = (int) strlen(rs);
while (rs_len != 0 && rs[rs_len - 1] == '/') {
rs_len --;
}
}
/* Make eSCL URL */
if (rs == NULL) {
/* Assume /eSCL by default */
u = str_printf("%s://%s:%d/eSCL/", scheme, str_addr, port);
} else if (rs_len == 0) {
/* Empty rs, avoid double '/' */
u = str_printf("%s://%s:%d/", scheme, str_addr, port);
} else {
u = str_printf("%s://%s:%d/%.*s/", scheme, str_addr, port, rs_len, rs);
}
uri = http_uri_new(u, true);
log_assert(mdns_log, uri != NULL);
mem_free(u);
return zeroconf_endpoint_new(ID_PROTO_ESCL, uri);
}
/* Handle AVAHI_RESOLVER_FOUND event
*/
static void
mdns_avahi_resolver_found (mdns_finding *mdns, MDNS_SERVICE service,
AvahiStringList *txt, const AvahiAddress *addr, uint16_t port,
AvahiIfIndex interface)
{
const char *txt_ty = NULL;
const char *txt_uuid = NULL;
const char *txt_scan = NULL;
const char *txt_rs = NULL;
AvahiStringList *s;
zeroconf_endpoint *endpoint;
ZEROCONF_METHOD method = mdns->finding.method;
ip_addr ip_addr = ip_addr_make(interface,
addr->proto == AVAHI_PROTO_INET ? AF_INET : AF_INET6, &addr->data);
/* Decode TXT record */
s = avahi_string_list_find(txt, "ty");
if (s != NULL && s->size > 3) {
txt_ty = (char*) s->text + 3;
}
s = avahi_string_list_find(txt, "uuid");
if (s != NULL && s->size > 5) {
txt_uuid = (char*) s->text + 5;
}
switch (service) {
case MDNS_SERVICE_IPP_TCP:
case MDNS_SERVICE_IPPS_TCP:
s = avahi_string_list_find(txt, "scan");
if (s != NULL && s->size > 5) {
txt_scan = (char*) s->text + 5;
}
break;
default:
break;
}
switch (service) {
case MDNS_SERVICE_USCAN_TCP:
case MDNS_SERVICE_USCANS_TCP:
s = avahi_string_list_find(txt, "rs");
if (s != NULL && s->size > 3) {
txt_rs = (char*) s->text + 3;
}
break;
default:
break;
}
/* Update finding */
if (mdns->finding.model == NULL && txt_ty != NULL) {
mdns->finding.model = str_dup(txt_ty);
}
if (!uuid_valid(mdns->finding.uuid) && txt_uuid != NULL) {
mdns->finding.uuid = uuid_parse(txt_uuid);
}
ip_addrset_add(mdns->finding.addrs, ip_addr);
/* Handle the event */
switch (service) {
case MDNS_SERVICE_IPP_TCP:
case MDNS_SERVICE_IPPS_TCP:
if (txt_scan != NULL && !strcasecmp(txt_scan, "t")) {
mdns->should_publish = true;
}
break;
case MDNS_SERVICE_USCAN_TCP:
case MDNS_SERVICE_USCANS_TCP:
endpoint = mdns_make_escl_endpoint(method, addr, port,
txt_rs, interface);
endpoint->next = mdns->finding.endpoints;
mdns->finding.endpoints = endpoint;
mdns->should_publish = true;
break;
case MDNS_SERVICE_SCANNER_TCP:
mdns->should_publish = true;
break;
case MDNS_SERVICE_UNKNOWN:
case NUM_MDNS_SERVICE:
log_internal_error(mdns_log);
}
}
/* AVAHI service resolver callback
*/
static void
mdns_avahi_resolver_callback (AvahiServiceResolver *r,
AvahiIfIndex interface, AvahiProtocol protocol,
AvahiResolverEvent event, const char *name, const char *type,
const char *domain, const char *host_name, const AvahiAddress *addr,
uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags,
void *userdata)
{
mdns_finding *mdns = userdata;
MDNS_SERVICE service = mdns_service_by_name(type);
(void) domain;
(void) host_name;
(void) flags;
/* Print debug message */
if (event == AVAHI_RESOLVER_FOUND) {
char buf[128];
avahi_address_snprint(buf, sizeof(buf), addr);
sprintf(buf + strlen(buf), ":%d", port);
mdns_debug(MDNS_ACTION_RESOLVE, interface, protocol, flags, type, name,
buf);
} else if (event == AVAHI_RESOLVER_FAILURE) {
mdns_perror(MDNS_ACTION_RESOLVE, interface, protocol, type, name);
} else {
mdns_debug(MDNS_ACTION_RESOLVE, interface, protocol, flags, type, name,
mdns_avahi_resolver_event_name(event));
}
/* Remove resolver from list of pending ones */
if (!ptr_array_del(mdns->resolvers, ptr_array_find(mdns->resolvers, r))) {
mdns_debug(MDNS_ACTION_RESOLVE, interface, protocol, flags, type, name,
"spurious avahi callback");
return;
}
avahi_service_resolver_free(r);
/* Handle event */
switch (event) {
case AVAHI_RESOLVER_FOUND:
mdns_avahi_resolver_found(mdns, service, txt, addr, port, interface);
break;
case AVAHI_RESOLVER_FAILURE:
break;
}
/* Perform appropriate actions, if resolving is done */
mdns_finding_publish_delay(mdns);
/* Notify WSDD about newly discovered address */
if (event == AVAHI_RESOLVER_FOUND) {
int af = addr->proto == AVAHI_PROTO_INET ? AF_INET : AF_INET6;
wsdd_send_directed_probe(interface, af, &addr->data);
}
}
/* AVAHI browser callback
*/
static void
mdns_avahi_browser_callback (AvahiServiceBrowser *b, AvahiIfIndex interface,
AvahiProtocol protocol, AvahiBrowserEvent event,
const char *name, const char *type, const char *domain,
AvahiLookupResultFlags flags, void* userdata)
{
mdns_finding *mdns;
// Newer clang produces a warning about casting from void* to an enum.
// userdata is only set to values from the MDNS_SERVICE enum, so we can
// safely suppress this warning with an extra cast through intptr_t.
MDNS_SERVICE service = (MDNS_SERVICE)(intptr_t) userdata;
ZEROCONF_METHOD method = mdns_service_to_method(service);
bool initscan = mdns_initscan[service];
(void) b;
(void) flags;
/* Print debug message */
mdns_debug(MDNS_ACTION_BROWSE, interface, protocol, flags, type, NULL,
mdns_avahi_browser_event_name(event));
if (event == AVAHI_BROWSER_NEW) {
size_t len = strlen(name);
char *buf = alloca(len + 3);
buf[0] = '"';
memcpy(buf + 1, name, len);
buf[len + 1] = '"';
buf[len + 2] = '\0';
mdns_debug(MDNS_ACTION_BROWSE, interface, protocol, flags, type, NULL,
buf);
}
switch (event) {
case AVAHI_BROWSER_NEW:
/* Add a device (or lookup for already added) */
mdns = mdns_finding_get(method, interface, name, initscan);
/* Initiate resolver -- look for IPv4 addresses */
AvahiServiceResolver *r;
r = avahi_service_resolver_new(mdns_avahi_client, interface,
protocol, name, type, domain, AVAHI_PROTO_INET, 0,
mdns_avahi_resolver_callback, mdns);
if (r == NULL) {
mdns_perror(MDNS_ACTION_RESOLVE, interface, AVAHI_PROTO_INET,
type, name);
mdns_avahi_client_restart_defer();
break;
}
mdns->resolvers = ptr_array_append(mdns->resolvers, r);
/* Initiate resolver -- look for IPv6 addresses */
r = avahi_service_resolver_new(mdns_avahi_client, interface,
protocol, name, type, domain, AVAHI_PROTO_INET6, 0,
mdns_avahi_resolver_callback, mdns);
if (r == NULL) {
mdns_perror(MDNS_ACTION_RESOLVE, interface, AVAHI_PROTO_INET6,
type, name);
mdns_avahi_client_restart_defer();
break;
}
/* Attach resolver to device state */
mdns->resolvers = ptr_array_append(mdns->resolvers, r);
break;
case AVAHI_BROWSER_REMOVE:
mdns = mdns_finding_find(method, interface, name);
if (mdns != NULL) {
mdns_finding_del(mdns);
}
break;
case AVAHI_BROWSER_FAILURE:
mdns_perror(MDNS_ACTION_BROWSE, interface, protocol, type, NULL);
mdns_avahi_client_restart_defer();
break;
case AVAHI_BROWSER_CACHE_EXHAUSTED:
break;
case AVAHI_BROWSER_ALL_FOR_NOW:
if (mdns_initscan[service]) {
mdns_initscan[service] = false;
mdns_initscan_count_dec(method);
}
break;
}
}
/* Start browser for specified service type
*/
static bool
mdns_avahi_browser_start_for_type (MDNS_SERVICE service, const char *type)
{
bool ok;
log_assert(mdns_log, mdns_avahi_browser[service] == NULL);
mdns_avahi_browser[service] = avahi_service_browser_new(mdns_avahi_client,
AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, type, NULL,
0, mdns_avahi_browser_callback, (void*) service);
ok = mdns_avahi_browser[service] != NULL;
if (!ok) {
log_debug(mdns_log, "avahi_service_browser_new(%s): %s",
type, avahi_strerror(avahi_client_errno(mdns_avahi_client)));
}
if (ok && mdns_initscan[service]) {
mdns_initscan_count_inc(mdns_service_to_method(service));
}
return mdns_avahi_browser[service] != NULL;
}
/* Start/restart service browser
*/
static bool
mdns_avahi_browser_start (void)
{
int i;
bool ok = true;
log_assert(mdns_log, !mdns_avahi_browser_running);
for (i = 0; ok && i < NUM_MDNS_SERVICE; i ++) {
ok = mdns_avahi_browser_start_for_type(i, mdns_service_name(i));
}
mdns_avahi_browser_running = true;
return ok;
}
/* Stop service browser
*/
static void
mdns_avahi_browser_stop (void)
{
MDNS_SERVICE service;
for (service = 0; service < NUM_MDNS_SERVICE; service ++) {
if (mdns_avahi_browser[service] != NULL) {
avahi_service_browser_free(mdns_avahi_browser[service]);
mdns_avahi_browser[service] = NULL;
if (mdns_initscan[service]) {
mdns_initscan_count_dec(mdns_service_to_method(service));
}
}
}
mdns_finding_del_all();
mdns_avahi_browser_running = false;
}
/* AVAHI client callback
*/
static void
mdns_avahi_client_callback (AvahiClient *client, AvahiClientState state,
void *userdata)
{
(void) client;
(void) userdata;
log_debug(mdns_log, "%s", mdns_avahi_client_state_name(state));
switch (state) {
case AVAHI_CLIENT_S_REGISTERING:
case AVAHI_CLIENT_S_RUNNING:
case AVAHI_CLIENT_S_COLLISION:
/* Note, first callback may come before avahi_client_new()
* return, so mdns_avahi_client may be still unset.
* Fix it here
*/
mdns_avahi_client = client;
if (!mdns_avahi_browser_running) {
if (!mdns_avahi_browser_start()) {
mdns_avahi_client_restart_defer();
}
}
break;
case AVAHI_CLIENT_FAILURE:
mdns_avahi_client_restart_defer();
break;
case AVAHI_CLIENT_CONNECTING:
break;
}
}
/* Timer for differed AVAHI client restart
*/
static void
mdns_avahi_restart_timer_callback(AvahiTimeout *t, void *userdata)
{
(void) t;
(void) userdata;
mdns_avahi_client_start();
}
/* Stop AVAHI client
*/
static void
mdns_avahi_client_stop (void)
{
if (mdns_avahi_client != NULL) {
avahi_client_free(mdns_avahi_client);
mdns_avahi_client = NULL;
}
}
/* Start/restart the AVAHI client
*/
static void
mdns_avahi_client_start (void)
{
int error;
log_assert(mdns_log, mdns_avahi_client == NULL);
mdns_avahi_client = avahi_client_new (mdns_avahi_poll,
AVAHI_CLIENT_NO_FAIL, mdns_avahi_client_callback, NULL, &error);
if (mdns_avahi_client == NULL) {
log_debug(mdns_log, "avahi_client_new failed: %s", avahi_strerror(error));
}
}
/* Deferred client restart
*/
static void
mdns_avahi_client_restart_defer (void)
{
struct timeval tv;
mdns_avahi_browser_stop();
mdns_avahi_client_stop();
gettimeofday(&tv, NULL);
tv.tv_sec += MDNS_AVAHI_CLIENT_RESTART_TIMEOUT;
mdns_avahi_poll->timeout_update(mdns_avahi_restart_timer, &tv);
}
/* Called by zeroconf to notify MDNS about initial scan timer expiration
*/
void
mdns_initscan_timer_expired (void)
{
}
/***** Asynchronous MDNS resolver *****/
/* mdns_resolver asynchronously resolves IP addresses using MDNS
*/
struct mdns_resolver {
int ifindex; /* Interface index */
ll_head pending; /* Pending queries */
};
/* mdns_query represents a single mdns_resolver query
*/
struct mdns_query {
char *name; /* Requested name */
mdns_resolver *resolver; /* Back link to resolver */
ip_addrset *answer; /* Collected addresses */
uint64_t dummy_callid; /* Used when no resolvers
can be created */
void (*callback)( /* Completion callback */
const mdns_query *query);
void *ptr; /* Callback's user data */
AvahiHostNameResolver **resolvers; /* Avahi host name resolver */
ll_node chain; /* In mdns_resolver::pending */
};
/* mdns_resolver_new creates a new MDNS resolver
*/
mdns_resolver*
mdns_resolver_new (int ifindex)
{
mdns_resolver *resolver = mem_new(mdns_resolver, 1);
resolver->ifindex = ifindex;
ll_init(&resolver->pending);
return resolver;
}
/* mdns_resolver_free frees the mdns_resolver previously created
* by mdns_resolver_new()
*/
void
mdns_resolver_free (mdns_resolver *resolver)
{
log_assert(mdns_log, ll_empty(&resolver->pending));
mem_free(resolver);
}
/* mdns_resolver_cancel cancels all pending queries
*/
void
mdns_resolver_cancel (mdns_resolver *resolver)
{
ll_node *node;
while ((node = ll_first(&resolver->pending)) != NULL) {
mdns_query *query = OUTER_STRUCT(node, mdns_query, chain);
mdns_query_cancel(query);
}
}
/* mdns_resolver_has_pending checks if resolver has pending queries
*/
bool
mdns_resolver_has_pending (mdns_resolver *resolver)
{
return !ll_empty(&resolver->pending);
}
/* mdns_query_free frees the mdns_query
*/
static void
mdns_query_free (mdns_query *query)
{
int i;
ll_del(&query->chain);
for (i = 0; query->resolvers[i] != NULL; i ++) {
avahi_host_name_resolver_free(query->resolvers[i]);
}
eloop_call_cancel(query->dummy_callid);
ip_addrset_free(query->answer);
mem_free(query->resolvers);
mem_free(query->name);
mem_free(query);
}
/* mdns_query_callback_exec executes completion callback, then
* frees the query
*/
static void
mdns_query_callback_exec (mdns_query *query)
{
char *s = ip_addrset_friendly_str(query->answer, NULL);
log_debug(mdns_log, "%s(%s): found %s", MDNS_ACTION_LOOKUP, query->name, s);
mem_free(s);
query->callback(query);
mdns_query_free(query);
}
/* AvahiHostNameResolver callback for mdns_query
*/
static void
mdns_query_callback (AvahiHostNameResolver *r, AvahiIfIndex interface,
AvahiProtocol protocol, AvahiResolverEvent event,
const char *name, const AvahiAddress *addr,
AvahiLookupResultFlags flags, void *userdata)
{
mdns_query *query = (mdns_query*) userdata;
char buf[256];
/* Print debug message */
switch (event) {
case AVAHI_RESOLVER_FOUND:
avahi_address_snprint(buf, sizeof(buf), addr);
mdns_debug(MDNS_ACTION_LOOKUP, interface, protocol, flags, NULL, name,
buf);
break;
case AVAHI_RESOLVER_FAILURE:
mdns_perror(MDNS_ACTION_LOOKUP, interface, protocol, NULL, name);
break;
default:
mdns_debug(MDNS_ACTION_LOOKUP, interface, protocol, flags, NULL, name,
mdns_avahi_resolver_event_name(event));
}
/* Remove resolver from list of pending ones */
if (!ptr_array_del(query->resolvers, ptr_array_find(query->resolvers, r))) {
mdns_debug(MDNS_ACTION_LOOKUP, interface, protocol, flags, NULL, name,
"spurious avahi callback");
return;
}
/* Handle event */
if (event == AVAHI_RESOLVER_FOUND) {
ip_addr ip = {
.ifindex = interface
};
bool ok = true;
switch (protocol) {
case AVAHI_PROTO_INET:
ip.af = AF_INET;
ip.ip.v4.s_addr = addr->data.ipv4.address;
break;
case AVAHI_PROTO_INET6:
ip.af = AF_INET6;
memcpy(&ip.ip.v6, addr->data.ipv6.address, 16);
break;
default:
/* Very unlikely to happen, but just in case...
*/
ok = false;
}
if (ok) {
ip_addrset_add(query->answer, ip);
}
}
/* Raise callback, if all is done */
if (mem_len(query->resolvers) == 0) {
mdns_query_callback_exec(query);
}
}
/* eloop_call() callback for mdns_resolver that was created
* without any active AvahiHostNameResolver
*
* The purpose of thus callback is to ensure that mdns_resolver
* completion callback will be executed in any case
*/
static void
mdns_query_dummy_callback (void *ptr)
{
mdns_query *query = (mdns_query*) ptr;
mdns_query_callback_exec(query);
}
/* mdns_query_sumbit submits a new MDNS query for the specified domain
* name. When resolving is done, successfully or not, callback will be
* called
*
* The ptr parameter is passed to the callback without any interpretation
* as a user-defined argument
*
* Answer is a set of discovered IP addresses. It is owned by resolver,
* callback should not free it and should not assume that it is still
* valid after return from callback
*/
mdns_query*
mdns_query_submit (mdns_resolver *resolver,
const char *name,
void (*callback)(const mdns_query *query),
void *ptr)
{
mdns_query *query = mem_new(mdns_query, 1);
static AvahiProtocol protos[] = {AVAHI_PROTO_INET, AVAHI_PROTO_INET6};
int i;
const char *name_fqdn = name;
/* Normalize name, if it is not FQDN */
if (!avahi_is_valid_fqdn(name_fqdn)) {
char *with_local;
size_t sz = strlen(name_fqdn);
with_local = alloca(strlen(name_fqdn) + 7);
memcpy(with_local, name_fqdn, sz);
strcpy(with_local + sz, ".local");
if (avahi_is_valid_fqdn(with_local)) {
name_fqdn = with_local;
}
}
/* Initialize a query structure */
query->name = str_dup(name);
query->resolver = resolver;
query->answer = ip_addrset_new();
query->dummy_callid = ELOOP_CALL_BADID;
query->callback = callback;
query->ptr = ptr;
query->resolvers = ptr_array_new(AvahiHostNameResolver*);
/* Create an AvahiHostNameResolver for each IPv4/v6 address family
*/
for (i = 0; i < 2; i ++) {
AvahiProtocol proto = protos[i];
AvahiHostNameResolver *r;
r = avahi_host_name_resolver_new(
mdns_avahi_client,
resolver->ifindex,
proto,
name_fqdn,
proto,
0,
mdns_query_callback,
query
);
if (r == NULL) {
mdns_perror(MDNS_ACTION_LOOKUP, resolver->ifindex, proto, NULL,
query->name);
} else {
query->resolvers = ptr_array_append(query->resolvers, r);
mdns_debug(MDNS_ACTION_LOOKUP, resolver->ifindex, proto, 0, NULL,
query->name, "started");
}
}
/* Make sure completion callback will be executed even
* if no resolvers were created
*/
if (mem_len(query->resolvers) == 0) {
query->dummy_callid = eloop_call(mdns_query_dummy_callback, query);
}
ll_push_end(&resolver->pending, &query->chain);
return query;
}
/* mdns_query_cancel cancels the pending query. mdns_query memory will
* be released and callback will not be called
*
* Note, mdns_query pointer is valid when obtained from mdns_query_sumbit
* and until canceled or return from callback.
*/
void
mdns_query_cancel (mdns_query *query)
{
mdns_query_free(query);
}
/* mdns_query_get_name returns domain name, as it was specified
* when query was submitted
*/
const char*
mdns_query_get_name (const mdns_query *query)
{
return query->name;
}
/* mdns_query_get_answer returns resolved addresses
*/
const ip_addrset*
mdns_query_get_answer (const mdns_query *query)
{
return query->answer;
}
/* mdns_query_set_ptr gets the user-defined ptr, associated
* with query when it was submitted
*/
void*
mdns_query_get_ptr (const mdns_query *query)
{
return query->ptr;
}
/***** Miscellaneous *****/
/* mdns_device_count_by_model returns count of distinct devices
* with model names matching the specified parent.
*
* Several instances of the same device (i.e. printer vs scanner) are
* counted only once per network interface.
*
* WSDD uses this function to decide when to use extended discovery
* time (some devices are known to be hard for WD-Discovery)
*
* Pattern is the glob-style expression, applied to the model name
* of discovered devices.
*/
unsigned int
mdns_device_count_by_model (int ifindex, const char *pattern)
{
mdns_finding **findings;
unsigned int findings_count = 0;
unsigned int i, answer;
ll_node *node;
/* Collect matching devices */
if (mdns_finding_list_count == 0) {
return 0;
}
findings = alloca(sizeof(*findings) * mdns_finding_list_count);
for (LL_FOR_EACH(node, &mdns_finding_list)) {
mdns_finding *mdns = OUTER_STRUCT(node, mdns_finding, node_list);
if (mdns->finding.ifindex == ifindex &&
mdns->finding.model != NULL &&
fnmatch(pattern, mdns->finding.model, 0) == 0) {
findings[findings_count] = mdns;
findings_count ++;
}
}
/* Sort by ifindex + name, then count only distinct devices */
qsort(findings, findings_count, sizeof(*findings),
zeroconf_finding_qsort_by_index_name);
if (findings_count <= 1) {
return findings_count;
}
answer = 1;
for (i = 1; i < findings_count; i ++) {
mdns_finding **p1 = &findings[i - 1];
mdns_finding **p2 = &findings[i];
if (zeroconf_finding_qsort_by_index_name(p1, p2) != 0) {
answer ++;
}
}
return answer;
}
/***** Initialization and cleanup *****/
/* Initialize MDNS
*/
SANE_Status
mdns_init (void)
{
int i;
mdns_log = log_ctx_new("MDNS", zeroconf_log);
ll_init(&mdns_finding_list);
if (!conf.discovery) {
log_debug(mdns_log, "devices discovery disabled");
zeroconf_finding_done(ZEROCONF_MDNS_HINT);
zeroconf_finding_done(ZEROCONF_USCAN_TCP);
zeroconf_finding_done(ZEROCONF_USCANS_TCP);
return SANE_STATUS_GOOD;
}
for (i = 0; i < NUM_MDNS_SERVICE; i ++) {
mdns_initscan[i] = true;
}
for (i = 0; i < NUM_ZEROCONF_METHOD; i ++) {
mdns_initscan_count[i] = 0;
}
mdns_avahi_poll = eloop_poll_get();
mdns_avahi_restart_timer =
mdns_avahi_poll->timeout_new(mdns_avahi_poll, NULL,
mdns_avahi_restart_timer_callback, NULL);
if (mdns_avahi_restart_timer == NULL) {
return SANE_STATUS_NO_MEM;
}
mdns_avahi_client_start();
if (mdns_avahi_client == NULL) {
return SANE_STATUS_NO_MEM;
}
return SANE_STATUS_GOOD;
}
/* Cleanup MDNS
*/
void
mdns_cleanup (void)
{
if (mdns_log == NULL) {
return; /* MDNS not initialized */
}
if (mdns_avahi_poll != NULL) {
mdns_avahi_browser_stop();
mdns_avahi_client_stop();
mdns_finding_del_all();
if (mdns_avahi_restart_timer != NULL) {
mdns_avahi_poll->timeout_free(mdns_avahi_restart_timer);
mdns_avahi_restart_timer = NULL;
}
mdns_avahi_poll = NULL;
}
log_ctx_free(mdns_log);
mdns_log = NULL;
}
/* vim:ts=8:sw=4:et
*/