blog.herby.sk

Sus scrofa f. sapiens
writebacks
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.

writebacks...

  • dvbportal wrote
    Very nice. I feel, I have to try it out myself.
    I see, you use your own storage identifiers. I didn't know that it is possible to do so. However, it leads to predictable storage access and a possible security problem when used in the public. But for now it doesn't matter. I am curious to see what comes next.
  • herby wrote
    As soon as I know an id, it's the problem in any shared storage solution that is not properly virtualized. For example Vezquex's upload app created urls based on ids. So if I see the url, i can simply gain access to the object and do whatever I see fit with it. It is false sense of security based on the fact the "ids are not publicly known".
    And proper virtualization (translating any id on the way there in the arguments and on the way back in the return values by overriding every low-level storage access method) solves this, cleanly.
    There is another, very nasty problem with constructed ids - whenever I recreate the object with same id, garbage collecting of data goes mad and the object disappears "randomly" after some time. With random ids, this is not an issue, every id is created only once, and later eventually garbage-collected. So I probably move away from constructed ids, or somehow make sure it is not created more times.
    Well, I thought of what should come next. I am probably hitting "microkernel versus monolithic" dilemma here. appjet._boot does too much things which are not needed in every case (in most cases writing a file is not needed functionality). In general, I'm in favour of microkernel, so maybe boot.js will get much leaner and some things moved to lib, but then I lose the simplicity of "one strong base" and the number of files absolutely necessary to deploy will grow.
    And of course, properly running multiple applications and making editor / saving code ready for writing also another code sections, especially library.
  • So I'm a little confused by the last two words above "especially library". Does that mean that your current code does not allow an application to import a library? Or does it mean that it doesn't look for a library code section and if that's the case does that also mean that an existing appjet library wouldn't work?
  • Herby wrote
    Te: "especially library"
    It it still evolving, very basic platform, so it only allows edit applications, no libraries (even if you call them lib-..., name does not matter, even if original Appjet or jgate). What matters id presence of /* appjet:library */ section, and no other sections except /* appjet:server */ is actually saved.
    So it's matter of some more exolution to enhance editor to accept all the rest of code sectiond.
  • Herby wrote
    typo
    it should have been "Re:" in the title :-(
  • Herby wrote
    another typo
    "even if" should have been "even in"

trackback

TrackBack ping me at:
http://blog.herby.sk/blosxom/Programming/AppJet platform clone/phase-3.trackback

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.

writebacks...

  • dvbportal wrote
    Top notch. It's getting better and better. I like the idea of the rescue call. I think, I need something similar. :-)
    One note on the Ruby webservice though. I like Ruby, but isn't the prerequisite of Ruby a hurdle for users of your platform code? I guess, most Windows users don't have it pre-installed. Anyway, from the architectural point of view it is irrelevant, what language is used for the webservice.
  • herby wrote
    Exactly... that's so simple a webservice, that it can be rewritten for use on the particular environment in any language.
    I plan to make it full-JS by using v8cgi instead of ruby ;-)

trackback

TrackBack ping me at:
http://blog.herby.sk/blosxom/Programming/AppJet platform clone/phase-2.trackback

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.

writebacks...

  • dvbportal wrote
    Wow, interesting idea.
    If I see it right, this method alone does not provide virtualized storage space, does it?
  • herby wrote
    No, this alone provides nothing except this is the seed from which it can grow.
    The second version will be able to grow, and it can really be used alone to start with and slowly step by step grow into full platform by itself. It is like Squeak which got crude first version using external complier, and from then on it evolved only using itself.

trackback

TrackBack ping me at:
http://blog.herby.sk/blosxom/Programming/AppJet platform clone/phase-1.trackback

comment...

Name:
URL/Email: [http://... or mailto:you@wherever] (optional)
Title: (optional)
Comments:
Checkcode:
CAPTCHA
Save my Name and URL/Email for next time