Subject: [PATCH Guile-Netlink 11/11] link: Add 'wait-for-link'.

From: Ludovic Courtès <[email-masked]>
Date: Tue, 23 May 2023 14:39:51 +0200

* ip/link.scm (message->event+link): New procedure.
(new-link-message->link): Use it.
(monitor-links, wait-for-link): New procedures.
* doc/guile-netlink.texi (Link): Document 'wait-for-link'.
---
 doc/guile-netlink.texi |   8 ++++
 ip/link.scm            | 102 ++++++++++++++++++++++++++++++++++-------
 2 files changed, 94 insertions(+), 16 deletions(-)

diff --git a/doc/guile-netlink.texi b/doc/guile-netlink.texi
index 4dbeafe..3355c27 100644
--- a/doc/guile-netlink.texi
+++ b/doc/guile-netlink.texi
@@ -567,6 +567,14 @@ Returns the list of existing links in the system, as a list of @code{<link>}
 objects.
 @end deffn
 
+@deffn {Scheme Procedure} wait-for-link @var{name} [#:blocking? #t]
+Wait until a link called @var{name} (a string such as @code{"ens3"}) shows
+up.
+
+When @var{blocking?} is false, use a non-blocking socket and cooperate via
+@code{current-read-waiter}---useful when using Fibers.
+@end deffn
+
 @deffn {Sceme Procedure} print-link @var{link}
 Display @var{link} on the standard output, using a format similar to
 @command{ip link} from @code{iproute2}.
diff --git a/ip/link.scm b/ip/link.scm
index 7e0ae6b..1323444 100644
--- a/ip/link.scm
+++ b/ip/link.scm
@@ -1,7 +1,8 @@
 ;;;; This file is part of Guile Netlink
 ;;;;
 ;;;; Copyright (C) 2021 Julien Lepiller <[email-masked]>
-;;;; 
+;;;; Copyright (C) 2023 Ludovic Courtès <[email-masked]>
+;;;;
 ;;;; This library 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
@@ -31,12 +32,14 @@
   #:use-module (srfi srfi-9)
   #:use-module (srfi srfi-34)
   #:use-module (srfi srfi-35)
+  #:use-module (srfi srfi-71)
   #:export (link-add
             link-del
             link-set
             link-show
             link-name->index
             get-links
+            wait-for-link
             print-link
 
             <link> make-link link?
@@ -59,24 +62,35 @@
   (addr  link-addr)
   (brd   link-brd))
 
+(define (message->event+link msg)
+  "If MSG relates to a link event, return two values: its kind (e.g.,
+RTM_NEWLINK) and its associated <link> value.  Otherwise return #f and #f."
+  (if (memv (message-kind msg)
+            (list RTM_NEWLINK
+                  RTM_DELLINK
+                  RTM_SETLINK))
+      (values (message-kind msg)
+              (let* ((data (message-data msg))
+                     (attrs (link-message-attrs data)))
+                (make-link (get-attr attrs IFLA_IFNAME)
+                           (link-message-index data)
+                           (link-message-kind data)
+                           (map int->device-flags (split-flags (link-message-flags data)))
+                           (get-attr attrs IFLA_MTU)
+                           (get-attr attrs IFLA_QDISC)
+                           (get-attr attrs IFLA_OPERSTATE)
+                           (get-attr attrs IFLA_LINKMODE)
+                           (get-attr attrs IFLA_GROUP)
+                           (get-attr attrs IFLA_TXQLEN)
+                           (get-attr attrs IFLA_ADDRESS)
+                           (get-attr attrs IFLA_BROADCAST))))
+      (values #f #f)))
+
 (define (new-link-message->link msg)
   "If MSG has type 'RTM_NEWLINK', return the corresponding <link> object.
 Otherwise return #f."
-  (and (eqv? (message-kind msg) RTM_NEWLINK)
-       (let* ((data (message-data msg))
-              (attrs (link-message-attrs data)))
-         (make-link (get-attr attrs IFLA_IFNAME)
-                    (link-message-index data)
-                    (link-message-kind data)
-                    (map int->device-flags (split-flags (link-message-flags data)))
-                    (get-attr attrs IFLA_MTU)
-                    (get-attr attrs IFLA_QDISC)
-                    (get-attr attrs IFLA_OPERSTATE)
-                    (get-attr attrs IFLA_LINKMODE)
-                    (get-attr attrs IFLA_GROUP)
-                    (get-attr attrs IFLA_TXQLEN)
-                    (get-attr attrs IFLA_ADDRESS)
-                    (get-attr attrs IFLA_BROADCAST)))))
+  (let ((kind link (message->event+link msg)))
+    (and (eqv? kind RTM_NEWLINK) link)))
 
 (define (get-links)
   (define request-num (random 65535))
@@ -390,3 +404,59 @@ balance-rr|active-backup|balance-xor|broadcast|802.3ad|balance-tlb|balance-alb"
     (let ((answer (receive-and-decode-msg sock %default-route-decoder)))
       (close-port sock)
       (answer-ok? (last answer)))))
+
+(define* (monitor-links proc init terminate?      ;TODO: Make public?
+                        #:key (blocking? #t))
+  "Wait for link events until @var{terminate?} returns true.  Call @var{init}
+with the initial list of links; use its result as the initial state.  From
+then on, call @code{(@var{proc} @var{event} @var{link} @var{state})} where
+@var{event} is a constant such as @code{RTM_NEWLINK} and @var{link} is the
+corresponding link.  Return the final state.
+
+When @code{blocking?} is false, use a non-blocking socket and cooperate via
+@code{current-read-waiter}---useful when using Fibers."
+  (define request-num (random 65536))
+  (define message
+    (make-message
+     RTM_GETLINK
+     (logior NLM_F_REQUEST NLM_F_DUMP)
+     request-num
+     0
+     (make-link-message AF_UNSPEC 0 0 0 0 '())))
+
+  (let ((sock (connect-route #:flags (if blocking? 0 SOCK_NONBLOCK))))
+    ;; Subscribe to the "link" group.
+    (add-socket-membership sock RTNLGRP_LINK)
+
+    (send-msg message sock)
+    (let* ((answer (receive-and-decode-msg sock %default-route-decoder))
+           (links (filter-map new-link-message->link answer)))
+      (let loop ((state (init links)))
+        (if (terminate? state)
+            (begin
+              (close-port sock)
+              state)
+            (loop (fold (lambda (msg state)
+                          (let ((event link (message->event+link msg)))
+                            (proc event link state)))
+                        state
+                        (receive-and-decode-msg sock %default-route-decoder))))))))
+
+
+(define* (wait-for-link name #:key (blocking? #t))
+  "Wait until a link called @var{name} (a string such as @code{\"ens3\"}) shows
+up.
+
+When @var{blocking?} is false, use a non-blocking socket and cooperate via
+@code{current-read-waiter}---useful when using Fibers."
+  (monitor-links (lambda (event link result)
+                   (and (= RTM_NEWLINK)
+                        (string=? (link-name link) name)
+                        link))
+                 (lambda (links)
+                   (find (lambda (link)
+                           (string=? (link-name link) name))
+                         links))
+                 (lambda (link)                   ;if LINK is true, terminate
+                   link)
+                 #:blocking? blocking?))
-- 
2.40.1