Categories
Archives
|
28 August 2009 Friday
As I was writing in the previous article, I had to make a decision whether to go microkernel or monolithic kernel way, and I chose the microkernel. Now it is not only a proposed code, but the platform is actually moved to be based on it.
Second big change I made, and by this decision I lost _some_ of the possibilities that were default to appjet, is the decision to merge the world of applications and libraries.
Of course these two coexisted peacefully before, but there still were /* appjet:server */ and /* appjet:library */ sections in library file. Now, the question was: keep this feature (which on the pros side has its simplicity to add testing code to the lib itself), or go another route, by throwing away all mentions of /* appjet:library */ from the developer.
To understand why this question appeared at all, one must understand how multi-app is (to be) implemented in this platform - an app is started by import("lib-something") and lib-something is built by the platform itself, just to make it possible to run the application code by calling the import.
Mechanistic approach is: for any application foo, let us create, for example, lib-!foo library, and let us prohibit "!" in regular names. That means for application fb-statuses, platform would create lib-!fb-statuses to run its server code section. For application lib-mmos-view, the platform creates lib-!lib-mmos-view to run the server code section, but it also creates lib-mmos-view, to enable importing its library section by other applications. I hope it's not too complicated.
Now the question was: what if we simply forget that applications are of two types - apps and libs, and there were only one type, but it could be utilised in two ways: either you run it, as an application, by writing http://foo.appdomain.net/, or you import it in any other application by calling import("lib-foo")? It looks like the application is there two times: once as an application, and once as a library. But if you forget app/lib dichotomy, there is just one bunch of code, which can work simultaneously as an app or as a lib.
This would greatly simplify the innards. For the app/lib foo there would only be one file - lib-foo. If you want to create an application, you create foo and then run it through url. If you want to create a library, you create bar and then import("lib-bar"). And if you can utilise both approaches, you are free to do it.
So, as everyone already guessed, I decided for this unified, one-file way. That means I cannot write the tests for a lib into its code, but no one prevents me to just create bar-tests application which runs the tests. The elegance of one-file solution is gone, but I finally let it go (partially because I do not plan to use full-file editing, anyway, but edit each code section separately; in that case I lost nothing, since there is no notion of "one file" anymore).
I restructuralized the platform to consist of boot.js (not editable anymore, only libs are), lib-admin which includes all the rescue, create and edit functionality, and the rest (which is now only two very little libs: lib-worker and lib-0). As in previous case, the platform is set up by running the server with configure.js as the main file, in two steps, with first step setting up the storage and the second step rescuing lib-admin. It's similar in this to previous configure, only it sets up a little differently structured beast:
// (c) 2009, Herbert Vojčík
// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
// ====== variables to configure ======
var pwd = "undefined";
var domain = "appdomain.net";
// please configure the path to the save script in saveRawSource below if needed
// ====== verbatim copy of lib-admin functions ====== begin ======
function appDataSafe(app) {
try {
return getStorable("obj-! " + app);
}
catch (ex) {
return null;
}
}
function appData(app) {
var result = appDataSafe(app);
if (result) {
return result;
}
printp(request.path, ": unknown app: ", app);
response.stop();
}
function appDataForce(app) {
var result = appDataSafe(app);
if (result) {
return result;
}
appjet._native.storage_create("obj-! " + app);
result = appData(app);
storage.apps.add(result);
return result;
}
function saveRawSource(files) {
var info = {pwd:storage.pwd};
for (var p in files) {
info[p + ".js"] = files[p];
}
wpost("http://staticdomain.net/fs.rb", info);
}
function saveAppSource(name, code, doBackup) {
var appdata = appData(name);
if (doBackup) {
appdata["server.bak"] = appdata["server"];
}
var files = {};
files["lib-" + name] = "/* appjet:version 0.1 */\n" + "/* appjet:library */\n" + code;
saveRawSource(files);
appdata["server"] = code;
}
function get_main() {
print(OL(LI(link("/rescue")), LI(link("/edit")), LI(link("/create"))));
}
function get_rescue() {
var app = request.params.app;
var appdata = appData(app);
var rescue = appdata["server.bak"];
saveAppSource(app, rescue);
printp("Last known good code of ", app, " restored.", BR(), CODE(rescue));
printp(link("http://admin." + appjet.mainDomain + "/edit?app=" + app, "Go edit it!"));
}
function get_edit() {
var app = request.params.app;
var appdata = appData(app);
page.head.write("<script>\n" + " var c=" + {s:appdata.server} + ".s;\n" + " window.onload=function(){var ta=document.getElementById('code');ta.defaultValue=ta.value=c;};\n" + "<" + "/script>");
page.setTitle(app + html(" – ") + "admin editor");
printp(B(app, ".", appjet.mainDomain), BR(), B({style:"color:#0c0"}, "/* appjet:server */"), BR(), FORM({method:"post", action:request.path}, TEXTAREA({id:"code", name:"code", style:"width:100%;height:80ex"}), INPUT({type:"hidden", name:"app", value:app}), INPUT({type:"submit"}), " ", INPUT({type:"reset", value:"Undo"})));
}
function post_edit() {
var app = request.params.app;
var appdata = appData(app);
var code = request.params.code;
if (code) {
saveAppSource(app, code, true);
}
response.redirect(request.path + "?app=" + encodeURIComponent(app));
}
function get_create() {
page.setTitle("admin app creation");
print(FORM({method:"post", action:request.path}, "Supply application name:", INPUT({name:"app"}), INPUT({type:"submit"})));
}
function post_create() {
var app = request.params.app;
var appdata = appDataSafe(app);
if (appdata) {
printp("Application ", B(CODE(app)), " already exists.");
get_create();
return;
}
if (!app.match(/[a-z][a-z0-9-]+[a-z0-9]/)) {
printp("Application name must", UL(LI("be at least three characters long,"), LI("begin with smallcaps letter,"), LI("end with smallcaps letter or digit,"), LI("and contain only smallcaps letters, digits, or minus sign/dash (-) characters.")));
get_create();
return;
}
appdata = appDataForce(app);
appdata["server.bak"] = "printp(\"Hello, world!\");";
response.redirect("http://admin." + appjet.mainDomain + "/rescue?app=" + app);
}
// ====== verbatim copy of lib-admin functions ====== end ======
switch (request.path) {
case '/1':
var adminBuilder = [
"""// (c) 2009, Herbert Vojčík
// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
"""
];
[appDataSafe, appData, appDataForce, saveRawSource, saveAppSource, get_main, get_rescue, get_edit, post_edit, get_create, post_create, ].
forEach(function(f) { adminBuilder.push(f); });
adminBuilder.push("""
import("storage");
appjet._internal.global = this; //dispatch fix
dispatch();""");
// ====== Storage dump, edited ====== begin ======
(function () {
var _an = appjet._native;
_an.storage_create("obj-! 0");
_an.storage_create("obj-M1OtYQoF");
_an.storage_create("obj-! admin");
_an.storage_create("obj-! worker");
_an.storage_coll_create("coll-LyqehQFl7");
_an.storage_put("obj-! 0", "server.bak", "string",
"""// (c) 2009, Herbert Vojčík
// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
if (request.headers.Host === "admin."+appjet.mainDomain) {
appjet.appName = "admin";
appjet._internal.queue.unshift("lib-admin");
} else {
import("lib-worker");
}""");
_an.storage_put("obj-root", "pwd", "string", pwd);
_an.storage_put("obj-root", "bootTable", "object", "obj-M1OtYQoF");
_an.storage_put("obj-root", "apps", "object", "coll-LyqehQFl7");
_an.storage_put("obj-M1OtYQoF", "admin."+domain, "string", "admin,,lib-admin");
_an.storage_put("obj-! admin", "server.bak", "string", adminBuilder.join(""));
_an.storage_put("obj-! worker", "server.bak", "string", "printp(\"Hello, world!\");");
_an.storage_coll_add("coll-LyqehQFl7", "obj-! worker");
_an.storage_coll_add("coll-LyqehQFl7", "obj-! admin");
_an.storage_coll_add("coll-LyqehQFl7", "obj-! 0");
})();
// ====== Storage dump, edited ====== end ======
printp("Storage configured. Backup version of admin, 0 and worker apps included; you may restore them afterwards using admin."+domain+"/rescue.");
printp("MD5 of password is:", BR(), md5(pwd), BR(),
"Please update the hash in the filesaving script and proceed to ", link("/2", "step 2"), " to restore lib-admin.js.");
break;
case '/2':
import("storage");
request.params.app="admin";
get_rescue();
printp("admin.js restored.");
printp("You may stop running configure.js. ",
"As the next step, set up your domain ("+domain+") in the last line of boot.js. ",
"Then start appjet with boot.js as the main file.");
break;
default:
printp(H1("Platform configuration"));
printp(BIG(link("/1", "Start the wizard")));
break;
}
Moving from old boot.js to microkernel and lib-admin enabled one thing - the cumbersome use of appjet._native.storage_xxx is no longer needed and lib-admin (being an app, a special one, but an app) can simply import("storage") (the reason why appjet._boot in previous boot.js could not import("storage") as well lies in the security, to be able to virtualize storage it must not be imported and used by the platform itself, but the user must be the one who imports it as the first - otherwise storage.js caching permits access to unwanted parts of the storage. I'll write more of this when I actually do the storage virtualization).
Second thing, which I already mentioned in "Microkernel" post, is that normal apps are run straight, without loading and compiling admin parts, like the filesaving. It is needed only by lib-admin, so only there it is.
Of course, fs.rb is also part of the distribution, but it was not changed at all, so I do not copy it again. Last, but not least, the configure.js does _not_ write down boot.js (it writes down lib-admin), so boot.js is also part of the distribution, as the third file:
/* appjet:version 0.1 */
// (c) 2009, Herbert Vojčík
// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
(function(amd) {
var _an = appjet._native;
var tableid = _an.storage_get("obj-root", "bootTable").value;
var appBoot = _an.storage_get(tableid, request.headers.Host).value;
var name, queue = appjet._internal.queue = appBoot ? appBoot.split(",") : ["0", "", "lib-0"];
appjet.appName = queue.shift();
appjet.mainDomain = queue.shift() || amd;
while((queue = appjet._internal.queue) && (name = queue.shift())) {
var agent = import({}, name);
if (typeof agent.runAgent === "function") { agent.runAgent(queue); }
}
})("appdomain.net");
You must manually setup your domain in boot.js, as configure.js tells you. And that's it. (Oh yes, and you should rescue the 0 and the worker). The setup process is not as nice as previous one - you must configure your domain in two places. I think I'll fix it for the next phase, and configure.js will write boot.js down as well.
As you may have noticed, the most precious part of the platform is now lib-admin, much like boot.js was before. It contains all the rescue, create and edit functionality (as yet). That's why there is a "safety belt" in lib-0 (as you can see from its code in configure.js) - if the standard, straight running of admin.appdomain.net url stops working for some reason, and the fallback lib-0 is imported instead, it tests for admin specifically and schedules it to run anyway, if the url matches.
What I plan to do next: first, get rid of the "obj-! "+name constructed storage ids, they are dangerous to use. There is a bug in storage implementation, and if you recreate existing id, it disappears after some random time, and keeps disappearing if created again. You must somehow export the storage, delete it, let the appjet create new one and import data back.
Next, the fixes to configure to be able to setup domain ar one place only.
Then there are more challenges, but I will probably set as a medium sized goal the ability to actually make the platform safely public, so multiapp, some kind of security and a little better editor (as a standalone app, not in lib-admin, it's an "emergency" editor, in fact, there must be one, but I don't plan to develop it further, it does its work decently).
ps. Yes, and I hope to create solid base soon (by solid base I mean such a "distribution" (that is, configure.js), which will not have to be changed any more, from which working base of a platform can be set up; and any future version of the platform could be built form this base by just adding new applications, changing existing ones (admin, 0, worker) or using admin consoie. In other words, so every future version can be defined just as The Base + bunch of sourcecode to add to it via editor(s) / to run in admin console). Even this one looked promising, but not yet. The next one, hopefully.
pps. Yes, and one more little thing. :-/ I will need to have to change the filesaving service. I realized I not only need to write files, but also to delete them, sometimes... I will do it simply - if I want to delete the file, I just ask the filesaving service to write nothing to it (truncate it to empty file), and use this as a protocol for "please, delete the file").
20 August 2009 Thursday
I mentioned in the comments that I probably have to solve the microkernel vs. monolithic kernel dilemma here, or, more precisely, its equivalent for the appjet platform clone. That is, to decide whether the boot.js will contain just the bare minimum for application booting (the microkernel way), in which case all other system services must be provided by another modules, or to provide the whole pack of system-related functionality in one file (the monolithic kernel way).
In present state, boot.js contained some unneeded functionality, for example, the two filesaving methods. They are needed for system-related apps, like the editor or the rescuer, but they are definitely useless for majority of real apps, if I imagine them being there (there are none, so far). I finally decided to go the microkernel way - to provide the minimum needed to run and communicate - exactly what the real microkernels do. All the rest must be provided by additional libs.
Not that I moved that way very much - I did not reworked any part of the system yet. But, I decided what the code of the microkernel will be (and this code, I very hope, stays unchanged for ever after - if you see some bugs, tell me, please):
(function() {
var _an = appjet._native;
var tableid = _an.storage_get("obj-root", "bootTable").value;
var appBoot = _an.storage_get(tableid, request.headers.Host).value;
var name, queue = appjet._internal.queue = appBoot ? appBoot.split(",") : ["0", "", "lib-0"];
appjet.appName = queue.shift();
appjet.mainDomain = queue.shift() || "appdomain.net";
while((queue = appjet._internal.queue) && (name = queue.shift())) {
var agent = import({}, name);
if (typeof agent.runAgent === "function") { agent.runAgent(queue); }
}
})();
Well, it may look complicated, but it's just a little dense. At first, it is backwards-compatible with traditional appjet way of thinking - certain host is always mapped to certain code. That will be implemented by adding a field for that host into storage.bootTable. Additionally, field for source.app.appdomain.net should be filled, with appropriate data. At the simplest case, you simply fill the app field with "appname,,lib-appname-impl" by which you say app name is "appname", main domain is the default (you must set it in the code, once), and "lib-appname-impl" should be imported; the source field can be something like "show-source,,lib-show-source" and it can then parse the host.
This may be enough and you can be happy with it. But these few lines of the code open lots of another possibilities to organize and run your code. You can more actively use the queue. For example, if you can put anything in the queue, why not to put something like "show-source,,lib-show-source,appname" into the source.appname.appdomain.net field? No parsing is then needed, you just get the app name to show source for from the queue itself.
If you begin to use queue actively inside the applications as well, you can create software agents (each lib representing one), which are able to send messages (putting the recipient into the queue, with optional arguments for him).
If you push recipient to the queue, with its optional arguments (these must be taken out by him, explicitly), then you are sending a message in a queue manner, it is appended to the back of the queue. You can also unshift instead of push - this is much more akin to unix exec() call - this message is then put in front of the queue, which means, in will be processed first, before any other ones that were there. In other view, you are replacing yourself by another agent (and if it supports identical set of arguments, you can just leave them there and unshift only its name).
But generally, unshifting behaviour is taken as inappropriate and pushing is favoured whenever possible in messaging agents paradigm. There must be a serious reason for unshift.
Another thing you can actively use, is runAgent function. If you define the function in your agent (that is, lib), it will be called for every occurence of the lib in the queue, that is, for every message. This is the normal thing for reusable agent, it should have the function defined. But if you choose not to include it, you have a single-action agent - it only processes first message, all other times, just nothing happens (courtesy of appjet lib caching, such agent should be parameterless, obviously). So the "microkernel" really passes messages between modules, in a way (the real microkernel does basically this and nothing more).
And again, you can just write old-fashioned apps and ignore all this - it will work without hassle.
ps. The code in this article is licensed by MIT license, (c) 2009 Herbert Vojčík
26 July 2009 Sunday
Now I'm going to scare you ;-) I got over to another important milestone: multiple applications, possibility to create new applications and editor able to edit any of the applications (boot, and the editor, of course, are also applications, so you can edit them while they run). Everything was done inside the platform, without external action. Inside configure.js I also reused the trick I used before the multiapp editor and so the files needed to deploy are only two. First of them is the filesaving script fs.rb, which wasn't changed at all, the second is large configure.js. Here it is:
// ====== variables to configure ======
var pwd = "undefined";
var domain = "appdomain.net";
// please configure the path to save script in saveRawSource below if needed
// ====== verbatim copy of appjet._boot definition from boot.js ====== begin ======
appjet._boot = {
run:
function (amd) {
appjet.mainDomain = amd;
if (request.headers.Host == "rescue." + amd) {
var app = request.params.app;
var appdata = new this.storable("obj-! " + app);
if (appdata.doesNotExist) {
printp("rescue: unknown app: ", app);
response.stop();
}
var rescue = appdata.getString("server.bak");
this.saveAppSource(app, rescue);
printp("Last known good code of ", app, " restored.", BR(), CODE(rescue));
printp(link("http://edit."+amd+"/?app="+app, "Go edit it!"));
response.stop();
} else if (request.headers.Host == "edit." + amd) {
import("lib-! edit");
} else if (request.headers.Host == "create." + amd) {
import("lib-! create");
} else {
import("lib-! worker");
}
}
,
saveRawSource:
function (files) {
var info = {pwd:new this.storable("obj-root").getString("pwd")};
for (var p in files) {
info[p + ".js"] = files[p];
}
wpost("http://staticdomain.net/fs.rb", info);
}
,
storable:
function (id) {
if (appjet._native.storage_getById(id).status != 0) {
return {doesNotExist:true};
}
this.getString = function (key) {
return appjet._native.storage_get(id, key).value;
};
this.putString = function (key, value) {
return appjet._native.storage_put(id, key, "string", value).status == 0;
};
}
,
saveAppSource:
function (name, code, doBackup) {
var appdata = new this.storable("obj-! " + name);
if (doBackup) {
appdata.putString("server.bak", appdata.getString("server"));
}
var files = {};
files["lib-! " + name] = "/* appjet:version 0.1 */\n" + "/* appjet:library */\n" + code;
files[name] = "/* appjet:version 0.1 */\n" + code;
this.saveRawSource(files);
appdata.putString("server", code);
}
,
};
// ====== verbatim copy of appjet._boot definition from boot.js ====== end ======
switch (request.path) {
case '/1':
var bootBuilder = ["// (c) 2009, Herbert Voj\u010d\xedk"+"""
// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
(appjet._boot = {
"""];
eachProperty(appjet._boot, function(name, f) {
bootBuilder.push(name, ": ", f, ",\n\n");
});
bootBuilder.push("}).run(\"", domain, "\");");
// ====== Storage dump, edited ====== begin ======
(function () {
var _an = appjet._native;
_an.storage_create("obj-! create");
_an.storage_create("obj-! worker");
_an.storage_create("obj-! edit");
_an.storage_create("obj-! boot");
_an.storage_coll_create("coll-LyqehQFl7");
_an.storage_put("obj-! create", "server.bak", "string", "// (c) 2009, Herbert Voj\u010d\xedk \r\n// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)\r\n\r\nimport(\"storage\");\r\n(function(app){\r\n\r\nif (!app) {\r\n return;\r\n}\r\nvar id = \"obj-! \"+app;\r\nif (appjet._native.storage_getById(id).status == 0) {\r\n printp(\"Application \", B(CODE(app)), \" already exists.\");\r\n return;\r\n}\r\nif (!app.match(/[a-z][a-z0-9-]+[a-z0-9]/)) {\r\n printp(\"Application name must\",UL(\r\n LI(\"be at least three characters long,\"),\r\n LI(\"begin with smallcaps letter,\"),\r\n LI(\"end with smallcaps letter or digit,\"),\r\n LI(\"and contain only smallcaps letters, digits, or minus sign/dash (-) characters.\")\r\n ));\r\n return;\r\n}\r\n\r\nappjet._native.storage_create(id);\r\nvar appdata = getStorable(id);\r\nappdata[\"server.bak\"] = 'printp(\"Hello, world!\");';\r\nstorage.apps.add(appdata);\r\nresponse.redirect(\"http://rescue.\"+appjet.mainDomain+\"/?app=\"+app);\r\n\r\n})(request.params.app);\r\n\r\nprint(FORM(\"Supply application name:\", INPUT({name:'app'}), INPUT({type:'submit'})));\r\n");
_an.storage_put("obj-root", "pwd", "string", pwd);
_an.storage_put("obj-root", "apps", "object", "coll-LyqehQFl7");
_an.storage_put("obj-! worker", "server.bak", "string", "printp(\"Hello, world!\");");
_an.storage_put("obj-! edit", "server.bak", "string", "// (c) 2009, Herbert Voj\u010d\xedk \r\n// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php) \r\n\r\nvar app = request.params.app;\r\nvar appdata = new appjet._boot.storable(\"obj-! \"+app);\r\nif (appdata.doesNotExist) { printp(\"editor: unknown app: \", app); response.stop(); }\r\n\r\nvar _code = request.params.code;\r\nif (_code) {\r\n\tappjet._boot.saveAppSource(app, _code, true);\r\n\tresponse.redirect(request.path+\"?app=\"+encodeURIComponent(app));\r\n}\r\n\r\nprintp(\r\nB(app,\".\",appjet.mainDomain),BR(),\r\nB({style:'color:#0c0'},\"/* appjet:server */\"),BR(),\r\nFORM({method:'post', action:request.path},\r\n\tTEXTAREA({name:'code', style:'width:100%;height:80ex'},\r\n\t\thtml(appdata.getString(\"server\"))\r\n\t),\r\n\tINPUT({type:'hidden', name:'app', value:app}),\r\n\tINPUT({type:'submit'})\r\n));\r\n");
_an.storage_put("obj-! boot", "server.bak", "string", bootBuilder.join(""));
_an.storage_coll_add("coll-LyqehQFl7", "obj-! worker");
_an.storage_coll_add("coll-LyqehQFl7", "obj-! edit");
_an.storage_coll_add("coll-LyqehQFl7", "obj-! boot");
_an.storage_coll_add("coll-LyqehQFl7", "obj-! create");
})();
// ====== Storage dump, edited ====== end ======
printp("Storage configured. Backup version of boot, create, edit and worker apps included; you may restore them afterwards using rescue."+domain+".");
printp("MD5 of password is:", BR(), md5(pwd), BR(),
"Please update the hash in the filesaving script and proceed to ", link("/2", "step 2"), " to restore boot.js.");
break;
case '/2':
var appdata = new appjet._boot.storable("obj-! boot");
var rescue = appdata.getString("server.bak");
appjet._boot.saveAppSource("boot", rescue);
printp("boot.js restored.");
printp("You may stop running configure.js and start running boot.js.");
break;
default:
printp(H1("Platform configuration"));
printp(BIG(link("/1", "Start the wizard")));
break;
}
Bulk data for the configure.js were prepared by the platform itself (the code of the appjet._boot as well as storage dump were presented by the platform and were only copypasted into configure.js; the storage dump was edited to be minimal needed). The structure of boot.js being
(appjet._boot={
... big inline object ..
... where all functionality resides ...
}).run("appdomain.net");
allowed for a cool metacircularity, which is present in configure.js as well. - There is verbatim copy of the big inline object in the code of configure.js, as a living code, not as a string.
- Then, due to structure of boot.js, its complete source is prepared in a few lines using reflexion on this live instance.
- Then, later, this live instance is used to write its own code that was created from it into the boot.js file.
I simply like it.
Configure now has three pages, path / leads to welcome screen with link to /1 which fills the storage and emits the password hash, but does not save anything (the hash may need to be updated, the saves would not work anyway). From there link goes to path /2 which then simply rescues (using the live instance of appjet._boot) the boot application, thus creating boot.js file.
After successful configuring, there are no applications except integrated rescue, but it can be used to unleash the rest, which is only present as backup in storage: edit, create, worker.
Platform now recognizes multiple urls. - rescue.appdomain.net/?app=xxx rescues application xxx from backup (every app has now its own backup).
- edit.appdomain.net/?app=xxx edits application xxx. Application "edit" must be present, rescue it first.
- create.appdomain.net/ creates new application. Application "create" must be present, rescue it first.
- anythingelse.appdomain.net/ runs the application "worker" (properly parsing hostname was postponed for later, multiapp editor was more urgent). Of course, the worker also must be rescued first, or you get an error.
I also think that now my idea of using import instead of eval is much more clearly visible, in the appjet._boot; in the way how saveAppSource saves the sourcecode and how run runs multiple applications.
To finish it, one of the cutest things is this: running rescue.appdomain.net/?app=boot is probably doomed. If you screwed your boot.js, it probably won't be working to rescue itself. No problem, stop the server (it's useless with broken boot.js anyway), start it with configure.js and run just the second step... and it wasn't planned, it just came out this way.
22 July 2009 Wednesday
The previous one was much too stiff, but now I have a real minimal platform ready, based on ideas I was sketching out before. It consists of three files. First, called configure.js (I've got it in the main dir of the platform), is used to set up the initial image (read: storage). The server should be run with it as the main file and (at least) one page requested.
import("storage");
storage.pwd = "undefined";
storage["worker.js.bak"] =
"""var _code = request.params.code;
if (_code) {
appjet._boot.saveWorkerSource(_code);
response.redirect(request.path);
}
print(FORM({method:'post'},
TEXTAREA({name:'code'},
html(appjet._boot.workerCode)
),
INPUT({type:'submit'})
));
""";
printp("Configured. MD5 of password is:", BR(), md5(storage.pwd));
Before running it, you may fill the password (it's used to authenticate to file saving plugin, so only you can save files, not any malicious app calling filesaving webservice). It creates the initial storage and prints the hash of a password for you. Then, there is, slightly changed, filesaving local webservice, fs.rb (I've got it in www subdirectory, where static files are served from and ruby scripts can be called as well):
#!/usr/local/bin/ruby
require "cgi"
require "digest/md5"
cgi = CGI.new
hashpwd = Digest::MD5.hexdigest(cgi["pwd"]);
cgi.params.delete("pwd");
if ("5e543256c480ac577d30f76f9120eb74" != hashpwd) then
raise "password does not match";
end
cgi.params.each do |name, code|
File.open("../apps/"+name, "wb") do |f| f.syswrite(code) end
end
cgi.out do "" end
If you edited password in configure.js, edit also its hash in this file. And last, but not least, is the bootstrapper, boot.js (I've got it in apps subdirectory, where all the apps and libs in the platform should reside). A bit longer than the previous one:
(appjet._boot = {
rawSaveSource: function (file, code) {
var info = { pwd: this.getString("pwd") };
info[file+".js"] = code;
wpost("http://staticdomain.net/fs.rb", info);
},
saveWorkerSource: function (code) {
if (this.workerCode) {
this.putString("worker.js.bak", this.workerCode);
}
this.rawSaveSource("lib-! worker",
"/* appjet:version 0.1 */\n" +
"/* appjet:library */\n" + code
);
this.putString("worker.js", code);
},
getString: function (key) {
return appjet._native.storage_get("obj-root", key).value;
},
putString: function (key, value) {
return appjet._native.storage_put("obj-root", key, "string", value).status == 0;
},
run: function (amd) {
appjet.mainDomain = amd;
if (request.headers.Host == "rescue."+amd) {
var rescue = this.getString("worker.js.bak");
this.saveWorkerSource(rescue);
printp("Last known good code restored.", BR(), CODE(rescue));
response.stop();
}
this.workerCode = this.getString("worker.js");
import("lib-! worker");
},
}).run("appdomain.net");
You should edit url to fs.rb (top) and the domain of your platform itself (bottom), then run the server with -c apps boot.js. First page you should request is rescue.appdomain.net. The rescue function restores last code that worked. If you enter code that breaks, simply load rescue.appdomain.net again. The miniplatform itself resides in any other subdomain (like foo.appdomain.net). It starts with really spartan interface, the lone TEXTAREA tag with worker code to edit and the submit button. You can immediately begin to improve and evoive your platform (first change I did was adding style:'width:100%;height:50ex' to the textarea, so I can see what I edit). And it is a platform - it really saves your changes into a file and allows you to save another files beyond the worker (including boot.js itself, when you're ready) using appjet._boot.rawSaveSource call.
11 July 2009 Saturday
I started making my own Appjet clone as well, though it is not public yet (it is on friend's server where I began squatting). Very quickly I got to the "reflexive", "eat you own dog food way". That is, to design the clone not for performance or "business value", but, more academically, so that as much as possible is done inside the platform itself and as low as possible is left for some sort of external support. For imports to work (and for applications as well, since I tried to go the no-eval way and use import instead), the files must be stored on disk. This is the thing that AppJet itself cannot do, so this platform needs supports in this aspect. But, over time, I came to the conclusion that. in fact, this is the only thing where external support is needed, every other thing, from innards of multiuser platform with virtualized storage space, to the IDE, can be written inside AppJet. So the files began getting smaller, the half-implemented features went away (YAGNI for the moment) and finally, I aimed at creating as small as possible bootstrap of a platform. Something very small but sort-of living and capable of evolving in itself. First thing: file saving. I decided to go webservice way: I call a service (through wpost) and it saves the files for me. It should have no other responsibility. Here's the code.
#!/usr/bin/env ruby
require "cgi"
cgi = CGI.new
File.umask(0002)
cgi.params.each do |name, code|
File.open("../apps/"+name, "wb") do |f| f.syswrite(code) end
end
cgi.out do "" end
Yes, in the future it will definitiely needs some improvements, like some sort of security, or anyone calls it to write files virtually anywhere (where the script has access, of course). Sources are in apps subdirectory, this file (fs.rb) and static web if it will be used later is in www subdirectory, in main directory I have the appjet jar. And now for the living being, Here it is.
var info = {};
info[appjet.appName] = appjet._native.codeSection("server")[0].display;
wpost("http://foo.net/fs.rb", info);
It resides in apps subdirectory and is used as the main file for running appjet.jar. Look at it. It's already living. Whenever you load a page, it keeps its own life - it writes its new copy over itself. It is not capable of more than keeping the "life", but it is good enough. This is the starting point. A few more lines, and it gains the ability to evolve. Anyway, this is phase 1. PS.: For those who don't get it. This is not the standalone application. It replicates itself - it really writes over the file it resides in, even if it is with the same content. It's new file, and if you reload the page, this new file will serve the page. This is the minimal (very uncapable) Appjet platform. PPS.: Yes, you can write eval(request.query) and get much smaller beast, but I don't want to use eval. It's a cheat (it's deus ex machina - without input it does not do anything in itself and stagnates, it's empty jar filled with eval's orders, but has no own behaviour; the above one does and it's what is wanted, plus the capability acquire new behaviour (which it lacks)). And eval has its own problems, I simply don't like it. PPPS.: Feel free to clone it and lead it along, too. If you like.
|