In /etc/shiny-server/shiny-server.conf I have an SSL configuration that looks like:
server {
# ...
ssl /etc/path/to/ssl-key.pem /etc/path/to/ssl.cert;
# ...
}
Now, from an R REPL on the server on which Shiny Server is running, I'm curious if there's a way to retrieve the configuration data from shiny-server.conf through any sort of (semi-) official API.
Something like:
# server.R
library(shiny)
shinyServer(function(input, output, session){
# ... ?
})
That will yield something to the effective of:
"ssl": ["/etc/path/to/ssl-key.pem", "/etc/path/to/ssl.cert"]
I don't think you can get access to that, to be honest, as it might be a security risk to allow apps (and therefore "regular users") to see configuration items. (It might include secrets for SAML/LDAP/... configuration for instance.)
Now I don't know that this answer will solve that problem, it might enable you to find what you need instead.
Note: I intentionally filter out several types of objects within session, as I've found that either they crash (they're complex compound objects, perhaps who-knows-what in them) or they're obviously just extraneous. One could possibly saveRDS this to a file and retrieve it from the server if you want to get more visibility to internals being filtered out.
library(shiny)
ui <- bootstrapPage(
h3("Parsed query string"),
verbatimTextOutput("queryText"),
h3("URL components"),
verbatimTextOutput("sessionText"),
h3("EnvVars"),
verbatimTextOutput("envvarText")
)
server <- function(input, output, session) {
# Parse the GET query string
output$queryText <- renderText({
query <- parseQueryString(session$clientData$url_search)
# Return a string with key-value pairs
paste(names(query), query, sep = "=", collapse=", ")
})
# Return the components of the URL in a string:
output$sessionText <- renderText({
cls <- sapply(session, function(a) class(a)[1])
nms <- names(cls[ cls %in% c("list", "character", "numeric", "integer", "NULL", "logical", "environment") ])
nms <- setdiff(nms, ".__enclos_env__")
paste(
capture.output(
str(
sapply(nms,
function(sessnm) {
if (inherits(session[[sessnm]], c("environment", "reactivevalues"))) {
sapply(names(session[[sessnm]]), function(nm) session[[sessnm]][[nm]], simplify = FALSE)
} else if (inherits(session[[sessnm]], c("character", "numeric", "integer"))) {
session[[sessnm]]
} else class(session[[sessnm]])
}, simplify = FALSE),
nchar.max = 1e5,
vec.len = 1e5
)
),
collapse = "\n"
)
})
# Dump the environment variables
output$envvarText <- renderText({
paste(
capture.output(
str(as.list(Sys.getenv()))
),
collapse = "\n"
)
})
}
shinyApp(ui, server)
This renders something like this (with some privacy blocked out, and fuzzy in general since, well, your results my differ depending on your specific server.
This is on RStudio Connect v1.8.2, hosted on Ubuntu 16.04. The authentication is via SAML; other auth methods might have slightly different (more or less) fields.
Related
I have a shiny app which has many text inputs. I could not get the save data part right, for example, to save to a local drive. Any suggestions?
server = function(input, output) {
values <- reactiveValues()
#Initial Dataframe
values$df <- data.frame(matrix(ncol=4,nrow=0, dimnames=list(NULL, c("Name", "date","Traning", "certificate"))))
FinalData =observe({
if(input$submit >0) {
isolate(values$df <- rbind(values$df,data.frame("name" = input$name,"date" = input$date,
"training" = input$training, "certificate" = input$certificate)))
# saveRDS(values$df)
# saveRDS(FinalData)
}})
#display the inputs
output$Combined_table = renderDataTable({values$df})
}
)
Try this demonstration:
library(shiny)
.log <- function(...) message(format(Sys.time(), format = "[ %H:%M:%S ]"), " ", ...)
.read <- function(path) if (file.exists(path)) return(readRDS(path))
shinyApp(
ui = fluidPage(
textInput("txt", "Text: "),
actionButton("btn", "Submit"),
tableOutput("tbl")
),
server = function(input, output, session) {
.log("hello world")
rv <- reactiveValues()
rv$df <- data.frame(row = 0L, word = "a", stringsAsFactors = FALSE)[0,]
observeEvent(req(input$btn), {
.log("submit!")
rv$df <- rbind(rv$df,
data.frame(row = input$btn, word = input$txt,
stringsAsFactors = FALSE))
.log("saveRDS: ", nrow(rv$df))
saveRDS(rv$df, "local.rds")
})
filedata <- reactiveFileReader(1000, session, "local.rds", .read)
output$tbl <- renderTable(filedata())
}
)
The engineering of this app:
I use a reactiveValues like you did, in order to keep the in-memory data. (Note: iteratively adding rows to a frame is bad in the long-run. If this is low-volume adding, then you're probably fine, but it scales badly. Each time a row is added, it copies the entire frame, doubling memory consumption.)
I pre-fill the $df with a zero-row frame, just for formatting. Nothing fancy here.
observe and observeEvent do not return something you are interested in, it should be operating completely by side-effect. It does return something, but it is really only meaningful to shiny internals.
saveRDS as you do, nothing fancy, but it works.
I added a shiny::reactiveFileReader in order to demonstrate that the file was being saved. When the shiny table shows an update, it's because (1) the data was added to the underlying frame; (2) the frame was saved to the "local.rds" file; then (3) reactiveFileReader noticed that the underlying file exists and has changed, causing (4) it to call my .read function to read the contents and return it as reactive data into filedata. This block is completely unnecessary in general, just for demonstration here.
I create a function .read for this reactiveFileReader that is resilient to the file not existing first. If the file does not exist, it invisibly returns NULL. There may be better ways to do this.
I've created a dynamic fileInput in shiny using lapply. When I want to read the file, I've also used lapply in an observer.
The problem of using lapply here is, it is triggered every time I upload a new file and thus, reads all files again and again if a new file is uploaded.
Here I provide a Hello World app. The lapply function depends on an input paramter which I abtracted from for simplicity.
library(shiny)
ui <- fluidPage(
titlePanel("Hello World"),
sidebarLayout(
sidebarPanel(),
mainPanel(
lapply(1:2, function(i) {
fileInput(
paste0("file", i),
label = NULL,
multiple = F,
accept = c(
"text/csv",
"text/comma-separated-values,text/plain",
".csv"
),
buttonLabel = paste("File", i)
)
}),
verbatimTextOutput("list")
)
)
)
server <- function(input, output) {
r <- reactiveValues()
observe({
lapply(1:2, function(i) {
file <- input[[paste0("file",i)]]
if(is.null(file)) return()
isolate({
r$file[[paste(i)]] <- readr::read_csv2(file = file$datapath)
})
})
})
output$list <- renderPrint(reactiveValuesToList(r))
}
shinyApp(ui = ui, server = server)
How to replace the loop or add a requirement to lapply?
While I started down the road of cache-invalidation in the comments, I think something else may work better for you since you have a fixed number of fileInput fields: swap the lapply and observe lines in your code (plus a couple of other tweaks).
server <- function(input, output) {
lapply(paste0("file", 1:2), function(nm) {
observeEvent(input[[ nm ]], {
req(input[[nm]], file.exists(input[[nm]]$datapath))
readr::read_csv2(file = input[[nm]]$datapath)
})
})
}
Explanation:
I'm creating a list of reactive blocks instead of a reactive block operating on a list. This means "file1" won't react to "file2".
I short-cutted the definition of the input names by putting paste0(...) in the data of the lapply instead of in the function, though it'd be just as easy to do
lapply(1:2, function(i) {
nm <- paste0("file", i)
# ...
})
It's important to have nm defined outside of the observeEvent, and it has to do with delayed evaluation and namespace search order. I fell prey to this a few years ago and was straightened out by Joe Cheng: you can't use a for loop, it must be some environment-preserving operation like this.
N.B.: this is a stub of code, and it is far from complete: having an observe or observeEvent read the data and then discard it is wrong ... it's missing something. Ideally, this should really be a reactive or eventReactive block, or the processed data should be stored in a reactiveValues or reactiveVal. For example:
server <- function(input, output) {
mydata <- lapply(paste0("file", 1:2), function(nm) {
observeEvent(input[[ nm ]], {
req(input[[nm]], file.exists(input[[nm]]$datapath))
readr::read_csv2(file = input[[nm]]$datapath)
})
})
observe({
# the following are identical, the latter more declarative
mydata[[1]]
mydata[["file1"]]
})
}
(And another note about defensive programming: you cannot control perfectly how readr::read_csv2 reacts to that file ... it may error out for some reason. One further step would be to wrap it in tryCatch(..., error = function(e) { errfun(e); NULL; }) where errfun(e) does something meaningful with the error message (logs it and/or gives it to the user in a modal popup) and then returns NULL so that reactive blocks downstream can use req(mydata[[1]]) and will not try to process the NULL.
server <- function(input, output) {
mydata <- lapply(paste0("file", 1:2), function(nm) {
observeEvent(input[[ nm ]], {
req(input[[nm]])
file <- input[[nm]]
tryCatch(
readr::read_csv2(file = input[[nm]]$datapath),
error = function(e) { errfun(e); NULL; })
})
})
observe({
# the following are identical, the latter more declarative
mydata[[1]]
mydata[["file1"]]
})
}
In shiny, it is possible to call client-side callbacks written in javascript from the server's logic. Say in ui.R you have some JavaScript including a function called setText:
tags$script('
Shiny.addCustomMessageHandler("setText", function(text) {
document.getElementById("output").innerHTML = text;
})
')
then in your server.R you can call session$sendCustomMessage(type='foo', 'foo').
Suppose I have a long-running function which returns some data to plot. If I do this normally, the R thread is busy while running this function, and so can't handle additional requests in this time. It would be really useful to be able to run this function using the futures package, so that it runs asynchronously to the code, and call the callback asyncronously. However, when I tried this is just didn't seem to work.
Sorry if this isn't very clear. As a simple example, the following should work until you uncomment the two lines trying to invoke future in server.R. Once those lines are uncommented, the callback never gets called. Obviously it's not actually useful in the context of this example, but I think it would be very useful in general.
ui.R:
library(shiny)
shinyUI(fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("max",
"Max random number:",
min = 1,
max = 50,
value = 30)
),
mainPanel(
verbatimTextOutput('output'),
plotOutput('plot')
)
),
tags$script('
Shiny.addCustomMessageHandler("setText", function(text) {
document.getElementById("output").innerHTML = text;
})
')
))
server.R:
library(shiny)
library(future)
plan(multiprocess)
shinyServer(function(input, output, session) {
output$plot <- reactive({
max <- input$max
#f <- future({
session$sendCustomMessage(type='setText', 'Please wait')
Sys.sleep(3)
x <- runif(1,0,max)
session$sendCustomMessage(type='setText', paste('Your random number is', x))
return(NULL)
#})
})
})
Here is a solution on how you could use the future package in a shiny app.
It is possible to have multiple sessions with no session blocking another session when running a computationally intensive task or waiting for a sql query to be finished. I suggest to open two sessions (just open http://127.0.0.1:14072/ in two tabs) and play with the buttons to test the functionality.
run_app.R:
library(shiny)
library(future)
library(shinyjs)
runApp(host = "127.0.0.1", port = 14072, launch.browser = TRUE)
ui.R:
ui <- fluidPage(
useShinyjs(),
textOutput("existsFutureData"),
numericInput("duration", "Duration", value = 5, min = 0),
actionButton("start_proc", h5("get data")),
actionButton("start_proc_future", h5("get data using future")),
checkboxInput("checkbox_syssleep", label = "Use Sys.sleep", value = FALSE),
h5('Table data'),
dataTableOutput('tableData'),
h5('Table future data'),
dataTableOutput('tableFutureData')
)
server.R:
plan(multiprocess)
fakeDataProcessing <- function(duration, sys_sleep = FALSE) {
if(sys_sleep) {
Sys.sleep(duration)
} else {
current_time <- Sys.time()
while (current_time + duration > Sys.time()) { }
}
return(data.frame(test = Sys.time()))
}
#fakeDataProcessing(5)
############################ SERVER ############################
server <- function(input, output, session) {
values <- reactiveValues(runFutureData = FALSE, futureDataLoaded = 0L)
future.env <- new.env()
output$existsFutureData <- renderText({ paste0("exists(futureData): ", exists("futureData", envir = future.env)," | futureDataLoaded: ", values$futureDataLoaded) })
get_data <- reactive({
if (input$start_proc > 0) {
shinyjs::disable("start_proc")
isolate({ data <- fakeDataProcessing(input$duration) })
shinyjs::enable("start_proc")
data
}
})
observeEvent(input$start_proc_future, {
shinyjs::disable("start_proc_future")
duration <- input$duration # This variable needs to be created for use in future object. When using fakeDataProcessing(input$duration) an error occurs: 'Warning: Error in : Operation not allowed without an active reactive context.'
checkbox_syssleep <- input$checkbox_syssleep
future.env$futureData %<-% fakeDataProcessing(duration, sys_sleep = checkbox_syssleep)
future.env$futureDataObj <- futureOf(future.env$futureData)
values$runFutureData <- TRUE
check_if_future_data_is_loaded$resume()
},
ignoreNULL = TRUE,
ignoreInit = TRUE
)
check_if_future_data_is_loaded <- observe({
invalidateLater(1000)
if (resolved(future.env$futureDataObj)) {
check_if_future_data_is_loaded$suspend()
values$futureDataLoaded <- values$futureDataLoaded + 1L
values$runFutureData <- FALSE
shinyjs::enable("start_proc_future")
}
}, suspended = TRUE)
get_futureData <- reactive({ if(values$futureDataLoaded > 0) future.env$futureData })
output$tableData <- renderDataTable(get_data())
output$tableFutureData <- renderDataTable(get_futureData())
session$onSessionEnded(function() {
check_if_future_data_is_loaded$suspend()
})
}
I retooled André le Blond's excellent answer to and made a gist showing a generic asynchronous task processor which can be used either by itself or with Shiny: FutureTaskProcessor.R
Note it contains two files: FutureProcessor.R which is the stand alone asynchronous task handler and app.R which is a Shiny App showing use of the async handler within Shiny.
One admittedly complicated workaround to the single-threaded nature of R within Shiny apps is to do the following:
Splinter off an external R process (run another R script located in
the Shiny app directory, or any directory accessible from within the
Shiny session) from within R (I've tried this splintering before,
and it works).
Configure that script to output its results to a temp directory (assuming you're running Shiny on a Unix-based system) and give the output file a unique filename (preferably named within the namespace of the current session (i.e. "/tmp/[SHINY SESSION HASH ID]_example_output_file.RData".
Use Shiny's invalidateLater() function to check for the presence of that output file.
Load the output file into the Shiny session workspace.
Finally, trash collect by deleting the generated output file after loading.
I hope this helps.
Context
I need to use R metaprogramming for use with the framework Shiny, as their modules and event handlers depend on metaprogramming for passing expressions and executing it in the right context for evaluation to chain with pre-defined input, output, and session objects.
To simplify the problem, I'm going to use the example of a source() function.
Example
To run Shiny, Shiny requires two objects: ui and server. The server is a function: server <- function(input, output, session).
All I want to do is this:
server <- function(input, output, session) {
source('fileA.R', local=TRUE)
source('fileB.R', local=TRUE)
}
This works on its own. However, I have about ten different files to include. So I created a function to return the required code to execute within it:
run_server <- function(str) {
names <- strsplit(x=str, split='/')
code <- character()
for (i in 1:length(names)) {
module_names <- ''
filenames <- ''
module_names[i] <- names[[i]][length(names[[i]])]
filenames[i] <- paste0(str[i], '/server_', module_names[i], '.R')
code <- paste0(code, "source(\'", filenames[i], "\'", ", local = TRUE)", "\n")
}
return(parse(text=code))
}
This works fine... except that it returns the type expression from R, which is different than the return type of quote or substitute. It also means that if I try to use eval/evalq on this, it doesn't work the way I expect.
I've resorted to trying to create the server function using another helper function:
make_server <- function(...) {
code <- eval(substitute(...))
func <- bquote(function(input, output, session) {
.(code)
})
return(
func
)
}
But once again, the function body is a bit weird--it still has the expression() function wrapped around the contents of its body:
function(input, output, session) {
expression(source("modules/upload_rds/server_upload_rds.R",
local = TRUE), source("modules/download_rds/server_download_rds.R",
local = TRUE), source("modules/fileupload/server_fileupload.R",
local = TRUE), source("modules/normalization/server_normalization.R",
local = TRUE), source("modules/outlier_zmedian/server_outlier_zmedian.R",
local = TRUE), source("modules/basic_nmf/server_basic_nmf.R",
local = TRUE), source("modules/monocle/server_monocle.R",
local = TRUE), source("modules/deseq2/server_deseq2.R",
local = TRUE), source("modules/fem_analysis/server_fem_analysis.R",
local = TRUE), source("modules/topGO/server_topGO.R",
local = TRUE), source("modules/cluster_profiler/server_cluster_profiler.R",
local = TRUE), source("modules/scde/server_scde.R", local = TRUE),
source("modules/edgeR/server_edgeR.R", local = TRUE))
}
And this doesn't evaluate as if the code was simply copy-pasted or truly substituted like a C preprocessor would.
expression() vs. quote()
Reading this answer here, it appears that an expression is a vector of calls, etc. So the natural thing to do would be to just apply something like unlist() or c(). But this doesn't work. Does anyone have a solution to this?
I have a big shiny app with about 60 different inputs and it's still growing. Since I use this program a lot, I wanted the settings to be stored until next time I run the app. I made a csv-file that looks something like this:
input,value
input_a,10
input_b,#FFF000
input_c,hide
input_d,65400
I load the csv-file in ui.R and server.R with (not sure why I have to load it two times...)
config <- data.frame(lapply(read.csv(".//config.csv"), as.character), stringsAsFactors = FALSE)
and have inputs like this
sliderInput(
"input_a",
"Number of cats:",
min = 1,max = 50,
value = config[config$input %in% "input_a", "value"]
)
In server.R, I let input changes replace the value in the table and also save the table to the file
observe({
config[config$input %in% "input_a", "value"] <- input$input_a
config[config$input %in% "input_b", "value"] <- input$input_b
config[config$input %in% "input_c", "value"] <- input$input_c
config[config$input %in% "input_d", "value"] <- input$input_d
write.table(config, file = ".//config.csv", col.names = TRUE, row.names = FALSE, quote = FALSE, sep = ",")
})
I'm sure there is a better way to do this, I searched and checked the other similar questions, I started with dget and dput, but then decided to have all relevant settings in one simple file. Sorry if I missed the most relevant question when I searched.
What I don't like about this is that the program also saves the table when it loads the program, before I make any input changes.
How can I get rid of that unnecessary save every time I run the program?
I don't understand all the "reactivity" in shiny, it's still a bit to complicated for me, I don't really know anything about R or programming, just trying to optimize my program since it gets slower with every new "feature" I add.
I don't see any problem with keeping settings like that, but there might be a better way, and in anycase I would wrap it in a function like I did here.
And here is how you implement writing only "on exit" though (also please note the session parameter which is often not used):
library(shiny)
settingsdf <- data.frame(input=c("input_a","input_b","input_c"),
value=c(10,"#FF000","hide"),
stringsAsFactors=F)
setSetting <- function(pname,pval){
idx <- which(settingsdf$input==pname)
if (length(idx)==1){
print(pval)
settingsdf[ idx,2] <<- pval
}
}
shinyApp(
ui = fluidPage(
selectInput("region", "Region:", choices = colnames(WorldPhones)),
plotOutput("phonePlot")
),
server = function(input, output, session) {
output$phonePlot <- renderPlot({
if (length(input$region)>0){
setSetting("input_a",input$region)
barplot(WorldPhones[,input$region]*1000,
ylab = "Number of Telephones", xlab = "Year")
}
})
session$onSessionEnded(function() {
write.csv(settingsdf,"settings.csv")
})
},
options = list(height = 500)
)
Note that I am compressing the ui.R and server.R files into a single file which is not normally done but is nicer for these little examples.
This is not perfect code, I don't read the settings in and initialize the variables, and I use the <<- operator, which some people frown on. But it should help you along.
Update
Here is a more complex version that loads and saves the parameters, and encapsulates them for use. It is better, although it probably should use S3 objects...
library(shiny)
# Settings code
settingsdf <- data.frame(input=c("input_a","region"),
value=c(10,"Asia"),stringsAsFactors=F)
setfname <- "settings.csv"
setSetting <- function(pname,pval){
idx <- which(settingsdf$input==pname)
if (length(idx)==1){
settingsdf[ idx,"value"] <<- pval
}
}
getSetting <- function(pname){
idx <- which(settingsdf$input==pname)
if (length(idx)==1){
rv <- settingsdf[ idx,"value"]
return(rv)
} else {
return("")
}
}
readSettings <- function(){
if (file.exists(setfname)){
settingsdf <<- read.csv(setfname,stringsAsFactors=F)
}
}
writeSettings <- function(){
write.csv(settingsdf,setfname,row.names=F)
}
# ShinyApp
shinyApp(
ui = fluidPage(
selectInput("region","Region:", choices = colnames(WorldPhones)),
plotOutput("phonePlot")
),
server = function(input, output, session) {
readSettings()
vlastinput <- getSetting("region")
if (vlastinput!=""){
updateSelectInput(session, "region", selected = vlastinput )
}
output$phonePlot <- renderPlot({
if (length(input$region)>0){
vlastinput <- input$region
setSetting("region",vlastinput)
barplot(WorldPhones[,input$region]*1000,
ylab = "Number of Telephones", xlab = "Year")
}
})
session$onSessionEnded(function() {
writeSettings()
})
},
options = list(height = 500)
)
Yielding: