123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2017 Sou Bunnbu <iyzsong@gmail.com>
- ;;; Copyright © 2015 Steve Sprang <scs@stevesprang.com>
- ;;; Copyright © 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
- ;;; Copyright © 2018 Clément Lassieur <clement@lassieur.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 progress)
- #:use-module (guix records)
- #:use-module (srfi srfi-19)
- #:use-module (rnrs io ports)
- #:use-module (rnrs bytevectors)
- #:use-module (ice-9 format)
- #:use-module (ice-9 match)
- #:export (<progress-reporter>
- progress-reporter
- make-progress-reporter
- progress-reporter?
- call-with-progress-reporter
- start-progress-reporter!
- stop-progress-reporter!
- progress-reporter-report!
- progress-reporter/silent
- progress-reporter/file
- progress-reporter/bar
- progress-reporter/trace
- progress-report-port
- display-download-progress
- erase-current-line
- progress-bar
- byte-count->string
- current-terminal-columns
- dump-port*))
- ;;; Commentary:
- ;;;
- ;;; Helper to write progress report code for downloads, etc.
- ;;;
- ;;; Code:
- (define-record-type* <progress-reporter>
- progress-reporter make-progress-reporter progress-reporter?
- (start progress-reporter-start) ; thunk
- (report progress-reporter-report) ; procedure
- (stop progress-reporter-stop)) ; thunk
- (define (call-with-progress-reporter reporter proc)
- "Start REPORTER for progress reporting, and call @code{(@var{proc} report)}
- with the resulting report procedure. When @var{proc} returns, the REPORTER is
- stopped."
- (match reporter
- (($ <progress-reporter> start report stop)
- (dynamic-wind start (lambda () (proc report)) stop))))
- (define (start-progress-reporter! reporter)
- "Low-level procedure to start REPORTER."
- (match reporter
- (($ <progress-reporter> start report stop)
- (start))))
- (define (progress-reporter-report! reporter . args)
- "Low-level procedure to lead REPORTER to emit a report."
- (match reporter
- (($ <progress-reporter> start report stop)
- (apply report args))))
- (define (stop-progress-reporter! reporter)
- "Low-level procedure to stop REPORTER."
- (match reporter
- (($ <progress-reporter> start report stop)
- (stop))))
- (define progress-reporter/silent
- (make-progress-reporter noop noop noop))
- ;;;
- ;;; File download progress report.
- ;;;
- (define (nearest-exact-integer x)
- "Given a real number X, return the nearest exact integer, with ties going to
- the nearest exact even integer."
- (inexact->exact (round x)))
- (define (duration->seconds duration)
- "Return the number of seconds represented by DURATION, a 'time-duration'
- object, as an inexact number."
- (+ (time-second duration)
- (/ (time-nanosecond duration) 1e9)))
- (define (seconds->string duration)
- "Given DURATION in seconds, return a string representing it in 'mm:ss' or
- 'hh:mm:ss' format, as needed."
- (if (not (number? duration))
- "00:00"
- (let* ((total-seconds (nearest-exact-integer duration))
- (extra-seconds (modulo total-seconds 3600))
- (num-hours (quotient total-seconds 3600))
- (hours (and (positive? num-hours) num-hours))
- (mins (quotient extra-seconds 60))
- (secs (modulo extra-seconds 60)))
- (format #f "~@[~2,'0d:~]~2,'0d:~2,'0d" hours mins secs))))
- (define (byte-count->string size)
- "Given SIZE in bytes, return a string representing it in a human-readable
- way."
- (let ((KiB 1024.)
- (MiB (expt 1024. 2))
- (GiB (expt 1024. 3))
- (TiB (expt 1024. 4)))
- (cond
- ((< size KiB) (format #f "~dB" (nearest-exact-integer size)))
- ((< size MiB) (format #f "~dKiB" (nearest-exact-integer (/ size KiB))))
- ((< size GiB) (format #f "~,1fMiB" (/ size MiB)))
- ((< size TiB) (format #f "~,2fGiB" (/ size GiB)))
- (else (format #f "~,3fTiB" (/ size TiB))))))
- (define (string-pad-middle left right len)
- "Combine LEFT and RIGHT with enough padding in the middle so that the
- resulting string has length at least LEN (it may overflow). If the string
- does not overflow, the last char in RIGHT will be flush with the LEN
- column."
- (let* ((total-used (+ (string-length left)
- (string-length right)))
- (num-spaces (max 1 (- len total-used)))
- (padding (make-string num-spaces #\space)))
- (string-append left padding right)))
- (define (rate-limited proc interval)
- "Return a procedure that will forward the invocation to PROC when the time
- elapsed since the previous forwarded invocation is greater or equal to
- INTERVAL (a time-duration object), otherwise does nothing and returns #f."
- (let ((previous-at #f))
- (lambda args
- (let* ((now (current-time time-monotonic))
- (forward-invocation (lambda ()
- (set! previous-at now)
- (apply proc args))))
- (if previous-at
- (let ((elapsed (time-difference now previous-at)))
- (if (time>=? elapsed interval)
- (forward-invocation)
- #f))
- (forward-invocation))))))
- (define current-terminal-columns
- ;; Number of columns of the terminal.
- (make-parameter 80))
- (define* (progress-bar % #:optional (bar-width 20))
- "Return % as a string representing an ASCII-art progress bar. The total
- width of the bar is BAR-WIDTH."
- (let* ((bar-width (max 3 (- bar-width 2)))
- (fraction (/ % 100))
- (filled (inexact->exact (floor (* fraction bar-width))))
- (empty (- bar-width filled)))
- (format #f "[~a~a]"
- (make-string filled #\#)
- (make-string empty #\space))))
- (define (erase-current-line port)
- "Write an ANSI erase-current-line sequence to PORT to erase the whole line and
- move the cursor to the beginning of the line."
- (display "\r\x1b[K" port))
- (define* (display-download-progress file size
- #:key
- (tty? #t)
- start-time (transferred 0)
- (log-port (current-error-port)))
- "Write the progress report to LOG-PORT. Use START-TIME (a SRFI-19 time
- object) and TRANSFERRED (a total number of bytes) to determine the
- throughput. When TTY? is false, assume LOG-PORT is not a tty and do not emit
- ANSI escape codes."
- (define elapsed
- (duration->seconds
- (time-difference (current-time (time-type start-time))
- start-time)))
- (cond ((and (not tty?)
- size (not (zero? size))
- transferred)
- ;; Display a dot for at most every 10%.
- (when (zero? (modulo (round (* 100. (/ transferred size))) 10))
- (display "." log-port)
- (force-output log-port)))
- ((and (number? size) (not (zero? size)))
- (let* ((% (* 100.0 (/ transferred size)))
- (throughput (/ transferred elapsed))
- (left (format #f " ~a ~a" file
- (byte-count->string size)))
- (right (format #f "~a/s ~a ~a~6,1f%"
- (byte-count->string throughput)
- (seconds->string elapsed)
- (progress-bar %) %)))
- (erase-current-line log-port)
- (display (string-pad-middle left right
- (current-terminal-columns))
- log-port)
- (force-output log-port)))
- (else
- ;; If we don't know the total size, the last transfer will have a 0B
- ;; size. Don't display it.
- (unless (zero? transferred)
- (let* ((throughput (/ transferred elapsed))
- (left (format #f " ~a" file))
- (right (format #f "~a/s ~a | ~a transferred"
- (byte-count->string throughput)
- (seconds->string elapsed)
- (byte-count->string transferred))))
- (erase-current-line log-port)
- (display (string-pad-middle left right
- (current-terminal-columns))
- log-port)
- (force-output log-port))))))
- (define %progress-interval
- ;; Default interval between subsequent outputs for rate-limited displays.
- (make-time time-duration 200000000 0))
- (define* (progress-reporter/file file size
- #:optional (log-port (current-output-port))
- #:key (abbreviation basename))
- "Return a <progress-reporter> object to show the progress of FILE's download,
- which is SIZE bytes long. The progress report is written to LOG-PORT, with
- ABBREVIATION used to shorten FILE for display."
- (let ((start-time (current-time time-monotonic))
- (transferred 0))
- (define (render)
- (display-download-progress (abbreviation file) size
- #:start-time start-time
- #:transferred transferred
- #:log-port log-port))
- (progress-reporter
- (start render)
- ;; Report the progress every 300ms or longer.
- (report
- (let ((rate-limited-render (rate-limited render %progress-interval)))
- (lambda (value)
- (set! transferred value)
- (rate-limited-render))))
- ;; Don't miss the last report.
- (stop render))))
- (define* (progress-reporter/bar total
- #:optional
- (prefix "")
- (port (current-error-port)))
- "Return a reporter that shows a progress bar every time one of the TOTAL
- tasks is performed. Write PREFIX at the beginning of the line."
- (define done 0)
- (define (draw-bar)
- (let* ((ratio (* 100. (/ done total))))
- (erase-current-line port)
- (if (string-null? prefix)
- (display (progress-bar ratio (current-terminal-columns)) port)
- (let ((width (- (current-terminal-columns)
- (string-length prefix) 3)))
- (display prefix port)
- (display " " port)
- (display (progress-bar ratio width) port)))
- (force-output port)))
- (define draw-bar/rate-limited
- (rate-limited draw-bar %progress-interval))
- (define (report-progress)
- (set! done (+ 1 done))
- (unless (> done total)
- (draw-bar/rate-limited)))
- (progress-reporter
- (start (lambda ()
- (set! done 0)))
- (report report-progress)
- (stop (lambda ()
- (erase-current-line port)
- (unless (string-null? prefix)
- (display prefix port)
- (newline port))
- (force-output port)))))
- (define* (progress-reporter/trace file url size
- #:optional (log-port (current-output-port)))
- "Like 'progress-reporter/file', but instead of returning human-readable
- progress reports, write \"build trace\" lines to be processed elsewhere."
- (define total 0) ;bytes transferred
- (define (report-progress transferred)
- (define message
- (format #f "@ download-progress ~a ~a ~a ~a~%"
- file url (or size "-") transferred))
- (display message log-port) ;should be atomic
- (flush-output-port log-port))
- (progress-reporter
- (start (lambda ()
- (set! total 0)
- (display (format #f "@ download-started ~a ~a ~a~%"
- file url (or size "-"))
- log-port)))
- (report (let ((report (rate-limited report-progress %progress-interval)))
- (lambda (transferred)
- (set! total transferred)
- (report transferred))))
- (stop (lambda ()
- (let ((size (or size total)))
- (report-progress size)
- (display (format #f "@ download-succeeded ~a ~a ~a~%"
- file url size)
- log-port))))))
- ;; TODO: replace '(@ (guix build utils) dump-port))'.
- (define* (dump-port* in out
- #:key (buffer-size 16384)
- (reporter progress-reporter/silent))
- "Read as much data as possible from IN and write it to OUT, using chunks of
- BUFFER-SIZE bytes. After each successful transfer of BUFFER-SIZE bytes or
- less, report the total number of bytes transferred to the REPORTER, which
- should be a <progress-reporter> object."
- (define buffer
- (make-bytevector buffer-size))
- (call-with-progress-reporter reporter
- (lambda (report)
- (let loop ((total 0)
- (bytes (get-bytevector-n! in buffer 0 buffer-size)))
- (or (eof-object? bytes)
- (let ((total (+ total bytes)))
- (put-bytevector out buffer 0 bytes)
- (report total)
- (loop total (get-bytevector-n! in buffer 0 buffer-size))))))))
- (define* (progress-report-port reporter port
- #:key
- (close? #t)
- download-size)
- "Return a port that continuously reports the bytes read from PORT using
- REPORTER, which should be a <progress-reporter> object. When CLOSE? is true,
- PORT is closed when the returned port is closed.
- When DOWNLOAD-SIZE is passed, do not read more than DOWNLOAD-SIZE bytes from
- PORT. This is important to avoid blocking when the remote side won't close
- the underlying connection."
- (match reporter
- (($ <progress-reporter> start report stop)
- (let* ((total 0)
- (read! (lambda (bv start count)
- (let* ((count (if download-size
- (min count (- download-size total))
- count))
- (n (match (get-bytevector-n! port bv start count)
- ((? eof-object?) 0)
- (x x))))
- (set! total (+ total n))
- (report total)
- n))))
- (start)
- (make-custom-binary-input-port "progress-port-proc"
- read! #f #f
- (lambda ()
- ;; XXX: Kludge! When used through
- ;; 'decompressed-port', this port ends
- ;; up being closed twice: once in a
- ;; child process early on, and at the
- ;; end in the parent process. Ignore
- ;; the early close so we don't output
- ;; a spurious "download-succeeded"
- ;; trace.
- (unless (zero? total)
- (stop))
- (when close?
- (close-port port))))))))
|