blob: 8dceab0c38e76dc568cfb2e047ea307cb8e35545 [file] [edit]
// Copyright 2024 Google LLC
//
// 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
//
// https://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.
package manager
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"github.com/GoogleCloudPlatform/galog"
acmpb "github.com/GoogleCloudPlatform/google-guest-agent/internal/acp/proto/google_guest_agent/acp"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/ps"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/resource"
)
// stopStep implements the plugin stop.
type stopStep struct {
// Cleanup is set to true to notify plugins to remove any state stored on
// disk. Stop request can be sent as part of plugin restart which does not
// require cleanup whereas plugin remove does require.
cleanup bool
}
// Name returns the name of the step.
func (ss *stopStep) Name() string { return "StopPluginStep" }
// Status returns the plugin state for current step.
func (ss *stopStep) Status() acmpb.CurrentPluginStates_StatusValue {
return acmpb.CurrentPluginStates_STOPPING
}
// Status returns the plugin state for current step.
func (ss *stopStep) ErrorStatus() acmpb.CurrentPluginStates_StatusValue {
// This step is not expected to fail as agent would kill the plugin if stop
// fails.
return acmpb.CurrentPluginStates_STATE_VALUE_UNSPECIFIED
}
// isSameExecutablePath checks if the executable path is the same as the plugin
// entry path.
func (p *Plugin) isSameExecutablePath(executable string) bool {
if runtime.GOOS == "windows" {
// On Windows, when the process is running and the binary is being deleted
// path show up similar to -
// C:\Users\<username>\AppData\Local\Temp\ProcessName.exe.old805949437"
return strings.Contains(executable, filepath.Base(p.EntryPath))
}
// If the pathname has been unlinked/deleted, the /proc returned executable
// path will contain the string '(deleted)' appended to the original pathname.
entryPath := strings.TrimSuffix(executable, " (deleted)")
return p.EntryPath == entryPath
}
func (ss *stopStep) stopPlugin(ctx context.Context, p *Plugin) error {
pluginPid := p.pid()
proc, err := ps.FindPid(pluginPid)
if err != nil {
return fmt.Errorf("%q plugin process(%d) not found: %w", p.FullName(), pluginPid, err)
}
// If plugin is not running, we can skip the stop RPC.
// Ensures PID is not reused by a different process and then attempts to
// stop and kill the plugin process.
if !p.isSameExecutablePath(proc.Exe) {
galog.Infof("Plugin PID (%d) is being reused by a different process running from (%q) different from expected binary(%q), skipping stop RPC", pluginPid, proc.Exe, p.EntryPath)
return nil
}
galog.Infof("Stopping %q plugin process (%d) running from %q", p.FullName(), pluginPid, proc.Exe)
if _, err := p.Stop(ctx, ss.cleanup); err != nil {
galog.Warnf("Stop %s plugin failed with error: %v", p.FullName(), err)
}
// Make sure plugin process exited by attempting to kill.
// When waiting for the process to exit, once all child processes have exited,
// the ECHILD error is returned. This can also happen if the process has
// already exited, or the process is not a child of the current process.
if err := ps.KillProcess(pluginPid, ps.KillModeWait); err != nil && !errors.Is(err, syscall.ECHILD) {
return fmt.Errorf("kill %s plugin process (%d) completed with error: %v", p.FullName(), pluginPid, err)
}
sendEvent(ctx, p, acmpb.PluginEventMessage_PLUGIN_STOPPED, "Successfully stopped the plugin.")
return nil
}
func (ss *stopStep) Run(ctx context.Context, p *Plugin) error {
if err := ss.stopPlugin(ctx, p); err != nil {
// Its unlikely for kill to fail as process is running as root and is a best
// effort. Just log the error for debugging in-case it happens.
galog.Warnf("Kill %s plugin process completed with: %v", p.FullName(), err)
}
p.clientMu.Lock()
if p.client != nil {
if err := p.client.Close(); err != nil {
galog.Warnf("Close %s plugin client failed with error: %v", p.FullName(), err)
}
p.client = nil
}
p.clientMu.Unlock()
p.setState(acmpb.CurrentPluginStates_STOPPED)
p.setPid(0)
// Cleanup is set to true only on plugin removal.
if ss.cleanup {
if err := cleanup(ctx, p); err != nil {
// Not a critical step in plugin removal, just log a message.
galog.Debugf("Unable to cleanup plugin state: %v", err)
}
}
return nil
}
// Cleanup removes all known paths associated with this plugin.
func cleanup(ctx context.Context, p *Plugin) error {
galog.Infof("Cleaning up %q plugin state", p.FullName())
var errs []error
// Remove resource constraint first before attempting any file removal.
// On windows [JobObjects] are used for setting resource limits that can
// prevent manager from cleanup/removing files.
if err := resource.RemoveConstraint(ctx, p.FullName()); err != nil {
errs = append(errs, fmt.Errorf("resource constraint removal failed: %w", err))
}
// Files paths of core plugins are managed by package manager do not remove.
if !p.IsLocal() {
if err := os.RemoveAll(p.InstallPath); err != nil {
errs = append(errs, fmt.Errorf("%s plugin install path (%s) removal failed with error: %w", p.FullName(), p.InstallPath, err))
}
}
if p.Protocol == udsProtocol {
if err := os.RemoveAll(p.Address); err != nil {
errs = append(errs, fmt.Errorf("%s plugin socket file (%s) removal failed with error: %w", p.FullName(), p.Address, err))
}
}
stateFile := p.stateFile()
if err := os.RemoveAll(stateFile); err != nil {
errs = append(errs, fmt.Errorf("%s plugin state (%s) removal failed with error: %w", p.FullName(), stateFile, err))
}
p.Address = ""
return errors.Join(errs...)
}