Skip to content

Add executable path option #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 31, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ This project uses [semantic versioning](http://semver.org/spec/v2.0.0.html). Ref
*[Semantic Versioning in Practice](https://www.jering.tech/articles/semantic-versioning-in-practice)*
for an overview of semantic versioning.

## [Unreleased](https://github.com/JeringTech/Javascript.NodeJS/compare/6.0.0-beta.2...HEAD)
## [Unreleased](https://github.com/JeringTech/Javascript.NodeJS/compare/6.0.0-beta.3...HEAD)

## [6.0.0-beta.3](https://github.com/JeringTech/Javascript.NodeJS/compare/6.0.0-beta.2...6.0.0-beta.3) - Mar 31, 2021
### Additions
- Added `NodeJSProcessOptions.ExecutablePath` option. ([#106](https://github.com/JeringTech/Javascript.NodeJS/pull/106)).

## [6.0.0-beta.2](https://github.com/JeringTech/Javascript.NodeJS/compare/6.0.0-beta.1...6.0.0-beta.2) - Feb 24, 2021
### Additions
Expand Down
3 changes: 2 additions & 1 deletion ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,8 @@ The next two sections list all available options.
#### NodeJSProcessOptions
| Option | Type | Description | Default |
| ------ | ---- | ----------- | ------- |
| ProjectPath | `string` | The base path for resolving paths of NodeJS modules on disk. If this value is `null`, whitespace or an empty string and the application is an ASP.NET Core application, project path is `IHostingEnvironment.ContentRootPath` | The current directory (value returned by `Directory.GetCurrentDirectory()`) |
| ProjectPath | `string` | The base path for resolving paths of NodeJS modules on disk. If this value is `null`, whitespace or an empty string and the application is an ASP.NET Core application, project path is `IHostingEnvironment.ContentRootPath`. | The current directory (value returned by `Directory.GetCurrentDirectory()`) |
| ExecutablePath | `string` | The value used to locate the NodeJS executable. This value may be an absolute path, a relative path, or a file name. If this value is a relative path, the executable's path is resolved relative to `Directory.GetCurrentDirectory()`. If this value is a file name, the executable's path is resolved using the path environment variable. If this value is `null`, whitespace or an empty string, it is overridden with the file name "node". | `null` |
| NodeAndV8Options | `string` | NodeJS and V8 options in the form "[NodeJS options] [V8 options]". The full list of NodeJS options can be found here: https://nodejs.org/api/cli.html#cli_options. | `null` |
| Port | `int` | The port that the server running on NodeJS will listen on. If set to 0, the OS will choose the port. | `0` |
| EnvironmentVariables | `IDictionary<string, string>` | The environment variables for the NodeJS process. The full list of NodeJS environment variables can be found here: https://nodejs.org/api/cli.html#cli_environment_variables. If this value doesn't contain an element with key "NODE_ENV" and the application is an ASP.NET Core application, an element with key "NODE_ENV" is added with value "development" if `IHostingEnvironment.EnvironmentName` is `EnvironmentName.Development` or "production" otherwise. | An Empty `IDictionary<string, string>` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public ConfigureNodeJSProcessOptions(IServiceScopeFactory serviceScopeFactory)
/// <param name="options">The target <see cref="NodeJSProcessOptions"/> to configure.</param>
public void Configure(NodeJSProcessOptions options)
{
// Set executable path if unspecified
if (string.IsNullOrWhiteSpace(options.ExecutablePath))
{
options.ExecutablePath = "node";
}

// Check whether project path already specified
bool projectPathSpecified = !string.IsNullOrWhiteSpace(options.ProjectPath);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal ProcessStartInfo CreateStartInfo(string nodeServerScript)
nodeServerScript = EscapeCommandLineArg(nodeServerScript); // TODO can we escape before embedding? Would avoid an allocation every time we start a NodeJS process.

int currentProcessPid = Process.GetCurrentProcess().Id;
var startInfo = new ProcessStartInfo("node")
var startInfo = new ProcessStartInfo(_nodeJSProcessOptions.ExecutablePath)
{
Arguments = $"{_nodeJSProcessOptions.NodeAndV8Options} -e \"{nodeServerScript}\" -- --parentPid {currentProcessPid} --port {_nodeJSProcessOptions.Port}",
UseShellExecute = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ public class NodeJSProcessOptions
/// </summary>
public string ProjectPath { get; set; } = Directory.GetCurrentDirectory();

/// <summary>
/// <para>The value used to locate the NodeJS executable.</para>
/// <para>This value may be an absolute path, a relative path, or a file name.</para>
/// <para>If this value is a relative path, the executable's path is resolved relative to <see cref="Directory.GetCurrentDirectory"/>.</para>
/// <para>If this value is a file name, the executable's path is resolved using the path environment variable.</para>
/// <para>If this value is <c>null</c>, whitespace or an empty string, it is overridden with the file name "node".</para>
/// <para>Defaults to <c>null</c>.</para>
/// </summary>
public string ExecutablePath { get; set; }

/// <summary>
/// <para>NodeJS and V8 options in the form [NodeJS options] [V8 options].</para>
/// <para>The full list of NodeJS options can be found here: https://nodejs.org/api/cli.html#cli_options.</para>
Expand Down
31 changes: 31 additions & 0 deletions test/NodeJS/ConfigureNodeJSProcessOptionsUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,37 @@ public class ConfigureNodeJSProcessOptionsUnitTests
{
private readonly MockRepository _mockRepository = new MockRepository(MockBehavior.Default);

[Theory]
[MemberData(nameof(Configure_SetsExecutablePathIfItIsNullWhitespaceOrAnEmptyString_Data))]
public void Configure_SetsExecutablePathIfItIsNullWhitespaceOrAnEmptyString(string dummyExecutablePath)
{
// Arrange
Mock<IServiceProvider> mockServiceProvider = _mockRepository.Create<IServiceProvider>();
mockServiceProvider.Setup(s => s.GetService(typeof(IHostingEnvironment))).Returns(null); // Called by the extension method GetService<T>
Mock<IServiceScope> mockServiceScope = _mockRepository.Create<IServiceScope>();
mockServiceScope.Setup(s => s.ServiceProvider).Returns(mockServiceProvider.Object);
Mock<IServiceScopeFactory> mockServiceScopeFactory = _mockRepository.Create<IServiceScopeFactory>();
mockServiceScopeFactory.Setup(s => s.CreateScope()).Returns(mockServiceScope.Object);
var dummyOptions = new NodeJSProcessOptions { ExecutablePath = dummyExecutablePath };
ConfigureNodeJSProcessOptions testSubject = CreateConfigureNodeJSProcessOptions(mockServiceScopeFactory.Object);

// Act
testSubject.Configure(dummyOptions);

// AssertS
Assert.Equal("node", dummyOptions.ExecutablePath);
}

public static IEnumerable<object[]> Configure_SetsExecutablePathIfItIsNullWhitespaceOrAnEmptyString_Data()
{
return new object[][]
{
new object[]{null},
new object[]{" "},
new object[]{string.Empty}
};
}

[Fact]
public void Configure_DoesNothingIfProjectPathAndNodeEnvAlreadySpecified()
{
Expand Down
109 changes: 109 additions & 0 deletions test/NodeJS/HttpNodeJSServiceIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
Expand Down Expand Up @@ -45,9 +46,60 @@ public HttpNodeJSServiceIntegrationTests(ITestOutputHelper testOutputHelper)
_testOutputHelper = testOutputHelper;
}

[Fact(Timeout = TIMEOUT_MS)]
public async void ExecutablePath_CanBeAbsolute()
{
// Arrange
const string dummyArg = "success";
string absoluteExecutablePath = GetNodeAbsolutePath();
Assert.NotNull(absoluteExecutablePath); // Node executable must be present on test machine
HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath, executablePath: absoluteExecutablePath);

// Act
DummyResult result = await testSubject.
InvokeFromFileAsync<DummyResult>(DUMMY_RETURNS_ARG_MODULE_FILE, args: new[] { dummyArg }).ConfigureAwait(false);

// Assert
Assert.Equal(dummyArg, result.Result);
}

[Fact(Timeout = TIMEOUT_MS)]
public async void ExecutablePath_CanBeRelative()
{
// Arrange
const string dummyArg = "success";
string absoluteExecutablePath = GetNodeAbsolutePath();
Assert.NotNull(absoluteExecutablePath); // Node executable must be present on test machine
string relativeExecutablePath = GetRelativePath(Directory.GetCurrentDirectory(), absoluteExecutablePath);
HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath, executablePath: relativeExecutablePath);

// Act
DummyResult result = await testSubject.
InvokeFromFileAsync<DummyResult>(DUMMY_RETURNS_ARG_MODULE_FILE, args: new[] { dummyArg }).ConfigureAwait(false);

// Assert
Assert.Equal(dummyArg, result.Result);
}

[Fact(Timeout = TIMEOUT_MS)]
public async void ExecutablePath_CanBeAFileName()
{
// Arrange
const string dummyArg = "success";
HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath, executablePath: "node");

// Act
DummyResult result = await testSubject.
InvokeFromFileAsync<DummyResult>(DUMMY_RETURNS_ARG_MODULE_FILE, args: new[] { dummyArg }).ConfigureAwait(false);

// Assert
Assert.Equal(dummyArg, result.Result);
}

[Fact(Timeout = TIMEOUT_MS)]
public async void InvokeFromFileAsync_WithTypeParameter_InvokesFromFile()
{
// Arrange
const string dummyArg = "success";
HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath);

Expand Down Expand Up @@ -1115,6 +1167,7 @@ public async void NewProcessRetries_RetriesFailedInvocationInNewProcess()
/// </summary>
private HttpNodeJSService CreateHttpNodeJSService(StringBuilder loggerStringBuilder = default,
string projectPath = default,
string executablePath = default,
ServiceCollection services = default)
{
services ??= new ServiceCollection();
Expand All @@ -1123,6 +1176,10 @@ private HttpNodeJSService CreateHttpNodeJSService(StringBuilder loggerStringBuil
{
services.Configure<NodeJSProcessOptions>(options => options.ProjectPath = projectPath);
}
if (executablePath != null)
{
services.Configure<NodeJSProcessOptions>(options => options.ExecutablePath = executablePath);
}
services.AddLogging(lb =>
{
lb.
Expand Down Expand Up @@ -1192,5 +1249,57 @@ private void TryDeleteWatchDirectory()
// Do nothing
}
}

private string GetNodeAbsolutePath()
{
#if NETCOREAPP3_1 || NET5_0
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return GetNodeAbsolutePathCore("Path", ';', "node.exe");
}
else
{
return GetNodeAbsolutePathCore("PATH", ':', "node");
}
#else
// net461
return GetNodeAbsolutePathCore("Path", ';', "node.exe");
#endif
}

private string GetNodeAbsolutePathCore(string environmentVariableName, char pathSeparator, string executableFile)
{
string pathEnvironmentVariable = Environment.GetEnvironmentVariable(environmentVariableName);
string[] directories = pathEnvironmentVariable.Split(new char[] { pathSeparator }, StringSplitOptions.RemoveEmptyEntries);

foreach (string directory in directories)
{
string nodeAbsolutePath = Path.Combine(directory, executableFile);
if (File.Exists(nodeAbsolutePath))
{
return nodeAbsolutePath;
}
}

return null;
}

private string GetRelativePath(string directoryRelativeTo, string path)
{
#if NETCOREAPP3_1 || NET5_0
return Path.GetRelativePath(directoryRelativeTo, path);
#else

// net461 doesn't support GetRelativePath. Make sure directory ends with '/'.
if (!directoryRelativeTo.EndsWith("/") && !directoryRelativeTo.EndsWith("\\"))
{
directoryRelativeTo += "/";
}
var relativeToUri = new Uri(directoryRelativeTo);
var pathUri = new Uri(path);
Uri relativeUri = relativeToUri.MakeRelativeUri(pathUri);
return Uri.UnescapeDataString(relativeUri.ToString());
#endif
}
}
}
1 change: 1 addition & 0 deletions test/NodeJS/NodeJSProcessUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ private Process CreateProcess()
{
var dummyNodeJSProcessFactory = new NodeJSProcessFactory(null);
ProcessStartInfo dummyProcessStartInfo = dummyNodeJSProcessFactory.CreateStartInfo(_dummyLongRunningScript);
dummyProcessStartInfo.FileName = "node";

return dummyNodeJSProcessFactory.CreateProcess(dummyProcessStartInfo);
}
Expand Down