Electron Desktop
The RStudio IDE is a single-page web application. It is distributed both as a Linux server application (RStudio Server / RStudio Workbench) and a desktop application (Windows, Mac, Linux). More details here: Understanding the Architecture.
RStudio Server/Workbench is used via a web browser pointed at the server; RStudio Desktop is displayed in what is essentially a custom Chromium-based web browser and local server in one. Currently this is built using QtWebEngine (the sources live in src/cpp/desktop
).
The Electron desktop (under development) replaces QtWebEngine with Electron. The sources live in src/node/desktop
; it is written primarily in TypeScript.
The majority of the user interface (the JavaScript-based single-page application shown inside the window frame) is built from the exact same sources (Java/GWT transpiled to JavaScript) as existing RStudio Desktop and RStudio Server. The same is true for the rsession.exe
code (C++/R).
To build and run the Electron desktop from the ground up, a full RStudio development environment is needed (Qt is not needed, though), and you need to have built the native components such as "rsession" and run the GWT compiler (or have ant desktop
running).
As with any Electron/node project, just go into the src/node/desktop
folder and run npm ci
to install dependencies (or npm i
to install updated dependencies).
src/node/desktop
html/ (static HTML files used to show status before core UI has loaded)
src/
core/ (utilities ported from C++ src/cpp/core and shared_core)
main/ (main process code ported from src/desktop)
renderer/ (main window renderer process code)
ui/ (other renderer process entrypoints)
test/
int/ (integration tests using Playwright and mocha.js)
unit/ (unit tests using mocha.js)
scripts/ (build-time/dev tools)
package.json
tsconfig.json
.eslintrc.js
package-lock.json
The Electron code roughly mirrors the structure of the C++ version.
Code needed from the shared core
C++ project has been rewritten, and lives under core
. Nothing in core
should depend on Electron, but can depend on node.js. This will make it easier to reuse this core code in other node.js-based projects in the future.
Code in core
should always have unit tests.
The bulk of the desktop code is in main
, named for fact it runs in the main
Electron process and can leverage the node.js runtime (bundled with Electron).
The main
code should have unit tests when possible, but by its nature some of it is better served with integration tests.
The renderer
folder contains a relatively small amount of code that runs in the Electron renderer process (same process where Chromium runs) via a preload
script and facilitates communication between the single-page app (aka RStudio's GWT code) and the desktop code running in the main
process. The Electron app is locked down as recommended by latest Electron security guidance, with Context Isolation enabled, Remote Module disabled, and node integration disabled for the renderer process.
More on Electron process model and preload scripts here: https://www.electronjs.org/docs/latest/tutorial/process-model
The coding style is defined via a prettier. Run npm run format
to ensure all sources match the expected style.
Use console
instance methods in startup code that runs before logging is initialized.
- e.g.
console.log()
,console.error()
, etc.
An example of logging from startup code in src/node/desktop/src/main/args-manager.ts
:
import { safeError } from '../core/err';
console.log(' --version Display RStudio version');
console.error(safeError(error));
Use Logger interface methods (definitions in winston-logger) in the main process code once logging has been initialized.
This is generally what we want to use, since it pays attention to the --log-level
and also logs to a file.
- e.g.
logger().logInfo()
,logger().logError()
, etc.
An example of logging from main process code in src/node/desktop/src/main/gwt-callback.ts
:
import { logger } from '../core/logger';
logger().logError(error);
Use Logger Bridge helpers, which will then be invoked as regular logging calls in the main process via the ContextBridge
mechanism.
- e.g.
logString(level, message)
andlogError(error)
An example of logging from preload in src/node/desktop/src/renderer/preload.ts
:
import { getDesktopLoggerBridge, logString } from './logger-bridge';
logString('debug', `[preload] ignoring unsupported apiKey: '${apiKey}'`);
An example of logging from renderer code in src/node/desktop/src/ui/widgets/choose-r/load.ts
:
import { logString } from '../../renderer-logging';
logString('debug', `Error occurred when trying to browse for R: ${err}`);
Currently not being used, but you can also do logging from any code running in the renderer (e.g. GWT code, dev tools console), via: window.desktopLogger.logString()
. This would only work for Electron desktop so would require additional logic to prevent invocation on Server or Qt desktop.
General guidance:
- exported functions/methods should not throw exceptions as a way to indicate potential runtime errors; an exception should generally mean there's a code issue that needs to be fixed
- code internal to modules can use exceptions and try/catch if desired, but they should not "escape" from the module
- be aware that third-party APIs may use exceptions to indicate errors; any synchronous node.js API that talks to the system (e.g. filesystem or child_process) should be assumed to throw; async APIs generally use callbacks instead, but read the docs (or the source code) to be sure
- when throwing, always throw an
Error
or subclass ofError
- when catching, annotate as
unknown
and either use thesafeError()
function or your own type checking viainstanceof
ortypeof
- for a function that returns success/failure (where something interesting may be returned for the error case) define the function as returning the type
Err
(fromerr.ts
), and either return anError
orsuccess()
- for a function that returns a result, but could return an error, use the
Expected<>
pattern inexpected.ts
- use async code if it makes sense; be aware of the differences in style needed when using
Promises
directly via.then.catch
versus theawait
keyword
Unit tests are written using Mocha with the Chai Assertion Library and the SinonJS mocking library.
To run all unit tests, use npm test
. To run a subset of tests, prefix the test's description (or test group description) with "WIP" and run with npm run testwip
and only those tests with be run.
To see test coverage, use npm run testcover
.
Integration tests are executed with Selenium. They are maintained by the QA team.
The Electron app has not been packaged or cannot be found. Run npm run package
to package the app. It should be in src/node/desktop/out
.
For M1, the tests currently look for the x64 package. The x64 version can be packaged by specifying the architecture: npm run package --arch x64
.
It may be failing to start the R session. Check this by trying to run the RStudio entrypoint from your package (src/node/desktop/out/RStudio-darwin-x64/RStudio.app/Contents/MacOS/RStudio
, adjust for your platform). On Mac, don't run the RStudio.app as it hides output so you won't see what went wrong.
You might see this error:
terminating because inserted dylib '/Library/Frameworks/R.framework/Resources/lib/libR.dylib' could not be loaded: tried: '/Library/Frameworks/R.framework/Resources/lib/libR.dylib' (mach-o file, but is an incompatible architecture (have 'arm64', need 'x86_64'))
This means the version of R used is the wrong architecture. You can use RSwitch to swap to the expected version.
- Visit https://www.npmjs.com/package/electron to see the latest release
- If there is, try it out (does it build, can I run the app, and do the unit tests pass)
- Check the release notes for each build by looking at this URL: https://releases.electronjs.org/ (it's also nice to link the release notes in the PR)
Patch releases are lower risk to upgrade to. Major and minor updates may be riskier to adopt so ensure there is enough time before release to upgrade and test.
These environment variables may be set to help with the development, testing, and troubleshooting of certain features.
Variable | Values | Default | Description |
---|---|---|---|
RSTUDIO_ENABLE_CRASHPAD | 1, true, 0, false | false | When enabled, an Electron crash saves a dump file |
RSTUDIO_DESKTOP_PROMPT_FOR_R | any string | empty | When non-empty, the Choose R dialog will be displayed at launch (Windows only) |
RSTUDIO_QUERY_FONTS | 1, true, 0, false | true | When set to false, skip querying for fonts |
RSTUDIO_DESKTOP_MODAL_DEVTOOLS | any string | empty | When non-empty, Chrome devtools will be displayed for standalone modal dialogs such as Choose R and Licensing |
RS_LOG_CONF_FILE | path to logging.conf | empty | Locate logging.conf in a non-standard location |
RS_LOG_LEVEL | error, warning, info, debug | error | Controls logging level |
RSTUDIO_DESKTOP_LOG_LEVEL | error, warning, info, debug | error | Controls logging level (synonym for RS_LOG_LEVEL) |
RS_NO_SPLASH | any string | empty | When non-empty, suppress the splash screen at startup |
RS_SPLASH_DELAY | integer | 500 | Delay before showing splash screen, in milliseconds |
Developing
- Beginners guide
- RStudio Development
- Git conventions
- Accessibility
- Development with Vagrant
- Electron desktop
- GWT
- Internationalization (i18n)
- Node Native Modules
Issues
Personal development environment
- Installing RStudio Dependencies
- M1 Mac Dev Machine Setup
- Visual Markdown Editing
- IDE Development Using Visual Studio Code
Building
Coding standards
Tests
Other topics