summaryrefslogtreecommitdiff
path: root/appl/cmd/plumber.b
diff options
context:
space:
mode:
Diffstat (limited to 'appl/cmd/plumber.b')
-rw-r--r--appl/cmd/plumber.b766
1 files changed, 766 insertions, 0 deletions
diff --git a/appl/cmd/plumber.b b/appl/cmd/plumber.b
new file mode 100644
index 00000000..016eb623
--- /dev/null
+++ b/appl/cmd/plumber.b
@@ -0,0 +1,766 @@
+implement Plumber;
+
+include "sys.m";
+ sys: Sys;
+
+include "draw.m";
+ draw: Draw;
+
+include "sh.m";
+
+include "regex.m";
+ regex: Regex;
+
+include "string.m";
+ str: String;
+
+include "../lib/plumbing.m";
+ plumbing: Plumbing;
+ Pattern, Rule: import plumbing;
+
+include "plumbmsg.m";
+ plumbmsg: Plumbmsg;
+ Msg, Attr: import plumbmsg;
+
+include "arg.m";
+
+Plumber: module
+{
+ init: fn(ctxt: ref Draw->Context, argl: list of string);
+};
+
+Input: adt
+{
+ inc: chan of ref Inmesg;
+ resc: chan of int;
+ io: ref Sys->FileIO;
+};
+
+Output: adt
+{
+ name: string;
+ outc: chan of string;
+ io: ref Sys->FileIO;
+ queue: list of array of byte;
+ started: int;
+ startup: string;
+ waiting: int;
+};
+
+Port: adt
+{
+ name: string;
+ startup: string;
+ alwaysstart: int;
+};
+
+Match: adt
+{
+ p0, p1: int;
+};
+
+Inmesg: adt
+{
+ msg: ref Msg;
+ text: string; # if kind is text
+ p0,p1: int;
+ match: array of Match;
+ port: int;
+ startup: string;
+ args: list of string;
+ attrs: list of ref Attr;
+ clearclick: int;
+ set: int;
+ # $ arguments
+ _n: array of string;
+ _dir: string;
+ _file: string;
+};
+
+# Message status after processing
+HANDLED: con -1;
+UNKNOWN: con -2;
+NOTSTARTED: con -3;
+
+output: array of ref Output;
+
+input: ref Input;
+
+stderr: ref Sys->FD;
+pgrp: int;
+rules: list of ref Rule;
+titlectl: chan of string;
+ports: list of ref Port;
+wmstartup := 0;
+wmchan := "/chan/wm";
+verbose := 0;
+
+context: ref Draw->Context;
+
+usage()
+{
+ sys->fprint(stderr, "Usage: plumb [-vw] [-c wmchan] [initfile ...]\n");
+ raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+ context = ctxt;
+
+ sys = load Sys Sys->PATH;
+ draw = load Draw Draw->PATH;
+ stderr = sys->fildes(2);
+
+ regex = load Regex Regex->PATH;
+ plumbing = load Plumbing Plumbing->PATH;
+ str = load String String->PATH;
+
+ err: string;
+ nogrp := 0;
+
+ arg := load Arg Arg->PATH;
+ arg->init(args);
+ while ((opt := arg->opt()) != 0) {
+ case opt {
+ 'w' =>
+ wmstartup = 1;
+ 'c' =>
+ if ((wmchan = arg->arg()) == nil)
+ usage();
+ 'v' =>
+ verbose = 1;
+ 'n' =>
+ nogrp = 1;
+ * =>
+ usage();
+ }
+ }
+ args = arg->argv();
+ arg = nil;
+
+ (rules, err) = plumbing->init(regex, args);
+ if(err != nil){
+ sys->fprint(stderr, "plumb: %s\n", err);
+ raise "fail:init";
+ }
+
+ plumbmsg = load Plumbmsg Plumbmsg->PATH;
+ plumbmsg->init(0, nil, 0);
+
+ if(nogrp)
+ pgrp = sys->pctl(0, nil);
+ else
+ pgrp = sys->pctl(sys->NEWPGRP, nil);
+
+ r := rules;
+ for(i:=0; i<len rules; i++){
+ rule := hd r;
+ r = tl r;
+ for(j:=0; j<len rule.action; j++)
+ if(rule.action[j].pred == "to" || rule.action[j].pred == "alwaysstart"){
+ p := findport(rule.action[j].arg);
+ if(p == nil){
+ p = ref Port(rule.action[j].arg, nil, rule.action[j].pred == "alwaysstart");
+ ports = p :: ports;
+ }
+ for(k:=0; k<len rule.action; k++)
+ if(rule.action[k].pred == "start")
+ p.startup = rule.action[k].arg;
+ break;
+ }
+ }
+
+ input = ref Input;
+ input.io = makefile("plumb.input");
+ if(input.io == nil)
+ shutdown();
+ input.inc = chan of ref Inmesg;
+ input.resc = chan of int;
+ spawn receiver(input);
+
+ output = array[len ports] of ref Output;
+
+ pp := ports;
+ for(i=0; i<len output; i++){
+ p := hd pp;
+ pp = tl pp;
+ output[i] = ref Output;
+ output[i].name = p.name;
+ output[i].io = makefile("plumb."+p.name);
+ if(output[i].io == nil)
+ shutdown();
+ output[i].outc = chan of string;
+ output[i].started = 0;
+ output[i].startup = p.startup;
+ output[i].waiting = 0;
+ }
+
+ # spawn so we return without needing to run plumb in background
+ spawn sender(input, output);
+}
+
+findport(name: string): ref Port
+{
+ for(p:=ports; p!=nil; p=tl p)
+ if((hd p).name == name)
+ return hd p;
+ return nil;
+}
+
+makefile(file: string): ref Sys->FileIO
+{
+ io := sys->file2chan("/chan", file);
+ if(io == nil){
+ sys->fprint(stderr, "plumb: can't establish /chan/%s: %r\n", file);
+ return nil;
+ }
+ return io;
+}
+
+receiver(input: ref Input)
+{
+
+ for(;;){
+ (nil, msg, nil, wc) := <-input.io.write;
+ if(wc == nil)
+ ; # not interested in EOF; leave channel open
+ else{
+ input.inc <-= parse(msg);
+ res := <- input.resc;
+ err := "";
+ if(res == UNKNOWN)
+ err = "no matching plumb rule";
+ wc <-= (len msg, err);
+ }
+ }
+}
+
+sender(input: ref Input, output: array of ref Output)
+{
+ outputc := array[len output] of chan of (int, int, int, Sys->Rread);
+
+ for(;;){
+ alt{
+ in := <-input.inc =>
+ if(in == nil){
+ input.resc <-= HANDLED;
+ break;
+ }
+ (j, msg) := process(in);
+ case j {
+ HANDLED =>
+ break;
+ UNKNOWN =>
+ if(in.msg.src != "acme")
+ sys->fprint(stderr, "plumb: don't know who message goes to\n");
+ NOTSTARTED =>
+ sys->fprint(stderr, "plumb: can't start application\n");
+ * =>
+ output[j].queue = append(output[j].queue, msg);
+ outputc[j] = output[j].io.read;
+ }
+ input.resc <-= j;
+
+ (j, tmp) := <-outputc =>
+ (nil, nbytes, nil, rc) := tmp;
+ if(rc == nil) # no interest in EOF
+ break;
+ msg := hd output[j].queue;
+ if(nbytes < len msg){
+ rc <-= (nil, "buffer too short for message");
+ break;
+ }
+ output[j].queue = tl output[j].queue;
+ if(output[j].queue == nil)
+ outputc[j] = nil;
+ rc <-= (msg, nil);
+ }
+ }
+}
+
+parse(a: array of byte): ref Inmesg
+{
+ msg := Msg.unpack(a);
+ if(msg == nil)
+ return nil;
+ i := ref Inmesg;
+ i.msg = msg;
+ if(msg.dst != nil){
+ if(control(i))
+ return nil;
+ toport(i, msg.dst);
+ }else
+ i.port = -1;
+ i.match = array[10] of { * => Match(-1, -1)};
+ i._n = array[10] of string;
+ i.attrs = plumbmsg->string2attrs(i.msg.attr);
+ return i;
+}
+
+append(l: list of array of byte, a: array of byte): list of array of byte
+{
+ if(l == nil)
+ return a :: nil;
+ return hd l :: append(tl l, a);
+}
+
+shutdown()
+{
+ fname := sys->sprint("#p/%d/ctl", pgrp);
+ if((fdesc := sys->open(fname, sys->OWRITE)) != nil)
+ sys->write(fdesc, array of byte "killgrp\n", 8);
+ raise "fail:error";
+}
+
+# Handle control messages
+control(in: ref Inmesg): int
+{
+ msg := in.msg;
+ if(msg.kind!="text" || msg.dst!="plumb")
+ return 0;
+ text := string msg.data;
+ case text {
+ "start" =>
+ start(msg.src, 1);
+ "stop" =>
+ start(msg.src, -1);
+ * =>
+ sys->fprint(stderr, "plumb: unrecognized control message from %s: %s\n", msg.src, text);
+ }
+ return 1;
+}
+
+start(port: string, startstop: int)
+{
+ for(i:=0; i<len output; i++)
+ if(port == output[i].name){
+ output[i].waiting = 0;
+ output[i].started += startstop;
+ return;
+ }
+ sys->fprint(stderr, "plumb: \"start\" message from unrecognized port %s\n", port);
+}
+
+startup(dir, prog: string, args: list of string, wait: chan of int)
+{
+ if(wmstartup){
+ fd := sys->open(wmchan, Sys->OWRITE);
+ if(fd != nil){
+ sys->fprint(fd, "s %s", str->quoted(dir :: prog :: args));
+ wait <-= 1;
+ return;
+ }
+ }
+
+ sys->pctl(Sys->NEWFD|Sys->NEWPGRP|Sys->FORKNS, list of {0, 1, 2});
+ wait <-= 1;
+ wait = nil;
+ mod := load Command prog;
+ if(mod == nil){
+ sys->fprint(stderr, "plumb: can't load %s: %r\n", prog);
+ return;
+ }
+ sys->chdir(dir);
+ mod->init(context, prog :: args);
+}
+
+# See if messages should be queued while waiting for program to connect
+shouldqueue(out: ref Output): int
+{
+ p := findport(out.name);
+ if(p == nil){
+ sys->fprint(stderr, "plumb: can't happen in shouldqueue\n");
+ return 0;
+ }
+ if(p.alwaysstart)
+ return 0;
+ return out.waiting;
+}
+
+# Determine destination of input message, reformat for output
+process(in: ref Inmesg): (int, array of byte)
+{
+ if(!clarify(in))
+ return (UNKNOWN, nil);
+ if(in.port < 0)
+ return (UNKNOWN, nil);
+ a := in.msg.pack();
+ j := in.port;
+ if(a == nil)
+ j = UNKNOWN;
+ else if(output[j].started==0 && !shouldqueue(output[j])){
+ path: string;
+ args: list of string;
+ if(in.startup!=nil){
+ path = macro(in, in.startup);
+ args = expand(in, in.args);
+ }else if(output[j].startup != nil){
+ path = output[j].startup;
+ args = in.text :: nil;
+ }else
+ return (NOTSTARTED, nil);
+ log(sys->sprint("start %s port %s\n", path, output[j].name));
+ wait := chan of int;
+ output[j].waiting = 1;
+ spawn startup(in.msg.dir, path, args, wait);
+ <-wait;
+ return (HANDLED, nil);
+ }else{
+ if(in.msg.kind != "text")
+ text := sys->sprint("message of type %s", in.msg.kind);
+ else{
+ text = in.text;
+ for(i:=0; i<len text; i++){
+ if(text[i]=='\n'){
+ text = text[0:i];
+ break;
+ }
+ if(i > 50) {
+ text = text[0:i]+"...";
+ break;
+ }
+ }
+ }
+ log(sys->sprint("send \"%s\" to %s", text, output[j].name));
+ }
+ return (j, a);
+}
+
+# expand $arguments
+expand(in: ref Inmesg, args: list of string): list of string
+{
+ a: list of string;
+ while(args != nil){
+ a = macro(in, hd args) :: a;
+ args = tl args;
+ }
+ while(a != nil){
+ args = hd a :: args;
+ a = tl a;
+ }
+ return args;
+}
+
+# resolve all ambiguities, fill in any missing fields
+clarify(in: ref Inmesg): int
+{
+ in.clearclick = 0;
+ in.set = 0;
+ msg := in.msg;
+ if(msg.kind != "text")
+ return 0;
+ in.text = string msg.data;
+ if(msg.dst != "")
+ return 1;
+ return dorules(in, rules);
+}
+
+dorules(in: ref Inmesg, rules: list of ref Rule): int
+{
+ if (verbose)
+ log("msg: " + inmesg2s(in));
+ for(r:=rules; r!=nil; r=tl r) {
+ if(matchrule(in, hd r)){
+ applyrule(in, hd r);
+ if (verbose)
+ log("yes");
+ return 1;
+ } else if (verbose)
+ log("no");
+ }
+ return 0;
+}
+
+inmesg2s(in: ref Inmesg): string
+{
+ m := in.msg;
+ s := sys->sprint("src=%s; dst=%s; dir=%s; kind=%s; attr='%s'",
+ m.src, m.dst, m.dir, m.kind, m.attr);
+ if (m.kind == "text")
+ s += "; data='" + string m.data + "'";
+ return s;
+}
+
+matchrule(in: ref Inmesg, r: ref Rule): int
+{
+ pats := r.pattern;
+ for(i:=0; i<len in.match; i++)
+ in.match[i] = (-1,-1);
+ # no rules at all implies success, so return if any fail
+ for(i=0; i<len pats; i++)
+ if(matchpattern(in, pats[i]) == 0)
+ return 0;
+ return 1;
+}
+
+applyrule(in: ref Inmesg, r: ref Rule)
+{
+ acts := r.action;
+ for(i:=0; i<len acts; i++)
+ applypattern(in, acts[i]);
+ if(in.clearclick){
+ al: list of ref Attr;
+ for(l:=in.attrs; l!=nil; l=tl l)
+ if((hd l).name != "click")
+ al = hd l :: al;
+ in.attrs = al;
+ in.msg.attr = plumbmsg->attrs2string(al);
+ if(in.set){
+ in.text = macro(in, "$0");
+ in.msg.data = array of byte in.text;
+ }
+ }
+}
+
+matchpattern(in: ref Inmesg, p: ref Pattern): int
+{
+ msg := in.msg;
+ text: string;
+ case p.field {
+ "src" => text = msg.src;
+ "dst" => text = msg.dst;
+ "dir" => text = msg.dir;
+ "kind" => text = msg.kind;
+ "attr" => text = msg.attr;
+ "data" => text = in.text;
+ * =>
+ sys->fprint(stderr, "plumb: don't recognize pattern field %s\n", p.field);
+ return 0;
+ }
+ if (verbose)
+ log(sys->sprint("'%s' %s '%s'\n", text, p.pred, p.arg));
+ case p.pred {
+ "is" =>
+ return text == p.arg;
+ "isfile" or "isdir" =>
+ text = p.arg;
+ if(p.expand)
+ text = macro(in, text);
+ if(len text == 0)
+ return 0;
+ if(len in.msg.dir!=0 && text[0] != '/' && text[0]!='#')
+ text = in.msg.dir+"/"+text;
+ text = cleanname(text);
+ (ok, dir) := sys->stat(text);
+ if(ok < 0)
+ return 0;
+ if(p.pred=="isfile" && (dir.mode&Sys->DMDIR)==0){
+ in._file = text;
+ return 1;
+ }
+ if(p.pred=="isdir" && (dir.mode&Sys->DMDIR)!=0){
+ in._dir = text;
+ return 1;
+ }
+ return 0;
+ "matches" =>
+ (clickspecified, val) := plumbmsg->lookup(in.attrs, "click");
+ if(p.field != "data")
+ clickspecified = 0;
+ if(!clickspecified){
+ # easy case. must match whole string
+ matches := regex->execute(p.regex, text);
+ if(matches == nil)
+ return 0;
+ (p0, p1) := matches[0];
+ if(p0!=0 || p1!=len text)
+ return 0;
+ in.match = matches;
+ setvars(in, text);
+ return 1;
+ }
+ matches := clickmatch(p.regex, text, int val);
+ if(matches == nil)
+ return 0;
+ (p0, p1) := matches[0];
+ # assumes all matches are in same sequence
+ if(in.match[0].p0 != -1)
+ return p0==in.match[0].p0 && p1==in.match[0].p1;
+ in.match = matches;
+ setvars(in, text);
+ in.clearclick = 1;
+ in.set = 1;
+ return 1;
+ "set" =>
+ text = p.arg;
+ if(p.expand)
+ text = macro(in, text);
+ case p.field {
+ "src" => msg.src = text;
+ "dst" => msg.dst = text;
+ "dir" => msg.dir = text;
+ "kind" => msg.kind = text;
+ "attr" => msg.attr = text;
+ "data" => in.text = text;
+ msg.data = array of byte text;
+ msg.kind = "text";
+ in.set = 0;
+ }
+ return 1;
+ * =>
+ sys->fprint(stderr, "plumb: don't recognize pattern predicate %s\n", p.pred);
+ }
+ return 0;
+}
+
+applypattern(in: ref Inmesg, p: ref Pattern): int
+{
+ if(p.field != "plumb"){
+ sys->fprint(stderr, "plumb: don't recognize action field %s\n", p.field);
+ return 0;
+ }
+ case p.pred {
+ "to" or "alwaysstart" =>
+ if(in.port >= 0) # already specified
+ return 1;
+ toport(in, p.arg);
+ "start" =>
+ in.startup = p.arg;
+ in.args = p.extra;
+ * =>
+ sys->fprint(stderr, "plumb: don't recognize action %s\n", p.pred);
+ }
+ return 1;
+}
+
+toport(in: ref Inmesg, name: string): int
+{
+ for(i:=0; i<len output; i++)
+ if(name == output[i].name){
+ in.msg.dst = name;
+ in.port = i;
+ return i;
+ }
+ in.port = -1;
+ sys->fprint(stderr, "plumb: unrecognized port %s\n", name);
+ return -1;
+}
+
+# simple heuristic: look for leftmost match that reaches click position
+clickmatch(re: ref Regex->Arena, text: string, click: int): array of Match
+{
+ for(i:=0; i<=click && i < len text; i++){
+ matches := regex->executese(re, text, (i, -1), i == 0, 1);
+ if(matches == nil)
+ continue;
+ (p0, p1) := matches[0];
+
+ if(p0>=i && p1>=click)
+ return matches;
+ }
+ return nil;
+}
+
+setvars(in: ref Inmesg, text: string)
+{
+ for(i:=0; i<len in.match && in.match[i].p0>=0; i++)
+ in._n[i] = text[in.match[i].p0:in.match[i].p1];
+ for(; i<len in._n; i++)
+ in._n[i] = "";
+}
+
+macro(in: ref Inmesg, text: string): string
+{
+ word := "";
+ i := 0;
+ j := 0;
+ for(;;){
+ if(i == len text)
+ break;
+ if(text[i++] != '$')
+ continue;
+ if(i == len text)
+ break;
+ word += text[j:i-1];
+ (res, skip) := dollar(in, text[i:]);
+ word += res;
+ i += skip;
+ j = i;
+ }
+ if(j < len text)
+ word += text[j:];
+ return word;
+}
+
+dollar(in: ref Inmesg, text: string): (string, int)
+{
+ if(text[0] == '$')
+ return ("$", 1);
+ if('0'<=text[0] && text[0]<='9')
+ return (in._n[text[0]-'0'], 1);
+ if(len text < 3)
+ return ("$", 0);
+ case text[0:3] {
+ "src" => return (in.msg.src, 3);
+ "dst" => return (in.msg.dst, 3);
+ "dir" => return (in._dir, 3);
+ }
+ if(len text< 4)
+ return ("$", 0);
+ case text[0:4] {
+ "attr" => return (in.msg.attr, 4);
+ "data" => return (in.text, 4);
+ "file" => return (in._file, 4);
+ "kind" => return (in.msg.kind, 4);
+ }
+ return ("$", 0);
+}
+
+# compress ../ references and do other cleanups
+cleanname(name: string): string
+{
+ # compress multiple slashes
+ n := len name;
+ for(i:=0; i<n-1; i++)
+ if(name[i]=='/' && name[i+1]=='/'){
+ name = name[0:i]+name[i+1:];
+ --i;
+ n--;
+ }
+ # eliminate ./
+ for(i=0; i<n-1; i++)
+ if(name[i]=='.' && name[i+1]=='/' && (i==0 || name[i-1]=='/')){
+ name = name[0:i]+name[i+2:];
+ --i;
+ n -= 2;
+ }
+ found: int;
+ do{
+ # compress xx/..
+ found = 0;
+ for(i=1; i<=n-3; i++)
+ if(name[i:i+3] == "/.."){
+ if(i==n-3 || name[i+3]=='/'){
+ found = 1;
+ break;
+ }
+ }
+ if(found)
+ for(j:=i-1; j>=0; --j)
+ if(j==0 || name[j-1]=='/'){
+ i += 3; # character beyond ..
+ if(i<n && name[i]=='/')
+ ++i;
+ name = name[0:j]+name[i:];
+ n -= (i-j);
+ break;
+ }
+ }while(found);
+ # eliminate trailing .
+ if(n>=2 && name[n-2]=='/' && name[n-1]=='.')
+ --n;
+ if(n == 0)
+ return ".";
+ if(n != len name)
+ name = name[0:n];
+ return name;
+}
+
+log(s: string)
+{
+ if(len s == 0)
+ return;
+ if(s[len s-1] != '\n')
+ s[len s] = '\n';
+ sys->print("plumb: %s", s);
+}