shithub: svc

Download patch

ref: 063d86a3015f5f254389e61b57e7f14822e889c3
parent: 50bc9f2c47441facf926715dca4e73da381d6a5d
author: Michael Misch <michaelmisch1985@gmail.com>
date: Sun Feb 22 00:02:41 EST 2026

Move up a directory
d

--- /dev/null
+++ b/fs/buffer.c
@@ -1,0 +1,113 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "svc.h"
+
+static void
+bufferFree(Buffer *match)
+{
+	if(match->name)
+		free(match->name);
+	free(match);
+}
+
+char *
+bufferDrop(Buffer *base, char *name)
+{
+	Buffer *mp, *bp;
+	qlock(&base->l);
+	for(bp = base; bp->next; bp = bp->next){
+		mp = bp->next;
+		if(strcmp(bp->next->name, name) == 0){
+			if(mp && mp->next)
+				bp->next = mp->next;
+			else
+				bp->next = nil;
+			if(mp)
+				bufferFree(mp);
+		}
+	}
+	qunlock(&base->l);			
+	return nil;
+}
+
+char *
+bufferPush(Buffer *base, char *name)
+{
+	Buffer *b, *ep;
+	char p[1024];
+
+	qlock(&base->l);
+	for(ep = base; ep->next; ep = ep->next){
+		if(ep && strcmp(ep->name, name) == 0){
+			qunlock(&base->l);
+			return "buffer exists";
+		}
+		if(ep->next == nil)
+			break;
+	}
+	
+	b = mallocz(sizeof(*b), 1);
+	strcpy(b->name, name);
+	b->notify = nil;
+	b->unread = 0;
+	b->tag = -1;
+	b->rz.l = &b->l;
+	snprint(p, sizeof(p), "%s/%s/%s", logdir, base->name, name);
+	if(access(p, 0) == 0)
+		b->fd = open(p, OWRITE);
+	else
+		b->fd = create(p, OWRITE, 0644);
+	seek(b->fd, 0, 2);
+	ep->next = b;
+	qunlock(&base->l);
+	return nil;
+}
+
+Buffer *
+bufferSearch(Buffer *base, char *name)
+{
+	Buffer *sp;
+	qlock(&base->l);
+	for(sp = base; sp; sp = sp->next)
+		if(strcmp(sp->name, name) == 0){
+			qunlock(&base->l);
+			return sp;
+		}
+	qunlock(&base->l);
+	return nil;
+}
+
+Buffer *
+bufferSearchTag(Buffer *base, ulong tag)
+{
+	Buffer *sp;
+	qlock(&base->l);
+	for(sp = base; sp; sp = sp->next)
+		if(sp->tag == tag){
+			qunlock(&base->l);
+			return sp;
+		}
+	qunlock(&base->l);
+	return nil;
+}
+
+Buffer*
+bufferCreate(Channel *cmds, Channel *input)
+{
+	Buffer *b;
+
+	b = mallocz(sizeof(*b), 1);
+	b->cmds = cmds;
+	b->input = input;
+	b->tag = -1;
+	b->unread = 0;
+	b->notify = nil;
+	b->next = nil;
+	b->rz.l = &b->l;
+
+	return b;
+}
--- /dev/null
+++ b/fs/client.c
@@ -1,0 +1,431 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "svc.h"
+
+enum {
+	Qcroot,
+		Qtabs,
+		Qctl,
+		Qinput,
+		Qtitle,
+		Qstatus,
+		Qaside,
+		Qfeed,
+		Qnotify,
+	Qmax,
+};
+
+static char *cltab[] = {
+	"/",
+		"tabs",
+		"ctl",
+		"input",
+		"title",
+		"status",
+		"aside",
+		"feed",
+		"notify",
+	nil,
+};
+
+typedef struct Clfid Clfid;
+typedef struct Client Client;
+
+struct Clfid
+{
+	int	level;
+	int	tag;
+	Client	*cl;
+};
+
+struct Client
+{
+	Ref;
+
+	Buffer	*current;
+	int	fd;
+	int	showmarkdown;
+};
+
+static Client client[256];
+static Buffer *root;
+static int flushtag;
+static int nclient;
+static int time0;
+static Srv *fs;
+
+static Client*
+newclient(char *aname)
+{
+	Client *cl;
+	int i;
+
+	for(i = 0; i < nclient; i++)
+		if(client[i].ref == 0)
+			break;
+	if(i >= nelem(client))
+		return nil;
+	if(i == nclient)
+		nclient++;
+	cl = &client[i];
+	cl->ref++;
+
+	cl->current = bufferSearch(root, aname);
+	if(cl->current)
+		cl->current->tag = 0;
+	cl->showmarkdown = 0;
+	cl->fd = -1;
+
+	return cl;
+}
+
+static void
+freeclient(Client *cl)
+{
+	if(cl == nil || cl->ref == 0)
+		return;
+	cl->ref--;
+	if(cl->ref > 0)
+		return;
+
+	memset(cl, 0, sizeof(*cl));
+}
+
+static void
+clmkqid(Qid *q, int level, void *aux)
+{
+	q->type = 0;
+	q->vers = 0;
+	if(level == Qcroot)
+		q->type = QTDIR;
+	else
+		q->path = (level<<24) | (((uintptr)aux ^ time0) & 0x00ffffff);
+}
+
+static void
+clmkdir(Dir *d, int level, void *aux)
+{
+	memset(d, 0, sizeof(*d));
+	clmkqid(&d->qid, level, aux);
+	d->mode = 0444;
+	d->atime = d->mtime = time(0);
+	d->uid = estrdup(user);
+	d->gid = estrdup(user);
+	d->muid = estrdup(user);
+	if(d->qid.type & QTDIR)
+		d->mode |= DMDIR | 0111;
+	switch(level){
+	case Qctl:
+		d->mode = 0666;
+	default:
+		d->name = estrdup(cltab[level]);
+	}
+}
+
+void
+clattach(Req *r)
+{
+	Clfid *f;
+	char path[1024];
+
+	f = mallocz(sizeof(*f), 1);
+	f->cl = newclient(r->ifcall.aname);
+
+	/* Attach request has buffer */
+	if(r->ifcall.aname[0]){
+		memset(path, 0, sizeof(path));
+		snprint(path, sizeof(path), "%s/%s/%s", logdir, root->name, r->ifcall.aname);
+		f->cl->fd = open(path, OREAD);
+	}
+	f->level = Qcroot;
+	clmkqid(&r->fid->qid, f->level, nil);
+	r->ofcall.qid = r->fid->qid;
+	r->fid->aux = f;
+	respond(r, nil);
+}
+
+
+
+void
+clstat(Req *r)
+{
+	Clfid *f;
+
+	f = r->fid->aux;
+	clmkdir(&r->d, f->level, f->cl);
+	respond(r, nil);
+}
+
+
+char*
+clwalk1(Fid *fid, char *name, Qid *qid)
+{
+	Clfid *f;
+	int i;
+
+	if(!(fid->qid.type&QTDIR))
+		return "walk in non-directory";
+
+	f = fid->aux;
+	for(i=f->level+1; i < nelem(cltab); i++){
+		if(cltab[i]){
+			if(strcmp(name, cltab[i]) == 0)
+				break;
+		}
+	}
+	if(i >= nelem(cltab))
+		return "directory entry not found";
+	f->level = i;
+	clmkqid(qid, f->level, f->cl);
+	fid->qid = *qid;
+	return nil;	
+}
+
+char *
+clclone(Fid *oldfid, Fid *newfid)
+{
+	Clfid *f, *o;
+
+	o = oldfid->aux;
+	if(o == nil)
+		return "bad fid";
+	f = mallocz(sizeof(*f), 1);
+	memmove(f, o, sizeof(*f));
+	if(f->cl)
+		incref(f->cl);
+	newfid->aux = f;
+	return nil;
+}
+
+void
+clopen(Req *r)
+{
+	respond(r, nil);
+}
+
+static int
+rootgen(int i, Dir *d, void *aux)
+{
+	i += Qcroot+1;
+	if(i < Qmax){
+		clmkdir(d, i, aux);
+		return 0;
+	}
+	return -1;
+}
+
+void
+clread(Req *r)
+{
+	Clfid *f;
+	Buffer *b;
+	Notify *np;
+	char buf[1024];
+	int n;
+
+	f = r->fid->aux;
+	n = 0;
+	switch(f->level){
+	case Qcroot:
+		dirread9p(r, rootgen, f->cl);
+		respond(r, nil);
+		return;
+	case Qfeed:
+		// Catch EOF
+		if(f->cl->fd < 0 || !f->cl->current){
+			respond(r, "no feed");
+			return;
+		}
+		srvrelease(fs);
+		b = f->cl->current;
+Again:
+		if(b->tag == flushtag){
+			flushtag = -1;
+			r->ofcall.count = 0;
+			srvacquire(fs);
+			respond(r, nil);
+			return;
+		}
+		n = pread(f->cl->fd, r->ofcall.data, r->ifcall.count,	r->ifcall.offset);
+		if(n > 0){
+			r->ofcall.count = n;
+			srvacquire(fs);
+			respond(r, nil);
+			return;
+		} else if(n < 0) {
+			srvacquire(fs);
+			responderror(r);
+			return;
+		}
+		qlock(&b->l);
+		b->unread = 0;
+		rsleep(&b->rz);
+		qunlock(&b->l);
+		goto Again;
+	case Qtitle:
+		if(f->cl->current && f->cl->current->title){
+			b = f->cl->current;
+			memset(buf, 0, sizeof(buf));
+			qlock(&b->l);
+			snprint(buf, sizeof(buf), "%s", b->title);
+			qunlock(&b->l);
+String:
+			readstr(r, buf);
+			respond(r, nil);
+			return;	
+		}
+		break;
+	case Qstatus:
+		if(f->cl->current && f->cl->current->status){
+			b = f->cl->current;
+			memset(buf, 0, sizeof(buf));
+			qlock(&b->l);
+			snprint(buf, sizeof(buf), "%s",b->status);
+			qunlock(&b->l);
+			goto String;
+		}
+		break;
+	case Qaside:
+		if(f->cl->current && f->cl->current->aside){
+			b = f->cl->current;
+			memset(buf, 0, sizeof(buf));
+			qlock(&b->l);
+			snprint(buf, sizeof(buf), "%s", b->aside);
+			qunlock(&b->l);
+			goto String;
+		}
+		break;
+	case Qnotify:
+		if(f->cl->current && f->cl->current->notify){
+			b = f->cl->current;
+			memset(buf, 0, sizeof(buf));
+			qlock(&b->l);
+			for(np = b->notify; np; np = np->next)
+				n = snprint(buf + n, sizeof(buf), "!%s\n", np->data);
+			qunlock(&b->l);
+			goto String;
+		}
+		break;
+	case Qtabs:
+		qlock(&root->l);
+		memset(buf, 0, sizeof(buf));
+		for(b = root->next; b; b = b->next){
+			n += snprint(buf + n, sizeof(buf) - n, "%N\n", b);
+		}
+		qunlock(&root->l);
+		goto String;
+	}
+	if(!f->cl->current)
+		respond(r, "no buffer selected");
+	else
+		respond(r, "no data available");
+}
+
+void
+clwrite(Req *r)
+{
+	Buffer *b;
+	Clfid *f;
+	Cmd cmd;
+	char path[1024];
+
+	f = r->fid->aux;
+	memset(&cmd, 0, sizeof(Cmd));
+	switch(f->level){
+	case Qctl:
+		convS2C(&cmd, r->ifcall.data, r->ifcall.count);
+		switch(cmd.type){
+		case BufferCmd:
+			b = bufferSearch(root, cmd.buffer);
+			if(!b){
+				respond(r, "No buffers available");
+				return;
+			}
+			qlock(&b->l);
+			if(f->cl && f->cl->fd > 0){
+				b->tag = flushtag = 1;
+				rwakeup(&b->rz);
+				close(f->cl->fd);
+			}
+			f->cl->current = b;
+			b->tag = r->tag;
+			qunlock(&b->l);
+			memset(path, 0, sizeof(path));
+			snprint(path, sizeof(path), "%s/%s/%s", logdir, root->name, cmd.buffer);
+			f->cl->fd = open(path, OREAD);
+			r->fid->aux = f;
+			goto Out;
+		case MarkdownCmd:
+			f->cl->showmarkdown = !f->cl->showmarkdown;
+			goto Out;
+		default:
+			sendp(root->cmds, &cmd);
+			sendp(root->cmds, nil);
+			goto Out;
+		}
+	case Qinput:
+		r->ofcall.count = 0;
+		if(f->cl->current == nil){
+			respond(r, "No buffer selected");
+			return;
+		}
+		cmd.type = InputCmd;
+		strcpy(cmd.buffer, f->cl->current->name);
+		cmd.data = strdup(r->ifcall.data);
+		cmd.data[r->ifcall.count] = 0;
+		sendp(root->cmds, &cmd);
+		sendp(root->cmds, nil);
+	}
+Out:
+	respond(r, nil);
+	return;
+}
+
+void
+clflush(Req *r)
+{
+	Buffer *b;
+	flushtag = r->tag;
+	if(b = bufferSearchTag(root, flushtag)){
+		qlock(&b->l);
+		rwakeup(&b->rz);
+		qunlock(&b->l);
+	}
+	respond(r, nil);
+}
+
+void
+cldestroyfid(Fid *fid)
+{
+	Clfid *f;
+
+	if(f = fid->aux){
+		// TODO: Uncomment once we use this in aux/listen
+		//fid->aux = nil;
+		//if(f->cl)
+		//	freeclient(f->cl);
+	}
+	free(f);
+}
+
+void
+clstart(Srv *s)
+{
+	// TODO: Set up note handler
+	root = emalloc(sizeof(*root));
+	USED(root);
+	root = (Buffer*)s->aux;
+	flushtag = -1;
+	fs = s;
+	time0 = time(0);
+}
+
+void
+clend(Srv*)
+{
+	exits(nil);
+}
--- /dev/null
+++ b/fs/cmd.c
@@ -1,0 +1,31 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "svc.h"
+
+int
+Cconv(Fmt *fp)
+{
+	char s[CmdSize];
+	Cmd *c;
+
+	c = va_arg(fp->args, Cmd*);
+	switch(c->type){
+	case ServiceCmd:
+		if(strlen(c->data) > 0)
+			snprint(s, sizeof(s), "%s %s\n%s", c->svccmd, c->buffer, c->data);
+		else
+			snprint(s, sizeof(s), "%s %s", c->svccmd, c->buffer);
+		break;
+	case InputCmd:
+		snprint(s, sizeof(s), "input %s\n%s", c->buffer, c->data);
+		break;
+	case FlushCmd:
+		snprint(s, sizeof(s), "flush");
+		break;
+	}
+	return fmtstrcpy(fp, s);
+}
--- /dev/null
+++ b/fs/convS2C.c
@@ -1,0 +1,175 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+#include "svc.h"
+
+// TODO: Read runes instead of ascii
+
+struct state
+{
+	Cmd cmd;
+	char *base;
+	int size;
+	int (*fn)(struct state *s);	
+};
+
+static int parse_cmd(struct state *s);
+static int parse_from(struct state *s);
+static int parse_data(struct state *s);
+
+uint
+convS2C(Cmd *cmd, char *c, uint n)
+{
+	struct state s;
+	int t;
+
+	s.fn = (*parse_cmd);
+	s.cmd.type = ErrorCmd;
+	s.cmd.data = "";
+	s.base = c;
+	s.size = n;
+	for(;;){
+		t = s.fn(&s);
+		if(t < 0)
+			return t;
+		if(t == 0)
+			break;
+	}
+
+	cmd->type = s.cmd.type;
+	cmd->data = s.cmd.data;
+	strcpy(cmd->buffer, s.cmd.buffer);
+	strcpy(cmd->svccmd, s.cmd.svccmd);
+	return sizeof(cmd);
+}
+
+static int
+parse_cmd(struct state *s)
+{
+	int n = 0;
+	for(;;){
+		if(n > s->size)
+			return -1;
+		switch(s->base[n]){
+		case '\0':
+		case '\r':
+		case '\n':
+			// Check we aren't at "quit"
+			s->base[n] = '\0';
+			s->cmd.type = CloneCmd;
+			if(strncmp("quit", s->base, n) == 0)
+				s->cmd.type = QuitCmd;
+			else
+				s->cmd.data = strdup(s->base);
+			return 0;
+		case ' ':
+		case '\t':
+			// The overwhelming majority will be feed activity
+			if(strncmp(s->base, "feed", 4) == 0)
+				s->cmd.type = FeedCmd;
+			else if(strncmp(s->base, "asi", 3) == 0)
+				s->cmd.type = SideCmd;
+			else if(strncmp(s->base, "nav", 3) == 0)
+				s->cmd.type = NavCmd;
+			else if(strncmp(s->base, "titl", 4) == 0)
+				s->cmd.type = TitleCmd;
+			else if(strncmp(s->base, "ima", 3) == 0)
+				s->cmd.type = ImageCmd;
+			else if(strncmp(s->base, "del", 3) == 0)
+				s->cmd.type = DeleteCmd;
+			else if(strncmp(s->base, "rem", 3) == 0)
+				s->cmd.type = RemoveCmd;
+			else if(strncmp(s->base, "noti", 4) == 0)
+				s->cmd.type = NotifyCmd;
+			else if(strncmp(s->base, "err", 3) == 0)
+				s->cmd.type = ErrorCmd;
+			else if(strncmp(s->base, "stat", 4) == 0)
+				s->cmd.type = StatusCmd;
+			else if(strncmp(s->base, "crea", 4) == 0)
+				s->cmd.type = CreateCmd;
+			else if(strncmp(s->base, "buff", 4) == 0)
+				s->cmd.type = BufferCmd;
+			else if(strncmp(s->base, "mark", 4) == 0)
+				s->cmd.type = MarkdownCmd;
+			else {
+				s->cmd.type = ServiceCmd;
+				snprint(s->cmd.svccmd, n+1, s->base);
+			}
+			s->size -= n;
+			n++;
+			s->base += n;
+			s->fn = (*parse_from);
+			return 1;
+		default:
+			n++;
+		}
+	}
+}
+
+static int
+parse_from(struct state *s)
+{
+	int n = 0;
+	for(;;){
+		if(n > s->size || n > MaxBuflen)
+			return -1;
+		switch(s->base[n]){
+		// leading spaces/tabs, ignore
+		case ' ':
+		case '\t':
+			// NOTE: This moves our pointer forward so n++ isn't necessary
+			s->size--;
+			s->base++;
+			break;
+		case 0:
+			strncpy(s->cmd.buffer, s->base, n);
+			return 0;
+		case '\n':
+		case '\r':
+			s->base[n] = '\0';
+			strncpy(s->cmd.buffer, s->base, n+1);
+			// return if we have no data to parse
+			if (n >= s->size)
+				return 0;
+			s->size -=n;
+			n++;
+			s->base += n;
+			s->fn = (*parse_data);
+			return 1;
+		default:
+			n++;
+		}
+	}
+}
+
+static int
+parse_data(struct state *s)
+{
+	int n = 0;
+	char c;
+
+	for(;;) {
+		if(n >= s->size || n >= MaxBuflen)
+			return -1;
+		c = s->base[n];
+		switch(c){
+		// Useful control chars
+		case '\n':
+		case '\t':
+		case '\r':
+			break;
+		default:
+			// Anything under a space is an unhandled control char
+			if(c < ' '){
+				if(n > 0 ){	
+					s->cmd.data = strdup(s->base);
+					s->cmd.data[n] = 0;
+				}
+				return 0;
+			}
+		}
+		n++;
+	}
+}
--- /dev/null
+++ b/fs/fs.c
@@ -1,0 +1,87 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "svc.h"
+
+void*
+emalloc(int n)
+{
+	void *v;
+	v = emalloc9p(n);
+	setmalloctag(v, getcallerpc(&n));
+	memset(v, 0, n);
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	s = estrdup9p(s);
+	setmalloctag(s, getcallerpc(&s));
+	return s;
+}
+
+Srv svcfs = 
+{
+	.start=svcstart,
+	.attach=svcattach,
+	.stat=svcstat,
+	.walk1=svcwalk1,
+	.clone=svcclone,
+	.open=svcopen,
+	.read=svcread,
+	.write=svcwrite,
+	.flush=svcflush,
+	.destroyfid=svcdestroyfid,
+	.end=svcend,
+};
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-Dd] [-m mtpt] [-s service] [-l logdir]\n", argv0);
+	exits("usage");
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	char *svcfile;
+	fmtinstall('N', Tconv);
+	fmtinstall('!', Nconv);
+	fmtinstall('C', Cconv);
+	user = getuser();
+	mtpt = "/mnt/svc";
+	logdir = "/tmp/svc";
+	svcfile = nil;
+
+	ARGBEGIN {
+	case 'D': 
+		chatty9p++;
+		break;
+	case 'm':
+		mtpt = EARGF(usage());
+		break;
+	case 'l':
+		logdir = EARGF(usage());
+		break;
+	case 'd':
+		debug++;
+		break;
+	case 's':
+		svcfile = EARGF(usage());
+		break;
+	default:
+		usage();
+	} ARGEND;
+
+	argv0 = "alt/fs";
+
+	create(logdir, OREAD, DMDIR | 0755);
+	threadpostmountsrv(&svcfs, svcfile, mtpt, MCREATE);
+	exits(nil);
+}
--- /dev/null
+++ b/fs/mkfile
@@ -1,0 +1,20 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin/svc
+
+TARG=fs
+
+HFILES=svc.h
+
+OFILES=\
+	fs.$O\
+	buffer.$O\
+	client.$O\
+	cmd.$O\
+	convS2C.$O\
+	service.$O\
+	notification.$O\
+	tabs.$O\
+
+</sys/src/cmd/mkone
+
--- /dev/null
+++ b/fs/notification.c
@@ -1,0 +1,16 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "svc.h"
+
+int
+Nconv(Fmt *fp)
+{
+	Notify *n;
+
+	n = va_arg(fp->args, Notify*);
+	return fmtstrcpy(fp, n->data);
+}
--- /dev/null
+++ b/fs/service.c
@@ -1,0 +1,518 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "svc.h"
+
+#define SERVICEID(c)	((int)(((Service*)(c)) - service))
+
+enum {
+	Qsroot,
+		Qclone,
+		Qlist,
+		Qservices,
+			Qctl,
+	Qmax,
+};
+
+static char *svctab[] = {
+	"/",
+		"clone",
+		"list",
+		nil,
+			"ctl",
+	nil,
+};
+
+Srv clfs = 
+{
+	.start=clstart,
+	.attach=clattach,
+	.stat=clstat,
+	.walk1=clwalk1,
+	.clone=clclone,
+	.open=clopen,
+	.read=clread,
+	.write=clwrite,
+	.flush=clflush,
+	.destroyfid=cldestroyfid,
+	.end=clend,
+};
+
+typedef struct Svcfid Svcfid;
+typedef struct Service Service;
+
+struct Svcfid
+{	int	level;
+	Service 	*svc;
+};
+
+struct Service
+{
+	Ref;
+
+	Channel	*cmds;
+	Channel   *input;
+	Buffer	*base;
+	char	*name;
+	int	isInitialized;
+	int	childpid;
+};
+
+static Srv *fs;
+Service service[64];
+int nservice;
+Lock svclock;
+
+static Service*
+newservice(void)
+{
+	Service *svc;
+	int i;
+
+	lock(&svclock);
+	for(i = 0; i < nservice; i++)
+		if(service[i].ref == 0)
+			break;
+	if(i >= nelem(service))
+		return nil;
+	if(i == nservice)
+		nservice++;
+	svc = &service[i];
+	svc->ref++;
+
+	// NOTE: If you're sending more commands than this before they are processed, up this number
+	// But also it might be time to question your design, because commands really should not be taking long
+	svc->cmds = chancreate(sizeof(Cmd*), 8);
+	svc->base = bufferCreate(svc->cmds, svc->input);
+	svc->isInitialized = 0;
+	unlock(&svclock);
+	return svc;
+}
+
+static void
+rmservice(Service *s)
+{
+	int i, found = 0;
+
+	lock(&svclock);
+	for(i = 0; i < nservice; i++){
+		if(service[i].name == s->name)
+			found++;
+		/* Shift  back */
+		if(found && i < nservice)
+			service[i] = service[i+1];
+	}
+	if(found)
+		nservice--;
+	unlock(&svclock);
+}
+
+static void
+freeservice(Service *s)
+{
+	if(s == nil)
+		return;
+	chanfree(s->cmds);
+	memset(s, 0, sizeof(*s));
+}
+
+static void*
+wfaux(Svcfid *f)
+{
+	if(f->level < Qservices)
+		return nil;
+	return f->svc;
+}
+
+static void
+svcmkqid(Qid *q, int level, void *)
+{
+	q->type = 0;
+	q->vers = 0;
+	switch(level){
+	case Qsroot:
+	case Qservices:
+		q->type = QTDIR;
+	default:
+		;
+	}
+}
+
+static void
+svcmkdir(Dir *d, int level, void *aux)
+{
+	char buf[1024];
+
+	memset(d, 0, sizeof(*d));
+	svcmkqid(&d->qid, level, aux);
+	d->mode = 0444;
+	d->atime = d->mtime = time(0);
+	d->uid = estrdup(user);
+	d->gid = estrdup(user);
+	d->muid = estrdup(user);
+	if(d->qid.type & QTDIR)
+		d->mode |= DMDIR | 0111;
+	switch(level){
+	case Qservices:
+		memset(buf, 0, sizeof(buf));
+		snprint(buf, sizeof(buf), "%d", SERVICEID(aux));
+		d->name = estrdup(buf);
+		break;
+	case Qctl:
+	case Qclone:
+	case Qlist:
+		d->mode = 0666;
+		/* Fallthrough */
+	default:
+		d->name = estrdup(svctab[level]);
+	}
+}
+
+void
+svcattach(Req *r)
+{
+	Svcfid *f;
+
+	// No anames
+	if(r->ifcall.aname && r->ifcall.aname[0]){
+		respond(r, "invalid attach specifier");
+		return;
+	}
+	f = mallocz(sizeof(*f), 1);
+	f->level = Qsroot;
+	svcmkqid(&r->fid->qid, f->level, wfaux(f));
+	r->ofcall.qid = r->fid->qid;
+	r->fid->aux = f;
+	respond(r, nil);
+}
+
+void
+svcstat(Req *r)
+{
+	Svcfid *f;
+
+	f = r->fid->aux;
+	svcmkdir(&r->d, f->level, wfaux(f));
+	respond(r, nil);
+}
+
+char*
+svcwalk1(Fid *fid, char *name, Qid *qid)
+{
+	Svcfid *f;
+	int i, j;
+
+	if(!(fid->qid.type&QTDIR))
+		return "walk in non-directory";
+
+	f = fid->aux;
+	if(strcmp(name, "..")==0){
+		switch(f->level){
+		case Qsroot:
+		case Qlist:
+			break;
+		case Qservices:
+			f->level = Qsroot;
+			break;
+		default:
+			f->level = Qservices;
+		}
+	} else {
+		for(i = f->level+1; i < nelem(svctab); i++){
+			if(svctab[i])
+				if(strcmp(name, svctab[i]) == 0)
+					goto Out;
+			if(i == Qservices){
+				j = atoi(name);
+				if(j >= 0 && j < nservice){
+					f->svc = &service[j];
+					incref(f->svc);
+					goto Out;
+				}
+			}
+		}
+Out:
+		if(i >= nelem(svctab))
+			return "directory entry not found";
+		f->level = i;
+	}
+	svcmkqid(qid, f->level, wfaux(f));
+	fid->qid = *qid;
+	return nil;
+}
+
+char *
+svcclone(Fid *oldfid, Fid *newfid)
+{
+	Svcfid *f, *o;
+	o = oldfid->aux;
+	if(o == nil)
+		return "bad fid";
+	f = mallocz(sizeof(*f), 1);
+	memmove(f, o, sizeof(*f));
+	if(f->svc)
+		incref(f->svc);
+	newfid->aux = f;
+	return nil;
+}
+
+void
+svcopen(Req *r)
+{
+	Svcfid *f;
+	Service *svc;
+
+	f = r->fid->aux;
+	if(f->level == Qclone){
+		if((svc = newservice()) == nil){
+			respond(r, "no more services");
+			return;
+		}
+		f->level = Qctl;
+		f->svc = svc;
+		svcmkqid(&r->fid->qid, f->level, wfaux(f));
+		r->ofcall.qid = r->fid->qid;
+	}
+	respond(r, nil);
+}
+
+static int
+rootgen(int i, Dir *d, void *)
+{
+	i += Qsroot+1;
+	if(i < Qservices){
+		svcmkdir(d, i, 0);
+		return 0;
+	}
+	i -= Qservices;
+	if(i < nservice){
+		svcmkdir(d, Qservices, &service[i]);
+		return 0;
+	}
+	return -1;
+}
+
+static int
+servicegen(int i, Dir *d, void *aux)
+{
+	i += Qservices+1;
+	if(i >= Qmax)
+		return -1;
+	svcmkdir(d, i, aux);
+	return 0;
+}
+
+enum {
+	CmdRead,
+	InputRead,
+};
+
+
+void
+svcread(Req *r)
+{
+
+	Cmd *cmd;
+	char buf[CmdSize];
+	Svcfid *f;
+	int i, n;
+
+	f = r->fid->aux;
+	memset(buf, 0, CmdSize);
+
+	switch(f->level){
+	case Qsroot:
+		dirread9p(r, rootgen, nil);
+		respond(r, nil);
+		return;
+	case Qservices:
+		dirread9p(r, servicegen, nil);
+		respond(r, nil);
+		return;
+	case Qlist:
+		n = 0;
+		for(i = 0; i < nservice; i++)
+			n += snprint(buf+n, sizeof(buf) - n, "%s\n", service[i].name);
+		readstr(r, buf);
+		respond(r, nil);
+		return;
+	case Qctl:
+		/* Wait for any data/command from the client */
+		srvrelease(fs);
+		cmd = recvp(f->svc->cmds);
+		if(cmd && cmd->type != FlushCmd){
+			n = snprint(r->ofcall.data, r->ifcall.count, "%C", cmd);
+			r->ofcall.count = n;
+		}
+		srvacquire(fs);
+		respond(r, nil);
+		return;
+
+	}
+	respond(r, "not implemented");
+}
+
+
+void
+svcwrite(Req *r)
+{
+	int n;
+	Svcfid *f;
+	Cmd cmd;
+	Buffer *b;
+	Dir *d;
+	char *p, *s;
+	char path[1024];
+
+	f = r->fid->aux;
+
+	if(f->level == Qctl){
+		n = r->ofcall.count = r->ifcall.count;
+		p = mallocz(n, 1);
+		memmove(p, r->ifcall.data, n);
+		n = convS2C(&cmd, p, n);
+		if(n <= 0){
+			respond(r, "malformed command");
+			goto Out;
+		}
+		switch(cmd.type){
+		case CloneCmd:
+			if(!f->svc->isInitialized){
+				f->svc->name = estrdup(cmd.data);
+				strcpy(f->svc->base->name, cmd.data);
+				memset(path, 0, sizeof(path));
+				snprint(path, sizeof(path), "%s/%s", logdir, cmd.data);
+				close(create(path, OREAD, DMDIR | 0755));
+				clfs.aux = f->svc->base;
+				f->svc->childpid = threadpostsrv(&clfs, strdup(cmd.data));
+				if(f->svc->childpid > 0){
+					s = emalloc(10+strlen(cmd.data));
+					strcpy(s, "/srv/");
+					strcat(s, cmd.data);
+					d = dirstat(s);
+					d->mode = 0666;
+					if(dirwstat(s, d) < 0){
+						rmservice(f->svc);
+						respond(r, "unable to set mode");
+						goto Out;
+					}
+					free(s);
+					f->svc->isInitialized++;
+					r->fid->aux = f;
+					respond(r, nil);
+				} else {
+					rmservice(f->svc);
+					respond(r, "Unable to post to srv");
+				}
+				goto Out;
+			}
+			/* Ignore, something weird going on */
+			respond(r, nil);
+			goto Out;
+		case CreateCmd:
+			respond(r, bufferPush(f->svc->base, cmd.buffer));
+			goto Out;
+		case NotifyCmd:
+			respond(r, "not implemented yet");
+			goto Out;
+		case DeleteCmd:
+			respond(r, bufferDrop(f->svc->base, cmd.buffer));
+			goto Out;
+		case ErrorCmd:
+			respond(r, "not implemented yet");
+			goto Out;
+		}
+		if(!cmd.data){
+			respond(r, "expected data");
+			goto Out;
+		}
+		if(b = bufferSearch(f->svc->base, cmd.buffer)) {
+			qlock(&b->l);
+			switch(cmd.type){
+			case FeedCmd:
+
+				d = dirfstat(b->fd);
+				pwrite(b->fd, cmd.data, strlen(cmd.data), d->length);
+				free(d);
+				if(rwakeupall(&b->rz) == 0)
+					b->unread++;
+				break;
+			case StatusCmd:
+				strcpy(b->status, cmd.data);
+				break;
+			case TitleCmd:
+				strcpy(b->title, cmd.data);
+				break;
+			case SideCmd:
+				strcpy(b->aside, cmd.data);
+				break;
+			}
+			qunlock(&b->l);
+			respond(r, nil);
+		} else
+			respond(r, "buffer not found");
+	Out:
+		free(p);
+		return;
+	}
+	respond(r, "not implemented");
+}
+
+void
+svcflush(Req *r)
+{
+	Cmd cmd;
+	int i;
+
+	for(i = 0; i < nservice; i++)
+		if(service[i].ref > 0)
+			if((bufferSearchTag(service[i].base, r->tag))){
+				memset(&cmd, 0, sizeof(Cmd));
+				cmd.type = FlushCmd;
+				send(service[i].cmds, &cmd);
+				break;
+			}
+	respond(r, nil);
+}
+
+void
+svcdestroyfid(Fid *fid)
+{
+	Svcfid *f;
+
+	if(f = fid->aux){
+		if(f->svc && f->svc->childpid)
+			postnote(PNPROC, f->svc->childpid, "done");
+		// TODO: Uncomment this after we are good to go, this is our keepalive roughly
+		//fid->aux = nil;
+		//if(f->svc)
+		//	freeservice(f->svc);
+	}
+	free(f);
+}
+
+void
+svcstart(Srv* s)
+{
+	fs = s;
+	if(mtpt != nil)
+		unmount(nil, mtpt);
+}
+
+void
+svcend(Srv*)
+{
+	int i;
+
+	if(mtpt != nil)
+		unmount(nil, mtpt);
+	for(i = 0; i < nservice; i++)
+		if(service[i].ref){
+			postnote(PNPROC, service[i].childpid, "done");
+		}
+	postnote(PNPROC, getpid(), "done");
+	threadexitsall(nil);
+}
--- /dev/null
+++ b/fs/svc.h
@@ -1,0 +1,109 @@
+typedef struct Buffer Buffer;
+typedef struct Notify Notify;
+typedef struct Cmd Cmd;
+
+enum
+{
+	CloneCmd,
+	CreateCmd,
+	DeleteCmd,
+	RemoveCmd,
+	NotifyCmd,
+	ErrorCmd,
+	StatusCmd,
+	SideCmd,
+	NavCmd,
+	TitleCmd,
+	ImageCmd,
+	FeedCmd,
+	QuitCmd,
+	ServiceCmd,
+	InputCmd,
+	FlushCmd,
+	BufferCmd,
+	OpenCmd,
+	MarkdownCmd,
+
+	// TODO: Move data to the stack
+	MaxBuflen = 128,
+	MaxDatalen = 1024,
+	CmdSize = MaxBuflen * 2 + 1 + MaxDatalen,
+};
+
+struct Buffer
+{
+	QLock       l;
+	char	name[MaxBuflen];
+	char	title[512];
+	char	status[512];
+	char	*aside;
+	int	fd;	// feed
+	int	tag;	// feed
+	int	unread;
+	Channel	*cmds;
+	Channel   *input;
+	Notify	*notify;
+	Buffer	*next;
+	Rendez	rz;
+};
+
+struct Notify
+{
+	char	*data;
+	Notify	*next;
+};
+
+struct Cmd
+{
+	// Potentially big
+	int	type;
+	char	buffer[MaxBuflen];
+	char	svccmd[MaxBuflen];
+	char	*data;
+};
+
+Buffer *bufferCreate(Channel*, Channel*);
+Buffer *bufferSearch(Buffer*, char*);
+Buffer *bufferSearchTag(Buffer*, ulong);
+char *bufferDrop(Buffer*, char*);
+char *bufferPush(Buffer*, char*);
+void bufferDestroy(Buffer*);
+
+int Tconv(Fmt*);
+int Nconv(Fmt*);
+int Cconv(Fmt*);
+
+void* emalloc(int);
+char* estrdup(char*);
+
+char *mtpt;
+char *srvpt;
+char *user;
+char *logdir;
+int debug;
+
+uint convS2C(Cmd*, char*, uint);
+
+void clattach(Req*);
+void clstat(Req*);
+char *clwalk1(Fid*, char*, Qid*);
+char *clclone(Fid*, Fid*);
+void clopen(Req*);
+void clread(Req*);
+void clwrite(Req*);
+void clflush(Req*);
+void cldestroyfid(Fid*);
+void clstart(Srv*);
+void clend(Srv*);
+
+void svcattach(Req*);
+void svcstat(Req*);
+char *svcwalk1(Fid*, char*, Qid*);
+char *svcclone(Fid*, Fid*);
+void svcopen(Req*);
+void svcread(Req*);
+void svcwrite(Req*);
+void svcflush(Req*);
+void svcdestroyfid(Fid*);
+void svcstart(Srv*);
+void svcend(Srv*);
--- /dev/null
+++ b/fs/tabs.c
@@ -1,0 +1,21 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "svc.h"
+
+int
+Tconv(Fmt *fp)
+{
+	char s[1024];
+	Buffer *b;
+
+	b = va_arg(fp->args, Buffer*);
+	if(b->notify)
+		snprint(s, sizeof(s), "![%d] %s", b->unread, b->name);
+	else
+		snprint(s, sizeof(s), "[%d] %s", b->unread, b->name);
+	return fmtstrcpy(fp, s);
+}
--