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

Commit c3defb6

Browse files
committed
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.
1 parent 34eba7d commit c3defb6

File tree

6 files changed

+254
-9
lines changed

6 files changed

+254
-9
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2+
3+
namespace VueCliMiddleware.Tests
4+
{
5+
[TestClass]
6+
public class PidUtilsTests
7+
{
8+
[TestMethod]
9+
public void KillPort_8080_KillsVueServe()
10+
{
11+
bool success = PidUtils.KillPort(8080, true, true);
12+
Assert.IsTrue(success);
13+
}
14+
}
15+
}

src/VueCliMiddleware/Util/Internals.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public static int FindAvailablePort()
7777
}
7878
}
7979

80-
/// <summary>
80+
/// <summary>
8181
/// Wraps a <see cref="StreamReader"/> to expose an evented API, issuing notifications
8282
/// when the stream emits partial lines, completed lines, or finally closes.
8383
/// </summary>

src/VueCliMiddleware/Util/KillPort.cs

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
using Microsoft.Extensions.Logging;
2+
using System;
3+
using System.Diagnostics;
4+
using System.Runtime.InteropServices;
5+
using System.Text.RegularExpressions;
6+
using System.Collections.Generic;
7+
using System.Threading.Tasks;
8+
9+
namespace VueCliMiddleware
10+
{
11+
public static class PidUtils
12+
{
13+
14+
const string ssPidRegex = @"(?:^|"",|"",pid=)(\d+)";
15+
const string portRegex = @"[^]*[.:](\\d+)$";
16+
17+
public static int GetPortPid(ushort port)
18+
{
19+
int pidOut = -1;
20+
21+
int portColumn = 1; // windows
22+
int pidColumn = 4; // windows
23+
string pidRegex = null;
24+
25+
List<string[]> results = null;
26+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
27+
{
28+
results = RunProcessReturnOutputSplit("netstat", "-anv -p tcp");
29+
results.AddRange(RunProcessReturnOutputSplit("netstat", "-anv -p udp"));
30+
portColumn = 3;
31+
pidColumn = 8;
32+
}
33+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
34+
{
35+
results = RunProcessReturnOutputSplit("ss", "-tunlp");
36+
portColumn = 4;
37+
pidColumn = 6;
38+
pidRegex = ssPidRegex;
39+
}
40+
else
41+
{
42+
results = RunProcessReturnOutputSplit("netstat", "-ano");
43+
}
44+
45+
46+
foreach (var line in results)
47+
{
48+
if (line.Length <= portColumn || line.Length <= pidColumn) continue;
49+
try
50+
{
51+
// split lines to words
52+
var portMatch = Regex.Match(line[portColumn], $"[.:]({port})");
53+
if (portMatch.Success)
54+
{
55+
int portValue = int.Parse(portMatch.Groups[1].Value);
56+
57+
if (pidRegex == null)
58+
{
59+
pidOut = int.Parse(line[pidColumn]);
60+
return pidOut;
61+
}
62+
else
63+
{
64+
var pidMatch = Regex.Match(line[pidColumn], pidRegex);
65+
if (pidMatch.Success)
66+
{
67+
pidOut = int.Parse(pidMatch.Groups[1].Value);
68+
}
69+
}
70+
}
71+
}
72+
catch (Exception ex)
73+
{
74+
// ignore line error
75+
}
76+
}
77+
78+
return pidOut;
79+
}
80+
81+
private static List<string[]> RunProcessReturnOutputSplit(string fileName, string arguments)
82+
{
83+
string result = RunProcessReturnOutput(fileName, arguments);
84+
if (result == null) return null;
85+
86+
string[] lines = result.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
87+
var lineWords = new List<string[]>();
88+
foreach (var line in lines)
89+
{
90+
lineWords.Add(line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
91+
}
92+
return lineWords;
93+
}
94+
95+
private static string RunProcessReturnOutput(string fileName, string arguments)
96+
{
97+
Process process = null;
98+
try
99+
{
100+
var si = new ProcessStartInfo(fileName, arguments)
101+
{
102+
UseShellExecute = false,
103+
RedirectStandardOutput = true,
104+
RedirectStandardError = true,
105+
CreateNoWindow = true
106+
};
107+
108+
process = Process.Start(si);
109+
var stdOutT = process.StandardOutput.ReadToEndAsync();
110+
var stdErrorT = process.StandardError.ReadToEndAsync();
111+
if (!process.WaitForExit(10000))
112+
{
113+
try { process?.Kill(); } catch { }
114+
}
115+
116+
if (Task.WaitAll(new Task[] { stdOutT, stdErrorT }, 10000))
117+
{
118+
// if success, return data
119+
return (stdOutT.Result + Environment.NewLine + stdErrorT.Result).Trim();
120+
}
121+
return null;
122+
}
123+
catch (Exception ex)
124+
{
125+
return null;
126+
}
127+
finally
128+
{
129+
process?.Close();
130+
}
131+
}
132+
133+
public static bool Kill(string process, bool ignoreCase = true, bool force = false, bool tree = true)
134+
{
135+
var args = new List<string>();
136+
try
137+
{
138+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
139+
{
140+
if (force) { args.Add("-9"); }
141+
if (ignoreCase) { args.Add("-i"); }
142+
args.Add(process);
143+
RunProcessReturnOutput("pkill", string.Join(" ", args));
144+
}
145+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
146+
{
147+
if (force) { args.Add("-9"); }
148+
if (ignoreCase) { args.Add("-I"); }
149+
args.Add(process);
150+
RunProcessReturnOutput("killall", string.Join(" ", args));
151+
}
152+
else
153+
{
154+
if (force) { args.Add("/f"); }
155+
if (tree) { args.Add("/T"); }
156+
args.Add("/im");
157+
args.Add(process);
158+
return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false;
159+
}
160+
return true;
161+
}
162+
catch (Exception)
163+
{
164+
165+
}
166+
return false;
167+
}
168+
169+
public static bool Kill(int pid, bool force = false, bool tree = true)
170+
{
171+
var args = new List<string>();
172+
try
173+
{
174+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
175+
{
176+
if (force) { args.Add("-9"); }
177+
RunProcessReturnOutput("kill", "");
178+
}
179+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
180+
{
181+
if (force) { args.Add("-9"); }
182+
RunProcessReturnOutput("kill", "");
183+
}
184+
else
185+
{
186+
if (force) { args.Add("/f"); }
187+
if (tree) { args.Add("/T"); }
188+
args.Add("/PID");
189+
args.Add(pid.ToString());
190+
return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false;
191+
}
192+
return true;
193+
}
194+
catch (Exception ex)
195+
{
196+
}
197+
return false;
198+
}
199+
200+
public static bool KillPort(ushort port, bool force = false, bool tree = true) => Kill(GetPortPid(port), force: force, tree: tree);
201+
202+
}
203+
204+
205+
}

src/VueCliMiddleware/Util/ScriptRunner.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ internal class ScriptRunner
2828

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

31+
public Process RunnerProcess => _runnerProcess;
32+
33+
private Process _runnerProcess;
34+
35+
public void Kill()
36+
{
37+
try { _runnerProcess?.Kill(); } catch { }
38+
try { _runnerProcess?.WaitForExit(); } catch { }
39+
}
40+
3141
public ScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars, ScriptRunnerType runner)
3242
{
3343
if (string.IsNullOrEmpty(workingDirectory))
@@ -71,9 +81,9 @@ public ScriptRunner(string workingDirectory, string scriptName, string arguments
7181
}
7282
}
7383

74-
var process = LaunchNodeProcess(processStartInfo);
75-
StdOut = new EventedStreamReader(process.StandardOutput);
76-
StdErr = new EventedStreamReader(process.StandardError);
84+
_runnerProcess = LaunchNodeProcess(processStartInfo);
85+
StdOut = new EventedStreamReader(_runnerProcess.StandardOutput);
86+
StdErr = new EventedStreamReader(_runnerProcess.StandardError);
7787
}
7888

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

92104
StdErr.OnReceivedLine += line =>
93105
{
94106
if (!string.IsNullOrWhiteSpace(line))
95107
{
96-
logger.LogError(StripAnsiColors(line + "\r\n"));
108+
//logger.LogError(StripAnsiColors(line).TrimEnd('\n'));
109+
// making this console for debug purpose
110+
Console.Error.Write(line);
97111
}
98112
};
99113

src/VueCliMiddleware/VueDevelopmentServerMiddleware.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal static class VueCliMiddleware
1818

1919
public static void Attach(
2020
ISpaBuilder spaBuilder,
21-
string scriptName, int port = 0, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = DefaultRegex)
21+
string scriptName, int port = 8080, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = DefaultRegex)
2222
{
2323
var sourcePath = spaBuilder.Options.SourcePath;
2424
if (string.IsNullOrEmpty(sourcePath))
@@ -61,7 +61,15 @@ private static async Task<int> StartVueCliServerAsync(
6161
string sourcePath, string npmScriptName, ILogger logger, int portNumber, ScriptRunnerType runner, string regex)
6262
{
6363
if (portNumber < 80)
64+
{
6465
portNumber = TcpPortFinder.FindAvailablePort();
66+
}
67+
else
68+
{
69+
// if the port we want to use is occupied, terminate the process utilizing that port.
70+
// this occurs when "stop" is used from the debugger and the middleware does not have the opportunity to kill the process
71+
PidUtils.KillPort((ushort)portNumber);
72+
}
6573
logger.LogInformation($"Starting server on port {portNumber}...");
6674

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

7687
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))

src/VueCliMiddleware/VueDevelopmentServerMiddlewareExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public static class VueCliMiddlewareExtensions
2525
public static void UseVueCli(
2626
this ISpaBuilder spaBuilder,
2727
string npmScript,
28-
int port = 0,
29-
ScriptRunnerType runner = ScriptRunnerType.Npm,
28+
int port = 8080,
29+
ScriptRunnerType runner = ScriptRunnerType.Npm,
3030
string regex = VueCliMiddleware.DefaultRegex)
3131
{
3232
if (spaBuilder == null)

0 commit comments

Comments
 (0)