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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "scoundrel-remote-eval",
3
3
  "type": "module",
4
- "version": "1.0.5",
4
+ "version": "1.0.7",
5
5
  "description": "",
6
6
  "main": "src/index.js",
7
7
  "scripts": {
@@ -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
- delete this.commands[commandId]
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 = (event) => {
44
- logger.log(() =>"onSocketOpen")
37
+ onSocketOpen = (_event) => {
38
+ logger.log("onSocketOpen")
45
39
  }
46
40
 
47
41
  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
- })
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) => {
@@ -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.backend.send({
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.backend.send({
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.backend.send({
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.backend.send({
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.backend.send({
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.backend.send({
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.backend.send({
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.backend.send({
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.backend.send({command: "serialize_reference", reference_id: referenceId})
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
 
@@ -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.log(() => `stderr: ${data}`)
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 = undefined
75
+ this.waitForPidResolve = null
76
+ this.waitForPidReject = null
77
+ this.waitForPidRejectError = null
51
78
  }
52
79
  }
53
80
  }
@@ -19,7 +19,7 @@ export default class WebSocketClient {
19
19
 
20
20
  if (!this.onCommandCallback) throw new Error("Command callback hasn't been set")
21
21
 
22
- this.onCommandCallback(data.command_id, data.data)
22
+ this.onCommandCallback(data)
23
23
  }
24
24
 
25
25
  async send(data) {
@@ -6,7 +6,7 @@ export default class WebSocket {
6
6
  this.wss.on("connection", this.onConnection)
7
7
  }
8
8
 
9
- close = () => this.wss.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 = (callback) => {
17
+ onNewClient(callback) {
18
18
  if (!callback) throw new Error("No callback was given")
19
19
 
20
20
  this.onNewClientCallback = callback
@@ -1,39 +1,21 @@
1
- import Client from "./client/index.js"
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._classes = {}
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, this)
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
- }