123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
- ;;;
- ;;; 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 (gnu installer connman)
- #:use-module (gnu installer utils)
- #:use-module (guix records)
- #:use-module (ice-9 match)
- #:use-module (ice-9 popen)
- #:use-module (ice-9 regex)
- #:use-module (srfi srfi-11)
- #:use-module (srfi srfi-34)
- #:use-module (srfi srfi-35)
- #:export (<technology>
- technology
- technology?
- technology-name
- technology-type
- technology-powered?
- technology-connected?
- <service>
- service
- service?
- service-name
- service-type
- service-path
- service-strength
- service-state
- &connman-error
- connman-error?
- connman-error-command
- connman-error-output
- connman-error-status
- &connman-connection-error
- connman-connection-error?
- connman-connection-error-service
- connman-connection-error-output
- &connman-password-error
- connman-password-error?
- &connman-already-connected-error
- connman-already-connected-error?
- connman-state
- connman-technologies
- connman-enable-technology
- connman-disable-technology
- connman-scan-technology
- connman-services
- connman-connect
- connman-disconnect
- connman-online?
- connman-connect-with-auth))
- ;;; Commentary:
- ;;;
- ;;; This module provides procedures for talking with the connman daemon.
- ;;; The best approach would have been using connman dbus interface.
- ;;; However, as Guile dbus bindings are not available yet, the console client
- ;;; "connmanctl" is used to talk with the daemon.
- ;;;
- ;;;
- ;;; Technology record.
- ;;;
- ;; The <technology> record encapsulates the "Technology" object of connman.
- ;; Technology type will be typically "ethernet", "wifi" or "bluetooth".
- (define-record-type* <technology>
- technology make-technology
- technology?
- (name technology-name) ; string
- (type technology-type) ; string
- (powered? technology-powered?) ; boolean
- (connected? technology-connected?)) ; boolean
- ;;;
- ;;; Service record.
- ;;;
- ;; The <service> record encapsulates the "Service" object of connman.
- ;; Service type is the same as the technology it is associated to, path is a
- ;; unique identifier given by connman, strength describes the signal quality
- ;; if applicable. Finally, state is "idle", "failure", "association",
- ;; "configuration", "ready", "disconnect" or "online".
- (define-record-type* <service>
- service make-service
- service?
- (name service-name) ; string
- (type service-type) ; string
- (path service-path) ; string
- (strength service-strength) ; integer
- (state service-state)) ; string
- ;;;
- ;;; Condition types.
- ;;;
- (define-condition-type &connman-error &error
- connman-error?
- (command connman-error-command)
- (output connman-error-output)
- (status connman-error-status))
- (define-condition-type &connman-connection-error &error
- connman-connection-error?
- (service connman-connection-error-service)
- (output connman-connection-error-output))
- (define-condition-type &connman-password-error &connman-connection-error
- connman-password-error?)
- (define-condition-type &connman-already-connected-error
- &connman-connection-error connman-already-connected-error?)
- ;;;
- ;;; Procedures.
- ;;;
- (define (connman-run command env arguments)
- "Run the given COMMAND, with the specified ENV and ARGUMENTS. The error
- output is discarded and &connman-error condition is raised if the command
- returns a non zero exit code."
- (let* ((command `("env" ,env ,command ,@arguments "2>" "/dev/null"))
- (command-string (string-join command " "))
- (pipe (open-input-pipe command-string))
- (output (read-lines pipe))
- (ret (close-pipe pipe)))
- (case (status:exit-val ret)
- ((0) output)
- (else (raise (condition (&connman-error
- (command command)
- (output output)
- (status ret))))))))
- (define (connman . arguments)
- "Run connmanctl with the specified ARGUMENTS. Set the LANG environment
- variable to C because the command output will be parsed and we don't want it
- to be translated."
- (connman-run "connmanctl" "LANG=C" arguments))
- (define (parse-keys keys)
- "Parse the given list of strings KEYS, under the following format:
- '((\"KEY = VALUE\") (\"KEY2 = VALUE2\") ...)
- Return the corresponding association list of '((KEY . VALUE) (KEY2 . VALUE2)
- ...) elements."
- (let ((key-regex (make-regexp "([^ ]+) = ([^$]+)")))
- (map (lambda (key)
- (let ((match-key (regexp-exec key-regex key)))
- (cons (match:substring match-key 1)
- (match:substring match-key 2))))
- keys)))
- (define (connman-state)
- "Return the state of connman. The nominal states are 'offline, 'idle,
- 'ready, 'oneline. If an unexpected state is read, 'unknown is
- returned. Finally, an error is raised if the comman output could not be
- parsed, usually because the connman daemon is not responding."
- (let* ((output (connman "state"))
- (state-keys (parse-keys output)))
- (let ((state (assoc-ref state-keys "State")))
- (if state
- (cond ((string=? state "offline") 'offline)
- ((string=? state "idle") 'idle)
- ((string=? state "ready") 'ready)
- ((string=? state "online") 'online)
- (else 'unknown))
- (raise (condition
- (&message
- (message "Could not determine the state of connman."))))))))
- (define (split-technology-list technologies)
- "Parse the given strings list TECHNOLOGIES, under the following format:
- '((\"/net/connman/technology/xxx\")
- (\"KEY = VALUE\")
- ...
- (\"/net/connman/technology/yyy\")
- (\"KEY2 = VALUE2\")
- ...)
- Return the corresponding '(((\"KEY = VALUE\") ...) ((\"KEY2 = VALUE2\") ...))
- list so that each keys of a given technology are gathered in a separate list."
- (let loop ((result '())
- (cur-list '())
- (input (reverse technologies)))
- (if (null? input)
- result
- (let ((item (car input)))
- (if (string-match "/net/connman/technology" item)
- (loop (cons cur-list result) '() (cdr input))
- (loop result (cons item cur-list) (cdr input)))))))
- (define (string->boolean string)
- (equal? string "True"))
- (define (connman-technologies)
- "Return a list of available <technology> records."
- (define (technology-output->technology output)
- (let ((keys (parse-keys output)))
- (technology
- (name (assoc-ref keys "Name"))
- (type (assoc-ref keys "Type"))
- (powered? (string->boolean (assoc-ref keys "Powered")))
- (connected? (string->boolean (assoc-ref keys "Connected"))))))
- (let* ((output (connman "technologies"))
- (technologies (split-technology-list output)))
- (map technology-output->technology technologies)))
- (define (connman-enable-technology technology)
- "Enable the given TECHNOLOGY."
- (let ((type (technology-type technology)))
- (connman "enable" type)))
- (define (connman-disable-technology technology)
- "Disable the given TECHNOLOGY."
- (let ((type (technology-type technology)))
- (connman "disable" type)))
- (define (connman-scan-technology technology)
- "Run a scan for the given TECHNOLOGY."
- (let ((type (technology-type technology)))
- (connman "scan" type)))
- (define (connman-services)
- "Return a list of available <services> records."
- (define (service-output->service path output)
- (let* ((service-keys
- (match output
- ((_ . rest) rest)))
- (keys (parse-keys service-keys)))
- (service
- (name (assoc-ref keys "Name"))
- (type (assoc-ref keys "Type"))
- (path path)
- (strength (and=> (assoc-ref keys "Strength") string->number))
- (state (assoc-ref keys "State")))))
- (let* ((out (connman "services"))
- (out-filtered (delete "" out))
- (services-path (map (lambda (service)
- (match (string-split service #\ )
- ((_ ... path) path)))
- out-filtered))
- (services-output (map (lambda (service)
- (connman "services" service))
- services-path)))
- (map service-output->service services-path services-output)))
- (define (connman-connect service)
- "Connect to the given SERVICE."
- (let ((path (service-path service)))
- (connman "connect" path)))
- (define (connman-disconnect service)
- "Disconnect from the given SERVICE."
- (let ((path (service-path service)))
- (connman "disconnect" path)))
- (define (connman-online?)
- (let ((state (connman-state)))
- (eq? state 'online)))
- (define (connman-connect-with-auth service password-proc)
- "Connect to the given SERVICE with the password returned by calling
- PASSWORD-PROC. This is only possible in the interactive mode of connmanctl
- because authentication is done by communicating with an agent.
- As the open-pipe procedure of Guile do not allow to read from stderr, we have
- to merge stdout and stderr using bash redirection. Then error messages are
- extracted from connmanctl output using a regexp. This makes the whole
- procedure even more unreliable.
- Raise &connman-connection-error if an error occurred during connection. Raise
- &connman-password-error if the given password is incorrect."
- (define connman-error-regexp (make-regexp "Error[ ]*([^\n]+)\n"))
- (define (match-connman-error str)
- (let ((match-error (regexp-exec connman-error-regexp str)))
- (and match-error (match:substring match-error 1))))
- (define* (read-regexps-or-error port regexps error-handler)
- "Read characters from port until an error is detected, or one of the given
- REGEXPS is matched. If an error is detected, call ERROR-HANDLER with the error
- string as argument. Raise an error if the eof is reached before one of the
- regexps is matched."
- (let loop ((res ""))
- (let ((char (read-char port)))
- (cond
- ((eof-object? char)
- (raise (condition
- (&message
- (message "Unable to find expected regexp.")))))
- ((match-connman-error res)
- =>
- (lambda (match)
- (error-handler match)))
- ((or-map (lambda (regexp)
- (and (regexp-exec regexp res) regexp))
- regexps)
- =>
- (lambda (match)
- match))
- (else
- (loop (string-append res (string char))))))))
- (define* (read-regexp-or-error port regexp error-handler)
- "Same as READ-REGEXPS-OR-ERROR above, but with a single REGEXP."
- (read-regexps-or-error port (list regexp) error-handler))
- (define (connman-error->condition path error)
- (cond
- ((string-match "Already connected" error)
- (condition (&connman-already-connected-error
- (service path)
- (output error))))
- (else
- (condition (&connman-connection-error
- (service path)
- (output error))))))
- (define (run-connection-sequence pipe)
- "Run the connection sequence using PIPE as an opened port to an
- interactive connmanctl process."
- (let* ((path (service-path service))
- (error-handler (lambda (error)
- (raise
- (connman-error->condition path error)))))
- ;; Start the agent.
- (format pipe "agent on\n")
- (read-regexp-or-error pipe (make-regexp "Agent registered") error-handler)
- ;; Let's try to connect to the service. If the service does not require
- ;; a password, the connection might succeed right after this call.
- ;; Otherwise, connmanctl will prompt us for a password.
- (format pipe "connect ~a\n" path)
- (let* ((connected-regexp (make-regexp (format #f "Connected ~a" path)))
- (passphrase-regexp (make-regexp "\nPassphrase\\?[ ]*"))
- (regexps (list connected-regexp passphrase-regexp))
- (result (read-regexps-or-error pipe regexps error-handler)))
- ;; A password is required.
- (when (eq? result passphrase-regexp)
- (format pipe "~a~%" (password-proc))
- ;; Now, we have to wait for the connection to succeed. If an error
- ;; occurs, it is most likely because the password is incorrect.
- ;; In that case, we escape from an eventual retry loop that would
- ;; add complexity to this procedure, and raise a
- ;; &connman-password-error condition.
- (read-regexp-or-error pipe connected-regexp
- (lambda (error)
- ;; Escape from retry loop.
- (format pipe "no\n")
- (raise
- (condition (&connman-password-error
- (service path)
- (output error))))))))))
- ;; XXX: Find a better way to read stderr, like with the "subprocess"
- ;; procedure of racket that return input ports piped on the process stdin and
- ;; stderr.
- (let ((pipe (open-pipe "connmanctl 2>&1" OPEN_BOTH)))
- (dynamic-wind
- (const #t)
- (lambda ()
- (run-connection-sequence pipe)
- #t)
- (lambda ()
- (format pipe "quit\n")
- (close-pipe pipe)))))
|