diff --git a/.gitignore b/.gitignore index 99c66bc..4078760 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ go.work.sum # .vscode/ testbuild.sh +testlinuxbuild.sh +timekeep-release changes.md \ No newline at end of file diff --git a/README.md b/README.md index 0eb3276..383926e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ 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) @@ -15,7 +17,6 @@ A process activity tracker, it runs as a background service recording start/stop - [WakaTime](#wakatime) - [File Locations](#file-locations) - [Current Limitations](#current-limitations) -- [To-Do](#to-do) - [Contributing & Issues](#contributing--issues) - [License](#license) @@ -30,8 +31,8 @@ A process activity tracker, it runs as a background service recording start/stop - WakaTime integration allows for tracking external program usage alongside your IDE/web-browsing stats ## How It Works -- Windows: embeds a PowerShell script to subscribe to WMI process start/stop events. -- Linux: polls `/proc`, resolves process identity via `/proc//exe` (readlink) -> fallback to `/proc//cmdline` -> last-resort `/proc//comm`, then matches by basename. +- Windows: Embeds a PowerShell script to subscribe to WMI process start/stop events. Runs a pre-monitoring script to find any tracked programs already running on service start +- Linux: Polls `/proc`, resolves process identity via `/proc//exe` (readlink) -> fallback to `/proc//cmdline` -> last-resort `/proc//comm`, then matches by basename. - Session model: A session begins when the first process for a tracked program starts. Additional processes (ex. multiple windows) are added to the active session. The session ends only when the last process terminates, giving an accurate picture of total time with that program. ## Usage @@ -122,6 +123,11 @@ 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 { - category := "" - project := "" - if program.Category.Valid { - category = program.Category.String - } - if program.Project.Valid { - project = program.Project.String - } - sm.EnsureProgram(program.Name, category, project) + toTrack := updateSessionsMapOnRefresh(sm, programs) - toTrack = append(toTrack, program.Name) - } + go e.MonitorProcesses(e.RunCtx, logger, sm, pr, a, h, toTrack) + } - go e.MonitorProcesses(ctx, logger, sm, pr, a, h, toTrack) + if e.Config.WakaTime.Enabled { + e.StartHeartbeats(e.RunCtx, logger, sm) } - newConfig, err := config.Load() - if err != nil { - logger.Printf("ERROR: Failed to load config: %s", err) - return + logger.Printf("INFO: Process monitor refresh with %d programs", len(programs)) +} + +// Takes list of programs from database, and updates session map by adding/removing/altering based on any changes from last database grab +func updateSessionsMapOnRefresh(sm *sessions.SessionManager, programs []database.TrackedProgram) []string { + desired := make(map[string]struct{}, len(programs)) + toTrack := make([]string, 0, len(programs)) + + sm.Mu.Lock() + currentKeys := make([]string, 0, len(sm.Programs)) + for k := range sm.Programs { + currentKeys = append(currentKeys, k) } - e.Config = newConfig + for _, p := range programs { + name := p.Name + cat := "" + if p.Category.Valid { + cat = p.Category.String + } + proj := "" + if p.Project.Valid { + proj = p.Project.String + } - if e.Config.WakaTime.Enabled { - e.StartHeartbeats(ctx, logger, sm) + sm.EnsureProgram(name, cat, proj) + desired[name] = struct{}{} + toTrack = append(toTrack, name) } - logger.Printf("INFO: Process monitor refresh with %d programs", len(programs)) + if len(desired) == 0 { + for _, k := range currentKeys { + delete(sm.Programs, k) + } + } else { + for _, k := range currentKeys { + if _, keep := desired[k]; !keep { + delete(sm.Programs, k) + } + } + } + sm.Mu.Unlock() + + return toTrack } diff --git a/cmd/service/internal/events/events_linux.go b/cmd/service/internal/events/events_linux.go index 64dd245..4fe8f14 100644 --- a/cmd/service/internal/events/events_linux.go +++ b/cmd/service/internal/events/events_linux.go @@ -20,10 +20,7 @@ import ( // 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) { - if e.cancel != nil { - e.cancel() - e.cancel = nil - } + logger.Println("INFO: Executing main process monitor") ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() @@ -31,16 +28,17 @@ func (e *EventController) MonitorProcesses(ctx context.Context, logger *log.Logg for { select { case <-ctx.Done(): + logger.Println("INFO: Monitor context cancelled") return case <-ticker.C: - livePIDS := e.checkForProcessStartEvents(ctx, logger, sm, a) - e.checkForProcessStopEvents(ctx, logger, sm, pr, a, h, livePIDS) + livePIDS := e.checkForProcessStartEvents(logger, sm, a) + e.checkForProcessStopEvents(logger, sm, pr, a, h, livePIDS) } } } // Polls /proc and loops over PID entries, looking for any new PIDS belonging to tracked programs -func (e *EventController) checkForProcessStartEvents(ctx context.Context, logger *log.Logger, sm *sessions.SessionManager, a repository.ActiveRepository) map[int]struct{} { +func (e *EventController) checkForProcessStartEvents(logger *log.Logger, sm *sessions.SessionManager, a repository.ActiveRepository) map[int]struct{} { entries, err := os.ReadDir("/proc") // Read /proc if err != nil { logger.Printf("ERROR: Couldn't read /proc: %s", err) @@ -79,7 +77,7 @@ func (e *EventController) checkForProcessStartEvents(ctx context.Context, logger } sm.Mu.Unlock() - sm.CreateSession(ctx, logger, a, identity, pid) + sm.CreateSession(context.Background(), logger, a, identity, pid) } return live @@ -87,7 +85,7 @@ func (e *EventController) checkForProcessStartEvents(ctx context.Context, logger // Takes the PID entries found in the previous check function, and compares them against map of active PIDs, to determine if // any active sessions need ending -func (e *EventController) checkForProcessStopEvents(ctx context.Context, logger *log.Logger, sm *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository, livePIDs map[int]struct{}) { +func (e *EventController) checkForProcessStopEvents(logger *log.Logger, sm *sessions.SessionManager, pr repository.ProgramRepository, a repository.ActiveRepository, h repository.HistoryRepository, livePIDs map[int]struct{}) { if livePIDs == nil { livePIDs = map[int]struct{}{} } @@ -119,16 +117,11 @@ func (e *EventController) checkForProcessStopEvents(ctx context.Context, logger sm.Mu.Unlock() for _, eend := range ends { - sm.EndSession(ctx, logger, pr, a, h, eend.program, eend.pid) + sm.EndSession(context.Background(), logger, pr, a, h, eend.program, eend.pid) } } -func (e *EventController) StopProcessMonitor() { - if e.cancel != nil { - e.cancel() - e.cancel = nil - } -} +func (e *EventController) StopProcessMonitor() {} // 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 501d3c8..5b952fc 100644 --- a/cmd/service/internal/events/events_wakatime.go +++ b/cmd/service/internal/events/events_wakatime.go @@ -87,6 +87,7 @@ func (e *EventController) sendWakaHeartbeat(ctx context.Context, logger *log.Log if project != "" { projectToUse = project } + logger.Printf("INFO: Sending WakaTime heartbeat for %s, category %s, project %s", program, category, projectToUse) args := []string{ "--key", e.Config.WakaTime.APIKey, diff --git a/cmd/service/internal/events/events_windows.go b/cmd/service/internal/events/events_windows.go index 99b6a7f..d1f805f 100644 --- a/cmd/service/internal/events/events_windows.go +++ b/cmd/service/internal/events/events_windows.go @@ -20,6 +20,9 @@ import ( //go:embed monitor.ps1 var monitorScript string +//go:embed premonitor.ps1 +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) { e.startProcessMonitor(ctx, logger, programs) @@ -27,6 +30,14 @@ func (e *EventController) MonitorProcesses(ctx context.Context, logger *log.Logg // Runs the powershell WMI script, to monitor process events func (e *EventController) startProcessMonitor(ctx context.Context, logger *log.Logger, programs []string) { + // Check if context is already cancelled + select { + case <-ctx.Done(): + logger.Println("WARNING: Context already cancelled, not starting monitor") + return + default: + } + programList := strings.Join(programs, ",") scriptTempDir := filepath.Join("C:\\", "ProgramData", "TimeKeep", "scripts_temp") @@ -59,12 +70,13 @@ func (e *EventController) startProcessMonitor(ctx context.Context, logger *log.L time.Sleep(100 * time.Millisecond) // Pause to allow tempfile to finish writing before it attempts to execute args := []string{"-ExecutionPolicy", "Bypass", "-File", tempFile.Name(), "-Programs", programList} - cmd := exec.Command("powershell", args...) + cmd := exec.CommandContext(ctx, "powershell", args...) e.PsProcess = cmd var stderr bytes.Buffer cmd.Stderr = &stderr + logger.Println("INFO: Executing monitor script") if err := cmd.Start(); err != nil { logger.Printf("ERROR: Failed to start PowerShell monitor: %s", err) e.PsProcess = nil @@ -107,3 +119,83 @@ func (e *EventController) StopProcessMonitor() { e.PsProcess = nil } } + +// 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: + } + + programList := strings.Join(programs, ",") + + scriptTempDir := filepath.Join("C:\\", "ProgramData", "TimeKeep", "scripts_temp") + + if err := os.MkdirAll(scriptTempDir, 0o755); err != nil { + logger.Printf("ERROR: Failed to create PowerShell script temp directory '%s': %s", scriptTempDir, err) + return + } + + tempFile, err := os.CreateTemp(scriptTempDir, "premonitor*.ps1") + if err != nil { + logger.Printf("ERROR: Failed to create temp script file in '%s': %s", scriptTempDir, err) + return + } + + defer tempFile.Close() + + if _, err := tempFile.WriteString(premonitorScript); err != nil { + logger.Printf("ERROR: Failed to write script: %s", err) + return + } + + if err := tempFile.Sync(); err != nil { + logger.Printf("ERROR: Failed to sync temp script file to disk: %s", err) + return + } + + tempFile.Close() + + time.Sleep(100 * time.Millisecond) + + args := []string{"-ExecutionPolicy", "Bypass", "-File", tempFile.Name(), "-Programs", programList} + cmd := exec.CommandContext(ctx, "powershell", args...) + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + logger.Println("INFO: Executing pre-monitor script") + if err := cmd.Start(); err != nil { + logger.Printf("ERROR: Failed to start PowerShell script: %s", err) + if stderr.Len() > 0 { + logger.Printf("INFO: PowerShell stderr (on Start() failure): %s", stderr.String()) + } + } + + go func() { + defer os.Remove(tempFile.Name()) + + 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 { + logger.Println("INFO: PowerShell pre-monitor process exited successfully.") + } + + if stderr.Len() > 0 { + logger.Printf("PowerShell stderr output: %s", stderr.String()) + } else { + logger.Println("INFO: No PowerShell stderr output.") + } + }() +} diff --git a/cmd/service/internal/events/premonitor.ps1 b/cmd/service/internal/events/premonitor.ps1 new file mode 100644 index 0000000..f4c4add --- /dev/null +++ b/cmd/service/internal/events/premonitor.ps1 @@ -0,0 +1,55 @@ +<# + This script runs before the main process monitoring script. It queries for processes that belong to programs being tracked + that are already running, and sends the service a synthetic "process_start" event, immediately opening an active session for + that program. +#> +param( + [string]$Programs +) + +# Fail fast on errors +$ErrorActionPreference = "Stop" + +# Connect to named pipe opened by service +$pipe = New-Object System.IO.Pipes.NamedPipeClientStream(".", "Timekeep", "Out") +$pipe.Connect() +$writer = New-Object System.IO.StreamWriter($pipe) +$writer.AutoFlush = $true + +try { + $tracked = $Programs -split "," + + if (-not $tracked -or $tracked.Count -eq 0) { + exit 0 + } + + # Build a set for quick membership checks + $set = @{} + foreach ($n in $tracked) { $set[$n] = $true } + + # Enumerate current processes and emit synthetic start events + Get-CimInstance Win32_Process | ForEach-Object { + $name = $_.Name + if ($name -and $set.ContainsKey($name.ToLower())) { + $data = @{ + action = "process_start" + name = $name + pid = [int]$_.ProcessId + } + $writer.WriteLine(($data | ConvertTo-Json -Compress)) + } + } +} +catch { + $err = @{ + action = "ps_error" + message = $_.Exception.Message + } + $writer.WriteLine(($err | ConvertTo-Json -Compress)) + exit 1 +} +finally { + $writer.Flush() + $writer.Dispose() + $pipe.Dispose() +} \ No newline at end of file diff --git a/cmd/service/internal/logs/logging_linux.go b/cmd/service/internal/logs/logging_linux.go index 2bee6ce..a8b8f33 100644 --- a/cmd/service/internal/logs/logging_linux.go +++ b/cmd/service/internal/logs/logging_linux.go @@ -6,6 +6,6 @@ import "path/filepath" // Get path for logging file func getLogPath() (string, error) { - logDir := "/var/log/timekeep/logs" + logDir := "/var/log/timekeep" return filepath.Join(logDir, "timekeep.log"), nil } diff --git a/cmd/service/internal/sessions/sessions.go b/cmd/service/internal/sessions/sessions.go index aee9113..fc36fc3 100644 --- a/cmd/service/internal/sessions/sessions.go +++ b/cmd/service/internal/sessions/sessions.go @@ -29,17 +29,26 @@ func NewSessionManager() *SessionManager { } // Make sure map is initialized, add program to map if not already present +// Caller MUST hold sm.Mu Lock func (sm *SessionManager) EnsureProgram(name, category, project string) { - sm.Mu.Lock() - defer sm.Mu.Unlock() - if sm.Programs == nil { sm.Programs = make(map[string]*Tracked) } name = strings.ToLower(name) - if _, ok := sm.Programs[name]; !ok { + tracked, ok := sm.Programs[name] + + if !ok { // Program not in tracked list? sm.Programs[name] = &Tracked{Category: category, Project: project, PIDs: make(map[int]struct{})} + return + } + + if tracked.Category != category { // Category change? + tracked.Category = category + } + + if tracked.Project != project { // Project change? + tracked.Project = project } } diff --git a/cmd/service/service_linux.go b/cmd/service/service_linux.go index 805314c..388166b 100644 --- a/cmd/service/service_linux.go +++ b/cmd/service/service_linux.go @@ -27,6 +27,9 @@ func RunService(name string, isDebug *bool) error { // Main daemon management function func (s *timekeepService) Manage() (string, error) { + logger := s.logger.Logger + + logger.Println("INFO: Starting Manage function") usage := "Usage: timekeep install | remove | start | stop | status" if len(os.Args) > 1 { @@ -47,13 +50,18 @@ func (s *timekeepService) Manage() (string, error) { } } - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + serviceCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - programs, err := s.prRepo.GetAllPrograms(ctx) + 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 } + logger.Printf("DEBUG: Have %d programs", len(programs)) if len(programs) > 0 { toTrack := []string{} for _, program := range programs { @@ -65,24 +73,27 @@ 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) } - go s.eventCtrl.MonitorProcesses(ctx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo, toTrack) + 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(ctx, s.logger.Logger, s.sessions) + s.eventCtrl.StartHeartbeats(runCtx, s.logger.Logger, s.sessions) } - go s.transport.Listen(ctx, s.logger.Logger, s.eventCtrl, s.sessions, s.prRepo, s.asRepo, s.hsRepo) + go s.transport.Listen(serviceCtx, s.logger.Logger, s.eventCtrl, s.sessions, s.prRepo, s.asRepo, s.hsRepo) - <-ctx.Done() + <-serviceCtx.Done() s.logger.Logger.Println("INFO: Received shutdown signal") - s.closeService(ctx) + s.closeService(s.logger.Logger) return "INFO: Daemon stopped.", nil } diff --git a/cmd/service/service_setup.go b/cmd/service/service_setup.go index 3b6029c..04f7c27 100644 --- a/cmd/service/service_setup.go +++ b/cmd/service/service_setup.go @@ -2,6 +2,7 @@ package main import ( "context" + "log" "github.com/jms-guy/timekeep/cmd/service/internal/daemons" "github.com/jms-guy/timekeep/cmd/service/internal/events" @@ -91,18 +92,24 @@ func NewTimekeepService(pr repository.ProgramRepository, ar repository.ActiveRep } // Service shutdown function -func (s *timekeepService) closeService(ctx context.Context) { +func (s *timekeepService) closeService(logger *log.Logger) { + logger.Println("INFO: Closing service") if s.eventCtrl.Config.WakaTime.Enabled { // Stop WakaTime heartbeats + logger.Println("INFO: Stopping heartbeats") s.eventCtrl.StopHeartbeats() } + logger.Println("INFO: Stopping process monitor") s.eventCtrl.StopProcessMonitor() // Stop any current monitoring function - s.logger.FileCleanup() // Close open logging file s.sessions.Mu.Lock() for program, tracked := range s.sessions.Programs { // End any active sessions if len(tracked.PIDs) != 0 { - s.sessions.MoveSessionToHistory(ctx, s.logger.Logger, s.prRepo, s.asRepo, s.hsRepo, program) + logger.Println("INFO: Ending active sessions") + s.sessions.MoveSessionToHistory(context.Background(), s.logger.Logger, s.prRepo, s.asRepo, s.hsRepo, program) } } + + s.logger.FileCleanup() // Close open logging file + s.sessions.Mu.Unlock() } diff --git a/cmd/service/service_windows.go b/cmd/service/service_windows.go index d2d70a8..dd4f64d 100644 --- a/cmd/service/service_windows.go +++ b/cmd/service/service_windows.go @@ -41,10 +41,14 @@ func (s *timekeepService) Execute(args []string, r <-chan svc.ChangeRequest, sta status <- svc.Status{State: svc.StartPending} - ctx, cancel := context.WithCancel(context.Background()) + serviceCtx, cancel := context.WithCancel(context.Background()) defer cancel() - programs, err := s.prRepo.GetAllPrograms(ctx) + 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) status <- svc.Status{State: svc.Stopped} @@ -61,19 +65,22 @@ func (s *timekeepService) Execute(args []string, r <-chan svc.ChangeRequest, sta if program.Project.Valid { project = program.Project.String } + s.sessions.Mu.Lock() s.sessions.EnsureProgram(program.Name, category, project) + s.sessions.Mu.Unlock() toTrack = append(toTrack, program.Name) } - s.eventCtrl.MonitorProcesses(ctx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo, toTrack) + 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) } if s.eventCtrl.Config.WakaTime.Enabled { - s.eventCtrl.StartHeartbeats(ctx, s.logger.Logger, s.sessions) + s.eventCtrl.StartHeartbeats(runCtx, s.logger.Logger, s.sessions) } - go s.transport.Listen(ctx, s.logger.Logger, s.eventCtrl, s.sessions, s.prRepo, s.asRepo, s.hsRepo) + go s.transport.Listen(serviceCtx, s.logger.Logger, s.eventCtrl, s.sessions, s.prRepo, s.asRepo, s.hsRepo) status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} @@ -87,26 +94,25 @@ loop: status <- c.CurrentStatus case svc.Stop, svc.Shutdown: // Service needs to be stopped or shutdown + status <- svc.Status{State: svc.StopPending} s.logger.Logger.Println("INFO: Received stop signal") + s.closeService(s.logger.Logger) + s.eventCtrl.Cancel() cancel() - s.closeService(ctx) break loop case svc.Pause: // Service needs to be paused, without shutdown + status <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted} s.logger.Logger.Println("INFO: Pausing service") if s.eventCtrl.Config.WakaTime.Enabled { s.eventCtrl.StopHeartbeats() } s.eventCtrl.StopProcessMonitor() - status <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted} case svc.Continue: // Resume paused execution state of service - s.logger.Logger.Println("INFO: Resuming service") - s.eventCtrl.RefreshProcessMonitor(ctx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo) - if s.eventCtrl.Config.WakaTime.Enabled { - s.eventCtrl.StartHeartbeats(ctx, s.logger.Logger, s.sessions) - } status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + s.logger.Logger.Println("INFO: Resuming service") + s.eventCtrl.RefreshProcessMonitor(serviceCtx, s.logger.Logger, s.sessions, s.prRepo, s.asRepo, s.hsRepo) default: s.logger.Logger.Printf("ERROR: Unexpected service control request #%d", c) @@ -114,7 +120,5 @@ loop: } } - status <- svc.Status{State: svc.StopPending} - return false, 0 } diff --git a/docs/commands.md b/docs/commands.md index 12bb64d..c8af441 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -43,7 +43,7 @@ - `status` - Gets current state of Timekeep service - - `timekeep ping` + - `timekeep status` - `update` - Update a given program's category/project fields diff --git a/scripts/install.sh b/scripts/install.sh index fa592ae..39214e8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -33,6 +33,10 @@ 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 <