Skip to content

Commit 1ab07cc

Browse files
committed
more robust agent install
1 parent 01f0c1f commit 1ab07cc

File tree

2 files changed

+116
-13
lines changed

2 files changed

+116
-13
lines changed

cmd/agent/install.go

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"log"
78
"os"
89
"os/exec"
910
"path/filepath"
1011
"runtime"
1112
"strings"
13+
"syscall"
1214
"text/template"
1315
"time"
1416
)
@@ -300,17 +302,20 @@ func installMacOS(agentPath, serverURL, joinKey string) error {
300302
return fmt.Errorf("failed to write plist: %w", err)
301303
}
302304

303-
// Load the launch agent
305+
// Load the launch agent (this starts it immediately due to RunAtLoad=true)
306+
log.Printf("[INFO] Loading launch agent from %s", plistPath)
304307
cmd := exec.Command("launchctl", "load", plistPath) //nolint:noctx // local command
305308
if _, err := cmd.CombinedOutput(); err != nil {
306309
// Try to unload first in case it's already loaded
310+
log.Print("[INFO] Agent may already be loaded, attempting to unload first")
307311
_ = exec.Command("launchctl", "unload", plistPath).Run() //nolint:errcheck,noctx // Best effort
308312
// Try loading again
309313
cmd = exec.Command("launchctl", "load", plistPath) //nolint:noctx // local command
310314
if output, err := cmd.CombinedOutput(); err != nil {
311315
return fmt.Errorf("failed to load launch agent: %w\nOutput: %s", err, output)
312316
}
313317
}
318+
log.Print("[INFO] Launch agent loaded successfully - agent should be running now")
314319

315320
return nil
316321
}
@@ -408,10 +413,12 @@ WantedBy=default.target
408413
}
409414

410415
// Start service
416+
log.Print("[INFO] Starting systemd service")
411417
cmd = exec.Command("systemctl", "--user", "start", "gitmdm-agent.service") //nolint:noctx // local command
412418
if err := cmd.Run(); err != nil {
413419
return fmt.Errorf("failed to start service: %w", err)
414420
}
421+
log.Print("[INFO] Systemd service started successfully")
415422

416423
return nil
417424
}
@@ -463,13 +470,19 @@ func installCron(agentPath, _, _ string) error {
463470
return nil // Already installed
464471
}
465472

466-
// Add new cron job (without sensitive data in command line)
467-
newEntry := fmt.Sprintf("@reboot %s", agentPath)
473+
// Add new cron jobs: run at reboot and every 15 minutes
474+
// The PID file mechanism in the agent prevents duplicate processes
475+
entries := []string{
476+
fmt.Sprintf("@reboot %s", agentPath),
477+
fmt.Sprintf("*/15 * * * * %s", agentPath), // Every 15 minutes
478+
}
468479
newCron := currentCron
469480
if !strings.HasSuffix(newCron, "\n") && newCron != "" {
470481
newCron += "\n"
471482
}
472-
newCron += newEntry + "\n"
483+
for _, entry := range entries {
484+
newCron += entry + "\n"
485+
}
473486

474487
// Install new crontab
475488
cmd = exec.Command("crontab", "-") //nolint:noctx // local command
@@ -479,15 +492,52 @@ func installCron(agentPath, _, _ string) error {
479492
}
480493

481494
// Try to start the agent immediately in background
482-
// Security note: agentPath is constructed from filepath.Join with constants,
483-
// but we use exec.Command directly to avoid shell injection risks
484-
cmd = exec.Command(agentPath) //nolint:noctx // agent spawns its own context
485-
cmd.Stdin = nil
486-
cmd.Stdout = nil
487-
cmd.Stderr = nil
488-
if err := cmd.Start(); err == nil {
489-
// Detach from the process
490-
_ = cmd.Process.Release() //nolint:errcheck // best effort cleanup
495+
// Use nohup to ensure the process survives after installer exits
496+
log.Printf("[INFO] Starting agent in background: %s", agentPath)
497+
498+
// Check if nohup is available
499+
nohupPath, nohupErr := exec.LookPath("nohup")
500+
if nohupErr == nil {
501+
// Use nohup with proper detachment
502+
cmd = exec.Command(nohupPath, agentPath) //nolint:noctx // agent spawns its own context
503+
cmd.Stdin = nil
504+
// Redirect stdout/stderr to /dev/null to fully detach
505+
devNull, err := os.Open("/dev/null")
506+
if err == nil {
507+
cmd.Stdout = devNull
508+
cmd.Stderr = devNull
509+
defer func() { _ = devNull.Close() }() //nolint:errcheck // defer close
510+
}
511+
512+
// Set process group to detach from parent
513+
cmd.SysProcAttr = &syscall.SysProcAttr{
514+
Setpgid: true,
515+
}
516+
517+
if err := cmd.Start(); err == nil {
518+
// Process started successfully
519+
log.Printf("[INFO] Agent started successfully with PID %d using nohup", cmd.Process.Pid)
520+
// Don't wait for it to finish
521+
go func() {
522+
_ = cmd.Wait() //nolint:errcheck // Reap the child when it exits
523+
}()
524+
} else {
525+
log.Printf("[WARN] Failed to start agent with nohup: %v", err)
526+
}
527+
} else {
528+
// Fallback to direct execution without nohup
529+
cmd = exec.Command(agentPath) //nolint:noctx // agent spawns its own context
530+
cmd.Stdin = nil
531+
cmd.Stdout = nil
532+
cmd.Stderr = nil
533+
534+
if err := cmd.Start(); err == nil {
535+
// Detach from the process
536+
_ = cmd.Process.Release() //nolint:errcheck // best effort cleanup
537+
log.Printf("[INFO] Agent started successfully with PID %d (without nohup)", cmd.Process.Pid)
538+
} else {
539+
log.Printf("[WARN] Failed to start agent immediately: %v (will start via cron in 15 minutes)", err)
540+
}
491541
}
492542

493543
return nil

cmd/agent/main.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,52 @@ func setupLogging() (*os.File, error) {
286286
return logFile, nil
287287
}
288288

289+
// checkAndCreatePIDFile checks if another instance is running and creates a PID file.
290+
// Returns true if we should continue running, false if another instance is active.
291+
func checkAndCreatePIDFile() (exists bool, cleanup func()) {
292+
homeDir, err := os.UserHomeDir()
293+
if err != nil {
294+
log.Printf("[WARN] Failed to get home directory for PID file: %v", err)
295+
return true, func() {} // Continue without PID file
296+
}
297+
298+
pidPath := filepath.Join(homeDir, ".gitmdm", "agent.pid")
299+
// Check if PID file exists and if that process is still running
300+
if pidData, err := os.ReadFile(pidPath); err == nil {
301+
oldPID, err := strconv.Atoi(strings.TrimSpace(string(pidData)))
302+
if err == nil {
303+
// Check if process exists by sending signal 0
304+
if process, err := os.FindProcess(oldPID); err == nil {
305+
if err := process.Signal(syscall.Signal(0)); err == nil {
306+
log.Printf("[INFO] Agent already running with PID %d, exiting", oldPID)
307+
return false, func() {}
308+
}
309+
}
310+
log.Printf("[INFO] Removing stale PID file for non-existent process %d", oldPID)
311+
}
312+
}
313+
314+
// Write our PID
315+
pid := os.Getpid()
316+
if err := os.WriteFile(pidPath, []byte(strconv.Itoa(pid)), 0o600); err != nil {
317+
log.Printf("[WARN] Failed to write PID file: %v", err)
318+
return true, func() {} // Continue without PID file
319+
}
320+
321+
log.Printf("[INFO] Created PID file with PID %d", pid)
322+
323+
// Return cleanup function
324+
cleanup = func() {
325+
if err := os.Remove(pidPath); err != nil && !os.IsNotExist(err) {
326+
log.Printf("[WARN] Failed to remove PID file: %v", err)
327+
} else {
328+
log.Print("[INFO] Removed PID file")
329+
}
330+
}
331+
332+
return true, cleanup
333+
}
334+
289335
func main() {
290336
// Set up panic recovery first
291337
defer func() {
@@ -365,6 +411,13 @@ func main() {
365411
log.Fatal(err)
366412
}
367413

414+
// Check PID file to avoid duplicate processes
415+
shouldRun, cleanupPID := checkAndCreatePIDFile()
416+
if !shouldRun {
417+
return // Another instance is already running
418+
}
419+
defer cleanupPID()
420+
368421
ctx, cancel := context.WithCancel(context.Background())
369422
defer cancel()
370423

0 commit comments

Comments
 (0)