Merge pull request #638 from ethereum/addgitService

Add git service in remixd & use it from the terminal
website_link
yann300 4 years ago committed by GitHub
commit 25b83afd05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  2. 3
      apps/remix-ide/src/app.js
  3. 18
      apps/remix-ide/src/app/files/git-handle.js
  4. 5
      apps/remix-ide/src/app/files/remixd-handle.js
  5. 2
      apps/remix-ide/src/app/panels/file-panel.js
  6. 3
      apps/remix-ide/src/app/panels/styles/terminal-styles.js
  7. 13
      apps/remix-ide/src/app/panels/terminal.js
  8. 1
      apps/remix-ide/src/app/ui/auto-complete-popup.js
  9. 9
      apps/remix-ide/src/remixAppManager.js
  10. 29
      libs/remixd/src/bin/remixd.ts
  11. 1
      libs/remixd/src/serviceList.ts
  12. 50
      libs/remixd/src/services/gitClient.ts
  13. 4
      libs/remixd/src/types/index.ts
  14. 18
      libs/remixd/src/websocket.ts

@ -69,6 +69,17 @@ module.exports = {
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.setSolidityCompilerVersion('soljson-v0.6.2+commit.bacdbe57.js') // open-zeppelin moved to pragma ^0.6.0 .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']) .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') .clickLaunchIcon('pluginManager')
.scrollAndClick('#pluginManager article[id="remixPluginManagerListItem_remixd"] button') .scrollAndClick('#pluginManager article[id="remixPluginManagerListItem_remixd"] button')
.end() .end()

@ -390,7 +390,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
debug, debug,
analysis, analysis,
test, test,
filePanel.remixdHandle filePanel.remixdHandle,
filePanel.gitHandle
]) ])
try { try {

@ -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)
}
}

@ -40,6 +40,7 @@ export class RemixdHandle extends WebsocketPlugin {
deactivate () { deactivate () {
this.fileSystemExplorer.hide() this.fileSystemExplorer.hide()
if (super.socket) super.deactivate() if (super.socket) super.deactivate()
this.call('manager', 'deactivatePlugin', 'git')
this.locahostProvider.close((error) => { this.locahostProvider.close((error) => {
if (error) console.log(error) if (error) console.log(error)
}) })
@ -51,7 +52,8 @@ export class RemixdHandle extends WebsocketPlugin {
} }
async canceled () { 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) }, 3000)
this.locahostProvider.init(_ => this.fileSystemExplorer.ensureRoot()) this.locahostProvider.init(_ => this.fileSystemExplorer.ensureRoot())
this.call('manager', 'activatePlugin', 'git')
} }
} }
if (this.locahostProvider.isConnected()) { if (this.locahostProvider.isConnected()) {

@ -5,6 +5,7 @@ var yo = require('yo-yo')
var EventManager = require('../../lib/events') var EventManager = require('../../lib/events')
var FileExplorer = require('../files/file-explorer') var FileExplorer = require('../files/file-explorer')
var { RemixdHandle } = require('../files/remixd-handle.js') var { RemixdHandle } = require('../files/remixd-handle.js')
var { GitHandle } = require('../files/git-handle.js')
var globalRegistry = require('../../global/registry') var globalRegistry = require('../../global/registry')
var css = require('./styles/file-panel-styles') var css = require('./styles/file-panel-styles')
@ -60,6 +61,7 @@ module.exports = class Filepanel extends ViewPlugin {
var fileSystemExplorer = createProvider('localhost') var fileSystemExplorer = createProvider('localhost')
self.remixdHandle = new RemixdHandle(fileSystemExplorer, self._deps.fileProviders.localhost, appManager) self.remixdHandle = new RemixdHandle(fileSystemExplorer, self._deps.fileProviders.localhost, appManager)
self.gitHandle = new GitHandle()
const explorers = yo` const explorers = yo`
<div> <div>

@ -54,6 +54,9 @@ var css = csjs`
padding : 1ch; padding : 1ch;
margin-top : 2ch; margin-top : 2ch;
} }
.block > pre {
max-height : 200px;
}
.cli { .cli {
line-height : 1.7em; line-height : 1.7em;
font-family : monospace; font-family : monospace;

@ -485,7 +485,8 @@ class Terminal extends Plugin {
return self._view.el return self._view.el
function wrapScript (script) { function wrapScript (script) {
if (script.startsWith('remix.')) return script const isKnownScript = ['remix.', 'git'].some(prefix => script.trim().startsWith(prefix))
if (isKnownScript) return script
return ` return `
try { try {
const ret = ${script}; const ret = ${script};
@ -746,10 +747,16 @@ class Terminal extends Plugin {
} }
} }
try { 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`<pre>${result}</pre>`)
done() done()
} catch (error) { } catch (error) {
done(error.message) done(error.message || error)
} }
} }
} }

@ -31,7 +31,6 @@ class AutoCompletePopup {
self._elementsToShow = 4 self._elementsToShow = 4
self._selectedElement = 0 self._selectedElement = 0
this.extraCommands = [] this.extraCommands = []
this.extendAutocompletion()
} }
render () { render () {

@ -11,7 +11,7 @@ const requiredModules = [ // services + layout views + system views
'terminal', 'settings', 'pluginManager'] 'terminal', 'settings', 'pluginManager']
export function isNative (name) { export function isNative (name) {
const nativePlugins = ['vyper', 'workshops', 'debugger'] const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd']
return nativePlugins.includes(name) || requiredModules.includes(name) return nativePlugins.includes(name) || requiredModules.includes(name)
} }
@ -34,7 +34,7 @@ export class RemixAppManager extends PluginManager {
async canDeactivatePlugin (from, to) { async canDeactivatePlugin (from, to) {
if (requiredModules.includes(to.name)) return false if (requiredModules.includes(to.name)) return false
return from.name === 'manager' return isNative(from.name)
} }
async canCall (from, to, method, message) { async canCall (from, to, method, message) {
@ -70,11 +70,6 @@ export class RemixAppManager extends PluginManager {
this.event.emit('deactivate', plugin) this.event.emit('deactivate', plugin)
} }
async ensureDeactivated (apiName) {
await this.deactivatePlugin(apiName)
this.event.emit('ensureDeactivated', apiName)
}
isRequired (name) { isRequired (name) {
return requiredModules.includes(name) return requiredModules.includes(name)
} }

@ -8,6 +8,23 @@ import * as fs from 'fs-extra'
import * as path from 'path' import * as path from 'path'
import * as program from 'commander' 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> = []
function startService<S extends 'git' | 'folder'> (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 () => { (async () => {
program program
.usage('-s <shared folder>') .usage('-s <shared folder>')
@ -19,7 +36,6 @@ import * as program from 'commander'
console.log('\nExample:\n\n remixd -s ./ --remix-ide http://localhost:8080') console.log('\nExample:\n\n remixd -s ./ --remix-ide http://localhost:8080')
}).parse(process.argv) }).parse(process.argv)
// eslint-disable-next-line // eslint-disable-next-line
const killCallBack: Array<Function> = []
if (!program.remixIde) { if (!program.remixIde) {
console.log('\x1b[33m%s\x1b[0m', '[WARN] You can only connect to remixd from one of the supported origins.') 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] 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') console.log('\x1b[33m%s\x1b[0m', '[WARN] Symbolic links are not forwarded to Remix IDE\n')
try { try {
const sharedFolderClient = new servicesList.Sharedfolder() startService('folder', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => {
const websocketHandler = new WebSocket(65520, { remixIdeUrl: program.remixIde }, sharedFolderClient)
websocketHandler.start((ws: WS) => {
sharedFolderClient.setWebSocket(ws) sharedFolderClient.setWebSocket(ws)
sharedFolderClient.setupNotifications(program.sharedFolder) sharedFolderClient.setupNotifications(program.sharedFolder)
sharedFolderClient.sharedFolder(program.sharedFolder, program.readOnly || false) 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) { } catch (error) {
throw new Error(error) throw new Error(error)
} }

@ -1 +1,2 @@
export { RemixdClient as Sharedfolder } from './services/remixdClient' export { RemixdClient as Sharedfolder } from './services/remixdClient'
export { GitClient } from './services/gitClient'

@ -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!')
}
}

@ -3,9 +3,9 @@ import * as Websocket from 'ws'
type ServiceListKeys = keyof typeof ServiceList; type ServiceListKeys = keyof typeof ServiceList;
export type SharedFolder = typeof ServiceList[ServiceListKeys] export type Service = typeof ServiceList[ServiceListKeys]
export type SharedFolderClient = InstanceType<typeof ServiceList[ServiceListKeys]> export type ServiceClient = InstanceType<typeof ServiceList[ServiceListKeys]>
export type WebsocketOpt = { export type WebsocketOpt = {
remixIdeUrl: string remixIdeUrl: string

@ -1,15 +1,15 @@
import * as WS from 'ws' import * as WS from 'ws'
import * as http from 'http' 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 { getDomain } from './utils'
import { createClient } from '@remixproject/plugin-ws' import { createClient } from '@remixproject/plugin-ws'
export default class WebSocket { export default class WebSocket {
server: http.Server server: http.Server
wsServer: WS.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) => { this.server = http.createServer((request, response) => {
console.log((new Date()) + ' Received request for ' + request.url) console.log((new Date()) + ' Received request for ' + request.url)
response.writeHead(404) response.writeHead(404)
@ -17,25 +17,25 @@ export default class WebSocket {
}) })
const loopback = '127.0.0.1' const loopback = '127.0.0.1'
this.server.listen(this.port, loopback, function () { this.server.listen(this.port, loopback, () => {
console.log((new Date()) + ' remixd is listening on ' + loopback + ':65520') console.log(`${new Date()} remixd is listening on ${loopback}:${this.port}`)
}) })
this.wsServer = new WS.Server({ this.wsServer = new WS.Server({
server: this.server, server: this.server,
verifyClient: (info, done) => { verifyClient: (info, done) => {
if (!originIsAllowed(info.origin, this)) { if (!originIsAllowed(info.origin, this)) {
done(false) done(false)
console.log((new Date()) + ' Connection from origin ' + info.origin + ' rejected.') console.log(`${new Date()} connection from origin ${info.origin}`)
return return
} }
done(true) done(true)
} }
}) })
this.wsServer.on('connection', (ws) => { this.wsServer.on('connection', (ws) => {
const { sharedFolder } = this const client = this.getclient()
createClient(ws, sharedFolder as any) createClient(ws, client as any)
if (callback) callback(ws) if (callback) callback(ws, client)
}) })
} }

Loading…
Cancel
Save