@@ -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\n Output: %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
0 commit comments