From 285197f7f18fbb25eae940b4181db3bbd7e5f0e5 Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Thu, 27 Jan 2022 11:46:06 +0000 Subject: [PATCH] Add fs get and write functions --- examples/react-app/package-lock.json | 26 +++-- examples/react-app/package.json | 2 +- examples/react-app/src/App.tsx | 29 ++++- package-lock.json | 46 ++++---- package.json | 8 +- src/core/devbook.ts | 36 +++++- src/core/evaluationContext.ts | 137 ++++++++++++++++++----- src/core/runningEnvironment/webSocket.ts | 72 ++++++++++++ src/react/useDevbook.ts | 3 + 9 files changed, 290 insertions(+), 69 deletions(-) diff --git a/examples/react-app/package-lock.json b/examples/react-app/package-lock.json index ff64bad..aac3220 100644 --- a/examples/react-app/package-lock.json +++ b/examples/react-app/package-lock.json @@ -17,7 +17,7 @@ "@codemirror/lang-javascript": "^0.19.6", "@codemirror/language": "^0.19.7", "@codemirror/matchbrackets": "^0.19.3", - "@devbookhq/sdk": "^1.0.2", + "@devbookhq/sdk": "file:../..", "@devbookhq/splitter": "^1.3.2", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -36,8 +36,7 @@ }, "../..": { "name": "@devbookhq/sdk", - "version": "0.1.5", - "extraneous": true, + "version": "1.0.3", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@rollup/plugin-node-resolve": "^13.1.3", @@ -2117,9 +2116,8 @@ "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==" }, "node_modules/@devbookhq/sdk": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@devbookhq/sdk/-/sdk-1.0.2.tgz", - "integrity": "sha512-ILPx0zDTXEJYhVMapO73ILwvQAzcYV6LZlXFNtk+ALlkir+g/w0lJCPEsMRR0h893g81coZ36T2cebiQHSVtfw==" + "resolved": "../..", + "link": true }, "node_modules/@devbookhq/splitter": { "version": "1.3.2", @@ -17308,9 +17306,19 @@ "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==" }, "@devbookhq/sdk": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@devbookhq/sdk/-/sdk-1.0.2.tgz", - "integrity": "sha512-ILPx0zDTXEJYhVMapO73ILwvQAzcYV6LZlXFNtk+ALlkir+g/w0lJCPEsMRR0h893g81coZ36T2cebiQHSVtfw==" + "version": "file:../..", + "requires": { + "@rollup/plugin-node-resolve": "^13.1.3", + "@types/node": "^17.0.10", + "@types/react": "^17.0.38", + "@types/react-dom": "^17.0.11", + "rollup": "^2.64.0", + "rollup-plugin-auto-external": "^2.0.0", + "rollup-plugin-polyfill-node": "^0.8.0", + "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-typescript2": "^0.31.1", + "typescript": "^4.5.4" + } }, "@devbookhq/splitter": { "version": "1.3.2", diff --git a/examples/react-app/package.json b/examples/react-app/package.json index 19c142f..44ac52b 100644 --- a/examples/react-app/package.json +++ b/examples/react-app/package.json @@ -13,7 +13,7 @@ "@codemirror/lang-javascript": "^0.19.6", "@codemirror/language": "^0.19.7", "@codemirror/matchbrackets": "^0.19.3", - "@devbookhq/sdk": "^1.0.2", + "@devbookhq/sdk": "file:../..", "@devbookhq/splitter": "^1.3.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/examples/react-app/src/App.tsx b/examples/react-app/src/App.tsx index aefa358..ee748eb 100644 --- a/examples/react-app/src/App.tsx +++ b/examples/react-app/src/App.tsx @@ -1,6 +1,7 @@ import { useState, useCallback, + useEffect, } from 'react'; import { @@ -19,7 +20,7 @@ console.log('Hostname:', os.hostname()); console.log(process.env)` const initialCmd = -`ls -l + `ls -l ` function App() { @@ -28,8 +29,30 @@ function App() { const [cmd, setCmd] = useState(initialCmd); const [execType, setExecType] = useState('code'); - const { stderr, stdout, runCode, runCmd, status } = useDevbook({ debug: true, env: Env.NodeJS }); - console.log({ stdout, stderr }) + const { + stderr, + stdout, + runCode, + runCmd, + status, + fs, + } = useDevbook({ debug: true, env: Env.NodeJS }); + console.log({ stdout, stderr }); + + useEffect(() => { + async function init() { + if (!fs) return + if (status !== DevbookStatus.Connected) return + + // setInterval(async () => { + const random = Math.random() + await fs.write('/src/indexues.js', random.toString()) + const content = await fs.get('/src/indexues.js') + console.log('content', content) + // }, 2000) + } + init() + }, [fs, status]) const handleEditorChange = useCallback((content: string) => { if (execType === 'code') { diff --git a/package-lock.json b/package-lock.json index ed27557..1fdef20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,24 @@ { "name": "@devbookhq/sdk", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@devbookhq/sdk", - "version": "1.0.3", + "version": "1.0.4", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@rollup/plugin-node-resolve": "^13.1.3", - "@types/node": "^17.0.10", + "@types/node": "^17.0.12", "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", - "rollup": "^2.64.0", + "rollup": "^2.66.1", "rollup-plugin-auto-external": "^2.0.0", "rollup-plugin-polyfill-node": "^0.8.0", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.1", - "typescript": "^4.5.4" + "typescript": "^4.5.5" } }, "node_modules/@babel/code-frame": { @@ -142,9 +142,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz", - "integrity": "sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==", + "version": "17.0.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.12.tgz", + "integrity": "sha512-4YpbAsnJXWYK/fpTVFlMIcUIho2AYCi4wg5aNPrG1ng7fn/1/RZfCIpRCiBX+12RVa34RluilnvCqD+g3KiSiA==", "dev": true }, "node_modules/@types/prop-types": { @@ -842,9 +842,9 @@ } }, "node_modules/rollup": { - "version": "2.64.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.64.0.tgz", - "integrity": "sha512-+c+lbw1lexBKSMb1yxGDVfJ+vchJH3qLbmavR+awDinTDA2C5Ug9u7lkOzj62SCu0PKUExsW36tpgW7Fmpn3yQ==", + "version": "2.66.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.66.1.tgz", + "integrity": "sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -1149,9 +1149,9 @@ "dev": true }, "node_modules/typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -1308,9 +1308,9 @@ "dev": true }, "@types/node": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz", - "integrity": "sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==", + "version": "17.0.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.12.tgz", + "integrity": "sha512-4YpbAsnJXWYK/fpTVFlMIcUIho2AYCi4wg5aNPrG1ng7fn/1/RZfCIpRCiBX+12RVa34RluilnvCqD+g3KiSiA==", "dev": true }, "@types/prop-types": { @@ -1878,9 +1878,9 @@ } }, "rollup": { - "version": "2.64.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.64.0.tgz", - "integrity": "sha512-+c+lbw1lexBKSMb1yxGDVfJ+vchJH3qLbmavR+awDinTDA2C5Ug9u7lkOzj62SCu0PKUExsW36tpgW7Fmpn3yQ==", + "version": "2.66.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.66.1.tgz", + "integrity": "sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -2124,9 +2124,9 @@ "dev": true }, "typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true }, "universalify": { diff --git a/package.json b/package.json index 49ff4cc..87c5a18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devbookhq/sdk", - "version": "1.0.3", + "version": "1.0.4", "description": "Devbook allows visitors of your docs to interact with and execute any code snippet or shell command in a private VM", "homepage": "https://usedevbook.com", "license": "SEE LICENSE IN LICENSE", @@ -24,15 +24,15 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "^13.1.3", - "@types/node": "^17.0.10", + "@types/node": "^17.0.12", "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", - "rollup": "^2.64.0", + "rollup": "^2.66.1", "rollup-plugin-auto-external": "^2.0.0", "rollup-plugin-polyfill-node": "^0.8.0", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.1", - "typescript": "^4.5.4" + "typescript": "^4.5.5" }, "files": [ "lib", diff --git a/src/core/devbook.ts b/src/core/devbook.ts index b84faf5..ddb73c9 100644 --- a/src/core/devbook.ts +++ b/src/core/devbook.ts @@ -7,6 +7,11 @@ import { SessionStatus } from './session/sessionManager' const generateExecutionID = makeIDGenerator(6) +export interface FS { + get: (path: string) => Promise + write: (path: string, content: string) => Promise +} + /** * States of a {@link Devbook} connection to a VM. */ @@ -36,6 +41,13 @@ class Devbook { private executionID: string private readonly contextID: string + get fs(): FS { + return { + get: this.getFile.bind(this), + write: this.writeFile.bind(this), + } + } + private _isDestroyed = false private get isDestroyed() { return this._isDestroyed @@ -137,6 +149,26 @@ class Devbook { }) } + async getFile(path: string) { + if (this.status !== DevbookStatus.Connected) throw new Error('Not connected to the VM yet.') + + return this.context.getFile({ + templateID: this.opts.env, + path, + }) + + } + + async writeFile(path: string, content: string) { + if (this.status !== DevbookStatus.Connected) throw new Error('Not connected to the VM yet.') + + return this.context.updateFile({ + templateID: this.opts.env, + path, + content, + }) + } + /** * Run `command` in the VM. * @@ -145,7 +177,7 @@ class Devbook { * @param command Command to run */ runCmd(command: string) { - if (this.status === DevbookStatus.Disconnected) return + if (this.status !== DevbookStatus.Connected) throw new Error('Not connected to the VM yet.') this.executionID = generateExecutionID() @@ -164,7 +196,7 @@ class Devbook { * @param code Code to run */ runCode(code: string) { - if (this.status === DevbookStatus.Disconnected) return + if (this.status !== DevbookStatus.Connected) throw new Error('Not connected to the VM yet.') this.executionID = generateExecutionID() diff --git a/src/core/evaluationContext.ts b/src/core/evaluationContext.ts index dc67b42..8be7449 100644 --- a/src/core/evaluationContext.ts +++ b/src/core/evaluationContext.ts @@ -25,6 +25,7 @@ export interface EvaluationContextOpts { } type FSWriteSubscriber = (payload: rws.RunningEnvironment_FSEventWrite['payload']) => void +type FileContentSubscriber = (payload: rws.RunningEnvironment_FileContent['payload']) => void class EvaluationContext { private readonly logger: Logger @@ -34,6 +35,7 @@ class EvaluationContext { } private fsWriteSubscribers: FSWriteSubscriber[] = [] + private fileContentSubscribers: FileContentSubscriber[] = [] private envs: RunningEnvironment[] = [] private readonly unsubscribeConnHandler: () => void @@ -77,6 +79,88 @@ class EvaluationContext { this.fsWriteSubscribers = [] } + async getFile({ templateID, path: filepath }: { templateID: Env, path: string }) { + this.logger.log('Get file', { templateID, path }) + const env = this.envs.find(env => env.templateID === templateID) + if (!env) { + this.logger.error('Environment not found', { templateID, path }) + return + } + if (!env.isReady) { + this.logger.error('Environment is not ready', { templateID, path }) + return + } + + let resolveFileContent: (content: string) => void + const fileContent = new Promise((resolve, reject) => { + resolveFileContent = resolve + setTimeout(() => { + reject('Timeout') + }, 10000) + }) + + const fileContentSubscriber: FileContentSubscriber = (payload) => { + if (!payload.path.endsWith(filepath)) return + resolveFileContent(payload.content) + } + this.subscribeFileContent(fileContentSubscriber) + + envWS.getFile(this.opts.conn, { + path: filepath, + environmentID: env.id, + }) + + try { + const content = await fileContent + return content + } catch (err: any) { + throw new Error(`Error retriving file ${filepath}: ${err.message}`) + } finally { + this.unsubscribeFileContent(fileContentSubscriber) + } + } + + async updateFile({ templateID, path: filepath, content }: { templateID: Env, path: string, content: string }) { + this.logger.log('Update file', { templateID, filepath }) + const env = this.envs.find(env => env.templateID === templateID) + if (!env) { + this.logger.error('Environment not found', { templateID, filepath }) + return + } + if (!env.isReady) { + this.logger.error('Environment is not ready', { templateID, filepath }) + return + } + + let resolveFileWritten: (value: void) => void + const fileWritten = new Promise((resolve, reject) => { + resolveFileWritten = resolve + setTimeout(() => { + reject('Timeout') + }, 10000) + }) + + const fsWriteSubscriber = (payload: rws.RunningEnvironment_FSEventWrite['payload']) => { + if (!payload.path.endsWith(filepath)) return + resolveFileWritten() + } + this.subscribeFSWrite(fsWriteSubscriber) + + envWS.writeFile(this.opts.conn, { + environmentID: env.id, + path: filepath, + content, + }) + + try { + await fileWritten + } catch (err: any) { + throw new Error(`File ${filepath} not written to VM: ${err.message}`) + } finally { + this.unsubscribeFSWrite(fsWriteSubscriber) + } + } + async executeCode({ templateID, executionID, code }: { templateID: Env, executionID: string, code: string }) { this.logger.log('Execute code', { templateID, executionID }) @@ -94,37 +178,12 @@ class EvaluationContext { const basename = `${executionID}${extension}` const filepath = path.join('/src', basename) - // TODO: Make sure that the file is already written when we send the execCmd message. - // let resolveFileWritten: (value: void) => void - // const fileWritten = new Promise((resolve, reject) => { - // resolveFileWritten = resolve - // setTimeout(() => { - // reject() - // }, 10000) - // }) - - // const fsWriteSubscriber = (payload: rws.RunningEnvironment_FSEventWrite['payload']) => { - // console.log('fs >>') - // if (!payload.path.endsWith(filepath)) return - // resolveFileWritten() - // } - // this.subscribeFSWrite(fsWriteSubscriber) - envWS.writeFile(this.opts.conn, { environmentID: env.id, path: filepath, content: code, }) - // try { - // // await fileWritten - // } catch (err: any) { - // this.logger.error(`File ${filepath} not written to VM`) - // return - // } finally { - // this.unsubscribeFSWrite(fsWriteSubscriber) - // } - // Send command to execute file as code const vmFilepath = path.join(templates[templateID].root_dir, filepath) const command = templates[templateID].toCommand(vmFilepath) @@ -170,13 +229,22 @@ class EvaluationContext { this.opts.onEnvChange?.(env) } + private subscribeFileContent(subscriber: FileContentSubscriber) { + this.fileContentSubscribers.push(subscriber) + } + + private unsubscribeFileContent(subscriber: FileContentSubscriber) { + const index = this.fileContentSubscribers.indexOf(subscriber) + if (index > -1) { + this.fileContentSubscribers.splice(index, 1); + } + } + private subscribeFSWrite(subscriber: FSWriteSubscriber) { - console.log('len', this.fsWriteSubscribers.length) this.fsWriteSubscribers.push(subscriber) } private unsubscribeFSWrite(subscriber: FSWriteSubscriber) { - console.log('len', this.fsWriteSubscribers.length) const index = this.fsWriteSubscribers.indexOf(subscriber) if (index > -1) { this.fsWriteSubscribers.splice(index, 1); @@ -215,6 +283,11 @@ class EvaluationContext { this.vmenv_handleFSEventWrite(msg.payload) break } + case rws.MessageType.RunningEnvironment.FileContent: { + const msg = message as rws.RunningEnvironment_FileContent + this.vmenv_handleFileContent(msg.payload) + break + } default: this.logger.warn('Unknown message type', { message }) } @@ -239,6 +312,16 @@ class EvaluationContext { this.fsWriteSubscribers.forEach(s => s(payload)) } + private vmenv_handleFileContent(payload: rws.RunningEnvironment_FileContent['payload']) { + this.logger.log('[vmenv] Handling "FileContent"', { environmentID: payload.environmentID, path: payload.path }) + const env = this.envs.find(e => e.id === payload.environmentID) + if (!env) { + this.logger.warn('Environment not found', { payload }) + return + } + this.fileContentSubscribers.forEach(s => s(payload)) + } + private vmenv_handleStartAck(payload: rws.RunningEnvironment_StartAck['payload']) { this.logger.log('[vmenv] Handling "StartAck"', { payload }) const env = this.envs.find(e => e.id === payload.environmentID) diff --git a/src/core/runningEnvironment/webSocket.ts b/src/core/runningEnvironment/webSocket.ts index 54c1496..4df56a4 100644 --- a/src/core/runningEnvironment/webSocket.ts +++ b/src/core/runningEnvironment/webSocket.ts @@ -2,6 +2,75 @@ import * as rws from 'src/common-ts/RunnerWebSocket' import { TemplateConfig } from 'src/common-ts/TemplateConfig' import { WebSocketConnection } from 'src/core/webSocketConnection' + +/** + * Request to receive the `RunningEnvironment.DirContent` message in the future + * containting the content of the directory at the given path. + */ +function listDir( + conn: WebSocketConnection, { + environmentID, + path, + }: { + environmentID: string, + path: string, + }, +) { + const msg: rws.RunningEnvironment_ListDir = { + type: rws.MessageType.RunningEnvironment.ListDir, + payload: { + environmentID, + path, + }, + } + conn.send(msg) +} + +/** + * Request to create a dir in the environment's filesystem. + */ +function createDir( + conn: WebSocketConnection, { + environmentID, + path, + }: { + environmentID: string, + path: string, + }, +) { + const msg: rws.RunningEnvironment_CreateDir = { + type: rws.MessageType.RunningEnvironment.CreateDir, + payload: { + environmentID, + path, + }, + } + conn.send(msg) +} + +/** + * Request to get a file from the environment's filesystem. + */ +function getFile( + conn: WebSocketConnection, { + environmentID, + path, + }: { + environmentID: string, + path: string, + }, +) { + const msg: rws.RunningEnvironment_GetFile = { + type: rws.MessageType.RunningEnvironment.GetFile, + payload: { + environmentID, + path, + }, + } + conn.send(msg) +} + + function start( conn: WebSocketConnection, { environmentID, @@ -94,4 +163,7 @@ export { execCmd, writeFile, deleteFile, + getFile, + createDir, + listDir, } diff --git a/src/react/useDevbook.ts b/src/react/useDevbook.ts index ae151b1..604df4c 100644 --- a/src/react/useDevbook.ts +++ b/src/react/useDevbook.ts @@ -9,6 +9,7 @@ import { DevbookStatus, Env, } from 'src/core' +import { FS } from 'src/core/devbook' /** * Options passed to the {@link useDevbook} hook. @@ -64,6 +65,7 @@ export interface State { * @param command Command to run */ runCmd: (command: string) => void + fs?: FS } /** @@ -126,6 +128,7 @@ function useDevbook({ runCmd, runCode, status, + fs: devbook?.fs, } }