Turn a Shiny Dashboard into a Desktop App

Because sometimes bureaucracy gets in the way

Jefferey Cave
10 min readNov 6, 2021

Shiny is popular web publishing service, unfortunately, not every application can be deployed on servers. This tutorial demonstrates a simple means by which to deploy a shiny app to desktop by creating a Site Specific Browser. Mostly to skip the bureaucratic begging for a server.

Available on GitLab: Jeff Cave / shinyapp-desktop · GitLab

An R-Shiny dashboard can be run as a desktop application, from a double click, to give non-technical users a seamless experience.

About two years ago, I had one of the Data Scientists in our organization call me with a problem. They had just spent a significant amount of time putting together a dashboard in R and Shiny and were wondering where they could host the dashboard to share it with clients.

They wanted to know where our Shiny server was stored and how they could publish to it.

It took everything I had not to laugh at them.

The thing I was working on at the moment he had called was a generic deployment system for exactly that kind of project, but was running into all kinds of negotiations with Security, Finance, Architecture … everybody has to have their say. To get him the server he wanted, I estimated years.

Like any large organization, the bureaucracy must be fed.

This was a huge blow to the Data Scientist, his team had been developing the dashboard for months. The business had invested precious effort in describing their informational needs. The team had demonstrated the value of Shiny. They were now ready to realize all that effort, and the organization’s statement was: we can’t do that.

That’s a lot of wasted effort.

After some discussion, I took serious pity on him and his team (and myself, I’d actually invested a lot of my coffee breaks coaching his junior Data Scientists).

  • Your customer’s need the dashboard now? (yes)
  • Is the dashboard computationally expensive? (no)
  • Do you have a shared folder you could publish the application to? (yes)
  • Are the customer’s at all technically savvy? (no)

I told him to give me the weekend and I’d give him a prototype solution on Monday.

Project

The intent is not to teach how to do complex mathematics, or to write Shiny Apps, but rather to demonstrate how to configure a project within the organizational environment. It is hoped that this will act as a spring-board, helping users get setup quickly.

The code itself is a simple demo app exported from RStudio. The real trick is to get it to run on the desktop environment.

The expectation is that the developer wants to deploy a shared application, but is in an environment where there is no Shiny server, and is not likely to be one anytime soon. Rather than wait, the developer can take advantage of a shared folder structure, and an old feature of FireFox, to run the application on individual desktops. While not being as elegant, it represents a solution that is likely suitable for most reporting needs, and can be implemented immediately (aka: use the tools already present).

Pre-Requisites

The demo assumes you have RStudio installed, and will be interacting with the system via PowerShell. There is no reason this will not work on Linux, however it is not what we use at the office, so not what it was tested on.

Checkout the base project

To get started, clone the sample project and open it in RStudio

  1. Navigate: https://gitlab.com/jefferey-cave/shinyapp-desktop
  2. Click: Fork
  3. Open your copy of the project
  4. Get the clone URL
  5. Go to command line and checkout project
cd ~/Project/Folder
git clone https://gitlab.com/jefferey-cave/shinyapp-desktop.git
cd shinyapp-desktop
ls -al

You should see a listing of all the files in the project.

Before we go any further, we should probably check to see that the project runs on our computer. This ensures that there are no basic configuration issues before the actual work begins.

  1. Double click on the file desktopshiny.Rproj
  2. Open: app.R
  3. Click Run App

You should see the shiny app open in the in-built browser. There may be some dependency resolution that needs to happen.

Proof that the application is running and any problems we may experience are not with the computer configuration or app code.

Create a Starter Script

While it is great to know that the application works, it is not currently a great user experience. We have been asked to create an app for those less technically inclined, and they should not need an instruction manual to get up and running.

We can ease their experience by creating a starter script that loads their application from a shortcut.

The first thing to note is the output during the run of our Shiny dashboard. When we click on the button Run App we see the exact R command being executed to achieve all of this, and its output:

shiny::runApp()Listening on http://127.0.0.1:5436

Try copy/pasting your URL into your local browser, you should see the same app.

Knowing that there is an R command that will start our application, we can skip the IDE and run the application using rscript

  1. Stop your app in RStudio
  2. Open a PowerShell terminal
  3. Change to your project folder
  4. Run your project using rscript

rscript.exe -e "shiny::runApp('.')"

Your app should be started, but without having to have the customer load the IDE:

Listening on http://127.0.0.1:3145

Try pointing your browser at that new URL. You should be looking at the app.

NOTE

The port changes every time you start. It is randomly assigned at start. You can specify the port that will be used; however if we put together more than one dashboard, having a random port means less coordination between data scientists (it should just work).

We will not be making assumptions about the start conditions and will not set a static port.

If we check the runApp parameters, there is one extra parameter we can include to make this a little more user friendly:

rscript.exe -e "shiny::runApp('.',launch.browser=TRUE)"

Save that in a text file called start.ps1.

You now have a basic script for users to start your interactive report. Having your customer click on the start script will give them a basically seamless experience.

Creating a new browser instance

Since Shiny advertises the port it is listening on, it is possible for us to capture that information, and then instantiate a special browser instance on behalf of the user.

For our user, it would be nice if we were able to start a browser instance just for the application. We also would like to stop the Shiny instance when the browser stops using it.

There is no data. There is only XUL!

(the XUL platform slogan)

For this example, we will use Firefox as it is based on the XUL platform which has a well documented and modifiable interface. To summarize (in a brutal way), FireFox is itself a webpage that can be dynamically modified (if you know how). We are going to use this feature to create a primitive Site Specific Browser.

Using PowerShell, we can extend our shiny server starting script to listen for the advertised port. We can then use this advertised URL and port to start a Firefox instance.

# Start an instance of the app using `rscript` and 
# capture `stderr` for the port number
& "rscript.exe" -e "shiny::runApp('.')" 2>&1 |
% {
# look for the `url` line
if($_ -like '*Listening on*'){
# Parse the input for the url
("$_" -replace ".*Listening on ",'').Trim();
}
} |
% {
# open FireFox using the discovered URL
& "C:\Program Files\Mozilla Firefox\firefox.exe" $_
};

This solution gets us part way there: we are now starting a unique instance of the browser for the shiny app.

The issue is that when we terminate our Firefox instance, our Shiny instance continues to run in the background. We must manually stop it.

We can continue to modify our script to start both the Shiny dashboard and the Firefox instance separately, then allow our script to maintain enough intelligence about them to monitor their independent process states.

# Start the `shiny` "thread"
$shiny = Start-Job -Name "shiny" -ArgumentList($pwd) -ScriptBlock{
param($workingdir);
cd $workingdir;
# Start the shiny app and print the URL
& "rscript.exe" -e "shiny::runApp('.')" 2>&1 | % {
if($_ -like '*Listening on*'){
("$_" -replace ".*Listening on ",'').Trim()
}
}
}

At this point Shiny is started as a job, and jobs maintain a reference which can then be used to stop the job at a later time. This does add the problem that we need to read from the output stream slightly differently.

#poll the shiny thread for output
while ($shiny.HasMoreData -or $shiny.State -eq "Running") {
$url = $shiny.ChildJobs[0].output.readall();
# when we find the URL ... stop
if($url){
break;
}
}

Now, we have the URL at the script level, and can proceed to start Firefox.

Again, we want to create it as a separate process that we can monitor.

# create an array of arguments
$args = @('-profile','./profile','-new-instance',"-url `"$url`"");
# start the firefox instance
$ff = Start-Process "C:\Program Files\Mozilla Firefox\firefox.exe" -ArgumentList $args -PassThru -Wait

This will block the script until FF stops.

Pay close attention to the arguments passed to Firefox.

  • profile: This uses a pre-existing profile that is customized to our purposes.
  • new-instance: Ensures, it does not re-use any instances of FireFox that may already be open
  • Wait: Ensures that the PowerShell job blocks processing until it ($ff)completes

This forces a “new-instance” and a new “profile” and a “wait” until Firefox completes. This custom profile is used to manipulate the way Firefox appears to the user. For the adventurous, go inspect the included profile to see ways you can manipulate Firefox to make it behave more like we want to.

Our final step is to simply stop the shiny process once the Firefox process has terminated.

Stop-Job $shiny.Id

The completed script looks like:

$shiny = Start-Job -Name "shiny" -ArgumentList($pwd) -ScriptBlock{
param($workingdir);
cd $workingdir;
& "rscript.exe" -e "shiny::runApp('.')" 2>&1 | % {
if($_ -like '*Listening on*'){
("$_" -replace ".*Listening on ",'').Trim()
}
}
}
while ($shiny.HasMoreData -or $shiny.State -eq "Running") {
$url = $shiny.ChildJobs[0].output.readall()
if($url){
break;
}
}
$args = @('-profile','./profile','-new-instance',"-url `"$url`"");
$ff = Start-Process "C:\Program Files\Mozilla Firefox\firefox.exe" -ArgumentList $args -PassThru -Wait
Stop-Job $shiny.Id

At this point you should be able to simply run the script

.\run.ps1

and (eventually… it’s a little slow) see your app running in a window.

Our dashboard, running as a standalone desktop application. The icon can be changed by modifying the files in the “profile” folder.

Creating a Shortcut

One of the issues with creating a PowerShell script is that regular users can’t run it without a bit of “know-how”. The easiest way to get around this is to create an old fashioned BAT file.

cd <working directory>
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\run.ps1"

If you run this as a regular user, you will likely get an error. The problem is that Windows (in their infinite wisdom) makes scripting unavailable to users by default. This is to protect them from malicious scripts.

In order to activate the script, you need to indicate that you “know what you are doing”.

Since we only want our users to see the desktop window, we may as well hide the console window while we are at it.

cd <working directory>
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -windowstyle hidden -executionpolicy bypass ".\run.ps1"
  • executionpolicy: allows the script to be run
  • windowstyle: allows us to hide the terminal window

Given this is a desktop application, a shortcut file (lnk) is probably a better option than the above bat file. These allow us to specify all the same parameters, but also an icon file, while removing all the console windows.

The link settings diaglogue, showing it filled in and with the icon set
  1. Navigate to the working directory in Windows Explorer
  2. Right-Click > New ... > Shortcut
  3. Set the properties
    - Target: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -windowstyle hidden -executionpolicy bypass ".\run.ps1"
    - Start in: <working directory>
    -Change Icon ...

By setting those three options, your users are a double click away from a reasonably seamless desktop experience.

A Happy(ish) Colleague

The lead for the Data Science team I was working with was (reasonably) happy with the solution. It was a hack, but it got his team up and running in a matter of days.

We both agreed that optimal solution was to get a Shiny Server installed on-prem and link the URL form the internal website, so I put him in touch with the correct procurement experts as well as giving him this solution. I don’t know what ever came of the procurement.

Found this useful or interesting? Consider leaving a tip … it helps.

We did some refinements for the team’s solution (mostly automating the deploy to the shared filesystem from GitLab’s CI/CD features), but for the most part the above solution represents a quick and dirty way for Data Scientists get their work in front of decision makers.

To this day, the internal website maintains a link starting with file:/// that points to the shared network filesystem.

For those paying attention, you may notice that this solution is not constrained to Shiny, but to any served web application: Node, Python, or perhaps something tucked away in a docker instance. This can also be used as a means of constraining a user to a web based application, perhaps for keeping students from cheating in a test, or keeping a temporary labour pool focused on their tasks.

I make no claims that this is the right solution, what I suggest is that it is a feasible solution. Any organization that can build a Shiny application, also has the tools to implement this solution. I share this solution in the hopes that it helps another Data Developer when Bureaucracy gets in the way.

Engage

Please leave a comment or ask a question, and if you found this content valuable, please remember to click on follow.

--

--

Jefferey Cave

I’m interested in the beauty of data and complex systems. I use story telling to help others see that beauty. https://www.buymeacoffee.com/jeffereycave