Skip to content
This repository has been archived by the owner on Nov 6, 2023. It is now read-only.

Commit

Permalink
Adding a utility to kill a process by PID.
Browse files Browse the repository at this point in the history
This is for when "stop" is clicked in visual studio.
Stop doesn't stop the NPM process when using Kestrel.
Hosting in IIS doesn't have this issue.
  • Loading branch information
EEParker committed Jun 13, 2019
1 parent 34eba7d commit c3defb6
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 9 deletions.
15 changes: 15 additions & 0 deletions src/VueCliMiddleware.Tests/PidUtilsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace VueCliMiddleware.Tests
{
[TestClass]
public class PidUtilsTests
{
[TestMethod]
public void KillPort_8080_KillsVueServe()
{
bool success = PidUtils.KillPort(8080, true, true);
Assert.IsTrue(success);
}
}
}
2 changes: 1 addition & 1 deletion src/VueCliMiddleware/Util/Internals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public static int FindAvailablePort()
}
}

/// <summary>
/// <summary>
/// Wraps a <see cref="StreamReader"/> to expose an evented API, issuing notifications
/// when the stream emits partial lines, completed lines, or finally closes.
/// </summary>
Expand Down
205 changes: 205 additions & 0 deletions src/VueCliMiddleware/Util/KillPort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace VueCliMiddleware
{
public static class PidUtils
{

const string ssPidRegex = @"(?:^|"",|"",pid=)(\d+)";
const string portRegex = @"[^]*[.:](\\d+)$";

public static int GetPortPid(ushort port)
{
int pidOut = -1;

int portColumn = 1; // windows
int pidColumn = 4; // windows
string pidRegex = null;

List<string[]> results = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
results = RunProcessReturnOutputSplit("netstat", "-anv -p tcp");
results.AddRange(RunProcessReturnOutputSplit("netstat", "-anv -p udp"));
portColumn = 3;
pidColumn = 8;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
results = RunProcessReturnOutputSplit("ss", "-tunlp");
portColumn = 4;
pidColumn = 6;
pidRegex = ssPidRegex;
}
else
{
results = RunProcessReturnOutputSplit("netstat", "-ano");
}


foreach (var line in results)
{
if (line.Length <= portColumn || line.Length <= pidColumn) continue;
try
{
// split lines to words
var portMatch = Regex.Match(line[portColumn], $"[.:]({port})");
if (portMatch.Success)
{
int portValue = int.Parse(portMatch.Groups[1].Value);

if (pidRegex == null)
{
pidOut = int.Parse(line[pidColumn]);
return pidOut;
}
else
{
var pidMatch = Regex.Match(line[pidColumn], pidRegex);
if (pidMatch.Success)
{
pidOut = int.Parse(pidMatch.Groups[1].Value);
}
}
}
}
catch (Exception ex)
{
// ignore line error
}
}

return pidOut;
}

private static List<string[]> RunProcessReturnOutputSplit(string fileName, string arguments)
{
string result = RunProcessReturnOutput(fileName, arguments);
if (result == null) return null;

string[] lines = result.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
var lineWords = new List<string[]>();
foreach (var line in lines)
{
lineWords.Add(line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
}
return lineWords;
}

private static string RunProcessReturnOutput(string fileName, string arguments)
{
Process process = null;
try
{
var si = new ProcessStartInfo(fileName, arguments)
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

process = Process.Start(si);
var stdOutT = process.StandardOutput.ReadToEndAsync();
var stdErrorT = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(10000))
{
try { process?.Kill(); } catch { }
}

if (Task.WaitAll(new Task[] { stdOutT, stdErrorT }, 10000))
{
// if success, return data
return (stdOutT.Result + Environment.NewLine + stdErrorT.Result).Trim();
}
return null;
}
catch (Exception ex)
{
return null;
}
finally
{
process?.Close();
}
}

public static bool Kill(string process, bool ignoreCase = true, bool force = false, bool tree = true)
{
var args = new List<string>();
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
if (force) { args.Add("-9"); }
if (ignoreCase) { args.Add("-i"); }
args.Add(process);
RunProcessReturnOutput("pkill", string.Join(" ", args));
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
if (force) { args.Add("-9"); }
if (ignoreCase) { args.Add("-I"); }
args.Add(process);
RunProcessReturnOutput("killall", string.Join(" ", args));
}
else
{
if (force) { args.Add("/f"); }
if (tree) { args.Add("/T"); }
args.Add("/im");
args.Add(process);
return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false;
}
return true;
}
catch (Exception)
{

}
return false;
}

public static bool Kill(int pid, bool force = false, bool tree = true)
{
var args = new List<string>();
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
if (force) { args.Add("-9"); }
RunProcessReturnOutput("kill", "");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
if (force) { args.Add("-9"); }
RunProcessReturnOutput("kill", "");
}
else
{
if (force) { args.Add("/f"); }
if (tree) { args.Add("/T"); }
args.Add("/PID");
args.Add(pid.ToString());
return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false;
}
return true;
}
catch (Exception ex)
{
}
return false;
}

public static bool KillPort(ushort port, bool force = false, bool tree = true) => Kill(GetPortPid(port), force: force, tree: tree);

}


}
24 changes: 19 additions & 5 deletions src/VueCliMiddleware/Util/ScriptRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ internal class ScriptRunner

private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));

public Process RunnerProcess => _runnerProcess;

private Process _runnerProcess;

public void Kill()
{
try { _runnerProcess?.Kill(); } catch { }
try { _runnerProcess?.WaitForExit(); } catch { }
}

public ScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars, ScriptRunnerType runner)
{
if (string.IsNullOrEmpty(workingDirectory))
Expand Down Expand Up @@ -71,9 +81,9 @@ public ScriptRunner(string workingDirectory, string scriptName, string arguments
}
}

var process = LaunchNodeProcess(processStartInfo);
StdOut = new EventedStreamReader(process.StandardOutput);
StdErr = new EventedStreamReader(process.StandardError);
_runnerProcess = LaunchNodeProcess(processStartInfo);
StdOut = new EventedStreamReader(_runnerProcess.StandardOutput);
StdErr = new EventedStreamReader(_runnerProcess.StandardError);
}

public void AttachToLogger(ILogger logger)
Expand All @@ -85,15 +95,19 @@ public void AttachToLogger(ILogger logger)
{
// NPM tasks commonly emit ANSI colors, but it wouldn't make sense to forward
// those to loggers (because a logger isn't necessarily any kind of terminal)
logger.LogInformation(StripAnsiColors(line) + "\r\n");
//logger.LogInformation(StripAnsiColors(line).TrimEnd('\n'));
// making this console for debug purpose
Console.Write(line);
}
};

StdErr.OnReceivedLine += line =>
{
if (!string.IsNullOrWhiteSpace(line))
{
logger.LogError(StripAnsiColors(line + "\r\n"));
//logger.LogError(StripAnsiColors(line).TrimEnd('\n'));
// making this console for debug purpose
Console.Error.Write(line);
}
};

Expand Down
13 changes: 12 additions & 1 deletion src/VueCliMiddleware/VueDevelopmentServerMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal static class VueCliMiddleware

public static void Attach(
ISpaBuilder spaBuilder,
string scriptName, int port = 0, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = DefaultRegex)
string scriptName, int port = 8080, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = DefaultRegex)
{
var sourcePath = spaBuilder.Options.SourcePath;
if (string.IsNullOrEmpty(sourcePath))
Expand Down Expand Up @@ -61,7 +61,15 @@ private static async Task<int> StartVueCliServerAsync(
string sourcePath, string npmScriptName, ILogger logger, int portNumber, ScriptRunnerType runner, string regex)
{
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);
}
logger.LogInformation($"Starting server on port {portNumber}...");

var envVars = new Dictionary<string, string>
Expand All @@ -71,6 +79,9 @@ private static async Task<int> StartVueCliServerAsync(
{ "BROWSER", "none" }, // We don't want vue-cli to open its own extra browser window pointing to the internal dev server port
};
var npmScriptRunner = new ScriptRunner(sourcePath, npmScriptName, $"--port {portNumber:0}", envVars, runner: runner);
AppDomain.CurrentDomain.DomainUnload += (s, e) => npmScriptRunner?.Kill();
AppDomain.CurrentDomain.ProcessExit += (s, e) => npmScriptRunner?.Kill();
AppDomain.CurrentDomain.UnhandledException += (s, e) => npmScriptRunner?.Kill();
npmScriptRunner.AttachToLogger(logger);

using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public static class VueCliMiddlewareExtensions
public static void UseVueCli(
this ISpaBuilder spaBuilder,
string npmScript,
int port = 0,
ScriptRunnerType runner = ScriptRunnerType.Npm,
int port = 8080,
ScriptRunnerType runner = ScriptRunnerType.Npm,
string regex = VueCliMiddleware.DefaultRegex)
{
if (spaBuilder == null)
Expand Down

0 comments on commit c3defb6

Please sign in to comment.