// 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 // 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//... // // A backend supplies events and, where its protocol allows, takes // changes back. Everything else lives here. package cal import ( "bufio" "bytes" "crypto/rand" "encoding/hex" "fmt" "os" "sort" "strconv" "strings" "sync" "time" "github.com/emersion/go-ical" "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() s.addNew() 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 } // addNew publishes the file you write an event to. // // cat invite.ics >/mnt/pim/calendars/work/new // // Writes are buffered and the object is created on close, because that // is when the thing you are writing is complete -- cp and cat both do // create, write, clunk, so the commit point falls out of 9p rather than // having to be invented. func (s *Server) addNew() { create, ok := s.be.(Creator) if !ok { return // a backend that cannot add events does not offer the file } var mu sync.Mutex buf := map[uint64][]byte{} st := s.fsys.NewStat("new", s.user, s.user, 0222) base := fs.NewStaticFile(st, []byte("")) s.root.AddChild(&fs.WrappedFile{ File: base, WriteF: func(fid uint64, off uint64, data []byte) (uint32, error) { mu.Lock() buf[fid] = append(buf[fid], data...) mu.Unlock() return uint32(len(data)), nil }, CloseF: func(fid uint64) error { mu.Lock() b := buf[fid] delete(buf, fid) mu.Unlock() if len(b) == 0 { return nil } // Either a whole icalendar object, or the key: value form // the tree itself emits. The second exists so a script // never has to spell iCalendar, where a comma in a summary // is a syntax error waiting to happen. var c *ical.Calendar var err error if bytes.HasPrefix(bytes.TrimSpace(b), []byte("BEGIN:VCALENDAR")) { c, err = ical.NewDecoder(bytes.NewReader(b)).Decode() } else { c, err = Compose(bufio.NewReader(bytes.NewReader(b)), newUID()) } if err != nil { Warnf("new: %v", err) return err } if len(c.Events()) == 0 { Warnf("new: no VEVENT") return fmt.Errorf("new: no VEVENT") } if err := create.Create(c); err != nil { Warnf("new: %v", err) return err } if _, err := s.be.Sync(); err == nil { s.Reload() } return nil }, }) } // newUID mints an identifier for an event we are creating. This is the // one place a UID is generated rather than echoed back: as organiser we // own the event, so we name it. func newUID() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { return fmt.Sprintf("%d@pim", time.Now().UnixNano()) } host, _ := os.Hostname() if host == "" { host = "pim" } return hex.EncodeToString(b[:]) + "@" + host }