blob: 849d859b358233e23c2c910205db05e78399172a [file] [log] [blame]
package crash
import (
"context"
"fmt"
"cloud.google.com/go/pubsub"
"github.com/google/uuid"
"github.com/mark3labs/mcp-go/mcp"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/mcp/common"
"google.golang.org/protobuf/encoding/protojson"
pb "go.skia.org/infra/mcp/services/crash/proto"
)
type CrashService struct {
}
// Initialize the service with the provided arguments.
func (s CrashService) Init(serviceArgs string) error {
return nil
}
// GetTools returns the supported tools by the service.
func (s CrashService) GetTools() []common.Tool {
return []common.Tool{
{
Name: "Predator",
Description: "Sends a Predator request to search for culprit CLs in a regression range.",
Arguments: []common.ToolArgument{
{
Name: "stacktrace",
Description: "The stacktrace to find a culprit for." +
" Each frame in the stack has fields " +
"<frame index> <address> in <symbol> <source file>:<line number> e.g. " +
"#0 0x12125186c in foo() /path/to/foo.cc:12. Prepare the stacktrace in this format.",
Required: true,
},
{
Name: "last_good_version",
Description: "The last Chrome version without the crash. Chrome versions are formatted like " +
"123.0.456.0.",
Required: true,
},
{
Name: "first_bad_version",
Description: "The first Chrome version with the crash. Chrome versions are formatted like " +
"123.0.456.0.",
Required: true,
},
},
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
stacktrace, err := request.RequireString("stacktrace")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
stacktrace = "CRASHED [ERROR @ 0x123]\n" + stacktrace
last_good_version, err := request.RequireString("last_good_version")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
first_bad_version, err := request.RequireString("first_bad_version")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
dashboard_url, err := sendPredatorRequest(stacktrace, last_good_version, first_bad_version)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(fmt.Sprintf(
"Looking between %s and %s for a culprit for %s."+
"Your results should appear at https://predator-for-me.appspot.com/cracas/dashboard with signature %s",
last_good_version, first_bad_version, stacktrace, dashboard_url)), nil
},
},
}
}
func (s *CrashService) Shutdown() error {
return nil
}
func (s *CrashService) GetResources() []common.Resource {
return []common.Resource{}
}
func sendPredatorRequest(stacktrace, last_good_version, first_bad_version string) (string, error) {
projectID := "predator-for-me"
topicID := "cracas"
last_good_version_metadata := &pb.PredatorRequest_CustomizedData_HistoricalMetaData{
CrashCount: 0,
Cpm: 0.01,
ChromeVersion: last_good_version,
}
first_bad_version_metadata := &pb.PredatorRequest_CustomizedData_HistoricalMetaData{
CrashCount: 1,
Cpm: 1,
ChromeVersion: first_bad_version,
}
id := uuid.NewString()
predator_request := &pb.PredatorRequest{
StackTrace: []string{stacktrace},
ChromeVersion: first_bad_version,
Platform: "all",
ClientId: "cracas",
Signature: id,
CustomizedData: &pb.PredatorRequest_CustomizedData{
Channel: "dev",
HistoricalMetadata: []*pb.PredatorRequest_CustomizedData_HistoricalMetaData{
last_good_version_metadata,
first_bad_version_metadata,
},
},
CrashIdentifiers: &pb.CrashIdentifiers{
Uuid: id,
},
}
jsonBytes, err := protojson.MarshalOptions{
Multiline: true,
Indent: " ",
UseProtoNames: true,
}.Marshal(predator_request)
if err != nil {
return "", err
}
err = publishMessage(projectID, topicID, jsonBytes)
if err != nil {
return "", err
}
return id, nil
}
// publishMessage publishes a message to a Pub/Sub topic.
func publishMessage(projectID, topicID string, msg []byte) error {
ctx := context.Background()
client, err := pubsub.NewClient(ctx, projectID)
if err != nil {
return fmt.Errorf("pubsub.NewClient: %v", err)
}
defer client.Close()
t := client.Topic(topicID)
result := t.Publish(ctx, &pubsub.Message{
Data: []byte(msg),
})
// Block until the result is returned and a server-generated
// ID is returned for the published message.
id, err := result.Get(ctx)
if err != nil {
return fmt.Errorf("Get: %v", err)
}
sklog.Infof("Published a message %s; msg ID: %v\n", string(msg), id)
return nil
}