feat: Initialize Zastava Webhook service with TLS and Authority authentication

- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint.
- Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately.
- Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly.
- Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
This commit is contained in:
master
2025-10-19 18:36:22 +03:00
parent 2062da7a8b
commit d099a90f9b
966 changed files with 91038 additions and 1850 deletions

View File

@@ -0,0 +1,44 @@
using System.Diagnostics;
using System.IO;
namespace Mongo2Go.Helper
{
public class FileSystem : IFileSystem
{
public void CreateFolder(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
public void DeleteFolder(string path)
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
}
public void DeleteFile(string fullFileName)
{
if (File.Exists(fullFileName))
{
File.Delete(fullFileName);
}
}
public void MakeFileExecutable (string path)
{
//when on linux or osx we must set the executeble flag on mongo binarys
var p = Process.Start("chmod", $"+x {path}");
p.WaitForExit();
if (p.ExitCode != 0)
{
throw new IOException($"Could not set executable bit for {path}");
}
}
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace Mongo2Go.Helper
{
public static class FolderSearch
{
private static readonly char[] _separators = { Path.DirectorySeparatorChar };
public static string CurrentExecutingDirectory()
{
string filePath = new Uri(typeof(FolderSearch).GetTypeInfo().Assembly.CodeBase).LocalPath;
return Path.GetDirectoryName(filePath);
}
public static string FindFolder(this string startPath, string searchPattern)
{
if (startPath == null || searchPattern == null)
{
return null;
}
string currentPath = startPath;
foreach (var part in searchPattern.Split(_separators, StringSplitOptions.None))
{
if (!Directory.Exists(currentPath))
{
return null;
}
string[] matchesDirectory = Directory.GetDirectories(currentPath, part);
if (!matchesDirectory.Any())
{
return null;
}
if (matchesDirectory.Length > 1)
{
currentPath = MatchVersionToAssemblyVersion(matchesDirectory)
?? matchesDirectory.OrderBy(x => x).Last();
}
else
{
currentPath = matchesDirectory.First();
}
}
return currentPath;
}
public static string FindFolderUpwards(this string startPath, string searchPattern)
{
if (string.IsNullOrEmpty(startPath))
{
return null;
}
string matchingFolder = startPath.FindFolder(searchPattern);
return matchingFolder ?? startPath.RemoveLastPart().FindFolderUpwards(searchPattern);
}
internal static string RemoveLastPart(this string path)
{
if (!path.Contains(Path.DirectorySeparatorChar))
{
return null;
}
List<string> parts = path.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None).ToList();
parts.RemoveAt(parts.Count() - 1);
return string.Join(Path.DirectorySeparatorChar.ToString(), parts.ToArray());
}
/// <summary>
/// Absolute path stays unchanged, relative path will be relative to current executing directory (usually the /bin folder)
/// </summary>
public static string FinalizePath(string fileName)
{
string finalPath;
if (Path.IsPathRooted(fileName))
{
finalPath = fileName;
}
else
{
finalPath = Path.Combine(CurrentExecutingDirectory(), fileName);
finalPath = Path.GetFullPath(finalPath);
}
return finalPath;
}
private static string MatchVersionToAssemblyVersion(string[] folders)
{
var version = typeof(FolderSearch).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
foreach (var folder in folders)
{
var lastFolder = new DirectoryInfo(folder).Name;
if (lastFolder == version)
return folder;
}
return null;
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Mongo2Go.Helper
{
public interface IFileSystem
{
void CreateFolder(string path);
void DeleteFolder(string path);
void DeleteFile(string fullFileName);
void MakeFileExecutable (string path );
}
}

View File

@@ -0,0 +1,7 @@
namespace Mongo2Go.Helper
{
public interface IMongoBinaryLocator
{
string Directory { get; }
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
namespace Mongo2Go.Helper
{
public interface IMongoDbProcess : IDisposable
{
IEnumerable<string> StandardOutput { get; }
IEnumerable<string> ErrorOutput { get; }
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.Extensions.Logging;
namespace Mongo2Go.Helper
{
public interface IMongoDbProcessStarter
{
IMongoDbProcess Start(string binariesDirectory, string dataDirectory, int port, bool singleNodeReplSet, string additionalMongodArguments, ushort singleNodeReplSetWaitTimeout = MongoDbDefaults.SingleNodeReplicaSetWaitTimeout, ILogger logger = null);
IMongoDbProcess Start(string binariesDirectory, string dataDirectory, int port, bool doNotKill, bool singleNodeReplSet, string additionalMongodArguments, ushort singleNodeReplSetWaitTimeout = MongoDbDefaults.SingleNodeReplicaSetWaitTimeout, ILogger logger = null);
}
}

View File

@@ -0,0 +1,10 @@
namespace Mongo2Go.Helper
{
public interface IPortPool
{
/// <summary>
/// Returns and reserves a new port
/// </summary>
int GetNextOpenPort();
}
}

View File

@@ -0,0 +1,8 @@
namespace Mongo2Go.Helper
{
public interface IPortWatcher
{
int FindOpenPort();
bool IsPortAvailable(int portNumber);
}
}

View File

@@ -0,0 +1,7 @@
namespace Mongo2Go.Helper
{
public interface IProcessWatcher
{
bool IsProcessRunning(string processName);
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace Mongo2Go.Helper
{
public class MongoBinaryLocator : IMongoBinaryLocator
{
private readonly string _nugetPrefix = Path.Combine("packages", "Mongo2Go*");
private readonly string _nugetCachePrefix = Path.Combine("packages", "mongo2go", "*");
private readonly string _nugetCacheBasePrefix = Path.Combine("mongo2go", "*");
public const string DefaultWindowsSearchPattern = @"tools\mongodb-windows*\bin";
public const string DefaultLinuxSearchPattern = "tools/mongodb-linux*/bin";
public const string DefaultOsxSearchPattern = "tools/mongodb-macos*/bin";
public const string WindowsNugetCacheLocation = @"%USERPROFILE%\.nuget\packages";
public static readonly string OsxAndLinuxNugetCacheLocation = Environment.GetEnvironmentVariable("HOME") + "/.nuget/packages";
private string _binFolder = string.Empty;
private readonly string _searchPattern;
private readonly string _nugetCacheDirectory;
private readonly string _additionalSearchDirectory;
public MongoBinaryLocator(string searchPatternOverride, string additionalSearchDirectory)
{
_additionalSearchDirectory = additionalSearchDirectory;
_nugetCacheDirectory = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
_searchPattern = DefaultOsxSearchPattern;
_nugetCacheDirectory = _nugetCacheDirectory ?? OsxAndLinuxNugetCacheLocation;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
_searchPattern = DefaultLinuxSearchPattern;
_nugetCacheDirectory = _nugetCacheDirectory ?? OsxAndLinuxNugetCacheLocation;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_searchPattern = DefaultWindowsSearchPattern;
_nugetCacheDirectory = _nugetCacheDirectory ?? Environment.ExpandEnvironmentVariables(WindowsNugetCacheLocation);
}
else
{
throw new MonogDbBinariesNotFoundException($"Unknown OS: {RuntimeInformation.OSDescription}");
}
if (!string.IsNullOrEmpty(searchPatternOverride))
{
_searchPattern = searchPatternOverride;
}
}
public string Directory {
get {
if (string.IsNullOrEmpty(_binFolder)){
return _binFolder = ResolveBinariesDirectory ();
} else {
return _binFolder;
}
}
}
private string ResolveBinariesDirectory()
{
var searchDirectories = new[]
{
// First search from the additional search directory, if provided
_additionalSearchDirectory,
// Then search from the project directory
FolderSearch.CurrentExecutingDirectory(),
// Finally search from the nuget cache directory
_nugetCacheDirectory
};
return FindBinariesDirectory(searchDirectories.Where(x => !string.IsNullOrWhiteSpace(x)).ToList());
}
private string FindBinariesDirectory(IList<string> searchDirectories)
{
foreach (var directory in searchDirectories)
{
var binaryFolder =
// First try just the search pattern
directory.FindFolderUpwards(_searchPattern) ??
// Next try the search pattern with nuget installation prefix
directory.FindFolderUpwards(Path.Combine(_nugetPrefix, _searchPattern)) ??
// Finally try the search pattern with the nuget cache prefix
directory.FindFolderUpwards(Path.Combine(_nugetCachePrefix, _searchPattern)) ??
// Finally try the search pattern with the basic nuget cache prefix
directory.FindFolderUpwards(Path.Combine(_nugetCacheBasePrefix, _searchPattern));
if (binaryFolder != null) return binaryFolder;
}
throw new MonogDbBinariesNotFoundException(
$"Could not find Mongo binaries using the search patterns \"{_searchPattern}\", \"{Path.Combine(_nugetPrefix, _searchPattern)}\", \"{Path.Combine(_nugetCachePrefix, _searchPattern)}\", and \"{Path.Combine(_nugetCacheBasePrefix, _searchPattern)}\". " +
$"You can override the search pattern and directory when calling MongoDbRunner.Start. We have detected the OS as {RuntimeInformation.OSDescription}.\n" +
$"We walked up to root directory from the following locations.\n {string.Join("\n", searchDirectories)}");
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
namespace Mongo2Go.Helper
{
// IDisposable and friends
public partial class MongoDbProcess
{
~MongoDbProcess()
{
Dispose(false);
}
public bool Disposed { get; private set; }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (Disposed)
{
return;
}
if (disposing)
{
// we have no "managed resources" - but we leave this switch to avoid an FxCop CA1801 warnig
}
if (_process == null)
{
return;
}
if (_process.DoNotKill)
{
return;
}
if (!_process.HasExited)
{
_process.Kill();
_process.WaitForExit();
}
_process.Dispose();
_process = null;
Disposed = true;
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace Mongo2Go.Helper
{
public partial class MongoDbProcess : IMongoDbProcess
{
private WrappedProcess _process;
public IEnumerable<string> ErrorOutput { get; set; }
public IEnumerable<string> StandardOutput { get; set; }
internal MongoDbProcess(WrappedProcess process)
{
_process = process;
}
}
}

View File

@@ -0,0 +1,92 @@
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Core.Servers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
namespace Mongo2Go.Helper
{
public class MongoDbProcessStarter : IMongoDbProcessStarter
{
private const string ProcessReadyIdentifier = "waiting for connections";
private const string Space = " ";
private const string ReplicaSetName = "singleNodeReplSet";
private const string ReplicaSetReadyIdentifier = "transition to primary complete; database writes are now permitted";
/// <summary>
/// Starts a new process. Process can be killed
/// </summary>
public IMongoDbProcess Start(string binariesDirectory, string dataDirectory, int port, bool singleNodeReplSet, string additionalMongodArguments, ushort singleNodeReplSetWaitTimeout = MongoDbDefaults.SingleNodeReplicaSetWaitTimeout, ILogger logger = null)
{
return Start(binariesDirectory, dataDirectory, port, false, singleNodeReplSet, additionalMongodArguments, singleNodeReplSetWaitTimeout, logger);
}
/// <summary>
/// Starts a new process.
/// </summary>
public IMongoDbProcess Start(string binariesDirectory, string dataDirectory, int port, bool doNotKill, bool singleNodeReplSet, string additionalMongodArguments, ushort singleNodeReplSetWaitTimeout = MongoDbDefaults.SingleNodeReplicaSetWaitTimeout, ILogger logger = null)
{
string fileName = @"{0}{1}{2}".Formatted(binariesDirectory, System.IO.Path.DirectorySeparatorChar.ToString(), MongoDbDefaults.MongodExecutable);
string arguments = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) ?
@"--dbpath ""{0}"" --port {1} --bind_ip 127.0.0.1".Formatted(dataDirectory, port) :
@"--tlsMode disabled --dbpath ""{0}"" --port {1} --bind_ip 127.0.0.1".Formatted(dataDirectory, port);
arguments = singleNodeReplSet ? arguments + Space + "--replSet" + Space + ReplicaSetName : arguments;
arguments += MongodArguments.GetValidAdditionalArguments(arguments, additionalMongodArguments);
WrappedProcess wrappedProcess = ProcessControl.ProcessFactory(fileName, arguments);
wrappedProcess.DoNotKill = doNotKill;
ProcessOutput output = ProcessControl.StartAndWaitForReady(wrappedProcess, 5, ProcessReadyIdentifier, logger);
if (singleNodeReplSet)
{
var replicaSetReady = false;
// subscribe to output from mongod process and check for replica set ready message
wrappedProcess.OutputDataReceived += (_, args) => replicaSetReady |= !string.IsNullOrWhiteSpace(args.Data) && args.Data.IndexOf(ReplicaSetReadyIdentifier, StringComparison.OrdinalIgnoreCase) >= 0;
MongoClient client = new MongoClient("mongodb://127.0.0.1:{0}/?directConnection=true&replicaSet={1}".Formatted(port, ReplicaSetName));
var admin = client.GetDatabase("admin");
var replConfig = new BsonDocument(new List<BsonElement>()
{
new BsonElement("_id", ReplicaSetName),
new BsonElement("members",
new BsonArray {new BsonDocument {{"_id", 0}, {"host", "127.0.0.1:{0}".Formatted(port)}}})
});
var command = new BsonDocument("replSetInitiate", replConfig);
admin.RunCommand<BsonDocument>(command);
// wait until replica set is ready or until the timeout is reached
SpinWait.SpinUntil(() => replicaSetReady, TimeSpan.FromSeconds(singleNodeReplSetWaitTimeout));
if (!replicaSetReady)
{
throw new TimeoutException($"Replica set initialization took longer than the specified timeout of {singleNodeReplSetWaitTimeout} seconds. Please consider increasing the value of {nameof(singleNodeReplSetWaitTimeout)}.");
}
// wait until transaction is ready or until the timeout is reached
SpinWait.SpinUntil(() =>
client.Cluster.Description.Servers.Any(s => s.State == ServerState.Connected && s.IsDataBearing),
TimeSpan.FromSeconds(singleNodeReplSetWaitTimeout));
if (!client.Cluster.Description.Servers.Any(s => s.State == ServerState.Connected && s.IsDataBearing))
{
throw new TimeoutException($"Cluster readiness for transactions took longer than the specified timeout of {singleNodeReplSetWaitTimeout} seconds. Please consider increasing the value of {nameof(singleNodeReplSetWaitTimeout)}.");
}
}
MongoDbProcess mongoDbProcess = new MongoDbProcess(wrappedProcess)
{
ErrorOutput = output.ErrorOutput,
StandardOutput = output.StandardOutput
};
return mongoDbProcess;
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Diagnostics;
using System.IO;
namespace Mongo2Go.Helper
{
public static class MongoImportExport
{
/// <summary>
/// Input File: Absolute path stays unchanged, relative path will be relative to current executing directory (usually the /bin folder)
/// </summary>
public static ProcessOutput Import(string binariesDirectory, int port, string database, string collection, string inputFile, bool drop, string additionalMongodArguments = null)
{
string finalPath = FolderSearch.FinalizePath(inputFile);
if (!File.Exists(finalPath))
{
throw new FileNotFoundException("File not found", finalPath);
}
string fileName = Path.Combine("{0}", "{1}").Formatted(binariesDirectory, MongoDbDefaults.MongoImportExecutable);
string arguments = @"--host localhost --port {0} --db {1} --collection {2} --file ""{3}""".Formatted(port, database, collection, finalPath);
if (drop) { arguments += " --drop"; }
arguments += MongodArguments.GetValidAdditionalArguments(arguments, additionalMongodArguments);
Process process = ProcessControl.ProcessFactory(fileName, arguments);
return ProcessControl.StartAndWaitForExit(process);
}
/// <summary>
/// Output File: Absolute path stays unchanged, relative path will be relative to current executing directory (usually the /bin folder)
/// </summary>
public static ProcessOutput Export(string binariesDirectory, int port, string database, string collection, string outputFile, string additionalMongodArguments = null)
{
string finalPath = FolderSearch.FinalizePath(outputFile);
string fileName = Path.Combine("{0}", "{1}").Formatted(binariesDirectory, MongoDbDefaults.MongoExportExecutable);
string arguments = @"--host localhost --port {0} --db {1} --collection {2} --out ""{3}""".Formatted(port, database, collection, finalPath);
arguments += MongodArguments.GetValidAdditionalArguments(arguments, additionalMongodArguments);
Process process = ProcessControl.ProcessFactory(fileName, arguments);
return ProcessControl.StartAndWaitForExit(process);
}
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Mongo2Go.Helper
{
/// <summary>
/// Structure of a log generated by mongod. Used to deserialize the logs
/// and pass them to an ILogger.
/// See: https://docs.mongodb.com/manual/reference/log-messages/#json-log-output-format
/// Note: "truncated" and "size" are not parsed as we're unsure how to
/// properly parse and use them.
/// </summary>
class MongoLogStatement
{
[JsonPropertyName("t")]
public MongoDate MongoDate { get; set; }
/// <summary>
/// Severity of the logs as defined by MongoDB. Mapped to LogLevel
/// as defined by Microsoft.
/// D1-D2 mapped to Debug level. D3-D5 mapped Trace level.
/// </summary>
[JsonPropertyName("s")]
public string Severity { get; set; }
public LogLevel Level
{
get
{
if (string.IsNullOrEmpty(Severity))
return LogLevel.None;
switch (Severity)
{
case "F": return LogLevel.Critical;
case "E": return LogLevel.Error;
case "W": return LogLevel.Warning;
case "I": return LogLevel.Information;
case "D":
case "D1":
case "D2":
return LogLevel.Debug;
case "D3":
case "D4":
case "D5":
default:
return LogLevel.Trace;
}
}
}
[JsonPropertyName("c")]
public string Component { get; set; }
[JsonPropertyName("ctx")]
public string Context { get; set; }
[JsonPropertyName("id")]
public int? Id { get; set; }
[JsonPropertyName("msg")]
public string Message { get; set; }
[JsonPropertyName("tags")]
public IEnumerable<string> Tags { get; set; }
[JsonPropertyName("attr")]
public IDictionary<string, JsonElement> Attributes { get; set; }
}
class MongoDate
{
[JsonPropertyName("$date")]
public DateTime DateTime { get; set; }
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
namespace Mongo2Go.Helper
{
public static class MongodArguments
{
private const string ArgumentSeparator = "--";
private const string Space = " ";
/// <summary>
/// Returns the <paramref name="additionalMongodArguments" /> if it is verified that it does not contain any mongod argument already defined by Mongo2Go.
/// </summary>
/// <param name="existingMongodArguments">mongod arguments defined by Mongo2Go</param>
/// <param name="additionalMongodArguments">Additional mongod arguments</param>
/// <exception cref="T:System.ArgumentException"><paramref name="additionalMongodArguments" /> contains at least one mongod argument already defined by Mongo2Go</exception>
/// <returns>A string with the additional mongod arguments</returns>
public static string GetValidAdditionalArguments(string existingMongodArguments, string additionalMongodArguments)
{
if (string.IsNullOrWhiteSpace(additionalMongodArguments))
{
return string.Empty;
}
var existingMongodArgumentArray = existingMongodArguments.Trim().Split(new[] { ArgumentSeparator }, StringSplitOptions.RemoveEmptyEntries);
var existingMongodArgumentOptions = new List<string>();
for (var i = 0; i < existingMongodArgumentArray.Length; i++)
{
var argumentOptionSplit = existingMongodArgumentArray[i].Split(' ');
if (argumentOptionSplit.Length == 0
|| string.IsNullOrWhiteSpace(argumentOptionSplit[0].Trim()))
{
continue;
}
existingMongodArgumentOptions.Add(argumentOptionSplit[0].Trim());
}
var additionalMongodArgumentArray = additionalMongodArguments.Trim().Split(new[] { ArgumentSeparator }, StringSplitOptions.RemoveEmptyEntries);
var validAdditionalMongodArguments = new List<string>();
var duplicateMongodArguments = new List<string>();
for (var i = 0; i < additionalMongodArgumentArray.Length; i++)
{
var additionalArgument = additionalMongodArgumentArray[i].Trim();
var argumentOptionSplit = additionalArgument.Split(' ');
if (argumentOptionSplit.Length == 0
|| string.IsNullOrWhiteSpace(argumentOptionSplit[0].Trim()))
{
continue;
}
if (existingMongodArgumentOptions.Contains(argumentOptionSplit[0].Trim()))
{
duplicateMongodArguments.Add(argumentOptionSplit[0].Trim());
}
validAdditionalMongodArguments.Add(ArgumentSeparator + additionalArgument);
}
if (duplicateMongodArguments.Count != 0)
{
throw new ArgumentException($"mongod arguments defined by Mongo2Go ({string.Join(", ", existingMongodArgumentOptions)}) cannot be overriden. Please remove the following additional argument(s): {string.Join(", ", duplicateMongodArguments)}.");
}
return validAdditionalMongodArguments.Count == 0
? string.Empty
: Space + string.Join(" ", validAdditionalMongodArguments);
}
}
}

View File

@@ -0,0 +1,24 @@
#if NETSTANDARD2_0
using System;
namespace Mongo2Go.Helper
{
public static class NetStandard21Compatibility
{
/// <summary>
/// Returns a value indicating whether a specified string occurs within this <paramref name="string"/>, using the specified comparison rules.
/// </summary>
/// <param name="string">The string to operate on.</param>
/// <param name="value">The string to seek.</param>
/// <param name="comparisonType">One of the enumeration values that specifies the rules to use in the comparison.</param>
/// <returns><see langword="true"/> if the <paramref name="value"/> parameter occurs within this string, or if <paramref name="value"/> is the empty string (""); otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/></exception>
public static bool Contains(this string @string, string value, StringComparison comparisonType)
{
if (@string == null) throw new ArgumentNullException(nameof(@string));
return @string.IndexOf(value, comparisonType) >= 0;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
using System;
namespace Mongo2Go.Helper
{
public class NoFreePortFoundException : Exception
{
public NoFreePortFoundException() { }
public NoFreePortFoundException(string message) : base(message) { }
public NoFreePortFoundException(string message, Exception inner) : base(message, inner) { }
}
}

View File

@@ -0,0 +1,37 @@
using System;
namespace Mongo2Go.Helper
{
/// <summary>
/// Intention: port numbers won't be assigned twice to avoid connection problems with integration tests
/// </summary>
public sealed class PortPool : IPortPool
{
private static readonly PortPool Instance = new PortPool();
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static PortPool()
{
}
// Singleton
private PortPool()
{
}
public static PortPool GetInstance
{
get { return Instance; }
}
/// <summary>
/// Returns and reserves a new port
/// </summary>
public int GetNextOpenPort()
{
IPortWatcher portWatcher = PortWatcherFactory.CreatePortWatcher();
return portWatcher.FindOpenPort();
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
namespace Mongo2Go.Helper
{
public class PortWatcher : IPortWatcher
{
public int FindOpenPort()
{
// Locate a free port on the local machine by binding a socket to
// an IPEndPoint using IPAddress.Any and port 0. The socket will
// select a free port.
int listeningPort = 0;
Socket portSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
IPEndPoint socketEndPoint = new IPEndPoint(IPAddress.Any, 0);
portSocket.Bind(socketEndPoint);
socketEndPoint = (IPEndPoint)portSocket.LocalEndPoint;
listeningPort = socketEndPoint.Port;
}
finally
{
portSocket.Close();
}
return listeningPort;
}
public bool IsPortAvailable(int portNumber)
{
IPEndPoint[] tcpConnInfoArray = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners();
return tcpConnInfoArray.All(endpoint => endpoint.Port != portNumber);
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Runtime.InteropServices;
namespace Mongo2Go.Helper
{
public class PortWatcherFactory
{
public static IPortWatcher CreatePortWatcher()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? (IPortWatcher) new UnixPortWatcher()
: new PortWatcher();
}
}
}

View File

@@ -0,0 +1,163 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Threading;
namespace Mongo2Go.Helper
{
public static class ProcessControl
{
public static WrappedProcess ProcessFactory(string fileName, string arguments)
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
};
WrappedProcess process = new WrappedProcess { StartInfo = startInfo };
return process;
}
public static ProcessOutput StartAndWaitForExit(Process process)
{
List<string> errorOutput = new List<string>();
List<string> standardOutput = new List<string>();
process.ErrorDataReceived += (sender, args) => errorOutput.Add(args.Data);
process.OutputDataReceived += (sender, args) => standardOutput.Add(args.Data);
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
process.WaitForExit();
process.CancelErrorRead();
process.CancelOutputRead();
return new ProcessOutput(errorOutput, standardOutput);
}
/// <summary>
/// Reads from Output stream to determine if process is ready
/// </summary>
public static ProcessOutput StartAndWaitForReady(Process process, int timeoutInSeconds, string processReadyIdentifier, ILogger logger = null)
{
if (timeoutInSeconds < 1 ||
timeoutInSeconds > 10)
{
throw new ArgumentOutOfRangeException("timeoutInSeconds", "The amount in seconds should have a value between 1 and 10.");
}
// Determine when the process is ready, and store the error and standard outputs
// to eventually return them.
List<string> errorOutput = new List<string>();
List<string> standardOutput = new List<string>();
bool processReady = false;
void OnProcessOnErrorDataReceived(object sender, DataReceivedEventArgs args) => errorOutput.Add(args.Data);
void OnProcessOnOutputDataReceived(object sender, DataReceivedEventArgs args)
{
standardOutput.Add(args.Data);
if (!string.IsNullOrEmpty(args.Data) && args.Data.IndexOf(processReadyIdentifier, StringComparison.OrdinalIgnoreCase) >= 0)
{
processReady = true;
}
}
process.ErrorDataReceived += OnProcessOnErrorDataReceived;
process.OutputDataReceived += OnProcessOnOutputDataReceived;
if (logger == null)
WireLogsToConsoleAndDebugOutput(process);
else
WireLogsToLogger(process, logger);
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
int lastResortCounter = 0;
int timeOut = timeoutInSeconds * 10;
while (!processReady)
{
Thread.Sleep(100);
if (++lastResortCounter > timeOut)
{
// we waited X seconds.
// for any reason the detection did not worked, eg. the identifier changed
// lets assume everything is still ok
break;
}
}
//unsubscribing writing to list - to prevent memory overflow.
process.ErrorDataReceived -= OnProcessOnErrorDataReceived;
process.OutputDataReceived -= OnProcessOnOutputDataReceived;
return new ProcessOutput(errorOutput, standardOutput);
}
/// <summary>
/// Send the mongod process logs to .NET's console and debug outputs.
/// </summary>
/// <param name="process"></param>
private static void WireLogsToConsoleAndDebugOutput(Process process)
{
void DebugOutputHandler(object sender, DataReceivedEventArgs args) => Debug.WriteLine(args.Data);
void ConsoleOutputHandler(object sender, DataReceivedEventArgs args) => Console.WriteLine(args.Data);
//Writing to debug trace & console to enable test runners to capture the output
process.ErrorDataReceived += DebugOutputHandler;
process.ErrorDataReceived += ConsoleOutputHandler;
process.OutputDataReceived += DebugOutputHandler;
process.OutputDataReceived += ConsoleOutputHandler;
}
/// <summary>
/// Parses and redirects mongod logs to ILogger.
/// </summary>
/// <param name="process"></param>
/// <param name="logger"></param>
private static void WireLogsToLogger(Process process, ILogger logger)
{
// Parse the structured log and wire it to logger
void OnReceivingLogFromMongod(object sender, DataReceivedEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.Data))
return;
try
{
var log = JsonSerializer.Deserialize<MongoLogStatement>(args.Data);
logger.Log(log.Level,
"{message} - {attributes} - {date} - {component} - {context} - {id} - {tags}",
log.Message, log.Attributes, log.MongoDate.DateTime, log.Component, log.Context, log.Id, log.Tags);
}
catch (Exception ex) when (ex is JsonException || ex is NotSupportedException)
{
logger.LogWarning(ex,
"Failed parsing the mongod logs {log}. It could be that the format has changed. " +
"See: https://docs.mongodb.com/manual/reference/log-messages/#std-label-log-message-json-output-format",
args.Data);
}
catch (Exception)
{
// Nothing else to do. Swallow the exception and do not wire the logs.
}
};
process.ErrorDataReceived += OnReceivingLogFromMongod;
process.OutputDataReceived += OnReceivingLogFromMongod;
}
}
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace Mongo2Go.Helper
{
public class ProcessOutput
{
public ProcessOutput(IEnumerable<string> errorOutput, IEnumerable<string> standardOutput)
{
StandardOutput = standardOutput;
ErrorOutput = errorOutput;
}
public IEnumerable<string> StandardOutput { get; private set; }
public IEnumerable<string> ErrorOutput { get; private set; }
}
}

View File

@@ -0,0 +1,13 @@
using System.Diagnostics;
using System.Linq;
namespace Mongo2Go.Helper
{
public class ProcessWatcher : IProcessWatcher
{
public bool IsProcessRunning(string processName)
{
return Process.GetProcessesByName(processName).Any();
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Globalization;
namespace Mongo2Go.Helper
{
/// <summary>
/// saves about 40 keystrokes
/// </summary>
public static class StringFormatExtension
{
/// <summary>
/// Populates the template using the provided arguments and the invariant culture
/// </summary>
public static string Formatted(this string template, params object[] args)
{
return template.Formatted(CultureInfo.InvariantCulture, args);
}
/// <summary>
/// Populates the template using the provided arguments using the provided formatter
/// </summary>
public static string Formatted(this string template, IFormatProvider formatter, params object[] args)
{
return string.IsNullOrEmpty(template) ? string.Empty : string.Format(formatter, template, args);
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Net;
using System.Net.Sockets;
namespace Mongo2Go.Helper
{
public class UnixPortWatcher : IPortWatcher
{
public int FindOpenPort ()
{
// Locate a free port on the local machine by binding a socket to
// an IPEndPoint using IPAddress.Any and port 0. The socket will
// select a free port.
int listeningPort = 0;
Socket portSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
IPEndPoint socketEndPoint = new IPEndPoint(IPAddress.Any, 0);
portSocket.Bind(socketEndPoint);
socketEndPoint = (IPEndPoint)portSocket.LocalEndPoint;
listeningPort = socketEndPoint.Port;
}
finally
{
portSocket.Close();
}
return listeningPort;
}
public bool IsPortAvailable (int portNumber)
{
TcpListener tcpListener = new TcpListener (IPAddress.Loopback, portNumber);
try {
tcpListener.Start ();
return true;
}
catch (SocketException) {
return false;
} finally
{
tcpListener.Stop ();
}
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Diagnostics;
namespace Mongo2Go.Helper
{
public class WrappedProcess : Process
{
public bool DoNotKill { get; set; }
}
}