From 7a7bb1f4ac36fd7cf2e5ab88c0b78a7a7d406fc4 Mon Sep 17 00:00:00 2001 From: RSDuck Date: Sat, 15 Apr 2017 20:02:30 +0200 Subject: [PATCH 1/2] Reimplemented elrpc client --- package.json | 3 +- src/elrpc/elrpc.ts | 98 ++++++++++++++++++++++++++++ src/elrpc/sexp.ts | 145 ++++++++++++++++++++++++++++++++++++++++++ src/nimSignature.ts | 2 +- src/nimSuggestExec.ts | 27 ++++---- typings/elparser.d.ts | 8 --- typings/elrpc.d.ts | 30 --------- 7 files changed, 259 insertions(+), 54 deletions(-) create mode 100644 src/elrpc/elrpc.ts create mode 100644 src/elrpc/sexp.ts delete mode 100644 typings/elparser.d.ts delete mode 100644 typings/elrpc.d.ts diff --git a/package.json b/package.json index edddc29..d514d85 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,7 @@ "lint": "node ./node_modules/tslint/bin/tslint ./src/*.ts ./test/*.ts" }, "dependencies": { - "nedb": "1.8.0", - "elrpc": "0.1.0" + "nedb": "1.8.0" }, "devDependencies": { "typescript": "^2.0.3", diff --git a/src/elrpc/elrpc.ts b/src/elrpc/elrpc.ts new file mode 100644 index 0000000..042ba84 --- /dev/null +++ b/src/elrpc/elrpc.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------- + * Copyright (C) Xored Software Inc., RSDuck All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------*/ +import net = require("net"); +import sexp = require("./sexp"); + +function envelope(content: string): string { + return ("000000" + content.length.toString(16)).slice(-6) + content; +} + +function generateUID(): number { + return Math.floor(Math.random() * 10000); +} + +export class EPCPeer { + private socket: net.Socket; + + private receivedBuffer: Buffer; + private sessions = new Map void>(); + + constructor(socket: net.Socket) { + this.socket = socket; + this.receivedBuffer = new Buffer(0); + this.socket.on('data', data => { + this.receivedBuffer = Buffer.concat([this.receivedBuffer, data]); + while (this.receivedBuffer.length > 0) { + if (this.receivedBuffer.length >= 6) { + let length = parseInt(this.receivedBuffer.toString("utf8", 0, 6), 16); + if (this.receivedBuffer.length >= length + 6) { + let content = sexp.parseSExp(this.receivedBuffer.toString("utf8", 6, 6 + length)); + if (content) { + let guid = content[0][1]; + this.sessions[guid](content[0]); + this.sessions.delete(guid); + + let endTime = Date.now(); + } else { + this.sessions.forEach(session => { + session("Received invalid SExp data") + }); + } + + this.receivedBuffer = this.receivedBuffer.slice(6 + length); + } else + return; + } + } + }); + this.socket.on("error", (err) => { + console.error(err); + this.sessions.forEach(session => { + session(err); + }); + }); + } + + callMethod(method: string, ...parameter: sexp.SExp[]): Promise { + return new Promise((resolve, reject) => { + let guid = generateUID(); + + let payload = "(call " + guid + " " + method + " " + sexp.toString({ kind: "list", elements: parameter }) + ")"; + + this.sessions[guid] = (data) => { + if (!(data instanceof Array)) { + reject(data); + } else { + switch (data[0]) { + case "return": + resolve(data[2]); + break; + case "return-error": + case "epc-error": + reject(data[2]); + break; + } + } + }; + this.socket.write(envelope(payload)); + }); + } + + stop() { + this.socket.destroy(); + } +} + +export function startClient(port: number): Promise { + return new Promise((resolve, reject) => { + try { + let socket = net.createConnection(port, "localhost", () => { + resolve(new EPCPeer(socket)); + }); + } catch (e) { + reject(e); + } + }); +} diff --git a/src/elrpc/sexp.ts b/src/elrpc/sexp.ts new file mode 100644 index 0000000..7526434 --- /dev/null +++ b/src/elrpc/sexp.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------- + * Copyright (C) Xored Software Inc., RSDuck All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------*/ +export interface SExpCons { kind: "cons"; car, cdr: SExp } +export interface SExpList { kind: "list"; elements: Array } +export interface SExpNumber { kind: "number"; n: number } +export interface SExpIdent { kind: "ident"; ident: string } +export interface SExpString { kind: "string"; str: string } +export interface SExpNil { kind: "nil"; } + +export type SExp = SExpCons | SExpList | SExpNumber | SExpIdent | SExpString | SExpNil + +export function toJS(sexp: SExp): any { + switch (sexp.kind) { + case "cons": + return [toJS(sexp.car), toJS(sexp.cdr)]; + case "list": + return sexp.elements.map(element => toJS(element)); + case "number": + return sexp.n; + case "ident": + return sexp.ident; + case "string": + return sexp.str; + case "nil": + return null; + } +} + +export function toString(sexp: SExp) { + switch (sexp.kind) { + case "cons": + return "(" + toString(sexp.car) + " . " + toString(sexp.cdr) + ")"; + case "list": + let stringRepr = "("; + sexp.elements.forEach(element => { + stringRepr += toString(element) + " "; + }); + return stringRepr.substr(0, stringRepr.length - 1) + ")"; + case "number": + return sexp.n.toString(); + case "ident": + return sexp.ident; + case "string": + return "\"" + sexp.str.replace("\n", "\\n").replace("\r", "\\r").replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + case "nil": + return "nil"; + } +} + +//outputs the SExp directly as a JS object to reduce the processing time +export function parseSExp(input: string): /*SExp*/ any[] | string { + let ptr = 0; + + function parseSymbol(): /*SExpIdent | SExpNumber | SExpNil*/ string | number | null { + let symbolStart = ptr; + while (ptr < input.length && !input[ptr].match(/[ )]/)) { + if (input[ptr] == "\\" && input[ptr + 1] == " ") + ptr += 2; + else + ptr++; + } + let sym = input.substring(symbolStart, ptr); + + if (/^-?\d+$/.test(sym)) + //return { kind: "number", n: parseInt(sym) }; + return parseInt(sym); + //else if (/^-?\d+.\d+$/.test(sym)) + // return { kind: "number", n: parseFloat(sym) }; + else if (sym == "nil") + //return { kind: "nil" }; + return null; + //else if (/^#x-?[\da-fA-F]+$/.test(sym)) + // return { kind: "number", n: parseInt(sym.substr(2), 16) }; + //return { kind: "ident", ident: sym }; + return sym; + } + + function parseString(): /*SExpString*/ string { + let hasEscapes = false; + let startPos = ptr; + while (ptr < input.length && input[ptr] != "\"") { + if (input[ptr] == "\\") { + if (ptr + 1 >= input.length) + throw "Expected character after a escape seqence introducing backslash"; + //string += input[ptr] + input[ptr + 1]; + ptr += 2; + hasEscapes = true; + } else + //string += input[ptr++]; + ptr++; + } + let string = input.substring(startPos, ptr); + //return { kind: "string", str: hasEscapes ? JSON.parse("\"" + string + "\"") : string }; + return hasEscapes ? JSON.parse("\"" + string + "\"") : string; + } + + function parseListOrCon(root?: boolean): /*SExpList | SExpCons*/ any[] { + //let items: SExp[] = []; + let items = []; + let cons = false; + while (ptr < input.length) { + if (/[^() "]/.test(input[ptr])) { + let sym = parseSymbol(); + //if (sym.kind == "ident" && sym.ident == ".") { + if (sym === ".") { + if (items.length == 1) + cons = true; + else + throw "Invalid cons cell syntax"; + } + items.push(sym); + } else if (input[ptr] == "(") { + ptr++; + items.push(parseListOrCon()); + } else if (input[ptr] == "\"") { + ptr++; + items.push(parseString()); + } + if (input[ptr++] == ")") + break; + //ptr++; + } + if (input[ptr - 1] != ")" && !root) + throw "Premature end, expected closing bracket"; + + if (cons) { + if (items.length == 3) { + //return { kind: "cons", car: items[0], cdr: items[2] }; + return [items[0], items[2]]; + } else + throw "Invalid cons cell syntax"; + } + + //return { kind: "list", elements: items }; + return items; + } + + try { + return parseListOrCon(true); + } catch (e) { + return e; + } +} diff --git a/src/nimSignature.ts b/src/nimSignature.ts index 5ecdbea..188646b 100644 --- a/src/nimSignature.ts +++ b/src/nimSignature.ts @@ -66,7 +66,7 @@ export class NimSignatureHelpProvider implements vscode.SignatureHelpProvider { } } - execNimSuggest(NimSuggestType.con, filename, position.line + 1, position.character - 1, getDirtyFile(document)) + execNimSuggest(NimSuggestType.con, filename, position.line + 1, position.character, getDirtyFile(document)) .then(items => { var signatures = new vscode.SignatureHelp(); var isModule = 0; diff --git a/src/nimSuggestExec.ts b/src/nimSuggestExec.ts index 68cea38..7af38ca 100644 --- a/src/nimSuggestExec.ts +++ b/src/nimSuggestExec.ts @@ -11,14 +11,14 @@ import path = require('path'); import os = require('os'); import fs = require('fs'); import net = require('net'); -import elrpc = require('elrpc'); -import elparser = require('elparser'); +import elrpc = require('./elrpc/elrpc'); +import sexp = require('./elrpc/sexp'); import { prepareConfig, getProjectFile, isProjectMode, getNimExecPath, removeDirSync, correctBinname } from './nimUtils'; import { hideNimStatus, showNimStatus } from './nimStatus'; class NimSuggestProcessDescription { process: cp.ChildProcess; - rpc: elrpc.RPCServer; + rpc: elrpc.EPCPeer; } let nimSuggestProcessCache: { [project: string]: PromiseLike } = {}; @@ -157,7 +157,7 @@ export function initNimSuggest(ctx: vscode.ExtensionContext) { let versionOutput = cp.spawnSync(getNimSuggestPath(), ['--version'], { cwd: vscode.workspace.rootPath }).output.toString(); let versionArgs = /.+Version\s([\d|\.]+)\s\(.+/g.exec(versionOutput); if (versionArgs && versionArgs.length === 2) { - _nimSuggestVersion = versionArgs[1]; + _nimSuggestVersion = versionArgs[1]; } console.log(versionOutput); @@ -186,7 +186,7 @@ export async function execNimSuggest(suggestType: NimSuggestType, filename: stri let desc = await getNimSuggestProcess(projectFile); let suggestCmd = NimSuggestType[suggestType]; trace(desc.process.pid, projectFile, suggestCmd + ' ' + normalizedFilename + ':' + line + ':' + column); - let ret = await desc.rpc.callMethod(new elparser.ast.SExpSymbol(suggestCmd), normalizedFilename, line, column, dirtyFile); + let ret = await desc.rpc.callMethod(suggestCmd, { kind: "string", str: normalizedFilename }, { kind: "number", n: line }, { kind: "number", n: column }, { kind: "string", str: dirtyFile }); trace(desc.process.pid, projectFile + '=' + suggestCmd + ' ' + normalizedFilename, ret); var result: NimSuggestResult[] = []; @@ -205,8 +205,8 @@ export async function execNimSuggest(suggestType: NimSuggestType, filename: stri item.column = parts[6]; var doc = parts[7]; if (doc !== '') { - doc = doc.replace(/\\,u000A|\\,u000D\\,u000A/g, '\n'); doc = doc.replace(/\`\`/g, '`'); + doc = doc.replace(/\.\. code-block:: (\w+)\r?\n(( .*\r?\n?)+)/g, '```$1\n$2\n```\n'); doc = doc.replace(/\`([^\<\`]+)\<([^\>]+)\>\`\_/g, '\[$1\]\($2\)'); } item.documentation = doc; @@ -272,16 +272,17 @@ async function getNimSuggestProcess(nimProject: string): Promise { - client.socket.on('error', err => { - console.error(err); - }); - resolve({ process: process, rpc: client }); - }, (reason: any) => { - reject(reason); + elrpc.startClient(portNumber).then((peer) => { + resolve({ process: process, rpc: peer }); }); } }); + process.stdout.once('data', (data) => { + console.log(data.toString()); + }); + process.stderr.once('data', (data) => { + console.log(data.toString()); + }); process.on('close', (code: number, signal: string) => { if (code !== 0) { console.error('nimsuggest closed with code: ' + code + ', signal: ' + signal); diff --git a/typings/elparser.d.ts b/typings/elparser.d.ts deleted file mode 100644 index e5e8f20..0000000 --- a/typings/elparser.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Type definitions for elparser -declare module "elparser" { - export module ast { - export class SExpSymbol { - constructor(s: any); - } - } -} \ No newline at end of file diff --git a/typings/elrpc.d.ts b/typings/elrpc.d.ts deleted file mode 100644 index 95c3a34..0000000 --- a/typings/elrpc.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Type definitions for elrpc -// Project: https://github.com/kiwanami/node-elrpc - -declare module "elrpc" { - import * as net from 'net'; - - /** - * Connect to the TCP port and return RPCServer object. - * @param {number} port - * @param {Method[]} [methods] - * @param {string} [host] - * @return Promise RPCServer - */ - export function startClient(port: number, methods?: Method[], host?: string): PromiseLike; - - export class Method { - name: string - } - - export class RPCServer { - socket: net.Socket - /** - * Stop the RPCServer connection. - * All live sessions are terminated with EPCStackException error. - */ - stop(); - callMethod(...args: any[]): PromiseLike; - queryMethod(): PromiseLike; - } -} \ No newline at end of file From 822aa11b0a6c5c42c0495be45a675e92cf1090d6 Mon Sep 17 00:00:00 2001 From: RSDuck Date: Wed, 19 Apr 2017 00:26:15 +0200 Subject: [PATCH 2/2] Improved error handling --- src/elrpc/elrpc.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/elrpc/elrpc.ts b/src/elrpc/elrpc.ts index 042ba84..cd42095 100644 --- a/src/elrpc/elrpc.ts +++ b/src/elrpc/elrpc.ts @@ -19,6 +19,8 @@ export class EPCPeer { private receivedBuffer: Buffer; private sessions = new Map void>(); + private socketClosed = false; + constructor(socket: net.Socket) { this.socket = socket; this.receivedBuffer = new Buffer(0); @@ -47,16 +49,20 @@ export class EPCPeer { } } }); - this.socket.on("error", (err) => { - console.error(err); + this.socket.on("close", (error) => { + console.error("Connection closed" + (error ? " due to an error" : "")); this.sessions.forEach(session => { - session(err); + session("Connection closed"); }); + this.socketClosed = true; }); } callMethod(method: string, ...parameter: sexp.SExp[]): Promise { return new Promise((resolve, reject) => { + if (this.socketClosed) + reject("Connection closed"); + let guid = generateUID(); let payload = "(call " + guid + " " + method + " " + sexp.toString({ kind: "list", elements: parameter }) + ")"; @@ -81,7 +87,8 @@ export class EPCPeer { } stop() { - this.socket.destroy(); + if (!this.socketClosed) + this.socket.destroy(); } }