├── .Rbuildignore ├── .gitignore ├── inst ├── shinyParallel_server │ ├── ui.R │ ├── server.R │ └── global.R └── shiny │ └── shinyParallelServer │ ├── ui.R │ ├── server.R │ └── global.R ├── NAMESPACE ├── shinyParallel.Rproj ├── DESCRIPTION ├── man ├── installShinyParallel.Rd └── runApp.Rd ├── R ├── installShinyParallel.R └── runApp.R ├── README.md └── README.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README\.Rmd$ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | -------------------------------------------------------------------------------- /inst/shinyParallel_server/ui.R: -------------------------------------------------------------------------------- 1 | library("shiny") 2 | 3 | shinyUI( 4 | fluidPage( 5 | shiny::htmlOutput(outputId = "htmlSess"), 6 | dataTableOutput(outputId = "stattable") 7 | ) 8 | ) 9 | -------------------------------------------------------------------------------- /inst/shiny/shinyParallelServer/ui.R: -------------------------------------------------------------------------------- 1 | library("shiny") 2 | 3 | shinyUI( 4 | fluidPage( 5 | shiny::htmlOutput(outputId = "htmlSess"), 6 | dataTableOutput(outputId = "stattable") 7 | ) 8 | ) 9 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(installShinyParallel) 4 | export(runApp) 5 | importFrom(R.utils,createLink) 6 | importFrom(callr,r_bg) 7 | importFrom(shiny,runApp) 8 | -------------------------------------------------------------------------------- /shinyParallel.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 4 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | BuildType: Package 16 | PackageUseDevtools: Yes 17 | PackageInstallArgs: --no-multiarch --with-keep.source 18 | PackageRoxygenize: rd,collate,namespace 19 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: shinyParallel 2 | Type: Package 3 | Title: Multi-Session Shiny Apps 4 | Version: 0.1.001 5 | Author: Juan Cruz Rodriguez 6 | Maintainer: Juan Cruz Rodriguez 7 | Description: Reimplementation of the shiny runApp function. 8 | It allows defining the number of sessions to run shiny at. 9 | License: GPL (>= 2) 10 | Imports: 11 | R.utils, 12 | callr, 13 | shiny 14 | Encoding: UTF-8 15 | LazyData: true 16 | RoxygenNote: 7.1.1 17 | -------------------------------------------------------------------------------- /man/installShinyParallel.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/installShinyParallel.R 3 | \name{installShinyParallel} 4 | \alias{installShinyParallel} 5 | \title{Installs a multi-session Shiny app in a server} 6 | \usage{ 7 | installShinyParallel( 8 | appDir = getwd(), 9 | appName = basename(appDir), 10 | max.sessions = getOption("shinyParallel.max.sessions", 20L), 11 | users.per.session = getOption("shinyParallel.users.per.session", Inf), 12 | shinyServerPath = "/srv/shiny-server/" 13 | ) 14 | } 15 | \arguments{ 16 | \item{appDir}{The application to run. Should be one of the following: 17 | \itemize{ 18 | \item A directory containing \code{server.R}, plus, either \code{ui.R} or 19 | a \code{www} directory that contains the file \code{index.html}. 20 | \item A directory containing \code{app.R}. 21 | }} 22 | 23 | \item{appName}{Name of the app (path to access it on the server).} 24 | 25 | \item{max.sessions}{Number of sessions to use. Defaults to the 26 | \code{shinyParallel.max.sessions} option, is set, or \code{2L} if not.} 27 | 28 | \item{users.per.session}{Maximum number of admited users per each session. 29 | Defaults to the 30 | \code{shinyParallel.users.per.session} option, is set, or \code{Inf} if 31 | not.} 32 | 33 | \item{shinyServerPath}{Path where shiny-server apps are installed by default.} 34 | } 35 | \description{ 36 | Installs a Shiny app in a Shiny server, with the multi-session feature 37 | enabled. 38 | It will run in \code{max.sessions}, each with the Shiny app working. 39 | So, comunication between users is limited, if this needs to be done, then 40 | save and load data on hard disk (or use RStudio server pro). 41 | } 42 | \examples{ 43 | \dontrun{ 44 | # If we have a Shiny app at '~/myShinyApp', i.e., we can test our app by: 45 | # shinyParallel::runApp('~/myShinyApp'); 46 | 47 | # then we can install the app by typing 48 | shinyParallel::installShinyParallel("~/myShinyApp") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /inst/shinyParallel_server/server.R: -------------------------------------------------------------------------------- 1 | library("shiny") 2 | 3 | # get indicated ports (or default), status 4 | # a port will refer to an appLink number 5 | portsStatus <- reactiveVal(getPortsStatus(max.sessions)) 6 | 7 | # list of processes (names correspond to appLink number) 8 | processes <- reactiveVal(list()) 9 | 10 | shinyServer(function(input, output, session) { 11 | # check if wants to see users dashboard, or the app 12 | isAdmin <- isolate(parseQueryString(session$clientData$url_search)) 13 | 14 | if ("admin" %in% names(isAdmin)) { 15 | output$stattable <- renderDataTable({ 16 | stattable <- do.call(rbind, lapply(processes(), function(actProc) { 17 | c( 18 | Session = actProc$port, 19 | Users = actProc$users 20 | ) 21 | })) 22 | 23 | # if there are no processes, also return some info 24 | if (is.null(stattable)) { 25 | stattable <- data.frame(Session = "NULL", Users = "0") 26 | } 27 | return(stattable) 28 | }) 29 | } else { 30 | cData <- session$clientData 31 | proc <- assignProcess(portsStatus, processes, users.per.session, cData) 32 | 33 | # could not assign process 34 | if (length(proc) == 1 && is.na(proc)) { 35 | showModal(modalDialog("Retrying... If waiting too long, refresh page.", 36 | title = "Server is full.", 37 | footer = NULL 38 | )) 39 | output$htmlSess <- renderUI( 40 | shiny::tags$meta("http-equiv" = "refresh", content = 5) 41 | ) 42 | } else { 43 | output$htmlSess <- renderUI( 44 | shiny::tags$iframe( 45 | src = proc$url, 46 | style = paste("top: 0", "left: 0", "width: 100%", "height: 100%", 47 | "position: absolute", "border: none", 48 | sep = "; " 49 | ) 50 | ) 51 | ) 52 | 53 | onSessionEnded(function() { 54 | deassignProcess(portsStatus, processes, proc, users.per.session) 55 | }) 56 | } 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /inst/shiny/shinyParallelServer/server.R: -------------------------------------------------------------------------------- 1 | library("shiny") 2 | library("callr") 3 | 4 | # loading shinyParallel::runApp args (were saved on HD) 5 | # attaching 'appDir', 'ports', 'max.sessions', 'users.per.session', 'host', 6 | # 'workerId', 'display.mode', 'test.mode' 7 | list2env(get(load(file = paste0(tempdir(), "/env.RData"))), environment()) 8 | 9 | # get indicated ports (or default), status 10 | portsStatus <- reactiveVal(getPortsStatus(host, ports, max.sessions)) 11 | 12 | # list of processes (names correspond to ports) 13 | processes <- reactiveVal(list()) 14 | 15 | shinyServer(function(input, output, session) { 16 | # check if wants to see users dashboard, or the app 17 | isAdmin <- isolate(parseQueryString(session$clientData$url_search)) 18 | 19 | if ("admin" %in% names(isAdmin)) { 20 | output$stattable <- renderDataTable({ 21 | stattable <- do.call(rbind, lapply(processes(), function(actProc) { 22 | c( 23 | PID = actProc$process$get_pid(), 24 | Port = actProc$port, 25 | Users = actProc$users 26 | ) 27 | })) 28 | 29 | # if there are no processes, also return some info 30 | if (is.null(stattable)) { 31 | stattable <- data.frame( 32 | PID = "NULL", Port = "NULL", 33 | Users = "0" 34 | ) 35 | } 36 | return(stattable) 37 | }) 38 | } else { 39 | proc <- assignProcess( 40 | portsStatus, processes, 41 | appDir, max.sessions, users.per.session, host, workerId, 42 | display.mode, test.mode 43 | ) 44 | 45 | # could not assign process 46 | if (length(proc) == 1 && is.na(proc)) { 47 | showModal(modalDialog("Retrying...", 48 | title = "Server is full.", 49 | footer = NULL 50 | )) 51 | output$htmlSess <- renderUI( 52 | shiny::tags$meta("http-equiv" = "refresh", content = 5) 53 | ) 54 | } else { 55 | output$htmlSess <- renderUI( 56 | shiny::tags$iframe( 57 | src = proc$url, 58 | style = paste("top: 0", "left: 0", "width: 100%", "height: 100%", 59 | "position: absolute", "border: none", 60 | sep = "; " 61 | ) 62 | ) 63 | ) 64 | 65 | onSessionEnded(function() { 66 | deassignProcess(portsStatus, processes, proc, users.per.session) 67 | }) 68 | } 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /inst/shinyParallel_server/global.R: -------------------------------------------------------------------------------- 1 | source("settings.R") 2 | 3 | sP_appLinks_dir <- paste0("../shinyParallel_appLinks/", appName, "/") 4 | 5 | getPortsStatus <- function(max.sessions) { 6 | ports <- dir(sP_appLinks_dir)[seq_len(max.sessions)] 7 | ports <- data.frame( 8 | port = ports, 9 | status = factor(rep("Av", length(ports)), 10 | # Available, Full 11 | levels = c("Av", "Full") 12 | ) 13 | ) 14 | return(ports) 15 | } 16 | 17 | assignProcess <- function(portsStatus, processes, users.per.session, cData) { 18 | poStatus <- isolate(portsStatus()) 19 | prStatus <- isolate(processes()) 20 | 21 | # server is full 22 | if (!any(poStatus$status == "Av")) { 23 | return(NA) 24 | } 25 | 26 | avPortIdxs <- which(poStatus$status == "Av") 27 | # First try to get port with no process running 28 | avPortIdx <- avPortIdxs[!poStatus$port[avPortIdxs] %in% names(prStatus)] 29 | 30 | if (length(avPortIdx) > 0) { 31 | # Use any, as we have some ports with no process 32 | avPortIdx <- avPortIdx[[1]] 33 | } else { 34 | # Get the port of the process with less connected users 35 | avPortIdx <- which(poStatus$port == names(which.min(lapply( 36 | prStatus[as.character(poStatus$port[avPortIdxs])], 37 | function(x) x$users 38 | )))) 39 | } 40 | 41 | avPort <- poStatus$port[[avPortIdx]] 42 | avPortStr <- as.character(avPort) 43 | 44 | if (!avPortStr %in% names(prStatus)) { 45 | sessUrl <- urlFromClientData(cData) 46 | 47 | # if the available port does not have a process then create one 48 | shinyProc <- createProcess(avPortStr, sessUrl) 49 | prStatus[[avPortStr]] <- shinyProc 50 | } 51 | 52 | prStatus[[avPortStr]]$users <- prStatus[[avPortStr]]$users + 1 53 | if (prStatus[[avPortStr]]$users >= users.per.session) { 54 | poStatus$status[avPortIdx] <- "Full" 55 | } 56 | 57 | portsStatus(poStatus) 58 | processes(prStatus) 59 | return(prStatus[[avPortStr]]) 60 | } 61 | 62 | urlFromClientData <- function(cData) { 63 | res <- isolate(paste0(cData$url_protocol, "//", cData$url_hostname)) 64 | port <- isolate(cData$url_port) 65 | if (port != "") { 66 | res <- paste0(res, ":", port) 67 | } 68 | 69 | return(res) 70 | } 71 | 72 | createProcess <- function(port, sessUrl) { 73 | fullUrl <- paste0(sessUrl, "/", sP_appLinks_dir, port) 74 | return(list(url = fullUrl, port = port, users = 0)) 75 | } 76 | 77 | deassignProcess <- function(portsStatus, processes, proc, users.per.session = 0) { 78 | poStatus <- isolate(portsStatus()) 79 | prStatus <- isolate(processes()) 80 | 81 | # get updated proc 82 | proc <- prStatus[[as.character(proc$port)]] 83 | proc$users <- proc$users - 1 84 | # update processes status 85 | prStatus[[as.character(proc$port)]] <- proc 86 | 87 | if (proc$users == 0) { 88 | # if no users then kill process 89 | prStatus <- prStatus[names(prStatus) != proc$port] 90 | } 91 | # put Av if it is not full 92 | poStatus$status[poStatus$port == proc$port] <- "Av" 93 | 94 | portsStatus(poStatus) 95 | processes(prStatus) 96 | 97 | return(NA) 98 | } 99 | -------------------------------------------------------------------------------- /R/installShinyParallel.R: -------------------------------------------------------------------------------- 1 | #' Installs a multi-session Shiny app in a server 2 | #' 3 | #' Installs a Shiny app in a Shiny server, with the multi-session feature 4 | #' enabled. 5 | #' It will run in \code{max.sessions}, each with the Shiny app working. 6 | #' So, comunication between users is limited, if this needs to be done, then 7 | #' save and load data on hard disk (or use RStudio server pro). 8 | #' 9 | #' @param appDir The application to run. Should be one of the following: 10 | #' \itemize{ 11 | #' \item A directory containing \code{server.R}, plus, either \code{ui.R} or 12 | #' a \code{www} directory that contains the file \code{index.html}. 13 | #' \item A directory containing \code{app.R}. 14 | #' } 15 | #' @param appName Name of the app (path to access it on the server). 16 | #' @param max.sessions Number of sessions to use. Defaults to the 17 | #' \code{shinyParallel.max.sessions} option, is set, or \code{2L} if not. 18 | #' @param users.per.session Maximum number of admited users per each session. 19 | #' Defaults to the 20 | #' \code{shinyParallel.users.per.session} option, is set, or \code{Inf} if 21 | #' not. 22 | #' @param shinyServerPath Path where shiny-server apps are installed by default. 23 | #' 24 | #' @examples 25 | #' \dontrun{ 26 | #' # If we have a Shiny app at '~/myShinyApp', i.e., we can test our app by: 27 | #' # shinyParallel::runApp('~/myShinyApp'); 28 | #' 29 | #' # then we can install the app by typing 30 | #' shinyParallel::installShinyParallel("~/myShinyApp") 31 | #' } 32 | #' @export 33 | #' @importFrom R.utils createLink 34 | installShinyParallel <- function(appDir = getwd(), 35 | appName = basename(appDir), 36 | max.sessions = getOption("shinyParallel.max.sessions", 20L), 37 | users.per.session = 38 | getOption("shinyParallel.users.per.session", Inf), 39 | shinyServerPath = "/srv/shiny-server/") { 40 | if (!(max.sessions > 0 && users.per.session > 0)) { 41 | stop("max.sessions and users.per.session must be greater than 0.") 42 | } 43 | 44 | if (file.access(shinyServerPath, mode = 2) != 0) { 45 | stop(paste0( 46 | "Current user cant write to ", shinyServerPath, 47 | ' path. Maybe run it as root: "sudo R"' 48 | )) 49 | } 50 | 51 | if (!file.exists(shinyServerPath)) { 52 | stop(paste0( 53 | "Is shiny server installed? Can not find server path: ", 54 | shinyServerPath 55 | )) 56 | } 57 | 58 | if (appName == "") { 59 | stop("Please provide appName.") 60 | } 61 | 62 | appDir <- normalizePath(appDir) 63 | 64 | ## copy shinyServer files 65 | print("Copying shinyParallel server files.") 66 | 67 | # try to find shinyParallel server files 68 | serverFiles <- system.file("shinyParallel_server", package = "shinyParallel") 69 | if (serverFiles == "") { 70 | stop("Error. Try re-installing `shinyParallel`.") 71 | } 72 | 73 | toPath <- paste0(shinyServerPath, appName) 74 | 75 | dir.create(toPath, showWarnings = FALSE) 76 | invisible(lapply(dir(serverFiles, full.names = TRUE), function(x) file.copy(x, toPath))) 77 | 78 | cat(paste0( 79 | "users.per.session <- ", users.per.session, ";\n", 80 | "max.sessions <- ", max.sessions, ";\n", 81 | "appName <- '", appName, "';\n" 82 | ), file = paste0(toPath, "/settings.R")) 83 | 84 | ## copy app files 85 | print("Copying app files.") 86 | appLinksPath <- paste0(shinyServerPath, "shinyParallel_appLinks") 87 | dir.create(appLinksPath, showWarnings = FALSE) 88 | actAppLinksPath <- paste0(appLinksPath, "/", appName) 89 | dir.create(actAppLinksPath, showWarnings = FALSE) 90 | oldWd <- getwd() # backup WD 91 | setwd(actAppLinksPath) 92 | invisible(lapply(seq_len(max.sessions), function(i) { 93 | createLink(paste0(appName, "_", i), appDir, overwrite = TRUE) 94 | })) 95 | # setwd(oldWd); # restore WD 96 | return() 97 | } 98 | -------------------------------------------------------------------------------- /inst/shiny/shinyParallelServer/global.R: -------------------------------------------------------------------------------- 1 | library("httpuv") 2 | 3 | getPortsStatus <- function(host, ports, max.sessions) { 4 | # Reject ports in this range that are considered unsafe by Chrome 5 | # http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome 6 | # https://github.com/rstudio/shiny/issues/1784 7 | if (is.null(ports)) { 8 | ports <- setdiff(3000:8000, c(3659, 4045, 6000, 6665:6669, 6697)) 9 | } 10 | 11 | if (is.vector(ports)) { 12 | ports <- data.frame( 13 | port = ports, 14 | status = factor(rep("DK", length(ports)), 15 | # Dont Know, Not Working, Available, Full 16 | levels = c("DK", "NW", "Av", "Full") 17 | ) 18 | ) 19 | } 20 | 21 | # if we have reached max sessions then return the same object 22 | if (sum(ports$status %in% c("Av", "Full")) >= max.sessions) { 23 | return(ports) 24 | } 25 | 26 | # if there are no more ports to try, then lets try on previous not working 27 | if (sum(ports$status == "DK") == 0) { 28 | ports$status[ports$status == "NW"] <- "DK" 29 | } 30 | 31 | # test all ports until any is available 32 | for (i in which(ports$status == "DK")) { 33 | # Test port to see if we can use it 34 | tmp <- try(httpuv::startServer(host, ports$port[[i]], list()), silent = TRUE) 35 | ports$status[[i]] <- "NW" 36 | if (!inherits(tmp, "try-error")) { 37 | httpuv::stopServer(tmp) 38 | ports$status[[i]] <- "Av" 39 | } 40 | if (sum(ports$status %in% c("Av", "Full")) == max.sessions || 41 | is.infinite(max.sessions)) { 42 | break 43 | } 44 | } 45 | 46 | return(ports) 47 | } 48 | 49 | assignProcess <- function(portsStatus, processes, 50 | appDir, max.sessions, users.per.session, host, 51 | workerId, display.mode, test.mode) { 52 | poStatus <- isolate(portsStatus()) 53 | prStatus <- isolate(processes()) 54 | 55 | # if there is no available port then try to find any 56 | if (sum(poStatus$status == "Av") == 0) { 57 | poStatus <- getPortsStatus(host, poStatus, max.sessions) 58 | } 59 | 60 | # could not find any port to use, or server is full 61 | if (!any(poStatus$status == "Av")) { 62 | return(NA) 63 | } 64 | 65 | avPortIdxs <- which(poStatus$status == "Av") 66 | # First try to get port with no process running 67 | avPortIdx <- avPortIdxs[!poStatus$port[avPortIdxs] %in% names(prStatus)] 68 | 69 | if (length(avPortIdx) > 0) { 70 | # Use any, as we have some ports with no process 71 | avPortIdx <- avPortIdx[[1]] 72 | } else { 73 | # Get the port of the process with less connected users 74 | avPortIdx <- which(poStatus$port == names(which.min(lapply( 75 | prStatus[as.character(poStatus$port[avPortIdxs])], 76 | function(x) x$users 77 | )))) 78 | } 79 | 80 | avPort <- poStatus$port[[avPortIdx]] 81 | avPortStr <- as.character(avPort) 82 | 83 | if (!avPortStr %in% names(prStatus)) { 84 | # if the available port does not have a process then create one 85 | shinyProc <- createProcess( 86 | appDir, avPort, host, workerId, display.mode, 87 | test.mode 88 | ) 89 | if (length(shinyProc) == 1 && is.na(shinyProc)) { 90 | return(NA) 91 | } 92 | prStatus[[avPortStr]] <- shinyProc 93 | } 94 | 95 | prStatus[[avPortStr]]$users <- prStatus[[avPortStr]]$users + 1 96 | if (prStatus[[avPortStr]]$users >= users.per.session) { 97 | poStatus$status[avPortIdx] <- "Full" 98 | } 99 | 100 | portsStatus(poStatus) 101 | processes(prStatus) 102 | return(prStatus[[avPortStr]]) 103 | } 104 | 105 | createProcess <- function(appDir, port, host, workerId, display.mode, 106 | test.mode) { 107 | shinyProc <- r_bg( 108 | function(appDir, port, host, workerId, display.mode, test.mode) { 109 | shiny::runApp( 110 | appDir = appDir, port = port, host = host, workerId = workerId, 111 | display.mode = display.mode, test.mode = test.mode 112 | ) 113 | }, 114 | args = list( 115 | appDir = appDir, port = port, host = host, workerId = workerId, 116 | display.mode = display.mode, test.mode = test.mode 117 | ) 118 | ) 119 | sessUrl <- NA 120 | for (i in seq_len(10)) { # give n retries 121 | errLines <- shinyProc$read_error_lines() 122 | errLines <- errLines[grep("Listening on ", errLines)] 123 | if (length(errLines) > 0) { 124 | sessUrl <- sub(".*http", "http", errLines) 125 | break 126 | } 127 | Sys.sleep(1) # give 1 second to start server 128 | } 129 | if (is.na(sessUrl)) { 130 | # if after n retries it did not work then give message 131 | print(shinyProc$read_output_lines()) 132 | return(NA) 133 | } 134 | 135 | return(c(process = shinyProc, url = sessUrl, port = port, users = 0)) 136 | } 137 | 138 | deassignProcess <- function(portsStatus, processes, proc, users.per.session = 0) { 139 | poStatus <- isolate(portsStatus()) 140 | prStatus <- isolate(processes()) 141 | 142 | # get updated proc 143 | proc <- prStatus[[as.character(proc$port)]] 144 | proc$users <- proc$users - 1 145 | # update processes status 146 | prStatus[[as.character(proc$port)]] <- proc 147 | 148 | if (proc$users == 0) { 149 | # if no users then kill process 150 | proc$process$kill() 151 | prStatus <- prStatus[names(prStatus) != proc$port] 152 | poStatus$status[poStatus$port == proc$port] <- "Av" 153 | } else if (proc$users < users.per.session) { 154 | # put Av if it is not full 155 | poStatus$status[poStatus$port == proc$port] <- "Av" 156 | } 157 | 158 | portsStatus(poStatus) 159 | processes(prStatus) 160 | 161 | return(NA) 162 | } 163 | -------------------------------------------------------------------------------- /man/runApp.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/runApp.R 3 | \name{runApp} 4 | \alias{runApp} 5 | \title{Run Shiny Application} 6 | \usage{ 7 | runApp( 8 | appDir = getwd(), 9 | ports = getOption("shiny.ports"), 10 | max.sessions = getOption("shinyParallel.max.sessions", 20L), 11 | users.per.session = getOption("shinyParallel.users.per.session", Inf), 12 | launch.browser = getOption("shiny.launch.browser", interactive()), 13 | host = getOption("shiny.host", "127.0.0.1"), 14 | workerId = "", 15 | quiet = FALSE, 16 | display.mode = c("auto", "normal", "showcase"), 17 | test.mode = getOption("shiny.testmode", FALSE) 18 | ) 19 | } 20 | \arguments{ 21 | \item{appDir}{The application to run. Should be one of the following: 22 | \itemize{ 23 | \item A directory containing \code{server.R}, plus, either \code{ui.R} or 24 | a \code{www} directory that contains the file \code{index.html}. 25 | \item A directory containing \code{app.R}. 26 | \item An \code{.R} file containing a Shiny application, ending with an 27 | expression that produces a Shiny app object. 28 | \item A list with \code{ui} and \code{server} components. 29 | \item A Shiny app object created by \code{\link{shinyApp}}. 30 | }} 31 | 32 | \item{ports}{The TCP ports that the application should listen on. First port 33 | will be used for shinyParallel server, and the remaining for each session. 34 | If the \code{ports} are not specified, and the \code{shiny.ports} option is 35 | set (with \code{options(shiny.ports = c(XX,..,ZZ)}), then those ports will 36 | be used. Otherwise, use random ports.} 37 | 38 | \item{max.sessions}{Number of sessions to use. Defaults to the 39 | \code{shinyParallel.max.sessions} option, is set, or \code{2L} if not.} 40 | 41 | \item{users.per.session}{Maximum number of admited users per each session. 42 | Defaults to the 43 | \code{shinyParallel.users.per.session} option, is set, or \code{Inf} if 44 | not.} 45 | 46 | \item{launch.browser}{If true, the system's default web browser will be 47 | launched automatically after the app is started. Defaults to true in 48 | interactive sessions only. This value of this parameter can also be a 49 | function to call with the application's URL.} 50 | 51 | \item{host}{The IPv4 address that the application should listen on. Defaults 52 | to the \code{shiny.host} option, if set, or \code{"127.0.0.1"} if not. See 53 | Details.} 54 | 55 | \item{workerId}{Can generally be ignored. Exists to help some editions of 56 | Shiny Server Pro route requests to the correct process.} 57 | 58 | \item{quiet}{Should Shiny status messages be shown? Defaults to FALSE.} 59 | 60 | \item{display.mode}{The mode in which to display the application. If set to 61 | the value \code{"showcase"}, shows application code and metadata from a 62 | \code{DESCRIPTION} file in the application directory alongside the 63 | application. If set to \code{"normal"}, displays the application normally. 64 | Defaults to \code{"auto"}, which displays the application in the mode given 65 | in its \code{DESCRIPTION} file, if any.} 66 | 67 | \item{test.mode}{Should the application be launched in test mode? This is 68 | only used for recording or running automated tests. Defaults to the 69 | \code{shiny.testmode} option, or FALSE if the option is not set.} 70 | } 71 | \description{ 72 | Runs a Shiny application. This function normally does not return; interrupt R 73 | to stop the application (usually by pressing Ctrl+C or Esc). 74 | It runs \code{max.sessions} processes, each with a shiny::runApp working. 75 | So, comunication between users is limited, if this needs to be done, then 76 | save and load data on hard disk (or use RStudio server pro). 77 | } 78 | \details{ 79 | The host parameter was introduced in Shiny 0.9.0. Its default value of 80 | \code{"127.0.0.1"} means that, contrary to previous versions of Shiny, only 81 | the current machine can access locally hosted Shiny apps. To allow other 82 | clients to connect, use the value \code{"0.0.0.0"} instead (which was the 83 | value that was hard-coded into Shiny in 0.8.0 and earlier). 84 | } 85 | \examples{ 86 | \dontrun{ 87 | # Start app in the current working directory 88 | shinyParallel::runApp() 89 | 90 | # Start app in a subdirectory called myapp 91 | shinyParallel::runApp("myapp") 92 | } 93 | 94 | ## Only run this example in interactive R sessions 95 | if (interactive()) { 96 | options(device.ask.default = FALSE) 97 | 98 | # Apps can be run without a server.r and ui.r file 99 | shinyParallel::runApp(list( 100 | ui = bootstrapPage( 101 | numericInput("n", "Number of obs", 100), 102 | plotOutput("plot") 103 | ), 104 | server = function(input, output) { 105 | output$plot <- renderPlot({ 106 | hist(runif(input$n)) 107 | }) 108 | } 109 | )) 110 | 111 | 112 | # Another example 113 | shinyParallel::runApp(list( 114 | ui = fluidPage(column(3, wellPanel( 115 | numericInput("n", label = "Is it prime?", value = 7, min = 1), 116 | actionButton("check", "Check!") 117 | ))), 118 | server = function(input, output) { 119 | # Check if n is prime. 120 | # Not R optimized. 121 | # No Fermat, Miller-Rabin, Solovay-Strassen, Frobenius, etc tests. 122 | # Check if n is divisable up to n-1 !! 123 | isPrime <- function(n) { 124 | res <- TRUE 125 | i <- 2 126 | while (i < n) { 127 | res <- res && n \%\% i != 0 128 | i <- i + 1 129 | } 130 | return(res) 131 | } 132 | observeEvent(input$check, { 133 | showModal(modalDialog( 134 | ifelse(isPrime(isolate(input$n)), 135 | "Yes it is!", "Nope, not a prime." 136 | ), 137 | footer = NULL, 138 | easyClose = TRUE 139 | )) 140 | }) 141 | } 142 | )) 143 | 144 | 145 | # Running a Shiny app object 146 | app <- shinyApp( 147 | ui = bootstrapPage( 148 | numericInput("n", "Number of obs", 100), 149 | plotOutput("plot") 150 | ), 151 | server = function(input, output) { 152 | output$plot <- renderPlot({ 153 | hist(runif(input$n)) 154 | }) 155 | } 156 | ) 157 | shinyParallel::runApp(app) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ShinyParallel 2 | ================ 3 | 4 | Run [Shiny](http://shiny.rstudio.com/) applications in a multi-session 5 | mode. 6 | 7 | Prevents that if a user executes a long computing task penalizing 8 | others. 9 | 10 | ShinyParallel manages incoming users and redistributes them between 11 | multiple sessions created for your Shiny app. It provides two modes of 12 | use: 13 | 14 | - From an R console: ShinyParallel reimplements the function 15 | `shiny::runApp()`. In this sense, the only thing to do to 16 | run an app in multi-session mode is to call it using 17 | `shinyParallel::runApp()`. 18 | - Installing ShinyParallel in a Shiny server (**may require root**): 19 | by the `shinyParallel::installShinyParallel()` function, 20 | ShinyParallel is installed in your Shiny server for any desired app. 21 | 22 | **Note:** ShinyParallel should work on any operating system that 23 | supports R, however it has been tested only under Linux (Ubuntu). 24 | 25 | ## Features 26 | 27 | - Run a Shiny app in multiple sessions (processes / physical cores). 28 | - Decide the maximum number of users per session. 29 | - It allows to visualize the number of users currently present in each 30 | session. 31 | 32 | ## Installation 33 | 34 | ShinyParallel is currently only available as a GitHub package. To 35 | install it run the following from an R console: 36 | 37 | ``` r 38 | if (!require("remotes")) { 39 | install.packages("remotes") 40 | } 41 | remotes::install_github("jcrodriguez1989/shinyParallel") 42 | ``` 43 | 44 | ## runApp mode 45 | 46 | ### Usage 47 | 48 | If you run your Shiny app like this: 49 | 50 | ``` r 51 | runApp(appDir=myApp, ) 52 | ``` 53 | 54 | Just replace it by: 55 | 56 | ``` r 57 | shinyParallel::runApp(appDir=myApp, ) 58 | ``` 59 | 60 | The only parameter that varies is `port`, in `shinyParallel::runApp` the 61 | parameter is modified by `ports`. And instead of being `numeric` of 62 | length 1, it will now be numeric of length equal to the number of ports 63 | available to use. Where the first port will be used by the ShinyParallel 64 | app, and the rest by the generated sessions. 65 | 66 | The `shinyParallel::runApp` function has two additional parameters: 67 | 68 | - `max.sessions`: Maximum number of sessions to use. 69 | - `users.per.session`: Maximum number of admited users per each 70 | session. 71 | 72 | ### Example 73 | 74 | ``` r 75 | library("shiny") 76 | 77 | # Create a Shiny app object 78 | app <- shinyApp( 79 | ui = fluidPage( 80 | column(3, wellPanel( 81 | numericInput("n", label = "Is it prime?", value = 7, min = 1), 82 | actionButton("check", "Check!") 83 | )) 84 | ), 85 | server = function(input, output) { 86 | # Check if n is prime. 87 | # Not R optimized. 88 | # No Fermat, Miller-Rabin, Solovay-Strassen, Frobenius, etc tests. 89 | # Check if n is divisable up to n-1 !! 90 | isPrime <- function(n) { 91 | res <- TRUE 92 | i <- 2 93 | while (i < n) { 94 | res <- res && n %% i != 0 95 | i <- i + 1 96 | } 97 | return(res) 98 | } 99 | observeEvent(input$check, { 100 | showModal(modalDialog( 101 | ifelse(isPrime(isolate(input$n)), 102 | "Yes it is!", "Nope, not a prime." 103 | ), 104 | footer = NULL, 105 | easyClose = TRUE 106 | )) 107 | }) 108 | } 109 | ) 110 | 111 | # Run it with Shiny 112 | shiny::runApp(app) 113 | # Run it with ShinyParallel default params 114 | shinyParallel::runApp(app) 115 | # Run it with ShinyParallel, give one session per user 116 | shinyParallel::runApp(app, max.sessions = Inf, users.per.session = 1) 117 | ``` 118 | 119 | In this example, if the app is run with `shiny::runApp`, and a user 120 | wants to calculate if the number 179424691 is prime then the app will be 121 | blocked for other users for some minutes, if the app is run with 122 | `shinyParallel::runApp` not. 123 | 124 | If the shiny app url is `http://:/` then enter 125 | `http://:/?admin` to view a panel that lists the number of 126 | users currently present in each session. 127 | 128 | ## installShinyParallel mode 129 | 130 | ### Usage 131 | 132 | If your application is at ``, i.e., from an R terminal 133 | `runApp()` starts the app, then to install it on the server 134 | just run R as root (or make sure the actual user has write permissions 135 | on the Shiny server) and run the `installShinyParallel()` 136 | command. 137 | 138 | ### Example 139 | 140 | First, let’s create our Shiny app, from a Linux terminal type: 141 | 142 | ``` bash 143 | cd ~ 144 | mkdir myShinyApp 145 | echo " 146 | library('shiny') 147 | 148 | # Create a Shiny app object 149 | app <- shinyApp( 150 | ui = fluidPage( 151 | column(3, wellPanel( 152 | numericInput('n', label = 'Is it prime?', value = 7, min = 1), 153 | actionButton('check', 'Check!') 154 | ) 155 | )), 156 | server = function(input, output) { 157 | # Check if n is prime. 158 | # Not R optimized. 159 | # No Fermat, Miller-Rabin, Solovay-Strassen, Frobenius, etc tests. 160 | # Check if n is divisable up to n-1 !! 161 | isPrime <- function(n) { 162 | res <- TRUE 163 | i <- 2 164 | while (i < n) { 165 | res <- res && n %% i != 0 166 | i <- i + 1 167 | } 168 | return(res) 169 | } 170 | observeEvent(input\$check, { 171 | showModal(modalDialog( 172 | ifelse(isPrime(isolate(input\$n)), 173 | 'Yes it is!', 'Nope, not a prime.'), 174 | footer = NULL, 175 | easyClose = TRUE 176 | )) 177 | }) 178 | } 179 | ) 180 | " > myShinyApp/app.R 181 | ``` 182 | 183 | So now we can try our app, and install it with multi-session feature, 184 | from a R (sudo) console type: 185 | 186 | ``` r 187 | library("shinyParallel") 188 | # And install it 189 | shinyParallel::installShinyParallel("./myShinyApp", 190 | max.sessions = 20, 191 | users.per.session = 5 192 | ) 193 | ``` 194 | 195 | ## Limitations 196 | 197 | - Each session that ShinyParallel generates is independent of the 198 | others, i.e., the global variables of a session (shiny app) will not 199 | be modified in another one. Two users present in different session 200 | will not be able to interact with the same values of the variables. 201 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ShinyParallel" 3 | output: github_document 4 | --- 5 | 6 | Run [Shiny](http://shiny.rstudio.com/) applications in a multi-session mode. 7 | 8 | Prevents that if a user executes a long computing task penalizing 9 | others. 10 | 11 | ShinyParallel manages incoming users and redistributes them between multiple 12 | sessions created for your Shiny app. It provides two modes of use: 13 | 14 | * From an R console: ShinyParallel reimplements the function 15 | `shiny::runApp()`. In this sense, the only thing to do to run an app 16 | in multi-session mode is to call it using `shinyParallel::runApp()`. 17 | * Installing ShinyParallel in a Shiny server (**may require root**): by the 18 | `shinyParallel::installShinyParallel()` function, ShinyParallel is 19 | installed in your Shiny server for any desired app. 20 | 21 | **Note:** ShinyParallel should work on any operating system that supports R, 22 | however it has been tested only under Linux (Ubuntu). 23 | 24 | ## Features 25 | 26 | * Run a Shiny app in multiple sessions (processes / physical cores). 27 | * Decide the maximum number of users per session. 28 | * It allows to visualize the number of users currently present in each session. 29 | 30 | ## Installation 31 | 32 | ShinyParallel is currently only available as a GitHub package. To install it run 33 | the following from an R console: 34 | 35 | ```{r eval=FALSE} 36 | if (!require("remotes")) { 37 | install.packages("remotes") 38 | } 39 | remotes::install_github("jcrodriguez1989/shinyParallel") 40 | ``` 41 | 42 | ## runApp mode 43 | 44 | ### Usage 45 | 46 | If you run your Shiny app like this: 47 | 48 | ```{r eval=FALSE} 49 | runApp(appDir=myApp, ) 50 | ``` 51 | 52 | Just replace it by: 53 | 54 | ```{r eval=FALSE} 55 | shinyParallel::runApp(appDir=myApp, ) 56 | ``` 57 | 58 | The only parameter that varies is `port`, in `shinyParallel::runApp` the 59 | parameter is modified by `ports`. And instead of being `numeric` of length 1, it 60 | will now be numeric of length equal to the number of ports available to use. 61 | Where the first port will be used by the ShinyParallel app, and the rest by the 62 | generated sessions. 63 | 64 | The `shinyParallel::runApp` function has two additional parameters: 65 | 66 | * `max.sessions`: Maximum number of sessions to use. 67 | * `users.per.session`: Maximum number of admited users per each session. 68 | 69 | ### Example 70 | 71 | ```{r eval=FALSE} 72 | library("shiny") 73 | 74 | # Create a Shiny app object 75 | app <- shinyApp( 76 | ui = fluidPage( 77 | column(3, wellPanel( 78 | numericInput("n", label = "Is it prime?", value = 7, min = 1), 79 | actionButton("check", "Check!") 80 | )) 81 | ), 82 | server = function(input, output) { 83 | # Check if n is prime. 84 | # Not R optimized. 85 | # No Fermat, Miller-Rabin, Solovay-Strassen, Frobenius, etc tests. 86 | # Check if n is divisable up to n-1 !! 87 | isPrime <- function(n) { 88 | res <- TRUE 89 | i <- 2 90 | while (i < n) { 91 | res <- res && n %% i != 0 92 | i <- i + 1 93 | } 94 | return(res) 95 | } 96 | observeEvent(input$check, { 97 | showModal(modalDialog( 98 | ifelse(isPrime(isolate(input$n)), 99 | "Yes it is!", "Nope, not a prime." 100 | ), 101 | footer = NULL, 102 | easyClose = TRUE 103 | )) 104 | }) 105 | } 106 | ) 107 | 108 | # Run it with Shiny 109 | shiny::runApp(app) 110 | # Run it with ShinyParallel default params 111 | shinyParallel::runApp(app) 112 | # Run it with ShinyParallel, give one session per user 113 | shinyParallel::runApp(app, max.sessions = Inf, users.per.session = 1) 114 | ``` 115 | 116 | In this example, if the app is run with `shiny::runApp`, and a user wants to 117 | calculate if the number 179424691 is prime then the app will be blocked for 118 | other users for some minutes, if the app is run with `shinyParallel::runApp` 119 | not. 120 | 121 | If the shiny app url is `http://:/` then enter 122 | `http://:/?admin` to view a panel that lists the number of users 123 | currently present in each session. 124 | 125 | ## installShinyParallel mode 126 | 127 | ### Usage 128 | 129 | If your application is at ``, i.e., from an R terminal 130 | `runApp()` starts the app, then to install it on the server just run 131 | R as root (or make sure the actual user has write permissions on the Shiny 132 | server) and run the `installShinyParallel()` command. 133 | 134 | ### Example 135 | 136 | First, let's create our Shiny app, from a Linux terminal type: 137 | ```{bash eval=F} 138 | cd ~ 139 | mkdir myShinyApp 140 | echo " 141 | library('shiny') 142 | 143 | # Create a Shiny app object 144 | app <- shinyApp( 145 | ui = fluidPage( 146 | column(3, wellPanel( 147 | numericInput('n', label = 'Is it prime?', value = 7, min = 1), 148 | actionButton('check', 'Check!') 149 | ) 150 | )), 151 | server = function(input, output) { 152 | # Check if n is prime. 153 | # Not R optimized. 154 | # No Fermat, Miller-Rabin, Solovay-Strassen, Frobenius, etc tests. 155 | # Check if n is divisable up to n-1 !! 156 | isPrime <- function(n) { 157 | res <- TRUE 158 | i <- 2 159 | while (i < n) { 160 | res <- res && n %% i != 0 161 | i <- i + 1 162 | } 163 | return(res) 164 | } 165 | observeEvent(input\$check, { 166 | showModal(modalDialog( 167 | ifelse(isPrime(isolate(input\$n)), 168 | 'Yes it is!', 'Nope, not a prime.'), 169 | footer = NULL, 170 | easyClose = TRUE 171 | )) 172 | }) 173 | } 174 | ) 175 | " > myShinyApp/app.R 176 | ``` 177 | 178 | So now we can try our app, and install it with multi-session feature, from a R 179 | (sudo) console type: 180 | 181 | ```{r eval=F} 182 | library("shinyParallel") 183 | # And install it 184 | shinyParallel::installShinyParallel("./myShinyApp", 185 | max.sessions = 20, 186 | users.per.session = 5 187 | ) 188 | ``` 189 | 190 | 191 | ## Limitations 192 | 193 | * Each session that ShinyParallel generates is independent of the others, i.e., 194 | the global variables of a session (shiny app) will not be modified in another 195 | one. Two users present in different session will not be able to interact with 196 | the same values of the variables. 197 | -------------------------------------------------------------------------------- /R/runApp.R: -------------------------------------------------------------------------------- 1 | #' Run Shiny Application 2 | #' 3 | #' Runs a Shiny application. This function normally does not return; interrupt R 4 | #' to stop the application (usually by pressing Ctrl+C or Esc). 5 | #' It runs \code{max.sessions} processes, each with a shiny::runApp working. 6 | #' So, comunication between users is limited, if this needs to be done, then 7 | #' save and load data on hard disk (or use RStudio server pro). 8 | #' 9 | #' The host parameter was introduced in Shiny 0.9.0. Its default value of 10 | #' \code{"127.0.0.1"} means that, contrary to previous versions of Shiny, only 11 | #' the current machine can access locally hosted Shiny apps. To allow other 12 | #' clients to connect, use the value \code{"0.0.0.0"} instead (which was the 13 | #' value that was hard-coded into Shiny in 0.8.0 and earlier). 14 | #' 15 | #' @param appDir The application to run. Should be one of the following: 16 | #' \itemize{ 17 | #' \item A directory containing \code{server.R}, plus, either \code{ui.R} or 18 | #' a \code{www} directory that contains the file \code{index.html}. 19 | #' \item A directory containing \code{app.R}. 20 | #' \item An \code{.R} file containing a Shiny application, ending with an 21 | #' expression that produces a Shiny app object. 22 | #' \item A list with \code{ui} and \code{server} components. 23 | #' \item A Shiny app object created by \code{\link{shinyApp}}. 24 | #' } 25 | #' @param ports The TCP ports that the application should listen on. First port 26 | #' will be used for shinyParallel server, and the remaining for each session. 27 | #' If the \code{ports} are not specified, and the \code{shiny.ports} option is 28 | #' set (with \code{options(shiny.ports = c(XX,..,ZZ)}), then those ports will 29 | #' be used. Otherwise, use random ports. 30 | #' @param max.sessions Number of sessions to use. Defaults to the 31 | #' \code{shinyParallel.max.sessions} option, is set, or \code{2L} if not. 32 | #' @param users.per.session Maximum number of admited users per each session. 33 | #' Defaults to the 34 | #' \code{shinyParallel.users.per.session} option, is set, or \code{Inf} if 35 | #' not. 36 | #' @param launch.browser If true, the system's default web browser will be 37 | #' launched automatically after the app is started. Defaults to true in 38 | #' interactive sessions only. This value of this parameter can also be a 39 | #' function to call with the application's URL. 40 | #' @param host The IPv4 address that the application should listen on. Defaults 41 | #' to the \code{shiny.host} option, if set, or \code{"127.0.0.1"} if not. See 42 | #' Details. 43 | #' @param workerId Can generally be ignored. Exists to help some editions of 44 | #' Shiny Server Pro route requests to the correct process. 45 | #' @param quiet Should Shiny status messages be shown? Defaults to FALSE. 46 | #' @param display.mode The mode in which to display the application. If set to 47 | #' the value \code{"showcase"}, shows application code and metadata from a 48 | #' \code{DESCRIPTION} file in the application directory alongside the 49 | #' application. If set to \code{"normal"}, displays the application normally. 50 | #' Defaults to \code{"auto"}, which displays the application in the mode given 51 | #' in its \code{DESCRIPTION} file, if any. 52 | #' @param test.mode Should the application be launched in test mode? This is 53 | #' only used for recording or running automated tests. Defaults to the 54 | #' \code{shiny.testmode} option, or FALSE if the option is not set. 55 | #' 56 | #' @examples 57 | #' \dontrun{ 58 | #' # Start app in the current working directory 59 | #' shinyParallel::runApp() 60 | #' 61 | #' # Start app in a subdirectory called myapp 62 | #' shinyParallel::runApp("myapp") 63 | #' } 64 | #' 65 | #' ## Only run this example in interactive R sessions 66 | #' if (interactive()) { 67 | #' options(device.ask.default = FALSE) 68 | #' 69 | #' # Apps can be run without a server.r and ui.r file 70 | #' shinyParallel::runApp(list( 71 | #' ui = bootstrapPage( 72 | #' numericInput("n", "Number of obs", 100), 73 | #' plotOutput("plot") 74 | #' ), 75 | #' server = function(input, output) { 76 | #' output$plot <- renderPlot({ 77 | #' hist(runif(input$n)) 78 | #' }) 79 | #' } 80 | #' )) 81 | #' 82 | #' 83 | #' # Another example 84 | #' shinyParallel::runApp(list( 85 | #' ui = fluidPage(column(3, wellPanel( 86 | #' numericInput("n", label = "Is it prime?", value = 7, min = 1), 87 | #' actionButton("check", "Check!") 88 | #' ))), 89 | #' server = function(input, output) { 90 | #' # Check if n is prime. 91 | #' # Not R optimized. 92 | #' # No Fermat, Miller-Rabin, Solovay-Strassen, Frobenius, etc tests. 93 | #' # Check if n is divisable up to n-1 !! 94 | #' isPrime <- function(n) { 95 | #' res <- TRUE 96 | #' i <- 2 97 | #' while (i < n) { 98 | #' res <- res && n %% i != 0 99 | #' i <- i + 1 100 | #' } 101 | #' return(res) 102 | #' } 103 | #' observeEvent(input$check, { 104 | #' showModal(modalDialog( 105 | #' ifelse(isPrime(isolate(input$n)), 106 | #' "Yes it is!", "Nope, not a prime." 107 | #' ), 108 | #' footer = NULL, 109 | #' easyClose = TRUE 110 | #' )) 111 | #' }) 112 | #' } 113 | #' )) 114 | #' 115 | #' 116 | #' # Running a Shiny app object 117 | #' app <- shinyApp( 118 | #' ui = bootstrapPage( 119 | #' numericInput("n", "Number of obs", 100), 120 | #' plotOutput("plot") 121 | #' ), 122 | #' server = function(input, output) { 123 | #' output$plot <- renderPlot({ 124 | #' hist(runif(input$n)) 125 | #' }) 126 | #' } 127 | #' ) 128 | #' shinyParallel::runApp(app) 129 | #' } 130 | #' @export 131 | #' @importFrom callr r_bg 132 | #' @importFrom shiny runApp 133 | runApp <- function(appDir = getwd(), 134 | ports = getOption("shiny.ports"), 135 | max.sessions = getOption("shinyParallel.max.sessions", 20L), 136 | users.per.session = 137 | getOption("shinyParallel.users.per.session", Inf), 138 | launch.browser = getOption( 139 | "shiny.launch.browser", 140 | interactive() 141 | ), 142 | host = getOption("shiny.host", "127.0.0.1"), 143 | workerId = "", 144 | quiet = FALSE, 145 | display.mode = c("auto", "normal", "showcase"), 146 | test.mode = getOption("shiny.testmode", FALSE)) { 147 | # args distribution: 148 | # appDir (shiny) 149 | # ports (both) must be a vector of ports, one per session 150 | # launch.browser (shinyParallel) function to open main url 151 | # host (both) 152 | # workerId (both) 153 | # quiet (shinyParallel) 154 | # display.mode (shiny) 155 | # test.mode (shiny) 156 | 157 | if (!(max.sessions > 0 && users.per.session > 0)) { 158 | stop("max.sessions and users.per.session must be greater than 0.") 159 | } 160 | 161 | # we need one port for the server, and n ports for n sessions 162 | if (length(ports) > 0 && length(ports) < (max.sessions + 1)) { 163 | stop("Must give at least max.sessions + 1 ports.") 164 | } 165 | 166 | # args to be used by each shiny created session 167 | env2save <- as.list(environment())[ 168 | c( 169 | "appDir", "ports", "max.sessions", "users.per.session", "host", 170 | "workerId", "display.mode", "test.mode" 171 | ) 172 | ] 173 | 174 | # try to load the shinyParallel server app 175 | serverAppDir <- system.file("shiny", "shinyParallelServer", 176 | package = "shinyParallel" 177 | ) 178 | if (serverAppDir == "") { 179 | stop("Could not find GUI directory. Try re-installing `shinyParallel`.") 180 | } 181 | 182 | # the shiny app will run with the same tempdir, so in this way we can pass 183 | # the environment 184 | save(env2save, file = paste0(tempdir(), "/env.RData")) 185 | shiny::runApp( 186 | appDir = serverAppDir, port = ports[[1]], 187 | launch.browser = launch.browser, host = host, workerId = workerId, 188 | quiet = quiet 189 | ) 190 | } 191 | --------------------------------------------------------------------------------