rshinyshiny-serversystem2

How does one terminate all processes upon Shiny app end?


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.


Solution

  • @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)