posit-dev / r-shinylive

https://posit-dev.github.io/r-shinylive/
Other
173 stars 17 forks source link

Test if running under shinylive or server side? #45

Open barryrowlingson opened 10 months ago

barryrowlingson commented 10 months ago

I've got a shiny app which has some issues running under shiny live, which I think I've narrowed down to its use of shinyscreenshot. Commenting out the screenshotButton in the ui fixes it.

But is there a way of testing if the app is running under shinylive so I can include the button if it is running on a server and therefore does work? Something like:

tabPanel(
    h1("Some Data"),
    plotOutput("data_graph"),
    if_shiny_live(
       h2("No screenshots"),
       screenshotButton(label="Snap", selector="id")
    )
)

or maybe an ifelse structure.

coatless commented 10 months ago

Not sure if there is a built-in function call and/or session status to detect if the app running using Shiny or Shinylive.

However, the following should work to detect whether the app is running using shinylive as it checks how base R was compiled:

is_shinylive <- function() { R.Version()$arch == "wasm32"}

Feel free check the detection with this Demo app.

The only other way would be to check if the webr support package is present in the instance and contains the canvas function. The latter matters as there is a different R package using the name webr on CRAN.

barryrowlingson commented 10 months ago

That works nicely. Until people start running entire servers using wasm32 architecture, of course. Sounds silly now, but who would have thought of nodejs when Netscape introduced Javascript! :)

For completeness, here's a chunk of my ui object with shinylive detection using your function:

                  tabPanel(h6("Interviewer feedback"),
                           uiOutput("interviewer_names"),
                           uiOutput("interviewer_feedback"),
                           plotOutput("interviewer_feedback_graph"),
                           if(is_shinylive()){
                               h6("No screenshots with shinylive")
                               }else{
                                   screenshotButton(label = "Save feedback", selector = ".tab-content")
                               }
                           )
coatless commented 10 months ago

@barryrowlingson I understand the concern; but, given the intricacies of the WASM build and Shiny requirements + communication, I'm not sure we'll ever see shiny running on wasm outside of the shinylive case.

barryrowlingson commented 10 months ago

Agree, this is fine. Would you add an is_shinylive function to the package in due course or is the advice to users to write their own as above?

gadenbuie commented 10 months ago

I think this would be great to include in the shinylive R package. Would either of you like to contribute a PR?

I think @coatless's proposed approach is great; for future compatibility we might want to account for wasm64.

BTW, I made a little app showcasing the function on shinylive.io.

georgestagg commented 10 months ago

There's also the possibility of

R.Version()$os == "emscripten"

if we're worried about just checking for wasm32/wasm64. However, I think the arch checks alone are reasonable, at least for now.


I think this would be great to include in the shinylive R package.

Perhaps it makes sense as part of the shiny package itself? The shinylive R package is more useful for building and exporting an app, rather than at runtime in the app. For example, in the shinylive.io example above you can remove library(shinylive) and the app still works as expected.

gadenbuie commented 10 months ago

Perhaps it makes sense as part of the shiny package itself? The shinylive R package is more useful for building and exporting an app, rather than at runtime in the app. For example, in the shinylive.io example above you can remove library(shinylive) and the app still works as expected.

True; I was including library(shinylive) to test that using the shinylive package inside shinylive wasn't going to cause problems. 😄

Personally, I'd rather keep this check in shinylive. It's a bit closer to the place where the implementation might change. And shiny's a bigger and much slower to move package. Not that I'm super worried about this particular function, but it does seem possible that other runtime utility needs might pop up in the future.

georgestagg commented 10 months ago

Ah, gotcha, that makes sense.

One minor issue with loading the shinylive package within shinylive is that it depends on the curl package, currently unsupported by webR. So, you do see a couple of extra warnings in the logs as the app launches in the browser.

We should probably also check with @wch to see if there is some equivalent test for Python apps already in place that we should mimic.

coatless commented 10 months ago

@georgestagg a quick grep through the {py-shiny} found a check against pyodide:

https://github.com/posit-dev/py-shiny/blob/6ed505b18b3cfaab72d0a0a20762c4f0dc49b035/shiny/_shinyenv.py

https://github.com/posit-dev/py-shiny/blob/6ed505b18b3cfaab72d0a0a20762c4f0dc49b035/shiny/__init__.py#L26-L31

For parity with the Python version of shiny, I suppose this means is_shinylive() should be a function in {shiny} R package. It's also probably more efficient to check the full OS name instead of two checks against the arch (wasm32/wasm64 [is this coming?]).

So, I suppose the PR'd function should be:

is_shinylive <- function() { R.Version()$os == "emscripten" }
georgestagg commented 10 months ago

wasm32/wasm64 [is this coming?]

Not in the short term for webR. Perhaps in the future, when the Memory64 proposal has been standardised as part of the Wasm spec.

gadenbuie commented 10 months ago

is_pyodide in Shiny for Python is needed to correctly run shiny apps; in Shiny for R we don't have such a need. If added to Shiny for R, the analogous function would be is_emscripten() or is_wasm(), but it's worth noting that the python version is internal so we don't necessarily need to follow its lead.

If we keep the function in shinylive, it makes it easier to adjust if we end up needing to differentiate between flavors of shinylive (shinylive.io vs self-hosted shinylive apps). It also opens the door to having shinylive (the package) help with runtime calls inside shinylive apps if anything comes up in the future.

Also we should consider using in_ or on_ as the function prefix, i.e. in_emscripten() or on_wasm() or in_shinylive().

coatless commented 10 months ago

@gadenbuie if we opted to place the is_shinylive() function with the {r-shinylive} package that would be another R package required to be downloaded, installed, and loaded during runtime of the app. So, I'm a huge fan of being able to avoid adding any further dependencies.

How about adding to the upstream {shinylive} assets different environment variables and two different check functions in {r-shiny} that read from the different set environment variables?


For the is_shinylive_app(), there could be the following environment variable set during the preload routine:

Sys.setenv("SHINYLIVE_APP" = "TRUE")

Around line 144 in useWebR.tsx:

https://github.com/posit-dev/shinylive/blob/47cfff93b8c2fda760bfeb1cb7918e81edf14df8/src/hooks/useWebR.tsx#L144

Then, there could be a check function defined inside of {r-shiny} as:

is_shinylive_app <- function() { Sys.getenv("SHINYLIVE_APP", "FALSE") == "TRUE" }

On the need to detect shinylive.io, maybe modify the runAppOpts passed to runApp() in the different upstream {shinylive} site templates to flag its an official app? This would allow the appropriate environment definition to be added or omitted.

https://github.com/posit-dev/shinylive/blob/47cfff93b8c2fda760bfeb1cb7918e81edf14df8/site_template/editor/index.html#L17-L21

Another possible option here would be to define a function that can access session$clientData$url_hostname to retrieve the domain and check against a fixed string.

is_shinylive_io_app <- function() { session$clientData$url_hostname == "shinylive.io"}

Feel free to tinker with the suggestions inside of the demo app.

georgestagg commented 10 months ago

FWIW I think Garrick's argument is sound. Shiny is indeed a slow-moving package and if we start thinking about the possibility of other runtime utility functions, perhaps even functions that only make sense when running under Shinylive, it would probably be better to group and organise such functions within the shinylive R package. The development flexibility of making changes outside a wider Shiny release is a benefit too.

The environment variable is also a strong idea. It sounds perfectly reasonable to me that we could see if we are running in the Shinylive environment by checking an environment variable, and they could be set by posit-dev/shinylive as part of the runtime initialisation so that they are set for all apps.

Then, a user who wants to check for Shinylive without loading the shinylive package would simply check the env var using base R's Sys.getenv(), rather than using a convenience function.

So, my vote would be for some setup like:

Then, some or all of the following convenience functions are added to the shinylive R package, depending on what we think is really useful:


I should also mention that I just realised that, technically, posit-dev/shinylive could also directly define an in_shinylive() function in the global environment as part of initialisation, but personally I don't think that's such a good solution here.

wch commented 10 months ago

This package is intended for use outside of shinylive -- it creates shinylive bundles. Runtime environment checks are a completely different set of functionality. Because of that, and because the proposed functions are so simple, I think it would be best not to put them in this package. It's also possible that this package will evolve in ways in the future that make it larger, which will increase the overhead even more for doing one of these checks.

It'll never be possible to run non-shinylive shiny in emscripten, so if you have a shiny app and you are running on emscripten, you can be certain that you're in shinylive. I've added an is_emscripten() function to staticimports, so people can just cut and paste it.

is_emscripten <- function() Sys.info()[["sysname"]] == "Emscripten"

https://github.com/wch/staticimports/blob/35ceec8d9d9429d9244aedc3ee6a1e8d62d59f79/inst/staticexports/os.R#L9

(@georgestagg note that I used Sys.info()[["sysname"]] intead of R.Version()$os, to keep it consistent with the other similar functions.)

wch commented 10 months ago

I think it could make sense to add a is_shinylive() function to shiny that basically is just the same as this is_emscripten function. If there are unforeseen changes in the future so that this for some reason is no longer an effective test, we can just change that function.

gadenbuie commented 10 months ago

Regarding environment variables, another interesting option to consider is setting R_CONFIG_ACTIVE to something like "shinylive" in a shinylive context. This env var is consulted by the config package and is set by Posit Connect.

georgestagg commented 10 months ago

Following up on the environment variable discussion above, it's likely we don't need to set any environment variables from Shinylive after all, since R itself already sets R_PLATFORM in the environment.

So, some other base R options could be,

in_emscripten <- function() grepl("emscripten", Sys.getenv('R_PLATFORM'))
in_wasm <- function() Sys.getenv('R_PLATFORM') == "wasm32-unknown-emscripten"

or similar.

We briefly discussed setting a WEBR environment variable within webR itself, but Pyodide does not do this for Python so it would have no direct equivalent there.

gadenbuie commented 10 months ago

I'm proposing we do it for convenience with existing R tooling, rather than out of necessity or feature parity with Python. I'll move that suggestion to a new issue in https://github.com/posit-dev/shinylive/