var clite = { state:{ isinit:false, terminal:null, input:{ type:'text', form:null, field:null } }, events:{ keyup:function(e) { try { if (e.key == 'Down' || e.key == 'ArrowDown') { clite.shell.history.down(); return false; } if (e.key == 'Up' || e.key == 'ArrowUp') { clite.shell.history.up(); return false; } var t = clite.state.input.field.value; if (e.key != 'Enter') { clite.shell.history.setCurrent(t); return true; } clite.shell.parse(t); return false; } catch(err) {return false;} }, refocus:function(e) { try { clite.term.genForm(); clite.state.input.field.focus(); } catch(err) {} } }, core:{ execSafe:function(f) { try{ f(); } catch(e) { clite.log.write(e.message); return false; } return true; }, execSafeAsync:function(f) { setTimeout(f,10); }, load:{ script:function(name,callback) { var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.src = name; script.onload = callback; head.appendChild(script); }, file:function(name,callback) { if (window.location.protocol == 'file:') { // this is a dirty hack and I hate it, just let me open a file: path! clite.term.setCustom({type:'file',callback:callback}); clite.shell.writeLine('open file: '+name); return; } fetch(name) .then(response => response.text()) .then((data) => { callback(data); }); } } }, init:function() { if (this.state.isinit) return; this.state.isinit = true; this.core.execSafeAsync(function() { clite.term.clear(false); // setup vfs clite.log.write('Setting up VFS'); clite.vfs.init(); var vfsapi = clite.vfs.getApi(); clite.log.init(vfsapi); clite.log.write('Mounting wfs on /'); // mount core (root) filesystem using data/filesys.txt clite.core.load.file('data/filesys.txt',function(data) { if (data == null) { clite.log.write('No Filesystem Found'); return; } vfsapi.mkFile('/dev/wfs'); var n = vfsapi.getNode('/dev/wfs'); if (!n) { clite.log.write('wfs device failure'); return; } n.data.content = data; n.perms = 'cr--r-----'; n.isdev = true; var fd = clite.io.open('/dev/wfs'); var l; while ((l = clite.io.readLine(fd)) != null) { if (l.length <1 || l[0] == '#') continue; var parts = l.split(':'); if (parts.length != 5) continue; var url = parts[0]; var path = parts[1]; var uid = parseInt(parts[2]); var gid = parseInt(parts[3]); var perms = parts[4]; var fn = vfsapi.getNode(path); // TODO: make the full path if needed if (!fn) { if (perms[0] == 'd') { vfsapi.mkDir(path); }else{ vfsapi.mkFile(path); } fn = vfsapi.getNode(path); } if (!fn) continue; fn.perms = perms; fn.uid = uid; fn.gid = gid; fn.name = clite.lib.basename(path); fn.data.remote = url; fn.data.content = null; } clite.io.close(fd); // populate /dev (data/filesys.txt is /dev/wfs already) clite.log.write('Populating /dev'); // TODO: are there any other devices we need? // populate /bin clite.log.write('Populating /bin'); // create a temporary function to load in commands clite.commands = { data:null, load:function(name,fn) { vfsapi.mkFile('/bin/'+name); var f = vfsapi.getNode('/bin/'+name); if (!f) return; f.perms = '-r-xr-xr-x'; f.data.content = fn; } } clite.core.load.script('clite/commands.js',function() { clite.core.execSafe(clite.commands.data); clite.commands = null; // check cookies for login clite.log.write('Checking for user session'); if (clite.user.init(vfsapi)) {// if logged in: clite.log.write('found user session?'); // create user session }else{// if new user: clite.log.write('Generating guest session'); // read in /usr/share/introduction and write to shell var fd = clite.io.open('/usr/share/introduction',function(fd) { clite.user.genGuest(); if (!fd) return; var d = clite.io.readAll(fd); if (d != null) clite.shell.writeLine(d); clite.io.close(fd); }); } }); // end command load callback }); // end filesys load callback }); } }; clite.user = { init:function(vfs) { // returns true if there's a user login or false for guest var udata = { name:'root', uid:0, group:0, groups:[] }; clite.user.getUID = function() { return udata.uid; } clite.user.getGID = function() { return udata.gid; } clite.user.checkGID = function(gid) { if (gid == udata.gid) return true; if (udata.groups.indexOf(gid) > -1) return true; return false; } function setLogin(user,uid,gid,groups) { udata.name = user; udata.uid = uid; udata.group = gid; udata.groups = groups; clite.shell.env.HOME = '/usr/home/'+user; clite.shell.env.PWD = '/usr/home/'+user; clite.shell.env.USER = user; clite.shell.prompt.generate(); clite.user.hasLogin = function() { return true; } } clite.user.genGuest = function() { clite.term.clear(); vfs.mkDir('/usr/home/guest'); var n = vfs.getNode('/usr/home/guest'); if (n) { n.perms = '-rwx------'; n.uid = 1; n.gid = 1; } setLogin('guest',1,1,[]); clite.events.refocus(); } return false; }, getUID:function() { // get the user's user id return 0; }, getGID:function() { // get the user's group id return 0; }, checkGID:function(gid) { // check if the user is in a group return true; }, hasLogin:function() { // check if we're in a user session return false; }, genGuest:null }; clite.io = { init:function() { var vfsapi = clite.vfs.getApi(); var perms = { // -rwxrwxrwx user1-3,group4-6,other7-9 checkReadable:function(p,uid,gid) { if (p[7] == 'r') return true; if (clite.user.checkGID(gid) && p[4] == 'r') return true; if (uid == clite.user.getUID() && p[1] == 'r') return true; return false; }, checkWritable:function(p,uid,gid) { if (p[8] == 'w') return true; if (clite.user.checkGID(gid) && p[5] == 'w') return true; if (uid == clite.user.getUID() && p[2] == 'w') return true; return false; }, checkExecutable:function(p,uid,gid) { if (p[9] == 'x') return true; if (clite.user.checkGID(gid) && p[6] == 'x') return true; if (uid == clite.user.getUID() && p[3] == 'x') return true; return false; } }; function getFileDes(path,link,cb) { var n = vfsapi.getNode(path); if (!n) return null; if (n.data.islink && !link) // if link is false, follow links n = vfsapi.getNode(n.data.content); var p = (n.data.remote && n.data.content == null); var fd = Object.create({ node:n, pos:0, canread:false, canwrite:false, canexec:false, remote:{ ispending:p, callback:cb } }); if (p) { clite.core.load.file(n.data.remote,function(d) { n.data.content = d; fd.remote.ispending = false; try{ fd.remote.callback(fd); } catch(err) {} }); } fd.canread = perms.checkReadable(fd.node.perms,fd.node.uid,fd.node.gid); fd.canwrite = perms.checkWritable(fd.node.perms,fd.node.uid,fd.node.gid); fd.canexec = perms.checkExecutable(fd.node.perms,fd.node.uid,fd.node.gid); if (!p && cb != null) { clite.core.execSafeAsync(function() {cb(fd);}); } return fd; } clite.io.creat = function(path,type) { switch (type) { case 'd': return vfsapi.mkDir(path); break; case 'l': return vfsapi.mkLink(path); break; default: return vfsapi.mkFile(path); } } clite.io.open = function(path,cb) { return getFileDes(path,false,cb); } clite.io.close = function(fd) { try{ fd.node = null; delete fd; } catch(err) {} } clite.io.read = function(fd) { if (fd.node.data.content == null) return null; if (fd.pos >= fd.node.data.content.length) return null; return fd.node.data.content[fd.pos++]; } clite.io.readLine = function(fd) { if (fd.node.data.content == null) return null; if (fd.pos >= fd.node.data.content.length) return null; if (typeof fd.node.data.content != 'string') return null; var e = fd.node.data.content.indexOf('\n',fd.pos); var l = ''; if (e < 0) { l = fd.node.data.content.substring(fd.pos); }else{ l = fd.node.data.content.substring(fd.pos,e); fd.pos++; } fd.pos += l.length; return l; } clite.io.readAll = function(fd) { return fd.node.data.content; } clite.io.write = function(fd,data) { if (!fd.canwrite) return false; if (typeof fd.data.content != 'string') return false; if (typeof data != 'string') return false; if (fd.pos + data.length >= fd.data.content.length) { fd.data.content = fd.data.content.substring(0,fd.pos)+data; fd.pos = fd.data.content.length; }else{ var b = fd.data.content.substring(0,fd.pos); var e = fd.data.content.substring(fd.pos+data.length); fd.data.content = b+data+e; } return true; } clite.io.ftruncate = function(fd,len) { if (!fd.canwrite) return false; if (typeof fd.data.content != 'string') return false; fd.data.content = fd.data.content.substring(0,len); if (fd.pos > len) fd.pos = len; return true; } clite.io.truncate = function(path,len) { var fd = getFileDes(path,false,null); if (!fd) return false; var r = clite.io.ftruncate(fd,len); close(fd); return r; } clite.io.seek = function(fd,pos) { if (fd.node.data.content == null) return 0; if (fd.node.data.content.length < 1) return 0; if (pos<0) return fd.pos; fd.pos = pos; if (fd.pos >= fd.node.data.content.length) fd.pos = fd.node.data.content.length-1; return fd.pos; } clite.io.remove = function(path) { var fd = getFileDes(path,true,null); if (!fd || !fd.canwrite) return false; return vfsapi.remove(path); } clite.io.link = function(path,target) { if (!getFileDes(target,false,null)) return false; var fd = getFileDes(path,true,null); if (fd) { if (!fd.canwrite) return false; fd.node.data.content = target; return true; } return vfsapi.mkLink(path,target); } }, creat:null, open:null, close:null, read:null, readLine:null, readAll:null, write:null, ftruncate:null, truncate:null, seek:null, remove:null, link:null }; clite.vfs = { isinit:false, init:function() { var vfsdata = { fs:{}, api:{} }; function findNodeChild(node,name) { for (var i=0; i 0) { if (!n.data.isdir || !checkCanOpen(n)) return null; var p = parts.shift(); if (!p || p == '') continue; var nn = findNodeChild(n,p); if (!nn) return null; n = nn; } if (!checkCanOpen(n)) return null; return n; } vfsdata.api.getFile = function(path) { var n = vfsdata.api.getNode(path); if (!n) return null; if (n.data.islink) // get the actual file data, not the link data return vfsdata.api.getFile(n.data.content); return n.data.content; } vfsdata.api.mkDir = function(path) { if (vfsdata.api.getNode(path) != null) return false; var parts = path.split('/'); var name = parts.pop(); var dir = parts.join('/'); if (dir.length < 1) dir = '/'; var parent = vfsdata.api.getNode(dir); if (!parent || !parent.data.isdir) return false; var n = mkNode(); n.name = name; n.data.parent = parent; n.data.content = []; n.data.isdir = true; n.perms = 'drwxr-xr-x'; n.uid = clite.user.getUID(); n.gid = clite.user.getGID(); parent.data.content.push(n); return true; } vfsdata.api.mkFile = function(path) { if (vfsdata.api.getNode(path) != null) return false; var parts = path.split('/'); var name = parts.pop(); var dir = parts.join('/'); if (dir.length < 1) dir = '/'; var parent = vfsdata.api.getNode(dir); if (!parent || !parent.data.isdir) return false; var n = mkNode(); n.name = name; n.data.parent = parent; n.data.content = ''; n.perms = '-rw-r--r--'; n.uid = clite.user.getUID(); n.gid = clite.user.getGID(); parent.data.content.push(n); return true; } vfsdata.api.mkLink = function(path,target) { if (vfsdata.api.getNode(path) != null) return false; var parts = path.split('/'); var name = parts.pop(); var dir = parts.join('/'); if (dir.length < 1) dir = '/'; var parent = vfsdata.api.getNode(dir); if (!parent || !parent.data.isdir) return false; var n = mkNode(); n.name = name; n.data.parent = parent; n.data.islink = true; n.data.content = target; n.perms = 'lrwxr-xr-x'; n.uid = clite.user.getUID(); n.gid = clite.user.getGID(); parent.data.content.push(n); return true; } vfsdata.api.remove = function(path) { return false; } // make some directories vfsdata.api.mkDir('/bin'); vfsdata.api.mkDir('/dev'); vfsdata.api.mkDir('/etc'); vfsdata.api.mkDir('/usr'); vfsdata.api.mkDir('/usr/clite'); vfsdata.api.mkDir('/usr/clite/web'); vfsdata.api.mkDir('/usr/home'); vfsdata.api.mkDir('/usr/share'); vfsdata.api.mkDir('/var'); vfsdata.api.mkFile('/var/logs'); clite.io.init(); vfsdata.api.isinit = true; clite.core.execSafeAsync(function() {clite.vfs.init = null;}); }, getApi:null }; clite.log = { init:function(vfs) { clite.log.write = function(txt) { if (!clite.user.hasLogin()) { try { clite.term.writeLine(txt); } catch(e) {} } var n = vfs.getNode('/var/logs'); if (n) n.data.content += txt+'\n'; console.log(txt); } clite.core.execSafeAsync(function(){clite.log.init = null;}); }, write:function(txt) { try { clite.term.writeLine(txt); } catch(e) {} console.log(txt); } }; clite.term = { clear:function(form) { document.getElementById('terminal').innerHTML = ''; if (form) clite.term.genForm(); }, preventInput:function() { var f = document.getElementById('form'); if (!f) return; f.parentNode.removeChild(f); }, hasInput:function() { var f = document.getElementById('form'); if (!f) return false; return true; }, writeLine:function(txt) { var a = document.createElement('article'); a.innerHTML = txt; var t = document.getElementById('terminal'); if (!t) return; t.appendChild(a); }, genForm:function() { var f = document.getElementById('form'); if (!f) f = document.createElement('form'); f.id = 'form'; f.innerHTML = ''; var l = document.createElement('label'); l.innerHTML = clite.shell.prompt.getHTML(); f.appendChild(l); var i = document.createElement('input'); i.type = clite.term.getType(); i.value = clite.shell.history.getCurrent(); f.appendChild(i); clite.state.input.form = f; clite.state.input.field = i; var t = document.getElementById('terminal'); if (!t) return; t.appendChild(f); i.onkeyup = clite.events.keyup; i.onfocusout = clite.events.refocus; i.onblur = clite.events.refocus; if (i.type == 'file') { // this is only needed due to dirty hacks to read in a file: path i.onchange = function(e) { var reader = new FileReader(); reader.onload = function() { var cb = clite.state.input.type.callback; clite.term.setPass(false); clite.term.writeLine('loaded'); clite.term.preventInput(); cb(reader.result); }; reader.onerror = function() { var cb = clite.state.input.type.callback; clite.term.setPass(false); clite.term.writeLine('load failed'); clite.term.preventInput(); cb(null); }; reader.readAsText(e.target.files[0]); } } f.onsubmit = function() {return false;}; }, setPass:function(is) { clite.state.input.type = (!!is) ? 'password' : 'text'; }, setCustom:function(type) { clite.state.input.type = type; }, getType:function() { if (typeof clite.state.input.type == 'string') return clite.state.input.type; if (typeof clite.state.input.type == 'object' && typeof clite.state.input.type.type == 'string') return clite.state.input.type.type; return 'text'; } }; clite.shell = { exit:function(v) { clite.shell.prompt.pop(); clite.events.refocus(); }, readLine:function(cb) { }, writeLine:function(txt) { clite.term.writeLine(txt); clite.events.refocus(); }, resolvePath:function(txt) { // TODO: resolve using PATH (check each colon separated path) return clite.lib.resolvePath(txt,clite.shell.env.PATH) }, exec:function(args,io) { var path = clite.shell.resolvePath(args[0]); var fd = clite.io.open(path); // TODO: should errors here write to stderr? (io.error) if (!fd) { clite.term.writeLine('Shell: unknown command: '+args[0]); io.exit(-1); return; } var r = clite.core.execSafe(function() { var rr = fd.node.data.content(args,Object.create(clite.shell.env),io); if (typeof rr == 'number') io.exit(rr); }); if (!r) { clite.term.writeLine('Shell: error in command:'+args[0]); io.exit(-1); } clite.io.close(fd); }, parse:function(txt) { if (txt.length < 1) { clite.shell.writeLine(clite.shell.prompt.getHTML()); return; } clite.shell.history.add(txt); clite.shell.history.resetCurrent(); clite.shell.writeLine(clite.shell.prompt.getHTML()+txt); clite.shell.prompt.push(''); clite.term.preventInput(); // ready stdio (stdout,stdin) var stdio = { error:clite.term.writeLine, write:clite.term.writeLine, read:clite.shell.readLine, exit:clite.shell.exit, istty:true }; // check for a macro var args = clite.lib.strToArgs(txt); // then either run the macro or expand the program to be executed via PATH if (typeof clite.shell.macro[args[0]] != 'undefined') { var r = clite.shell.macro[args[0]](args,stdio); clite.shell.exit(r); }else{ clite.shell.exec(args,stdio); } }, env:{ USER:'root', PWD:'/', HOME:'/', PATH:'/bin' }, macro:{ clear:function(args,io) { clite.term.clear(); }, cd:function(args,io) { var dir = clite.shell.env.HOME; if (args.length > 1) { dir = clite.lib.resolvePath(args[1]); } var fd = clite.io.open(dir); if (!fd) { io.error('invalid directory: '+dir); return; } clite.io.close(fd); clite.shell.env.PWD = dir; clite.shell.prompt.generate(); }, pwd:function(args,io) { io.write(clite.shell.env.PWD); }, echo:function(args,io) { args.shift(); var txt = args.join(' '); io.write(txt); }, which:function(args,io) { if (args.length <2) return; if (typeof clite.shell.macro[args[1]] != 'undefined') { return; }else{ var path = clite.shell.resolvePath(args[1]); var fd = clite.io.open(path); if (!fd) return; clite.io.close(fd); io.write(args[1]+' is '+path); } }, type:function(args,io) { if (args.length <2) return; if (typeof clite.shell.macro[args[1]] != 'undefined') { io.write(args[1]+' is a shell builtin'); }else{ var path = clite.shell.resolvePath(args[1]); var fd = clite.io.open(path); if (!fd) return; clite.io.close(fd); io.write(args[1]+' is '+path); } }, whoami:function(args,io) { io.write(clite.shell.env.USER); }, alias:function(args,io) { io.write('unimplemented'); }, export:function(args,io) { if (args.length == 1) { Object.keys(clite.shell.env,function(key) { io.write(key+'='+clite.shell.env[key]); }); return; } var parts = args[1].split('='); if (parts.length != 2) return; clite.shell.env[parts[0]] = parts[1]; } }, prompt:{ list:[], data:'# ', set:function(txt) { clite.shell.prompt.list = []; clite.shell.prompt.data = txt; }, push:function(txt) { clite.shell.prompt.list.push(clite.shell.prompt.data); clite.shell.prompt.data = txt; }, pop:function() { clite.shell.prompt.data = clite.shell.prompt.list.pop(); }, generate:function() { var p = clite.shell.env.USER+':'+clite.shell.env.PWD; if (clite.user.getUID() == 0) { p += '# '; }else{ p += '$ '; } clite.shell.prompt.set(p); }, get:function() { return clite.shell.prompt.data; }, getHTML:function() { return clite.lib.htmlEncode(clite.shell.prompt.data); } }, history:{ data:[], current:'', index:0, add:function(txt) { if (txt.length < 1) return; clite.shell.history.data.push(txt); clite.shell.history.index = clite.shell.history.data.length; }, up:function() { if (clite.shell.history.index == 0) return; clite.shell.history.index--; clite.events.refocus(); }, down:function() { if (clite.shell.history.index == clite.shell.history.data.length) return; clite.shell.history.index++; clite.events.refocus(); }, setCurrent:function(txt) { clite.shell.history.current = txt; }, getCurrent:function() { if (clite.shell.history.index == clite.shell.history.data.length) return clite.shell.history.current; return clite.shell.history.data[clite.shell.history.index]; }, resetCurrent:function() { clite.shell.history.current = ''; clite.shell.history.index = clite.shell.history.data.length; } } }; clite.lib = { // makes text html safe htmlEncode:function(txt) { return txt .replace(/&/g, '&') .replace(/'/g, '&apos') .replace(/"/g, '"') .replace(/>/g, '>') .replace(/ '/etc/foo': ('foo','bar') -> $PWD/bar/foo resolvePath:function(txt,base) { if (txt[0] == '/') return txt; if (txt[0] == '~') return clite.shell.env.HOME+txt.substring(1); if (typeof base != 'string') base = clite.shell.env.PWD; if (base[0] != '/') base = clite.shell.env.PWD+'/'+base; return base+'/'+txt; }, // convert a string into an argument array: 'ls /etc' -> ['ls',/etc] strToArgs:function(txt) { // TODO: split this properly (escape strings and insert env variables) var parts = []; var s = ''; var e = false; var q = false; for (var i=0; i 0) parts.push(s); return parts;//txt.split(' '); } }