From f7b66ff7cea72995a902df49e6cc132e3306465e Mon Sep 17 00:00:00 2001 From: jms-guy Date: Thu, 16 Oct 2025 13:44:09 -0400 Subject: [PATCH 1/7] debug statements --- cmd/service/service_linux.go | 1 + cmd/service/service_setup.go | 8 ++++---- sql/connection.go | 4 +++- sql/db_path_unsupported.go | 4 +++- sql/db_path_windows.go | 7 +++++-- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cmd/service/service_linux.go b/cmd/service/service_linux.go index 388166b..745ccea 100644 --- a/cmd/service/service_linux.go +++ b/cmd/service/service_linux.go @@ -57,6 +57,7 @@ func (s *timekeepService) Manage() (string, error) { s.eventCtrl.RunCtx = runCtx s.eventCtrl.Cancel = runCancel + logger.Printf("DEBUG: Getting initial programs") programs, err := s.prRepo.GetAllPrograms(context.Background()) if err != nil { return "ERROR: Failed to get programs", err diff --git a/cmd/service/service_setup.go b/cmd/service/service_setup.go index 04f7c27..3e736c5 100644 --- a/cmd/service/service_setup.go +++ b/cmd/service/service_setup.go @@ -27,18 +27,18 @@ type timekeepService struct { } func ServiceSetup() (*timekeepService, error) { - db, err := mysql.OpenLocalDatabase() + logger, err := logs.NewLogs() if err != nil { return nil, err } - store := repository.NewSqliteStore(db) - - logger, err := logs.NewLogs() + db, err := mysql.OpenLocalDatabase(logger.Logger) if err != nil { return nil, err } + store := repository.NewSqliteStore(db) + d, err := daemons.NewDaemonManager() if err != nil { return nil, err diff --git a/sql/connection.go b/sql/connection.go index c9109f0..e59c0af 100644 --- a/sql/connection.go +++ b/sql/connection.go @@ -17,12 +17,13 @@ import ( var embedMigrations embed.FS // Open database connection with embedded migrations -func OpenLocalDatabase() (*database.Queries, error) { +func OpenLocalDatabase(logger *log.Logger) (*database.Queries, error) { dbPath, err := getDatabasePath() if err != nil { return nil, err } + logger.Printf("DEBUG: Connecting to database at: %s", dbPath) // #nosec G301 if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil { return nil, fmt.Errorf("failed to create database directory: %w", err) @@ -45,6 +46,7 @@ func OpenLocalDatabase() (*database.Queries, error) { } queries := database.New(db) + logger.Printf("DEBUG: Queries set") return queries, nil } diff --git a/sql/db_path_unsupported.go b/sql/db_path_unsupported.go index 75cc271..35e52a8 100644 --- a/sql/db_path_unsupported.go +++ b/sql/db_path_unsupported.go @@ -2,6 +2,8 @@ package sql -func getDatabasePath() (string, error) { +import "log" + +func getDatabasePath(logger *log.Logger) (string, error) { return "", nil } diff --git a/sql/db_path_windows.go b/sql/db_path_windows.go index 1ddde58..dd8f6f4 100644 --- a/sql/db_path_windows.go +++ b/sql/db_path_windows.go @@ -1,10 +1,13 @@ // go: build windows package sql -import "path/filepath" +import ( + "log" + "path/filepath" +) // Gets database directory path for Windows -func getDatabasePath() (string, error) { +func getDatabasePath(logger *log.Logger) (string, error) { dataDir := `C:\ProgramData\TimeKeep` return filepath.Join(dataDir, "timekeep.db"), nil } From 71b8b1f589f4c558f5cfa19ea08a2fa72d51e989 Mon Sep 17 00:00:00 2001 From: jms-guy Date: Fri, 17 Oct 2025 13:02:37 -0400 Subject: [PATCH 2/7] changed linux service log output to journal --- README.md | 10 ++++---- cmd/service/internal/logs/logger.go | 12 +--------- cmd/service/internal/logs/logging_linux.go | 11 ++++++++- .../internal/logs/logging_unsupported.go | 9 +++++++ cmd/service/internal/logs/logging_windows.go | 24 ++++++++++++++++++- cmd/service/service_linux.go | 5 ---- cmd/service/service_setup.go | 2 +- scripts/install.sh | 7 +++--- sql/connection.go | 4 +--- sql/db_path_unsupported.go | 4 +--- sql/db_path_windows.go | 3 +-- 11 files changed, 54 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 383926e..58cfc0f 100644 --- a/README.md +++ b/README.md @@ -123,11 +123,6 @@ sudo mkdir -p /var/run/timekeep sudo chown "$USER_NAME":"$GROUP_NAME" /var/run/timekeep sudo chmod 755 /var/run/timekeep -# Create and set permissions for log directory -sudo mkdir -p /var/log/timekeep -sudo chown "$USER_NAME":"$GROUP_NAME" /var/log/timekeep -sudo chmod 755 /var/log/timekeep - # Create systemd service sudo tee /etc/systemd/system/timekeep.service > /dev/null < 0 { toTrack := []string{} for _, program := range programs { @@ -74,17 +72,14 @@ func (s *timekeepService) Manage() (string, error) { if program.Project.Valid { project = program.Project.String } - logger.Printf("DEBUG: Tracking %s", program.Name) s.sessions.EnsureProgram(program.Name, category, project) toTrack = append(toTrack, program.Name) } - logger.Printf("DEBUG: Entering main Monitor function") go s.eventCtrl.MonitorProcesses(runCtx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo, toTrack) } - logger.Printf("DEBUG: Starting heartbeats") if s.eventCtrl.Config.WakaTime.Enabled { s.eventCtrl.StartHeartbeats(runCtx, s.logger.Logger, s.sessions) } diff --git a/cmd/service/service_setup.go b/cmd/service/service_setup.go index 3e736c5..c708978 100644 --- a/cmd/service/service_setup.go +++ b/cmd/service/service_setup.go @@ -32,7 +32,7 @@ func ServiceSetup() (*timekeepService, error) { return nil, err } - db, err := mysql.OpenLocalDatabase(logger.Logger) + db, err := mysql.OpenLocalDatabase() if err != nil { return nil, err } diff --git a/scripts/install.sh b/scripts/install.sh index 39214e8..498bc7e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -33,10 +33,6 @@ sudo mkdir -p /var/run/timekeep sudo chown "$USER_NAME":"$GROUP_NAME" /var/run/timekeep sudo chmod 755 /var/run/timekeep -sudo mkdir -p /var/log/timekeep -sudo chown "$USER_NAME":"$GROUP_NAME" /var/log/timekeep -sudo chmod 755 /var/log/timekeep - sudo tee /etc/systemd/system/timekeep.service > /dev/null < Date: Fri, 17 Oct 2025 13:49:52 -0400 Subject: [PATCH 3/7] split runCtx into monCtx and wakaCtx --- .../internal/events/event_controller.go | 20 ++++------- cmd/service/internal/events/events_linux.go | 21 ++++++++++- .../internal/events/events_wakatime.go | 31 +++++++++++----- cmd/service/internal/events/events_windows.go | 35 ++++++++++--------- cmd/service/service_linux.go | 8 ++--- cmd/service/service_windows.go | 13 +++---- 6 files changed, 74 insertions(+), 54 deletions(-) diff --git a/cmd/service/internal/events/event_controller.go b/cmd/service/internal/events/event_controller.go index 42892f3..afd4fb1 100644 --- a/cmd/service/internal/events/event_controller.go +++ b/cmd/service/internal/events/event_controller.go @@ -27,9 +27,10 @@ type Command struct { } type EventController struct { - PsProcess *exec.Cmd // Powershell process for Windows event monitoring - RunCtx context.Context - Cancel context.CancelFunc // Event monitoring cancel context + PsProcess *exec.Cmd // Powershell process for Windows event monitoring + mu sync.Mutex // Mutex for context cancellations + MonCancel context.CancelFunc // Monitoring function cancel context + WakaCancel context.CancelFunc // WakaTime function cancel context Config *config.Config // Struct built from config file wakaHeartbeatTicker *time.Ticker // Ticker for WakaTime enabled heartbeats heartbeatMu sync.Mutex // Mutex for WakaTime heartbeat ticker @@ -83,17 +84,10 @@ func (e *EventController) HandleConnection(serviceCtx context.Context, logger *l } // Stops the currently running process monitoring script, and starts a new one with updated program list -func (e *EventController) RefreshProcessMonitor(ctx context.Context, logger *log.Logger, sm *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository) { +func (e *EventController) RefreshProcessMonitor(serviceCtx context.Context, logger *log.Logger, sm *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository) { e.StopHeartbeats() e.StopProcessMonitor() - if e.Cancel != nil { - e.Cancel() - } - runCtx, runCancel := context.WithCancel(ctx) - e.RunCtx = runCtx - e.Cancel = runCancel - newConfig, err := config.Load() if err != nil { logger.Printf("ERROR: Failed to load config: %s", err) @@ -111,11 +105,11 @@ func (e *EventController) RefreshProcessMonitor(ctx context.Context, logger *log if len(programs) > 0 { toTrack := updateSessionsMapOnRefresh(sm, programs) - go e.MonitorProcesses(e.RunCtx, logger, sm, pr, a, h, toTrack) + e.StartMonitor(serviceCtx, logger, sm, pr, a, h, toTrack) } if e.Config.WakaTime.Enabled { - e.StartHeartbeats(e.RunCtx, logger, sm) + e.StartHeartbeats(serviceCtx, logger, sm) } logger.Printf("INFO: Process monitor refresh with %d programs", len(programs)) diff --git a/cmd/service/internal/events/events_linux.go b/cmd/service/internal/events/events_linux.go index 4fe8f14..fc207eb 100644 --- a/cmd/service/internal/events/events_linux.go +++ b/cmd/service/internal/events/events_linux.go @@ -18,6 +18,18 @@ import ( "github.com/jms-guy/timekeep/internal/repository" ) +func (e *EventController) StartMonitor(parent context.Context, logger *log.Logger, sm *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository, programs []string) { + e.mu.Lock() + if e.MonCancel != nil { + e.MonCancel() + e.MonCancel = nil + } + ctx, cancel := context.WithCancel(parent) + e.MonCancel = cancel + e.mu.Unlock() + go e.MonitorProcesses(ctx, logger, sm, pr, a, h, programs) +} + // Main process monitoring function for Linux version func (e *EventController) MonitorProcesses(ctx context.Context, logger *log.Logger, sm *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository, programs []string) { logger.Println("INFO: Executing main process monitor") @@ -121,7 +133,14 @@ func (e *EventController) checkForProcessStopEvents(logger *log.Logger, sm *sess } } -func (e *EventController) StopProcessMonitor() {} +func (e *EventController) StopProcessMonitor() { + e.mu.Lock() + if e.MonCancel != nil { + e.MonCancel() + e.MonCancel = nil + } + e.mu.Unlock() +} // Read process /proc/{pid}/exe path to get program name func readExePath(pid int) (string, error) { diff --git a/cmd/service/internal/events/events_wakatime.go b/cmd/service/internal/events/events_wakatime.go index 5b952fc..37df591 100644 --- a/cmd/service/internal/events/events_wakatime.go +++ b/cmd/service/internal/events/events_wakatime.go @@ -11,14 +11,25 @@ import ( ) // Start WakaTime heartbeat ticker -func (e *EventController) StartHeartbeats(ctx context.Context, logger *log.Logger, sm *sessions.SessionManager) { +func (e *EventController) StartHeartbeats(parent context.Context, logger *log.Logger, sm *sessions.SessionManager) { + e.mu.Lock() + if e.WakaCancel != nil { + e.WakaCancel() + e.WakaCancel = nil + } + ctx, cancel := context.WithCancel(parent) + e.WakaCancel = cancel + e.mu.Unlock() + e.heartbeatMu.Lock() - e.wakaHeartbeatTicker = time.NewTicker(1 * time.Minute) + if e.wakaHeartbeatTicker != nil { + e.wakaHeartbeatTicker.Stop() + } + e.wakaHeartbeatTicker = time.NewTicker(time.Minute) ticker := e.wakaHeartbeatTicker e.heartbeatMu.Unlock() logger.Println("INFO: Starting WakaTime heartbeats") - go func() { defer func() { e.heartbeatMu.Lock() @@ -28,26 +39,22 @@ func (e *EventController) StartHeartbeats(ctx context.Context, logger *log.Logge } e.heartbeatMu.Unlock() }() - errorCount := 0 for { select { case <-ctx.Done(): logger.Println("INFO: Stopping WakaTime heartbeats") return - case <-ticker.C: if errorCount >= 5 { logger.Println("ERROR: WakaTime heartbeats failed 5 times consecutively, stopping") return } - if err := e.sendHeartbeats(ctx, logger, sm); err != nil { logger.Printf("ERROR: Failed to send WakaTime heartbeat: %s", err) errorCount++ continue } - errorCount = 0 } } @@ -105,11 +112,17 @@ func (e *EventController) sendWakaHeartbeat(ctx context.Context, logger *log.Log // Stops WakaTime heartbeat ticker after disabling integration func (e *EventController) StopHeartbeats() { - e.heartbeatMu.Lock() - defer e.heartbeatMu.Unlock() + e.mu.Lock() + if e.WakaCancel != nil { + e.WakaCancel() + e.WakaCancel = nil + } + e.mu.Unlock() + e.heartbeatMu.Lock() if e.wakaHeartbeatTicker != nil { e.wakaHeartbeatTicker.Stop() e.wakaHeartbeatTicker = nil } + e.heartbeatMu.Unlock() } diff --git a/cmd/service/internal/events/events_windows.go b/cmd/service/internal/events/events_windows.go index d1f805f..e7259b9 100644 --- a/cmd/service/internal/events/events_windows.go +++ b/cmd/service/internal/events/events_windows.go @@ -24,7 +24,15 @@ var monitorScript string var premonitorScript string // Main process monitoring function for Windows version -func (e *EventController) MonitorProcesses(ctx context.Context, logger *log.Logger, s *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository, programs []string) { +func (e *EventController) StartMonitor(parent context.Context, logger *log.Logger, s *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository, programs []string) { + e.mu.Lock() + if e.MonCancel != nil { + e.MonCancel() + e.MonCancel = nil + } + ctx, cancel := context.WithCancel(parent) + e.MonCancel = cancel + e.mu.Unlock() e.startProcessMonitor(ctx, logger, programs) } @@ -114,6 +122,13 @@ func (e *EventController) startProcessMonitor(ctx context.Context, logger *log.L // Stops the WMI powershell script func (e *EventController) StopProcessMonitor() { + e.mu.Lock() + if e.MonCancel != nil { + e.MonCancel() + e.MonCancel = nil + } + e.mu.Unlock() + if e.PsProcess != nil { _ = e.PsProcess.Process.Kill() e.PsProcess = nil @@ -121,14 +136,7 @@ func (e *EventController) StopProcessMonitor() { } // Runs the pre-monitoring script, gathering PIDs for tracked programs that are already running on service start -func (e *EventController) StartPreMonitor(ctx context.Context, logger *log.Logger, s *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository, programs []string) { - select { - case <-ctx.Done(): - logger.Println("WARNING: Context already cancelled, not starting pre-monitor") - return - default: - } - +func (e *EventController) StartPreMonitor(logger *log.Logger, s *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository, programs []string) { programList := strings.Join(programs, ",") scriptTempDir := filepath.Join("C:\\", "ProgramData", "TimeKeep", "scripts_temp") @@ -161,7 +169,7 @@ func (e *EventController) StartPreMonitor(ctx context.Context, logger *log.Logge time.Sleep(100 * time.Millisecond) args := []string{"-ExecutionPolicy", "Bypass", "-File", tempFile.Name(), "-Programs", programList} - cmd := exec.CommandContext(ctx, "powershell", args...) + cmd := exec.Command("powershell", args...) var stderr bytes.Buffer cmd.Stderr = &stderr @@ -179,13 +187,6 @@ func (e *EventController) StartPreMonitor(ctx context.Context, logger *log.Logge err := cmd.Wait() - select { - case <-ctx.Done(): - logger.Println("INFO: Powershell pre-monitor stopped due to context cancellation") - return - default: - } - if err != nil { logger.Printf("ERROR: PowerShell pre-monitor process exited with error: %s", err) } else { diff --git a/cmd/service/service_linux.go b/cmd/service/service_linux.go index d7a3741..8f1bebb 100644 --- a/cmd/service/service_linux.go +++ b/cmd/service/service_linux.go @@ -53,10 +53,6 @@ func (s *timekeepService) Manage() (string, error) { serviceCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - runCtx, runCancel := context.WithCancel(serviceCtx) - s.eventCtrl.RunCtx = runCtx - s.eventCtrl.Cancel = runCancel - programs, err := s.prRepo.GetAllPrograms(context.Background()) if err != nil { return "ERROR: Failed to get programs", err @@ -77,11 +73,11 @@ func (s *timekeepService) Manage() (string, error) { toTrack = append(toTrack, program.Name) } - go s.eventCtrl.MonitorProcesses(runCtx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo, toTrack) + s.eventCtrl.StartMonitor(serviceCtx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo, toTrack) } if s.eventCtrl.Config.WakaTime.Enabled { - s.eventCtrl.StartHeartbeats(runCtx, s.logger.Logger, s.sessions) + s.eventCtrl.StartHeartbeats(serviceCtx, s.logger.Logger, s.sessions) } go s.transport.Listen(serviceCtx, s.logger.Logger, s.eventCtrl, s.sessions, s.prRepo, s.asRepo, s.hsRepo) diff --git a/cmd/service/service_windows.go b/cmd/service/service_windows.go index dd4f64d..bc8e551 100644 --- a/cmd/service/service_windows.go +++ b/cmd/service/service_windows.go @@ -44,10 +44,6 @@ func (s *timekeepService) Execute(args []string, r <-chan svc.ChangeRequest, sta serviceCtx, cancel := context.WithCancel(context.Background()) defer cancel() - runCtx, runCancel := context.WithCancel(serviceCtx) - s.eventCtrl.RunCtx = runCtx - s.eventCtrl.Cancel = runCancel - programs, err := s.prRepo.GetAllPrograms(context.Background()) if err != nil { s.logger.Logger.Printf("ERROR: Failed to get programs: %s", err) @@ -72,12 +68,12 @@ func (s *timekeepService) Execute(args []string, r <-chan svc.ChangeRequest, sta toTrack = append(toTrack, program.Name) } - s.eventCtrl.StartPreMonitor(runCtx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo, toTrack) - s.eventCtrl.MonitorProcesses(runCtx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo, toTrack) + s.eventCtrl.StartPreMonitor(s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo, toTrack) + s.eventCtrl.StartMonitor(serviceCtx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo, toTrack) } if s.eventCtrl.Config.WakaTime.Enabled { - s.eventCtrl.StartHeartbeats(runCtx, s.logger.Logger, s.sessions) + s.eventCtrl.StartHeartbeats(serviceCtx, s.logger.Logger, s.sessions) } go s.transport.Listen(serviceCtx, s.logger.Logger, s.eventCtrl, s.sessions, s.prRepo, s.asRepo, s.hsRepo) @@ -97,7 +93,8 @@ loop: status <- svc.Status{State: svc.StopPending} s.logger.Logger.Println("INFO: Received stop signal") s.closeService(s.logger.Logger) - s.eventCtrl.Cancel() + s.eventCtrl.MonCancel() + s.eventCtrl.WakaCancel() cancel() break loop From 417e93ce91066f2e4acdd5d103da37d945d6e169 Mon Sep 17 00:00:00 2001 From: jms-guy Date: Mon, 20 Oct 2025 13:49:47 -0400 Subject: [PATCH 4/7] debug statements --- cmd/service/internal/events/events_linux.go | 6 +++++- cmd/service/internal/events/events_wakatime.go | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cmd/service/internal/events/events_linux.go b/cmd/service/internal/events/events_linux.go index fc207eb..ec43621 100644 --- a/cmd/service/internal/events/events_linux.go +++ b/cmd/service/internal/events/events_linux.go @@ -18,6 +18,8 @@ import ( "github.com/jms-guy/timekeep/internal/repository" ) +const grace = 3 * time.Second + func (e *EventController) StartMonitor(parent context.Context, logger *log.Logger, sm *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository, programs []string) { e.mu.Lock() if e.MonCancel != nil { @@ -123,7 +125,9 @@ func (e *EventController) checkForProcessStopEvents(logger *log.Logger, sm *sess continue } - ends = append(ends, toEnd{program, pid}) + if now.Sub(t.LastSeen) >= grace { + ends = append(ends, toEnd{program, pid}) + } } } sm.Mu.Unlock() diff --git a/cmd/service/internal/events/events_wakatime.go b/cmd/service/internal/events/events_wakatime.go index 37df591..11f5611 100644 --- a/cmd/service/internal/events/events_wakatime.go +++ b/cmd/service/internal/events/events_wakatime.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os" "os/exec" "time" @@ -104,10 +105,20 @@ func (e *EventController) sendWakaHeartbeat(ctx context.Context, logger *log.Log "--alternate-project", projectToUse, "--category", category, "--time", fmt.Sprintf("%d", time.Now().Unix()), + "--verbose", } cmd := exec.CommandContext(ctx, cliPath, args...) - return cmd.Run() + cmd.Env = append(os.Environ(), + "HOME=/home/jamieguy", + "PATH=/usr/local/bin:/usr/bin", + ) + out, err := cmd.CombinedOutput() + if err != nil { + logger.Printf("ERROR: wakatime-cli failed: %v, output: %s", err, out) + return err + } + return nil } // Stops WakaTime heartbeat ticker after disabling integration From c588c2e4355bd062c1e379413a9d6188051e5cb8 Mon Sep 17 00:00:00 2001 From: jms-guy Date: Wed, 22 Oct 2025 13:53:30 -0400 Subject: [PATCH 5/7] wakatime-cli working, sessions ending. fixing refresh issue --- .../internal/events/event_controller.go | 14 ++-- cmd/service/internal/events/events_linux.go | 1 + .../internal/events/events_wakatime.go | 82 ++++++++----------- scripts/install.sh | 4 + 4 files changed, 47 insertions(+), 54 deletions(-) diff --git a/cmd/service/internal/events/event_controller.go b/cmd/service/internal/events/event_controller.go index afd4fb1..b30cb6b 100644 --- a/cmd/service/internal/events/event_controller.go +++ b/cmd/service/internal/events/event_controller.go @@ -27,14 +27,12 @@ type Command struct { } type EventController struct { - PsProcess *exec.Cmd // Powershell process for Windows event monitoring - mu sync.Mutex // Mutex for context cancellations - MonCancel context.CancelFunc // Monitoring function cancel context - WakaCancel context.CancelFunc // WakaTime function cancel context - Config *config.Config // Struct built from config file - wakaHeartbeatTicker *time.Ticker // Ticker for WakaTime enabled heartbeats - heartbeatMu sync.Mutex // Mutex for WakaTime heartbeat ticker - version string // Timekeep version + PsProcess *exec.Cmd // Powershell process for Windows event monitoring + mu sync.Mutex // Mutex for context cancellations + MonCancel context.CancelFunc // Monitoring function cancel context + WakaCancel context.CancelFunc // WakaTime function cancel context + Config *config.Config // Struct built from config file + version string // Timekeep version } func NewEventController() *EventController { diff --git a/cmd/service/internal/events/events_linux.go b/cmd/service/internal/events/events_linux.go index ec43621..fa62133 100644 --- a/cmd/service/internal/events/events_linux.go +++ b/cmd/service/internal/events/events_linux.go @@ -29,6 +29,7 @@ func (e *EventController) StartMonitor(parent context.Context, logger *log.Logge ctx, cancel := context.WithCancel(parent) e.MonCancel = cancel e.mu.Unlock() + go e.MonitorProcesses(ctx, logger, sm, pr, a, h, programs) } diff --git a/cmd/service/internal/events/events_wakatime.go b/cmd/service/internal/events/events_wakatime.go index 11f5611..e47b88d 100644 --- a/cmd/service/internal/events/events_wakatime.go +++ b/cmd/service/internal/events/events_wakatime.go @@ -13,33 +13,23 @@ import ( // Start WakaTime heartbeat ticker func (e *EventController) StartHeartbeats(parent context.Context, logger *log.Logger, sm *sessions.SessionManager) { + newCtx, newCancel := context.WithCancel(parent) + e.mu.Lock() - if e.WakaCancel != nil { - e.WakaCancel() - e.WakaCancel = nil - } - ctx, cancel := context.WithCancel(parent) - e.WakaCancel = cancel + oldCancel := e.WakaCancel + e.WakaCancel = newCancel e.mu.Unlock() - e.heartbeatMu.Lock() - if e.wakaHeartbeatTicker != nil { - e.wakaHeartbeatTicker.Stop() + if oldCancel != nil { + oldCancel() } - e.wakaHeartbeatTicker = time.NewTicker(time.Minute) - ticker := e.wakaHeartbeatTicker - e.heartbeatMu.Unlock() logger.Println("INFO: Starting WakaTime heartbeats") - go func() { - defer func() { - e.heartbeatMu.Lock() - if e.wakaHeartbeatTicker != nil { - e.wakaHeartbeatTicker.Stop() - e.wakaHeartbeatTicker = nil - } - e.heartbeatMu.Unlock() - }() + + go func(ctx context.Context) { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + errorCount := 0 for { select { @@ -59,27 +49,28 @@ func (e *EventController) StartHeartbeats(parent context.Context, logger *log.Lo errorCount = 0 } } - }() + }(newCtx) } // Send specified heartbeats to WakaTime func (e *EventController) sendHeartbeats(ctx context.Context, logger *log.Logger, sm *sessions.SessionManager) error { - sm.Mu.Lock() - defer sm.Mu.Unlock() + type item struct{ program, category, project string } + items := []item{} - for program, tracked := range sm.Programs { - if len(tracked.PIDs) > 0 { - if tracked.Category != "" { - if err := e.sendWakaHeartbeat(ctx, logger, program, tracked.Category, tracked.Project); err != nil { - return err - } - logger.Printf("INFO: WakaTime heartbeat sent for %s, category %s", program, tracked.Category) - continue - } - logger.Printf("INFO: WakaTime heartbeat skipped for %s, no category set", program) + sm.Mu.Lock() + for p, t := range sm.Programs { + if len(t.PIDs) > 0 && t.Category != "" { + items = append(items, item{p, t.Category, t.Project}) } } + sm.Mu.Unlock() + for _, it := range items { + if err := e.sendWakaHeartbeat(ctx, logger, it.program, it.category, it.project); err != nil { + return err + } + logger.Printf("INFO: WakaTime heartbeat sent for %s, category %s", it.program, it.category) + } return nil } @@ -101,14 +92,18 @@ func (e *EventController) sendWakaHeartbeat(ctx context.Context, logger *log.Log "--key", e.Config.WakaTime.APIKey, "--entity", program, "--entity-type", "app", - "--plugin", "timekeep/" + e.version, - "--alternate-project", projectToUse, "--category", category, "--time", fmt.Sprintf("%d", time.Now().Unix()), "--verbose", + "--write", } - cmd := exec.CommandContext(ctx, cliPath, args...) + logger.Printf("DEBUG: cli=%s args=%v", cliPath, args) + + execCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(execCtx, cliPath, args...) cmd.Env = append(os.Environ(), "HOME=/home/jamieguy", "PATH=/usr/local/bin:/usr/bin", @@ -124,16 +119,11 @@ func (e *EventController) sendWakaHeartbeat(ctx context.Context, logger *log.Log // Stops WakaTime heartbeat ticker after disabling integration func (e *EventController) StopHeartbeats() { e.mu.Lock() - if e.WakaCancel != nil { - e.WakaCancel() - e.WakaCancel = nil - } + cancel := e.WakaCancel + e.WakaCancel = nil e.mu.Unlock() - e.heartbeatMu.Lock() - if e.wakaHeartbeatTicker != nil { - e.wakaHeartbeatTicker.Stop() - e.wakaHeartbeatTicker = nil + if cancel != nil { + cancel() } - e.heartbeatMu.Unlock() } diff --git a/scripts/install.sh b/scripts/install.sh index 498bc7e..e813751 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -41,8 +41,12 @@ After=network.target [Service] Type=simple ExecStart=/usr/local/bin/timekeepd +Environment=HOME=/home/jamieguy +Environment=PATH=/usr/local/bin:/usr/bin +WorkingDirectory=/home/jamieguy StandardOutput=journal StandardError=journal +KillMode=process Restart=always RestartSec=2s User=$USER_NAME From e73a2a9cbe98414a570e3c52faffed389dca6748 Mon Sep 17 00:00:00 2001 From: jms-guy Date: Thu, 23 Oct 2025 13:45:36 -0400 Subject: [PATCH 6/7] wakatime-cli issue resolved --- cmd/service/internal/events/events_wakatime.go | 1 + scripts/install.sh | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/service/internal/events/events_wakatime.go b/cmd/service/internal/events/events_wakatime.go index e47b88d..8731cf7 100644 --- a/cmd/service/internal/events/events_wakatime.go +++ b/cmd/service/internal/events/events_wakatime.go @@ -93,6 +93,7 @@ func (e *EventController) sendWakaHeartbeat(ctx context.Context, logger *log.Log "--entity", program, "--entity-type", "app", "--category", category, + "--alternate-project", projectToUse, "--time", fmt.Sprintf("%d", time.Now().Unix()), "--verbose", "--write", diff --git a/scripts/install.sh b/scripts/install.sh index e813751..18d5f7d 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -41,9 +41,6 @@ After=network.target [Service] Type=simple ExecStart=/usr/local/bin/timekeepd -Environment=HOME=/home/jamieguy -Environment=PATH=/usr/local/bin:/usr/bin -WorkingDirectory=/home/jamieguy StandardOutput=journal StandardError=journal KillMode=process From d482c9a186680029a88a0337db41b1c8a563afaa Mon Sep 17 00:00:00 2001 From: jms-guy Date: Thu, 23 Oct 2025 13:50:15 -0400 Subject: [PATCH 7/7] readme update --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 58cfc0f..9c39340 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ A process activity tracker, it runs as a background service recording start/stop events for select programs and aggregates active sessions, session history, and lifetime program usage. Now has [WakaTime](https://github.com/jms-guy/timekeep?tab=readme-ov-file#wakatime) integration. -**Linux version currently not working** - ## Table of Contents - [Features](#features) - [How It Works](#how-it-works) @@ -84,7 +82,7 @@ GOOS=windows go build -o timekeep-service.exe ./cmd/service GOOS=windows go build -o timekeep.exe ./cmd/cli # Install and start service (Run as Administrator) -sc.exe create timekeep binPath= "C:\Program Files\Timekeep\timekeep-service.exe" start= auto # Assuming this is the location of service binary +sc.exe create timekeep binPath= "Path to timekeep-service.exe binary" start= auto sc.exe start timekeep # Verify service is running @@ -134,6 +132,7 @@ Type=simple ExecStart=/usr/local/bin/timekeepd StandardOutput=journal StandardError=journal +KillMode=process Restart=always RestartSec=2s User=$USER_NAME @@ -182,7 +181,7 @@ To enable WakaTime integration, users must: Enable integration through timekeep. Set your WakaTime API key and wakatime-cli path either directly in the Timekeep [config](https://github.com/jms-guy/timekeep?tab=readme-ov-file#file-locations) file, or provide them through flags: -`timekeep wakatime enable --api-key YOUR-KEY --set-path wakatime-cli-PATH` +`timekeep wakatime enable --api-key "YOUR-KEY" --set-path "wakatime-cli-PATH"` ```json {