You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
373 lines
14 KiB
JavaScript
373 lines
14 KiB
JavaScript
"use strict";
|
|
|
|
/** ***** WebPerl - http://webperl.zero-g.net *****
|
|
*
|
|
* Copyright (c) 2018 Hauke Daempfling (haukex@zero-g.net)
|
|
* at the Leibniz Institute of Freshwater Ecology and Inland Fisheries (IGB),
|
|
* Berlin, Germany, http://www.igb-berlin.de
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the same terms as Perl 5 itself: either the GNU General Public
|
|
* License as published by the Free Software Foundation (either version 1,
|
|
* or, at your option, any later version), or the "Artistic License" which
|
|
* comes with Perl 5.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
* See the licenses for details.
|
|
*
|
|
* You should have received a copy of the licenses along with this program.
|
|
* If not, see http://perldoc.perl.org/index-licence.html
|
|
**/
|
|
|
|
/** Public Interface:
|
|
* Perl.output - override this for output somewhere else
|
|
* Perl.stateChanged - callback for state changes
|
|
* Perl.trace - enables debug/trace messages
|
|
* Perl.endAfterMain - see exit(0) discussion below
|
|
* Perl.init - initializes the Perl interpreter. Pass this function a callback to be called when init is done
|
|
* Perl.start - starts up the Perl interpreter
|
|
* Perl.eval - evaluates the given Perl string
|
|
* Perl.end - Ends the Perl interpreter
|
|
* Perl.makeOutputTextarea - creates a <textarea> and redirects output there (see HTML examples)
|
|
*/
|
|
|
|
/** On our patched perlmain.c and exit(0):
|
|
* Since we want the Perl process to persist while the webpage is open, we've patched perlmain.c so that:
|
|
* 1. Global destruction and END blocks aren't triggered until explicitly requested by calling emperl_end_perl()
|
|
* 2. emperl_end_perl() won't actually call exit() when the exit status is zero (because Emscripten complains when you call exit() and NO_EXIT_RUNTIME is set)
|
|
* This has some consequences:
|
|
* 1. An exit(0) in the main program won't have any effect other than stopping the execution of the main program at that point, the interpreter is kept running (?)
|
|
* 2. ... TODO Later: Any other consequences?
|
|
* As a result:
|
|
* - Just don't call exit(0);/exit; from Perl.
|
|
* Note that if you want to "end" the currently running Perl, so that global destruction is performed and END blocks are executed, there are several ways to do so:
|
|
* - From JS, set Perl.endAfterMain before initializing Perl (this enables a "hack" that calls emperl_end_perl() after main() finishes)
|
|
* - From JS, call Perl.end()
|
|
* - From Perl, WebPerl::end_perl() (TODO Later: This doesn't cause Module.onExit to be called, right?)
|
|
*/
|
|
|
|
var Module;
|
|
var Perl = {
|
|
trace: false, // user may enable this
|
|
endAfterMain: false, // user may enable this (before Perl.init)
|
|
// internal variables:
|
|
initStepsLeft: 2, // Must match number of Perl.initStepFinished() calls!
|
|
state: "Uninitialized",
|
|
readyCallback: null,
|
|
stdout_buf: "", stderr_buf: "", // for our default Perl.output implementation
|
|
};
|
|
|
|
/* TODO: Embedded script should be able to influence the running of Perl,
|
|
* the cleanest would probably be to set properties on the Perl object,
|
|
* such as Perl.autorun = false or Perl.argv = [...]. */
|
|
|
|
window.addEventListener("load", function () {
|
|
// Note: to get the content of script tags with jQuery: $('script[type="text/perl"]').html()
|
|
var scripts = [];
|
|
var script_src;
|
|
document.querySelectorAll("script[type='text/perl']")
|
|
.forEach(function (el) {
|
|
if (el.src) {
|
|
if (script_src || scripts.length)
|
|
console.error('Only a single Perl script may be loaded via "script src=", ignoring others');
|
|
else
|
|
script_src = el.src;
|
|
}
|
|
else {
|
|
if (script_src)
|
|
console.error('Only a single Perl script may be loaded via "script src=", ignoring others');
|
|
else
|
|
scripts.push(el.innerHTML);
|
|
}
|
|
});
|
|
if (script_src) {
|
|
console.debug("Perl: Found a script with src, fetching and running...", script_src);
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.addEventListener("load", function () {
|
|
//TODO Later: Might be nice to name the script in the virtual FS after the URL instead of a generic name
|
|
Perl._saveAndRun( this.responseText );
|
|
});
|
|
xhr.open("GET", script_src);
|
|
xhr.send();
|
|
}
|
|
else if (scripts.length) {
|
|
console.debug("Perl: Found",scripts.length,"embedded script(s), autorunning...");
|
|
var code = scripts.join(";\n");
|
|
|
|
// get the first five lines of code
|
|
var n = 5 + 1; // the contents of the <script> tag will usually begin with a newline
|
|
var i = -1;
|
|
while (n-- && i++ < code.length) {
|
|
i = code.indexOf("\n", i);
|
|
if (i < 0) break;
|
|
}
|
|
var head = i<0 ? code : code.substring(0,i);
|
|
// look for a "use WebPerl"
|
|
const regex = /^\s*use\s+WebPerl(\s|;)/m;
|
|
if (!regex.exec(head)) { // load WebPerl unless the user loaded it
|
|
console.debug("Perl: Autoloading WebPerl");
|
|
code = "use WebPerl 'js';\n" + code;
|
|
}
|
|
|
|
Perl._saveAndRun(code);
|
|
}
|
|
else console.debug("Perl: No embedded scripts");
|
|
});
|
|
Perl._saveAndRun = function (script) {
|
|
Perl.init(function () {
|
|
var file = "/tmp/scripts.pl";
|
|
try {
|
|
FS.writeFile( file, script );
|
|
console.debug("Perl: Saved script(s) to ",file,", now running");
|
|
window.addEventListener("beforeunload", function () {
|
|
// not really needed because we're unloading anyway, but for good measure, end Perl...
|
|
console.debug("Perl: beforeunload, ending...");
|
|
Perl.end();
|
|
});
|
|
// run Perl async so that the window has a chance to refresh
|
|
window.setTimeout(function () { Perl.start( [ file ] ); }, 1);
|
|
}
|
|
catch (err) { console.error("Perl:",err); alert("Save to "+file+"Failed: "+err); }
|
|
});
|
|
};
|
|
|
|
Perl.changeState = function (newState) {
|
|
var oldState = Perl.state;
|
|
Perl.state = newState;
|
|
if (Perl.stateChanged) Perl.stateChanged(oldState,newState);
|
|
};
|
|
Perl.stateChanged = function (from,to) { //TODO: allow multiple listeners
|
|
console.debug("Perl: state changed from "+from+" to "+to);
|
|
};
|
|
|
|
// chan: 1=STDOUT, 2=STDERR
|
|
// implementations are free to ignore the "chan" argument if they want to merge the two streams
|
|
Perl.output = function (str,chan) { // can be overridden by the user
|
|
var buf = chan==2 ? 'stderr_buf' : 'stdout_buf';
|
|
Perl[buf] += str;
|
|
var pos = Perl[buf].indexOf("\n");
|
|
while (pos>-1) {
|
|
console.log( chan==2?"STDERR":"STDOUT", Perl[buf].slice(0,pos) );
|
|
Perl[buf] = Perl[buf].slice(pos+1);
|
|
pos = Perl[buf].indexOf("\n");
|
|
}
|
|
};
|
|
Perl.outputLine = function (chan,text) { // internal function
|
|
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
|
Perl.output(text,chan);
|
|
Perl.output("\n",chan);
|
|
};
|
|
Perl.outputChar = function (chan,c) { // internal function
|
|
Perl.output(String.fromCharCode(c),chan);
|
|
};
|
|
|
|
Perl.makeOutputTextarea = function (id) {
|
|
var ta = document.createElement('textarea');
|
|
if (id) ta.id = id;
|
|
ta.rows = 24; ta.cols = 80;
|
|
ta.setAttribute("readonly",true);
|
|
Perl.output = function (str) {
|
|
ta.value = ta.value + str;
|
|
ta.scrollTop = ta.scrollHeight;
|
|
};
|
|
return ta;
|
|
};
|
|
|
|
var getScriptURL = (function() { // with thanks to https://stackoverflow.com/a/2976714
|
|
var scripts = document.getElementsByTagName('script');
|
|
var index = scripts.length - 1;
|
|
var myScript = scripts[index];
|
|
return function() { return myScript.src; };
|
|
})();
|
|
|
|
Perl.init = function (readyCallback) {
|
|
if (Perl.state != "Uninitialized")
|
|
throw "Perl: can't call init in state "+Perl.state;
|
|
Perl.changeState("Initializing");
|
|
var baseurl = new URL(getScriptURL());
|
|
if (baseurl.protocol=='file:')
|
|
// Note that a lot of things still won't work for file:// URLs
|
|
// because of the Same-Origin Policy.
|
|
// see e.g. https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSRequestNotHttp
|
|
baseurl = baseurl.href.substring(0,baseurl.href.lastIndexOf('/'));
|
|
else
|
|
baseurl = baseurl.origin + baseurl.pathname.substring(0,baseurl.pathname.lastIndexOf('/'));
|
|
Perl.readyCallback = readyCallback;
|
|
Module = {
|
|
noInitialRun: true,
|
|
noExitRuntime: true,
|
|
print: Perl.outputLine.bind(null,1), printErr: Perl.outputLine.bind(null,2),
|
|
stdout: Perl.outputChar.bind(null,1), stderr: Perl.outputChar.bind(null,2),
|
|
stdin: function () { return null },
|
|
arguments: ['--version'],
|
|
onAbort: function () {
|
|
Perl.changeState("Ended");
|
|
console.error("Perl: Aborted (state "+Perl.state+")");
|
|
alert("Perl aborted");
|
|
},
|
|
onExit: function (status) { // note this may be called multiple times
|
|
Perl.changeState("Ended");
|
|
if (status==0)
|
|
console.info( "Perl: Exit status "+status+" (state "+Perl.state+")");
|
|
else {
|
|
console.error("Perl: Exit status "+status+" (state "+Perl.state+")");
|
|
alert("Perl exited with exit status "+status+" in state "+Perl.state);
|
|
}
|
|
},
|
|
onRuntimeInitialized: function () {
|
|
console.debug("Perl: Module.onRuntimeInitialized");
|
|
Perl.initStepFinished();
|
|
},
|
|
preRun: [
|
|
function () {
|
|
console.debug("Perl: doing preRun, fetching IndexDB filesystem...");
|
|
try { FS.mkdir('/mnt'); } catch(e) {}
|
|
try { FS.mkdir('/mnt/idb'); } catch(e) {}
|
|
FS.mount(IDBFS, {} ,'/mnt/idb');
|
|
FS.syncfs(true, function (err) {
|
|
if (err) { console.error("Perl:",err); alert("Perl: Loading IDBFS failed: "+err); return; }
|
|
console.debug("Perl: IndexDB filesystem ready");
|
|
Perl.initStepFinished();
|
|
});
|
|
}
|
|
],
|
|
locateFile: function (file) {
|
|
var wasmRe = /\.(wast|wasm|asm\.js|data)$/;
|
|
if (wasmRe.exec(file))
|
|
return baseurl+"/"+file;
|
|
return file;
|
|
},
|
|
};
|
|
if (Perl.endAfterMain) {
|
|
Module.preRun.push(function () {
|
|
// patch _main so that afterwards we call emperl_end_perl
|
|
var origMain = Module._main;
|
|
Module._main = function() {
|
|
origMain.apply(this, arguments);
|
|
console.debug("Perl: main() has ended, ending perl...");
|
|
var status = ccall("emperl_end_perl","number",[],[]);
|
|
if (status==0) {
|
|
// we know that in this case, there is no event thrown to us (since exit() isn't called)
|
|
// so we have to transition states manually
|
|
Module.onExit(status);
|
|
}
|
|
return status;
|
|
};
|
|
});
|
|
}
|
|
console.debug("Perl: Fetching Emscripten/Perl...");
|
|
var script = document.createElement('script');
|
|
script.async = true; script.defer = true;
|
|
script.src = baseurl+"/emperl.js";
|
|
document.getElementsByTagName('head')[0].appendChild(script);
|
|
};
|
|
|
|
Perl.initStepFinished = function () {
|
|
if (Perl.state!="Initializing" || Perl.initStepsLeft<1)
|
|
throw "Perl: internal error: can't call initStepFinished in state "+Perl.state+" ("+Perl.initStepsLeft+")";
|
|
if (--Perl.initStepsLeft) {
|
|
console.debug("Perl: One init step done, but "+Perl.initStepsLeft+" steps left, waiting...");
|
|
return;
|
|
} else console.debug("Perl: All init steps done, doing final initialization...");
|
|
|
|
/* NOTE: Apparently, when NO_EXIT_RUNTIME is set, and exit() is called from the main program
|
|
* (from Module.callMain), Emscripten doesn't report this back to us via an ExitStatus exception
|
|
* like it does from ccall - it doesn't even call the addOnExit/ATEXIT or addOnPostRun/ATPOSTRUN
|
|
* handlers! So at the moment, the only reliable way I've found to catch the program exit()ing
|
|
* is to patch into Emscripten's (undocumented!) Module.quit... */
|
|
var origQuit = Module.quit;
|
|
Module.quit = function (status, exception) {
|
|
console.debug("Perl: quit with",exception);
|
|
Module.onExit(status);
|
|
origQuit(status,exception);
|
|
}
|
|
|
|
Perl.changeState("Ready");
|
|
if (Perl.readyCallback) Perl.readyCallback();
|
|
Perl.readyCallback = null;
|
|
};
|
|
|
|
Perl.start = function (argv) {
|
|
if (Perl.state!="Ready")
|
|
throw "Perl: can't call start in state "+Perl.state;
|
|
Perl.changeState("Running");
|
|
try {
|
|
// Note: currently callMain doesn't seem to throw ExitStatus exceptions, see discussion in Perl.initStepFinished
|
|
Module.callMain(argv ? argv : Module.arguments);
|
|
}
|
|
catch (e) {
|
|
if (e instanceof ExitStatus) {
|
|
console.debug("Perl: start:",e);
|
|
Module.onExit(e.status);
|
|
} else throw e;
|
|
}
|
|
};
|
|
|
|
Perl.eval = function (code) {
|
|
if (Perl.state!="Running")
|
|
throw "Perl: can't call eval in state "+Perl.state;
|
|
if (Perl.trace) console.debug('Perl: ccall webperl_eval_perl',code);
|
|
try {
|
|
return ccall("webperl_eval_perl","string",["string"],[code]);
|
|
}
|
|
catch (e) {
|
|
if (e instanceof ExitStatus) {
|
|
// the code caused perl to (try to) exit - now we need to call
|
|
// Perl's global destruction via our emperl_end_perl() function
|
|
Perl.end(); //TODO: Perl.end has already been called at this point (how?)
|
|
} else throw e;
|
|
}
|
|
};
|
|
|
|
/* Note that Emscripten apparently doesn't support re-running the program once it exits (?).
|
|
* So at the moment, once we end Perl, that's it. The only useful effect of ending Perl is
|
|
* that global destruction is executed and END blocks are called. But since a user may leave
|
|
* a webpage at any moment without warning, WebPerl scripts should not rely on normal termination! */
|
|
Perl.end = function () {
|
|
if (Perl.state!="Running") {
|
|
if (Perl.state=="Ended") {
|
|
console.warn("Perl: end called when already Ended");
|
|
return;
|
|
}
|
|
else throw "Perl: can't call end in state "+Perl.state;
|
|
}
|
|
else Perl.changeState("Ended");
|
|
try {
|
|
ccall("emperl_end_perl","number",[],[]);
|
|
}
|
|
catch (e) {
|
|
if (e instanceof ExitStatus) {
|
|
console.debug("Perl: end: ",e);
|
|
Module.onExit(e.status);
|
|
} else throw e;
|
|
}
|
|
};
|
|
|
|
Perl.next_glue_id = 0;
|
|
Perl.GlueTable = {};
|
|
Perl._glue_free_ids = {};
|
|
Perl.glue = function (ref) {
|
|
var id;
|
|
var free_ids = Object.keys(Perl._glue_free_ids);
|
|
if (free_ids.length>0) {
|
|
id = free_ids[0];
|
|
delete Perl._glue_free_ids[id];
|
|
if (Perl.trace) console.debug('Perl: Glue reused id',id,'to',ref);
|
|
}
|
|
else {
|
|
if (Perl.next_glue_id>=Number.MAX_SAFE_INTEGER)
|
|
throw "Perl.GlueTable is overflowing!"
|
|
id = ''+(Perl.next_glue_id++);
|
|
if (Perl.trace) console.debug('Perl: Glue id',id,'to',ref);
|
|
}
|
|
Perl.GlueTable[id] = ref;
|
|
return id;
|
|
}
|
|
Perl.unglue = function (id) {
|
|
if (Perl.trace) console.debug('Perl: Unglue id',id,'from',Perl.GlueTable[id]);
|
|
delete Perl.GlueTable[id];
|
|
Perl._glue_free_ids[id]=1;
|
|
}
|