shithub: fork

Download patch

ref: f9625c0942200c16a4ab48e4505b792cc75f9cc7
parent: db10b4c70cf1c7eeaca2b51d415bdf0f37da23c7
author: qwx <qwx@sciops.net>
date: Sat Aug 19 05:13:03 EDT 2023

ip/gping: themeshit

wasn't commited already?

--- /dev/null
+++ b/sys/src/cmd/ip/gping.c
@@ -1,0 +1,1055 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <auth.h>
+#include <fcall.h>
+#include <draw.h>
+#include <event.h>
+#include <ip.h>
+#include "icmp.h"
+
+#define	MAXNUM	8	/* maximum number of numbers on data line */
+
+typedef struct Graph	Graph;
+typedef struct Machine	Machine;
+typedef struct Req	Req;
+
+enum {
+	Gmsglen	= 16,
+};
+
+struct Graph
+{
+	int		colindex;
+	Rectangle	r;
+	long		*data;
+	int		ndata;
+	char		*label;
+	void		(*newvalue)(Machine*, long*, long*, long*);
+	void		(*update)(Graph*, long, long, long);
+	Machine		*mach;
+	int		overflow;
+	Image		*overtmp;
+	int		overtmplen;
+	char		msg[Gmsglen];
+	int		cursor;
+	int		vmax;
+};
+
+enum
+{
+	MSGLEN		= 64,
+
+	Rttmax		= 50,
+};
+
+struct Req
+{
+	int	seq;	/* sequence number */
+	vlong	time;	/* time sent */
+	Req	*next;
+};
+
+struct Machine
+{
+	Lock;
+	char	*name;
+	int	version;
+	int	pingfd;
+	int	nproc;
+
+	int	rttmsgs;
+	ulong	rttsum;
+	ulong	lastrtt;
+
+	int	lostmsgs;
+	int	rcvdmsgs;
+	ulong	lostavg;
+	int	unreachable;
+
+	ushort	seq;
+	Req	*list;
+};
+
+enum
+{
+	Ncolor		= 6,
+	Ysqueeze	= 2,	/* vertical squeezing of label text */
+	Labspace	= 2,	/* room around label */
+	Dot		= 2,	/* height of dot */
+	Opwid		= 5,	/* strlen("add  ") or strlen("drop ") */
+	NPROC		= 128,
+	NMACH		= 32,
+};
+
+enum Menu2
+{
+	Mrtt,
+	Mlost,
+	Nmenu2,
+};
+
+char	*menu2str[Nmenu2+1] = {
+	"add  sec rtt",
+	"add  % lost ",
+	nil,
+};
+
+
+void	rttval(Machine*, long*, long*, long*);
+void	lostval(Machine*, long*, long*, long*);
+
+Menu	menu2 = {menu2str, nil};
+int		present[Nmenu2];
+void		(*newvaluefn[Nmenu2])(Machine*, long*, long*, long*) = {
+	rttval,
+	lostval,
+};
+
+Image		*cols[Ncolor][3];
+
+enum{
+	Cback,
+	Cbord,
+	Ctext,
+	Cmix1,
+	Cmix2,
+	Nscolor = Cmix2,
+	Ccol1,
+	Ccol2,
+	Ccol3,
+	Ccol4,
+	Ccol5,
+	Ccol6,
+	Ntcolor,
+};
+
+ Image	*cols[Ncolor][3];
+Image	*tcols[Nscolor];
+Graph		*graph;
+Machine		mach[NMACH];
+int		pids[NPROC];
+int		npid;
+int 		parity;	/* toggled to avoid patterns in textured background */
+int		nmach;
+int		ngraph;	/* totaly number is ngraph*nmach */
+long		starttime;
+int		pinginterval;
+
+void	dropgraph(int);
+void	addgraph(int);
+void	startproc(void (*)(void*), void*);
+void	resize(void);
+long	rttscale(long);
+int	which2index(int);
+int	index2which(int);
+
+void
+killall(char *s)
+{
+	int i, pid;
+
+	pid = getpid();
+	for(i=0; i<NPROC; i++)
+		if(pids[i] && pids[i]!=pid)
+			postnote(PNPROC, pids[i], "kill");
+	exits(s);
+}
+
+void*
+emalloc(ulong sz)
+{
+	void *v;
+	v = malloc(sz);
+	if(v == nil) {
+		fprint(2, "%s: out of memory allocating %ld: %r\n", argv0, sz);
+		killall("mem");
+	}
+	memset(v, 0, sz);
+	return v;
+}
+
+void*
+erealloc(void *v, ulong sz)
+{
+	v = realloc(v, sz);
+	if(v == nil) {
+		fprint(2, "%s: out of memory reallocating %ld: %r\n", argv0, sz);
+		killall("mem");
+	}
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+	if((t = strdup(s)) == nil) {
+		fprint(2, "%s: out of memory in strdup(%.10s): %r\n", argv0, s);
+		killall("mem");
+	}
+	return t;
+}
+
+void
+mkcol(int i, int mix, int mix2, int c)
+{
+	cols[i][0] = allocimagemix(display, c, mix);
+	cols[i][1] = allocimagemix(display, c, mix2);
+	cols[i][2] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, c);
+}
+
+void
+colinit(Theme th[Ntcolor])
+{
+	mkcol(0, th[Cmix1].c, th[Cmix2].c, th[Ccol1].c);
+	mkcol(1, th[Cmix1].c, th[Cmix2].c, th[Ccol2].c);
+	mkcol(2, th[Cmix1].c, th[Cmix2].c, th[Ccol3].c);
+	mkcol(3, th[Cmix1].c, th[Cmix2].c, th[Ccol4].c);
+	mkcol(4, th[Cmix1].c, th[Cmix2].c, th[Ccol5].c);
+	mkcol(5, th[Cmix1].c, th[Cmix2].c, th[Ccol6].c);
+}
+
+void
+label(Point p, int dy, char *text)
+{
+	char *s;
+	Rune r[2];
+	int w, maxw, maxy;
+
+	p.x += Labspace;
+	maxy = p.y+dy;
+	maxw = 0;
+	r[1] = '\0';
+	for(s=text; *s; ){
+		if(p.y+font->height-Ysqueeze > maxy)
+			break;
+		w = chartorune(r, s);
+		s += w;
+		w = runestringwidth(font, r);
+		if(w > maxw)
+			maxw = w;
+		runestring(screen, p, tcols[Ctext], ZP, font, r);
+		p.y += font->height-Ysqueeze;
+	}
+}
+
+void
+hashmark(Point p, int dy, long v, long vmax, char *label)
+{
+	int y;
+	int x;
+
+	x = p.x + Labspace;
+	y = p.y + (dy*(vmax-v))/vmax;
+	draw(screen, Rect(p.x, y-1, p.x+Labspace, y+1), tcols[Cbord], nil, ZP);
+	if(dy > 5*font->height)
+		string(screen, Pt(x, y-font->height/2),
+			tcols[Ctext], ZP, font, label);
+}
+
+void
+hashmarks(Point p, int dy, int which)
+{
+	switch(index2which(which)){
+	case Mrtt:
+		hashmark(p, dy, rttscale(1000000), Rttmax, "1.");
+		hashmark(p, dy, rttscale(100000), Rttmax, "0.1");
+		hashmark(p, dy, rttscale(10000), Rttmax, "0.01");
+		hashmark(p, dy, rttscale(1000), Rttmax, "0.001");
+		break;
+	case Mlost:
+		hashmark(p, dy, 75, 100, " 75%");
+		hashmark(p, dy, 50, 100, " 50%");
+		hashmark(p, dy, 25, 100, " 25%");
+		break;
+	}
+}
+
+Point
+paritypt(int x)
+{
+	return Pt(x+parity, 0);
+}
+
+Point
+datapoint(Graph *g, int x, long v, long vmax)
+{
+	Point p;
+
+	p.x = x;
+	p.y = g->r.max.y - Dy(g->r)*v/vmax - Dot;
+	if(p.y < g->r.min.y)
+		p.y = g->r.min.y;
+	if(p.y > g->r.max.y-Dot)
+		p.y = g->r.max.y-Dot;
+	return p;
+}
+
+void
+drawdatum(Graph *g, int x, long prev, long v, long vmax)
+{
+	int c;
+	Point p, q;
+
+	c = g->colindex;
+	p = datapoint(g, x, v, vmax);
+	q = datapoint(g, x, prev, vmax);
+	if(p.y < q.y){
+		draw(screen, Rect(p.x, g->r.min.y, p.x+1, p.y), cols[c][0], nil, paritypt(p.x));
+		draw(screen, Rect(p.x, p.y, p.x+1, q.y+Dot), cols[c][2], nil, ZP);
+		draw(screen, Rect(p.x, q.y+Dot, p.x+1, g->r.max.y), cols[c][1], nil, ZP);
+	}else{
+		draw(screen, Rect(p.x, g->r.min.y, p.x+1, q.y), cols[c][0], nil, paritypt(p.x));
+		draw(screen, Rect(p.x, q.y, p.x+1, p.y+Dot), cols[c][2], nil, ZP);
+		draw(screen, Rect(p.x, p.y+Dot, p.x+1, g->r.max.y), cols[c][1], nil, ZP);
+	}
+	g->vmax = vmax;
+}
+
+void
+drawmark(Graph *g, int x)
+{
+	int c;
+
+	c = (g->colindex+1)&Ncolor;
+	draw(screen, Rect(x, g->r.min.y, x+1, g->r.max.y), cols[c][2], nil, ZP);
+}
+
+void
+redraw(Graph *g, int vmax)
+{
+	int i, c;
+
+	c = g->colindex;
+	draw(screen, g->r, cols[c][0], nil, paritypt(g->r.min.x));
+	for(i=1; i<Dx(g->r); i++)
+		drawdatum(g, g->r.max.x-i, g->data[i-1], g->data[i], vmax);
+	drawdatum(g, g->r.min.x, g->data[i], g->data[i], vmax);
+}
+
+void
+clearmsg(Graph *g)
+{
+	if(g->overtmp != nil)
+		draw(screen, g->overtmp->r, g->overtmp, nil, g->overtmp->r.min);
+	g->overflow = 0;
+}
+
+void
+drawmsg(Graph *g, char *msg)
+{
+	if(g->overtmp == nil)
+		return;
+
+	/* save previous contents of screen */
+	draw(g->overtmp, g->overtmp->r, screen, nil, g->overtmp->r.min);
+
+	/* draw message */
+	if(strlen(msg) > g->overtmplen)
+		msg[g->overtmplen] = 0;
+	string(screen, g->overtmp->r.min, tcols[Ctext], ZP, font, msg);
+}
+
+void
+clearcursor(Graph *g)
+{
+	int x;
+	long prev;
+
+	if(g->overtmp == nil)
+		return;
+
+	if(g->cursor > 0 && g->cursor < g->ndata){
+		x = g->r.max.x - g->cursor;
+		prev = 0;
+		if(g->cursor > 0)
+			prev = g->data[g->cursor-1];
+		drawdatum(g, x, prev, g->data[g->cursor], g->vmax);
+		g->cursor = -1;
+	}
+}
+
+void
+drawcursor(Graph *g, int x)
+{
+	if(g->overtmp == nil)
+		return;
+
+	draw(screen, Rect(x, g->r.min.y, x+1, g->r.max.y), cols[g->colindex][2], nil, ZP);
+}
+
+void
+update1(Graph *g, long v, long vmax, long mark)
+{
+	char buf[Gmsglen];
+
+	/* put back screen value sans message */
+	if(g->overflow || *g->msg){
+		clearmsg(g);
+		g->overflow = 0;
+	}
+
+	draw(screen, g->r, screen, nil, Pt(g->r.min.x+1, g->r.min.y));
+	drawdatum(g, g->r.max.x-1, g->data[0], v, vmax);
+	if(mark)
+		drawmark(g, g->r.max.x-1);
+	memmove(g->data+1, g->data, (g->ndata-1)*sizeof(g->data[0]));
+	g->data[0] = v;
+	if(v>vmax){
+		g->overflow = 1;
+		sprint(buf, "%ld", v);
+		drawmsg(g, buf);
+	} else if(*g->msg)
+		drawmsg(g, g->msg);
+
+	if(g->cursor >= 0){
+		g->cursor++;
+		if(g->cursor >= g->ndata){
+			g->cursor = -1;
+			if(*g->msg){
+				clearmsg(g);
+				*g->msg = 0;
+			}
+		}
+	}
+
+}
+
+void
+pinglost(Machine *m, Req*)
+{
+	m->lostmsgs++;
+}
+
+void
+pingreply(Machine *m, Req *r)
+{
+	ulong x;
+
+	x = r->time/1000LL;
+	m->rttsum += x;
+	m->rcvdmsgs++;
+	m->rttmsgs++;
+}
+
+
+void
+pingclean(Machine *m, ushort seq, vlong now)
+{
+	Req **l, *r;
+	vlong x, y;
+
+	y = 10LL*1000000000LL;
+	for(l = &m->list; *l; ){
+		r = *l;
+		x = now - r->time;
+		if(x > y || r->seq == seq){
+			*l = r->next;
+			r->time = x;
+			if(r->seq != seq)
+				pinglost(m, r);
+			else
+				pingreply(m, r);
+			free(r);
+		} else
+			l = &(r->next);
+	}
+}
+
+void
+pingsend(Machine *m)
+{
+	int i;
+	uchar buf[128];
+	char err[ERRMAX];
+	Icmphdr *ip;
+	Req *r;
+
+	ip = (Icmphdr *)(buf + (m->version==4? IPV4HDR_LEN: IPV6HDR_LEN));
+	memset(buf, 0, sizeof buf);
+	r = malloc(sizeof *r);
+	if(r == nil)
+		return;
+
+	for(i = ip->data-buf; i < MSGLEN; i++)
+		buf[i] = i;
+	ip->type = m->version==4? EchoRequest: EchoRequestV6;
+	ip->code = 0;
+	ip->seq[0] = m->seq;
+	ip->seq[1] = m->seq>>8;
+	r->seq = m->seq;
+	r->time = nsec();
+	lock(m);
+	pingclean(m, -1, r->time);
+	r->next = m->list;
+	m->list = r;
+	unlock(m);
+	if(write(m->pingfd, buf, MSGLEN) < MSGLEN){
+		errstr(err, sizeof err);
+		if(strstr(err, "unreach")||strstr(err, "exceed"))
+			m->unreachable++;
+	}
+	m->seq++;
+}
+
+void
+pingrcv(void *arg)
+{
+	int i, n;
+	uchar buf[512];
+	ushort x;
+	Icmphdr *ip;
+	Machine *m = arg;
+
+	ip = (Icmphdr *)(buf + (m->version==4? IPV4HDR_LEN: IPV6HDR_LEN));
+	for(;;){
+		n = read(m->pingfd, buf, sizeof(buf));
+		if(n <= 0)
+			break;
+		if(n < MSGLEN)
+			continue;
+		for(i = ip->data-buf; i < MSGLEN; i++)
+			if(buf[i] != (i&0xff))
+				break;
+		if(i != MSGLEN)
+			continue;
+		x = (ip->seq[1]<<8) | ip->seq[0];
+		if(ip->type != (m->version==4? EchoReply: EchoReplyV6) || ip->code != 0)
+			continue;
+		lock(m);
+		pingclean(m, x, nsec());
+		unlock(m);
+	}
+}
+
+void
+initmach(Machine *m, char *name)
+{
+	int cfd = -1;
+	char *p;
+
+	srand(time(0));
+	p = strchr(name, '!');
+	if(p){
+		p++;
+		m->name = estrdup(p+1);
+	}else
+		p = name;
+	m->name = estrdup(p);
+	m->nproc = 1;
+
+	m->version = 4;
+	if(strstr(name, "icmpv6!") != nil)
+		m->version = 6;
+again:
+	m->pingfd = dial(netmkaddr(m->name, m->version==4? "icmp": "icmpv6", "1"), nil, nil, &cfd);
+	if(m->pingfd < 0){
+		if(m->version == 4){
+			m->version = 6;
+			goto again;
+		}
+		sysfatal("dialing %s: %r", m->name);
+	}
+	write(cfd, "ignoreadvice", 12);
+	close(cfd);
+	startproc(pingrcv, m);
+}
+
+long
+rttscale(long x)
+{
+	if(x == 0)
+		return 0;
+	x = 10.0*log10(x) - 20.0;
+	if(x < 0)
+		x = 0;
+	return x;
+}
+
+double
+rttunscale(long x)
+{
+	double dx;
+
+	x += 20;
+	dx = x;
+	return pow(10.0, dx/10.0);
+}
+
+void
+rttval(Machine *m, long *v, long *vmax, long *mark)
+{
+	ulong x;
+
+	if(m->rttmsgs == 0){
+		x = m->lastrtt;
+	} else {
+		x = m->rttsum/m->rttmsgs;
+		m->rttsum = m->rttmsgs = 0;
+		m->lastrtt = x;
+	}
+
+	*v = rttscale(x);
+	*vmax = Rttmax;
+	*mark = 0;
+}
+
+void
+lostval(Machine *m, long *v, long *vmax, long *mark)
+{
+	ulong x;
+
+	if(m->rcvdmsgs+m->lostmsgs > 0)
+		x = (m->lostavg>>1) + (((m->lostmsgs*100)/(m->lostmsgs + m->rcvdmsgs))>>1);
+	else
+		x = m->lostavg;
+	m->lostavg = x;
+	m->lostmsgs = m->rcvdmsgs = 0;
+
+	if(m->unreachable){
+		m->unreachable = 0;
+		*mark = 100;
+	} else
+		*mark = 0;
+
+	*v = x;
+	*vmax = 100;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s machine [machine...]\n", argv0);
+	exits("usage");
+}
+
+void
+addgraph(int n)
+{
+	Graph *g, *ograph;
+	int i, j;
+	static int nadd;
+
+	if(n > nelem(menu2str))
+		abort();
+	/* avoid two adjacent graphs of same color */
+	if(ngraph>0 && graph[ngraph-1].colindex==nadd%Ncolor)
+		nadd++;
+	ograph = graph;
+	graph = emalloc(nmach*(ngraph+1)*sizeof(Graph));
+	for(i=0; i<nmach; i++)
+		for(j=0; j<ngraph; j++)
+			graph[i*(ngraph+1)+j] = ograph[i*ngraph+j];
+	free(ograph);
+	ngraph++;
+	for(i=0; i<nmach; i++){
+		g = &graph[i*ngraph+(ngraph-1)];
+		memset(g, 0, sizeof(Graph));
+		g->label = menu2str[n]+Opwid;
+		g->newvalue = newvaluefn[n];
+		g->update = update1;	/* no other update functions yet */
+		g->mach = &mach[i];
+		g->colindex = nadd%Ncolor;
+	}
+	present[n] = 1;
+	nadd++;
+}
+
+int
+which2index(int which)
+{
+	int i, n;
+
+	n = -1;
+	for(i=0; i<ngraph; i++){
+		if(strcmp(menu2str[which]+Opwid, graph[i].label) == 0){
+			n = i;
+			break;
+		}
+	}
+	if(n < 0){
+		fprint(2, "%s: internal error can't drop graph\n", argv0);
+		killall("error");
+	}
+	return n;
+}
+
+int
+index2which(int index)
+{
+	int i, n;
+
+	n = -1;
+	for(i=0; i<Nmenu2; i++){
+		if(strcmp(menu2str[i]+Opwid, graph[index].label) == 0){
+			n = i;
+			break;
+		}
+	}
+	if(n < 0){
+		fprint(2, "%s: internal error can't identify graph\n", argv0);
+		killall("error");
+	}
+	return n;
+}
+
+void
+dropgraph(int which)
+{
+	Graph *ograph;
+	int i, j, n;
+
+	if(which > nelem(menu2str))
+		abort();
+	/* convert n to index in graph table */
+	n = which2index(which);
+	ograph = graph;
+	graph = emalloc(nmach*(ngraph-1)*sizeof(Graph));
+	for(i=0; i<nmach; i++){
+		for(j=0; j<n; j++)
+			graph[i*(ngraph-1)+j] = ograph[i*ngraph+j];
+		free(ograph[i*ngraph+j].data);
+		freeimage(ograph[i*ngraph+j].overtmp);
+		for(j++; j<ngraph; j++)
+			graph[i*(ngraph-1)+j-1] = ograph[i*ngraph+j];
+	}
+	free(ograph);
+	ngraph--;
+	present[which] = 0;
+}
+
+void
+addmachine(char *name)
+{
+	if(ngraph > 0){
+		fprint(2, "%s: internal error: ngraph>0 in addmachine()\n", argv0);
+		usage();
+	}
+	if(nmach == NMACH)
+		sysfatal("too many machines");
+	initmach(&mach[nmach++], name);
+}
+
+
+void
+resize(void)
+{
+	int i, j, n, startx, starty, x, y, dx, dy, hashdx, ondata;
+	Graph *g;
+	Rectangle machr, r;
+	long v, vmax, mark;
+	char buf[128];
+
+	draw(screen, screen->r, tcols[Cback], nil, ZP);
+
+	/* label left edge */
+	x = screen->r.min.x;
+	y = screen->r.min.y + Labspace+font->height+Labspace;
+	dy = (screen->r.max.y - y)/ngraph;
+	dx = Labspace+stringwidth(font, "0")+Labspace;
+	startx = x+dx+1;
+	starty = y;
+	for(i=0; i<ngraph; i++,y+=dy){
+		draw(screen, Rect(x, y-1, screen->r.max.x, y), tcols[Cbord], nil, ZP);
+		draw(screen, Rect(x, y, x+dx, screen->r.max.y), cols[graph[i].colindex][0], nil, paritypt(x));
+		label(Pt(x, y), dy, graph[i].label);
+		draw(screen, Rect(x+dx, y, x+dx+1, screen->r.max.y), cols[graph[i].colindex][2], nil, ZP);
+	}
+
+	/* label right edge */
+	dx = Labspace+stringwidth(font, "0.001")+Labspace;
+	hashdx = dx;
+	x = screen->r.max.x - dx;
+	y = screen->r.min.y + Labspace+font->height+Labspace;
+	for(i=0; i<ngraph; i++,y+=dy){
+		draw(screen, Rect(x, y-1, screen->r.max.x, y), tcols[Cbord], nil, ZP);
+		draw(screen, Rect(x, y, x+dx, screen->r.max.y), cols[graph[i].colindex][0], nil, paritypt(x));
+		hashmarks(Pt(x, y), dy, i);
+		draw(screen, Rect(x+dx, y, x+dx+1, screen->r.max.y), cols[graph[i].colindex][2], nil, ZP);
+	}
+
+	/* label top edge */
+	dx = (screen->r.max.x - dx - startx)/nmach;
+	for(x=startx, i=0; i<nmach; i++,x+=dx){
+		draw(screen, Rect(x-1, starty-1, x, screen->r.max.y), tcols[Cbord], nil, ZP);
+		j = dx/stringwidth(font, "0");
+		n = mach[i].nproc;
+		if(n>1 && j>=1+3+(n>10)+(n>100)){	/* first char of name + (n) */
+			j -= 3+(n>10)+(n>100);
+			if(j <= 0)
+				j = 1;
+			snprint(buf, sizeof buf, "%.*s(%d)", j, mach[i].name, n);
+		}else
+			snprint(buf, sizeof buf, "%.*s", j, mach[i].name);
+		string(screen, Pt(x+Labspace, screen->r.min.y + Labspace), tcols[Ctext], ZP,
+			font, buf);
+	}
+	/* draw last vertical line */
+	draw(screen,
+		Rect(screen->r.max.x-hashdx-1, starty-1, screen->r.max.x-hashdx, screen->r.max.y),
+		tcols[Cbord], nil, ZP);
+
+	/* create graphs */
+	for(i=0; i<nmach; i++){
+		machr = Rect(startx+i*dx, starty, screen->r.max.x, screen->r.max.y);
+		if(i < nmach-1)
+			machr.max.x = startx+(i+1)*dx - 1;
+		else
+			machr.max.x = screen->r.max.x - hashdx - 1;
+		y = starty;
+		for(j=0; j<ngraph; j++, y+=dy){
+			g = &graph[i*ngraph+j];
+			/* allocate data */
+			ondata = g->ndata;
+			g->ndata = Dx(machr)+1;	/* may be too many if label will be drawn here; so what? */
+			g->data = erealloc(g->data, g->ndata*sizeof(long));
+			if(g->ndata > ondata)
+				memset(g->data+ondata, 0, (g->ndata-ondata)*sizeof(long));
+			/* set geometry */
+			g->r = machr;
+			g->r.min.y = y;
+			g->r.max.y = y+dy - 1;
+			if(j == ngraph-1)
+				g->r.max.y = screen->r.max.y;
+			draw(screen, g->r, cols[g->colindex][0], nil, paritypt(g->r.min.x));
+			g->overflow = 0;
+			*g->msg = 0;
+			freeimage(g->overtmp);
+			g->overtmp = nil;
+			g->overtmplen = 0;
+			r = g->r;
+			r.max.y = r.min.y+font->height;
+			n = (g->r.max.x - r.min.x)/stringwidth(font, "9");
+			if(n > 4){
+				if(n > Gmsglen)
+					n = Gmsglen;
+				r.max.x = r.min.x+stringwidth(font, "9")*n;
+				g->overtmplen = n;
+				g->overtmp = allocimage(display, r, screen->chan, 0, -1);
+			}
+			g->newvalue(g->mach, &v, &vmax, &mark);
+			redraw(g, vmax);
+		}
+	}
+
+	flushimage(display, 1);
+}
+
+void
+eresized(int new)
+{
+	lockdisplay(display);
+	if(new && getwindow(display, Refnone) < 0) {
+		fprint(2, "%s: can't reattach to window\n", argv0);
+		killall("reattach");
+	}
+	resize();
+	unlockdisplay(display);
+}
+
+void
+dobutton2(Mouse *m)
+{
+	int i;
+
+	for(i=0; i<Nmenu2; i++)
+		if(present[i])
+			memmove(menu2str[i], "drop ", Opwid);
+		else
+			memmove(menu2str[i], "add  ", Opwid);
+	i = emenuhit(3, m, &menu2);
+	if(i >= 0){
+		if(!present[i])
+			addgraph(i);
+		else if(ngraph > 1)
+			dropgraph(i);
+		resize();
+	}
+}
+
+void
+dobutton1(Mouse *m)
+{
+	int i, n, dx, dt;
+	Graph *g;
+	char *e;
+	double f;
+
+	for(i = 0; i < ngraph*nmach; i++){
+		if(ptinrect(m->xy, graph[i].r))
+			break;
+	}
+	if(i == ngraph*nmach)
+		return;
+
+	g = &graph[i];
+	if(g->overtmp == nil)
+		return;
+
+	/* clear any previous message and cursor */
+	if(g->overflow || *g->msg){
+		clearmsg(g);
+		*g->msg = 0;
+		clearcursor(g);
+	}
+
+	dx = g->r.max.x - m->xy.x;
+	g->cursor = dx;
+	dt = dx*pinginterval;
+	e = &g->msg[sizeof(g->msg)];
+	seprint(g->msg, e, "%s", ctime(starttime-dt/1000)+11);
+	g->msg[8] = 0;
+	n = 8;
+
+	switch(index2which(i)){
+	case Mrtt:
+		f = rttunscale(g->data[dx]);
+		seprint(g->msg+n, e, " %3.3g", f/1000000);
+		break;
+	case Mlost:
+		seprint(g->msg+n, e, " %ld%%", g->data[dx]);
+		break;
+	}
+
+	drawmsg(g, g->msg);
+	drawcursor(g, m->xy.x);
+}
+
+void
+mouseproc(void*)
+{
+	Mouse mouse;
+
+	for(;;){
+		mouse = emouse();
+		if(mouse.buttons == 4){
+			lockdisplay(display);
+			dobutton2(&mouse);
+			unlockdisplay(display);
+		} else if(mouse.buttons == 1){
+			lockdisplay(display);
+			dobutton1(&mouse);
+			unlockdisplay(display);
+		}
+	}
+}
+
+void
+startproc(void (*f)(void*), void *arg)
+{
+	int pid;
+
+	switch(pid = rfork(RFPROC|RFMEM|RFNOWAIT)){
+	case -1:
+		fprint(2, "%s: fork failed: %r\n", argv0);
+		killall("fork failed");
+	case 0:
+		f(arg);
+		killall("process died");
+		exits(nil);
+	}
+	pids[npid++] = pid;
+}
+
+void
+main(int argc, char *argv[])
+{
+	int i, j;
+	long v, vmax, mark;
+	char flags[10], *f, *p;
+
+	fmtinstall('V', eipfmt);
+
+	f = flags;
+	pinginterval = 5000;		/* 5 seconds */
+	ARGBEGIN{
+	case 'i':
+		p = ARGF();
+		if(p == nil)
+			usage();
+		pinginterval = atoi(p);
+		break;
+	default:
+		if(f - flags >= sizeof(flags)-1)
+			usage();
+		*f++ = ARGC();
+		break;
+	}ARGEND
+	*f = 0;
+
+	pids[npid++] = getpid();
+
+	for(i=0; i<argc; i++)
+		addmachine(argv[i]);
+
+	for(f = flags; *f; f++)
+		switch(*f){
+		case 'l':
+			addgraph(Mlost);
+			break;
+		case 'r':
+			addgraph(Mrtt);
+			break;
+		}
+
+	if(nmach == 0)
+		usage();
+
+	if(ngraph == 0){
+		addgraph(Mrtt);
+		addgraph(Mlost);
+	}
+
+	for(i=0; i<nmach; i++)
+		for(j=0; j<ngraph; j++)
+			graph[i*ngraph+j].mach = &mach[i];
+
+	if(initdraw(nil, nil, argv0) < 0){
+		fprint(2, "%s: initdraw failed: %r\n", argv0);
+		exits("initdraw");
+	}
+	display->locking = 1;	/* tell library we're using the display lock */
+
+	Theme th[Ntcolor] = {
+		[Cback] { "back",	DWhite },
+		[Cbord] { "border",	DBlack },
+		[Ctext] { "text",	DBlack },
+		[Cmix1] { "rioback",	DWhite },
+		[Cmix2] { "palehold",	DWhite },
+		[Ccol1] { "htext",	0xFFAAAAFF },
+		[Ccol2] { "high",	DPurpleblue },
+		[Ccol3] { "hold",	DYellowgreen },
+		[Ccol4] { "size",	DDarkgreen },
+		[Ccol5] { "title",	0x0088CCFF },
+		[Ccol6] { "paletext",	0x888888FF },
+	};
+	readtheme(th, nelem(th), nil);
+	for(i=0; i<nelem(tcols); i++)
+ 		tcols[i] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[i].c);
+	colinit(th);
+	einit(Emouse);
+	startproc(mouseproc, 0);
+
+	resize();
+
+	starttime = time(0);
+
+	unlockdisplay(display); /* display is still locked from initdraw() */
+	for(j = 0; ; j++){
+		lockdisplay(display);
+		if(j == nmach){
+			parity = 1-parity;
+			j = 0;
+			for(i=0; i<nmach*ngraph; i++){
+				graph[i].newvalue(graph[i].mach, &v, &vmax, &mark);
+				graph[i].update(&graph[i], v, vmax, mark);
+			}
+			starttime = time(0);
+		}
+		flushimage(display, 1);
+		unlockdisplay(display);
+		pingsend(&mach[j%nmach]);
+		sleep(pinginterval/nmach);
+	}
+}