11using System . Collections . Concurrent ;
22using System . ComponentModel ;
33using System . Diagnostics ;
4+ using System . Text ;
45using System . Text . Json ;
56using System . Text . RegularExpressions ;
67using AIShell . Abstraction ;
@@ -9,11 +10,14 @@ namespace Microsoft.Azure.Agent;
910
1011internal class DataRetriever : IDisposable
1112{
13+ private const string MetadataQueryTemplate = "{{\" command\" :\" {0}\" }}" ;
14+ private const string MetadataEndpoint = "https://cli-validation-tool-meta-qry.azurewebsites.net/api/command_metadata" ;
15+
1216 private static readonly Dictionary < string , NamingRule > s_azNamingRules ;
13- private static readonly ConcurrentDictionary < string , Command > s_azStaticDataCache ;
17+ private static readonly ConcurrentDictionary < string , AzCLICommand > s_azStaticDataCache ;
1418
15- private readonly string _staticDataRoot ;
1619 private readonly Task _rootTask ;
20+ private readonly HttpClient _httpClient ;
1721 private readonly SemaphoreSlim _semaphore ;
1822 private readonly List < ArgumentPair > _placeholders ;
1923 private readonly Dictionary < string , ArgumentPair > _placeholderMap ;
@@ -302,11 +306,11 @@ static DataRetriever()
302306 s_azStaticDataCache = new ( StringComparer . OrdinalIgnoreCase ) ;
303307 }
304308
305- internal DataRetriever ( ResponseData data )
309+ internal DataRetriever ( ResponseData data , HttpClient httpClient )
306310 {
307311 _stop = false ;
312+ _httpClient = httpClient ;
308313 _semaphore = new SemaphoreSlim ( 3 , 3 ) ;
309- _staticDataRoot = @"E:\yard\tmp\az-cli-out\az" ;
310314 _placeholders = new ( capacity : data . PlaceholderSet . Count ) ;
311315 _placeholderMap = new ( capacity : data . PlaceholderSet . Count ) ;
312316
@@ -453,31 +457,23 @@ private ArgumentInfo CreateArgInfo(ArgumentPair pair)
453457 private List < string > GetArgValues ( ArgumentPair pair )
454458 {
455459 // First, try to get static argument values if they exist.
460+ bool hasCompleter = true ;
456461 string command = pair . Command ;
457- if ( ! s_azStaticDataCache . TryGetValue ( command , out Command commandData ) )
462+
463+ AzCLICommand commandData = s_azStaticDataCache . GetOrAdd ( command , QueryForMetadata ) ;
464+ AzCLIParameter param = commandData ? . FindParameter ( pair . Parameter ) ;
465+
466+ if ( param is not null )
458467 {
459- string [ ] cmdElements = command . Split ( ' ' , StringSplitOptions . RemoveEmptyEntries ) ;
460- string dirPath = _staticDataRoot ;
461- for ( int i = 1 ; i < cmdElements . Length - 1 ; i ++ )
468+ if ( param . Choices ? . Count > 0 )
462469 {
463- dirPath = Path . Combine ( dirPath , cmdElements [ i ] ) ;
470+ return param . Choices ;
464471 }
465472
466- string filePath = Path . Combine ( dirPath , cmdElements [ ^ 1 ] + ".json" ) ;
467- commandData = File . Exists ( filePath )
468- ? JsonSerializer . Deserialize < Command > ( File . OpenRead ( filePath ) )
469- : null ;
470- s_azStaticDataCache . TryAdd ( command , commandData ) ;
471- }
472-
473- Option option = commandData ? . FindOption ( pair . Parameter ) ;
474- List < string > staticValues = option ? . Arguments ;
475- if ( staticValues ? . Count > 0 )
476- {
477- return staticValues ;
473+ hasCompleter = param . HasCompleter ;
478474 }
479475
480- if ( _stop ) { return null ; }
476+ if ( _stop || ! hasCompleter ) { return null ; }
481477
482478 // Then, try to get dynamic argument values using AzCLI tab completion.
483479 string commandLine = $ "{ pair . Command } { pair . Parameter } ";
@@ -551,6 +547,42 @@ private List<string> GetArgValues(ArgumentPair pair)
551547 }
552548 }
553549
550+ private AzCLICommand QueryForMetadata ( string azCommand )
551+ {
552+ AzCLICommand command = null ;
553+ var reqBody = new StringContent ( string . Format ( MetadataQueryTemplate , azCommand ) , Encoding . UTF8 , Utils . JsonContentType ) ;
554+ var request = new HttpRequestMessage ( HttpMethod . Get , MetadataEndpoint ) { Content = reqBody } ;
555+
556+ try
557+ {
558+ using var cts = new CancellationTokenSource ( 1200 ) ;
559+ var response = _httpClient . Send ( request , HttpCompletionOption . ResponseHeadersRead , cts . Token ) ;
560+
561+ if ( response . IsSuccessStatusCode )
562+ {
563+ using Stream stream = response . Content . ReadAsStream ( cts . Token ) ;
564+ using JsonDocument document = JsonDocument . Parse ( stream ) ;
565+
566+ JsonElement root = document . RootElement ;
567+ if ( root . TryGetProperty ( "data" , out JsonElement data ) &&
568+ data . TryGetProperty ( "metadata" , out JsonElement metadata ) )
569+ {
570+ command = metadata . Deserialize < AzCLICommand > ( Utils . JsonOptions ) ;
571+ }
572+ }
573+ else
574+ {
575+ // TODO: telemetry.
576+ }
577+ }
578+ catch ( Exception )
579+ {
580+ // TODO: telemetry.
581+ }
582+
583+ return command ;
584+ }
585+
554586 internal ( string command , string parameter ) GetMappedCommand ( string placeholderName )
555587 {
556588 if ( _placeholderMap . TryGetValue ( placeholderName , out ArgumentPair pair ) )
@@ -585,6 +617,22 @@ public void Dispose()
585617 _rootTask . Wait ( ) ;
586618 _semaphore . Dispose ( ) ;
587619 }
620+
621+ internal static void WarmUpMetadataService ( HttpClient httpClient )
622+ {
623+ // Send a request to the AzCLI metadata service to warm up the service (code start is slow).
624+ // We query for the command 'az sql server list' which only has 2 parameters,
625+ // so it should cause minimum processing on the server side.
626+ HttpRequestMessage request = new ( HttpMethod . Get , MetadataEndpoint )
627+ {
628+ Content = new StringContent (
629+ "{\" command\" :\" az sql server list\" }" ,
630+ Encoding . UTF8 ,
631+ Utils . JsonContentType )
632+ } ;
633+
634+ _ = httpClient . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead ) ;
635+ }
588636}
589637
590638internal class ArgumentPair
@@ -703,83 +751,3 @@ internal bool TryMatchName(string name, out string prodName, out string envName)
703751 return false ;
704752 }
705753}
706-
707- public class Option
708- {
709- public string Name { get ; }
710- public string [ ] Alias { get ; }
711- public string [ ] Short { get ; }
712- public string Attribute { get ; }
713- public string Description { get ; set ; }
714- public List < string > Arguments { get ; set ; }
715-
716- public Option ( string name , string description , string [ ] alias , string [ ] @short , string attribute , List < string > arguments )
717- {
718- ArgumentException . ThrowIfNullOrEmpty ( name ) ;
719- ArgumentException . ThrowIfNullOrEmpty ( description ) ;
720-
721- Name = name ;
722- Alias = alias ;
723- Short = @short ;
724- Attribute = attribute ;
725- Description = description ;
726- Arguments = arguments ;
727- }
728- }
729-
730- public sealed class Command
731- {
732- public List < Option > Options { get ; }
733- public string Examples { get ; }
734- public string Name { get ; }
735- public string Description { get ; }
736-
737- public Command ( string name , string description , List < Option > options , string examples )
738- {
739- ArgumentException . ThrowIfNullOrEmpty ( name ) ;
740- ArgumentException . ThrowIfNullOrEmpty ( description ) ;
741- ArgumentNullException . ThrowIfNull ( options ) ;
742-
743- Options = options ;
744- Examples = examples ;
745- Name = name ;
746- Description = description ;
747- }
748-
749- public Option FindOption ( string name )
750- {
751- foreach ( Option option in Options )
752- {
753- if ( name . StartsWith ( "--" ) )
754- {
755- if ( string . Equals ( option . Name , name , StringComparison . OrdinalIgnoreCase ) )
756- {
757- return option ;
758- }
759-
760- if ( option . Alias is not null )
761- {
762- foreach ( string alias in option . Alias )
763- {
764- if ( string . Equals ( alias , name , StringComparison . OrdinalIgnoreCase ) )
765- {
766- return option ;
767- }
768- }
769- }
770- }
771- else if ( option . Short is not null )
772- {
773- foreach ( string s in option . Short )
774- {
775- if ( string . Equals ( s , name , StringComparison . OrdinalIgnoreCase ) )
776- {
777- return option ;
778- }
779- }
780- }
781- }
782-
783- return null ;
784- }
785- }
0 commit comments