-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #48 from RSDuck/master
Reimplemented the elrpc client
- Loading branch information
Showing
7 changed files
with
266 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/*--------------------------------------------------------- | ||
* 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<number, (data: any) => void>(); | ||
|
||
private socketClosed = false; | ||
|
||
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 = <any[]>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("close", (error) => { | ||
console.error("Connection closed" + (error ? " due to an error" : "")); | ||
this.sessions.forEach(session => { | ||
session("Connection closed"); | ||
}); | ||
this.socketClosed = true; | ||
}); | ||
} | ||
|
||
callMethod(method: string, ...parameter: sexp.SExp[]): Promise<any[]> { | ||
return new Promise<any[]>((resolve, reject) => { | ||
if (this.socketClosed) | ||
reject("Connection closed"); | ||
|
||
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() { | ||
if (!this.socketClosed) | ||
this.socket.destroy(); | ||
} | ||
} | ||
|
||
export function startClient(port: number): Promise<EPCPeer> { | ||
return new Promise<EPCPeer>((resolve, reject) => { | ||
try { | ||
let socket = net.createConnection(port, "localhost", () => { | ||
resolve(new EPCPeer(socket)); | ||
}); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SExp> } | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.