using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Jering.Javascript.NodeJS;
using JavaScriptEngineSwitcher.Core;
using JavaScriptEngineSwitcher.Core.Constants;
using JavaScriptEngineSwitcher.Core.Helpers;
using JavaScriptEngineSwitcher.Core.Utilities;
using CoreStrings = JavaScriptEngineSwitcher.Core.Resources.Strings;
using WrapperCompilationException = JavaScriptEngineSwitcher.Core.JsCompilationException;
using WrapperException = JavaScriptEngineSwitcher.Core.JsException;
using WrapperRuntimeException = JavaScriptEngineSwitcher.Core.JsRuntimeException;
using WrapperScriptException = JavaScriptEngineSwitcher.Core.JsScriptException;
using WrapperTimeoutException = JavaScriptEngineSwitcher.Core.JsTimeoutException;
using WrapperUsageException = JavaScriptEngineSwitcher.Core.JsUsageException;
using JavaScriptEngineSwitcher.Node.Helpers;
namespace JavaScriptEngineSwitcher.Node
{
///
/// Adapter for the Node JS engine
///
public sealed class NodeJsEngine : JsEngineBase
{
///
/// Name of resource, which contains a JS engine helpers
///
private const string ENGINE_HELPERS_RESOURCE_NAME = "JavaScriptEngineSwitcher.Node.Resources.engine-helpers.js";
///
/// Name of JS engine
///
public const string EngineName = "NodeJsEngine";
///
/// Node JS service
///
private INodeJSService _jsService;
///
/// Version of original JS engine
///
private string _engineVersion = "0.0.0";
///
/// JS engine identifier
///
private string _engineId;
///
/// Number of milliseconds to wait before the script execution times out
///
private int _executionTimeout = -1;
///
/// Unique document name manager
///
private UniqueDocumentNameManager _documentNameManager = new UniqueDocumentNameManager(DefaultDocumentName);
///
/// Regular expression for working with the timeout error message
///
private static readonly Regex _timeoutErrorMessage = new Regex(@"^(?:Script execution|The Node invocation) " +
@"timed out after \d+ms");
///
/// Regular expression for working with the error details
///
private static readonly Regex _errorDetailsRegex = new Regex(@"^(?[^\r\n]+)(?:\r\n|\n|\r)" +
@"(?:" +
@"(?" + CommonRegExps.DocumentNamePattern + @")?:(?\d+)(?:\r\n|\n|\r)" +
@"(?[^\r\n]+)(?:\r\n|\n|\r)" +
@"(?[ \t]*\^)(?:\r\n|\n|\r){2}" +
@")?");
///
/// Regular expression for working with the error message with type
///
private static readonly Regex _errorMessageWithTypeRegex =
new Regex(@"^(?" + CommonRegExps.JsFullNamePattern + @"): (?[^\r\n]+)");
///
/// Constructs an instance of adapter for the Node JS engine
///
public NodeJsEngine()
: this(DefaultNodeJsService.Instance, new NodeSettings())
{ }
///
/// Constructs an instance of adapter for the Node JS engine
///
/// Node JS service
public NodeJsEngine(INodeJSService nodeJsService)
: this(nodeJsService, new NodeSettings())
{ }
///
/// Constructs an instance of adapter for the Node JS engine
///
/// Settings of the Node JS engine
public NodeJsEngine(NodeSettings settings)
: this(DefaultNodeJsService.Instance, settings)
{ }
///
/// Constructs an instance of adapter for the Node JS engine
///
/// Node JS service
/// Settings of the Node JS engine
public NodeJsEngine(INodeJSService service, NodeSettings settings)
{
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
_jsService = service;
try
{
Task versionTask = _jsService.InvokeFromStringAsync(
@"module.exports = (callback) => {
let version = process.versions.node;
callback(null , version);
};"
);
_engineVersion = versionTask.ConfigureAwait(false).GetAwaiter().GetResult();
}
catch (Exception e)
{
throw JsErrorHelpers.WrapEngineLoadException(e, EngineName, _engineVersion, true);
}
NodeSettings nodeSettings = settings ?? new NodeSettings();
_executionTimeout = (int)nodeSettings.TimeoutInterval.TotalMilliseconds;
_engineId = JsEngineIdGenerator.GetNextId();
InvokeEngineHelper("addContext", [_engineId, nodeSettings.UseBuiltinLibrary]);
}
private void InvokeEngineHelper(string exportName = null, object[] args = null)
{
Task cachedTask = _jsService.TryInvokeFromCacheAsync(ENGINE_HELPERS_RESOURCE_NAME,
exportName, args);
bool success = cachedTask.ConfigureAwait(false).GetAwaiter().GetResult();
if (!success)
{
Type type = typeof(NodeJsEngine);
Assembly assembly = type.Assembly;
using (Stream resourceStream = assembly.GetManifestResourceStream(ENGINE_HELPERS_RESOURCE_NAME))
{
Task task = _jsService.InvokeFromStreamAsync(resourceStream, ENGINE_HELPERS_RESOURCE_NAME,
exportName, args);
task.ConfigureAwait(false).GetAwaiter().GetResult();
}
}
}
private T InvokeEngineHelper(string exportName = null, object[] args = null)
{
Task<(bool, T)> cachedTask = _jsService.TryInvokeFromCacheAsync(ENGINE_HELPERS_RESOURCE_NAME,
exportName, args);
(bool success, T result) = cachedTask.ConfigureAwait(false).GetAwaiter().GetResult();
if (success)
{
return result;
}
else
{
Type type = typeof(NodeJsEngine);
Assembly assembly = type.Assembly;
using (Stream resourceStream = assembly.GetManifestResourceStream(ENGINE_HELPERS_RESOURCE_NAME))
{
Task task = _jsService.InvokeFromStreamAsync(resourceStream, ENGINE_HELPERS_RESOURCE_NAME,
exportName, args);
return task.ConfigureAwait(false).GetAwaiter().GetResult();
}
}
}
#region Mapping
///
/// Makes a mapping of value from the host type to a script type
///
/// The source value
/// The mapped value
private static object MapToScriptType(object value)
{
if (value is Undefined)
{
return null;
}
return value;
}
///
/// Makes a mapping of array items from the host type to a script type
///
/// The source array
/// The mapped array
public static object[] MapToScriptType(object[] args)
{
return args.Select(arg => MapToScriptType(arg)).ToArray();
}
#region Mapping
private WrapperException WrapInvocationException(InvocationException originalException)
{
WrapperException wrapperException;
string message = originalException.Message;
string description = string.Empty;
if (_timeoutErrorMessage.IsMatch(message))
{
message = CoreStrings.Runtime_ScriptTimeoutExceeded;
description = message;
var wrapperTimeoutException = new WrapperTimeoutException(message, EngineName, _engineVersion,
originalException)
{
Description = description
};
return wrapperTimeoutException;
}
string documentName = string.Empty;
int lineNumber = 0;
int columnNumber = 0;
string sourceLine = string.Empty;
Match detailsMatch = _errorDetailsRegex.Match(message);
int detailsLength = 0;
if (detailsMatch.Success)
{
GroupCollection detailsGroups = detailsMatch.Groups;
description = detailsGroups["description"].Value;
documentName = detailsGroups["documentName"].Success ?
detailsGroups["documentName"].Value : string.Empty;
lineNumber = detailsGroups["lineNumber"].Success ?
int.Parse(detailsGroups["lineNumber"].Value) : 0;
columnNumber = NodeJsErrorHelpers.GetColumnCountFromLine(detailsGroups["pointer"].Value);
sourceLine = detailsGroups["sourceLine"].Value;
detailsLength = detailsMatch.Length;
}
message = detailsLength > 0 ? message.Substring(detailsLength) : message;
Match messageWithTypeMatch = _errorMessageWithTypeRegex.Match(message);
if (messageWithTypeMatch.Success)
{
GroupCollection messageWithTypeGroups = messageWithTypeMatch.Groups;
string type = messageWithTypeGroups["type"].Value;
description = messageWithTypeGroups["description"].Value;
string sourceFragment = TextHelpers.GetTextFragmentFromLine(sourceLine, columnNumber);
WrapperScriptException wrapperScriptException;
if (type == JsErrorType.Syntax)
{
message = JsErrorHelpers.GenerateScriptErrorMessage(type, description, documentName,
lineNumber, columnNumber, sourceFragment);
wrapperScriptException = new WrapperCompilationException(message, EngineName, _engineVersion,
originalException);
}
else if (type == "UsageError")
{
wrapperException = new WrapperUsageException(description, EngineName, _engineVersion,
originalException);
wrapperException.Description = description;
return wrapperException;
}
else
{
ErrorLocationItem[] errorLocationItems = [];
int messageLength = message.Length;
int messageWithTypeLength = messageWithTypeMatch.Length;
if (messageWithTypeLength < messageLength)
{
string errorLocation = message.Substring(messageWithTypeLength);
errorLocationItems = NodeJsErrorHelpers.ParseErrorLocation(errorLocation);
errorLocationItems = NodeJsErrorHelpers.FilterErrorLocationItems(errorLocationItems);
if (errorLocationItems.Length > 0)
{
ErrorLocationItem firstErrorLocationItem = errorLocationItems[0];
documentName = firstErrorLocationItem.DocumentName;
lineNumber = firstErrorLocationItem.LineNumber;
columnNumber = firstErrorLocationItem.ColumnNumber;
firstErrorLocationItem.SourceFragment = sourceFragment;
}
}
string callStack = JsErrorHelpers.StringifyErrorLocationItems(errorLocationItems, true);
string callStackWithSourceFragment = JsErrorHelpers.StringifyErrorLocationItems(
errorLocationItems);
message = JsErrorHelpers.GenerateScriptErrorMessage(type, description,
callStackWithSourceFragment);
wrapperScriptException = new WrapperRuntimeException(message, EngineName, _engineVersion,
originalException)
{
CallStack = callStack
};
}
wrapperScriptException.Type = type;
wrapperScriptException.DocumentName = documentName;
wrapperScriptException.LineNumber = lineNumber;
wrapperScriptException.ColumnNumber = columnNumber;
wrapperScriptException.SourceFragment = sourceFragment;
wrapperException = wrapperScriptException;
}
else
{
wrapperException = new WrapperException(message, EngineName, _engineVersion,
originalException);
}
wrapperException.Description = description;
return wrapperException;
}
#endregion
#endregion
#region JsEngineBase overrides
protected override IPrecompiledScript InnerPrecompile(string code)
{
throw new NotSupportedException();
}
protected override IPrecompiledScript InnerPrecompile(string code, string documentName)
{
throw new NotSupportedException();
}
protected override object InnerEvaluate(string expression)
{
throw new NotSupportedException();
}
protected override object InnerEvaluate(string expression, string documentName)
{
throw new NotSupportedException();
}
protected override T InnerEvaluate(string expression)
{
return InnerEvaluate(expression, null);
}
protected override T InnerEvaluate(string expression, string documentName)
{
string uniqueDocumentName = _documentNameManager.GetUniqueName(documentName);
T result;
try
{
result = InvokeEngineHelper("evaluate", [_engineId, expression, uniqueDocumentName,
_executionTimeout]);
}
catch (InvocationException e)
{
throw WrapInvocationException(e);
}
return result;
}
protected override void InnerExecute(string code)
{
InnerExecute(code, null);
}
protected override void InnerExecute(string code, string documentName)
{
string uniqueDocumentName = _documentNameManager.GetUniqueName(documentName);
try
{
InvokeEngineHelper("execute", [_engineId, code, uniqueDocumentName, _executionTimeout]);
}
catch (InvocationException e)
{
throw WrapInvocationException(e);
}
}
protected override void InnerExecute(IPrecompiledScript precompiledScript)
{
throw new NotSupportedException();
}
protected override object InnerCallFunction(string functionName, params object[] args)
{
throw new NotSupportedException();
}
protected override T InnerCallFunction(string functionName, params object[] args)
{
T result;
object[] processedArgs = MapToScriptType(args);
try
{
result = InvokeEngineHelper("callFunction", [_engineId, functionName, processedArgs,
_executionTimeout]);
}
catch (InvocationException e)
{
throw WrapInvocationException(e);
}
return result;
}
protected override bool InnerHasVariable(string variableName)
{
return InvokeEngineHelper("hasVariable", [_engineId, variableName]);
}
protected override object InnerGetVariableValue(string variableName)
{
throw new NotSupportedException();
}
protected override T InnerGetVariableValue(string variableName)
{
T result;
try
{
result = InvokeEngineHelper("getVariableValue", [_engineId, variableName]);
}
catch (InvocationException e)
{
throw WrapInvocationException(e);
}
return result;
}
protected override void InnerSetVariableValue(string variableName, object value)
{
object processedValue = MapToScriptType(value);
try
{
InvokeEngineHelper("setVariableValue", [_engineId, variableName, processedValue]);
}
catch (InvocationException e)
{
throw WrapInvocationException(e);
}
}
protected override void InnerRemoveVariable(string variableName)
{
try
{
InvokeEngineHelper("removeVariable", [_engineId, variableName]);
}
catch (InvocationException e)
{
throw WrapInvocationException(e);
}
}
protected override void InnerEmbedHostObject(string itemName, object value)
{
throw new NotSupportedException();
}
protected override void InnerEmbedHostType(string itemName, Type type)
{
throw new NotSupportedException();
}
protected override void InnerInterrupt()
{
throw new NotSupportedException();
}
protected override void InnerCollectGarbage()
{
throw new NotSupportedException();
}
#region IJsEngine implementation
public override string Name
{
get { return EngineName; }
}
public override string Version
{
get { return _engineVersion; }
}
public override bool SupportsScriptPrecompilation
{
get { return false; }
}
public override bool SupportsScriptInterruption
{
get { return false; }
}
public override bool SupportsGarbageCollection
{
get { return false; }
}
#endregion
#region IDisposable implementation
public override void Dispose()
{
if (_disposedFlag.Set())
{
InvokeEngineHelper("removeContext", [_engineId]);
_jsService = null;
}
}
#endregion
#endregion
}
}