google_guest_agent/command/command.go (92 lines of code) (raw):

// Copyright 2023 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. // Package command facilitates calling commands within the guest-agent. package command import ( "context" "encoding/json" "fmt" "io" "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg" ) // Get returns the current command monitor which can be used to register command handlers. func Get() *Monitor { return cmdMonitor } // Handler functions are the business logic of commands. They must process json // encoded as a byte slice which contains a Command field and optional arbitrary // data, and return json which contains a Status, StatusMessage, and optional // arbitrary data (again encoded as a byte slice). Returned errors will be // passed onto the command requester. type Handler func([]byte) ([]byte, error) // Request is the basic request structure. Command determines which handler the // request is routed to. Callers may set additional arbitrary fields. type Request struct { Command string } // Response is the basic response structure. Handlers may set additional // arbitrary fields. type Response struct { // Status code for the request. Meaning is defined by the caller, but // conventially zero is success. Status int // StatusMessage is an optional message defined by the caller. Should generally // help a human understand what happened. StatusMessage string } var ( // CmdNotFoundError is return when there is no handler for the request command CmdNotFoundError = Response{ Status: 101, StatusMessage: "Could not find a handler for the requested command", } // BadRequestError is returned for invalid or unparseable JSON BadRequestError = Response{ Status: 102, StatusMessage: "Could not parse valid JSON from request", } // ConnError is returned for errors from the underlying communication protocol ConnError = Response{ Status: 103, StatusMessage: "Connection error", } // TimeoutError is returned when the timeout period elapses before valid JSON is receieved TimeoutError = Response{ Status: 104, StatusMessage: "Connection timeout before reading valid request", } // HandlerError is returned when the handler function returns an non-nil error. The status message will be replaced with the returnd error string. HandlerError = Response{ Status: 105, StatusMessage: "The command handler encountered an error processing your request", } // InternalErrorCode is the error code for internal command server errors. Returned when failing to marshal a response. InternalErrorCode = 106 internalError = []byte(`{"Status":106,"StatusMessage":"The command server encountered an internal error trying to respond to your request"}`) ) // RegisterHandler registers f as the handler for cmd. If a command.Server has // been initialized, it will be signalled to start listening for commands. func (m *Monitor) RegisterHandler(cmd string, f Handler) error { m.handlersMu.Lock() defer m.handlersMu.Unlock() if _, ok := m.handlers[cmd]; ok { return fmt.Errorf("cmd %s is already handled", cmd) } m.handlers[cmd] = f return nil } // UnregisterHandler clears the handlers for cmd. If a command.Server has been // intialized and there are no more handlers registered, the server will be // signalled to stop listening for commands. func (m *Monitor) UnregisterHandler(cmd string) error { m.handlersMu.Lock() defer m.handlersMu.Unlock() if _, ok := m.handlers[cmd]; !ok { return fmt.Errorf("cmd %s is not registered", cmd) } delete(m.handlers, cmd) return nil } // SendCommand sends a command request over the configured pipe. func SendCommand(ctx context.Context, req []byte) []byte { pipe := cfg.Get().Unstable.CommandPipePath if pipe == "" { pipe = DefaultPipePath } return SendCmdPipe(ctx, pipe, req) } // SendCmdPipe sends a command request over a specific pipe. Most callers // should use SendCommand() instead. func SendCmdPipe(ctx context.Context, pipe string, req []byte) []byte { conn, err := dialPipe(ctx, pipe) if err != nil { if b, err := json.Marshal(ConnError); err == nil { return b } return internalError } i, err := conn.Write(req) if err != nil || i != len(req) { if b, err := json.Marshal(ConnError); err == nil { return b } return internalError } data, err := io.ReadAll(conn) if err != nil { if b, err := json.Marshal(ConnError); err == nil { return b } return internalError } return data }