mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 18:19:40 +00:00
Move file-watching logic into .NET to avoid Node's fs.watch issues on Windows (#128)
This commit is contained in:
@@ -16,9 +16,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
/// 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.
|
||||
///
|
||||
/// TODO: Remove the file-watching logic from here and centralise it in OutOfProcessNodeInstance, implementing
|
||||
/// the actual watching in .NET code (not Node), for consistency across platforms.
|
||||
/// </summary>
|
||||
/// <seealso cref="Microsoft.AspNetCore.NodeServices.HostingModels.OutOfProcessNodeInstance" />
|
||||
internal class HttpNodeInstance : OutOfProcessNodeInstance
|
||||
@@ -35,26 +32,21 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
private bool _disposed;
|
||||
private int _portNumber;
|
||||
|
||||
public HttpNodeInstance(string projectPath, int port = 0, string[] watchFileExtensions = null)
|
||||
public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0)
|
||||
: base(
|
||||
EmbeddedResourceReader.Read(
|
||||
typeof(HttpNodeInstance),
|
||||
"/Content/Node/entrypoint-http.js"),
|
||||
projectPath,
|
||||
MakeCommandLineOptions(port, watchFileExtensions))
|
||||
watchFileExtensions,
|
||||
MakeCommandLineOptions(port))
|
||||
{
|
||||
_client = new HttpClient();
|
||||
}
|
||||
|
||||
private static string MakeCommandLineOptions(int port, string[] watchFileExtensions)
|
||||
private static string MakeCommandLineOptions(int port)
|
||||
{
|
||||
var result = "--port " + port;
|
||||
if (watchFileExtensions != null && watchFileExtensions.Length > 0)
|
||||
{
|
||||
result += " --watch " + string.Join(",", watchFileExtensions);
|
||||
}
|
||||
|
||||
return result;
|
||||
return $"--port {port}";
|
||||
}
|
||||
|
||||
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
@@ -18,17 +19,24 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
public abstract class OutOfProcessNodeInstance : INodeInstance
|
||||
{
|
||||
private const string ConnectionEstablishedMessage = "[Microsoft.AspNetCore.NodeServices:Listening]";
|
||||
private const string NeedsRestartMessage = "[Microsoft.AspNetCore.NodeServices:Restart]";
|
||||
private readonly TaskCompletionSource<object> _connectionIsReadySource = new TaskCompletionSource<object>();
|
||||
private bool _disposed;
|
||||
private readonly StringAsTempFile _entryPointScript;
|
||||
private FileSystemWatcher _fileSystemWatcher;
|
||||
private readonly Process _nodeProcess;
|
||||
private bool _nodeProcessNeedsRestart;
|
||||
private readonly string[] _watchFileExtensions;
|
||||
|
||||
public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null)
|
||||
public OutOfProcessNodeInstance(
|
||||
string entryPointScript,
|
||||
string projectPath,
|
||||
string[] watchFileExtensions,
|
||||
string commandLineArguments)
|
||||
{
|
||||
_entryPointScript = new StringAsTempFile(entryPointScript);
|
||||
_nodeProcess = LaunchNodeProcess(_entryPointScript.FileName, projectPath, commandLineArguments);
|
||||
_watchFileExtensions = watchFileExtensions;
|
||||
_fileSystemWatcher = BeginFileWatcher(projectPath);
|
||||
ConnectToInputOutputStreams();
|
||||
}
|
||||
|
||||
@@ -80,6 +88,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
if (disposing)
|
||||
{
|
||||
_entryPointScript.Dispose();
|
||||
EnsureFileSystemWatcherIsDisposed();
|
||||
}
|
||||
|
||||
// Make sure the Node process is finished
|
||||
@@ -93,6 +102,15 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureFileSystemWatcherIsDisposed()
|
||||
{
|
||||
if (_fileSystemWatcher != null)
|
||||
{
|
||||
_fileSystemWatcher.Dispose();
|
||||
_fileSystemWatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Process LaunchNodeProcess(string entryPointFilename, string projectPath, string commandLineArguments)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo("node")
|
||||
@@ -143,13 +161,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
_connectionIsReadySource.SetResult(null);
|
||||
initializationIsCompleted = true;
|
||||
}
|
||||
else if (evt.Data == NeedsRestartMessage)
|
||||
{
|
||||
// Temporarily, the file-watching logic is in Node, so look out for the
|
||||
// signal that we need to restart. This can be removed once the file-watching
|
||||
// logic is moved over to the .NET side.
|
||||
_nodeProcessNeedsRestart = true;
|
||||
}
|
||||
else if (evt.Data != null)
|
||||
{
|
||||
OnOutputDataReceived(evt.Data);
|
||||
@@ -177,6 +188,69 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
_nodeProcess.BeginErrorReadLine();
|
||||
}
|
||||
|
||||
private FileSystemWatcher BeginFileWatcher(string rootDir)
|
||||
{
|
||||
if (_watchFileExtensions == null || _watchFileExtensions.Length == 0)
|
||||
{
|
||||
// Nothing to watch
|
||||
return null;
|
||||
}
|
||||
|
||||
var watcher = new FileSystemWatcher(rootDir)
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName
|
||||
};
|
||||
watcher.Changed += OnFileChanged;
|
||||
watcher.Created += OnFileChanged;
|
||||
watcher.Deleted += OnFileChanged;
|
||||
watcher.Renamed += OnFileRenamed;
|
||||
watcher.EnableRaisingEvents = true;
|
||||
return watcher;
|
||||
}
|
||||
|
||||
private void OnFileChanged(object source, FileSystemEventArgs e)
|
||||
{
|
||||
if (IsFilenameBeingWatched(e.FullPath))
|
||||
{
|
||||
RestartDueToFileChange(e.FullPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFileRenamed(object source, RenamedEventArgs e)
|
||||
{
|
||||
if (IsFilenameBeingWatched(e.OldFullPath) || IsFilenameBeingWatched(e.FullPath))
|
||||
{
|
||||
RestartDueToFileChange(e.OldFullPath);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsFilenameBeingWatched(string fullPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var actualExtension = Path.GetExtension(fullPath) ?? string.Empty;
|
||||
return _watchFileExtensions.Any(actualExtension.Equals);
|
||||
}
|
||||
}
|
||||
|
||||
private void RestartDueToFileChange(string fullPath)
|
||||
{
|
||||
// TODO: Use proper logger
|
||||
Console.WriteLine($"Node will restart because file changed: {fullPath}");
|
||||
|
||||
_nodeProcessNeedsRestart = true;
|
||||
|
||||
// There's no need to watch for any more changes, since we're already restarting, and if the
|
||||
// restart takes some time (e.g., due to connection draining), we could end up getting duplicate
|
||||
// notifications.
|
||||
EnsureFileSystemWatcherIsDisposed();
|
||||
}
|
||||
|
||||
~OutOfProcessNodeInstance()
|
||||
{
|
||||
Dispose(false);
|
||||
|
||||
@@ -21,9 +21,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
/// The address of the pipe/socket is selected randomly here on the .NET side and sent to the child process as a
|
||||
/// command-line argument (the address space is wide enough that there's no real risk of a clash, unlike when
|
||||
/// selecting TCP port numbers).
|
||||
///
|
||||
/// TODO: Remove the file-watching logic from here and centralise it in OutOfProcessNodeInstance, implementing
|
||||
/// the actual watching in .NET code (not Node), for consistency across platforms.
|
||||
/// </summary>
|
||||
/// <seealso cref="Microsoft.AspNetCore.NodeServices.HostingModels.OutOfProcessNodeInstance" />
|
||||
internal class SocketNodeInstance : OutOfProcessNodeInstance
|
||||
@@ -38,16 +35,15 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
private StreamConnection _physicalConnection;
|
||||
private string _socketAddress;
|
||||
private VirtualConnectionClient _virtualConnectionClient;
|
||||
private readonly string[] _watchFileExtensions;
|
||||
|
||||
public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress): base(
|
||||
EmbeddedResourceReader.Read(
|
||||
typeof(SocketNodeInstance),
|
||||
"/Content/Node/entrypoint-socket.js"),
|
||||
projectPath,
|
||||
MakeNewCommandLineOptions(socketAddress, watchFileExtensions))
|
||||
watchFileExtensions,
|
||||
MakeNewCommandLineOptions(socketAddress))
|
||||
{
|
||||
_watchFileExtensions = watchFileExtensions;
|
||||
_socketAddress = socketAddress;
|
||||
}
|
||||
|
||||
@@ -188,15 +184,9 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
}
|
||||
}
|
||||
|
||||
private static string MakeNewCommandLineOptions(string listenAddress, string[] watchFileExtensions)
|
||||
private static string MakeNewCommandLineOptions(string listenAddress)
|
||||
{
|
||||
var result = "--listenAddress " + listenAddress;
|
||||
if (watchFileExtensions != null && watchFileExtensions.Length > 0)
|
||||
{
|
||||
result += " --watch " + string.Join(",", watchFileExtensions);
|
||||
}
|
||||
|
||||
return result;
|
||||
return $"--listenAddress {listenAddress}";
|
||||
}
|
||||
|
||||
#pragma warning disable 649 // These properties are populated via JSON deserialization
|
||||
|
||||
Reference in New Issue
Block a user