Skip to content

Commit cf3d2cf

Browse files
committed
Optionally perform sandbox execution via the WebSocket
1 parent a5d1a94 commit cf3d2cf

File tree

16 files changed

+368
-60
lines changed

16 files changed

+368
-60
lines changed

ui/frontend/actions.ts

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import fetch from 'isomorphic-fetch';
22
import { ThunkAction as ReduxThunkAction } from 'redux-thunk';
33
import url, { UrlObject } from 'url';
4+
import { z } from 'zod';
45

56
import {
67
clippyRequestSelector,
78
formatRequestSelector,
89
getCrateType,
910
isAutoBuildSelector,
1011
runAsTest,
12+
useWebsocketSelector,
1113
} from './selectors';
1214
import State from './state';
1315
import {
@@ -128,7 +130,27 @@ export enum ActionType {
128130
NotificationSeen = 'NOTIFICATION_SEEN',
129131
BrowserWidthChanged = 'BROWSER_WIDTH_CHANGED',
130132
SplitRatioChanged = 'SPLIT_RATIO_CHANGED',
131-
}
133+
WebSocketError = 'WEBSOCKET_ERROR',
134+
WebSocketConnected = 'WEBSOCKET_CONNECTED',
135+
WebSocketDisconnected = 'WEBSOCKET_DISCONNECTED',
136+
WebSocketFeatureFlagEnabled = 'WEBSOCKET_FEATURE_FLAG_ENABLED',
137+
WSExecuteRequest = 'WS_EXECUTE_REQUEST',
138+
WSExecuteResponse = 'WS_EXECUTE_RESPONSE',
139+
}
140+
141+
const ExecuteExtra = z.object({
142+
isAutoBuild: z.boolean(),
143+
});
144+
type ExecuteExtra = z.infer<typeof ExecuteExtra>;
145+
146+
export const WSExecuteResponse = z.object({
147+
type: z.literal(ActionType.WSExecuteResponse),
148+
success: z.boolean(),
149+
stdout: z.string(),
150+
stderr: z.string(),
151+
extra: ExecuteExtra,
152+
});
153+
export type WSExecuteResponse = z.infer<typeof WSExecuteResponse>;
132154

133155
export const initializeApplication = () => createAction(ActionType.InitializeApplication);
134156

@@ -284,18 +306,23 @@ interface ExecuteRequestBody {
284306
}
285307

286308
const performCommonExecute = (crateType: string, tests: boolean): ThunkAction => (dispatch, getState) => {
287-
dispatch(requestExecute());
288-
289309
const state = getState();
290310
const { code, configuration: { channel, mode, edition } } = state;
291311
const backtrace = state.configuration.backtrace === Backtrace.Enabled;
292312
const isAutoBuild = isAutoBuildSelector(state);
293313

294-
const body: ExecuteRequestBody = { channel, mode, edition, crateType, tests, code, backtrace };
295314

296-
return jsonPost<ExecuteResponseBody>(routes.execute, body)
297-
.then(json => dispatch(receiveExecuteSuccess({ ...json, isAutoBuild })))
298-
.catch(json => dispatch(receiveExecuteFailure({ ...json, isAutoBuild })));
315+
if (useWebsocketSelector(state)) {
316+
return dispatch(wsExecuteRequest(channel, mode, edition, crateType, tests, code, backtrace, { isAutoBuild }));
317+
} else {
318+
dispatch(requestExecute());
319+
320+
const body: ExecuteRequestBody = { channel, mode, edition, crateType, tests, code, backtrace };
321+
322+
return jsonPost<ExecuteResponseBody>(routes.execute, body)
323+
.then(json => dispatch(receiveExecuteSuccess({ ...json, isAutoBuild })))
324+
.catch(json => dispatch(receiveExecuteFailure({ ...json, isAutoBuild })));
325+
}
299326
};
300327

301328
function performAutoOnly(): ThunkAction {
@@ -476,6 +503,17 @@ const PRIMARY_ACTIONS: { [index in PrimaryAction]: () => ThunkAction } = {
476503
[PrimaryActionCore.Wasm]: performCompileToNightlyWasmOnly,
477504
};
478505

506+
const wsExecuteRequest = (
507+
channel: Channel,
508+
mode: Mode,
509+
edition: Edition,
510+
crateType: string,
511+
tests: boolean,
512+
code: string,
513+
backtrace: boolean,
514+
extra: ExecuteExtra,
515+
) => createAction(ActionType.WSExecuteRequest, { channel, mode, edition, crateType, tests, code, backtrace, extra });
516+
479517
export const performPrimaryAction = (): ThunkAction => (dispatch, getState) => {
480518
const state = getState();
481519
const primaryAction = PRIMARY_ACTIONS[state.configuration.primaryAction];
@@ -813,6 +851,11 @@ export const browserWidthChanged = (isSmall: boolean) =>
813851
export const splitRatioChanged = () =>
814852
createAction(ActionType.SplitRatioChanged);
815853

854+
export const websocketError = () => createAction(ActionType.WebSocketError);
855+
export const websocketConnected = () => createAction(ActionType.WebSocketConnected);
856+
export const websocketDisconnected = () => createAction(ActionType.WebSocketDisconnected);
857+
export const websocketFeatureFlagEnabled = () => createAction(ActionType.WebSocketFeatureFlagEnabled);
858+
816859
function parseChannel(s?: string): Channel | null {
817860
switch (s) {
818861
case 'stable':
@@ -966,4 +1009,10 @@ export type Action =
9661009
| ReturnType<typeof notificationSeen>
9671010
| ReturnType<typeof browserWidthChanged>
9681011
| ReturnType<typeof splitRatioChanged>
1012+
| ReturnType<typeof websocketError>
1013+
| ReturnType<typeof websocketConnected>
1014+
| ReturnType<typeof websocketDisconnected>
1015+
| ReturnType<typeof websocketFeatureFlagEnabled>
1016+
| ReturnType<typeof wsExecuteRequest>
1017+
| WSExecuteResponse
9691018
;

ui/frontend/configureStore.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import { Action, initializeApplication } from './actions';
88
import initializeLocalStorage from './local_storage';
99
import initializeSessionStorage from './session_storage';
1010
import playgroundApp, { State } from './reducers';
11+
import { websocketMiddleware } from './websocketMiddleware';
1112

1213
export default function configureStore(window: Window) {
1314
const baseUrl = url.resolve(window.location.href, '/');
15+
const websocket = websocketMiddleware(window);
1416

1517
const initialGlobalState = {
1618
globalConfiguration: {
@@ -29,7 +31,7 @@ export default function configureStore(window: Window) {
2931
sessionStorage.initialState,
3032
);
3133

32-
const middlewares = applyMiddleware<ThunkDispatch<State, {}, Action>, {}>(thunk);
34+
const middlewares = applyMiddleware<ThunkDispatch<State, {}, Action>, {}>(thunk, websocket);
3335
const composeEnhancers: typeof compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
3436
const enhancers = composeEnhancers(middlewares);
3537
const store = createStore(playgroundApp, initialState, enhancers);

ui/frontend/declarations.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,5 @@ interface Window {
2121
rustPlayground: {
2222
setCode(code: string): void;
2323
disableSyncChangesToStorage(): void;
24-
webSocket: WebSocket | null;
2524
};
2625
}

ui/frontend/index.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,21 @@ import {
1919
performVersionsLoad,
2020
reExecuteWithBacktrace,
2121
browserWidthChanged,
22+
websocketFeatureFlagEnabled,
2223
} from './actions';
2324
import { configureRustErrors } from './highlighting';
2425
import PageSwitcher from './PageSwitcher';
2526
import playgroundApp from './reducers';
2627
import Router from './Router';
2728
import configureStore from './configureStore';
28-
import openWebSocket from './websocket';
29-
30-
// openWebSocket() may return null.
31-
const socket = openWebSocket(window.location);
3229

3330
const store = configureStore(window);
3431

32+
const params = new URLSearchParams(window.location.search);
33+
if (params.has('websocket')) {
34+
store.dispatch(websocketFeatureFlagEnabled());
35+
}
36+
3537
const z = (evt: MediaQueryList | MediaQueryListEvent) => { store.dispatch(browserWidthChanged(evt.matches)); };
3638

3739
const maxWidthMediaQuery = window.matchMedia('(max-width: 1600px)');
@@ -57,9 +59,6 @@ window.rustPlayground = {
5759
disableSyncChangesToStorage: () => {
5860
store.dispatch(disableSyncChangesToStorage());
5961
},
60-
// Temporarily storing this as a global to prevent it from being
61-
// garbage collected (at least by Safari).
62-
webSocket: socket,
6362
};
6463

6564
const container = document.getElementById('playground');

ui/frontend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"reselect": "^4.0.0",
3030
"route-parser": "^0.0.5",
3131
"split-grid": "^1.0.9",
32-
"url": "^0.11.0"
32+
"url": "^0.11.0",
33+
"zod": "^3.20.3"
3334
},
3435
"devDependencies": {
3536
"@babel/core": "^7.0.0",

ui/frontend/reducers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import page from './page';
1111
import position from './position';
1212
import selection from './selection';
1313
import versions from './versions';
14+
import websocket from './websocket';
1415

1516
const playgroundApp = combineReducers({
1617
browser,
@@ -24,6 +25,7 @@ const playgroundApp = combineReducers({
2425
position,
2526
selection,
2627
versions,
28+
websocket,
2729
});
2830

2931
export type State = ReturnType<typeof playgroundApp>;

ui/frontend/reducers/output/execute.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface State {
1717
export default function execute(state = DEFAULT, action: Action) {
1818
switch (action.type) {
1919
case ActionType.ExecuteRequest:
20+
case ActionType.WSExecuteRequest:
2021
return start(DEFAULT, state);
2122
case ActionType.ExecuteSucceeded: {
2223
const { stdout = '', stderr = '', isAutoBuild } = action;
@@ -26,6 +27,10 @@ export default function execute(state = DEFAULT, action: Action) {
2627
const { error, isAutoBuild } = action;
2728
return finish(state, { error, isAutoBuild });
2829
}
30+
case ActionType.WSExecuteResponse: {
31+
const { stdout, stderr, extra: { isAutoBuild } } = action;
32+
return finish(state, { stdout, stderr, isAutoBuild });
33+
}
2934
default:
3035
return state;
3136
}

ui/frontend/reducers/output/meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export default function meta(state = DEFAULT, action: Action) {
3838
return { ...state, focus: Focus.Asm };
3939

4040
case ActionType.ExecuteRequest:
41+
case ActionType.WSExecuteRequest:
4142
return { ...state, focus: Focus.Execute };
4243

4344
case ActionType.RequestFormat:

ui/frontend/reducers/websocket.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Action, ActionType } from '../actions';
2+
3+
export type State = {
4+
connected: boolean;
5+
featureFlagEnabled: boolean;
6+
};
7+
8+
const DEFAULT: State = {
9+
connected: false,
10+
featureFlagEnabled: false,
11+
};
12+
13+
export default function websocket(state = DEFAULT, action: Action): State {
14+
switch (action.type) {
15+
case ActionType.WebSocketConnected:
16+
return { ...state, connected: true };
17+
18+
case ActionType.WebSocketDisconnected:
19+
return { ...state, connected: false };
20+
21+
case ActionType.WebSocketError:
22+
return { ...state };
23+
24+
case ActionType.WebSocketFeatureFlagEnabled:
25+
return { ...state, featureFlagEnabled: true };
26+
27+
default:
28+
return state;
29+
}
30+
}

ui/frontend/selectors/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,12 @@ export const offerCrateAutocompleteOnUse = createSelector(
334334
editionSelector,
335335
(edition) => edition !== Edition.Rust2015,
336336
);
337+
338+
const websocket = (state: State) => state.websocket;
339+
340+
export const websocketFeatureFlagEnabled = createSelector(websocket, (ws) => ws.featureFlagEnabled);
341+
342+
export const useWebsocketSelector = createSelector(
343+
websocket,
344+
(ws) => ws.connected && ws.featureFlagEnabled,
345+
);

0 commit comments

Comments
 (0)