* Copyright (c) Huawei Technologies Co., Ltd. 2025. 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 debug
import (
"bufio"
"fmt"
"net"
"os/exec"
"strconv"
"strings"
"io"
"github.com/spf13/cobra"
"cli/constant"
"cli/pkg/cmdio"
"cli/utils"
"cli/utils/colorprint"
)
type debugOptions struct {
cmdIO *cmdio.CmdIO
master string
}
const (
promptString = "(yrdb) "
splitpart = 2
)
var (
opts debugOptions
MasterInfo *utils.MasterInfo
)
var yrDebugCmd = &cobra.Command{
Use: "debug",
Short: fmt.Sprintf("Enter the %s debug shell", constant.PlatformName),
Long: fmt.Sprintf("Enter the %s debug shell", constant.PlatformName),
Example: utils.RawFormat(fmt.Sprintf(`
%s debug in interactive mode:
$ %s debug
%s debug with specified file path: 'master.info':
$ %s debug --master master.info
`, constant.CliName, constant.CliName, constant.CliName, constant.CliName)),
Args: utils.NoArgs,
RunE: yrDebug,
}
type CommandHandler func(*cmdio.CmdIO, []string) error
var commandTable = map[string]CommandHandler{
"help": handleHelp,
"h": handleHelp,
"quit": handleQuit,
"q": handleQuit,
"instance": handleInstance,
"i": handleInstance,
"info": handleInfo,
}
func InitCMD(cio *cmdio.CmdIO) *cobra.Command {
opts.cmdIO = cio
MasterInfo = &utils.MasterInfo{}
debugInstanceInfosMap = make(map[string]InstanceInfo)
yrDebugCmd.Flags().StringVarP(&opts.master, "master", "m", "", "Specify the 'master.info' file path")
return yrDebugCmd
}
func yrDebug(cmd *cobra.Command, args []string) error {
masterPath := opts.master
if masterPath == "" {
masterPath = constant.DefaultYuanRongCurrentMasterInfoPath
}
var err error
MasterInfo, err = utils.GetMasterInfoFromFile(masterPath)
if err != nil {
colorprint.PrintFail(opts.cmdIO.Out, "failed to load master.info: ", err.Error(), "\n")
return err
}
startDebugShell()
return nil
}
func startDebugShell() {
colorprint.PrintInteractive(opts.cmdIO.Out, "Type 'help' or 'h' for help, 'quit' or 'q' to exit.\n")
reader := bufio.NewReader(opts.cmdIO.In)
for {
colorprint.PrintInteractive(opts.cmdIO.Out, promptString)
input, err := reader.ReadString('\n')
if err != nil {
colorprint.PrintFail(opts.cmdIO.Out, "failed to read input: ", err.Error(), "\n")
continue
}
input = strings.TrimSpace(input)
parts := strings.Fields(input)
if len(parts) == 0 {
continue
}
handler, ok := commandTable[strings.ToLower(parts[0])]
if !ok {
colorprint.PrintFail(opts.cmdIO.Out, "unknown command: ", input, "\n")
colorprint.PrintInteractive(opts.cmdIO.Out, "Type 'help' or 'h' for a list of available commands.\n")
continue
}
if err := handler(opts.cmdIO, parts); err != nil {
if err.Error() == "exit" {
return
}
colorprint.PrintFail(opts.cmdIO.Out, err.Error(), "\n", "")
}
}
}
func handleHelp(cio *cmdio.CmdIO, parts []string) error {
colorprint.PrintSuccess(opts.cmdIO.Out, "Available commands:\n", "")
colorprint.PrintSuccess(opts.cmdIO.Out, " help/h: Show this help message\n", "")
colorprint.PrintSuccess(opts.cmdIO.Out, " instance/i <id>: Attach to the specified remote instance\n", "")
colorprint.PrintSuccess(opts.cmdIO.Out, " info instance/i: Show all instance ids and process status\n", "")
colorprint.PrintSuccess(opts.cmdIO.Out, " quit/q: Exit the debug shell\n", "")
return nil
}
func handleQuit(cio *cmdio.CmdIO, parts []string) error {
colorprint.PrintSuccess(opts.cmdIO.Out, "Exiting debug shell.\n", "")
return fmt.Errorf("exit")
}
func handleInstance(cio *cmdio.CmdIO, parts []string) error {
if len(parts) != splitpart {
return fmt.Errorf("invalid command. Usage: instance <id>")
}
instanceID := parts[1]
colorprint.PrintInteractive(opts.cmdIO.Out, fmt.Sprintf("Attaching to instance ID: %s\n", instanceID))
instanceInfo, ok := debugInstanceInfosMap[instanceID]
if !ok {
if err := queryDebugInstanceInfo(cio); err != nil {
return fmt.Errorf("failed to query debug instance info, err: %v", err)
}
if instanceInfo, ok = debugInstanceInfosMap[instanceID]; !ok {
return fmt.Errorf("failed to find instance ID %s", instanceID)
}
}
language := strings.ToLower(instanceInfo.Language)
if strings.HasPrefix(language, "python") {
colorprint.PrintInteractive(opts.cmdIO.Out,
fmt.Sprintf("Detected Python function (%s), connecting to local DebugServer\n",
instanceInfo.Language))
if err := connectToPythonDebugServer(instanceInfo.InstanceID); err != nil {
return fmt.Errorf("failed to connect to Python DebugServer: %v", err)
}
}else{
if err := startGDBClient(instanceInfo.DebugServer, strconv.Itoa(int(instanceInfo.PID))); err != nil {
return err
}
}
return nil
}
func startGDBClient(gdbserverAddress string, pid string) error {
cmd := exec.Command(
"gdb",
"-ex", "set pagination off",
"-ex", fmt.Sprintf("target extended-remote %s", gdbserverAddress),
"-ex", fmt.Sprintf("attach %s", pid),
"-ex", "handle SIGSTOP nostop",
)
cmd.Stdin = opts.cmdIO.In
cmd.Stdout = opts.cmdIO.Out
cmd.Stderr = opts.cmdIO.ErrOut
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start GDB client: %v", err)
}
if err := cmd.Wait(); err != nil {
return fmt.Errorf("GDB client exited with error: %v", err)
}
return nil
}
func handleInfo(cio *cmdio.CmdIO, parts []string) error {
if len(parts) < splitpart {
return fmt.Errorf("invalid command. Usage: info <subcommand>")
}
subcommand := strings.ToLower(parts[1])
switch subcommand {
case "instance", "i":
pageOutDebugInstanceInfos(cio)
return nil
default:
return fmt.Errorf("unknown subcommand: %s", subcommand)
}
return nil
}
func connectToPythonDebugServer(instanceID string) error {
instanceInfo, ok :=debugInstanceInfosMap[instanceID]
if !ok {
return fmt.Errorf("instance %s not found", instanceID)
}
addr := instanceInfo.DebugServer
if addr == "" {
return fmt.Errorf("debug server address is empty for instance %s", instanceID)
}
conn, err := net.Dial("tcp", addr)
if err != nil {
return fmt.Errorf("cannot connect to Python DebugServer at %s: %v", addr, err)
}
defer func() {
if closeErr := conn.Close(); closeErr != nil {
_ = closeErr
}
}()
colorprint.PrintInteractive(
opts.cmdIO.Out,
"Connected! Use Pdb commands like 'c'. Type 'quit' to exit.\n",
)
done := make(chan struct{})
go func() {
_, err := io.Copy(conn, opts.cmdIO.In)
if err != nil {
}
close(done)
}()
_, err = io.Copy(opts.cmdIO.Out, conn)
if err != nil {
}
<-done
return nil
}