diff --git a/apps/remix-ide-e2e/src/tests/remixd.test.ts b/apps/remix-ide-e2e/src/tests/remixd.test.ts index 333bb49ff7..c45c95a660 100644 --- a/apps/remix-ide-e2e/src/tests/remixd.test.ts +++ b/apps/remix-ide-e2e/src/tests/remixd.test.ts @@ -69,6 +69,17 @@ module.exports = { .clickLaunchIcon('solidity') .setSolidityCompilerVersion('soljson-v0.6.2+commit.bacdbe57.js') // open-zeppelin moved to pragma ^0.6.0 .testContracts('test_import_node_modules_with_github_import.sol', sources[4]['browser/test_import_node_modules_with_github_import.sol'], ['ERC20', 'test11']) + }, + + 'Run git status': function (browser) { + browser + .executeScript('git status') + .pause(3000) + .journalLastChildIncludes('On branch ') + }, + + 'Close Remixd': function (browser) { + browser .clickLaunchIcon('pluginManager') .scrollAndClick('#pluginManager article[id="remixPluginManagerListItem_remixd"] button') .end() diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 2bf1eee928..17878c51fd 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -390,7 +390,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org debug, analysis, test, - filePanel.remixdHandle + filePanel.remixdHandle, + filePanel.gitHandle ]) try { diff --git a/apps/remix-ide/src/app/files/git-handle.js b/apps/remix-ide/src/app/files/git-handle.js new file mode 100644 index 0000000000..a935068679 --- /dev/null +++ b/apps/remix-ide/src/app/files/git-handle.js @@ -0,0 +1,18 @@ +import { WebsocketPlugin } from '@remixproject/engine-web' +import * as packageJson from '../../../../../package.json' + +const profile = { + name: 'git', + displayName: 'Git', + url: 'ws://127.0.0.1:65521', + methods: ['execute'], + description: 'Using Remixd daemon, allow to access git API', + kind: 'other', + version: packageJson.version +} + +export class GitHandle extends WebsocketPlugin { + constructor () { + super(profile) + } +} diff --git a/apps/remix-ide/src/app/files/remixd-handle.js b/apps/remix-ide/src/app/files/remixd-handle.js index 1bfcb2cd17..311883e020 100644 --- a/apps/remix-ide/src/app/files/remixd-handle.js +++ b/apps/remix-ide/src/app/files/remixd-handle.js @@ -40,6 +40,7 @@ export class RemixdHandle extends WebsocketPlugin { deactivate () { this.fileSystemExplorer.hide() if (super.socket) super.deactivate() + this.call('manager', 'deactivatePlugin', 'git') this.locahostProvider.close((error) => { if (error) console.log(error) }) @@ -51,7 +52,8 @@ export class RemixdHandle extends WebsocketPlugin { } async canceled () { - this.appManager.ensureDeactivated('remixd') + this.call('manager', 'deactivatePlugin', 'remixd') + this.call('manager', 'deactivatePlugin', 'git') } /** @@ -82,6 +84,7 @@ export class RemixdHandle extends WebsocketPlugin { } }, 3000) this.locahostProvider.init(_ => this.fileSystemExplorer.ensureRoot()) + this.call('manager', 'activatePlugin', 'git') } } if (this.locahostProvider.isConnected()) { diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index 65e1664b7e..e210ca8ef0 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -5,6 +5,7 @@ var yo = require('yo-yo') var EventManager = require('../../lib/events') var FileExplorer = require('../files/file-explorer') var { RemixdHandle } = require('../files/remixd-handle.js') +var { GitHandle } = require('../files/git-handle.js') var globalRegistry = require('../../global/registry') var css = require('./styles/file-panel-styles') @@ -60,6 +61,7 @@ module.exports = class Filepanel extends ViewPlugin { var fileSystemExplorer = createProvider('localhost') self.remixdHandle = new RemixdHandle(fileSystemExplorer, self._deps.fileProviders.localhost, appManager) + self.gitHandle = new GitHandle() const explorers = yo`
diff --git a/apps/remix-ide/src/app/panels/styles/terminal-styles.js b/apps/remix-ide/src/app/panels/styles/terminal-styles.js index 25d2d598ac..7564e4f7ea 100644 --- a/apps/remix-ide/src/app/panels/styles/terminal-styles.js +++ b/apps/remix-ide/src/app/panels/styles/terminal-styles.js @@ -54,6 +54,9 @@ var css = csjs` padding : 1ch; margin-top : 2ch; } + .block > pre { + max-height : 200px; + } .cli { line-height : 1.7em; font-family : monospace; diff --git a/apps/remix-ide/src/app/panels/terminal.js b/apps/remix-ide/src/app/panels/terminal.js index 632e45c3e2..25f3e3e546 100644 --- a/apps/remix-ide/src/app/panels/terminal.js +++ b/apps/remix-ide/src/app/panels/terminal.js @@ -485,7 +485,8 @@ class Terminal extends Plugin { return self._view.el function wrapScript (script) { - if (script.startsWith('remix.')) return script + const isKnownScript = ['remix.', 'git'].some(prefix => script.trim().startsWith(prefix)) + if (isKnownScript) return script return ` try { const ret = ${script}; @@ -746,10 +747,16 @@ class Terminal extends Plugin { } } try { - await this.call('scriptRunner', 'execute', script) + let result + if (script.trim().startsWith('git')) { + result = await this.call('git', 'execute', script) + } else { + result = await this.call('scriptRunner', 'execute', script) + } + if (result) self.commands.html(yo`
${result}
`) done() } catch (error) { - done(error.message) + done(error.message || error) } } } diff --git a/apps/remix-ide/src/app/ui/auto-complete-popup.js b/apps/remix-ide/src/app/ui/auto-complete-popup.js index 152efcc457..36e4fa9697 100644 --- a/apps/remix-ide/src/app/ui/auto-complete-popup.js +++ b/apps/remix-ide/src/app/ui/auto-complete-popup.js @@ -31,7 +31,6 @@ class AutoCompletePopup { self._elementsToShow = 4 self._selectedElement = 0 this.extraCommands = [] - this.extendAutocompletion() } render () { diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 432acd0154..c6156355a0 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -11,7 +11,7 @@ const requiredModules = [ // services + layout views + system views 'terminal', 'settings', 'pluginManager'] export function isNative (name) { - const nativePlugins = ['vyper', 'workshops', 'debugger'] + const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd'] return nativePlugins.includes(name) || requiredModules.includes(name) } @@ -34,7 +34,7 @@ export class RemixAppManager extends PluginManager { async canDeactivatePlugin (from, to) { if (requiredModules.includes(to.name)) return false - return from.name === 'manager' + return isNative(from.name) } async canCall (from, to, method, message) { @@ -70,11 +70,6 @@ export class RemixAppManager extends PluginManager { this.event.emit('deactivate', plugin) } - async ensureDeactivated (apiName) { - await this.deactivatePlugin(apiName) - this.event.emit('ensureDeactivated', apiName) - } - isRequired (name) { return requiredModules.includes(name) } diff --git a/libs/remixd/src/bin/remixd.ts b/libs/remixd/src/bin/remixd.ts index 4c4b72631d..a973c13ead 100644 --- a/libs/remixd/src/bin/remixd.ts +++ b/libs/remixd/src/bin/remixd.ts @@ -8,6 +8,23 @@ import * as fs from 'fs-extra' import * as path from 'path' import * as program from 'commander' +const services = { + git: () => new servicesList.GitClient(), + folder: () => new servicesList.Sharedfolder() +} + +const ports = { + git: 65521, + folder: 65520 +} + +const killCallBack: Array = [] +function startService (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => void) { + const socket = new WebSocket(ports[service], { remixIdeUrl: program.remixIde }, () => services[service]()) + socket.start(callback) + killCallBack.push(socket.close.bind(socket)) +} + (async () => { program .usage('-s ') @@ -19,7 +36,6 @@ import * as program from 'commander' console.log('\nExample:\n\n remixd -s ./ --remix-ide http://localhost:8080') }).parse(process.argv) // eslint-disable-next-line - const killCallBack: Array = [] if (!program.remixIde) { console.log('\x1b[33m%s\x1b[0m', '[WARN] You can only connect to remixd from one of the supported origins.') @@ -38,15 +54,16 @@ import * as program from 'commander' console.log('\x1b[33m%s\x1b[0m', '[WARN] Any application that runs on your computer can potentially read from and write to all files in the directory.') console.log('\x1b[33m%s\x1b[0m', '[WARN] Symbolic links are not forwarded to Remix IDE\n') try { - const sharedFolderClient = new servicesList.Sharedfolder() - const websocketHandler = new WebSocket(65520, { remixIdeUrl: program.remixIde }, sharedFolderClient) - - websocketHandler.start((ws: WS) => { + startService('folder', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => { sharedFolderClient.setWebSocket(ws) sharedFolderClient.setupNotifications(program.sharedFolder) sharedFolderClient.sharedFolder(program.sharedFolder, program.readOnly || false) }) - killCallBack.push(websocketHandler.close.bind(websocketHandler)) + + startService('git', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => { + sharedFolderClient.setWebSocket(ws) + sharedFolderClient.sharedFolder(program.sharedFolder, program.readOnly || false) + }) } catch (error) { throw new Error(error) } diff --git a/libs/remixd/src/serviceList.ts b/libs/remixd/src/serviceList.ts index cb22bd880a..5db445ee66 100644 --- a/libs/remixd/src/serviceList.ts +++ b/libs/remixd/src/serviceList.ts @@ -1 +1,2 @@ export { RemixdClient as Sharedfolder } from './services/remixdClient' +export { GitClient } from './services/gitClient' diff --git a/libs/remixd/src/services/gitClient.ts b/libs/remixd/src/services/gitClient.ts new file mode 100644 index 0000000000..868a6c575e --- /dev/null +++ b/libs/remixd/src/services/gitClient.ts @@ -0,0 +1,50 @@ +import * as WS from 'ws' // eslint-disable-line +import { PluginClient } from '@remixproject/plugin' +const { spawn } = require('child_process') + +export class GitClient extends PluginClient { + methods: ['execute'] + websocket: WS + currentSharedFolder: string + readOnly: boolean + + setWebSocket (websocket: WS): void { + this.websocket = websocket + } + + sharedFolder (currentSharedFolder: string, readOnly: boolean): void { + this.currentSharedFolder = currentSharedFolder + this.readOnly = readOnly + } + + execute (cmd: string) { + assertCommand(cmd) + const options = { cwd: this.currentSharedFolder, shell: true } + const child = spawn(cmd, options) + let result = '' + let error = '' + return new Promise((resolve, reject) => { + child.stdout.on('data', (data) => { + result += data.toString() + }) + child.stderr.on('data', (err) => { + error += err.toString() + }) + child.on('close', () => { + if (error) reject(error) + else resolve(result) + }) + }) + } +} + +/** + * Validate that command can be run by service + * @param cmd + */ +function assertCommand (cmd) { + const regex = '^git\\s[^&|;]*$' + if (!RegExp(regex).test(cmd)) { // git then space and then everything else + throw new Error('Invalid command for service!') + } +} diff --git a/libs/remixd/src/types/index.ts b/libs/remixd/src/types/index.ts index 71587b5423..cde06fcf07 100644 --- a/libs/remixd/src/types/index.ts +++ b/libs/remixd/src/types/index.ts @@ -3,9 +3,9 @@ import * as Websocket from 'ws' type ServiceListKeys = keyof typeof ServiceList; -export type SharedFolder = typeof ServiceList[ServiceListKeys] +export type Service = typeof ServiceList[ServiceListKeys] -export type SharedFolderClient = InstanceType +export type ServiceClient = InstanceType export type WebsocketOpt = { remixIdeUrl: string diff --git a/libs/remixd/src/websocket.ts b/libs/remixd/src/websocket.ts index 6c89b380c1..dd9cc4f511 100644 --- a/libs/remixd/src/websocket.ts +++ b/libs/remixd/src/websocket.ts @@ -1,15 +1,15 @@ import * as WS from 'ws' import * as http from 'http' -import { WebsocketOpt, SharedFolderClient } from './types' // eslint-disable-line +import { WebsocketOpt, ServiceClient } from './types' // eslint-disable-line import { getDomain } from './utils' import { createClient } from '@remixproject/plugin-ws' export default class WebSocket { server: http.Server wsServer: WS.Server - constructor (public port: number, public opt: WebsocketOpt, public sharedFolder: SharedFolderClient) {} //eslint-disable-line + constructor (public port: number, public opt: WebsocketOpt, public getclient: () => ServiceClient) {} //eslint-disable-line - start (callback?: (ws: WS) => void): void { + start (callback?: (ws: WS, client: ServiceClient) => void): void { this.server = http.createServer((request, response) => { console.log((new Date()) + ' Received request for ' + request.url) response.writeHead(404) @@ -17,25 +17,25 @@ export default class WebSocket { }) const loopback = '127.0.0.1' - this.server.listen(this.port, loopback, function () { - console.log((new Date()) + ' remixd is listening on ' + loopback + ':65520') + this.server.listen(this.port, loopback, () => { + console.log(`${new Date()} remixd is listening on ${loopback}:${this.port}`) }) this.wsServer = new WS.Server({ server: this.server, verifyClient: (info, done) => { if (!originIsAllowed(info.origin, this)) { done(false) - console.log((new Date()) + ' Connection from origin ' + info.origin + ' rejected.') + console.log(`${new Date()} connection from origin ${info.origin}`) return } done(true) } }) this.wsServer.on('connection', (ws) => { - const { sharedFolder } = this + const client = this.getclient() - createClient(ws, sharedFolder as any) - if (callback) callback(ws) + createClient(ws, client as any) + if (callback) callback(ws, client) }) }