new files
diff --git a/src/cmd/netfiles/COPYING b/src/cmd/netfiles/COPYING
new file mode 100644
index 0000000..cea426d
--- /dev/null
+++ b/src/cmd/netfiles/COPYING
@@ -0,0 +1,24 @@
+
+Copyright (c) 2005 Russ Cox <rsc@swtch.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+These conditions shall not be whined about.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/src/cmd/netfiles/acme.c b/src/cmd/netfiles/acme.c
new file mode 100644
index 0000000..7519da7
--- /dev/null
+++ b/src/cmd/netfiles/acme.c
@@ -0,0 +1,636 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <9pclient.h>
+#include "acme.h"
+
+extern int *xxx;
+static CFsys *acmefs;
+Win *windows;
+static Win *last;
+
+void
+mountacme(void)
+{
+	if(acmefs == nil){
+		acmefs = nsmount("acme", nil);
+		if(acmefs == nil)
+			sysfatal("cannot mount acme: %r");
+	}
+}
+
+Win*
+newwin(void)
+{
+	Win *w;
+	CFid *fid;
+	char buf[100];
+	int id, n;
+
+	mountacme();
+	fid = fsopen(acmefs, "new/ctl", ORDWR);
+	if(fid == nil)
+		sysfatal("open new/ctl: %r");
+	n = fsread(fid, buf, sizeof buf-1);
+	if(n <= 0)
+		sysfatal("read new/ctl: %r");
+	buf[n] = 0;
+	id = atoi(buf);
+	if(id == 0)
+		sysfatal("read new/ctl: malformed message: %s", buf);
+
+	w = emalloc(sizeof *w);
+	w->id = id;
+	w->ctl = fid;
+	w->next = nil;
+	w->prev = last;
+	if(last)
+		last->next = w;
+	else
+		windows = w;
+	last = w;
+	return w;
+}
+
+void
+winclosefiles(Win *w)
+{
+	if(w->ctl){
+		fsclose(w->ctl);
+		w->ctl = nil;
+	}
+	if(w->body){
+		fsclose(w->body);
+		w->body = nil;
+	}
+	if(w->addr){
+		fsclose(w->addr);
+		w->addr = nil;
+	}
+	if(w->tag){
+		fsclose(w->tag);
+		w->tag = nil;
+	}
+	if(w->event){
+		fsclose(w->event);
+		w->event = nil;
+	}
+	if(w->data){
+		fsclose(w->data);
+		w->data = nil;
+	}
+	if(w->xdata){
+		fsclose(w->xdata);
+		w->xdata = nil;
+	}
+}
+
+void
+winfree(Win *w)
+{
+	winclosefiles(w);
+	if(w->c){
+		chanfree(w->c);
+		w->c = nil;
+	}
+	if(w->next)
+		w->next->prev = w->prev;
+	else
+		last = w->prev;
+	if(w->prev)
+		w->prev->next = w->next;
+	else
+		windows = w->next;
+	free(w);
+}
+
+void
+windeleteall(void)
+{
+	Win *w, *next;
+
+	for(w=windows; w; w=next){
+		next = w->next;
+		winctl(w, "delete");
+	}
+}
+
+static CFid*
+wfid(Win *w, char *name)
+{
+	char buf[100];
+	CFid **fid;
+
+	if(strcmp(name, "ctl") == 0)
+		fid = &w->ctl;
+	else if(strcmp(name, "body") == 0)
+		fid = &w->body;
+	else if(strcmp(name, "addr") == 0)
+		fid = &w->addr;
+	else if(strcmp(name, "tag") == 0)
+		fid = &w->tag;
+	else if(strcmp(name, "event") == 0)
+		fid = &w->event;
+	else if(strcmp(name, "data") == 0)
+		fid = &w->data;
+	else if(strcmp(name, "xdata") == 0)
+		fid = &w->xdata;
+	else{
+		fid = 0;
+		sysfatal("bad window file name %s", name);
+	}
+
+	if(*fid == nil){
+		snprint(buf, sizeof buf, "acme/%d/%s", w->id, name);
+		*fid = fsopen(acmefs, buf, ORDWR);
+		if(*fid == nil)
+			sysfatal("open %s: %r", buf);
+	}
+	return *fid;
+}
+
+int
+winopenfd(Win *w, char *name, int mode)
+{
+	char buf[100];
+	
+	snprint(buf, sizeof buf, "%d/%s", w->id, name);
+	return fsopenfd(acmefs, buf, mode);
+}
+
+int
+winctl(Win *w, char *fmt, ...)
+{
+	char *s;
+	va_list arg;
+	CFid *fid;
+	int n;
+
+	va_start(arg, fmt);
+	s = evsmprint(fmt, arg);
+	va_end(arg);
+
+	fid = wfid(w, "ctl");
+	n = fspwrite(fid, s, strlen(s), 0);
+	free(s);
+	return n;
+}
+
+int
+winname(Win *w, char *fmt, ...)
+{
+	char *s;
+	va_list arg;
+	int n;
+
+	va_start(arg, fmt);
+	s = evsmprint(fmt, arg);
+	va_end(arg);
+
+	n = winctl(w, "name %s\n", s);
+	free(s);
+	return n;
+}
+
+int
+winprint(Win *w, char *name, char *fmt, ...)
+{
+	char *s;
+	va_list arg;
+	int n;
+
+	va_start(arg, fmt);
+	s = evsmprint(fmt, arg);
+	va_end(arg);
+
+	n = fswrite(wfid(w, name), s, strlen(s));
+	free(s);
+	return n;
+}
+
+int
+winaddr(Win *w, char *fmt, ...)
+{
+	char *s;
+	va_list arg;
+	int n;
+
+	va_start(arg, fmt);
+	s = evsmprint(fmt, arg);
+	va_end(arg);
+
+	n = fswrite(wfid(w, "addr"), s, strlen(s));
+	free(s);
+	return n;
+}
+
+int
+winreadaddr(Win *w, uint *q1)
+{
+	char buf[40], *p;
+	uint q0;
+	int n;
+	
+	n = fspread(wfid(w, "addr"), buf, sizeof buf-1, 0);
+	if(n <= 0)
+		return -1;
+	buf[n] = 0;
+	q0 = strtoul(buf, &p, 10);
+	if(q1)
+		*q1 = strtoul(p, nil, 10);
+	return q0;
+}
+
+int
+winread(Win *w, char *file, void *a, int n)
+{
+	return fspread(wfid(w, file), a, n, 0);
+}
+
+int
+winwrite(Win *w, char *file, void *a, int n)
+{
+	return fswrite(wfid(w, file), a, n);
+}
+
+char*
+fsreadm(CFid *fid)
+{
+	char *buf;
+	int n, tot, m;
+	
+	m = 128;
+	buf = emalloc(m+1);
+	tot = 0;
+	while((n = fspread(fid, buf+tot, m-tot, tot)) > 0){
+		tot += n;
+		if(tot >= m){
+			m += 128;
+			buf = erealloc(buf, m+1);
+		}
+	}
+	if(n < 0){
+		free(buf);
+		return nil;
+	}
+	buf[tot] = 0;
+	return buf;
+}
+
+char*
+winmread(Win *w, char *file)
+{
+	return fsreadm(wfid(w, file));
+}
+
+char*
+winindex(void)
+{
+	CFid *fid;
+	char *s;
+	
+	mountacme();
+	if((fid = fsopen(acmefs, "index", ORDWR)) == nil)
+		return nil;
+	s = fsreadm(fid);
+	fsclose(fid);
+	return s;
+}
+
+int
+winseek(Win *w, char *file, int n, int off)
+{
+	return fsseek(wfid(w, file), n, off);
+}
+
+int
+winwriteevent(Win *w, Event *e)
+{
+	char buf[100];
+
+	snprint(buf, sizeof buf, "%c%c%d %d \n", e->c1, e->c2, e->q0, e->q1);
+	return fswrite(wfid(w, "event"), buf, strlen(buf));
+}
+
+int
+windel(Win *w, int sure)
+{
+	return winctl(w, sure ? "delete" : "del");
+}
+
+int
+winfd(Win *w, char *name, int mode)
+{
+	char buf[100];
+
+	snprint(buf, sizeof buf, "acme/%d/%s", w->id, name);
+	return fsopenfd(acmefs, buf, mode);
+}
+
+static void
+error(Win *w, char *msg)
+{
+	if(msg == nil)
+		longjmp(w->jmp, 1);
+	fprint(2, "%s: win%d: %s\n", argv0, w->id, msg);
+	longjmp(w->jmp, 2);
+}
+
+static int
+getec(Win *w, CFid *efd)
+{
+	if(w->nbuf <= 0){
+		w->nbuf = fsread(efd, w->buf, sizeof w->buf);
+		if(w->nbuf <= 0)
+			error(w, nil);
+		w->bufp = w->buf;
+	}
+	--w->nbuf;
+	return *w->bufp++;
+}
+
+static int
+geten(Win *w, CFid *efd)
+{
+	int n, c;
+
+	n = 0;
+	while('0'<=(c=getec(w,efd)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error(w, "event number syntax");
+	return n;
+}
+
+static int
+geter(Win *w, CFid *efd, char *buf, int *nb)
+{
+	Rune r;
+	int n;
+
+	r = getec(w, efd);
+	buf[0] = r;
+	n = 1;
+	if(r < Runeself)
+		goto Return;
+	while(!fullrune(buf, n))
+		buf[n++] = getec(w, efd);
+	chartorune(&r, buf);
+    Return:
+	*nb = n;
+	return r;
+}
+
+static void
+gete(Win *w, CFid *efd, Event *e)
+{
+	int i, nb;
+
+	e->c1 = getec(w, efd);
+	e->c2 = getec(w, efd);
+	e->q0 = geten(w, efd);
+	e->q1 = geten(w, efd);
+	e->flag = geten(w, efd);
+	e->nr = geten(w, efd);
+	if(e->nr > EVENTSIZE)
+		error(w, "event string too long");
+	e->nb = 0;
+	for(i=0; i<e->nr; i++){
+		/* e->r[i] = */ geter(w, efd, e->text+e->nb, &nb);
+		e->nb += nb;
+	}
+/* 	e->r[e->nr] = 0; */
+	e->text[e->nb] = 0;
+	if(getec(w, efd) != '\n')
+		error(w, "event syntax 2");
+}
+
+int
+winreadevent(Win *w, Event *e)
+{
+	CFid *efd;
+	int r;
+
+	if((r = setjmp(w->jmp)) != 0){
+		if(r == 1)
+			return 0;
+		return -1;
+	}
+	efd = wfid(w, "event");
+	gete(w, efd, e);
+	e->oq0 = e->q0;
+	e->oq1 = e->q1;
+
+	/* expansion */
+	if(e->flag&2){
+		gete(w, efd, &w->e2);
+		if(e->q0==e->q1){
+			w->e2.oq0 = e->q0;
+			w->e2.oq1 = e->q1;
+			w->e2.flag = e->flag;
+			*e = w->e2;
+		}
+	}
+
+	/* chorded argument */
+	if(e->flag&8){
+		gete(w, efd, &w->e3);	/* arg */
+		gete(w, efd, &w->e4);	/* location */
+		strcpy(e->arg, w->e3.text);
+		strcpy(e->loc, w->e4.text);
+	}
+
+	return 1;
+}
+
+int
+eventfmt(Fmt *fmt)
+{
+	Event *e;
+
+	e = va_arg(fmt->args, Event*);
+	return fmtprint(fmt, "%c%c %d %d %d %d %q", e->c1, e->c2, e->q0, e->q1, e->flag, e->nr, e->text);
+}
+
+void*
+emalloc(uint n)
+{
+	void *v;
+
+	v = mallocz(n, 1);
+	if(v == nil)
+		sysfatal("out of memory");
+	return v;
+}
+
+void*
+erealloc(void *v, uint n)
+{
+	v = realloc(v, n);
+	if(v == nil)
+		sysfatal("out of memory");
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	if(s == nil)
+		return nil;
+	s = strdup(s);
+	if(s == nil)
+		sysfatal("out of memory");
+	return s;
+}
+
+char*
+evsmprint(char *s, va_list v)
+{
+	s = vsmprint(s, v);
+	if(s == nil)
+		sysfatal("out of memory");
+	return s;
+}
+
+int
+pipewinto(Win *w, char *name, int errto, char *cmd, ...)
+{
+	va_list arg;
+	char *p;
+	int fd[3], pid;
+
+	va_start(arg, cmd);
+	p = evsmprint(cmd, arg);
+	va_end(arg);
+	fd[0] = winfd(w, name, OREAD);
+	fd[1] = dup(errto, -1);
+	fd[2] = dup(errto, -1);
+	pid = threadspawnl(fd, "rc", "rc", "-c", p, 0);
+	free(p);
+	return pid;
+}
+
+int
+pipetowin(Win *w, char *name, int errto, char *cmd, ...)
+{
+	va_list arg;
+	char *p;
+	int fd[3], pid, pfd[2];
+	char buf[1024];
+	int n;
+
+	/*
+	 * cannot use winfd here because of buffering caused
+	 * by pipe.  program might exit before final write to acme
+	 * happens.  so we might return before the final write.
+	 *
+	 * to avoid this, we tend the pipe ourselves.
+	 */
+	if(pipe(pfd) < 0)
+		sysfatal("pipe: %r");
+	va_start(arg, cmd);
+	p = evsmprint(cmd, arg);
+	va_end(arg);
+	fd[0] = open("/dev/null", OREAD);
+	fd[1] = pfd[1];
+	if(errto == 0)
+		fd[2] = dup(fd[1], -1);
+	else
+		fd[2] = dup(errto, -1);
+	pid = threadspawnl(fd, "rc", "rc", "-c", p, 0);
+	free(p);
+	while((n = read(pfd[0], buf, sizeof buf)) > 0)
+		winwrite(w, name, buf, n);
+	close(pfd[0]);
+	return pid;
+}
+
+char*
+sysrun(int errto, char *fmt, ...)
+{
+	static char buf[1024];
+	char *cmd;
+	va_list arg;
+	int n, fd[3], p[2], tot, pid;
+
+#undef pipe
+	if(pipe(p) < 0)
+		sysfatal("pipe: %r");
+	fd[0] = open("/dev/null", OREAD);
+	fd[1] = p[1];
+	if(errto == 0)
+		fd[2] = dup(fd[1], -1);
+	else
+		fd[2] = dup(errto, -1);
+
+	va_start(arg, fmt);
+	cmd = evsmprint(fmt, arg);
+	va_end(arg);
+	pid = threadspawnl(fd, "rc", "rc", "-c", cmd, 0);
+
+	tot = 0;
+	while((n = read(p[0], buf+tot, sizeof buf-tot)) > 0)
+		tot += n;
+	close(p[0]);
+	twait(pid);
+	if(n < 0)
+		return nil;
+	free(cmd);
+	if(tot == sizeof buf)
+		tot--;
+	buf[tot] = 0;
+	while(tot > 0 && isspace(buf[tot-1]))
+		tot--;
+	buf[tot] = 0;
+	if(tot == 0){
+		werrstr("no output");
+		return nil;
+	}
+	return estrdup(buf);
+}
+
+static void
+eventreader(void *v)
+{
+	Event e[2];
+	Win *w;
+	int i;
+	
+	w = v;
+	i = 0;
+	for(;;){
+		if(winreadevent(w, &e[i]) <= 0)
+			break;
+		sendp(w->c, &e[i]);
+		i = 1-i;	/* toggle */
+	}
+	sendp(w->c, nil);
+	threadexits(nil);
+}
+
+Channel*
+wineventchan(Win *w)
+{
+	if(w->c == nil){
+		w->c = chancreate(sizeof(Event*), 0);
+		threadcreate(eventreader, w, 32*1024);
+	}
+	return w->c;
+}
+
+char*
+wingetname(Win *w)
+{
+	int n;
+	char *p;
+	
+	n = winread(w, "tag", w->name, sizeof w->name-1);
+	if(n <= 0)
+		return nil;
+	w->name[n] = 0;
+	p = strchr(w->name, ' ');
+	if(p)
+		*p = 0;
+	return w->name;
+}
+
diff --git a/src/cmd/netfiles/acme.h b/src/cmd/netfiles/acme.h
new file mode 100644
index 0000000..50997e9
--- /dev/null
+++ b/src/cmd/netfiles/acme.h
@@ -0,0 +1,82 @@
+typedef struct Event Event;
+typedef struct Win Win;
+
+#define	EVENTSIZE	256
+struct Event
+{
+	int	c1;
+	int	c2;
+	int	oq0;
+	int	oq1;
+	int	q0;
+	int	q1;
+	int	flag;
+	int	nb;
+	int	nr;
+	char	text[EVENTSIZE*UTFmax+1];
+	char	arg[EVENTSIZE*UTFmax+1];
+	char	loc[EVENTSIZE*UTFmax+1];
+};
+
+struct Win
+{
+	int id;
+	CFid *ctl;
+	CFid *tag;
+	CFid *body;
+	CFid *addr;
+	CFid *event;
+	CFid *data;
+	CFid *xdata;
+	Channel *c;	/* chan(Event) */
+	Win *next;
+	Win *prev;
+	
+	/* events */
+	int nbuf;
+	char name[1024];
+	char buf[1024];
+	char *bufp;
+	jmp_buf jmp;
+	Event e2;
+	Event e3;
+	Event e4;
+};
+
+Win *newwin(void);
+
+int eventfmt(Fmt*);
+int pipewinto(Win *w, char *name, int, char *fmt, ...);
+int pipetowin(Win *w, char *name, int, char *fmt, ...);
+char *sysrun(int errto, char*, ...);
+int winaddr(Win *w, char *fmt, ...);
+int winctl(Win *w, char *fmt, ...);
+int windel(Win *w, int sure);
+int winfd(Win *w, char *name, int);
+char *winmread(Win *w, char *file);
+int winname(Win *w, char *fmt, ...);
+int winprint(Win *w, char *name, char *fmt, ...);
+int winread(Win *w, char *file, void *a, int n);
+int winseek(Win *w, char *file, int n, int off);
+int winreadaddr(Win *w, uint*);
+int winreadevent(Win *w, Event *e);
+int winwrite(Win *w, char *file, void *a, int n);
+int winwriteevent(Win *w, Event *e);
+int winopenfd(Win *w, char *name, int mode);
+void windeleteall(void);
+void winfree(Win *w);
+void winclosefiles(Win *w);
+Channel *wineventchan(Win *w);
+char *winindex(void);
+void mountacme(void);
+char *wingetname(Win *w);
+
+void *erealloc(void*, uint);
+void *emalloc(uint);
+char *estrdup(char*);
+char *evsmprint(char*, va_list);
+
+int twait(int);
+void twaitinit(void);
+
+extern Win *windows;
diff --git a/src/cmd/netfiles/main.c b/src/cmd/netfiles/main.c
new file mode 100644
index 0000000..0b58e48
--- /dev/null
+++ b/src/cmd/netfiles/main.c
@@ -0,0 +1,490 @@
+/*
+ * Remote file system editing client.
+ * Only talks to acme - external programs do all the hard work.
+ * 
+ * If you add a plumbing rule:
+
+# /n/ paths go to simulator in acme
+kind is text
+data matches '[a-zA-Z0-9_\-./]+('$addr')?'
+data matches '(/n/[a-zA-Z0-9_\-./]+)('$addr')?'
+plumb to netfileedit
+plumb client Netfiles
+
+ * then plumbed paths starting with /n/ will find their way here.
+ *
+ * Perhaps on startup should look for windows named /n/ and attach to them?
+ * Or might that be too aggressive?
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <9pclient.h>
+#include <plumb.h>
+#include "acme.h"
+
+char *root = "/n/";
+
+void
+usage(void)
+{
+	fprint(2, "usage: Netfiles\n");
+	threadexitsall("usage");
+}
+
+extern int chatty9pclient;
+int debug;
+#define dprint if(debug)print
+Win *mkwin(char*);
+int do3(Win *w, char *arg);
+
+enum {
+	STACK = 128*1024,
+};
+
+enum {
+	Put,
+	Get,
+	Del,
+	Delete,
+	Debug,
+	XXX
+};
+
+char *cmds[] = {
+	"Put",
+	"Get",
+	"Del",
+	"Delete",
+	"Debug",
+	nil
+};
+
+typedef struct Arg Arg;
+struct Arg
+{
+	char *file;
+	char *addr;
+	Channel *c;
+};
+
+Arg*
+arg(char *file, char *addr, Channel *c)
+{
+	Arg *a;
+		
+	a = emalloc(sizeof *a);
+	a->file = estrdup(file);
+	a->addr = estrdup(addr);
+	a->c = c;
+	return a;
+}
+
+/*
+ * return window id of a window named name or name/
+ * assumes name is cleaned.
+ */
+int
+nametowinid(char *name)
+{
+	char *index, *p, *next;
+	int len, n;
+
+	index = winindex();
+	n = -1;
+	len = strlen(name);
+	for(p=index; p && *p; p=next){
+		if((next = strchr(p, '\n')) != nil)
+			*next = 0;
+		if(strlen(p) <= 5*12)
+			continue;
+		if(memcmp(p+5*12, name, len)!=0)
+			continue;
+		if(p[5*12+len]!=' ' && (p[5*12+len]!='/' || p[5*12+len+1]!=' '))
+			continue;
+		n = atoi(p);
+		break;
+	}
+	free(index);
+	return n;
+}
+
+/* 
+ * look up window by name
+ */
+Win*
+nametowin(char *name)
+{
+	int id;
+	Win *w;
+	
+	id = nametowinid(name);
+	if(id == -1)
+		return nil;
+	for(w=windows; w; w=w->next)
+		if(w->id == id)
+			return w;
+	return nil;
+}
+
+/*
+ * look for s in list
+ */
+int
+lookup(char *s, char **list)
+{
+	int i;
+	
+	for(i=0; list[i]; i++)
+		if(strcmp(list[i], s) == 0)
+			return i;
+	return -1;
+}
+
+/*
+ * move to top of file
+ */
+void
+wintop(Win *w)
+{
+	winaddr(w, "#0");
+	winctl(w, "dot=addr");
+	winctl(w, "show");
+}
+
+/*
+ * Expand the click further than acme usually does -- all non-white space is okay.
+ */
+char*
+expandarg(Win *w, Event *e)
+{
+	if(e->c2 == 'l')
+		return estrdup(e->text);
+	dprint("expand %d %d %d %d\n", e->oq0, e->oq1, e->q0, e->q1);
+	if(e->oq0 == e->oq1 && e->q0 != e->q1)
+		winaddr(w, "#%ud+#1-/[^ \t\\n]*/,#%ud-#1+/[^ \t\\n]*/", e->q0, e->q1);
+	else
+		winaddr(w, "#%ud,#%ud", e->q0, e->q1);
+	return winmread(w, "xdata");
+}
+
+/*
+ * handle a plumbing message
+ */
+void
+doplumb(void *vm)
+{
+	char *addr;
+	Plumbmsg *m;
+	Win *w;
+	
+	m = vm;
+	if(m->ndata >= 1024){
+		fprint(2, "insanely long file name (%d bytes) in plumb message (%.32s...)\n",
+			m->ndata, m->data);
+		plumbfree(m);
+		return;
+	}
+	
+	addr = plumblookup(m->attr, "addr");
+	w = nametowin(m->data);
+	if(w == nil)
+		w = mkwin(m->data);
+	winaddr(w, "%s", addr);
+	winctl(w, "dot=addr");
+	winctl(w, "show");
+//	windecref(w);
+	plumbfree(m);
+}
+
+/*
+ * dispatch messages from the plumber
+ */
+void
+plumbthread(void *v)
+{
+	CFid *fid;
+	Plumbmsg *m;
+	
+	fid = plumbopenfid("netfileedit", OREAD);
+	if(fid == nil){
+		fprint(2, "cannot open plumb/netfileedit: %r\n");
+		return;
+	}
+	while((m = plumbrecvfid(fid)) != nil)
+		threadcreate(doplumb, m, STACK);
+	fsclose(fid);
+}
+
+/*
+ * parse /n/system/path
+ */
+int
+parsename(char *name, char **server, char **path)
+{
+	char *p, *nul;
+	
+	cleanname(name);
+	if(strncmp(name, "/n/", 3) != 0 && name[3] == 0)
+		return -1;
+	nul = nil;
+	if((p = strchr(name+3, '/')) == nil)
+		*path = estrdup("/");
+	else{
+		*path = estrdup(p);
+		*p = 0;
+		nul = p;
+	}
+	p = name+3;
+	if(p[0] == 0){
+		free(*path);
+		*server = *path = nil;
+		if(nul)
+			*nul = '/';
+		return -1;
+	}
+	*server = estrdup(p);
+	if(nul)
+		*nul = '/';
+	return 0;
+}
+
+/*
+ * shell out to find the type of a given file
+ */
+char*
+filestat(char *server, char *path)
+{
+	return sysrun(2, "9 netstat %q %q", server, path);
+}
+
+/*
+ * manage a single window
+ */
+void
+filethread(void *v)
+{
+	char *arg, *name, *p, *server, *path, *type;
+	Arg *a;
+	Channel *c;
+	Event *e;
+	Win *w;
+
+	a = v;
+	threadsetname("file %s", a->file);
+	w = newwin();
+	winname(w, a->file);
+	winprint(w, "tag", "Get Put Look ");
+	c = wineventchan(w);
+	
+	goto caseGet;
+	
+	while((e=recvp(c)) != nil){
+		if(e->c1!='K')
+			dprint("acme %E\n", e);
+		if(e->c1=='M')
+		switch(e->c2){
+		case 'x':
+		case 'X':
+			switch(lookup(e->text, cmds)){
+			caseGet:
+			case Get:
+				server = nil;
+				path = nil;
+				if(parsename(name=wingetname(w), &server, &path) < 0){
+					fprint(2, "Netfiles: bad name %s\n", name);
+					goto out;
+				}
+				type = filestat(server, path);
+				if(type == nil)
+					type = estrdup("");
+				if(strcmp(type, "file")==0 || strcmp(type, "directory")==0){
+					winaddr(w, ",");
+					winprint(w, "data", "[reading...]");
+					winaddr(w, ",");
+					if(strcmp(type, "file")==0)
+						twait(pipetowin(w, "data", 2, "9 netget %q %q", server, path));
+					else
+						twait(pipetowin(w, "data", 2, "9 netget -d %q %q | winid=%d mc", server, path, w->id));
+					cleanname(name);
+					if(strcmp(type, "directory")==0){
+						p = name+strlen(name);
+						if(p[-1] != '/'){
+							p[0] = '/';
+							p[1] = 0;
+						}
+					}
+					winname(w, name);
+					wintop(w);
+					winctl(w, "clean");
+					if(a && a->addr){
+						winaddr(w, "%s", a->addr);
+						winctl(w, "dot=addr");
+						winctl(w, "show");
+					}
+				}
+				free(type);
+			out:
+				free(server);
+				free(path);
+				if(a){
+					if(a->c){
+						sendp(a->c, w);
+						a->c = nil;
+					}
+					free(a->file);
+					free(a->addr);
+					free(a);
+					a = nil;
+				}
+				break;
+			case Put:
+				server = nil;
+				path = nil;
+				if(parsename(name=wingetname(w), &server, &path) < 0){
+					fprint(2, "Netfiles: bad name %s\n", name);
+					goto out;
+				}
+				if(twait(pipewinto(w, "body", 2, "9 netput %q %q", server, path)) >= 0){
+					cleanname(name);
+					winname(w, name);
+					winctl(w, "clean");
+				}
+				free(server);
+				free(path);
+				break;
+			case Del:
+				winctl(w, "del");
+				break;
+			case Delete:
+				winctl(w, "delete");
+				break;
+			case Debug:
+				debug = !debug;
+				break;
+			default:
+				winwriteevent(w, e);
+				break;
+			}
+			break;
+		case 'l':
+		case 'L':
+			arg = expandarg(w, e);
+			if(arg!=nil && do3(w, arg) < 0)
+				winwriteevent(w, e);
+			free(arg);
+			break;
+		}
+	}
+	winfree(w);
+}
+
+/*
+ * handle a button 3 click
+ */
+int
+do3(Win *w, char *text)
+{
+	char *addr, *name, *type, *server, *path, *p, *q;
+	static char lastfail[1000];
+
+	if(text[0] == '/')
+		name = estrdup(text);
+	else{
+		p = wingetname(w);
+		q = strrchr(p, '/');
+		*(q+1) = 0;
+		name = emalloc(strlen(p)+1+strlen(text)+1);
+		strcpy(name, p);
+		strcat(name, "/");
+		strcat(name, text);
+	}
+	dprint("do3 %s => %s\n", text, name);
+	if((addr = strchr(name, ':')) != nil)
+		*addr++ = 0;
+	cleanname(name);
+	if(strcmp(name, lastfail) == 0){
+		free(name);
+		return -1;
+	}
+	if(parsename(name, &server, &path) < 0){
+		free(name);
+		return -1;
+	}
+	type = filestat(server, path);
+	free(server);
+	free(path);
+	if(strcmp(type, "file")==0 || strcmp(type, "directory")==0){
+		w = nametowin(name);
+		if(w == nil)
+			w = mkwin(name);
+		winaddr(w, "%s", addr);
+		winctl(w, "dot=addr");
+		winctl(w, "show");
+		free(name);
+		free(type);
+		return 0;
+	}
+	/*
+	 * remember last name that didn't exist so that
+	 * only the first right-click is slow when searching for text.
+	 */
+	strecpy(lastfail, lastfail+sizeof lastfail, name);
+	free(name);
+	return -1;
+}
+
+Win*
+mkwin(char *name)
+{
+	Arg *a;
+	Channel *c;
+	Win *w;
+	
+	c = chancreate(sizeof(void*), 0);
+	a = arg(name, nil, c);
+	threadcreate(filethread, a, STACK);
+	w = recvp(c);
+	chanfree(c);
+	return w;
+}
+
+void
+loopthread(void *v)
+{
+	QLock lk;
+	
+	qlock(&lk);
+	qlock(&lk);
+}
+	
+void
+threadmain(int argc, char **argv)
+{
+	ARGBEGIN{
+	case '9':
+		chatty9pclient = 1;
+		break;
+	case 'D':
+		debug = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+	
+	if(argc)
+		usage();
+
+	threadnotify(nil, 0);	/* set up correct default handlers */
+
+	fmtinstall('E', eventfmt);
+	doquote = needsrcquote;
+	quotefmtinstall();
+	
+	twaitinit();
+	threadcreate(plumbthread, nil, STACK);
+	threadcreate(loopthread, nil, STACK);
+	threadexits(nil);
+}
+
diff --git a/src/cmd/netfiles/mkfile b/src/cmd/netfiles/mkfile
new file mode 100644
index 0000000..7b8a033
--- /dev/null
+++ b/src/cmd/netfiles/mkfile
@@ -0,0 +1,28 @@
+<$PLAN9/src/mkhdr
+
+TARG=Netfiles
+
+OFILES=\
+	acme.$O\
+	main.$O\
+	wait.$O\
+	
+HFILES=acme.h
+
+<$PLAN9/src/mkone
+
+XTARG=\
+	netget\
+	netput\
+	netstat\
+
+install:V:
+	for i in $XTARG; do
+		cp $i $BIN
+	done
+
+push:V:
+	tar cf - mkfile acme.c main.c wait.c acme.h netget netput netstat |
+	gzip >netfiles.tar.gz
+	scp netfiles.tar.gz swtch.com:www/swtch.com
+
diff --git a/src/cmd/netfiles/netfileget b/src/cmd/netfiles/netfileget
new file mode 100755
index 0000000..c621482
--- /dev/null
+++ b/src/cmd/netfiles/netfileget
@@ -0,0 +1,45 @@
+#!/usr/local/plan9/bin/rc
+
+f=getfile
+if(~ $1 -d){
+	f=getdir
+	shift
+}
+
+if(! ~ $#* 2){
+	echo 'usage: netget [-d] system path' >[1=2]
+	exit usage
+}
+
+ns=`{namespace}
+if(u test -S $ns/$1)
+	f=$f^9p
+
+t=/tmp/netget.$pid.$USER
+fn sigexit { rm -f $t }
+
+fn getfile {
+	rm -f $t
+	if(! echo get $2 $t | sftp -b - $1 >/dev/null)
+		exit 1
+	cat $t
+}
+
+fn getfile9p {
+	if(! 9p read $1/$2)
+		exit 1
+}
+
+fn getdir {
+	if(! {echo cd $2; echo ls -l} | sftp -b - $1 | sed '1,2d; s/sftp> //g; /^$/d' >$t)
+		exit 1
+	cat $t | awk '$NF == "." || $NF == ".." { next } {s = $NF; if($0 ~ /^d/) s = s "/"; print s}'
+}
+
+fn getdir9p {
+	9p ls -l $1/$2 | awk '{s=$NF; if($0 ~ /^d/) s=s"/"; print s}'
+}
+
+$f $1 $2
+exit 0
+
diff --git a/src/cmd/netfiles/netfileput b/src/cmd/netfiles/netfileput
new file mode 100755
index 0000000..baa3eb1
--- /dev/null
+++ b/src/cmd/netfiles/netfileput
@@ -0,0 +1,27 @@
+#!/usr/local/plan9/bin/rc
+
+if(! ~ $#* 2){
+	echo 'usage: netput system path' >[1=2]
+	exit usage
+}
+
+f=putfile
+ns=`{namespace}
+if(u test -S $ns/$1)
+	f=$f^9p
+
+t=/tmp/netget.$pid.$USER
+fn sigexit { rm -f $t }
+
+fn putfile{
+	cat >$t
+	if(! echo put $t $2 | sftp -b - $1 >/dev/null)
+		exit 1
+}
+fn putfile9p{
+	if(! 9p write $1/$2)
+		exit 1
+}
+
+$f $1 $2
+exit 0
diff --git a/src/cmd/netfiles/netfilestat b/src/cmd/netfiles/netfilestat
new file mode 100755
index 0000000..1d687e5
--- /dev/null
+++ b/src/cmd/netfiles/netfilestat
@@ -0,0 +1,52 @@
+#!/usr/local/plan9/bin/rc
+
+if(! ~ $#* 2){
+	echo usage: netisdir system path >[1=2]
+	exit usage
+}
+
+f=dostat
+ns=`{namespace}
+if(u test -S $ns/$1)
+	f=$f^9p
+
+t=/tmp/netisdir.$pid.$USER
+fn sigexit { rm -f $t }
+
+fn dostat {
+	{
+		echo !echo XXX connected
+		echo cd $2
+		echo !echo XXX directory exists
+	}  | sftp -b - $1 >$t >[2=1]
+	if(9 grep -s XXX.directory.exists $t){
+		echo directory
+		exit 0
+	}
+	if(9 grep -s 'is not a directory' $t){
+		echo file
+		exit 0
+	}
+	cat $t | sed 's/sftp> //g; /^$/d; /XXX/d; /^cd /d' >[1=2]
+	if(! 9 grep -s XXX.connected $t){
+		echo connect failed
+		exit 0
+	}
+	echo nonexistent
+	exit 0
+}
+
+fn dostat9p {
+	if(! 9p ls -ld $1/$2 >$t >[2]/dev/null){
+		echo nonexistent
+		exit 0
+	}
+	if(9 grep -s '^d' $t){
+		echo directory
+		exit 0
+	}
+	echo file
+	exit 0
+}
+
+$f $1 $2
diff --git a/src/cmd/netfiles/wait.c b/src/cmd/netfiles/wait.c
new file mode 100644
index 0000000..6f31a29
--- /dev/null
+++ b/src/cmd/netfiles/wait.c
@@ -0,0 +1,120 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <9pclient.h>
+#include "acme.h"
+
+extern int debug;
+
+#define dprint if(debug)print
+
+typedef struct Waitreq Waitreq;
+struct Waitreq
+{
+	int pid;
+	Channel *c;
+};
+
+/*
+ * watch the exiting children
+ */
+Channel *twaitchan;	/* chan(Waitreq) */
+void
+waitthread(void *v)
+{
+	Alt a[3];
+	Waitmsg *w, **wq;
+	Waitreq *rq, r;
+	int i, nrq, nwq;
+
+	threadsetname("waitthread");
+	a[0].c = threadwaitchan();
+	a[0].v = &w;
+	a[0].op = CHANRCV;
+	a[1].c = twaitchan;
+	a[1].v = &r;
+	a[1].op = CHANRCV;
+	a[2].op = CHANEND;
+
+	nrq = 0;
+	nwq = 0;
+	rq = nil;
+	wq = nil;
+	dprint("wait: start\n");
+	for(;;){
+	cont2:;
+		dprint("wait: alt\n");
+		switch(alt(a)){
+		case 0:
+			dprint("wait: pid %d exited\n", w->pid);
+			for(i=0; i<nrq; i++){
+				if(rq[i].pid == w->pid){
+					dprint("wait: match with rq chan %p\n", rq[i].c);
+					sendp(rq[i].c, w);
+					rq[i] = rq[--nrq];
+					goto cont2;
+				}
+			}
+			if(i == nrq){
+				dprint("wait: queueing waitmsg\n");
+				wq = erealloc(wq, (nwq+1)*sizeof(wq[0]));
+				wq[nwq++] = w;
+			}
+			break;
+		
+		case 1:
+			dprint("wait: req for pid %d chan %p\n", r.pid, r.c);
+			for(i=0; i<nwq; i++){
+				if(w->pid == r.pid){
+					dprint("wait: match with waitmsg\n");
+					sendp(r.c, w);
+					wq[i] = wq[--nwq];
+					goto cont2;
+				}
+			}
+			if(i == nwq){
+				dprint("wait: queueing req\n");
+				rq = erealloc(rq, (nrq+1)*sizeof(rq[0]));
+				rq[nrq] = r;
+				dprint("wait: queueing req pid %d chan %p\n", rq[nrq].pid, rq[nrq].c);
+				nrq++;
+			}
+			break;
+		}
+	}
+}
+
+Waitmsg*
+twaitfor(int pid)
+{
+	Waitreq r;
+	Waitmsg *w;
+	
+	r.pid = pid;
+	r.c = chancreate(sizeof(Waitmsg*), 1);
+	send(twaitchan, &r);
+	w = recvp(r.c);
+	chanfree(r.c);
+	return w;
+}
+
+int
+twait(int pid)
+{
+	int x;
+	Waitmsg *w;
+	
+	w = twaitfor(pid);
+	x = w->msg[0] != 0 ? -1 : 0;
+	free(w);
+	return x;
+}
+
+void
+twaitinit(void)
+{
+	threadwaitchan();	/* allocate it before returning */
+	twaitchan = chancreate(sizeof(Waitreq), 10);
+	threadcreate(waitthread, nil, 128*1024);
+}
+