Skip to content

Electron Desktop

Gary edited this page Apr 26, 2023 · 52 revisions

Background

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).

Dependencies

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).

Source Code Organization

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

Coding Conventions

The coding style is defined via a prettier. Run npm run format to ensure all sources match the expected style.

Logging

In startup code (before logging has been initialized)

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));

In main process code (after logging has been initialized)

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);

In preload scripts or the renderer process

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) and logError(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.

Error Handling

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 of Error
  • when catching, annotate as unknown and either use the safeError() function or your own type checking via instanceof or typeof
  • 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 (from err.ts), and either return an Error or success()
  • for a function that returns a result, but could return an error, use the Expected<> pattern in expected.ts
  • use async code if it makes sense; be aware of the differences in style needed when using Promises directly via .then.catch versus the await keyword

Unit Tests

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

Integration tests are executed with Selenium. They are maintained by the QA team.

Common errors running Electron

electron.launch: Failed to launch: Error: spawn

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.

RStudio starts but hangs (no UI)

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.

Keeping Electron Updated

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.

Environment Variables

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
Clone this wiki locally