Runno v0.0.0 - beta as heck

Make your code samples Runno.

Getting to know Runno

Using Runno

Paste some code into the textbox, set your runtime and then copy the iframe into your website. Now you've got a snippet of code that someone can run and edit!

For advanced uses, the iframe can be controlled with the host API. With the host API you can pass files, collect STDOUT and automate running in both interactive and headless modes. Or bake Runno into your app with the runtime web components.

Examples

Lets say you're writing a blog post and want to explain how an if statement works. You've written a code sample like this:

name = input("What's your name? ")
if "i" in name.lower():
  print("You've got an I in your name, how selfish.")
else:
  print("There's no I in your name.")

You can take that snippet to Runno, select the python runtime and paste in your code. Then you can select the embed field and copy the generated iframe. Paste it into your blog like:

<p>
  Here's an example of an if statement:
</p>

<iframe src="…" crossorigin allow="cross-origin-isolated" width="640" height="320" frameBorder="0"></iframe>

<p>
  You can use an if statement to…
</p>

And it would work something like:

Here's an example of an if statement:

You can use an if statement to…

Now your readers can run your example to try it out. They can even edit it and check they understand how it works!

If you find you're using Runno a lot on your blog, you can use the Runno Web Components (documentation). First follow the quickstart guide to make sure it's set up correctly.

Once you've got Runno installed you can use the same features without needing to use an iframe. Lets say you were explaining how infinite loops work in JavaScript:

<p>
  An infinite loop runs forever:
</p>

<runno-run runtime="quickjs" editor controls>
  while (true) {
    console.log("Help I'm trapped in a code factory!");
  }
</runno-run>

<p>
  Try changing the text to your own example!
</p>
            

And it would work in the same way as the iframe:

An infinite loop runs forever:

while (true) { console.log("Help I'm trapped in a code factory!"); }

Try changing the text to your own example!

How it works

Runno uses Web Assembly to run code in the browser. Code runs in a unix-like sandbox that connects to a web-based terminal emulator. This means it behaves a lot like running code natively on a linux terminal. It's not perfect, but it's handy for making simple code examples run.

When you click run on a Runno example the web assembly binary for that programming language is collected from WAPM (the Web Assembly Package Manager). It's then run in a Web Worker, inside a sandbox with an in-memory file system. It's connected up to a terminal emulator and simple IO is routed back and forth.

Why run in the browser?

The way Runno is built shifts the work of running code examples from the server to the client. Running code examples on your server is risky, it means having to implement protections from potentially hostile clients and limiting resources. This means that it's difficult to build a system that is open for anyone to use.

If you're writing a blog post, your own course, or writing documentation it's difficult to implement your own sandbox. You really want to use something that someone else has built. Even if an open source solution existed you'd have to deploy a server and maintain it! By running on the client you eliminate needing any servers or third party involvement.

Plus it's pretty cool that you can just run code in your browser!

Limitations

The programming languages available are limited by what has already been compiled to Web Assembly using WASI and uploaded to WAPM. A great way to help more languages become available would be to compile your favourite programming language tools to Web Assembly and upload them to WAPM.

WASI doesn't provide a full linux environment, so most system based interfaces won't work. Packages and modules are also difficult to install inside the Runno environment. If you want to import code, you'll need to provide that code alongside your example.

Runno is best for small code examples that use STDIN and STDOUT. That is typically the sort of thing that is taught in a CS1 course at university. This isn't an inherent limitation of the system though! As more work is put into Runno and the ecosystem of tools it depends on, more will become available.

Security

Running arbitrary code is always going to have some security risks, even if you're running it on the client. The goal of the Runno sandbox is to make it as safe as possible for a client to press run. They should be able to trust that their browser won't do anything they don't expect it to do.

Because Runno runs within the browser on the client, you don't have to worry about it damaging your servers. But we also need to make sure Runno won't damage your clients.

There are two main layers of sandboxing that help make Runno secure:

  1. By running as WebAssembly we create a layer of sandboxing, any system resources that the binary wants have to be passed through a layer of JavaScript that Runno controls.
  2. By embedding Runno inside an iframe from another domain we sandbox it from any secrets inside your webpage. This prevents Runno from having access to cookies or from making API calls as a user.

With these two layers combined it's difficult for a user to hurt themselves, for you to hurt a user, or for users to hurt each other. But that doesn't mean Runno is 100% safe.

Runno can quite easily be used to hog resources on a client. An infinite loop can lock up a tab. Large binaries can potentially be dynamically loaded from WAPM, using surprising amounts of bandwidth. While not ideal, these are typical risks a user has in navigating to any website.

The intention is for Runno to either fix security issues, or to disclose the risks upfront. So if you find a security issue with the way Runno works, please contact me (security@taybenlor.com)!

Big thanks to

A number of open-source projects and standards have been used to make Runno possible. These include:

  • Wasmer, WAPM, and WebAssembly.sh - the Wasmer team have built a lot of great tools for running Web Assembly. WAPM, the Web Assembly Package Manager is key to how Runno works. WebAssembly.sh was a great inspiration and starting point.
  • WASI and Web Assembly - a bunch of people from various working groups and especially the Bytecode Alliance have been involved in making WASM and WASI what it is today.
  • XTerm.js - a fully featured terminal that can be run in the browser
  • CodeMirror - a great web-based code editor that's always a delight to integrate
  • The extensive work by many in the web development community on making native code and APIs run successfully inside the browser.

Thanks to everyone who has built compatibility layers for the web. The web becomes a better platform with every new piece of software we can run on it!

Also big thanks to my friends who tolerate me constantly talking about WASM. Thanks especially to Shelley, Katie, Jesse, Jim, Odin, Hailey, Sam and other Sam.

Host API

The Host API lets you control a Runno iframe and receive the resulting IO and filesystem. Runno exposes methods for setting the editor, adding files, and running code in interactive or headless modes. There's also a bunch of helpers.

You'll need to be using a modern web stack that includes a bundler of some sort to use the Host API. If you get stuck, the source is relatively easy to understand .

Quickstart

Start by adding @runno/host to your package:

$ npm install @runno/host

Then you'll be able to import ConnectRunno.

import { ConnectRunno } from '@runno/host'

And use it with an existing Runno iframe.

const runno = await ConnectRunno(iframe);

To run a code sample interactively use interactiveRunCode.

const { result } = await runno.interactiveRunCode("python", codeSample);

The result has properties for stdin, stdout, stderr, tty, and fs (the file system). This represents the input that the user has typed, the output they received, any errors displayed and the full terminal text (input, output and errors).

If you want to run code without user input or output, you can use headlessRunCode.

const { result } = await runno.headlessRunCode("python", codeSample, stdin);

The stdin is optional, but necessary if the code expects input.

How it works

The connection between the iframe and the host is established via postMessage ( mdn docs ). Which is managed by the post-me npm package ( github repo).

Methods are exposed within the iframe that can be called using a promise-based interface. This makes it quite easy to integrate with your existing code while allowing the iframe to be hosted externally.

Cross-Origin Headers

To get the best experience your page should provide a Cross-Origin Isolated context so it can internally use SharedArrayBuffer. Without this Runno falls back to a lower performance hack that has the potential to break in the future.

To make your website Cross-Origin Isolated set the following headers in your HTTP response:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

You can test that your page is Cross-Origin Isolated by opening the browser console and checking crossOriginIsolated (see: mdn docs).

Supported Runtimes

Runno supports a number of runtimes based on existing packages published to WAPM. These runtimes will be supported by Runno going forward, but may change in underlying binary implementation.

  • python - Runs Python 3 code, not pinned to a particular version but is at least 3.6.
  • quickjs - Runs JavaScript code using the QuickJS engine.
  • sqlite - Runs SQLite commands. Currently just by piping SQL into the sqlite command.
  • clang - Compiles and runs C code using Ben Smith's llvm fork (see: wasm-clang).
  • clangpp - Compiles and runs C++ code using the same llvm fork.

Over time Runno will support more languages. If your favourite language isn't in this list then consider compiling its tools to Web Assembly and uploading them to WAPM. If you do that part I'll absolutely get support into Runno. For now it needs to be a WASI binary. Some good starting resources are this blog post and wasienv.

Client Params

The client supports parameters which can be used to customise its look and behaviour. These can be set as query parameters as part of the url, like: http://runno.run/?editor=0

  • controls - Whether to display controls, valid values are: 0 and 1.
  • editor - Whether to display the editor, valid values are: 0 and 1.
  • autorun - Whether to automatically run the given code on load, valid values are: 0 and 1.
  • runtime - Which runtime to use, see runtimes.
  • code - URL safe base64 string containing the code to use. See: url-safe-base64.

Methods and Helpers

Helpers

generateEmbedURL

export function generateEmbedURL(
  code: string,
  runtime: string,
  options?: {
    showControls?: boolean; // Default: true
    showEditor?: boolean; // Default: true
    autorun?: boolean; // Default: false
    baseUrl?: string; // Default: "https://runno.run/"
  }
): URL

Generates a URL that can be used as the src of an iframe to embed Runno. Arguments and options correspond to the options in Client Params.

generateEmbedHTML

export function generateEmbedHTML(url: URL): string

Generates a string representing the HTML for an iframe that can be embedded. It generates the same embed HTML as on this website.

ConnectRunno

export async function ConnectRunno(
  iframe: HTMLIFrameElement
): Promise<RunnoHost>

Connects to the Runno instance in the passed iframe. Returns a RunnoHost object that can be used to control the Runno client iframe.

RunnoHost methods

These methods are all exposed on the RunnoHost object returned by ConnectRunno.

showControls

showControls(): Promise<void>

Shows the controls (run button).

hideControls

hideControls(): Promise<void>

Hides the controls (run button).

showEditor

showEditor(): Promise<void>

Shows the editor.

hideEditor

hideEditor(): Promise<void>

Hides the editor.

setEditorProgram

setEditorProgram(
  syntax: Syntax,
  runtime: Runtime,
  code: string
): Promise<void>

Sets the program in the editor, how to highlight it, and how to run it. Syntax can be python, js, sql, cpp, or undefined.

getEditorProgram

getEditorProgram(): Promise<string>

Gets the current program in the editor.

interactiveRunCode

interactiveRunCode(runtime: Runtime, code: string): Promise<RunResult>

Runs some code interactively using the given Runtime. It does not change or update the program in the editor.

interactiveRunFS

interactiveRunFS(
  runtime: Runtime,
  entryPath: string,
  fs: FS
): Promise<RunResult>

Runs a filesystem object interactively using the given Runtime. The entryPath is used as the "main" file to run. It does not change or update the program in the editor.

interactiveUnsafeCommand

interactiveUnsafeCommand(command: string, fs: FS): Promise<RunResult>

Runs a concrete command in a similar way to webassembly.sh . This is internally used to support Runno's runtimes. You could use this to pull your own packages from WAPM and run them.

interactiveStop

interactiveStop(): Promise<void>

Stops the currently running program. The result will be returned as usual by the Promise associated with the call that started running code.

headlessRunCode

headlessRunCode(
  runtime: Runtime,
  code: string,
  stdin?: string
): Promise<RunResult>

Runs code headlessly. The user will not have an opportunity to interact with this code. You can provide stdin to specify the input. This is useful for testing code a user has written.

headlessRunFS

headlessRunFS(
  runtime: Runtime,
  entryPath: string,
  fs: FS,
  stdin?: string
): Promise<RunResult>

Corresponding headless version of running with an FS. See: interactiveRunFS

headlessUnsafeCommand

headlessUnsafeCommand(
  command: string,
  fs: FS,
  stdin?: string
): Promise<RunResult>

Corresponding headless version of running an unsafe command. See: interactiveUnsafeCommand

Important Types

Runtime

export type Runtime = "python" | "quickjs" | "sqlite" | "clang" | "clangpp";

Runtimes that Runno supports.

Syntax

export type Syntax = "python" | "js" | "sql" | "cpp" | undefined;

Syntax highlighting options that the Runno editor supports.

CommandResult

export type CommandResult = {
  stdin: string;
  stdout: string;
  stderr: string;
  tty: string;
  fs: FS;
  exit: number;
};

The result from running a command.

RunResult

export type RunResult = {
  result?: CommandResult;
  prepare?: CommandResult;
};

The result from running using Runno. If the runtime has a compilation or other type of preparation then the output from this step will be in the optional prepare field.

FS

export type FS = {
  [name: string]: File;
};

A snapshot of a filesystem that can be passed to Runno. If folders are necessary the slashes should be part of the name.

File

export type File = {
  name: string;
  content: string | Uint8Array;
};

A snapshot of a file that can be passed to Runno.

Web Components

The building blocks of Runno are available as Web Components that can be bundled into your page. You can then use them just like other HTML in your website.

You'll need to be using a modern web stack that includes a bundler of some sort to use the web components. If you haven't used web components before you can learn about them on MDN, or dive in. They work like normal HTML elements, you can style them with classes, give them ids etc - but also they have custom behaviour.

Quickstart

Start by adding @runno/runtime to your package:

$ npm install @runno/runtime

Then you'll be able to import defineElements.

import { defineElements } from '@runno/runtime'

And call it as part of your main script while the page loads.

defineElements();

Once you've called defineElements you can use runno elements on the page.

<runno-run runtime="python" editor controls>
print('Hello, World!')
</runno-run>

Which would render as:

print('Hello, World!')

How it works

The Runno iframe is implemented using these web components and so there is a lot of overlap in how they work. Rather than repeating the documentation:

  • Your webpage needs to be a Cross-Origin Isolated context.
  • The runno-run element implements the RunnoHost methods .
  • All the Runtime and Syntax options can have the same variables.
  • The code still runs within a WebAssembly sandbox isolating it from your page.

Some things work differently without the iframe:

  • For browsers that don't support SharedArrayBuffer the fallback is a prompt. It's possible to fix this with a ServiceWorker. Contact me if you need help.
  • Without the iframe as an extra layer, some security mitigations won't work. For example: it will be easier to lock up the tab with an infinite loop.
  • There's no autorun option.

Elements

runno-run

<runno-run
  syntax="Syntax" <!-- optional -->
  runtime="Runtime" <!-- optional -->
  code="string" <!-- optional -->
  editor <!-- optional, presence displays the editor -->
  controls <!-- optional, presence displays the controls -->
>
  <!-- content is treated as code if not set as an attribute -->
</runno-run>

The runno-run element implements all of Runno's public APIs and acts as a wrapper around the editor, terminal and controls. It's the main thing you should be using.

On top of the RunnoHost methods you can also call:

  • run() - runs the code in the editor (needs a runtime)
  • stop() - stops the code running
  • running - property, whether the element is currently running

runno-editor

<runno-editor
  syntax="Syntax" <!-- optional -->
  runtime="Runtime" <!-- optional -->
  code="string" <!-- optional -->
>
  <!-- content is treated as code if not set as an attribute -->
</runno-editor>

The editor doesn't do much by itself, but does provide a neat little instance of CodeMirror. You can also call:

  • program - gets the current program
  • setProgram(syntax: Syntax, runtime: Runtime, code: string) - sets the current program

runno-terminal

<runno-terminal></runno-terminal>

The terminal is responsible for actually running the code. Without a runno-run element to command it, it's a bit harder to use. But you might find something cool to do with it!

  • writeFile(path: string, content: string | Buffer | Uint8Array) - writes a file to the local file system
  • runCommand(command: string): Promise<RunResult> - Runs a raw command (see: interactiveUnsafeCommand)
  • isReadyForCommand() - Whether it's ready for a command (not running one)
  • stop() - Stops the currently running program
  • focus() - Focuses the input
  • clear() - Clears the terminal

runno-controls

<runno-controls
  running <!-- optional, presence displays the stop button -->
></runno-controls>

The controls don't do much without being hooked up! When the buttons are clicked, they fire events:

  • runno-run - when the run button is clicked
  • runno-stop - when the stop button is clicked

You can also cause these events to be triggered:

  • run() - emits a run event
  • stop() - emits a stop event

Helpers

headlessRunCommand

headlessRunCommand(
  command: string,
  fs: FS,
  stdin?: string
): Promise<RunResult>

Same as headlessUnsafeCommand in the Host API. Handy if you want to use Runno's runtime without any UI.

class RunnoProvider

class RunnoProvider(
  terminal: TerminalElement,
  editor: EditorElement,
  controls?: ControlsElement
)

If you wanted to piece together the bits of a runno-run element, this is how you'd do it. Construct a RunnoProvider with a terminal, editor and controls then call it like it's a RunnoHost.

Integration

If you'd like to do more with the web components or runtime, please contact me. I'm quite interested in adding extra features!