Skip to content

Commit 8ab9a1a

Browse files
Copilotrbtr
andcommitted
Add Windows service registration support to azure-cns
- Add command-line flags for service management (--service install/uninstall/run) - Implement Windows service handler using golang.org/x/sys/windows/svc - Add automatic service detection when started by Windows Service Manager - Configure service to auto-restart on failure with 5-second delays - Provide event log integration for service lifecycle events - Add stub implementation for non-Windows platforms - Update command-line help to show new service options Co-authored-by: rbtr <[email protected]>
1 parent 3ed191e commit 8ab9a1a

File tree

4 files changed

+312
-0
lines changed

4 files changed

+312
-0
lines changed

cns/service/main.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,19 @@ var args = acn.ArgumentList{
346346
Type: "string",
347347
DefaultValue: "",
348348
},
349+
{
350+
Name: acn.OptServiceAction,
351+
Shorthand: acn.OptServiceActionAlias,
352+
Description: "Windows service action: install, uninstall, or run as service",
353+
Type: "string",
354+
DefaultValue: "",
355+
ValueMap: map[string]interface{}{
356+
acn.OptServiceInstall: 0,
357+
acn.OptServiceUninstall: 0,
358+
acn.OptServiceRun: 0,
359+
"": 0,
360+
},
361+
},
349362
}
350363

351364
// init() is executed before main() whenever this package is imported
@@ -521,12 +534,54 @@ func main() {
521534
telemetryDaemonEnabled := acn.GetArg(acn.OptTelemetryService).(bool)
522535
cniConflistFilepathArg := acn.GetArg(acn.OptCNIConflistFilepath).(string)
523536
cniConflistScenarioArg := acn.GetArg(acn.OptCNIConflistScenario).(string)
537+
serviceAction := acn.GetArg(acn.OptServiceAction).(string)
524538

525539
if vers {
526540
printVersion()
527541
os.Exit(0)
528542
}
529543

544+
// Handle Windows service actions (install/uninstall)
545+
switch serviceAction {
546+
case acn.OptServiceInstall:
547+
if err := installService(); err != nil {
548+
fmt.Fprintf(os.Stderr, "Failed to install service: %v\n", err)
549+
os.Exit(1)
550+
}
551+
os.Exit(0)
552+
case acn.OptServiceUninstall:
553+
if err := uninstallService(); err != nil {
554+
fmt.Fprintf(os.Stderr, "Failed to uninstall service: %v\n", err)
555+
os.Exit(1)
556+
}
557+
os.Exit(0)
558+
case acn.OptServiceRun:
559+
// This is an explicit flag to run as service (for testing)
560+
// Normally the service manager would start us and we'd detect it automatically
561+
if err := runAsService(); err != nil {
562+
fmt.Fprintf(os.Stderr, "Failed to run as service: %v\n", err)
563+
os.Exit(1)
564+
}
565+
// The service control loop has exited, but we still need to run the main service logic
566+
// Fall through to continue with normal startup
567+
case "":
568+
// No service action specified, check if we're running as a service
569+
isService, err := isWindowsService()
570+
if err != nil {
571+
fmt.Fprintf(os.Stderr, "Failed to detect service mode: %v\n", err)
572+
os.Exit(1)
573+
}
574+
if isService {
575+
// We're being started by the Windows Service Manager
576+
if err := runAsService(); err != nil {
577+
fmt.Fprintf(os.Stderr, "Failed to run as service: %v\n", err)
578+
os.Exit(1)
579+
}
580+
// The service control loop has exited, but we still need to run the main service logic
581+
// Fall through to continue with normal startup
582+
}
583+
}
584+
530585
// Initialize CNS.
531586
var (
532587
err error

cns/service/service_other.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
// Copyright 2017 Microsoft. All rights reserved.
5+
// MIT License
6+
7+
package main
8+
9+
import (
10+
"fmt"
11+
)
12+
13+
// installService is not supported on non-Windows platforms
14+
func installService() error {
15+
return fmt.Errorf("service installation is only supported on Windows")
16+
}
17+
18+
// uninstallService is not supported on non-Windows platforms
19+
func uninstallService() error {
20+
return fmt.Errorf("service uninstallation is only supported on Windows")
21+
}
22+
23+
// runAsService is not supported on non-Windows platforms
24+
func runAsService() error {
25+
return fmt.Errorf("running as service is only supported on Windows")
26+
}
27+
28+
// isWindowsService always returns false on non-Windows platforms
29+
func isWindowsService() (bool, error) {
30+
return false, nil
31+
}

cns/service/service_windows.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
//go:build windows
2+
// +build windows
3+
4+
// Copyright 2017 Microsoft. All rights reserved.
5+
// MIT License
6+
7+
package main
8+
9+
import (
10+
"fmt"
11+
"os"
12+
"path/filepath"
13+
"time"
14+
15+
"golang.org/x/sys/windows/svc"
16+
"golang.org/x/sys/windows/svc/eventlog"
17+
"golang.org/x/sys/windows/svc/mgr"
18+
)
19+
20+
const (
21+
serviceName = "azure-cns"
22+
serviceDisplayName = "Azure Container Networking Service"
23+
serviceDescription = "Provides container networking services for Azure"
24+
)
25+
26+
// windowsService implements the svc.Handler interface for Windows service control
27+
type windowsService struct {
28+
runService func()
29+
}
30+
31+
// Execute is called by the Windows service manager and implements the service control loop
32+
func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
33+
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
34+
35+
changes <- svc.Status{State: svc.StartPending}
36+
37+
// Start the service in a goroutine
38+
go ws.runService()
39+
40+
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
41+
42+
// Service control loop
43+
loop:
44+
for {
45+
select {
46+
case c := <-r:
47+
switch c.Cmd {
48+
case svc.Interrogate:
49+
changes <- c.CurrentStatus
50+
case svc.Stop, svc.Shutdown:
51+
changes <- svc.Status{State: svc.StopPending}
52+
// Cancel the root context to signal shutdown
53+
if rootCtx != nil {
54+
// Send shutdown signal through the error channel
55+
select {
56+
case rootErrCh <- fmt.Errorf("service stop requested"):
57+
default:
58+
}
59+
}
60+
break loop
61+
default:
62+
// Log unexpected control request
63+
}
64+
}
65+
}
66+
67+
return
68+
}
69+
70+
// runAsService runs the application as a Windows service
71+
func runAsService() error {
72+
elog, err := eventlog.Open(serviceName)
73+
if err != nil {
74+
return fmt.Errorf("failed to open event log: %w", err)
75+
}
76+
defer elog.Close()
77+
78+
elog.Info(1, fmt.Sprintf("Starting %s service", serviceName))
79+
80+
ws := &windowsService{
81+
runService: func() {
82+
// The main service logic will run in the existing main() function
83+
// after runAsService() returns
84+
},
85+
}
86+
87+
err = svc.Run(serviceName, ws)
88+
if err != nil {
89+
elog.Error(1, fmt.Sprintf("Service failed: %v", err))
90+
return fmt.Errorf("failed to run service: %w", err)
91+
}
92+
93+
elog.Info(1, fmt.Sprintf("%s service stopped", serviceName))
94+
return nil
95+
}
96+
97+
// installService installs the CNS as a Windows service
98+
func installService() error {
99+
exepath, err := getExecutablePath()
100+
if err != nil {
101+
return fmt.Errorf("failed to get executable path: %w", err)
102+
}
103+
104+
m, err := mgr.Connect()
105+
if err != nil {
106+
return fmt.Errorf("failed to connect to service manager: %w", err)
107+
}
108+
defer m.Disconnect()
109+
110+
s, err := m.OpenService(serviceName)
111+
if err == nil {
112+
s.Close()
113+
return fmt.Errorf("service %s already exists", serviceName)
114+
}
115+
116+
s, err = m.CreateService(serviceName, exepath, mgr.Config{
117+
DisplayName: serviceDisplayName,
118+
Description: serviceDescription,
119+
StartType: mgr.StartAutomatic,
120+
ServiceStartName: "LocalSystem",
121+
})
122+
if err != nil {
123+
return fmt.Errorf("failed to create service: %w", err)
124+
}
125+
defer s.Close()
126+
127+
// Set recovery options to restart the service on failure
128+
err = s.SetRecoveryActions([]mgr.RecoveryAction{
129+
{Type: mgr.ServiceRestart, Delay: 5 * time.Second},
130+
{Type: mgr.ServiceRestart, Delay: 5 * time.Second},
131+
{Type: mgr.ServiceRestart, Delay: 5 * time.Second},
132+
}, 86400) // Reset failure count after 24 hours
133+
if err != nil {
134+
// This is not a fatal error, just log it
135+
fmt.Printf("Warning: failed to set recovery actions: %v\n", err)
136+
}
137+
138+
// Set up event log
139+
err = eventlog.InstallAsEventCreate(serviceName, eventlog.Error|eventlog.Warning|eventlog.Info)
140+
if err != nil {
141+
// Remove the service if we can't set up event log
142+
s.Delete()
143+
return fmt.Errorf("failed to setup event log: %w", err)
144+
}
145+
146+
fmt.Printf("Service %s installed successfully.\n", serviceName)
147+
fmt.Printf("Run 'net start %s' to start the service.\n", serviceName)
148+
return nil
149+
}
150+
151+
// uninstallService removes the CNS Windows service
152+
func uninstallService() error {
153+
m, err := mgr.Connect()
154+
if err != nil {
155+
return fmt.Errorf("failed to connect to service manager: %w", err)
156+
}
157+
defer m.Disconnect()
158+
159+
s, err := m.OpenService(serviceName)
160+
if err != nil {
161+
return fmt.Errorf("service %s is not installed: %w", serviceName, err)
162+
}
163+
defer s.Close()
164+
165+
// Try to stop the service if it's running
166+
status, err := s.Query()
167+
if err != nil {
168+
return fmt.Errorf("failed to query service status: %w", err)
169+
}
170+
171+
if status.State != svc.Stopped {
172+
status, err = s.Control(svc.Stop)
173+
if err != nil {
174+
return fmt.Errorf("failed to stop service: %w", err)
175+
}
176+
177+
// Wait for the service to stop
178+
timeout := time.Now().Add(10 * time.Second)
179+
for status.State != svc.Stopped {
180+
if time.Now().After(timeout) {
181+
return fmt.Errorf("timeout waiting for service to stop")
182+
}
183+
time.Sleep(300 * time.Millisecond)
184+
status, err = s.Query()
185+
if err != nil {
186+
return fmt.Errorf("failed to query service status: %w", err)
187+
}
188+
}
189+
}
190+
191+
err = s.Delete()
192+
if err != nil {
193+
return fmt.Errorf("failed to delete service: %w", err)
194+
}
195+
196+
// Remove event log
197+
err = eventlog.Remove(serviceName)
198+
if err != nil {
199+
// This is not fatal, just log it
200+
fmt.Printf("Warning: failed to remove event log: %v\n", err)
201+
}
202+
203+
fmt.Printf("Service %s uninstalled successfully.\n", serviceName)
204+
return nil
205+
}
206+
207+
// getExecutablePath returns the full path to the current executable
208+
func getExecutablePath() (string, error) {
209+
exepath, err := os.Executable()
210+
if err != nil {
211+
return "", err
212+
}
213+
return filepath.Abs(exepath)
214+
}
215+
216+
// isWindowsService checks if the application is running as a Windows service
217+
func isWindowsService() (bool, error) {
218+
return svc.IsWindowsService()
219+
}

common/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,11 @@ const (
145145
OptCNIConflistScenario = "cni-conflist-scenario"
146146
// OptCNIConflistScenarioAlias "shorthand" for the cni conflist scenairo, see above
147147
OptCNIConflistScenarioAlias = "cniconflistscenario"
148+
149+
// Windows service management
150+
OptServiceAction = "service"
151+
OptServiceActionAlias = "s"
152+
OptServiceInstall = "install"
153+
OptServiceUninstall = "uninstall"
154+
OptServiceRun = "run"
148155
)

0 commit comments

Comments
 (0)