Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers;

namespace Microsoft.Agents.A365.DevTools.Cli.Services;

Expand Down Expand Up @@ -47,8 +48,9 @@ public async Task<bool> ValidateAuthenticationAsync(string? expectedSubscription
return false;
}

// Parse the account information
var accountJson = JsonDocument.Parse(result.StandardOutput);
// Clean and parse the account information
var cleanedOutput = JsonDeserializationHelper.CleanAzureCliJsonOutput(result.StandardOutput);
var accountJson = JsonDocument.Parse(cleanedOutput);
var root = accountJson.RootElement;

var subscriptionId = root.GetProperty("id").GetString() ?? string.Empty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Agents.A365.DevTools.Cli.Models;
using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers;

namespace Microsoft.Agents.A365.DevTools.Cli.Services;

Expand Down Expand Up @@ -51,7 +52,15 @@ public async Task<bool> IsLoggedInAsync()
return null;
}

var accountJson = JsonSerializer.Deserialize<JsonElement>(result.StandardOutput);
var cleanedOutput = JsonDeserializationHelper.CleanAzureCliJsonOutput(result.StandardOutput);

if (string.IsNullOrWhiteSpace(cleanedOutput))
{
_logger.LogError("Azure CLI returned empty output");
return null;
}

var accountJson = JsonSerializer.Deserialize<JsonElement>(cleanedOutput);

return new AzureAccountInfo
{
Expand Down Expand Up @@ -89,7 +98,13 @@ public async Task<List<AzureResourceGroup>> ListResourceGroupsAsync()
return new List<AzureResourceGroup>();
}

var resourceGroupsJson = JsonSerializer.Deserialize<JsonElement[]>(result.StandardOutput);
var cleanedOutput = JsonDeserializationHelper.CleanAzureCliJsonOutput(result.StandardOutput);
if (string.IsNullOrWhiteSpace(cleanedOutput))
{
return new List<AzureResourceGroup>();
}

var resourceGroupsJson = JsonSerializer.Deserialize<JsonElement[]>(cleanedOutput);

return resourceGroupsJson?.Select(rg => new AzureResourceGroup
{
Expand Down Expand Up @@ -120,7 +135,13 @@ public async Task<List<AzureAppServicePlan>> ListAppServicePlansAsync()
return new List<AzureAppServicePlan>();
}

var plansJson = JsonSerializer.Deserialize<JsonElement[]>(result.StandardOutput);
var cleanedOutput = JsonDeserializationHelper.CleanAzureCliJsonOutput(result.StandardOutput);
if (string.IsNullOrWhiteSpace(cleanedOutput))
{
return new List<AzureAppServicePlan>();
}

var plansJson = JsonSerializer.Deserialize<JsonElement[]>(cleanedOutput);

return plansJson?.Select(plan => new AzureAppServicePlan
{
Expand Down Expand Up @@ -153,7 +174,13 @@ public async Task<List<AzureLocation>> ListLocationsAsync()
return new List<AzureLocation>();
}

var locationsJson = JsonSerializer.Deserialize<JsonElement[]>(result.StandardOutput);
var cleanedOutput = JsonDeserializationHelper.CleanAzureCliJsonOutput(result.StandardOutput);
if (string.IsNullOrWhiteSpace(cleanedOutput))
{
return new List<AzureLocation>();
}

var locationsJson = JsonSerializer.Deserialize<JsonElement[]>(cleanedOutput);

return locationsJson?.Select(loc => new AzureLocation
{
Expand All @@ -170,4 +197,4 @@ public async Task<List<AzureLocation>> ListLocationsAsync()
return new List<AzureLocation>();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;
using Microsoft.Agents.A365.DevTools.Cli.Exceptions;
using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers;
using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
Expand Down Expand Up @@ -63,7 +62,8 @@ public async Task<bool> CreateEndpointWithAgentBlueprintAsync(
return false;
}

var subscriptionInfo = JsonSerializer.Deserialize<JsonElement>(subscriptionResult.StandardOutput);
var cleanedOutput = JsonDeserializationHelper.CleanAzureCliJsonOutput(subscriptionResult.StandardOutput);
var subscriptionInfo = JsonSerializer.Deserialize<JsonElement>(cleanedOutput);
var tenantId = subscriptionInfo.GetProperty("tenantId").GetString();

if (string.IsNullOrEmpty(tenantId))
Expand All @@ -88,7 +88,7 @@ public async Task<bool> CreateEndpointWithAgentBlueprintAsync(

// Determine the audience (App ID) based on the environment
var audience = ConfigConstants.GetAgent365ToolsResourceAppId(config.Environment);
authToken = await _authService.GetAccessTokenAsync(audience, tenantId);
authToken = await _authService.GetAccessTokenAsync(audience);

if (string.IsNullOrWhiteSpace(authToken))
{
Expand Down Expand Up @@ -181,7 +181,8 @@ public async Task<bool> DeleteEndpointWithAgentBlueprintAsync(
return false;
}

var subscriptionInfo = JsonSerializer.Deserialize<JsonElement>(subscriptionResult.StandardOutput);
var cleanedOutput = JsonDeserializationHelper.CleanAzureCliJsonOutput(subscriptionResult.StandardOutput);
var subscriptionInfo = JsonSerializer.Deserialize<JsonElement>(cleanedOutput);
var tenantId = subscriptionInfo.GetProperty("tenantId").GetString();

if (string.IsNullOrEmpty(tenantId))
Expand Down Expand Up @@ -211,7 +212,7 @@ public async Task<bool> DeleteEndpointWithAgentBlueprintAsync(

_logger.LogInformation("Environment: {Environment}, Audience: {Audience}", config.Environment, audience);

authToken = await _authService.GetAccessTokenAsync(audience, tenantId);
authToken = await _authService.GetAccessTokenAsync(audience);

if (string.IsNullOrWhiteSpace(authToken))
{
Expand Down Expand Up @@ -242,60 +243,20 @@ public async Task<bool> DeleteEndpointWithAgentBlueprintAsync(

if (!response.IsSuccessStatusCode)
{
_logger.LogError("Failed to call delete endpoint. Status: {Status}", response.StatusCode);

var errorContent = await response.Content.ReadAsStringAsync();

// Parse the error response to provide cleaner user-facing messages
try
{
var errorJson = JsonSerializer.Deserialize<JsonElement>(errorContent);
if (errorJson.TryGetProperty("error", out var errorMessage))
{
var error = errorMessage.GetString();
if (errorJson.TryGetProperty("details", out var detailsElement))
{
var details = detailsElement.GetString();

// Check for common error scenarios and provide cleaner messages
if (details?.Contains("not found in any resource group") == true)
{
_logger.LogError("Failed to delete bot endpoint '{EndpointName}'. Status: {Status}", endpointName, response.StatusCode);
_logger.LogError("The bot service was not found. It may have already been deleted or may not exist.");
return false;
}
}

// Generic error with cleaned up message
_logger.LogError("Failed to delete bot endpoint. Status: {Status}", response.StatusCode);
_logger.LogError("{Error}", error);
}
else
{
// Couldn't parse error, show raw response
_logger.LogError("Failed to delete bot endpoint. Status: {Status}", response.StatusCode);
_logger.LogError("Error response: {Error}", errorContent);
}
}
catch
{
// JSON parsing failed, show raw error
_logger.LogError("Failed to delete bot endpoint. Status: {Status}", response.StatusCode);
_logger.LogError("Error response: {Error}", errorContent);
}
_logger.LogError("Error response: {Error}", errorContent);

return false;
}

_logger.LogInformation("Successfully received response from delete endpoint");
return true;
}
catch (AzureAuthenticationException ex)
{
_logger.LogError("Authentication failed: {Message}", ex.IssueDescription);
return false;
}
catch (Exception ex)
{
_logger.LogError("Failed to call delete endpoint: {Message}", ex.Message);
_logger.LogError(ex, "Failed to call delete endpoint directly");
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,42 @@ public static class JsonDeserializationHelper
return null;
}
}

/// <summary>
/// Cleans JSON output from Azure CLI by removing control characters and non-JSON content.
/// Azure CLI on Windows can output control characters (like 0x0C - form feed) and warning messages
/// that need to be stripped before JSON parsing.
/// </summary>
/// <param name="output">The raw output from Azure CLI</param>
/// <returns>Cleaned JSON string ready for parsing</returns>
public static string CleanAzureCliJsonOutput(string output)
{
if (string.IsNullOrWhiteSpace(output))
{
return string.Empty;
}

// Remove control characters (0x00-0x1F except \r, \n, \t)
// These characters can appear in Azure CLI output on Windows
var cleaned = new System.Text.StringBuilder(output.Length);
foreach (char c in output)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know what is different with your tenant?

When would we get control characters?

In preview001 or ztaitest12 or testcsaa where we have been testing so far, we didn't see this. Wondering if this can have a negative impact for those tenants/users.
How do we ensure that?

{
if (c >= 32 || c == '\n' || c == '\r' || c == '\t')
{
cleaned.Append(c);
}
}

var result = cleaned.ToString().Trim();

// Find the first { or [ to locate JSON start
// This handles cases where Azure CLI outputs warnings or other text before the JSON
int jsonStart = result.IndexOfAny(new[] { '{', '[' });
if (jsonStart > 0)
{
result = result.Substring(jsonStart);
}

return result;
}
}