From 08ec9683136f13ca4e40428a684f274c0b0c900f Mon Sep 17 00:00:00 2001 From: Aron Granberg Date: Thu, 18 Aug 2016 10:52:24 +0200 Subject: [PATCH 1/4] add watch command A common use case for 'thyme' is to record the active windows at regular intervals. To make this easier, the command 'watch' can now be used instead of 'track'. $ thyme watch -n -o thyme.json should be equivalent to $ watch -n thyme track -o thyme.json or $ while true; do thyme track -o thyme.json; sleep s; done; but is also usable on systems that do not have 'watch' installed and easier to use than a shell loop. If no interval is specified (-n or --interval) then it defaults to 30 seconds. This was originally suggested by @keegancsmith. --- cmd/thyme/main.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cmd/thyme/main.go b/cmd/thyme/main.go index 9ea4c9d..c7989c4 100644 --- a/cmd/thyme/main.go +++ b/cmd/thyme/main.go @@ -6,6 +6,7 @@ import ( "log" "os" "runtime" + "time" "github.com/jessevdk/go-flags" "github.com/sourcegraph/thyme" @@ -17,6 +18,9 @@ func init() { if _, err := CLI.AddCommand("track", "", "record current windows", &trackCmd); err != nil { log.Fatal(err) } + if _, err := CLI.AddCommand("watch", "", "record current windows at regular intervals (default 30s)", &watchCmd); err != nil { + log.Fatal(err) + } if _, err := CLI.AddCommand("show", "", "visualize data", &showCmd); err != nil { log.Fatal(err) } @@ -25,6 +29,33 @@ func init() { } } +// WatchCmd is the subcommand that tracks application usage at regular intervals. +type WatchCmd struct { + // The track command is a subset of the watch command + TrackCmd + Interval int64 `long:"interval" short:"n" description:"update interval (default 30 seconds)"` +} + +var watchCmd WatchCmd + +func (c *WatchCmd) Execute(args []string) error { + var interval time.Duration + if c.Interval <= 0 { + // Set default interval + interval = 30 * time.Second + } else { + interval = time.Duration(c.Interval) * time.Second + } + + // Loop until the user aborts the command + for { + c.TrackCmd.Execute(args) + + // Sleep for a while until the next time we should track active windows + time.Sleep(interval) + } +} + // TrackCmd is the subcommand that tracks application usage. type TrackCmd struct { Out string `long:"out" short:"o" description:"output file"` From 099f7a49b70e96f3f7042a997afb1f51b04ef2a8 Mon Sep 17 00:00:00 2001 From: Aron Granberg Date: Thu, 18 Aug 2016 14:10:09 +0200 Subject: [PATCH 2/4] add descriptive error message when we don't have accessibility priviliges --- darwin.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/darwin.go b/darwin.go index 8ae0612..7c8b842 100644 --- a/darwin.go +++ b/darwin.go @@ -186,7 +186,22 @@ func runAS(script string) (map[process][]*Window, error) { cmd.Stdin = bytes.NewBuffer([]byte(script)) b, err := cmd.CombinedOutput() if err != nil { - return nil, fmt.Errorf("AppleScript error: %s, output was:\n%s", err, string(b)) + // This is the error code for 'osascript is not allowed assistive access'. + // Add a more informative error message than the one applescript normally outputs. + if strings.Contains(string(b), "-25211") { + formatString := ` +AppleScript error: %s +You will need to enable privileges for "Terminal" (or Iterm2 if you are using that) in +System Preferences > Security & Privacy > Privacy > Accessibility. +See https://support.apple.com/en-us/HT202802 for details. + +output was: +%s` + + return nil, fmt.Errorf(formatString, err, string(b)) + } else { + return nil, fmt.Errorf("AppleScript error: %s, output was:\n%s", err, string(b)) + } } return parseASOutput(string(b)) } From c5b42f0fe96f5cca7927375deace21b323445029 Mon Sep 17 00:00:00 2001 From: Aron Granberg Date: Thu, 18 Aug 2016 14:11:26 +0200 Subject: [PATCH 3/4] fix 'watch' command not checking for errors --- cmd/thyme/main.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/thyme/main.go b/cmd/thyme/main.go index c7989c4..0e43a07 100644 --- a/cmd/thyme/main.go +++ b/cmd/thyme/main.go @@ -49,7 +49,10 @@ func (c *WatchCmd) Execute(args []string) error { // Loop until the user aborts the command for { - c.TrackCmd.Execute(args) + err := c.TrackCmd.Execute(args) + if err != nil { + return err + } // Sleep for a while until the next time we should track active windows time.Sleep(interval) From fb108495a1655688fc4fd84ce33018d5a5ca9e6d Mon Sep 17 00:00:00 2001 From: Aron Granberg Date: Thu, 18 Aug 2016 14:13:21 +0200 Subject: [PATCH 4/4] remove 'dep' command and replace with automatic checks when the program starts --- cmd/thyme/main.go | 16 ---------------- darwin.go | 12 +++++------- data.go | 11 +++++++---- linux.go | 28 +++++++++++++++++++++------- windows.go | 4 ++-- 5 files changed, 35 insertions(+), 36 deletions(-) diff --git a/cmd/thyme/main.go b/cmd/thyme/main.go index 0e43a07..12476bc 100644 --- a/cmd/thyme/main.go +++ b/cmd/thyme/main.go @@ -24,9 +24,6 @@ func init() { if _, err := CLI.AddCommand("show", "", "visualize data", &showCmd); err != nil { log.Fatal(err) } - if _, err := CLI.AddCommand("dep", "", "external dependencies that need to be installed", &depCmd); err != nil { - log.Fatal(err) - } } // WatchCmd is the subcommand that tracks application usage at regular intervals. @@ -157,19 +154,6 @@ func (c *ShowCmd) Execute(args []string) error { return nil } -type DepCmd struct{} - -var depCmd DepCmd - -func (c *DepCmd) Execute(args []string) error { - t, err := getTracker() - if err != nil { - return err - } - fmt.Println(t.Deps()) - return nil -} - func main() { run := func() error { _, err := CLI.Parse() diff --git a/darwin.go b/darwin.go index 7c8b842..7d769c7 100644 --- a/darwin.go +++ b/darwin.go @@ -90,13 +90,11 @@ end repeat ` ) -func (t *DarwinTracker) Deps() string { - return ` -You will need the osascript command-line utility. You can install it via the Apple developer tools ('xcode-select --install') or npm ('npm install --save osascript'). - -You will need to enable privileges for "Terminal" in System Preferences > Security & Privacy > Privacy > Accessibility. -See https://support.apple.com/en-us/HT202802 for details. -` +func (t *DarwinTracker) CheckDependencies() { + _, err := exec.LookPath("osascript") + if err != nil { + log.Fatal("You will need the osascript command-line utility. You can install it via the Apple developer tools ('xcode-select --install') or npm ('npm install --save osascript').") + } } func (t *DarwinTracker) Snap() (*Snapshot, error) { diff --git a/data.go b/data.go index 66889e7..2f0f03c 100644 --- a/data.go +++ b/data.go @@ -25,7 +25,9 @@ func NewTracker(name string) Tracker { if _, exists := trackers[name]; !exists { log.Fatalf("no Tracker constructor has been registered with name %s", name) } - return trackers[name]() + tracker := trackers[name]() + tracker.CheckDependencies() + return tracker } // Tracker tracks application usage. An implementation that satisfies @@ -36,9 +38,10 @@ type Tracker interface { // at the current time. Snap() (*Snapshot, error) - // Deps returns a string listing the dependencies that still need - // to be installed with instructions for how to install them. - Deps() string + // CheckDependencies checks for external dependencies (for example + // 'osascript' on OS X) and logs a fatal error if they are not available + // as well as instructions for how to install them. + CheckDependencies() } // Stream represents all the sampling data gathered by Thyme. diff --git a/linux.go b/linux.go index 4fd2210..f8d9618 100644 --- a/linux.go +++ b/linux.go @@ -2,6 +2,7 @@ package thyme import ( "fmt" + "os" "os/exec" "regexp" "strconv" @@ -22,13 +23,26 @@ func NewLinuxTracker() Tracker { return &LinuxTracker{} } -func (t *LinuxTracker) Deps() string { - return `Install the following command-line utilities via your package manager (e.g., apt) of choice: -* xdpyinfo -* xwininfo -* xdotool -* wmctrl -` +func (t *LinuxTracker) CheckDependencies() { + deps := map[string]string{ + "xdpyinfo": "x11-utils", + "xwininfo": "x11-utils", + "xdotool": "xdotool", + "wmctrl": "wmctrl", + } + + anyFailed := false + for k, v := range deps { + _, err := exec.LookPath(k) + if err != nil { + fmt.Printf("You need to install the command line utility '%s' (usually in the package named '%s') via your package manager of choice.\nFor example 'apt-get install %s'\n\n", k, v, v) + anyFailed = true + } + } + + if anyFailed { + os.Exit(1) + } } func (t *LinuxTracker) Snap() (*Snapshot, error) { diff --git a/windows.go b/windows.go index 3aeb167..927309b 100644 --- a/windows.go +++ b/windows.go @@ -40,8 +40,8 @@ var ( procGetWindowThreadProcessId = user.NewProc("GetWindowThreadProcessId") ) -func (t *WindowsTracker) Deps() string { - return "Nothing, Ready to Go!" +func (t *WindowsTracker) CheckDependencies() { + // Nothing, Ready to Go! } // getWindowTitle returns a title of a window of the provided system window handle