/* * Copyright (C) 2017 "IoT.bzh" * Author Sebastien Douheret <sebastien@iot.bzh> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package st import ( "encoding/json" "fmt" "os" "strconv" "strings" "time" "github.com/Sirupsen/logrus" ) // Events . type Events struct { MonitorTime time.Duration Debug bool stop chan bool st *SyncThing log *logrus.Logger cbArr map[string][]cbMap } type Event struct { Type string `json:"type"` Time time.Time `json:"time"` Data map[string]string `json:"data"` } type EventsCBData map[string]interface{} type EventsCB func(ev Event, cbData *EventsCBData) const ( EventFolderCompletion string = "FolderCompletion" EventFolderSummary string = "FolderSummary" EventFolderPaused string = "FolderPaused" EventFolderResumed string = "FolderResumed" EventFolderErrors string = "FolderErrors" EventStateChanged string = "StateChanged" ) var EventsAll string = EventFolderCompletion + "|" + EventFolderSummary + "|" + EventFolderPaused + "|" + EventFolderResumed + "|" + EventFolderErrors + "|" + EventStateChanged type STEvent struct { // Per-subscription sequential event ID. Named "id" for backwards compatibility with the REST API SubscriptionID int `json:"id"` // Global ID of the event across all subscriptions GlobalID int `json:"globalID"` Time time.Time `json:"time"` Type string `json:"type"` Data map[string]interface{} `json:"data"` } type cbMap struct { id int cb EventsCB filterID string data *EventsCBData } // NewEventListener Create a new instance of Event listener func (s *SyncThing) NewEventListener() *Events { _, dbg := os.LookupEnv("XDS_DEBUG_STEVENTS") // set to add more debug log return &Events{ MonitorTime: 100, // in Milliseconds Debug: dbg, stop: make(chan bool, 1), st: s, log: s.log, cbArr: make(map[string][]cbMap), } } // Start starts event monitoring loop func (e *Events) Start() error { go e.monitorLoop() return nil } // Stop stops event monitoring loop func (e *Events) Stop() { e.stop <- true } // Register Add a listener on an event func (e *Events) Register(evName string, cb EventsCB, filterID string, data *EventsCBData) (int, error) { if evName == "" || !strings.Contains(EventsAll, evName) { return -1, fmt.Errorf("Unknown event name") } if data == nil { data = &EventsCBData{} } cbList := []cbMap{} if _, ok := e.cbArr[evName]; ok { cbList = e.cbArr[evName] } id := len(cbList) (*data)["id"] = strconv.Itoa(id) e.cbArr[evName] = append(cbList, cbMap{id: id, cb: cb, filterID: filterID, data: data}) return id, nil } // UnRegister Remove a listener event func (e *Events) UnRegister(evName string, id int) error { cbKey, ok := e.cbArr[evName] if !ok { return fmt.Errorf("No event registered to such name") } // FIXME - NOT TESTED if id >= len(cbKey) { return fmt.Errorf("Invalid id") } else if id == len(cbKey) { e.cbArr[evName] = cbKey[:id-1] } else { e.cbArr[evName] = cbKey[id : id+1] } return nil } // GetEvents returns the Syncthing events func (e *Events) getEvents(since int) ([]STEvent, error) { var data []byte ev := []STEvent{} url := "events" if since != -1 { url += "?since=" + strconv.Itoa(since) } if err := e.st.client.HTTPGet(url, &data); err != nil { return ev, err } err := json.Unmarshal(data, &ev) return ev, err } // Loop to monitor Syncthing events func (e *Events) monitorLoop() { e.log.Infof("Event monitoring running...") since := 0 cntErrConn := 0 cntErrRetry := 1 for { select { case <-e.stop: e.log.Infof("Event monitoring exited") return case <-time.After(e.MonitorTime * time.Millisecond): if !e.st.Connected { cntErrConn++ time.Sleep(time.Second) if cntErrConn > cntErrRetry { e.log.Error("ST Event monitor: ST connection down") cntErrConn = 0 cntErrRetry *= 2 if _, err := e.getEvents(since); err == nil { e.st.Connected = true cntErrRetry = 1 // XXX - should we reset since value ? goto readEvent } } continue } readEvent: stEvArr, err := e.getEvents(since) if err != nil { e.log.Errorf("Syncthing Get Events: %v", err) e.st.Connected = false continue } // Process events for _, stEv := range stEvArr { since = stEv.SubscriptionID if e.Debug { e.log.Warnf("ST EVENT: %d %s\n %v", stEv.GlobalID, stEv.Type, stEv) } cbKey, ok := e.cbArr[stEv.Type] if !ok { continue } evData := Event{ Type: stEv.Type, Time: stEv.Time, } // Decode Events // FIXME: re-define data struct for each events // instead of map of string and use JSON marshing/unmarshing fID := "" evData.Data = make(map[string]string) switch stEv.Type { case EventFolderCompletion: fID = convString(stEv.Data["folder"]) evData.Data["completion"] = convFloat64(stEv.Data["completion"]) case EventFolderSummary: fID = convString(stEv.Data["folder"]) evData.Data["needBytes"] = convInt64(stEv.Data["needBytes"]) evData.Data["state"] = convString(stEv.Data["state"]) case EventFolderPaused, EventFolderResumed: fID = convString(stEv.Data["id"]) evData.Data["label"] = convString(stEv.Data["label"]) case EventFolderErrors: fID = convString(stEv.Data["folder"]) // TODO decode array evData.Data["errors"] = convString(stEv.Data["errors"]) case EventStateChanged: fID = convString(stEv.Data["folder"]) evData.Data["from"] = convString(stEv.Data["from"]) evData.Data["to"] = convString(stEv.Data["to"]) default: e.log.Warnf("Unsupported event type") } if fID != "" { evData.Data["id"] = fID } // Call all registered callbacks for _, c := range cbKey { if e.Debug { e.log.Warnf("EVENT CB fID=%s, filterID=%s", fID, c.filterID) } // Call when filterID is not set or when it matches if c.filterID == "" || (fID != "" && fID == c.filterID) { c.cb(evData, c.data) } } } } } } func convString(d interface{}) string { return d.(string) } func convFloat64(d interface{}) string { return strconv.FormatFloat(d.(float64), 'f', -1, 64) } func convInt64(d interface{}) string { return strconv.FormatInt(d.(int64), 10) }