├── .Rbuildignore ├── .travis.yml ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── NEWS.md ├── R ├── basegraphics.R ├── help.R ├── htmlwidgets.R ├── misc.R ├── raster.R └── rmote.R ├── README.md ├── drat.sh ├── inst ├── R.css └── index.html ├── man ├── plot_done.Rd ├── print.ggplot.Rd ├── print.help_files_with_topic.Rd ├── print.htmlwidget.Rd ├── print.trellis.Rd ├── rmote_device.Rd ├── rmote_off.Rd ├── rmote_on.Rd ├── start_rmote.Rd └── stop_rmote.Rd └── tests ├── testthat.R └── testthat └── test-package.R /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | man-roxygen 4 | \.git 5 | ^\.travis\.yml$ 6 | \.DS_Store 7 | ..Rcheck 8 | CONTRIBUTING\.md 9 | ^_ignore$ 10 | \.sublime\- 11 | ^drat.sh$ 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: r 2 | sudo: false 3 | cache: packages 4 | r_packages: 5 | - knitr 6 | - covr 7 | - drat 8 | after_success: 9 | - Rscript -e 'library("covr");codecov()' 10 | - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && bash 11 | drat.sh 12 | env: 13 | global: 14 | secure: bgnCZwyLqoO1vfbqtrEh1sac0R9thDMv84McCjuZNuXd+VBLd3dUQuji0qpaCNcTC8HL6f+DcBtUW1LMtdGB6UFFkzUpKWQRqWfXip0ltR7y9uTe5wmsGgPadRwwuTj+63kPfv/ci2Vt6X2dz1OLV5xZecScLZ+X3l5OwpjIvCFGRc5nAOWcvr6rJBwzIwUJKc/frk6ow4alDR6+Ua6ihvELbCoegbdwpc8BkjuQavsoVF+Iu2i5NgsrHqnWzABVDVRCdnqvjuTvrLIfuGDY+NF8UWFB63GS/Bpq2nAjHC5F0ke1V4fZRr+Vl7c65qz2D04N3eYG/XDJSFR4kLmu0eAJYmPMqvaKBlTtywabL8ioOydgNp5JylAo+N/xBhevtKOWv0cC9//fjC+lHaEJRwq9aklsZzh8g2szTQf6/e8LblzyRW3W6rIlpqlcKCmDO8ulu3rEgIL3uVzlYp5ifjPkfFX9+Gg4QcgjtYv9sXcRXngazXBpqepDYCC8C7R7wmN7M1L6+n7dmPBuLwqUXh3bKOQFKUmr6jmNUb6a7L4drYEwexkDB2g+watMiBSLI1S5k5GLq/qnw6lNIWA/DlFJX54nJs7Rq+3SrLKnmu2nQjmg5B3muBGiFUClTVAa0X8DS5e2h8PjCgBBUNOmPNdWrXe4fZ2Y7tZTQL1VKk0= 15 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: rmote 2 | Title: Serve Graphics over HTTP a from Remote Server 3 | Version: 0.3.4 4 | Authors@R: person("Ryan", "Hafen", email = "rhafen@gmail.com", role = c("aut", "cre")) 5 | Description: Serve graphics (lattice/ggplot, htmlwidgets, help) over http from 6 | an interactive R session running through ssh on a remote server. 7 | License: MIT + file LICENSE 8 | URL: https://github.com/cloudyr/rmote 9 | BugReports: https://github.com/cloudyr/rmote/issues 10 | LazyData: true 11 | Depends: 12 | R (>= 3.1) 13 | Imports: 14 | tools, 15 | htmltools, 16 | digest, 17 | servr (>= 0.3.1), 18 | lattice, 19 | ggplot2, 20 | htmlwidgets, 21 | webshot, 22 | png 23 | Suggests: 24 | Cairo, 25 | testthat, 26 | roxygen2 27 | RoxygenNote: 5.0.1 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ryan Hafen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,ggplot) 4 | S3method(print,help_files_with_topic) 5 | S3method(print,htmlwidget) 6 | S3method(print,trellis) 7 | export(plot_done) 8 | export(rmote_device) 9 | export(rmote_off) 10 | export(rmote_on) 11 | export(start_rmote) 12 | export(stop_rmote) 13 | import(ggplot2) 14 | import(htmltools) 15 | import(htmlwidgets) 16 | import(lattice) 17 | importFrom(digest,digest) 18 | importFrom(graphics,rasterImage) 19 | importFrom(png,readPNG) 20 | importFrom(servr,httd) 21 | importFrom(tools,Rd2HTML) 22 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | Version 0.3 2 | ---------------------------------------------------------------------- 3 | 4 | - Add history sidebar to viewer and option to use it or not (0.3.4) 5 | - Add history thumbnail functions for all output modes (0.3.4) 6 | - Clean up html and css (0.3.4) 7 | - Add `hostname` option that will show hostname in page title (0.3.3) 8 | - Add preliminary tests (0.3.3) 9 | - Ensure raster graphics don't render if someone/thing has explicitly opened their own device (0.3.3) 10 | - Fix iframe refresh issue (thanks @yihui) (0.3.2) 11 | - Add base R graphics support (0.3.1) 12 | - Add `rmote_on()` and `rmote_off()` 13 | - Add main index page with history 14 | 15 | Version 0.2 16 | ---------------------------------------------------------------------- 17 | 18 | - Add `capabilities()` checking for png 19 | - When the rmote server isn't running, restore default behavior of print methods 20 | - Add `rmote_device()` function to allow control over plot dimensions, etc. 21 | - Add `daemon = FALSE` option for potentially running a single dedicated rmote server for multiple R sessions on a remote server 22 | - Add `rmote_mode()` to toggle rmote on / off (useful for `daemon = FALSE` option) 23 | - Change start and stop to `rmote_start()` and `rmote_stop()` 24 | - Make graphics look good on retina displays by default 25 | - Use servr's new `httw()` function for live reloading 26 | - Remove packages from depends and import 27 | - Avoid use of `:::` 28 | -------------------------------------------------------------------------------- /R/basegraphics.R: -------------------------------------------------------------------------------- 1 | 2 | #' Let rmote know that a base R plot is complete and ready to serve 3 | #' @export 4 | plot_done <- function() { 5 | make_base_plot() 6 | invisible(NULL) 7 | } 8 | 9 | make_base_plot <- function() { 10 | rmb <- getOption("rmote_baseplot") 11 | if(!is.null(rmb)) { 12 | if(dev.cur() > 1) 13 | dev.off() 14 | res <- write_html(rmb$html) 15 | options(rmote_baseplot = NULL) 16 | if(is_history_on()) 17 | make_raster_thumb(res, rmb$cur_type, rmb$opts, rmb$ofile) 18 | } 19 | } 20 | 21 | set_base_plot_hook <- function() { 22 | options(prev_plot_hook = getHook("before.plot.new")) 23 | setHook("before.plot.new", function() { 24 | # if a device was opened up automatically, turn it off 25 | # (automatic devices don't have a path) 26 | fp <- attr(.Device, "filepath") 27 | if(is.null(fp) || fp == "Rplots.pdf") 28 | dev.off() 29 | 30 | # in case previous plot has never finished 31 | getFromNamespace("make_base_plot", "rmote")() 32 | 33 | # this will call png or pdf with appropriate options 34 | dummy <- structure(list(Sys.time()), class = "base_graphics") 35 | getFromNamespace("print_graphics", "rmote")(dummy) 36 | }, "replace") 37 | } 38 | 39 | unset_base_plot_hook <- function() { 40 | setHook("before.plot.new", getOption("prev_plot_hook"), "replace") 41 | } 42 | 43 | -------------------------------------------------------------------------------- /R/help.R: -------------------------------------------------------------------------------- 1 | 2 | #' Print help page to servr 3 | #' 4 | #' @param x help object 5 | #' @param \ldots additional parameters 6 | #' @S3method print help_files_with_topic 7 | #' @importFrom tools Rd2HTML 8 | print.help_files_with_topic <- function(x, ...) { 9 | 10 | file <- as.character(x) 11 | 12 | help_opt <- getOption("rmote_help", FALSE) 13 | 14 | if(is_rmote_on() && help_opt && length(file) == 1 && grepl("/help/", file)) { 15 | message("serving help through rmote") 16 | 17 | res <- try({ 18 | topic <- gsub(".*/help/(.*)", "\\1", file) 19 | package <- gsub(".*/(.*)/help/.*", "\\1", file) 20 | rdb_path <- file.path(system.file("help", package = package), package) 21 | tmp <- getFromNamespace("fetchRdDB", "tools")(rdb_path, topic) 22 | capture.output(tools::Rd2HTML(tmp)) 23 | }, silent = TRUE) 24 | if(!inherits(res, "try-error")) { 25 | server_dir <- get_server_dir() 26 | # move R.css over 27 | # file.path(R.home('doc'), 'html', 'R.css') 28 | if(!file.exists(file.path(server_dir, "R.css"))) 29 | file.copy(file.path(system.file(package = "rmote"), "R.css"), server_dir) 30 | 31 | ii <- get_output_index() 32 | writeLines(c("", res), 33 | file.path(server_dir, get_output_file(ii))) 34 | write_index(ii) 35 | 36 | if(is_history_on()) { 37 | message("making thumbnail") 38 | fbase <- file.path(server_dir, "thumbs") 39 | if(!file.exists(fbase)) 40 | dir.create(fbase) 41 | nf <- file.path(fbase, gsub("html$", "png", get_output_file(ii))) 42 | opts <- list(filename = nf, width = 300, height = 150) 43 | if(capabilities("cairo")) 44 | opts$type <- "cairo-png" 45 | do.call(png, opts) 46 | getFromNamespace("print.trellis", "lattice")(text_plot(paste("help:", topic))) 47 | dev.off() 48 | } 49 | 50 | return() 51 | } 52 | } else { 53 | getFromNamespace("print.help_files_with_topic", "utils")(x) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /R/htmlwidgets.R: -------------------------------------------------------------------------------- 1 | 2 | #' Print an htmlwidget to servr 3 | #' 4 | #' @param x htmlwidget object 5 | #' @param \ldots additional parameters 6 | #' @S3method print htmlwidget 7 | print.htmlwidget <- function(x, ...) { 8 | 9 | widget_opt <- getOption("rmote_htmlwidgets", FALSE) 10 | 11 | if(is_rmote_on() && widget_opt) { 12 | message("serving htmlwidgets through rmote") 13 | 14 | res <- try({ 15 | html <- htmltools::as.tags(x, standalone = TRUE) 16 | write_html(html) 17 | }) 18 | 19 | # make thumbnail of htmlwidget 20 | if(is_history_on()) { 21 | message("making thumbnail") 22 | fbase <- file.path(get_server_dir(), "thumbs") 23 | nf <- file.path(fbase, gsub("html$", "png", basename(res))) 24 | if(!inherits(res, "try-error")) { 25 | width <- x$width 26 | height <- x$height 27 | x$sizingPolicy$padding <- 0 28 | if(is.null(width)) width <- 600 29 | if(is.null(height)) height <- 400 30 | 31 | tf <- tempfile(fileext = ".png") 32 | ws_res <- try(webshot::webshot(paste0("file://", res), file = tf, 33 | selector = ".html-widget"), silent = TRUE) 34 | if(!inherits(ws_res, "try-error")) { 35 | suppressMessages(make_thumb(tf, nf, width = width, height = height)) 36 | } else { 37 | opts <- list(filename = nf, width = 300, height = 150) 38 | if(capabilities("cairo")) 39 | opts$type <- "cairo-png" 40 | do.call(png, opts) 41 | getFromNamespace("print.trellis", "lattice")(text_plot("htmlwidget")) 42 | dev.off() 43 | } 44 | } 45 | return() 46 | } 47 | 48 | } else { 49 | getFromNamespace("print.htmlwidget", "htmlwidgets")(x) 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /R/misc.R: -------------------------------------------------------------------------------- 1 | 2 | get_hostname <- function() { 3 | hn <- try(system("hostname", intern = TRUE, ignore.stderr = TRUE), silent = TRUE) 4 | if(inherits(hn, "try-error")) 5 | hn <- "" 6 | hn 7 | } 8 | 9 | get_output_index <- function() { 10 | server_dir <- get_server_dir() 11 | idx_path <- file.path(get_server_dir(), ".idx") 12 | if(!file.exists(idx_path)) 13 | return(1) 14 | as.integer(readLines(idx_path, warn = FALSE)[1]) + 1 15 | } 16 | 17 | get_output_file <- function(ii) { 18 | sprintf("output%04d.html", ii) 19 | } 20 | 21 | set_index_template <- function() { 22 | a <- readLines(system.file("index.html", package = "rmote"), warn = FALSE) 23 | if(getOption("rmote_hostname")) { 24 | idx <- grepl("rmote viewer", a[1:10]) 25 | hn <- get_hostname() 26 | if(length(idx) > 0 && hn != "") 27 | a[idx] <- gsub("rmote viewer", paste("rmote:", hn), a[idx]) 28 | } 29 | options(rmote_index = paste(c(a, ""), collapse = "\n")) 30 | } 31 | 32 | write_html <- function(html) { 33 | ii <- get_output_index() 34 | fp <- file.path(get_server_dir(), get_output_file(ii)) 35 | htmltools::save_html( 36 | tagList(HTML(""), html), 37 | file = fp) 38 | write_index(ii) 39 | fp 40 | } 41 | 42 | write_index <- function(ii) { 43 | if(is.null(getOption("rmote_index"))) 44 | set_index_template() 45 | res <- gsub("___max_page___", ii, getOption("rmote_index")) 46 | res <- gsub("___history___", ifelse(is_history_on(), "true", "false"), res) 47 | # write out file index 48 | cat(ii, file = file.path(get_server_dir(), ".idx")) 49 | cat(res, file = file.path(get_server_dir(), "index.html")) 50 | } 51 | 52 | is_rmote_on <- function() { 53 | getOption("rmote_on", FALSE) || length(servr::daemon_list()) > 0 54 | } 55 | 56 | is_history_on <- function() { 57 | getOption("rmote_history", TRUE) 58 | } 59 | 60 | no_other_devices <- function() { 61 | res <- length(dev.list()) == 0 62 | if(!res) { 63 | message("- not sending to rmote because another graphics device has been opened...") 64 | message("- sending to the open graphics device instead...") 65 | message("- to send to rmote, close all active graphics devices using graphics.off()") 66 | } 67 | res 68 | } 69 | 70 | get_server_dir <- function() { 71 | server_dir <- getOption("rmote_server_dir") 72 | if(is.null(server_dir)) 73 | stop("No setting for rmote_server_dir - make sure to call rmote_server_init()") 74 | 75 | if(!file.exists(server_dir)) 76 | dir.create(server_dir, recursive = TRUE, showWarnings = FALSE) 77 | 78 | server_dir 79 | } 80 | 81 | get_port <- function() { 82 | port <- getOption("rmote_server_port") 83 | if(is.null(port)) 84 | stop("No setting for rmote_server_port - make sure to call rmote_server_init()") 85 | 86 | port 87 | } 88 | 89 | text_plot <- function(text) { 90 | xyplot(NA ~ NA, xlab = "", ylab = "", 91 | scales = list(draw = FALSE), 92 | panel = function(x, y, ...) 93 | panel.text(0.5, 0.5, text, cex = 2.5)) 94 | } 95 | 96 | #' @importFrom png readPNG 97 | #' @importFrom graphics rasterImage 98 | make_thumb <- function(in_file, out_file, width, height) { 99 | fbase <- dirname(out_file) 100 | if(!dir.exists(fbase)) 101 | dir.create(fbase) 102 | 103 | max_height <- 150 104 | ratio <- max_height / height 105 | height <- ratio * height 106 | width <- ratio * width 107 | 108 | img <- png::readPNG(in_file) 109 | opts <- list(filename = out_file, width = width, height = height) 110 | if(capabilities("cairo")) 111 | opts$type <- "cairo-png" 112 | do.call(png, opts) 113 | par(mar = c(0,0,0,0), xaxs = "i", yaxs = "i", ann = FALSE) 114 | plot(1:2, type = "n", xaxt = "n", yaxt = "n", xlab = "", ylab = "") 115 | lim <- par() 116 | graphics::rasterImage(img, lim$usr[1], lim$usr[3], lim$usr[2], lim$usr[4]) 117 | dev.off() 118 | } 119 | 120 | dir.exists <- function(x) { 121 | if(file.exists(x) & file.info(x)$isdir) 122 | return(TRUE) 123 | return(FALSE) 124 | } 125 | -------------------------------------------------------------------------------- /R/raster.R: -------------------------------------------------------------------------------- 1 | #' Print trellis plot to servr 2 | #' 3 | #' @param x trellis object 4 | #' @param \ldots additional parameters 5 | #' @S3method print trellis 6 | #' @import htmltools 7 | #' @importFrom digest digest 8 | print.trellis <- function(x, ...) { 9 | print_graphics(x) 10 | } 11 | 12 | #' Print ggplot2 plot to servr 13 | #' 14 | #' @param x ggplot object 15 | #' @param \ldots additional parameters 16 | #' @S3method print ggplot 17 | print.ggplot <- function(x, ...) { 18 | print_graphics(x) 19 | } 20 | 21 | print_graphics <- function(x) { 22 | 23 | graphics_opt <- getOption("rmote_graphics", FALSE) 24 | 25 | if(is_rmote_on() && graphics_opt && no_other_devices()) { 26 | message("serving graphics through rmote") 27 | 28 | output_dir <- file.path(get_server_dir(), "plots") 29 | if(!file.exists(output_dir)) 30 | dir.create(output_dir, recursive = TRUE) 31 | 32 | opts <- getOption("rmote_device") 33 | if(is.null(opts)) { 34 | rmote_device() 35 | opts <- getOption("rmote_device") 36 | } 37 | 38 | if(is.null(opts$filename)) { 39 | plot_base <- digest::digest(x) 40 | } else { 41 | plot_base <- opts$filename 42 | } 43 | file <- file.path("plots", paste0(plot_base, ".", opts$type)) 44 | ofile <- file.path(output_dir, paste0(plot_base, ".", opts$type)) 45 | 46 | cur_type <- opts$type 47 | if(opts$type == "png") { 48 | ww <- opts$width 49 | hh <- opts$height 50 | if(opts$retina) { 51 | opts$width <- opts$width * 2 52 | opts$height <- opts$height * 2 53 | opts$res <- 150 54 | } 55 | html <- tags$html( 56 | tags$head(tags$title(paste("raster plot:", plot_base))), 57 | tags$body(tags$img(src = file, 58 | width = paste0(ww, "'x"), height = paste0(hh, "'x")))) 59 | opts$type <- NULL 60 | opts$retina <- NULL 61 | opts$filename <- ofile 62 | if(capabilities("cairo")) 63 | opts$type <- "cairo-png" 64 | do.call(png, opts) 65 | } else if(opts$type == "pdf") { 66 | html <- tags$html( 67 | tags$head(tags$title(paste("raster plot:", plot_base))), 68 | tags$body(tags$a(href = file, "pdf", target = "_blank"))) 69 | opts$type <- NULL 70 | opts$file <- ofile 71 | do.call(pdf, opts) 72 | } 73 | 74 | if(inherits(x, c("trellis", "ggplot"))) { 75 | if(inherits(x, "trellis")) 76 | getFromNamespace("print.trellis", "lattice")(x) 77 | if(inherits(x, "ggplot")) 78 | getFromNamespace("print.ggplot", "ggplot2")(x) 79 | dev.off() 80 | 81 | res <- write_html(html) 82 | 83 | # make thumbnail 84 | if(is_history_on()) 85 | make_raster_thumb(res, cur_type, opts, ofile) 86 | 87 | } else if(inherits(x, "base_graphics")) { 88 | message("when finished with plot commands, call plot_done()") 89 | options(rmote_baseplot = list(html = html, ofile = ofile, 90 | cur_type = cur_type, opts = opts)) 91 | } 92 | 93 | return() 94 | } else { 95 | if(inherits(x, "trellis")) 96 | return(getFromNamespace("print.trellis", "lattice")(x)) 97 | if(inherits(x, "ggplot")) 98 | return(getFromNamespace("print.ggplot", "ggplot2")(x)) 99 | } 100 | } 101 | 102 | #' Set device parameters for traditional grahpics plot output 103 | #' 104 | #' @param type either "png" or "pdf" 105 | #' @param filename optional name for file (should have no extension and no directories) 106 | #' @param retina if TRUE and type is "png", the png file will be plotted at twice its size and shown at the original size, to look better on high resolution displays 107 | #' @param \ldots parameters passed on to either \code{\link[grDevices]{png}} or \code{\link[grDevices]{pdf}} (such as width, height, etc.) 108 | #' @export 109 | rmote_device <- function(type = c("png", "pdf"), filename = NULL, retina = TRUE, ...) { 110 | 111 | type <- match.arg(type) 112 | opts <- list(...) 113 | opts$type <- type 114 | opts$filename <- filename 115 | if(type == "png") { 116 | opts$retina <- retina 117 | if(is.null(opts$width)) 118 | opts$width <- 480 119 | if(is.null(opts$height)) 120 | opts$height <- 480 121 | } 122 | 123 | options(rmote_device = opts) 124 | } 125 | 126 | make_raster_thumb <- function(res, cur_type, opts, ofile) { 127 | message("making thumbnail") 128 | fbase <- file.path(get_server_dir(), "thumbs") 129 | if(!file.exists(fbase)) 130 | dir.create(fbase) 131 | nf <- file.path(fbase, gsub("html$", "png", basename(res))) 132 | if(cur_type == "pdf") { 133 | opts <- list(filename = nf, width = 300, height = 150) 134 | if(capabilities("cairo")) 135 | opts$type <- "cairo-png" 136 | do.call(png, opts) 137 | getFromNamespace("print.trellis", "lattice")(text_plot("pdf file")) 138 | dev.off() 139 | } else { 140 | suppressMessages(make_thumb(ofile, nf, width = opts$width, height = opts$height)) 141 | } 142 | } 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /R/rmote.R: -------------------------------------------------------------------------------- 1 | #' @import ggplot2 2 | #' @import lattice 3 | #' @import htmlwidgets 4 | NULL 5 | 6 | #' Initialize a remote servr 7 | #' 8 | #' @param server_dir directory to launch servr from 9 | #' @param port port to run servr on 10 | #' @param daemon logical - should the server be started as a daemon? 11 | #' @param help (logical) send results of `?` to servr 12 | #' @param graphics (logical) send traditional lattice / ggplot2 plots to servr 13 | #' @param basegraphics (logical) send base graphics to servr 14 | #' @param htmlwidgets (logical) send htmlwidgets to servr 15 | #' @param hostname (logical) try to get hostname and use it in viewer page title 16 | #' @param history (logical) should history thumbnails be created and shown in the viewer? 17 | #' 18 | #' @export 19 | #' @importFrom servr httd 20 | start_rmote <- function( 21 | server_dir = file.path(tempdir(), "rmote_server"), 22 | port = 4321, daemon = TRUE, 23 | help = TRUE, graphics = TRUE, 24 | basegraphics = TRUE, htmlwidgets = TRUE, 25 | hostname = TRUE, history = TRUE) { 26 | 27 | if(!file.exists(server_dir)) 28 | dir.create(server_dir, recursive = TRUE, showWarnings = FALSE) 29 | 30 | options(rmote_on = TRUE) 31 | 32 | options(rmote_server_dir = server_dir) 33 | options(rmote_server_port = port) 34 | options(rmote_help = help) 35 | options(rmote_graphics = graphics) 36 | options(rmote_htmlwidgets = htmlwidgets) 37 | options(rmote_basegraphics = basegraphics) 38 | options(rmote_hostname = hostname) 39 | options(rmote_history = history) 40 | 41 | set_index_template() 42 | 43 | if(basegraphics) 44 | set_base_plot_hook() 45 | 46 | try(servr::httw(server_dir, pattern = "index.html", port = port, 47 | daemon = daemon, browser = FALSE), silent = TRUE) 48 | } 49 | 50 | #' Stop an rmote server 51 | #' @export 52 | stop_rmote <- function() { 53 | plot_done() 54 | if(getOption("rmote_basegraphics", FALSE)) 55 | unset_base_plot_hook() 56 | options(rmote_on = FALSE) 57 | servr::daemon_stop() 58 | } 59 | 60 | #' Set the rmote mode to be on 61 | #' 62 | #' @param server_dir directory where rmote server is running 63 | #' @param help (logical) send results of `?` to servr 64 | #' @param graphics (logical) send traditional lattice / ggplot2 plots to servr 65 | #' @param basegraphics (logical) send base graphics to servr 66 | #' @param htmlwidgets (logical) send htmlwidgets to servr 67 | #' @param hostname (logical) try to get hostname and use it in viewer page title 68 | #' @param history (logical) should history thumbnails be created and shown in the viewer? 69 | #' @note This is useful when running multiple R sessions on a server, where all will serve the same rmote process. It is not necessary to call this in the same session on which \code{\link{start_rmote}} has been called, but on any other R sessions. 70 | #' @export 71 | rmote_on <- function(server_dir, 72 | help = TRUE, graphics = TRUE, 73 | basegraphics = TRUE, htmlwidgets = TRUE, 74 | hostname = TRUE, history = TRUE 75 | ) { 76 | 77 | if(!file.exists(server_dir)) 78 | stop("The location of server_dir does not exist - no rmote server running here...") 79 | 80 | options(rmote_on = TRUE) 81 | 82 | options(rmote_server_dir = server_dir) 83 | options(rmote_help = help) 84 | options(rmote_graphics = graphics) 85 | options(rmote_htmlwidgets = htmlwidgets) 86 | options(rmote_basegraphics = basegraphics) 87 | options(rmote_hostname = hostname) 88 | options(rmote_history = history) 89 | 90 | if(basegraphics) 91 | set_base_plot_hook() 92 | 93 | invisible(NULL) 94 | } 95 | 96 | #' Set the rmote mode to be off 97 | #' 98 | #' @export 99 | rmote_off <- function() { 100 | plot_done() 101 | if(getOption("rmote_basegraphics", FALSE)) 102 | unset_base_plot_hook() 103 | options(rmote_on = FALSE) 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## rmote 2 | 3 | ![gif](https://cloud.githubusercontent.com/assets/1275592/9618810/99cf5b2c-50be-11e5-885d-a4de16919271.gif) 4 | 5 | ### Running R on a remote server 6 | 7 | R users often find themselves needing to log in to a remote machine to do analysis. Sometimes this is due to data restrictions, computing power on the remote machine, etc. Users can ssh in and run R in a terminal, but it is not possible to look at graphics, etc. 8 | 9 | There are currently three approaches that I am aware of to deal with this: 10 | 11 | 1. Install [RStudio Server](https://www.rstudio.com/products/rstudio-server-pro/) on the remote server and use that from a web browser on your local machine. Graphics output is shown in the IDE. 12 | 2. Use X11 forwarding (`ssh -X|Y`). Graphics output is sent back to your machine. 13 | 3. Use VNC with a linux desktop environment like KDE or GNOME. 14 | 15 | Whenever possible, #1 is by far the best way to go and is one of the beautiful things about RStudio Server. #2 is not a good choice - plots are very slow to render, the quality is terrible, and it doesn't support recent advances in plot outputs like [htmlwidgets](http://htmlwidgets.org) (unless you launch firefox through X11, which will mean you might get to look at one plot per hour). #3 is okay if it is available and you are comfortable working in one of these desktop environments. 16 | 17 | There could be other obvious ways to deal with this that I am oblivious to. 18 | 19 | ### A problem 20 | 21 | Often we do not have the choice of installing RStudio Server or a desktop environment on the remote machine. Also, some users prefer to work in a terminal sending commands from a favorite text editor on a local machine, but still want to see graphics. We would like to have something better than X11 forwarding to view graphics and other output when running R in a terminal on a remote machine. 22 | 23 | ### A solution 24 | 25 | The rmote package is an attempt to make working in R over ssh on a server a bit more pleasant in terms of viewing output. It uses [servr](https://github.com/yihui/servr) on the remote machine to serve R graphics as they are created. These can be viewed on the local machine in a web browser. The user's local browser will automatically refresh each time a new output is available. 26 | 27 | Currently there is support for lattice, ggplot2, htmlwidgets, and help output. 28 | 29 | ### Usage 30 | 31 | 1. Choose a port to run your remote server on (default is 4321) 32 | 2. ssh into the remote machine, mapping the port on the remote back to your local machine: 33 | 34 | ``` 35 | ssh -L 4321:localhost:4321 -L 8100:localhost:8100 user@remote 36 | ``` 37 | 38 | I also add port 8100 so I can forward shiny apps back to my local machine on a dedicated port. 39 | 40 | 3. On the remote machine launch R and install the latest version of servr and rmote (one time only) 41 | 42 | ```r 43 | install.packages("rmote", repos = c(CRAN = "http://cran.rstudio.com", 44 | tessera = "http://packages.tessera.io")) 45 | ``` 46 | 47 | or alternatively 48 | 49 | ```r 50 | devtools::install_github(c("yihui/servr", "hafen/rmote")) 51 | ``` 52 | 53 | Note that this package will probably never be on CRAN since it overwrites some standard R S3 methods. 54 | 55 | 4. Run the following in R on the remote: 56 | 57 | ```r 58 | rmote::start_rmote() 59 | ``` 60 | 61 | To view some of the options for this, see `?start_rmote`. One option is the port, which needs to match the one your forwarded in step 2 (4321 is the default.) 62 | 63 | 5. On your local machine, open up your web browser to `localhost:4321` 64 | 65 | Now as you create compatible plots on your remote machine, your browser on your local fmachine will automatically update to show the results. For example, try running each of the following in succession on the server: 66 | 67 | ```r 68 | ?plot 69 | 70 | qplot(mpg, wt, data=mtcars, colour=cyl) 71 | 72 | dotplot(variety ~ yield | year * site, data=barley) 73 | 74 | library(rbokeh) 75 | figure() %>% ly_hexbin(rnorm(10000), rnorm(10000)) 76 | ``` 77 | 78 | This process is slightly more tedious than `ssh -X` for initial setup, but much faster and more functional. 79 | 80 | ### Other useful utilities 81 | 82 | Another interesting package to check out that allows you to control R on a remote machine from your local R session is - [remoter](https://cran.r-project.org/web/packages/remoter/vignettes/remoter.html). 83 | 84 | If you must work on a remote terminal, here are some additional utilities that help make things nice: 85 | 86 | - [colorout](https://github.com/jalvesaq/colorout) 87 | - [Sublime](https://www.sublimetext.com) + [SFTP](http://wbond.net/sublime_packages/sftp) 88 | - [Vim](http://www.vim.org) + [Vim-R-plugin](https://github.com/vim-scripts/Vim-R-plugin) 89 | 90 | 91 | ## Installation ## 92 | 93 | [![CRAN](http://www.r-pkg.org/badges/version/rmote)](http://cran.r-project.org/package=rmote) 94 | [![Build Status](https://travis-ci.org/cloudyr/rmote.png?branch=master)](https://travis-ci.org/cloudyr/rmote) 95 | [![codecov.io](http://codecov.io/github/cloudyr/rmote/coverage.svg?branch=master)](http://codecov.io/github/cloudyr/rmote?branch=master) 96 | 97 | This package is not yet on CRAN. To install the latest development version you can install from the cloudyr drat repository: 98 | 99 | ```R 100 | # latest stable version 101 | install.packages("rmote", repos = c(getOption("repos"), "http://cloudyr.github.io/drat")) 102 | ``` 103 | 104 | Or, to pull a potentially unstable version directly from GitHub: 105 | 106 | ```R 107 | if(!require("ghit")){ 108 | install.packages("ghit") 109 | } 110 | ghit::install_github("cloudyr/rmote") 111 | ``` 112 | 113 | 114 | --- 115 | [![cloudyr project logo](http://i.imgur.com/JHS98Y7.png)](https://github.com/cloudyr) 116 | -------------------------------------------------------------------------------- /drat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset 3 | addToDrat(){ 4 | mkdir drat; cd drat 5 | 6 | ## Set up Repo parameters 7 | git init 8 | git config user.name "leeper" 9 | git config user.email "thosjleeper@gmail.com" 10 | git config --global push.default simple 11 | 12 | ## Get drat repo 13 | git remote add upstream "https://$GH_TOKEN@github.com/cloudyr/cloudyr.github.io.git" 14 | git fetch upstream 15 | git checkout master 16 | 17 | Rscript -e "drat::insertPackage('../$PKG_TARBALL', repodir = './drat')" 18 | git add --all 19 | git commit -m "add $PKG_TARBALL (build $TRAVIS_BUILD_ID)" 20 | git push 21 | 22 | } 23 | addToDrat 24 | -------------------------------------------------------------------------------- /inst/R.css: -------------------------------------------------------------------------------- 1 | /* 2 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:300); 3 | html, body { font-family: 'Open Sans', sans-serif;} 4 | */ 5 | -------------------------------------------------------------------------------- /inst/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | rmote: spacemanspiff.local 5 | 6 | 37 | 137 | 138 | 139 |
140 | 149 |
150 |
151 | 156 |
157 | 160 | 161 |
162 |
163 | 164 | 165 | -------------------------------------------------------------------------------- /man/plot_done.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/basegraphics.R 3 | \name{plot_done} 4 | \alias{plot_done} 5 | \title{Let rmote know that a base R plot is complete and ready to serve} 6 | \usage{ 7 | plot_done() 8 | } 9 | \description{ 10 | Let rmote know that a base R plot is complete and ready to serve 11 | } 12 | 13 | -------------------------------------------------------------------------------- /man/print.ggplot.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/raster.R 3 | \name{print.ggplot} 4 | \alias{print.ggplot} 5 | \title{Print ggplot2 plot to servr} 6 | \usage{ 7 | \method{print}{ggplot}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{ggplot object} 11 | 12 | \item{\ldots}{additional parameters} 13 | } 14 | \description{ 15 | Print ggplot2 plot to servr 16 | } 17 | 18 | -------------------------------------------------------------------------------- /man/print.help_files_with_topic.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/help.R 3 | \name{print.help_files_with_topic} 4 | \alias{print.help_files_with_topic} 5 | \title{Print help page to servr} 6 | \usage{ 7 | \method{print}{help_files_with_topic}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{help object} 11 | 12 | \item{\ldots}{additional parameters} 13 | } 14 | \description{ 15 | Print help page to servr 16 | } 17 | 18 | -------------------------------------------------------------------------------- /man/print.htmlwidget.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/htmlwidgets.R 3 | \name{print.htmlwidget} 4 | \alias{print.htmlwidget} 5 | \title{Print an htmlwidget to servr} 6 | \usage{ 7 | \method{print}{htmlwidget}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{htmlwidget object} 11 | 12 | \item{\ldots}{additional parameters} 13 | } 14 | \description{ 15 | Print an htmlwidget to servr 16 | } 17 | 18 | -------------------------------------------------------------------------------- /man/print.trellis.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/raster.R 3 | \name{print.trellis} 4 | \alias{print.trellis} 5 | \title{Print trellis plot to servr} 6 | \usage{ 7 | \method{print}{trellis}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{trellis object} 11 | 12 | \item{\ldots}{additional parameters} 13 | } 14 | \description{ 15 | Print trellis plot to servr 16 | } 17 | 18 | -------------------------------------------------------------------------------- /man/rmote_device.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/raster.R 3 | \name{rmote_device} 4 | \alias{rmote_device} 5 | \title{Set device parameters for traditional grahpics plot output} 6 | \usage{ 7 | rmote_device(type = c("png", "pdf"), filename = NULL, retina = TRUE, ...) 8 | } 9 | \arguments{ 10 | \item{type}{either "png" or "pdf"} 11 | 12 | \item{filename}{optional name for file (should have no extension and no directories)} 13 | 14 | \item{retina}{if TRUE and type is "png", the png file will be plotted at twice its size and shown at the original size, to look better on high resolution displays} 15 | 16 | \item{\ldots}{parameters passed on to either \code{\link[grDevices]{png}} or \code{\link[grDevices]{pdf}} (such as width, height, etc.)} 17 | } 18 | \description{ 19 | Set device parameters for traditional grahpics plot output 20 | } 21 | 22 | -------------------------------------------------------------------------------- /man/rmote_off.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rmote.R 3 | \name{rmote_off} 4 | \alias{rmote_off} 5 | \title{Set the rmote mode to be off} 6 | \usage{ 7 | rmote_off() 8 | } 9 | \description{ 10 | Set the rmote mode to be off 11 | } 12 | 13 | -------------------------------------------------------------------------------- /man/rmote_on.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rmote.R 3 | \name{rmote_on} 4 | \alias{rmote_on} 5 | \title{Set the rmote mode to be on} 6 | \usage{ 7 | rmote_on(server_dir, help = TRUE, graphics = TRUE, basegraphics = TRUE, 8 | htmlwidgets = TRUE, hostname = TRUE, history = TRUE) 9 | } 10 | \arguments{ 11 | \item{server_dir}{directory where rmote server is running} 12 | 13 | \item{help}{(logical) send results of `?` to servr} 14 | 15 | \item{graphics}{(logical) send traditional lattice / ggplot2 plots to servr} 16 | 17 | \item{basegraphics}{(logical) send base graphics to servr} 18 | 19 | \item{htmlwidgets}{(logical) send htmlwidgets to servr} 20 | 21 | \item{hostname}{(logical) try to get hostname and use it in viewer page title} 22 | 23 | \item{history}{(logical) should history thumbnails be created and shown in the viewer?} 24 | } 25 | \description{ 26 | Set the rmote mode to be on 27 | } 28 | \note{ 29 | This is useful when running multiple R sessions on a server, where all will serve the same rmote process. It is not necessary to call this in the same session on which \code{\link{start_rmote}} has been called, but on any other R sessions. 30 | } 31 | 32 | -------------------------------------------------------------------------------- /man/start_rmote.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rmote.R 3 | \name{start_rmote} 4 | \alias{start_rmote} 5 | \title{Initialize a remote servr} 6 | \usage{ 7 | start_rmote(server_dir = file.path(tempdir(), "rmote_server"), port = 4321, 8 | daemon = TRUE, help = TRUE, graphics = TRUE, basegraphics = TRUE, 9 | htmlwidgets = TRUE, hostname = TRUE, history = TRUE) 10 | } 11 | \arguments{ 12 | \item{server_dir}{directory to launch servr from} 13 | 14 | \item{port}{port to run servr on} 15 | 16 | \item{daemon}{logical - should the server be started as a daemon?} 17 | 18 | \item{help}{(logical) send results of `?` to servr} 19 | 20 | \item{graphics}{(logical) send traditional lattice / ggplot2 plots to servr} 21 | 22 | \item{basegraphics}{(logical) send base graphics to servr} 23 | 24 | \item{htmlwidgets}{(logical) send htmlwidgets to servr} 25 | 26 | \item{hostname}{(logical) try to get hostname and use it in viewer page title} 27 | 28 | \item{history}{(logical) should history thumbnails be created and shown in the viewer?} 29 | } 30 | \description{ 31 | Initialize a remote servr 32 | } 33 | 34 | -------------------------------------------------------------------------------- /man/stop_rmote.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rmote.R 3 | \name{stop_rmote} 4 | \alias{stop_rmote} 5 | \title{Stop an rmote server} 6 | \usage{ 7 | stop_rmote() 8 | } 9 | \description{ 10 | Stop an rmote server 11 | } 12 | 13 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | 3 | test_check("rmote") 4 | -------------------------------------------------------------------------------- /tests/testthat/test-package.R: -------------------------------------------------------------------------------- 1 | rmdr <- file.path(tempdir(), "rmote_server") 2 | f1 <- tempfile(fileext = ".png") 3 | f2 <- tempfile(fileext = ".png") 4 | f3 <- tempfile(fileext = ".png") 5 | 6 | rmote::start_rmote(rmdr) 7 | 8 | ?lm 9 | r1 <- file.exists(file.path(rmdr, "output0001.html")) 10 | 11 | ?glm 12 | r2 <- file.exists(file.path(rmdr, "output0002.html")) 13 | 14 | library(lattice) 15 | xyplot(1:10 ~ 1:10) 16 | r3 <- file.exists(file.path(rmdr, "output0003.html")) 17 | 18 | png(file = f1) 19 | xyplot(1:10 ~ 1:10) 20 | dev.off() 21 | r4 <- file.exists(f1) 22 | 23 | library(ggplot2) 24 | qplot(mpg, wt, data = mtcars) 25 | r5 <- file.exists(file.path(rmdr, "output0004.html")) 26 | 27 | png(file = f2) 28 | qplot(mpg, wt, data = mtcars) 29 | dev.off() 30 | r6 <- file.exists(f1) 31 | 32 | plot(1:10) 33 | rmote::plot_done() 34 | r7 <- file.exists(file.path(rmdr, "output0005.html")) 35 | 36 | png(file = f3) 37 | plot(1:10) 38 | dev.off() 39 | r8 <- file.exists(f3) 40 | 41 | # TODO: testthat doesn't like this 42 | # runs fine in .GlobalEnv in an interactive session 43 | # look into it... 44 | test_that("rmote working", { 45 | if(FALSE) { 46 | expect_true(r1) 47 | expect_true(r2) 48 | expect_true(r3) 49 | expect_true(r4) 50 | expect_true(r5) 51 | expect_true(r6) 52 | expect_true(r7) 53 | expect_true(r8) 54 | } 55 | }) 56 | 57 | rmote::stop_rmote() 58 | --------------------------------------------------------------------------------