Files
Alexander Pevzner 07ebd85003 Added SANE_AIRSCAN_DEVICE environment variable
This variable, if set, overrides all devices, manually configured
in the log files and disables auto discovery.

Examples:

    SANE_AIRSCAN_DEVICE="escl:Kyocera eSCL:http://192.168.1.102:9095/eSCL"
    SANE_AIRSCAN_DEVICE="wsd:Kyocera WSD:http://192.168.1.102:5358/WSDScanner"

Formal syntax:

    "PROTO:DEVICE NAME:URL"

    Where:

	- PROTO` is either `escl` or `wsd`.
	- DEVICE NAME will appear in the list of devices.
	- URL is the device URL, using `http:` or `https:` schemes.

The primary purpose of this variable is the automated testing
of the `sane-airscan` backend.
2025-04-29 12:16:41 +03:00

1492 lines
39 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
*
* ZeroConf (device discovery)
*/
#include "airscan.h"
#include <arpa/inet.h>
#include <net/if.h>
#include <fnmatch.h>
#include <stdlib.h>
#include <string.h>
/******************** Constants *********************/
/* Max time to wait until device table is ready, in milliseconds
*/
#define ZEROCONF_READY_TIMEOUT 5000
/******************** Local Types *********************/
/* zeroconf_device represents a single device
*/
struct zeroconf_device {
unsigned int devid; /* Unique ident */
uuid uuid; /* Device UUID */
ip_addrset *addrs; /* Device's addresses */
const char *mdns_name; /* Device's MDNS name, NULL for WSDD */
const char *model; /* Device model name */
unsigned int protocols; /* Supported protocols, set of 1<<ID_PROTO */
unsigned int methods; /* How device was discovered, set of
1 << ZEROCONF_METHOD */
ll_node node_list; /* In zeroconf_device_list */
ll_head findings; /* zeroconf_finding, by method */
zeroconf_device *buddy; /* "Buddy" device, MDNS vs WSDD */
};
/* Global variables
*/
log_ctx *zeroconf_log;
/* Static variables
*/
static ll_head zeroconf_device_list;
static pthread_cond_t zeroconf_initscan_cond;
static int zeroconf_initscan_bits;
static eloop_timer *zeroconf_initscan_timer;
/******************** Forward declarations *********************/
static zeroconf_endpoint*
zeroconf_endpoint_copy_single (const zeroconf_endpoint *endpoint);
static const char*
zeroconf_ident_split (const char *ident, unsigned int *devid, ID_PROTO *proto);
/******************** Discovery methods *********************/
/* Map ZEROCONF_METHOD to ID_PROTO
*/
static ID_PROTO
zeroconf_method_to_proto (ZEROCONF_METHOD method)
{
switch (method) {
case ZEROCONF_MDNS_HINT:
return ID_PROTO_UNKNOWN;
case ZEROCONF_USCAN_TCP:
case ZEROCONF_USCANS_TCP:
return ID_PROTO_ESCL;
case ZEROCONF_WSD:
return ID_PROTO_WSD;
case NUM_ZEROCONF_METHOD:
break;
}
return ID_PROTO_UNKNOWN;
}
/* Get ZEROCONF_METHOD, for debugging
*/
static const char*
zeroconf_method_name (ZEROCONF_METHOD method)
{
switch (method) {
case ZEROCONF_MDNS_HINT: return "ZEROCONF_MDNS_HINT";
case ZEROCONF_USCAN_TCP: return "ZEROCONF_USCAN_TCP";
case ZEROCONF_USCANS_TCP: return "ZEROCONF_USCANS_TCP";
case ZEROCONF_WSD: return "ZEROCONF_WSD";
case NUM_ZEROCONF_METHOD:
break;
}
return NULL;
}
/******************** Devices *********************/
/* Add new zeroconf_device
*/
static zeroconf_device*
zeroconf_device_add (zeroconf_finding *finding)
{
zeroconf_device *device = mem_new(zeroconf_device, 1);
device->devid = devid_alloc();
device->uuid = finding->uuid;
device->addrs = ip_addrset_new();
if (finding->name != NULL) {
device->mdns_name = str_dup(finding->name);
}
device->model = finding->model;
ll_init(&device->findings);
ll_push_end(&zeroconf_device_list, &device->node_list);
return device;
}
/* Delete the device
*/
static void
zeroconf_device_del (zeroconf_device *device)
{
ll_del(&device->node_list);
ip_addrset_free(device->addrs);
mem_free((char*) device->mdns_name);
devid_free(device->devid);
mem_free(device);
}
/* Check if device is MDNS device
*/
static bool
zeroconf_device_is_mdns (zeroconf_device *device)
{
return device->mdns_name != NULL;
}
/* Rebuild device->ifaces, device->protocols and device->methods
*/
static void
zeroconf_device_rebuild_sets (zeroconf_device *device)
{
ll_node *node;
device->protocols = 0;
device->methods = 0;
for (LL_FOR_EACH(node, &device->findings)) {
zeroconf_finding *finding;
ID_PROTO proto;
finding = OUTER_STRUCT(node, zeroconf_finding, list_node);
proto = zeroconf_method_to_proto(finding->method);
if (proto != ID_PROTO_UNKNOWN) {
device->protocols |= 1 << proto;
}
device->methods |= 1 << finding->method;
}
}
/* Update device->model
*/
static void
zeroconf_device_update_model (zeroconf_device *device)
{
ll_node *node;
zeroconf_finding *hint = NULL, *wsd = NULL;
for (LL_FOR_EACH(node, &device->findings)) {
zeroconf_finding *finding;
finding = OUTER_STRUCT(node, zeroconf_finding, list_node);
switch (finding->method) {
case ZEROCONF_USCAN_TCP:
case ZEROCONF_USCANS_TCP:
device->model = finding->model;
return;
case ZEROCONF_MDNS_HINT:
if (hint == NULL) {
hint = finding;
}
break;
case ZEROCONF_WSD:
if (wsd == NULL) {
wsd = finding;
}
break;
default:
log_internal_error(zeroconf_log);
}
}
device->model = hint ? hint->model : wsd->model;
}
/* Add zeroconf_finding to zeroconf_device
*/
static void
zeroconf_device_add_finding (zeroconf_device *device,
zeroconf_finding *finding)
{
log_assert(zeroconf_log, finding->device == NULL);
finding->device = device;
ll_push_end(&device->findings, &finding->list_node);
ip_addrset_merge(device->addrs, finding->addrs);
if (finding->endpoints != NULL) {
ID_PROTO proto = zeroconf_method_to_proto(finding->method);
if (proto != ID_PROTO_UNKNOWN) {
device->protocols |= 1 << proto;
}
device->methods |= 1 << finding->method;
}
zeroconf_device_update_model(device);
}
/* Delete zeroconf_finding from zeroconf_device
*/
static void
zeroconf_device_del_finding (zeroconf_finding *finding)
{
zeroconf_device *device = finding->device;
log_assert(zeroconf_log, device != NULL);
ll_del(&finding->list_node);
if (ll_empty(&device->findings)) {
zeroconf_device_del(device);
return;
}
zeroconf_device_rebuild_sets(device);
zeroconf_device_update_model(device);
}
/* Get model name
*/
static const char*
zeroconf_device_model (zeroconf_device *device)
{
if (device->model != NULL) {
return device->model;
}
/* If model name is not available, fall back to UUID */
return device->uuid.text;
}
/* Get device name
*/
static const char*
zeroconf_device_name (zeroconf_device *device)
{
if (zeroconf_device_is_mdns(device)) {
return device->mdns_name;
}
if (device->buddy != NULL) {
return device->buddy->mdns_name;
}
return zeroconf_device_model(device);
}
/* Get protocols, exposed by device
*/
static unsigned int
zeroconf_device_protocols (zeroconf_device *device)
{
unsigned int protocols = device->protocols;
if (!conf.proto_auto) {
return protocols;
}
if ((protocols & (1 << ID_PROTO_ESCL)) != 0) {
return 1 << ID_PROTO_ESCL;
}
if ((protocols & (1 << ID_PROTO_WSD)) != 0) {
return 1 << ID_PROTO_WSD;
}
return 0;
}
/* Get device endpoints.
* Caller is responsible to free the returned list
*/
static zeroconf_endpoint*
zeroconf_device_endpoints (zeroconf_device *device, ID_PROTO proto)
{
zeroconf_endpoint *endpoints = NULL;
ll_node *node;
for (LL_FOR_EACH(node, &device->findings)) {
zeroconf_finding *finding;
finding = OUTER_STRUCT(node, zeroconf_finding, list_node);
if (zeroconf_method_to_proto(finding->method) == proto) {
zeroconf_endpoint *ep, *ep2;
for (ep = finding->endpoints; ep != NULL; ep = ep->next) {
ep2 = zeroconf_endpoint_copy_single(ep);
ep2->next = endpoints;
endpoints = ep2;
}
}
}
return zeroconf_endpoint_list_sort_dedup(endpoints);
}
/* Find zeroconf_device by ident
* Protocol, encoded into ident, returned via second parameter
*/
static zeroconf_device*
zeroconf_device_find_by_ident (const char *ident, ID_PROTO *proto)
{
unsigned int devid;
const char *name;
ll_node *node;
zeroconf_device *device = NULL;
name = zeroconf_ident_split(ident, &devid, proto);
if (name == NULL) {
return NULL;
}
/* Lookup device */
for (LL_FOR_EACH(node, &zeroconf_device_list)) {
device = OUTER_STRUCT(node, zeroconf_device, node_list);
if (device->devid == devid &&
!strcmp(name, zeroconf_device_name(device))) {
break;
}
}
if (device == NULL)
return NULL;
/* Check that device supports requested protocol */
if ((device->protocols & (1 << *proto)) != 0) {
return device;
}
return NULL;
}
/* Check if device is blacklisted
*
* Returns reason string, if device is blacklisted, NULL, if not
*/
static const char*
zeroconf_device_is_blacklisted (zeroconf_device *device)
{
conf_blacklist *ent;
const char *name, *model;
if (conf.blacklist == NULL) {
return NULL;
}
name = zeroconf_device_name(device);
model = zeroconf_device_model(device);
for (ent = conf.blacklist; ent != NULL; ent = ent->next) {
if (ent->name != NULL && !fnmatch(ent->name, name, 0)) {
return "name";
}
if (ent->model != NULL && !fnmatch(ent->model, model, 0)) {
return "model";
}
if (ent->net.addr.af != AF_UNSPEC &&
ip_addrset_on_network(device->addrs, ent->net)) {
return "address";
}
}
return NULL;
}
/******************** Merging devices *********************/
/* Recompute device->buddy for all devices
*/
static void
zeroconf_merge_recompute_buddies (void)
{
ll_node *node, *node2;
zeroconf_device *device, *device2;
for (LL_FOR_EACH(node, &zeroconf_device_list)) {
device = OUTER_STRUCT(node, zeroconf_device, node_list);
device->buddy = NULL;
}
for (LL_FOR_EACH(node, &zeroconf_device_list)) {
device = OUTER_STRUCT(node, zeroconf_device, node_list);
for (node2 = ll_next(&zeroconf_device_list, node); node2 != NULL;
node2 = ll_next(&zeroconf_device_list, node2)) {
device2 = OUTER_STRUCT(node2, zeroconf_device, node_list);
if (zeroconf_device_is_mdns(device) !=
zeroconf_device_is_mdns(device2)) {
if (ip_addrset_is_intersect(device->addrs, device2->addrs)) {
device->buddy = device2;
device2->buddy = device;
}
}
}
}
}
/* Check that new finding should me merged with existent device
*/
static bool
zeroconf_merge_check (zeroconf_device *device, zeroconf_finding *finding)
{
if ((device->mdns_name == NULL) != (finding->name == NULL)) {
return false;
}
if (device->mdns_name != NULL &&
strcasecmp(device->mdns_name, finding->name)) {
return false;
}
if (uuid_equal(device->uuid, finding->uuid)) {
return true;
}
return false;
}
/* Find device, suitable for merging with specified finding
*/
static zeroconf_device*
zeroconf_merge_find (zeroconf_finding *finding)
{
ll_node *node;
for (LL_FOR_EACH(node, &zeroconf_device_list)) {
zeroconf_device *device;
device = OUTER_STRUCT(node, zeroconf_device, node_list);
if (zeroconf_merge_check(device, finding)) {
return device;
}
}
return NULL;
}
/******************** Ident Strings *********************/
/* Encode ID_PROTO for device ident
*/
static char
zeroconf_ident_proto_encode (ID_PROTO proto)
{
switch (proto) {
case ID_PROTO_ESCL: return 'e';
case ID_PROTO_WSD: return 'w';
case ID_PROTO_UNKNOWN:
case NUM_ID_PROTO:
break;
}
log_internal_error(zeroconf_log);
return 0;
}
/* Decode ID_PROTO from device ident
*/
static ID_PROTO
zeroconf_ident_proto_decode (char c)
{
switch (c) {
case 'e': return ID_PROTO_ESCL;
case 'w': return ID_PROTO_WSD;
}
return ID_PROTO_UNKNOWN;
}
/* Make device ident string
* The returned string must be released with mem_free()
*/
static const char*
zeroconf_ident_make (const char *name, unsigned int devid, ID_PROTO proto)
{
return str_printf("%c%x:%s", zeroconf_ident_proto_encode(proto),
devid, name);
}
/* Split device ident string.
* Returns NULL on error, device name on success.
* Device name points somewhere into the input buffer
*/
static const char*
zeroconf_ident_split (const char *ident, unsigned int *devid, ID_PROTO *proto)
{
const char *name;
char *end;
/* Find name */
name = strchr(ident, ':');
if (name == NULL) {
return NULL;
}
name ++;
/* Decode proto and devid */
*proto = zeroconf_ident_proto_decode(*ident);
if (*proto == ID_PROTO_UNKNOWN) {
return NULL;
}
ident ++;
*devid = (unsigned int) strtoul(ident, &end, 16);
if (end == ident || *end != ':') {
return NULL;
}
return name;
}
/******************** Endpoints *********************/
/* Create new zeroconf_endpoint. Newly created endpoint
* takes ownership of uri string
*/
zeroconf_endpoint*
zeroconf_endpoint_new (ID_PROTO proto, http_uri *uri)
{
zeroconf_endpoint *endpoint = mem_new(zeroconf_endpoint, 1);
endpoint->proto = proto;
endpoint->uri = uri;
if (proto == ID_PROTO_ESCL) {
// We own the uri, so modify without making a separate copy.
http_uri_fix_end_slash(endpoint->uri);
}
return endpoint;
}
/* Clone a single zeroconf_endpoint
*/
static zeroconf_endpoint*
zeroconf_endpoint_copy_single (const zeroconf_endpoint *endpoint)
{
zeroconf_endpoint *endpoint2 = mem_new(zeroconf_endpoint, 1);
*endpoint2 = *endpoint;
endpoint2->uri = http_uri_clone(endpoint->uri);
endpoint2->next = NULL;
return endpoint2;
}
/* Free single zeroconf_endpoint
*/
void
zeroconf_endpoint_free_single (zeroconf_endpoint *endpoint)
{
http_uri_free(endpoint->uri);
mem_free(endpoint);
}
/* Create a copy of zeroconf_endpoint list
*/
zeroconf_endpoint*
zeroconf_endpoint_list_copy (const zeroconf_endpoint *list)
{
zeroconf_endpoint *newlist = NULL, *last = NULL, *endpoint;
while (list != NULL) {
endpoint = zeroconf_endpoint_copy_single(list);
if (last != NULL) {
last->next = endpoint;
} else {
newlist = endpoint;
}
last = endpoint;
list = list->next;
}
return newlist;
}
/* Free zeroconf_endpoint list
*/
void
zeroconf_endpoint_list_free (zeroconf_endpoint *list)
{
while (list != NULL) {
zeroconf_endpoint *next = list->next;
zeroconf_endpoint_free_single(list);
list = next;
}
}
/* Compare two endpoints, for sorting
*/
static int
zeroconf_endpoint_cmp (const zeroconf_endpoint *e1, const zeroconf_endpoint *e2)
{
const struct sockaddr *a1 = http_uri_addr(e1->uri);
const struct sockaddr *a2 = http_uri_addr(e2->uri);
if (a1 != NULL && a2 != NULL) {
bool ll1 = ip_sockaddr_is_linklocal(a1);
bool ll2 = ip_sockaddr_is_linklocal(a2);
int cmp;
/* Prefer directly reachable addresses */
cmp = netif_distance_cmp(a1, a2);
if (cmp != 0) {
return cmp;
}
/* Prefer normal addresses, rather that link-local */
if (ll1 != ll2) {
return ll1 ? 1 : -1;
}
/* Be in trend: prefer IPv6 addresses */
if (a1->sa_family != a2->sa_family) {
return a1->sa_family == AF_INET6 ? -1 : 1;
}
}
/* Otherwise, sort lexicographically */
return strcmp(http_uri_str(e1->uri), http_uri_str(e2->uri));
}
/* Revert zeroconf_endpoint list
*/
static zeroconf_endpoint*
zeroconf_endpoint_list_revert (zeroconf_endpoint *list)
{
zeroconf_endpoint *prev = NULL, *next;
while (list != NULL) {
next = list->next;
list->next = prev;
prev = list;
list = next;
}
return prev;
}
/* Sort list of endpoints
*/
zeroconf_endpoint*
zeroconf_endpoint_list_sort (zeroconf_endpoint *list)
{
zeroconf_endpoint *halves[2] = {NULL, NULL};
int half = 0;
if (list == NULL || list->next == NULL) {
return list;
}
/* Split list into halves */
while (list != NULL) {
zeroconf_endpoint *next = list->next;
list->next = halves[half];
halves[half] = list;
half ^= 1;
list = next;
}
/* Sort each half, recursively */
for (half = 0; half < 2; half ++) {
halves[half] = zeroconf_endpoint_list_sort(halves[half]);
}
/* Now merge the sorted halves */
list = NULL;
while (halves[0] != NULL || halves[1] != NULL) {
zeroconf_endpoint *next;
if (halves[0] == NULL) {
half = 1;
} else if (halves[1] == NULL) {
half = 0;
} else if (zeroconf_endpoint_cmp(halves[0], halves[1]) < 0) {
half = 0;
} else {
half = 1;
}
next = halves[half]->next;
halves[half]->next = list;
list = halves[half];
halves[half] = next;
}
/* And revert the list, as after merging it is reverted */
return zeroconf_endpoint_list_revert(list);
}
/* Sort list of endpoints and remove duplicates
*/
zeroconf_endpoint*
zeroconf_endpoint_list_sort_dedup (zeroconf_endpoint *list)
{
zeroconf_endpoint *addr, *next;
if (list == NULL) {
return NULL;
}
list = zeroconf_endpoint_list_sort(list);
addr = list;
while ((next = addr->next) != NULL) {
if (zeroconf_endpoint_cmp(addr, next) == 0) {
addr->next = next->next;
zeroconf_endpoint_free_single(next);
} else {
addr = next;
}
}
return list;
}
/* Check if list of endpoints already contains the given
* endpoint (i.e., endpoint with the same URI and protocol)
*/
bool
zeroconf_endpoint_list_contains (const zeroconf_endpoint *list,
const zeroconf_endpoint *endpoint)
{
while (list != NULL) {
if (list->proto == endpoint->proto &&
http_uri_equal(list->uri, endpoint->uri)) {
return true;
}
list = list->next;
}
return false;
}
/* Check if endpoints list contains a non-link-local address
* of the specified address family
*/
bool
zeroconf_endpoint_list_has_non_link_local_addr (int af,
const zeroconf_endpoint *list)
{
for (;list != NULL; list = list->next) {
const struct sockaddr *addr = http_uri_addr(list->uri);
if (addr != NULL && addr->sa_family == af) {
if (!ip_sockaddr_is_linklocal(addr)) {
return true;
}
}
}
return false;
}
/******************** Static configuration *********************/
/* Look for device's static configuration by device name
*/
static conf_device*
zeroconf_find_static_by_name (const char *name)
{
conf_device *dev_conf;
for (dev_conf = conf.devices; dev_conf != NULL; dev_conf = dev_conf->next) {
if (!strcasecmp(dev_conf->name, name)) {
return dev_conf;
}
}
return NULL;
}
/* Look for device's static configuration by device ident
*/
static conf_device*
zeroconf_find_static_by_ident (const char *ident)
{
conf_device *dev_conf;
ID_PROTO proto;
unsigned int devid;
const char *name;
name = zeroconf_ident_split(ident, &devid, &proto);
if (name == NULL) {
return NULL;
}
for (dev_conf = conf.devices; dev_conf != NULL; dev_conf = dev_conf->next) {
if (dev_conf->devid == devid &&
dev_conf->proto == proto &&
!strcmp(dev_conf->name, name)) {
return dev_conf;
}
}
return NULL;
}
/***** Miscellaneous functions for zeroconf_finding *****/
/* Compare two pointers to pointers to zeroconf_finding (zeroconf_finding**)
* by index+name, for qsort
*/
int
zeroconf_finding_qsort_by_index_name (const void *p1, const void *p2)
{
const zeroconf_finding *f1 = *(const zeroconf_finding * const *) p1;
const zeroconf_finding *f2 = *(const zeroconf_finding * const *) p2;
if (f1->ifindex < f2->ifindex) {
return -1;
}
if (f1->ifindex > f2->ifindex) {
return 1;
}
return strcmp(f1->name, f2->name);
}
/******************** Events from discovery providers *********************/
/* Publish the zeroconf_finding.
*/
void
zeroconf_finding_publish (zeroconf_finding *finding)
{
size_t count, i;
zeroconf_device *device;
char ifname[IF_NAMESIZE];
const ip_addr *addrs;
ID_PROTO proto = zeroconf_method_to_proto(finding->method);
/* Print log messages */
if (if_indextoname(finding->ifindex, ifname) == NULL) {
strcpy(ifname, "?");
}
log_debug(zeroconf_log, "found %s", finding->uuid.text);
log_debug(zeroconf_log, " method: %s",
zeroconf_method_name(finding->method));
log_debug(zeroconf_log, " interface: %d (%s)", finding->ifindex, ifname);
log_debug(zeroconf_log, " name: %s",
finding->name ? finding->name : "-");
log_debug(zeroconf_log, " model: %s",
finding->model ? finding->model : "-");
log_debug(zeroconf_log, " addresses:");
addrs = ip_addrset_addresses(finding->addrs, &count);
for (i = 0; i < count; i ++) {
ip_straddr straddr = ip_addr_to_straddr(addrs[i], true);
log_debug(zeroconf_log, " %s", straddr.text);
}
if (proto != ID_PROTO_UNKNOWN) {
zeroconf_endpoint *ep;
log_debug(zeroconf_log, " protocol: %s", id_proto_name(proto));
log_debug(zeroconf_log, " endpoints:");
for (ep = finding->endpoints; ep != NULL; ep = ep->next) {
log_debug(zeroconf_log, " %s", http_uri_str(ep->uri));
}
}
/* Handle new finding */
device = zeroconf_merge_find(finding);
if (device != NULL) {
log_debug(zeroconf_log, " device: %4.4x (found)", device->devid);
} else {
device = zeroconf_device_add(finding);
log_debug(zeroconf_log, " device: %4.4x (created)", device->devid);
}
zeroconf_device_add_finding(device, finding);
zeroconf_merge_recompute_buddies();
pthread_cond_broadcast(&zeroconf_initscan_cond);
}
/* Withdraw the finding
*/
void
zeroconf_finding_withdraw (zeroconf_finding *finding)
{
char ifname[IF_NAMESIZE] = "?";
if_indextoname(finding->ifindex, ifname);
log_debug(zeroconf_log, "device gone %s", finding->uuid.text);
log_debug(zeroconf_log, " method: %s", zeroconf_method_name(finding->method));
log_debug(zeroconf_log, " interface: %d (%s)", finding->ifindex, ifname);
zeroconf_device_del_finding(finding);
zeroconf_merge_recompute_buddies();
pthread_cond_broadcast(&zeroconf_initscan_cond);
}
/* Notify zeroconf subsystem that initial scan
* for the method is done
*/
void
zeroconf_finding_done (ZEROCONF_METHOD method)
{
log_debug(zeroconf_log, "%s: initial scan finished",
zeroconf_method_name(method));
zeroconf_initscan_bits &= ~(1 << method);
pthread_cond_broadcast(&zeroconf_initscan_cond);
}
/******************** Support for SANE API *********************/
/* zeroconf_initscan_timer callback
*/
static void
zeroconf_initscan_timer_callback (void *unused)
{
(void) unused;
log_debug(zeroconf_log, "initial scan timer expired");
mdns_initscan_timer_expired();
wsdd_initscan_timer_expired();
zeroconf_initscan_timer = NULL;
pthread_cond_broadcast(&zeroconf_initscan_cond);
}
/* Check if initial scan is done
*/
static bool
zeroconf_initscan_done (void)
{
ll_node *node;
zeroconf_device *device;
/* If all discovery methods are done, we are done */
if (zeroconf_initscan_bits == 0) {
return true;
}
/* Regardless of options, all DNS-SD methods must be done */
if ((zeroconf_initscan_bits & ~(1 << ZEROCONF_WSD)) != 0) {
log_debug(zeroconf_log, "device_list wait: DNS-SD not finished...");
return false;
}
/* If we are here, ZEROCONF_WSD is not done yet,
* and if we are not in fast-wsdd mode, we must wait
*/
log_assert(zeroconf_log,
(zeroconf_initscan_bits & (1 << ZEROCONF_WSD)) != 0);
if (conf.wsdd_mode != WSDD_FAST) {
log_debug(zeroconf_log, "device_list wait: WSDD not finished...");
return false;
}
/* Check for completion, device by device:
*
* In manual protocol switch mode, WSDD buddy must be
* found for device, so we have a choice. Otherwise, it's
* enough if device has supported protocols
*/
for (LL_FOR_EACH(node, &zeroconf_device_list)) {
device = OUTER_STRUCT(node, zeroconf_device, node_list);
if (!conf.proto_auto) {
if (zeroconf_device_is_mdns(device) && device->buddy == NULL) {
log_debug(zeroconf_log,
"device_list wait: waiting for WSDD buddy for '%s' (%d)",
zeroconf_device_name(device), device->devid);
return false;
}
} else {
if (device->protocols == 0) {
log_debug(zeroconf_log,
"device_list wait: waiting for any proto for '%s' (%d)",
zeroconf_device_name(device), device->devid);
return false;
}
}
}
return true;
}
/* Wait until initial scan is done
*/
static void
zeroconf_initscan_wait (void)
{
bool ok = false;
log_debug(zeroconf_log, "device_list wait: requested");
for (;;) {
ok = zeroconf_initscan_done();
if (ok || zeroconf_initscan_timer == NULL) {
break;
}
eloop_cond_wait(&zeroconf_initscan_cond);
}
log_debug(zeroconf_log, "device_list wait: %s", ok ? "OK" : "timeout" );
}
/* Compare SANE_Device*, for qsort
*/
static int
zeroconf_device_list_qsort_cmp (const void *p1, const void *p2)
{
int cmp;
const SANE_Device *d1 = *(SANE_Device**) p1;
const SANE_Device *d2 = *(SANE_Device**) p2;
cmp = strcasecmp(d1->model, d2->model);
if (cmp == 0) {
cmp = strcasecmp(d1->vendor, d2->vendor);
}
if (cmp == 0) {
cmp = strcmp(d1->name, d2->name);
}
return cmp;
}
/* Format list of protocols, for zeroconf_device_list_log
*/
static void
zeroconf_device_list_fmt_protocols (char *buf, size_t buflen, unsigned int protocols)
{
ID_PROTO proto;
size_t off = 0;
buf[0] = '\0';
for (proto = 0; proto < NUM_ID_PROTO; proto ++) {
if ((protocols & (1 << proto)) != 0) {
off += snprintf(buf + off, buflen - off, " %s",
id_proto_name(proto));
}
}
if (buf[0] == '\0') {
strcpy(buf, " none");
}
}
/* Log device information in a context of zeroconf_device_list_get
*/
static void
zeroconf_device_list_log (zeroconf_device *device, const char *name,
unsigned int protocols)
{
char can[64];
char use[64];
zeroconf_device_list_fmt_protocols(can, sizeof(can), device->protocols);
zeroconf_device_list_fmt_protocols(use, sizeof(use), protocols);
log_debug(zeroconf_log, "%s (%d): can:%s, use:%s", name, device->devid,
can, use);
}
/* Get list of devices, in SANE format
*/
const SANE_Device**
zeroconf_device_list_get (void)
{
size_t dev_count = 0, dev_count_static = 0;
conf_device *dev_conf;
const SANE_Device **dev_list = sane_device_array_new();
ll_node *node;
int i;
log_debug(zeroconf_log, "zeroconf_device_list_get: requested");
/* Wait until device table is ready */
zeroconf_initscan_wait();
/* Build list of devices */
log_debug(zeroconf_log, "zeroconf_device_list_get: building list of devices");
dev_count = 0;
for (dev_conf = conf.devices; dev_conf != NULL; dev_conf = dev_conf->next) {
SANE_Device *info;
const char *proto;
const char *host;
size_t hostlen;
if (dev_conf->uri == NULL) {
continue;
}
info = mem_new(SANE_Device, 1);
proto = id_proto_name(dev_conf->proto);
dev_list = sane_device_array_append(dev_list, info);
dev_count ++;
info->name = zeroconf_ident_make(dev_conf->name, dev_conf->devid,
dev_conf->proto);
info->vendor = str_dup(proto);
info->model = str_dup(dev_conf->name);
host = http_uri_get_host(dev_conf->uri);
hostlen = strlen(host);
if (host[0] == '[') {
host ++;
hostlen -= 2;
}
info->type = str_printf("ip=%.*s", (int) hostlen, host);
}
dev_count_static = dev_count;
for (LL_FOR_EACH(node, &zeroconf_device_list)) {
zeroconf_device *device;
ID_PROTO proto;
const char *name, *model, *blacklisted;
unsigned int protocols;
device = OUTER_STRUCT(node, zeroconf_device, node_list);
name = zeroconf_device_name(device);
model = zeroconf_device_model(device);
protocols = zeroconf_device_protocols(device);
zeroconf_device_list_log(device, name, protocols);
if (zeroconf_find_static_by_name(name) != NULL) {
/* Static configuration overrides discovery */
log_debug(zeroconf_log,
"%s (%d): skipping, device clashes statically configured",
name, device->devid);
continue;
}
blacklisted = zeroconf_device_is_blacklisted(device);
if (blacklisted != NULL) {
log_debug(zeroconf_log,
"%s (%d): skipping, device is blacklisted by %s",
name, device->devid, blacklisted);
continue;
}
if (conf.proto_auto && !zeroconf_device_is_mdns(device)) {
zeroconf_device *device2 = device->buddy;
if (device2 != NULL && zeroconf_device_protocols(device2) != 0) {
log_debug(zeroconf_log,
"%s (%d): skipping, shadowed by %s (%d)",
name, device->devid,
zeroconf_device_name(device2), device2->devid);
continue;
}
}
if (protocols == 0) {
log_debug(zeroconf_log,
"%s (%d): skipping, none of supported protocols discovered",
name, device->devid);
continue;
}
for (proto = 0; proto < NUM_ID_PROTO; proto ++) {
if ((protocols & (1 << proto)) != 0) {
SANE_Device *info = mem_new(SANE_Device, 1);
const char *proto_name = id_proto_name(proto);
char *type;
dev_list = sane_device_array_append(dev_list, info);
dev_count ++;
info->name = zeroconf_ident_make(name, device->devid, proto);
info->vendor = str_dup(proto_name);
info->model = str_dup(conf.model_is_netname ? name : model);
//info->type = str_printf("%s network scanner", proto_name);
type = str_printf("ip=", proto_name);
type = ip_addrset_friendly_str(device->addrs, type);
info->type = type;
}
}
}
qsort(dev_list + dev_count_static, dev_count - dev_count_static,
sizeof(*dev_list), zeroconf_device_list_qsort_cmp);
log_debug(zeroconf_log, "zeroconf_device_list_get: resulting list:");
for (i = 0; dev_list[i] != NULL; i ++) {
log_debug(zeroconf_log,
" %-4s \"%s\"", dev_list[i]->vendor, dev_list[i]->name);
}
return dev_list;
}
/* Free list of devices, returned by zeroconf_device_list_get()
*/
void
zeroconf_device_list_free (const SANE_Device **dev_list)
{
if (dev_list != NULL) {
unsigned int i;
const SANE_Device *info;
for (i = 0; (info = dev_list[i]) != NULL; i ++) {
mem_free((void*) info->name);
mem_free((void*) info->vendor);
mem_free((void*) info->model);
mem_free((void*) info->type);
mem_free((void*) info);
}
sane_device_array_free(dev_list);
}
}
/*
* The format "protocol:name:url" is accepted to directly specify a device
* without listing it in the config or finding it with autodiscovery. Try
* to parse an identifier as that format. On success, returns a newly allocated
* zeroconf_devinfo that the caller must free with zeroconf_devinfo_free(). On
* failure, returns NULL.
*/
zeroconf_devinfo*
zeroconf_parse_devinfo_from_ident(const char *ident)
{
int buf_size;
char *buf = NULL;
ID_PROTO proto;
char *name;
char *uri_str;
http_uri *uri;
zeroconf_devinfo *devinfo;
if (ident == NULL) {
return NULL;
}
/* Copy the string so we can modify it in place while parsing. */
buf_size = strlen(ident) + 1;
buf = alloca(buf_size);
memcpy(buf, ident, buf_size);
name = strchr(buf, ':');
if (name == NULL) {
return NULL;
}
*name = '\0';
name++;
proto = id_proto_by_name(buf);
if (proto == ID_PROTO_UNKNOWN) {
return NULL;
}
uri_str = strchr(name, ':');
if (uri_str == NULL) {
return NULL;
}
*uri_str = '\0';
uri_str++;
if (*name == '\0') {
return NULL;
}
uri = http_uri_new(uri_str, true);
if (uri == NULL) {
return NULL;
}
/* Build a zeroconf_devinfo */
devinfo = mem_new(zeroconf_devinfo, 1);
devinfo->ident = str_dup(ident);
devinfo->name = str_dup(name);
devinfo->model = str_dup("");
devinfo->endpoints = zeroconf_endpoint_new(proto, uri);
return devinfo;
}
/* Lookup device by ident (ident is reported as SANE_Device::name)
* by zeroconf_device_list_get())
*
* Caller becomes owner of resources (name and list of endpoints),
* referred by the returned zeroconf_devinfo
*
* Caller must free these resources, using zeroconf_devinfo_free()
*/
zeroconf_devinfo*
zeroconf_devinfo_lookup (const char *ident)
{
conf_device *dev_conf = NULL;
zeroconf_device *device = NULL;
zeroconf_devinfo *devinfo;
ID_PROTO proto = ID_PROTO_UNKNOWN;
/* Check if the caller passed a direct device specification first. */
devinfo = zeroconf_parse_devinfo_from_ident(ident);
if (devinfo != NULL) {
return devinfo;
}
/* Wait until device table is ready */
zeroconf_initscan_wait();
/* Lookup a device, static first */
dev_conf = zeroconf_find_static_by_ident(ident);
if (dev_conf == NULL) {
device = zeroconf_device_find_by_ident(ident, &proto);
if (device == NULL) {
return NULL;
}
}
/* Build a zeroconf_devinfo */
devinfo = mem_new(zeroconf_devinfo, 1);
devinfo->ident = str_dup(ident);
if (dev_conf != NULL) {
http_uri *uri = http_uri_clone(dev_conf->uri);
devinfo->name = str_dup(dev_conf->name);
devinfo->model = str_dup("");
devinfo->endpoints = zeroconf_endpoint_new(dev_conf->proto, uri);
} else {
devinfo->name = str_dup(zeroconf_device_name(device));
devinfo->model = str_dup(device->model ? device->model : "");
devinfo->endpoints = zeroconf_device_endpoints(device, proto);
}
return devinfo;
}
/* Free zeroconf_devinfo, returned by zeroconf_devinfo_lookup()
*/
void
zeroconf_devinfo_free (zeroconf_devinfo *devinfo)
{
mem_free((char*) devinfo->ident);
mem_free((char*) devinfo->name);
mem_free((char*) devinfo->model);
zeroconf_endpoint_list_free(devinfo->endpoints);
mem_free(devinfo);
}
/******************** Initialization and cleanup *********************/
/* ZeroConf start/stop callback
*/
static void
zeroconf_start_stop_callback (bool start)
{
if (start) {
zeroconf_initscan_timer = eloop_timer_new(ZEROCONF_READY_TIMEOUT,
zeroconf_initscan_timer_callback, NULL);
} else {
if (zeroconf_initscan_timer != NULL) {
eloop_timer_cancel(zeroconf_initscan_timer);
zeroconf_initscan_timer = NULL;
}
pthread_cond_broadcast(&zeroconf_initscan_cond);
}
}
/* Initialize ZeroConf
*/
SANE_Status
zeroconf_init (void)
{
char *s;
conf_device *dev;
/* Initialize zeroconf */
zeroconf_log = log_ctx_new("zeroconf", NULL);
ll_init(&zeroconf_device_list);
pthread_cond_init(&zeroconf_initscan_cond, NULL);
if (conf.discovery) {
zeroconf_initscan_bits = (1 << ZEROCONF_MDNS_HINT) |
(1 << ZEROCONF_USCAN_TCP) |
(1 << ZEROCONF_USCANS_TCP) |
(1 << ZEROCONF_WSD);
}
eloop_add_start_stop_callback(zeroconf_start_stop_callback);
/* Dump zeroconf configuration to the log */
log_trace(zeroconf_log, "zeroconf configuration:");
s = conf.discovery ? "enable" : "disable";
log_trace(zeroconf_log, " discovery = %s", s);
s = conf.model_is_netname ? "network" : "hardware";
log_trace(zeroconf_log, " model = %s", s);
s = conf.proto_auto ? "auto" : "manual";
log_trace(zeroconf_log, " protocol = %s", s);
s = "?";
(void) s; /* Silence CLANG analyzer warning */
switch (conf.wsdd_mode) {
case WSDD_FAST: s = "fast"; break;
case WSDD_FULL: s = "full"; break;
case WSDD_OFF: s = "OFF"; break;
}
log_trace(zeroconf_log, " ws-discovery = %s", s);
if (conf.devices != NULL) {
log_trace(zeroconf_log, "statically configured devices:");
for (dev = conf.devices; dev != NULL; dev = dev->next) {
if (dev->uri != NULL) {
log_trace(zeroconf_log, " %s = %s, %s", dev->name,
http_uri_str(dev->uri), id_proto_name(dev->proto));
} else {
log_trace(zeroconf_log, " %s = disable", dev->name);
}
}
}
if (conf.blacklist != NULL) {
conf_blacklist *ent;
log_trace(zeroconf_log, "blacklist:");
for (ent = conf.blacklist; ent != NULL; ent = ent->next) {
if (ent->model != NULL) {
log_trace(zeroconf_log, " model = %s", ent->model);
}
if (ent->name != NULL) {
log_trace(zeroconf_log, " name = %s", ent->name);
}
if (ent->net.addr.af != AF_UNSPEC) {
ip_straddr straddr = ip_network_to_straddr(ent->net);
log_trace(zeroconf_log, " ip = %s", straddr.text);
}
}
}
return SANE_STATUS_GOOD;
}
/* Cleanup ZeroConf
*/
void
zeroconf_cleanup (void)
{
if (zeroconf_log != NULL) {
log_ctx_free(zeroconf_log);
zeroconf_log = NULL;
pthread_cond_destroy(&zeroconf_initscan_cond);
}
}
/* vim:ts=8:sw=4:et
*/