diff --git a/README.md b/README.md index 1bd87a5..70f6eb1 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,26 @@ -Sets up a websocket proxy to a JSON-RPC server +Sets up a websocket proxy for any number of language servers. + +Each server is run as a subprocess which is connected to by sending the client +to the URL / based on a configuration file defined locally. For example, +with the following defined as `servers.yml`: + +``` +langservers: + python: + - python + - python-langserver.py + - --stdio + go: + - /usr/local/bin/go + - langserver.go +``` + +The client would connect to `ws://localhost/python` to get a python language server Usage: ``` npm install npm run prepare -node dist/server.js --port 3000 --remotePath localhost --remotePort 2089 +node dist/server.js --port 3000 --languageServers servers.yml ``` - -The only required flag to the server is `--remotePort` diff --git a/package-lock.json b/package-lock.json index 5d70d1a..a15e7e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,25 +4,47 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/events": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", + "dev": true + }, "@types/node": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.1.tgz", - "integrity": "sha512-fZvabBkcFJzc+eJN2XTuhKhop1RKdlGQgjmQuxYuQJ6K5rMNoHr6tomb6q0E8Axe+WPyfe/lr7CnnkGvzNh5mA==", + "version": "10.12.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.3.tgz", + "integrity": "sha512-sfGmOtSMSbQ/AKG8V9xD1gmjquC9awIIZ/Kj309pHb2n3bcRAcGMQv5nJ6gCXZVsneGE4+ve8DXKRCsrg3TFzg==", "dev": true }, "@types/ws": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-0.0.39.tgz", - "integrity": "sha1-0jhsNHXrZOVhE3okWk0dE7H2n9E=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", + "integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==", "dev": true, "requires": { + "@types/events": "*", "@types/node": "*" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "brace-expansion": { "version": "1.1.11", @@ -32,14 +54,6 @@ "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - } } }, "concat-map": { @@ -48,6 +62,11 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -84,6 +103,15 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -110,7 +138,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -123,6 +151,11 @@ "glob": "^7.0.5" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, "typescript": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.6.tgz", @@ -130,17 +163,97 @@ "dev": true }, "vscode-ws-jsonrpc": { - "version": "0.0.2-2", - "resolved": "https://registry.npmjs.org/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-0.0.2-2.tgz", - "integrity": "sha512-hViHObJHtxD0KX8tvP6QL8fJGfH9mmDrEkdfLKj6Mf1uaxypoMBnjcZDCU3N4l7VriQiNRbohe/FlMrC3/0r7Q==", + "version": "file:../vscode-ws-jsonrpc", + "dev": true, "requires": { - "vscode-jsonrpc": "^3.6.0" + "vscode-jsonrpc": "^4.0.0" }, "dependencies": { + "@types/node": { + "version": "7.0.56", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "requires": { + "glob": "^7.0.5" + } + }, + "typescript": { + "version": "2.7.2", + "bundled": true + }, "vscode-jsonrpc": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz", - "integrity": "sha512-T24Jb5V48e4VgYliUXMnZ379ItbrXgOimweKaJshD84z+8q7ZOZjJan0MeDe+Ugb+uqERDVV8SBmemaGMSMugA==" + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "bundled": true } } }, @@ -151,9 +264,10 @@ "dev": true }, "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz", + "integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==", + "dev": true, "requires": { "async-limiter": "~1.0.0" } diff --git a/package.json b/package.json index b9d7754..0ce26fa 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,16 @@ "name": "jsonrpc-ws-proxy", "version": "0.0.1", "dependencies": { - "vscode-ws-jsonrpc": "^0.0.2-1", - "ws": "^5.0.0" + "js-yaml": "^3.12.0" }, "devDependencies": { - "@types/node": "^7.10.1", - "@types/ws": "0.0.39", + "@types/node": "^10.12.3", + "@types/ws": "^6.0.1", "minimist": "^1.2.0", "rimraf": "^2.6.2", - "typescript": "^3.0.1" + "typescript": "^3.0.1", + "vscode-ws-jsonrpc": "file:../vscode-ws-jsonrpc", + "ws": "^6.1.0" }, "scripts": { "prepare": "npm run clean && npm run build", diff --git a/src/server.ts b/src/server.ts index 002ed80..23a19b1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,45 +1,77 @@ #!/usr/bin/env node -import * as net from 'net'; import * as ws from 'ws'; import * as rpc from 'vscode-ws-jsonrpc'; import * as rpcServer from 'vscode-ws-jsonrpc/lib/server'; import * as parseArgs from 'minimist'; +import * as http from 'http'; +import * as yaml from 'js-yaml'; +import * as fs from 'fs'; let argv = parseArgs(process.argv.slice(2)); -if (argv.help) { - console.log(`Usage: server.js --port 3000 --remotePath localhost -- 2089`); +if (argv.help || !argv.languageServers) { + console.log(`Usage: server.js --port 3000 --languageServers config.yml`); process.exit(1); } -let serverPath : number = parseInt(argv.port) || 3000; -let remotePath : string = argv.remotePath || '127.0.0.1'; -let remotePort : number = parseInt(argv.remotePort); +let serverPort : number = parseInt(argv.port) || 3000; -if (!remotePort) { - console.log('--remotePort is required'); +let languageServers; +try { + let parsed = yaml.safeLoad(fs.readFileSync(argv.languageServers), 'utf8'); + if (!parsed.langservers) { + console.log('Your langservers file is not a valid format, see README.md'); + process.exit(1); + } + languageServers = parsed.langservers; +} catch (e) { + console.error(e); process.exit(1); } const wss : ws.Server = new ws.Server({ - port: serverPath, + port: serverPort, perMessageDeflate: false }, () => { - console.log(`Listening to http and ws requests on ${serverPath}`); + console.log(`Listening to http and ws requests on ${serverPort}`); }); -const localSocket = net.connect(remotePort, remotePath, () => { - console.log(`Connected to local server ${remotePath}:${remotePort}`); -}); -const localReader : rpc.SocketMessageReader = new rpc.SocketMessageReader(localSocket); -const localWriter : rpc.SocketMessageWriter = new rpc.SocketMessageWriter(localSocket); -const localConnection : rpcServer.IConnection = rpcServer.createConnection(localReader, localWriter, () => {}); +function toSocket(webSocket: ws): rpc.IWebSocket { + return { + send: content => webSocket.send(content), + onMessage: cb => webSocket.onmessage = event => cb(event.data), + onError: cb => webSocket.onerror = event => { + if ('message' in event) { + cb((event as any).message) + } + }, + onClose: cb => webSocket.onclose = event => cb(event.code, event.reason), + dispose: () => webSocket.close() + } +} -wss.on('connection', (client : ws) => { - // The ws interface is compatible with the expected socket interface, except binaryType support - const socket : rpc.IWebSocket = rpc.toSocket(client as any as WebSocket); - const connection : rpcServer.IConnection = rpcServer.createWebSocketConnection(socket); +wss.on('connection', (client : ws, request : http.IncomingMessage) => { + let langServer : string[]; + + Object.keys(languageServers).forEach((key) => { + if (request.url === '/' + key) { + langServer = languageServers[key]; + } + }); + if (!langServer || !langServer.length) { + console.error('Invalid language server', request.url); + client.close(); + return; + } + + let localConnection = rpcServer.createServerProcess('Example', langServer[0], langServer.slice(1)); + let socket : rpc.IWebSocket = toSocket(client); + let connection = rpcServer.createWebSocketConnection(socket); rpcServer.forward(connection, localConnection); console.log(`Forwarding new client`); + socket.onClose((code, reason) => { + console.log('Client closed', reason); + localConnection.dispose(); + }); }); diff --git a/tsconfig.json b/tsconfig.json index b80949f..2200544 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,10 @@ { "compilerOptions": { - "rootDir": "src", + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", "outDir": "dist", - "baseUrl": ".", - "skipLibCheck": true + "lib": ["es2016", "dom"] }, "include": [ "src"