From c3defb6351b736bd42d382e4d374892abd6b2c7b Mon Sep 17 00:00:00 2001 From: EEparker Date: Thu, 13 Jun 2019 11:13:38 -0500 Subject: [PATCH] Adding a utility to kill a process by PID. 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. --- src/VueCliMiddleware.Tests/PidUtilsTests.cs | 15 ++ src/VueCliMiddleware/Util/Internals.cs | 2 +- src/VueCliMiddleware/Util/KillPort.cs | 205 ++++++++++++++++++ src/VueCliMiddleware/Util/ScriptRunner.cs | 24 +- .../VueDevelopmentServerMiddleware.cs | 13 +- ...ueDevelopmentServerMiddlewareExtensions.cs | 4 +- 6 files changed, 254 insertions(+), 9 deletions(-) create mode 100644 src/VueCliMiddleware.Tests/PidUtilsTests.cs create mode 100644 src/VueCliMiddleware/Util/KillPort.cs diff --git a/src/VueCliMiddleware.Tests/PidUtilsTests.cs b/src/VueCliMiddleware.Tests/PidUtilsTests.cs new file mode 100644 index 0000000..a888d38 --- /dev/null +++ b/src/VueCliMiddleware.Tests/PidUtilsTests.cs @@ -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); + } + } +} diff --git a/src/VueCliMiddleware/Util/Internals.cs b/src/VueCliMiddleware/Util/Internals.cs index 6aa57fd..2e386e6 100644 --- a/src/VueCliMiddleware/Util/Internals.cs +++ b/src/VueCliMiddleware/Util/Internals.cs @@ -77,7 +77,7 @@ public static int FindAvailablePort() } } - /// + /// /// Wraps a to expose an evented API, issuing notifications /// when the stream emits partial lines, completed lines, or finally closes. /// diff --git a/src/VueCliMiddleware/Util/KillPort.cs b/src/VueCliMiddleware/Util/KillPort.cs new file mode 100644 index 0000000..7d63af8 --- /dev/null +++ b/src/VueCliMiddleware/Util/KillPort.cs @@ -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 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 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(); + 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(); + 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(); + 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); + + } + + +} \ No newline at end of file diff --git a/src/VueCliMiddleware/Util/ScriptRunner.cs b/src/VueCliMiddleware/Util/ScriptRunner.cs index a2d4cf3..6ba0bbc 100644 --- a/src/VueCliMiddleware/Util/ScriptRunner.cs +++ b/src/VueCliMiddleware/Util/ScriptRunner.cs @@ -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 envVars, ScriptRunnerType runner) { if (string.IsNullOrEmpty(workingDirectory)) @@ -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) @@ -85,7 +95,9 @@ 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); } }; @@ -93,7 +105,9 @@ public void AttachToLogger(ILogger logger) { 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); } }; diff --git a/src/VueCliMiddleware/VueDevelopmentServerMiddleware.cs b/src/VueCliMiddleware/VueDevelopmentServerMiddleware.cs index c56df40..978b0f1 100644 --- a/src/VueCliMiddleware/VueDevelopmentServerMiddleware.cs +++ b/src/VueCliMiddleware/VueDevelopmentServerMiddleware.cs @@ -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)) @@ -61,7 +61,15 @@ private static async Task 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 @@ -71,6 +79,9 @@ private static async Task 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)) diff --git a/src/VueCliMiddleware/VueDevelopmentServerMiddlewareExtensions.cs b/src/VueCliMiddleware/VueDevelopmentServerMiddlewareExtensions.cs index 8c8ec36..b2e0ef7 100644 --- a/src/VueCliMiddleware/VueDevelopmentServerMiddlewareExtensions.cs +++ b/src/VueCliMiddleware/VueDevelopmentServerMiddlewareExtensions.cs @@ -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)