scoundrel-remote-eval 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +29 -0
- package/spec/reference-with-proxy-spec.js +40 -0
- package/spec/support/jasmine.json +13 -0
- package/spec/web-socket-javascript-spec.js +38 -0
- package/spec/web-socket-python-spec.js +46 -0
- package/src/client/connections/web-socket/index.js +67 -0
- package/src/client/index.js +135 -0
- package/src/client/reference-proxy.js +29 -0
- package/src/client/reference.js +24 -0
- package/src/index.js +0 -0
- package/src/logger.js +28 -0
- package/src/python-web-socket-runner.js +60 -0
- package/src/server/client/index.js +62 -0
- package/src/server/connections/web-socket/client.js +28 -0
- package/src/server/connections/web-socket/index.js +22 -0
- package/src/server/index.js +17 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scoundrel-remote-eval",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "jasmine"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/kaspernj/scoundrel.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"scoundrel"
|
|
16
|
+
],
|
|
17
|
+
"author": "kasper@diestoeckels.de",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/kaspernj/scoundrel/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/kaspernj/scoundrel#readme",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"jasmine": "^5.1.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"ws": "^8.14.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Client from "../src/client/index.js"
|
|
2
|
+
import ClientWebSocket from "../src/client/connections/web-socket/index.js"
|
|
3
|
+
import referenceWithProxy from "../src/client/reference-proxy.js"
|
|
4
|
+
import Server from "../src/server/index.js"
|
|
5
|
+
import ServerWebSocket from "../src/server/connections/web-socket/index.js"
|
|
6
|
+
import {WebSocket, WebSocketServer} from "ws"
|
|
7
|
+
|
|
8
|
+
const shared = {}
|
|
9
|
+
|
|
10
|
+
describe("referenceWithProxy", () => {
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
shared.wss = new WebSocketServer({port: 8080})
|
|
13
|
+
shared.serverWebSocket = new ServerWebSocket(shared.wss)
|
|
14
|
+
shared.server = new Server(shared.serverWebSocket)
|
|
15
|
+
|
|
16
|
+
shared.ws = new WebSocket("http://localhost:8080")
|
|
17
|
+
shared.clientWebSocket = new ClientWebSocket(shared.ws)
|
|
18
|
+
|
|
19
|
+
await shared.clientWebSocket.waitForOpened()
|
|
20
|
+
|
|
21
|
+
shared.client = new Client(shared.clientWebSocket)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await shared.client.close()
|
|
26
|
+
await shared.server.close()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("creates a reference with a proxy", async () => {
|
|
30
|
+
const stringObjectReference = await shared.client.newObjectWithReference("Array")
|
|
31
|
+
const stringObject = referenceWithProxy(stringObjectReference)
|
|
32
|
+
|
|
33
|
+
await stringObject.push("test1")
|
|
34
|
+
await stringObject.push("test2")
|
|
35
|
+
|
|
36
|
+
const result = await stringObject.__serialize()
|
|
37
|
+
|
|
38
|
+
expect(result).toEqual(["test1", "test2"])
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Client from "../src/client/index.js"
|
|
2
|
+
import ClientWebSocket from "../src/client/connections/web-socket/index.js"
|
|
3
|
+
import Server from "../src/server/index.js"
|
|
4
|
+
import ServerWebSocket from "../src/server/connections/web-socket/index.js"
|
|
5
|
+
import {WebSocket, WebSocketServer} from "ws"
|
|
6
|
+
|
|
7
|
+
const shared = {}
|
|
8
|
+
|
|
9
|
+
describe("scoundrel - web-socket - javascript", () => {
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
shared.wss = new WebSocketServer({port: 8080})
|
|
12
|
+
shared.serverWebSocket = new ServerWebSocket(shared.wss)
|
|
13
|
+
shared.server = new Server(shared.serverWebSocket)
|
|
14
|
+
|
|
15
|
+
shared.ws = new WebSocket("http://localhost:8080")
|
|
16
|
+
shared.clientWebSocket = new ClientWebSocket(shared.ws)
|
|
17
|
+
|
|
18
|
+
await shared.clientWebSocket.waitForOpened()
|
|
19
|
+
|
|
20
|
+
shared.client = new Client(shared.clientWebSocket)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await shared.client.close()
|
|
25
|
+
await shared.server.close()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("creates a server and connects to it with the client", async () => {
|
|
29
|
+
const stringObject = await shared.client.newObjectWithReference("Array")
|
|
30
|
+
|
|
31
|
+
await stringObject.callMethod("push", "test1")
|
|
32
|
+
await stringObject.callMethod("push", "test2")
|
|
33
|
+
|
|
34
|
+
const result = await stringObject.serialize()
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual(["test1", "test2"])
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Client from "../src/client/index.js"
|
|
2
|
+
import ClientWebSocket from "../src/client/connections/web-socket/index.js"
|
|
3
|
+
import PythonWebSocketRunner from "../src/python-web-socket-runner.js"
|
|
4
|
+
import {WebSocket} from "ws"
|
|
5
|
+
|
|
6
|
+
const shared = {}
|
|
7
|
+
|
|
8
|
+
describe("scoundrel - web-socket - python", () => {
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
shared.pythonWebSocketRunner = new PythonWebSocketRunner()
|
|
11
|
+
|
|
12
|
+
await shared.pythonWebSocketRunner.runAndWaitForPid()
|
|
13
|
+
|
|
14
|
+
const ws = new WebSocket("ws://localhost:53874")
|
|
15
|
+
const clientWebSocket = new ClientWebSocket(ws)
|
|
16
|
+
|
|
17
|
+
await clientWebSocket.waitForOpened()
|
|
18
|
+
|
|
19
|
+
shared.client = new Client(clientWebSocket)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
shared.client?.close()
|
|
24
|
+
shared.pythonWebSocketRunner?.close()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("creates a server and connects to it with the client", async () => {
|
|
28
|
+
const stringObject = await shared.client.newObjectWithReference("[]")
|
|
29
|
+
|
|
30
|
+
await stringObject.callMethod("append", "test1")
|
|
31
|
+
await stringObject.callMethod("append", "test2")
|
|
32
|
+
|
|
33
|
+
const result = await stringObject.serialize()
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual(["test1", "test2"])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("imports classes and uses them", async () => {
|
|
39
|
+
const math = await shared.client.import("math")
|
|
40
|
+
const pi = await math.readAttributeWithReference("pi")
|
|
41
|
+
const cosOfPi = await math.callMethodWithReference("cos", pi)
|
|
42
|
+
const result = await cosOfPi.serialize()
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual(-1)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Logger from "../../../logger.js"
|
|
2
|
+
|
|
3
|
+
const logger = new Logger("Scoundrel WebSocket")
|
|
4
|
+
|
|
5
|
+
// logger.setDebug(true)
|
|
6
|
+
|
|
7
|
+
export default class WebSocket {
|
|
8
|
+
constructor(ws) {
|
|
9
|
+
this.ws = ws
|
|
10
|
+
this.ws.addEventListener("error", this.onSocketError)
|
|
11
|
+
this.ws.addEventListener("open", this.onSocketOpen)
|
|
12
|
+
this.ws.addEventListener("message", this.onSocketMessage)
|
|
13
|
+
|
|
14
|
+
this.commands = {}
|
|
15
|
+
this.commandsCount = 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async close() {
|
|
19
|
+
await this.ws.close()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onSocketError = (event) => {
|
|
23
|
+
logger.error(() => ["onSocketError", event])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onSocketMessage = (event) => {
|
|
27
|
+
const data = JSON.parse(event.data)
|
|
28
|
+
const commandId = data.command_id
|
|
29
|
+
|
|
30
|
+
if (!(commandId in this.commands)) throw new Error(`Command ${commandId} not found`)
|
|
31
|
+
|
|
32
|
+
const command = this.commands[commandId]
|
|
33
|
+
|
|
34
|
+
delete this.commands[commandId]
|
|
35
|
+
|
|
36
|
+
if (data.error) {
|
|
37
|
+
command.reject(new Error(data.error))
|
|
38
|
+
} else {
|
|
39
|
+
command.resolve(data.data)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onSocketOpen = (event) => {
|
|
44
|
+
logger.log(() =>"onSocketOpen")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
send(data) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const commandCount = ++this.commandsCount
|
|
50
|
+
const sendData = JSON.stringify({
|
|
51
|
+
command_id: commandCount,
|
|
52
|
+
data
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.commands[commandCount] = {resolve, reject}
|
|
56
|
+
|
|
57
|
+
logger.log(() => ["Sending", sendData])
|
|
58
|
+
|
|
59
|
+
this.ws.send(sendData)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
waitForOpened = () => new Promise((resolve, reject) => {
|
|
64
|
+
this.ws.addEventListener("open", resolve)
|
|
65
|
+
this.ws.addEventListener("error", reject)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import Logger from "../logger.js"
|
|
2
|
+
import Reference from "./reference.js"
|
|
3
|
+
|
|
4
|
+
const logger = new Logger("Scoundrel Client")
|
|
5
|
+
|
|
6
|
+
// logger.setDebug(true)
|
|
7
|
+
|
|
8
|
+
export default class Client {
|
|
9
|
+
constructor(backend) {
|
|
10
|
+
this.backend = backend
|
|
11
|
+
this.references = {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async close() {
|
|
15
|
+
this.backend.close()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async callMethodOnReference(referenceId, methodName, ...args) {
|
|
19
|
+
return await this.backend.send({
|
|
20
|
+
args: this.parseArg(args),
|
|
21
|
+
command: "call_method_on_reference",
|
|
22
|
+
method_name: methodName,
|
|
23
|
+
reference_id: referenceId,
|
|
24
|
+
with: "result"
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async callMethodOnReferenceWithReference(referenceId, methodName, ...args) {
|
|
29
|
+
const result = await this.backend.send({
|
|
30
|
+
args: this.parseArg(args),
|
|
31
|
+
command: "call_method_on_reference",
|
|
32
|
+
method_name: methodName,
|
|
33
|
+
reference_id: referenceId,
|
|
34
|
+
with: "reference"
|
|
35
|
+
})
|
|
36
|
+
const id = result.response
|
|
37
|
+
|
|
38
|
+
return this.spawnReference(id)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async evalWithReference(evalString) {
|
|
42
|
+
const result = await this.backend.send({
|
|
43
|
+
command: "eval",
|
|
44
|
+
eval_string: evalString,
|
|
45
|
+
with_reference: true
|
|
46
|
+
})
|
|
47
|
+
const id = result.object_id
|
|
48
|
+
|
|
49
|
+
return this.spawnReference(id)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async import(importName) {
|
|
53
|
+
const result = await this.backend.send({
|
|
54
|
+
command: "import",
|
|
55
|
+
import_name: importName
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
logger.log(() => ["import", {result}])
|
|
59
|
+
|
|
60
|
+
const id = result.object_id
|
|
61
|
+
|
|
62
|
+
return this.spawnReference(id)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async newObjectWithReference(className, ...args) {
|
|
66
|
+
const result = await this.backend.send({
|
|
67
|
+
args: this.parseArg(args),
|
|
68
|
+
command: "new_object_with_reference",
|
|
69
|
+
class_name: className
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (!result) throw new Error("Blank result given")
|
|
73
|
+
|
|
74
|
+
const id = result.object_id
|
|
75
|
+
|
|
76
|
+
return this.spawnReference(id)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
isPlainObject = (input) => {
|
|
80
|
+
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
parseArg(arg) {
|
|
88
|
+
if (Array.isArray(arg)) {
|
|
89
|
+
return arg.map((argInArray) => this.parseArg(argInArray))
|
|
90
|
+
} else if (arg instanceof Reference) {
|
|
91
|
+
return {
|
|
92
|
+
__scoundrel_object_id: arg.id,
|
|
93
|
+
__scoundrel_type: "reference"
|
|
94
|
+
}
|
|
95
|
+
} else if (this.isPlainObject(arg)) {
|
|
96
|
+
const newObject = {}
|
|
97
|
+
|
|
98
|
+
for (const key in arg) {
|
|
99
|
+
const value = arg[key]
|
|
100
|
+
|
|
101
|
+
newObject[key] = this.parseArg(value)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return newObject
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return arg
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async readAttributeOnReferenceWithReference(referenceId, attributeName) {
|
|
111
|
+
const result = await this.backend.send({
|
|
112
|
+
command: "read_attribute",
|
|
113
|
+
attribute_name: attributeName,
|
|
114
|
+
reference_id: referenceId,
|
|
115
|
+
with: "reference"
|
|
116
|
+
})
|
|
117
|
+
const id = result.response
|
|
118
|
+
|
|
119
|
+
return this.spawnReference(id)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async serializeReference(referenceId) {
|
|
123
|
+
const json = await this.backend.send({command: "serialize_reference", reference_id: referenceId})
|
|
124
|
+
|
|
125
|
+
return JSON.parse(json)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
spawnReference(id) {
|
|
129
|
+
const reference = new Reference(this, id)
|
|
130
|
+
|
|
131
|
+
this.references[id] = reference
|
|
132
|
+
|
|
133
|
+
return reference
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const proxyMethodSpawner = (reference, prop) => (...args) => reference.callMethodWithReference(prop, ...args)
|
|
2
|
+
|
|
3
|
+
const proxyObjectHandler = {
|
|
4
|
+
get(reference, prop) {
|
|
5
|
+
if (typeof reference == "function") reference = reference()
|
|
6
|
+
|
|
7
|
+
if (prop == "__serialize") {
|
|
8
|
+
const method = reference.serialize
|
|
9
|
+
const boundMethod = method.bind(reference)
|
|
10
|
+
|
|
11
|
+
return boundMethod
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return proxyMethodSpawner(reference, prop)
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
set(receiver, prop, newValue) {
|
|
18
|
+
throw new Error("set property isn't supported yet")
|
|
19
|
+
|
|
20
|
+
if (typeof receiver == "function") receiver = receiver()
|
|
21
|
+
if (!(prop in receiver)) throw new PropertyNotFoundError(`Property not found: ${prop}`)
|
|
22
|
+
|
|
23
|
+
return Reflect.set(receiver, prop, newValue)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const referenceProxy = (wrappedObject) => new Proxy(wrappedObject, proxyObjectHandler)
|
|
28
|
+
|
|
29
|
+
export default referenceProxy
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export default class Reference {
|
|
2
|
+
constructor(client, id) {
|
|
3
|
+
this.client = client
|
|
4
|
+
this.id = id
|
|
5
|
+
|
|
6
|
+
if (!id) throw new Error(`Invalid ID given: ${id}`)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async callMethod(methodName, ...args) {
|
|
10
|
+
return await this.client.callMethodOnReference(this.id, methodName, ...args)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async callMethodWithReference(methodName, ...args) {
|
|
14
|
+
return await this.client.callMethodOnReferenceWithReference(this.id, methodName, ...args)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async readAttributeWithReference(attributeName, ...args) {
|
|
18
|
+
return await this.client.readAttributeOnReferenceWithReference(this.id, attributeName, ...args)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async serialize() {
|
|
22
|
+
return await this.client.serializeReference(this.id)
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.js
ADDED
|
File without changes
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export default class Logger {
|
|
2
|
+
constructor(scopeName) {
|
|
3
|
+
this.debug = false
|
|
4
|
+
this.scopeName = scopeName
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
setDebug(newValue) {
|
|
8
|
+
this.debug = newValue
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
log(...args) {
|
|
12
|
+
if (!this.debug) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (args.length == 1 && typeof args[0] == "function") {
|
|
17
|
+
const callbackArgs = args[0]()
|
|
18
|
+
|
|
19
|
+
if (Array.isArray(callbackArgs)) {
|
|
20
|
+
console.log(this.scopeName, ...callbackArgs)
|
|
21
|
+
} else {
|
|
22
|
+
console.log(this.scopeName, callbackArgs)
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
console.log(this.scopeName, ...args)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {exec, spawn} from "child_process"
|
|
2
|
+
import Logger from "./logger.js"
|
|
3
|
+
import {realpath} from "node:fs/promises"
|
|
4
|
+
|
|
5
|
+
const logger = new Logger("Scoundrel PythonWebSocketRunner")
|
|
6
|
+
|
|
7
|
+
export default class PythonWebSocketRunner {
|
|
8
|
+
constructor() {
|
|
9
|
+
process.on("exit", this.onProcessExit)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
runAndWaitForPid() {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
this.waitForPidResolve = resolve
|
|
15
|
+
this.run()
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async run() {
|
|
20
|
+
const filePath = `${process.cwd()}/../python/server/web-socket.py`
|
|
21
|
+
const fileRealPath = await realpath(filePath)
|
|
22
|
+
const child = spawn("python3", [fileRealPath])
|
|
23
|
+
child.stdout.on("data", this.onChildStdout)
|
|
24
|
+
child.stderr.on("data", this.onChildStderr)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onProcessExit = () => {
|
|
28
|
+
if (this.pid) {
|
|
29
|
+
this.close()
|
|
30
|
+
logger.log(() => `onProcessExit: Killing Python process with PID ${this.pid}`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onChildStderr = (data) => {
|
|
35
|
+
logger.log(() => `stderr: ${data}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onChildStdout = (data) => {
|
|
39
|
+
logger.log(() => `stdout: ${data}`)
|
|
40
|
+
|
|
41
|
+
const match = (`${data}`).match(/^Started with PID (\d+) on (.+):(.+)\n$/)
|
|
42
|
+
|
|
43
|
+
if (match) {
|
|
44
|
+
this.pid = match[1]
|
|
45
|
+
|
|
46
|
+
logger.log(() => `Registered PID ${this.pid}`)
|
|
47
|
+
|
|
48
|
+
if (this.waitForPidResolve) {
|
|
49
|
+
this.waitForPidResolve()
|
|
50
|
+
this.waitForPidResolve = undefined
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
close() {
|
|
56
|
+
if (this.pid) {
|
|
57
|
+
exec(`kill ${this.pid}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export default class ServerClient {
|
|
2
|
+
constructor(clientBackend) {
|
|
3
|
+
this.clientBackend = clientBackend
|
|
4
|
+
this.clientBackend.onCommand(this.onCommand)
|
|
5
|
+
this.objects = {}
|
|
6
|
+
this.objectsCount = 0
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
onCommand = (commandId, data) => {
|
|
10
|
+
try {
|
|
11
|
+
if (data.command == "new_object_with_reference") {
|
|
12
|
+
const className = data.class_name
|
|
13
|
+
const objectId = ++this.objectsCount
|
|
14
|
+
let object
|
|
15
|
+
|
|
16
|
+
if (typeof className == "string") {
|
|
17
|
+
const classInstance = global[className]
|
|
18
|
+
|
|
19
|
+
if (!classInstance) throw new Error(`No such class: ${className}`)
|
|
20
|
+
|
|
21
|
+
object = new global[className](...data.args)
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error(`Don't know how to handle class name: ${typeof className}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.objects[objectId] = object
|
|
27
|
+
|
|
28
|
+
this.respondToCommand(commandId, {object_id: objectId})
|
|
29
|
+
} else if (data.command == "call_method_on_reference") {
|
|
30
|
+
const referenceId = data.reference_id
|
|
31
|
+
const object = this.objects[referenceId]
|
|
32
|
+
|
|
33
|
+
if (!object) throw new Error(`No object by that ID: ${referenceId}`)
|
|
34
|
+
|
|
35
|
+
const method = object[data.method_name]
|
|
36
|
+
|
|
37
|
+
if (!method) throw new Error(`No method called '${data.method_name}' on a '${object.constructor.name}'`)
|
|
38
|
+
|
|
39
|
+
const response = method.call(object, ...data.args)
|
|
40
|
+
|
|
41
|
+
this.respondToCommand(commandId, {response})
|
|
42
|
+
} else if (data.command == "serialize_reference") {
|
|
43
|
+
const referenceId = data.reference_id
|
|
44
|
+
const object = this.objects[referenceId]
|
|
45
|
+
|
|
46
|
+
if (!object) throw new Error(`No object by that ID: ${referenceId}`)
|
|
47
|
+
|
|
48
|
+
this.respondToCommand(commandId, JSON.stringify(object))
|
|
49
|
+
} else {
|
|
50
|
+
this.clientBackend.send({type: "command_response", command_id: commandId, error: `Unknown command: ${data.command}`})
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.clientBackend.send({type: "command_response", command_id: commandId, error: `Unknown command: ${error.message}`})
|
|
54
|
+
|
|
55
|
+
console.error(error)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
respondToCommand(commandId, data) {
|
|
60
|
+
this.clientBackend.send({type: "command_response", command_id: commandId, data})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export default class WebSocketClient {
|
|
2
|
+
constructor(ws) {
|
|
3
|
+
this.ws = ws
|
|
4
|
+
|
|
5
|
+
ws.on("error", this.onError)
|
|
6
|
+
ws.on("message", this.onMessage)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
onCommand(callback) {
|
|
10
|
+
this.onCommandCallback = callback
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
onError = (error) => {
|
|
14
|
+
console.error("WebSocketClient error", error)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onMessage = (rawData) => {
|
|
18
|
+
const data = JSON.parse(rawData)
|
|
19
|
+
|
|
20
|
+
if (!this.onCommandCallback) throw new Error("Command callback hasn't been set")
|
|
21
|
+
|
|
22
|
+
this.onCommandCallback(data.command_id, data.data)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async send(data) {
|
|
26
|
+
await this.ws.send(JSON.stringify(data))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import WebSocketClient from "./client.js"
|
|
2
|
+
|
|
3
|
+
export default class WebSocket {
|
|
4
|
+
constructor(webSocketServer) {
|
|
5
|
+
this.wss = webSocketServer
|
|
6
|
+
this.wss.on("connection", this.onConnection)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
close = () => this.wss.close()
|
|
10
|
+
|
|
11
|
+
onConnection = (ws) => {
|
|
12
|
+
if (!this.onNewClientCallback) throw new Error("'onNewClient' hasn't been called")
|
|
13
|
+
|
|
14
|
+
this.onNewClientCallback(new WebSocketClient(ws))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onNewClient = (callback) => {
|
|
18
|
+
if (!callback) throw new Error("No callback was given")
|
|
19
|
+
|
|
20
|
+
this.onNewClientCallback = callback
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Client from "./client/index.js"
|
|
2
|
+
|
|
3
|
+
export default class ScoundrelServer {
|
|
4
|
+
constructor(backend) {
|
|
5
|
+
this.backend = backend
|
|
6
|
+
this.backend.onNewClient(this.onNewClient)
|
|
7
|
+
this.clients = []
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
close = () => this.backend.close()
|
|
11
|
+
|
|
12
|
+
onNewClient = (clientBackend) => {
|
|
13
|
+
const client = new Client(clientBackend)
|
|
14
|
+
|
|
15
|
+
this.clients.push(client)
|
|
16
|
+
}
|
|
17
|
+
}
|