diff --git a/com/isoterminal.js b/com/isoterminal.js index b10bd12..ddceaf7 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -304,15 +304,17 @@ if( typeof AFRAME != 'undefined '){ remotekeyboard: "com/isoterminal/feat/remotekeyboard.js", indexhtml: "com/isoterminal/feat/index.html.js", indexjs: "com/isoterminal/feat/index.js.js", - autorestore: "com/isoterminal/feat/autorestore.js", pastedropFeat: "com/isoterminal/feat/pastedrop.js", httpfs: "com/isoterminal/feat/httpfs.js", + autorestore: "com/isoterminal/feat/autorestore.js", } if( this.data.emulator == 'fbterm' ){ features['fbtermjs'] = "com/isoterminal/term.js" features['fbterm'] = "com/isoterminal/feat/term.js" } await AFRAME.utils.require(features) + // this one extends autorestore.js + await AFRAME.utils.require({remotestorage: "com/isoterminal/feat/remotestorage.js"}) this.el.setAttribute("selfcontainer","") diff --git a/com/isoterminal/ISOTerminal.js b/com/isoterminal/ISOTerminal.js index 6b20e66..d7af75f 100644 --- a/com/isoterminal/ISOTerminal.js +++ b/com/isoterminal/ISOTerminal.js @@ -256,6 +256,7 @@ ISOTerminal.prototype.startVM = function(opts){ this.v86opts = opts this.addEventListener('emulator-started', async (e) => { + if( this.boot.fromImage ) return this.emit('serial-output-string', "\r[!] downloading session...please wait\n\r[!] this could take a while depending on your connection..\n\r") let line = '' this.ready = false @@ -278,12 +279,17 @@ ISOTerminal.prototype.startVM = function(opts){ } ISOTerminal.prototype.bootISO = function(){ + const getImage = (str) => decodeURIComponent( + str.match(/\&img=/) ? str.replace(/.*img=/,'').replace(/\&.*/,'') : '' + ) let msglib = this.getLoaderMsg() this.emit('status',msglib.loadmsg) let msg = "\n\r" + msglib.empowermsg + msglib.text_color + msglib.loadmsg + msglib.text_reset this.emit('serial-output-string', msg) - this.emit('runISO',{...this.v86opts, bufferLatency: this.opts.bufferLatency }) - + if( getImage(this.boot.hash) ){ + this.boot.fromImage = true + } + this.emit('runISO',{...this.v86opts, bufferLatency: this.opts.bufferLatency, img: getImage(this.boot.hash) }) } diff --git a/com/isoterminal/feat/autorestore.js b/com/isoterminal/feat/autorestore.js index aa9341c..8e58c3c 100644 --- a/com/isoterminal/feat/autorestore.js +++ b/com/isoterminal/feat/autorestore.js @@ -1,8 +1,16 @@ +// this is restoring state to/from the v86 emulator +// however instead of passing the huge blob between webworker/browser +// we transfer it via localforage as a base64 string + if( typeof emulator != 'undefined' ){ // inside worker-thread importScripts("localforage.js") // we don't instance it again here (just use its functions) this.restore_state = async function(data){ + // fastforward instance state + this.opts.muteUntilPrompt = false + this.ready = true + return new Promise( (resolve,reject) => { localforage.getItem("state", async (err,stateBase64) => { if( stateBase64 && !err ){ @@ -15,15 +23,20 @@ if( typeof emulator != 'undefined' ){ }) } this.save_state = async function(){ - console.log("saving session") - let state = await emulator.save_state() - localforage.setDriver([ - localforage.INDEXEDDB, - localforage.WEBSQL, - localforage.LOCALSTORAGE - ]).then( () => { - localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) ) - console.log("state saved") + return new Promise( async (resolve,reject ) => { + console.log("saving session") + let state = await emulator.save_state() + localforage.setDriver([ + localforage.INDEXEDDB, + localforage.WEBSQL, + localforage.LOCALSTORAGE + ]) + .then( () => { + localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) ) + console.log("state saved") + resolve() + }) + .catch( reject ) }) } @@ -32,42 +45,44 @@ if( typeof emulator != 'undefined' ){ // inside browser-thread ISOTerminal.addEventListener('emulator-started', function(e){ this.autorestore(e) + this.emit("autorestore-installed") }) - ISOTerminal.prototype.autorestore = async function(e){ + ISOTerminal.prototype.restore = async function(e){ - localforage.setDriver([ - localforage.INDEXEDDB, - localforage.WEBSQL, - localforage.LOCALSTORAGE - ]).then( () => { + const onGetItem = (err,stateBase64) => { + const askConfirm = () => { + if( window.localStorage.getItem("restorestate") == "true" ) return true + try{ + const scene = document.querySelector('a-scene'); + if( scene.is('ar-mode') ) scene.exitAR() + if( scene.is('vr-mode') ) scene.exitVR() + }catch(e){} + return confirm( "Continue old session?" ) + } - localforage.getItem("state", async (err,stateBase64) => { - const askConfirm = () => { - if( window.localStorage.getItem("restorestate") == "true" ) return true - try{ - const scene = document.querySelector('a-scene'); - if( scene.is('ar-mode') ) scene.exitAR() - if( scene.is('vr-mode') ) scene.exitVR() - }catch(e){} - return confirm('continue last session?') - } - if( stateBase64 && !err && document.location.hash.length < 2 && askConfirm() ){ - this.noboot = true // see feat/boot.js - try{ - await this.worker.restore_state() + if( stateBase64 && !err && document.location.hash.length < 2 && askConfirm() ){ + this.noboot = true // see feat/boot.js + try{ + this.worker.restore_state() + .then( () => { // simulate / fastforward boot events this.postBoot( () => { - this.send("l\n") - this.send("hook wakeup\n") + // force redraw terminal issue + this.send("l") + setTimeout( () => this.send("l"), 200 ) + //this.send("12") + this.emit("exec",["source /etc/profile.sh; hook wakeup\n"]) + this.emit("restored") }) - }catch(e){ console.error(e) } - } - }) - - this.save = async () => { - await this.worker.save_state() + }) + }catch(e){ console.error(e) } } + } + + const doRestore = () => { + + localforage.getItem("state", (err,stateBase64) => onGetItem(err,stateBase64) ) window.addEventListener("beforeunload", function (e) { var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later"; @@ -75,7 +90,17 @@ if( typeof emulator != 'undefined' ){ return confirmationMessage; //Webkit, Safari, Chrome }); - }) + } + + localforage.setDriver([ + localforage.INDEXEDDB, + localforage.WEBSQL, + localforage.LOCALSTORAGE + ]) + .then( () => doRestore() ) + } + ISOTerminal.prototype.autorestore = ISOTerminal.prototype.restore // alias to launch during boot + } diff --git a/com/isoterminal/feat/boot.js b/com/isoterminal/feat/boot.js index 0f0db3e..6674117 100644 --- a/com/isoterminal/feat/boot.js +++ b/com/isoterminal/feat/boot.js @@ -1,5 +1,5 @@ ISOTerminal.addEventListener('ready', function(e){ - setTimeout( () => this.boot(), 50 ) // because of autorestore.js + setTimeout( () => this.boot(), 50 ) // allow other features/plugins to settle first (autorestore.js e.g.) }) ISOTerminal.prototype.bootMenu = function(e){ @@ -17,7 +17,7 @@ ISOTerminal.prototype.bootMenu = function(e){ }else{ // autoboot if( this.term ){ - this.term.handler( e.detail.bootMenu || e.detail.bootMenuURL ) + this.term.handler( String(e.detail.bootMenu || e.detail.bootMenuURL).charAt(0) ) this.term.handler("\n") } } @@ -33,10 +33,17 @@ ISOTerminal.prototype.boot = async function(e){ 'export BROWSER=1', ] for ( let i in document.location ){ - if( typeof document.location[i] == 'string' ){ + if( typeof document.location[i] == 'string' && !String(i).match(/(hash|search)/) ){ env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') ) } } + + // we export the cached hash/query (because they might be gone due to remotestorage plugin) + if( this.boot.hash.charAt(2) == '&' ){ // strip bootoption + this.boot.hashExBoot = `#` + this.boot.hash.substr(3) + } + env.push( 'export HASH="'+decodeURIComponent( this.boot.hashExBoot || this.boot.hash ) +'"' ) + env.push( 'export QUERY="'+decodeURIComponent( this.boot.query ) +'"' ) await this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) ) if( this.serial_input == 0 ){ @@ -47,8 +54,12 @@ ISOTerminal.prototype.boot = async function(e){ } -// here REPL's can be defined +ISOTerminal.prototype.boot.fromImage = false ISOTerminal.prototype.boot.menu = [] +ISOTerminal.prototype.boot.hash = document.location.hash +ISOTerminal.prototype.boot.query = document.location.search + +// here REPL's can be defined // REPL: iso if( typeof window.PromiseWorker != 'undefined' ){ // if xrsh v86 is able to run in in worker diff --git a/com/isoterminal/feat/remotestorage.js b/com/isoterminal/feat/remotestorage.js new file mode 100644 index 0000000..3d654e2 --- /dev/null +++ b/com/isoterminal/feat/remotestorage.js @@ -0,0 +1,365 @@ +/* Remote storage feature + * + * NOTE: this feature extends the localstorage functions. + * this file can be excluded without crippling the + * core localstorage mechanism. + */ + +// see https://remotestorage.io/rs.js/docs/data-modules/ +ISOTerminal.prototype.remoteStorageModule = { + name: 'xrsh', + builder: function(privateClient, publicClient) { + return { + exports: { + add: function(data,opts) { + if( !data || !opts.filename || !opts.mimetype) throw 'webxr.add() needs filedata + filename + mimetype' + const client = opts.client = opts.public ? publicClient : privateClient; + return client.storeFile(opts.mimetype, opts.filename,data) + }, + + getListing: function(a,opts){ + opts = opts || {} + const client = opts.public ? publicClient : privateClient; + return client.getListing(a) + }, + + getFile: function(file,opts){ + opts = opts || {} + const client = opts.public ? publicClient : privateClient; + return client.getFile(file) + }, + + remove: function(file,opts){ + opts = opts || {} + const client = opts.public ? publicClient : privateClient; + return client.remove(file) + } + + } + } + } +}; + +// this is the HTML which we append to the remotestorage widget: +// https://remotestorage.io/rs.js/docs/getting-started/connect-widget.html +const _rs_widget_html = ` +
+ + +   + filemanager +
+
+
+
+
WebBrowser session:
+
+ + +
+
+ + + + +` + +const widgetForm = function(opts){ + let el = document.querySelector("#remotestorage-widget select#states") + const $widget = document.querySelector("#remotestorage-widget .rs-widget") + const $result = document.querySelector("#remotestorage-widget #result") + if( !el ){ + + let div = document.createElement('div') + div.innerHTML = _rs_widget_html + $widget.appendChild(div) + $widget.querySelector("h3").innerText = "Click here to connect your storage" + + el = $widget.querySelector("select#states") + el.addEventListener('change', () => { + if( el.options.selectedIndex == 0 ) return; // ignore default option + this.remoteStorage.widget.filename = el.value + opts.status = "loading..." + $widget.classList.add(["blink"]) + this.remoteStorage.xrsh.getFile(el.value) + .then( (data,err) => { + this.restore({remotestorage:true, data:data.data}) + }) + }) + + this.setupStorageListeners($widget,opts) + } + + this.updateStorageForm(el,$widget,$result,opts) +} + +ISOTerminal.prototype.remoteStorageWidget = function(){ + if( this.remoteStorageWidget.installed ) return // only do once + let apis = { + //dropbox: "ce876ce", + //googledrive: "c3983c3" + } + const modules = [ this.remoteStorageModule ] + const remoteStorage = this.remoteStorage = new RemoteStorage({logging: true, modules }) + remoteStorage.setApiKeys(apis) + + remoteStorage.on('not-connected', (e) => { + this.remoteStorage.connected = false + this.remoteStorage.widget.form({show:false}) + }) + remoteStorage.on('ready', (e) => { }) + remoteStorage.on('connected', (e) => { + this.remoteStorage.connected = true + this.remoteStorage.widget.form({update:true,show:true}) + }) + + remoteStorage.access.claim( `xrsh`, 'rw'); // our data dir + remoteStorage.caching.enable( `/xrsh/` ) // local-first, remotestorage-second + remoteStorage.caching.enable( `/public/xrsh/` ) // local-first, remotestorage-second + + // create widget + let opts = {} + opts.modalBackdrop = false + opts.leaveOpen = false + const widget = remoteStorage.widget = new window.Widget(remoteStorage, opts) + widget.attach(); + widget.form = widgetForm.bind(this) + + this.remoteStorageWidget.installed = true +} + +ISOTerminal.prototype.setupStorageListeners = function($widget,opts){ + + this.addEventListener("ready", () => { + opts.loading = false + opts.status = "session loaded" + }) + + // setup events + $widget.querySelector("button#save").addEventListener("click", () => { + this.save({remotestorage:true}) + }) + $widget.querySelector("button#saveLocal").addEventListener("click", async () => { + if( confirm("save current session?") ){ + $widget.classList.add(['blink']) + await this.worker.save_state() + $widget.classList.remove(['blink']) + alert("succesfully saved session") + } + }) + $widget.querySelector("button#restoreLocal").addEventListener("click", async () => { + this.restore({localstorage:true}) + }) + $widget.querySelector("button#saveLocalFile").addEventListener("click", () => { + alert("saveFile") + }) + $widget.querySelector("button#restoreLocalFile").addEventListener("click", () => { + alert("saveFile") + }) + $widget.addEventListener("click", () => this.remoteStorage.widget.open() ) + this.addEventListener("restored", () => this.remoteStorage.widget.form({loading:false, status:"session restored"}) ) +} + +ISOTerminal.prototype.updateStorageForm = function(el,$widget,$result,opts){ + + if( typeof opts.show != 'undefined' ){ + $widget.querySelector('.form#remote').style.display = opts.show ? 'block' : 'none' + } + + if( typeof opts.status != 'undefined'){ + $result.innerText = opts.status + } + + if( opts.public ){ + const link = this.remoteStorage.remote.href + '/public/xrsh/' + this.remoteStorage.widget.filename + const linkWebView = document.location.href.replace(/(\?|#).*/,'') + `#1&img=${link}` + $result.innerHTML += `
public weblink` + } + + if( typeof opts.loading != 'undefined' ){ + $widget.classList[ opts.loading ? 'add' : 'remove' ](["blink"]) + } + + if( opts.update ){ + + const createOption = (opt) => { + let option = document.createElement("option") + option.value = opt.value + option.innerText = opt.name || opt.value + el.appendChild(option) + } + + el.innerHTML = '' + createOption({value:"-- snapshots --"}) + this.remoteStorage.xrsh.getListing() + .then( (data,err) => { + for( let file in data ){ + createOption({value:file}) + } + }) + + } +} + +const requireFiles = () => AFRAME.utils.require({remotestorageWidget:"assets/aframe-remotestorage.min.js"}) +const getForage = () => localforage.setDriver([ + localforage.INDEXEDDB, + localforage.WEBSQL, + localforage.LOCALSTORAGE + ]) + +// we extend the autorestore feature at the init-event +ISOTerminal.addEventListener('init', function(e){ + + const decorateSave = () => { + + let localSave = this.save + + this.save = async (opts) => { + requireFiles() + .then( getForage ) + .then( async () => { + + if( opts.localstorage ){ + this.save.localstorage(opts) // default behaviour + } + + this.remoteStorageWidget() + + if( opts.remotestorage ){ + this.remoteStorage.widget.open() // force open dialog + + let filename = String(this.remoteStorage.widget.filename || "snapshot").replace(/\.bin/,'') + filename = prompt("please name your snapshot:", filename ) + filename = filename.replace(/\.bin*/,'') + '.bin' + + // make it public or not (disabled for now as it does not work flawlessly) + const public = false // confirm('create a public link?') + this.remoteStorage.widget.filename = filename + + this.remoteStorage.widget.form({loading:true,status: "saving.."}) + + await this.worker.save_state() + localforage.getItem("state", async (err,stateBase64) => { + this.remoteStorage.xrsh.add( stateBase64, {filename, mimetype: 'application/x-v86-base64', public}) + .then( () => { + console.log("saved to remotestorage") + setTimeout( + () => this.remoteStorage.widget.form({update:true, loading:false, status:"saved succesfully", public}) + ,500 ) + }) + .catch( console.error ) + }) + } + + }) + } + this.save.localstorage = localSave + } + + const decorateRestore = () => { + + let localRestore = this.restore + + this.restore = async (opts) => { + requireFiles() + .then( getForage ) + .then( async () => { + + document.querySelector("#remotestorage-widget .rs-widget").classList.add(['blink']) + if( opts.localstorage ){ + this.restore.localstorage.apply(this,opts) // default behaviour + } + if( opts.remotestorage ){ + localforage.setItem( "state", opts.data ) + .then( () => this.restore.localstorage.call(this,opts) ) // now trigger default behaviour + } + + }) + } + this.restore.localstorage = localRestore + this.autorestore = this.restore // reroute automapping + } + + // decorate save() and restore() of autorestore.js after it's declared (hence the setTimeout) + this.addEventListener('autorestore-installed', () => { + decorateSave() + decorateRestore() + }) + +}) + +// decorate autorestore +const autorestoreLocalStorage = ISOTerminal.prototype.autorestore; +ISOTerminal.prototype.autorestore = function(data){ + requireFiles().then( async () => { + this.remoteStorageWidget() + }) +} + diff --git a/com/isoterminal/worker.js b/com/isoterminal/worker.js index 1e1dca4..df5279b 100644 --- a/com/isoterminal/worker.js +++ b/com/isoterminal/worker.js @@ -40,6 +40,7 @@ this.runISO = async function(opts){ emulator.add_listener("emulator-started", function(){ importScripts("feat/9pfs_utils.js") this.postMessage({event:"emulator-started",data:false}); + if( opts.img ) this.restoreImage(opts) }.bind(this)); /* @@ -66,13 +67,12 @@ this.runISO = async function(opts){ }) } - - importScripts("feat/javascript.js") importScripts("feat/index.html.js") importScripts("feat/autorestore.js") if( opts.overlayfs ) await this.addOverlayFS(opts) + } /* * forward events/functions so non-worker world can reach them @@ -113,3 +113,21 @@ this.addOverlayFS = async function(opts){ } }) } + +this.restoreImage = function(opts){ + + this.postMessage({event:"serial0-output-string",data:`\n\r[!] loading session image:\n\r[!] ${opts.img}\n\r[!] please wait...internet speed matters here..\n`}) + + fetch( opts.img ) + .then( (res) => { + this.postMessage({event:"serial0-output-string",data:`\r[v] image downloaded..restoring..\n\r`}) + return res + }) + .then( (res) => res.arrayBuffer() ) + .then( async (buf) => { + await this.emulator.restore_state(buf) + this.postMessage({event:"serial0-output-string",data:`[v] restored\n\r`}) + + }) + .catch( console.error ) +} diff --git a/com/launcher.js b/com/launcher.js index 613d4ee..06adb8a 100644 --- a/com/launcher.js +++ b/com/launcher.js @@ -99,6 +99,7 @@ AFRAME.registerComponent('launcher', { flex-direction: row; align-items: flex-start; height: 50px; + width:764px; overflow:hidden; position: fixed; bottom: 10px; @@ -307,6 +308,8 @@ AFRAME.registerSystem('launcher',{ register: function(launchable){ try{ let {name, description, cb} = launchable + const exist = this.registered.find( (r) => r.manifest.name == name ) + if( exist ) return // already registered this.registered.push({ manifest: {name, description, icons: launchable.icon ? [{src:launchable.icon}] : [] }, launcher: cb