blog.herby.sk

Sus scrofa f. sapiens
07 2009

Categories

Archives

26 July 2009
Sunday

More editor functionality

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.

  1. There is verbatim copy of the big inline object in the code of configure.js, as a living code, not as a string.
  2. Then, due to structure of boot.js, its complete source is prepared in a few lines using reflexion on this live instance.
  3. 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 spartan miniplatform

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

A living thing

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.