r-lib/processx

Stdout and stderr are not in chronological order when redirected to the same file.

wlandau opened this issue · 3 comments

When I start an R process and set stdout and stderr to the same file path, the ordering of text in the output is counterintuitive. The stderr stream appears first, then part of the R startup message (with part of the first line missing), and then stdout. There's also something happening in the last couple lines that I do not understand. All this happens even if writing to stderr is the last thing the task does. Is this expected under normal circumstances?

log <- tempfile()
handle <- processx::process$new(
  command = file.path(R.home("bin"), "R"),
  args = c("-e", "print(\"this-print\"); message(\"this-message\")"),
  cleanup = TRUE,
  stdout = log,
  stderr = log
)
while (is.null(handle$get_exit_status())) {
  Sys.sleep(0.1)
}
writeLines(readLines(log))
#> this-message
#> 3.2 (2023-10-31) -- "Eye Holes"
#> Copyright (C) 2023 The R Foundation for Statistical Computing
#> Platform: aarch64-apple-darwin20 (64-bit)
#> 
#> R is free software and comes with ABSOLUTELY NO WARRANTY.
#> You are welcome to redistribute it under certain conditions.
#> Type 'license()' or 'licence()' for distribution details.
#> 
#>   Natural language support but running in an English locale
#> 
#> R is a collaborative project with many contributors.
#> Type 'contributors()' for more information and
#> 'citation()' on how to cite R or R packages in publications.
#> 
#> Type 'demo()' for some demos, 'help()' for on-line help, or
#> 'help.start()' for an HTML browser interface to help.
#> Type 'q()' to quit R.
#> 
#> > print("this-print"); message("this-message")
#> [1] "this-print"
#> > 
#> >

Created on 2024-01-04 with reprex v2.0.2

I would prefer to see:

#> R version 4.3.2 (2023-10-31) -- "Eye Holes"
#> Copyright (C) 2023 The R Foundation for Statistical Computing
#> Platform: aarch64-apple-darwin20 (64-bit)
#> 
#> R is free software and comes with ABSOLUTELY NO WARRANTY.
#> You are welcome to redistribute it under certain conditions.
#> Type 'license()' or 'licence()' for distribution details.
#> 
#>   Natural language support but running in an English locale
#> 
#> R is a collaborative project with many contributors.
#> Type 'contributors()' for more information and
#> 'citation()' on how to cite R or R packages in publications.
#> 
#> Type 'demo()' for some demos, 'help()' for on-line help, or
#> 'help.start()' for an HTML browser interface to help.
#> Type 'q()' to quit R.
#> 
#> > print("this-print"); message("this-message")
#> [1] "this-print"
#> this-message

Is there a way to make the output lines respect chronological order when redirecting to the same log file in processx?

Oddly enough, callr prints the streams in the order I would expect. However, I would still prefer to use processx without callr for my purposes.

log <- tempfile()
callr::r(
  \() {
    print("this-print")
    message("this-message")
  },
  cmdargs = character(0L),
  cleanup = TRUE,
  stdout = log,
  stderr = log
)
#> NULL
writeLines(readLines(log))
#> 
#> R version 4.3.2 (2023-10-31) -- "Eye Holes"
#> Copyright (C) 2023 The R Foundation for Statistical Computing
#> Platform: aarch64-apple-darwin20 (64-bit)
#> 
#> R is free software and comes with ABSOLUTELY NO WARRANTY.
#> You are welcome to redistribute it under certain conditions.
#> Type 'license()' or 'licence()' for distribution details.
#> 
#>   Natural language support but running in an English locale
#> 
#> R is a collaborative project with many contributors.
#> Type 'contributors()' for more information and
#> 'citation()' on how to cite R or R packages in publications.
#> 
#> Type 'demo()' for some demos, 'help()' for on-line help, or
#> 'help.start()' for an HTML browser interface to help.
#> Type 'q()' to quit R.
#> 
#> > {
#> +     tryCatch(withCallingHandlers({
#> +         NULL
#> +         saveRDS(do.call(do.call, c(readRDS("/var/folders/4v/vh7xp8553lsbl49svl48g7p00000gp/T//Rtmp8fEOYH/callr-fun-da9e9c611aa"), 
#> +             list(envir = .GlobalEnv, quote = TRUE)), envir = .GlobalEnv, 
#> +             quote = TRUE), file = "/var/folders/4v/vh7xp8553lsbl49svl48g7p00000gp/T//Rtmp8fEOYH/callr-res-da9e2ab1b4e9", 
#> +             compress = FALSE)
#> +         flush(stdout())
#> +         flush(stderr())
#> +         NULL
#> +         invisible()
#> +     }, error = function(e) {
#> +         {
#> +             callr_data <- as.environment("tools:callr")$`__callr_data__`
#> +             err <- callr_data$err
#> +             if (FALSE) {
#> +                 assign(".Traceback", .traceback(4), envir = callr_data)
#> +                 dump.frames("__callr_dump__")
#> +                 assign(".Last.dump", .GlobalEnv$`__callr_dump__`, 
#> +                   envir = callr_data)
#> +                 rm("__callr_dump__", envir = .GlobalEnv)
#> +             }
#> +             e <- err$process_call(e)
#> +             e2 <- err$new_error("error in callr subprocess")
#> +             class(e2) <- c("callr_remote_error", class(e2))
#> +             e2 <- err$add_trace_back(e2)
#> +             cut <- which(e2$trace$scope == "global")[1]
#> +             if (!is.na(cut)) {
#> +                 e2$trace <- e2$trace[-(1:cut), ]
#> +             }
#> +             saveRDS(list("error", e2, e), file = paste0("/var/folders/4v/vh7xp8553lsbl49svl48g7p00000gp/T//Rtmp8fEOYH/callr-res-da9e2ab1b4e9", 
#> +                 ".error"))
#> +         }
#> +     }, interrupt = function(e) {
#> +         {
#> +             callr_data <- as.environment("tools:callr")$`__callr_data__`
#> +             err <- callr_data$err
#> +             if (FALSE) {
#> +                 assign(".Traceback", .traceback(4), envir = callr_data)
#> +                 dump.frames("__callr_dump__")
#> +                 assign(".Last.dump", .GlobalEnv$`__callr_dump__`, 
#> +                   envir = callr_data)
#> +                 rm("__callr_dump__", envir = .GlobalEnv)
#> +             }
#> +             e <- err$process_call(e)
#> +             e2 <- err$new_error("error in callr subprocess")
#> +             class(e2) <- c("callr_remote_error", class(e2))
#> +             e2 <- err$add_trace_back(e2)
#> +             cut <- which(e2$trace$scope == "global")[1]
#> +             if (!is.na(cut)) {
#> +                 e2$trace <- e2$trace[-(1:cut), ]
#> +             }
#> +             saveRDS(list("error", e2, e), file = paste0("/var/folders/4v/vh7xp8553lsbl49svl48g7p00000gp/T//Rtmp8fEOYH/callr-res-da9e2ab1b4e9", 
#> +                 ".error"))
#> +         }
#> +     }, callr_message = function(e) {
#> +         try(signalCondition(e))
#> +     }), error = function(e) {
#> +         NULL
#> +         if (TRUE) {
#> +             try(stop(e))
#> +         }
#> +         else {
#> +             invisible()
#> +         }
#> +     }, interrupt = function(e) {
#> +         NULL
#> +         if (TRUE) {
#> +             e
#> +         }
#> +         else {
#> +             invisible()
#> +         }
#> +     })
#> + }
#> [1] "this-print"
#> this-message
#> >

Created on 2024-01-04 with reprex v2.0.2

This seems like a bug to me, but a better way to collect stdout and stderr together is stderr = "2>&1", see the docs.

log <- tempfile()
handle <- processx::process$new(
  command = file.path(R.home("bin"), "R"),
  args = c("-q", "-e", "print(\"this-print\"); message(\"this-message\")"),
  cleanup = TRUE,
  stdout = log,
  stderr = "2>&1"
)
handle$wait()
writeLines(readLines(log))
> print("this-print"); message("this-message")
[1] "this-print"
this-message

Awesome, works for me. Thanks!