| // 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...) | |
| } |