diff options
Diffstat (limited to 'appl/collab')
| -rw-r--r-- | appl/collab/clients/chat.b | 224 | ||||
| -rw-r--r-- | appl/collab/clients/poll.b | 282 | ||||
| -rw-r--r-- | appl/collab/clients/poller.b | 330 | ||||
| -rw-r--r-- | appl/collab/clients/whiteboard.b | 586 | ||||
| -rw-r--r-- | appl/collab/collabsrv.b | 176 | ||||
| -rw-r--r-- | appl/collab/connect.b | 156 | ||||
| -rw-r--r-- | appl/collab/lib/messages.b | 86 | ||||
| -rw-r--r-- | appl/collab/lib/messages.m | 42 | ||||
| -rw-r--r-- | appl/collab/mkfile | 62 | ||||
| -rw-r--r-- | appl/collab/proxy.b | 177 | ||||
| -rw-r--r-- | appl/collab/proxy.m | 5 | ||||
| -rwxr-xr-x | appl/collab/runcollab | 8 | ||||
| -rw-r--r-- | appl/collab/servers/chatsrv.b | 263 | ||||
| -rw-r--r-- | appl/collab/servers/memfssrv.b | 20 | ||||
| -rw-r--r-- | appl/collab/servers/mpx.b | 301 | ||||
| -rw-r--r-- | appl/collab/servers/wbsrv.b | 226 | ||||
| -rw-r--r-- | appl/collab/service.m | 4 | ||||
| -rw-r--r-- | appl/collab/srvmgr.b | 190 | ||||
| -rw-r--r-- | appl/collab/srvmgr.m | 22 |
19 files changed, 3160 insertions, 0 deletions
diff --git a/appl/collab/clients/chat.b b/appl/collab/clients/chat.b new file mode 100644 index 00000000..d9c78d4b --- /dev/null +++ b/appl/collab/clients/chat.b @@ -0,0 +1,224 @@ +implement Chat; + +include "sys.m"; + sys: Sys; + +include "draw.m"; + draw: Draw; + +include "tk.m"; + tk: Tk; + +include "tkclient.m"; + tkclient: Tkclient; + +Chat: module { + init: fn(ctxt: ref Draw->Context, args: list of string); +}; + +stderr: ref Sys->FD; + +tksetup := array [] of { + "frame .f", + "text .f.t -state disabled -wrap word -yscrollcommand {.f.sb set}", + "scrollbar .f.sb -orient vertical -command {.f.t yview}", + "entry .e -bg white", + "bind .e <Key-\n> {send cmd send}", + "pack .f.sb -in .f -side left -fill y", + "pack .f.t -in .f -side left -fill both -expand 1", + "pack .f -side top -fill both -expand 1", + "pack .e -side bottom -fill x", + "pack propagate . 0", + "update", +}; + +init(ctxt: ref Draw->Context, args: list of string) +{ + sys = load Sys Sys->PATH; + sys->pctl(Sys->NEWPGRP, nil); + stderr = sys->fildes(2); + + draw = load Draw Draw->PATH; + if (draw == nil) + badmodule(Draw->PATH); + + tk = load Tk Tk->PATH; + if (tk == nil) + badmodule(Tk->PATH); + + tkclient = load Tkclient Tkclient->PATH; + if (tkclient == nil) + badmodule(Tkclient->PATH); + + + if (len args < 2) { + sys->fprint(stderr, "usage: chat [servicedir] room\n"); + raise "fail:init"; + } + args = tl args; + + servicedir := "/n/remote/services"; + if(len args == 2) + (servicedir, args) = (hd args, tl args); + room := hd args; + + tkclient->init(); + (win, winctl) := tkclient->toplevel(ctxt, nil, sys->sprint("Chat %s", room), Tkclient->Appl); + + cmd := chan of string; + tk->namechan(win, cmd, "cmd"); + tkcmds(win, tksetup); + tkcmd(win, ". configure -height 300"); + fittoscreen(win); + tkclient->onscreen(win, nil); + tkclient->startinput(win, "kbd"::"ptr"::nil); + + msgs := chan of string; + conn := chan of (string, ref Sys->FD); + spawn connect(servicedir, room, msgs, conn); + msgsfd: ref Sys->FD; + + for (;;) alt { + (e, fd) := <-conn => + if (msgsfd == nil) { + if (e == nil) { + output(win, "*** connected"); + msgsfd = fd; + } else + output(win, "*** " + e); + } else { + output(win, "*** disconnected"); + msgsfd = nil; + } + + txt := <-msgs => + output(win, txt); + + <- cmd => + msg := tkcmd(win, ".e get"); + if (msgsfd != nil && msg != nil) { + tkcmd(win, ".f.t see end"); + tkcmd(win, ".e delete 0 end"); + tkcmd(win, "update"); + d := array of byte msg; + sys->write(msgsfd, d, len d); + } + + s := <-win.ctxt.kbd => + tk->keyboard(win, s); + s := <-win.ctxt.ptr => + tk->pointer(win, *s); + s := <-win.ctxt.ctl or + s = <-win.wreq or + s = <-winctl => + tkclient->wmctl(win, s); + } +} + +err(s: string) +{ + sys->fprint(stderr, "chat: %s\n", s); + raise "fail:err"; +} + +badmodule(path: string) +{ + err(sys->sprint("can't load module %s: %r", path)); +} + +tkcmds(t: ref Tk->Toplevel, cmds: array of string) +{ + for (i := 0; i < len cmds; i++) + tkcmd(t, cmds[i]); +} + +tkcmd(t: ref Tk->Toplevel, cmd: string): string +{ + s := tk->cmd(t, cmd); + if (s != nil && s[0] == '!') + sys->fprint(stderr, "chat: tk error: %s [%s]\n", s, cmd); + return s; +} + +connect(dir, name: string, msgs: chan of string, conn: chan of (string, ref Sys->FD)) +{ + (ctlfd, srvdir, emsg) := opensvc(dir, "chat", name); + if(ctlfd == nil) { + conn <-= (emsg, nil); + return; + } + srvpath := srvdir+"/msgs"; + msgsfd := sys->open(srvpath, Sys->ORDWR); + if(msgsfd == nil) { + conn <-= (sys->sprint("internal error: can't open %s: %r", srvpath), nil); + return; + } + conn <-= (nil, msgsfd); + buf := array[Sys->ATOMICIO] of byte; + while((n := sys->read(msgsfd, buf, len buf)) > 0) + msgs <-= string buf[0:n]; + conn <-= (nil, nil); +} + +opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string) +{ + ctlfd := sys->open(dir+"/ctl", Sys->ORDWR); + if(ctlfd == nil) + return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir)); + if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0) + return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name)); + buf := array [32] of byte; + sys->seek(ctlfd, big 0, Sys->SEEKSTART); + n := sys->read(ctlfd, buf, len buf); + if (n <= 0) + return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir)); + return (ctlfd, dir+"/"+string buf[0:n], nil); +} + +firstmsg := 1; +output(win: ref Tk->Toplevel, txt: string) +{ + if (firstmsg) + firstmsg = 0; + else + txt = "\n" + txt; + yview := tkcmd(win, ".f.t yview"); + (nil, toks) := sys->tokenize(yview, " "); + toks = tl toks; + + tkcmd(win, ".f.t insert end '" + txt); + if (hd toks == "1") + tkcmd(win, ".f.t see end"); + tkcmd(win, "update"); +} + +KEYBOARDH: con 90; + +fittoscreen(win: ref Tk->Toplevel) +{ + Point, Rect: import draw; + if (win.image == nil || win.image.screen == nil) + return; + r := win.image.screen.image.r; + scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y)- KEYBOARDH); + bd := int tkcmd(win, ". cget -bd"); + winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2); + if (winsize.x > scrsize.x) + tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2)); + if (winsize.y > scrsize.y) + tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2)); + actr: Rect; + actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty")); + actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2, + int tkcmd(win, ". cget -actheight") + bd*2)); + (dx, dy) := (actr.dx(), actr.dy()); + if (actr.max.x > r.max.x) + (actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx); + if (actr.max.y > r.max.y) + (actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy); + if (actr.min.x < r.min.x) + (actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx); + if (actr.min.y < r.min.y) + (actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy); + tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y); +} diff --git a/appl/collab/clients/poll.b b/appl/collab/clients/poll.b new file mode 100644 index 00000000..ffb79c73 --- /dev/null +++ b/appl/collab/clients/poll.b @@ -0,0 +1,282 @@ +implement Poll; + +include "sys.m"; + sys: Sys; + +include "draw.m"; + +include "tk.m"; + tk: Tk; + +include "tkclient.m"; + tkclient: Tkclient; + +include "dialog.m"; + dialog: Dialog; + +include "arg.m"; + +Poll: module +{ + init: fn(nil: ref Draw->Context, nil: list of string); +}; + +Maxanswer: con 4; + +contents := array[] of { + "frame .f", + "frame .a", + "radiobutton .a.a1 -state disabled -variable answer -value A -text {A} -command {send entry A}", + "radiobutton .a.a2 -state disabled -variable answer -value B -text {B} -command {send entry B}", + "radiobutton .a.a3 -state disabled -variable answer -value C -text {C} -command {send entry C}", + "radiobutton .a.a4 -state disabled -variable answer -value D -text {D} -command {send entry D}", + "pack .a.a1 -side top -fill x -expand 1", + "pack .a.a2 -side top -fill x -expand 1", + "pack .a.a3 -side top -fill x -expand 1", + "pack .a.a4 -side top -fill x -expand 1", + "pack .a -side top -fill both -expand 1", + "pack .f -side top -fill both", +}; + +dbcontents := array[] of { + "text .f.t -state disabled -wrap word -yscrollcommand {.f.sb set} -height 4h", + "scrollbar .f.sb -orient vertical -command {.f.t yview}", + "pack .f.sb -side left -fill y", + "pack .f.t -side left -fill both -expand 1", +}; + +usage() +{ + sys->fprint(sys->fildes(2), "usage: poll [-d] [servicedir] pollname\n"); + raise "fail:usage"; +} + +init(ctxt: ref Draw->Context, args: list of string) +{ + sys = load Sys Sys->PATH; + tk = load Tk Tk->PATH; + tkclient = load Tkclient Tkclient->PATH; + dialog = load Dialog Dialog->PATH; + + arg := load Arg Arg->PATH; + if(arg == nil){ + sys->fprint(sys->fildes(2), "poll: can't load %s: %r\n", Arg->PATH); + raise "fail:load"; + } + arg->init(args); + debug := 0; + while((ch := arg->opt()) != 0) + case ch { + 'd' => + debug = 1; + * => + usage(); + } + args = arg->argv(); + arg = nil; + if(len args < 1) + usage(); + sys->pctl(Sys->NEWPGRP, nil); + + servicedir := "/n/remote/services"; + if(len args == 2) + (servicedir, args) = (hd args, tl args); + pollname := hd args; + + (cfd, dir, emsg) := opensvc(servicedir, "mpx", pollname); + if(cfd == nil){ + sys->fprint(sys->fildes(2), "poll: can't access poll %s: %s\n", pollname, emsg); + raise "fail:error"; + } + fd := sys->open(dir+"/leaf", Sys->ORDWR); + if(fd == nil){ + sys->fprint(sys->fildes(2), "poll: can't open %s/leaf: %r\n", dir); + raise "fail:open"; + } + + tkclient->init(); + dialog->init(); + (frame, wmctl) := tkclient->toplevel(ctxt, nil, sys->sprint("Poll %s", pollname), Tkclient->Appl); + entry := chan of string; + tk->namechan(frame, entry, "entry"); + tkcmds(frame, contents); + if(debug) + tkcmds(frame, dbcontents); + tkcmd(frame, "pack propagate . 0"); + fittoscreen(frame); + tk->cmd(frame, "update"); + tkclient->onscreen(frame, nil); + tkclient->startinput(frame, "kbd"::"ptr"::nil); + + in := chan of string; + spawn reader(fd, in); + first := 1; + lastval := -1; + qno := -1; + for(;;) + alt{ + s := <-frame.ctxt.kbd => + tk->keyboard(frame, s); + s := <-frame.ctxt.ptr => + tk->pointer(frame, *s); + s := <-frame.ctxt.ctl or + s = <-frame.wreq or + s = <-wmctl => + tkclient->wmctl(frame, s); + + msg := <-entry => + if(fd == nil){ + dialog->prompt(ctxt, frame.image, "error -fg red", "Error", "Lost connection to polling station", 0, "Dismiss"::nil); + break; + } + n := msg[0]-'A'; + lastval = n; + selectonly(frame, n, Maxanswer, "disabled"); + if(qno >= 0) { + # send our answer to the polling station + if(sys->fprint(fd, "%d %s", qno, msg) < 0){ + sys->fprint(sys->fildes(2), "poll: write error: %r\n"); + fd = nil; + } + qno = -1; # only one go at it + } + + s := <-in => + if(s != nil){ + if(debug){ + t := s; + if(!first) + t = "\n"+t; + first = 0; + tk->cmd(frame, ".f.t insert end '" + t); + tk->cmd(frame, ".f.t see end"); + tk->cmd(frame, "update"); + } + (nf, flds) := sys->tokenize(s, " "); + if(nf > 1 && hd flds == "error:"){ + dialog->prompt(ctxt, frame.image, "error -fg red", "Error", sys->sprint("polling station reports: %s", s), 0, "Dismiss"::nil); + break; + } + if(nf < 4) + break; + # seq clientid op name data + op, name: string; + flds = tl flds; # ignore seq + flds = tl flds; # ignore clientid + (op, flds) = (hd flds, tl flds); + (name, flds) = (hd flds, tl flds); + case op { + "M" => + # poll qno nanswer opt + # stop qno + selectonly(frame, -1, Maxanswer, "disabled"); + if(len flds < 2) + break; + (op, flds) = (hd flds, tl flds); + (s, flds) = (hd flds, tl flds); + case op { + "poll" => + qno = int s; + (s, flds) = (hd flds, tl flds); + n := int s; + if(n > Maxanswer) + n = Maxanswer; + if(n < 2) + n = 2; + selectonly(frame, -1, n, "normal"); + lastval = -1; + "stop" => + selectonly(frame, lastval, Maxanswer, "disabled"); + } + "L" => + dialog->prompt(ctxt, frame.image, "error -fg red", "Notice", sys->sprint("Poller (%s) has gone", name), 0, "Exit"::nil); + tkclient->wmctl(frame, "exit"); + } + }else{ + dialog->prompt(ctxt, frame.image, "error -fg red", "Notice", "Polling station closed", 0, "Exit"::nil); + tkclient->wmctl(frame, "exit"); + } + } +} + +selectonly(t: ref Tk->Toplevel, n: int, top: int, state: string) +{ + for(i := 0; i < top; i++){ + path := sys->sprint(".a.a%d", i+1); + if(i != n) + tkcmd(t, path+" deselect"); + else + tkcmd(t, path+" select"); + tkcmd(t, path+" configure -state "+state); + } + tk->cmd(t, "update"); +} + +reader(fd: ref Sys->FD, c: chan of string) +{ + buf := array[Sys->ATOMICIO] of byte; + while((n := sys->read(fd, buf, len buf)) > 0) + c <-= string buf[0:n]; + if(n < 0) + c <-= sys->sprint("error: %r"); + c <-= nil; +} + +opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string) +{ + ctlfd := sys->open(dir+"/ctl", Sys->ORDWR); + if(ctlfd == nil) + return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir)); + if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0) + return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name)); + buf := array [32] of byte; + sys->seek(ctlfd, big 0, Sys->SEEKSTART); + n := sys->read(ctlfd, buf, len buf); + if (n <= 0) + return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir)); + return (ctlfd, dir+"/"+string buf[0:n], nil); +} + +tkcmds(t: ref Tk->Toplevel, cmds: array of string) +{ + for (i := 0; i < len cmds; i++) + tkcmd(t, cmds[i]); +} + +tkcmd(t: ref Tk->Toplevel, cmd: string): string +{ + s := tk->cmd(t, cmd); + if (s != nil && s[0] == '!') + sys->fprint(sys->fildes(2), "poll: tk error: %s [%s]\n", s, cmd); + return s; +} + +fittoscreen(win: ref Tk->Toplevel) +{ + draw := load Draw Draw->PATH; + Point, Rect: import draw; + if (win.image == nil || win.image.screen == nil) + return; + r := win.image.screen.image.r; + scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y)); + bd := int tkcmd(win, ". cget -bd"); + winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2); + if (winsize.x > scrsize.x) + tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2)); + if (winsize.y > scrsize.y) + tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2)); + actr: Rect; + actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty")); + actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2, + int tkcmd(win, ". cget -actheight") + bd*2)); + (dx, dy) := (actr.dx(), actr.dy()); + if (actr.max.x > r.max.x) + (actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx); + if (actr.max.y > r.max.y) + (actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy); + if (actr.min.x < r.min.x) + (actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx); + if (actr.min.y < r.min.y) + (actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy); + tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y); +} diff --git a/appl/collab/clients/poller.b b/appl/collab/clients/poller.b new file mode 100644 index 00000000..90365606 --- /dev/null +++ b/appl/collab/clients/poller.b @@ -0,0 +1,330 @@ +implement Poller; + +include "sys.m"; + sys: Sys; + +include "draw.m"; + draw: Draw; + Rect, Point: import draw; + +include "tk.m"; + tk: Tk; + +include "tkclient.m"; + tkclient: Tkclient; + +include "dialog.m"; + dialog: Dialog; + +include "arg.m"; + +Poller: module +{ + init: fn(nil: ref Draw->Context, nil: list of string); +}; + +Maxanswer: con 4; # Tk below isn't parametrised, but could be + +contents := array[] of { + "frame .f", + "frame .f.n", + "label .f.l -anchor nw -text {Number of answers: }", + "radiobutton .f.n.a2 -text {2} -variable nanswer -value 2", + "radiobutton .f.n.a3 -text {3} -variable nanswer -value 3", + "radiobutton .f.n.a4 -text {4} -variable nanswer -value 4", + "pack .f.n.a2 .f.n.a3 .f.n.a4 -side left", + + "frame .f.b", + "button .f.b.start -text {Start} -command {send cmd start}", + "button .f.b.stop -text {Stop} -state disabled -command {send cmd stop}", + "pack .f.b.start .f.b.stop -side left", + + "canvas .f.c -height 230 -width 200", + + "pack .f.l -side top -fill x", + "pack .f.n -side top -fill x", + "pack .f.b -side top -fill x -expand 1", + "pack .f.c -side top -pady 2", + "pack .f -side top -fill both -expand 1", +}; + +dbcontents := array[] of { + "text .f.t -state disabled -wrap word -height 4h -yscrollcommand {.f.sb set}", # message log + "scrollbar .f.sb -orient vertical -command {.f.t yview}", + "pack .f.sb -side left -fill y", + "pack .f.t -side left -fill both", +}; + +Bar: adt { + frame: ref Tk->Toplevel; + canvas: string; + border: string; + inside: string; + label: string; + r: Rect; + v: real; + + draw: fn(nil: self ref Bar); +}; + +usage() +{ + sys->fprint(sys->fildes(2), "usage: poller [-d] [servicedir] pollname\n"); + raise "fail:usage"; +} + +init(ctxt: ref Draw->Context, args: list of string) +{ + sys = load Sys Sys->PATH; + draw = load Draw Draw->PATH; + tk = load Tk Tk->PATH; + tkclient = load Tkclient Tkclient->PATH; + dialog = load Dialog Dialog->PATH; + + arg := load Arg Arg->PATH; + if(arg == nil){ + sys->fprint(sys->fildes(2), "poller: can't load %s: %r\n", Arg->PATH); + raise "fail:load"; + } + arg->init(args); + debug := 0; + while((ch := arg->opt()) != 0) + case ch { + 'd' => + debug = 1; + * => + usage(); + } + args = arg->argv(); + arg = nil; + if(len args < 1) + usage(); + sys->pctl(Sys->NEWPGRP, nil); + + servicedir := "/n/remote/services"; + if(len args == 2) + (servicedir, args) = (hd args, tl args); + pollname := hd args; + + (cfd, dir, emsg) := opensvc(servicedir, "mpx", pollname); + if(cfd == nil){ + sys->fprint(sys->fildes(2), "poller: can't access polling station %s: %s\n", pollname, emsg); + raise "fail:error"; + } + fd := sys->open(dir+"/root", Sys->ORDWR); + if(fd == nil){ + sys->fprint(sys->fildes(2), "poller: can't open %s/root: %r\n", dir); + raise "fail:open"; + } + + tkclient->init(); + dialog->init(); + (frame, wmctl) := tkclient->toplevel(ctxt, nil, sys->sprint("Poller: %s", pollname), Tkclient->Appl); + cmd := chan of string; + tk->namechan(frame, cmd, "cmd"); + tkcmds(frame, contents); + if(debug) + tkcmds(frame, dbcontents); + tkcmd(frame, "pack propagate . 0"); + fittoscreen(frame); + tk->cmd(frame, "update"); + tkclient->onscreen(frame, nil); + tkclient->startinput(frame, "kbd"::"ptr"::nil); + + bars := mkbars(frame, ".f.c", Maxanswer); + count: array of int; + + in := chan of string; + spawn reader(fd, in); + first := 1; + qno := 0; + nanswer := 0; + opt := 0; + total := 0; + for(;;) + alt{ + s := <-frame.ctxt.kbd => + tk->keyboard(frame, s); + s := <-frame.ctxt.ptr => + tk->pointer(frame, *s); + s := <-frame.ctxt.ctl or + s = <-frame.wreq or + s = <-wmctl => + tkclient->wmctl(frame, s); + + c := <-cmd => + if(fd == nil){ + dialog->prompt(ctxt, frame.image, "error -fg red", "Error", "Lost connection to polling station", 0, "Dismiss"::nil); + break; + } + case c { + "start" => + s := tkcmd(frame, "variable nanswer"); + if(s == nil || s[0] == '!'){ + dialog->prompt(ctxt, frame.image, "error -fg red", "Error", "Please select number of answers", 0, "Ok"::nil); + break; + } + nanswer = int s; + count = array[Maxanswer] of {* => 0}; + total = 0; + qno++; + #opt = (int tkcmd(frame, "variable none") << 1) | int tkcmd(frame, "variable all"); + tkcmd(frame, ".f.b.start configure -state disabled"); + tkcmd(frame, ".f.b.stop configure -state normal"); + if(sys->fprint(fd, "poll %d %d %d", qno, nanswer, opt) <= 0) + sys->fprint(sys->fildes(2), "poller: write error: %r\n"); + "stop" => + tkcmd(frame, ".f.b.stop configure -state disabled"); + tkcmd(frame, "update"); + if(sys->fprint(fd, "stop %d", qno) <= 0) + sys->fprint(sys->fildes(2), "poller: write error: %r\n"); + # stop ... + tkcmd(frame, ".f.b.start configure -state normal"); + } + tk->cmd(frame, "update"); + + s := <-in => + if(s != nil){ + if(debug){ + t := s; + if(!first) + t = "\n"+t; + first = 0; + tkcmd(frame, ".f.t insert end '" + t); + tkcmd(frame, ".f.t see end"); + tkcmd(frame, "update"); + } + r := getresult(s, qno); + if(r < 0) + break; + if(r >= 0 && r < len count){ + count[r]++; + total++; + for(i:=0; i < len count; i++){ + bars[i].v = real count[i]/real total; + bars[i].draw(); + } + tk->cmd(frame, "update"); + } + #sys->print("%d %d\n", qno, r); + }else + fd = nil; + } +} + +mkbars(t: ref Tk->Toplevel, canvas: string, nbars: int): array of ref Bar +{ + x := 0; + a := array[nbars] of ref Bar; + for(i := 0; i < nbars; i++){ + b := ref Bar(t, canvas, nil, nil, nil, Rect((x,2),(x+20,202)), 0.0); + b.border = tkcmd(t, sys->sprint("%s create rectangle %d %d %d %d", + canvas, b.r.min.x,b.r.min.y,b.r.max.x,b.r.max.y)); + r := b.r.inset(1); + b.inside = tkcmd(t, sys->sprint("%s create rectangle %d %d %d %d -fill red", + canvas, r.max.x, r.max.y,r.max.x,r.max.y)); + b.label = tkcmd(t, sys->sprint("%s create text %d %d -justify center -anchor n -text '0%%", + canvas, (r.min.x+r.max.x)/2, r.max.y+4)); + a[i] = b; + x += 50; + } + tk->cmd(t, "update"); + return a; +} + +Bar.draw(b: self ref Bar) +{ + r := b.r.inset(2); + y := r.max.y - int (b.v * real r.dy()); + tkcmd(b.frame, sys->sprint("%s coords %s %d %d %d %d", + b.canvas, b.inside, r.min.x, y, r.max.x, r.max.y)); + tkcmd(b.frame, sys->sprint("%s itemconfigure %s -text '%.0f%%", + b.canvas, b.label, b.v*100.0)); +} + +getresult(msg: string, qno: int): int +{ + (nf, flds) := sys->tokenize(msg, " "); + if(nf < 5 || hd flds == "error:") + return -1; # not of interest + op := hd tl tl flds; + flds = tl tl tl flds; + if(op != "m") + return -1; # not a message from leaf + if(len flds < 3) + return -1; # bad format + flds = tl flds; # ignore user name + if(int hd flds != qno) + return -1; # not current question + result := hd tl flds; + if(result[0] >= 'A' && result[0] <= 'D') + return result[0]-'A'; + return -1; +} + +reader(fd: ref Sys->FD, c: chan of string) +{ + buf := array[Sys->ATOMICIO] of byte; + while((n := sys->read(fd, buf, len buf)) > 0) + c <-= string buf[0:n]; + if(n < 0) + c <-= sys->sprint("error: %r"); + c <-= nil; +} + +opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string) +{ + ctlfd := sys->open(dir+"/ctl", Sys->ORDWR); + if(ctlfd == nil) + return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir)); + if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0) + return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name)); + buf := array [32] of byte; + sys->seek(ctlfd, big 0, Sys->SEEKSTART); + n := sys->read(ctlfd, buf, len buf); + if (n <= 0) + return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir)); + return (ctlfd, dir+"/"+string buf[0:n], nil); +} + +tkcmds(t: ref Tk->Toplevel, cmds: array of string) +{ + for (i := 0; i < len cmds; i++) + tkcmd(t, cmds[i]); +} + +tkcmd(t: ref Tk->Toplevel, cmd: string): string +{ + s := tk->cmd(t, cmd); + if (s != nil && s[0] == '!') + sys->fprint(sys->fildes(2), "poller: tk error: %s [%s]\n", s, cmd); + return s; +} + +fittoscreen(win: ref Tk->Toplevel) +{ + if (win.image == nil || win.image.screen == nil) + return; + r := win.image.screen.image.r; + scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y)); + bd := int tkcmd(win, ". cget -bd"); + winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2); + if (winsize.x > scrsize.x) + tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2)); + if (winsize.y > scrsize.y) + tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2)); + actr: Rect; + actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty")); + actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2, + int tkcmd(win, ". cget -actheight") + bd*2)); + (dx, dy) := (actr.dx(), actr.dy()); + if (actr.max.x > r.max.x) + (actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx); + if (actr.max.y > r.max.y) + (actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy); + if (actr.min.x < r.min.x) + (actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx); + if (actr.min.y < r.min.y) + (actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy); + tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y); +} diff --git a/appl/collab/clients/whiteboard.b b/appl/collab/clients/whiteboard.b new file mode 100644 index 00000000..e05e1b45 --- /dev/null +++ b/appl/collab/clients/whiteboard.b @@ -0,0 +1,586 @@ +implement Whiteboard; + +include "sys.m"; + sys: Sys; + +include "draw.m"; + draw: Draw; + Screen, Display, Image, Rect, Point, Font: import draw; + +include "tk.m"; + tk: Tk; + +include "tkclient.m"; + tkclient: Tkclient; + +Whiteboard: module { + init: fn(ctxt: ref Draw->Context, args: list of string); +}; + +ERASEWIDTH: con 6; + + +stderr: ref Sys->FD; +srvfd: ref Sys->FD; +disp: ref Display; +font: ref Draw->Font; +drawctxt: ref Draw->Context; + +tksetup := array[] of { + "frame .f -bd 2", + "frame .c -bg white -width 234 -height 279", + "menu .penmenu", + ".penmenu add command -command {send cmd pen 0} -bitmap @/icons/whiteboard/0.bit", + ".penmenu add command -command {send cmd pen 1} -bitmap @/icons/whiteboard/1.bit", + ".penmenu add command -command {send cmd pen 2} -bitmap @/icons/whiteboard/2.bit", + ".penmenu add command -command {send cmd pen erase} -bitmap @/icons/whiteboard/erase.bit", + "menubutton .pen -menu .penmenu -bitmap @/icons/whiteboard/1.bit", + "button .colour -bg black -activebackground black -command {send cmd getcolour}", + "pack .c -in .f", + "pack .f -side top -anchor center", + "pack .pen -side left", + "pack .colour -side left -fill both -expand 1", + "update", +}; + +tkconnected := array[] of { + "bind .c <Button-1> {send cmd down %x %y}", + "bind .c <ButtonRelease-1> {send cmd up %x %y}", + "update", +}; + +init(ctxt: ref Draw->Context, args: list of string) +{ + sys = load Sys Sys->PATH; + sys->pctl(Sys->NEWPGRP, nil); + stderr = sys->fildes(2); + + draw = load Draw Draw->PATH; + tk = load Tk Tk->PATH; + + tkclient = load Tkclient Tkclient->PATH; + if (tkclient == nil) + badmod(Tkclient->PATH); + + if (len args < 2) { + sys->fprint(stderr, "Usage: whiteboard [servicedir] id\n"); + raise "fail:init"; + } + + args = tl args; + servicedir := "/n/remote/services"; + if(len args == 2) + (servicedir, args) = (hd args, tl args); + wbid := hd args; + + disp = ctxt.display; + if (disp == nil) { + sys->fprint(stderr, "bad Draw->Context\n"); + raise "fail:init"; + } + drawctxt = ctxt; + + tkclient->init(); + (win, winctl) := tkclient->toplevel(ctxt, nil, "Whiteboard", 0); + font = Font.open(disp, tkcmd(win, ". cget -font")); + if(font == nil) + font = Font.open(disp, "*default*"); + cmd := chan of string; + tk->namechan(win, cmd, "cmd"); + tkcmds(win, tksetup); + tkclient->onscreen(win, nil); + tkclient->startinput(win, "kbd" :: "ptr" :: nil); + cimage := makeimage(win); + + sc := chan of array of (Point, Point); + cc := chan of (string, ref Image, ref Sys->FD, ref Sys->FD); + connected := 0; + sfd: ref Sys->FD; + ctlfd: ref Sys->FD; # must keep this open to keep service active + + showtext(cimage, "connecting..."); + spawn connect(servicedir, wbid, cc); + + err: string; + strokeimg: ref Image; +Connect: + for (;;) alt { + (err, strokeimg, sfd, ctlfd) = <-cc => + if (err == nil) + break Connect; + else + showtext(cimage, "Error: " + err); + + s := <-winctl or + s = <-win.wreq or + s = <-win.ctxt.ctl => + oldimg := win.image; + err = tkclient->wmctl(win, s); + if(s[0] == '!' && err == nil && win.image != oldimg){ + cimage = makeimage(win); + showtext(cimage, "connecting..."); + } + p := <-win.ctxt.ptr => + tk->pointer(win, *p); + c := <-win.ctxt.kbd => + tk->keyboard(win, c); + } + + tkcmd(win, ".c configure -width " + string strokeimg.r.dx()); + tkcmd(win, ".c configure -height " + string strokeimg.r.dy()); + tkcmds(win, tkconnected); + tkcmd(win, "update"); + cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min); + + strokesin := chan of (int, int, array of Point); + strokesout := chan of (int, int, Point, Point); + spawn reader(sfd, strokesin); + spawn writer(sfd, strokesout); + + pendown := 0; + p0, p1: Point; + + getcolour := 0; + white := disp.white; + whitepen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, Draw->White); + pencolour := Draw->Black; + penwidth := 1; + erase := 0; + drawpen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour); + + for (;;) alt { + s := <-winctl or + s = <-win.ctxt.ctl or + s = <-win.wreq => + oldimg := win.image; + err = tkclient->wmctl(win, s); + if(s[0] == '!' && err == nil && win.image != oldimg){ + cimage = makeimage(win); + cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min); + } + p := <-win.ctxt.ptr => + tk->pointer(win, *p); + c := <-win.ctxt.kbd => + tk->keyboard(win, c); + (colour, width, strokes) := <-strokesin => + if (strokes == nil) + tkclient->settitle(win, "Whiteboard (Disconnected)"); + else { + pen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, colour); + drawstrokes(cimage, cimage.r.min, pen, width, strokes); + drawstrokes(strokeimg, strokeimg.r.min, pen, width, strokes); + } + + c := <-cmd => + (nil, toks) := sys->tokenize(c, " "); + case hd toks { + "down" => + toks = tl toks; + x := int hd toks; + y := int hd tl toks; + if (!pendown) { + pendown = 1; + p0 = Point(x, y); + continue; + } + p1 = Point(x, y); + if (p1.x == p0.x && p1.y == p0.y) + continue; + pen := drawpen; + colour := pencolour; + width := penwidth; + if (erase) { + pen = whitepen; + colour = Draw->White; + width = ERASEWIDTH; + } + drawstroke(cimage, cimage.r.min, p0, p1, pen, width); + drawstroke(strokeimg, strokeimg.r.min, p0, p1, pen, width); + strokesout <-= (colour, width, p0, p1); + p0 = p1; + "up" => + pendown = 0; + + "getcolour" => + pendown = 0; + if (!getcolour) + spawn colourmenu(cmd); + "colour" => + pendown = 0; + getcolour = 0; + toks = tl toks; + if (toks == nil) + # colourmenu was dismissed + continue; + erase = 0; + tkcmd(win, ".pen configure -bitmap @/icons/whiteboard/" + string penwidth + ".bit"); + tkcmd(win, "update"); + pencolour = int hd toks; + toks = tl toks; + tkcolour := hd toks; + drawpen = disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour); + tkcmd(win, ".colour configure -bg " + tkcolour + " -activebackground " + tkcolour); + tkcmd(win, "update"); + + "pen" => + pendown = 0; + p := hd tl toks; + i := ""; + if (p == "erase") { + erase = 1; + i = "erase.bit"; + } else { + erase = 0; + penwidth = int p; + i = p + ".bit"; + } + tkcmd(win, ".pen configure -bitmap @/icons/whiteboard/" + i); + tkcmd(win, "update"); + } + + } +} + +makeimage(win: ref Tk->Toplevel): ref Draw->Image +{ + if(win.image == nil) + return nil; + scr := Screen.allocate(win.image, win.image.display.white, 0); + w := scr.newwindow(tk->rect(win, ".c", Tk->Local), Draw->Refnone, Draw->Nofill); + return w; +} + +showtext(img: ref Image, s: string) +{ + r := img.r; + r.max.y = img.r.min.y + font.height; + img.draw(r, disp.white, nil, (0, 0)); + img.text(r.min, disp.black, (0, 0), font, s); +} + +penmenu(t: ref Tk->Toplevel, p: Point) +{ + topy := int tkcmd(t, ".penmenu yposition 0"); + boty := int tkcmd(t, ".penmenu yposition end"); + dy := boty - topy; + p.y -= dy; + tkcmd(t, ".penmenu post " + string p.x + " " + string p.y); +} + +colourcmds := array[] of { + "label .l -height 10", + "frame .c -height 224 -width 224", + "pack .l -fill x -expand 1", + "pack .c -side bottom -fill both -expand 1", + "pack propagate . 0", + "bind .c <Button-1> {send cmd push %x %y}", + "bind .c <ButtonRelease-1> {send cmd release}", +}; + +lastcolour := "255"; +lasttkcolour := "#000000"; + +colourmenu(c: chan of string) +{ + (t, winctl) := tkclient->toplevel(drawctxt, nil, "Whiteboard", Tkclient->OK); + cmd := chan of string; + tk->namechan(t, cmd, "cmd"); + tkcmds(t, colourcmds); + tkcmd(t, ".l configure -bg " + lasttkcolour); + tkcmd(t, "update"); + tkclient->onscreen(t, "onscreen"); + tkclient->startinput(t, "kbd" :: "ptr" :: nil); + + drawcolours(t.image, tk->rect(t, ".c", Tk->Local)); + + for(;;) alt { + p := <-t.ctxt.ptr => + tk->pointer(t, *p); + s := <-t.ctxt.kbd => + tk->keyboard(t, s); + s := <-winctl or + s = <-t.ctxt.ctl or + s = <-t.wreq => + case s{ + "ok" => + c <-= "colour " + lastcolour + " " + lasttkcolour; + return; + "exit" => + c <-= "colour"; + return; + * => + oldimage := t.image; + e := tkclient->wmctl(t, s); + if(s[0] == '!' && e == nil && oldimage != t.image) + drawcolours(t.image, tk->rect(t, ".c", Tk->Local)); + } + + press := <-cmd => + (n, word) := sys->tokenize(press, " "); + case hd word { + "push" => + (lastcolour, lasttkcolour) = color(int hd tl word, int hd tl tl word, tk->rect(t, ".c", 0).size()); + tkcmd(t, ".l configure -bg " + lasttkcolour); + } + } +} + +drawcolours(img: ref Image, cr: Rect) +{ + # use writepixels because it's much faster than allocating all those colors. + tmp := disp.newimage(((0,0),(cr.dx(),cr.dy()/16+1)), Draw->CMAP8, 0, 0); + if(tmp == nil) + return; + buf := array[tmp.r.dx()*tmp.r.dy()] of byte; + dx := cr.dx(); + dy := cr.dy(); + for(y:=0; y<16; y++){ + for(i:=tmp.r.dx()-1; i>=0; --i) + buf[i] = byte (16*y+(16*i)/dx); + for(k:=tmp.r.dy()-1; k>=1; --k) + buf[dx*k:] = buf[0:dx]; + tmp.writepixels(tmp.r, buf); + r: Rect; + r.min.x = cr.min.x; + r.max.x = cr.max.x; + r.min.y = cr.min.y+(dy*y)/16; + r.max.y = cr.min.y+(dy*(y+1))/16; + img.draw(r, tmp, nil, tmp.r.min); + } +} + +color(x, y: int, size: Point): (string, string) +{ + x = (16*x)/size.x; + y = (16*y)/size.y; + col := 16*y+x; + (r, g, b) := disp.cmap2rgb(col); + tks := sys->sprint("#%.2x%.2x%.2x", r, g, b); + return (string disp.cmap2rgba(col), tks); +} + +opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string) +{ + ctlfd := sys->open(dir+"/ctl", Sys->ORDWR); + if(ctlfd == nil) + return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir)); + if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0) + return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name)); + buf := array [32] of byte; + sys->seek(ctlfd, big 0, Sys->SEEKSTART); + n := sys->read(ctlfd, buf, len buf); + if (n <= 0) + return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir)); + return (ctlfd, dir+"/"+string buf[0:n], nil); +} + +connect(dir, name: string, res: chan of (string, ref Image, ref Sys->FD, ref Sys->FD)) +{ + (ctlfd, srvdir, emsg) := opensvc(dir, "whiteboard", name); + if(ctlfd == nil) { + res <-= (emsg, nil, nil, nil); + return; + } + + bitpath := srvdir + "/wb.bit"; + strokepath := srvdir + "/strokes"; + + sfd := sys->open(strokepath, Sys->ORDWR); + if (sfd == nil) { + err := sys->sprint("cannot open whiteboard data: %r"); + res <-= (err, nil, nil, nil); + srvfd = nil; + return; + } + + bfd := sys->open(bitpath, Sys->OREAD); + if (bfd == nil) { + err := sys->sprint("cannot open whiteboard image: %r"); + res <-= (err, nil, nil, nil); + srvfd = nil; + return; + } + + img := disp.readimage(bfd); + if (img == nil) { + err := sys->sprint("cannot read whiteboard image: %r"); + res <-= (err, nil, nil, nil); + srvfd = nil; + return; + } +sys->print("read image ok\n"); + + # make sure image is depth 8 (because of image.line() bug) + if (img.depth != 8) { +sys->print("depth is %d, not 8\n", img.depth); + nimg := disp.newimage(img.r, Draw->CMAP8, 0, 0); + if (nimg == nil) { + res <-= ("cannot allocate local image", nil, nil, nil); + srvfd = nil; + return; + } + nimg.draw(nimg.r, img, nil, img.r.min); + img = nimg; + } + + res <-= (nil, img, sfd, ctlfd); +} + +reader(fd: ref Sys->FD, sc: chan of (int, int, array of Point)) +{ + buf := array [Sys->ATOMICIO] of byte; + + for (;;) { + n := sys->read(fd, buf, len buf); + if (n <= 0) { + sc <-= (0, 0, nil); + return; + } + s := string buf[0:n]; + (npts, toks) := sys->tokenize(s, " "); + if (npts & 1) + # something wrong + npts--; + if (npts < 6) + # ignore + continue; + + colour, width: int; + (colour, toks) = (int hd toks, tl toks); + (width, toks) = (int hd toks, tl toks); + pts := array [(npts - 2)/ 2] of Point; + for (i := 0; toks != nil; i++) { + x, y: int; + (x, toks) = (int hd toks, tl toks); + (y, toks) = (int hd toks, tl toks); + pts[i] = Point(x, y); + } + sc <-= (colour, width, pts); + pts = nil; + } +} + +Wmsg: adt { + data: array of byte; + datalen: int; + next: cyclic ref Wmsg; +}; + +writer(fd: ref Sys->FD, sc: chan of (int, int, Point, Point)) +{ + lastcol := -1; + lastw := -1; + lastpt := Point(-1, -1); + curmsg: ref Wmsg; + nextmsg: ref Wmsg; + + eofc := chan of int; + wc := chan of ref Wmsg; + wseof := 0; + spawn wslave(fd, wc, eofc); + + for (;;) { + colour := -1; + width := 0; + p0, p1: Point; + + if (curmsg == nil || wseof) + (colour, width, p0, p1) = <-sc; + else alt { + wseof = <-eofc => + ; + + (colour, width, p0, p1) = <-sc => + ; + + wc <-= curmsg => + curmsg = curmsg.next; + continue; + } + + newseq := 0; + if (curmsg == nil) { + curmsg = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil); + nextmsg = curmsg; + newseq = 1; + } + + if (colour != lastcol || width != lastw || p0.x != lastpt.x || p0.y != lastpt.y) + newseq = 1; + + d: array of byte = nil; + if (!newseq) { + d = sys->aprint(" %d %d", p1.x, p1.y); + if (nextmsg.datalen + len d >= Sys->ATOMICIO) { + nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil); + nextmsg = nextmsg.next; + newseq = 1; + } + } + if (newseq) { + d = sys->aprint(" %d %d %d %d %d %d", colour, width, p0.x, p0.y, p1.x, p1.y); + if (nextmsg.datalen != 0) { + nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil); + nextmsg = nextmsg.next; + } + } + nextmsg.data[nextmsg.datalen:] = d; + nextmsg.datalen += len d; + lastcol = colour; + lastw = width; + lastpt = p1; + } +} + +wslave(fd: ref Sys->FD, wc: chan of ref Wmsg, eof: chan of int) +{ + for (;;) { + wm := <-wc; + n := sys->write(fd, wm.data, wm.datalen); + if (n != wm.datalen) + break; + } + eof <-= 1; +} + +drawstroke(img: ref Image, offset, p0, p1: Point, pen: ref Image, width: int) +{ + p0 = p0.add(offset); + p1 = p1.add(offset); + img.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, p0); +} + +drawstrokes(img: ref Image, offset: Point, pen: ref Image, width: int, pts: array of Point) +{ + if (len pts < 2) + return; + p0, p1: Point; + p0 = pts[0].add(offset); + for (i := 1; i < len pts; i++) { + p1 = pts[i].add(offset); + img.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, p0); + p0 = p1; + } +} + +badmod(mod: string) +{ + sys->fprint(stderr, "cannot load %s: %r\n", mod); + raise "fail:bad module"; +} + +tkcmd(t: ref Tk->Toplevel, cmd: string): string +{ + s := tk->cmd(t, cmd); + if (s != nil && s[0] == '!') { + sys->fprint(stderr, "%s\n", cmd); + sys->fprint(stderr, "tk error: %s\n", s); + } + return s; +} + +tkcmds(t: ref Tk->Toplevel, cmds: array of string) +{ + for (i := 0; i < len cmds; i++) + tkcmd(t, cmds[i]); +} diff --git a/appl/collab/collabsrv.b b/appl/collab/collabsrv.b new file mode 100644 index 00000000..f9f26011 --- /dev/null +++ b/appl/collab/collabsrv.b @@ -0,0 +1,176 @@ +implement Collabsrv; + +include "sys.m"; + sys: Sys; + +include "draw.m"; + +include "keyring.m"; +include "security.m"; + auth: Auth; + +include "srvmgr.m"; +include "proxy.m"; + +include "arg.m"; + +Collabsrv: module +{ + init: fn (ctxt: ref Draw->Context, args: list of string); +}; + +authinfo: ref Keyring->Authinfo; + +stderr: ref Sys->FD; +Srvreq, Srvreply: import Srvmgr; + +usage() +{ + sys->fprint(stderr, "usage: collabsrv [-k keyfile] [-n netaddress] [dir]\n"); + raise "fail:usage"; +} + +init(nil: ref Draw->Context, args: list of string) +{ + sys = load Sys Sys->PATH; + sys->pctl(Sys->NEWPGRP, nil); + stderr = sys->fildes(2); + + (err, user) := user(); + if (err != nil) + error(err); + + netaddr := "tcp!*!9999"; + keyfile := "/usr/" + user + "/keyring/default"; + root := "/services/collab"; + + arg := load Arg Arg->PATH; + arg->init(args); + while ((opt := arg->opt()) != 0) + case opt { + 'k' => + keyfile = arg->arg(); + if (keyfile == nil) + usage(); + if (keyfile[0] != '/' && (len keyfile < 2 || keyfile[0:2] != "./")) + keyfile = "/usr/" + user + "/keyring/" + keyfile; + 'n' => + netaddr = arg->arg(); + if (netaddr == nil) + usage(); + * => + usage(); + } + args = arg->argv(); + arg = nil; + if(args != nil) + root = hd args; + + auth = load Auth Auth->PATH; + if (auth == nil) + badmodule(Auth->PATH); + + kr := load Keyring Keyring->PATH; + if (kr == nil) + badmodule(Keyring->PATH); + + srvmgr := load Srvmgr Srvmgr->PATH; + if (srvmgr == nil) + badmodule(Srvmgr->PATH); + + err = auth->init(); + if (err != nil) + error(sys->sprint("failed to init Auth: %s", err)); + + authinfo = kr->readauthinfo(keyfile); + kr = nil; + if (authinfo == nil) + error(sys->sprint("cannot read %s: %r", keyfile)); + + netaddr = netmkaddr(netaddr, "tcp", "9999"); + (ok, c) := sys->announce(netaddr); + if (ok < 0) + error(sys->sprint("cannot announce %s: %r", netaddr)); + + rc: chan of ref Srvreq; + (err, rc) = srvmgr->init(root); + if (err != nil) + error(err); + + sys->print("Srvmgr started\n"); + + for (;;) { + (okl, nc) := sys->listen(c); + if (okl < 0) { + sys->print("listen failed: %r\n"); + sys->sleep(1000); + return; + } + fd := sys->open(nc.dir+"/data", Sys->ORDWR); + if(nc.cfd != nil) + sys->fprint(nc.cfd, "keepalive"); + nc.cfd = nil; + if (fd != nil) + spawn newclient(rc, fd, root); + fd = nil; + } +} + +badmodule(path: string) +{ + error(sys->sprint("cannot load module %s: %r", path)); +} + +error(s: string) +{ + sys->fprint(stderr, "collabsrv: %s\n", s); + raise "fail:error"; +} + +user(): (string, string) +{ + sys = load Sys Sys->PATH; + fd := sys->open("/dev/user", sys->OREAD); + if(fd == nil) + return (sys->sprint("can't open /dev/user: %r"), nil); + buf := array[128] of byte; + n := sys->read(fd, buf, len buf); + if(n <= 0) + return (sys->sprint("failed to read /dev/user: %r"), nil); + return (nil, string buf[0:n]); +} + +newclient(rc: chan of ref Srvreq, fd: ref Sys->FD, root: string) +{ + algs := "none" :: "clear" :: "md4" :: "md5" :: nil; + sys->print("new client\n"); + proxy := load Proxy Proxy->PATH; + if (proxy == nil) { + sys->fprint(stderr, "collabsrv: cannot load %s: %r\n", Proxy->PATH); + return; + } + sys->pctl(Sys->NEWPGRP|Sys->FORKNS|Sys->FORKENV, nil); + s := ""; + (fd, s) = auth->server(algs, authinfo, fd, 1); + if (fd == nil){ + sys->fprint(stderr, "collabsrv: cannot authenticate: %s\n", s); + return; + } + sys->fprint(stderr, "uname: %s\n", s); + spawn proxy->init(root, fd, rc, s); +} + +netmkaddr(addr, net, svc: string): string +{ + if(net == nil) + net = "net"; + (n, l) := sys->tokenize(addr, "!"); + if(n <= 1){ + if(svc== nil) + return sys->sprint("%s!%s", net, addr); + return sys->sprint("%s!%s!%s", net, addr, svc); + } + if(svc == nil || n > 2) + return addr; + return sys->sprint("%s!%s", addr, svc); +} diff --git a/appl/collab/connect.b b/appl/collab/connect.b new file mode 100644 index 00000000..a8dbffde --- /dev/null +++ b/appl/collab/connect.b @@ -0,0 +1,156 @@ +implement Connect; + +include "sys.m"; + sys: Sys; + +include "draw.m"; +include "keyring.m"; +include "security.m"; +include "arg.m"; + +Connect: module +{ + init: fn(ctxt: ref Draw->Context, argv: list of string); +}; + +remotedir := "/n/remote"; +localdir := "/n/ftree/collab"; +COLLABPORT: con "9999"; # TO DO: needs symbolic name in services + +usage() +{ + sys->fprint(sys->fildes(2), "Usage: connect [-v] [-C cryptoalg] [-k keyring] [net!addr [localdir]]\n"); + raise "fail:usage"; +} + +init(nil: ref Draw->Context, args: list of string) +{ + sys = load Sys Sys->PATH; + + vflag := 0; + alg := "none"; + keyfile := ""; + netaddr := "$collab"; + + arg := load Arg Arg->PATH; + if(arg != nil){ + arg->init(args); + while((c := arg->opt()) != 0) + case c { + 'C' => + alg = arg->arg(); + 'k' => + keyfile = arg->arg(); + 'v' => + vflag++; + * => + usage(); + } + } + args = arg->argv(); + arg = nil; + + if(args != nil){ + netaddr = hd args; + args = tl args; + if(args != nil) + localdir = hd args; + } + + if(vflag) + sys->print("connect: dial %s\n", netaddr); + (fd, user) := authdial(netaddr, keyfile, alg); + if(vflag) + sys->print("remote username is %s\n", user); + if(sys->mount(fd, nil, remotedir, Sys->MREPL, nil) < 0) + error(sys->sprint("can't mount %s on %s: %r", netaddr, remotedir)); + fd = nil; + + connectdir := remotedir+"/collab"; + if (sys->bind(connectdir, localdir, Sys->MCREATE|Sys->MREPL) < 0){ + error(sys->sprint("cannot bind %s onto %s: %r\n", connectdir, localdir)); + raise "fail:error"; + } + + # if something such as ftree is running and watching for changes, tell it about this one + fd = sys->open("/chan/nsupdate", Sys->OWRITE); + if(fd != nil) + sys->fprint(fd, "/n/ftree/collab"); + if(vflag) + sys->print("collab connected\n"); +} + +authdial(addr, keyfile, alg: string): (ref Sys->FD, string) +{ + cert : string; + + kr := load Keyring Keyring->PATH; + if(kr == nil) + error(sys->sprint("cannot load %s: %r", Keyring->PATH)); + + kd := "/usr/" + user() + "/keyring/"; + if (keyfile == nil) { + cert = kd + netmkaddr(addr, "tcp", ""); + (ok, nil) := sys->stat(cert); + if (ok < 0) + cert = kd + "default"; + } + else if (len keyfile > 0 && keyfile[0] != '/') + cert = kd + keyfile; + else + cert = keyfile; + ai := kr->readauthinfo(cert); + if (ai == nil) + error(sys->sprint("cannot read authentication data from %s: %r", cert)); + + au := load Auth Auth->PATH; + if(au == nil) + error(sys->sprint("cannot load %s: %r", Auth->PATH)); + err := au->init(); + if(err != nil) + error(sys->sprint("cannot init Auth: %s", err)); + + (ok, c) := sys->dial(netmkaddr(addr, "tcp", COLLABPORT), nil); + if(ok < 0) + error(sys->sprint("can't dial %s: %r", addr)); + (fd, id_or_err) := au->client(alg, ai, c.dfd); + if(fd == nil) + error(sys->sprint("authentication failed: %s", id_or_err)); + + return (fd, id_or_err); +} + +user(): string +{ + fd := sys->open("/dev/user", sys->OREAD); + if(fd == nil) + return ""; + + buf := array[Sys->NAMEMAX] of byte; + n := sys->read(fd, buf, len buf); + if(n < 0) + return ""; + + return string buf[0:n]; +} + +netmkaddr(addr, net, svc: string): string +{ + if(net == nil) + net = "net"; + (n, l) := sys->tokenize(addr, "!"); + if(n <= 1){ + if(svc== nil) + return sys->sprint("%s!%s", net, addr); + return sys->sprint("%s!%s!%s", net, addr, svc); + } + if(svc == nil || n > 2) + return addr; + return sys->sprint("%s!%s", addr, svc); +} + +error(m: string) +{ + sys->fprint(sys->fildes(2), "connect: %s\n", m); + raise "fail:error"; +} diff --git a/appl/collab/lib/messages.b b/appl/collab/lib/messages.b new file mode 100644 index 00000000..ab982339 --- /dev/null +++ b/appl/collab/lib/messages.b @@ -0,0 +1,86 @@ +implement Messages; + +# +# message queues and their users +# + +include "messages.m"; + +clientidgen := 1; + +init() +{ + clientidgen = 1; +} + +Msglist.new(): ref Msglist +{ + msgs := ref Msglist; + msgs.tail = ref Msg; # valid Msg when .next != nil + return msgs; +} + +Msglist.queue(msgs: self ref Msglist): ref Msg +{ + return msgs.tail; +} + +Msglist.wait(msgs: self ref Msglist, u: ref User, rd: ref Readreq) +{ + msgs.readers = (u, rd) :: msgs.readers; # list reversed, but currently does not matter +} + +Msglist.write(msgs: self ref Msglist, m: ref Msg): list of (ref User, ref Readreq) +{ + tail := msgs.tail; + tail.from = m.from; + tail.data = m.data; + tail.next = ref Msg(nil, nil, nil); + msgs.tail = tail.next; # next message will be formed in tail.next + rl := msgs.readers; + msgs.readers = nil; + return rl; +} + +Msglist.flushtag(msgs: self ref Msglist, tag: int) +{ + rl := msgs.readers; + msgs.readers = nil; + for(; rl != nil; rl = tl rl){ + (nil, req) := hd rl; + if(req.tag != tag) + msgs.readers = hd rl :: msgs.readers; + } +} + +Msglist.flushfid(msgs: self ref Msglist, fid: int) +{ + rl := msgs.readers; + msgs.readers = nil; + for(; rl != nil; rl = tl rl){ + (nil, req) := hd rl; + if(req.fid != fid) + msgs.readers = hd rl :: msgs.readers; + } +} + +User.new(fid: int, name: string): ref User +{ + return ref User(clientidgen++, fid, name, nil); +} + +User.initqueue(u: self ref User, msgs: ref Msglist) +{ + u.queue = msgs.tail; +} + +User.read(u: self ref User): ref Msg +{ + if((m := u.queue).next != nil){ + u.queue = m.next; + m = ref *m; # copy to ensure no aliasing + m.next = nil; + return m; + } + return nil; +} diff --git a/appl/collab/lib/messages.m b/appl/collab/lib/messages.m new file mode 100644 index 00000000..ab1c679a --- /dev/null +++ b/appl/collab/lib/messages.m @@ -0,0 +1,42 @@ +Messages: module +{ + PATH: con "/dis/collab/lib/messages.dis"; + + Msg: adt { + from: cyclic ref User; + data: array of byte; + next: cyclic ref Msg; + }; + + Msglist: adt { + tail: ref Msg; + readers: list of (ref User, ref Readreq); + + new: fn(): ref Msglist; + flushfid: fn(nil: self ref Msglist, fid: int); + flushtag: fn(nil: self ref Msglist, tag: int); + wait: fn(nil: self ref Msglist, u: ref User, r: ref Readreq); + write: fn(nil: self ref Msglist, m: ref Msg): list of (ref User, ref Readreq); + queue: fn(nil: self ref Msglist): ref Msg; + }; + + Readreq: adt { + tag: int; + fid: int; + count: int; + offset: big; + }; + + User: adt { + id: int; + fid: int; + name: string; + queue: cyclic ref Msg; + + new: fn(fid: int, name: string): ref User; + initqueue: fn(nil: self ref User, msgs: ref Msglist); + read: fn(nil: self ref User): ref Msg; + }; + + init: fn(); +}; diff --git a/appl/collab/mkfile b/appl/collab/mkfile new file mode 100644 index 00000000..b26c9626 --- /dev/null +++ b/appl/collab/mkfile @@ -0,0 +1,62 @@ +<../../mkconfig + +SERVERS=\ + servers/chatsrv.dis \ + servers/memfssrv.dis \ + servers/mpx.dis \ + servers/wbsrv.dis \ + +CLIENTS=\ + clients/chat.dis \ + clients/poll.dis \ + clients/poller.dis \ + clients/whiteboard.dis \ + +LIB=\ + lib/messages.dis \ + +MAIN=\ + collabsrv.dis \ + connect.dis \ + proxy.dis \ + srvmgr.dis \ + +MODULES=\ + proxy.m\ + service.m\ + srvmgr.m\ + +SYSMODULES=\ + arg.m\ + cfg.m\ + draw.m\ + keyring.m\ + security.m\ + sys.m\ + +DEST=$ROOT/dis/collab + +ALL = $SERVERS $CLIENTS $LIB $MAIN + +all:V: $ALL + +install:V: ${SERVERS:%=$DEST/%} \ + ${CLIENTS:%=$DEST/%} \ + ${LIB:%=$DEST/%} \ + ${MAIN:%=$DEST/%} + +$DEST/%.dis: %.dis + cp $stem.dis $target + +%.dis: $MODULES ${SYSMODULES:%=$ROOT/module/%} + +%.dis: %.b + limbo -gw -I$ROOT/module -Ilib -I. -o $stem.dis $stem.b + +$ENGINES $MAIN $LIB: service.m srvmgr.m proxy.m lib/messages.m + +clean:NV: + rm -f *.dis *.sbl */*.dis */*.sbl + +nuke:NV: clean + cd $DEST && rm -f $ALL diff --git a/appl/collab/proxy.b b/appl/collab/proxy.b new file mode 100644 index 00000000..83999495 --- /dev/null +++ b/appl/collab/proxy.b @@ -0,0 +1,177 @@ +implement Proxy; + +include "sys.m"; + sys: Sys; + +include "srvmgr.m"; +include "proxy.m"; + +Srvreq, Srvreply: import Srvmgr; + +init(root: string, fd: ref Sys->FD, rc: chan of ref Srvreq, user: string) +{ + sys = load Sys Sys->PATH; + + sys->chdir(root); + sys->bind("export/services", "export/services", Sys->MCREATE); + sys->bind("#s", "export/services", Sys->MBEFORE); + + ctlio := sys->file2chan("export/services", "ctl"); + + hangup := chan of int; + spawn export(fd, "export", hangup); + fd = nil; + + for (;;) alt { + <- hangup => + # closedown all clients + sys->print("client exit [%s]\n", user); + rmclients(rc); + return; + (offset, count, fid, r) := <- ctlio.read => + client := fid2client(fid); + if (r == nil) { + if (client != nil) + rmclient(rc, client); + continue; + } + if (client == nil) { + rreply(r, (nil, "service not set")); + continue; + } + rreply(r, reads(client.path, offset, count)); + + (offset, data, fid, w) := <- ctlio.write => + client := fid2client(fid); + if (w == nil) { + if (client != nil) + rmclient(rc, client); + continue; + } + if (client != nil) { + wreply(w, (0, "service set")); + continue; + } + err := newclient(rc, user, fid, string data); + if (err != nil) + wreply(w, (0, err)); + else + wreply(w, (len data, nil)); + } + +} + +rreply(rc: chan of (array of byte, string), reply: (array of byte, string)) +{ + alt { + rc <-= reply =>; + * =>; + } +} + +wreply(wc: chan of (int, string), reply: (int, string)) +{ + alt { + wc <-= reply=>; + * =>; + } +} + +reads(str: string, off, nbytes: int): (array of byte, string) +{ + bstr := array of byte str; + slen := len bstr; + if(off < 0 || off >= slen) + return (nil, nil); + if(off + nbytes > slen) + nbytes = slen - off; + if(nbytes <= 0) + return (nil, nil); + return (bstr[off:off+nbytes], nil); +} + +export(exportfd: ref Sys->FD, dir: string, done: chan of int) +{ + sys->export(exportfd, dir, Sys->EXPWAIT); + done <-= 1; +} + +Client: adt { + fid: int; + path: string; + sname: string; + id: string; +}; + +clients: list of ref Client; +freepaths: list of string; +nextpath := 0; + +fid2client(fid: int): ref Client +{ + for(cl := clients; cl != nil; cl = tl cl) + if ((c := hd cl).fid == fid) + return c; + return nil; +} + +newclient(rc: chan of ref Srvreq, user: string, fid: int, cmd: string): string +{ +sys->print("new Client %s [%s]\n", user, cmd); + for (i := 0; i < len cmd; i++) + if (cmd[i] == ' ') + break; + if (i == 0 || i == len cmd) + return "bad command"; + + sname := cmd[:i]; + id := cmd[i:]; + reply := chan of Srvreply; + rc <-= ref Srvreq.Acquire(sname, id, user, reply); + (err, root, fd) := <- reply; + if (err != nil) + return err; + + path := ""; + if (freepaths != nil) + (path, freepaths) = (hd freepaths, tl freepaths); + else + path = string nextpath++; + + sys->mount(fd, nil, "mnt", Sys->MREPL, nil); # connection to the active service fs + mkdir("export/services/"+path); + sys->bind("mnt/"+root, "export/services/"+path, Sys->MREPL|Sys->MCREATE); + sys->unmount("mnt", nil); + clients = ref Client(fid, path, sname, id) :: clients; + return nil; +} + +rmclient(rc: chan of ref Srvreq, client: ref Client) +{ +sys->print("rmclient [%s %s]\n", client.sname, client.id); + nl: list of ref Client; + for(cl := clients; cl != nil; cl = tl cl) + if((c := hd cl) == client){ + sys->unmount("export/services/" + client.path, nil); + freepaths = client.path :: freepaths; + rc <-= ref Srvreq.Release(client.sname, client.id); + } else + nl = c :: nl; + clients = nl; +} + +rmclients(rc: chan of ref Srvreq) +{ + for(cl := clients; cl != nil; cl = tl cl){ + c := hd cl; +sys->print("rmclients [%s %s]\n", c.sname, c.id); + rc <-= ref Srvreq.Release(c.sname, c.id); + } + clients = nil; +} + +mkdir(path: string) +{ + sys->print("mkdir [%s]\n", path); + sys->create(path, Sys->OREAD, 8r777 | Sys->DMDIR); +} diff --git a/appl/collab/proxy.m b/appl/collab/proxy.m new file mode 100644 index 00000000..d4169bc7 --- /dev/null +++ b/appl/collab/proxy.m @@ -0,0 +1,5 @@ +Proxy: module +{ + PATH: con "/dis/collab/proxy.dis"; + init: fn (root: string, fd: ref Sys->FD, rc: chan of ref Srvmgr->Srvreq, user: string); +}; diff --git a/appl/collab/runcollab b/appl/collab/runcollab new file mode 100755 index 00000000..c8e980dc --- /dev/null +++ b/appl/collab/runcollab @@ -0,0 +1,8 @@ +#!/dis/sh + +load std +and {~ $#* 0} {echo usage: runcollab collabname >[2=1]; exit usage} +pctl forkns +or {bind -bc /services/$1 /services/collab} {exit fail} +or {bind /dis/collab/servers /services/collab/servers} {exit fail} +collab/collabsrv /services/collab diff --git a/appl/collab/servers/chatsrv.b b/appl/collab/servers/chatsrv.b new file mode 100644 index 00000000..072c0617 --- /dev/null +++ b/appl/collab/servers/chatsrv.b @@ -0,0 +1,263 @@ +implement Service; + +# +# simple text-based chat service +# + +include "sys.m"; + sys: Sys; + Qid: import Sys; + +include "styx.m"; + styx: Styx; + Tmsg, Rmsg: import Styx; + +include "styxservers.m"; + styxservers: Styxservers; + Styxserver, Navigator: import styxservers; + nametree: Nametree; + Tree: import nametree; + +include "../service.m"; + +Qdir, Qusers, Qmsgs: con iota; + +tc: chan of ref Tmsg; +srv: ref Styxserver; + +user := "inferno"; + +dir(name: string, perm: int, path: int): Sys->Dir +{ + d := sys->zerodir; + d.name = name; + d.uid = user; + d.gid = user; + d.qid.path = big path; + if(perm & Sys->DMDIR) + d.qid.qtype = Sys->QTDIR; + else + d.qid.qtype = Sys->QTFILE; + d.mode = perm; + return d; +} + +init(nil: list of string): (string, string, ref Sys->FD) +{ + sys = load Sys Sys->PATH; + styx = load Styx Styx->PATH; + if(styx == nil) + return (sys->sprint("can't load %s: %r", Styx->PATH), nil, nil); + styxservers = load Styxservers Styxservers->PATH; + if(styxservers == nil) + return (sys->sprint("can't load %s: %r", Styxservers->PATH), nil, nil); + nametree = load Nametree Nametree->PATH; + if(nametree == nil) + return (sys->sprint("can't load %s: %r", Nametree->PATH), nil, nil); + styx->init(); + styxservers->init(styx); + nametree->init(); + + (tree, treeop) := nametree->start(); + tree.create(big Qdir, dir(".", Sys->DMDIR|8r555, Qdir)); + tree.create(big Qdir, dir("users", 8r444, Qusers)); + tree.create(big Qdir, dir("msgs", 8r666, Qmsgs)); + + p := array [2] of ref Sys->FD; + if (sys->pipe(p) < 0){ + tree.quit(); + return (sys->sprint("cannot create pipe: %r"), nil, nil); + } + + nextmsg = ref Msg (0, nil, nil, nil); + + (tc, srv) = Styxserver.new(p[1], Navigator.new(treeop), big Qdir); + spawn chatsrv(tree); + + return (nil, "/", p[0]); +} + +chatsrv(tree: ref Tree) +{ + while((tmsg := <-tc) != nil){ + pick tm := tmsg { + Readerror => + break; + Flush => + cancelpending(tm.tag); + srv.reply(ref Rmsg.Flush(tm.tag)); + Open => + c := srv.open(tm); + if (c == nil) + break; + if (int c.path == Qmsgs){ + newmsgclient(tm.fid, c.uname); + #root[0].qid.vers++; # TO DO + } + Read => + c := srv.getfid(tm.fid); + if (c == nil || !c.isopen) { + srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid)); + break; + } + case int c.path { + Qdir => + srv.read(tm); + Qmsgs => + mc := getmsgclient(tm.fid); + if (mc == nil) { + srv.reply(ref Rmsg.Error(tm.tag, "internal error -- lost client")); + continue; + } + tm.offset = big 0; + msg := getnextmsg(mc); + if (msg == nil) { + if(mc.pending != nil) + srv.reply(ref Rmsg.Error(tm.tag, "read already pending")); + else + mc.pending = tm; + continue; + } + srv.reply(styxservers->readstr(tm, msg)); + Qusers => + srv.reply(styxservers->readstr(tm, usernames())); + * => + srv.reply(ref Rmsg.Error(tm.tag, "phase error -- bad path")); + } + Write => + c := srv.getfid(tm.fid); + if (c == nil || !c.isopen) { + srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid)); + continue; + } + if (int c.path != Qmsgs) { + srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Eperm)); + continue; + } + writemsgclients(tm.fid, c.uname, string tm.data); + srv.reply(ref Rmsg.Write(tm.tag, len tm.data)); + Clunk => + c := srv.clunk(tm); + if (c != nil && int c.path == Qmsgs){ + closemsgclient(tm.fid); + # root[0].qid.vers++; # TO DO + } + * => + srv.default(tmsg); + } + } + tree.quit(); + sys->print("chatsrv exit\n"); +} + +Msg: adt { + fromfid: int; + from: string; + msg: string; + next: cyclic ref Msg; +}; + +Msgclient: adt { + fid: int; + name: string; + nextmsg: ref Msg; + pending: ref Tmsg.Read; + next: cyclic ref Msgclient; +}; + +nextmsg: ref Msg; +msgclients: ref Msgclient; + +usernames(): string +{ + s := ""; + for (c := msgclients; c != nil; c = c.next) + s += c.name+"\n"; + return s; +} + +newmsgclient(fid: int, name: string) +{ + writemsgclients(fid, nil, "+++ " + name + " has arrived"); + msgclients = ref Msgclient(fid, name, nextmsg, nil, msgclients); +} + +getmsgclient(fid: int): ref Msgclient +{ + for (c := msgclients; c != nil; c = c.next) + if (c.fid == fid) + return c; + return nil; +} + +cancelpending(tag: int) +{ + for (c := msgclients; c != nil; c = c.next) + if((tm := c.pending) != nil && tm.tag == tag){ + c.pending = nil; + break; + } +} + +closemsgclient(fid: int) +{ + prev: ref Msgclient; + s := ""; + for (c := msgclients; c != nil; c = c.next) { + if (c.fid == fid) { + if (prev == nil) + msgclients = c.next; + else + prev.next = c.next; + s = "--- " + c.name + " has left"; + break; + } + prev = c; + } + if (s != nil) + writemsgclients(fid, nil, s); +} + +writemsgclients(fromfid: int, from: string, msg: string) +{ + nm := ref Msg(0, nil, nil, nil); + nextmsg.fromfid = fromfid; + nextmsg.from = from; + nextmsg.msg = msg; + nextmsg.next = nm; + + for (c := msgclients; c != nil; c = c.next) { + if (c.pending != nil) { + s := msgtext(c, nextmsg); + srv.reply(styxservers->readstr(c.pending, s)); + c.pending = nil; + c.nextmsg = nm; + } + } + nextmsg = nm; +} + +getnextmsg(mc: ref Msgclient): string +{ +# uncomment next two lines to eliminate queued messages to self +# while(mc.nextmsg.next != nil && mc.nextmsg.fromfid == mc.fid) +# mc.nextmsg = mc.nextmsg.next; + if ((m := mc.nextmsg).next != nil){ + mc.nextmsg = m.next; + return msgtext(mc, m); + } + return nil; +} + +msgtext(mc: ref Msgclient, m: ref Msg): string +{ + prefix := ""; + if (m.from != nil) { + # not a system message + if (mc.fid == m.fromfid) + prefix = "<you>: "; + else + prefix = m.from + ": "; + } + return prefix + m.msg; +} diff --git a/appl/collab/servers/memfssrv.b b/appl/collab/servers/memfssrv.b new file mode 100644 index 00000000..8c44cd5d --- /dev/null +++ b/appl/collab/servers/memfssrv.b @@ -0,0 +1,20 @@ +implement Service; + +include "sys.m"; +include "../service.m"; +include "memfs.m"; + +init(nil : list of string) : (string, string, ref Sys->FD) +{ + sys := load Sys Sys->PATH; + memfs := load MemFS MemFS->PATH; + if (memfs == nil) { + err := sys->sprint("cannot load %s: %r", MemFS->PATH); + return (err, nil, nil); + } + err := memfs->init(); + if (err != nil) + return (err, nil, nil); + fd := memfs->newfs(1024 * 512); + return (nil, "/", fd); +} diff --git a/appl/collab/servers/mpx.b b/appl/collab/servers/mpx.b new file mode 100644 index 00000000..ea0c2c1c --- /dev/null +++ b/appl/collab/servers/mpx.b @@ -0,0 +1,301 @@ +implement Service; + +# +# 1 to many and many to 1 multiplexor +# + +include "sys.m"; + sys: Sys; + Qid: import Sys; + +include "styx.m"; + styx: Styx; + Tmsg, Rmsg: import styx; + +include "styxservers.m"; + styxservers: Styxservers; + Styxserver, Navigator: import styxservers; + nametree: Nametree; + Tree: import nametree; + +include "service.m"; + +include "messages.m"; + messages: Messages; + Msg, Msglist, Readreq, User: import messages; + +Qdir, Qroot, Qusers, Qleaf: con iota; + +srv: ref Styxserver; +clientidgen := 0; + +Einactive: con "not currently active"; + +toleaf: ref Msglist; +toroot: ref Msglist; +userlist: list of ref User; + +user := "inferno"; + +dir(name: string, perm: int, path: int): Sys->Dir +{ + d := sys->zerodir; + d.name = name; + d.uid = user; + d.gid = user; + d.qid.path = big path; + if(perm & Sys->DMDIR) + d.qid.qtype = Sys->QTDIR; + else + d.qid.qtype = Sys->QTFILE; + d.mode = perm; + return d; +} + +init(nil: list of string): (string, string, ref Sys->FD) +{ + sys = load Sys Sys->PATH; + styx = load Styx Styx->PATH; + if(styx == nil) + return (sys->sprint("can't load %s: %r", Styx->PATH), nil, nil); + styxservers = load Styxservers Styxservers->PATH; + if(styxservers == nil) + return (sys->sprint("can't load %s: %r", Styxservers->PATH), nil, nil); + nametree = load Nametree Nametree->PATH; + if(nametree == nil) + return (sys->sprint("can't load %s: %r", Nametree->PATH), nil, nil); + styx->init(); + styxservers->init(styx); +styxservers->traceset(1); + nametree->init(); + messages = load Messages Messages->PATH; + if(messages == nil) + return (sys->sprint("can't load %s: %r", Messages->PATH), nil, nil); + + (tree, treeop) := nametree->start(); + tree.create(big Qdir, dir(".", Sys->DMDIR|8r555, Qdir)); + tree.create(big Qdir, dir("leaf", 8r666, Qleaf)); + tree.create(big Qdir, dir("root", 8r666, Qroot)); + tree.create(big Qdir, dir("users", 8r444, Qusers)); + + p := array [2] of ref Sys->FD; + if (sys->pipe(p) < 0){ + tree.quit(); + return (sys->sprint("can't create pipe: %r"), nil, nil); + } + + toleaf = Msglist.new(); + toroot = Msglist.new(); + + tc: chan of ref Tmsg; + (tc, srv) = Styxserver.new(p[1], Navigator.new(treeop), big Qdir); + spawn mpx(tc, tree); + + return (nil, "/", p[0]); +} + +mpx(tc: chan of ref Tmsg, tree: ref Tree) +{ + root: ref User; + while((tmsg := <-tc) != nil){ + pick tm := tmsg { + Readerror => + break; + Open => + c := srv.getfid(tm.fid); + if(c == nil || c.isopen){ + srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid)); + continue; + } + case int c.path { + Qroot => + if(root != nil){ + srv.reply(ref Rmsg.Error(tm.tag, sys->sprint("interaction already directed by %s", root.name))); + continue; + } + c = srv.open(tm); + if (c == nil) + continue; + root = ref User(0, tm.fid, c.uname, nil); + root.initqueue(toroot); + Qleaf => + if(root == nil){ + srv.reply(ref Rmsg.Error(tm.tag, Einactive)); + continue; + } + c = srv.open(tm); + if (c == nil) + continue; + userarrives(tm.fid, c.uname); + # mpxdir[1].qid.vers++; # TO DO + * => + srv.open(tm); + } + Read => + c := srv.getfid(tm.fid); + if (c == nil || !c.isopen) { + srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid)); + continue; + } + case int c.path { + Qdir => + srv.read(tm); + Qroot => + tm.offset = big 0; + m := qread(toroot, root, tm, 1); + if(m != nil) + srv.reply(ref Rmsg.Read(tm.tag, m.data)); + Qleaf => + u := fid2user(tm.fid); + if (u == nil) { + srv.reply(ref Rmsg.Error(tm.tag, "internal error -- lost user")); + continue; + } + tm.offset = big 0; + m := qread(toleaf, u, tm, 0); + if(m == nil){ + if(root == nil) + srv.reply(ref Rmsg.Read(tm.tag, nil)); + else + qread(toleaf, u, tm, 1); # put us on the wait queue + }else + srv.reply(ref Rmsg.Read(tm.tag, m.data)); + Qusers => + srv.reply(styxservers->readstr(tm, usernames())); + * => + srv.reply(ref Rmsg.Error(tm.tag, "phase error -- bad path")); + } + Write => + c := srv.getfid(tm.fid); + if (c == nil || !c.isopen) { + srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid)); + continue; + } + case int c.path { + Qroot => + qwrite(toleaf, msg(root, 'M', tm.data)); + srv.reply(ref Rmsg.Write(tm.tag, len tm.data)); + Qleaf => + u := fid2user(tm.fid); + if(u == nil) { + srv.reply(ref Rmsg.Error(tm.tag, "internal error -- lost user")); + continue; + } + if(root == nil){ + srv.reply(ref Rmsg.Error(tm.tag, Einactive)); + continue; + } + qwrite(toroot, msg(u, 'm', tm.data)); + srv.reply(ref Rmsg.Write(tm.tag, len tm.data)); + * => + srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Eperm)); + } + Flush => + cancelpending(tm.tag); + srv.reply(ref Rmsg.Flush(tm.tag)); + Clunk => + c := srv.getfid(tm.fid); + if(c.isopen){ + case int c.path { + Qroot => + # shut down? + qwrite(toleaf, msg(root, 'L', nil)); + root = nil; + Qleaf => + userleaves(tm.fid); + # mpxdir[1].qid.vers++; # TO DO + } + } + * => + srv.default(tmsg); + } + } + tree.quit(); + sys->print("mpx exit\n"); +} + +mpxseqgen := 0; + +time(): int +{ + return ++mpxseqgen; # server time; assumes 2^31-1 is large enough +} + +userarrives(fid: int, name: string) +{ + u := User.new(fid, name); + qwrite(toroot, msg(u, 'a', nil)); + u.initqueue(toleaf); # sees leaf messages from now on + userlist = u :: userlist; +} + +fid2user(fid: int): ref User +{ + for(ul := userlist; ul != nil; ul = tl ul) + if((u := hd ul).fid == fid) + return u; + return nil; +} + +userleaves(fid: int) +{ + ul := userlist; + userlist = nil; + u: ref User; + for(; ul != nil; ul = tl ul) + if((hd ul).fid != fid) + userlist = hd ul :: userlist; + else + u = hd ul; + if(u != nil) + qwrite(toroot, msg(u, 'l', nil)); +} + +usernames(): string +{ + s := ""; + for(ul := userlist; ul != nil; ul = tl ul){ + u := hd ul; + s += string u.id+" "+u.name+"\n"; + } + return s; +} + +qwrite(msgs: ref Msglist, m: ref Msg) +{ + pending := msgs.write(m); + for(; pending != nil; pending = tl pending){ + (u, req) := hd pending; + m = u.read(); # must succeed, or the code is wrong + data := m.data; + if(req.count < len data) + data = data[0:req.count]; + srv.reply(ref Rmsg.Read(req.tag, data)); + } +} + +qread(msgs: ref Msglist, u: ref User, tm: ref Tmsg.Read, wait: int): ref Msg +{ + m := u.read(); + if(m != nil){ + if(tm.count < len m.data) + m.data = m.data[0:tm.count]; + }else if(wait) + msgs.wait(u, ref Readreq(tm.tag, tm.fid, tm.count, tm.offset)); + return m; +} + +cancelpending(tag: int) +{ + toroot.flushtag(tag); + toleaf.flushtag(tag); +} + +msg(u: ref User, op: int, data: array of byte): ref Msg +{ + a := sys->aprint("%ud %d %c %s ", time(), u.id, op, u.name); + m := ref Msg(u, array[len a + len data] of byte, nil); + m.data[0:] = a; + m.data[len a:] = data; + return m; +} diff --git a/appl/collab/servers/wbsrv.b b/appl/collab/servers/wbsrv.b new file mode 100644 index 00000000..2492db7e --- /dev/null +++ b/appl/collab/servers/wbsrv.b @@ -0,0 +1,226 @@ +implement Service; + +include "sys.m"; + sys: Sys; + +include "draw.m"; + draw: Draw; + Chans, Display, Image, Rect, Point : import draw; + +include "../service.m"; + +WBW : con 234; +WBH : con 279; + +init(nil : list of string) : (string, string, ref Sys->FD) +{ + sys = load Sys Sys->PATH; + draw = load Draw Draw->PATH; + if (draw == nil) + return ("cannot load Draw module", nil, nil); + + p := array [2] of ref Sys->FD; + if (sys->pipe(p) == -1) + return (sys->sprint("cannot create pipe: %r"), nil, nil); + + display := Display.allocate(nil); + if (display == nil) + return (sys->sprint("cannot allocate display: %r"), nil, nil); + + r := Rect(Point(0,0), Point(WBW, WBH)); + wb := display.newimage(r, Draw->CMAP8, 0, Draw->White); + if (wb == nil) + return (sys->sprint("cannot allocate whiteboard image: %r"), nil, nil); + + nextmsg = ref Msg (nil, nil); + spawn wbsrv(p[1], wb); + return (nil, "/chan", p[0]); +} + +wbsrv(fd : ref Sys->FD, wb: ref Image) +{ + sys->pctl(Sys->FORKNS, nil); + sys->unmount(nil, "/chan"); + sys->bind("#s", "/chan", Sys->MREPL); + + bit := sys->file2chan("/chan", "wb.bit"); + strokes := sys->file2chan("/chan", "strokes"); + + hangup := chan of int; + spawn export(fd, hangup); + + nwbbytes := draw->bytesperline(wb.r, wb.depth) * wb.r.dy(); + bithdr := sys->aprint("%11s %11d %11d %11d %11d ", wb.chans.text(), 0, 0, WBW, WBH); + + for (;;) alt { + <-hangup => + sys->print("whiteboard:hangup\n"); + return; + + (offset, count, fid, r) := <-bit.read => + if (r == nil) { + closeclient(fid); + continue; + } + c := getclient(fid); + if (c == nil) { + # new client + c = newclient(fid); + data := array [len bithdr + nwbbytes] of byte; + data[0:] = bithdr; + wb.readpixels(wb.r, data[len bithdr:]); + c.bitdata = data; + } + if (offset >= len c.bitdata) { + rreply(r, (nil, nil)); + continue; + } + rreply(r, (c.bitdata[offset:], nil)); + + (offset, data, fid, w) := <-bit.write => + if (w != nil) + wreply(w, (0, "permission denied")); + + (offset, count, fid, r) := <-strokes.read => + if (r == nil) { + closeclient(fid); + continue; + } + c := getclient(fid); + if (c == nil) { + c = newclient(fid); + c.nextmsg = nextmsg; + } + d := c.nextmsg.data; + if (d == nil) { + c.pending = r; + c.pendlen = count; + continue; + } + c.nextmsg = c.nextmsg.next; + rreply(r, (d, nil)); + + (offset, data, fid, w) := <-strokes.write => + if (w == nil) { + closeclient(fid); + continue; + } + err := drawstrokes(wb, data); + if (err != nil) { + wreply(w, (0, err)); + continue; + } + wreply(w, (len data, nil)); + writeclients(data); + } +} + +rreply(rc: chan of (array of byte, string), reply: (array of byte, string)) +{ + alt { + rc <-= reply =>; + * =>; + } +} + +wreply(wc: chan of (int, string), reply: (int, string)) +{ + alt { + wc <-= reply=>; + * =>; + } +} + +export(fd : ref Sys->FD, done : chan of int) +{ + sys->export(fd, "/", Sys->EXPWAIT); + done <-= 1; +} + +Msg : adt { + data : array of byte; + next : cyclic ref Msg; +}; + +Client : adt { + fid : int; + bitdata : array of byte; # bit file client + nextmsg : ref Msg; # strokes file client + pending : Sys->Rread; + pendlen : int; +}; + +nextmsg : ref Msg; +clients : list of ref Client; + +newclient(fid : int) : ref Client +{ + c := ref Client(fid, nil, nil, nil, 0); + clients = c :: clients; + return c; +} + +getclient(fid : int) : ref Client +{ + for(cl := clients; cl != nil; cl = tl cl) + if((c := hd cl).fid == fid) + return c; + return nil; +} + +closeclient(fid : int) +{ + nl: list of ref Client; + for(cl := clients; cl != nil; cl = tl cl) + if((hd cl).fid != fid) + nl = hd cl :: nl; + clients = nl; +} + +writeclients(data : array of byte) +{ + nm := ref Msg(nil, nil); + nextmsg.data = data; + nextmsg.next = nm; + + for(cl := clients; cl != nil; cl = tl cl){ + if ((c := hd cl).pending != nil) { + n := c.pendlen; + if (n > len data) + n = len data; + alt{ + c.pending <-= (data[0:n], nil) => ; + * => ; + } + c.pending = nil; + c.nextmsg = nm; + } + } + nextmsg = nm; +} + +# data: colour width p0 p1 pn* + +drawstrokes(wb: ref Image, data : array of byte) : string +{ + (n, toks) := sys->tokenize(string data, " "); + if (n < 6 || n & 1) + return "bad data"; + + colour, width, x, y : int; + (colour, toks) = (int hd toks, tl toks); + (width, toks) = (int hd toks, tl toks); + (x, toks) = (int hd toks, tl toks); + (y, toks) = (int hd toks, tl toks); + pen := wb.display.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, colour); + p0 := Point(x, y); + while (toks != nil) { + (x, toks) = (int hd toks, tl toks); + (y, toks) = (int hd toks, tl toks); + p1 := Point(x, y); + # could use poly() instead of line() + wb.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, pen.r.min); + p0 = p1; + } + return nil; +} diff --git a/appl/collab/service.m b/appl/collab/service.m new file mode 100644 index 00000000..4d160a5b --- /dev/null +++ b/appl/collab/service.m @@ -0,0 +1,4 @@ +Service: module +{ + init: fn (args: list of string): (string, string, ref Sys->FD); +}; diff --git a/appl/collab/srvmgr.b b/appl/collab/srvmgr.b new file mode 100644 index 00000000..4794f65d --- /dev/null +++ b/appl/collab/srvmgr.b @@ -0,0 +1,190 @@ +implement Srvmgr; + +include "sys.m"; + sys: Sys; + +include "srvmgr.m"; +include "service.m"; +include "cfg.m"; + +Srvinfo: adt { + name: string; + path: string; + args: list of string; +}; + +services: list of ref Srvinfo; + +init(srvdir: string): (string, chan of ref Srvreq) +{ + sys = load Sys Sys->PATH; + cfg := load Cfg Cfg->PATH; + cfgpath := srvdir + "/services.cfg"; + if (cfg == nil) + return (sys->sprint("cannot load %s: %r", Cfg->PATH), nil); + err := cfg->init(cfgpath); + if (err != nil) + return (err, nil); + + (err, services) = parsecfg(cfgpath, srvdir, cfg); + if (err != nil) + return (err, nil); + + rc := chan of ref Srvreq; + spawn srv(rc); + return (nil, rc); +} + +parsecfg(p, srvdir: string, cfg: Cfg): (string, list of ref Srvinfo) +{ + srvlist: list of ref Srvinfo; + Record, Tuple: import cfg; + + for (slist := cfg->getkeys(); slist != nil; slist = tl slist) { + name := hd slist; + matches := cfg->lookup(name); + if (len matches > 1) { + (nil, duplicate) := hd tl matches; + primary := hd duplicate.tuples; + lnum := primary.lnum; + err := sys->sprint("%s:%d: duplicate service name %s", p, lnum, name); + return (err, nil); + } + (nil, r) := hd matches; + lnum := (hd r.tuples).lnum; + + (path, tuple) := r.lookup("path"); + if (path == nil) { + err := sys->sprint("%s:%d: missing path for service %s", p, lnum, name); + return (err, nil); + } + if (path[0] != '/') + path = srvdir + "/" + path; + + args: list of string = nil; + for (tuples := tl r.tuples; tuples != nil; tuples = tl tuples) { + t := hd tuples; + arg := t.lookup("arg"); + if (arg != nil) + args = arg :: args; + } + nargs: list of string = nil; + for (; args != nil; args = tl args) + nargs = hd args :: nargs; + srvlist = ref Srvinfo(name, path, args) ::srvlist; + } + if (srvlist == nil) { + err := sys->sprint("%s: no services", p); + return (err, nil); + } + return (nil, srvlist); +} + +srv(rc: chan of ref Srvreq) +{ + for (;;) { + req := <- rc; + id := req.sname + " " + req.id; + pick r := req { + Acquire => + # r.user not used, but could control access + service := acquire(id); + err := ""; + if (service.fd == nil) { + (err, service.root, service.fd) = startservice(req.sname); + if (err != nil) + release(id); + } + r.reply <-= (err, service.root, service.fd); + Release => + release(id); + } + } +} + +# +# returns (error, service root, service FD) +# +startservice(name: string): (string, string, ref Sys->FD) +{ +sys->print("startservice [%s]\n", name); + srv: ref Srvinfo; + for (sl := services; sl != nil; sl = tl sl) { + s := hd sl; + if (s.name == name) { + srv = s; + break; + } + } + if (srv == nil) + return ("unknown service", nil, nil); + + service := load Service srv.path; + if (service == nil) { + err := sys->sprint("cannot load %s: %r", srv.path); + return (err, nil, nil); + } + + return service->init(srv.args); +} + +Srvmap: adt { + id: string; + root: string; + fd: ref Sys->FD; + nref: int; + next: cyclic ref Srvmap; +}; + +PRIME: con 211; +buckets := array[PRIME] of ref Srvmap; + +hash(id: string): int +{ + # HashPJW + h := 0; + for (i := 0; i < len id; i++) { + h = (h << 4) + id[i]; + g := h & int 16rf0000000; + if (g != 0) { + h = h ^ ((g >> 24) & 16rff); + h = h ^ g; + } + } + if (h < 0) + h &= ~(1<<31); + return int (h % PRIME); +} + +acquire(id: string): ref Srvmap +{ + h := hash(id); + for (p := buckets[h]; p != nil; p = p.next) + if (p.id == id) { + p.nref++; + return p; + } + p = ref Srvmap(id, nil, nil, 1, buckets[h]); + buckets[h] = p; + return p; +} + +release(id: string) +{ + h :=hash(id); + prev: ref Srvmap; + for (p := buckets[h]; p != nil; p = p.next) { + if (p.id == id){ + p.nref--; + if (p.nref == 0) { + sys->print("release [%s]\n", p.id); + if (prev == nil) + buckets[h] = p.next; + else + prev.next = p.next; + } + return; + } + prev = p; + } +} diff --git a/appl/collab/srvmgr.m b/appl/collab/srvmgr.m new file mode 100644 index 00000000..c1bd3a78 --- /dev/null +++ b/appl/collab/srvmgr.m @@ -0,0 +1,22 @@ +Srvmgr: module +{ + PATH: con "/dis/collab/srvmgr.dis"; + Srvreq: adt { + sname: string; + id: string; + pick { + Acquire => + uname: string; + reply: chan of Srvreply; + Release => + } + }; + + Srvreply: type ( + string, # error + string, # root path + ref Sys->FD # styx fd + ); + + init: fn(cfg: string): (string, chan of ref Srvreq); +}; |
