Initial commit

This commit is contained in:
thecookingsenpai 2023-12-25 13:29:59 +01:00
commit 7a7f027796
9 changed files with 2827 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
node_modules/.bin/
node_modules/
spin.zip

202
README.md Normal file
View File

@ -0,0 +1,202 @@
# Spin - The multithreading manager for Electron
## Pre-alpha version
### Introduction
Spin is a template that allows you to start a simple Electron application with included a boilerplate code that contains methods for managing multithreading processes in a simple way.
Some of the methods are related to code execution, others allows also to customize the page HTML in the children processes.
### Architecture
The project makes an extensive use of the worker_threads npm module, available in all the modern npm versions.
The majority of the code is in main.js, enclosed in a SECTION comment tag and commented. In the included worker example script, you can easily examine and apply Spin methods to your project.
### Examples
in main.js, after creating a window, you can add something like:
```javascript
start_subroutine("process", "./process.js", { })
```
Now the code in process.js will be executed and a listener will be added to
monitor messages from process.js .
In process.js you can write whatever you like but you have to include Spin code as provided in the example worker script.
You can use a code like this to communicate from process.js to the main process:
```javascript
let message_id = getHTML("id_in_html")
print("Sent message: " + message_id)
```
This way you are using the included code to get the html of an element with the specified HTML id field. The response is managed by the parse_message method that is triggered each time a message from main is received.
As you already know your message_id, you can use the check_response method to determine which is the response of the main process:
```javascript
let response = check_response(message_id, callback_method);
```
Take into account that callback_method can be any method ( with or without parameters ) that you want to define. The only requirement is accepting a first parameter with the message id as check_response will pass it to the method. Having the response stored into messages_awaiting, you can do something like this:
```javascript
function callback_method(message_id) {
print("[*] I am printing the result: " + messages_awaiting[message_id]);
}
```
As a living example, the send_message method uses check_response with a method that prints the result to the console, to ensure that the message has been properly received.
On another hand, you can write your own methods to send and receive messages
to and from the main process.
To do this, follow the instructions in the next paragraph.
### Adding your handlers
The main.js code contains a method called parseMessage, which is called when the message from the worker is received. You are free to edit or add a condition like:
```javascript
else if (type == "your_customized_type") {
// Do something
return true, "Your customized response"
}
```
As you can see, the only required part is the return value defined as a boolean plus the message (it can be false plus error message too, of course).
As a last step, you can use your customized handle in your worker code, for example doing the following:
```javascript
function myFunction() {
let payload = {
"type": 'your_customized_type',
"your_field": 'your_field_content',
"another_field": 'another_content'
}
let msg_id = send_message(payload)
return msg_id
}
```
Then you can use that function in the same way as a built-in Spin function:
```javascript
let msg_id = myFunction()
let status
let response
[status, response] = waitForResponse(msg_id, 1000)
```
### Managing the messages from the main process
As you can easily see, the worker process contains the start() method which is called when the script is started. You can play with this but remember to keep the included code to be executed as soon as possible:
```javascript
function start () {
// NOTE Handling messages from the main process
parentPort.on('message', (message) => {
// NOTE Handle responses
let j_message = JSON.parse(message.toString('utf8'))
parse_message(j_message)
});
```
The parse_message(message) function is called every time the main process
send a message to the worker. By default, the method parses the message and if type is "response", it looks for the message id field in the message. If the message id is found in the waiting queue dictionary:
```javascript
messages_awaiting[id]
```
And its response value is null:
```javascript
messages_awaiting[id].response==null
```
The method assign to the response field of the waiting queue dictionary the message contained in the message from main process:
```javascript
messages_awaiting[id].response = JSON.stringify(message)
```
The above code is already included and is reported just to illustrate the mechanism behind what happens.
From your code, you can then check in any way you want the response field of the message you sent to the main process as described above or do whatever you want playing with parse_message method as for example adding different types of messages.
In main.js, of course, you will need to insert a code corresponding to your custom handler, as:
```javascript
subroutine["routine_name"].postMessage(JSON.stringify({
"type": "your_type",
"your_field": "your_content"
}))
```
For example in a specific method, or on a specific condition.
### Proof of Concept
To better illustrate how this codebase works, let's make an example.
By using the following methods in your worker file (e.g. worker_template.js) inside the start() method:
```javascript
setHTML("editable", "<h2>2D Engine test</h2>")
setBackgroundStyle("backgroundColor", "red")
setBackgroundStyle("width", "90%")
setBackgroundStyle("height", "75vh")
setBackgroundStyle( "border", "1px solid navy")
setStyle("background", "margin", "auto")
```
You will obtain:
![alt text](https://raw.githubusercontent.com/thecookingsenpai/Spin/main/poc.png)
### Recommendations
- Adding custom handlers requires paying attention to existing ones and to code organization. It is very easy to implement too many custom handlers and to ending up making the code's complexity explode
- This codebase is intended as a simple example of implementing the Spin methods and is not intended to be used directly in production environments
- Please pay especially attention to security, for example sanitizing or filtering (or avoiding) unsupervised code execution through exec or through cross executed javascript
- Once again, before using this in production environments remove all the methods that you don't need and double check security
- This codebase is still in development
### Credits
Coded by TheCookingSenpai
### License
Distributed under the CreativeCommons CC-BY-SA 4.0 License
https://creativecommons.org/licenses/by-sa/4.0/
You are free to:
Share — copy and redistribute the material in any medium or format
Adapt — remix, transform, and build upon the material
for any purpose, even commercially.
This license is acceptable for Free Cultural Works.
The licensor cannot revoke these freedoms as long as you follow the license terms.
Under the following terms:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
- SharehareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
- No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
Notices:
You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation.
No warranties are given.
The license may not give you all of the permissions necessary for your intended use.
For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.

23
index.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<meta charset="UTF-8">
<!-- !! IMPORTANT !! -->
<!-- Content-Security-Policy no longer required. Will show warning in devtools. Can be ignored -->
<!-- <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'"> -->
<title>Spin Engine Demo</title>
</head>
<body>
<h1>Spin Engine Demo</h1>
<div id="editable">
</div>
<!-- JS -->
<script>
// All the scripts are originated from main, but you can add any other script here
</script>
</body>
</html>

192
main.js Normal file
View File

@ -0,0 +1,192 @@
// NOTE Loading modules (app and BrowserWindow) for main and rederer
const {app, BrowserWindow} = require('electron')
const { Worker } = require('worker_threads')
// SECTION Subroutine management
// NOTE Loading subprocesses and starting the listeners
subroutines = {}
function start_subroutine(name, path, data) {
if (name in subroutines) {
console.log("[x] Subroutine already exists")
alert("Error launching the subroutine: subroutine already exists")
return
}
subroutines[name] = new Worker(path, { workerData: data });
subroutines[name].on('message', (message) => {
// On message load it
let j_message = JSON.parse(message.toString('utf8'))
console.log("[*] Message from " + name)
console.log(j_message)
// Extract the message id
let msg_id = j_message.id
console.log("ID: " + msg_id)
var result;
var message;
// Send it to the parser
var packed = parseMessage(j_message, name)
result = packed[0]
message = packed[1]
console.log(result)
console.log(message)
// Manage errors
if (result == "error") {
console.log("[x] Error in " + name + ".js")
alert("Error in the subroutine " + name + " at " + path + ": " + message)
}
else {
console.log("[+] Message from " + name + " parsed")
}
// Send the response back solving promises if any
solved = null
if (message.toString().includes("Promise")) {
// In case of promise, solve it and send it
message.then((value) => {
console.log("Promise resolved: " + value)
let payload = '{ "type": "response", "id": "' + msg_id + '", "result": "' + result + '", "message": "' + value + '"}'
console.log("[*] Sending response: " + JSON.stringify(payload))
subroutines[name].postMessage(JSON.stringify(payload))
})
} else {
// In case of normal message, send it
let payload = '{ "type": "response", "id": "' + msg_id + '", "result": "' + result + '", "message": "' + message + '"}'
console.log("[*] Sending response: " + JSON.stringify(payload))
subroutines[name].postMessage(JSON.stringify(payload))
}
})
}
// NOTE Parsing the response
function parseMessage (j_response, sender) {
// Schema being like:
/*
{
"type": "render|info|error",
[...] (properties of the above)
}
*/
console.log("Parsing message from " + sender + ": " + JSON.stringify(j_response))
let type = j_response.type
let id = j_response.id
if (type == "error") {
console.log("[x] Error in " + sender + ".js")
console.log(j_response.message)
return ["error", j_response.message]
}
// NOTE Rendering an element (overwriting it if exists)
else if (type == "render") {
let element = j_response.element
console.log("[*] Rendering")
// Loading element to replace
console.log("Element: " + element)
// Normalizing and loading the HTML to insert
let newHTML = j_response.html.replace("'", '"')
console.log("HTML: " + newHTML)
// Executing the rendering command
let renderCmd = "document.getElementById('" + j_response.element + "').innerHTML += '" + newHTML + "'"
console.log(renderCmd)
mainWindow.webContents.executeJavaScript(renderCmd)
console.log("[+] Rendered")
return ["ok", "Rendered"]
}
// NOTE Changing style of an existing element
else if (type == "style") {
let element = j_response.element
let property = j_response.property
let value = j_response.value
console.log("[*] Styling")
// Loading element to replace
console.log("Element: " + element)
// Normalizing and loading the HTML to insert
let newProperty = j_response.property.replace("'", '"')
console.log("Property: " + newProperty)
// Executing the rendering command
let renderCmd = "document.getElementById('" + element + "').style." + property + "='" + value + "'"
console.log(renderCmd)
mainWindow.webContents.executeJavaScript(renderCmd)
console.log("[+] Styled")
return ["ok", "Styled"]
}
// NOTE Executing a js command
// REVIEW Remember to sanitize it
else if (j_response.type == "exec") {
let command = j_response.command
console.log("[*] Executing")
console.log(command)
mainWindow.webContents.executeJavaScript(command + ";")
console.log("[+] Executed")
return ["ok", "Executed"]
}
// NOTE Getting an existing element
else if (type == "get") {
let element = j_response.element
console.log("[*] Getting " + element + " for renderer")
let cmd = "document.getElementById('" + element + "');"
console.log(cmd)
let response = mainWindow.webContents.executeJavaScript("document.getElementById('" + element + "').innerHTML;")
console.log("[+] Got " + element + " for renderer")
console.log('[>] Returning ' + response)
return ["ok", response]
}
else if (type == "info") {
console.log("[i] Info from " + sender)
console.log(j_response.message)
return ["ok", "Info"]
}
}
// !SECTION Subroutine management
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
// Create a new BrowserWindow when `app` is ready
// NOTE This is just a definition which will be executed later
function createWindow () {
mainWindow = new BrowserWindow({
width: 1280, height: 720,
webPreferences: {
// --- !! IMPORTANT !! ---
// Disable 'contextIsolation' to allow 'nodeIntegration'
// 'contextIsolation' defaults to "true" as from Electron v12
contextIsolation: false,
nodeIntegration: true
}
})
// NOTE Load index.html into the new BrowserWindow
mainWindow.loadFile('index.html')
// Open DevTools - Remove for PRODUCTION!
//mainWindow.webContents.openDevTools();
// NOTE Listen for window being closed
mainWindow.on('closed', () => {
mainWindow = null
})
// NOTE Once the window is loaded, start the subroutines
// INFO: graphics subroutine is responsible for the loading of the graphics
start_subroutine("template", "./worker_template.js", { })
}
// NOTE Listener for app ready that will execute the createWindow function
app.on('ready', createWindow)
// NOTE Quit when all windows are closed - (Not macOS - Darwin)
app.on('window-all-closed', () => {
// also on mac
app.quit()
})
// NOTE When app icon is clicked and app is running, (macOS) recreate the BrowserWindow
app.on('activate', () => {
if (mainWindow === null) createWindow()
})

2169
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"appId": "com.electron.yourapp",
"name": "yourapp",
"version": "1.0.0",
"description": "Your description",
"main": "main.js",
"files": [],
"mac": {
"target": "dmg",
"icon": "path/to.icns",
"category": "public.app-category.category"
},
"win": {
"target": "nsis",
"icon": "path/to.ico",
"requestedExecutionLevel": "highestAvailable"
},
"linux": {
"target": "AppImage",
"icon": "path/to.png"
},
"scripts": {
"start": "electron .",
"watch": "nodemon --exec electron .",
"reset": "git reset --hard"
},
"keywords": [
"Electron",
"Spin",
"other keywords"
],
"author": "You",
"license": "License type",
"devDependencies": {
"electron": "latest",
"nodemon": "^2.0.0"
},
"dependencies": {
"fs": "^0.0.1-security",
"observable-slim": "^0.1.6"
}
}

BIN
poc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

0
style.css Normal file
View File

191
worker_template.js Normal file
View File

@ -0,0 +1,191 @@
const fs = require('fs');
const { parentPort } = require('worker_threads')
// NOTE Is possible to load data from the main process
// someVariable = workerData.someProperty
// SECTION Utilities
// NOTE Waiting list for the responses
let messages_awaiting = {}
let ready_responses = {}
// NOTE Allow generating a message id
String.prototype.hashCode = function() {
var hash = 0,
i, chr;
if (this.length === 0) return hash;
for (i = 0; i < this.length; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return Math.abs(hash).toString();
}
// NOTE Sending messages to the main process
function send_message(message, callback=null) {
// Adding the message id to the message
let _message = JSON.stringify(message)
let msg_id = _message.hashCode()
message["id"] = msg_id
let string_message = JSON.stringify(message)
// Creating a waiting structure for the response
messages_awaiting[msg_id] = {
"message": string_message,
"timestamp": Date.now(),
"response": null
}
// Sending as a string
parentPort.postMessage(string_message)
// Allowing the calling function to wait for the response
if(callback) {
check_response(msg_id, callback)
} else {
check_response(msg_id, react_to_response)
}
return msg_id
}
// NOTE Parsing the messages from the main process
function parse_message(message_str) {
let message = JSON.parse(JSON.parse(message_str))
console.log("Parsing: " + message_str)
let type = message["type"]
// Handle responses
if (type == "response") {
// Extracting the message id
let id = message.id
// Checking if the message is in the awaiting list
if (messages_awaiting[id]) {
// Updating the response if it is empty
if (messages_awaiting[id].response == null) {
messages_awaiting[id].response = JSON.stringify(message)
console.log("[*] Response received for message " + id + ": " + messages_awaiting[id].response)
// Setting the ready flag
ready_responses[id].ready = true
return
}
else {
console.log("[!] Response already received: " + id)
return
}
}
else {
console.log("[!] Message not found: " + id)
return
}
} else {
console.log("[!] Unknown message type: " + type)
console.log(message)
return
}
}
// NOTE Checking if the response is ready and calling the callback function
function check_response(message_id, callback) {
try {
// Recalled response
if(ready_responses[message_id].ready) {
callback(message_id)
return messages_awaiting[message_id].response
}
}
catch(err) {
// Setting a listener for the response readiness
ready_responses[message_id] = new Proxy( {ready: false}, {
set (target, prop, val) {
console.log("[*] Response ready for message " + message_id)
target[prop] = val;
callback(message_id)
return messages_awaiting[message_id].response
}
});
}
}
// !SECTION Utilities
// SECTION Methods to operate on the page background
// REVIEW Remove the methods you won't use, as those are just examples
function setBackgroundStyle(property, value) {
let payload = {
"type": "style",
"element": "background",
"property": property,
"value": value
}
let msg_id = send_message(payload)
return msg_id
}
// !SECTION Background methods
// SECTION General low level method
// REVIEW Remove the methods you won't use, as those are just examples
function setStyle(element, property, value) {
let cmd = property + ": " + value + ";"
let payload = {
"type": "style",
"element": element,
"property": property,
"value": value
}
let msg_id = send_message(payload)
return msg_id
}
function setHTML(element, html) {
let payload = {
"type": "render",
"element": element,
"html": html
}
let msg_id = send_message(payload)
return msg_id
}
function getHTML(element) {
let payload = {
"type": "get",
"element": element
}
let message_id = send_message(payload)
return message_id
}
// !SECTION General low level method
// ANCHOR Example of reacting to a response
function react_to_response(message_id) {
console.log("[reacted] Message id: " + message_id)
let response = messages_awaiting[message_id].response
console.log("[reacted] Response: " + response)
}
// ANCHOR Main process
// INFO Note that the main process is sync, so you can't use async/await here
function start () {
console.log("[*] Starting worker")
// NOTE Handling messages from the main process
parentPort.on('message', (message) => {
console.log('[+] Message from parent:', message);
parse_message(message)
});
// INFO You can write whatever you want here
// NOTE Examples
let msg_id = setHTML("editable", "<h2>Hello world!</h2>")
console.log("[-] Waiting for response " + msg_id)
}
// ANCHOR Entry pont
// REVIEW Remember to execute the included start() code if you change this
start()