diff --git a/.gitignore b/.gitignore index ade3b91..711de40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ vendor/ bin/ -log/ \ No newline at end of file +log/ + +# coverage +*.html +*.out \ No newline at end of file diff --git a/Makefile b/Makefile index dacc7dc..c40c0c4 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +COVER_PROFILE=cover.out +COVER_HTML=cover.html + +.PHONY: $(COVER_PROFILE) $(COVER_HTML) + all: open build: clean @@ -16,14 +21,21 @@ clean: start: go run cmd/main.go -test: - go test -v -race -failfast ./... +test:coverage + +coverage: $(COVER_HTML) + +$(COVER_HTML): $(COVER_PROFILE) + go tool cover -html=$(COVER_PROFILE) -o $(COVER_HTML) + +$(COVER_PROFILE): + go test -v -failfast -race -coverprofile=$(COVER_PROFILE) ./... vet: go vet $(shell glide nv) lint: - go list ./... | grep -v vendor | xargs -L1 golint -set_exit_status + go list ./... | grep -v vendor | grep -v /assets/ |xargs -L1 golint -set_exit_status .PHONY: build .PHONY: clean \ No newline at end of file diff --git a/pkg/mousemover/doc.go b/pkg/mousemover/doc.go new file mode 100644 index 0000000..8ee78a0 --- /dev/null +++ b/pkg/mousemover/doc.go @@ -0,0 +1,24 @@ +/* +Ever felt the need to keep your machine awake without having to resort to the age-old +methods of installing an app that you don't trust or playing a video? Well, not anymore! + +Introducing the simplest app on the market that has the sole purpose of moving your mouse +pointer at regular intervals so that your machine never sleeps! And best of all, it works +ONLY when you are not working, so be rest assured that the mouse won't start moving on its +own without the machine actually being idle. + + +Installation + +The libary can be installed using: + go get -u github.com/automatic-mouse-mover/pkg/mousemover + +Usage + +Clone this repo and run Make, it should create the amm.app and open the folder where +it was built for you. You just have to drag and drop it to the Applications folder on your mac. + +Refer to the README for more details. + +*/ +package mousemover diff --git a/pkg/mousemover/mouseMover.go b/pkg/mousemover/mouseMover.go index 89389f3..42c6a2d 100644 --- a/pkg/mousemover/mouseMover.go +++ b/pkg/mousemover/mouseMover.go @@ -2,13 +2,8 @@ package mousemover import ( "fmt" - "os" - "sync" "time" - "github.com/sirupsen/logrus" - log "github.com/sirupsen/logrus" - "github.com/go-vgo/robotgo" "github.com/prashantgupta24/activity-tracker/pkg/activity" "github.com/prashantgupta24/activity-tracker/pkg/tracker" @@ -16,25 +11,18 @@ import ( var instance *MouseMover -//MouseMover is the main struct for the app -type MouseMover struct { - quit chan struct{} - mutex sync.RWMutex - runningStatus bool - logFile *os.File -} - const ( timeout = 100 //ms logDir = "log" - logFileName = "logFile-amm-1" + logFileName = "logFile-amm-2" ) //Start the main app func (m *MouseMover) Start() { - if m.isRunning() { + if m.state.isRunning() { return } + m.state = &state{} m.quit = make(chan struct{}) heartbeatInterval := 60 //value always in seconds @@ -47,37 +35,43 @@ func (m *MouseMover) Start() { } heartbeatCh := activityTracker.Start() + m.run(heartbeatCh, activityTracker) +} - go func(m *MouseMover) { - logger := getLogger(m, false) //set writeToFile=true only for debugging - m.updateRunningStatus(true) +func (m *MouseMover) run(heartbeatCh chan *tracker.Heartbeat, activityTracker *tracker.Instance) { + go func() { + state := m.state + if state != nil && state.isRunning() { + return + } + state.updateRunningStatus(true) + + logger := getLogger(m, false, logFileName) //set writeToFile=true only for debugging movePixel := 10 - var lastMoved time.Time - isSystemSleeping := false - didNotMoveTimes := 0 for { select { case heartbeat := <-heartbeatCh: if !heartbeat.WasAnyActivity { - if isSystemSleeping { + if state.isSystemSleeping() { logger.Infof("system sleeping") continue } mouseMoveSuccessCh := make(chan bool) - go moveAndCheck(movePixel, mouseMoveSuccessCh) + go moveAndCheck(state, movePixel, mouseMoveSuccessCh) select { case wasMouseMoveSuccess := <-mouseMoveSuccessCh: if wasMouseMoveSuccess { - lastMoved = time.Now() - logger.Infof("moved mouse at : %v\n\n", lastMoved) + state.updateLastMouseMovedTime(time.Now()) + logger.Infof("moved mouse at : %v\n\n", state.getLastMouseMovedTime()) movePixel *= -1 - didNotMoveTimes = 0 + state.updateDidNotMoveCount(0) } else { - didNotMoveTimes++ + didNotMoveCount := state.getDidNotMoveCount() + state.updateDidNotMoveCount(didNotMoveCount + 1) msg := fmt.Sprintf("Mouse pointer cannot be moved at %v. Last moved at %v. Happened %v times. See README for details.", - time.Now(), lastMoved, didNotMoveTimes) + time.Now(), state.getLastMouseMovedTime(), state.getDidNotMoveCount()) logger.Errorf(msg) - if didNotMoveTimes >= 3 { + if state.getDidNotMoveCount() >= 3 { go func() { robotgo.ShowAlert("Error with Automatic Mouse Mover", msg) }() @@ -88,59 +82,32 @@ func (m *MouseMover) Start() { logger.Errorf("timeout happened after %vms while trying to move mouse", timeout) } } else { - logger.Infof("activity detected in the last %v seconds.", int(heartbeatInterval)) + logger.Infof("activity detected in the last %v seconds.", int(activityTracker.HeartbeatInterval)) logger.Infof("Activity type:\n") for activityType, times := range heartbeat.ActivityMap { logger.Infof("activityType : %v times: %v\n", activityType, len(times)) if activityType == activity.MachineSleep { - isSystemSleeping = true + state.updateMachineSleepStatus(true) } else if activityType == activity.MachineWake { - isSystemSleeping = false + state.updateMachineSleepStatus(false) } } logger.Infof("\n\n\n") } case <-m.quit: logger.Infof("stopping mouse mover") - m.updateRunningStatus(false) + state.updateRunningStatus(false) activityTracker.Quit() return } } - }(m) -} - -func (m *MouseMover) isRunning() bool { - m.mutex.RLock() - defer m.mutex.RUnlock() - return m.runningStatus -} -func (m *MouseMover) updateRunningStatus(isRunning bool) { - m.mutex.Lock() - defer m.mutex.Unlock() - m.runningStatus = isRunning -} - -func moveAndCheck(movePixel int, mouseMoveSuccessCh chan bool) { - currentX, currentY := robotgo.GetMousePos() - moveToX := currentX + movePixel - moveToY := currentY + movePixel - robotgo.MoveMouse(moveToX, moveToY) - - //check if mouse moved. Sometimes mac users need to give - //extra permission for controlling the mouse - movedX, movedY := robotgo.GetMousePos() - if movedX == currentX && movedY == currentY { - mouseMoveSuccessCh <- false - } else { - mouseMoveSuccessCh <- true - } + }() } //Quit the app func (m *MouseMover) Quit() { //making it idempotent - if m != nil && m.isRunning() { + if m != nil && m.state.isRunning() { m.quit <- struct{}{} } if m.logFile != nil { @@ -151,35 +118,9 @@ func (m *MouseMover) Quit() { //GetInstance gets the singleton instance for mouse mover app func GetInstance() *MouseMover { if instance == nil { - instance = &MouseMover{} + instance = &MouseMover{ + state: &state{}, + } } return instance } - -func getLogger(m *MouseMover, doWriteToFile bool) *log.Logger { - logger := log.New() - logger.Formatter = &logrus.TextFormatter{ - FullTimestamp: true, - } - - if doWriteToFile { - _, err := os.Stat(logDir) - if err != nil { - if os.IsNotExist(err) { - err = os.Mkdir(logDir, os.ModePerm) - if err != nil { - log.Fatalf("error creating dir: %v", err) - } - } - } - - logFile, err := os.OpenFile(logDir+"/"+logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - log.Fatalf("error opening file: %v", err) - } - logger.SetOutput(logFile) - m.logFile = logFile - } - - return logger -} diff --git a/pkg/mousemover/mouseMoverUtil.go b/pkg/mousemover/mouseMoverUtil.go new file mode 100644 index 0000000..3034aa1 --- /dev/null +++ b/pkg/mousemover/mouseMoverUtil.go @@ -0,0 +1,107 @@ +package mousemover + +import ( + "os" + "time" + + "github.com/go-vgo/robotgo" + "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" +) + +func getLogger(m *MouseMover, doWriteToFile bool, filename string) *log.Logger { + logger := log.New() + logger.Formatter = &logrus.TextFormatter{ + FullTimestamp: true, + } + + if doWriteToFile { + _, err := os.Stat(logDir) + if err != nil { + if os.IsNotExist(err) { + err = os.Mkdir(logDir, os.ModePerm) + if err != nil { + log.Fatalf("error creating dir: %v", err) + } + } + } + + logFile, err := os.OpenFile(logDir+"/"+filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Fatalf("error opening file: %v", err) + } + logger.SetOutput(logFile) + m.logFile = logFile + } + + return logger +} + +func moveAndCheck(state *state, movePixel int, mouseMoveSuccessCh chan bool) { + if state.override != nil { //we don't want to move mouse for tests + mouseMoveSuccessCh <- state.override.valueToReturn + return + } + currentX, currentY := robotgo.GetMousePos() + moveToX := currentX + movePixel + moveToY := currentY + movePixel + robotgo.MoveMouse(moveToX, moveToY) + + //check if mouse moved. Sometimes mac users need to give + //extra permission for controlling the mouse + movedX, movedY := robotgo.GetMousePos() + if movedX == currentX && movedY == currentY { + mouseMoveSuccessCh <- false + } else { + mouseMoveSuccessCh <- true + } +} + +//getters and setters for state variable +func (s *state) isRunning() bool { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.isAppRunning +} + +func (s *state) updateRunningStatus(isRunning bool) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.isAppRunning = isRunning +} + +func (s *state) isSystemSleeping() bool { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.isSysSleeping +} + +func (s *state) updateMachineSleepStatus(isSleeping bool) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.isSysSleeping = isSleeping +} + +func (s *state) getLastMouseMovedTime() time.Time { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.lastMouseMovedTime +} + +func (s *state) updateLastMouseMovedTime(time time.Time) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.lastMouseMovedTime = time +} + +func (s *state) getDidNotMoveCount() int { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.didNotMoveCount +} + +func (s *state) updateDidNotMoveCount(count int) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.didNotMoveCount = count +} diff --git a/pkg/mousemover/mouseMover_test.go b/pkg/mousemover/mouseMover_test.go index 7b48d2b..29e41e2 100644 --- a/pkg/mousemover/mouseMover_test.go +++ b/pkg/mousemover/mouseMover_test.go @@ -1,44 +1,177 @@ package mousemover import ( + "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + + "github.com/prashantgupta24/activity-tracker/pkg/activity" + "github.com/prashantgupta24/activity-tracker/pkg/tracker" ) type TestMover struct { suite.Suite + activityTracker *tracker.Instance + heartbeatCh chan *tracker.Heartbeat } func TestSuite(t *testing.T) { suite.Run(t, new(TestMover)) } +//Run once before all tests +func (suite *TestMover) SetupSuite() { + heartbeatInterval := 60 + workerInterval := 10 + + suite.activityTracker = &tracker.Instance{ + HeartbeatInterval: heartbeatInterval, + WorkerInterval: workerInterval, + } + + suite.heartbeatCh = make(chan *tracker.Heartbeat) +} + //Run once before each test func (suite *TestMover) SetupTest() { instance = nil } - +func (suite *TestMover) TestAppStart() { + t := suite.T() + mouseMover := GetInstance() + mouseMover.run(suite.heartbeatCh, suite.activityTracker) + time.Sleep(time.Millisecond * 500) //wait for app to start + assert.True(t, mouseMover.state.isRunning(), "app should have started") +} func (suite *TestMover) TestSingleton() { t := suite.T() + mouseMover1 := GetInstance() - mouseMover1.Start() + mouseMover1.run(suite.heartbeatCh, suite.activityTracker) time.Sleep(time.Millisecond * 500) mouseMover2 := GetInstance() - assert.True(t, mouseMover2.isRunning(), "instance should not have started") + assert.True(t, mouseMover2.state.isRunning(), "instance should have started") } -func (suite *TestMover) TestAppStartAndStop() { + +func (suite *TestMover) TestLogFile() { t := suite.T() mouseMover := GetInstance() - mouseMover.Start() - time.Sleep(time.Millisecond * 500) //wait for app to start - assert.True(t, mouseMover.isRunning(), "app should have started") + logFileName := "test1" - mouseMover.Quit() - time.Sleep(time.Millisecond * 500) //wait for app to stop - assert.False(t, mouseMover.isRunning(), "app should have stopped") + getLogger(mouseMover, true, logFileName) + + filePath := logDir + "/" + logFileName + assert.FileExists(t, filePath, "log file should exist") + os.RemoveAll(filePath) +} +func (suite *TestMover) TestSystemSleepAndWake() { + t := suite.T() + mouseMover := GetInstance() + + state := &state{ + override: &override{ + valueToReturn: true, + }, + } + mouseMover.state = state + heartbeatCh := make(chan *tracker.Heartbeat) + + mouseMover.run(heartbeatCh, suite.activityTracker) + time.Sleep(time.Millisecond * 500) //wait for app to start + assert.True(t, mouseMover.state.isRunning(), "instance should have started") + assert.False(t, mouseMover.state.isSystemSleeping(), "machine should not be sleeping") + + //fake a machine-sleep activity + machineSleepActivityMap := make(map[activity.Type][]time.Time) + var sleepTimeArray []time.Time + sleepTimeArray = append(sleepTimeArray, time.Now()) + machineSleepActivityMap[activity.MachineSleep] = sleepTimeArray + heartbeatCh <- &tracker.Heartbeat{ + WasAnyActivity: true, + ActivityMap: machineSleepActivityMap, + } + time.Sleep(time.Millisecond * 500) //wait for it to be registered + assert.True(t, mouseMover.state.isSystemSleeping(), "machine should be sleeping now") + + //assert app is sleeping + heartbeatCh <- &tracker.Heartbeat{ + WasAnyActivity: false, + } + + time.Sleep(time.Millisecond * 500) //wait for it to be registered + assert.True(t, time.Time.IsZero(state.getLastMouseMovedTime()), "should be default but is ", state.getLastMouseMovedTime()) + assert.Equal(t, state.getDidNotMoveCount(), 0, "should be 0") + + //fake a machine-wake activity + machineWakeActivityMap := make(map[activity.Type][]time.Time) + var wakeTimeArray []time.Time + wakeTimeArray = append(wakeTimeArray, time.Now()) + machineWakeActivityMap[activity.MachineWake] = wakeTimeArray + heartbeatCh <- &tracker.Heartbeat{ + WasAnyActivity: true, + ActivityMap: machineWakeActivityMap, + } + + time.Sleep(time.Millisecond * 500) //wait for it to be registered + assert.False(t, mouseMover.state.isSystemSleeping(), "machine should be awake now") +} + +func (suite *TestMover) TestMouseMoveSuccess() { + t := suite.T() + mouseMover := GetInstance() + + state := &state{ + override: &override{ + valueToReturn: true, + }, + } + mouseMover.state = state + heartbeatCh := make(chan *tracker.Heartbeat) + + mouseMover.run(heartbeatCh, suite.activityTracker) + time.Sleep(time.Millisecond * 500) //wait for app to start + assert.True(t, state.isRunning(), "instance should have started") + assert.False(t, state.isSystemSleeping(), "machine should not be sleeping") + assert.True(t, time.Time.IsZero(state.getLastMouseMovedTime()), "should be default") + assert.Equal(t, state.getDidNotMoveCount(), 0, "should be 0") + + heartbeatCh <- &tracker.Heartbeat{ + WasAnyActivity: false, + } + + time.Sleep(time.Millisecond * 500) //wait for it to be registered + assert.False(t, time.Time.IsZero(state.getLastMouseMovedTime()), "should be default but is ", state.getLastMouseMovedTime()) +} + +func (suite *TestMover) TestMouseMoveFailure() { + t := suite.T() + mouseMover := GetInstance() + + state := &state{ + override: &override{ + valueToReturn: false, + }, + } + mouseMover.state = state + heartbeatCh := make(chan *tracker.Heartbeat) + + mouseMover.run(heartbeatCh, suite.activityTracker) + time.Sleep(time.Millisecond * 500) //wait for app to start + assert.True(t, state.isRunning(), "instance should have started") + assert.False(t, state.isSystemSleeping(), "machine should not be sleeping") + assert.True(t, time.Time.IsZero(state.getLastMouseMovedTime()), "should be default") + assert.Equal(t, state.getDidNotMoveCount(), 0, "should be 0") + + heartbeatCh <- &tracker.Heartbeat{ + WasAnyActivity: false, + } + + time.Sleep(time.Millisecond * 500) //wait for it to be registered + assert.True(t, time.Time.IsZero(state.getLastMouseMovedTime()), "should be default but is ", state.getLastMouseMovedTime()) + assert.NotEqual(t, state.getDidNotMoveCount(), 0, "should not be 0") } diff --git a/pkg/mousemover/types.go b/pkg/mousemover/types.go new file mode 100644 index 0000000..f7265e7 --- /dev/null +++ b/pkg/mousemover/types.go @@ -0,0 +1,29 @@ +package mousemover + +import ( + "os" + "sync" + "time" +) + +//MouseMover is the main struct for the app +type MouseMover struct { + quit chan struct{} + logFile *os.File + state *state +} + +//state manages the internal working of the app +type state struct { + mutex sync.RWMutex + isAppRunning bool + isSysSleeping bool + lastMouseMovedTime time.Time + didNotMoveCount int + override *override +} + +//only needed for tests +type override struct { + valueToReturn bool +}