From 4bbe67043125bb9dd47ce51345b38082642d9362 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Sat, 27 Oct 2018 14:12:10 -0400 Subject: [PATCH 1/5] Create a cloud provider to create and manage a connection to the web socket. --- src/lib/cloud-provider.js | 104 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/lib/cloud-provider.js diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js new file mode 100644 index 000000000..b2c500287 --- /dev/null +++ b/src/lib/cloud-provider.js @@ -0,0 +1,104 @@ +import log from './log.js'; + + +class CloudProvider { + constructor (cloudHost, vm, user, projectId) { + this.vm = vm; + this.username = user; + this.projectId = projectId; + + // Open a websocket connection to the clouddata server + this.openConnection(cloudHost); + } + + /** + * Open a new websocket connection to the clouddata server. + * @param {string} cloudHost The cloud data server to connect to. + */ + openConnection (cloudHost) { + if (window.WebSocket === null) { + log.warn('Websocket support is not available in this browser'); + this.connection = null; + return; + } + + this.connection = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + cloudHost); + + this.connection.onerror = e => { + log.error(`Websocket connection error: ${JSON.stringify(e)}`); + + // TODO Add re-connection attempt logic here + }; + + this.connection.onmessage = event => { + const messageString = event.data; + log.info(`Received websocket message: ${messageString}`); + const message = JSON.parse(messageString); + if (message.method === 'set') { + const varData = { + projectId: this.projectId, + varUpdate: { + projectId: message.project_id, + name: message.name, + value: message.value + } + }; + this.vm.postIOData('cloud', varData); + } + }; + + this.connection.onopen = () => { + this.writeToServer('handshake'); + log.info(`Successfully connected to clouddata server.`); + }; + + this.connection.onclose = () => { + log.info(`Closed connection to websocket`); + }; + } + + /** + * Format and send a message to the cloud data server. + * @param {string} methodName The message method, indicating the action to perform. + * @param {string} dataName The name of the cloud variable this message pertains to + * @param {string | number} dataValue The value to set the cloud variable to + * @param {number} dataIndex The index of the item to update (for cloud lists) + * @param {string} dataNewName The new name for the cloud variable (if renaming) + */ + writeToServer (methodName, dataName, dataValue, dataIndex, dataNewName) { + const msg = {}; + msg.method = methodName; + msg.user = this.username; + msg.project_id = this.projectId; + + if (dataName) msg.name = dataName; + if (dataValue) msg.value = dataValue; + if (dataIndex) msg.index = dataIndex; + if (dataNewName) msg.new_name = dataNewName; + + const dataToWrite = JSON.stringify(msg); + this.sendCloudData(dataToWrite); + } + + /** + * Send a formatted message to the cloud data server. + * @param {string} data The formatted message to send. + */ + sendCloudData (data) { + this.connection.send(`${data}\n`); + log.info(`Sent message to clouddata server: ${data}`); + } + + /** + * Provides an API for the VM's cloud IO device to update + * a cloud variable on the server. + * @param {string} name The name of the variable to update + * @param {string | number} value The new value for the variable + */ + updateVariable (name, value) { + this.writeToServer('set', name, value); + } + +} + +export default CloudProvider; From 25b8fac357872fbe2fe9dea3432d63bb833f231d Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Sat, 27 Oct 2018 14:13:42 -0400 Subject: [PATCH 2/5] Set up cloud provider and attach it to the VM. Obtain information about cloud host and username from props. --- src/containers/gui.jsx | 2 ++ src/lib/vm-listener-hoc.jsx | 1 - src/lib/vm-manager-hoc.jsx | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index e7766cf7a..db8e8b77b 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -71,6 +71,7 @@ class GUI extends React.Component { const { /* eslint-disable no-unused-vars */ assetHost, + cloudHost, error, hideIntro, isError, @@ -101,6 +102,7 @@ class GUI extends React.Component { GUI.propTypes = { assetHost: PropTypes.string, children: PropTypes.node, + cloudHost: PropTypes.string, error: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), fetchingProject: PropTypes.bool, hideIntro: PropTypes.bool, diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index 5e18cdaf1..d130cfcdd 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -102,7 +102,6 @@ const vmListenerHOC = function (WrappedComponent) { /* eslint-disable no-unused-vars */ attachKeyboardEvents, shouldEmitTargetsUpdate, - username, onBlockDragUpdate, onKeyDown, onKeyUp, diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx index b8e3c2cd0..2d4c9c691 100644 --- a/src/lib/vm-manager-hoc.jsx +++ b/src/lib/vm-manager-hoc.jsx @@ -5,6 +5,7 @@ import {connect} from 'react-redux'; import VM from 'scratch-vm'; import AudioEngine from 'scratch-audio'; +import CloudProvider from '../lib/cloud-provider'; import { LoadingStates, @@ -46,6 +47,18 @@ const vmManagerHOC = function (WrappedComponent) { return this.props.vm.loadProject(this.props.projectData) .then(() => { this.props.onLoadedProject(this.props.loadingState, this.props.canSave); + // If the cloud host exists, open a cloud connection and + // set the vm's cloud provider. + if (this.props.cloudHost) { + // TODO check if we should actually + // connect to cloud data based on info from the loaded project and + // info about the user (e.g. scratcher status) + this.props.vm.setCloudProvider(new CloudProvider( + this.props.cloudHost, + this.props.vm, + this.props.username, + this.props.projectId)); + } }) .catch(e => { this.props.onError(e); @@ -54,12 +67,14 @@ const vmManagerHOC = function (WrappedComponent) { render () { const { /* eslint-disable no-unused-vars */ + cloudHost, fontsLoaded, loadingState, onError: onErrorProp, onLoadedProject: onLoadedProjectProp, projectData, projectId, + userName, /* eslint-enable no-unused-vars */ isLoadingWithId: isLoadingWithIdProp, vm, @@ -77,6 +92,7 @@ const vmManagerHOC = function (WrappedComponent) { VMManager.propTypes = { canSave: PropTypes.bool, + cloudHost: PropTypes.string, fontsLoaded: PropTypes.bool, isLoadingWithId: PropTypes.bool, loadingState: PropTypes.oneOf(LoadingStates), @@ -84,6 +100,7 @@ const vmManagerHOC = function (WrappedComponent) { onLoadedProject: PropTypes.func, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + userName: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired }; From e0a1fdfa3829a7eb2221530002c6b8ec2f5b18be Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Mon, 29 Oct 2018 01:02:09 -0400 Subject: [PATCH 3/5] Add API to close connection and clean up cloud provider. --- src/lib/cloud-provider.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js index b2c500287..0b7c380b1 100644 --- a/src/lib/cloud-provider.js +++ b/src/lib/cloud-provider.js @@ -99,6 +99,26 @@ class CloudProvider { this.writeToServer('set', name, value); } + /** + * Closes the connection to the web socket and clears the cloud + * provider of references related to the cloud data project. + */ + requestCloseConnection () { + this.connection.close(); + this.clear(); + } + + /** + * Clear this provider of references related to the project + * and current state. + */ + clear () { + this.connection = null; + this.vm = null; + this.username = null; + this.projectId = null; + } + } export default CloudProvider; From dc7db4c262414554822b9255ff79b9bd2bde4107 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Mon, 29 Oct 2018 18:43:26 -0400 Subject: [PATCH 4/5] Add docs and make capitalization/naming of props consistent. --- src/lib/cloud-provider.js | 16 ++++++++++++---- src/lib/vm-manager-hoc.jsx | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js index 0b7c380b1..ff3d34c40 100644 --- a/src/lib/cloud-provider.js +++ b/src/lib/cloud-provider.js @@ -2,9 +2,19 @@ import log from './log.js'; class CloudProvider { - constructor (cloudHost, vm, user, projectId) { + /** + * A cloud data provider which creates and manages a web socket connection + * to the Scratch cloud data server. This provider is responsible for + * interfacing with the VM's cloud io device. + * @param {string} cloudHost The url for the cloud data server + * @param {VirtualMachine} vm The Scratch virtual machine to interface with + * @param {string} username The username to associate cloud data updates with + * @param {string} projectId The id associated with the project containing + * cloud data. + */ + constructor (cloudHost, vm, username, projectId) { this.vm = vm; - this.username = user; + this.username = username; this.projectId = projectId; // Open a websocket connection to the clouddata server @@ -36,9 +46,7 @@ class CloudProvider { const message = JSON.parse(messageString); if (message.method === 'set') { const varData = { - projectId: this.projectId, varUpdate: { - projectId: message.project_id, name: message.name, value: message.value } diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx index 2d4c9c691..3d7e9a2e9 100644 --- a/src/lib/vm-manager-hoc.jsx +++ b/src/lib/vm-manager-hoc.jsx @@ -74,7 +74,7 @@ const vmManagerHOC = function (WrappedComponent) { onLoadedProject: onLoadedProjectProp, projectData, projectId, - userName, + username, /* eslint-enable no-unused-vars */ isLoadingWithId: isLoadingWithIdProp, vm, @@ -100,7 +100,7 @@ const vmManagerHOC = function (WrappedComponent) { onLoadedProject: PropTypes.func, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - userName: PropTypes.string, + username: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired }; From 52b3d44eb2a1ab553e1cfcdca1537c4b6a75daa0 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Tue, 30 Oct 2018 13:06:23 -0400 Subject: [PATCH 5/5] Update VM package. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 699d9a263..d9662fff3 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "scratch-render": "0.1.0-prerelease.20181024220305", "scratch-storage": "1.1.0", "scratch-svg-renderer": "0.2.0-prerelease.20181024192149", - "scratch-vm": "0.2.0-prerelease.20181025092837", + "scratch-vm": "0.2.0-prerelease.20181030160328", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0",