using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.NodeServices.Util;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.NodeServices.HostingModels
{
///
/// A specialisation of the OutOfProcessNodeInstance base class that uses HTTP to perform RPC invocations.
///
/// The Node child process starts an HTTP listener on an arbitrary available port (except where a nonzero
/// port number is specified as a constructor parameter), and signals which port was selected using the same
/// input/output-based mechanism that the base class uses to determine when the child process is ready to
/// accept RPC invocations.
///
///
internal class HttpNodeInstance : OutOfProcessNodeInstance
{
private static readonly Regex PortMessageRegex =
new Regex(@"^\[Microsoft.AspNetCore.NodeServices.HttpNodeHost:Listening on port (\d+)\]$");
private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
private readonly HttpClient _client;
private bool _disposed;
private int _portNumber;
public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0, INodeInstanceOutputLogger nodeInstanceOutputLogger = null)
: base(
EmbeddedResourceReader.Read(
typeof(HttpNodeInstance),
"/Content/Node/entrypoint-http.js"),
projectPath,
watchFileExtensions,
MakeCommandLineOptions(port),
nodeInstanceOutputLogger)
{
_client = new HttpClient();
}
private static string MakeCommandLineOptions(int port)
{
return $"--port {port}";
}
protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo)
{
var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings);
var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json");
var response = await _client.PostAsync("http://localhost:" + _portNumber, payload);
if (!response.IsSuccessStatusCode)
{
var responseErrorString = await response.Content.ReadAsStringAsync();
throw new Exception("Call to Node module failed with error: " + responseErrorString);
}
var responseContentType = response.Content.Headers.ContentType;
switch (responseContentType.MediaType)
{
case "text/plain":
// String responses can skip JSON encoding/decoding
if (typeof(T) != typeof(string))
{
throw new ArgumentException(
"Node module responded with non-JSON string. This cannot be converted to the requested generic type: " +
typeof(T).FullName);
}
var responseString = await response.Content.ReadAsStringAsync();
return (T)(object)responseString;
case "application/json":
var responseJson = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject(responseJson);
case "application/octet-stream":
// Streamed responses have to be received as System.IO.Stream instances
if (typeof(T) != typeof(Stream))
{
throw new ArgumentException(
"Node module responded with binary stream. This cannot be converted to the requested generic type: " +
typeof(T).FullName + ". Instead you must use the generic type System.IO.Stream.");
}
return (T)(object)(await response.Content.ReadAsStreamAsync());
default:
throw new InvalidOperationException("Unexpected response content type: " + responseContentType.MediaType);
}
}
protected override void OnOutputDataReceived(string outputData)
{
// Watch for "port selected" messages, and when observed, store the port number
// so we can use it when making HTTP requests. The child process will always send
// one of these messages before it sends a "ready for connections" message.
var match = _portNumber != 0 ? null : PortMessageRegex.Match(outputData);
if (match != null && match.Success)
{
_portNumber = int.Parse(match.Groups[1].Captures[0].Value);
}
else
{
base.OnOutputDataReceived(outputData);
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!_disposed)
{
if (disposing)
{
_client.Dispose();
}
_disposed = true;
}
}
}
}