mirror of
https://github.com/rstudio/shiny.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
40 Commits
resize-obs
...
non-blocki
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4c91f62a1 | ||
|
|
88b4facb8f | ||
|
|
957b50d3b6 | ||
|
|
da50bf2249 | ||
|
|
72636ef4a0 | ||
|
|
02a5e0b40f | ||
|
|
0ff93d411f | ||
|
|
bd250962e4 | ||
|
|
3a0e8627a4 | ||
|
|
b813adec56 | ||
|
|
f29fa65af9 | ||
|
|
36e7a330d6 | ||
|
|
6d984266f9 | ||
|
|
c27c186c0f | ||
|
|
2907e83c42 | ||
|
|
45985690b2 | ||
|
|
ce11abe46d | ||
|
|
7e8903f754 | ||
|
|
664cbe2858 | ||
|
|
63af3649c8 | ||
|
|
3cb928e894 | ||
|
|
8e63d08d8a | ||
|
|
1db26f60af | ||
|
|
1432920a7e | ||
|
|
3882d1e4c3 | ||
|
|
532c17081a | ||
|
|
329bc979c6 | ||
|
|
0456847883 | ||
|
|
6bbe29a390 | ||
|
|
c8bfa93747 | ||
|
|
27134d9c66 | ||
|
|
48540283a4 | ||
|
|
49b76badcc | ||
|
|
935de77aee | ||
|
|
8b53c6d2fd | ||
|
|
3ccbad7a70 | ||
|
|
13812b45a7 | ||
|
|
08680d9566 | ||
|
|
620e5a277b | ||
|
|
bb26c0f4d3 |
@@ -126,6 +126,7 @@ Encoding: UTF-8
|
||||
Roxygen: list(markdown = TRUE)
|
||||
RoxygenNote: 7.3.3
|
||||
Collate:
|
||||
'app-handle.R'
|
||||
'globals.R'
|
||||
'app-state.R'
|
||||
'app_template.R'
|
||||
|
||||
@@ -276,6 +276,7 @@ export(snapshotPreprocessInput)
|
||||
export(snapshotPreprocessOutput)
|
||||
export(span)
|
||||
export(splitLayout)
|
||||
export(startApp)
|
||||
export(stopApp)
|
||||
export(strong)
|
||||
export(submitButton)
|
||||
|
||||
7
NEWS.md
7
NEWS.md
@@ -1,5 +1,12 @@
|
||||
# shiny (development version)
|
||||
|
||||
## New features
|
||||
|
||||
* New `startApp()` runs a Shiny app in non-blocking mode, returning a
|
||||
`ShinyAppHandle` object with `stop()`, `status()`, `url()`, and `result()`
|
||||
methods. When a new app is started, any previously running non-blocking app
|
||||
is automatically stopped.
|
||||
|
||||
# shiny 1.13.0
|
||||
|
||||
## New features
|
||||
|
||||
73
R/app-handle.R
Normal file
73
R/app-handle.R
Normal file
@@ -0,0 +1,73 @@
|
||||
# Handle returned by startApp()
|
||||
ShinyAppHandle <- R6::R6Class("ShinyAppHandle",
|
||||
cloneable = FALSE,
|
||||
|
||||
public = list(
|
||||
initialize = function(appUrl, cleanupFn) {
|
||||
private$appUrl <- appUrl
|
||||
private$cleanupFn <- cleanupFn
|
||||
|
||||
reg.finalizer(self, function(e) {
|
||||
tryCatch(e$stop(), error = function(cnd) NULL)
|
||||
}, onexit = TRUE)
|
||||
},
|
||||
|
||||
stop = function() {
|
||||
if (self$status() != "running") {
|
||||
return(invisible(self))
|
||||
}
|
||||
private$stopped <- TRUE
|
||||
private$captureResult()
|
||||
private$cleanupFn()
|
||||
private$cleanupFn <- NULL
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
url = function() private$appUrl,
|
||||
|
||||
status = function() {
|
||||
if (!private$stopped) {
|
||||
"running"
|
||||
} else if (!is.null(private$resultError)) {
|
||||
"error"
|
||||
} else {
|
||||
"success"
|
||||
}
|
||||
},
|
||||
|
||||
result = function() {
|
||||
if (self$status() == "running") {
|
||||
stop("App is still running. Use status() to check if the app has stopped.")
|
||||
}
|
||||
if (!is.null(private$resultError)) {
|
||||
stop(private$resultError)
|
||||
}
|
||||
private$resultValue
|
||||
},
|
||||
|
||||
print = function(...) {
|
||||
cat("Shiny app handle\n")
|
||||
cat(" URL: ", private$appUrl, "\n", sep = "")
|
||||
cat(" Status:", self$status(), "\n")
|
||||
invisible(self)
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
appUrl = NULL,
|
||||
cleanupFn = NULL,
|
||||
# Whether this handle has been stopped. Distinct from .globals$stopped
|
||||
# which tracks whether a stop was requested (set by stopApp() or stop()).
|
||||
stopped = FALSE,
|
||||
resultValue = NULL,
|
||||
resultError = NULL,
|
||||
|
||||
captureResult = function() {
|
||||
if (isTRUE(.globals$reterror)) {
|
||||
private$resultError <- .globals$retval
|
||||
} else if (!is.null(.globals$retval)) {
|
||||
private$resultValue <- .globals$retval$value
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
305
R/runapp.R
305
R/runapp.R
@@ -46,6 +46,12 @@
|
||||
#' only used for recording or running automated tests. Defaults to the
|
||||
#' `shiny.testmode` option, or FALSE if the option is not set.
|
||||
#'
|
||||
#' @return The value passed to [stopApp()], or throws an error if the app was
|
||||
#' stopped with an error.
|
||||
#'
|
||||
#' @seealso [startApp()] for non-blocking mode, [stopApp()] to stop a running
|
||||
#' app.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # Start app in the current working directory
|
||||
@@ -93,18 +99,14 @@ runApp <- function(
|
||||
display.mode=c("auto", "normal", "showcase"),
|
||||
test.mode=getOption('shiny.testmode', FALSE)
|
||||
) {
|
||||
|
||||
# * Wrap **all** execution of the app inside the otel promise domain
|
||||
# * While this could be done at a lower level, it allows for _anything_ within
|
||||
# shiny's control to allow for the opportunity to have otel active spans be
|
||||
# reactivated upon promise domain restoration
|
||||
promises::local_otel_promise_domain()
|
||||
|
||||
on.exit({
|
||||
handlerManager$clear()
|
||||
}, add = TRUE)
|
||||
|
||||
if (isRunning()) {
|
||||
# Check for nested blocking runApp() before sourcing app code
|
||||
if (isRunning() && is.null(.globals$runningHandle)) {
|
||||
stop("Can't call `runApp()` from within `runApp()`. If your ",
|
||||
"application code contains `runApp()`, please remove it.")
|
||||
}
|
||||
@@ -116,14 +118,13 @@ runApp <- function(
|
||||
warn = max(1, getOption("warn", default = 1)),
|
||||
pool.scheduler = scheduleTask
|
||||
)
|
||||
on.exit(options(ops), add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Global onStart/onStop callbacks
|
||||
# ============================================================================
|
||||
# Invoke user-defined onStop callbacks, before the application's internal
|
||||
# onStop callbacks.
|
||||
on.exit({
|
||||
# Ensure options are restored and onStop callbacks fire even if
|
||||
# as.shiny.appobj() errors. Once .setupShinyApp() succeeds, the returned
|
||||
# cleanup function takes over and this guard becomes a no-op.
|
||||
setupComplete <- FALSE
|
||||
on.exit(if (!setupComplete) {
|
||||
options(ops)
|
||||
.globals$onStopCallbacks$invoke()
|
||||
.globals$onStopCallbacks <- Callbacks$new()
|
||||
}, add = TRUE)
|
||||
@@ -135,32 +136,140 @@ runApp <- function(
|
||||
# ============================================================================
|
||||
appParts <- as.shiny.appobj(appDir)
|
||||
|
||||
# ============================================================================
|
||||
# Initialize app state object
|
||||
# ============================================================================
|
||||
# This is so calls to getCurrentAppState() can be used to find (A) whether an
|
||||
# app is running and (B), get options and data associated with the app.
|
||||
initCurrentAppState(appParts)
|
||||
on.exit(clearCurrentAppState(), add = TRUE)
|
||||
# Any shinyOptions set after this point will apply to the current app only
|
||||
# (and will not persist after the app stops).
|
||||
result <- .setupShinyApp(
|
||||
appDir, appParts, port, launch.browser, host,
|
||||
workerId, quiet, display.mode, test.mode, ops = ops
|
||||
)
|
||||
setupComplete <- TRUE
|
||||
on.exit(result$cleanup(), add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# shinyOptions
|
||||
# Run event loop via httpuv
|
||||
# ============================================================================
|
||||
# A unique identifier associated with this run of this application. It is
|
||||
# shared across sessions.
|
||||
shinyOptions(appToken = createUniqueId(8))
|
||||
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
|
||||
# reactive(), Callbacks$invoke(), and others
|
||||
..stacktraceoff..(
|
||||
captureStackTraces({
|
||||
while (!.globals$stopped) {
|
||||
..stacktracefloor..(serviceApp())
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
# Set up default cache for app.
|
||||
if (is.null(getShinyOption("cache", default = NULL))) {
|
||||
shinyOptions(cache = cachem::cache_mem(max_size = 200 * 1024^2))
|
||||
if (isTRUE(.globals$reterror)) {
|
||||
stop(.globals$retval)
|
||||
} else if (.globals$retval$visible) {
|
||||
.globals$retval$value
|
||||
} else {
|
||||
invisible(.globals$retval$value)
|
||||
}
|
||||
}
|
||||
|
||||
# Extract appOptions (which is a list) and store them as shinyOptions, for
|
||||
# this app. (This is the only place we have to store settings that are
|
||||
# accessible both the UI and server portion of the app.)
|
||||
applyCapturedAppOptions(appParts$appOptions)
|
||||
#' Start Shiny Application (Non-Blocking)
|
||||
#'
|
||||
#' Starts a Shiny application in non-blocking mode, returning a
|
||||
#' `ShinyAppHandle` immediately while the app runs in the background.
|
||||
#' The `later` event loop services the app, so the R console remains
|
||||
#' available for interaction.
|
||||
#'
|
||||
#' @inheritParams runApp
|
||||
#'
|
||||
#' @return A `ShinyAppHandle` object with methods `stop()`, `status()`,
|
||||
#' `url()`, and `result()`. The `status()` method returns `"running"`,
|
||||
#' `"success"`, or `"error"`. The `result()` method throws an error if called
|
||||
#' while running, or re-throws the error if the app stopped with an error.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # Start app in the background
|
||||
#' handle <- startApp("myapp")
|
||||
#'
|
||||
#' # Check status
|
||||
#' handle$status()
|
||||
#' handle$url()
|
||||
#'
|
||||
#' # Stop the app
|
||||
#' handle$stop()
|
||||
#' }
|
||||
#'
|
||||
#' @seealso [runApp()] for blocking mode, [stopApp()] to stop a running app.
|
||||
#' @export
|
||||
startApp <- function(
|
||||
appDir = getwd(),
|
||||
port = getOption("shiny.port"),
|
||||
launch.browser = getOption("shiny.launch.browser", interactive()),
|
||||
host = getOption("shiny.host", "127.0.0.1"),
|
||||
workerId = "",
|
||||
quiet = FALSE,
|
||||
display.mode = c("auto", "normal", "showcase"),
|
||||
test.mode = getOption("shiny.testmode", FALSE)
|
||||
) {
|
||||
# OTEL: `local_otel_promise_domain()` ties its lifetime to this frame,
|
||||
# which exits as soon as the handle is returned — before any request is
|
||||
# served. A persistent global install would instead leak into unrelated
|
||||
# user promises between ticks. Wrap the synchronous setup below (covers
|
||||
# onStart) and each service iteration in `serviceNonBlocking()` (covers
|
||||
# handlers and observers). The domain is dormant between ticks, so it
|
||||
# stays out of user promises created at the console.
|
||||
|
||||
# Make warnings print immediately
|
||||
# Set pool.scheduler to support pool package
|
||||
ops <- options(
|
||||
# Raise warn level to 1, but don't lower it
|
||||
warn = max(1, getOption("warn", default = 1)),
|
||||
pool.scheduler = scheduleTask
|
||||
)
|
||||
|
||||
# Ensure options are restored and onStop callbacks fire even if
|
||||
# as.shiny.appobj() errors. See matching guard in runApp().
|
||||
setupComplete <- FALSE
|
||||
on.exit(if (!setupComplete) {
|
||||
options(ops)
|
||||
.globals$onStopCallbacks$invoke()
|
||||
.globals$onStopCallbacks <- Callbacks$new()
|
||||
}, add = TRUE)
|
||||
|
||||
require(shiny)
|
||||
|
||||
result <- promises::with_otel_promise_domain({
|
||||
appParts <- as.shiny.appobj(appDir)
|
||||
.setupShinyApp(
|
||||
appDir, appParts, port, launch.browser, host,
|
||||
workerId, quiet, display.mode, test.mode, ops = ops
|
||||
)
|
||||
})
|
||||
setupComplete <- TRUE
|
||||
|
||||
handle <- ShinyAppHandle$new(result$appUrl, result$cleanup)
|
||||
.globals$runningHandle <- handle
|
||||
serviceNonBlocking(handle, .globals$serviceGeneration)
|
||||
handle
|
||||
}
|
||||
|
||||
# Shared initialization for runApp() and startApp().
|
||||
# Handles all app setup: options, state, httpuv server, browser launch, etc.
|
||||
# Returns list(appUrl, cleanup) where cleanup() tears down the app.
|
||||
# On setup failure, internal on.exit handlers clean up partial state.
|
||||
.setupShinyApp <- function(appDir, appParts, port, launch.browser, host,
|
||||
workerId, quiet, display.mode, test.mode, ops,
|
||||
caller = parent.frame()) {
|
||||
# Guard on.exit handlers with this flag so they only fire on setup failure.
|
||||
# On success, cleanup responsibility is handed to the caller via the
|
||||
# returned cleanup function.
|
||||
cleanupOnExit <- TRUE
|
||||
|
||||
on.exit(if (cleanupOnExit) handlerManager$clear(), add = TRUE)
|
||||
|
||||
if (isRunning()) {
|
||||
if (!is.null(.globals$runningHandle)) {
|
||||
message("Stopping running Shiny app.")
|
||||
.globals$runningHandle$stop()
|
||||
} else {
|
||||
stop("Can't start a new app while another is running. ",
|
||||
"If your application code contains `runApp()` or `startApp()`, remove it. ",
|
||||
"Otherwise, stop the current app first with stopApp().")
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# runApp options set via shinyApp(options = list(...))
|
||||
@@ -182,25 +291,55 @@ runApp <- function(
|
||||
# | no | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
# | yes | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
#
|
||||
# I tried to make this as compact and intuitive as possible,
|
||||
# given that there are four distinct possibilities to check
|
||||
# `missing()` runs in the caller's frame: with defaults on the outer
|
||||
# formals, arguments are no longer missing by the time they reach here.
|
||||
appOps <- appParts$options
|
||||
findVal <- function(arg, default) {
|
||||
if (arg %in% names(appOps)) appOps[[arg]] else default
|
||||
}
|
||||
if (evalq(missing(port), caller)) port <- findVal("port", port)
|
||||
if (evalq(missing(launch.browser), caller)) launch.browser <- findVal("launch.browser", launch.browser)
|
||||
if (evalq(missing(host), caller)) host <- findVal("host", host)
|
||||
if (evalq(missing(quiet), caller)) quiet <- findVal("quiet", quiet)
|
||||
if (evalq(missing(display.mode), caller)) display.mode <- findVal("display.mode", display.mode)
|
||||
if (evalq(missing(test.mode), caller)) test.mode <- findVal("test.mode", test.mode)
|
||||
|
||||
if (missing(port))
|
||||
port <- findVal("port", port)
|
||||
if (missing(launch.browser))
|
||||
launch.browser <- findVal("launch.browser", launch.browser)
|
||||
if (missing(host))
|
||||
host <- findVal("host", host)
|
||||
if (missing(quiet))
|
||||
quiet <- findVal("quiet", quiet)
|
||||
if (missing(display.mode))
|
||||
display.mode <- findVal("display.mode", display.mode)
|
||||
if (missing(test.mode))
|
||||
test.mode <- findVal("test.mode", test.mode)
|
||||
on.exit(if (cleanupOnExit) options(ops), add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Global onStart/onStop callbacks
|
||||
# ============================================================================
|
||||
on.exit(if (cleanupOnExit) {
|
||||
.globals$onStopCallbacks$invoke()
|
||||
.globals$onStopCallbacks <- Callbacks$new()
|
||||
}, add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Initialize app state object
|
||||
# ============================================================================
|
||||
# This is so calls to getCurrentAppState() can be used to find (A) whether an
|
||||
# app is running and (B), get options and data associated with the app.
|
||||
initCurrentAppState(appParts)
|
||||
on.exit(if (cleanupOnExit) clearCurrentAppState(), add = TRUE)
|
||||
# Any shinyOptions set after this point will apply to the current app only
|
||||
# (and will not persist after the app stops).
|
||||
|
||||
# ============================================================================
|
||||
# shinyOptions
|
||||
# ============================================================================
|
||||
# A unique identifier associated with this run of this application. It is
|
||||
# shared across sessions.
|
||||
shinyOptions(appToken = createUniqueId(8))
|
||||
|
||||
# Set up default cache for app.
|
||||
if (is.null(getShinyOption("cache", default = NULL))) {
|
||||
shinyOptions(cache = cachem::cache_mem(max_size = 200 * 1024^2))
|
||||
}
|
||||
|
||||
# Extract appOptions (which is a list) and store them as shinyOptions, for
|
||||
# this app. (This is the only place we have to store settings that are
|
||||
# accessible both the UI and server portion of the app.)
|
||||
applyCapturedAppOptions(appParts$appOptions)
|
||||
|
||||
if (is.null(host) || is.na(host)) host <- '0.0.0.0'
|
||||
|
||||
@@ -286,7 +425,7 @@ runApp <- function(
|
||||
|
||||
# If display mode is specified as an argument, apply it (overriding the
|
||||
# value specified in DESCRIPTION, if any).
|
||||
display.mode <- match.arg(display.mode)
|
||||
display.mode <- match.arg(display.mode, c("auto", "normal", "showcase"))
|
||||
if (display.mode == "normal") {
|
||||
setShowcaseDefault(0)
|
||||
}
|
||||
@@ -340,24 +479,21 @@ runApp <- function(
|
||||
# onStart/onStop callbacks
|
||||
# ============================================================================
|
||||
# Set up the onStop before we call onStart, so that it gets called even if an
|
||||
# error happens in onStart.
|
||||
# error happens in onStart or later during startup.
|
||||
if (!is.null(appParts$onStop))
|
||||
on.exit(appParts$onStop(), add = TRUE)
|
||||
on.exit(if (cleanupOnExit) appParts$onStop(), add = TRUE)
|
||||
if (!is.null(appParts$onStart))
|
||||
appParts$onStart()
|
||||
|
||||
# ============================================================================
|
||||
# Start/stop httpuv app
|
||||
# Start httpuv app
|
||||
# ============================================================================
|
||||
server <- startApp(appParts, port, host, quiet)
|
||||
server <- startHttpuvApp(appParts, port, host, quiet)
|
||||
|
||||
# Make the httpuv server object accessible. Needed for calling
|
||||
# addResourcePath while app is running.
|
||||
shinyOptions(server = server)
|
||||
|
||||
on.exit({
|
||||
stopServer(server)
|
||||
}, add = TRUE)
|
||||
on.exit(if (cleanupOnExit) stopServer(server), add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Launch web browser
|
||||
@@ -388,39 +524,52 @@ runApp <- function(
|
||||
# Application hooks
|
||||
# ============================================================================
|
||||
callAppHook("onAppStart", appUrl)
|
||||
on.exit({
|
||||
callAppHook("onAppStop", appUrl)
|
||||
}, add = TRUE)
|
||||
on.exit(if (cleanupOnExit) callAppHook("onAppStop", appUrl), add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Run event loop via httpuv
|
||||
# ============================================================================
|
||||
# Initialize globals used by the event loop and stopApp()
|
||||
.globals$reterror <- NULL
|
||||
.globals$retval <- NULL
|
||||
.globals$stopped <- FALSE
|
||||
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
|
||||
# reactive(), Callbacks$invoke(), and others
|
||||
..stacktraceoff..(
|
||||
captureStackTraces({
|
||||
while (!.globals$stopped) {
|
||||
..stacktracefloor..(serviceApp())
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (isTRUE(.globals$reterror)) {
|
||||
stop(.globals$retval)
|
||||
# Invalidate any stale non-blocking service loops from a previous app.
|
||||
# Each app launch gets a fresh generation so old callbacks become no-ops.
|
||||
.globals$serviceGeneration <- (.globals$serviceGeneration %||% 0L) + 1L
|
||||
|
||||
# Setup complete - disable on.exit cleanup, hand off to caller
|
||||
cleanupOnExit <- FALSE
|
||||
|
||||
list(
|
||||
appUrl = appUrl,
|
||||
cleanup = .createCleanup(server, appParts, appUrl, ops)
|
||||
)
|
||||
}
|
||||
|
||||
# Consolidated cleanup function for app teardown
|
||||
.createCleanup <- function(server, appParts, appUrl, ops) {
|
||||
cleanedUp <- FALSE
|
||||
function() {
|
||||
if (cleanedUp) return()
|
||||
cleanedUp <<- TRUE
|
||||
|
||||
.globals$stopped <- TRUE
|
||||
.globals$runningHandle <- NULL
|
||||
handlerManager$clear()
|
||||
options(ops)
|
||||
.globals$onStopCallbacks$invoke()
|
||||
.globals$onStopCallbacks <- Callbacks$new()
|
||||
clearCurrentAppState()
|
||||
if (!is.null(appParts$onStop)) appParts$onStop()
|
||||
stopServer(server)
|
||||
callAppHook("onAppStop", appUrl)
|
||||
}
|
||||
else if (.globals$retval$visible)
|
||||
.globals$retval$value
|
||||
else
|
||||
invisible(.globals$retval$value)
|
||||
}
|
||||
|
||||
#' Stop the currently running Shiny app
|
||||
#'
|
||||
#' Stops the currently running Shiny app, returning control to the caller of
|
||||
#' [runApp()].
|
||||
#' [runApp()]. Despite the similar names, `stopApp()` is not the
|
||||
#' counterpart of [startApp()] — it is the counterpart of [runApp()],
|
||||
#' controlling its return value via `returnValue`.
|
||||
#'
|
||||
#' @param returnValue The value that should be returned from
|
||||
#' [runApp()].
|
||||
|
||||
56
R/server.R
56
R/server.R
@@ -387,7 +387,7 @@ removeSubApp <- function(path) {
|
||||
handlerManager$removeWSHandler(path)
|
||||
}
|
||||
|
||||
startApp <- function(appObj, port, host, quiet) {
|
||||
startHttpuvApp <- function(appObj, port, host, quiet) {
|
||||
appHandlers <- createAppHandlers(appObj$httpHandler, appObj$serverFuncSource)
|
||||
handlerManager$addHandler(appHandlers$http, "/", tail = TRUE)
|
||||
handlerManager$addWSHandler(appHandlers$ws, "/", tail = TRUE)
|
||||
@@ -479,9 +479,12 @@ startApp <- function(appObj, port, host, quiet) {
|
||||
}
|
||||
}
|
||||
|
||||
# Run an application that was created by \code{\link{startApp}}. This
|
||||
# Run an application that was created by \code{\link{startHttpuvApp}}. This
|
||||
# function should normally be called in a \code{while(TRUE)} loop.
|
||||
serviceApp <- function() {
|
||||
serviceApp <- function(
|
||||
# rely on lazy evaluation for maximum efficiency
|
||||
timeout = max(1, min(maxTimeout, timerCallbacks$timeToNextEvent(), later::next_op_secs()))
|
||||
) {
|
||||
timerCallbacks$executeElapsed()
|
||||
|
||||
flushReact()
|
||||
@@ -491,13 +494,58 @@ serviceApp <- function() {
|
||||
# to keep the session responsive to user input
|
||||
maxTimeout <- ifelse(interactive(), 100, 1000)
|
||||
|
||||
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent(), later::next_op_secs()))
|
||||
service(timeout)
|
||||
|
||||
flushReact()
|
||||
flushPendingSessions()
|
||||
}
|
||||
|
||||
# Non-blocking service loop using later callbacks.
|
||||
# Uses 1ms delay between iterations to yield CPU for console interaction.
|
||||
# The generation token (incremented on every runApp() call) ensures that when
|
||||
# a new app starts, any stale service loop from a previous non-blocking app
|
||||
# exits cleanly instead of continuing to run.
|
||||
# Each iteration wraps `serviceApp()` in `with_otel_promise_domain()` so the
|
||||
# OTEL domain is active while Shiny processes its own work — handlers,
|
||||
# later callbacks, promise fulfillments — all executed synchronously inside
|
||||
# `serviceApp()`. Span wrapping is attached at promise-registration time, so
|
||||
# callbacks registered inside an iteration stay instrumented when they fire
|
||||
# later. The domain is dormant between ticks, keeping it out of unrelated
|
||||
# user promises created while the console is interactive.
|
||||
serviceNonBlocking <- function(handle, generation) {
|
||||
serviceLoop <- function() {
|
||||
if (!identical(.globals$serviceGeneration, generation)) {
|
||||
return(invisible())
|
||||
}
|
||||
if (!.globals$stopped) {
|
||||
promises::with_otel_promise_domain(
|
||||
..stacktraceoff..(
|
||||
captureStackTraces(
|
||||
tryCatch(
|
||||
..stacktracefloor..(serviceApp(.shinyServiceDelaySecs * 1000)),
|
||||
error = function(e) {
|
||||
.globals$stopped <- TRUE
|
||||
.globals$retval <- e
|
||||
.globals$reterror <- TRUE
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
if (!identical(.globals$serviceGeneration, generation)) {
|
||||
return(invisible())
|
||||
}
|
||||
if (!.globals$stopped) {
|
||||
later::later(serviceLoop, delay = .shinyServiceDelaySecs)
|
||||
} else {
|
||||
handle$stop()
|
||||
}
|
||||
}
|
||||
later::later(serviceLoop, delay = .shinyServiceDelaySecs)
|
||||
}
|
||||
|
||||
.shinyServiceDelaySecs <- 0.001
|
||||
.shinyServerMinVersion <- '0.3.4'
|
||||
|
||||
#' Check whether a Shiny application is running
|
||||
|
||||
@@ -60,6 +60,10 @@ in its \code{DESCRIPTION} file, if any.}
|
||||
only used for recording or running automated tests. Defaults to the
|
||||
\code{shiny.testmode} option, or FALSE if the option is not set.}
|
||||
}
|
||||
\value{
|
||||
The value passed to \code{\link[=stopApp]{stopApp()}}, or throws an error if the app was
|
||||
stopped with an error.
|
||||
}
|
||||
\description{
|
||||
Runs a Shiny application. This function normally does not return; interrupt R
|
||||
to stop the application (usually by pressing Ctrl+C or Esc).
|
||||
@@ -109,3 +113,7 @@ if (interactive()) {
|
||||
runApp(app)
|
||||
}
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=startApp]{startApp()}} for non-blocking mode, \code{\link[=stopApp]{stopApp()}} to stop a running
|
||||
app.
|
||||
}
|
||||
|
||||
91
man/startApp.Rd
Normal file
91
man/startApp.Rd
Normal file
@@ -0,0 +1,91 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/runapp.R
|
||||
\name{startApp}
|
||||
\alias{startApp}
|
||||
\title{Start Shiny Application (Non-Blocking)}
|
||||
\usage{
|
||||
startApp(
|
||||
appDir = getwd(),
|
||||
port = getOption("shiny.port"),
|
||||
launch.browser = getOption("shiny.launch.browser", interactive()),
|
||||
host = getOption("shiny.host", "127.0.0.1"),
|
||||
workerId = "",
|
||||
quiet = FALSE,
|
||||
display.mode = c("auto", "normal", "showcase"),
|
||||
test.mode = getOption("shiny.testmode", FALSE)
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
\item{appDir}{The application to run. Should be one of the following:
|
||||
\itemize{
|
||||
\item A directory containing \code{server.R}, plus, either \code{ui.R} or
|
||||
a \code{www} directory that contains the file \code{index.html}.
|
||||
\item A directory containing \code{app.R}.
|
||||
\item An \code{.R} file containing a Shiny application, ending with an
|
||||
expression that produces a Shiny app object.
|
||||
\item A list with \code{ui} and \code{server} components.
|
||||
\item A Shiny app object created by \code{\link[=shinyApp]{shinyApp()}}.
|
||||
}}
|
||||
|
||||
\item{port}{The TCP port that the application should listen on. If the
|
||||
\code{port} is not specified, and the \code{shiny.port} option is set (with
|
||||
\code{options(shiny.port = XX)}), then that port will be used. Otherwise,
|
||||
use a random port between 3000:8000, excluding ports that are blocked
|
||||
by Google Chrome for being considered unsafe: 3659, 4045, 5060,
|
||||
5061, 6000, 6566, 6665:6669 and 6697. Up to twenty random
|
||||
ports will be tried.}
|
||||
|
||||
\item{launch.browser}{If true, the system's default web browser will be
|
||||
launched automatically after the app is started. Defaults to true in
|
||||
interactive sessions only. The value of this parameter can also be a
|
||||
function to call with the application's URL.}
|
||||
|
||||
\item{host}{The IPv4 address that the application should listen on. Defaults
|
||||
to the \code{shiny.host} option, if set, or \code{"127.0.0.1"} if not. See
|
||||
Details.}
|
||||
|
||||
\item{workerId}{Can generally be ignored. Exists to help some editions of
|
||||
Shiny Server Pro route requests to the correct process.}
|
||||
|
||||
\item{quiet}{Should Shiny status messages be shown? Defaults to FALSE.}
|
||||
|
||||
\item{display.mode}{The mode in which to display the application. If set to
|
||||
the value \code{"showcase"}, shows application code and metadata from a
|
||||
\code{DESCRIPTION} file in the application directory alongside the
|
||||
application. If set to \code{"normal"}, displays the application normally.
|
||||
Defaults to \code{"auto"}, which displays the application in the mode given
|
||||
in its \code{DESCRIPTION} file, if any.}
|
||||
|
||||
\item{test.mode}{Should the application be launched in test mode? This is
|
||||
only used for recording or running automated tests. Defaults to the
|
||||
\code{shiny.testmode} option, or FALSE if the option is not set.}
|
||||
}
|
||||
\value{
|
||||
A \code{ShinyAppHandle} object with methods \code{stop()}, \code{status()},
|
||||
\code{url()}, and \code{result()}. The \code{status()} method returns \code{"running"},
|
||||
\code{"success"}, or \code{"error"}. The \code{result()} method throws an error if called
|
||||
while running, or re-throws the error if the app stopped with an error.
|
||||
}
|
||||
\description{
|
||||
Starts a Shiny application in non-blocking mode, returning a
|
||||
\code{ShinyAppHandle} immediately while the app runs in the background.
|
||||
The \code{later} event loop services the app, so the R console remains
|
||||
available for interaction.
|
||||
}
|
||||
\examples{
|
||||
\dontrun{
|
||||
# Start app in the background
|
||||
handle <- startApp("myapp")
|
||||
|
||||
# Check status
|
||||
handle$status()
|
||||
handle$url()
|
||||
|
||||
# Stop the app
|
||||
handle$stop()
|
||||
}
|
||||
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=runApp]{runApp()}} for blocking mode, \code{\link[=stopApp]{stopApp()}} to stop a running app.
|
||||
}
|
||||
@@ -12,5 +12,7 @@ stopApp(returnValue = invisible())
|
||||
}
|
||||
\description{
|
||||
Stops the currently running Shiny app, returning control to the caller of
|
||||
\code{\link[=runApp]{runApp()}}.
|
||||
\code{\link[=runApp]{runApp()}}. Despite the similar names, \code{stopApp()} is not the
|
||||
counterpart of \code{\link[=startApp]{startApp()}} — it is the counterpart of \code{\link[=runApp]{runApp()}},
|
||||
controlling its return value via \code{returnValue}.
|
||||
}
|
||||
|
||||
275
tests/testthat/test-non-blocking.R
Normal file
275
tests/testthat/test-non-blocking.R
Normal file
@@ -0,0 +1,275 @@
|
||||
# Prevent browser launch in interactive sessions
|
||||
withr::local_options(list(shiny.launch.browser = FALSE), .local_envir = teardown_env())
|
||||
|
||||
test_that("ShinyAppHandle lifecycle and API (success path)", {
|
||||
app <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
|
||||
handle <- startApp(app, launch.browser = FALSE, quiet = TRUE)
|
||||
|
||||
# While running
|
||||
|
||||
expect_equal(handle$status(), "running")
|
||||
expect_match(handle$url(), "^http://")
|
||||
expect_error(handle$result(), "App is still running")
|
||||
|
||||
output <- capture.output(print(handle))
|
||||
expect_match(output[1], "Shiny app handle")
|
||||
expect_match(output[2], "URL:")
|
||||
expect_match(output[3], "running")
|
||||
|
||||
# stop() returns invisible self
|
||||
ret <- withVisible(handle$stop())
|
||||
expect_false(ret$visible)
|
||||
expect_identical(ret$value, handle)
|
||||
|
||||
# After stop
|
||||
expect_equal(handle$status(), "success")
|
||||
expect_null(handle$result())
|
||||
|
||||
output <- capture.output(print(handle))
|
||||
expect_match(output[3], "success")
|
||||
|
||||
# Double stop is a silent no-op
|
||||
expect_no_warning(handle$stop())
|
||||
expect_equal(handle$status(), "success")
|
||||
})
|
||||
|
||||
test_that("ShinyAppHandle lifecycle (error path)", {
|
||||
app <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
|
||||
handle <- startApp(app, launch.browser = FALSE, quiet = TRUE)
|
||||
|
||||
stopApp(stop("test_error", call. = FALSE))
|
||||
while (handle$status() == "running") {
|
||||
later::run_now(timeoutSecs = 1)
|
||||
}
|
||||
|
||||
expect_equal(handle$status(), "error")
|
||||
expect_error(handle$result(), "test_error")
|
||||
|
||||
output <- capture.output(print(handle))
|
||||
expect_match(output[3], "error")
|
||||
})
|
||||
|
||||
test_that("handle captures result from stopApp", {
|
||||
app <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
|
||||
handle <- startApp(app, launch.browser = FALSE, quiet = TRUE)
|
||||
|
||||
stopApp("test_result")
|
||||
while (handle$status() == "running") {
|
||||
later::run_now(timeoutSecs = 1)
|
||||
}
|
||||
|
||||
expect_equal(handle$status(), "success")
|
||||
expect_equal(handle$result(), "test_result")
|
||||
})
|
||||
|
||||
test_that("non-blocking auto-stops previous app when starting new one", {
|
||||
app1 <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
app2 <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
|
||||
handle1 <- startApp(app1, launch.browser = FALSE, quiet = TRUE)
|
||||
expect_equal(handle1$status(), "running")
|
||||
|
||||
# Starting a second non-blocking app should auto-stop the first
|
||||
handle2 <- startApp(app2, launch.browser = FALSE, quiet = TRUE)
|
||||
on.exit(handle2$stop(), add = TRUE)
|
||||
|
||||
expect_equal(handle1$status(), "success")
|
||||
expect_equal(handle2$status(), "running")
|
||||
|
||||
handle2$stop()
|
||||
})
|
||||
|
||||
test_that("replacing a non-blocking app does not leave stale service loops", {
|
||||
generations_seen <- integer(0)
|
||||
|
||||
# Mock serviceApp to record which generation is active when called
|
||||
local_mocked_bindings(
|
||||
serviceApp = function(timeout) {
|
||||
generations_seen[[length(generations_seen) + 1L]] <<-
|
||||
.globals$serviceGeneration
|
||||
},
|
||||
.package = "shiny"
|
||||
)
|
||||
|
||||
app1 <- shinyApp(ui = fluidPage(), server = function(input, output) {})
|
||||
app2 <- shinyApp(ui = fluidPage(), server = function(input, output) {})
|
||||
|
||||
handle1 <- startApp(app1, launch.browser = FALSE, quiet = TRUE)
|
||||
gen1 <- .globals$serviceGeneration
|
||||
|
||||
handle2 <- startApp(app2, launch.browser = FALSE, quiet = TRUE)
|
||||
on.exit(handle2$stop(), add = TRUE)
|
||||
gen2 <- .globals$serviceGeneration
|
||||
|
||||
# Reset and let service loops run
|
||||
generations_seen <- integer(0)
|
||||
while (length(generations_seen) < 5L) later::run_now(timeoutSecs = 1)
|
||||
|
||||
# Only the new generation should be servicing
|
||||
expect_true(length(generations_seen) > 0)
|
||||
expect_true(all(generations_seen == gen2))
|
||||
|
||||
handle2$stop()
|
||||
})
|
||||
|
||||
test_that("starting a blocking app invalidates stale non-blocking service loops", {
|
||||
service_calls <- 0L
|
||||
|
||||
local_mocked_bindings(
|
||||
serviceApp = function(timeout) {
|
||||
service_calls <<- service_calls + 1L
|
||||
},
|
||||
.package = "shiny"
|
||||
)
|
||||
|
||||
ns <- asNamespace("shiny")
|
||||
g <- get(".globals", envir = ns)
|
||||
|
||||
# Simulate a non-blocking app at generation 1
|
||||
assign("serviceGeneration", 1L, envir = g)
|
||||
assign("stopped", FALSE, envir = g)
|
||||
shiny:::serviceNonBlocking(list(stop = function() {}), 1L)
|
||||
|
||||
# Simulate stopping app 1, then starting a blocking app which bumps generation
|
||||
assign("stopped", TRUE, envir = g)
|
||||
assign("serviceGeneration", 2L, envir = g)
|
||||
assign("stopped", FALSE, envir = g)
|
||||
|
||||
later::run_now(timeoutSecs = 1)
|
||||
|
||||
expect_equal(service_calls, 0L)
|
||||
})
|
||||
|
||||
test_that("nested runApp in blocking mode still errors", {
|
||||
inner_app <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
|
||||
outer_app <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {},
|
||||
onStart = function() {
|
||||
runApp(inner_app, launch.browser = FALSE, quiet = TRUE)
|
||||
}
|
||||
)
|
||||
|
||||
expect_error(
|
||||
runApp(outer_app, launch.browser = FALSE, quiet = TRUE),
|
||||
"from within `runApp"
|
||||
)
|
||||
})
|
||||
|
||||
test_that("cleanup callbacks run when stopped", {
|
||||
stopped <- FALSE
|
||||
app <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
onStop(function() stopped <<- TRUE)
|
||||
|
||||
handle <- startApp(app, launch.browser = FALSE, quiet = TRUE)
|
||||
handle$stop()
|
||||
|
||||
expect_true(stopped)
|
||||
})
|
||||
|
||||
test_that("old handle doesn't see new app's result", {
|
||||
app1 <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
|
||||
handle1 <- startApp(app1, launch.browser = FALSE, quiet = TRUE)
|
||||
|
||||
stopApp("result1")
|
||||
while (handle1$status() == "running") {
|
||||
later::run_now(1)
|
||||
}
|
||||
expect_equal(handle1$result(), "result1")
|
||||
|
||||
# Start and stop app2
|
||||
app2 <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
handle2 <- startApp(app2, launch.browser = FALSE, quiet = TRUE)
|
||||
|
||||
stopApp("result2")
|
||||
while (handle2$status() == "running") {
|
||||
later::run_now(timeoutSecs = 1)
|
||||
}
|
||||
expect_equal(handle2$result(), "result2")
|
||||
|
||||
# handle1 should still have its original result
|
||||
expect_equal(handle1$result(), "result1")
|
||||
})
|
||||
|
||||
test_that("global isRunning() works with non-blocking apps", {
|
||||
app <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
|
||||
expect_false(isRunning())
|
||||
|
||||
handle <- startApp(app, launch.browser = FALSE, quiet = TRUE)
|
||||
on.exit(handle$stop(), add = TRUE)
|
||||
|
||||
expect_true(isRunning())
|
||||
|
||||
handle$stop()
|
||||
expect_false(isRunning())
|
||||
})
|
||||
|
||||
test_that("startup failure clears app state (regression test)", {
|
||||
# If startup fails after initCurrentAppState() but before cleanupOnExit <- FALSE,
|
||||
# the app state must be cleared so subsequent runApp() calls don't fail with
|
||||
# "Can't start a new app while another is running"
|
||||
|
||||
# Create an app that fails during onStart (which runs after initCurrentAppState)
|
||||
failing_app <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {},
|
||||
onStart = function() stop("Intentional startup failure")
|
||||
)
|
||||
|
||||
# This should fail
|
||||
expect_error(
|
||||
startApp(failing_app, launch.browser = FALSE, quiet = TRUE),
|
||||
"Intentional startup failure"
|
||||
)
|
||||
|
||||
# isRunning() should return FALSE - no app is actually running
|
||||
expect_false(isRunning())
|
||||
|
||||
# A subsequent runApp() call should work
|
||||
working_app <- shinyApp(
|
||||
ui = fluidPage(),
|
||||
server = function(input, output) {}
|
||||
)
|
||||
|
||||
handle <- startApp(working_app, launch.browser = FALSE, quiet = TRUE)
|
||||
on.exit(handle$stop(), add = TRUE)
|
||||
|
||||
expect_equal(handle$status(), "running")
|
||||
handle$stop()
|
||||
})
|
||||
@@ -139,6 +139,7 @@ reference:
|
||||
desc: Functions that are used to run or stop Shiny applications.
|
||||
contents:
|
||||
- runApp
|
||||
- startApp
|
||||
- runGadget
|
||||
- runExample
|
||||
- runGadget
|
||||
|
||||
Reference in New Issue
Block a user