Compare commits

...

2 Commits

Author SHA1 Message Date
Leon van Kammen e5a24bbb66 fixed worker issues
/ test (push) Successful in 5s Details
2024-10-18 11:50:56 +00:00
Leon van Kammen 2105120819 feat/codemirror: work in progress [might break]
/ test (push) Successful in 5s Details
2024-10-15 10:32:16 +00:00
12 changed files with 269 additions and 150 deletions

View File

@ -4,6 +4,8 @@ AFRAME.registerComponent('codemirror', {
schema: { schema: {
file: { type:"string"}, file: { type:"string"},
term: { type:"selector", default: "[isoterminal]" }, term: { type:"selector", default: "[isoterminal]" },
width: { type:"number", default:900},
height: { type:"number", default:700},
}, },
init: function () { init: function () {
@ -12,7 +14,7 @@ AFRAME.registerComponent('codemirror', {
if( this.data.file && this.data.file[0] != '/'){ if( this.data.file && this.data.file[0] != '/'){
this.data.file = "root/"+this.data.file this.data.file = "root/"+this.data.file
} }
this.isoterminal = this.data.term.components.isoterminal.isoterminal this.isoterminal = this.data.term.components.isoterminal.term
//this.el.innerHTML = ` ` //this.el.innerHTML = ` `
this.requireAll() this.requireAll()
}, },
@ -32,8 +34,9 @@ AFRAME.registerComponent('codemirror', {
html: (me) => `<div class="codemirror"> html: (me) => `<div class="codemirror">
</div>`, </div>`,
css: (me) => `.codemirror{ css: (me) => `.CodeMirror{
width:100%; width: ${me.com.data.width}px !important;
height: ${me.com.data.height}px !important;
} }
.codemirror *{ .codemirror *{
font-size: 14px; font-size: 14px;
@ -42,7 +45,8 @@ AFRAME.registerComponent('codemirror', {
letter-spacing: 0 !important; letter-spacing: 0 !important;
text-shadow: 0px 0px 10px #F075; text-shadow: 0px 0px 10px #F075;
} }
.wb-body + .codemirror{ overflow:hidden; } #${me.dom.id} .wb-body { overflow:hidden; }
.CodeMirror { .CodeMirror {
margin-top:18px; margin-top:18px;
} }
@ -53,7 +57,7 @@ AFRAME.registerComponent('codemirror', {
}, },
createEditor: function(value){ createEditor: function(value){
this.el.setAttribute("window", `title: codemirror; uid: ${this.el.dom.id}; attach: #overlay; dom: #${this.el.dom.id};`) this.el.setAttribute("window", `title: codemirror; uid: ${this.el.dom.id}; attach: #overlay; dom: #${this.el.dom.id}; width: ${this.data.width}px; height: ${this.data.height}px`)
this.editor = CodeMirror( this.el.dom, { this.editor = CodeMirror( this.el.dom, {
value, value,
mode: "htmlmixed", mode: "htmlmixed",
@ -67,10 +71,9 @@ AFRAME.registerComponent('codemirror', {
} }
}) })
this.editor.setOption("theme", "shadowfox") this.editor.setOption("theme", "shadowfox")
this.editor.updateFile = AFRAME.utils.throttleLeadingAndTrailing( (file,str) => { this.editor.updateFile = AFRAME.utils.throttle( (file,str) => {
this.updateFile(file,str), this.updateFile(file,str)
2000 }, 1500)
})
this.editor.on('change', (instance,changeObj) => { this.editor.on('change', (instance,changeObj) => {
this.editor.updateFile( this.data.file, instance.getValue() ) this.editor.updateFile( this.data.file, instance.getValue() )
}) })
@ -84,16 +87,19 @@ AFRAME.registerComponent('codemirror', {
// we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1) // we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1)
// as it would require all kindof ugly stringescaping // as it would require all kindof ugly stringescaping
console.log("updating "+file) console.log("updating "+file)
await this.isoterminal.emulator.fs9p.update_file( file, str) console.log(str)
await this.isoterminal.worker['emulator.update_file'](file, term.convert.toUint8Array(str) )
}, },
events:{ events:{
// component events // component events
DOMready: function(e){ DOMready: function(e){
this.isoterminal.worker.postMessage.promise({event:'read_file',data: this.data.file })
this.isoterminal.worker['emulator.read_file'](this.data.file)
.then( this.isoterminal.convert.Uint8ArrayToString ) .then( this.isoterminal.convert.Uint8ArrayToString )
.then( (str) => { .then( (str) => {
console.log("creating editor")
this.createEditor( str ) this.createEditor( str )
}) })
.catch( (e) => { .catch( (e) => {

View File

@ -12,7 +12,7 @@
* *
* plane textcanvas enter-VR * plane textcanvas enter-VR
* enter-AR * enter-AR
* * renderer=canvas
* *
* ISOTerminal.js * ISOTerminal.js
* *
@ -40,13 +40,15 @@ if( typeof AFRAME != 'undefined '){
padding: { type: 'number',"default": 18 }, padding: { type: 'number',"default": 18 },
minimized: { type: 'boolean',"default":false}, minimized: { type: 'boolean',"default":false},
maximized: { type: 'boolean',"default":false}, maximized: { type: 'boolean',"default":false},
muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
HUD: { type: 'boolean',"default":false}, // link to camera movement HUD: { type: 'boolean',"default":false}, // link to camera movement
transparent: { type:'boolean', "default":false }, // need good gpu transparent: { type:'boolean', "default":false }, // need good gpu
xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower) xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower)
memory: { type: 'number', "default":64 }, // VM memory (in MB) memory: { type: 'number', "default":64 }, // VM memory (in MB)
bufferLatency: { type: 'number', "default":300 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture) bufferLatency: { type: 'number', "default":300 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
canvasLatency: { type: 'number', "default":300 } // in ms: time between canvas re-draws canvasLatency: { type: 'number', "default":500 }, // in ms: time between canvas re-draws
renderer: { type: 'string', "default":"canvas" },// 'dom' or 'canvas' (=faster) for immersive mode
debug: { type: 'boolean', "default":false }
}, },
init: function(){ init: function(){
@ -65,21 +67,22 @@ if( typeof AFRAME != 'undefined '){
}, },
requires:{ requires:{
com: "com/dom.js", com: "com/dom.js",
window: "com/window.js", window: "com/window.js",
v86: "com/isoterminal/libv86.js", v86: "com/isoterminal/libv86.js",
// allow xrsh to selfcontain scene + itself // allow xrsh to selfcontain scene + itself
xhook: "https://jpillora.com/xhook/dist/xhook.min.js", xhook: "https://jpillora.com/xhook/dist/xhook.min.js",
selfcontain: "com/selfcontainer.js", selfcontain: "com/selfcontainer.js",
// html to texture // html to texture
htmlinxr: "com/html-as-texture-in-xr.js", htmlinxr: "com/html-as-texture-in-xr.js",
// isoterminal features // isoterminal features
ISOTerminal: "com/isoterminal/ISOTerminal.js", PromiseWorker: "com/isoterminal/PromiseWorker.js",
localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js" ISOTerminal: "com/isoterminal/ISOTerminal.js",
localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js"
}, },
dom: { dom: {
//scale: 0.5, scale: 1.0,
events: ['click','keydown'], events: ['click','keydown'],
html: (me) => `<div class="isoterminal"> html: (me) => `<div class="isoterminal">
</div>`, </div>`,
@ -120,6 +123,10 @@ if( typeof AFRAME != 'undefined '){
} }
.XR .wb-body:has(> .isoterminal){ .XR .wb-body:has(> .isoterminal){
background: transparent;
}
.XR .isoterminal{
background: #000; background: #000;
} }
.isoterminal *, .isoterminal *,
@ -210,10 +217,12 @@ if( typeof AFRAME != 'undefined '){
} }
// init isoterminal // init isoterminal
this.isoterminal = new ISOTerminal(instance,this.data) this.term = new ISOTerminal(instance,this.data)
instance.addEventListener('DOMready', () => { instance.addEventListener('DOMready', () => {
//instance.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) if( this.data.renderer == 'dom' ){
instance.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`)
}
//instance.winbox.resize(720,380) //instance.winbox.resize(720,380)
let size = `width: ${Math.floor(this.data.cols*8.65)}; height: ${Math.floor(this.data.rows*21.1)}` let size = `width: ${Math.floor(this.data.cols*8.65)}; height: ${Math.floor(this.data.rows*21.1)}`
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`) instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`)
@ -221,17 +230,17 @@ if( typeof AFRAME != 'undefined '){
instance.addEventListener('window.oncreate', (e) => { instance.addEventListener('window.oncreate', (e) => {
instance.dom.classList.add('blink') instance.dom.classList.add('blink')
instance.setAttribute("xterm",`cols: ${this.data.cols}; rows: ${this.data.rows}; canvasLatency: ${this.data.canvasLatency}`) instance.setAttribute("xterm",`cols: ${this.data.cols}; rows: ${this.data.rows}; canvasLatency: ${this.data.canvasLatency}; XRrenderer: ${this.data.renderer}`)
instance.addEventListener("xterm-input", (e) => this.isoterminal.send(e.detail,0) ) instance.addEventListener("xterm-input", (e) => this.term.send(e.detail,0) )
// run iso // run iso
let opts = {dom:instance.dom} let opts = {dom:instance.dom}
for( let i in this.data ) opts[i] = this.data[i] for( let i in this.data ) opts[i] = this.data[i]
this.isoterminal.start(opts) this.term.start(opts)
}) })
instance.setAttribute("dom", "") instance.setAttribute("dom", "")
this.isoterminal.addEventListener('postReady', (e)=>{ this.term.addEventListener('postReady', (e)=>{
// bugfix: send window dimensions to xterm (xterm.js does that from dom-sizechange to xterm via escape codes) // bugfix: send window dimensions to xterm (xterm.js does that from dom-sizechange to xterm via escape codes)
let wb = instance.winbox let wb = instance.winbox
if( this.data.maximized ){ if( this.data.maximized ){
@ -240,12 +249,13 @@ if( typeof AFRAME != 'undefined '){
}else wb.resize() }else wb.resize()
}) })
this.isoterminal.addEventListener('ready', (e)=>{ this.term.addEventListener('ready', (e) => {
instance.dom.classList.remove('blink') instance.dom.classList.remove('blink')
this.isoterminal.emit('status',"running") this.term.emit('status',"running")
if( this.data.debug ) this.runTests()
}) })
this.isoterminal.addEventListener('status', function(e){ this.term.addEventListener('status', function(e){
let msg = e.detail let msg = e.detail
const w = instance.winbox const w = instance.winbox
if(!w) return if(!w) return
@ -265,7 +275,7 @@ if( typeof AFRAME != 'undefined '){
if( this.el.components.xterm ){ if( this.el.components.xterm ){
this.el.components.xterm.term.focus() this.el.components.xterm.term.focus()
} }
if( this.el.components.window ){ if( this.el.components.window && this.data.renderer == 'canvas'){
this.el.components.window.show( showdom ) this.el.components.window.show( showdom )
} }
} }
@ -287,6 +297,14 @@ if( typeof AFRAME != 'undefined '){
} }
}, },
runTests: async function(){
await AFRAME.utils.require({
"test_util": "tests/util.js",
"test_isoterminal":"tests/ISOTerminal.js"
})
console.test.run()
},
events:{ events:{
// combined AFRAME+DOM reactive events // combined AFRAME+DOM reactive events

View File

@ -22,7 +22,7 @@ ISOTerminal.prototype.emit = function(event,data,sender){
// this feels complex, but actually keeps event- and function-names more concise in codebase // this feels complex, but actually keeps event- and function-names more concise in codebase
this.dispatchEvent( evObj ) this.dispatchEvent( evObj )
if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj) if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj)
if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}, this.getTransferable(data) ) if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}, PromiseWorker.prototype.getTransferable(data) )
if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] ) if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] )
//}) //})
} }
@ -128,46 +128,30 @@ ISOTerminal.prototype.start = function(opts){
//disable_jit: false, //disable_jit: false,
filesystem: {}, filesystem: {},
autostart: true, autostart: true,
debug: this.opts.debug ? true : false
}; };
this
.setupWorker(opts)
.startVM(opts)
}
ISOTerminal.prototype.setupWorker = function(opts){
/* /*
* the WebWorker (which runs v86) * the WebWorker (which runs v86)
* *
*/ */
this.worker = new PromiseWorker( "com/isoterminal/worker.js", (cb,event,data) => {
this.worker = new Worker("com/isoterminal/worker.js"); if( !data.promiseId ) this.emit(event,data,"worker") // forward event to world
this.worker.onmessage = (e) => {
const {event,data} = e.data
const cb = (event,data) => () => {
if( data.promiseId ){
this.workerPromise.resolver(data) // forward to promise resolver
}else this.emit(event,data,"worker") // forward event to world
}
this.preventFrameDrop( cb(event,data) ) this.preventFrameDrop( cb(event,data) )
} })
/* return this
* postMessage.promise basically performs this.worker.postMessage }
* in a promise way (to easily retrieve async output)
*/
this.worker.postMessage.promise = function(data){
if( typeof data != 'object' ) data = {data}
this.resolvers = this.resolvers || {}
this.id = this.id == undefined ? 0 : this.id
data.id = this.id++
// Send id and task to WebWorker
this.preventFrameDrop( () => this.worker.postMessage(data,getTransferable(data) ) )
return new Promise(resolve => this.resolvers[data.id] = resolve);
}.bind(this.worker.postMessage)
this.worker.postMessage.promise.resolver = function(data){
if( !data || !data.promiseId ) throw 'promiseId not given'
this.resolvers[ data.promiseId ](data);
delete this.resolvers[ data.promiseId ]; // Prevent memory leak
}.bind(this.worker.postMessage)
ISOTerminal.prototype.startVM = function(opts){
this.emit('runISO',{...opts, bufferLatency: this.opts.bufferLatency }) this.emit('runISO',{...opts, bufferLatency: this.opts.bufferLatency })
const loading = [ const loading = [
@ -228,10 +212,10 @@ ISOTerminal.prototype.start = function(opts){
\r local-first, polyglot, unixy WebXR IDE & runtime \r local-first, polyglot, unixy WebXR IDE & runtime
\r \r
\r credits: NLnet | @nlnet@nlnet.nl \r credits: NLnet | @nlnet@nlnet.nl
\r MrDoob | THREE.js
\r Diego Marcos | AFRAME.js
\r Leon van Kammen | @lvk@mastodon.online \r Leon van Kammen | @lvk@mastodon.online
\r Fabien Benetou | @utopiah@mastodon.pirateparty.be \r Fabien Benetou | @utopiah@mastodon.pirateparty.be
\r Mr Doob | THREE.js
\r Diego Marcos | AFRAME.js
` `
const text_color = "\r" const text_color = "\r"
@ -303,16 +287,3 @@ ISOTerminal.prototype.preventFrameDrop = function(cb){
} }
} }
ISOTerminal.prototype.getTransferable = function(data){
function isTransferable(obj) {
return obj instanceof ArrayBuffer ||
obj instanceof MessagePort ||
obj instanceof ImageBitmap ||
(typeof OffscreenCanvas !== 'undefined' && obj instanceof OffscreenCanvas) ||
(typeof ReadableStream !== 'undefined' && obj instanceof ReadableStream) ||
(typeof WritableStream !== 'undefined' && obj instanceof WritableStream) ||
(typeof TransformStream !== 'undefined' && obj instanceof TransformStream);
}
if( isTransferable(data) ) console.log("Transferable!")
if( isTransferable(data) ) return isTransferable(data) ? [data] : undefined
}

View File

@ -0,0 +1,77 @@
/*
* This is basically a Javascript Proxy for 'new Worker()'
* which allows calling worker-functions via promises.
*
* It's basically comlink without the fat.
*
* const w = new PromiseWorker("worker.js", (cb,event_or_fn,data) => {
* cb(event_or_fn,data) // decorate/ratelimit/hooks here
* })
* w.foo().then( console.dir )
*
* in worker.js define a function: this.foo = () => return {foo:"Bar"}
*/
function PromiseWorker(file, onmessage){
let proxy
const worker = new Worker(file)
worker.onmessage = (e) => { // handle messages originating from worker
const {event,data} = e.data // this is worker.onmessage(...)
const cb = (event,data) => () => { //
if( data.promiseId ){ //
proxy.resolver(data) // forward to promise resolver
}
}
onmessage(cb,event,data)
}
return proxy = new Proxy(this,{
get(me,k){
if( k.match(/(postMessage|onmessage)/) ) return worker[k].bind(worker)
if( k.match(/(resolver|promise)/) ) return this[k].bind(this)
// promisify postMessage(...) call
return function(){
return this.promise(me,{event: k, data: [...arguments] })
}
},
promise(me,msg){
if( typeof msg != 'object' || !msg.event || !msg.data ){
throw 'worker.promise({event:..,data:..}) did not receive correct msg : '+JSON.stringify(msg)
}
this.resolvers = this.resolvers || {last:1,pending:{}}
msg.data.promiseId = this.resolvers.last++
// Send id and task to WebWorker
worker.postMessage(msg, PromiseWorker.prototype.getTransferable(msg.data) )
return new Promise( resolve => this.resolvers.pending[ msg.data.promiseId ] = resolve );
},
resolver(data){
if( !data || !data.promiseId ) throw 'promiseId not given'
this.resolvers.pending[ data.promiseId ](data.result);
delete this.resolvers.pending[ data.promiseId ]; // Prevent memory leak
}
})
}
PromiseWorker.prototype.getTransferable = function(data){
let objs = []
function isTransferable(obj) {
return obj instanceof ArrayBuffer ||
obj instanceof MessagePort ||
obj instanceof ImageBitmap ||
(typeof OffscreenCanvas !== 'undefined' && obj instanceof OffscreenCanvas) ||
(typeof ReadableStream !== 'undefined' && obj instanceof ReadableStream) ||
(typeof WritableStream !== 'undefined' && obj instanceof WritableStream) ||
(typeof TransformStream !== 'undefined' && obj instanceof TransformStream);
}
for( var i in data ){
if( isTransferable(data[i]) ) objs.push(data[i])
}
if( objs.length ) debugger
return objs.length ? objs : undefined
}

View File

@ -12,15 +12,21 @@ emulator.fs9p.update_file = async function(file,data){
} }
const inode = this.GetInode(p.id); const inode = this.GetInode(p.id);
const buf = typeof data == 'string' ? convert.toUint8Array(data) : data const buf = typeof data == 'string' ? convert.toUint8Array(data) : data || ""
await this.Write(p.id,0, buf.length, buf ) if( buf.length == 0 ) return new Promise( (resolve,reject) => resolve(data) )
// update inode
inode.size = buf.length
const now = Math.round(Date.now() / 1000);
inode.atime = inode.mtime = now;
me.postMessage({event:'exec',data:[`touch /mnt/${file}`]}) // update inode
return new Promise( (resolve,reject) => resolve(buf) )
try{
await this.Write(p.id,0, buf.length, buf )
// update inode
inode.size = buf.length
const now = Math.round(Date.now() / 1000);
inode.atime = inode.mtime = now;
me.postMessage({event:'exec',data:[`touch /mnt/${file}`]}) // update inode
return new Promise( (resolve,reject) => resolve(buf) )
}catch(e){
console.error({file,data})
return new Promise( (resolve,reject) => reject(e) )
}
} }
emulator.fs9p.append_file = async function(file,data){ emulator.fs9p.append_file = async function(file,data){
@ -44,5 +50,3 @@ emulator.fs9p.append_file = async function(file,data){
} }
this['fs9p.append_file'] = function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
this['fs9p.update_file'] = function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }

View File

@ -9,7 +9,7 @@ if( typeof emulator != 'undefined' ){
this['emulator.save_state'] = async function(){ this['emulator.save_state'] = async function(){
console.log("saving session") console.log("saving session")
let state = await emulator.save_state() let state = await emulator.save_state()
this.postMessage({event:"state_saved",data:state}) this.postMessage({event:"state_saved",data:state},[state])
} }
@ -34,7 +34,10 @@ if( typeof emulator != 'undefined' ){
this.addEventListener('state_restored', function(){ this.addEventListener('state_restored', function(){
// simulate / fastforward boot events // simulate / fastforward boot events
this.postBoot( () => this.send("l\n") ) this.postBoot( () => {
this.send("l\n")
this.send("hook wakeup\n")
})
}) })
this.worker.postMessage({event:'emulator.restore_state',data:state}) this.worker.postMessage({event:'emulator.restore_state',data:state})

View File

@ -2,22 +2,31 @@
if( typeof emulator != 'undefined' ){ if( typeof emulator != 'undefined' ){
// inside worker-thread // inside worker-thread
// unix to js device this.listenIndexHTML = () => {
this.emulator.readFromPipe( 'root/index.html', async (data) => { const file = "dev/browser/html"
const buf = await emulator.read_file("root/index.html") emulator.readFromPipe( file, async (data) => {
const decoder = new TextDecoder('utf-8'); const buf = await emulator.read_file( file )
const html = decoder.decode(buf).replace(/^#!\/bin\/html/,'') // remove leftover shebangs if any const decoder = new TextDecoder('utf-8');
try{ const html = decoder.decode(buf).replace(/^#!\/bin\/html/,'') // remove leftover shebangs if any
this.postMessage({event:'runHTML',data:[html]}) try{
}catch(e){ this.postMessage({event:'runHTML',data:[html]})
console.error(e) }catch(e){
} console.error(file)
}) console.error(e)
}
})
}
}else{ }else{
// inside browser-thread // inside browser-thread
ISOTerminal.addEventListener('emulator-started', function(){
this.addEventListener('ready', async () => {
this.worker.listenIndexHTML()
})
})
ISOTerminal.prototype.runHTML = function(html){ ISOTerminal.prototype.runHTML = function(html){
let $scene = document.querySelector("a-scene") let $scene = document.querySelector("a-scene")
let $root = document.querySelector("a-entity#root") let $root = document.querySelector("a-entity#root")
@ -26,6 +35,7 @@ if( typeof emulator != 'undefined' ){
$root.id = "root" $root.id = "root"
$scene.appendChild($root) $scene.appendChild($root)
} }
console.log(html)
$root.innerHTML = html $root.innerHTML = html
} }

View File

@ -2,8 +2,8 @@ if( typeof emulator != 'undefined' ){
// inside worker-thread // inside worker-thread
// unix to js device // unix to js device
this.emulator.readFromPipe( 'root/index.js', async (data) => { this.emulator.readFromPipe( 'dev/browser/js', async (data) => {
const buf = await emulator.read_file("root/index.js") const buf = await emulator.read_file("dev/browser/js")
const decoder = new TextDecoder('utf-8'); const decoder = new TextDecoder('utf-8');
const js = decoder.decode(buf).replace(/^#!\/bin\/js/,'') // remove leftover shebangs if any const js = decoder.decode(buf).replace(/^#!\/bin\/js/,'') // remove leftover shebangs if any
try{ try{

View File

@ -20,10 +20,20 @@ if( typeof emulator != 'undefined' ){
}else{ }else{
// inside browser-thread // inside browser-thread
ISOTerminal.addEventListener('javascript-eval', function(e){ ISOTerminal.addEventListener('javascript-eval', async function(e){
const {script,PID} = e.detail const {script,PID} = e.detail
let res = (new Function(`${script}`))() let res
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2) try{
res = (new Function(`${script}`))()
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
}catch(e){
console.error(e)
console.info(script)
res = "error: "+e.toString()
if( e.filename ){
res += "\n"+e.filename+":"+e.lineno+":"+e.colno
}
}
// update output to 9p with PID as filename (in /mnt/run) // update output to 9p with PID as filename (in /mnt/run)
this.emit('fs9p.update_file', [`run/${PID}`, this.convert.toUint8Array(res)] ) this.emit('fs9p.update_file', [`run/${PID}`, this.convert.toUint8Array(res)] )
}) })

View File

@ -9,17 +9,17 @@ ISOTerminal.prototype.redirectConsole = function(handler){
log.apply(log, args); log.apply(log, args);
}; };
console.error = (...args)=>{ console.error = (...args)=>{
const textArg = args[0].message?args[0].message:args[0]; const textArg = args[0]
handler( textArg+'\n', '\x1b[31merror\x1b[0m'); handler( textArg+'\n', '\x1b[31merror\x1b[0m');
err.apply(log, args); err.apply(log, args);
}; };
console.dir = (...args)=>{ console.dir = (...args)=>{
const textArg = args[0].message?args[0].message:args[0]; const textArg = args[0]
handler( JSON.stringify(textArg,null,2)+'\n'); handler( JSON.stringify(textArg,null,2)+'\n');
dir.apply(log, args); dir.apply(log, args);
}; };
console.warn = (...args)=>{ console.warn = (...args)=>{
const textArg = args[0].message?args[0].message:args[0]; const textArg = args[0]
handler(textArg+'\n','\x1b[38;5;208mwarn\x1b[0m'); handler(textArg+'\n','\x1b[38;5;208mwarn\x1b[0m');
err.apply(log, args); err.apply(log, args);
}; };
@ -34,7 +34,7 @@ ISOTerminal.addEventListener('emulator-started', function(){
str.trim().split("\n").map( (line) => { str.trim().split("\n").map( (line) => {
finalStr += '\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n' finalStr += '\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n'
}) })
this.emit('fs9p.append_file', ["/dev/browser/console",finalStr]) this.emit('append_file', ["/dev/browser/console",finalStr])
}) })
window.addEventListener('error', function(event) { window.addEventListener('error', function(event) {
@ -46,7 +46,6 @@ ISOTerminal.addEventListener('emulator-started', function(){
}); });
window.addEventListener('unhandledrejection', function(event) { window.addEventListener('unhandledrejection', function(event) {
console.error('Unhandled promise rejection:')
console.error(event); console.error(event);
}); });

View File

@ -2,12 +2,14 @@ importScripts("libv86.js");
importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions) importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions)
this.runISO = function(opts){ this.runISO = function(opts){
this.opts = opts
if( opts.debug ) console.dir(opts)
if( opts.cdrom && !opts.cdrom.url.match(/^http/) ) opts.cdrom.url = "../../"+opts.cdrom.url if( opts.cdrom && !opts.cdrom.url.match(/^http/) ) opts.cdrom.url = "../../"+opts.cdrom.url
if( opts.bzimage && !opts.cdrom.url.match(/^http/) ) opts.bzimage.url = "../../"+opts.bzimage.url if( opts.bzimage && !opts.cdrom.url.match(/^http/) ) opts.bzimage.url = "../../"+opts.bzimage.url
console.dir(opts)
let emulator = this.emulator = new V86(opts); let emulator = this.emulator = new V86(opts);
console.log("worker:started emulator") console.log("[worker.js] started emulator")
// event forwarding // event forwarding
@ -40,8 +42,10 @@ this.runISO = function(opts){
/* /*
* forward events/functions so non-worker world can reach them * forward events/functions so non-worker world can reach them
*/ */
this['emulator.create_file'] = function(){ emulator.create_file.apply(emulator, arguments[0]) } this['emulator.create_file'] = async function(){ return emulator.create_file.apply(emulator, arguments[0]) }
this['emulator.read_file'] = function(){ emulator.read_file.apply(emulator, arguments[0]) } this['emulator.read_file'] = async function(){ return emulator.read_file.apply(emulator, arguments[0]) }
this['emulator.append_file'] = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
this['emulator.update_file'] = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }
// filename will be read from 9pfs: "/mnt/"+filename // filename will be read from 9pfs: "/mnt/"+filename
emulator.readFromPipe = function(filename,cb){ emulator.readFromPipe = function(filename,cb){
@ -60,11 +64,23 @@ this.runISO = function(opts){
* forward events/functions so non-worker world can reach them * forward events/functions so non-worker world can reach them
*/ */
this['serial0-input'] = function(c){ this.emulator.bus.send( 'serial0-input', c) } // to /dev/ttyS0 this['serial0-input'] = function(c){ emulator.bus.send( 'serial0-input', c) } // to /dev/ttyS0
this['serial1-input'] = function(c){ this.emulator.bus.send( 'serial1-input', c) } // to /dev/ttyS1 this['serial1-input'] = function(c){ emulator.bus.send( 'serial1-input', c) } // to /dev/ttyS1
this['serial2-input'] = function(c){ this.emulator.bus.send( 'serial2-input', c) } // to /dev/ttyS2 this['serial2-input'] = function(c){ emulator.bus.send( 'serial2-input', c) } // to /dev/ttyS2
this.onmessage = function(e){ this.onmessage = async function(e){
let {event,data} = e.data let {event,data} = e.data
if( this[event] ) this[event](data) if( this[event] ){
if( this.opts?.debug ) console.log(`[worker.js] this.${event}(${JSON.stringify(data).substr(0,60)})`)
try{
let result = await this[event](data)
if( data.promiseId ){ // auto-callback to ISOTerminal.worker.promise(...)
this.postMessage({event,data: {...data,result}})
}
}catch(e){
if( data.promiseId ){ // auto-callback to ISOTerminal.worker.promise(...)
this.postMessage({event,data: {...data,error:e}})
}
}
}
} }

View File

@ -92,6 +92,7 @@ const TERMINAL_THEME = {
AFRAME.registerComponent('xterm', { AFRAME.registerComponent('xterm', {
schema: Object.assign({ schema: Object.assign({
XRrenderer: { type: 'string', default: 'canvas', },
cols: { type: 'number', default: 110, }, cols: { type: 'number', default: 110, },
rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) }, rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) },
canvasLatency:{ type:'number', default: 200 } canvasLatency:{ type:'number', default: 200 }
@ -108,21 +109,33 @@ AFRAME.registerComponent('xterm', {
overflow: hidden; overflow: hidden;
`) `)
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
// and terminal text (this.el.planeText.getObject("mesh"))
this.el.setAttribute("geometry",`primitive: box; width:2.07; height:${this.data.rows*5.3/this.data.cols}*2; depth: -0.12`)
this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ")
this.el.planeText = document.createElement('a-entity')
this.el.planeText.setAttribute("geometry",`primitive: plane; width:2; height:${this.data.rows*5/this.data.cols}*2`)
this.el.appendChild(this.el.planeText)
this.el.terminalElement = terminalElement this.el.terminalElement = terminalElement
// we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode if( this.data.XRrenderer == 'canvas' ){
this.el.sceneEl.addEventListener('enter-vr', this.enterImmersive.bind(this) ) // setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
this.el.sceneEl.addEventListener('enter-ar', this.enterImmersive.bind(this) ) // and terminal text (this.el.planeText.getObject("mesh"))
this.el.sceneEl.addEventListener('exit-vr', this.exitImmersive.bind(this) ) this.el.setAttribute("geometry",`primitive: box; width:2.07; height:${this.data.rows*5.3/this.data.cols}*2; depth: -0.12`)
this.el.sceneEl.addEventListener('exit-ar', this.exitImmersive.bind(this) ) this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ")
this.el.planeText = document.createElement('a-entity')
this.el.planeText.setAttribute("geometry",`primitive: plane; width:2; height:${this.data.rows*5/this.data.cols}*2`)
this.el.appendChild(this.el.planeText)
// we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode
this.el.sceneEl.addEventListener('enter-vr', this.enterImmersive.bind(this) )
this.el.sceneEl.addEventListener('enter-ar', this.enterImmersive.bind(this) )
this.el.sceneEl.addEventListener('exit-vr', this.exitImmersive.bind(this) )
this.el.sceneEl.addEventListener('exit-ar', this.exitImmersive.bind(this) )
}
this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => {
if( this.el.sceneEl.renderer.xr.isPresenting ){
// workaround
// xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode)
//this.term._core.viewport._innerRefresh()
this.term._core.renderer._renderDebouncer._innerRefresh()
}
}, this.data.canvasLatency)
// Build up a theme object // Build up a theme object
const theme = Object.keys(this.data).reduce((theme, key) => { const theme = Object.keys(this.data).reduce((theme, key) => {
@ -143,21 +156,13 @@ AFRAME.registerComponent('xterm', {
disableStdin: false, disableStdin: false,
rows: this.data.rows, rows: this.data.rows,
cols: this.data.cols, cols: this.data.cols,
fontFamily: 'Cousine, monospace',
fontSize: this.fontSize, fontSize: this.fontSize,
lineHeight: 1.15, lineHeight: 1.15,
useFlowControl: true, useFlowControl: true,
rendererType: this.renderType // 'dom' // 'canvas' rendererType: this.renderType // 'dom' // 'canvas'
}) })
this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => {
if( this.el.sceneEl.renderer.xr.isPresenting ){
// workaround
// xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode)
//this.term._core.viewport._innerRefresh()
this.term._core.renderer._renderDebouncer._innerRefresh()
}
}, this.data.canvasLatency)
this.term.open(terminalElement) this.term.open(terminalElement)
this.term.focus() this.term.focus()
this.setRenderType('dom') this.setRenderType('dom')