mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-13 00:48:09 -05:00
1011 lines
34 KiB
R
1011 lines
34 KiB
R
#' @include server-input-handlers.R
|
|
|
|
appsByToken <- Map$new()
|
|
|
|
# Provide a character representation of the WS that can be used
|
|
# as a key in a Map.
|
|
wsToKey <- function(WS) {
|
|
as.character(WS$socket)
|
|
}
|
|
|
|
.globals$clients <- function(req) NULL
|
|
|
|
|
|
clearClients <- function() {
|
|
.globals$clients <- function(req) NULL
|
|
}
|
|
|
|
|
|
registerClient <- function(client) {
|
|
.globals$clients <- append(.globals$clients, client)
|
|
}
|
|
|
|
|
|
.globals$resources <- list()
|
|
|
|
.globals$showcaseDefault <- 0
|
|
|
|
.globals$showcaseOverride <- FALSE
|
|
|
|
#' Resource Publishing
|
|
#'
|
|
#' Adds a directory of static resources to Shiny's web server, with the given
|
|
#' path prefix. Primarily intended for package authors to make supporting
|
|
#' JavaScript/CSS files available to their components.
|
|
#'
|
|
#' @param prefix The URL prefix (without slashes). Valid characters are a-z,
|
|
#' A-Z, 0-9, hyphen, period, and underscore; and must begin with a-z or A-Z.
|
|
#' For example, a value of 'foo' means that any request paths that begin with
|
|
#' '/foo' will be mapped to the given directory.
|
|
#' @param directoryPath The directory that contains the static resources to be
|
|
#' served.
|
|
#'
|
|
#' @details You can call \code{addResourcePath} multiple times for a given
|
|
#' \code{prefix}; only the most recent value will be retained. If the
|
|
#' normalized \code{directoryPath} is different than the directory that's
|
|
#' currently mapped to the \code{prefix}, a warning will be issued.
|
|
#'
|
|
#' @seealso \code{\link{singleton}}
|
|
#'
|
|
#' @examples
|
|
#' addResourcePath('datasets', system.file('data', package='datasets'))
|
|
#' @export
|
|
addResourcePath <- function(prefix, directoryPath) {
|
|
prefix <- prefix[1]
|
|
if (!grepl('^[a-z][a-z0-9\\-_.]*$', prefix, ignore.case=TRUE, perl=TRUE)) {
|
|
stop("addResourcePath called with invalid prefix; please see documentation")
|
|
}
|
|
|
|
if (prefix %in% c('shared')) {
|
|
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
|
|
"please use a different prefix")
|
|
}
|
|
|
|
directoryPath <- normalizePath(directoryPath, mustWork=TRUE)
|
|
|
|
existing <- .globals$resources[[prefix]]
|
|
|
|
.globals$resources[[prefix]] <- list(directoryPath=directoryPath,
|
|
func=staticHandler(directoryPath))
|
|
}
|
|
|
|
resourcePathHandler <- function(req) {
|
|
if (!identical(req$REQUEST_METHOD, 'GET'))
|
|
return(NULL)
|
|
|
|
path <- req$PATH_INFO
|
|
|
|
match <- regexpr('^/([^/]+)/', path, perl=TRUE)
|
|
if (match == -1)
|
|
return(NULL)
|
|
len <- attr(match, 'capture.length')
|
|
prefix <- substr(path, 2, 2 + len - 1)
|
|
|
|
resInfo <- .globals$resources[[prefix]]
|
|
if (is.null(resInfo))
|
|
return(NULL)
|
|
|
|
suffix <- substr(path, 2 + len, nchar(path))
|
|
|
|
subreq <- as.environment(as.list(req, all.names=TRUE))
|
|
subreq$PATH_INFO <- suffix
|
|
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, substr(path, 1, 2 + len), sep='')
|
|
|
|
return(resInfo$func(subreq))
|
|
}
|
|
|
|
#' Define Server Functionality
|
|
#'
|
|
#' Defines the server-side logic of the Shiny application. This generally
|
|
#' involves creating functions that map user inputs to various kinds of output.
|
|
#' In older versions of Shiny, it was necessary to call \code{shinyServer()} in
|
|
#' the \code{server.R} file, but this is no longer required as of Shiny 0.10.
|
|
#' Now the \code{server.R} file may simply return the appropriate server
|
|
#' function (as the last expression in the code), without calling
|
|
#' \code{shinyServer()}.
|
|
#'
|
|
#' Call \code{shinyServer} from your application's \code{server.R}
|
|
#' file, passing in a "server function" that provides the server-side logic of
|
|
#' your application.
|
|
#'
|
|
#' The server function will be called when each client (web browser) first loads
|
|
#' the Shiny application's page. It must take an \code{input} and an
|
|
#' \code{output} parameter. Any return value will be ignored. It also takes an
|
|
#' optional \code{session} parameter, which is used when greater control is
|
|
#' needed.
|
|
#'
|
|
#' See the \href{http://rstudio.github.com/shiny/tutorial/}{tutorial} for more
|
|
#' on how to write a server function.
|
|
#'
|
|
#' @param func The server function for this application. See the details section
|
|
#' for more information.
|
|
#'
|
|
#' @examples
|
|
#' \dontrun{
|
|
#' # A very simple Shiny app that takes a message from the user
|
|
#' # and outputs an uppercase version of it.
|
|
#' shinyServer(function(input, output, session) {
|
|
#' output$uppercase <- renderText({
|
|
#' toupper(input$message)
|
|
#' })
|
|
#' })
|
|
#'
|
|
#'
|
|
#' # It is also possible for a server.R file to simply return the function,
|
|
#' # without calling shinyServer().
|
|
#' # For example, the server.R file could contain just the following:
|
|
#' function(input, output, session) {
|
|
#' output$uppercase <- renderText({
|
|
#' toupper(input$message)
|
|
#' })
|
|
#' }
|
|
#' }
|
|
#' @export
|
|
shinyServer <- function(func) {
|
|
.globals$server <- list(func)
|
|
invisible(func)
|
|
}
|
|
|
|
decodeMessage <- function(data) {
|
|
readInt <- function(pos) {
|
|
packBits(rawToBits(data[pos:(pos+3)]), type='integer')
|
|
}
|
|
|
|
if (readInt(1) != 0x01020202L) {
|
|
# Treat message as UTF-8
|
|
charData <- rawToChar(data)
|
|
Encoding(charData) <- 'UTF-8'
|
|
return(jsonlite::fromJSON(charData, simplifyVector=FALSE))
|
|
}
|
|
|
|
i <- 5
|
|
parts <- list()
|
|
while (i <= length(data)) {
|
|
length <- readInt(i)
|
|
i <- i + 4
|
|
if (length != 0)
|
|
parts <- append(parts, list(data[i:(i+length-1)]))
|
|
else
|
|
parts <- append(parts, list(raw(0)))
|
|
i <- i + length
|
|
}
|
|
|
|
mainMessage <- decodeMessage(parts[[1]])
|
|
mainMessage$blobs <- parts[2:length(parts)]
|
|
return(mainMessage)
|
|
}
|
|
|
|
createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
|
appvars <- new.env()
|
|
appvars$server <- NULL
|
|
|
|
sys.www.root <- system.file('www', package='shiny')
|
|
|
|
# This value, if non-NULL, must be present on all HTTP and WebSocket
|
|
# requests as the Shiny-Shared-Secret header or else access will be
|
|
# denied (403 response for HTTP, and instant close for websocket).
|
|
sharedSecret <- getOption('shiny.sharedSecret')
|
|
|
|
appHandlers <- list(
|
|
http = joinHandlers(c(
|
|
sessionHandler,
|
|
httpHandlers,
|
|
sys.www.root,
|
|
resourcePathHandler,
|
|
reactLogHandler)),
|
|
ws = function(ws) {
|
|
if (!is.null(sharedSecret)
|
|
&& !identical(sharedSecret, ws$request$HTTP_SHINY_SHARED_SECRET)) {
|
|
ws$close()
|
|
return(TRUE)
|
|
}
|
|
|
|
if (!is.null(getOption("shiny.observer.error", NULL))) {
|
|
warning(
|
|
call. = FALSE,
|
|
"options(shiny.observer.error) is no longer supported; please unset it!"
|
|
)
|
|
stopApp()
|
|
}
|
|
|
|
shinysession <- ShinySession$new(ws)
|
|
appsByToken$set(shinysession$token, shinysession)
|
|
shinysession$setShowcase(.globals$showcaseDefault)
|
|
|
|
messageHandler <- function(binary, msg) {
|
|
withReactiveDomain(shinysession, {
|
|
# To ease transition from websockets-based code. Should remove once we're stable.
|
|
if (is.character(msg))
|
|
msg <- charToRaw(msg)
|
|
|
|
traceOption <- getOption('shiny.trace', FALSE)
|
|
if (isTRUE(traceOption) || traceOption == "recv") {
|
|
if (binary)
|
|
message("RECV ", '$$binary data$$')
|
|
else
|
|
message("RECV ", rawToChar(msg))
|
|
}
|
|
|
|
if (identical(charToRaw("\003\xe9"), msg))
|
|
return()
|
|
|
|
msg <- decodeMessage(msg)
|
|
|
|
# Set up a restore context from .clientdata_url_search before
|
|
# handling all the input values, because the restore context may be
|
|
# used by an input handler (like the one for "shiny.file"). This
|
|
# should only happen once, when the app starts.
|
|
if (is.null(shinysession$restoreContext)) {
|
|
bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")
|
|
if (bookmarkStore == "disable") {
|
|
# If bookmarking is disabled, use empty context
|
|
shinysession$restoreContext <- RestoreContext$new()
|
|
} else {
|
|
# If there's bookmarked state, save it on the session object
|
|
shinysession$restoreContext <- RestoreContext$new(msg$data$.clientdata_url_search)
|
|
}
|
|
}
|
|
|
|
withRestoreContext(shinysession$restoreContext, {
|
|
|
|
msg$data <- applyInputHandlers(msg$data)
|
|
|
|
switch(
|
|
msg$method,
|
|
init = {
|
|
|
|
serverFunc <- withReactiveDomain(NULL, serverFuncSource())
|
|
if (!identicalFunctionBodies(serverFunc, appvars$server)) {
|
|
appvars$server <- serverFunc
|
|
if (!is.null(appvars$server))
|
|
{
|
|
# Tag this function as the Shiny server function. A debugger may use this
|
|
# tag to give this function special treatment.
|
|
# It's very important that it's appvars$server itself and NOT a copy that
|
|
# is invoked, otherwise new breakpoints won't be picked up.
|
|
attr(appvars$server, "shinyServerFunction") <- TRUE
|
|
registerDebugHook("server", appvars, "Server Function")
|
|
}
|
|
}
|
|
|
|
# Check for switching into/out of showcase mode
|
|
if (.globals$showcaseOverride &&
|
|
exists(".clientdata_url_search", where = msg$data)) {
|
|
mode <- showcaseModeOfQuerystring(msg$data$.clientdata_url_search)
|
|
if (!is.null(mode))
|
|
shinysession$setShowcase(mode)
|
|
}
|
|
|
|
shinysession$manageInputs(msg$data)
|
|
|
|
# The client tells us what singletons were rendered into
|
|
# the initial page
|
|
if (!is.null(msg$data$.clientdata_singletons)) {
|
|
shinysession$singletons <- strsplit(
|
|
msg$data$.clientdata_singletons, ',')[[1]]
|
|
}
|
|
|
|
local({
|
|
args <- argsForServerFunc(serverFunc, shinysession)
|
|
|
|
withReactiveDomain(shinysession, {
|
|
do.call(
|
|
# No corresponding ..stacktraceoff; the server func is pure
|
|
# user code
|
|
wrapFunctionLabel(appvars$server, "server",
|
|
..stacktraceon = TRUE
|
|
),
|
|
args
|
|
)
|
|
})
|
|
})
|
|
},
|
|
update = {
|
|
shinysession$manageInputs(msg$data)
|
|
},
|
|
shinysession$dispatch(msg)
|
|
)
|
|
shinysession$manageHiddenOutputs()
|
|
|
|
if (exists(".shiny__stdout", globalenv()) &&
|
|
exists("HTTP_GUID", ws$request)) {
|
|
# safe to assume we're in shiny-server
|
|
shiny_stdout <- get(".shiny__stdout", globalenv())
|
|
|
|
# eNter a flushReact
|
|
writeLines(paste("_n_flushReact ", get("HTTP_GUID", ws$request),
|
|
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
|
|
sep=""), con=shiny_stdout)
|
|
flush(shiny_stdout)
|
|
|
|
flushReact()
|
|
|
|
# eXit a flushReact
|
|
writeLines(paste("_x_flushReact ", get("HTTP_GUID", ws$request),
|
|
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
|
|
sep=""), con=shiny_stdout)
|
|
flush(shiny_stdout)
|
|
} else {
|
|
flushReact()
|
|
}
|
|
|
|
flushAllSessions()
|
|
})
|
|
})
|
|
}
|
|
ws$onMessage(function(binary, msg) {
|
|
# If unhandled errors occur, make sure they get properly logged
|
|
withLogErrors(messageHandler(binary, msg))
|
|
})
|
|
|
|
ws$onClose(function() {
|
|
shinysession$wsClosed()
|
|
appsByToken$remove(shinysession$token)
|
|
})
|
|
|
|
return(TRUE)
|
|
}
|
|
)
|
|
return(appHandlers)
|
|
}
|
|
|
|
# Determine what arguments should be passed to this serverFunc. All server funcs
|
|
# must take input and output, but clientData (obsolete) and session are
|
|
# optional.
|
|
argsForServerFunc <- function(serverFunc, session) {
|
|
args <- list(input = session$input, output = .createOutputWriter(session))
|
|
|
|
paramNames <- names(formals(serverFunc))
|
|
|
|
# The clientData and session arguments are optional; check if
|
|
# each exists
|
|
|
|
if ("clientData" %in% paramNames)
|
|
args$clientData <- session$clientData
|
|
|
|
if ("session" %in% paramNames)
|
|
args$session <- session
|
|
|
|
args
|
|
}
|
|
|
|
getEffectiveBody <- function(func) {
|
|
# Note: NULL values are OK. isS4(NULL) returns FALSE, body(NULL)
|
|
# returns NULL.
|
|
if (isS4(func) && class(func) == "functionWithTrace")
|
|
body(func@original)
|
|
else
|
|
body(func)
|
|
}
|
|
|
|
identicalFunctionBodies <- function(a, b) {
|
|
identical(getEffectiveBody(a), getEffectiveBody(b))
|
|
}
|
|
|
|
handlerManager <- HandlerManager$new()
|
|
|
|
addSubApp <- function(appObj, autoRemove = TRUE) {
|
|
path <- createUniqueId(16, "/app")
|
|
appHandlers <- createAppHandlers(appObj$httpHandler, appObj$serverFuncSource)
|
|
|
|
# remove the leading / from the path so a relative path is returned
|
|
# (needed for the case where the root URL for the Shiny app isn't /, such
|
|
# as portmapped URLs)
|
|
finalPath <- paste(
|
|
substr(path, 2, nchar(path)),
|
|
"/?w=", workerId(),
|
|
"&__subapp__=1",
|
|
sep="")
|
|
handlerManager$addHandler(routeHandler(path, appHandlers$http), finalPath)
|
|
handlerManager$addWSHandler(routeWSHandler(path, appHandlers$ws), finalPath)
|
|
|
|
if (autoRemove) {
|
|
# If a session is currently active, remove this subapp automatically when
|
|
# the current session ends
|
|
onReactiveDomainEnded(getDefaultReactiveDomain(), function() {
|
|
removeSubApp(finalPath)
|
|
})
|
|
}
|
|
|
|
return(finalPath)
|
|
}
|
|
|
|
removeSubApp <- function(path) {
|
|
handlerManager$removeHandler(path)
|
|
handlerManager$removeWSHandler(path)
|
|
}
|
|
|
|
startApp <- function(appObj, port, host, quiet) {
|
|
appHandlers <- createAppHandlers(appObj$httpHandler, appObj$serverFuncSource)
|
|
handlerManager$addHandler(appHandlers$http, "/", tail = TRUE)
|
|
handlerManager$addWSHandler(appHandlers$ws, "/", tail = TRUE)
|
|
|
|
if (is.numeric(port) || is.integer(port)) {
|
|
if (!quiet) {
|
|
message('\n', 'Listening on http://', host, ':', port)
|
|
}
|
|
return(startServer(host, port, handlerManager$createHttpuvApp()))
|
|
} else if (is.character(port)) {
|
|
if (!quiet) {
|
|
message('\n', 'Listening on domain socket ', port)
|
|
}
|
|
mask <- attr(port, 'mask')
|
|
if (is.null(mask)) {
|
|
stop("`port` is not a valid domain socket (missing `mask` attribute). ",
|
|
"Note that if you're using the default `host` + `port` ",
|
|
"configuration (and not domain sockets), then `port` must ",
|
|
"be numeric, not a string.")
|
|
}
|
|
return(startPipeServer(port, mask, handlerManager$createHttpuvApp()))
|
|
}
|
|
}
|
|
|
|
# Run an application that was created by \code{\link{startApp}}. This
|
|
# function should normally be called in a \code{while(TRUE)} loop.
|
|
serviceApp <- function() {
|
|
if (timerCallbacks$executeElapsed()) {
|
|
for (shinysession in appsByToken$values()) {
|
|
shinysession$manageHiddenOutputs()
|
|
}
|
|
|
|
flushReact()
|
|
flushAllSessions()
|
|
}
|
|
|
|
# If this R session is interactive, then call service() with a short timeout
|
|
# to keep the session responsive to user input
|
|
maxTimeout <- ifelse(interactive(), 100, 1000)
|
|
|
|
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent()))
|
|
service(timeout)
|
|
}
|
|
|
|
.shinyServerMinVersion <- '0.3.4'
|
|
|
|
#' Run Shiny Application
|
|
#'
|
|
#' Runs a Shiny application. This function normally does not return; interrupt R
|
|
#' to stop the application (usually by pressing Ctrl+C or Esc).
|
|
#'
|
|
#' The host parameter was introduced in Shiny 0.9.0. Its default value of
|
|
#' \code{"127.0.0.1"} means that, contrary to previous versions of Shiny, only
|
|
#' the current machine can access locally hosted Shiny apps. To allow other
|
|
#' clients to connect, use the value \code{"0.0.0.0"} instead (which was the
|
|
#' value that was hard-coded into Shiny in 0.8.0 and earlier).
|
|
#'
|
|
#' @param 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}}.
|
|
#' }
|
|
#' @param 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.
|
|
#' @param 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. This value of this parameter can also be a
|
|
#' function to call with the application's URL.
|
|
#' @param 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.
|
|
#' @param workerId Can generally be ignored. Exists to help some editions of
|
|
#' Shiny Server Pro route requests to the correct process.
|
|
#' @param quiet Should Shiny status messages be shown? Defaults to FALSE.
|
|
#' @param 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.
|
|
#' @param 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.
|
|
#'
|
|
#' @examples
|
|
#' \dontrun{
|
|
#' # Start app in the current working directory
|
|
#' runApp()
|
|
#'
|
|
#' # Start app in a subdirectory called myapp
|
|
#' runApp("myapp")
|
|
#' }
|
|
#'
|
|
#' ## Only run this example in interactive R sessions
|
|
#' if (interactive()) {
|
|
#' # Apps can be run without a server.r and ui.r file
|
|
#' runApp(list(
|
|
#' ui = bootstrapPage(
|
|
#' numericInput('n', 'Number of obs', 100),
|
|
#' plotOutput('plot')
|
|
#' ),
|
|
#' server = function(input, output) {
|
|
#' output$plot <- renderPlot({ hist(runif(input$n)) })
|
|
#' }
|
|
#' ))
|
|
#'
|
|
#'
|
|
#' # Running a Shiny app object
|
|
#' app <- shinyApp(
|
|
#' ui = bootstrapPage(
|
|
#' numericInput('n', 'Number of obs', 100),
|
|
#' plotOutput('plot')
|
|
#' ),
|
|
#' server = function(input, output) {
|
|
#' output$plot <- renderPlot({ hist(runif(input$n)) })
|
|
#' }
|
|
#' )
|
|
#' runApp(app)
|
|
#' }
|
|
#' @export
|
|
runApp <- 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)) {
|
|
on.exit({
|
|
handlerManager$clear()
|
|
}, add = TRUE)
|
|
|
|
# Enable per-app Shiny options
|
|
oldOptionSet <- .globals$options
|
|
on.exit({
|
|
.globals$options <- oldOptionSet
|
|
},add = TRUE)
|
|
|
|
# Make warnings print immediately
|
|
# Set pool.scheduler to support pool package
|
|
ops <- options(warn = 1, pool.scheduler = scheduleTask)
|
|
on.exit(options(ops), add = TRUE)
|
|
|
|
appParts <- as.shiny.appobj(appDir)
|
|
|
|
# The lines below set some of the app's running options, which
|
|
# can be:
|
|
# - left unspeficied (in which case the arguments' default
|
|
# values from `runApp` kick in);
|
|
# - passed through `shinyApp`
|
|
# - passed through `runApp` (this function)
|
|
# - passed through both `shinyApp` and `runApp` (the latter
|
|
# takes precedence)
|
|
#
|
|
# Matrix of possibilities:
|
|
# | IN shinyApp | IN runApp | result | check |
|
|
# |-------------|-----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------|
|
|
# | no | no | use defaults | exhaust all possibilities: if it's missing (runApp does not specify); THEN if it's not in shinyApp appParts$options; THEN use defaults |
|
|
# | yes | no | use shinyApp | if it's missing (runApp does not specify); THEN if it's in shinyApp appParts$options; THEN use shinyApp |
|
|
# | 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
|
|
appOps <- appParts$options
|
|
findVal <- function(arg, default) {
|
|
if (arg %in% names(appOps)) appOps[[arg]] else default
|
|
}
|
|
|
|
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)
|
|
|
|
if (is.null(host) || is.na(host)) host <- '0.0.0.0'
|
|
|
|
workerId(workerId)
|
|
|
|
if (inShinyServer()) {
|
|
# If SHINY_PORT is set, we're running under Shiny Server. Check the version
|
|
# to make sure it is compatible. Older versions of Shiny Server don't set
|
|
# SHINY_SERVER_VERSION, those will return "" which is considered less than
|
|
# any valid version.
|
|
ver <- Sys.getenv('SHINY_SERVER_VERSION')
|
|
if (utils::compareVersion(ver, .shinyServerMinVersion) < 0) {
|
|
warning('Shiny Server v', .shinyServerMinVersion,
|
|
' or later is required; please upgrade!')
|
|
}
|
|
}
|
|
|
|
# Showcase mode is disabled by default; it must be explicitly enabled in
|
|
# either the DESCRIPTION file for directory-based apps, or via
|
|
# the display.mode parameter. The latter takes precedence.
|
|
setShowcaseDefault(0)
|
|
|
|
.globals$testMode <- test.mode
|
|
if (test.mode) {
|
|
message("Running application in test mode.")
|
|
}
|
|
|
|
# If appDir specifies a path, and display mode is specified in the
|
|
# DESCRIPTION file at that path, apply it here.
|
|
if (is.character(appDir)) {
|
|
# if appDir specifies a .R file (single-file Shiny app), look for the
|
|
# DESCRIPTION in the parent directory
|
|
desc <- file.path.ci(
|
|
if (tolower(tools::file_ext(appDir)) == "r")
|
|
dirname(appDir)
|
|
else
|
|
appDir, "DESCRIPTION")
|
|
if (file.exists(desc)) {
|
|
con <- file(desc, encoding = checkEncoding(desc))
|
|
on.exit(close(con), add = TRUE)
|
|
settings <- read.dcf(con)
|
|
if ("DisplayMode" %in% colnames(settings)) {
|
|
mode <- settings[1, "DisplayMode"]
|
|
if (mode == "Showcase") {
|
|
setShowcaseDefault(1)
|
|
if ("IncludeWWW" %in% colnames(settings)) {
|
|
.globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
|
|
if (is.na(.globals$IncludeWWW)) {
|
|
stop("In your Description file, `IncludeWWW` ",
|
|
"must be set to `True` (default) or `False`")
|
|
}
|
|
} else {
|
|
.globals$IncludeWWW <- TRUE
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
## default is to show the .js, .css and .html files in the www directory
|
|
## (if not in showcase mode, this variable will simply be ignored)
|
|
if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
|
|
.globals$IncludeWWW <- TRUE
|
|
}
|
|
|
|
# If display mode is specified as an argument, apply it (overriding the
|
|
# value specified in DESCRIPTION, if any).
|
|
display.mode <- match.arg(display.mode)
|
|
if (display.mode == "normal") {
|
|
setShowcaseDefault(0)
|
|
}
|
|
else if (display.mode == "showcase") {
|
|
setShowcaseDefault(1)
|
|
}
|
|
|
|
require(shiny)
|
|
|
|
# determine port if we need to
|
|
if (is.null(port)) {
|
|
|
|
# Try up to 20 random ports. If we don't succeed just plow ahead
|
|
# with the final value we tried, and let the "real" startServer
|
|
# somewhere down the line fail and throw the error to the user.
|
|
#
|
|
# If we (think we) succeed, save the value as .globals$lastPort,
|
|
# and try that first next time the user wants a random port.
|
|
|
|
for (i in 1:20) {
|
|
if (!is.null(.globals$lastPort)) {
|
|
port <- .globals$lastPort
|
|
.globals$lastPort <- NULL
|
|
}
|
|
else {
|
|
# Try up to 20 random ports
|
|
while (TRUE) {
|
|
port <- p_randomInt(3000, 8000)
|
|
# Reject ports in this range that are considered unsafe by Chrome
|
|
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
|
|
if (!port %in% c(3659, 4045, 6000, 6665:6669)) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
# Test port to see if we can use it
|
|
tmp <- try(startServer(host, port, list()), silent=TRUE)
|
|
if (!inherits(tmp, 'try-error')) {
|
|
stopServer(tmp)
|
|
.globals$lastPort <- port
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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.)
|
|
unconsumeAppOptions(appParts$appOptions)
|
|
|
|
# Set up the onEnd before we call onStart, so that it gets called even if an
|
|
# error happens in onStart.
|
|
if (!is.null(appParts$onEnd))
|
|
on.exit(appParts$onEnd(), add = TRUE)
|
|
if (!is.null(appParts$onStart))
|
|
appParts$onStart()
|
|
|
|
server <- startApp(appParts, port, host, quiet)
|
|
|
|
on.exit({
|
|
stopServer(server)
|
|
}, add = TRUE)
|
|
|
|
if (!is.character(port)) {
|
|
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
|
|
browseHost <- if (identical(host, "0.0.0.0")) "127.0.0.1" else host
|
|
|
|
appUrl <- paste("http://", browseHost, ":", port, sep="")
|
|
if (is.function(launch.browser))
|
|
launch.browser(appUrl)
|
|
else if (launch.browser)
|
|
utils::browseURL(appUrl)
|
|
} else {
|
|
appUrl <- NULL
|
|
}
|
|
|
|
# call application hooks
|
|
callAppHook("onAppStart", appUrl)
|
|
on.exit({
|
|
callAppHook("onAppStop", appUrl)
|
|
}, add = TRUE)
|
|
|
|
.globals$reterror <- NULL
|
|
.globals$retval <- NULL
|
|
.globals$stopped <- FALSE
|
|
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
|
|
# reactive(), Callbacks$invoke(), and others
|
|
..stacktraceoff..(
|
|
captureStackTraces({
|
|
# If any observers were created before runApp was called, this will make
|
|
# sure they run once the app starts. (Issue #1013)
|
|
scheduleFlush()
|
|
|
|
while (!.globals$stopped) {
|
|
serviceApp()
|
|
Sys.sleep(0.001)
|
|
}
|
|
})
|
|
)
|
|
|
|
if (isTRUE(.globals$reterror)) {
|
|
stop(.globals$retval)
|
|
}
|
|
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
|
|
#' \code{\link{runApp}}.
|
|
#'
|
|
#' @param returnValue The value that should be returned from
|
|
#' \code{\link{runApp}}.
|
|
#' @export
|
|
stopApp <- function(returnValue = invisible()) {
|
|
# reterror will indicate whether retval is an error (i.e. it should be passed
|
|
# to stop() when the serviceApp loop stops) or a regular value (in which case
|
|
# it should simply be returned with the appropriate visibility).
|
|
.globals$reterror <- FALSE
|
|
..stacktraceoff..(
|
|
tryCatch(
|
|
{
|
|
captureStackTraces(
|
|
.globals$retval <- withVisible(..stacktraceon..(force(returnValue)))
|
|
)
|
|
},
|
|
error = function(e) {
|
|
.globals$retval <- e
|
|
.globals$reterror <- TRUE
|
|
}
|
|
)
|
|
)
|
|
|
|
.globals$stopped <- TRUE
|
|
httpuv::interrupt()
|
|
}
|
|
|
|
#' Run Shiny Example Applications
|
|
#'
|
|
#' Launch Shiny example applications, and optionally, your system's web browser.
|
|
#'
|
|
#' @param example The name of the example to run, or \code{NA} (the default) to
|
|
#' list the available examples.
|
|
#' @param port The TCP port that the application should listen on. Defaults to
|
|
#' choosing a random port.
|
|
#' @param 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.
|
|
#' @param 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.
|
|
#' @param display.mode The mode in which to display the example. Defaults to
|
|
#' \code{showcase}, but may be set to \code{normal} to see the example without
|
|
#' code or commentary.
|
|
#'
|
|
#' @examples
|
|
#' ## Only run this example in interactive R sessions
|
|
#' if (interactive()) {
|
|
#' # List all available examples
|
|
#' runExample()
|
|
#'
|
|
#' # Run one of the examples
|
|
#' runExample("01_hello")
|
|
#'
|
|
#' # Print the directory containing the code for all examples
|
|
#' system.file("examples", package="shiny")
|
|
#' }
|
|
#' @export
|
|
runExample <- function(example=NA,
|
|
port=NULL,
|
|
launch.browser=getOption('shiny.launch.browser',
|
|
interactive()),
|
|
host=getOption('shiny.host', '127.0.0.1'),
|
|
display.mode=c("auto", "normal", "showcase")) {
|
|
examplesDir <- system.file('examples', package='shiny')
|
|
dir <- resolve(examplesDir, example)
|
|
if (is.null(dir)) {
|
|
if (is.na(example)) {
|
|
errFun <- message
|
|
errMsg <- ''
|
|
}
|
|
else {
|
|
errFun <- stop
|
|
errMsg <- paste('Example', example, 'does not exist. ')
|
|
}
|
|
|
|
errFun(errMsg,
|
|
'Valid examples are "',
|
|
paste(list.files(examplesDir), collapse='", "'),
|
|
'"')
|
|
}
|
|
else {
|
|
runApp(dir, port = port, host = host, launch.browser = launch.browser,
|
|
display.mode = display.mode)
|
|
}
|
|
}
|
|
|
|
#' Run a gadget
|
|
#'
|
|
#' Similar to \code{runApp}, but handles \code{input$cancel} automatically, and
|
|
#' if running in RStudio, defaults to viewing the app in the Viewer pane.
|
|
#'
|
|
#' @param app Either a Shiny app object as created by
|
|
#' \code{\link[=shiny]{shinyApp}} et al, or, a UI object.
|
|
#' @param server Ignored if \code{app} is a Shiny app object; otherwise, passed
|
|
#' along to \code{shinyApp} (i.e. \code{shinyApp(ui = app, server = server)}).
|
|
#' @param port See \code{\link[=shiny]{runApp}}.
|
|
#' @param viewer Specify where the gadget should be displayed--viewer pane,
|
|
#' dialog window, or external browser--by passing in a call to one of the
|
|
#' \code{\link{viewer}} functions.
|
|
#' @param stopOnCancel If \code{TRUE} (the default), then an \code{observeEvent}
|
|
#' is automatically created that handles \code{input$cancel} by calling
|
|
#' \code{stopApp()} with an error. Pass \code{FALSE} if you want to handle
|
|
#' \code{input$cancel} yourself.
|
|
#' @return The value returned by the gadget.
|
|
#'
|
|
#' @examples
|
|
#' \dontrun{
|
|
#' library(shiny)
|
|
#'
|
|
#' ui <- fillPage(...)
|
|
#'
|
|
#' server <- function(input, output, session) {
|
|
#' ...
|
|
#' }
|
|
#'
|
|
#' # Either pass ui/server as separate arguments...
|
|
#' runGadget(ui, server)
|
|
#'
|
|
#' # ...or as a single app object
|
|
#' runGadget(shinyApp(ui, server))
|
|
#' }
|
|
#' @export
|
|
runGadget <- function(app, server = NULL, port = getOption("shiny.port"),
|
|
viewer = paneViewer(), stopOnCancel = TRUE) {
|
|
|
|
if (!is.shiny.appobj(app)) {
|
|
app <- shinyApp(app, server)
|
|
}
|
|
|
|
if (isTRUE(stopOnCancel)) {
|
|
app <- decorateServerFunc(app, function(input, output, session) {
|
|
observeEvent(input$cancel, {
|
|
stopApp(stop("User cancel", call. = FALSE))
|
|
})
|
|
})
|
|
}
|
|
|
|
if (is.null(viewer)) {
|
|
viewer <- utils::browseURL
|
|
}
|
|
|
|
shiny::runApp(app, port = port, launch.browser = viewer)
|
|
}
|
|
|
|
# Add custom functionality to a Shiny app object's server func
|
|
decorateServerFunc <- function(appobj, serverFunc) {
|
|
origServerFuncSource <- appobj$serverFuncSource
|
|
appobj$serverFuncSource <- function() {
|
|
origServerFunc <- origServerFuncSource()
|
|
function(input, output, session) {
|
|
serverFunc(input, output, session)
|
|
|
|
# The clientData and session arguments are optional; check if
|
|
# each exists
|
|
args <- argsForServerFunc(origServerFunc, session)
|
|
do.call(origServerFunc, args)
|
|
}
|
|
}
|
|
appobj
|
|
}
|
|
|
|
#' Viewer options
|
|
#'
|
|
#' Use these functions to control where the gadget is displayed in RStudio (or
|
|
#' other R environments that emulate RStudio's viewer pane/dialog APIs). If
|
|
#' viewer APIs are not available in the current R environment, then the gadget
|
|
#' will be displayed in the system's default web browser (see
|
|
#' \code{\link[utils]{browseURL}}).
|
|
#'
|
|
#' @return A function that takes a single \code{url} parameter, suitable for
|
|
#' passing as the \code{viewer} argument of \code{\link{runGadget}}.
|
|
#'
|
|
#' @rdname viewer
|
|
#' @name viewer
|
|
NULL
|
|
|
|
#' @param minHeight The minimum height (in pixels) desired to show the gadget in
|
|
#' the viewer pane. If a positive number, resize the pane if necessary to show
|
|
#' at least that many pixels. If \code{NULL}, use the existing viewer pane
|
|
#' size. If \code{"maximize"}, use the maximum available vertical space.
|
|
#' @rdname viewer
|
|
#' @export
|
|
paneViewer <- function(minHeight = NULL) {
|
|
viewer <- getOption("viewer")
|
|
if (is.null(viewer)) {
|
|
utils::browseURL
|
|
} else {
|
|
function(url) {
|
|
viewer(url, minHeight)
|
|
}
|
|
}
|
|
}
|
|
|
|
#' @param dialogName The window title to display for the dialog.
|
|
#' @param width,height The desired dialog width/height, in pixels.
|
|
#' @rdname viewer
|
|
#' @export
|
|
dialogViewer <- function(dialogName, width = 600, height = 600) {
|
|
viewer <- getOption("shinygadgets.showdialog")
|
|
if (is.null(viewer)) {
|
|
utils::browseURL
|
|
} else {
|
|
function(url) {
|
|
viewer(dialogName, url, width = width, height = height)
|
|
}
|
|
}
|
|
}
|
|
|
|
#' @param browser See \code{\link[utils]{browseURL}}.
|
|
#' @rdname viewer
|
|
#' @export
|
|
browserViewer <- function(browser = getOption("browser")) {
|
|
function(url) {
|
|
utils::browseURL(url, browser = browser)
|
|
}
|
|
}
|
|
|
|
# Returns TRUE if we're running in Shiny Server or other hosting environment,
|
|
# otherwise returns FALSE.
|
|
inShinyServer <- function() {
|
|
nzchar(Sys.getenv('SHINY_PORT'))
|
|
}
|