rstudio/shiny

Validate path as shiny app

JosiahParry opened this issue ยท 11 comments

The shiny::runApp() function is very flexible and will run an app from a path e.g. app.R www/index.html or from a directory that contains a ui.R and server.R function.

It would be great to have a function to validate that a path is indeed a shiny app without having to run runApp() and catch if an error occurs.

If this is for a third-party tool to help run Shiny apps, I'd recommend either requiring your users to follow a naming convention established by your tool or to ask users to explicitly name the primary file containing the Shiny app.

Unfortunately, when shiny::runApp() is given an .R file that purports to produce a Shiny application, it'd be very difficult to confirm that the .R file indeed creates a Shiny app without actually running the code.

Understood. Do you have a suggestion on how to try validating a shiny app via shiny::runApp()? The hard part is that it's blocking. ๐Ÿค”

You could maybe start the app in a background R session with callr? You could also source the app script and inspect the return value. According to the runApp() docs it should be a shiny app object or a list with ui and server components (if it doesn't follow naming conventions).

If you can describe your use case a little more, I might be able to provide some more targeted advice. ๐Ÿ˜„

The goal is to prepare a shiny app for deployment. The directory is zipped, copied, and moved else where. Then from that location shiny::runApp() is called supplying only the directory path as that is the most flexible (based on doc).

However before all of that stuff is done, my goal is to verify that the app is indeed a certified runnable shiny app!

{callr} is a good helper. thank you! I think this is sufficient and quite handy. Though I'm sure i'm missing something with the error and listening on check.

iis_shiny_app <- function(app_dir, max_check_time = 10L, poll_duration = 5000L) {
  # store when check first starts
  start_check_time <- Sys.time()

  # helper function to start shiny app on random port
  run_app <- function(app_dir) shiny::runApp(app_dir, port = httpuv::randomPort())

  # start app in background process
  rp <- callr::r_bg(run_app, list(app_dir = app_dir))

  # set polling duration (5 sec default)
  .poll_res <- rp$poll_io(poll_duration) # set polling duration
  while (TRUE) {
    # read a line from the output
    line <- rp$read_error_lines()
    # if there is output check
    if (length(line) > 0) {
      error_detected <- any(grepl("Error in shinyAppDir", line))
      app_start_detected <- any(grepl("Listening on", line))
      if (error_detected || app_start_detected) {
        break
      }
    }
    # check the time diff
    check_length <- difftime(
      Sys.time(),
      start_check_time,
      units = "secs"
    )

    # if ther time check is exceeded, error out
    if (check_length > max_check_time) {
      cli::cli_abort(
        c(
          "Cannot determine if valid shiny app",
          i = "Waited {max_check_time} seconds."
        )
      )
    }
  }

  # kill the process
  .killed_process <- rp$kill()

  # return the check
  !error_detected
}

Why don't you use shinyAppDir()?

It will throw an error if the directory doesn't contain a vaild shiny app, which you could tryCatch.

library(shiny)
try_app <- tryCatch({shinyAppDir("C:/path/to/shiny/app")}, error = function(e) {e})
is.shiny.appobj(try_app)

@ismirsehregal because in the case of success the function will run indefinitely

@JosiahParry: No it won't, as shinyAppDir doesn't run the app, it only creates a shiny app object which you need to print or directly pass to runApp to be executed (just as shinyApp()).

Please see ?shinyAppDir

Value: An object that represents the app. Printing the object or passing it to runApp() will run the app.


Example:

library(shiny)
valid_path <- file.path(find.package("shiny"), "examples", "01_hello")
invalid_path <- file.path(find.package("shiny"), "examples", "invalid")
try_app <- tryCatch({shinyAppDir(valid_path)}, error = function(e) {e})
if (is.shiny.appobj(try_app)) {
  message("This is a valid shiny app directory.")
  if (interactive()) {
    run <- readline(message("Type 'yes' or 'y' to run it: "))
    if (tolower(run) == "yes" || tolower(run) == "y") {
      # print(try_app) # alternative
      runApp(try_app)
    }
  }
} else {
  message("This is not a shiny app.")
}

@ismirsehregal thank you for this hint! This works much better:

is_shiny_app <- function(path) {
    tryCatch(
      !is.null(shiny::shinyAppDir(path)),
      error = function(cond) FALSE
    )
}

Thanks @ismirsehregal and @JosiahParry, that's an elegant solution!

I feel that this has been resolved as a one off matter but the shiny package would benefit from having an official way to do this.

Iโ€™d respectfully request that this issue be reopened as a feature request.

Thanks!