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 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,13 @@
1
+ {
2
+ "spec_dir": "spec",
3
+ "spec_files": [
4
+ "**/*[sS]pec.?(m)js"
5
+ ],
6
+ "helpers": [
7
+ "helpers/**/*.?(m)js"
8
+ ],
9
+ "env": {
10
+ "stopSpecOnExpectationFailure": false,
11
+ "random": true
12
+ }
13
+ }
@@ -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
+ }