I tend to tease myself a bit about the "useless demos" I like to build, but almost consistently I end up learning something new. It may not be an earth shattering realization of something incredibly deep, but generally, if I learn something, and if I can share it, I consider it a win. Case in point - running BASIC programs in a serverless environment.
I have quite the soft spot in my heart for BASIC. I learned to code with Applesoft BASIC on a 2e (or 2+, not sure now) and I can still remember the joy of getting my first program to run. (After an incredibly stupid error that happened because I didn't read the docs. Thankfully that never occurred again.) I recently came across a great little on line BASIC interpreter at http://calormen.com/jsbasic/ and when I noticed it was open source, I thought it would be cool to get this up and running in OpenWhisk.
Now - let me be clear. This is a bad idea for (at least) three reasons.
- The code is already 100% client-side. If my use-case is a client-side application, then putting it on OpenWhisk doesn't gain me anything. In fact, it slows things down as my code would have to make a HTTP call to the server to run the code.
- BASIC is an interactive language. It can prompt you for input which doesn't necessarily make sense in a "run and return the output" context.
- Finally, Applesoft BASIC in particular has a graphics mode. (Two actually.) In theory I could setup OpenWhisk to return images (and it would be fun to get that working), it doesn't necessarily make sense for my demo.
Of course, why should I let that stop me? I began by working on the action code. I ran into a problem right away as the documentation for the library is a bit lacking. But when I filed a bug report on it I got a response very quickly. The biggest issue is that I have to tell the library what to do on input and output. For input, I do nothing (I'm just not going to support program input) and for output, I just store it up into a string. Here is the action I built:
const basic = require('./basic').basic;
function main(args) {
let result = '';
let program = basic.compile(args.input);
program.init({
tty: {
getCursorPosition: function() { return { x: 0, y: 0 }; },
setCursorPosition: function() { },
getScreenSize: function() { return { width: 80, height: 24 }; },
writeChar: function(ch) {
//console.log('writeChar called with: '+ch);
result += ch;
},
writeString: function(string) {
//console.log('writeString called with: '+string);
result += string+'\n';
},
readChar: function(callback) {
//callback(host.console.getc());
callback('');
},
readLine: function(callback, prompt) {
//host.console.puts(prompt);
//callback(host.console.gets().replace(/[\r\n]*/, ''));
callback('');
}
}
});
driver = function() {
var state;
do {
try {
state = program.step(driver);
} catch(e) {
console.log('ERROR!');
return {
error:e
}
}
// may throw basic.RuntimeError
} while (state === basic.STATE_RUNNING);
}
driver(); // step until done or blocked
return {result:result};
}
exports.main = main;
Basically I initialize the code with a string input (the BASIC code) and then "run" the program via the driver
function until it is complete. This will totally fail if you write code expecting input, or if you use graphics modes, but it lets basic stuff work just fine. (Wow, I'm typing "basic" a lot.) Now let's look at the front end. I wrote it all in one quick file so forgive the mix of HTML, CSS, and JS.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<style>
#code {
width: 500px;
height: 300px;
}
</style>
</head>
<body>
<h1>Serverless Basic</h1>
<textarea id="code">
10 print "hello"
</textarea>
<p/>
<button id="runButton">Run</button>
<div id="result"><h2>Output</h2><pre></pre></div>
<script>
let API = 'https://openwhisk.ng.bluemix.net/api/v1/web/rcamden%40us.ibm.com_My%20Space/basic/basic.json';
let $code, $runButton, $result;
document.addEventListener('DOMContentLoaded', init, false);
function init(e) {
$code = document.querySelector('#code');
$runButton = document.querySelector('#runButton');
$result = document.querySelector('#result pre');
$runButton.addEventListener('click', runCode, false);
}
function runCode(e) {
let code = $code.value;
if(code === '') return;
console.log('code',code);
fetch(API, {
method:'POST',
body:'input='+encodeURIComponent(code),
headers:{
'Content-Type':'application/x-www-form-urlencoded; charset=utf-8'
}
}).then((res) => res.json()).then((res) => {
if(res.error) {
$result.innerHTML = "An error was thrown. Write better code.";
} else {
$result.innerHTML = res.result;
}
}).catch((err) => {
console.error('error', err);
});
}
</script>
</body>
</html>
It's a relatively simple web page. I use a text area for input (with some sample code in there already), a button to run it, and a div to display the output. Here's where I ran into two things that tripped me up.
To send data to my action, I wanted to use a POST instead of a GET. With the Fetch() API, this isn't too hard, but all the demos I saw used a FormData object. Doing this sends the data as a multipart form. From what I can tell, this is not support by OpenWhisk. To be clear, OpenWhisk ran just fine on this request, but it didn't take the form fields and automatically turn them into arguments. I could have handled that myself, but I wanted to keep the code as is.
In order to send a urlencoded fetch call, I first tried just adding the header you see above. But apparently - if you send a FormData() object, that will override the urlencoded value and keep it as a multipart post instead. So I had to manually urlencode my form post. Since it was just one value though it wasn't too hard. I'm still new at Fetch so if I missed something obvious, let me know.
If you want to run this yourself, you can do so here: https://cfjedimaster.github.io/Serverless-Examples/basic/test.html
And yes, you can write an infinite loop. I can remember doing that on machines at Sears back in the old days. (Never anything naughty of course.) OpenWhisk will automatically kill the process after 60 seconds so I'm not too concerned about you doing that, but, please, don't. ;)
Oh, and the code for the client and action may be found here: https://github.com/cfjedimaster/Serverless-Examples/tree/master/basic
Enjoy!