Skip to content

Commit a337345

Browse files
authored
Fix multi-host support (#151)
Fixes #152
1 parent c907108 commit a337345

File tree

7 files changed

+199
-110
lines changed

7 files changed

+199
-110
lines changed

CSharp/TfsCmdlets/Cmdlets/RestApi/InvokeRestApi.cs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ public class InvokeRestApi : CmdletBase
102102
[Parameter()]
103103
public string UseHost { get; set; }
104104

105+
/// <summary>
106+
/// Prevents the automatic expansion (unwrapping) of the 'value' property in the response JSON.
107+
/// </summary>
108+
[Parameter()]
109+
public SwitchParameter NoAutoUnwrap { get; set; }
110+
105111
/// <summary>
106112
/// Returns the API response as an unparsed string. If omitted, JSON responses will be
107113
/// parsed, converted and returned as objects (via ConvertFrom-Json).
@@ -156,14 +162,12 @@ protected override void DoProcessRecord()
156162

157163
if (Uri.TryCreate(Path, UriKind.Absolute, out var uri))
158164
{
159-
var host = uri.Host;
160-
161-
if (host.EndsWith(".dev.azure.com"))
165+
if(string.IsNullOrEmpty(UseHost) && !uri.Host.Equals(tpc.Uri.Host, StringComparison.OrdinalIgnoreCase))
162166
{
163-
UseHost = host;
167+
UseHost = uri.Host;
164168
}
165-
166-
Path = uri.AbsolutePath.Replace("%7Borganization%7D/", "");
169+
170+
Path = uri.AbsolutePath.Replace("/%7Borganization%7D/", "");
167171

168172
if (uri.AbsoluteUri.StartsWith(tpc.Uri.AbsoluteUri))
169173
{
@@ -202,19 +206,30 @@ protected override void DoProcessRecord()
202206

203207
this.Log($"Path '{Path}', version '{ApiVersion}'");
204208

205-
if(tpc.IsHosted && !string.IsNullOrEmpty(UseHost))
209+
string host = null;
210+
211+
if(tpc.IsHosted)
206212
{
207-
GenericHttpClient.UseHost(UseHost);
213+
if(!string.IsNullOrEmpty(UseHost))
214+
{
215+
host = UseHost;
216+
}
217+
else if (!tpc.Uri.Host.Equals("dev.azure.com", StringComparison.OrdinalIgnoreCase))
218+
{
219+
host = tpc.Uri.Host;
220+
}
208221
}
209222

210223
var client = this.GetService<IRestApiService>();
224+
211225
var task = client.InvokeAsync(tpc, Path, Method, Body,
212226
RequestContentType, ResponseContentType,
213227
AdditionalHeaders.ToDictionary<string, string>(),
214228
QueryParameters.ToDictionary<string, string>(),
215-
ApiVersion);
229+
ApiVersion,
230+
host);
216231

217-
this.Log($"{Method} {client.Uri.AbsoluteUri}");
232+
this.Log($"{Method} {client.Url.AbsoluteUri}");
218233

219234
if (AsTask)
220235
{
@@ -227,7 +242,7 @@ protected override void DoProcessRecord()
227242
var responseType = result.Content.Headers.ContentType.MediaType;
228243

229244
WriteObject(!Raw && responseType.Equals("application/json")
230-
? PSJsonConverter.Deserialize(responseBody)
245+
? PSJsonConverter.Deserialize(responseBody, (bool) NoAutoUnwrap)
231246
: responseBody);
232247
}
233248

CSharp/TfsCmdlets/Extensions/ObjectExtensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,23 @@ public static void CopyFrom(this object self, object parent)
2727
}
2828
}
2929
}
30+
31+
public static T GetHiddenField<T>(this object self, string fieldName)
32+
{
33+
var field = self.GetType().GetField(fieldName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
34+
return (T) field.GetValue(self);
35+
}
36+
37+
public static void SetHiddenField(this object self, string fieldName, object value)
38+
{
39+
var field = self.GetType().GetField(fieldName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
40+
field.SetValue(self, value);
41+
}
42+
43+
public static object CallHiddenMethod(this object self, string methodName, params object[] parameters)
44+
{
45+
var method = self.GetType().GetMethod(methodName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
46+
return method.Invoke(self, parameters);
47+
}
3048
}
3149
}

CSharp/TfsCmdlets/HttpClient/GenericHttpClient.cs

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,40 +17,38 @@ public class GenericHttpClient : VssHttpClientBase
1717
{
1818
#region Constructors and fields
1919

20-
private static string _Host;
21-
2220
/// <summary>
2321
/// Creates a new instance of the GenericHttpClient class
2422
/// </summary>
25-
public GenericHttpClient(Uri baseUrl, VssCredentials credentials) : base(SetHost(baseUrl), credentials)
23+
public GenericHttpClient(Uri baseUrl, VssCredentials credentials) : base(baseUrl, credentials)
2624
{
2725
}
2826

2927
/// <summary>
3028
/// Creates a new instance of the GenericHttpClient class
3129
/// </summary>
32-
public GenericHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings) : base(SetHost(baseUrl), credentials, settings)
30+
public GenericHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings) : base(baseUrl, credentials, settings)
3331
{
3432
}
3533

3634
/// <summary>
3735
/// Creates a new instance of the GenericHttpClient class
3836
/// </summary>
39-
public GenericHttpClient(Uri baseUrl, VssCredentials credentials, params DelegatingHandler[] handlers) : base(SetHost(baseUrl), credentials, handlers)
37+
public GenericHttpClient(Uri baseUrl, VssCredentials credentials, params DelegatingHandler[] handlers) : base(baseUrl, credentials, handlers)
4038
{
4139
}
4240

4341
/// <summary>
4442
/// Creates a new instance of the GenericHttpClient class
4543
/// </summary>
46-
public GenericHttpClient(Uri baseUrl, HttpMessageHandler pipeline, bool disposeHandler) : base(SetHost(baseUrl), pipeline, disposeHandler)
44+
public GenericHttpClient(Uri baseUrl, HttpMessageHandler pipeline, bool disposeHandler) : base(baseUrl, pipeline, disposeHandler)
4745
{
4846
}
4947

5048
/// <summary>
5149
/// Creates a new instance of the GenericHttpClient class
5250
/// </summary>
53-
public GenericHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings, params DelegatingHandler[] handlers) : base(SetHost(baseUrl), credentials, settings, handlers)
51+
public GenericHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings, params DelegatingHandler[] handlers) : base(baseUrl, credentials, settings, handlers)
5452
{
5553
}
5654

@@ -61,28 +59,6 @@ public GenericHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequest
6159
/// </summary>
6260
public Uri Uri { get; private set; }
6361

64-
/// <summary>
65-
/// Specifies an alternate host name for APIs not hosted in "dev.azure.com"
66-
/// </summary>
67-
/// <param name="host">An alternate host, such as "vsaex.dev.azure.com" or "vssps.dev.azure.com".</param>
68-
public static void UseHost(string host)
69-
{
70-
_Host = host;
71-
}
72-
73-
private static Uri SetHost(Uri baseUrl)
74-
{
75-
if (_Host == null)
76-
{
77-
return baseUrl;
78-
}
79-
80-
baseUrl = (new UriBuilder(baseUrl) { Host = _Host }).Uri;
81-
_Host = null;
82-
83-
return baseUrl;
84-
}
85-
8662
/// <summary>
8763
/// Sends a GET request to an Azure DevOps API
8864
/// </summary>
Lines changed: 56 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
using System;
2+
using System.Collections;
3+
using System.Linq;
4+
using System.Collections.Concurrent;
25
using System.Collections.Generic;
36
using System.Net.Http;
47
using System.Threading.Tasks;
58
using Microsoft.VisualStudio.Services.Operations;
9+
using Microsoft.VisualStudio.Services.WebApi;
10+
using TfsCmdlets.Extensions;
611
using TfsCmdlets.HttpClient;
712
using TfsCmdlets.Services;
813

@@ -11,7 +16,7 @@ namespace TfsCmdlets.Services
1116
public interface IRestApiService : IService
1217
{
1318
Task<HttpResponseMessage> InvokeAsync(
14-
Models.Connection connection,
19+
Models.Connection connection,
1520
string path,
1621
string method = "GET",
1722
string body = null,
@@ -23,7 +28,7 @@ Task<HttpResponseMessage> InvokeAsync(
2328
string serviceHostName = null);
2429

2530
Task<T> InvokeAsync<T>(
26-
Models.Connection connection,
31+
Models.Connection connection,
2732
string path,
2833
string method = "GET",
2934
string body = null,
@@ -35,7 +40,7 @@ Task<T> InvokeAsync<T>(
3540
string serviceHostName = null);
3641

3742
Task<OperationReference> QueueOperationAsync(
38-
Models.Connection connection,
43+
Models.Connection connection,
3944
string path,
4045
string method = "GET",
4146
string body = null,
@@ -46,17 +51,19 @@ Task<OperationReference> QueueOperationAsync(
4651
string apiVersion = "4.1",
4752
string serviceHostName = null);
4853

49-
Uri Uri { get; }
54+
Uri Url {get;}
5055
}
5156

5257
[Exports(typeof(IRestApiService))]
5358
internal class RestApiServiceImpl : BaseService, IRestApiService
5459
{
5560
private GenericHttpClient _client;
5661

57-
Uri IRestApiService.Uri => _client?.Uri;
62+
public Uri Url => _client.Uri;
5863

59-
Task<HttpResponseMessage> IRestApiService.InvokeAsync(Models.Connection connection, string path,
64+
Task<HttpResponseMessage> IRestApiService.InvokeAsync(
65+
Models.Connection connection,
66+
string path,
6067
string method,
6168
string body,
6269
string requestContentType,
@@ -66,28 +73,10 @@ Task<HttpResponseMessage> IRestApiService.InvokeAsync(Models.Connection connecti
6673
string apiVersion,
6774
string serviceHostName)
6875
{
69-
var conn = connection.InnerConnection;
70-
path = path.TrimStart('/');
71-
72-
if (!string.IsNullOrEmpty(serviceHostName))
73-
{
74-
if (!serviceHostName.Contains("."))
75-
{
76-
Logger.Log($"Converting service prefix {serviceHostName} to {serviceHostName}.dev.azure.com");
77-
serviceHostName += ".dev.azure.com";
78-
}
79-
80-
Logger.Log($"Using service host {serviceHostName}");
81-
GenericHttpClient.UseHost(serviceHostName);
82-
}
83-
84-
_client = conn.GetClient<GenericHttpClient>();
85-
86-
var task = _client.InvokeAsync(new HttpMethod(method), path, body,
87-
requestContentType, responseContentType, additionalHeaders, queryParameters,
88-
apiVersion);
89-
90-
return task;
76+
return GetClient(connection, serviceHostName)
77+
.InvokeAsync(new HttpMethod(method), path.TrimStart('/'), body,
78+
requestContentType, responseContentType, additionalHeaders, queryParameters,
79+
apiVersion);
9180
}
9281

9382
Task<T> IRestApiService.InvokeAsync<T>(
@@ -102,28 +91,10 @@ Task<T> IRestApiService.InvokeAsync<T>(
10291
string apiVersion,
10392
string serviceHostName)
10493
{
105-
var conn = connection.InnerConnection;
106-
path = path.TrimStart('/');
107-
108-
if (!string.IsNullOrEmpty(serviceHostName))
109-
{
110-
if (!serviceHostName.Contains("."))
111-
{
112-
Logger.Log($"Converting service prefix {serviceHostName} to {serviceHostName}.dev.azure.com");
113-
serviceHostName += ".dev.azure.com";
114-
}
115-
116-
Logger.Log($"Using service host {serviceHostName}");
117-
GenericHttpClient.UseHost(serviceHostName);
118-
}
119-
120-
_client = conn.GetClient<GenericHttpClient>();
121-
122-
var task = _client.InvokeAsync<T>(new HttpMethod(method), path, body,
123-
requestContentType, responseContentType, additionalHeaders, queryParameters,
124-
apiVersion);
125-
126-
return task;
94+
return GetClient(connection, serviceHostName)
95+
.InvokeAsync<T>(new HttpMethod(method), path.TrimStart('/'), body,
96+
requestContentType, responseContentType, additionalHeaders, queryParameters,
97+
apiVersion);
12798
}
12899

129100
Task<OperationReference> IRestApiService.QueueOperationAsync(
@@ -138,17 +109,41 @@ Task<OperationReference> IRestApiService.QueueOperationAsync(
138109
string apiVersion,
139110
string serviceHostName)
140111
{
141-
return ((IRestApiService)this).InvokeAsync<OperationReference>(
142-
connection,
143-
path,
144-
method,
145-
body,
146-
requestContentType,
147-
responseContentType,
148-
additionalHeaders,
149-
queryParameters,
150-
apiVersion,
151-
serviceHostName);
112+
return GetClient(connection, serviceHostName)
113+
.InvokeAsync<OperationReference>(new HttpMethod(method), path.TrimStart('/'), body,
114+
requestContentType, responseContentType, additionalHeaders,
115+
queryParameters, apiVersion);
116+
}
117+
118+
private GenericHttpClient GetClient(Models.Connection connection, string serviceHostName)
119+
{
120+
var conn = connection.InnerConnection;
121+
var host = serviceHostName ?? conn.Uri.Host;
122+
123+
if (!host.Contains("."))
124+
{
125+
Logger.Log($"Converting service prefix {serviceHostName} to {serviceHostName}.dev.azure.com");
126+
host += ".dev.azure.com";
127+
}
128+
129+
Logger.Log($"Using service host {host}");
130+
131+
var client = conn.GetClient<GenericHttpClient>();
132+
var uri = (new UriBuilder(client.BaseAddress) { Host = host }).Uri;
133+
134+
if (client.BaseAddress.Host != uri.Host)
135+
{
136+
var pipeline = conn.GetHiddenField<HttpMessageHandler>("m_pipeline");
137+
client = new GenericHttpClient(uri, pipeline, false);
138+
139+
#if NETCOREAPP3_1_OR_GREATER
140+
conn.CallHiddenMethod("RegisterClientServiceInstance", typeof(GenericHttpClient), client);
141+
#else
142+
throw new NotImplementedException("RegisterClientServiceInstance is not implemented in PS Desktop");
143+
#endif
144+
}
145+
146+
return _client = client;
152147
}
153148
}
154149
}

CSharp/TfsCmdlets/Util/PSJsonConverter.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ namespace TfsCmdlets.Util
44
{
55
internal class PSJsonConverter
66
{
7-
internal static object Deserialize(string input)
7+
internal static object Deserialize(string input, bool noAutoUnwrap)
88
{
9-
var sb = ScriptBlock.Create("ConvertFrom-Json -InputObject $args[0]");
9+
string script;
10+
11+
script = noAutoUnwrap ?
12+
"ConvertFrom-Json -InputObject $args[0]" :
13+
"$o = ConvertFrom-Json -InputObject $args[0]; if ($o.Value) {return $o.Value} else {return $o}";
14+
15+
var sb = ScriptBlock.Create(script);
1016
var jsonObject = sb.Invoke(input);
1117

1218
return jsonObject;

0 commit comments

Comments
 (0)