diff mbox series

[bug#44800,v2,1/3] Add Avahi support.

Message ID 20201124132145.217751-2-othacehe@gnu.org
State Accepted
Headers show
Series publish: Add Avahi support. | expand

Checks

Context Check Description
cbaines/comparison success View comparision
cbaines/git branch success View Git branch
cbaines/applying patch success View Laminar job
cbaines/issue success View issue
cbaines/comparison success View comparision
cbaines/git branch success View Git branch
cbaines/applying patch success View Laminar job
cbaines/issue success View issue

Commit Message

Mathieu Othacehe Nov. 24, 2020, 1:21 p.m. UTC
* guix/avahi.scm: New file.
* Makefile.am (MODULES): Add it.
* configure.ac: Add Guile-Avahi dependency.
* doc/guix.texi (Requirements): Document it.
* gnu/packages/package-management.scm (guix)[native-inputs]: Add
"guile-avahi",
[propagated-inputs]: ditto.
* guix/self.scm (specification->package): Add guile-avahi.
(compiled-guix): Ditto.
---
 Makefile.am                         |   1 +
 configure.ac                        |   6 +
 doc/guix.texi                       |   1 +
 gnu/packages/package-management.scm |   5 +-
 guix/avahi.scm                      | 170 ++++++++++++++++++++++++++++
 guix/self.scm                       |   9 +-
 6 files changed, 189 insertions(+), 3 deletions(-)
 create mode 100644 guix/avahi.scm

Comments

Ludovic Courtès Nov. 27, 2020, 5:04 p.m. UTC | #1
Mathieu Othacehe <othacehe@gnu.org> skribis:

> * guix/avahi.scm: New file.
> * Makefile.am (MODULES): Add it.
> * configure.ac: Add Guile-Avahi dependency.
> * doc/guix.texi (Requirements): Document it.
> * gnu/packages/package-management.scm (guix)[native-inputs]: Add
> "guile-avahi",
> [propagated-inputs]: ditto.
> * guix/self.scm (specification->package): Add guile-avahi.
> (compiled-guix): Ditto.

[...]

> --- a/configure.ac
> +++ b/configure.ac
> @@ -161,6 +161,12 @@ if test "x$have_guile_lzlib" != "xyes"; then
>    AC_MSG_ERROR([Guile-lzlib is missing; please install it.])
>  fi
>  
> +dnl Check for Guile-Avahi.
> +GUILE_MODULE_AVAILABLE([have_guile_avahi], [(avahi)])
> +if test "x$have_guile_avahi" != "xyes"; then
> +  AC_MSG_ERROR([Guile-Avahi is missing; please install it.])
> +fi

I wonder if we could/should make it an optional dependency.

(guix avahi) would need to autoload (avahi), which might be slightly
annoying.

An argument in favor of making it mandatory is that it would help make
the feature more widely used, and thus more widely useful.

> +(define-record-type* <avahi-service>
> +  avahi-service make-avahi-service
> +  avahi-service?
> +  (name avahi-service-name)
> +  (type avahi-service-type)
> +  (interface avahi-service-interface)
> +  (local-address avahi-service-local-address)
> +  (address avahi-service-address)
> +  (port avahi-service-port)
> +  (txt avahi-service-txt))

You could use (srfi srfi-9) ‘define-record-type’ since the extra (guix
records) features are not necessary here.

> +(define* (avahi-publish-service-thread name
> +                                       #:key
> +                                       type port
> +                                       (stop-loop? (const #f))
> +                                       (timeout 100)
> +                                       (txt '()))
> +  "Publish the service TYPE using Avahi, for the given PORT, on all interfaces
> +and for all protocols. Also, advertise the given TXT record list.
> +
> +This procedure starts a new thread running the Avahi event loop.  It exits
> +when STOP-LOOP? procedure returns true."
> +  (define client-callback
> +    (lambda (client state)
> +      (when (eq? state client-state/s-running)
> +        (let ((group (make-entry-group client (const #t))))
> +          (apply
> +           add-entry-group-service! group interface/unspecified
> +           protocol/unspecified '()
> +           name type #f #f port txt)
> +          (commit-entry-group group)))))
> +
> +  (call-with-new-thread
> +   (lambda ()
> +     (let* ((poll (make-simple-poll))
> +            (client (make-client (simple-poll poll)
> +                                 (list
> +                                  client-flag/ignore-user-config)
> +                                 client-callback)))
> +       (while (not (stop-loop?))
> +         (iterate-simple-poll poll timeout))))))

(I wanted to add an API in Guile-Avahi to “invert inversion of control”
so that one could escape callback hell but never got around to
completing it.)

> +(define (interface->ip-address interface)
> +  "Return the local IP address of the given INTERFACE."
> +  (let ((address
> +         (network-interface-address
> +          (socket AF_INET SOCK_STREAM 0) interface)))
> +    (inet-ntop (sockaddr:fam address) (sockaddr:addr address))))

Make sure to close the socket.

Can’t we obtain the IP address without creating a socket actually?  Noob
here.

> +    ;; Handle service resolution events.
> +    (cond ((eq? event resolver-event/found)
> +           (info (G_ "resolved service `~a' at `~a:~a'~%")
> +                 service-name (inet-ntop family address) port)

IWBN to not add UI code in here.

Thanks,
Ludo’.
Simon Tournier Nov. 27, 2020, 5:09 p.m. UTC | #2
Hi Ludo,

On Fri, 27 Nov 2020 at 18:04, Ludovic Courtès <ludo@gnu.org> wrote:

> (I wanted to add an API in Guile-Avahi to “invert inversion of control”
> so that one could escape callback hell but never got around to
> completing it.)

Out of curiosity, what do you mean by “invert inversion of control”?


Cheers,
simon
Ludovic Courtès Nov. 28, 2020, 11:02 a.m. UTC | #3
Hi,

zimoun <zimon.toutoune@gmail.com> skribis:

> On Fri, 27 Nov 2020 at 18:04, Ludovic Courtès <ludo@gnu.org> wrote:
>
>> (I wanted to add an API in Guile-Avahi to “invert inversion of control”
>> so that one could escape callback hell but never got around to
>> completing it.)
>
> Out of curiosity, what do you mean by “invert inversion of control”?

Users of the Avahi client library are supposed to pass “callbacks”,
which leads to “inversion of control”: the library decides when you code
is called.  See <https://en.wikipedia.org/wiki/Inversion_of_control>.

Inverting it would mean providing a natural way for users to call the
library.

HTH!

Ludo’.
Simon Tournier Nov. 28, 2020, 6:59 p.m. UTC | #4
Hi,

On Sat, 28 Nov 2020 at 12:02, Ludovic Courtès <ludo@gnu.org> wrote:

> Users of the Avahi client library are supposed to pass “callbacks”,
> which leads to “inversion of control”: the library decides when you code
> is called.  See <https://en.wikipedia.org/wiki/Inversion_of_control>.
>
> Inverting it would mean providing a natural way for users to call the
> library.

Thanks!  I did not know the concept name and that it was so formalized.

Really helpful.


Cheers,
simon
Mathieu Othacehe Nov. 29, 2020, 2:18 p.m. UTC | #5
Hey Ludo,

Thanks for the review :)

> An argument in favor of making it mandatory is that it would help make
> the feature more widely used, and thus more widely useful.

Yes, and most of the other Guile dependencies are mandatory, so having
Guile-Avahi also mandatory feels safer. Plus I'm sure that it can be
useful for other use cases such as offloading.

> You could use (srfi srfi-9) ‘define-record-type’ since the extra (guix
> records) features are not necessary here.

I use (guix records) feature allowing to call the record constructor
with non positional arguments.

> (I wanted to add an API in Guile-Avahi to “invert inversion of control”
> so that one could escape callback hell but never got around to
> completing it.)

Sure, it would be nice, but given libavahi design, not an easy task :).

> Can’t we obtain the IP address without creating a socket actually?  Noob
> here.

I think we can use "getifaddrs" and the its "network-interfaces"
binding, but the resulting code does not seem more readable.

>
>> +    ;; Handle service resolution events.
>> +    (cond ((eq? event resolver-event/found)
>> +           (info (G_ "resolved service `~a' at `~a:~a'~%")
>> +                 service-name (inet-ntop family address) port)
>
> IWBN to not add UI code in here.

Sure, removed!

Mathieu
diff mbox series

Patch

diff --git a/Makefile.am b/Makefile.am
index d63f2ae4b7..7049da9594 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -73,6 +73,7 @@  include gnu/local.mk
 include po/doc/local.mk
 
 MODULES =					\
+  guix/avahi.scm				\
   guix/base16.scm				\
   guix/base32.scm				\
   guix/base64.scm				\
diff --git a/configure.ac b/configure.ac
index 6e718afdd1..307e8b361f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -161,6 +161,12 @@  if test "x$have_guile_lzlib" != "xyes"; then
   AC_MSG_ERROR([Guile-lzlib is missing; please install it.])
 fi
 
+dnl Check for Guile-Avahi.
+GUILE_MODULE_AVAILABLE([have_guile_avahi], [(avahi)])
+if test "x$have_guile_avahi" != "xyes"; then
+  AC_MSG_ERROR([Guile-Avahi is missing; please install it.])
+fi
+
 dnl Guile-newt is used by the graphical installer.
 GUILE_MODULE_AVAILABLE([have_guile_newt], [(newt)])
 
diff --git a/doc/guix.texi b/doc/guix.texi
index ea220fbd63..e9cf25fc90 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -829,6 +829,7 @@  Guile,, gnutls-guile, GnuTLS-Guile});
 or later;
 @item @uref{https://notabug.org/guile-zlib/guile-zlib, Guile-zlib};
 @item @uref{https://notabug.org/guile-lzlib/guile-lzlib, Guile-lzlib};
+@item @uref{https://www.nongnu.org/guile-avahi/, Guile-Avahi};
 @item
 @c FIXME: Specify a version number once a release has been made.
 @uref{https://gitlab.com/guile-git/guile-git, Guile-Git}, version 0.3.0
diff --git a/gnu/packages/package-management.scm b/gnu/packages/package-management.scm
index 4a6881d475..03abfdfee2 100644
--- a/gnu/packages/package-management.scm
+++ b/gnu/packages/package-management.scm
@@ -294,6 +294,7 @@  $(prefix)/etc/init.d\n")))
                                (guile  ,@(if (%current-target-system)
                                              '((assoc-ref native-inputs "guile"))
                                              '((assoc-ref inputs "guile"))))
+                               (avahi  (assoc-ref inputs "guile-avahi"))
                                (gcrypt (assoc-ref inputs "guile-gcrypt"))
                                (json   (assoc-ref inputs "guile-json"))
                                (sqlite (assoc-ref inputs "guile-sqlite3"))
@@ -305,7 +306,7 @@  $(prefix)/etc/init.d\n")))
                                (ssh    (assoc-ref inputs "guile-ssh"))
                                (gnutls (assoc-ref inputs "gnutls"))
                                (locales (assoc-ref inputs "glibc-utf8-locales"))
-                               (deps   (list gcrypt json sqlite gnutls
+                               (deps   (list avahi gcrypt json sqlite gnutls
                                              git bs ssh zlib lzlib))
                                (effective
                                 (read-line
@@ -349,6 +350,7 @@  $(prefix)/etc/init.d\n")))
                        ;; cross-compilation.
                        ("guile" ,guile-3.0-latest) ;for faster builds
                        ("gnutls" ,gnutls)
+                       ("guile-avahi" ,guile-avahi)
                        ("guile-gcrypt" ,guile-gcrypt)
                        ("guile-json" ,guile-json-4)
                        ("guile-sqlite3" ,guile-sqlite3)
@@ -399,6 +401,7 @@  $(prefix)/etc/init.d\n")))
          ("glibc-utf8-locales" ,glibc-utf8-locales)))
       (propagated-inputs
        `(("gnutls" ,(if (%current-target-system) gnutls-3.6.14 gnutls))
+         ("guile-avahi" ,guile-avahi)
          ("guile-gcrypt" ,guile-gcrypt)
          ("guile-json" ,guile-json-4)
          ("guile-sqlite3" ,guile-sqlite3)
diff --git a/guix/avahi.scm b/guix/avahi.scm
new file mode 100644
index 0000000000..cd38619df6
--- /dev/null
+++ b/guix/avahi.scm
@@ -0,0 +1,170 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2020 Mathieu Othacehe <othacehe@gnu.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (guix avahi)
+  #:use-module (guix records)
+  #:use-module (guix ui)
+  #:use-module (guix build syscalls)
+  #:use-module (avahi)
+  #:use-module (avahi client)
+  #:use-module (avahi client lookup)
+  #:use-module (avahi client publish)
+  #:use-module (ice-9 threads)
+  #:export (avahi-service
+            avahi-service?
+            avahi-service-name
+            avahi-service-type
+            avahi-service-interface
+            avahi-service-local-address
+            avahi-service-address
+            avahi-service-port
+            avahi-service-txt
+
+            avahi-publish-service-thread
+            avahi-browse-service-thread))
+
+(define-record-type* <avahi-service>
+  avahi-service make-avahi-service
+  avahi-service?
+  (name avahi-service-name)
+  (type avahi-service-type)
+  (interface avahi-service-interface)
+  (local-address avahi-service-local-address)
+  (address avahi-service-address)
+  (port avahi-service-port)
+  (txt avahi-service-txt))
+
+(define* (avahi-publish-service-thread name
+                                       #:key
+                                       type port
+                                       (stop-loop? (const #f))
+                                       (timeout 100)
+                                       (txt '()))
+  "Publish the service TYPE using Avahi, for the given PORT, on all interfaces
+and for all protocols. Also, advertise the given TXT record list.
+
+This procedure starts a new thread running the Avahi event loop.  It exits
+when STOP-LOOP? procedure returns true."
+  (define client-callback
+    (lambda (client state)
+      (when (eq? state client-state/s-running)
+        (let ((group (make-entry-group client (const #t))))
+          (apply
+           add-entry-group-service! group interface/unspecified
+           protocol/unspecified '()
+           name type #f #f port txt)
+          (commit-entry-group group)))))
+
+  (call-with-new-thread
+   (lambda ()
+     (let* ((poll (make-simple-poll))
+            (client (make-client (simple-poll poll)
+                                 (list
+                                  client-flag/ignore-user-config)
+                                 client-callback)))
+       (while (not (stop-loop?))
+         (iterate-simple-poll poll timeout))))))
+
+(define (interface->ip-address interface)
+  "Return the local IP address of the given INTERFACE."
+  (let ((address
+         (network-interface-address
+          (socket AF_INET SOCK_STREAM 0) interface)))
+    (inet-ntop (sockaddr:fam address) (sockaddr:addr address))))
+
+(define* (avahi-browse-service-thread proc
+                                      #:key
+                                      types
+                                      (family AF_INET)
+                                      (stop-loop? (const #f))
+                                      (timeout 100))
+  "Browse services which type is part of the TYPES list, using Avahi.  The
+search is restricted to services with the given FAMILY.  Each time a service
+is found or removed, PROC is called and passed as argument the corresponding
+AVAHI-SERVICE record.  If a service is available on multiple network
+interfaces, it will only be reported on the first interface found.
+
+This procedure starts a new thread running the Avahi event loop.  It exits
+when STOP-LOOP? procedure returns true."
+  (define %known-hosts
+    ;; Set of Avahi discovered hosts.
+    (make-hash-table))
+
+  (define (service-resolver-callback resolver interface protocol event
+                                     service-name service-type domain
+                                     host-name address-type address port
+                                     txt flags)
+    ;; Handle service resolution events.
+    (cond ((eq? event resolver-event/found)
+           (info (G_ "resolved service `~a' at `~a:~a'~%")
+                 service-name (inet-ntop family address) port)
+           ;; Add the service if the host is unknown.  This means that if a
+           ;; service is available on multiple network interfaces for a single
+           ;; host, only the first interface found will be considered.
+           (unless (hash-ref %known-hosts service-name)
+             (let* ((address (inet-ntop family address))
+                    (local-address (interface->ip-address interface))
+                    (service* (avahi-service
+                               (name service-name)
+                               (type service-type)
+                               (interface interface)
+                               (local-address local-address)
+                               (address address)
+                               (port port)
+                               (txt txt))))
+               (hash-set! %known-hosts service-name service*)
+               (proc 'new-service service*))))
+          ((eq? event resolver-event/failure)
+           (report-error (G_ "failed to resolve service `~a'~%")
+                         service-name)))
+    (free-service-resolver! resolver))
+
+  (define (service-browser-callback browser interface protocol event
+                                    service-name service-type
+                                    domain flags)
+    (cond
+     ((eq? event browser-event/new)
+      (make-service-resolver (service-browser-client browser)
+                             interface protocol
+                             service-name service-type domain
+                             protocol/unspecified '()
+                             service-resolver-callback))
+     ((eq? event browser-event/remove)
+      (let ((service (hash-ref %known-hosts service-name)))
+        (when service
+            (proc 'remove-service service)
+            (hash-remove! %known-hosts service-name))))))
+
+  (define client-callback
+    (lambda (client state)
+      (if (eq? state client-state/s-running)
+          (for-each (lambda (type)
+                      (make-service-browser client
+                                            interface/unspecified
+                                            protocol/inet
+                                            type #f '()
+                                            service-browser-callback))
+                    types))))
+
+  (let* ((poll (make-simple-poll))
+         (client (make-client (simple-poll poll)
+                              '() ;; no flags
+                              client-callback)))
+    (and (client? client)
+         (while (not (stop-loop?))
+           (iterate-simple-poll poll timeout)))))
diff --git a/guix/self.scm b/guix/self.scm
index 026dcd9c1a..257c8eefde 100644
--- a/guix/self.scm
+++ b/guix/self.scm
@@ -50,6 +50,7 @@ 
                (module-ref (resolve-interface module) variable))))
     (match-lambda
       ("guile"      (ref '(gnu packages guile) 'guile-3.0/libgc-7))
+      ("guile-avahi" (ref '(gnu packages guile) 'guile-avahi))
       ("guile-json" (ref '(gnu packages guile) 'guile-json-4))
       ("guile-ssh"  (ref '(gnu packages ssh)   'guile-ssh))
       ("guile-git"  (ref '(gnu packages guile) 'guile-git))
@@ -784,6 +785,9 @@  Info manual."
                         (xz (specification->package "xz"))
                         (guix (specification->package "guix")))
   "Return a file-like object that contains a compiled Guix."
+  (define guile-avahi
+    (specification->package "guile-avahi"))
+
   (define guile-json
     (specification->package "guile-json"))
 
@@ -812,8 +816,9 @@  Info manual."
     (match (append-map (lambda (package)
                          (cons (list "x" package)
                                (package-transitive-propagated-inputs package)))
-                       (list guile-gcrypt gnutls guile-git guile-json
-                             guile-ssh guile-sqlite3 guile-zlib guile-lzlib))
+                       (list guile-gcrypt gnutls guile-git guile-avahi
+                             guile-json guile-ssh guile-sqlite3 guile-zlib
+                             guile-lzlib))
       (((labels packages _ ...) ...)
        packages)))