Stop npm task/script, after IIS Express stop

Viewed 650

So this is a funny one. I have a dotnetcore 3 project using EntityFrameworkCore and VueJS. Right. I have in my Startup.cs:Configure(app,env) a statement that looks as follows:

app.UseSpa(spa =>
{
  if (env.IsDevelopment())
    spa.Options.SourcePath = "ClientApp";
  else
    spa.Options.SourcePath = "dist";

  if (env.IsDevelopment())
    {
      spa.UseVueCli(npmScript: "serve", port: 4444, forceKill: true);
    }
});

As you can see I have manually specified the port to be 4444 if I didn't do this the application would try and run on port 8080 or 8080++ until the application would hit a free port on the machine. This is ok for when you would only run the application once and then it never crashed or Visual Studio never crashed or you'd have to stop debugging your code to manage your project filestructure.. You get my point.. In development you NEVER only just fire a command once and be done with it. Not in my experience at least.

But nonetheless this is what this setup assumes that you do, because it never stops the npm-script when the IIS Express service shuts down, but it attempts to relaunch the, in my case, VueJS application, on the same localhost port, which ensures that the connection to the relaunched application is not established correctly and thus mayhem ensues. Unless you kill the previous npm-script task through the windows terminal that is.

So my question here is - does anyone know how to stop the script from running and killing the task that keeps it alive, when I stop my IIS Express server? This is kinda baffling to me to be honest :)

The middleware class if anyone is interested :)

#region Assembly VueCliMiddleware, Version=3.1.0.0, Culture=neutral, PublicKeyToken=null
// C:\Users\<USER>\.nuget\packages\vueclimiddleware\3.1.1\lib\netcoreapp3.1\VueCliMiddleware.dll
#endregion

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.SpaServices;
using VueCliMiddleware;

namespace VueCliMiddleware
{
    public static class VueCliMiddlewareExtensions
    {
        public static IEndpointConventionBuilder MapToVueCliProxy(this IEndpointRouteBuilder endpoints, string pattern, SpaOptions options, string npmScript = "serve", int port = 8080, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = "running at", bool forceKill = false);
        public static IEndpointConventionBuilder MapToVueCliProxy(this IEndpointRouteBuilder endpoints, string pattern, string sourcePath, string npmScript = "serve", int port = 8080, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = "running at", bool forceKill = false);
        public static IEndpointConventionBuilder MapToVueCliProxy(this IEndpointRouteBuilder endpoints, SpaOptions options, string npmScript = "serve", int port = 8080, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = "running at", bool forceKill = false);
        public static IEndpointConventionBuilder MapToVueCliProxy(this IEndpointRouteBuilder endpoints, string sourcePath, string npmScript = "serve", int port = 8080, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = "running at", bool forceKill = false);
        public static void UseVueCli(this ISpaBuilder spaBuilder, string npmScript = "serve", int port = 8080, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = "running at", bool forceKill = false);
    }
}
2 Answers

Ok, Disclaimer: this is a total hack, works only if you know in advance your port (at least, It works on my machine ;) ), needs HEAVY refactoring, and I give no guaranties.

Few knows that you can attach to your application lifecycle pretty easily, in Startup class:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        lifetime.ApplicationStopping.Register(OnStopping); // <== Magic is here!
    }
    // [Blah blah, yada yada, your super cool code here]
}

private void OnStopping()
{
    // Things to do before application is stopped
}

My idea is to get the process that is listening on your known tcp port (let's say 4444) and kill it (dramatic music).

Quicker way I know to retrieve PID from port is using netstat -nao. OnStopping code will look something like this:

private void OnStopping()
{
    // Following code runs on Windows platform only, 
    // if current platform is something else, we just exit
    if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;

    Process netstat = new Process();
    netstat.StartInfo = new ProcessStartInfo
    {
        FileName = "netstat",
        Arguments = "-nao",
        RedirectStandardOutput = true
    };

    netstat.Start();
    var output = netstat.StandardOutput.ReadToEnd();
    var ports = output.Split('\n');
    var f = ports.FirstOrDefault(p => p.Contains("LISTENING") && p.Contains("127.0.0.1:4444"));
    var pid = int.Parse(f.Split(' ').Last());
    var clientServer = Process.GetProcessById(pid);
    clientServer.Kill();
    clientServer.WaitForExit();
}

Please let me know what you think, and if it works for you.

Partial answer (sort of). From https://github.com/EEParker/aspnetcore-vueclimiddleware/blob/master/src/VueCliMiddleware/VueDevelopmentServerMiddleware.cs

The following:

if (portNumber < 80)
{
  portNumber = TcpPortFinder.FindAvailablePort();
}
else
{
  // if the port we want to use is occupied, terminate the process utilizing that port.
  // this occurs when "stop" is used from the debugger and the middleware does not have the opportunity to kill the process
  PidUtils.KillPort((ushort)portNumber, forceKill);
}

You could build this locally and then trace through to see if/why this is not working as expected. It seems like the whole point of forceKill is to address this issue.

Related