func (c *Init) Run(ctx *cli.Context) error { c.Println("Writing version control hooks...") dir, err := os.Getwd() if err != nil { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) } err = vc.Hook() if err != nil { return errwrap.Wrapf("Failed to write hooks: {{err}}", err) } c.Println("Hooks written!") err = NewStart().Run(ctx) if err != nil { return err } err = NewPull().Run(ctx) if err != nil { if errwrap.Contains(err, vcs.ErrNoRemote.Error()) { c.Println("No remote found, skipping pull") } else { return err } } return nil }
func (k *Keeper) Load() error { f, err := os.Open(k.ledgerPath) if err != nil { if !os.IsNotExist(err) { return errwrap.Wrapf(fmt.Sprintf("Failed to open '%s': {{err}}", k.ledgerPath), err) } } else { defer f.Close() dec := json.NewDecoder(f) err := dec.Decode(k) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to decode JSON in '%s': {{err}}", k.ledgerPath), err) } //immediately restart and link save channel if not paused for _, t := range k.keeperData.Timers { t.SetSave(k.save) if !t.IsPaused() { t.Start() } } } return nil }
func (c *Pull) Run(ctx *cli.Context) error { dir, err := os.Getwd() if err != nil { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) } remote := ctx.Args().First() if remote == "" { remote, err = vc.DefaultRemote() if err != nil { return errwrap.Wrapf("Failed to determine default remote: {{err}}", err) } } err = vc.Pull(remote) if err != nil { if err == vcs.ErrNoRemoteTimeData { c.Printf("Remote '%s' has no time data (yet), nothing to pull\n", remote) return nil } return errwrap.Wrapf("Failed to pull time data: {{err}}", err) } c.Println("Time data was pulled successfully") return nil }
func (s *Server) timersCreate(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { s.Respond(w, err) return } if dirs, ok := r.Form["dir"]; !ok { s.Respond(w, fmt.Errorf("dir parameter is mandatory")) return } else { for _, dir := range dirs { t, err := NewTimer(dir) if err != nil { s.Respond(w, errwrap.Wrapf("Failed to create new timer: {{err}}", err)) return } err = s.keeper.Add(t) if err != nil { s.Respond(w, errwrap.Wrapf("Failed to add new timer to keeper: {{err}}", err)) return } } } w.WriteHeader(http.StatusCreated) }
func (c *Install) Run(ctx *cli.Context) error { c.Println("Installing the Timeglass background service...") //attempt to install cmd := exec.Command("glass-daemon", "install") cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout err := cmd.Run() if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to install Daemon: {{err}}"), err) } c.Println("Starting the Timeglass background service...") //attempt to start cmd = exec.Command("glass-daemon", "start") cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout err = cmd.Run() if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to start Daemon: {{err}}"), err) } c.Println("Done!") return nil }
func (c *Sum) Run(ctx *cli.Context) error { dir, err := os.Getwd() if err != nil { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } //write the vcs vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) } //retrieve commits through piped stdin or arguments commits := []string{} if !isatty.IsTerminal(os.Stdin.Fd()) { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { commits = append(commits, scanner.Text()) } if err := scanner.Err(); err != nil { c.Println(err) } } else { if ctx.Args().First() != "" { commits = append(commits, ctx.Args().First()) } commits = append(commits, ctx.Args().Tail()...) } if len(commits) == 0 { return fmt.Errorf("Please provide at least one commit through STDIN or as an argument.") } //map time data from the vcs list := []vcs.TimeData{} for _, c := range commits { data, err := vc.Show(c) if err != nil { if err == vcs.ErrNoCommitTimeData { //ignore if a commit has no time attached continue } return errwrap.Wrapf(fmt.Sprintf("Failed to show time notes for '%s': {{err}}", c), err) } list = append(list, data) } //reduce to output var total time.Duration for _, data := range list { total += data.Total() } fmt.Fprintln(os.Stdout, total) return nil }
func (s *Server) timersInfo(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { s.Respond(w, err) return } timers := []*Timer{} if dirs, ok := r.Form["dir"]; !ok { s.Respond(w, fmt.Errorf("dir parameter is mandatory")) return } else { for _, dir := range dirs { t, err := s.keeper.Get(dir) if err != nil { s.Respond(w, errwrap.Wrapf("Failed to get timer: {{err}}", err)) return } timers = append(timers, t) } } s.Respond(w, timers) }
func (c *Client) Call(method string, params url.Values) ([]byte, error) { loc := fmt.Sprintf("%s/api/%s?%s", c.endpoint, method, params.Encode()) resp, err := c.Get(loc) if err != nil { return nil, ErrRequestFailed } body := bytes.NewBuffer(nil) defer resp.Body.Close() _, err = io.Copy(body, resp.Body) if err != nil { return body.Bytes(), errwrap.Wrapf(fmt.Sprintf("Failed to buffer response body: {{err}}"), err) } if resp.StatusCode > 299 { errresp := &struct { Error string }{} err := json.Unmarshal(body.Bytes(), &errresp) if err != nil || errresp.Error == "" { return body.Bytes(), fmt.Errorf("Unexpected StatusCode returned from Deamon: '%d', body: '%s'", resp.StatusCode, body.String()) } else if strings.Contains(errresp.Error, "No known timer") { return body.Bytes(), ErrTimerNotFound } return body.Bytes(), fmt.Errorf(errresp.Error) } return body.Bytes(), nil }
func (t *Timer) Stop() { if !t.running { return } if t.monitor != nil { //@todo Remove this at some point, it normalizes //time after rapid stop start usage on OSX //for unkown reasons, long term solution should probably //involve some mechanism that prevents the darwin monitor //form stopping to quickly after being started <-time.After(time.Millisecond) err := t.monitor.Stop() if err != nil { log.Print(errwrap.Wrapf("Failed to stop monitor: {{err}}", err)) } t.monitor = nil } t.stopto <- struct{}{} t.stoptick <- struct{}{} t.timerData.Paused = true t.running = false }
func (c *Punch) Run(ctx *cli.Context) error { dir, err := os.Getwd() if err != nil { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } var input string if isatty.IsTerminal(os.Stdin.Fd()) { c.Println("Reading input from argument...") input = ctx.Args().First() } else { c.Println("Reading input from Stdin...") bytes, err := ioutil.ReadAll(os.Stdin) if err != nil { return errwrap.Wrapf("Failed to read time from Stdin: {{err}}", err) } input = string(bytes) } if input == "" { return fmt.Errorf("Please provide the time you spent as the first argument") } t, err := time.ParseDuration(input) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to parse provided argument '%s' as a valid duration (e.g 1h2m10s): {{err}}", input), err) } //write the vcs vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) } c.Printf("Persisting %s to version control...", t) err = vc.Persist(t) if err != nil { return errwrap.Wrapf("Failed to log time into VCS: {{err}}", err) } c.Println("Done!") return nil }
func (c *Start) Run(ctx *cli.Context) error { dir, err := os.Getwd() if err != nil { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) } c.Println("Starting timer...") client := NewClient() err = client.CreateTimer(vc.Root()) if err != nil { return errwrap.Wrapf("Failed to create timer: {{err}}", err) } c.Println("Timer started!") return nil }
func (c *Pause) Run(ctx *cli.Context) error { dir, err := os.Getwd() if err != nil { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) } c.Printf("Pausing timer...") client := NewClient() err = client.PauseTimer(vc.Root()) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to pause timer: {{err}}"), err) } c.Printf("Done!") return nil }
func SystemTimeglassPathCreateIfNotExist() (string, error) { path, err := SystemTimeglassPath() if err != nil { return "", err } err = os.MkdirAll(path, 0755) if err != nil { return "", errwrap.Wrapf(fmt.Sprintf("Failed to create Timeglass system dir '%s': {{err}}", path), err) } return path, nil }
func (c *Client) Info() (map[string]interface{}, error) { data, err := c.Call("", url.Values{}) if err != nil { return nil, err } v := map[string]interface{}{} err = json.Unmarshal(data, &v) if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("Failed to deserialize '%s' into map: {{err}}", data), err) } return v, nil }
func (t *MBU) UnmarshalJSON(data []byte) error { raw, err := strconv.Unquote(string(data)) if err != nil { return err } parsed, err := time.ParseDuration(raw) if err != nil { return errwrap.Wrapf("Failed to parse duration: {{err}}", err) } *t = MBU(parsed) return nil }
func (p *daemon) Start(s service.Service) error { var err error path, err := SystemTimeglassPathCreateIfNotExist() if err != nil { return errwrap.Wrapf("Failed to find Timeglass system path: {{err}}", err) } p.keeper, err = NewKeeper(path) if err != nil { return errwrap.Wrapf("Failed to create time keeper: {{err}}", err) } bind := "127.0.0.1:3838" p.server, err = NewServer(bind, p.keeper) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to create server on '%s': {{err}}, is the service already running?", bind), err) } go p.server.checkVersion() go p.keeper.Start() go p.run() return nil }
func (k *Keeper) Save() error { f, err := os.OpenFile(k.ledgerPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { return err } defer f.Close() enc := json.NewEncoder(f) err = enc.Encode(k) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Error saving ledger to '%s': {{err}}", k.ledgerPath), err) } return nil }
func NewLogger(w io.Writer) (*Logger, error) { l := &Logger{} path, err := SystemTimeglassPathCreateIfNotExist() if err != nil { return nil, errwrap.Wrapf("Failed to find Timeglass system path: {{err}}", err) } l.path = filepath.Join(path, "daemon.log") l.file, err = os.OpenFile(l.path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return nil, err } l.Writer = io.MultiWriter(l.file, w) return l, nil }
func (c *Client) ReadTimer(dir string) (*daemon.Timer, error) { timers := []*daemon.Timer{} params := url.Values{} params.Set("dir", dir) data, err := c.Call("timers.info", params) if err != nil { return nil, err } err = json.Unmarshal(data, &timers) if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("Failed to deserialize '%s' into a list of timers: {{err}}", data), err) } if len(timers) < 1 { return nil, fmt.Errorf("Expected at least one timer from the daemon") } return timers[0], nil }
func (s *Server) timersDelete(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { s.Respond(w, err) return } if dirs, ok := r.Form["dir"]; !ok { s.Respond(w, fmt.Errorf("dir parameter is mandatory")) return } else { for _, dir := range dirs { err := s.keeper.Remove(dir) if err != nil { s.Respond(w, errwrap.Wrapf("Failed to remove timer: {{err}}", err)) return } } } w.WriteHeader(http.StatusNoContent) }
func NewServer(httpb string, keeper *Keeper) (*Server, error) { l, err := net.Listen("tcp", httpb) if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("Failed to create listener on '%s': {{err}}", httpb), err) } mux := http.NewServeMux() s := &Server{ keeper: keeper, httpb: httpb, listener: l, Server: &http.Server{Handler: mux}, } mux.HandleFunc("/api/", s.api) mux.HandleFunc("/api/timers.create", s.timersCreate) mux.HandleFunc("/api/timers.pause", s.timersPause) mux.HandleFunc("/api/timers.delete", s.timersDelete) mux.HandleFunc("/api/timers.reset", s.timersReset) mux.HandleFunc("/api/timers.info", s.timersInfo) return s, nil }
func ReadConfig(dir, sysdir string) (*Config, error) { //get system wide config sysconf := &Config{} sysconfp := filepath.Join(sysdir, confFilename) sysconfdata, err := ioutil.ReadFile(sysconfp) if err != nil && !os.IsNotExist(err) { return nil, errwrap.Wrapf(fmt.Sprintf("Failed to read system configuration file '%s' even though it exist: {{err}}", sysconfp), err) } else if os.IsNotExist(err) { sysconf = DefaultConfig } else { err := json.Unmarshal(sysconfdata, &sysconf) if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("Failed to parse system configuration '%s': {{err}}", sysconfp), err) } err = mergo.Merge(sysconf, DefaultConfig) if err != nil { return nil, errwrap.Wrapf("Failed to merge system config with default config: {{err}} ", err) } } //get project wide config conf := &Config{} confp := filepath.Join(dir, confFilename) confdata, err := ioutil.ReadFile(confp) if err != nil && !os.IsNotExist(err) { return nil, errwrap.Wrapf(fmt.Sprintf("Failed to read project configuration file '%s' even though it exist: {{err}}", confp), err) } else if os.IsNotExist(err) { conf = sysconf } else { err := json.Unmarshal(confdata, &conf) if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("Failed to parse project configuration '%s': {{err}}", confp), err) } err = mergo.Merge(conf, sysconf) if err != nil { return nil, errwrap.Wrapf("Failed to merge project config with system config: {{err}} ", err) } } return conf, nil }
func (c *Push) Run(ctx *cli.Context) error { dir, err := os.Getwd() if err != nil { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } sysdir, err := daemon.SystemTimeglassPath() if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to get system config path: {{err}}"), err) } conf, err := config.ReadConfig(dir, sysdir) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to read configuration: {{err}}"), err) } //hooks require us require us to check the refs that are pushed over stdin //to prevent inifinte push loop refs := "" if !isatty.IsTerminal(os.Stdin.Fd()) { bytes, err := ioutil.ReadAll(os.Stdin) if err != nil { return errwrap.Wrapf("Failed to read from stdin: {{err}}", err) } refs = string(bytes) //when `glass push` triggers the pre-push hook it will not //provide any refs on stdin //this probalby means means there is nothing left to push and //we return here to prevent recursive push if refs == "" { return nil } //configuration can explicitly request not to push time data automatically //on hook usage if !conf.AutoPush { return nil } } vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) } remote := ctx.Args().First() if remote == "" { remote, err = vc.DefaultRemote() if err != nil { return errwrap.Wrapf("Failed to determine default remote: {{err}}", err) } } err = vc.Push(remote, refs) if err != nil { if err == vcs.ErrNoLocalTimeData { c.Printf("Local clone has no time data (yet), nothing to push to '%s'. Start a timer and commit changes to record local time data.\n", remote) return nil } return errwrap.Wrapf("Failed to push time data: {{err}}", err) } return nil }
func (c *Status) Run(ctx *cli.Context) error { dir, err := os.Getwd() if err != nil { return errwrap.Wrapf("Failed to fetch current working dir: {{err}}", err) } vc, err := vcs.GetVCS(dir) if err != nil { return errwrap.Wrapf("Failed to setup VCS: {{err}}", err) } sysdir, err := daemon.SystemTimeglassPath() if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to get system config path: {{err}}"), err) } conf, err := config.ReadConfig(vc.Root(), sysdir) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to read configuration: {{err}}"), err) } client := NewClient() //fetch information on overall daemon c.Printf("Fetching daemon info...") dinfo, err := client.Info() if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to fetch daemon info: {{err}}"), err) } curr, _ := strconv.Atoi(strings.Replace(dinfo["version"].(string), ".", "", 2)) recent, _ := strconv.Atoi(strings.Replace(dinfo["newest_version"].(string), ".", "", 2)) if curr != 0 && recent > curr { c.Println("A new version is available, please upgrade: https://github.com/timeglass/glass/releases") } //fetch information on the timer specific to this directory c.Printf("Fetching timer info...") timer, err := client.ReadTimer(vc.Root()) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to fetch timer: {{err}}"), err) } if reason := timer.HasFailed(); reason != "" { c.Printf("Timer has failed: %s", reason) } else { if timer.IsPaused() { c.Printf("Timer is currently: PAUSED") } else { c.Printf("Timer is currently: RUNNING") } } tmpls := ctx.String("template") if ctx.Bool("commit-template") { tmpls = conf.CommitMessage } //we got some template specified if tmpls != "" { //parse temlate and only report error if we're talking to a human tmpl, err := template.New("commit-msg").Parse(tmpls) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to parse commit_message: '%s' in configuration as a text/template: {{err}}", conf.CommitMessage), err) } //execute template and write to stdout err = tmpl.Execute(os.Stdout, timer.Time()) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Failed to execute commit_message: template for time '%s': {{err}}", timer.Time()), err) } } else { //just print c.Printf("Timer reads: %s", timer.Time()) } return nil }
// Start get called in a multitude of different situations: // - when the service starts after a reboot and loads timer state from the ledger // - when a new timer is added (for a new project) func (t *Timer) Start() { var err error //already running and not failed? no-op if t.running && t.HasFailed() == "" { return } t.timerData.Failed = "" //load project/ system specific configuration sysdir, err := SystemTimeglassPathCreateIfNotExist() if err != nil { err = errwrap.Wrapf(fmt.Sprintf("Failed to read system config: {{err}}"), err) t.timerData.Failed = err.Error() } conf, err := config.ReadConfig(t.Dir(), sysdir) if err != nil { err = errwrap.Wrapf(fmt.Sprintf("Failed to read configuration for '%s': {{err}}, using default", t.Dir()), err) t.timerData.Failed = err.Error() conf = config.DefaultConfig } t.timerData.MBU = time.Duration(conf.MBU) t.timerData.Timeout = 4 * t.timerData.MBU //lazily initiate control members t.stopto = make(chan struct{}) t.stoptick = make(chan struct{}) t.reset = make(chan struct{}) //setup monitor, if not done yet wakeup := make(chan monitor.DirEvent) merrs := make(chan error) if t.monitor == nil { t.monitor, err = monitor.New(t.Dir(), monitor.Recursive, t.timerData.Latency) if err != nil { err = errwrap.Wrapf(fmt.Sprintf("Failed to create monitor for directory '%s': {{err}}", t.Dir()), err) t.timerData.Failed = err.Error() log.Print(err) } else { wakeup, err = t.monitor.Start() if err != nil { err = errwrap.Wrapf("Failed to start monitor: {{err}}", err) t.timerData.Failed = err.Error() log.Print(err) } merrs = t.monitor.Errors() } } else { wakeup = t.monitor.Events() merrs = t.monitor.Errors() } //handle stops, pauses, timeouts and wakeups log.Printf("Timer for project '%s' was started (and unpaused) explicitely", t.Dir()) t.timerData.Paused = false t.running = true go func() { for { t.EmitSave() select { case <-t.stopto: log.Printf("Timer for project '%s' was stopped (and paused) explicitely", t.Dir()) return case merr := <-merrs: log.Printf("Monitor Error: %s", merr) t.timerData.Failed = merr.Error() case <-time.After(t.timerData.Timeout): if !t.IsPaused() { log.Printf("Timer for project '%s' timed out after %s", t.Dir(), t.timerData.Timeout) } t.Pause() case ev := <-wakeup: if t.IsPaused() { log.Printf("Timer for project '%s' woke up after some activity in '%s'", t.Dir(), ev.Dir()) t.Unpause() } else { log.Printf("Timer saw activity for project '%s' in '%s' but is already unpaused", t.Dir(), ev.Dir()) } } } }() //handle time modifications here go func() { for { if !t.timerData.Paused { t.timerData.Time += t.timerData.MBU } t.EmitSave() select { case <-t.stoptick: return case <-t.reset: t.timerData.Time = 0 log.Printf("Timer for project '%s' was reset", t.Dir()) case <-time.After(t.timerData.MBU): } } }() }