1
0
mirror of https://git.savannah.gnu.org/git/guix.git synced 2026-07-08 04:24:07 +02:00

Compare commits

...

6 Commits

Author SHA1 Message Date
self 9101a99d70 fix: preserve substituter trace ordering
Drain pending substituter stderr before waking a substitution goal after the fd 4 status reply. This avoids daemon-side substituter-succeeded traces overtaking the child's final download progress trace on the client status stream.

Recheck descriptor readiness before reads because draining one descriptor can consume readiness observed by the worker's earlier select call.
2026-05-18 22:56:17 +02:00
self d240c0dc76 fix: ignore invalid Guix progress updates
Reject nonsensical download-progress values before updating status or rendering progress. This keeps malformed trace fragments from producing invalid progress math while preserving the previous behavior for ordinary malformed non-numeric events.\n\nCover both status reduction and quiet event rendering with overrun progress tests.
2026-05-17 09:59:53 +02:00
self 5205cfb34c fix: preserve malformed Guix system-error details
Catch formatter failures while reporting Guile system-error payloads and print the raw fields instead. This keeps the original low-level failure visible during rollout diagnostics instead of replacing it with a secondary ui.scm formatter backtrace.
2026-05-17 09:59:47 +02:00
self 586b204d94 fix: write Guix trace progress lines atomically
Bypass ordinary Guile port buffering for file-descriptor-backed trace ports so download trace records are emitted as complete lines in the common case. This keeps daemon progress events from being combined with adjacent human-readable output.\n\nAdd a file-backed progress reporter test so the direct descriptor write path is exercised.
2026-05-17 01:42:12 +02:00
self b73ddc8cc1 fix: harden status trace parsing
Treat carriage returns as record delimiters when parsing multiplexed build-log payloads, matching the top-level daemon output parser. Without this, CR-delimited substitute progress traces can be coalesced into one malformed status event.

Also require parsed download sizes and progress counters to be numeric before accepting download events, so malformed traces cannot poison download state and crash the progress renderer.

Add regression coverage for CR-delimited multiplexed traces and malformed download progress.
2026-05-16 00:43:24 +02:00
self e751052400 status: Guard against stray 'download-succeeded' events. 2026-05-03 01:44:04 +02:00
6 changed files with 296 additions and 55 deletions
+2
View File
@@ -120,6 +120,8 @@
(name "sharlatan"))
("F494 72F4 7A59 00D5 C235 F212 89F9 6D48 08F3 59C7"
(name "snape"))
("6688 9153 C51C 4613 A493 A525 2F0D FD14 EF99 DAC3"
(name "steffen"))
("6580 7361 3BFC C5C7 E2E4 5D45 DC51 8FC8 7F97 16AA"
(name "vagrantc"))
(;; primary: "C955 CC5D C048 7FB1 7966 40A9 199A F6A3 67E9 4ABB"
+53 -9
View File
@@ -23,6 +23,8 @@
#:use-module (guix records)
#:autoload (guix build syscalls) (terminal-string-width)
#:use-module (srfi srfi-19)
#:use-module (ice-9 ports)
#:use-module (ice-9 rw)
#:use-module (rnrs io ports)
#:use-module (rnrs bytevectors)
#:use-module (ice-9 format)
@@ -269,6 +271,50 @@ ANSI escape codes."
;; Default interval between subsequent outputs for rate-limited displays.
(make-time time-duration 200000000 0))
(define %maximum-atomic-trace-line-length
;; POSIX guarantees writes to a pipe are atomic up to PIPE_BUF bytes. POSIX
;; requires PIPE_BUF to be at least 512 bytes; Linux uses 4096 bytes. Guix
;; trace lines are normally far smaller than this.
4096)
(define (port-file-descriptor port)
"Return PORT's file descriptor, or #f if PORT is not backed by one."
(catch #t
(lambda ()
(fileno port))
(lambda _
#f)))
(define (write-string/direct file-descriptor str)
"Write STR directly to FILE-DESCRIPTOR."
(let ((len (string-length str)))
(let loop ((start 0))
(when (< start len)
(let ((written (write-string/partial str file-descriptor start len)))
(if (zero? written)
(error "short trace write" str)
(loop (+ start written))))))))
(define (write-trace-line str port)
"Write trace line STR to PORT.
For file ports, bypass Guile's ordinary port buffering so STR is emitted as one
kernel write in the common case. This prevents Guix build trace lines from
being combined with adjacent human-readable output in the daemon log stream."
(match (and (<= (string-length str) %maximum-atomic-trace-line-length)
(port-file-descriptor port))
(#f
(display str port)
(force-output port))
(file-descriptor
(catch #t
(lambda ()
(force-output port)
(write-string/direct file-descriptor str))
(lambda _
(display str port)
(force-output port))))))
(define* (progress-reporter/file file size
#:optional (log-port (current-output-port))
#:key (abbreviation basename))
@@ -344,15 +390,14 @@ progress reports, write \"build trace\" lines to be processed elsewhere."
(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))
(write-trace-line message log-port))
(progress-reporter
(start (lambda ()
(set! total 0)
(display (format #f "@ download-started ~a ~a ~a~%"
file url (or size "-"))
log-port)))
(write-trace-line (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)
@@ -360,9 +405,9 @@ progress reports, write \"build trace\" lines to be processed elsewhere."
(stop (lambda ()
(let ((size (or size total)))
(report-progress size)
(display (format #f "@ download-succeeded ~a ~a ~a~%"
file url size)
log-port))))))
(write-trace-line (format #f "@ download-succeeded ~a ~a ~a~%"
file url size)
log-port))))))
;; TODO: replace '(@ (guix build utils) dump-port))'.
(define* (dump-port* in out
@@ -425,4 +470,3 @@ the underlying connection."
(stop))
(when close?
(close-port port))))))))
+67 -42
View File
@@ -150,6 +150,15 @@
(lambda (download)
(string=? item (download-item download))))
(define (valid-progress-values? size transferred)
"Return true when SIZE and TRANSFERRED form a sensible progress update."
(and (real? size)
(real? transferred)
(not (negative? size))
(not (negative? transferred))
(or (zero? size)
(<= transferred size))))
(define %phase-start-rx
;; Match the "starting phase" message emitted by 'gnu-build-system'.
(make-regexp "^starting phase [`']([^']+)'"))
@@ -255,7 +264,7 @@ compute a new status based on STATUS."
;; they're not as informative as 'download-started' and
;; 'download-succeeded'.
(('download-started item uri (= string->number size))
(('download-started item uri (= string->number (? number? size)))
;; This is presumably a fixed-output derivation so move it from
;; 'building' to 'downloading'. XXX: This doesn't work in 'check' mode
;; because ITEM is different from DRV's output.
@@ -270,19 +279,26 @@ compute a new status based on STATUS."
(downloading (cons (download item uri #:size size
#:start (current-time time-monotonic))
(build-status-downloading status)))))
(('download-succeeded item uri (= string->number size))
(let ((current (find (matching-download item)
(build-status-downloading status))))
(build-status
(inherit status)
(downloading (delq current (build-status-downloading status)))
(downloads-completed
(cons (download item uri
#:size size
#:start (download-start current)
#:transferred size
#:end (current-time time-monotonic))
(build-status-downloads-completed status))))))
(('download-succeeded item uri (= string->number (? number? size)))
(match (find (matching-download item)
(build-status-downloading status))
(#f
;; Download traces can arrive without a tracked 'download-started'
;; event due to trace ordering or spurious duplicate completion
;; events. Ignore the completion trace instead of crashing while
;; trying to read fields from #f.
status)
(current
(build-status
(inherit status)
(downloading (delq current (build-status-downloading status)))
(downloads-completed
(cons (download item uri
#:size size
#:start (download-start current)
#:transferred size
#:end (current-time time-monotonic))
(build-status-downloads-completed status)))))))
(('substituter-succeeded item _ ...)
(match (find (matching-download item)
(build-status-downloading status))
@@ -304,22 +320,24 @@ compute a new status based on STATUS."
#:end (current-time time-monotonic))
(build-status-downloads-completed status)))))))
(('download-progress item uri
(= string->number size)
(= string->number transferred))
(let ((downloads (remove (matching-download item)
(build-status-downloading status)))
(current (find (matching-download item)
(build-status-downloading status))))
(build-status
(inherit status)
(downloading (cons (download item uri
#:size size
#:start
(or (and current
(download-start current))
(current-time time-monotonic))
#:transferred transferred)
downloads)))))
(= string->number (? number? size))
(= string->number (? number? transferred)))
(if (valid-progress-values? size transferred)
(let ((downloads (remove (matching-download item)
(build-status-downloading status)))
(current (find (matching-download item)
(build-status-downloading status))))
(build-status
(inherit status)
(downloading (cons (download item uri
#:size size
#:start
(or (and current
(download-start current))
(current-time time-monotonic))
#:transferred transferred)
downloads))))
status))
(('build-log (? integer? pid) line)
(update-build status pid line))
(_
@@ -571,8 +589,8 @@ The channels you are pulling from are: ~a.")
(format port (info (G_ "downloading from ~a ...")) uri)
(newline port)))
(('download-progress item uri
(= string->number size)
(= string->number transferred))
(= string->number (? number? size))
(= string->number (? number? transferred)))
;; Print a progress bar, but only if there's only one on-going
;; job--otherwise the output would be intermingled.
(when (= 1 (simultaneous-jobs status))
@@ -581,14 +599,15 @@ The channels you are pulling from are: ~a.")
(#f #f) ;shouldn't happen!
(download
;; XXX: It would be nice to memoize the abbreviation.
(let ((uri (if (string-contains uri "/nar/")
(nar-uri-abbreviation uri)
(basename uri))))
(display-download-progress uri size
#:tty? tty?
#:start-time
(download-start download)
#:transferred transferred))))))
(when (valid-progress-values? size transferred)
(let ((uri (if (string-contains uri "/nar/")
(nar-uri-abbreviation uri)
(basename uri))))
(display-download-progress uri size
#:tty? tty?
#:start-time
(download-start download)
#:transferred transferred)))))))
(('substituter-succeeded item _ ...)
(when (extended-build-trace-supported?)
;; If there are no jobs running, we already reported download completion
@@ -705,12 +724,18 @@ found."
(else (loop (+ 1 offset) (- count 1))))))
(define (split-lines str)
"Split STR into lines in a way that preserves newline characters."
"Split STR into lines in a way that preserves newline characters.
Treat carriage returns as line delimiters as well: progress traces emitted by
'guix substitute' can use them, and a multiplexed build-log payload may contain
several such traces."
(let loop ((str str)
(result '()))
(if (string-null? str)
(reverse result)
(match (string-index str #\newline)
(match (string-index str
(lambda (chr)
(or (char=? chr #\newline)
(char=? chr #\return))))
(#f
(loop "" (cons str result)))
(index
+22 -2
View File
@@ -757,6 +757,23 @@ evaluating the tests and bodies of CLAUSES."
(lambda () exp ...)
#:unwind? #f))
(define (system-error-message proc format-string format-args rest)
"Format a Guile 'system-error' payload, falling back to raw fields.
Some low-level callers can raise malformed 'system-error' payloads. Avoid
masking the original failure with a second exception from the error reporter."
(catch #t
(lambda ()
(apply format #f format-string format-args))
(lambda (key . args)
(format #f
"unprintable system-error payload: proc=~s format=~s args=~s rest=~s formatter-error=~s"
proc
format-string
format-args
rest
(cons key args)))))
(define (call-with-error-handling thunk)
"Call THUNK within a user-friendly error handler."
(define (port-filename* port)
@@ -915,9 +932,12 @@ directories:~{ ~a~}~%")
(((exception-predicate &exception-with-kind-and-args) c)
(if (eq? 'system-error (exception-kind c)) ;EPIPE & co.
(match (exception-args c)
((proc format-string format-args . _)
((proc format-string format-args . rest)
(leave (G_ "~a: ~a~%") proc
(apply format #f format-string format-args))))
(system-error-message proc
format-string
format-args
rest))))
(raise c)))
((message-condition? c)
+68 -2
View File
@@ -3698,6 +3698,9 @@ private:
substituter. */
string status;
/* Drain currently pending substituter stderr. */
void drainSubstituterOutput();
void tryNext();
public:
@@ -4032,6 +4035,8 @@ void SubstitutionGoal::handleChildOutput(int fd, const string & data)
if (fd == substituter->builderOut.readSide) {
/* DATA may consist of several lines. Process them one by one. */
string input = data;
bool gotStatus = false;
while (!input.empty()) {
/* Process up to the first newline. */
size_t end = input.find_first_of("\n");
@@ -4040,13 +4045,18 @@ void SubstitutionGoal::handleChildOutput(int fd, const string & data)
/* Update the goal's state accordingly. */
if (status == "") {
status = trimmed;
worker.wakeUp(shared_from_this());
gotStatus = true;
} else {
printMsg(lvlError, std::format("unexpected substituter message '{}'", input));
}
input = (end != string::npos) ? input.substr(end + 1) : "";
}
if (gotStatus) {
drainSubstituterOutput();
worker.wakeUp(shared_from_this());
}
}
}
@@ -4057,6 +4067,40 @@ void SubstitutionGoal::handleEOF(int fd)
}
void SubstitutionGoal::drainSubstituterOutput()
{
if (!(verbosity >= settings.buildVerbosity) || !substituter) return;
while (true) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(substituter->fromAgent.readSide, &fds);
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
int ready = select(substituter->fromAgent.readSide + 1, &fds, 0, 0, &timeout);
if (ready == -1) {
if (errno == EINTR) continue;
throw SysError("waiting for substituter stderr");
}
if (ready == 0 || !FD_ISSET(substituter->fromAgent.readSide, &fds))
return;
unsigned char buffer[4096];
ssize_t rd = read(substituter->fromAgent.readSide, buffer, sizeof(buffer));
if (rd == -1) {
if (errno == EINTR) continue;
throw SysError("reading from substituter stderr");
}
if (rd == 0) return;
writeToStderr(string((char *) buffer, rd));
}
}
//////////////////////////////////////////////////////////////////////
@@ -4343,6 +4387,25 @@ void Worker::waitForInput()
time_t after = time(0);
/* Process all available file descriptors. */
auto stillReadable = [](int fd) {
while (true) {
fd_set probe;
FD_ZERO(&probe);
FD_SET(fd, &probe);
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
int ready = select(fd + 1, &probe, 0, 0, &timeout);
if (ready == -1) {
if (errno == EINTR) continue;
throw SysError("checking child output readiness");
}
return ready > 0 && FD_ISSET(fd, &probe);
}
};
/* Since goals may be canceled from inside the loop below (causing
them go be erased from the `children' map), we have to be
@@ -4360,7 +4423,10 @@ void Worker::waitForInput()
set<int> fds2(j->second.fds);
for (auto& k : fds2) {
if (FD_ISSET(k, &fds)) {
/* A goal may drain another descriptor that was marked by
the original select() call. Recheck before read() so
the worker does not block on stale readiness. */
if (FD_ISSET(k, &fds) && stillReadable(k)) {
unsigned char buffer[4096];
ssize_t rd = read(k, buffer, sizeof(buffer));
if (rd == -1) {
+84
View File
@@ -18,6 +18,9 @@
(define-module (test-status)
#:use-module (guix status)
#:use-module (guix progress)
#:use-module ((guix build utils)
#:select (call-with-temporary-output-file))
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-64)
#:use-module (srfi srfi-71)
@@ -116,6 +119,74 @@
(display "@ substituter-succeeded baz\n" port)
(list first (get-status)))))
(test-equal "compute-status, stray download-succeeded"
(build-status)
(let ((port get-status
(build-event-output-port (lambda (event status)
(compute-status event status
#:current-time
(const 'now))))))
(display "@ download-succeeded bar http://example.org/bar 999\n" port)
(get-status)))
(test-equal "compute-status, malformed download-progress"
(build-status
(downloading (list (download "bar" "http://example.org/bar"
#:size 999
#:start 'now))))
(let ((port get-status
(build-event-output-port (lambda (event status)
(compute-status event status
#:current-time
(const 'now))))))
(display "@ download-started bar http://example.org/bar 999\n" port)
(display "@ download-progress bar http://example.org/bar 999 nope\n" port)
(get-status)))
(test-equal "progress-reporter/trace, complete trace lines"
(string-append "@ download-started bar http://example.org/bar 999\n"
"@ download-progress bar http://example.org/bar 999 42\n"
"@ download-progress bar http://example.org/bar 999 999\n"
"@ download-succeeded bar http://example.org/bar 999\n")
(call-with-temporary-output-file
(lambda (file port)
(let ((reporter (progress-reporter/trace "bar" "http://example.org/bar"
999 port)))
(start-progress-reporter! reporter)
(progress-reporter-report! reporter 42)
(stop-progress-reporter! reporter)
(force-output port)
(call-with-input-file file get-string-all)))))
(test-equal "compute-status, overrun download-progress"
(build-status
(downloading (list (download "bar" "http://example.org/bar"
#:size 999
#:start 'now))))
(let ((port get-status
(build-event-output-port (lambda (event status)
(compute-status event status
#:current-time
(const 'now))))))
(display "@ download-started bar http://example.org/bar 999\n" port)
(display "@ download-progress bar htt@ 999 1000\n" port)
(get-status)))
(test-assert "print-build-event, overrun download-progress"
(let ((output (open-output-string)))
(call-with-values
(lambda ()
(build-event-output-port
(build-status-updater
(lambda (event old-status status)
(print-build-event/quiet event old-status status output
#:colorize? #f)))))
(lambda (port get-status)
(display "@ download-started bar http://example.org/bar 999\n" port)
(display "@ download-progress bar htt@ 999 1000\n" port)
(force-output port)
(build-status? (get-status))))))
(test-equal "build-output-port, UTF-8"
'((build-log #f "lambda is λ!\n"))
(let ((port get-status (build-event-output-port cons '()))
@@ -149,6 +220,19 @@
(force-output port)
(get-status)))
(test-equal "build-output-port, multiplexed CR-delimited traces"
'((download-started "bar" "http://example.org/bar" "999")
(build-log 121 "@ substituter-succeeded bar\n"))
(let* ((port get-status (build-event-output-port cons '()))
(payload (string->utf8
"@ download-started bar http://example.org/bar 999\r@ substituter-succeeded bar\n")))
(display (format #f "@ build-log 121 ~a\n"
(bytevector-length payload))
port)
(put-bytevector port payload)
(force-output port)
(reverse (get-status))))
(test-equal "compute-status, multiplexed build output"
(list (build-status
(building (list (build "foo.drv" "x86_64-linux" #:id 121)))