diff options
Diffstat (limited to 'pim/lib/cal/server.go')
| -rw-r--r-- | pim/lib/cal/server.go | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/pim/lib/cal/server.go b/pim/lib/cal/server.go new file mode 100644 index 0000000..f9da81e --- /dev/null +++ b/pim/lib/cal/server.go @@ -0,0 +1,325 @@ +// Package cal serves a calendar as a file tree. +// +// The tree is the interface and it is the same for every backend: +// +// ctl read: state. write: refresh, window <days> +// query write a query, read the answer, as with cs(8) +// alarm blocking read; one line per alarm due +// changed blocking read; one line per rebuild +// events/date/yyyy/mm/dd/hhmm-summary +// events/uuid/<uid>/... +// +// A backend supplies events and, where its protocol allows, takes +// changes back. Everything else lives here. +package cal + +import ( + "fmt" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/knusbaum/go9p" + "github.com/knusbaum/go9p/fs" +) + +// Argv0 prefixes diagnostics. A command sets it to its own name. +var Argv0 = "cal" + +func Warnf(format string, a ...interface{}) { + fmt.Fprintf(os.Stderr, "%s: %s\n", Argv0, fmt.Sprintf(format, a...)) +} + +// Server presents one calendar. +type Server struct { + be Backend + + fsys *fs.FS + root *fs.StaticDir + user string + + window time.Duration + + // /srv records only a name, an owner and a mode, so a server that + // wants to be identifiable has to say so itself. + srv string + conf string + pid int + wd string + started time.Time + + mu sync.Mutex + evs []*Event + evdir map[*Event]string // event -> its directory name under uuid/ + index []index // every published occurrence, for query + gen int + alarm fs.Stream + changed fs.Stream +} + +// Config is what a command must decide and the library will not. +type Config struct { + User string + Srv string // service name posted in /srv + Conf string // path of the config file, for ctl + Window time.Duration // how far either side of now to expand +} + +// New builds a server over a backend, without serving it. +func New(be Backend, c Config) *Server { + fsys, root := fs.NewFS(c.User, c.User, 0555) + s := &Server{ + be: be, fsys: fsys, root: root, user: c.User, + window: c.Window, srv: c.Srv, conf: c.Conf, + pid: os.Getpid(), started: time.Now(), + } + s.wd, _ = os.Getwd() + s.addCtl() + s.addAlarm() + s.addQuery() + s.addChanged() + return s +} + +// Serve syncs once, publishes the tree, and keeps it current for as long +// as the process runs. Nothing outside ever asks for a reload; watchers +// read changed instead. +func (s *Server) Serve() error { + if _, err := s.be.Sync(); err != nil { + Warnf("%s: %v", s.be.Name(), err) + } + if err := s.Reload(); err != nil { + return err + } + if d := s.be.Refresh(); d > 0 { + go s.poll(d) + } + Warnf("serving /srv/%s", s.srv) + return go9p.PostSrv(s.srv, s.fsys.Server()) +} + +// Report loads and says what was found, without serving. +func (s *Server) Report() error { + if _, err := s.be.Sync(); err != nil { + return err + } + t := time.Now() + evs, err := s.be.Events() + if err != nil { + return err + } + load := time.Since(t) + + t = time.Now() + now := time.Now() + insts := expand(evs, now.Add(-s.window), now.Add(s.window)) + exp := time.Since(t) + + var recur, alarms int + for _, e := range evs { + if e.set != nil { + recur++ + } + alarms += len(e.Alarms) + } + fmt.Printf("calendar %s\n", s.be.Name()) + fmt.Printf("caps %s\n", s.be.Caps()) + fmt.Printf("events %d\n", len(evs)) + fmt.Printf("recurring %d\n", recur) + fmt.Printf("alarms %d\n", alarms) + fmt.Printf("instances %d (window +/-%d days)\n", + len(insts), int(s.window/(24*time.Hour))) + fmt.Printf("load %v\n", load.Round(time.Millisecond)) + fmt.Printf("expand %v\n", exp.Round(time.Millisecond)) + return nil +} + +func (s *Server) poll(every time.Duration) { + for { + time.Sleep(every) + changed, err := s.be.Sync() + if err != nil { + Warnf("%s: %v", s.be.Name(), err) + continue + } + if !changed { + continue // a poll that changes nothing wakes nobody + } + if err := s.Reload(); err != nil { + Warnf("%s: reload: %v", s.be.Name(), err) + } + } +} + +// Reload rebuilds the whole tree from the backend's current events. +func (s *Server) Reload() error { + evs, err := s.be.Events() + if err != nil { + return err + } + now := time.Now() + insts := expand(evs, now.Add(-s.window), now.Add(s.window)) + + s.mu.Lock() + s.evs = evs + s.index = nil + s.gen++ + gen := s.gen + s.mu.Unlock() + + s.evdir = nil + s.root.DeleteChild("events") + s.buildEvents(s.root, evs) + s.buildWhen(s.root, insts) + + go s.schedule(gen, insts) + Warnf("loaded %d events, %d instances", len(evs), len(insts)) + if s.changed != nil { + // wake anything watching, so a gui knows to re-walk + s.changed.Write([]byte(fmt.Sprintf("reload %d events %d instances %d\n", + gen, len(evs), len(insts)))) + } + return nil +} + +func (s *Server) addCtl() { + st := s.fsys.NewStat("ctl", s.user, s.user, 0666) + base := fs.NewStaticFile(st, []byte("")) + s.root.AddChild(&fs.WrappedFile{ + File: base, + ReadF: func(fid uint64, off uint64, count uint64) ([]byte, error) { + b := []byte(s.ctlText()) + if off >= uint64(len(b)) { + return []byte{}, nil + } + end := off + count + if end > uint64(len(b)) { + end = uint64(len(b)) + } + return b[off:end], nil + }, + WriteF: func(fid uint64, off uint64, data []byte) (uint32, error) { + if err := s.control(string(data)); err != nil { + return 0, err + } + return uint32(len(data)), nil + }, + }) +} + +func (s *Server) ctlText() string { + s.mu.Lock() + n := len(s.evs) + s.mu.Unlock() + + text := fmt.Sprintf( + "srv /srv/%s\npid %d\nuser %s\nstarted %s\n"+ + "args %s\nwd %s\nconfig %s\n"+ + "window %d\nevents %d\ncaps %s\n", + s.srv, s.pid, s.user, s.started.Format(time.RFC3339), + strings.Join(os.Args, " "), s.wd, orNone(s.conf), + int(s.window/(24*time.Hour)), n, s.be.Caps()) + + if d, ok := s.be.(Describer); ok { + text += d.Describe() + } + when, err := s.be.Status() + line := fmt.Sprintf("cal %s refresh %v fetched %s", + s.be.Name(), s.be.Refresh(), when.Format(time.RFC3339)) + if err != "" { + line += " error " + err + } + return text + line + "\n" +} + +func (s *Server) control(cmd string) error { + f := strings.Fields(cmd) + if len(f) == 0 { + return nil + } + switch f[0] { + case "refresh": + if _, err := s.be.Sync(); err != nil { + return err + } + return s.Reload() + case "window": + if len(f) != 2 { + return fmt.Errorf("usage: window days") + } + n, err := strconv.Atoi(f[1]) + if err != nil || n <= 0 { + return fmt.Errorf("bad window %q", f[1]) + } + s.mu.Lock() + s.window = time.Duration(n) * 24 * time.Hour + s.mu.Unlock() + return s.Reload() + } + return fmt.Errorf("unknown command %q", f[0]) +} + +// addAlarm creates the blocking alarm file. A read blocks until the next +// alarm is due; every reader gets every alarm. +func (s *Server) addAlarm() { + stream := fs.NewBlockingStream(8) + st := s.fsys.NewStat("alarm", s.user, s.user, 0444) + s.alarm = stream + s.root.AddChild(fs.NewStreamFile(st, stream)) +} + +// addChanged creates the changed file. A read blocks until the tree has +// been rebuilt, so a gui learns to walk it again without polling. +func (s *Server) addChanged() { + stream := fs.NewBlockingStream(8) + st := s.fsys.NewStat("changed", s.user, s.user, 0444) + s.changed = stream + s.root.AddChild(fs.NewStreamFile(st, stream)) +} + +type firing struct { + at time.Time + inst Instance +} + +// schedule fires the alarms for one generation of the tree, and exits as +// soon as a later reload has bumped the generation. +func (s *Server) schedule(gen int, insts []Instance) { + now := time.Now() + var fs_ []firing + for _, in := range insts { + for _, d := range in.Ev.Alarms { + at := in.Start.Add(d) + if at.After(now) { + fs_ = append(fs_, firing{at, in}) + } + } + } + sort.Slice(fs_, func(i, j int) bool { return fs_[i].at.Before(fs_[j].at) }) + + for _, f := range fs_ { + if d := time.Until(f.at); d > 0 { + time.Sleep(d) + } + s.mu.Lock() + stale := gen != s.gen + s.mu.Unlock() + if stale { + return + } + s.alarm.Write([]byte(fmt.Sprintf("%s\t%s\t%s\n", + f.at.Format(time.RFC3339), + f.inst.Start.Format(time.RFC3339), + f.inst.Ev.Summary))) + } +} + +func orNone(s string) string { + if s == "" { + return "(none)" + } + return s +} |
