Skip to content

Commit 54835ad

Browse files
authored
Improve code comments (#293)
* wip * lots of documentation for the sandbox code * update docs
1 parent 7694c8e commit 54835ad

File tree

15 files changed

+476
-181
lines changed

15 files changed

+476
-181
lines changed

packages/editor/src/runtime/executor/executionHosts/ExecutionHost.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,33 @@ import React from "react";
22
import { lifecycle } from "vscode-lib";
33
import { TypeCellCodeModel } from "../../../models/TypeCellCodeModel";
44

5+
/**
6+
* The ExecutionHost is responsible for rendering the output of a notebook/code cell.
7+
* The abstraction is introduced to support the Iframe Sandbox
8+
*
9+
* There are two types of ExecutionHosts:
10+
* - Local
11+
* - Sandboxed
12+
*
13+
* The LocalExecutionHost just renders output in the same document as the container, and can be used for testing
14+
* The SandboxExecutionHost renders an iframe and evaluates end-user code in there, to isolate the code execution in a different domain.
15+
*
16+
* ExecutionHosts are called from NotebookRenderer (which renders the monaco editors for cells)
17+
*/
518
export type ExecutionHost = lifecycle.IDisposable & {
19+
/**
20+
* Render the container of the execution host:
21+
* - the LocalExecutionHost doesn't use this
22+
* - the Sandbox uses this to render the <iframe>
23+
*/
624
renderContainer(): React.ReactElement;
725

26+
/**
27+
* Render the cell output:
28+
* - the LocalExecutionHost renders the Cell Output directly
29+
* - the Sandbox renders a "fake" div (OutputShadow) that has the same size as the actual output.
30+
* The actual output is rendered in the <iframe> which is rendered in renderContainer()
31+
*/
832
renderOutput(
933
model: TypeCellCodeModel,
1034
setHovering?: (hover: boolean) => void

packages/editor/src/runtime/executor/executionHosts/local/LocalExecutionHost.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ import { TypeCellModuleCompiler } from "../../resolver/typecell/TypeCellModuleCo
1313
import { VisualizerExtension } from "../../../extensions/visualizer/VisualizerExtension";
1414

1515
let ENGINE_ID = 0;
16-
export default class LocalExecutionHost extends lifecycle.Disposable implements ExecutionHost {
16+
17+
export default class LocalExecutionHost
18+
extends lifecycle.Disposable
19+
implements ExecutionHost
20+
{
1721
private disposed: boolean = false;
1822

19-
private readonly outputs = observable.map<string, ModelOutput>(
20-
undefined,
21-
{
22-
deep: false,
23-
}
24-
);
23+
private readonly outputs = observable.map<string, ModelOutput>(undefined, {
24+
deep: false,
25+
});
2526

2627
private readonly engine: Engine<CompiledCodeModel>;
2728
private readonly id = ENGINE_ID++;
@@ -34,32 +35,35 @@ export default class LocalExecutionHost extends lifecycle.Disposable implements
3435
super();
3536
this.engine = new Engine(
3637
getTypeCellResolver(documentId, "LEH-" + this.id, (moduleName) => {
37-
return new TypeCellModuleCompiler(moduleName, monacoInstance)
38+
return new TypeCellModuleCompiler(moduleName, monacoInstance);
3839
})
3940
);
4041
this.engine.registerModelProvider(compileEngine);
4142

42-
const visualizerExtension = this._register(new VisualizerExtension(compileEngine, documentId, monacoInstance));
43+
const visualizerExtension = this._register(
44+
new VisualizerExtension(compileEngine, documentId, monacoInstance)
45+
);
4346

44-
this._register(visualizerExtension.onUpdateVisualizers(e => {
45-
for (let [path, visualizers] of Object.entries(e)) {
46-
this.outputs.get(path)!.updateVisualizers(visualizers);
47-
}
48-
}));
47+
this._register(
48+
visualizerExtension.onUpdateVisualizers((e) => {
49+
for (let [path, visualizers] of Object.entries(e)) {
50+
this.outputs.get(path)!.updateVisualizers(visualizers);
51+
}
52+
})
53+
);
4954

5055
this._register(
5156
this.engine.onOutput(({ model, output }) => {
5257
let modelOutput = this.outputs.get(model.path);
5358
if (!modelOutput) {
5459
modelOutput = this._register(
55-
new ModelOutput(
56-
this.engine.observableContext.context
57-
)
60+
new ModelOutput(this.engine.observableContext.context)
5861
);
5962
this.outputs.set(model.path, modelOutput);
6063
}
6164
modelOutput.updateValue(output);
62-
}));
65+
})
66+
);
6367
}
6468

6569
public renderContainer() {

packages/editor/src/runtime/executor/executionHosts/sandboxed/FreezeAlert.tsx

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,39 @@ import * as React from "react";
22
import Flag from "@atlaskit/flag";
33
import { VscWarning } from "react-icons/vsc";
44

5+
/**
6+
* A popup that is shown when we haven't received a "pong" message for a while.
7+
* There might be an infinite loop or other error in the user code.
8+
*/
59
export const FreezeAlert = (props: {
6-
// onDismiss: () => void;
7-
onReload: () => void;
10+
// onDismiss: () => void;
11+
onReload: () => void;
812
}) => {
9-
return (
10-
<Flag
11-
css={{
12-
zIndex: 2000,
13-
backgroundColor: "rgb(222, 53, 11)",
14-
}}
15-
appearance="error"
16-
icon={
17-
<VscWarning
18-
css={{
19-
width: "24px",
20-
height: "24px",
21-
padding: "2px",
22-
}}
23-
/>
24-
}
25-
id="error"
26-
key="error"
27-
title="The document is not responding"
28-
description="It seems like your document has frozen. Perhaps there is an infinite loop in the code?
29-
Fix any code errors and click Reload to retry."
30-
actions={[
31-
// { content: "Dismiss", onClick: props.onDismiss },
32-
{ content: "Reload", onClick: props.onReload },
33-
]}
13+
return (
14+
<Flag
15+
css={{
16+
zIndex: 2000,
17+
backgroundColor: "rgb(222, 53, 11)",
18+
}}
19+
appearance="error"
20+
icon={
21+
<VscWarning
22+
css={{
23+
width: "24px",
24+
height: "24px",
25+
padding: "2px",
26+
}}
3427
/>
35-
);
36-
}
28+
}
29+
id="error"
30+
key="error"
31+
title="The document is not responding"
32+
description="It seems like your document has frozen. Perhaps there is an infinite loop in the code?
33+
Fix any code errors and click Reload to retry."
34+
actions={[
35+
// { content: "Dismiss", onClick: props.onDismiss },
36+
{ content: "Reload", onClick: props.onReload },
37+
]}
38+
/>
39+
);
40+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* The methods the host exposes that are callable by the iframe (from iframesandbox/FrameConnection)
3+
*/
4+
export type HostBridgeMethods = {
5+
/**
6+
* This is used to resolve imported TypeCell modules. When the Javascript evaluated in the import,
7+
* it can require another TypeCell notebook using `import * as nb from "!@username/notebook"`.
8+
*
9+
* The host then needs to fetch this module (!@username/notebook) from TypeCell, compile it, and
10+
* send the compiled javascript back to the iframe. It also keeps watching the TypeCell module for changes
11+
* and sends changes across the bridge.
12+
*/
13+
registerTypeCellModuleCompiler: (moduleName: string) => Promise<void>;
14+
unregisterTypeCellModuleCompiler: (moduleName: string) => Promise<void>;
15+
16+
/**
17+
* Call when the mouse exits the output of a cell
18+
*/
19+
mouseLeave: () => Promise<void>;
20+
21+
/**
22+
* Call when dimensenions of the output of cell `id` have changed.
23+
*/
24+
setDimensions: (
25+
id: string,
26+
dimensions: { width: number; height: number }
27+
) => Promise<void>;
28+
};

packages/editor/src/runtime/executor/executionHosts/sandboxed/ModelForwarder.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,24 @@
11
import { CompiledCodeModel } from "../../../../models/CompiledCodeModel";
22
import { event, lifecycle } from "vscode-lib";
3+
import { IframeBridgeMethods } from "./iframesandbox/IframeBridgeMethods";
34

45
type ModelProvider = {
56
onDidCreateCompiledModel: event.Event<CompiledCodeModel>;
67
compiledModels: CompiledCodeModel[];
78
};
89

9-
export type MessageBridge = {
10-
updateModels: (
11-
bridgeId: string,
12-
models: {
13-
modelId: string;
14-
model: {
15-
value: string;
16-
};
17-
}[]
18-
) => Promise<void>;
19-
updateModel: (
20-
bridgeId: string,
21-
modelId: string,
22-
model: {
23-
value: string;
24-
}
25-
) => Promise<void>;
26-
deleteModel: (bridgeId: string, modelId: string) => Promise<void>;
27-
};
10+
export type MessageBridge = Pick<
11+
IframeBridgeMethods,
12+
"updateModels" | "updateModel" | "deleteModel"
13+
>;
2814

15+
/**
16+
* The ModelForwarder bridges a ModelProvider and the MessageBridge.
17+
*
18+
* It:
19+
* - listens to changes in the ModelProvider...
20+
* - ...and forwards these changes over the MessageBridge
21+
*/
2922
export class ModelForwarder extends lifecycle.Disposable {
3023
private disposed = false;
3124
constructor(

packages/editor/src/runtime/executor/executionHosts/sandboxed/OutputShadow.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ import { runInAction } from "mobx";
22
import { observer } from "mobx-react-lite";
33
import { useEffect, useRef } from "react";
44

5+
/**
6+
* The OutputShadow is a "fake" empty div which the SandboxedExecutionHost renders
7+
* in the position of the cell output.
8+
* However, the actual cell output is rendered by the Iframe (iframe/Frame.tsx).
9+
*
10+
* The dimensions of the OutputShadow are passed in from the iframe (via the bridge),
11+
* so the iframe knows where to render the actual output.
12+
*
13+
* The position of the OutputShadow are passed to the iframe over the bridge (by the host).
14+
*/
515
export const OutputShadow = observer(
616
(props: {
717
dimensions: { width: number; height: number };
@@ -11,6 +21,9 @@ export const OutputShadow = observer(
1121
}) => {
1222
const divRef = useRef<HTMLDivElement>(null);
1323

24+
// Monitor the position of the OutputShadow so we can pass
25+
// updates to the iframe. The iframe then knows at which x, y position
26+
// it needs to render the output
1427
useEffect(() => {
1528
const updatePositions = () => {
1629
if (!divRef.current) {
@@ -28,7 +41,9 @@ export const OutputShadow = observer(
2841
}
2942
});
3043
};
31-
const handle = setInterval(updatePositions, 20); // TODO: replace with MutationObserver?
44+
// We use setInterval to monitor the positions.
45+
// TODO: can we use MutationObserver or something else for this?
46+
const handle = setInterval(updatePositions, 20);
3247
return () => {
3348
clearInterval(handle);
3449
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Iframe sandbox architecture
2+
3+
The actual end-user code that users can enter in TypeCell code cells, gets executed in an iframe that runs on a different domain.
4+
5+
## Why?
6+
7+
End-user code should not be able to access the TypeCell application javascript. Otherwise, it could delete / create / modify Notebooks by the user without the user's permission. Or for example, steal authentication cookies and send them to a third party using `fetch`.
8+
9+
## Architecture
10+
11+
`NotebookRenderer` calls `SandboxedExecutionHost.renderContainer()`. This creates an Iframe where the end-user code is evaluated and the outputs are rendered.
12+
13+
`NotebookRenderer` calls `SandboxedExecutionHost.renderOutput()` whereever the output of cells should be rendered. `SandboxedExecutionHost` renders a so-called `OutputShadow` div in the location. This div is used for two reasons:
14+
15+
- We keep track of it's location (x,y position), so that in the Iframe, we know at which location we need to render the cell output
16+
- We update its dimensions with the actual dimensions of the output. This is done so that the rest of the document flows accordingly (i.e.: other cells / content below the cell are moved down when the output gets larger).
17+
18+
## Bridge
19+
20+
We use PostMessage communication to communicate between the Host and the Iframe. This is done using the [Penpal](https://github.com/Aaronius/penpal) library.
21+
22+
The interfaces are:
23+
24+
- [IframeBridgeMethods](./iframesandbox/IframeBridgeMethods.ts): methods the host can call on the iframe
25+
- [HostBridgeMethods](./HostBridgeMethods.ts): methods the iframe can call on the host
26+
27+
The main data that's being communicated across the bridge:
28+
29+
- The host sends javascript code of the code cells (code models) to the iframe
30+
- The host sends the position of code cell outputs (OutputShadow (x, y) positions) to the iframe
31+
- The iframe sends dimensions of rendered output to the host (so that it can change the dimensions of OutputShadow)
32+
- When the user mouse-outs an Output, the iframe sends a mouseleave event to the host. The host then re-acquires mouse pointer events by setting pointerEvents:none on the iframe.
33+
34+
## Files
35+
36+
- [iframesandbox](./iframesandbox) directory contains the files that are used in the iframe
37+
38+
## Modules
39+
40+
An extra complexity is when client code imports a TypeCell module (e.g.: `import * as nb from "@user/notebook"`). The iframe signals this required module import to the Host, upon which the host starts watching and compiling the notebook. It then sends the compiled javascript code back to the iframe across the bridge.

0 commit comments

Comments
 (0)