Mon 21 Jul 22:43:21 CEST 2025

This commit is contained in:
sbosse 2025-07-21 23:19:40 +02:00
parent d8af5db1a1
commit 91478e358a

839
js/ui/webui/vm.js Normal file
View File

@ -0,0 +1,839 @@
/*
KISS: Generic worker-based VM w/o SharedMemory for isolated JAM instances:
Low level API:
--------------
var vm = VM({
VM:JS.VM
})
var p1 = await vm.worker({
print:print,
error:error
}), p2 = await vm.worker({
print:print,
error:error
});
await p1.init()
await p1.ready()
await p1.run('sema1=Semaphore('+sema1.index+'); print("CP1.1"); ')
print('p1 ready')
await p2.init()
await p2.ready()
await p2.run('sema1=Semaphore('+sema1.index+'); print("CP2.1"); ')
print('p2 ready')
await p1.run('async function foo() { print(await sema1.level()); print("CP1.2");} foo()')
await p1.run('async function foo() { print(await sema1.down()); print("CP1.3"); } foo()')
// sema1.up();
await p2.run('async function foo() { print(await sema1.up()); print("CP2.3"); } foo()')
*/
var version = '1.1.1';
console.log('vm',version);
// Default JS multi.worker VM module
JS = {
run : function (command,env) {
with (env) {
return eval(command)
}
}
}
// A generic JS VM passed to VM workers
JS.VM = function VM (options) {
if (!(this instanceof VM)) return new VM(options);
this.env={};
if (options.print) this.env.print=options.print;
if (options.printError) this.env.printError=this.env.error=options.printError;
if (options.error) this.env.printError=this.env.error=options.error;
}
JS.VM.prototype.init = function () {
}
JS.VM.prototype.kill = function () {
}
JS.VM.prototype.run = function (code,print,printError) {
if (print) this.env.print=print;
if (printError) this.env.printError=this.env.error=printError;
try {
with (this.env) {
return eval(code)
}
} catch (e) { this.env.printError(e) }
}
JS.VM.prototype.status = function (code,print,printError) {
}
JS.VM.prototype.get = function (key) {
return this[key];
}
JS.VM.prototype.set = function (key,val) {
this[key]=val;
}
var VMs=[];
var VMindex=0;
// Unique VM Identifier UVI: web:vm#
// Environment,
// Init
// Rpc
// store:number,
// nmp,
// VM
// ports
function VM(options) {
if (!(this instanceof VM)) return new VM(options);
var self=this, index = VMindex++;
options=options||{};
this.options=options;
this.workerIndex=0;
this.workers=[];
this.handlers=[];
this.verbose=options.verbose||0;
this.index=index;
this.id='web:'+index;
if (options.Environment) this.Environment=options.Environment;
if (VM.Environment) this.Environment=VM.Environment;
if (options.Init) this.Init=options.Init;
if (VM.Init) this.Init=VM.Init;
if (options.Rpc) this.Rpc=options.Rpc;
if (VM.Rpc) this.Rpc=VM.Rpc;
// The real VM constructor used for creating the real VM inside worker
// Methods: init, run, kill, status
if (options.VM) this.VM=options.VM;
else this.virtual=true;
if (options.error) this.error=options.error;
if (options.print) this.print=options.print;
VMs.push(this);
}
VM.prototype.debug = function (f) {
return function () {
try {
f.apply(null,arguments);
} catch (e) { console.log(e) }
}
}
VM.prototype.init = function (wid,options) {
// Create and initialize the real VM inside worker
if (this.workers[wid]) return;
var worker = this.workers[wid];
}
VM.prototype.load = function (wid,code) {
if (this.workers[wid]) return;
var worker = this.workers[wid];
}
VM.prototype.kill = function (wid) {
if (this.workers[wid]) return;
var worker = this.workers[wid];
}
VM.prototype.reset = function (options) {
for (var wird in this.workers) {
var worker = this.workers[wid];
if (worker) worker.kill();
}
}
VM.prototype.run = function (wid,code) {
if (this.workers[wid]) return;
var worker = this.workers[wid];
return worker.run(code);
}
VM.prototype.get = function (wid,key) {
if (this.workers[wid]) return;
var worker = this.workers[wid];
}
VM.prototype.set = function (wid,key,val) {
if (this.workers[wid]) return;
var worker = this.workers[wid];
}
// Send a network message to worker
VM.prototype.message = function (wid,message) {
if (this.workers[wid]) return;
var worker = this.workers[wid];
}
VM.prototype.on = function(ev, handler) {
if (this.handlers[ev]) {
var prev = this.handlers[ev];
this.handlers[ev]=function (arg) {
handler(arg);
prev(arg);
}
} else this.handlers[ev]=handler;
}
VM.prototype.emit = function (ev,arg) {
if (this.handlers[ev]) this.handlers[ev](arg);
}
// Start a VM inside a Web Worker
/*
typoef @options = {
verbose,
print,
printError,
log,
Rpc,
Environment,
Init,
}
*/
VM.prototype.worker = function (options) {
if (this.virtual) throw "ENOTSUPPORTED";
var print = console.log,
log = console.log,
error = console.error,
self = this,
verbose = options.verbose==undefined?this.verbose:options.verbose,
id = this.workerIndex++,
nid = this.id+':'+id;
if (verbose>1) console.log('CP: VM.worker');
if (this.print) print=this.print;
if (this.error) error=this.error;
if (options.print) print=options.print;
if (options.error) error=options.error;
else if (options.printError) error=options.printError;
else if (options.print) error=options.print;
if (options.log) log=options.log;
var environment = {
// Http : Http,
// Code : Code,
BufferInit : BufferInit,
/* Real VM, not meta VM!! */
VM : this.VM,
VMproto : this.VM && this.VM.prototype,
Channel : {
channelID:0,
channels:[],
create : function () {
var id = Channel.channelID++;
var chan = {
waiters : [],
handlers : {},
queue : [],
idn : id,
id : 'channel'+id,
cancel : function () {
this.waiters.forEach(function (waiter) {
waiter(null);
})
this.waiters=[];
this.queue=[];
},
destroy : function () {
chan.cancel();
delete Channels[id];
},
// enqueue received data
enqueue: function (data) {
if (this.queue.length==0 && this.waiters.length) {
var wakeup = this.waiters.shift();
wakeup(data);
} else if (this.handlers.receive) {
this.handlers.receive(data);
} else this.queue.push(data);
},
on : function (ev,handler) { this.handlers[ev]=handler },
off : function (ev) { delete this.handlers[ev] },
// client API
send: function (data) {
if (this.forward) {
this.forward(data);
} else if (this.queue.length==0 && this.waiters.length) {
var wakeup = this.waiters.shift();
wakeup(data);
} else this.queue.push(data);
},
receive : async function (cb) {
var self=this;
if (cb) {
if (this.queue.length) return cb(this.queue.shift());
else return this.waiters.push(cb);
}
if (this.queue.length) return this.queue.shift();
// needs await prefix before receive in async function (Code.run)
return new Promise(function (resolve,reject) {
self.waiters.push(function (_data) {
if (_data===undefined) reject(); // interrupted
else resolve(_data);
});
})
},
receiver : function (cb) {
this.handlers.receive=cb;
}
}
Channel.channels[id]=chan;
return chan;
}
},
InspectInit : InspectInit,
Utils : Utils,
on : function (ev,handler) { __handlers[ev]=handler },
once : function (ev,handler) { __handlers1[ev]=handler },
__handlers :[],
__handlers1 :[],
__toinit : {
Buffer : function () {
BufferInit();
},
// Http : function () { Http(self) },
Inspect : function () { inspect = InspectInit() },
VM : function () { if (options.verbose) print('VM setup'); if (VM) VM.prototype=VMproto; },
},
__init : function () {
try {
if (typeof Polyfill != 'undefined') Polyfill()
_=undefined;
for (var p in __toinit) {
__toinit[p](self)
}
} catch (e) { console.log('__init failed',e); }
},
options : {
verbose:verbose
}
}
if (options.Environment) {
for(var p in options.Environment) environment[p]=options.Environment[p];
}
if (options.Init) {
for(var p in options.Init) environment.__toinit[p]=options.Init[p];
}
if (this.Environment) {
for(var p in this.Environment) environment[p]=this.Environment[p];
}
if (this.Init) {
for(var p in this.Init) environment.__toinit[p]=this.Init[p];
}
function workerFunction(id,verbose) {
(function() {
var oldLog = console.log;
console.log = function() {
oldLog.call(console, Array.prototype.slice.call(arguments).join(" , "));
}
})();
var Env = {
emit : function (ev,arg) {
self.postMessage({command:'emit',event:ev,argument:toString(arg)})
},
error: function (a) {
self.postMessage({command:'error',arguments:toString(Array.prototype.slice.call(arguments))})
},
fork : function (_options) {
/* fork a new worker thread, wait for initialisation */
return new Promise(function (resolve,rejeect) {
once('fork',resolve);
self.postMessage({command:'fork',options:toString(Array.prototype.slice.call(_options))})
})
},
log : function () {
self.postMessage({command:'log',arguments:toString(Array.prototype.slice.call(arguments))})
},
print: function () {
self.postMessage({command:'print',arguments:toString(Array.prototype.slice.call(arguments))})
},
time : function () { return Date.now() },
_rpcID : 0,
_rpcCallbacks:[],
rpc: function (op,request,callback,event) {
var id = Env._rpcID++;
// console.log('Env.rpc',op,request,id,event);
Env._rpcCallbacks[id]=callback;
// if event == undefined then the reply is returned immediately (once)
// if event != undefined then the reply is returned if the event occurs (one ore more times)
// if event === true then reply is return delayed asynchronously (one time event)
if (typeof event == 'string') self.postMessage({command:'rpc',call:op,request:toString(request),id:id,event:event});
else if (event === true) self.postMessage({command:'rpc',call:op,request:toString(request),id:id,asyn:true});
else self.postMessage({command:'rpc',call:op,request:toString(request),id:id});
},
toString:toString,
workerId:id,
}
function isArray(o) {
if (o==undefined || o ==null) return false;
else return typeof o == "array" || (typeof o == "object" && o.constructor === Array);
}
function isObject(o) {
return typeof o == "object";
}
function isTypedArray(o) {
return isObject(o) && o.buffer instanceof ArrayBuffer
}
function TypedArrayToName(ftyp) {
if (ftyp==Int8Array || ftyp instanceof Int8Array) return 'Int8Array';
if (ftyp==Uint8Array || ftyp instanceof Uint8Array) return 'Uint8Array';
if (ftyp==Int16Array || ftyp instanceof Int16Array) return 'Int16Array';
if (ftyp==Uint16Array || ftyp instanceof Uint16Array) return 'Uint16Array';
if (ftyp==Int32Array || ftyp instanceof Int32Array) return 'Int32Array';
if (ftyp==Uint32Array || ftyp instanceof Uint32Array) return 'Uint32Array';
if (ftyp==Float32Array || ftyp instanceof Float32Array) return 'Float32Array';
if (ftyp==Float64Array || ftyp instanceof Float64Array) return 'Float64Array';
return ftyp.toString()
}
function ofString(source,mask) {
var code;
mask=mask||{}
try {
// execute script in private context
with (mask) {
eval('"use strict"; code = '+source);
}
} catch (e) { console.log(e,source) };
return code;
}
function toString(o) {
var usebuffer=false;
var p,i,keys,s='',sep,tokens;
if (o===null) return 'null';
else if (isArray(o)) {
s='[';sep='';
for(p in o) {
s=s+sep+toString(o[p]);
sep=',';
}
s+=']';
} else if (typeof Buffer != 'undefined' && o instanceof Buffer) {
s='Buffer([';sep='';
for(i=0;i<o.length;i++) {
s=s+sep+toString(o[i]);
sep=',';
}
s+='])';
} else if (o instanceof Error) {
s='(new Error("'+o.toString()+'"))';
} else if (o instanceof SyntaxError) {
s='(new SyntaxError("'+o.toString()+'"))';
} else if (isTypedArray(o)) {
s='(new '+TypedArrayToName(o)+'([';sep='';
var b=Array.prototype.slice.call(o);
for(i=0;i<b.length;i++) {
s=s+sep+String(b[i]);
sep=',';
}
s+=']))';
} else if (typeof o == 'object') {
s='{';sep='';keys=Object.keys(o);
for(i in keys) {
p=keys[i];
if (o[p]==undefined) continue;
s=s+sep+"'"+p+"'"+':'+toString(o[p]);
sep=',';
}
s+='}';
if (o.__constructor__) s = '(function () { var o='+s+'; o.__proto__='+o.__constructor__+'.prototype; return o})()';
} else if (typeof o == 'string') {
s=JSON.stringify(o)
} else if (typeof o == 'function') {
s=o.toString(true); // try minification (true) if supported by platform
if (tokens=s.match(/function[ ]+([a-zA-Z0-9]+)[ ]*\(\)[ ]*{[^\[]*\[native code\][^}]*}/)) {
return tokens[1];
} else return s;
} else if (o != undefined)
s=o.toString();
else s='undefined';
return s;
}
function compile(code,context,data) {
// contruct functional scope
var pars = Object.keys(context),
args = pars.map(function (key) { return context[key] });
pars.unshift('__dummy');
if (data!==undefined) { pars.push('__data'); args.push(data) };
pars.push(code);
var foo = new (Function.prototype.bind.apply(Function,pars));
return foo.apply(self,args);
}
function evalf (code,data,assign,_id) {
// execute function with arguments (data,ports), optional assignment of result, and send result to parent process via evals message
// support shareable data
var result,isasync=/^[ ]*async[ ]+function/.test(code);
if (data==undefined) data=null;
result = compile ('try { var __result=true; '+
(assign?
(isasync? '(async function () { '+
assign+' = await '
: assign+' = ')
: (isasync? '(async function () { '
: '__result=')
)+
'('+
code+
')(__data)'+
(isasync && !assign?'} catch (e) {error(e.toString(),e.stack)};if (__result===undefined) __result=null;self.postMessage({command:"evals",status:toString(__result),id:'+_id+'});'
:'')+
(isasync? '})()'
: '')+
'} catch (e) { console.log(e); error(e.toString(),e.stack) }; return __result',Env,data);
return result;
}
if (verbose) Env.print('VM Worker #'+id+' ready.');
Env.emit('ready');
// Proecss messages from master
self.onmessage = function(e) {
var o,data,key,reply;
try {
var message = e.data;
switch (message.command) {
case 'create':
data = ofString(message.data,Env);
data.print=Env.print;
data.printError=Env.error;
data.rpc=Env.rpc;
with (Env) { self.vm = new VM(data); if(typeof vm == 'object') if(verbose) print('VM (worker #'+id+') created.'); emit('create') }
break;
case 'evalf':
var result = evalf(message.code,message.data, message.assign,message.id);
if (!/^[ ]*async[ ]+function/.test(message.code)) {
if (result===undefined) result=null;
self.postMessage({command:'evals',status:toString(result),id:message.id});
}
break;
case 'environment':
data = ofString(message.data,Env);
for(key in data) {
Env[key]=data[key];
}
if (typeof Env.__init == 'function') {
with (Env) { eval ('__init()') }
}
if (verbose) Env.print('VM Worker #'+id+': got environment');
break;
case 'event':
data = ofString(message.data);
if (Env.__handlers[message.event]) Env.__handlers[message.event](data);
if (Env.__handlers1[message.event]) { Env.__handlers1[message.event](data); delete Env.__handlers1[message.event] };
break;
case 'init':
with (Env) { if (self.vm) self.vm.init(); emit('init') }
break;
case 'run':
with (Env) { try { var result = self.vm.run(message.code); emit('run',result) } catch (e) { console.log(e); error(e) } }
break;
case 'rpc':
// reply for an rcp request
data = ofString(message.reply);
if (Env._rpcCallbacks[message.id]) {
var handler = Env._rpcCallbacks[message.id];
if (!message.event && !message.more) delete Env._rpcCallbacks[message.id];
handler(data)
}
break;
case 'share':
if (message.eval) {
try {
evalf(message.eval,message.data,message.key,message.id);;
} catch (e) { console.log(e) }
} else {
}
}
} catch (e) {
Env.error(e.toString(),e.stack.toString());
}
}
}
var handlers=[], handlers1=[];
function emit(ev,arg) {
if (handlers1[ev]) {
handlers1[ev](arg);
delete handlers[ev];
} else if (handlers[ev]) handlers[ev](arg);
else self.emit(ev,arg);
}
// worker private event handlers
function on(ev,handler) {
if (ev=='stdout') print=function () { emit('stdout',arguments) };
if (ev=='stderr') error=function () { emit('stderr',arguments) };
if (handlers[ev]) {
var prev = handlers[ev];
handlers[ev]=function (arg) {
handler(arg);
prev(arg);
}
} else handlers[ev]=handler;
}
function once(ev,handler) {
handlers1[ev]=handler;
}
var _ready=false;
handlers1.ready= function () { _ready=true }
var dataObj = '(' + workerFunction.toString() + ')('+id+','+verbose+');'; // here is the trick to convert the above fucntion to string
var blob;
if (verbose>1) console.log('CP: VM.worker -> new Blob');
try {
blob = new Blob([dataObj.replace('"use strict";', '')], {type: 'application/javascript'});
} catch (e) {
// Backwards-compatibility
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
blob = new BlobBuilder();
blob.append(dataObj.replace('"use strict";', ''));
blob = blob.getBlob();
}
var blobURL = (window.URL ? URL : webkitURL).createObjectURL(blob, {
type: 'application/javascript; charset=utf-8'
});
// if (verbose>1)
console.log('CP: VM.worker -> new Worker '+id);
var worker = new Worker(blobURL); // spawn new worker
if (verbose) print('VM Worker #'+id+' started.');
// Process messages from worker
worker.onmessage = function(e) {
var message = e.data;
switch (message.command) {
case 'error':
// console.log(Utils.ofString(message.arguments))
error.apply(null,Utils.ofString(message.arguments));
break;
case 'emit':
emit(message.event,Utils.ofString(message.argument));
break;
case 'evals':
/* reply for a rpc */
emit('evals'+message.id,Utils.ofString(message.status));
break;
case 'fork':
handler.fork(Utils.ofString(message.options), function (_handler) {
worker.postMessage({command:'event',event:'fork.ready',data:{id:_handler.id}});
});
break;
case 'log':
log.apply(null,Utils.ofString(message.arguments));
break;
case 'print':
print.apply(null,Utils.ofString(message.arguments));
break;
case 'rpc':
if (!self.Rpc) worker.postMessage({command:'rpc',reply:Utils.toString({error:"ENORPC"})});
var request = Utils.ofString(message.request);
// console.log('RPC',message)
if (!self.Rpc[message.call]) worker.postMessage({command:'rpc',reply:Utils.toString({error:"ENOTEXIST"}),id:message.id});
try {
if (message.asyn) {
self.Rpc[message.call](request, function (reply) {
worker.postMessage({command:'rpc',reply:Utils.toString(reply),id:message.id})
});
} else if (!message.event) {
// synchronous request with immediate reply
var reply = self.Rpc[message.call](request);
worker.postMessage({command:'rpc',reply:Utils.toString(reply),id:message.id})
} else {
// install asynchronous background event emitter
self.Rpc[message.call](request,message.event,function (reply,more) {
worker.postMessage({command:'rpc',reply:Utils.toString(reply),id:message.id,event:message.event,more:more})
});
}
} catch (e) {
worker.postMessage({command:'rpc',reply:Utils.toString({error:e.toString()}),id:message.id})
}
break;
}
}
if (verbose>1) console.log('CP: VM.worker -> post environment');
worker.postMessage({command:'environment',data:Utils.toString(environment)}); // Send data to our worker.
function create () {
return new Promise(function (resolve) {
once('create',resolve);
delete options.error;
delete options.print;
delete options.printError;
worker.postMessage({command:'create',data:Utils.toString(options)}); // Send data to our worker.
if (options.share) {
// any other sharable objects (IPC, data, ..)
for(var p in options.share) {
share(p,options.share[p]);
}
}
});
}
function evalf (f,data) {
return new Promise(function (resolve) {
var _id=handler.tid++;
once('evals'+_id,resolve);
if (options.verbose>1) console.log('CP: VM.evalf '+f.toString());
worker.postMessage({command:'evalf',code:f.toString(),data:data,id:_id}); // Send data to our worker.
})
}
function event (ev,data) {
worker.postMessage({command:'event',event:ev,data:data});
}
// Fork a new worker from this worker configuration
function fork (_options,callback) {
self.worker(Object.assign(Object.assign({},_options),options)).then(function (handler) {
handler.init().then(function() {
if (callback) callback(handler);
})
})
}
function init () {
return new Promise(function (resolve) {
once('init',function () {
resolve();
});
worker.postMessage({command:'init'}); // Send data to our worker.
})
}
function kill (sig) {
console.log('vm.worker '+id+': Termination.');
if (verbose) print('vm.worker '+id+': Termination.');
worker.terminate();
emit('kill');
self.emit('kill'+id);
}
function ready () {
return new Promise(function (resolve) {
if (_ready) resolve();
once('ready',resolve);
})
}
function run (code,data,ev) {
return new Promise(function (resolve) {
once('run',resolve);
if (typeof code == 'function') code='('+code.toString()+')('+JSON.stringify(data)+')';
if (ev) {
if (/\([ ]*async/.test(code)) code += ('.then(function () { emit("'+ev+'",'+id+')}).catch(function (e) { error(e); emit("'+ev+'",'+id+') })');
else code += (';emit("'+ev+'",'+id+')');
}
if (verbose>1) console.log('CP: VM.run '+code);
worker.postMessage({command:'run',code:code}); // Send data to our worker.
})
}
function share (key,data,eval) {try {
if (!eval && typeof data == 'object' && typeof data.__share == 'function') {
var share = data.__share(); // remote: key=eval(data)
data=share.data;
eval=share.eval;
}
if (!eval && Utils.isArray(data) && typeof data[0] == 'object' && typeof data[0].__share == 'function') {
var n = data.length;
worker.postMessage({command:'run',code:key+'=[];'});
for(var i=0;i<n;i++) {
var share = data[i].__share(); // remote: key=eval(data)
worker.postMessage({command:'share',key:key+'['+i+']',data:share.data,eval:share.eval.toString()});
}
return;
}
if (!eval) eval=function(data) { return data };
worker.postMessage({command:'share',key:key,data:data,eval:eval?eval.toString():undefined});
return;
} catch (e) { console.log(e) }
}
function send (message) {
worker.postMessage(message); // Send data to our worker.
}
var handler = {
id : id,
nid : nid,
tid : 0, // incremental transaction id's
error : function (f) { error=f },
eval : evalf,
event : event,
fork : fork,
init : init,
kill : kill,
on : on,
once : once,
print : function (f) { print=f },
ready : ready,
run : run,
share : share,
send : send,
children : [],
worker : worker,
}
this.workers[id]=handler;
return new Promise(function (resolve) {
create().then(function () {
resolve(handler)
})
})
}
// Universial Channel IPC Object (superclass, only two end-points)
// Requires message wrapping for webworker/remote sharing
// blocking code operations using async/await
Channel = {
channelID:0,
channels:[],
create : function () {
var id = Channel.channelID++;
var chan = {
waiters : [],
handlers : {},
queue : [],
idn : id,
id : 'channel'+id,
cancel : function () {
this.waiters.forEach(function (waiter) {
waiter(null);
})
this.waiters=[];
this.queue=[];
},
destroy : function () {
chan.cancel();
delete Channels[id];
},
// enqueue received data
enqueue: function (data) {
if (this.queue.length==0 && this.waiters.length) {
var wakeup = this.waiters.shift();
wakeup(data);
} else if (this.handlers.receive) {
this.handlers.receive(data);
} else this.queue.push(data);
},
on : function (ev,handler) { this.handlers[ev]=handler },
off : function (ev) { delete this.handlers[ev] },
// client API
send: function (data) {
if (this.forward) {
this.forward(data);
} else if (this.queue.length==0 && this.waiters.length) {
var wakeup = this.waiters.shift();
wakeup(data);
} else this.queue.push(data);
},
receive : async function (cb) {
var self=this;
if (cb) {
if (this.queue.length) return cb(this.queue.shift());
else return this.waiters.push(cb);
}
if (this.queue.length) return this.queue.shift();
// needs await prefix before receive in async function (Code.run)
return new Promise(function (resolve,reject) {
self.waiters.push(function (_data) {
if (_data===undefined) reject(); // interrupted
else resolve(_data);
});
})
},
receiver : function (cb) {
this.handlers.receive=cb;
}
}
Channel.channels[id]=chan;
return chan;
}
}
VM.version=version;
if (typeof window == 'object') window.VM=VM;
if (typeof global == 'object') global.VM=VM;
if (typeof module != 'undefined') module.exports = VM