| /* | 
 |  * 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>1)print | 
 | #define cprint 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 | 
 | }; | 
 |  | 
 | char *debugstr[] = { | 
 | 	"off", | 
 | 	"minimal", | 
 | 	"chatty" | 
 | }; | 
 |  | 
 | 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; | 
 | } | 
 |  | 
 | Win* | 
 | winbyid(int id) | 
 | { | 
 | 	Win *w; | 
 | 	 | 
 | 	for(w=windows; w; w=w->next) | 
 | 		if(w->id == id) | 
 | 			return w; | 
 | 	return nil; | 
 | } | 
 |  | 
 | /* | 
 |  * return Win* of a window named name or name/ | 
 |  * assumes name is cleaned. | 
 |  */ | 
 | Win* | 
 | nametowin(char *name) | 
 | { | 
 | 	char *index, *p, *next; | 
 | 	int len, n; | 
 | 	Win *w; | 
 |  | 
 | 	index = winindex(); | 
 | 	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); | 
 | 		if((w = winbyid(n)) != nil){ | 
 | 			free(index); | 
 | 			return w; | 
 | 		} | 
 | 	} | 
 | 	free(index); | 
 | 	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"); | 
 | } | 
 |  | 
 | int | 
 | isdot(Win *w, uint xq0, uint xq1) | 
 | { | 
 | 	uint q0, q1; | 
 |  | 
 | 	winctl(w, "addr=dot"); | 
 | 	q0 = winreadaddr(w, &q1); | 
 | 	return xq0==q0 && xq1==q1; | 
 | } | 
 |  | 
 | /* | 
 |  * Expand the click further than acme usually does -- all non-white space is okay. | 
 |  */ | 
 | char* | 
 | expandarg(Win *w, Event *e) | 
 | { | 
 | 	uint q0, q1; | 
 | 	 | 
 | 	if(e->c2 == 'l')	/* in tag - no choice but to accept acme's expansion */ | 
 | 		return estrdup(e->text); | 
 | 	winaddr(w, ","); | 
 | 	winctl(w, "addr=dot"); | 
 |  | 
 | 	q0 = winreadaddr(w, &q1); | 
 | 	cprint("acme expanded %d-%d into %d-%d (dot %d-%d)\n", | 
 | 		e->oq0, e->oq1, e->q0, e->q1, q0, q1); | 
 |  | 
 | 	if(e->oq0 == e->oq1 && e->q0 != e->q1 && !isdot(w, e->q0, e->q1)){ | 
 | 		winaddr(w, "#%ud+#1-/[^ \t\\n]*/,#%ud-#1+/[^ \t\\n]*/", e->q0, e->q1); | 
 | 		q0 = winreadaddr(w, &q1); | 
 | 		cprint("\tre-expand to %d-%d\n", q0, 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; | 
 | 	 | 
 | 	threadsetname("plumbthread"); | 
 | 	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) | 
 | { | 
 | 	char *type; | 
 | 	static struct { | 
 | 		char *server; | 
 | 		char *path; | 
 | 		char *type; | 
 | 	} cache; | 
 |  | 
 | 	if(cache.server && strcmp(server, cache.server) == 0) | 
 | 	if(cache.path && strcmp(path, cache.path) == 0){ | 
 | 		type = estrdup(cache.type); | 
 | 		cprint("9 netfilestat %q %q => %s (cached)\n", server, path, type); | 
 | 		return type; | 
 | 	} | 
 |  | 
 | 	type = sysrun(2, "9 netfilestat %q %q", server, path); | 
 |  | 
 | 	free(cache.server); | 
 | 	free(cache.path); | 
 | 	free(cache.type); | 
 | 	cache.server = estrdup(server); | 
 | 	cache.path = estrdup(path); | 
 | 	cache.type = estrdup(type); | 
 |  | 
 | 	cprint("9 netfilestat %q %q => %s\n", server, path, type); | 
 | 	return type; | 
 | } | 
 |  | 
 | /* | 
 |  * 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, ","); | 
 | 					cprint("9 netfileget %s%q %q\n",  | 
 | 						strcmp(type, "file") == 0 ? "" : "-d", server, path); | 
 | 					if(strcmp(type, "file")==0) | 
 | 						twait(pipetowin(w, "data", 2, "9 netfileget %q %q", server, path)); | 
 | 					else | 
 | 						twait(pipetowin(w, "data", 2, "9 netfileget -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; | 
 | 				} | 
 | 				cprint("9 netfileput %q %q\n", server, path); | 
 | 				if(twait(pipewinto(w, "body", 2, "9 netfileput %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+1)%3; | 
 | 				print("Netfiles debug %s\n", debugstr[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] == '/'){ | 
 | 		p = nil; | 
 | 		name = estrdup(text); | 
 | 	}else{ | 
 | 		p = wingetname(w); | 
 | 		if(text[0] != ':'){ | 
 | 			q = strrchr(p, '/'); | 
 | 			*(q+1) = 0; | 
 | 		} | 
 | 		name = emalloc(strlen(p)+1+strlen(text)+1); | 
 | 		strcpy(name, p); | 
 | 		if(text[0] != ':') | 
 | 			strcat(name, "/"); | 
 | 		strcat(name, text); | 
 | 	} | 
 | 	cprint("button3 %s %s => %s\n", p, text, name); | 
 | 	if((addr = strchr(name, ':')) != nil) | 
 | 		*addr++ = 0; | 
 | 	cleanname(name); | 
 | 	cprint("b3 \t=> name=%s addr=%s\n", name, addr); | 
 | 	if(strcmp(name, lastfail) == 0){ | 
 | 		cprint("b3 \t=> nonexistent (cached)\n"); | 
 | 		free(name); | 
 | 		return -1; | 
 | 	} | 
 | 	if(parsename(name, &server, &path) < 0){ | 
 | 		cprint("b3 \t=> parsename failed\n"); | 
 | 		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); | 
 | 			cprint("b3 \t=> creating new window %d\n", w->id); | 
 | 		}else | 
 | 			cprint("b3 \t=> reusing window %d\n", w->id); | 
 | 		if(addr){ | 
 | 			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. | 
 | 	 */ | 
 | 	cprint("b3 caching %s => type %s\n", name, type); | 
 | 	strecpy(lastfail, lastfail+sizeof lastfail, name); | 
 | 	free(name); | 
 | 	free(type); | 
 | 	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; | 
 | 	 | 
 | 	threadsetname("loopthread"); | 
 | 	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(); | 
 |  | 
 | 	cprint("netfiles starting\n"); | 
 |  | 
 | 	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); | 
 | } | 
 |  |