diff --git a/Packages/src/Cli~/dist/darwin-amd64/uloop b/Packages/src/Cli~/dist/darwin-amd64/uloop index de27850e74..f0848e75e4 100755 Binary files a/Packages/src/Cli~/dist/darwin-amd64/uloop and b/Packages/src/Cli~/dist/darwin-amd64/uloop differ diff --git a/Packages/src/Cli~/dist/darwin-arm64/uloop b/Packages/src/Cli~/dist/darwin-arm64/uloop index 738e178e79..6c457bc930 100755 Binary files a/Packages/src/Cli~/dist/darwin-arm64/uloop and b/Packages/src/Cli~/dist/darwin-arm64/uloop differ diff --git a/Packages/src/Cli~/dist/windows-amd64/uloop.exe b/Packages/src/Cli~/dist/windows-amd64/uloop.exe index 3808b7fbef..7d685f7561 100755 Binary files a/Packages/src/Cli~/dist/windows-amd64/uloop.exe and b/Packages/src/Cli~/dist/windows-amd64/uloop.exe differ diff --git a/Packages/src/Cli~/internal/cli/busy_status.go b/Packages/src/Cli~/internal/cli/busy_status.go new file mode 100644 index 0000000000..22e8c8e415 --- /dev/null +++ b/Packages/src/Cli~/internal/cli/busy_status.go @@ -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 +} diff --git a/Packages/src/Cli~/internal/cli/error_envelope.go b/Packages/src/Cli~/internal/cli/error_envelope.go index e9dd376b99..a560e97d45 100644 --- a/Packages/src/Cli~/internal/cli/error_envelope.go +++ b/Packages/src/Cli~/internal/cli/error_envelope.go @@ -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{ @@ -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, @@ -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, @@ -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 "" -} diff --git a/Packages/src/Cli~/internal/cli/error_envelope_test.go b/Packages/src/Cli~/internal/cli/error_envelope_test.go index df13522d75..730e64e4a0 100644 --- a/Packages/src/Cli~/internal/cli/error_envelope_test.go +++ b/Packages/src/Cli~/internal/cli/error_envelope_test.go @@ -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.", @@ -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) { diff --git a/Packages/src/Cli~/internal/cli/string_helpers.go b/Packages/src/Cli~/internal/cli/string_helpers.go new file mode 100644 index 0000000000..d9e11f631b --- /dev/null +++ b/Packages/src/Cli~/internal/cli/string_helpers.go @@ -0,0 +1,10 @@ +package cli + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +}