3

I have the below server.R code in shiny app where a system command is run inside future which gives an output.vcf file. Upon creation of this file the progress bar is removed and a second system command is run to convert out.vcf to out.txt

The system commands are used as R could not read huge vectors on a 32Gb machine. Hence some system commands are used to process the data.

The output produced in the first system command i.e. out.vcf has to be rendered to downloadHandler and the output from the second command out.txt has to be returned to renderDataTable.

Could someone suggest an efficient way of doing this? possibly running both the system commands inside the future() and returning the outputs to downloadHandler and renderDataTable.

server <- function(input, output, session) {
file_rows <- reactiveVal()

observeEvent(input$run, {
  prog <- Progress$new(session)
  prog$set(message = "Analysis in progress",
    detail = "This may take a while...",
    value = NULL)

  path <- input$uploadFile$datapath
  nrows <- input$nrows

  future({
    system(paste(
      "cat",
      input$uploadFile$datapath,
      "|",
      paste0("head -", input$nrows) ,
      ">",
      "out.vcf"
    ),
      intern = TRUE)
   read.delim("out.vcf")
  }) %...>%
    file_rows() %>%
    finally(~prog$close())
})



observeEvent(req(file_rows()), {
updateTabsetPanel(session, "input_tab", "results")
    rows_input <- file_rows()

    system(paste(
      "cat",
      rows_input,
      "|",
      paste(some system command"),
      ">",
      "out.txt"
    ),
      intern = TRUE)

##How could we render the content of "out.txt" from the above system command to datatable in the below code#######  
    output$out_table <-
      DT::renderDataTable(DT::datatable(
        out.txt,
        options = list(
          searching = TRUE,
          pageLength = 10,
          rownames(NULL),
          scrollX = T
        )
      ))

##How could we render the content of "out.vcf" from the first system command to downloadHandler in the below code#######    
output$out_VCFdownList <- downloadHandler(
      filename = function() {
        paste0("output", ".vcf")
      },
      content = function(file) {
        write.vcf("out.vcf from first system command ", file)
      }
    )
  })
Timur Shtatland
  • 12,024
  • 2
  • 30
  • 47
chas
  • 1,565
  • 5
  • 26
  • 54
  • Side note: you should probably use `shQuote` around your variable arguments within `system`. Do you need to return read-in and actually return the *contents* of `"out.vcf"`, or would it be sufficient to return just the filename signalling that its creation is complete? Then, your second `system` call could do `"cat out.csv | ..."` directly instead of trying to `cat` improperly formatted (for shell work) output? – r2evans Jul 11 '19 at 12:51
  • It is not required to return the contents of `out.vcf`, since i am not able to write the code signalling the completion, i am returning the contents of `out.vcf`. It would be great that could be done so that the second system command could directly read from the file. – chas Jul 11 '19 at 15:46
  • are you planning to add a minimum reproducible example? and could you explain what the downloadhandler is required for? (users dont have access to the directories where the out.vcf is stored?) – Tonio Liebrand Jul 23 '19 at 09:25
  • The code is a small piece from a big shiny app and i find it difficult to create a reproducible example. The shiny app runs on our local server and the users are allowed to used the app via the url. So the directory where the `out.vcf` is written from the first system command is not physically available to the users to browse to the disk and the file can be retrieved only from the downloadhandler. – chas Jul 23 '19 at 09:46
  • In the above code, i have used `read.delim("out.vcf")` after `out.vcf` is created to signal the completion of the first system command to the `observeEvent`. However, this is an inefficient way as R utilizes the maximum RAM to store the R object. Ideally, it would be sufficient to return the filename signaling that its creation is complete as @r2evans hinted. – chas Jul 23 '19 at 09:54
  • Subsequently, the second system command need to be executed on the file returned by the first system command. After executing both the system commands inside the `future()`, the output from first system command should be sent to `downloadHandler` and the output from second system command to `renderDataTable`. – chas Jul 23 '19 at 09:54
  • I think you've certainly identified the *"efficient way of doing this"*: in your single `future` block, make the first `system` call, and if its output is good, then make the second system call, and if that output is good, then return from the `future` and signal completion. In your `downloadHandler`, your `content=` argument is something like `function(fn) if (file.exists("previous-out.vcf") file.rename("previous-out.vcf", fn)`. – r2evans Jul 23 '19 at 14:53
  • i am absolutely stuck there itself i.e. looking for the piece of code that does execute system command and verify that output is good and then execute second command inside future. – chas Jul 23 '19 at 16:09
  • If you don't need to capture the stdout of the command, then with `system2(..., stdout=FALSE)` or `system(..., intern=FALSE)`, the value returned is an error code, "0" for success, non-zero for some error (and that number typically depends on the command called). – r2evans Jul 23 '19 at 21:32
  • (Though realize you should be using `stdout=some_file_name` to capture the command's output into a file.) – r2evans Jul 23 '19 at 21:52

1 Answers1

0

Try this simple "Happy to Glad" converter (and line-numberer).

Goal of this shiny app: given a text file, convert all occurrences of the string happy (case-sensitive) with glad. An input file, for demonstration:

This is a happy file.
It attempts to be very happy.

And the sample app, using a simple two-step command process.

Update: I've updated it to provide (1) progress, and (2) download of each file. Over to you if you want to disable one or the other download.

library(shiny)
library(future)
library(promises)
plan(transparent)

ui <- fluidPage(
  titlePanel("\"Happy\" to \"Glad\"!"),
  sidebarLayout(
    sidebarPanel(
      fileInput("infile", "Upload a text file:"),
      tags$hr(),
      actionButton("act", "Convert!"),
      tags$hr(),
      splitLayout(
        downloadButton("download1", label = "Download 1!"),
        downloadButton("download2", label = "Download 2!")
      )
    ),
    mainPanel(
      textAreaInput("intext", label = "Input", rows = 3),
      tags$hr(),
      textAreaInput("outtext", label = "Gladified", rows = 3)
    )
  )
)

server <- function(input, output, session) {
  outfile1 <- reactiveVal(NULL)
  outfile2 <- reactiveVal(NULL)

  observeEvent(input$act, {
    req(input$infile)
    prog <- Progress$new(session)
    prog$set(message = "Step 1 in progress",
             detail = "This may take a few moments...",
             value = NULL)
    future({
      Sys.sleep(2)
      outf1 <- tempfile()
      ret1 <- system2("sed", c("-e", "s/happy/glad/g",
                               shQuote(input$infile$datapath)),
                      stdout = outf1)
      if (ret1 == 0L && file.exists(outf1)) {
        outfile1(outf1)
      } else outf1 <- NULL
      outf1
    }) %...>%
      {
        outf1 <- .
        if (is.null(outf1) || !file.exists(outf1)) {
          prog$set(message = "Problems with Step 1?",
                   detail = "(process interrupted ...)",
                   value = NULL)
        } else {
          prog$set(message = "Step 2 in progress",
                   detail = "This may take a few moments...",
                   value = NULL)
        }
        outf1
      } %...>%
      {
        future({
          outf1 <- .
          if (!is.null(outf1inf) && file.exists(outf1)) {
            Sys.sleep(2)
            outf2 <- tempfile()
            ret2 <- system2("cat", c("-n", shQuote(outf1)),
                            stdout = outf2)
            if (ret2 == 0L && file.exists(outf2)) {
              outfile2(outf2)
            } else outf2 <- NULL
          }
          list(outf1, outf2)
        })
      } %...>%
      {
        bothfiles <- .
        if (is.null(bothfiles[[1]])) {
          # do nothing, we already saw the progress-error
        } else if (is.null(bothfiles[[2]]) || !file.exists(bothfiles[[2]])) {
          prog$set(message = "Problems with Step 2?",
                   detail = "(process interrupted ...)",
                   value = NULL)
        }
      } %>%
      finally(~ prog$close())
  })

  observeEvent(input$infile, {
    req(input$infile$datapath, file.exists(input$infile$datapath))
    txt <- readLines(input$infile$datapath, n = 10)
    updateTextAreaInput(session, "intext", value = paste(txt, collapse = "\n"))
  })

  observeEvent(outfile2(), {
    req(outfile2(), file.exists(outfile2()))
    txt <- readLines(outfile2(), n = 10)
    updateTextAreaInput(session, "outtext", value = paste(txt, collapse = "\n"))
  })

  output$download1 <- downloadHandler(
    filename = function() {
      req(input$infile)
      paste0(basename(input$infile$name), "_gladified")
    },
    content = function(file) {
      req(outfile1())
      file.copy(outfile1(), file)
    }
  )

  output$download2 <- downloadHandler(
    filename = function() {
      req(input$infile)
      paste0(basename(input$infile$name), "_gladified_and_numbered")
    },
    content = function(file) {
      req(outfile2())
      file.copy(outfile2(), file)
    }
  )

}

shinyApp(ui, server)

Notes:

  • It is not very smart, so for each if (ret1 == 0L), you should have an else clause that presents some error message to the user if non-zero;
  • It is a little inefficient in that it makes a copy of the output file instead of renaming it. I chose this because renaming it would allow a download only once.
  • I haven't spent a lot of time troubleshooting what happens with failed processing; while I think the progress markers I've put in are decent, you might want more testing of fail-states;
  • It would likely be useful to include judicious use of shinyjs::toggleState on the download buttons so that you can't download what does not exist.
  • And finally, I'm not truly thrilled with having such a gargantuan observeEvent with multiple future steps; it'd be nice to function-ize the steps or generalize for an arbitrary number of steps.

screenshot of shiny app, mid-process

r2evans
  • 141,215
  • 6
  • 77
  • 149
  • This is not technically "signalling on file-creation", as that might suggest use of `shiny::reactiveFileReader`. Not horrible, granted, but I feel that polling can be unnecessary, especially when we control both the file creation and use. – r2evans Jul 23 '19 at 22:39
  • little confused, `outfile()` is returning the output from second system command whereas the `downloadHandler` is required to get the output from first system command. – chas Jul 24 '19 at 08:55
  • also how could we embed the progress status of the `future` as in the OP. – chas Jul 24 '19 at 10:55
  • If you need `outfile()` to point to file1, then ... point it to file1. I'll edit and show what I mean ... it's just a pointer to the file you want. – r2evans Jul 24 '19 at 16:13
  • 1
    i will give a try and then accept the answer!! And could u elaborate on functionizing as it may be crucial for my app to implement such functions. – chas Jul 24 '19 at 17:29
  • I prefer to keep my `server` components as short as possible, using semantic function names. While I don't assume that a function will be used more than once (a common reason to convert a process into a generalized function), if nothing else it makes the intent of that `observe` block much more self-evident, while still allowing you to see neighboring `output$`/`reactive`/`observe` chunks clearly. Once you start working with shiny modules, it becomes a lot more evident how to do this, but that takes time (and is out of scope for this Q/A). – r2evans Jul 24 '19 at 17:32
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/196946/discussion-between-chas-and-r2evans). – chas Jul 24 '19 at 18:15
  • Whenever you deal with promises and a `{...}` block of code, the data from the previous promise is passed into this block as `.`. Most of the time, I find it useful for visualizing code to reassign it, but `.` can still technically be used. Some of the time, it is actually *required* to assign it elsewhere, as in when the dot is interpreted specially within other function calls (e.g., other promises, many `dplyr` or related functions, etc). – r2evans Jul 29 '19 at 12:57
  • And neither `outfile(outf1)` nor `outf1` nor is used in the second system command. Could lmk how to use `outf1` in the second system command as input? – chas Jul 29 '19 at 16:52
  • (1) In the *second* system command, why would you want to change the `reactiveVal` of `outf1`? It should already have been set earlier in the promise-chain. (2) I named the *input filename* for any of the blocks `inf` for consistency (I thought it might make it easier to extend later), but for clarity (though with slight loss of generality), I just changed `inf` to be `outf1` or `bothfiles`, depending on its location in the chain. – r2evans Jul 29 '19 at 17:02
  • Please don't edit an *answer* to extend your *question*. I don't have your code, as this answer works as-is. The [link](https://community.rstudio.com/t/shiny-future-error-in-ctx-oninvalidate-reactive-context-was-created-in-one-process-and-accessed-from-another/17349/3) might be appropriate, but only in the context of your code. I suggest you open a new question. – r2evans Sep 06 '19 at 14:48