Merge pull request #2891 from ethereum/remixd-websocket

Remixd Websocket Plugin
pull/5370/head
yann300 4 years ago committed by GitHub
commit 26cf43c451
  1. 2
      ci/browser_tests_chrome.sh
  2. 2
      ci/browser_tests_firefox.sh
  3. 2
      embark/index.js
  4. 7077
      package-lock.json
  5. 2
      package.json
  6. 13
      src/app.js
  7. 32
      src/app/files/file-explorer.js
  8. 54
      src/app/files/fileProvider.js
  9. 245
      src/app/files/remixDProvider.js
  10. 34
      src/app/files/remixd-handle.js
  11. 2
      test-browser/tests/fileManager_api.test.js

@ -7,7 +7,7 @@ setupRemixd () {
cd contracts
echo 'sharing folder: '
echo $PWD
./../node_modules/remixd/bin/remixd -s $PWD --remix-ide http://127.0.0.1:8080 &
./../node_modules/.bin/remixd -s $PWD --remix-ide http://127.0.0.1:8080 &
cd ..
}

@ -7,7 +7,7 @@ setupRemixd () {
cd contracts
echo 'sharing folder: '
echo $PWD
./../node_modules/remixd/bin/remixd -s $PWD --remix-ide http://127.0.0.1:8080 &
./../node_modules/.bin/remixd -s $PWD --remix-ide http://127.0.0.1:8080 &
cd ..
}

@ -13,7 +13,7 @@ const DEFAULT_OPTIONS = {
module.exports = (embark) => {
// plugin options
const readOnly = embark.pluginConfig.readOnly || false
const {protocol, host, port} = merge.recursive(DEFAULT_OPTIONS, embark.pluginConfig.remixIde)
const { protocol, host, port } = merge.recursive(DEFAULT_OPTIONS, embark.pluginConfig.remixIde)
// globals
const remixIdeUrl = `${protocol}://${host}` + `${port ? `:${port}` : ''}`

7077
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -62,7 +62,7 @@
"remix-solidity": "0.3.31",
"remix-tabs": "1.0.48",
"remix-tests": "0.1.34",
"remixd": "0.1.8-alpha.16",
"remixd": "0.2.1-alpha.1",
"request": "^2.83.0",
"rimraf": "^2.6.1",
"selenium-standalone": "^6.17.0",

@ -5,7 +5,6 @@ var csjs = require('csjs-inject')
var yo = require('yo-yo')
var remixLib = require('remix-lib')
var registry = require('./global/registry')
var Remixd = require('./lib/remixd')
var loadFileFromParent = require('./loadFilesFromParent')
var { OffsetToLineColumnConverter } = require('./lib/offsetToLineColumnConverter')
var QueryParams = require('./lib/query-params')
@ -122,6 +121,7 @@ var css = csjs`
class App {
constructor (api = {}, events = {}, opts = {}) {
var self = this
self.appManager = new RemixAppManager({})
self._components = {}
self._view = {}
self._view.splashScreen = yo`
@ -145,14 +145,7 @@ class App {
self._components.filesProviders = {}
self._components.filesProviders['browser'] = new FileProvider('browser')
registry.put({api: self._components.filesProviders['browser'], name: 'fileproviders/browser'})
var remixd = new Remixd(65520)
registry.put({api: remixd, name: 'remixd'})
remixd.event.register('system', (message) => {
if (message.error) toolTip(message.error)
})
self._components.filesProviders['localhost'] = new RemixDProvider(remixd)
self._components.filesProviders['localhost'] = new RemixDProvider(self.appManager)
registry.put({api: self._components.filesProviders['localhost'], name: 'fileproviders/localhost'})
registry.put({api: self._components.filesProviders, name: 'fileproviders'})
@ -235,7 +228,7 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
}
// APP_MANAGER
const appManager = new RemixAppManager({})
const appManager = self.appManager
const pluginLoader = appManager.pluginLoader
const workspace = pluginLoader.get()
const engine = new Engine(appManager)

@ -102,9 +102,9 @@ function fileExplorer (localRegistry, files, menuItems) {
function fileAdded (filepath) {
self.ensureRoot(() => {
var folderpath = filepath.split('/').slice(0, -1).join('/')
const folderpath = filepath.split('/').slice(0, -1).join('/')
const currentTree = self.treeView.nodeAt(folderpath)
var currentTree = self.treeView.nodeAt(folderpath)
if (currentTree && self.treeView.isExpanded(folderpath)) {
self.files.resolveDirectory(folderpath, (error, fileTree) => {
if (error) console.error(error)
@ -141,10 +141,14 @@ function fileExplorer (localRegistry, files, menuItems) {
}
function fileRemoved (filepath) {
var label = self.treeView.labelAt(filepath)
const label = self.treeView.labelAt(filepath)
filepath = filepath.split('/').slice(0, -1).join('/')
if (label && label.parentElement) {
label.parentElement.removeChild(label)
}
self.updatePath(filepath)
}
function fileRenamed (oldName, newName, isFolder) {
@ -240,15 +244,6 @@ function fileExplorer (localRegistry, files, menuItems) {
if (!removeFolder) {
tooltip(`failed to remove ${key}. Make sure the directory is empty before removing it.`)
} else {
const provider = fileManager.currentFileProvider()
if (provider) {
const { type } = provider
return self.updatePath(type)
}
self.updatePath('browser')
}
}, () => {})
}
@ -291,15 +286,6 @@ function fileExplorer (localRegistry, files, menuItems) {
if (!removeFile) {
tooltip(`Failed to remove file ${key}.`)
} else {
const provider = fileManager.currentFileProvider()
if (provider) {
const { type } = provider
return self.updatePath(type)
}
self.updatePath('browser')
}
},
() => {}
@ -449,12 +435,12 @@ fileExplorer.prototype.uploadFile = function (event) {
let files = this.files
function loadFile () {
var fileReader = new FileReader()
fileReader.onload = function (event) {
fileReader.onload = async function (event) {
if (helper.checkSpecialChars(file.name)) {
modalDialogCustom.alert('Special characters are not allowed')
return
}
var success = files.set(name, event.target.result)
var success = await files.set(name, event.target.result)
if (!success) {
modalDialogCustom.alert('Failed to create file ' + name)
} else {

@ -157,35 +157,38 @@ class FileProvider {
* @param {*} path is the folder to be removed
*/
remove (path) {
path = this.removePrefix(path)
if (window.remixFileSystem.existsSync(path)) {
const stat = window.remixFileSystem.statSync(path)
try {
if (!stat.isDirectory()) {
return this.removeFile(path)
} else {
const items = window.remixFileSystem.readdirSync(path)
if (items.length !== 0) {
items.forEach((item, index) => {
const curPath = `${path}/${item}`
if (window.remixFileSystem.statSync(curPath).isDirectory()) { // delete folder
this.remove(curPath)
} else { // delete file
this.removeFile(curPath)
}
})
if (window.remixFileSystem.readdirSync(path).length === 0) window.remixFileSystem.rmdirSync(path, console.log)
return new Promise((resolve, reject) => {
path = this.removePrefix(path)
if (window.remixFileSystem.existsSync(path)) {
const stat = window.remixFileSystem.statSync(path)
try {
if (!stat.isDirectory()) {
resolve(this.removeFile(path))
} else {
// folder is empty
window.remixFileSystem.rmdirSync(path, console.log)
const items = window.remixFileSystem.readdirSync(path)
if (items.length !== 0) {
items.forEach((item, index) => {
const curPath = `${path}/${item}`
if (window.remixFileSystem.statSync(curPath).isDirectory()) { // delete folder
this.remove(curPath)
} else { // delete file
this.removeFile(curPath)
}
})
if (window.remixFileSystem.readdirSync(path).length === 0) window.remixFileSystem.rmdirSync(path, console.log)
} else {
// folder is empty
window.remixFileSystem.rmdirSync(path, console.log)
}
this.event.trigger('fileRemoved', [this._normalizePath(path)])
}
} catch (e) {
console.log(e)
return resolve(false)
}
} catch (e) {
console.log(e)
return false
}
}
return true
return resolve(true)
})
}
removeFile (path) {
@ -219,6 +222,7 @@ class FileProvider {
window.remixFileSystem.readdir(path, (error, files) => {
var ret = {}
if (files) {
files.forEach(element => {
const absPath = (path === '/' ? '' : path) + '/' + element

@ -1,12 +1,10 @@
'use strict'
var EventManager = require('../../lib/events')
var pathtool = require('path')
module.exports = class RemixDProvider {
constructor (remixd) {
constructor (appManager) {
this.event = new EventManager()
this._remixd = remixd
this.remixd = remixapi(remixd, this)
this._appManager = appManager
this.type = 'localhost'
this.error = { 'EEXIST': 'File already exists' }
this._isReady = false
@ -14,39 +12,34 @@ module.exports = class RemixDProvider {
this._readOnlyMode = false
this.filesContent = {}
this.files = {}
}
_registerEvent () {
var remixdEvents = ['connecting', 'connected', 'errored', 'closed']
remixdEvents.forEach((value) => {
remixd.event.register(value, (event) => {
this._appManager.on('remixd', value, (event) => {
this.event.trigger(value, [event])
})
})
remixd.event.register('notified', (data) => {
if (data.scope === 'sharedfolder') {
if (data.name === 'created') {
this.init(() => {
this.event.trigger('fileAdded', [this.type + '/' + data.value.path, data.value.isReadOnly, data.value.isFolder])
})
} else if (data.name === 'removed') {
this.init(() => {
this.event.trigger('fileRemoved', [this.type + '/' + data.value.path])
})
} else if (data.name === 'changed') {
this._remixd.call('sharedfolder', 'get', {path: data.value}, (error, content) => {
if (error) {
console.log(error)
} else {
var path = this.type + '/' + data.value
this.filesContent[path] = content
this.event.trigger('fileExternallyChanged', [path, content])
}
})
} else if (data.name === 'rootFolderChanged') {
// new path has been set, we should reset
this.event.trigger('folderAdded', [this.type + '/'])
}
}
this._appManager.on('remixd', 'folderAdded', (path) => {
this.event.trigger('folderAdded', [this.addPrefix(path)])
})
this._appManager.on('remixd', 'fileAdded', (path) => {
this.event.trigger('fileAdded', [this.addPrefix(path)])
})
this._appManager.on('remixd', 'fileChanged', (path) => {
this.event.trigger('fileChanged', [this.addPrefix(path)])
})
this._appManager.on('remixd', 'fileRemoved', (path) => {
this.event.trigger('fileRemoved', [this.addPrefix(path)])
})
this._appManager.on('remixd', 'fileRenamed', (oldPath, newPath) => {
this.event.trigger('fileRemoved', [this.addPrefix(oldPath), this.addPrefix(newPath)])
})
}
@ -55,38 +48,35 @@ module.exports = class RemixDProvider {
}
close (cb) {
this.remixd.exit()
this._isReady = false
cb()
}
init (cb) {
this._remixd.ensureSocket((error) => {
if (error) return cb(error)
this._isReady = !error
this._remixd.call('sharedfolder', 'folderIsReadOnly', {}, (error, result) => {
this._readOnlyMode = result
cb(error)
})
if (this._isReady) return cb && cb()
this._appManager.call('remixd', 'folderIsReadOnly', {})
.then((result) => {
this._isReady = true
this._readOnlyMode = result
this._registerEvent()
cb && cb()
}).catch((error) => {
cb && cb(error)
})
}
// @TODO: refactor all `this._remixd.call(....)` uses into `this.remixd[api](...)`
// where `api = ...`:
// this.remixd.read(path, (error, content) => {})
// this.remixd.write(path, content, (error, result) => {})
// this.remixd.rename(path1, path2, (error, result) => {})
// this.remixd.remove(path, (error, result) => {})
// this.remixd.dir(path, (error, filesList) => {})
//
// this.remixd.exists(path, (error, isValid) => {})
async exists (path, cb) {
exists (path, cb) {
if (!this._isReady) return cb && cb('provider not ready')
const unprefixedpath = this.removePrefix(path)
const callId = await this._remixd.call('sharedfolder', 'exists', {path: unprefixedpath})
const result = await this._remixd.receiveResponse(callId)
return cb(null, result)
return this._appManager.call('remixd', 'exists', { path: unprefixedpath })
.then((result) => {
if (cb) return cb(null, result)
return result
}).catch((error) => {
if (cb) return cb(error)
throw new Error(error)
})
}
getNormalizedName (path) {
@ -98,67 +88,75 @@ module.exports = class RemixDProvider {
}
get (path, cb) {
if (!this._isReady) return cb && cb('provider not ready')
var unprefixedpath = this.removePrefix(path)
this._remixd.call('sharedfolder', 'get', {path: unprefixedpath}, (error, file) => {
if (!error) {
this.filesContent[path] = file.content
if (file.readonly) { this._readOnlyFiles[path] = 1 }
cb(error, file.content)
} else {
// display the last known content.
// TODO should perhaps better warn the user that the file is not synced.
cb(null, this.filesContent[path])
}
this._appManager.call('remixd', 'get', { path: unprefixedpath })
.then((file) => {
this.filesContent[path] = file.content
if (file.readonly) { this._readOnlyFiles[path] = 1 }
cb(null, file.content)
}).catch((error) => {
// display the last known content.
// TODO should perhaps better warn the user that the file is not synced.
if (this.filesContent[path]) return cb(null, this.filesContent[path])
else cb(error)
})
}
set (path, content, cb) {
var unprefixedpath = this.removePrefix(path)
this._remixd.call('sharedfolder', 'set', {path: unprefixedpath, content: content}, (error, result) => {
if (cb) return cb(error, result)
var path = this.type + '/' + unprefixedpath
this.event.trigger('fileChanged', [path])
async set (path, content, cb) {
if (!this._isReady) return cb && cb('provider not ready')
const unprefixedpath = this.removePrefix(path)
return this._appManager.call('remixd', 'set', { path: unprefixedpath, content: content }).then(async (result) => {
if (cb) return cb(null, result)
}).catch((error) => {
if (cb) return cb(error)
throw new Error(error)
})
return true
}
isReadOnly (path) {
return this._readOnlyMode || this._readOnlyFiles[path] === 1
}
async remove (path) {
var unprefixedpath = this.removePrefix(path)
const callId = await this._remixd.call('sharedfolder', 'remove', {path: unprefixedpath}, (error, result) => {
if (error) console.log(error)
var path = this.type + '/' + unprefixedpath
delete this.filesContent[path]
this.init(() => {
this.event.trigger('fileRemoved', [path])
remove (path) {
return new Promise((resolve, reject) => {
if (!this._isReady) return reject('provider not ready')
const unprefixedpath = this.removePrefix(path)
this._appManager.call('remixd', 'remove', { path: unprefixedpath })
.then(result => {
const path = this.type + '/' + unprefixedpath
delete this.filesContent[path]
resolve(true)
this.init()
}).catch(error => {
if (error) console.log(error)
resolve(false)
})
})
return await this._remixd.receiveResponse(callId)
}
rename (oldPath, newPath, isFolder) {
var unprefixedoldPath = this.removePrefix(oldPath)
var unprefixednewPath = this.removePrefix(newPath)
this._remixd.call('sharedfolder', 'rename', {oldPath: unprefixedoldPath, newPath: unprefixednewPath}, (error, result) => {
if (error) {
console.log(error)
if (this.error[error.code]) error = this.error[error.code]
this.event.trigger('fileRenamedError', [this.error[error.code]])
} else {
var newPath = this.type + '/' + unprefixednewPath
var oldPath = this.type + '/' + unprefixedoldPath
this.filesContent[newPath] = this.filesContent[oldPath]
delete this.filesContent[oldPath]
this.init(() => {
this.event.trigger('fileRenamed', [oldPath, newPath, isFolder])
})
}
const unprefixedoldPath = this.removePrefix(oldPath)
const unprefixednewPath = this.removePrefix(newPath)
if (!this._isReady) return new Promise((resolve, reject) => reject('provider not ready'))
return this._appManager.call('remixd', 'rename', { oldPath: unprefixedoldPath, newPath: unprefixednewPath })
.then(result => {
const newPath = this.type + '/' + unprefixednewPath
const oldPath = this.type + '/' + unprefixedoldPath
this.filesContent[newPath] = this.filesContent[oldPath]
delete this.filesContent[oldPath]
this.init(() => {
this.event.trigger('fileRenamed', [oldPath, newPath, isFolder])
})
return result
}).catch(error => {
console.log(error)
if (this.error[error.code]) error = this.error[error.code]
this.event.trigger('fileRenamedError', [this.error[error.code]])
})
return true
}
isExternalFolder (path) {
@ -171,56 +169,33 @@ module.exports = class RemixDProvider {
return path
}
addPrefix (path) {
if (path.indexOf(this.type + '/') === 0) return path
if (path[0] === '/') return 'localhost' + path
return 'localhost/' + path
}
resolveDirectory (path, callback) {
var self = this
if (path[0] === '/') path = path.substring(1)
if (!path) return callback(null, { [self.type]: { } })
path = self.removePrefix(path)
self.remixd.dir(path, callback)
const unprefixedpath = this.removePrefix(path)
if (!this._isReady) return callback && callback('provider not ready')
this._appManager.call('remixd', 'resolveDirectory', { path: unprefixedpath }).then((result) => {
callback(null, result)
}).catch(callback)
}
async isDirectory (path) {
const unprefixedpath = this.removePrefix(path)
const callId = await this._remixd.call('sharedfolder', 'isDirectory', {path: unprefixedpath})
return await this._remixd.receiveResponse(callId)
if (!this._isReady) throw new Error('provider not ready')
return await this._appManager.call('remixd', 'isDirectory', {path: unprefixedpath})
}
async isFile (path) {
const unprefixedpath = this.removePrefix(path)
const callId = await this._remixd.call('sharedfolder', 'isFile', {path: unprefixedpath})
return await this._remixd.receiveResponse(callId)
if (!this._isReady) throw new Error('provider not ready')
return await this._appManager.call('remixd', 'isFile', { path: unprefixedpath })
}
}
function remixapi (remixd, self) {
const read = (path, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'get', { path }, (error, content) => callback(error, content))
}
const write = (path, content, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'set', { path, content }, (error, result) => callback(error, result))
}
const rename = (path, newpath, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'rename', { oldPath: path, newPath: newpath }, (error, result) => callback(error, result))
}
const remove = (path, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'remove', { path }, (error, result) => callback(error, result))
}
const dir = (path, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'resolveDirectory', { path }, (error, filesList) => callback(error, filesList))
}
const exit = () => { remixd.close() }
const api = { read, write, rename, remove, dir, exit, event: remixd.event }
return api
}

@ -1,5 +1,5 @@
import isElectron from 'is-electron'
import { Plugin } from '@remixproject/engine'
import { WebsocketPlugin } from '@remixproject/engine'
import * as packageJson from '../../../package.json'
var yo = require('yo-yo')
var modalDialog = require('../ui/modaldialog')
@ -20,14 +20,15 @@ var css = csjs`
const profile = {
name: 'remixd',
methods: [],
url: 'ws://127.0.0.1:65520',
methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'rename', 'remove', 'isDirectory', 'list'],
events: [],
description: 'Using Remixd daemon, allow to access file system',
kind: 'other',
version: packageJson.version
}
export class RemixdHandle extends Plugin {
export class RemixdHandle extends WebsocketPlugin {
constructor (fileSystemExplorer, locahostProvider, appManager) {
super(profile)
this.fileSystemExplorer = fileSystemExplorer
@ -36,16 +37,19 @@ export class RemixdHandle extends Plugin {
}
deactivate () {
this.fileSystemExplorer.hide()
if (super.socket) super.deactivate()
this.locahostProvider.close((error) => {
if (error) console.log(error)
})
}
activate () {
this.fileSystemExplorer.show()
this.connectToLocalhost()
}
canceled () {
async canceled () {
this.appManager.ensureDeactivated('remixd')
}
@ -55,7 +59,7 @@ export class RemixdHandle extends Plugin {
*
* @param {String} txHash - hash of the transaction
*/
connectToLocalhost () {
async connectToLocalhost () {
let connection = (error) => {
if (error) {
console.log(error)
@ -65,13 +69,11 @@ export class RemixdHandle extends Plugin {
)
this.canceled()
} else {
this.fileSystemExplorer.ensureRoot()
this.locahostProvider.init(_ => this.fileSystemExplorer.ensureRoot())
}
}
if (this.locahostProvider.isConnected()) {
this.locahostProvider.close((error) => {
if (error) console.log(error)
})
this.deactivate()
} else if (!isElectron()) {
// warn the user only if he/she is in the browser context
modalDialog(
@ -79,7 +81,12 @@ export class RemixdHandle extends Plugin {
remixdDialog(),
{ label: 'Connect',
fn: () => {
this.locahostProvider.init((error) => connection(error))
try {
super.activate()
setTimeout(() => { connection() }, 2000)
} catch (error) {
connection(error)
}
}
},
{ label: 'Cancel',
@ -89,7 +96,12 @@ export class RemixdHandle extends Plugin {
}
)
} else {
this.locahostProvider.init((error) => connection(error))
try {
super.activate()
setTimeout(() => { connection() }, 2000)
} catch (error) {
connection(error)
}
}
}
}

@ -86,7 +86,7 @@ module.exports = {
.addFile('removeFile.js', { content: executeRemove })
.executeScript(`remix.exeCurrent()`)
.pause(2000)
.waitForElementNotVisible('[data-id="treeViewLibrowser/old_contract.sol"]')
.waitForElementNotPresent('[data-id="treeViewLibrowser/old_contract.sol"]')
.end()
},

Loading…
Cancel
Save