internal/daemon/daemon_windows.go (149 lines of code) (raw):
// Copyright 2024 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package daemon
import (
"context"
"fmt"
"time"
"github.com/GoogleCloudPlatform/galog"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/retry"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
)
const (
// GuestAgent is the name of the guest agent daemon.
GuestAgent = "GCEAgent"
// GuestAgentManager is the name of the guest agent manager daemon.
GuestAgentManager = "GCEAgentManager"
// GuestAgentCompatManager is the name of the guest agent compat manager
// daemon.
GuestAgentCompatManager = "GCEWindowsCompatManager"
)
// serviceState is a map of windows service states to strings.
var serviceState = map[svc.State]string{
1: "Stopped",
2: "StartPending",
3: "StopPending",
4: "Running",
5: "ContinuePending",
6: "PausePending",
7: "Paused",
}
func init() {
// Client is the client for interacting with windows services.
Client = winServiceClient{}
}
// winServiceClient is the windows implementation of ClientInterface.
type winServiceClient struct{}
func (w winServiceClient) RestartService(ctx context.Context, service string, method RestartMethod) error {
if err := w.StopDaemon(ctx, service); err != nil {
return fmt.Errorf("failed to stop service %q: %w", service, err)
}
if err := w.StartDaemon(ctx, service); err != nil {
return fmt.Errorf("failed to start service %q: %w", service, err)
}
return nil
}
func (winServiceClient) CheckUnitExists(ctx context.Context, unit string) (bool, error) {
return false, fmt.Errorf("checking unit existence not supported on Windows")
}
func (winServiceClient) ReloadDaemon(ctx context.Context, daemon string) error {
return fmt.Errorf("reloading daemons not supported on Windows")
}
func (winServiceClient) UnitStatus(ctx context.Context, unit string) (ServiceStatus, error) {
mgr, ctrlr, err := openSvcController(unit)
defer closeSvcController(ctrlr, mgr)
if err != nil {
return Unknown, fmt.Errorf("failed to open service controller for %q: %w", unit, err)
}
status, err := ctrlr.Query()
if err != nil {
return Unknown, fmt.Errorf("failed to query service %q: %w", unit, err)
}
switch status.State {
case svc.Running:
return Active, nil
case svc.Stopped:
return Inactive, nil
default:
return Unknown, fmt.Errorf("unknown service state: %q", serviceState[status.State])
}
}
func (winServiceClient) StopDaemon(ctx context.Context, daemon string) error {
mgr, ctrlr, err := openSvcController(daemon)
defer closeSvcController(ctrlr, mgr)
if err != nil {
return fmt.Errorf("failed to open service controller for %q: %w", daemon, err)
}
if _, err := ctrlr.Control(svc.Stop); err != nil {
return fmt.Errorf("failed to stop service %q: %w", daemon, err)
}
return waitForState(ctx, ctrlr, svc.Stopped)
}
func (winServiceClient) EnableService(ctx context.Context, daemon string) error {
m, ctrlr, err := openSvcController(daemon)
defer closeSvcController(ctrlr, m)
if err != nil {
return fmt.Errorf("failed to open service controller for %q: %w", daemon, err)
}
currentConfig, err := ctrlr.Config()
if err != nil {
return fmt.Errorf("failed to get current config for service %q: %w", daemon, err)
}
// Set the start type to Automatic Delayed Start, this matches the current
// set up for the guest agent services.
currentConfig.StartType = mgr.StartAutomatic
currentConfig.DelayedAutoStart = true
return ctrlr.UpdateConfig(currentConfig)
}
func (winServiceClient) DisableService(ctx context.Context, daemon string) error {
m, ctrlr, err := openSvcController(daemon)
defer closeSvcController(ctrlr, m)
if err != nil {
return fmt.Errorf("failed to open service controller for %q: %w", daemon, err)
}
currentConfig, err := ctrlr.Config()
if err != nil {
return fmt.Errorf("failed to get current config for service %q: %w", daemon, err)
}
currentConfig.StartType = mgr.StartDisabled
return ctrlr.UpdateConfig(currentConfig)
}
func (winServiceClient) StartDaemon(ctx context.Context, daemon string) error {
mgr, ctrlr, err := openSvcController(daemon)
defer closeSvcController(ctrlr, mgr)
if err != nil {
return fmt.Errorf("failed to open service controller for %q: %w", daemon, err)
}
if err := ctrlr.Start(); err != nil {
return fmt.Errorf("failed to start service %q: %w", daemon, err)
}
return waitForState(ctx, ctrlr, svc.Running)
}
// waitForState waits for a service to reach a specific state. It returns an
// error if the service does not reach the expected state within timeout.
func waitForState(ctx context.Context, ctrlr *mgr.Service, state svc.State) error {
waitFunc := func() error {
status, err := ctrlr.Query()
if err != nil {
return fmt.Errorf("failed to query service %q: %w", ctrlr.Name, err)
}
if status.State != state {
return fmt.Errorf("service %q expected state %q, got %q", ctrlr.Name, serviceState[state], serviceState[status.State])
}
galog.Infof("Service %q reached state %q", ctrlr.Name, serviceState[state])
return nil
}
policy := retry.Policy{MaxAttempts: 5, BackoffFactor: 2, Jitter: time.Second * 2}
return retry.Run(ctx, policy, waitFunc)
}
// openSvcController connects to the windows service controller manager and
// opens a controller for a given service name. It returns service manager and
// [name] service controller. Make sure to call closeSvcController to disconnect
// and release all resources.
func openSvcController(name string) (*mgr.Mgr, *mgr.Service, error) {
m, err := mgr.Connect()
if err != nil {
return m, nil, fmt.Errorf("failed to connect to windows services: %w", err)
}
svcCtrlr, err := m.OpenService(name)
if err != nil {
return m, svcCtrlr, fmt.Errorf("failed to open service for %q: %w", name, err)
}
return m, svcCtrlr, nil
}
// closeSvcController closes the service controller and disconnects from the
// service controller manager.
func closeSvcController(svcCtrlr *mgr.Service, svc *mgr.Mgr) {
if svcCtrlr != nil {
if err := svcCtrlr.Close(); err != nil {
galog.Warnf("Failed to close service controller(%q): %v", svcCtrlr.Name, err)
}
}
if svc != nil {
if err := svc.Disconnect(); err != nil {
galog.Warnf("Failed to disconnect from windows services: %v", err)
}
}
}