;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès ;;; Copyright © 2014 Nikita Karetnikov ;;; Copyright © 2018 Kyle Meyer ;;; Copyright © 2020 Christopher Baines ;;; Copyright © 2021 Maxime Devos ;;; ;;; 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 string)) #:use-module (guix progress) #:use-module (guix store) #:use-module (guix scripts substitute) #:autoload (gnutls) (error/invalid-session) #:use-module (ice-9 match) #:use-module (ice-9 binary-ports) #:use-module (ice-9 vlist) #:use-module (ice-9 format)) (define %fetch-timeout ;; Number of seconds after which networking is considered "slow". 5) (define %random-state (seed->random-state (+ (ash (cdr (gettimeofday)) 32) (getpid)))) (define-syntax-rule (with-timeout duration handler body ...) "Run BODY; when DURATION seconds have expired, call HANDLER, and run BODY again." (begin (sigaction SIGALRM (lambda (signum) (sigaction SIGALRM SIG_DFL) handler)) (alarm duration) (call-with-values (lambda () (let try () (catch 'system-error (lambda () body ...) (lambda args ;; Before Guile v2.0.9-39-gfe51c7b, the SIGALRM triggers EINTR ;; because of the bug at ;; . ;; When that happens, try again. Note: SA_RESTART cannot be ;; used because of . (if (= EINTR (system-error-errno args)) (begin ;; Wait a little to avoid bursts. (usleep (random 3000000 %random-state)) (try)) (apply throw args)))))) (lambda result (alarm 0) (sigaction SIGALRM SIG_DFL) (apply values result))))) (define* (fetch uri #:key (buffered? #t) (timeout? #t) (keep-alive? #f) (port #f)) "Return a binary input port to URI and the number of bytes it's expected to provide. When PORT is true, use it as the underlying I/O port for HTTP transfers; when PORT is false, open a new connection for URI. When KEEP-ALIVE? is true, the connection (typically PORT) is kept open once data has been fetched from URI." (case (uri-scheme uri) ((file) (let ((port (open-file (uri-path uri) (if buffered? "rb" "r0b")))) (values port (stat:size (stat port))))) ((http https) (guard (c ((http-get-error? c) (leave (G_ "download from '~a' failed: ~a, ~s~%") (uri->string (http-get-error-uri c)) (http-get-error-code c) (http-get-error-reason c)))) ;; Test this with: ;; sudo tc qdisc add dev eth0 root netem delay 1500ms ;; and then cancel with: ;; sudo tc qdisc del dev eth0 root (let ((port port)) (with-timeout (if timeout? %fetch-timeout 0) (begin (warning (G_ "while fetching ~a: server is somewhat slow~%") (uri->string uri)) (warning (G_ "try `--no-substitutes' if the problem persists~%"))) (call-with-connection-error-handling uri (lambda () (http-fetch uri #:text? #f #:open-connection open-connection-for-uri/cached #:keep-alive? #t #:buffered? #f #:verify-certificate? #f))))))) (else (leave (G_ "unsupported substitute URI scheme: ~a~%") (uri->string uri))))) (define (narinfo-request cache-url path) "Return an HTTP request for the narinfo of PATH at CACHE-URL." (let ((url (string-append cache-url "/" (store-path-hash-part path) ".narinfo")) (headers '((User-Agent . "GNU Guile")))) (build-request (string->uri url) #:method 'GET #:headers headers))) (define (at-most max-length lst) "If LST is shorter than MAX-LENGTH, return it and the empty list; otherwise return its MAX-LENGTH first elements and its tail." (let loop ((len 0) (lst lst) (result '())) (match lst (() (values (reverse result) '())) ((head . tail) (if (>= len max-length) (values (reverse result) lst) (loop (+ 1 len) tail (cons head result))))))) (define* (http-multiple-get base-uri proc seed requests #:key port (verify-certificate? #t) (open-connection guix:open-connection-for-uri) (keep-alive? #t) (batch-size 1000)) "Send all of REQUESTS to the server at BASE-URI. Call PROC for each response, passing it the request object, the response, a port from which to read the response body, and the previous result, starting with SEED, à la 'fold'. Return the final result. When PORT is specified, use it as the initial connection on which HTTP requests are sent; otherwise call OPEN-CONNECTION to open a new connection for a URI. When KEEP-ALIVE? is false, close the connection port before returning." (let connect ((port port) (requests requests) (result seed)) (define batch (at-most batch-size requests)) ;; (format (current-error-port) "connecting (~a requests left)..." ;; (length requests)) (let ((p (or port (open-connection base-uri #:verify-certificate? verify-certificate?)))) ;; For HTTPS, P is not a file port and does not support 'setvbuf'. (when (file-port? p) (setvbuf p 'block (expt 2 16))) ;; Send BATCH in a row. ;; XXX: Do our own caching to work around inefficiencies when ;; communicating over TLS: . (let-values (((buffer get) (open-bytevector-output-port))) ;; Inherit the HTTP proxying property from P. (set-http-proxy-port?! buffer (http-proxy-port? p)) (for-each (cut write-request <> buffer) batch) (put-bytevector p (get)) (force-output p)) ;; Now start processing responses. (let loop ((sent batch) (processed 0) (result result)) (match sent (() (match (drop requests processed) (() (unless keep-alive? (close-port p)) (reverse result)) (remainder (connect p remainder result)))) ((head tail ...) (let* ((resp (read-response p)) (body (response-body-port resp)) (result (proc head resp body result))) ;; The server can choose to stop responding at any time, in which ;; case we have to try again. Check whether that is the case. ;; Note that even upon "Connection: close", we can read from BODY. (match (assq 'connection (response-headers resp)) (('connection 'close) (close-port p) (connect #f ;try again (drop requests (+ 1 processed)) result)) (_ (loop tail (+ 1 processed) result)))))))))) ;keep going (define (read-to-eof port) "Read from PORT until EOF is reached. The data are discarded." (dump-port port (%make-void-port "w"))) (define (narinfo-from-file file url) "Attempt to read a narinfo from FILE, using URL as the cache URL. Return #f if file doesn't exist, and the narinfo otherwise." (catch 'system-error (lambda () (call-with-input-file file (cut read-narinfo <> url))) (lambda args (if (= ENOENT (system-error-errno args)) #f (apply throw args))))) (define %unreachable-hosts ;; Set of names of unreachable hosts. (make-hash-table)) (define %max-cached-connections ;; Maximum number of connections kept in cache by ;; 'open-connection-for-uri/cached'. 16) (define open-connection-for-uri/cached (let ((cache '())) (lambda* (uri #:key fresh? (timeout %fetch-timeout) verify-certificate?) "Return a connection for URI, possibly reusing a cached connection. When FRESH? is true, delete any cached connections for URI and open a new one. Return #f if URI's scheme is 'file' or #f. When true, TIMEOUT is the maximum number of milliseconds to wait for connection establishment. When VERIFY-CERTIFICATE? is true, verify HTTPS server certificates." (define host (uri-host uri)) (define scheme (uri-scheme uri)) (define key (list host scheme (uri-port uri))) (and (not (memq scheme '(file #f))) (match (assoc-ref cache key) (#f ;; Open a new connection to URI and evict old entries from ;; CACHE, if any. (let-values (((socket) (guix:open-connection-for-uri uri #:verify-certificate? verify-certificate? #:timeout timeout)) ((new-cache evicted) (at-most (- %max-cached-connections 1) cache))) (for-each (match-lambda ((_ . port) (false-if-exception (close-port port)))) evicted) (set! cache (alist-cons key socket new-cache)) socket)) (socket (if (or fresh? (port-closed? socket)) (begin (false-if-exception (close-port socket)) (set! cache (alist-delete key cache)) (open-connection-for-uri/cached uri #:timeout timeout #:verify-certificate? verify-certificate?)) (begin ;; Drain input left from the previous use. (drain-input socket) socket)))))))) (define* (process-substitution/http destination narinfo #:key print-build-trace?) (let-values (((uri compression file-size) (narinfo-best-uri narinfo))) (unless print-build-trace? (format (current-error-port) (G_ "Downloading ~a...~%") (uri->string uri))) (let*-values (((raw download-size) ;; 'guix publish' without '--cache' doesn't specify a ;; Content-Length, so DOWNLOAD-SIZE is #f in this case. (fetch uri)) ((progress) (let* ((dl-size (or download-size (and (equal? compression "none") (narinfo-size narinfo)))) (reporter (if print-build-trace? (progress-reporter/trace destination (uri->string uri) dl-size (current-error-port)) (progress-reporter/file (uri->string uri) dl-size (current-error-port) #:abbreviation nar-uri-abbreviation)))) ;; Keep RAW open upon completion so we can later reuse ;; the underlying connection. (progress-report-port reporter raw #:close? #f))) ((input pids) ;; NOTE: This 'progress' port of current process will be ;; closed here, while the child process doing the ;; reporting will close it upon exit. (decompressed-port (string->symbol compression) progress))) (values input #:after-input-close (lambda () ;; Wait for the reporter to finish. (every (compose zero? cdr waitpid) pids) ;; Skip a line after what 'progress-reporter/file' printed, ;; and another one to visually separate substitutions. (display "\n\n" (current-error-port))))))) (define* (call-with-connection-error-handling uri proc) "Call PROC, and catch if a connection fails, print a warning and return #f." (define host (uri-host uri)) (catch #t proc (match-lambda* (('getaddrinfo-error error) (unless (hash-ref %unreachable-hosts host) (hash-set! %unreachable-hosts host #t) ;warn only once (warning (G_ "~a: host not found: ~a~%") host (gai-strerror error))) #f) (('system-error . args) (unless (hash-ref %unreachable-hosts host) (hash-set! %unreachable-hosts host #t) (warning (G_ "~a: connection failed: ~a~%") host (strerror (system-error-errno `(system-error ,@args))))) #f) (args (apply throw args))))) (define* (fetch-narinfos/http url paths #:key (open-connection guix:open-connection-for-uri)) "Retrieve all the narinfos for PATHS from the cache at URL and return them. The URI scheme of URL must currently be http, https or file." (define update-progress! (let ((done 0) (total (length paths))) (lambda () (display "\r\x1b[K" (current-error-port)) ;erase current line (force-output (current-error-port)) (format (current-error-port) (G_ "updating substitutes from '~a'... ~5,1f%") url (* 100. (/ done total))) (set! done (+ 1 done))))) (define hash-part->path (let ((mapping (fold (lambda (path result) (vhash-cons (store-path-hash-part path) path result)) vlist-null paths))) (lambda (hash) (match (vhash-assoc hash mapping) (#f #f) ((_ . path) path))))) (define (handle-narinfo-response request response port result) (let* ((code (response-code response)) (len (response-content-length response)) (cache (response-cache-control response)) (ttl (and cache (assoc-ref cache 'max-age)))) (update-progress!) ;; Make sure to read no more than LEN bytes since subsequent bytes may ;; belong to the next response. (if (= code 200) ; hit (let ((narinfo (read-narinfo port url #:size len))) (if (string=? (dirname (narinfo-path narinfo)) (%store-prefix)) (begin (cache-narinfo! url (narinfo-path narinfo) narinfo ttl) (cons narinfo result)) result)) (let* ((path (uri-path (request-uri request))) (hash-part (basename (string-drop-right path 8)))) ;drop ".narinfo" (if len (get-bytevector-n port len) (read-to-eof port)) (cache-narinfo! url (hash-part->path hash-part) #f (if (or (= 404 code) (= 202 code)) ttl %narinfo-transient-error-ttl)) result)))) (define (do-fetch uri) (case (and=> uri uri-scheme) ((http https) ;; Note: Do not check HTTPS server certificates to avoid depending ;; on the X.509 PKI. We can do it because we authenticate ;; narinfos, which provides a much stronger guarantee. (let* ((requests (map (cut narinfo-request url <>) paths)) (result (begin (update-progress!) (call-with-connection-error-handling uri (lambda () (http-multiple-get uri handle-narinfo-response '() requests #:open-connection open-connection #:verify-certificate? #f)))))) (newline (current-error-port)) result)) ((file #f) (let* ((base (string-append (uri-path uri) "/")) (files (map (compose (cut string-append base <> ".narinfo") store-path-hash-part) paths))) (filter-map (cut narinfo-from-file <> url) files))) (else (leave (G_ "~s: unsupported server URI scheme~%") (if uri (uri-scheme uri) url))))) (do-fetch (string->uri url))) (define http-substituter (make-substituter 'http process-substitution/http fetch-narinfos/http '(http https file)))