scoundrel-remote-eval 1.0.5 → 1.0.7
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 +1 -1
- package/spec/reference-with-proxy-spec.js +30 -0
- package/spec/web-socket-javascript-spec.js +15 -0
- package/spec/web-socket-python-spec.js +9 -0
- package/src/client/connections/web-socket/index.js +11 -27
- package/src/client/index.js +280 -17
- package/src/client/reference.js +39 -0
- package/src/logger.js +20 -0
- package/src/python-web-socket-runner.js +30 -3
- package/src/server/connections/web-socket/client.js +1 -1
- package/src/server/connections/web-socket/index.js +2 -2
- package/src/server/index.js +6 -24
- package/src/server/client/index.js +0 -103
package/package.json
CHANGED
|
@@ -19,6 +19,12 @@ describe("referenceWithProxy", () => {
|
|
|
19
19
|
await shared.clientWebSocket.waitForOpened()
|
|
20
20
|
|
|
21
21
|
shared.client = new Client(shared.clientWebSocket)
|
|
22
|
+
|
|
23
|
+
shared.serverClient = shared.server.getClients()[0]
|
|
24
|
+
|
|
25
|
+
if (!shared.serverClient) {
|
|
26
|
+
throw new Error("No client connected to server")
|
|
27
|
+
}
|
|
22
28
|
})
|
|
23
29
|
|
|
24
30
|
afterEach(async () => {
|
|
@@ -54,4 +60,28 @@ describe("referenceWithProxy", () => {
|
|
|
54
60
|
expect(firstValue).toEqual("test1")
|
|
55
61
|
expect(secondValue).toEqual("test2")
|
|
56
62
|
})
|
|
63
|
+
|
|
64
|
+
it("calls methods", async () => {
|
|
65
|
+
const stringObjectReference = await shared.client.newObjectWithReference("Array")
|
|
66
|
+
const stringObject = referenceWithProxy(stringObjectReference)
|
|
67
|
+
|
|
68
|
+
await stringObject.push("test1")
|
|
69
|
+
await stringObject.push("test2")
|
|
70
|
+
|
|
71
|
+
const result = await stringObject.__serialize()
|
|
72
|
+
|
|
73
|
+
expect(result).toEqual(["test1", "test2"])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("calls methods on the client from the server", async () => {
|
|
77
|
+
const stringObjectReference = await shared.serverClient.newObjectWithReference("Array")
|
|
78
|
+
const stringObject = referenceWithProxy(stringObjectReference)
|
|
79
|
+
|
|
80
|
+
await stringObject.push("test1")
|
|
81
|
+
await stringObject.push("test2")
|
|
82
|
+
|
|
83
|
+
const result = await stringObject.__serialize()
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual(["test1", "test2"])
|
|
86
|
+
})
|
|
57
87
|
})
|
|
@@ -46,4 +46,19 @@ describe("scoundrel - web-socket - javascript", () => {
|
|
|
46
46
|
|
|
47
47
|
expect(result).toEqual("test1, test2")
|
|
48
48
|
})
|
|
49
|
+
|
|
50
|
+
it("handles errors from method calls", async () => {
|
|
51
|
+
const stringObject = await shared.client.newObjectWithReference("Array")
|
|
52
|
+
|
|
53
|
+
let caughtError = null
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await stringObject.callMethod("nonExistentMethod")
|
|
57
|
+
} catch (error) {
|
|
58
|
+
caughtError = error
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
expect(caughtError).toBeInstanceOf(Error)
|
|
62
|
+
expect(caughtError.message).toEqual("No method called 'nonExistentMethod' on a 'Array'")
|
|
63
|
+
})
|
|
49
64
|
})
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import Client from "../src/client/index.js"
|
|
2
2
|
import ClientWebSocket from "../src/client/connections/web-socket/index.js"
|
|
3
|
+
import Logger from "../src/logger.js"
|
|
3
4
|
import PythonWebSocketRunner from "../src/python-web-socket-runner.js"
|
|
4
5
|
import {WebSocket} from "ws"
|
|
5
6
|
|
|
6
7
|
const shared = {}
|
|
8
|
+
const logger = new Logger("Scoundrel WebSocket Python Spec")
|
|
7
9
|
|
|
8
10
|
describe("scoundrel - web-socket - python", () => {
|
|
9
11
|
beforeEach(async () => {
|
|
12
|
+
logger.log("Starting Python with client")
|
|
10
13
|
shared.pythonWebSocketRunner = new PythonWebSocketRunner()
|
|
11
14
|
|
|
15
|
+
logger.log("Running Python WebSocket runner and waiting for PID")
|
|
12
16
|
await shared.pythonWebSocketRunner.runAndWaitForPid()
|
|
13
17
|
|
|
18
|
+
logger.log("Starting WebSocket client connection")
|
|
14
19
|
const ws = new WebSocket("ws://localhost:53874")
|
|
20
|
+
|
|
21
|
+
logger.log("Creating ClientWebSocket")
|
|
15
22
|
const clientWebSocket = new ClientWebSocket(ws)
|
|
16
23
|
|
|
24
|
+
logger.log("Waiting for WebSocket to open")
|
|
17
25
|
await clientWebSocket.waitForOpened()
|
|
18
26
|
|
|
27
|
+
logger.log("Creating Scoundrel Client")
|
|
19
28
|
shared.client = new Client(clientWebSocket)
|
|
20
29
|
})
|
|
21
30
|
|
|
@@ -19,45 +19,29 @@ export default class WebSocket {
|
|
|
19
19
|
await this.ws.close()
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
onCommand(callback) {
|
|
23
|
+
this.onCommandCallback = callback
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
onSocketError = (event) => {
|
|
23
27
|
logger.error(() => ["onSocketError", event])
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
onSocketMessage = (event) => {
|
|
27
31
|
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
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (data.error) {
|
|
37
|
-
command.reject(new Error(data.error))
|
|
38
|
-
} else {
|
|
39
|
-
command.resolve(data.data)
|
|
40
|
-
}
|
|
33
|
+
logger.log(() => ["Client::Connections::WebSocket onSocketMessage", data])
|
|
34
|
+
this.onCommandCallback(data)
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
onSocketOpen = (
|
|
44
|
-
logger.log(
|
|
37
|
+
onSocketOpen = (_event) => {
|
|
38
|
+
logger.log("onSocketOpen")
|
|
45
39
|
}
|
|
46
40
|
|
|
47
41
|
send(data) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
})
|
|
42
|
+
const sendData = JSON.stringify(data)
|
|
43
|
+
logger.log(() => ["Sending", sendData])
|
|
44
|
+
this.ws.send(sendData)
|
|
61
45
|
}
|
|
62
46
|
|
|
63
47
|
waitForOpened = () => new Promise((resolve, reject) => {
|
package/src/client/index.js
CHANGED
|
@@ -6,19 +6,44 @@ const logger = new Logger("Scoundrel Client")
|
|
|
6
6
|
// logger.setDebug(true)
|
|
7
7
|
|
|
8
8
|
export default class Client {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new Scoundrel Client
|
|
11
|
+
*
|
|
12
|
+
* @param {any} backend The backend connection (e.g., WebSocket)
|
|
13
|
+
*/
|
|
9
14
|
constructor(backend) {
|
|
10
15
|
this.backend = backend
|
|
16
|
+
this.backend.onCommand(this.onCommand)
|
|
17
|
+
|
|
18
|
+
this.outgoingCommands = {}
|
|
19
|
+
this.incomingCommands = {}
|
|
20
|
+
this.outgoingCommandsCount = 0
|
|
21
|
+
|
|
22
|
+
this._classes = {}
|
|
23
|
+
this._objects = {}
|
|
11
24
|
this.references = {}
|
|
25
|
+
this.objects = {}
|
|
26
|
+
this.objectsCount = 0
|
|
12
27
|
}
|
|
13
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Closes the client connection
|
|
31
|
+
*/
|
|
14
32
|
async close() {
|
|
15
33
|
this.backend.close()
|
|
16
34
|
}
|
|
17
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Calls a method on a reference and returns the result directly
|
|
38
|
+
*
|
|
39
|
+
* @param {number} referenceId
|
|
40
|
+
* @param {string} methodName
|
|
41
|
+
* @param {...any} args
|
|
42
|
+
* @returns {Promise<any>}
|
|
43
|
+
*/
|
|
18
44
|
async callMethodOnReference(referenceId, methodName, ...args) {
|
|
19
|
-
const result = await this.
|
|
45
|
+
const result = await this.sendCommand("call_method_on_reference", {
|
|
20
46
|
args: this.parseArg(args),
|
|
21
|
-
command: "call_method_on_reference",
|
|
22
47
|
method_name: methodName,
|
|
23
48
|
reference_id: referenceId,
|
|
24
49
|
with: "result"
|
|
@@ -27,10 +52,17 @@ export default class Client {
|
|
|
27
52
|
return result.response
|
|
28
53
|
}
|
|
29
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Calls a method on a reference and returns a new reference
|
|
57
|
+
*
|
|
58
|
+
* @param {number} referenceId
|
|
59
|
+
* @param {string} methodName
|
|
60
|
+
* @param {...any} args
|
|
61
|
+
* @returns {Promise<Reference>}
|
|
62
|
+
*/
|
|
30
63
|
async callMethodOnReferenceWithReference(referenceId, methodName, ...args) {
|
|
31
|
-
const result = await this.
|
|
64
|
+
const result = await this.sendCommand("call_method_on_reference", {
|
|
32
65
|
args: this.parseArg(args),
|
|
33
|
-
command: "call_method_on_reference",
|
|
34
66
|
method_name: methodName,
|
|
35
67
|
reference_id: referenceId,
|
|
36
68
|
with: "reference"
|
|
@@ -40,9 +72,14 @@ export default class Client {
|
|
|
40
72
|
return this.spawnReference(id)
|
|
41
73
|
}
|
|
42
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Evaluates a string and returns a new reference
|
|
77
|
+
*
|
|
78
|
+
* @param {string} evalString
|
|
79
|
+
* @returns {Promise<Reference>}
|
|
80
|
+
*/
|
|
43
81
|
async evalWithReference(evalString) {
|
|
44
|
-
const result = await this.
|
|
45
|
-
command: "eval",
|
|
82
|
+
const result = await this.sendCommand("eval", {
|
|
46
83
|
eval_string: evalString,
|
|
47
84
|
with_reference: true
|
|
48
85
|
})
|
|
@@ -51,22 +88,35 @@ export default class Client {
|
|
|
51
88
|
return this.spawnReference(id)
|
|
52
89
|
}
|
|
53
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Imports a module and returns a reference to it
|
|
93
|
+
*
|
|
94
|
+
* @param {string} importName
|
|
95
|
+
* @returns {Promise<Reference>}
|
|
96
|
+
*/
|
|
54
97
|
async import(importName) {
|
|
55
|
-
const result = await this.
|
|
56
|
-
command: "import",
|
|
98
|
+
const result = await this.sendCommand("import", {
|
|
57
99
|
import_name: importName
|
|
58
100
|
})
|
|
59
101
|
|
|
60
102
|
logger.log(() => ["import", {result}])
|
|
61
103
|
|
|
104
|
+
if (!result) throw new Error("No result given")
|
|
105
|
+
if (!result.object_id) throw new Error(`No object ID given in result: ${JSON.stringify(result)}`)
|
|
106
|
+
|
|
62
107
|
const id = result.object_id
|
|
63
108
|
|
|
64
109
|
return this.spawnReference(id)
|
|
65
110
|
}
|
|
66
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Gets a registered object by name
|
|
114
|
+
*
|
|
115
|
+
* @param {string} objectName
|
|
116
|
+
* @returns {Promise<Reference>}
|
|
117
|
+
*/
|
|
67
118
|
async getObject(objectName) {
|
|
68
|
-
const result = await this.
|
|
69
|
-
command: "get_object",
|
|
119
|
+
const result = await this.sendCommand("get_object", {
|
|
70
120
|
object_name: objectName
|
|
71
121
|
})
|
|
72
122
|
|
|
@@ -77,10 +127,15 @@ export default class Client {
|
|
|
77
127
|
return this.spawnReference(id)
|
|
78
128
|
}
|
|
79
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Spawns a new reference to an object
|
|
132
|
+
*
|
|
133
|
+
* @param {string} id
|
|
134
|
+
* @returns {Promise<Reference>}
|
|
135
|
+
*/
|
|
80
136
|
async newObjectWithReference(className, ...args) {
|
|
81
|
-
const result = await this.
|
|
137
|
+
const result = await this.sendCommand("new_object_with_reference", {
|
|
82
138
|
args: this.parseArg(args),
|
|
83
|
-
command: "new_object_with_reference",
|
|
84
139
|
class_name: className
|
|
85
140
|
})
|
|
86
141
|
|
|
@@ -88,6 +143,8 @@ export default class Client {
|
|
|
88
143
|
|
|
89
144
|
const id = result.object_id
|
|
90
145
|
|
|
146
|
+
if (!id) throw new Error(`No object ID given in result: ${JSON.stringify(result)}`)
|
|
147
|
+
|
|
91
148
|
return this.spawnReference(id)
|
|
92
149
|
}
|
|
93
150
|
|
|
@@ -99,6 +156,121 @@ export default class Client {
|
|
|
99
156
|
return false
|
|
100
157
|
}
|
|
101
158
|
|
|
159
|
+
onCommand = ({command, command_id: commandID, data, error, errorStack, ...restArgs}) => {
|
|
160
|
+
logger.log(() => ["onCommand", {command, commandID, data, error, errorStack, restArgs}])
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
if (!command) {
|
|
164
|
+
throw new Error(`No command key given in data: ${Object.keys(restArgs).join(", ")}`)
|
|
165
|
+
} else if (command == "get_object") {
|
|
166
|
+
const serverObject = this._getRegisteredObject(data.object_name)
|
|
167
|
+
let object
|
|
168
|
+
|
|
169
|
+
if (serverObject) {
|
|
170
|
+
object = serverObject
|
|
171
|
+
} else {
|
|
172
|
+
object = global[data.object_name]
|
|
173
|
+
|
|
174
|
+
if (!object) throw new Error(`No such object: ${data.object_name}`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const objectId = ++this.objectsCount
|
|
178
|
+
|
|
179
|
+
this.objects[objectId] = object
|
|
180
|
+
this.respondToCommand(commandID, {object_id: objectId})
|
|
181
|
+
} else if (command == "new_object_with_reference") {
|
|
182
|
+
const className = data.class_name
|
|
183
|
+
let object
|
|
184
|
+
|
|
185
|
+
if (typeof className == "string") {
|
|
186
|
+
const ClassInstance = this.getClass(className) || global[className]
|
|
187
|
+
|
|
188
|
+
if (!ClassInstance) throw new Error(`No such class: ${className}`)
|
|
189
|
+
|
|
190
|
+
object = new ClassInstance(...data.args)
|
|
191
|
+
} else {
|
|
192
|
+
throw new Error(`Don't know how to handle class name: ${typeof className}`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const objectId = ++this.objectsCount
|
|
196
|
+
|
|
197
|
+
this.objects[objectId] = object
|
|
198
|
+
this.respondToCommand(commandID, {object_id: objectId})
|
|
199
|
+
} else if (command == "call_method_on_reference") {
|
|
200
|
+
const referenceId = data.reference_id
|
|
201
|
+
const object = this.objects[referenceId]
|
|
202
|
+
|
|
203
|
+
if (!object) throw new Error(`No object by that ID: ${referenceId}`)
|
|
204
|
+
|
|
205
|
+
const method = object[data.method_name]
|
|
206
|
+
|
|
207
|
+
if (!method) throw new Error(`No method called '${data.method_name}' on a '${object.constructor.name}'`)
|
|
208
|
+
|
|
209
|
+
const response = method.call(object, ...data.args)
|
|
210
|
+
|
|
211
|
+
this.respondToCommand(commandID, {response})
|
|
212
|
+
} else if (command == "serialize_reference") {
|
|
213
|
+
const referenceId = data.reference_id
|
|
214
|
+
const object = this.objects[referenceId]
|
|
215
|
+
|
|
216
|
+
if (!object) throw new Error(`No object by that ID: ${referenceId}`)
|
|
217
|
+
|
|
218
|
+
this.respondToCommand(commandID, JSON.stringify(object))
|
|
219
|
+
} else if (command == "read_attribute") {
|
|
220
|
+
const attributeName = data.attribute_name
|
|
221
|
+
const referenceId = data.reference_id
|
|
222
|
+
const returnWith = data.with
|
|
223
|
+
const object = this.objects[referenceId]
|
|
224
|
+
|
|
225
|
+
if (!object) throw new Error(`No object by that ID: ${referenceId}`)
|
|
226
|
+
|
|
227
|
+
const attribute = object[attributeName]
|
|
228
|
+
|
|
229
|
+
if (returnWith == "reference") {
|
|
230
|
+
const objectId = ++this.objectsCount
|
|
231
|
+
|
|
232
|
+
this.objects[objectId] = attribute
|
|
233
|
+
this.respondToCommand(commandID, {response: objectId})
|
|
234
|
+
} else {
|
|
235
|
+
this.respondToCommand(commandID, {response: attribute})
|
|
236
|
+
}
|
|
237
|
+
} else if (command == "command_response") {
|
|
238
|
+
if (!(commandID in this.outgoingCommands)) {
|
|
239
|
+
throw new Error(`Outgoing command ${commandID} not found: ${Object.keys(this.outgoingCommands).join(", ")}`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const savedCommand = this.outgoingCommands[commandID]
|
|
243
|
+
|
|
244
|
+
delete this.outgoingCommands[commandID]
|
|
245
|
+
|
|
246
|
+
if (error) {
|
|
247
|
+
const errorToThrow = new Error(error)
|
|
248
|
+
|
|
249
|
+
if (errorStack) {
|
|
250
|
+
errorToThrow.stack = `${errorStack}\n\n${errorToThrow.stack}`
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
savedCommand.reject(errorToThrow)
|
|
254
|
+
} else {
|
|
255
|
+
logger.log(() => [`Resolving command ${commandID} with data`, data])
|
|
256
|
+
savedCommand.resolve(data.data)
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
throw new Error(`Unknown command: ${command}`)
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
this.send({command: "command_response", command_id: commandID, error: error.message, errorStack: error.stack})
|
|
263
|
+
|
|
264
|
+
logger.error(error)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Parases an argument for sending to the server
|
|
270
|
+
*
|
|
271
|
+
* @param {any} arg
|
|
272
|
+
* @returns {any}
|
|
273
|
+
*/
|
|
102
274
|
parseArg(arg) {
|
|
103
275
|
if (Array.isArray(arg)) {
|
|
104
276
|
return arg.map((argInArray) => this.parseArg(argInArray))
|
|
@@ -122,9 +294,15 @@ export default class Client {
|
|
|
122
294
|
return arg
|
|
123
295
|
}
|
|
124
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Reads an attribute on a reference and returns a new reference
|
|
299
|
+
*
|
|
300
|
+
* @param {number} referenceId
|
|
301
|
+
* @param {string} attributeName
|
|
302
|
+
* @returns {Promise<Reference>}
|
|
303
|
+
*/
|
|
125
304
|
async readAttributeOnReferenceWithReference(referenceId, attributeName) {
|
|
126
|
-
const result = await this.
|
|
127
|
-
command: "read_attribute",
|
|
305
|
+
const result = await this.sendCommand("read_attribute", {
|
|
128
306
|
attribute_name: attributeName,
|
|
129
307
|
reference_id: referenceId,
|
|
130
308
|
with: "reference"
|
|
@@ -134,9 +312,15 @@ export default class Client {
|
|
|
134
312
|
return this.spawnReference(id)
|
|
135
313
|
}
|
|
136
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Reads an attribute on a reference and returns the result directly
|
|
317
|
+
*
|
|
318
|
+
* @param {number} referenceId
|
|
319
|
+
* @param {string} attributeName
|
|
320
|
+
* @returns {Promise<any>}
|
|
321
|
+
*/
|
|
137
322
|
async readAttributeOnReference(referenceId, attributeName) {
|
|
138
|
-
const result = await this.
|
|
139
|
-
command: "read_attribute",
|
|
323
|
+
const result = await this.sendCommand("read_attribute", {
|
|
140
324
|
attribute_name: attributeName,
|
|
141
325
|
reference_id: referenceId,
|
|
142
326
|
with: "result"
|
|
@@ -144,12 +328,91 @@ export default class Client {
|
|
|
144
328
|
return result.response
|
|
145
329
|
}
|
|
146
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Registers a class by name
|
|
333
|
+
*
|
|
334
|
+
* @param {string} className
|
|
335
|
+
* @param {any} classInstance
|
|
336
|
+
*/
|
|
337
|
+
registerClass(className, classInstance) {
|
|
338
|
+
if (className in this._classes) throw new Error(`Class already exists: ${className}`)
|
|
339
|
+
|
|
340
|
+
this._classes[className] = classInstance
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Gets a registered class by name
|
|
345
|
+
*
|
|
346
|
+
* @param {string} className
|
|
347
|
+
* @returns {any}
|
|
348
|
+
*/
|
|
349
|
+
getClass(className) {
|
|
350
|
+
return this._classes[className]
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Registers an object by name
|
|
355
|
+
*
|
|
356
|
+
* @param {string} objectName
|
|
357
|
+
* @param {any} objectInstance
|
|
358
|
+
*/
|
|
359
|
+
registerObject(objectName, objectInstance) {
|
|
360
|
+
if (objectName in this._objects) throw new Error(`Object already exists: ${objectName}`)
|
|
361
|
+
|
|
362
|
+
this._objects[objectName] = objectInstance
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Gets a registered object by name
|
|
367
|
+
*
|
|
368
|
+
* @param {string} objectName
|
|
369
|
+
* @returns {any}
|
|
370
|
+
*/
|
|
371
|
+
_getRegisteredObject(objectName) {
|
|
372
|
+
return this._objects[objectName]
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
respondToCommand(commandId, data) {
|
|
376
|
+
this.sendCommand("command_response", {command_id: commandId, data})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
sendCommand(command, data) {
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
const outgoingCommandCount = ++this.outgoingCommandsCount
|
|
382
|
+
const commandData = {
|
|
383
|
+
command,
|
|
384
|
+
command_id: outgoingCommandCount,
|
|
385
|
+
data
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.outgoingCommands[outgoingCommandCount] = {resolve, reject}
|
|
389
|
+
logger.log(() => ["Sending", commandData])
|
|
390
|
+
this.send(commandData)
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
send(data) {
|
|
395
|
+
this.backend.send(data)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Serializes a reference and returns the result directly
|
|
400
|
+
*
|
|
401
|
+
* @param {number} referenceId
|
|
402
|
+
* @returns {Promise<any>}
|
|
403
|
+
*/
|
|
147
404
|
async serializeReference(referenceId) {
|
|
148
|
-
const json = await this.
|
|
405
|
+
const json = await this.sendCommand("serialize_reference", {reference_id: referenceId})
|
|
149
406
|
|
|
150
407
|
return JSON.parse(json)
|
|
151
408
|
}
|
|
152
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Spawns a new reference to an object
|
|
412
|
+
*
|
|
413
|
+
* @param {string} id
|
|
414
|
+
* @returns {Promise<Reference>}
|
|
415
|
+
*/
|
|
153
416
|
spawnReference(id) {
|
|
154
417
|
const reference = new Reference(this, id)
|
|
155
418
|
|
package/src/client/reference.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export default class Reference {
|
|
2
|
+
/**
|
|
3
|
+
* Creates a new Reference
|
|
4
|
+
*
|
|
5
|
+
* @param {any} client The client instance
|
|
6
|
+
* @param {string} id The reference ID
|
|
7
|
+
*/
|
|
2
8
|
constructor(client, id) {
|
|
3
9
|
this.client = client
|
|
4
10
|
this.id = id
|
|
@@ -6,22 +12,55 @@ export default class Reference {
|
|
|
6
12
|
if (!id) throw new Error(`Invalid ID given: ${id}`)
|
|
7
13
|
}
|
|
8
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Calls a method on the reference
|
|
17
|
+
*
|
|
18
|
+
* @param {string} methodName
|
|
19
|
+
* @param {...any} args
|
|
20
|
+
* @returns {Promise<any>}
|
|
21
|
+
*/
|
|
9
22
|
async callMethod(methodName, ...args) {
|
|
10
23
|
return await this.client.callMethodOnReference(this.id, methodName, ...args)
|
|
11
24
|
}
|
|
12
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Calls a method on the reference using another reference as argument
|
|
28
|
+
*
|
|
29
|
+
* @param {string} methodName
|
|
30
|
+
* @param {...any} args
|
|
31
|
+
* @returns {Promise<any>}
|
|
32
|
+
*/
|
|
13
33
|
async callMethodWithReference(methodName, ...args) {
|
|
14
34
|
return await this.client.callMethodOnReferenceWithReference(this.id, methodName, ...args)
|
|
15
35
|
}
|
|
16
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Reads an attribute from the reference
|
|
39
|
+
*
|
|
40
|
+
* @param {string} attributeName
|
|
41
|
+
* @param {...any} args
|
|
42
|
+
* @returns {Promise<any>}
|
|
43
|
+
*/
|
|
17
44
|
async readAttribute(attributeName, ...args) {
|
|
18
45
|
return await this.client.readAttributeOnReference(this.id, attributeName, ...args)
|
|
19
46
|
}
|
|
20
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Reads an attribute from the reference using another reference as argument
|
|
50
|
+
*
|
|
51
|
+
* @param {string} attributeName
|
|
52
|
+
* @param {...any} args
|
|
53
|
+
* @returns {Promise<any>}
|
|
54
|
+
*/
|
|
21
55
|
async readAttributeWithReference(attributeName, ...args) {
|
|
22
56
|
return await this.client.readAttributeOnReferenceWithReference(this.id, attributeName, ...args)
|
|
23
57
|
}
|
|
24
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Serializes the reference and returns the result directly
|
|
61
|
+
*
|
|
62
|
+
* @returns {Promise<any>}
|
|
63
|
+
*/
|
|
25
64
|
async serialize() {
|
|
26
65
|
return await this.client.serializeReference(this.id)
|
|
27
66
|
}
|
package/src/logger.js
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
export default class Logger {
|
|
2
|
+
/**
|
|
3
|
+
* Creates a new Logger instance
|
|
4
|
+
*
|
|
5
|
+
* @param {string} scopeName The name of the scope for the logger
|
|
6
|
+
*/
|
|
2
7
|
constructor(scopeName) {
|
|
3
8
|
this.debug = false
|
|
4
9
|
this.scopeName = scopeName
|
|
5
10
|
}
|
|
6
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Enables or disables debug logging
|
|
14
|
+
*
|
|
15
|
+
* @param {boolean} newValue
|
|
16
|
+
*/
|
|
7
17
|
setDebug(newValue) {
|
|
8
18
|
this.debug = newValue
|
|
9
19
|
}
|
|
10
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Logs an error message to the console if debug is enabled
|
|
23
|
+
*
|
|
24
|
+
* @param {...any} args
|
|
25
|
+
*/
|
|
11
26
|
error(...args) {
|
|
12
27
|
return this._sendToConsole("error", ...args)
|
|
13
28
|
}
|
|
14
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Logs a message to the console if debug is enabled
|
|
32
|
+
*
|
|
33
|
+
* @param {...any} args
|
|
34
|
+
*/
|
|
15
35
|
log(...args) {
|
|
16
36
|
return this._sendToConsole("log", ...args)
|
|
17
37
|
}
|
|
@@ -4,14 +4,17 @@ import {realpath} from "node:fs/promises"
|
|
|
4
4
|
|
|
5
5
|
const logger = new Logger("Scoundrel PythonWebSocketRunner")
|
|
6
6
|
|
|
7
|
+
// logger.setDebug(true)
|
|
8
|
+
|
|
7
9
|
export default class PythonWebSocketRunner {
|
|
8
10
|
constructor() {
|
|
9
11
|
process.on("exit", this.onProcessExit)
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
runAndWaitForPid() {
|
|
13
|
-
return new Promise((resolve) => {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
14
16
|
this.waitForPidResolve = resolve
|
|
17
|
+
this.waitForPidReject = reject
|
|
15
18
|
this.run()
|
|
16
19
|
})
|
|
17
20
|
}
|
|
@@ -20,6 +23,8 @@ export default class PythonWebSocketRunner {
|
|
|
20
23
|
const filePath = `${process.cwd()}/../python/server/web-socket.py`
|
|
21
24
|
const fileRealPath = await realpath(filePath)
|
|
22
25
|
const child = spawn("python3", [fileRealPath])
|
|
26
|
+
|
|
27
|
+
child.on("exit", this.onChildExit)
|
|
23
28
|
child.stdout.on("data", this.onChildStdout)
|
|
24
29
|
child.stderr.on("data", this.onChildStderr)
|
|
25
30
|
}
|
|
@@ -31,8 +36,28 @@ export default class PythonWebSocketRunner {
|
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
onChildExit = (code, signal) => {
|
|
40
|
+
logger.log(() => `Child process exited with code ${code} and signal ${signal}`)
|
|
41
|
+
|
|
42
|
+
if (this.waitForPidRejectError) {
|
|
43
|
+
this.waitForPidReject(this.waitForPidRejectError)
|
|
44
|
+
this.waitForPidResolve = null
|
|
45
|
+
this.waitForPidReject = null
|
|
46
|
+
this.waitForPidRejectError = null
|
|
47
|
+
} else if (this.waitForPidReject) {
|
|
48
|
+
this.waitForPidReject(new Error(`Python process exited before PID was received (code: ${code}, signal: ${signal})`))
|
|
49
|
+
this.waitForPidResolve = null
|
|
50
|
+
this.waitForPidReject = null
|
|
51
|
+
this.waitForPidRejectError = null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
onChildStderr = (data) => {
|
|
35
|
-
logger.
|
|
56
|
+
logger.error(() => `stderr: ${data}`)
|
|
57
|
+
|
|
58
|
+
if (this.waitForPidReject) {
|
|
59
|
+
this.waitForPidRejectError = new Error(`Python process stderr before PID was received: ${data}`)
|
|
60
|
+
}
|
|
36
61
|
}
|
|
37
62
|
|
|
38
63
|
onChildStdout = (data) => {
|
|
@@ -47,7 +72,9 @@ export default class PythonWebSocketRunner {
|
|
|
47
72
|
|
|
48
73
|
if (this.waitForPidResolve) {
|
|
49
74
|
this.waitForPidResolve()
|
|
50
|
-
this.waitForPidResolve =
|
|
75
|
+
this.waitForPidResolve = null
|
|
76
|
+
this.waitForPidReject = null
|
|
77
|
+
this.waitForPidRejectError = null
|
|
51
78
|
}
|
|
52
79
|
}
|
|
53
80
|
}
|
|
@@ -6,7 +6,7 @@ export default class WebSocket {
|
|
|
6
6
|
this.wss.on("connection", this.onConnection)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
close
|
|
9
|
+
close() { this.wss.close() }
|
|
10
10
|
|
|
11
11
|
onConnection = (ws) => {
|
|
12
12
|
if (!this.onNewClientCallback) throw new Error("'onNewClient' hasn't been called")
|
|
@@ -14,7 +14,7 @@ export default class WebSocket {
|
|
|
14
14
|
this.onNewClientCallback(new WebSocketClient(ws))
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
onNewClient
|
|
17
|
+
onNewClient(callback) {
|
|
18
18
|
if (!callback) throw new Error("No callback was given")
|
|
19
19
|
|
|
20
20
|
this.onNewClientCallback = callback
|
package/src/server/index.js
CHANGED
|
@@ -1,39 +1,21 @@
|
|
|
1
|
-
import Client from "
|
|
1
|
+
import Client from "../client/index.js"
|
|
2
|
+
import EventEmitter from "events"
|
|
2
3
|
|
|
3
4
|
export default class ScoundrelServer {
|
|
4
5
|
constructor(backend) {
|
|
5
6
|
this.backend = backend
|
|
6
7
|
this.backend.onNewClient(this.onNewClient)
|
|
7
8
|
this.clients = []
|
|
8
|
-
this.
|
|
9
|
-
this._objects = {}
|
|
9
|
+
this.events = new EventEmitter()
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
close() { this.backend.close() }
|
|
13
|
+
getClients() { return this.clients }
|
|
13
14
|
|
|
14
15
|
onNewClient = (clientBackend) => {
|
|
15
|
-
const client = new Client(clientBackend
|
|
16
|
+
const client = new Client(clientBackend)
|
|
16
17
|
|
|
17
18
|
this.clients.push(client)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
registerClass(className, classInstance) {
|
|
21
|
-
if (className in this._classes) throw new Error(`Class already exists: ${className}`)
|
|
22
|
-
|
|
23
|
-
this._classes[className] = classInstance
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
registerObject(objectName, objectInstance) {
|
|
27
|
-
if (objectName in this._objects) throw new Error(`Object already exists: ${objectName}`)
|
|
28
|
-
|
|
29
|
-
this._objects[objectName] = objectInstance
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
getClass(className) {
|
|
33
|
-
return this._classes[className]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
getObject(objectName) {
|
|
37
|
-
return this._objects[objectName]
|
|
19
|
+
this.events.emit("newClient", client)
|
|
38
20
|
}
|
|
39
21
|
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
export default class ServerClient {
|
|
2
|
-
constructor(clientBackend, server) {
|
|
3
|
-
this.clientBackend = clientBackend
|
|
4
|
-
this.clientBackend.onCommand(this.onCommand)
|
|
5
|
-
this.objects = {}
|
|
6
|
-
this.objectsCount = 0
|
|
7
|
-
this.server = server
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
onCommand = (commandId, data) => {
|
|
11
|
-
try {
|
|
12
|
-
if (data.command == "get_object") {
|
|
13
|
-
const serverObject = this.server.getObject(data.object_name)
|
|
14
|
-
let object
|
|
15
|
-
|
|
16
|
-
if (serverObject) {
|
|
17
|
-
object = serverObject
|
|
18
|
-
} else {
|
|
19
|
-
object = global[data.object_name]
|
|
20
|
-
|
|
21
|
-
if (!object) throw new Error(`No such object: ${data.object_name}`)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const objectId = ++this.objectsCount
|
|
25
|
-
|
|
26
|
-
this.objects[objectId] = object
|
|
27
|
-
this.respondToCommand(commandId, {object_id: objectId})
|
|
28
|
-
} else if (data.command == "new_object_with_reference") {
|
|
29
|
-
const className = data.class_name
|
|
30
|
-
let object
|
|
31
|
-
|
|
32
|
-
if (typeof className == "string") {
|
|
33
|
-
const ServerClassInstance = this.server.getClass(className)
|
|
34
|
-
|
|
35
|
-
if (ServerClassInstance) {
|
|
36
|
-
object = new ServerClassInstance(...data.args)
|
|
37
|
-
} else {
|
|
38
|
-
const classInstance = global[className]
|
|
39
|
-
|
|
40
|
-
if (!classInstance) throw new Error(`No such class: ${className}`)
|
|
41
|
-
|
|
42
|
-
object = new global[className](...data.args)
|
|
43
|
-
}
|
|
44
|
-
} else {
|
|
45
|
-
throw new Error(`Don't know how to handle class name: ${typeof className}`)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const objectId = ++this.objectsCount
|
|
49
|
-
|
|
50
|
-
this.objects[objectId] = object
|
|
51
|
-
this.respondToCommand(commandId, {object_id: objectId})
|
|
52
|
-
} else if (data.command == "call_method_on_reference") {
|
|
53
|
-
const referenceId = data.reference_id
|
|
54
|
-
const object = this.objects[referenceId]
|
|
55
|
-
|
|
56
|
-
if (!object) throw new Error(`No object by that ID: ${referenceId}`)
|
|
57
|
-
|
|
58
|
-
const method = object[data.method_name]
|
|
59
|
-
|
|
60
|
-
if (!method) throw new Error(`No method called '${data.method_name}' on a '${object.constructor.name}'`)
|
|
61
|
-
|
|
62
|
-
const response = method.call(object, ...data.args)
|
|
63
|
-
|
|
64
|
-
this.respondToCommand(commandId, {response})
|
|
65
|
-
} else if (data.command == "serialize_reference") {
|
|
66
|
-
const referenceId = data.reference_id
|
|
67
|
-
const object = this.objects[referenceId]
|
|
68
|
-
|
|
69
|
-
if (!object) throw new Error(`No object by that ID: ${referenceId}`)
|
|
70
|
-
|
|
71
|
-
this.respondToCommand(commandId, JSON.stringify(object))
|
|
72
|
-
} else if (data.command == "read_attribute") {
|
|
73
|
-
const attributeName = data.attribute_name
|
|
74
|
-
const referenceId = data.reference_id
|
|
75
|
-
const returnWith = data.with
|
|
76
|
-
const object = this.objects[referenceId]
|
|
77
|
-
|
|
78
|
-
if (!object) throw new Error(`No object by that ID: ${referenceId}`)
|
|
79
|
-
|
|
80
|
-
const attribute = object[attributeName]
|
|
81
|
-
|
|
82
|
-
if (returnWith == "reference") {
|
|
83
|
-
const objectId = ++this.objectsCount
|
|
84
|
-
|
|
85
|
-
this.objects[objectId] = attribute
|
|
86
|
-
this.respondToCommand(commandId, {response: objectId})
|
|
87
|
-
} else {
|
|
88
|
-
this.respondToCommand(commandId, {response: attribute})
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
this.clientBackend.send({type: "command_response", command_id: commandId, error: `Unknown command: ${data.command}`})
|
|
92
|
-
}
|
|
93
|
-
} catch (error) {
|
|
94
|
-
this.clientBackend.send({type: "command_response", command_id: commandId, error: `Unknown command: ${error.message}`})
|
|
95
|
-
|
|
96
|
-
console.error(error)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
respondToCommand(commandId, data) {
|
|
101
|
-
this.clientBackend.send({type: "command_response", command_id: commandId, data})
|
|
102
|
-
}
|
|
103
|
-
}
|