Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified Packages/src/Cli~/dist/darwin-amd64/uloop
Binary file not shown.
Binary file modified Packages/src/Cli~/dist/darwin-arm64/uloop
Binary file not shown.
Binary file modified Packages/src/Cli~/dist/windows-amd64/uloop.exe
Binary file not shown.
43 changes: 43 additions & 0 deletions Packages/src/Cli~/internal/cli/busy_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cli

import (
"encoding/json"
"fmt"
"io"
)

const cliStatusBusy = "Busy"

type cliStatusEnvelope struct {
Status string `json:"Status"`
Message string `json:"Message"`
}

func writeBusyStatusEnvelope(writer io.Writer, message string) {
encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ")
_ = encoder.Encode(cliStatusEnvelope{
Status: cliStatusBusy,
Message: message,
})
}

func unityServerBusyMessage(fallback string, data map[string]any, requestedCommand string) string {
runningToolName := rpcStringData(data, "runningToolName")
requestedToolName := firstNonEmpty(rpcStringData(data, "requestedToolName"), requestedCommand)
if runningToolName == "" || requestedToolName == "" {
return fallback
}
return fmt.Sprintf(
"'%s' was not executed because Unity is busy running '%s'. Retry after the running tool completes.",
requestedToolName,
runningToolName)
}

func rpcStringData(data map[string]any, key string) string {
value, ok := data[key].(string)
if !ok {
return ""
}
return value
}
24 changes: 12 additions & 12 deletions Packages/src/Cli~/internal/cli/error_envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ type errorContext struct {
}

func writeErrorEnvelope(writer io.Writer, err cliError) {
if err.ErrorCode == errorCodeUnityServerBusy {
writeBusyStatusEnvelope(writer, err.Message)
return
}
encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ")
_ = encoder.Encode(cliErrorEnvelope{
Expand Down Expand Up @@ -202,7 +206,7 @@ func classifyError(err error, context errorContext) cliError {
return cliUpdateRequiredError(rpcErr, details, decodedData, context)
}
if rpcDataType(decodedData) == "server_busy" {
return unityServerBusyError(rpcErr, details, context)
return unityServerBusyError(rpcErr, details, decodedData, context)
}
return cliError{
ErrorCode: errorCodeUnityRPCError,
Expand Down Expand Up @@ -307,11 +311,16 @@ func rpcDataType(data map[string]any) string {
return value
}

func unityServerBusyError(rpcErr *unityipc.RPCError, details map[string]any, context errorContext) cliError {
func unityServerBusyError(
rpcErr *unityipc.RPCError,
details map[string]any,
data map[string]any,
context errorContext,
) cliError {
return cliError{
ErrorCode: errorCodeUnityServerBusy,
Phase: errorPhaseDispatch,
Message: rpcErr.Message,
Message: unityServerBusyMessage(rpcErr.Message, data, context.command),
Retryable: true,
SafeToRetry: true,
ProjectRoot: context.projectRoot,
Expand Down Expand Up @@ -485,12 +494,3 @@ func isSafeRetryCommand(command string) bool {
return false
}
}

func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
33 changes: 33 additions & 0 deletions Packages/src/Cli~/internal/cli/error_envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ func TestClassifyCliUpdateRequiredRPCError(t *testing.T) {
}

func TestClassifyServerBusyRPCError(t *testing.T) {
// Verifies server_busy RPC failures keep retryable classification while using the lightweight message.
err := &unityipc.RPCError{
Code: -32603,
Message: "Unity is busy running 'compile'. Retry 'get-logs' after the running tool completes.",
Expand All @@ -230,6 +231,38 @@ func TestClassifyServerBusyRPCError(t *testing.T) {
if !cliErr.Retryable || !cliErr.SafeToRetry {
t.Fatalf("retry flags mismatch: %#v", cliErr)
}
expectedMessage := "'get-logs' was not executed because Unity is busy running 'compile'. Retry after the running tool completes."
if cliErr.Message != expectedMessage {
t.Fatalf("message mismatch: %s", cliErr.Message)
}
}

func TestWriteClassifiedServerBusyRPCErrorWritesBusyStatus(t *testing.T) {
// Verifies server_busy output avoids the full error envelope because busy is a temporary state.
err := &unityipc.RPCError{
Code: -32603,
Message: "Unity is busy running 'compile'. Retry 'get-logs' after the running tool completes.",
Data: json.RawMessage(
`{"type":"server_busy","runningToolName":"compile","requestedToolName":"get-logs","message":"Unity is busy running 'compile'. Retry 'get-logs' after the running tool completes."}`),
}
var stderr bytes.Buffer

writeClassifiedError(&stderr, err, errorContext{projectRoot: "/tmp/MyProject", command: "get-logs"})

var envelope cliStatusEnvelope
if err := json.Unmarshal(stderr.Bytes(), &envelope); err != nil {
t.Fatalf("stderr is not valid JSON: %v\n%s", err, stderr.String())
}
if envelope.Status != cliStatusBusy {
t.Fatalf("status mismatch: %#v", envelope)
}
expectedMessage := "'get-logs' was not executed because Unity is busy running 'compile'. Retry after the running tool completes."
if envelope.Message != expectedMessage {
t.Fatalf("message mismatch: %#v", envelope)
}
if bytes.Contains(stderr.Bytes(), []byte("Success")) || bytes.Contains(stderr.Bytes(), []byte("errorCode")) {
t.Fatalf("busy output should not include error envelope fields: %s", stderr.String())
}
}

func TestWriteToolFailureClassifiesDispatchedDisconnect(t *testing.T) {
Expand Down
10 changes: 10 additions & 0 deletions Packages/src/Cli~/internal/cli/string_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package cli

func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
Loading