--- title: "SciViews socket server" author: "Philippe Grosjean (phgrosjean@sciviews.org)" date: "`r Sys.Date()`" output: rmarkdown::html_vignette: fig_caption: yes vignette: > %\VignetteIndexEntry{SciViews socket server} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` > The SciViews {svSocket} server implements an R server that is mainly designed to interact with the R prompt from a separate process. The {svSocket} clients and the R console share the same global environment (`.GlobalEnv`). So, everybody interacts with the same variables. Use cases are to build a separate R console, to monitor or query an R session, or even, to build a complete GUI or IDE on top of R. Examples of such GUIs are [Tinn-R](https://tinn-r.org/en/) and Komodo IDE with the [SciViews-K extension](https://github.com/SciViews/sciviewsk). ## Quick install and use The SciViews {svSocket} package provides a stateful, multi-client and preemptive socket server. Socket transactions are operational even when R is busy in its main event loop (calculation done at the prompt). This R socket server uses the excellent asynchronous socket ports management by Tcl, and thus, it needs a working version of Tcl/Tk (\>= 8.4), the {tcltk} R package, and R with `isTRUE(capabilities("tcltk"))`. Install the {svSocket} package as usual: ```{r install, eval=FALSE} install.packages("svSocket") ``` (For the development version, install first `devtools` or `remotes`, and then use something like `remotes::github_install("SciViews/svSocket")`) ```{r launch-server, eval=FALSE} # Start a separate R process with a script that launch a socket server on 8889 # and wait for the variable `done` in `.GlobalEnv` to finish rscript <- Sys.which("Rscript") system2(rscript, "--vanilla -e 'svSocket::start_socket_server(8889); while (!exists(\"done\")) Sys.sleep(1)'", wait = FALSE) ``` Sometimes (on MacOS Catalina or more, for instance), you are prompted to allow for R to use a socket. Of course, you have to allow to get an operational server. What this code does is: - Get the path to `Rscript` using `Sys.which` -Start it with a command that launches the socket server on port 8889[^1] (`svSocket::start_socket_server(8889)`) - Wait that the variable `done` appears in `.GlobalEnv` as a simple mean to keep the server alive. - Since `system2()` is invoked with `wait = FALSE`, the command exits as soon as the server is created and you can interact at the R prompt. [^1]: Of course, this port must be free. If not, just use another value. As you can see, creating a socket server with {svSocket} is easy. Now, let's connect to it. It is also very simple: ```{r no_server_version, include=FALSE} server_ready <- FALSE ``` ```{r wait, include=FALSE, eval=FALSE} # Launch the server rscript <- Sys.which("Rscript") try(system2(rscript, "--vanilla -e 'svSocket::start_socket_server(8889); while (!exists(\"done\")) Sys.sleep(1)'", wait = FALSE), silent = TRUE) # Leave enough time for the server to get ready Sys.sleep(5) # and make sure we can connect to it con <- try(socketConnection(host = "localhost", port = 8889, blocking = FALSE, timeout = 30), silent = TRUE) server_ready <- !inherits(con, "try-error") ``` ```{r connect, eval=FALSE} con <- socketConnection(host = "localhost", port = 8889, blocking = FALSE, timeout = 30) ``` Here, we use the `socketConnection()` that comes with base R in non-blocking mode and a time out at 30 sec. So, {svSocket} is even not required to connect an R client to our server. The connection is not blocking. We have the control of the prompt immediately. Now, we can execute code in our R server process with `svSocket::eval_socket_server()`: ```{r eval1, eval=server_ready} library(svSocket) eval_socket_server(con, '1 + 1') ``` The instruction is indeed executed in the server, and the result is returned to the client transparently. You now master two separate R process: the original one where you interact at the R prompt (`>`), and the server process that you can interact with through `eval_socket_server()`. To understand this, we will create the variable `x` on both processes, but with different values: ```{r evalx, eval=server_ready} # Local x x <- "local" # x on the server eval_socket_server(con, 'x <- "server"') ``` Now, let's look what is available on the server side: ```{r evalx2, eval=server_ready} eval_socket_server(con, 'ls()') eval_socket_server(con, 'x') ``` Obviously, there is only one variable, `x`, with value `"server"`. All right ... and in our original process? ```{r localx, eval=server_ready} ls() x ``` We have `x`, but also `con` and `rsscript`. The value of `x` locally is `"local"`. As you can see, commands and results are rather similar. And since the processes are different, so are `x` in both processes. If you want to transfer data to the server, you can still use `eval_socket_server()`, with the name you want for the variable on the server instead of a string with R code, and as third argument, the name of the local variable whose the content will be copied to the server. `eval_socket_server()` manages to send the data to the server (by serializing the data on your side and deserializing it on the server). Here, we will transfer the `iris` data frame to the server under the `iris2` name: ```{r iris2, eval=server_ready} data(iris) eval_socket_server(con, iris2, iris) eval_socket_server(con, "ls()") # iris2 is there eval_socket_server(con, "head(iris2)") # ... and its content is OK ``` For more examples on using `eval_socket_server`, see its man page with `?eval_socket_server`. ## Lower-level interaction The {svSocket} server also allows, of course, for a lower-level interaction with the server, with a lot more options. Here, you send something to the server using `cat, file = con)` (make sure to end your command by a carriage return `\n`, or the server will wait indefinitely for the next part of the instruction!), and you read results using `readLines(con)`. Of course, you must wait that R processes the command before reading results back: ```{r low-level, eval=server_ready} # Send a command to the R server (low-level version) cat('{Sys.sleep(2); "Done!"}\n', file = con) # Wait for, and get response from the server res <- NULL while (!length(res)) { Sys.sleep(0.01) res <- readLines(con) } res ``` Here you got the results send back as strings. If you want to display them on the client's console as if it was output there (like `eval_socket_server()` does), you should use `cat( sep = "\n")`: ```{r cat, eval=server_ready} cat(res, "\n") ``` For convenience, we will wrap all this a function `run_socket_server()`: ```{r runServer, eval=server_ready} run_socket_server <- function(con, code) { cat(code, "\n", file = con) res <- NULL while (!length(res)) { Sys.sleep(0.01) res <- readLines(con) } # Use this instruction to output results as if code was run at the prompt #cat(res, "\n") invisible(res) } ``` The transaction is now much easier: ```{r runServer2, eval=server_ready} (run_socket_server(con, '{Sys.sleep(2); "Done!"}')) ``` ## Configuration and special instructions Now at the low-level (not within `eval_socket_server()` but within our `run_socket_server()` function), one can insert special code `<<>>` with `X` being a marker that the server will use on his side. The last instruction we send instructed the server to wait for 2 sec and then, to return `"Done!"`. We have send the instruction to the server and *wait* for it to finish processing and then, we got the results back. Thus, you original R process is locked down the time the server processes the code. Note that we had to lock it down on our side using the `while(!length(res))` construct. It means that communication between the server and the client is asynchronous, but process of the command must be made synchronous. If you want to return immediately in the calling R process *before* the instruction is processed in the server, you could just consider to drop the `while(...)` section. **This is not a good idea!** Indeed, R will send results back and you will read them on the next `readLines(con)` you issue, and mix it with, perhaps, the result of the next instruction. So, here, we really must tell the R server to send nothing back to us. This is done by inserting the special instruction `<<>>` on one line (surrounded by `\n`). This way, we still have to wait for the instruction to be processed on the server, but nothing is returned back to the client. Also, sending `<<>>` will result into an immediate finalization of the transaction by the server *before* the instruction is processed. ```{r async, eval=server_ready} (run_socket_server(con, '\n<<>>{Sys.sleep(2); "Done!"}')) ``` Here, we got the result immediately, but it is *not* the results of the code execution. Our R server simply indicates that he got our code, he parsed it and is about to process it on his side by returning an empty string `""`. There are several special instructions you can use. See `?par_socket_server` for further details. The server can be configured to behave in a given way, and that configuration is persistent from one connection to the other for the same client. The function `par_socket_server()` allows to change or query the configuration. Its first argument is the name of the client on the server-side... but from the client, you don't necessarily know which name the server gave to you (one can connect several different clients at the same time, and default name is `sock` followed by Tcl name of the client socket connection). Using `<<>>` as a placeholder for this name circumvents the problem. `par_socket_server()` returns an environment that contains configuration variables. Here is the list of configuration item for us: ```{r pars1, eval=server_ready} cat(run_socket_server(con, 'ls(envir = svSocket::par_socket_server(<<>>))'), sep = "\n") ``` The `bare` item indicates if the server sends bare results, or also returns a prompt, acting more like a terminal. By default, it returns the bare results. Here is the current value: ```{r pars2, eval=server_ready} cat(run_socket_server(con, 'svSocket::par_socket_server(<<>>)$bare'), sep = "\n") ``` And here is how you can change it: ```{r bare-false, eval=server_ready} (run_socket_server(con, '\n<<>>svSocket::par_socket_server(<<>>, bare = FALSE)')) (run_socket_server(con, '1 + 1')) ``` When `bare = FALSE` the server issues the formatted command with a prompt (`:>` by default) and the result. For more information about {svSocket} server configuration, see `?par_socket_server`. There are also functions to manipulate the pool of clients and their configurations from the server-side, and the server can also send unattended data to client, see `?send_socket_clients`. finally, the workhorse function on the server-side is `process_socket_server()`. See `?process_socket_server` to learn more about it. You can provide your own process function to the server if you need to. ## Disconnection Don't forget to close the connection, once you have done using `close(con)`, and you can also close the server from the server-side by using `stop_socket_server()`. But never use it from the client-side because you will obviously break in the middle of the transaction. If you want to close the server from the client, you have to install a mechanisms that will nicely shut down the server *after* the transaction is processed. Here, we have installed such a mechanism by detecting the presence of a variable named `done` on the server-side. So: ```{r close, eval=server_ready} # Usually, the client does not stop the server, but it is possible here eval_socket_server(con, 'done <- NULL') # The server will stop after this transaction close(con) ``` You can also access the {svSocket} server from another language. There is a very basic example written in Tcl in the `/etc` subdirectory of the {svSocket} package. See the `ReadMe.txt` file there. The Tcl script `SimpleClient.tcl` implements a Tcl client in a few dozens of code lines. For other examples, you can inspect the code of [SciViews-K](https://github.com/SciViews/sciviewsk), and the code of [Tinn-R](https://tinn-r.org/en/) for a Pascal version. Writing clients in C, Java, Python, etc. should not be difficult if you inspire you from these examples. Finally, there is another implementation of a similar R server through HTTP in the {svHttp} package.