4

I am using Shiny with a local server installed.

My Shiny app runs a heavy local program using system/system2/processx::run. I am running it synchronously (wait=T). If the user closes the browser window of the Shiny, I would love the heavy program to end. If the user re-opens the browser window, I want the Shiny app to be ready again for executing the local program.

How can this be achieved?

When I use system/system2/processx::run, it seems that the app waits for the heavy program to finish and does not stop it upon close.

Reprex:

library(shiny)
library(processx)

ui <- fluidPage(
  actionButton("runBtn", label="Run a program that consumes many resources") ,
)

server <- function(input, output, session) {
  observeEvent(input$runBtn,
      run("sleep", "240"))
}
shinyApp(ui, server)

When I close the browser window with the reprex, and then try to re-open it, it takes time till the process ends. I want it to be available more or less immediately.

P.S. I am using Linux; a system-specific hack is fine.

Sam
  • 127
  • 10
  • 2
    Have a look at the `session` object it has several callbacks which you can use such as `onSessionEnded` https://shiny.rstudio.com/reference/shiny/latest/session.html. Then you can maybe kill the `PID` of the background script with `system` command https://stat.ethz.ch/R-manual/R-devel/library/base/html/system.html – Pork Chop Sep 22 '21 at 13:44
  • @PorkChop Thanks. However, I need the process to run synchronously, and not as a background process. I need the app to pause while the process is running, so it is the natural way to accomplish that. – Sam Sep 23 '21 at 09:19

1 Answers1

1

@PorkChop's comment is pointing in the right direction. However, I'd recommend using processx::process rather than run as it provides us with methods to control the started process from within R. See ?process. (run by the way is also based on the process class.)

The main problem here is, that running the process synchronously (wait=TRUE) blocks the R session. Accordingly onStop won't fire until R is back in control. Therefore you can't trigger anything once the browser window was closed because the shiny-session continues to run until the external program is finished and R can close the shiny-session.

On session end, the below code checks if the asynchronously started process is still alive and kills it if necessary (tested on windows only).

library(shiny)
library(processx)

ui <- fluidPage(
  actionButton("runBtn", label="Run a program that consumes many resources"),
  actionButton("stopSession", "Stop session")
)

server <- function(input, output, session) {
  
  myProcess <- NULL
  
  observeEvent(input$stopSession, {
    cat(sprintf("Closing session %s\n", session$token))
    session$close()
  })
  
  observeEvent(input$runBtn,
               {
                 if(Sys.info()[["sysname"]]=="Windows"){
                   writeLines(text = c("ping 127.0.0.1 -n 60 > nul"), con = "sleep.bat")
                   myProcess <<- process$new("cmd.exe", c("/c", "call", "sleep.bat"), supervise = TRUE, stdout = "")
                 } else {
                   myProcess <<- process$new("sleep", "60", supervise = TRUE, stdout = "")
                 }
                 # myProcess$wait() # wait for the process to finish
               })
  
  onStop(function(){
    cat(sprintf("Session %s was closed\n", session$token))
    if(!is.null(myProcess)){
      if(myProcess$is_alive()){
        myProcess$kill()
      }
    }
    
  })
}

shinyApp(ui, server)

Regarding the different session callback functions see this related post.


As requested here the process is wrapped in a reactiveVal:

library(shiny)
library(processx)

ui <- fluidPage(
  actionButton("runBtn", label="Run a program that consumes many resources"),
  actionButton("stopSession", "Stop session")
)

server <- function(input, output, session) {
  
  myProcess <- reactiveVal(NULL)
  
  observeEvent(input$stopSession, {
    cat(sprintf("Closing session %s\n", session$token))
    session$close()
  })
  
  observeEvent(input$runBtn,
               {
                 if(Sys.info()[["sysname"]]=="Windows"){
                   writeLines(text = c("ping 127.0.0.1 -n 60 > nul"), con = "sleep.bat")
                   myProcess(process$new("cmd.exe", c("/c", "call", "sleep.bat"), supervise = TRUE, stdout = ""))
                 } else {
                   myProcess(process$new("sleep", "60", supervise = TRUE, stdout = ""))
                 }
                 # myProcess()$wait() # wait for the process to finish
               })
  
  onStop(function(){
    cat(sprintf("Session %s was closed\n", session$token))
    if(!is.null(isolate(myProcess()))){
      if(isolate(myProcess()$is_alive())){
        isolate(myProcess()$kill())
      }
    }
    
  })
}

shinyApp(ui, server)
ismirsehregal
  • 30,045
  • 5
  • 31
  • 78
  • Thanks. However I need the process to run synchronously, not in background. (Meaning that the app should not function as long as the process runs). I guess that I could manually suspend the app till the process finishes, but a much simpler solution would be to stop the synchronous process on session end (if possible). – Sam Sep 23 '21 at 09:17
  • 1
    In my eyes that's the problem of your code. When you are running the process synchronously the R session is blocked. Accordingly `onStop` won't fire until R is back in control. Therefore you can't trigger anything once the browser window was closed because the shiny-session runs on until the external program is finished and R can close the shiny-session. If you want R to kill that process it needs time to run something. You can test this statement via `process$wait()` - please see my edit. – ismirsehregal Sep 23 '21 at 09:51
  • 1
    Accordingly I'd rather run the process asynchronously and disable the required UI elements while the process is active (which shouldn't be a big deal - e.g. see [this](https://rdrr.io/cran/shinyjs/man/stateFuncs.html)). – ismirsehregal Sep 23 '21 at 10:03
  • I see. Can you please add to the full answer a statement saying that this is not possible as long as the process is run synchronously? Then the user who looks at your answer will not have to dig through the comments to see the answer, and I will mark your answer as accepted. – Sam Sep 23 '21 at 10:21
  • So I need to create a timer that checks whether the asynchronous process has finished running and then restores the app? Do I need to use the ```promises``` & ```future``` packages? A tip would be great. – Sam Sep 23 '21 at 10:24
  • Also, can this be modularized? Can a process be wrapped in reactiveVal() ? This way the module would (1) run a timer disabling all UI elements until the process finish. (2) return the process run. The main server function would close this process when the session ends. – Sam Sep 23 '21 at 11:37
  • You could solve this via `promises` & `future` but I wouldn't recommend it in this case, because promises currently aim at providing inter-session responsiveness rather than intra-session responsiveness - However, [here](https://github.com/rstudio/promises/issues/23#issuecomment-386687705) is a workaround. You could use `reactivePoll` to check when the process finished (`process$is_alive()`) and finally use `process$get_result()`. – I added a version of the code wrapping the process in a `reactiveVal`. – ismirsehregal Sep 23 '21 at 11:59
  • btw, should not the supervisor have managed the functionality even without the manual process$kill()? As the manual says, if TRUE, the supervisor will ensure that the process is killed when the R process exits. – Sam Sep 23 '21 at 15:33
  • 1
    Yes, the supervisor will kill the process after R exits. However, ending the shiny-session (e.g. via closing the browser tab) doesn't end the R-session. The server function keeps on running, waiting for the next shiny-session. – ismirsehregal Sep 23 '21 at 17:54
  • Do you think it is better to wrap the process in a reactiveVal or not? (I have issued with both - https://stackoverflow.com/questions/69342809/using-reactivity-in-shiny-from-a-function and https://stackoverflow.com/questions/69349659/using-isolate-when-accessing-a-reactive-value ) – Sam Sep 28 '21 at 12:00
  • 1
    I don't see any advantage in wrapping it in a `reactiveVal`. – ismirsehregal Sep 28 '21 at 21:10