diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..93f1361991 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log diff --git a/package.json b/package.json index 7c547fdc85..fd6a099aa5 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "remixd", - "version": "0.0.1", + "version": "0.0.9", "description": "remix server", - "main": "index.js", + "main": "./src/main.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node ./src/main.js" }, "repository": { "type": "git", @@ -16,10 +17,20 @@ "ethereum", "solidity" ], - "author": "", + "author": "cpp ethereum team", "license": "MIT", "bugs": { "url": "https://github.com/ethereum/remixd/issues" }, - "homepage": "https://github.com/ethereum/remixd#readme" + "homepage": "https://github.com/ethereum/remixd#readme", + "dependencies": { + "commander": "^2.9.0", + "fs-extra": "^3.0.1", + "isbinaryfile": "^3.0.2", + "watch": "^1.0.2", + "websocket": "^1.0.24" + }, + "bin": { + "remixd": "./src/main.js" + } } diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000000..5563388a65 --- /dev/null +++ b/src/main.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node +var Router = require('./router') +var program = require('commander') +program +.usage('remix -S ') +.description('Provide a two ways connection between the local computer and Remix IDE') +.option('-S, --shared-folder ', 'Folder to share with Remix IDE') +.parse(process.argv) + +if (!program.sharedFolder) { + program.outputHelp() + process.exit(1) +} + +var router = new Router() +router.start(program.sharedFolder) diff --git a/src/router.js b/src/router.js new file mode 100644 index 0000000000..60bcb1310c --- /dev/null +++ b/src/router.js @@ -0,0 +1,28 @@ +var servicesList = require('./servicesList') +var Websocket = require('./websocket') + +class Router { + start (sharedFolder) { + this.websocket = new Websocket() + this.websocket.start((message) => { + this.call(message.id, message.service, message.fn, message.args) + }) + servicesList['sharedfolder'].setupNotifications(this.websocket, sharedFolder) + servicesList['sharedfolder'].sharedFolder(sharedFolder) + } + + call (callid, name, fn, args) { + servicesList[name][fn](args, (error, data) => { + var response = { + id: callid, + type: 'reply', + scope: name, + result: data, + error: error + } + this.websocket.send(JSON.stringify(response)) + }) + } +} + +module.exports = Router diff --git a/src/services/sharedFolder.js b/src/services/sharedFolder.js new file mode 100644 index 0000000000..d4d5f3739d --- /dev/null +++ b/src/services/sharedFolder.js @@ -0,0 +1,87 @@ +var utils = require('../utils') +var isbinaryfile = require('isbinaryfile') +var fs = require('fs-extra') +var watch = require('watch') + +module.exports = { + monitors: [], + trackDownStreamUpdate: {}, + + sharedFolder: function (sharedFolder) { + this.sharedFolder = sharedFolder + }, + + list: function (args, cb) { + cb(null, utils.walkSync(this.sharedFolder, {}, this.sharedFolder)) + }, + + get: function (args, cb) { + var path = utils.absolutePath(args.path, this.sharedFolder) + isbinaryfile(path, (error, isBinary) => { + if (error) console.log(error) + if (isBinary) { + cb(null, '') + } else { + fs.readFile(path, 'utf8', (error, data) => { + if (error) console.log(error) + cb(error, data) + }) + } + }) + }, + + set: function (args, cb) { + var path = utils.absolutePath(args.path, this.sharedFolder) + this.trackDownStreamUpdate[path] = path + fs.writeFile(path, args.content, 'utf8', (error, data) => { + if (error) console.log(error) + cb(error, data) + }) + }, + + rename: function (args, cb) { + var oldpath = utils.absolutePath(args.oldPath, this.sharedFolder) + var newpath = utils.absolutePath(args.newPath, this.sharedFolder) + fs.move(oldpath, newpath, (error, data) => { + if (error) console.log(error) + cb(error, data) + }) + }, + + remove: function (args, cb) { + var path = utils.absolutePath(args.path, this.sharedFolder) + fs.remove(path, (error, data) => { + if (error) console.log(error) + cb(error, data) + }) + }, + + setupNotifications: function (websocket, path) { + watch.createMonitor(path, (monitor) => { + this.monitors.push(monitor) + monitor.on('created', (f, stat) => { + isbinaryfile(f, (error, isBinary) => { + if (error) console.log(error) + if (stat.isDirectory()) { + this.setupNotifications(websocket, f) + } + if (websocket.connection) websocket.send(message('created', { path: utils.relativePath(f, this.sharedFolder), isReadOnly: isBinary, isFolder: stat.isDirectory() })) + }) + }) + monitor.on('changed', (f, curr, prev) => { + if (this.trackDownStreamUpdate[f]) { + delete this.trackDownStreamUpdate[f] + return + } + if (websocket.connection) websocket.send(message('changed', utils.relativePath(f, this.sharedFolder))) + }) + monitor.on('removed', (f, stat) => { + if (websocket.connection) websocket.send(message('removed', { path: utils.relativePath(f, this.sharedFolder), isFolder: stat.isDirectory() })) + }) + }) + } +} + +function message (name, value) { + return JSON.stringify({type: 'notification', scope: 'sharedfolder', name: name, value: value}) +} diff --git a/src/servicesList.js b/src/servicesList.js new file mode 100644 index 0000000000..17f2c114ca --- /dev/null +++ b/src/servicesList.js @@ -0,0 +1,3 @@ +module.exports = { + sharedfolder: require('./services/sharedFolder') +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000000..cc6a11920d --- /dev/null +++ b/src/utils.js @@ -0,0 +1,59 @@ +var fs = require('fs-extra') +var path = require('path') +var isbinaryfile = require('isbinaryfile') +var pathModule = require('path') + +module.exports = { + absolutePath: absolutePath, + relativePath: relativePath, + walkSync: walkSync +} + +/** + * returns the absolute path of the given @arg path + * + * @param {String} path - relative path (Unix style which is the one used by Remix IDE) + * @param {String} sharedFolder - absolute shared path. platform dependent representation. + * @return {String} platform dependent absolute path (/home/user1/.../... for unix, c:\user\...\... for windows) + */ +function absolutePath (path, sharedFolder) { + path = normalizePath(path) + if (path.indexOf(sharedFolder) !== 0) { + path = pathModule.resolve(sharedFolder, path) + } + return path +} + +/** + * return the relative path of the given @arg path + * + * @param {String} path - absolute platform dependent path + * @param {String} sharedFolder - absolute shared path. platform dependent representation + * @return {String} relative path (Unix style which is the one used by Remix IDE) + */ +function relativePath (path, sharedFolder) { + var relative = pathModule.relative(sharedFolder, path) + return normalizePath(relative) +} + +function normalizePath (path) { + if (process.platform === 'win32') { + return path.replace(/\\/g, '/') + } + return path +} + +function walkSync (dir, filelist, sharedFolder) { + var files = fs.readdirSync(dir) + filelist = filelist || {} + files.forEach(function (file) { + var subElement = path.join(dir, file) + if (fs.statSync(subElement).isDirectory()) { + filelist = walkSync(subElement, filelist, sharedFolder) + } else { + var relative = relativePath(subElement, sharedFolder) + filelist[relative] = isbinaryfile.sync(subElement) + } + }) + return filelist +} diff --git a/src/websocket.js b/src/websocket.js new file mode 100644 index 0000000000..72de1a9a4b --- /dev/null +++ b/src/websocket.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node +var WebSocketServer = require('websocket').server +var http = require('http') + +class WebSocket { + constructor () { + this.connection = null + } + + start (callback) { + var server = http.createServer(function (request, response) { + console.log((new Date()) + ' Received request for ' + request.url) + response.writeHead(404) + response.end() + }) + server.listen(65520, function () { + console.log((new Date()) + ' Remixd is listening on port 65520') + }) + + this.wsServer = new WebSocketServer({ + httpServer: server, + autoAcceptConnections: false + }) + + this.wsServer.on('request', (request) => { + if (!originIsAllowed(request.origin)) { + request.reject() + console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.') + return + } + if (this.connection) { + console.log('closing previous connection') + this.wsServer.closeAllConnections() + this.connection = null + return + } + + this.connection = request.accept('echo-protocol', request.origin) + console.log((new Date()) + ' Connection accepted.') + this.connection.on('message', (message) => { + if (message.type === 'utf8') { + callback(JSON.parse(message.utf8Data)) + } + }) + this.connection.on('close', (reasonCode, description) => { + console.log((new Date()) + ' Remix ' + this.connection.remoteAddress + ' disconnected.') + this.connection = null + }) + }) + } + + send (data) { + this.connection.sendUTF(data) + } +} + +function originIsAllowed (origin) { + console.log('origin', origin) + return true +} + +module.exports = WebSocket