From ce1c8360a5b7fb68d7f0d78e60ebade57d3f0e56 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 4 May 2020 13:32:28 -0500 Subject: [PATCH 01/37] Add event emitter utility --- src/common/EventEmitter.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/common/EventEmitter.js diff --git a/src/common/EventEmitter.js b/src/common/EventEmitter.js new file mode 100644 index 000000000..29d48460e --- /dev/null +++ b/src/common/EventEmitter.js @@ -0,0 +1,26 @@ +/* globals define */ +define([ +], function( +) { + class EventEmitter { + constructor() { + this._handlers = {}; + } + + on(event, fn) { + if (!this._handlers[event]) { + this._handlers[event] = []; + } + this._handlers[event].push(fn); + } + + emit(event) { + const handlers = this._handlers[event] || []; + const args = Array.prototype.slice.call(arguments, 1); + handlers.forEach(fn => fn.apply(null, args)); + } + // TODO: Can I make this an official stream in node? + } + + return EventEmitter; +}); From aab7f469389a3a7f29b78ec9fb4b19f0caba637a Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 4 May 2020 13:32:54 -0500 Subject: [PATCH 02/37] WIP Add basic interactive compute support --- src/common/compute/interactive/message.js | 45 +++++++++ src/common/compute/interactive/session.js | 96 +++++++++++++++++++ src/common/compute/interactive/task.js | 44 +++++++++ src/routers/InteractiveCompute/Channel.js | 23 +++++ .../InteractiveCompute/InteractiveCompute.js | 93 ++++++++++++++++++ src/routers/InteractiveCompute/Session.js | 43 +++++++++ .../InteractiveCompute/job-files/index.js | 34 +++++++ .../job-files/start-session.js | 19 ++++ 8 files changed, 397 insertions(+) create mode 100644 src/common/compute/interactive/message.js create mode 100644 src/common/compute/interactive/session.js create mode 100644 src/common/compute/interactive/task.js create mode 100644 src/routers/InteractiveCompute/Channel.js create mode 100644 src/routers/InteractiveCompute/InteractiveCompute.js create mode 100644 src/routers/InteractiveCompute/Session.js create mode 100644 src/routers/InteractiveCompute/job-files/index.js create mode 100644 src/routers/InteractiveCompute/job-files/start-session.js diff --git a/src/common/compute/interactive/message.js b/src/common/compute/interactive/message.js new file mode 100644 index 000000000..349edc193 --- /dev/null +++ b/src/common/compute/interactive/message.js @@ -0,0 +1,45 @@ +/* globals define */ +(function(root, factory){ + if(typeof define === 'function' && define.amd) { + define([], function(){ + return (root.utils = factory()); + }); + } else if(typeof module === 'object' && module.exports) { + module.exports = (root.utils = factory()); + } +}(this, function() { + const Constants = makeEnum('STDOUT', 'STDERR', 'CLOSE'); + + function makeEnum() { + const names = Array.prototype.slice.call(arguments); + const obj = {}; + names.forEach((name, i) => obj[name] = i); + return obj; + } + + class Message { + constructor(type, data) { + this.type = type; + this.data = data; + } + + static decode(serialized) { + const {type, data} = JSON.parse(serialized); + return new Message(type, data); + } + + static encode(type, data) { + //const buffer = Buffer.allocUnsafe(1); + //buffer.writeUInt8(type, 0); + //if (!data.isBuffer) { + //data = Buffer.from(data); + //} + //return buffer.concat(data); + return JSON.stringify({type, data: data.toString()}); + } + } + Object.assign(Message, Constants); + Message.Constants = Constants; + + return Message; +})); diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js new file mode 100644 index 000000000..1cbd0b250 --- /dev/null +++ b/src/common/compute/interactive/session.js @@ -0,0 +1,96 @@ +/* globals define */ +define([ + 'deepforge/utils', + 'deepforge/compute/interactive/task', + 'deepforge/compute/interactive/message', +], function( + utils, + Task, + Message, +) { + const {defer} = utils; + class InteractiveSession { + constructor(computeID, config) { + this.currentTask = null; + // TODO: Get the server address... + // TODO: detect if ssl + const address = 'ws://localhost:8889'; + this.ws = new WebSocket(address); + this.connected = defer(); + this.ws.onopen = () => { + this.ws.send(JSON.stringify([computeID, config])); + this.checkReady(); + this.connected.resolve(); + }; + + this.ready = null; + } + + checkReady() { + if (this.isIdle() && this.ready) { + this.ready.resolve(); + } + } + + isIdle() { + return !this.currentTask && this.ws.readyState === WebSocket.OPEN; + } + + ensureIdle(action) { + if (!this.isIdle()) { + throw new Error(`Cannot ${action} when not idle.`); + } + } + + spawn(cmd) { + this.ensureIdle('spawn a task'); + + const task = new Task(this.ws, cmd); + this.runTask(task); + return task; + } + + async runTask(task) { + this.ensureIdle('spawn a task'); + + this.currentTask = task; + await task.run(); + this.currentTask = null; + this.checkReady(); + } + + async whenConnected() { + return this.connected.promise; + } + + async whenReady() { + this.ready = this.ready || defer(); + return this.ready.promise; + } + + async exec(cmd) { + this.ensureIdle('exec a task'); + const task = new Task(this.ws, cmd); + const result = { + stdout: '', + stderr: '', + exitCode: 0 + }; + task.on(Message.STDOUT, data => result.stdout += data.toString()); + task.on(Message.STDERR, data => result.stderr += data.toString()); + task.on(Message.CLOSE, code => result.exitCode = code); + await this.runTask(task); + return result; + } + + static async new(computeID, config={}) { + const session = new InteractiveSession(computeID, config); + await session.whenConnected(); + return session; + } + } + + Object.assign(InteractiveSession, Message.Constants); + + return InteractiveSession; +}); diff --git a/src/common/compute/interactive/task.js b/src/common/compute/interactive/task.js new file mode 100644 index 000000000..509062fc2 --- /dev/null +++ b/src/common/compute/interactive/task.js @@ -0,0 +1,44 @@ +/* globals define */ +define([ + 'deepforge/EventEmitter', + 'deepforge/compute/interactive/message', + 'deepforge/utils', +], function( + EventEmitter, + Message, + utils, +) { + + class Task extends EventEmitter { + constructor(ws, cmd) { + super(); + this.ws = ws; + this.cmd = cmd; + } + + async run() { + const deferred = utils.defer(); + + this.ws.send(this.cmd); + this.ws.onmessage = async wsMsg => { + const data = wsMsg.data instanceof Blob ? + await wsMsg.data.text() : wsMsg.data; + + const msg = Message.decode(data); + this.emitMessage(msg); + if (msg.type === Message.CLOSE) { + this.ws.onmessage = null; + deferred.resolve(); + } + }; + + return deferred.promise; + } + + emitMessage(msg) { + this.emit(msg.type, msg.data); + } + } + + return Task; +}); diff --git a/src/routers/InteractiveCompute/Channel.js b/src/routers/InteractiveCompute/Channel.js new file mode 100644 index 000000000..d7151bd8b --- /dev/null +++ b/src/routers/InteractiveCompute/Channel.js @@ -0,0 +1,23 @@ +/* globals requireJS */ +const EventEmitter = requireJS('deepforge/EventEmitter'); + +class Channel extends EventEmitter { + constructor(ws1, ws2) { + super(); + this.ws1 = ws1; + this.ws2 = ws2; + this.ws1.on('message', data => this.ws2.send(data)); + this.ws2.on('message', data => this.ws1.send(data)); + this.ws1.onclose = + this.ws2.onclose = () => this.close(); + } + + async close () { + this.ws1.close(); + this.ws2.close(); + this.emit('close'); + } +} + +Channel.CLOSE = 'close'; +module.exports = Channel; diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js new file mode 100644 index 000000000..40127ab00 --- /dev/null +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -0,0 +1,93 @@ +/* globals requireJS */ +'use strict'; + +const WebSocket = require('ws'); +const router = require('express').Router(); +const Compute = requireJS('deepforge/compute/index'); +const BlobClient = requireJS('blob/BlobClient'); +const InteractiveSession = require('./Session'); + +class ComputeBroker { + constructor(logger, blobClient) { + this.logger = logger.fork('broker'); + this.initSessions = []; + this.clientServer = null; + this.workerServer = null; + this.blobClient = blobClient; + } + + listen (port) { + // TODO: Can I piggyback off the webgme server? Maybe using a different path? + + this.clientServer = new WebSocket.Server({port}); // FIXME: this might be tricky on the current deployment + this.workerServer = new WebSocket.Server({port: port + 1}); + + this.clientServer.on('connection', ws => { + ws.once('message', data => { + // TODO: Create the worker job + try { + const [id, config] = JSON.parse(data); + const backend = Compute.getBackend(id); + const client = backend.getClient(this.logger, this.blobClient, config); + const session = new InteractiveSession(this.blobClient, client, ws); + this.initSessions.push(session); + } catch (err) { + this.logger.warn(`Error creating session: ${err}`); + } + }); + }); + + this.workerServer.on('connection', ws => { + // TODO: Could I connect to a different path? + ws.once('message', data => { + console.log('connecting worker with client...'); + const id = data.toString(); + const index = this.initSessions.findIndex(session => session.id === id); + if (index > -1) { + const [session] = this.initSessions.splice(index, 1); + session.setWorkerWebSocket(ws); + } else { + console.error(`Session not found for ${id}`); + } + }); + }); + } +} + +// TODO: Create the interactive compute broker +function initialize(middlewareOpts) { + const logger = middlewareOpts.logger.fork('InteractiveCompute'); + + const {gmeConfig} = middlewareOpts; + // TODO: Do I need to add authorization for the blob client? + const blobClient = new BlobClient({ + logger: logger, + serverPort: gmeConfig.server.port, + server: '127.0.0.1', + httpsecure: false, + }); + const broker = new ComputeBroker(logger, blobClient); + broker.listen(gmeConfig.server.port + 1); + logger.debug('initializing ...'); + + // TODO: additional auth required? + // TODO: connect with websockets... + + logger.debug('ready'); +} + +function start(callback) { + callback(); +} + +function stop(callback) { + callback(); +} + + +module.exports = { + initialize: initialize, + router: router, + start: start, + stop: stop +}; diff --git a/src/routers/InteractiveCompute/Session.js b/src/routers/InteractiveCompute/Session.js new file mode 100644 index 000000000..3e8d1ef0d --- /dev/null +++ b/src/routers/InteractiveCompute/Session.js @@ -0,0 +1,43 @@ +/* globals requireJS */ +const JobFiles = require('./job-files'); +const chance = require('chance')(); +const config = require('../../../config'); +const SERVER_URL = `http://localhost:${config.server.port + 2}`; // FIXME +const Channel = require('./Channel'); +const EventEmitter = requireJS('deepforge/EventEmitter'); + +class Session extends EventEmitter { + constructor(blobClient, compute, clientSocket) { + super(); + this.compute = compute; + this.clientSocket = clientSocket; + this.id = chance.guid(); + this.wsChannel = null; + this.initialize(blobClient); + } + + setWorkerWebSocket(socket) { + this.workerSocket = socket; + this.emit('connected'); + + this.queuedMsgs.forEach(msg => this.workerSocket.send(msg)); + this.wsChannel = new Channel(this.clientSocket, this.workerSocket); + this.wsChannel.on(Channel.CLOSE, () => this.close()); + } + + async initialize(blobClient) { + this.queuedMsgs = []; + this.clientSocket.on('message', data => this.queuedMsgs.push(data)); + + const files = new JobFiles(blobClient, SERVER_URL, this.id); + const hash = await files.upload(); + this.jobInfo = this.compute.createJob(hash); + this.compute.on('data', (id, data) => console.log('-->', data.toString())); + } + + async close () { + await this.compute.cancelJob(await this.jobInfo); + } +} + +module.exports = Session; diff --git a/src/routers/InteractiveCompute/job-files/index.js b/src/routers/InteractiveCompute/job-files/index.js new file mode 100644 index 000000000..e1a65cf73 --- /dev/null +++ b/src/routers/InteractiveCompute/job-files/index.js @@ -0,0 +1,34 @@ +/* globals requireJS */ +const Files = requireJS('deepforge/plugin/GeneratedFiles'); +const fs = require('fs'); +const path = require('path'); +const START_SESSION = fs.readFileSync(path.join(__dirname, 'start-session.js'), 'utf8'); +const interactiveDir = path.join(__dirname, '..', '..', '..', 'common', 'compute', 'interactive'); +const MESSAGE = fs.readFileSync(path.join(interactiveDir, 'message.js'), 'utf8'); + +class StartSessionFiles extends Files { + constructor(blobClient, url, id) { + super(blobClient); + this.id = id; + this.createFiles(url, id); + } + + createFiles(url, id) { + const config = JSON.stringify({ + cmd: 'node', + args: ['start-session.js', url, id], + outputInterval: -1, + resultArtifacts: [] + }, null, 2); + this.addFile('executor_config.json', config); + this.addFile('start-session.js', START_SESSION); + this.addFile('message.js', MESSAGE); + } + + async upload() { + const name = `interactive-session-init-${this.id}`; + return await this.save(name); + } +} + +module.exports = StartSessionFiles; diff --git a/src/routers/InteractiveCompute/job-files/start-session.js b/src/routers/InteractiveCompute/job-files/start-session.js new file mode 100644 index 000000000..d25fb6cef --- /dev/null +++ b/src/routers/InteractiveCompute/job-files/start-session.js @@ -0,0 +1,19 @@ +const Message = require('./message'); +const {spawn} = require('child_process'); +const [, , SERVER_URL, ID] = process.argv; +const WebSocket = require('ws'); + +const ws = new WebSocket(SERVER_URL); +ws.on('open', () => ws.send(ID)); + +// TODO: Should this run in a single subprocess or in many? +// For now, let's run it in a single subprocess... +ws.on('message', function(data) { + // TODO: Run the command and send the results back + // TODO: Queue the commands here? + const [cmd, ...opts] = data.split(' '); + const subprocess = spawn(cmd, opts); + subprocess.stdout.on('data', data => ws.send(Message.encode(Message.STDOUT, data))); + subprocess.stderr.on('data', data => ws.send(Message.encode(Message.STDERR, data))); + subprocess.on('close', code => ws.send(Message.encode(Message.CLOSE, code))); +}); From e305aeab3c4bba181253557463511de994c1bccf Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 4 May 2020 14:19:57 -0500 Subject: [PATCH 03/37] WIP interactive compute. Add dependencies and router --- config/config.webgme.js | 5 ++ package-lock.json | 82 +++++++++++++++---- package.json | 4 +- .../InteractiveCompute/InteractiveCompute.js | 4 +- 4 files changed, 77 insertions(+), 18 deletions(-) diff --git a/config/config.webgme.js b/config/config.webgme.js index 50eae288b..07aa81844 100644 --- a/config/config.webgme.js +++ b/config/config.webgme.js @@ -51,6 +51,11 @@ config.rest.components['SciServerAuth'] = { mount: 'routers/SciServerAuth', options: {} }; +config.rest.components['InteractiveCompute'] = { + src: __dirname + '/../src/routers/InteractiveCompute/InteractiveCompute.js', + mount: 'routers/InteractiveCompute', + options: {} +}; // Visualizer descriptors config.visualization.visualizerDescriptors.push(__dirname + '/../src/visualizers/Visualizers.json'); diff --git a/package-lock.json b/package-lock.json index 535601c2f..3606da0d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1270,9 +1270,9 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" }, "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, "asynckit": { "version": "0.4.0", @@ -2026,9 +2026,9 @@ } }, "chance": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/chance/-/chance-1.0.12.tgz", - "integrity": "sha512-W99uMuboG5CT1iToDmizEH6yQYqICzZnrSRbbXPuJErzFWLPaoiEDvwnKbESjDo/8st1n3pyh70VBMmfqPmf+Q==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.4.tgz", + "integrity": "sha512-pXPDSu3knKlb6H7ahQfpq//J9mSOxYK8SMtp8MV/nRJh8aLRDIl0ipLH8At8+nVogVwtvPZzyIzY/EbcY/cLuQ==" }, "change-case": { "version": "2.3.1", @@ -3269,6 +3269,16 @@ "requires": { "ms": "2.0.0" } + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } } } }, @@ -3297,6 +3307,16 @@ "requires": { "ms": "2.0.0" } + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } } } }, @@ -12076,6 +12096,23 @@ "debug": "~3.1.0", "engine.io-parser": "~2.1.0", "ws": "~3.3.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + } } }, "engine.io-client": { @@ -12094,6 +12131,23 @@ "ws": "~3.3.1", "xmlhttprequest-ssl": "~1.5.4", "yeast": "0.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + } } }, "es6-promise": { @@ -14849,6 +14903,11 @@ "xmlbuilder": "4.2.1" } }, + "chance": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.0.12.tgz", + "integrity": "sha512-W99uMuboG5CT1iToDmizEH6yQYqICzZnrSRbbXPuJErzFWLPaoiEDvwnKbESjDo/8st1n3pyh70VBMmfqPmf+Q==" + }, "commander": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", @@ -15285,14 +15344,9 @@ "dev": true }, "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.5.tgz", + "integrity": "sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA==" }, "xml2js": { "version": "0.4.17", diff --git a/package.json b/package.json index 7b0320bea..f41d7e3ce 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@babel/preset-env": "^7.7.1", "@babel/runtime": "^7.7.2", "aws-sdk": "^2.616.0", + "chance": "^1.1.4", "commander": "^2.20.3", "deepforge-user-management-page": "github:deepforge-dev/user-management-page", "dotenv": "^2.0.0", @@ -53,7 +54,8 @@ "webgme-cli": "^2.7.3", "webgme-easydag": "github:dfst/webgme-easydag", "webgme-fab": "github:dfst/webgme-fab", - "webgme-simple-nodes": "^2.1.3" + "webgme-simple-nodes": "^2.1.3", + "ws": "^7.2.5" }, "devDependencies": { "chai": "^3.0.0", diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index 40127ab00..0c022c190 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -48,13 +48,13 @@ class ComputeBroker { session.setWorkerWebSocket(ws); } else { console.error(`Session not found for ${id}`); + ws.close(); } }); }); } } -// TODO: Create the interactive compute broker function initialize(middlewareOpts) { const logger = middlewareOpts.logger.fork('InteractiveCompute'); @@ -71,8 +71,6 @@ function initialize(middlewareOpts) { logger.debug('initializing ...'); // TODO: additional auth required? - // TODO: connect with websockets... - logger.debug('ready'); } From 27a18eb4fdca098a71dd62768cf2c21cee826ecd Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 4 May 2020 15:13:50 -0500 Subject: [PATCH 04/37] misc code cleanup --- src/routers/InteractiveCompute/InteractiveCompute.js | 3 --- src/routers/InteractiveCompute/job-files/start-session.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index 0c022c190..9e22f9386 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -24,7 +24,6 @@ class ComputeBroker { this.clientServer.on('connection', ws => { ws.once('message', data => { - // TODO: Create the worker job try { const [id, config] = JSON.parse(data); const backend = Compute.getBackend(id); @@ -38,9 +37,7 @@ class ComputeBroker { }); this.workerServer.on('connection', ws => { - // TODO: Could I connect to a different path? ws.once('message', data => { - console.log('connecting worker with client...'); const id = data.toString(); const index = this.initSessions.findIndex(session => session.id === id); if (index > -1) { diff --git a/src/routers/InteractiveCompute/job-files/start-session.js b/src/routers/InteractiveCompute/job-files/start-session.js index d25fb6cef..4e45727ba 100644 --- a/src/routers/InteractiveCompute/job-files/start-session.js +++ b/src/routers/InteractiveCompute/job-files/start-session.js @@ -13,7 +13,7 @@ ws.on('message', function(data) { // TODO: Queue the commands here? const [cmd, ...opts] = data.split(' '); const subprocess = spawn(cmd, opts); + subprocess.on('close', code => ws.send(Message.encode(Message.CLOSE, code))); subprocess.stdout.on('data', data => ws.send(Message.encode(Message.STDOUT, data))); subprocess.stderr.on('data', data => ws.send(Message.encode(Message.STDERR, data))); - subprocess.on('close', code => ws.send(Message.encode(Message.CLOSE, code))); }); From c9168bfd75e672cf4fa6bb36e933b8b0b1cf7179 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Tue, 5 May 2020 07:20:20 -0500 Subject: [PATCH 05/37] Combine COMPLETE/CLOSE messages --- src/common/compute/interactive/message.js | 3 +- src/common/compute/interactive/session.js | 9 ++++- src/common/compute/interactive/task.js | 10 +++-- .../job-files/start-session.js | 37 ++++++++++++++++--- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/common/compute/interactive/message.js b/src/common/compute/interactive/message.js index 349edc193..ec8782447 100644 --- a/src/common/compute/interactive/message.js +++ b/src/common/compute/interactive/message.js @@ -8,7 +8,8 @@ module.exports = (root.utils = factory()); } }(this, function() { - const Constants = makeEnum('STDOUT', 'STDERR', 'CLOSE'); + const Constants = makeEnum('STDOUT', 'STDERR', 'RUN', 'ADD_ARTIFACT', + 'ADD_FILE', 'ADD_USER_DATA', 'COMPLETE'); function makeEnum() { const names = Array.prototype.slice.call(arguments); diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index 1cbd0b250..0a94c1c7c 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -78,11 +78,18 @@ define([ }; task.on(Message.STDOUT, data => result.stdout += data.toString()); task.on(Message.STDERR, data => result.stderr += data.toString()); - task.on(Message.CLOSE, code => result.exitCode = code); + task.on(Message.COMPLETE, code => result.exitCode = code); await this.runTask(task); return result; } + async addArtifact(name, dataInfo, type) { + this.ensureIdle('add artifact'); + const msg = new Message(Message.ADD_ARTIFACT, [name, dataInfo, type]); + const task = new Task(this.ws, msg); + await this.runTask(task); + } + static async new(computeID, config={}) { const session = new InteractiveSession(computeID, config); await session.whenConnected(); diff --git a/src/common/compute/interactive/task.js b/src/common/compute/interactive/task.js index 509062fc2..29407c43d 100644 --- a/src/common/compute/interactive/task.js +++ b/src/common/compute/interactive/task.js @@ -10,24 +10,26 @@ define([ ) { class Task extends EventEmitter { - constructor(ws, cmd) { + constructor(ws, msg) { super(); this.ws = ws; - this.cmd = cmd; + this.msg = msg; } async run() { const deferred = utils.defer(); - this.ws.send(this.cmd); + this.ws.send(this.msg.encode()); this.ws.onmessage = async wsMsg => { const data = wsMsg.data instanceof Blob ? await wsMsg.data.text() : wsMsg.data; const msg = Message.decode(data); + console.log('received msg:', msg); this.emitMessage(msg); - if (msg.type === Message.CLOSE) { + if (msg.type === Message.COMPLETE) { this.ws.onmessage = null; + console.log('resolving', this.msg); deferred.resolve(); } }; diff --git a/src/routers/InteractiveCompute/job-files/start-session.js b/src/routers/InteractiveCompute/job-files/start-session.js index 4e45727ba..f7fd268cf 100644 --- a/src/routers/InteractiveCompute/job-files/start-session.js +++ b/src/routers/InteractiveCompute/job-files/start-session.js @@ -2,18 +2,43 @@ const Message = require('./message'); const {spawn} = require('child_process'); const [, , SERVER_URL, ID] = process.argv; const WebSocket = require('ws'); +const fs = require('fs').promises; +const path = require('path'); const ws = new WebSocket(SERVER_URL); ws.on('open', () => ws.send(ID)); // TODO: Should this run in a single subprocess or in many? // For now, let's run it in a single subprocess... -ws.on('message', function(data) { +ws.on('message', async function(data) { // TODO: Run the command and send the results back // TODO: Queue the commands here? - const [cmd, ...opts] = data.split(' '); - const subprocess = spawn(cmd, opts); - subprocess.on('close', code => ws.send(Message.encode(Message.CLOSE, code))); - subprocess.stdout.on('data', data => ws.send(Message.encode(Message.STDOUT, data))); - subprocess.stderr.on('data', data => ws.send(Message.encode(Message.STDERR, data))); + const msg = Message.decode(data); + if (msg.type === Message.RUN) { + const [cmd, ...opts] = msg.data.split(' '); + const subprocess = spawn(cmd, opts); + subprocess.on('close', code => ws.send(Message.encode(Message.COMPLETE, code))); + subprocess.stdout.on('data', data => ws.send(Message.encode(Message.STDOUT, data))); + subprocess.stderr.on('data', data => ws.send(Message.encode(Message.STDERR, data))); + } else if (msg.type === Message.ADD_ARTIFACT) { + console.log('adding artifact...'); + const [name, dataInfo, type] = msg.data; + console.log(name, dataInfo, type); + console.log(msg); + await mkdirp(['artifacts', name]); + // TODO: make artifacts/ directory if needed? + } }); + +async function mkdirp() { + const dirs = Array.prototype.slice.call(arguments); + for (let i = 0; i < dirs.length; i++) { + try { + await fs.mkdir(dirs[i]); + } catch (err) { + if (err.code !== 'EEXIST') { + throw err; + } + } + } +} From 8c6768a070a08dbe412a4731cb3da931308f79e7 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Tue, 5 May 2020 09:23:32 -0500 Subject: [PATCH 06/37] Refactor tasks in interactive compute --- src/common/compute/interactive/session.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index 0a94c1c7c..5a7748f23 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -45,7 +45,8 @@ define([ spawn(cmd) { this.ensureIdle('spawn a task'); - const task = new Task(this.ws, cmd); + const msg = new Message(Message.RUN, cmd); + const task = new Task(this.ws, msg); this.runTask(task); return task; } @@ -70,7 +71,8 @@ define([ async exec(cmd) { this.ensureIdle('exec a task'); - const task = new Task(this.ws, cmd); + const msg = new Message(Message.RUN, cmd); + const task = new Task(this.ws, msg); const result = { stdout: '', stderr: '', @@ -83,9 +85,9 @@ define([ return result; } - async addArtifact(name, dataInfo, type) { + async addArtifact(name, dataInfo, type, auth) { this.ensureIdle('add artifact'); - const msg = new Message(Message.ADD_ARTIFACT, [name, dataInfo, type]); + const msg = new Message(Message.ADD_ARTIFACT, [name, dataInfo, type, auth]); const task = new Task(this.ws, msg); await this.runTask(task); } From bef1cf57fdf98fbea2c4e24962d61c7fb80507a3 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Tue, 5 May 2020 10:01:56 -0500 Subject: [PATCH 07/37] WIP refactor compute backend, client, local compute --- src/common/compute/backends/ComputeBackend.js | 58 ++-- src/common/compute/backends/ComputeClient.js | 87 ++--- src/common/compute/backends/local/Client.js | 328 +++++++++--------- 3 files changed, 237 insertions(+), 236 deletions(-) diff --git a/src/common/compute/backends/ComputeBackend.js b/src/common/compute/backends/ComputeBackend.js index 3815f2335..7cf202e46 100644 --- a/src/common/compute/backends/ComputeBackend.js +++ b/src/common/compute/backends/ComputeBackend.js @@ -1,43 +1,41 @@ /* globals define, requirejs */ define([ - 'module', - 'q', ], function( - module, - Q, ) { - const ComputeBackend = function(id, metadata) { - const {name, dashboard, client} = metadata; - this.id = id; - this.name = name; - this.dashboardPath = dashboard; - this.clientPath = client || './Client'; - }; - - ComputeBackend.prototype.getClient = function(logger, blobClient, config) { - if (require.isBrowser) { - throw new Error('Compute clients cannot be loaded in the browser.'); + class ComputeBackend { + constructor (id, metadata) { + const {name, dashboard, client} = metadata; + this.id = id; + this.name = name; + this.dashboardPath = dashboard; + this.clientPath = client || './Client'; } - const Client = requirejs(`deepforge/compute/backends/${this.id}/${this.clientPath}`); - return new Client(logger, blobClient, config); - }; + getClient (logger, blobClient, config) { + if (require.isBrowser) { + throw new Error('Compute clients cannot be loaded in the browser.'); + } + + const Client = requirejs(`deepforge/compute/backends/${this.id}/${this.clientPath}`); + return new Client(logger, blobClient, config); + } - ComputeBackend.prototype.getDashboard = async function() { - if (this.dashboardPath) { - const absPath = `deepforge/compute/backends/${this.id}/${this.dashboardPath}`; - return await this.require(absPath); - } else { - return null; + async getDashboard () { + if (this.dashboardPath) { + const absPath = `deepforge/compute/backends/${this.id}/${this.dashboardPath}`; + return await this.require(absPath); + } else { + return null; + } } - }; - ComputeBackend.prototype.require = function(path) { // helper for loading async - const deferred = Q.defer(); - require([path], deferred.resolve, deferred.reject); - return deferred.promise; - }; + require (path) { // helper for loading async + return new Promise((resolve, reject) => + require([path], resolve, reject) + ); + } + } return ComputeBackend; }); diff --git a/src/common/compute/backends/ComputeClient.js b/src/common/compute/backends/ComputeClient.js index efcbf90b7..c1abe2300 100644 --- a/src/common/compute/backends/ComputeClient.js +++ b/src/common/compute/backends/ComputeClient.js @@ -1,47 +1,50 @@ /* globals define */ define([], function() { - const ComputeClient = function(logger, blobClient) { - this.logger = logger.fork('compute'); - this.blobClient = blobClient; - this._events = {}; - }; - - ComputeClient.prototype.cancelJob = function(/*job*/) { - unimplemented(this.logger, 'cancelJob'); - }; - - ComputeClient.prototype.createJob = async function(/*hash*/) { - unimplemented(this.logger, 'createJob'); - }; - - ComputeClient.prototype.getStatus = async function(/*jobInfo*/) { - unimplemented(this.logger, 'getStatus'); - }; - - ComputeClient.prototype.getResultsInfo = async function(/*jobInfo*/) { - unimplemented(this.logger, 'getResultsInfo'); - }; - - ComputeClient.prototype.getConsoleOutput = async function(/*hash*/) { - unimplemented(this.logger, 'getConsoleOutput'); - }; - - ComputeClient.prototype.isFinishedStatus = function(status) { - const notFinishedStatuses = [this.QUEUED, this.PENDING, this.RUNNING]; - return !notFinishedStatuses.includes(status); - }; - - // Some functions for event support - ComputeClient.prototype.on = function(ev, cb) { - this._events[ev] = this._events[ev] || []; - this._events[ev].push(cb); - }; - - ComputeClient.prototype.emit = function(ev) { - const args = Array.prototype.slice.call(arguments, 1); - const handlers = this._events[ev] || []; - return Promise.all(handlers.map(fn => fn.apply(this, args))); - }; + + class ComputeClient { + constructor (logger, blobClient) { + this.logger = logger.fork('compute'); + this.blobClient = blobClient; + this._events = {}; + } + + cancelJob (/*job*/) { + unimplemented(this.logger, 'cancelJob'); + } + + createJob (/*hash*/) { + unimplemented(this.logger, 'createJob'); + } + + getStatus (/*jobInfo*/) { + unimplemented(this.logger, 'getStatus'); + } + + getResultsInfo (/*jobInfo*/) { + unimplemented(this.logger, 'getResultsInfo'); + } + + getConsoleOutput (/*hash*/) { + unimplemented(this.logger, 'getConsoleOutput'); + } + + isFinishedStatus (status) { + const notFinishedStatuses = [this.QUEUED, this.PENDING, this.RUNNING]; + return !notFinishedStatuses.includes(status); + } + + // Some functions for event support + on (ev, cb) { + this._events[ev] = this._events[ev] || []; + this._events[ev].push(cb); + } + + emit (ev) { + const args = Array.prototype.slice.call(arguments, 1); + const handlers = this._events[ev] || []; + return Promise.all(handlers.map(fn => fn.apply(this, args))); + } + } ComputeClient.prototype.QUEUED = 'queued'; ComputeClient.prototype.PENDING = 'pending'; diff --git a/src/common/compute/backends/local/Client.js b/src/common/compute/backends/local/Client.js index 3e4eeed12..b6f05e527 100644 --- a/src/common/compute/backends/local/Client.js +++ b/src/common/compute/backends/local/Client.js @@ -46,195 +46,195 @@ define([ const symlink = promisify(fs.symlink); const touch = async name => await closeFile(await openFile(name, 'w')); - const LocalExecutor = function() { - ComputeClient.apply(this, arguments); - - this.jobQueue = []; - this.currentJob = null; - this.subprocess = null; - this.canceled = false; - }; - - LocalExecutor.prototype = Object.create(ComputeClient.prototype); - - LocalExecutor.prototype.cancelJob = function(jobInfo) { - const {hash} = jobInfo; - - if (this.currentJob === hash) { - this.canceled = true; - this.subprocess.kill(); - } else if (this.jobQueue.includes(hash)) { - const i = this.jobQueue.indexOf(hash); - this.jobQueue.splice(i, 1); - this._onJobCompleted(hash, new JobResults(this.CANCELED)); + class LocalExecutor extends ComputeClient { + constructor () { + super(...arguments); + + this.jobQueue = []; + this.currentJob = null; + this.subprocess = null; + this.canceled = false; } - }; - - LocalExecutor.prototype.getStatus = async function(jobInfo) { - const {hash} = jobInfo; - if (hash === this.currentJob) { - return this.RUNNING; - } else if (this.jobQueue.includes(hash)) { - return this.QUEUED; - } else { - return await this._getJobFile(hash, 'status.txt', 'Job Not Found'); + + cancelJob (jobInfo) { + const {hash} = jobInfo; + + if (this.currentJob === hash) { + this.canceled = true; + this.subprocess.kill(); + } else if (this.jobQueue.includes(hash)) { + const i = this.jobQueue.indexOf(hash); + this.jobQueue.splice(i, 1); + this._onJobCompleted(hash, new JobResults(this.CANCELED)); + } } - }; - - LocalExecutor.prototype.getConsoleOutput = async function(job) { - const msg = 'Console output data not found.'; - return await this._getJobFile(job.hash, STDOUT_FILE, msg); - }; - - LocalExecutor.prototype.getResultsInfo = async function(job) { - const msg = 'Metadata about result types not found.'; - const resultsTxt = await this._getJobFile(job.hash, 'results.json', msg); - return JSON.parse(resultsTxt); - }; - - LocalExecutor.prototype._getJobFile = async function(hash, name, notFoundMsg) { - const filename = path.join(this._getWorkingDir(hash), name); - try { - return await readFile(filename, 'utf8'); - } catch (err) { - if (err.code === 'ENOENT') { - throw new Error(notFoundMsg); + + async getStatus (jobInfo) { + const {hash} = jobInfo; + if (hash === this.currentJob) { + return this.RUNNING; + } else if (this.jobQueue.includes(hash)) { + return this.QUEUED; + } else { + return await this._getJobFile(hash, 'status.txt', 'Job Not Found'); } - throw err; } - }; - LocalExecutor.prototype.createJob = async function(hash) { - this.jobQueue.push(hash); - this._processNextJob(); + async getConsoleOutput (job) { + const msg = 'Console output data not found.'; + return await this._getJobFile(job.hash, STDOUT_FILE, msg); + } - return {hash}; - }; + async getResultsInfo (job) { + const msg = 'Metadata about result types not found.'; + const resultsTxt = await this._getJobFile(job.hash, 'results.json', msg); + return JSON.parse(resultsTxt); + } - LocalExecutor.prototype._onJobCompleted = async function(hash, jobResults) { - if (hash === this.currentJob) { - this.currentJob = null; + async _getJobFile (hash, name, notFoundMsg) { + const filename = path.join(this._getWorkingDir(hash), name); + try { + return await readFile(filename, 'utf8'); + } catch (err) { + if (err.code === 'ENOENT') { + throw new Error(notFoundMsg); + } + throw err; + } } - const tmpdir = this._getWorkingDir(hash); - //await this._cleanDirectory(tmpdir); - await writeFile(path.join(tmpdir, 'status.txt'), jobResults.status); + async createJob (hash) { + this.jobQueue.push(hash); + this._processNextJob(); + + return {hash}; + } + + async _onJobCompleted (hash, jobResults) { + if (hash === this.currentJob) { + this.currentJob = null; + } + + const tmpdir = this._getWorkingDir(hash); + //await this._cleanDirectory(tmpdir); + await writeFile(path.join(tmpdir, 'status.txt'), jobResults.status); - this.emit('update', hash, jobResults.status); - this.emit('end', hash, jobResults); - this._processNextJob(); - }; + this.emit('update', hash, jobResults.status); + this.emit('end', hash, jobResults); + this._processNextJob(); + } - LocalExecutor.prototype._cleanDirectory = async function(workdir) { - const SKIP_FILES = ['results.json', STDOUT_FILE]; - const files = (await readdir(workdir)) - .filter(name => !SKIP_FILES.includes(name)) - .map(name => path.join(workdir, name)); + async _cleanDirectory (workdir) { + const SKIP_FILES = ['results.json', STDOUT_FILE]; + const files = (await readdir(workdir)) + .filter(name => !SKIP_FILES.includes(name)) + .map(name => path.join(workdir, name)); - return Promise.all(files.map(file => rm_rf(file))); - }; + return Promise.all(files.map(file => rm_rf(file))); + } - LocalExecutor.prototype._processNextJob = function() { - if (this.currentJob) return; + _processNextJob () { + if (this.currentJob) return; - this.currentJob = this.jobQueue.shift(); - if (this.currentJob) { - return this._createJob(this.currentJob); + this.currentJob = this.jobQueue.shift(); + if (this.currentJob) { + return this._createJob(this.currentJob); + } } - }; - - LocalExecutor.prototype._getWorkingDir = function(hash) { - return path.join(os.tmpdir(), `deepforge-local-exec-${hash}`); - }; - - LocalExecutor.prototype._createJob = async function(hash) { - const jobInfo = {hash}; - this.emit('update', jobInfo.hash, this.PENDING); - const tmpdir = this._getWorkingDir(hash); - try { - await mkdir(tmpdir); - } catch (err) { - if (err.code === 'EEXIST') { - await rm_rf(tmpdir); + + _getWorkingDir (hash) { + return path.join(os.tmpdir(), `deepforge-local-exec-${hash}`); + } + + async _createJob (hash) { + const jobInfo = {hash}; + this.emit('update', jobInfo.hash, this.PENDING); + const tmpdir = this._getWorkingDir(hash); + try { await mkdir(tmpdir); - } else { - throw err; + } catch (err) { + if (err.code === 'EEXIST') { + await rm_rf(tmpdir); + await mkdir(tmpdir); + } else { + throw err; + } } + this.logger.info('created working directory at', tmpdir); + + // Fetch the required files from deepforge + await this.prepareWorkspace(hash, tmpdir); + + // Spin up a subprocess + const config = JSON.parse(await readFile(tmpdir.replace(path.sep, '/') + '/executor_config.json', 'utf8')); + + const env = process.env; + env.DEEPFORGE_ROOT = DEEPFORGE_ROOT; + const options = { + cwd: tmpdir, + env, + }; + this.logger.info(`Running ${config.cmd} ${config.args.join(' ')}`); + this.subprocess = spawn(config.cmd, config.args, options); + this.emit('update', jobInfo.hash, this.RUNNING); + this.subprocess.stdout.on('data', data => this.onConsoleOutput(tmpdir, hash, data)); + + this.subprocess.on('close', async code => { + const status = this.canceled ? this.CANCELED : + (code !== 0 ? this.FAILED : this.SUCCESS); + + const jobResults = new JobResults(status); + this.canceled = false; + + this._onJobCompleted(hash, jobResults); + }); } - this.logger.info('created working directory at', tmpdir); - - // Fetch the required files from deepforge - await this.prepareWorkspace(hash, tmpdir); - - // Spin up a subprocess - const config = JSON.parse(await readFile(tmpdir.replace(path.sep, '/') + '/executor_config.json', 'utf8')); - - const env = process.env; - env.DEEPFORGE_ROOT = DEEPFORGE_ROOT; - const options = { - cwd: tmpdir, - env, - }; - this.logger.info(`Running ${config.cmd} ${config.args.join(' ')}`); - this.subprocess = spawn(config.cmd, config.args, options); - this.emit('update', jobInfo.hash, this.RUNNING); - this.subprocess.stdout.on('data', data => this.onConsoleOutput(tmpdir, hash, data)); - - this.subprocess.on('close', async code => { - const status = this.canceled ? this.CANCELED : - (code !== 0 ? this.FAILED : this.SUCCESS); - - const jobResults = new JobResults(status); - this.canceled = false; - this._onJobCompleted(hash, jobResults); - }); - }; - - LocalExecutor.prototype.onConsoleOutput = async function(workdir, hash, data) { - const filename = path.join(workdir, STDOUT_FILE); - appendFile(filename, data); - this.emit('data', hash, data); - }; - - LocalExecutor.prototype._getAllFiles = async function(workdir) { - const dirs = (await readdir(workdir)) - .filter(n => !n.includes('node_modules')) - .map(name => path.join(workdir, name)); - const files = []; - - // Read each directory - while (dirs.length) { - const abspath = dirs.shift(); - const isDirectory = (await statFile(abspath)).isDirectory(); - if (isDirectory) { - const childpaths = (await readdir(abspath)) - .map(name => path.join(abspath, name)); - dirs.push.apply(dirs, childpaths); - } else { - files.push(abspath); - } + async onConsoleOutput (workdir, hash, data) { + const filename = path.join(workdir, STDOUT_FILE); + appendFile(filename, data); + this.emit('data', hash, data); } - return files; - }; + async _getAllFiles (workdir) { + const dirs = (await readdir(workdir)) + .filter(n => !n.includes('node_modules')) + .map(name => path.join(workdir, name)); + const files = []; + + // Read each directory + while (dirs.length) { + const abspath = dirs.shift(); + const isDirectory = (await statFile(abspath)).isDirectory(); + if (isDirectory) { + const childpaths = (await readdir(abspath)) + .map(name => path.join(abspath, name)); + dirs.push.apply(dirs, childpaths); + } else { + files.push(abspath); + } + } + + return files; + } - LocalExecutor.prototype.prepareWorkspace = async function(hash, dirname) { - this.logger.info('about to fetch job data'); - const content = new Buffer(new Uint8Array(await this.blobClient.getObject(hash))); // TODO: Handle errors... - const zipPath = path.join(dirname, `${hash}.zip`); - await writeFile(zipPath, content); - this.logger.info(`Fetched job data: ${zipPath}`); + async prepareWorkspace (hash, dirname) { + this.logger.info('about to fetch job data'); + const content = new Buffer(new Uint8Array(await this.blobClient.getObject(hash))); // TODO: Handle errors... + const zipPath = path.join(dirname, `${hash}.zip`); + await writeFile(zipPath, content); + this.logger.info(`Fetched job data: ${zipPath}`); - this.logger.info(`unzipping ${zipPath} in ${dirname}`); - await unzip(zipPath, dirname); + this.logger.info(`unzipping ${zipPath} in ${dirname}`); + await unzip(zipPath, dirname); - // Set up a symbolic link to the node_modules - await symlink(NODE_MODULES, path.join(dirname, 'node_modules')); + // Set up a symbolic link to the node_modules + await symlink(NODE_MODULES, path.join(dirname, 'node_modules')); - // Prepare for the stdout - await touch(path.join(dirname, STDOUT_FILE)); - }; + // Prepare for the stdout + await touch(path.join(dirname, STDOUT_FILE)); + } + } async function unzip(filename, dirname) { const args = UNZIP_ARGS.concat(path.basename(filename)); From ce384b02ccb0d9e86c34fec45d5ba9d8277b9f94 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Tue, 5 May 2020 10:02:40 -0500 Subject: [PATCH 08/37] Add deepforge serialization files and addArtifact support --- .../InteractiveCompute/job-files/index.js | 10 ++- .../job-files/start-session.js | 80 +++++++++++++++++-- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/routers/InteractiveCompute/job-files/index.js b/src/routers/InteractiveCompute/job-files/index.js index e1a65cf73..60ac06fa5 100644 --- a/src/routers/InteractiveCompute/job-files/index.js +++ b/src/routers/InteractiveCompute/job-files/index.js @@ -1,10 +1,14 @@ /* globals requireJS */ const Files = requireJS('deepforge/plugin/GeneratedFiles'); +const Templates = requireJS('plugin/GenerateJob/GenerateJob/templates/index'); const fs = require('fs'); const path = require('path'); const START_SESSION = fs.readFileSync(path.join(__dirname, 'start-session.js'), 'utf8'); -const interactiveDir = path.join(__dirname, '..', '..', '..', 'common', 'compute', 'interactive'); +const srcDir = path.join(__dirname, '..', '..', '..'); +const interactiveDir = path.join(srcDir, 'common', 'compute', 'interactive'); const MESSAGE = fs.readFileSync(path.join(interactiveDir, 'message.js'), 'utf8'); +const _ = requireJS('underscore'); +const CONSTANTS = requireJS('deepforge/Constants'); class StartSessionFiles extends Files { constructor(blobClient, url, id) { @@ -23,6 +27,10 @@ class StartSessionFiles extends Files { this.addFile('executor_config.json', config); this.addFile('start-session.js', START_SESSION); this.addFile('message.js', MESSAGE); + this.addFile('utils.build.js', Templates.UTILS); + this.addFile('deepforge/__init__.py', Templates.DEEPFORGE_INIT); + const serializeTpl = _.template(Templates.DEEPFORGE_SERIALIZATION); + this.addFile('deepforge/serialization.py', serializeTpl(CONSTANTS)); } async upload() { diff --git a/src/routers/InteractiveCompute/job-files/start-session.js b/src/routers/InteractiveCompute/job-files/start-session.js index f7fd268cf..0323ec7ef 100644 --- a/src/routers/InteractiveCompute/job-files/start-session.js +++ b/src/routers/InteractiveCompute/job-files/start-session.js @@ -4,6 +4,7 @@ const [, , SERVER_URL, ID] = process.argv; const WebSocket = require('ws'); const fs = require('fs').promises; const path = require('path'); +const requirejs = require('requirejs'); const ws = new WebSocket(SERVER_URL); ws.on('open', () => ws.send(ID)); @@ -15,30 +16,93 @@ ws.on('message', async function(data) { // TODO: Queue the commands here? const msg = Message.decode(data); if (msg.type === Message.RUN) { - const [cmd, ...opts] = msg.data.split(' '); + const [cmd, ...opts] = parseCommand(msg.data); const subprocess = spawn(cmd, opts); subprocess.on('close', code => ws.send(Message.encode(Message.COMPLETE, code))); subprocess.stdout.on('data', data => ws.send(Message.encode(Message.STDOUT, data))); subprocess.stderr.on('data', data => ws.send(Message.encode(Message.STDERR, data))); } else if (msg.type === Message.ADD_ARTIFACT) { console.log('adding artifact...'); - const [name, dataInfo, type] = msg.data; - console.log(name, dataInfo, type); console.log(msg); - await mkdirp(['artifacts', name]); - // TODO: make artifacts/ directory if needed? + const [name, dataInfo, type, config={}] = msg.data; + const dirs = ['artifacts', name]; + await mkdirp(...dirs); + requirejs([ + './utils.build', + ], function( + Utils, + ) { + const {Storage} = Utils; + + async function saveArtifact() { + let exitCode = 0; + try { + const client = await Storage.getClient(dataInfo.backend, null, config); + const dataPath = path.join(...dirs.concat('data')); + const buffer = await client.getFile(dataInfo); + await fs.writeFile(dataPath, buffer); + const filePath = path.join(...dirs.concat('__init__.py')); + await fs.writeFile(filePath, initFile(name, type)); + } catch (err) { + exitCode = 1; + console.log('ERROR:', err); + } + ws.send(Message.encode(Message.COMPLETE, exitCode)); + } + + saveArtifact(); + }); } }); +function parseCommand(cmd) { + const chunks = ['']; + let quoteChar = null; + for (let i = 0; i < cmd.length; i++) { + const letter = cmd[i]; + const isQuoteChar = letter === '"' || letter === '\''; + const isInQuotes = !!quoteChar; + + if (!isInQuotes && isQuoteChar) { + quoteChar = letter; + } else if (quoteChar === letter) { + quoteChar = null; + } else { + const isNewChunk = letter === ' ' && !isInQuotes; + if (isNewChunk) { + chunks.push(''); + } else { + const lastChunk = chunks[chunks.length - 1]; + chunks[chunks.length - 1] = lastChunk + letter; + } + } + } + console.log(`parsed command:\n${cmd}\n${chunks.join('|')}`); + return chunks; +} + async function mkdirp() { const dirs = Array.prototype.slice.call(arguments); - for (let i = 0; i < dirs.length; i++) { + await dirs.reduce(async (lastDirPromise, nextDir) => { + const dir = path.join(await lastDirPromise, nextDir); try { - await fs.mkdir(dirs[i]); + await fs.mkdir(dir); } catch (err) { if (err.code !== 'EEXIST') { throw err; } } - } + return dir; + }, process.cwd()); +} + +function initFile(name, type) { + const dataPathCode = `path.join(path.dirname(__file__), 'data')`; + return [ + 'import deepforge', + 'from os import path', + `name = '${name}'`, + `type = '${type}'`, + `data = deepforge.serialization.load('${type}', open(${dataPathCode}, 'rb'))` + ].join('\n'); } From 204fe34cbe6807da2c4ea8e69044532f9918e788 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Tue, 5 May 2020 10:13:43 -0500 Subject: [PATCH 09/37] Add session.close to interactive sessions --- src/common/compute/interactive/session.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index 5a7748f23..44a5009f8 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -92,6 +92,10 @@ define([ await this.runTask(task); } + close() { + this.ws.close(); + } + static async new(computeID, config={}) { const session = new InteractiveSession(computeID, config); await session.whenConnected(); From 576601364b0617929d212c15dd51cb05ff3abc58 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Tue, 5 May 2020 11:45:59 -0500 Subject: [PATCH 10/37] WIP IC Add member method for encoding msgs --- src/common/compute/interactive/message.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/common/compute/interactive/message.js b/src/common/compute/interactive/message.js index ec8782447..aca2948be 100644 --- a/src/common/compute/interactive/message.js +++ b/src/common/compute/interactive/message.js @@ -29,14 +29,21 @@ return new Message(type, data); } - static encode(type, data) { + encode() { + return Message.encode(this.type, this.data); + } + + static encode(type, data=0) { //const buffer = Buffer.allocUnsafe(1); //buffer.writeUInt8(type, 0); //if (!data.isBuffer) { //data = Buffer.from(data); //} //return buffer.concat(data); - return JSON.stringify({type, data: data.toString()}); + if (typeof Buffer !== 'undefined' && data instanceof Buffer) { + data = data.toString(); + } + return JSON.stringify({type, data}); } } Object.assign(Message, Constants); From 1cb1c38cd2e061c9f07c842bf233802f76528f5e Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Wed, 13 May 2020 10:42:57 -0500 Subject: [PATCH 11/37] WIP IC Throw error on exec w/ non-zero exitCode --- src/common/compute/interactive/errors.js | 16 ++++++++++++++++ src/common/compute/interactive/session.js | 6 ++++++ 2 files changed, 22 insertions(+) create mode 100644 src/common/compute/interactive/errors.js diff --git a/src/common/compute/interactive/errors.js b/src/common/compute/interactive/errors.js new file mode 100644 index 000000000..4f05764f5 --- /dev/null +++ b/src/common/compute/interactive/errors.js @@ -0,0 +1,16 @@ +/* globals define */ +define([ +], function( +) { + class CommandFailedError extends Error { + constructor(cmd, result) { + const {exitCode, stderr} = result; + const msg = stderr ? + `Command "${cmd}" failed with exit code ${exitCode}:\n${stderr}` : + `Command "${cmd}" failed with exit code ${exitCode}.`; + super(msg); + } + } + + return {CommandFailedError}; +}); diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index 44a5009f8..13ab13779 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -3,12 +3,15 @@ define([ 'deepforge/utils', 'deepforge/compute/interactive/task', 'deepforge/compute/interactive/message', + 'deepforge/compute/interactive/errors', ], function( utils, Task, Message, + Errors, ) { const {defer} = utils; + const {CommandFailedError} = Errors; class InteractiveSession { constructor(computeID, config) { this.currentTask = null; @@ -82,6 +85,9 @@ define([ task.on(Message.STDERR, data => result.stderr += data.toString()); task.on(Message.COMPLETE, code => result.exitCode = code); await this.runTask(task); + if (result.exitCode) { + throw new CommandFailedError(cmd, result); + } return result; } From 69f3e97a886f15e9dfad0e4f58b81a903d2db533 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Wed, 13 May 2020 10:52:38 -0500 Subject: [PATCH 12/37] WIP IC add session w/ auto queueing support --- .../compute/interactive/session-with-queue.js | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/common/compute/interactive/session-with-queue.js diff --git a/src/common/compute/interactive/session-with-queue.js b/src/common/compute/interactive/session-with-queue.js new file mode 100644 index 000000000..769dec160 --- /dev/null +++ b/src/common/compute/interactive/session-with-queue.js @@ -0,0 +1,71 @@ +/* globals define*/ +define([ + 'deepforge/compute/interactive/session', + 'deepforge/compute/interactive/task', + 'deepforge/compute/interactive/message', + 'deepforge/compute/interactive/errors', + 'deepforge/utils', +], function( + Session, + Task, + Message, + Errors, + utils, +) { + const {defer} = utils; + class SessionWithQueue extends Session { + constructor(computeID, config, size=20) { + super(computeID, config); + this.size = size; + this.tasks = []; + } + + async runTask(task) { + const queuedTask = this.queueTask(task); + await queuedTask.promise; + } + + queueTask(task) { + const queuedTask = new QueuedTask(task); + this.tasks.push(queuedTask); + this.checkTaskQueue(); + return queuedTask; + } + + async checkTaskQueue() { + if (this.isIdle() && this.tasks.length) { + this.runNextTask(); + } + } + + ensureIdle(action) { + if (action === 'run task') { + super.ensureIdle(action); + } + } + + async runNextTask() { + const queuedTask = this.tasks[0]; + await super.runTask(queuedTask.unwrap()); + this.tasks.shift(); + queuedTask.resolve(); + this.checkTaskQueue(); + } + } + + class QueuedTask { + constructor(task) { + this.innerTask = task; + const deferred = defer(); + this.promise = deferred.promise; + this.resolve = deferred.resolve; + this.reject = deferred.reject; + } + + unwrap() { + return this.innerTask; + } + } + + return SessionWithQueue; +}); From 7fb5acd5329f21ae55c66817dc51c61f6fb7fa6d Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Wed, 13 May 2020 10:53:01 -0500 Subject: [PATCH 13/37] WIP IC Rename action name in ensureIdle --- src/common/compute/interactive/session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index 13ab13779..c43ffeded 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -55,7 +55,7 @@ define([ } async runTask(task) { - this.ensureIdle('spawn a task'); + this.ensureIdle('run task'); this.currentTask = task; await task.run(); From f6151e09eeaa5b0b229a678bd11fabd8d695fbef Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Wed, 13 May 2020 14:42:32 -0500 Subject: [PATCH 14/37] WIP code cleanup --- src/common/compute/interactive/message.js | 6 ------ src/routers/InteractiveCompute/InteractiveCompute.js | 2 -- .../InteractiveCompute/job-files/start-session.js | 9 +-------- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/common/compute/interactive/message.js b/src/common/compute/interactive/message.js index aca2948be..528f28ba7 100644 --- a/src/common/compute/interactive/message.js +++ b/src/common/compute/interactive/message.js @@ -34,12 +34,6 @@ } static encode(type, data=0) { - //const buffer = Buffer.allocUnsafe(1); - //buffer.writeUInt8(type, 0); - //if (!data.isBuffer) { - //data = Buffer.from(data); - //} - //return buffer.concat(data); if (typeof Buffer !== 'undefined' && data instanceof Buffer) { data = data.toString(); } diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index 9e22f9386..fdb1d0b90 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -18,7 +18,6 @@ class ComputeBroker { listen (port) { // TODO: Can I piggyback off the webgme server? Maybe using a different path? - this.clientServer = new WebSocket.Server({port}); // FIXME: this might be tricky on the current deployment this.workerServer = new WebSocket.Server({port: port + 1}); @@ -67,7 +66,6 @@ function initialize(middlewareOpts) { broker.listen(gmeConfig.server.port + 1); logger.debug('initializing ...'); - // TODO: additional auth required? logger.debug('ready'); } diff --git a/src/routers/InteractiveCompute/job-files/start-session.js b/src/routers/InteractiveCompute/job-files/start-session.js index 0323ec7ef..86d01e3b5 100644 --- a/src/routers/InteractiveCompute/job-files/start-session.js +++ b/src/routers/InteractiveCompute/job-files/start-session.js @@ -9,11 +9,7 @@ const requirejs = require('requirejs'); const ws = new WebSocket(SERVER_URL); ws.on('open', () => ws.send(ID)); -// TODO: Should this run in a single subprocess or in many? -// For now, let's run it in a single subprocess... ws.on('message', async function(data) { - // TODO: Run the command and send the results back - // TODO: Queue the commands here? const msg = Message.decode(data); if (msg.type === Message.RUN) { const [cmd, ...opts] = parseCommand(msg.data); @@ -22,8 +18,6 @@ ws.on('message', async function(data) { subprocess.stdout.on('data', data => ws.send(Message.encode(Message.STDOUT, data))); subprocess.stderr.on('data', data => ws.send(Message.encode(Message.STDERR, data))); } else if (msg.type === Message.ADD_ARTIFACT) { - console.log('adding artifact...'); - console.log(msg); const [name, dataInfo, type, config={}] = msg.data; const dirs = ['artifacts', name]; await mkdirp(...dirs); @@ -45,7 +39,7 @@ ws.on('message', async function(data) { await fs.writeFile(filePath, initFile(name, type)); } catch (err) { exitCode = 1; - console.log('ERROR:', err); + console.error(`addArtifact(${name}) failed:`, err); } ws.send(Message.encode(Message.COMPLETE, exitCode)); } @@ -77,7 +71,6 @@ function parseCommand(cmd) { } } } - console.log(`parsed command:\n${cmd}\n${chunks.join('|')}`); return chunks; } From d6740e73c9fb13bb55bf764c56e13da9dc39388c Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 8 May 2020 14:37:20 -0500 Subject: [PATCH 15/37] WIP create server in "before" --- test/unit/common/api/JobOriginClient.spec.js | 16 ++++++------ .../ExecutePipeline/ExecutePipeline.js | 7 +++--- test/unit/routers/ExecPulse/ExecPulse.spec.js | 3 ++- .../routers/JobLogsAPI/JobLogsAPI.spec.js | 25 +++++++++++-------- .../routers/JobOriginAPI/JobOriginAPI.spec.js | 20 ++++++++------- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/test/unit/common/api/JobOriginClient.spec.js b/test/unit/common/api/JobOriginClient.spec.js index 54fd63a51..67f9949f0 100644 --- a/test/unit/common/api/JobOriginClient.spec.js +++ b/test/unit/common/api/JobOriginClient.spec.js @@ -1,18 +1,13 @@ var testFixture = require('../../../globals'), expect = testFixture.expect, gmeConfig = testFixture.getGmeConfig(), - server = testFixture.WebGME.standaloneServer(gmeConfig), Logger = require('webgme-engine/src/server/logger'), logger = Logger.createWithGmeConfig('gme', gmeConfig, true), JobOriginClient = testFixture.requirejs('deepforge/api/JobOriginClient'); describe('JobOriginClient', function() { - var client = new JobOriginClient({ - logger: logger, - origin: server.getUrl(), - projectId: 'testProject', - branchName: 'master' - }), + var server, + client, hashes = {}, getJobInfo = function() { var hash = 'hashOrigin'+Math.ceil(Math.random()*100000); @@ -31,6 +26,13 @@ describe('JobOriginClient', function() { }; before(function(done) { + server = testFixture.WebGME.standaloneServer(gmeConfig), + client = new JobOriginClient({ + logger: logger, + origin: server.getUrl(), + projectId: 'testProject', + branchName: 'master' + }); server.start(done); }); diff --git a/test/unit/plugins/ExecutePipeline/ExecutePipeline.js b/test/unit/plugins/ExecutePipeline/ExecutePipeline.js index 746b91734..08fa50cb0 100644 --- a/test/unit/plugins/ExecutePipeline/ExecutePipeline.js +++ b/test/unit/plugins/ExecutePipeline/ExecutePipeline.js @@ -32,11 +32,12 @@ describe('ExecutePipeline', function () { Pipeline.ComplexPipeline = '/f/3'; Pipeline.ExportPlugin = '/f/s'; - const server = new testFixture.WebGME.standaloneServer(gmeConfig); - server.start = promisify(server.start); - server.stop = promisify(server.stop); + let server; before(async function () { + server = new testFixture.WebGME.standaloneServer(gmeConfig); + server.start = promisify(server.start); + server.stop = promisify(server.stop); gmeAuth = await testFixture.clearDBAndGetGMEAuth(gmeConfig, projectName); // This uses in memory storage. Use testFixture.getMongoStorage to persist test to database. storage = testFixture.getMemoryStorage(logger, gmeConfig, gmeAuth); diff --git a/test/unit/routers/ExecPulse/ExecPulse.spec.js b/test/unit/routers/ExecPulse/ExecPulse.spec.js index 0b3ea9922..be907e169 100644 --- a/test/unit/routers/ExecPulse/ExecPulse.spec.js +++ b/test/unit/routers/ExecPulse/ExecPulse.spec.js @@ -5,8 +5,8 @@ describe('ExecPulse', function() { superagent = testFixture.superagent, expect = testFixture.expect, gmeConfig = testFixture.getGmeConfig(), - server = testFixture.WebGME.standaloneServer(gmeConfig), mntPt = require('../../../../webgme-setup.json').components.routers.ExecPulse.mount, + server, urlFor = function(action) { return [ server.getUrl(), @@ -20,6 +20,7 @@ describe('ExecPulse', function() { }; before(function(done) { + server = testFixture.WebGME.standaloneServer(gmeConfig), server.start(done); }); diff --git a/test/unit/routers/JobLogsAPI/JobLogsAPI.spec.js b/test/unit/routers/JobLogsAPI/JobLogsAPI.spec.js index 007bbc944..7e50de030 100644 --- a/test/unit/routers/JobLogsAPI/JobLogsAPI.spec.js +++ b/test/unit/routers/JobLogsAPI/JobLogsAPI.spec.js @@ -6,7 +6,6 @@ var testFixture = require('../../../globals'), path = testFixture.path, gmeConfig = testFixture.getGmeConfig(), blobDir = gmeConfig.blob.fsDir, - server = testFixture.WebGME.standaloneServer(gmeConfig), mntPt = 'execution/logs', rm_rf = require('rimraf'), exists = require('exists-file'); @@ -16,6 +15,12 @@ describe('JobLogsAPI', function() { branch = 'master', jobId = encodeURIComponent('/4/q/l'), firstLog = 'hello world', + server, + url; + + before(function(done) { + testFixture.mkdir(blobDir); + server = testFixture.WebGME.standaloneServer(gmeConfig); url = [ server.getUrl(), mntPt, @@ -23,9 +28,6 @@ describe('JobLogsAPI', function() { branch, jobId ].join('/'); - - before(function(done) { - testFixture.mkdir(blobDir); server.start(done); }); @@ -78,15 +80,16 @@ describe('JobLogsAPI', function() { }); describe('delete', function() { - var delUrl = [ - server.getUrl(), - mntPt, - 'testProject', - 'other', - encodeURIComponent('/4/8/l') - ].join('/'); + var delUrl; before(function(done) { + delUrl = [ + server.getUrl(), + mntPt, + 'testProject', + 'other', + encodeURIComponent('/4/8/l') + ].join('/'); superagent.patch(delUrl) .send({patch: firstLog}) .end(function (err, res) { diff --git a/test/unit/routers/JobOriginAPI/JobOriginAPI.spec.js b/test/unit/routers/JobOriginAPI/JobOriginAPI.spec.js index 435c91d81..7cf30bd0f 100644 --- a/test/unit/routers/JobOriginAPI/JobOriginAPI.spec.js +++ b/test/unit/routers/JobOriginAPI/JobOriginAPI.spec.js @@ -2,18 +2,12 @@ var testFixture = require('../../../globals'), superagent = testFixture.superagent, expect = testFixture.expect, gmeConfig = testFixture.getGmeConfig(), - server = testFixture.WebGME.standaloneServer(gmeConfig), mntPt = 'job/origins'; describe('JobOriginAPI', function() { - var hashes = {}, - getUrl = function(hash) { - return [ - server.getUrl(), - mntPt, - hash - ].join('/'); - }, + var server, + getUrl, + hashes = {}, getJobInfo = function() { var hash = 'hash'+Math.ceil(Math.random()*100000); @@ -33,6 +27,14 @@ describe('JobOriginAPI', function() { }; before(function(done) { + server = testFixture.WebGME.standaloneServer(gmeConfig); + getUrl = function(hash) { + return [ + server.getUrl(), + mntPt, + hash + ].join('/'); + }; server.start(done); }); From 27a82ea8a1097ba5180f74445d2ae2c5f86a78f0 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Wed, 13 May 2020 15:10:52 -0500 Subject: [PATCH 16/37] Refactor interactive client code (improve testability) --- .../job-files/start-session.js | 137 ++++++++++-------- .../InteractiveCompute/start-session.js | 27 ++++ 2 files changed, 107 insertions(+), 57 deletions(-) create mode 100644 test/unit/routers/InteractiveCompute/start-session.js diff --git a/src/routers/InteractiveCompute/job-files/start-session.js b/src/routers/InteractiveCompute/job-files/start-session.js index 86d01e3b5..67e1cd95a 100644 --- a/src/routers/InteractiveCompute/job-files/start-session.js +++ b/src/routers/InteractiveCompute/job-files/start-session.js @@ -1,77 +1,90 @@ -const Message = require('./message'); const {spawn} = require('child_process'); -const [, , SERVER_URL, ID] = process.argv; const WebSocket = require('ws'); const fs = require('fs').promises; const path = require('path'); const requirejs = require('requirejs'); +let Message; -const ws = new WebSocket(SERVER_URL); -ws.on('open', () => ws.send(ID)); +class InteractiveClient { + constructor(id, host) { + this.id = id; + this.host = host; + this.ws = null; + } + + connect() { + this.ws = new WebSocket(this.host); + this.ws.on('open', () => this.ws.send(this.id)); + this.ws.on('message', data => this.onMessage(Message.decode(data))); + } -ws.on('message', async function(data) { - const msg = Message.decode(data); - if (msg.type === Message.RUN) { - const [cmd, ...opts] = parseCommand(msg.data); - const subprocess = spawn(cmd, opts); - subprocess.on('close', code => ws.send(Message.encode(Message.COMPLETE, code))); - subprocess.stdout.on('data', data => ws.send(Message.encode(Message.STDOUT, data))); - subprocess.stderr.on('data', data => ws.send(Message.encode(Message.STDERR, data))); - } else if (msg.type === Message.ADD_ARTIFACT) { - const [name, dataInfo, type, config={}] = msg.data; - const dirs = ['artifacts', name]; - await mkdirp(...dirs); - requirejs([ - './utils.build', - ], function( - Utils, - ) { - const {Storage} = Utils; + async sendMessage(type, data) { + this.ws.send(Message.encode(Message[type], data)); + } + + async onMessage(msg) { + if (msg.type === Message.RUN) { + const [cmd, ...opts] = InteractiveClient.parseCommand(msg.data); + const subprocess = spawn(cmd, opts); + subprocess.on('close', code => this.sendMessage(Message.COMPLETE, code)); + subprocess.stdout.on('data', data => this.sendMessage(Message.STDOUT, data)); + subprocess.stderr.on('data', data => this.sendMessage(Message.STDERR, data)); + } else if (msg.type === Message.ADD_ARTIFACT) { + const [name, dataInfo, type, config={}] = msg.data; + const dirs = ['artifacts', name]; + await mkdirp(...dirs); + requirejs([ + './utils.build', + ], function( + Utils, + ) { + const {Storage} = Utils; - async function saveArtifact() { - let exitCode = 0; - try { - const client = await Storage.getClient(dataInfo.backend, null, config); - const dataPath = path.join(...dirs.concat('data')); - const buffer = await client.getFile(dataInfo); - await fs.writeFile(dataPath, buffer); - const filePath = path.join(...dirs.concat('__init__.py')); - await fs.writeFile(filePath, initFile(name, type)); - } catch (err) { - exitCode = 1; - console.error(`addArtifact(${name}) failed:`, err); + async function saveArtifact() { + let exitCode = 0; + try { + const client = await Storage.getClient(dataInfo.backend, null, config); + const dataPath = path.join(...dirs.concat('data')); + const buffer = await client.getFile(dataInfo); + await fs.writeFile(dataPath, buffer); + const filePath = path.join(...dirs.concat('__init__.py')); + await fs.writeFile(filePath, initFile(name, type)); + } catch (err) { + exitCode = 1; + console.error(`addArtifact(${name}) failed:`, err); + } + this.sendMessage(Message.COMPLETE, exitCode); } - ws.send(Message.encode(Message.COMPLETE, exitCode)); - } - saveArtifact(); - }); + saveArtifact(); + }); + } } -}); -function parseCommand(cmd) { - const chunks = ['']; - let quoteChar = null; - for (let i = 0; i < cmd.length; i++) { - const letter = cmd[i]; - const isQuoteChar = letter === '"' || letter === '\''; - const isInQuotes = !!quoteChar; + static parseCommand(cmd) { + const chunks = ['']; + let quoteChar = null; + for (let i = 0; i < cmd.length; i++) { + const letter = cmd[i]; + const isQuoteChar = letter === '"' || letter === '\''; + const isInQuotes = !!quoteChar; - if (!isInQuotes && isQuoteChar) { - quoteChar = letter; - } else if (quoteChar === letter) { - quoteChar = null; - } else { - const isNewChunk = letter === ' ' && !isInQuotes; - if (isNewChunk) { - chunks.push(''); + if (!isInQuotes && isQuoteChar) { + quoteChar = letter; + } else if (quoteChar === letter) { + quoteChar = null; } else { - const lastChunk = chunks[chunks.length - 1]; - chunks[chunks.length - 1] = lastChunk + letter; + const isNewChunk = letter === ' ' && !isInQuotes; + if (isNewChunk) { + chunks.push(''); + } else { + const lastChunk = chunks[chunks.length - 1]; + chunks[chunks.length - 1] = lastChunk + letter; + } } } + return chunks; } - return chunks; } async function mkdirp() { @@ -99,3 +112,13 @@ function initFile(name, type) { `data = deepforge.serialization.load('${type}', open(${dataPathCode}, 'rb'))` ].join('\n'); } + +module.exports = {InteractiveClient}; + +const isImportedModule = require.main !== module; +if (!isImportedModule) { + Message = require('./message'); + const [, , SERVER_URL, ID] = process.argv; + const client = new InteractiveClient(ID, SERVER_URL); + client.connect(); +} diff --git a/test/unit/routers/InteractiveCompute/start-session.js b/test/unit/routers/InteractiveCompute/start-session.js new file mode 100644 index 000000000..1255eafdb --- /dev/null +++ b/test/unit/routers/InteractiveCompute/start-session.js @@ -0,0 +1,27 @@ +describe('InteractiveClient', function() { + const testFixture = require('../../../globals'); + const assert = require('assert').strict; + const {InteractiveClient} = require(testFixture.PROJECT_ROOT + '/src/routers/InteractiveCompute/job-files/start-session'); + + describe('parseCommand', function() { + it('should parse separate words ("ab cd efg h")', function() { + const cmd = 'ab cd efg h'; + const chunks = InteractiveClient.parseCommand(cmd); + assert.equal(chunks.join(' '), cmd); + }); + + it('should parse "ab \'cd efg h\'"', function() { + const cmd = 'ab \'cd efg h\''; + const chunks = InteractiveClient.parseCommand(cmd); + assert.equal(chunks.length, 2); + assert.equal(chunks[0], 'ab'); + }); + + it('should parse "ab "cd efg h""', function() { + const cmd = 'ab "cd efg h"'; + const chunks = InteractiveClient.parseCommand(cmd); + assert.equal(chunks.length, 2); + assert.equal(chunks[0], 'ab'); + }); + }); +}); From 0af3c372c2afe2b44dcc3223251f357dd689ba80 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 11 May 2020 16:57:51 -0500 Subject: [PATCH 17/37] addFile support for interactive sessions --- src/common/compute/interactive/session.js | 7 ++++ .../job-files/start-session.js | 42 ++++++++++++------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index c43ffeded..d038b446f 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -98,6 +98,13 @@ define([ await this.runTask(task); } + async addFile(filepath, content) { + this.ensureIdle('add file'); + const msg = new Message(Message.ADD_FILE, [filepath, content]); + const task = new Task(this.ws, msg); + await this.runTask(task); + } + close() { this.ws.close(); } diff --git a/src/routers/InteractiveCompute/job-files/start-session.js b/src/routers/InteractiveCompute/job-files/start-session.js index 67e1cd95a..38291dd9a 100644 --- a/src/routers/InteractiveCompute/job-files/start-session.js +++ b/src/routers/InteractiveCompute/job-files/start-session.js @@ -41,26 +41,28 @@ class InteractiveClient { const {Storage} = Utils; async function saveArtifact() { - let exitCode = 0; - try { - const client = await Storage.getClient(dataInfo.backend, null, config); - const dataPath = path.join(...dirs.concat('data')); - const buffer = await client.getFile(dataInfo); - await fs.writeFile(dataPath, buffer); - const filePath = path.join(...dirs.concat('__init__.py')); - await fs.writeFile(filePath, initFile(name, type)); - } catch (err) { - exitCode = 1; - console.error(`addArtifact(${name}) failed:`, err); - } - this.sendMessage(Message.COMPLETE, exitCode); + const client = await Storage.getClient(dataInfo.backend, null, config); + const dataPath = path.join(...dirs.concat('data')); + const buffer = await client.getFile(dataInfo); + await fs.writeFile(dataPath, buffer); + const filePath = path.join(...dirs.concat('__init__.py')); + await fs.writeFile(filePath, initFile(name, type)); } - saveArtifact(); + runTask(saveArtifact); }); + } else if (msg.type === Message.ADD_FILE) { + runTask(() => this.writeFile(msg)); } } + async writeFile(msg) { + const [filepath, content] = msg.data; + const dirs = path.dirname(filepath).split(path.sep); + await mkdirp(...dirs); + await fs.writeFile(filepath, content); + } + static parseCommand(cmd) { const chunks = ['']; let quoteChar = null; @@ -68,7 +70,6 @@ class InteractiveClient { const letter = cmd[i]; const isQuoteChar = letter === '"' || letter === '\''; const isInQuotes = !!quoteChar; - if (!isInQuotes && isQuoteChar) { quoteChar = letter; } else if (quoteChar === letter) { @@ -87,6 +88,17 @@ class InteractiveClient { } } +async function runTask(fn) { + let exitCode = 0; + try { + await fn(); + } catch (err) { + exitCode = 1; + console.log('ERROR:', err); + } + ws.send(Message.encode(Message.COMPLETE, exitCode)); +} + async function mkdirp() { const dirs = Array.prototype.slice.call(arguments); await dirs.reduce(async (lastDirPromise, nextDir) => { From 7ca42bd1e4f7b791b70c1a0149a0c39aa86c54a8 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 14 May 2020 11:19:29 -0500 Subject: [PATCH 18/37] Use Session.new for creation (throws on failure) --- src/common/compute/interactive/session-with-queue.js | 4 ++++ src/common/compute/interactive/session.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/common/compute/interactive/session-with-queue.js b/src/common/compute/interactive/session-with-queue.js index 769dec160..453d153cb 100644 --- a/src/common/compute/interactive/session-with-queue.js +++ b/src/common/compute/interactive/session-with-queue.js @@ -51,6 +51,10 @@ define([ queuedTask.resolve(); this.checkTaskQueue(); } + + static async new(id, config) { + return await Session.new(id, config, SessionWithQueue); + } } class QueuedTask { diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index d038b446f..7172a07c2 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -109,8 +109,8 @@ define([ this.ws.close(); } - static async new(computeID, config={}) { - const session = new InteractiveSession(computeID, config); + static async new(computeID, config={}, SessionClass=InteractiveSession) { + const session = new SessionClass(computeID, config); await session.whenConnected(); return session; } From 211d6c8e6cc3666840d8f1a6bbf0211c0c7b15b3 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 14 May 2020 11:20:20 -0500 Subject: [PATCH 19/37] Detect compute creation errors --- src/common/compute/interactive/session.js | 18 ++++++++++++++++-- .../InteractiveCompute/InteractiveCompute.js | 3 +++ src/routers/InteractiveCompute/Session.js | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index 7172a07c2..bc19275a4 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -22,8 +22,22 @@ define([ this.connected = defer(); this.ws.onopen = () => { this.ws.send(JSON.stringify([computeID, config])); - this.checkReady(); - this.connected.resolve(); + this.ws.onmessage = async (wsMsg) => { + const data = wsMsg.data instanceof Blob ? + await wsMsg.data.text() : wsMsg.data; + + const msg = Message.decode(data); + if (msg.type === Message.COMPLETE) { + const err = msg.data; + this.ws.onmessage = null; + if (err) { + this.connected.reject(err); + } else { + this.connected.resolve(); + this.checkReady(); + } + } + }; }; this.ready = null; diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index fdb1d0b90..bdebfae02 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -5,6 +5,7 @@ const WebSocket = require('ws'); const router = require('express').Router(); const Compute = requireJS('deepforge/compute/index'); const BlobClient = requireJS('blob/BlobClient'); +const Message = requireJS('deepforge/compute/interactive/message'); const InteractiveSession = require('./Session'); class ComputeBroker { @@ -30,7 +31,9 @@ class ComputeBroker { const session = new InteractiveSession(this.blobClient, client, ws); this.initSessions.push(session); } catch (err) { + ws.send(Message.encode(Message.COMPLETE, err.message)); this.logger.warn(`Error creating session: ${err}`); + ws.close(); } }); }); diff --git a/src/routers/InteractiveCompute/Session.js b/src/routers/InteractiveCompute/Session.js index 3e8d1ef0d..9423cb842 100644 --- a/src/routers/InteractiveCompute/Session.js +++ b/src/routers/InteractiveCompute/Session.js @@ -5,6 +5,7 @@ const config = require('../../../config'); const SERVER_URL = `http://localhost:${config.server.port + 2}`; // FIXME const Channel = require('./Channel'); const EventEmitter = requireJS('deepforge/EventEmitter'); +const Message = requireJS('deepforge/compute/interactive/message'); class Session extends EventEmitter { constructor(blobClient, compute, clientSocket) { @@ -20,6 +21,7 @@ class Session extends EventEmitter { this.workerSocket = socket; this.emit('connected'); + this.clientSocket.send(Message.encode(Message.COMPLETE)); this.queuedMsgs.forEach(msg => this.workerSocket.send(msg)); this.wsChannel = new Channel(this.clientSocket, this.workerSocket); this.wsChannel.on(Channel.CLOSE, () => this.close()); From 16faa7fe53fc77ce1118edc43f8c9ddd7608af0d Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 14 May 2020 11:21:37 -0500 Subject: [PATCH 20/37] rm old debug logs --- src/common/compute/interactive/task.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/compute/interactive/task.js b/src/common/compute/interactive/task.js index 29407c43d..67bc6a7e2 100644 --- a/src/common/compute/interactive/task.js +++ b/src/common/compute/interactive/task.js @@ -25,11 +25,9 @@ define([ await wsMsg.data.text() : wsMsg.data; const msg = Message.decode(data); - console.log('received msg:', msg); this.emitMessage(msg); if (msg.type === Message.COMPLETE) { this.ws.onmessage = null; - console.log('resolving', this.msg); deferred.resolve(); } }; From 3cf060e777fb55e267ac82793132b41733fb7635 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 14 May 2020 12:16:37 -0500 Subject: [PATCH 21/37] Rename start-session.js to start.js --- src/routers/InteractiveCompute/job-files/index.js | 6 +++--- .../job-files/{start-session.js => start.js} | 0 .../InteractiveCompute/{start-session.js => start.js} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/routers/InteractiveCompute/job-files/{start-session.js => start.js} (100%) rename test/unit/routers/InteractiveCompute/{start-session.js => start.js} (94%) diff --git a/src/routers/InteractiveCompute/job-files/index.js b/src/routers/InteractiveCompute/job-files/index.js index 60ac06fa5..2ce7ebe13 100644 --- a/src/routers/InteractiveCompute/job-files/index.js +++ b/src/routers/InteractiveCompute/job-files/index.js @@ -3,7 +3,7 @@ const Files = requireJS('deepforge/plugin/GeneratedFiles'); const Templates = requireJS('plugin/GenerateJob/GenerateJob/templates/index'); const fs = require('fs'); const path = require('path'); -const START_SESSION = fs.readFileSync(path.join(__dirname, 'start-session.js'), 'utf8'); +const START_SESSION = fs.readFileSync(path.join(__dirname, 'start.js'), 'utf8'); const srcDir = path.join(__dirname, '..', '..', '..'); const interactiveDir = path.join(srcDir, 'common', 'compute', 'interactive'); const MESSAGE = fs.readFileSync(path.join(interactiveDir, 'message.js'), 'utf8'); @@ -20,12 +20,12 @@ class StartSessionFiles extends Files { createFiles(url, id) { const config = JSON.stringify({ cmd: 'node', - args: ['start-session.js', url, id], + args: ['start.js', url, id], outputInterval: -1, resultArtifacts: [] }, null, 2); this.addFile('executor_config.json', config); - this.addFile('start-session.js', START_SESSION); + this.addFile('start.js', START_SESSION); this.addFile('message.js', MESSAGE); this.addFile('utils.build.js', Templates.UTILS); this.addFile('deepforge/__init__.py', Templates.DEEPFORGE_INIT); diff --git a/src/routers/InteractiveCompute/job-files/start-session.js b/src/routers/InteractiveCompute/job-files/start.js similarity index 100% rename from src/routers/InteractiveCompute/job-files/start-session.js rename to src/routers/InteractiveCompute/job-files/start.js diff --git a/test/unit/routers/InteractiveCompute/start-session.js b/test/unit/routers/InteractiveCompute/start.js similarity index 94% rename from test/unit/routers/InteractiveCompute/start-session.js rename to test/unit/routers/InteractiveCompute/start.js index 1255eafdb..5b5358462 100644 --- a/test/unit/routers/InteractiveCompute/start-session.js +++ b/test/unit/routers/InteractiveCompute/start.js @@ -1,7 +1,7 @@ describe('InteractiveClient', function() { const testFixture = require('../../../globals'); const assert = require('assert').strict; - const {InteractiveClient} = require(testFixture.PROJECT_ROOT + '/src/routers/InteractiveCompute/job-files/start-session'); + const {InteractiveClient} = require(testFixture.PROJECT_ROOT + '/src/routers/InteractiveCompute/job-files/start'); describe('parseCommand', function() { it('should parse separate words ("ab cd efg h")', function() { From f27c843535b4c14b77a2fd5a3c04e90ee2226bc8 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 14 May 2020 12:42:35 -0500 Subject: [PATCH 22/37] Fixed bugs with start.js code on interactive compute --- .../InteractiveCompute/job-files/start.js | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/routers/InteractiveCompute/job-files/start.js b/src/routers/InteractiveCompute/job-files/start.js index 38291dd9a..a3362ef7f 100644 --- a/src/routers/InteractiveCompute/job-files/start.js +++ b/src/routers/InteractiveCompute/job-files/start.js @@ -19,7 +19,7 @@ class InteractiveClient { } async sendMessage(type, data) { - this.ws.send(Message.encode(Message[type], data)); + this.ws.send(Message.encode(type, data)); } async onMessage(msg) { @@ -35,9 +35,9 @@ class InteractiveClient { await mkdirp(...dirs); requirejs([ './utils.build', - ], function( + ], ( Utils, - ) { + ) => { const {Storage} = Utils; async function saveArtifact() { @@ -49,10 +49,10 @@ class InteractiveClient { await fs.writeFile(filePath, initFile(name, type)); } - runTask(saveArtifact); + this.runTask(saveArtifact); }); } else if (msg.type === Message.ADD_FILE) { - runTask(() => this.writeFile(msg)); + this.runTask(() => this.writeFile(msg)); } } @@ -63,6 +63,17 @@ class InteractiveClient { await fs.writeFile(filepath, content); } + async runTask(fn) { + let exitCode = 0; + try { + await fn(); + } catch (err) { + exitCode = 1; + console.log('Task failed with error:', err); + } + this.sendMessage(Message.COMPLETE, exitCode); + } + static parseCommand(cmd) { const chunks = ['']; let quoteChar = null; @@ -86,17 +97,7 @@ class InteractiveClient { } return chunks; } -} -async function runTask(fn) { - let exitCode = 0; - try { - await fn(); - } catch (err) { - exitCode = 1; - console.log('ERROR:', err); - } - ws.send(Message.encode(Message.COMPLETE, exitCode)); } async function mkdirp() { From 46c1ea2cbb3a194b42b0dfd22459045c2524db61 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 18 May 2020 09:52:10 -0500 Subject: [PATCH 23/37] Send ERROR message on job failure --- src/common/compute/backends/ComputeClient.js | 19 ++++++++++++------- src/common/compute/interactive/message.js | 2 +- src/routers/InteractiveCompute/Session.js | 6 ++++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/common/compute/backends/ComputeClient.js b/src/common/compute/backends/ComputeClient.js index c1abe2300..102873dda 100644 --- a/src/common/compute/backends/ComputeClient.js +++ b/src/common/compute/backends/ComputeClient.js @@ -46,13 +46,18 @@ define([], function() { } } - ComputeClient.prototype.QUEUED = 'queued'; - ComputeClient.prototype.PENDING = 'pending'; - ComputeClient.prototype.RUNNING = 'running'; - ComputeClient.prototype.SUCCESS = 'success'; - ComputeClient.prototype.FAILED = 'failed'; - ComputeClient.prototype.CANCELED = 'canceled'; - ComputeClient.prototype.NOT_FOUND = 'NOT_FOUND'; + const Constants = { + QUEUED: 'queued', + PENDING: 'pending', + RUNNING: 'running', + SUCCESS: 'success', + FAILED: 'failed', + CANCELED: 'canceled', + NOT_FOUND: 'NOT_FOUND', + }; + + Object.assign(ComputeClient, Constants); + Object.assign(ComputeClient.prototype, Constants); function unimplemented(logger, name) { const msg = `${name} is not implemented for current compute backend!`; diff --git a/src/common/compute/interactive/message.js b/src/common/compute/interactive/message.js index 528f28ba7..b98d87d65 100644 --- a/src/common/compute/interactive/message.js +++ b/src/common/compute/interactive/message.js @@ -9,7 +9,7 @@ } }(this, function() { const Constants = makeEnum('STDOUT', 'STDERR', 'RUN', 'ADD_ARTIFACT', - 'ADD_FILE', 'ADD_USER_DATA', 'COMPLETE'); + 'ADD_FILE', 'ADD_USER_DATA', 'COMPLETE', 'ERROR'); function makeEnum() { const names = Array.prototype.slice.call(arguments); diff --git a/src/routers/InteractiveCompute/Session.js b/src/routers/InteractiveCompute/Session.js index 9423cb842..f7df48d59 100644 --- a/src/routers/InteractiveCompute/Session.js +++ b/src/routers/InteractiveCompute/Session.js @@ -6,6 +6,7 @@ const SERVER_URL = `http://localhost:${config.server.port + 2}`; // FIXME const Channel = require('./Channel'); const EventEmitter = requireJS('deepforge/EventEmitter'); const Message = requireJS('deepforge/compute/interactive/message'); +const ComputeClient = requireJS('deepforge/compute/backends/ComputeClient'); class Session extends EventEmitter { constructor(blobClient, compute, clientSocket) { @@ -35,6 +36,11 @@ class Session extends EventEmitter { const hash = await files.upload(); this.jobInfo = this.compute.createJob(hash); this.compute.on('data', (id, data) => console.log('-->', data.toString())); + this.compute.on('end', (id, info) => { + if (info.status !== ComputeClient.SUCCESS) { + this.clientSocket.send(Message.encode(Message.ERROR, info)); + } + }); } async close () { From 3cab40a16a08b20c2109388e47a6f6bb97a285c4 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 4 Jun 2020 10:44:39 -0500 Subject: [PATCH 24/37] Combine wss ports. Add DEEPFORGE_INTERACTIVE_COMPUTE_HOST --- docker/docker-compose.yml | 2 + .../InteractiveCompute/InteractiveCompute.js | 59 ++++++++++--------- src/routers/InteractiveCompute/Session.js | 4 +- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index f90a5e1ec..de6078371 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,11 +9,13 @@ services: environment: - "MONGO_URI=mongodb://mongo:27017/deepforge" - "DEEPFORGE_HOST=https://dev.deepforge.org" + - "DEEPFORGE_INTERACTIVE_COMPUTE_HOST=https://dev-compute.deepforge.org" - "DEEPFORGE_PUBLIC_KEY=/token_keys/public_key" - "DEEPFORGE_PRIVATE_KEY=/token_keys/private_key" image: deepforge/kitchen-sink:latest ports: - "8888:8888" + - "8889:8889" volumes: - "$HOME/.deepforge/blob:/data/blob" - "${TOKEN_KEYS_DIR}:/token_keys" diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index bdebfae02..4f36fdf8b 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -12,46 +12,49 @@ class ComputeBroker { constructor(logger, blobClient) { this.logger = logger.fork('broker'); this.initSessions = []; - this.clientServer = null; - this.workerServer = null; + this.wss = null; this.blobClient = blobClient; } listen (port) { // TODO: Can I piggyback off the webgme server? Maybe using a different path? - this.clientServer = new WebSocket.Server({port}); // FIXME: this might be tricky on the current deployment - this.workerServer = new WebSocket.Server({port: port + 1}); + this.wss = new WebSocket.Server({port}); // FIXME: this might be tricky on the current deployment - this.clientServer.on('connection', ws => { + this.wss.on('connection', ws => { ws.once('message', data => { - try { - const [id, config] = JSON.parse(data); - const backend = Compute.getBackend(id); - const client = backend.getClient(this.logger, this.blobClient, config); - const session = new InteractiveSession(this.blobClient, client, ws); - this.initSessions.push(session); - } catch (err) { - ws.send(Message.encode(Message.COMPLETE, err.message)); - this.logger.warn(`Error creating session: ${err}`); - ws.close(); - } - }); - }); - - this.workerServer.on('connection', ws => { - ws.once('message', data => { - const id = data.toString(); - const index = this.initSessions.findIndex(session => session.id === id); - if (index > -1) { - const [session] = this.initSessions.splice(index, 1); - session.setWorkerWebSocket(ws); + const isClient = data.startsWith('['); + if (isClient) { + this.onClientConnected(ws, ...JSON.parse(data)); } else { - console.error(`Session not found for ${id}`); - ws.close(); + this.onWorkerConnected(ws, data); } }); }); } + + onClientConnected (ws, id, config) { + try { + const backend = Compute.getBackend(id); + const client = backend.getClient(this.logger, this.blobClient, config); + const session = new InteractiveSession(this.blobClient, client, ws); + this.initSessions.push(session); + } catch (err) { + ws.send(Message.encode(Message.COMPLETE, err.message)); + this.logger.warn(`Error creating session: ${err}`); + ws.close(); + } + } + + onWorkerConnected (ws, id) { + const index = this.initSessions.findIndex(session => session.id === id); + if (index > -1) { + const [session] = this.initSessions.splice(index, 1); + session.setWorkerWebSocket(ws); + } else { + this.logger.warn(`Session not found for ${id}`); + ws.close(); + } + } } function initialize(middlewareOpts) { diff --git a/src/routers/InteractiveCompute/Session.js b/src/routers/InteractiveCompute/Session.js index f7df48d59..b5e155665 100644 --- a/src/routers/InteractiveCompute/Session.js +++ b/src/routers/InteractiveCompute/Session.js @@ -2,7 +2,8 @@ const JobFiles = require('./job-files'); const chance = require('chance')(); const config = require('../../../config'); -const SERVER_URL = `http://localhost:${config.server.port + 2}`; // FIXME +const SERVER_URL = process.env.DEEPFORGE_INTERACTIVE_COMPUTE_HOST || + `http://localhost:${config.server.port + 1}`; const Channel = require('./Channel'); const EventEmitter = requireJS('deepforge/EventEmitter'); const Message = requireJS('deepforge/compute/interactive/message'); @@ -35,7 +36,6 @@ class Session extends EventEmitter { const files = new JobFiles(blobClient, SERVER_URL, this.id); const hash = await files.upload(); this.jobInfo = this.compute.createJob(hash); - this.compute.on('data', (id, data) => console.log('-->', data.toString())); this.compute.on('end', (id, info) => { if (info.status !== ComputeClient.SUCCESS) { this.clientSocket.send(Message.encode(Message.ERROR, info)); From a3148bc9724efa3768978e41940a2172ab6fb360 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 4 Jun 2020 11:01:02 -0500 Subject: [PATCH 25/37] ensure client ws open before sending --- src/routers/InteractiveCompute/Session.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routers/InteractiveCompute/Session.js b/src/routers/InteractiveCompute/Session.js index b5e155665..d394643be 100644 --- a/src/routers/InteractiveCompute/Session.js +++ b/src/routers/InteractiveCompute/Session.js @@ -37,7 +37,9 @@ class Session extends EventEmitter { const hash = await files.upload(); this.jobInfo = this.compute.createJob(hash); this.compute.on('end', (id, info) => { - if (info.status !== ComputeClient.SUCCESS) { + const isError = this.clientSocket.readyState === this.clientState.OPEN && + info.status !== ComputeClient.SUCCESS; + if (isError) { this.clientSocket.send(Message.encode(Message.ERROR, info)); } }); From 0f5f535aa6f831f7870a7bb084ac0777eab942f9 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 4 Jun 2020 11:01:13 -0500 Subject: [PATCH 26/37] async fn -> fn --- src/routers/InteractiveCompute/Channel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routers/InteractiveCompute/Channel.js b/src/routers/InteractiveCompute/Channel.js index d7151bd8b..09a03ede0 100644 --- a/src/routers/InteractiveCompute/Channel.js +++ b/src/routers/InteractiveCompute/Channel.js @@ -12,7 +12,7 @@ class Channel extends EventEmitter { this.ws2.onclose = () => this.close(); } - async close () { + close () { this.ws1.close(); this.ws2.close(); this.emit('close'); From 8c32beb59a5e910eb569fe67bb5de686994e03dd Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Thu, 4 Jun 2020 11:44:47 -0500 Subject: [PATCH 27/37] use router lifecycle hooks --- .../InteractiveCompute/InteractiveCompute.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index 4f36fdf8b..2d79a2c5a 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -32,6 +32,10 @@ class ComputeBroker { }); } + stop () { + this.wss.close(); + } + onClientConnected (ws, id, config) { try { const backend = Compute.getBackend(id); @@ -57,10 +61,12 @@ class ComputeBroker { } } +let broker = null; +let gmeConfig; function initialize(middlewareOpts) { const logger = middlewareOpts.logger.fork('InteractiveCompute'); - const {gmeConfig} = middlewareOpts; + gmeConfig = middlewareOpts.gmeConfig; // TODO: Do I need to add authorization for the blob client? const blobClient = new BlobClient({ logger: logger, @@ -68,18 +74,19 @@ function initialize(middlewareOpts) { server: '127.0.0.1', httpsecure: false, }); - const broker = new ComputeBroker(logger, blobClient); - broker.listen(gmeConfig.server.port + 1); + broker = new ComputeBroker(logger, blobClient); logger.debug('initializing ...'); logger.debug('ready'); } function start(callback) { + broker.listen(gmeConfig.server.port + 1); callback(); } function stop(callback) { + broker.stop(); callback(); } From bcfe155ac428f01aad85038b28d771e8ffeb46a3 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 8 Jun 2020 13:39:34 -0500 Subject: [PATCH 28/37] Update package-lock --- package-lock.json | 1109 +++++++++------------------------------------ 1 file changed, 218 insertions(+), 891 deletions(-) diff --git a/package-lock.json b/package-lock.json index faa862867..48a62ccb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -986,11 +986,6 @@ } } }, - "acorn-walk": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz", - "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==" - }, "address": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz", @@ -1886,11 +1881,6 @@ "node-releases": "^1.1.40" } }, - "bson": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz", - "integrity": "sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==" - }, "buffer": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", @@ -1916,11 +1906,6 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" }, - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" - }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", @@ -2598,8 +2583,8 @@ "version": "github:deepforge-dev/user-management-page#4872b8fd124b31381ee74ce32dabcdf137490be5", "from": "github:deepforge-dev/user-management-page", "requires": { - "body-parser": "^1.18.3", - "ejs": "^2.4.2", + "body-parser": "^1.19.0", + "ejs": "^2.7.4", "express": "4.16.3" }, "dependencies": { @@ -4002,589 +3987,109 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", - "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "optional": true, "requires": { "bindings": "^1.5.0", - "nan": "^2.12.1", - "node-pre-gyp": "*" + "nan": "^2.12.1" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "^2.0.0" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.3", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "optional": true - }, "ini": { "version": "1.3.5", - "bundled": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "needle": { - "version": "2.4.0", - "bundled": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.7", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "optional": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "requires": { - "is-glob": "^2.0.0" - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "^1.3.4" - }, - "dependencies": { - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - } - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "got": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/got/-/got-2.4.0.tgz", - "integrity": "sha1-5Ah6LNWbXSDy0WnchdIWntnon1Y=", - "requires": { - "duplexify": "^3.2.0", - "infinity-agent": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "object-assign": "^2.0.0", - "prepend-http": "^1.0.0", - "read-all-stream": "^1.0.0", - "statuses": "^1.2.1", - "timed-out": "^2.0.0" - }, - "dependencies": { + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + } + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "got": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/got/-/got-2.4.0.tgz", + "integrity": "sha1-5Ah6LNWbXSDy0WnchdIWntnon1Y=", + "requires": { + "duplexify": "^3.2.0", + "infinity-agent": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "object-assign": "^2.0.0", + "prepend-http": "^1.0.0", + "read-all-stream": "^1.0.0", + "statuses": "^1.2.1", + "timed-out": "^2.0.0" + }, + "dependencies": { "object-assign": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", @@ -5549,14 +5054,6 @@ "astw": "^2.0.0" } }, - "linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", - "requires": { - "uc.micro": "^1.0.1" - } - }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -5794,23 +5291,6 @@ "pify": "^3.0.0" } }, - "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", - "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "markdown-it-anchor": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.7.tgz", - "integrity": "sha512-REFmIaSS6szaD1bye80DMbp7ePwsPNvLTR5HunsUcZ0SG0rWJQ+Pz24R4UlTKtjKBPhxo0v0tOBDYjZQQknW8Q==" - }, "marked": { "version": "0.3.17", "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.17.tgz", @@ -5825,11 +5305,6 @@ "inherits": "^2.0.1" } }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -6195,15 +5670,6 @@ } } }, - "mongodb-core": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.19.tgz", - "integrity": "sha512-Jt4AtWUkpuW03kRdYGxga4O65O1UHlFfvvInslEfLlGi+zDMxbBe3J2NVmN9qPJ957Mn6Iz0UpMtV80cmxCVxw==", - "requires": { - "bson": "~1.0.4", - "require_optional": "~1.0.0" - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -6240,9 +5706,9 @@ "dev": true }, "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", "optional": true }, "ncp": { @@ -10546,32 +10012,11 @@ "resolve-from": "^1.0.0" } }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" - }, - "dependencies": { - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - } - } - }, "requirejs": { "version": "2.1.20", "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.1.20.tgz", "integrity": "sha1-EUgiyRfsh5NFCy2qoeubvxEB6TE=" }, - "requirejs-text": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/requirejs-text/-/requirejs-text-2.0.15.tgz", - "integrity": "sha1-ExOHM2E/xEV7fhJH6Mt1HfeqVCk=" - }, "requizzle": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz", @@ -11473,11 +10918,6 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, "uid2": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", @@ -11802,8 +11242,7 @@ "q": "1.5.1", "require-uncached": "1.0.3", "requirejs": "2.3.5", - "webgme-engine": "github:webgme/webgme-engine#master", - "webgme-user-management-page": "0.4.3" + "webgme-user-management-page": "^0.5.0" }, "dependencies": { "acorn": { @@ -12377,6 +11816,19 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", @@ -12626,7 +12078,7 @@ }, "webgme-engine": { "version": "github:webgme/webgme-engine#319eb95678cd3f23f1087f5912fc24ef7b8b73a0", - "from": "github:webgme/webgme-engine#master", + "from": "webgme-engine@github:webgme/webgme-engine#319eb95678cd3f23f1087f5912fc24ef7b8b73a0", "requires": { "adm-zip": "0.4.11", "agentkeepalive": "3.4.1", @@ -12672,6 +12124,108 @@ "winston": "2.4.3" } }, + "webgme-user-management-page": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webgme-user-management-page/-/webgme-user-management-page-0.5.0.tgz", + "integrity": "sha512-sk/sYFTiVf5ntGoNi/ZdZQsufG5UasnZCjThZW0IsW8WeuD0Njak6gVQwMLOid5XX2nLNpAmjGKSrowF0YyoLw==", + "requires": { + "body-parser": "^1.19.0", + "ejs": "^2.7.4", + "express": "4.16.3" + }, + "dependencies": { + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + } + } + }, "webgme-webhook-manager": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/webgme-webhook-manager/-/webgme-webhook-manager-0.1.1.tgz", @@ -14951,238 +14505,11 @@ "resolved": "https://registry.npmjs.org/webgme-ot/-/webgme-ot-0.0.16.tgz", "integrity": "sha512-Aict9Ka1tDDXZ9mZ9BX/4F3AV/KVf1qSoxK0UHtfxM1sPuGr3a4nXAhnccl/3jbHEjSHNphn71lmfff6y3+HmA==" }, - "webgme-rust-components": { - "version": "github:webgme/webgme-rust-components#ad446234b6c02fd722e7e454015857ee523fb172", - "from": "github:webgme/webgme-rust-components" - }, "webgme-simple-nodes": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/webgme-simple-nodes/-/webgme-simple-nodes-2.1.3.tgz", "integrity": "sha512-BpnbOp4OBz/OWwsnEn41XyBcpwkPs6vn/h+hs3KAUNVQm6k3CdyIKPKa9WUYw5SEEmJD00alfGPTbNA+YmSn8A==" }, - "webgme-user-management-page": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/webgme-user-management-page/-/webgme-user-management-page-0.4.3.tgz", - "integrity": "sha512-nNFmHJF0lobNZCdPEF06SJYNTFU6NtfoBmYiMZAsE1ZCFBsyL9qsr/TmiAP/e2/U+dSeRFYXoY7EdPi9Sb7P/A==", - "requires": { - "body-parser": "^1.18.3", - "ejs": "^2.4.2", - "express": "4.16.3" - }, - "dependencies": { - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - } - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "express": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.3", - "qs": "6.5.1", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.1", - "http-errors": "~1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "~2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "~1.6.15" - } - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": ">= 1.3.1 < 2" - } - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" - }, - "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", - "requires": { - "mime-db": "1.43.0" - } - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - } - } - }, "webgme-webhook-manager": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/webgme-webhook-manager/-/webgme-webhook-manager-0.1.0.tgz", From bed7d7267506f7fd5bd91fa0f54e1435527430fd Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Mon, 8 Jun 2020 14:15:44 -0500 Subject: [PATCH 29/37] Fix tests --- test/unit/routers/InteractiveCompute/start.js | 8 +- test/unit/utils/utils.spec.js | 89 +++++++------------ 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/test/unit/routers/InteractiveCompute/start.js b/test/unit/routers/InteractiveCompute/start.js index 5b5358462..7e6428eba 100644 --- a/test/unit/routers/InteractiveCompute/start.js +++ b/test/unit/routers/InteractiveCompute/start.js @@ -1,7 +1,11 @@ -describe('InteractiveClient', function() { +describe('InteractiveClient worker script', function() { const testFixture = require('../../../globals'); const assert = require('assert').strict; - const {InteractiveClient} = require(testFixture.PROJECT_ROOT + '/src/routers/InteractiveCompute/job-files/start'); + let InteractiveClient; + + before(() => { + InteractiveClient = require(testFixture.PROJECT_ROOT + '/src/routers/InteractiveCompute/job-files/start').InteractiveClient; + }); describe('parseCommand', function() { it('should parse separate words ("ab cd efg h")', function() { diff --git a/test/unit/utils/utils.spec.js b/test/unit/utils/utils.spec.js index 51cae25f1..1e6bdf730 100644 --- a/test/unit/utils/utils.spec.js +++ b/test/unit/utils/utils.spec.js @@ -1,13 +1,13 @@ /*jshint node:true, mocha:true*/ 'use strict'; -var testFixture = require('../../globals'), - path = testFixture.path, - assert = require('assert'), - SEED_DIR = testFixture.DF_SEED_DIR, - fs = require('fs'); +describe('misc utils', function () { + var testFixture = require('../../globals'), + path = testFixture.path, + assert = require('assert'), + SEED_DIR = testFixture.DF_SEED_DIR, + fs = require('fs'); -describe('utils', function () { var gmeConfig = testFixture.getGmeConfig(), GraphChecker = testFixture.requirejs('deepforge/GraphChecker'), MODELS_DIR = path.join(__dirname, '..', 'test-cases', 'models'), @@ -22,58 +22,37 @@ describe('utils', function () { commitHash, checker; - before(function (done) { - testFixture.clearDBAndGetGMEAuth(gmeConfig, projectName) - .then(function (gmeAuth_) { - gmeAuth = gmeAuth_; - // This uses in memory storage. Use testFixture.getMongoStorage to persist test to database. - storage = testFixture.getMemoryStorage(logger, gmeConfig, gmeAuth); - return storage.openDatabase(); - }) - .then(function () { - var importParam = { - projectSeed: path.join(SEED_DIR, 'devUtilTests', 'devUtilTests.webgmex'), - projectName: projectName, - branchName: 'master', - logger: logger, - gmeConfig: gmeConfig - }; + before(async function () { + gmeAuth = await testFixture.clearDBAndGetGMEAuth(gmeConfig, projectName); + storage = testFixture.getMemoryStorage(logger, gmeConfig, gmeAuth); + await storage.openDatabase(); + const importParam = { + projectSeed: path.join(SEED_DIR, 'devUtilTests', 'devUtilTests.webgmex'), + projectName: projectName, + branchName: 'master', + logger: logger, + gmeConfig: gmeConfig + }; - return testFixture.importProject(storage, importParam); - }) - .then(function (importResult) { - project = importResult.project; - core = importResult.core; - checker = new GraphChecker({ - core: core, - ignore: { - attributes: ['calculateDimensionality', 'dimensionalityTransform'] - } - }); - commitHash = importResult.commitHash; - return project.createBranch('test', commitHash); - }) - .then(function () { - return project.getBranchHash('test'); - }) - .then(function (branchHash) { - return Q.ninvoke(project, 'loadObject', branchHash); - }) - .then(function (commitObject) { - return Q.ninvoke(core, 'loadRoot', commitObject.root); - }) - .then(function (root) { - rootNode = root; - }) - .nodeify(done); + const importResult = await testFixture.importProject(storage, importParam); + project = importResult.project; + core = importResult.core; + checker = new GraphChecker({ + core: core, + ignore: { + attributes: ['calculateDimensionality', 'dimensionalityTransform'] + } + }); + commitHash = importResult.commitHash; + await project.createBranch('test', commitHash); + const branchHash = await project.getBranchHash('test'); + const commitObject = await Q.ninvoke(project, 'loadObject', branchHash); + rootNode = await Q.ninvoke(core, 'loadRoot', commitObject.root); }); - after(function (done) { - storage.closeDatabase() - .then(function () { - return gmeAuth.unload(); - }) - .nodeify(done); + after(async function () { + await storage.closeDatabase(); + await gmeAuth.unload(); }); var run = function(nodePath, filename, result, done) { From f125462bd3b809995f37b4046c2c13fc1f1c4be6 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Tue, 9 Jun 2020 09:12:51 -0500 Subject: [PATCH 30/37] Add integration tests and auth for blob client --- src/common/compute/interactive/session.js | 33 +++++++++++---- src/common/compute/interactive/task.js | 15 ++++++- .../InteractiveCompute/InteractiveCompute.js | 27 ++++++------ src/routers/InteractiveCompute/Session.js | 3 +- test/integration/InteractiveCompute.js | 42 +++++++++++++++++++ 5 files changed, 96 insertions(+), 24 deletions(-) create mode 100644 test/integration/InteractiveCompute.js diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index bc19275a4..5245d9fc8 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -12,19 +12,19 @@ define([ ) { const {defer} = utils; const {CommandFailedError} = Errors; + const isNodeJs = typeof window === 'undefined'; + const WebSocket = isNodeJs ? require('ws') : window.WebSocket; + class InteractiveSession { - constructor(computeID, config) { + constructor(computeID, config={}) { this.currentTask = null; - // TODO: Get the server address... - // TODO: detect if ssl - const address = 'ws://localhost:8889'; + const address = this.getServerURL(); this.ws = new WebSocket(address); this.connected = defer(); this.ws.onopen = () => { - this.ws.send(JSON.stringify([computeID, config])); + this.ws.send(JSON.stringify([computeID, config, this.getGMEToken()])); this.ws.onmessage = async (wsMsg) => { - const data = wsMsg.data instanceof Blob ? - await wsMsg.data.text() : wsMsg.data; + const data = await Task.getMessageData(wsMsg); const msg = Message.decode(data); if (msg.type === Message.COMPLETE) { @@ -43,6 +43,25 @@ define([ this.ready = null; } + getServerURL() { + if (isNodeJs) { + return 'ws://127.0.0.1:8081'; + } + const isSecure = location.protocol.includes('s'); + const protocol = isSecure ? 'wss' : 'ws'; + //const address = location.origin.replace(location.protocol, protocol); + return `${protocol}://localhost:8889`; // TODO + } + + getGMEToken() { + if (isNodeJs) { + return ''; + } + + const [, token] = (document.cookie || '').split('='); + return token; + } + checkReady() { if (this.isIdle() && this.ready) { this.ready.resolve(); diff --git a/src/common/compute/interactive/task.js b/src/common/compute/interactive/task.js index 67bc6a7e2..702404272 100644 --- a/src/common/compute/interactive/task.js +++ b/src/common/compute/interactive/task.js @@ -9,6 +9,7 @@ define([ utils, ) { + const isNodeJs = typeof window === 'undefined'; class Task extends EventEmitter { constructor(ws, msg) { super(); @@ -21,8 +22,7 @@ define([ this.ws.send(this.msg.encode()); this.ws.onmessage = async wsMsg => { - const data = wsMsg.data instanceof Blob ? - await wsMsg.data.text() : wsMsg.data; + const data = await Task.getMessageData(wsMsg); const msg = Message.decode(data); this.emitMessage(msg); @@ -38,6 +38,17 @@ define([ emitMessage(msg) { this.emit(msg.type, msg.data); } + + static async getMessageData(wsMsg) { + if (isNodeJs) { + return wsMsg.data; + } + + const data = wsMsg.data instanceof Blob ? + await wsMsg.data.text() : wsMsg.data; + return data; + } + } return Task; diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index 2d79a2c5a..49bc51635 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -9,11 +9,10 @@ const Message = requireJS('deepforge/compute/interactive/message'); const InteractiveSession = require('./Session'); class ComputeBroker { - constructor(logger, blobClient) { + constructor(logger) { this.logger = logger.fork('broker'); this.initSessions = []; this.wss = null; - this.blobClient = blobClient; } listen (port) { @@ -24,7 +23,7 @@ class ComputeBroker { ws.once('message', data => { const isClient = data.startsWith('['); if (isClient) { - this.onClientConnected(ws, ...JSON.parse(data)); + this.onClientConnected(port, ws, ...JSON.parse(data)); } else { this.onWorkerConnected(ws, data); } @@ -36,11 +35,18 @@ class ComputeBroker { this.wss.close(); } - onClientConnected (ws, id, config) { + onClientConnected (port, ws, id, config, gmeToken) { try { const backend = Compute.getBackend(id); - const client = backend.getClient(this.logger, this.blobClient, config); - const session = new InteractiveSession(this.blobClient, client, ws); + const blobClient = new BlobClient({ + logger: this.logger.fork('BlobClient'), + serverPort: port-1, + server: '127.0.0.1', + httpsecure: false, + webgmeToken: gmeToken + }); + const client = backend.getClient(this.logger, blobClient, config); + const session = new InteractiveSession(blobClient, client, ws); this.initSessions.push(session); } catch (err) { ws.send(Message.encode(Message.COMPLETE, err.message)); @@ -67,14 +73,7 @@ function initialize(middlewareOpts) { const logger = middlewareOpts.logger.fork('InteractiveCompute'); gmeConfig = middlewareOpts.gmeConfig; - // TODO: Do I need to add authorization for the blob client? - const blobClient = new BlobClient({ - logger: logger, - serverPort: gmeConfig.server.port, - server: '127.0.0.1', - httpsecure: false, - }); - broker = new ComputeBroker(logger, blobClient); + broker = new ComputeBroker(logger); logger.debug('initializing ...'); logger.debug('ready'); diff --git a/src/routers/InteractiveCompute/Session.js b/src/routers/InteractiveCompute/Session.js index d394643be..14533f5d2 100644 --- a/src/routers/InteractiveCompute/Session.js +++ b/src/routers/InteractiveCompute/Session.js @@ -1,4 +1,5 @@ /* globals requireJS */ +const WebSocket = require('ws'); const JobFiles = require('./job-files'); const chance = require('chance')(); const config = require('../../../config'); @@ -37,7 +38,7 @@ class Session extends EventEmitter { const hash = await files.upload(); this.jobInfo = this.compute.createJob(hash); this.compute.on('end', (id, info) => { - const isError = this.clientSocket.readyState === this.clientState.OPEN && + const isError = this.clientSocket.readyState === WebSocket.OPEN && info.status !== ComputeClient.SUCCESS; if (isError) { this.clientSocket.send(Message.encode(Message.ERROR, info)); diff --git a/test/integration/InteractiveCompute.js b/test/integration/InteractiveCompute.js new file mode 100644 index 000000000..ec83f3232 --- /dev/null +++ b/test/integration/InteractiveCompute.js @@ -0,0 +1,42 @@ +describe('InteractiveCompute', function() { + const assert = require('assert').strict; + const {promisify} = require('util'); + const testFixture = require('../globals'); + const gmeConfig = testFixture.getGmeConfig(); + const server = new testFixture.WebGME.standaloneServer(gmeConfig); + server.start = promisify(server.start); + server.stop = promisify(server.stop); + let session; + + before(async function() { + await server.start(); + }); + + beforeEach(async function() { + const Session = testFixture.requirejs('deepforge/compute/interactive/session-with-queue'); + session = await Session.new('local'); + }); + afterEach(() => session.close()); + + it('should be able to exec commands', async function() { + const {exitCode, stdout} = await session.exec('ls'); + assert.equal(exitCode, 0); + const files = stdout.split('\n'); + assert(files.includes('start.js')); + }); + + it('should be able to spawn commands', function(done) { + const Message = testFixture.requirejs('deepforge/compute/interactive/message'); + const task = session.spawn('ls'); + task.on(Message.COMPLETE, exitCode => { + assert.equal(exitCode, 0); + done(); + }); + }); + + it('should be able to add files', async function() { + await session.addFile('test.txt', 'hello world'); + const {stdout} = await session.exec('cat test.txt'); + assert.equal(stdout, 'hello world'); + }); +}); From 1d924d20c6373be781f1831acb6600f226de184a Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 19 Jun 2020 12:56:59 -0500 Subject: [PATCH 31/37] Add InteractiveComputeHost info to gmeConfig.extensions --- config/config.extensions.js | 6 ++++++ config/index.js | 3 ++- src/common/compute/interactive/session.js | 17 +++++++++-------- 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 config/config.extensions.js diff --git a/config/config.extensions.js b/config/config.extensions.js new file mode 100644 index 000000000..df926579f --- /dev/null +++ b/config/config.extensions.js @@ -0,0 +1,6 @@ + +module.exports = config => { + config.extensions = {}; + config.extensions.InteractiveComputeHost = process.env.DEEPFORGE_INTERACTIVE_COMPUTE_HOST; + return config; +}; diff --git a/config/index.js b/config/index.js index 4586eaef9..d7842c13a 100644 --- a/config/index.js +++ b/config/index.js @@ -11,5 +11,6 @@ var env = process.env.NODE_ENV || 'default', config = require(configFilename), validateConfig = require('webgme/config/validator'); -validateConfig(configFilename); +validateConfig(config); +config = require(__dirname + '/config.extensions.js')(config); module.exports = config; diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index 5245d9fc8..588687411 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -4,11 +4,13 @@ define([ 'deepforge/compute/interactive/task', 'deepforge/compute/interactive/message', 'deepforge/compute/interactive/errors', + 'deepforge/gmeConfig', ], function( utils, Task, Message, Errors, + gmeConfig, ) { const {defer} = utils; const {CommandFailedError} = Errors; @@ -18,7 +20,8 @@ define([ class InteractiveSession { constructor(computeID, config={}) { this.currentTask = null; - const address = this.getServerURL(); + const address = gmeConfig.extensions.InteractiveComputeHost || + this.getDefaultServerURL(); this.ws = new WebSocket(address); this.connected = defer(); this.ws.onopen = () => { @@ -43,14 +46,12 @@ define([ this.ready = null; } - getServerURL() { - if (isNodeJs) { - return 'ws://127.0.0.1:8081'; - } - const isSecure = location.protocol.includes('s'); + getDefaultServerURL() { + const isSecure = !isNodeJs && location.protocol.includes('s'); const protocol = isSecure ? 'wss' : 'ws'; - //const address = location.origin.replace(location.protocol, protocol); - return `${protocol}://localhost:8889`; // TODO + const defaultHost = isNodeJs ? '127.0.0.1' : + location.origin.replace(location.protocol + '://', ''); + return `${protocol}://${defaultHost}:${gmeConfig.server.port + 1}`; } getGMEToken() { From 077134117124cae3c28612ea7e817a3f81a29fe5 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 19 Jun 2020 13:28:05 -0500 Subject: [PATCH 32/37] Fixed default server URL detection --- src/common/compute/interactive/session.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js index 588687411..458b05969 100644 --- a/src/common/compute/interactive/session.js +++ b/src/common/compute/interactive/session.js @@ -50,7 +50,9 @@ define([ const isSecure = !isNodeJs && location.protocol.includes('s'); const protocol = isSecure ? 'wss' : 'ws'; const defaultHost = isNodeJs ? '127.0.0.1' : - location.origin.replace(location.protocol + '://', ''); + location.origin + .replace(location.protocol + '//', '') + .replace(/:[0-9]+$/, ''); return `${protocol}://${defaultHost}:${gmeConfig.server.port + 1}`; } From 268ed9c2096ff7b39ba6debd33d34016fe7c887b Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 19 Jun 2020 13:31:23 -0500 Subject: [PATCH 33/37] Update the router to use the interactive compute from the gme config --- src/routers/InteractiveCompute/Session.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routers/InteractiveCompute/Session.js b/src/routers/InteractiveCompute/Session.js index 14533f5d2..d7bb6ee5f 100644 --- a/src/routers/InteractiveCompute/Session.js +++ b/src/routers/InteractiveCompute/Session.js @@ -2,9 +2,9 @@ const WebSocket = require('ws'); const JobFiles = require('./job-files'); const chance = require('chance')(); -const config = require('../../../config'); -const SERVER_URL = process.env.DEEPFORGE_INTERACTIVE_COMPUTE_HOST || - `http://localhost:${config.server.port + 1}`; +const gmeConfig = requireJS('deepforge/gmeConfig'); +const SERVER_URL = gmeConfig.extensions.InteractiveComputeHost || + `http://localhost:${gmeConfig.server.port + 1}`; const Channel = require('./Channel'); const EventEmitter = requireJS('deepforge/EventEmitter'); const Message = requireJS('deepforge/compute/interactive/message'); From d1f52aa4e93134bcca226a49871ea23fe2f0cc5d Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 19 Jun 2020 13:43:09 -0500 Subject: [PATCH 34/37] Remove old debug comments --- src/routers/InteractiveCompute/InteractiveCompute.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index 49bc51635..29aebabcb 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -16,8 +16,7 @@ class ComputeBroker { } listen (port) { - // TODO: Can I piggyback off the webgme server? Maybe using a different path? - this.wss = new WebSocket.Server({port}); // FIXME: this might be tricky on the current deployment + this.wss = new WebSocket.Server({port}); this.wss.on('connection', ws => { ws.once('message', data => { From 7d6d875f2eab911dcafe91fa5cc07db682261b00 Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 19 Jun 2020 14:26:17 -0500 Subject: [PATCH 35/37] add start/stop interactive compute logs --- src/routers/InteractiveCompute/InteractiveCompute.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index 29aebabcb..4b5a68fbb 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -79,11 +79,13 @@ function initialize(middlewareOpts) { } function start(callback) { + console.log('>>> start'); broker.listen(gmeConfig.server.port + 1); callback(); } function stop(callback) { + console.log('>>> stop'); broker.stop(); callback(); } From 50d664a2155d105838580dccc1a37fcfa8efeacd Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 19 Jun 2020 14:54:49 -0500 Subject: [PATCH 36/37] Stop the server after the interactive compute integration tests --- test/integration/InteractiveCompute.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/integration/InteractiveCompute.js b/test/integration/InteractiveCompute.js index ec83f3232..87cd62521 100644 --- a/test/integration/InteractiveCompute.js +++ b/test/integration/InteractiveCompute.js @@ -11,6 +11,9 @@ describe('InteractiveCompute', function() { before(async function() { await server.start(); }); + after(async function() { + await server.stop(); + }); beforeEach(async function() { const Session = testFixture.requirejs('deepforge/compute/interactive/session-with-queue'); From ee768734e3b99cec20d7149485a38d55a16097aa Mon Sep 17 00:00:00 2001 From: Brian Broll Date: Fri, 19 Jun 2020 15:33:50 -0500 Subject: [PATCH 37/37] Remove debug logs --- src/routers/InteractiveCompute/InteractiveCompute.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/routers/InteractiveCompute/InteractiveCompute.js b/src/routers/InteractiveCompute/InteractiveCompute.js index 4b5a68fbb..29aebabcb 100644 --- a/src/routers/InteractiveCompute/InteractiveCompute.js +++ b/src/routers/InteractiveCompute/InteractiveCompute.js @@ -79,13 +79,11 @@ function initialize(middlewareOpts) { } function start(callback) { - console.log('>>> start'); broker.listen(gmeConfig.server.port + 1); callback(); } function stop(callback) { - console.log('>>> stop'); broker.stop(); callback(); }