@@ -2,6 +2,7 @@ package main
22
33import (
44 "encoding/json"
5+ "errors"
56 "fmt"
67 "os"
78 "os/exec"
@@ -28,7 +29,7 @@ type AgentConfig struct {
2829// os.UserConfigDir() returns:
2930// - macOS: ~/Library/Application Support
3031// - Linux/BSD: $XDG_CONFIG_HOME or ~/.config
31- // - Windows: %AppData%
32+ // - Windows: %AppData%.
3233func configDir () (string , error ) {
3334 configDir , err := os .UserConfigDir ()
3435 if err != nil {
@@ -58,6 +59,40 @@ func loadConfig() (*AgentConfig, error) {
5859 return & config , nil
5960}
6061
62+ // installExecutable copies the executable to the target path, handling busy files.
63+ func installExecutable (exePath , targetPath string ) error {
64+ // Stop any existing instance (but not ourselves)
65+ // Note: We accept the TOCTOU risk here as it's a best-effort cleanup
66+ if _ , err := os .Stat (targetPath ); err == nil {
67+ // Use exec.Command directly to avoid shell injection
68+ // The targetPath is safe (constructed from constants) but better to be explicit
69+ cmd := exec .Command ("pkill" , "-f" , targetPath ) //nolint:noctx // killing existing process doesn't need context
70+ _ = cmd .Run () //nolint:errcheck // Best effort
71+ time .Sleep (500 * time .Millisecond )
72+ }
73+
74+ // Try direct copy first
75+ if err := copyFile (exePath , targetPath ); err != nil {
76+ // Handle "text file busy" error by copying to temp and renaming
77+ if ! strings .Contains (strings .ToLower (err .Error ()), "text file busy" ) {
78+ return fmt .Errorf ("failed to copy executable: %w" , err )
79+ }
80+
81+ // Copy to temp file and rename
82+ tempPath := targetPath + ".new"
83+ if err := copyFile (exePath , tempPath ); err != nil {
84+ return fmt .Errorf ("failed to copy executable to temp file: %w" , err )
85+ }
86+ if err := os .Rename (tempPath , targetPath ); err != nil {
87+ _ = os .Remove (targetPath ) //nolint:errcheck // Try removing old file
88+ if err := os .Rename (tempPath , targetPath ); err != nil {
89+ return fmt .Errorf ("failed to replace executable: %w" , err )
90+ }
91+ }
92+ }
93+ return nil
94+ }
95+
6196// installAgent installs the agent to run automatically at system startup.
6297func installAgent (serverURL , joinKey string ) error {
6398 // Get home directory
@@ -68,7 +103,7 @@ func installAgent(serverURL, joinKey string) error {
68103
69104 // Create installation directory
70105 targetDir := filepath .Join (homeDir , installDir )
71- if err := os .MkdirAll (targetDir , 0o755 ); err != nil {
106+ if err := os .MkdirAll (targetDir , 0o755 ); err != nil { //nolint:gosec // standard directory permissions for program dir
72107 return fmt .Errorf ("failed to create directory %s: %w" , targetDir , err )
73108 }
74109
@@ -86,39 +121,13 @@ func installAgent(serverURL, joinKey string) error {
86121 fmt .Printf ("Agent is already installed at %s\n " , targetPath )
87122 // Just need to ensure autostart is configured
88123 } else {
89- // Stop any existing instance (but not ourselves)
90- // Note: We accept the TOCTOU risk here as it's a best-effort cleanup
91- if _ , err := os .Stat (targetPath ); err == nil {
92- // Use exec.Command directly to avoid shell injection
93- // The targetPath is safe (constructed from constants) but better to be explicit
94- cmd := exec .Command ("pkill" , "-f" , targetPath ) //nolint:noctx
95- _ = cmd .Run () //nolint:errcheck // Best effort
96- time .Sleep (500 * time .Millisecond )
97- }
98-
99- // Try direct copy first
100- if err := copyFile (exePath , targetPath ); err != nil {
101- // Handle "text file busy" error by copying to temp and renaming
102- if ! strings .Contains (strings .ToLower (err .Error ()), "text file busy" ) {
103- return fmt .Errorf ("failed to copy executable: %w" , err )
104- }
105-
106- // Copy to temp file and rename
107- tempPath := targetPath + ".new"
108- if err := copyFile (exePath , tempPath ); err != nil {
109- return fmt .Errorf ("failed to copy executable to temp file: %w" , err )
110- }
111- if err := os .Rename (tempPath , targetPath ); err != nil {
112- _ = os .Remove (targetPath ) //nolint:errcheck // Try removing old file
113- if err := os .Rename (tempPath , targetPath ); err != nil {
114- return fmt .Errorf ("failed to replace executable: %w" , err )
115- }
116- }
124+ if err := installExecutable (exePath , targetPath ); err != nil {
125+ return err
117126 }
118127 }
119128
120129 // Make executable
121- if err := os .Chmod (targetPath , 0o755 ); err != nil {
130+ if err := os .Chmod (targetPath , 0o755 ); err != nil { //nolint:gosec // executable needs execute permission
122131 return fmt .Errorf ("failed to set executable permissions: %w" , err )
123132 }
124133
@@ -205,7 +214,7 @@ func copyFile(src, dst string) error {
205214 if err != nil {
206215 return err
207216 }
208- return os .WriteFile (dst , data , 0o755 )
217+ return os .WriteFile (dst , data , 0o755 ) //nolint:gosec // executable needs execute permission
209218}
210219
211220// isSystemdUserAvailable checks if systemd user services are available and working.
@@ -243,7 +252,7 @@ func installMacOS(agentPath, serverURL, joinKey string) error {
243252
244253 // Create LaunchAgents directory if it doesn't exist
245254 launchAgentsDir := filepath .Join (homeDir , "Library" , "LaunchAgents" )
246- if err := os .MkdirAll (launchAgentsDir , 0o755 ); err != nil {
255+ if err := os .MkdirAll (launchAgentsDir , 0o755 ); err != nil { //nolint:gosec // standard permissions for LaunchAgents
247256 return fmt .Errorf ("failed to create LaunchAgents directory: %w" , err )
248257 }
249258
@@ -298,13 +307,12 @@ func installMacOS(agentPath, serverURL, joinKey string) error {
298307
299308 // Load the launch agent
300309 cmd := exec .Command ("launchctl" , "load" , plistPath ) //nolint:noctx // local command
301- output , err := cmd .CombinedOutput ()
302- if err != nil {
310+ if _ , err := cmd .CombinedOutput (); err != nil {
303311 // Try to unload first in case it's already loaded
304312 _ = exec .Command ("launchctl" , "unload" , plistPath ).Run () //nolint:errcheck,noctx // Best effort
305313 // Try loading again
306314 cmd = exec .Command ("launchctl" , "load" , plistPath ) //nolint:noctx // local command
307- if output , err = cmd .CombinedOutput (); err != nil {
315+ if output , err : = cmd .CombinedOutput (); err != nil {
308316 return fmt .Errorf ("failed to load launch agent: %w\n Output: %s" , err , output )
309317 }
310318 }
@@ -347,7 +355,7 @@ func installLinux(agentPath, serverURL, joinKey string) error {
347355 }
348356
349357 serviceDir := filepath .Join (homeDir , ".config" , "systemd" , "user" )
350- if err := os .MkdirAll (serviceDir , 0o755 ); err != nil {
358+ if err := os .MkdirAll (serviceDir , 0o755 ); err != nil { //nolint:gosec // standard permissions for systemd services
351359 return fmt .Errorf ("failed to create systemd directory: %w" , err )
352360 }
353361
@@ -447,7 +455,7 @@ func uninstallLinux() error {
447455func installCron (agentPath , _ , _ string ) error {
448456 // Check if crontab is available
449457 if _ , err := exec .LookPath ("crontab" ); err != nil {
450- return fmt . Errorf ("neither systemd user services nor cron are available - manual startup required" )
458+ return errors . New ("neither systemd user services nor cron are available - manual startup required" )
451459 }
452460
453461 // Get current crontab
@@ -478,13 +486,13 @@ func installCron(agentPath, _, _ string) error {
478486 // Try to start the agent immediately in background
479487 // Security note: agentPath is constructed from filepath.Join with constants,
480488 // but we use exec.Command directly to avoid shell injection risks
481- cmd = exec .Command (agentPath ) //nolint:noctx
489+ cmd = exec .Command (agentPath ) //nolint:noctx // agent spawns its own context
482490 cmd .Stdin = nil
483- cmd .Stdout = nil
491+ cmd .Stdout = nil
484492 cmd .Stderr = nil
485493 if err := cmd .Start (); err == nil {
486494 // Detach from the process
487- _ = cmd .Process .Release () //nolint:errcheck
495+ _ = cmd .Process .Release () //nolint:errcheck // best effort cleanup
488496 }
489497
490498 return nil
@@ -530,6 +538,8 @@ func uninstallCron() error {
530538}
531539
532540// installWindows installs Windows Task Scheduler task.
541+ //
542+ //nolint:unused // Windows-specific function needed for cross-platform support
533543func installWindows (agentPath , _ , _ string ) error {
534544 // Create the task XML content
535545 taskXML := fmt .Sprintf (`<?xml version="1.0" encoding="UTF-16"?>
@@ -583,39 +593,42 @@ func installWindows(agentPath, _, _ string) error {
583593 if err != nil {
584594 return fmt .Errorf ("failed to create temp file: %w" , err )
585595 }
586- defer os .Remove (tempFile .Name ()) //nolint:errcheck
596+ defer os .Remove (tempFile .Name ()) //nolint:errcheck // best effort cleanup
587597
588598 if _ , err := tempFile .WriteString (taskXML ); err != nil {
589599 return fmt .Errorf ("failed to write task XML: %w" , err )
590600 }
591- tempFile .Close () //nolint:errcheck
601+ _ = tempFile .Close () //nolint:errcheck // file already written
592602
593603 // Delete existing task if present (ignore errors)
594- cmd := exec .Command ("schtasks" , "/Delete" , "/TN" , "GitMDM Agent" , "/F" ) //nolint:noctx
595- _ = cmd .Run () //nolint:errcheck
604+ cmd := exec .Command ("schtasks" , "/Delete" , "/TN" , "GitMDM Agent" , "/F" ) //nolint:noctx // Windows task management doesn't need context
605+ _ = cmd .Run () //nolint:errcheck // best effort cleanup
596606
597607 // Create the scheduled task
598- cmd = exec .Command ("schtasks" , "/Create" , "/TN" , "GitMDM Agent" , "/XML" , tempFile .Name ()) //nolint:noctx
608+ //nolint:noctx,gosec,lll // Windows task management doesn't need context, XML file is trusted
609+ cmd = exec .Command ("schtasks" , "/Create" , "/TN" , "GitMDM Agent" , "/XML" , tempFile .Name ())
599610 output , err := cmd .CombinedOutput ()
600611 if err != nil {
601612 return fmt .Errorf ("failed to create scheduled task: %w\n Output: %s" , err , output )
602613 }
603614
604615 // Start the task immediately
605- cmd = exec .Command ("schtasks" , "/Run" , "/TN" , "GitMDM Agent" ) //nolint:noctx
606- _ = cmd .Run () //nolint:errcheck // Best effort
616+ cmd = exec .Command ("schtasks" , "/Run" , "/TN" , "GitMDM Agent" ) //nolint:noctx // Windows task management doesn't need context
617+ _ = cmd .Run () //nolint:errcheck // Best effort
607618
608619 return nil
609620}
610621
611622// uninstallWindows removes Windows Task Scheduler task.
623+ //
624+ //nolint:unused // Windows-specific function needed for cross-platform support
612625func uninstallWindows () error {
613626 // Stop the task
614- cmd := exec .Command ("schtasks" , "/End" , "/TN" , "GitMDM Agent" ) //nolint:noctx
615- _ = cmd .Run () //nolint:errcheck // Best effort
627+ cmd := exec .Command ("schtasks" , "/End" , "/TN" , "GitMDM Agent" ) //nolint:noctx // Windows task management doesn't need context
628+ _ = cmd .Run () //nolint:errcheck // Best effort
616629
617630 // Delete the task
618- cmd = exec .Command ("schtasks" , "/Delete" , "/TN" , "GitMDM Agent" , "/F" ) //nolint:noctx
631+ cmd = exec .Command ("schtasks" , "/Delete" , "/TN" , "GitMDM Agent" , "/F" ) //nolint:noctx // Windows task management doesn't need context
619632 output , err := cmd .CombinedOutput ()
620633 if err != nil {
621634 if ! strings .Contains (string (output ), "The system cannot find" ) {
@@ -625,8 +638,8 @@ func uninstallWindows() error {
625638 }
626639
627640 // Try to stop any running agent process
628- cmd = exec .Command ("taskkill" , "/F" , "/IM" , "gitmdm-agent.exe" ) //nolint:noctx
629- _ = cmd .Run () //nolint:errcheck // Best effort
641+ cmd = exec .Command ("taskkill" , "/F" , "/IM" , "gitmdm-agent.exe" ) //nolint:noctx // Windows process management doesn't need context
642+ _ = cmd .Run () //nolint:errcheck // Best effort
630643
631644 return nil
632645}
0 commit comments