├── config.yml ├── package.json ├── server.js ├── shinylauncher.js └── README.md /config.yml: -------------------------------------------------------------------------------- 1 | app: 2 | available: [1234, 1235] 3 | port_server: 8080 4 | cmd: "prenomsapp::run_app()" 5 | title: "My Shiny App" 6 | no_port: "No port available" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haddock", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "socket.io": "^2.4.0", 6 | "yaml": "^1.4.0" 7 | }, 8 | "author": "Colin Fay ", 9 | "description": "A Node server for Shiny Apps." 10 | } 11 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const shiny = require('./shinylauncher'); 3 | const YAML = require('yaml') 4 | const fs = require('fs') 5 | const file = YAML.parse(fs.readFileSync('./config.yml', 'utf8')) 6 | 7 | // list available ports 8 | var available = file["app"]["available"] 9 | var port_server = file["app"]["port_server"] 10 | 11 | shiny.launchShinyApps(file["app"]["cmd"], available) 12 | 13 | var server = http.createServer(function(req, res) { 14 | if (req.url != '/favicon.ico') { 15 | if (available.length == 0){ 16 | shiny.noPort( 17 | res, 18 | title = file["app"]["title"], 19 | message = file["app"]["no_port"] 20 | ) 21 | } else { 22 | var port = available.shift(); 23 | shiny.logOpen(port, available) 24 | shiny.genPage( 25 | res, 26 | port, 27 | title = file["app"]["title"], 28 | port_server 29 | ); 30 | } 31 | } 32 | }); 33 | 34 | 35 | var io = require('socket.io').listen(server); 36 | 37 | io.sockets.on('connection', function (socket, pseudo) { 38 | 39 | socket.on('message', function (message) { 40 | available.push(message) 41 | shiny.logFree(message, available) 42 | }); 43 | 44 | }); 45 | 46 | 47 | server.listen(port_server); 48 | -------------------------------------------------------------------------------- /shinylauncher.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | launchShiny = function(cmd, port) { 4 | var code = "R -e 'options(shiny.port = " + port + ");" + cmd + "'" 5 | var process = exec(code) 6 | return process 7 | }; 8 | 9 | exports.launchShinyApps = function(cmd, available) { 10 | for (var i = 0; i < available.length; i++) { 11 | launchShiny(cmd, available[i]); 12 | } 13 | 14 | }; 15 | 16 | exports.genPage = function(res, port_apps, title, port_server) { 17 | var txt = ''+ 18 | ''+ 19 | ' '+ 20 | ' '+ 21 | ' ' + title + ''+ 22 | ' '+ 23 | ' '+ 24 | ' '+ 25 | '' + 26 | '' + 34 | ' '+ 35 | '' 36 | res.writeHead(200, {"Content-Type": "text/html"}); 37 | res.write(txt); 38 | res.end(); 39 | return res 40 | }; 41 | 42 | 43 | 44 | exports.noPort = function(res, title, message) { 45 | res.writeHead(200, {"Content-Type": "text/html"}); 46 | var txt = ''+ 47 | ''+ 48 | ' '+ 49 | ' '+ 50 | ' ' + title + ''+ 51 | ' '+ 52 | ' '+ 53 | '

'+ message + '

' + 54 | ' '+ 55 | ''; 56 | res.write(txt); 57 | res.end(); 58 | res 59 | }; 60 | 61 | exports.logFree = function(message, available) { 62 | console.log(" ") 63 | console.log('Freeing port: ' + message); 64 | console.log("--------") 65 | console.log("available :") 66 | console.log(available) 67 | console.log("--------") 68 | }; 69 | 70 | exports.logOpen= function(port, available) { 71 | console.log(" ") 72 | console.log('Opening app at port: ' + port); 73 | console.log("--------") 74 | console.log("available :") 75 | console.log(available) 76 | console.log("--------") 77 | }; 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Haddock 2 | 3 | A very experimental Proof Of Concept for a Shiny server built with NodeJS. 4 | 5 | ## How does it work? 6 | 7 | Before launching the server, you'll have to define a list of N available port to open on your server. Once the node app is launched, the first thing it does is launching N instances of the Shiny App on the N ports you've defined. 8 | 9 | When a new user connect to the server, they either get an available app (a free port), or an error message. 10 | 11 | What happens in the background is that the Node JS app keeps track of the "available" ports, and serve an available port throught an iframe to the user. 12 | 13 | ## How to use haddock 14 | 15 | Install deps 16 | 17 | ``` 18 | npm install yaml 19 | npm install socket.io 20 | ``` 21 | 22 | ### Getting the project 23 | 24 | First, `git clone` the project on your server. 25 | 26 | ``` 27 | git clone https://github.com/ColinFay/haddock.git 28 | cd haddock 29 | ``` 30 | 31 | ### Config 32 | 33 | Change things in the `config.yml` file: 34 | 35 | + available ports 36 | 37 | ``` 38 | available: [1234, 1235] 39 | ``` 40 | 41 | An array of all the ports you want to open (one shiny app will be launched by port listed here). 42 | 43 | + cmd 44 | 45 | ``` 46 | cmd: "prenomsapp::run_app()" 47 | ``` 48 | 49 | The cmd element (here `prenomsapp::run_app()`) is the command used to launch the Shiny App with a command line call. In the background, it will be passed to `R -e "{cmd}"`. It can be either a call to a package that launch the app, or a `shiny::runApp(/folder/to/app.R)`. Well, in fact it can be any kind of R code as long as it can be passed to `R -e "{cmd}"`. 50 | 51 | + Content & Message of webpage 52 | 53 | ``` 54 | title: "My Shiny App" 55 | ``` 56 | 57 | This piece of code defines the title displayed on the pages 58 | 59 | ``` 60 | no_port: "No port available" 61 | ``` 62 | 63 | Sentence displayed when there is no port available. 64 | 65 | + Port of the server 66 | 67 | ``` 68 | port_server: 8080 69 | ``` 70 | 71 | This port is the port used to access the node app. 72 | 73 | ### Run 74 | 75 | Go to your terminal, and run: 76 | 77 | ``` 78 | node server.js 79 | ``` 80 | 81 | Then go to your browser and open at the port you've specified on `server.listen` (default is 8080). 82 | 83 | ## Roadmap 84 | 85 | ### Near future 86 | 87 | + Today, the node app doesn't launch any new shiny app when there is no port available. That should be made possible. Then, the user could define a number of open ports at launch, then a threshold, and when the threshold of available port is reached, new shiny processes are launched. 88 | 89 | + The server should be able to host multiple Shiny Apps. 90 | 91 | + There should be a home page listing all the available app. 92 | 93 | + Launching shiny apps contained in Docker containers should be possible. 94 | 95 | ### Far future 96 | 97 | + Anything you could expect from a decent server :) (auth, ...) 98 | 99 | 100 | ## FAQ 101 | 102 | ### Should I use it in production? 103 | 104 | Of course you shouldn't. This is just a Proof Of Concept and won't be production ready until a significant amount of work (that is to say probably never). 105 | 106 | For a production environment, please refer to [Shiny Server](https://www.rstudio.com/products/shiny/shiny-server/). 107 | 108 | ### The Node code seems weird 109 | 110 | I'm using this project as an excuse to learn NodeJS, so... sorry :) 111 | 112 | ### Should I contribute? 113 | 114 | Of course you should. Please add any comment you might have in the issue section, and feel free to do PR! 115 | 116 | --------------------------------------------------------------------------------