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; }
}
}

View File

@@ -0,0 +1,92 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;netstandard2.1</TargetFrameworks>
<Authors>Johannes Hoppe and many contributors</Authors>
<Description>Mongo2Go is a managed wrapper around MongoDB binaries. It targets .NET Framework 4.7.2 and .NET Standard 2.1.
This Nuget package contains the executables of mongod, mongoimport and mongoexport v4.4.4 for Windows, Linux and macOS.
Mongo2Go has two use cases:
1. Providing multiple, temporary and isolated MongoDB databases for integration tests
2. Providing a quick to set up MongoDB database for a local developer environment</Description>
<Company>HAUS HOPPE - ITS</Company>
<Copyright>Copyright © 2012-2025 Johannes Hoppe and many ❤️ contributors</Copyright>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageIcon>icon.png</PackageIcon>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/Mongo2Go/Mongo2Go</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/Mongo2Go/Mongo2Go/releases</PackageReleaseNotes>
<PackageTags>MongoDB Mongo unit test integration runner</PackageTags>
<RepositoryUrl>https://github.com/Mongo2Go/Mongo2Go</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<AssemblyTitle>Mongo2Go</AssemblyTitle>
<AssemblyDescription>Mongo2Go is a managed wrapper around MongoDB binaries.</AssemblyDescription>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netstandard2.1|AnyCPU'">
<WarningLevel>4</WarningLevel>
<NoWarn>1701;1702;1591;1573</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|netstandard2.1|AnyCPU'">
<WarningLevel>4</WarningLevel>
<NoWarn>1701;1702;1591;1573</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net48|AnyCPU'">
<NoWarn>1701;1702;1591;1573</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net48|AnyCPU'">
<NoWarn>1701;1702;1591;1573</NoWarn>
</PropertyGroup>
<PropertyGroup Label="Restoring">
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<RestoreLockedMode Condition="$(ContinuousIntegrationBuild) == 'true'">true</RestoreLockedMode>
</PropertyGroup>
<PropertyGroup Label="SourceLink">
<DebugType>embedded</DebugType>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
</PropertyGroup>
<PropertyGroup Label="MinVer">
<MinVerTagPrefix>v</MinVerTagPrefix>
</PropertyGroup>
<ItemGroup>
<None Update="packages.lock.json" Visible="false" />
<None Include="../mongo2go_200_200.png" Visible="false">
<Pack>true</Pack>
<PackagePath>icon.png</PackagePath>
</None>
<None Include="../../tools/mongodb*/**" Visible="false">
<Pack>true</Pack>
<PackagePath>tools</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="MinVer" Version="2.5.0" PrivateAssets="all" />
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
<PackageReference Include="System.Text.Json" Version="6.0.10" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Mongo2GoTests" />
</ItemGroup>
<Target Name="PrintPackageVersionForGitHubActions" AfterTargets="Pack">
<Message Importance="high" Text="version=$(PackageVersion)" />
<Message Importance="high" Text="nupkg-filename=$(PackageId).$(PackageVersion).nupkg" />
</Target>
</Project>

View File

@@ -0,0 +1,22 @@
namespace Mongo2Go
{
public static class MongoDbDefaults
{
public const string ProcessName = "mongod";
public const string MongodExecutable = "mongod";
public const string MongoExportExecutable = "mongoexport";
public const string MongoImportExecutable = "mongoimport";
public const int DefaultPort = 27017;
// but we don't want to get in trouble with productive systems
public const int TestStartPort = 27018;
public const string Lockfile = "mongod.lock";
public const int SingleNodeReplicaSetWaitTimeout = 10;
}
}

View File

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

View File

@@ -0,0 +1,54 @@
using System;
namespace Mongo2Go
{
// IDisposable and friends
public partial class MongoDbRunner
{
~MongoDbRunner()
{
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 (State != State.Running)
{
return;
}
if (disposing)
{
// we have no "managed resources" - but we leave this switch to avoid an FxCop CA1801 warnig
}
if (_mongoDbProcess != null)
{
_mongoDbProcess.Dispose();
}
// will be null if we are working in debugging mode (single instance)
if (_dataDirectoryWithPort != null)
{
// finally clean up the data directory we created previously
_fileSystem.DeleteFolder(_dataDirectoryWithPort);
}
Disposed = true;
State = State.Stopped;
}
}
}

View File

@@ -0,0 +1,221 @@
using Microsoft.Extensions.Logging;
using Mongo2Go.Helper;
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Mongo2Go
{
/// <summary>
/// Mongo2Go main entry point
/// </summary>
public partial class MongoDbRunner : IDisposable
{
private readonly IMongoDbProcess _mongoDbProcess;
private readonly IFileSystem _fileSystem;
private readonly string _dataDirectoryWithPort;
private readonly int _port;
private readonly IMongoBinaryLocator _mongoBin;
/// <summary>
/// State of the current MongoDB instance
/// </summary>
public State State { get; private set; }
/// <summary>
/// Connections string that should be used to establish a connection the MongoDB instance
/// </summary>
public string ConnectionString { get; private set; }
/// <summary>
/// Starts Multiple MongoDB instances with each call
/// On dispose: kills them and deletes their data directory
/// </summary>
/// <param name="logger">(Optional) If null, mongod logs are wired to .NET's Console and Debug output (provided you haven't added the --quiet additional argument).
/// If not null, mongod logs are parsed and wired to the provided logger.</param>
/// <remarks>Should be used for integration tests</remarks>
public static MongoDbRunner Start(string dataDirectory = null, string binariesSearchPatternOverride = null, string binariesSearchDirectory = null, bool singleNodeReplSet = false, string additionalMongodArguments = null, ushort singleNodeReplSetWaitTimeout = MongoDbDefaults.SingleNodeReplicaSetWaitTimeout, ILogger logger = null)
{
if (dataDirectory == null) {
dataDirectory = GetTemporaryDataDirectory();
}
// this is required to support multiple instances to run in parallel
dataDirectory += Guid.NewGuid().ToString().Replace("-", "").Substring(0, 20);
return new MongoDbRunner(
PortPool.GetInstance,
new FileSystem(),
new MongoDbProcessStarter(),
new MongoBinaryLocator(binariesSearchPatternOverride, binariesSearchDirectory),
dataDirectory,
singleNodeReplSet,
additionalMongodArguments,
singleNodeReplSetWaitTimeout,
logger);
}
/// <summary>
/// !!!
/// This method is only used for an internal unit test. Use MongoDbRunner.Start() instead.
/// But if you find it to be useful (eg. to change every aspect on your own) feel free to implement the interfaces on your own!
/// </summary>
/// <remarks>see https://github.com/Mongo2Go/Mongo2Go/issues/41 </remarks>
[Obsolete("Use MongoDbRunner.Start() if possible.")]
public static MongoDbRunner StartUnitTest(
IPortPool portPool,
IFileSystem fileSystem,
IMongoDbProcessStarter processStarter,
IMongoBinaryLocator mongoBin,
string dataDirectory = null,
string additionalMongodArguments = null)
{
return new MongoDbRunner(
portPool,
fileSystem,
processStarter,
mongoBin,
dataDirectory,
additionalMongodArguments: additionalMongodArguments);
}
/// <summary>
/// Only starts one single MongoDB instance (even on multiple calls), does not kill it, does not delete data
/// </summary>
/// <remarks>
/// Should be used for local debugging only
/// WARNING: one single instance on one single machine is not a suitable setup for productive environments!!!
/// </remarks>
public static MongoDbRunner StartForDebugging(string dataDirectory = null, string binariesSearchPatternOverride = null, string binariesSearchDirectory = null, bool singleNodeReplSet = false, int port = MongoDbDefaults.DefaultPort, string additionalMongodArguments = null, ushort singleNodeReplSetWaitTimeout = MongoDbDefaults.SingleNodeReplicaSetWaitTimeout)
{
return new MongoDbRunner(
new ProcessWatcher(),
new PortWatcher(),
new FileSystem(),
new MongoDbProcessStarter(),
new MongoBinaryLocator(binariesSearchPatternOverride, binariesSearchDirectory), port, dataDirectory, singleNodeReplSet, additionalMongodArguments, singleNodeReplSetWaitTimeout);
}
/// <summary>
/// !!!
/// This method is only used for an internal unit test. Use MongoDbRunner.StartForDebugging() instead.
/// But if you find it to be useful (eg. to change every aspect on your own) feel free to implement the interfaces on your own!
/// </summary>
/// <remarks>see https://github.com/Mongo2Go/Mongo2Go/issues/41 </remarks>
[Obsolete("Use MongoDbRunner.StartForDebugging() if possible.")]
public static MongoDbRunner StartForDebuggingUnitTest(
IProcessWatcher processWatcher,
IPortWatcher portWatcher,
IFileSystem fileSystem,
IMongoDbProcessStarter processStarter,
IMongoBinaryLocator mongoBin,
string dataDirectory = null,
string additionalMongodArguments = null)
{
return new MongoDbRunner(
processWatcher,
portWatcher,
fileSystem,
processStarter,
mongoBin,
MongoDbDefaults.DefaultPort,
dataDirectory,
additionalMongodArguments: additionalMongodArguments);
}
/// <summary>
/// Executes Mongoimport on the associated MongoDB Instace
/// </summary>
public void Import(string database, string collection, string inputFile, bool drop, string additionalMongodArguments = null)
{
MongoImportExport.Import(_mongoBin.Directory, _port, database, collection, inputFile, drop, additionalMongodArguments);
}
/// <summary>
/// Executes Mongoexport on the associated MongoDB Instace
/// </summary>
public void Export(string database, string collection, string outputFile, string additionalMongodArguments = null)
{
MongoImportExport.Export(_mongoBin.Directory, _port, database, collection, outputFile, additionalMongodArguments);
}
/// <summary>
/// usage: local debugging
/// </summary>
private MongoDbRunner(IProcessWatcher processWatcher, IPortWatcher portWatcher, IFileSystem fileSystem, IMongoDbProcessStarter processStarter, IMongoBinaryLocator mongoBin, int port, string dataDirectory = null, bool singleNodeReplSet = false, string additionalMongodArguments = null, ushort singleNodeReplSetWaitTimeout = MongoDbDefaults.SingleNodeReplicaSetWaitTimeout)
{
_fileSystem = fileSystem;
_mongoBin = mongoBin;
_port = port;
MakeMongoBinarysExecutable();
ConnectionString = singleNodeReplSet
? "mongodb://127.0.0.1:{0}/?directConnection=true&replicaSet=singleNodeReplSet&readPreference=primary".Formatted(_port)
: "mongodb://127.0.0.1:{0}/".Formatted(_port);
if (processWatcher.IsProcessRunning(MongoDbDefaults.ProcessName) && !portWatcher.IsPortAvailable(_port))
{
State = State.AlreadyRunning;
return;
}
if (!portWatcher.IsPortAvailable(_port))
{
throw new MongoDbPortAlreadyTakenException("MongoDB can't be started. The TCP port {0} is already taken.".Formatted(_port));
}
if (dataDirectory == null) {
dataDirectory = GetTemporaryDataDirectory();
}
_fileSystem.CreateFolder(dataDirectory);
_fileSystem.DeleteFile("{0}{1}{2}".Formatted(dataDirectory, Path.DirectorySeparatorChar.ToString(), MongoDbDefaults.Lockfile));
_mongoDbProcess = processStarter.Start(_mongoBin.Directory, dataDirectory, _port, true, singleNodeReplSet, additionalMongodArguments, singleNodeReplSetWaitTimeout);
State = State.Running;
}
/// <summary>
/// usage: integration tests
/// </summary>
private MongoDbRunner(IPortPool portPool, IFileSystem fileSystem, IMongoDbProcessStarter processStarter, IMongoBinaryLocator mongoBin, string dataDirectory = null, bool singleNodeReplSet = false, string additionalMongodArguments = null, ushort singleNodeReplSetWaitTimeout = MongoDbDefaults.SingleNodeReplicaSetWaitTimeout, ILogger logger = null)
{
_fileSystem = fileSystem;
_port = portPool.GetNextOpenPort();
_mongoBin = mongoBin;
if (dataDirectory == null) {
dataDirectory = GetTemporaryDataDirectory();
}
MakeMongoBinarysExecutable();
ConnectionString = singleNodeReplSet
? "mongodb://127.0.0.1:{0}/?directConnection=true&replicaSet=singleNodeReplSet&readPreference=primary".Formatted(_port)
: "mongodb://127.0.0.1:{0}/".Formatted(_port);
_dataDirectoryWithPort = "{0}_{1}".Formatted(dataDirectory, _port);
_fileSystem.CreateFolder(_dataDirectoryWithPort);
_fileSystem.DeleteFile("{0}{1}{2}".Formatted(_dataDirectoryWithPort, Path.DirectorySeparatorChar.ToString(), MongoDbDefaults.Lockfile));
_mongoDbProcess = processStarter.Start(_mongoBin.Directory, _dataDirectoryWithPort, _port, singleNodeReplSet, additionalMongodArguments, singleNodeReplSetWaitTimeout, logger);
State = State.Running;
}
private void MakeMongoBinarysExecutable()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
_fileSystem.MakeFileExecutable(Path.Combine(_mongoBin.Directory, MongoDbDefaults.MongodExecutable));
_fileSystem.MakeFileExecutable(Path.Combine(_mongoBin.Directory, MongoDbDefaults.MongoExportExecutable));
_fileSystem.MakeFileExecutable(Path.Combine(_mongoBin.Directory, MongoDbDefaults.MongoImportExecutable));
}
}
private static string GetTemporaryDataDirectory() => Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
}
}

View File

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

View File

@@ -0,0 +1,9 @@
namespace Mongo2Go
{
public enum State
{
Stopped,
Running,
AlreadyRunning
}
}

View File

@@ -0,0 +1,478 @@
{
"version": 1,
"dependencies": {
".NETFramework,Version=v4.7.2": {
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Direct",
"requested": "[6.0.0, )",
"resolved": "6.0.0",
"contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Memory": "4.5.4"
}
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.0.3, )",
"resolved": "1.0.3",
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net472": "1.0.3"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
"contentHash": "aZyGyGg2nFSxix+xMkPmlmZSsnGQ3w+mIG23LTxJZHN+GPwTQ5FpPgDo7RMOq+Kcf5D4hFWfXkGhoGstawX13Q==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.0.0",
"Microsoft.SourceLink.Common": "1.0.0"
}
},
"MinVer": {
"type": "Direct",
"requested": "[2.5.0, )",
"resolved": "2.5.0",
"contentHash": "+vgY+COxnu93nZEVYScloRuboNRIYkElokxTdtKLt6isr/f6GllPt0oLfrHj7fzxgj7SC5xMZg5c2qvd6qyHDQ=="
},
"MongoDB.Driver": {
"type": "Direct",
"requested": "[3.1.0, )",
"resolved": "3.1.0",
"contentHash": "+O7lKaIl7VUHptE0hqTd7UY1G5KDp/o8S4upG7YL4uChMNKD/U6tz9i17nMGHaD/L2AiPLgaJcaDe2XACsegGA==",
"dependencies": {
"DnsClient": "1.6.1",
"Microsoft.Extensions.Logging.Abstractions": "2.0.0",
"MongoDB.Bson": "3.1.0",
"SharpCompress": "0.30.1",
"Snappier": "1.0.0",
"System.Buffers": "4.5.1",
"System.Net.Http": "4.3.4",
"System.Runtime.InteropServices.RuntimeInformation": "4.3.0",
"ZstdSharp.Port": "0.7.3"
}
},
"System.Text.Json": {
"type": "Direct",
"requested": "[6.0.10, )",
"resolved": "6.0.10",
"contentHash": "NSB0kDipxn2ychp88NXWfFRFlmi1bst/xynOutbnpEfRCT9JZkZ7KOmF/I/hNKo2dILiMGnqblm+j1sggdLB9g==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
"System.Buffers": "4.5.1",
"System.Memory": "4.5.4",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"System.Text.Encodings.Web": "6.0.0",
"System.Threading.Tasks.Extensions": "4.5.4",
"System.ValueTuple": "4.5.0"
}
},
"DnsClient": {
"type": "Transitive",
"resolved": "1.6.1",
"contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==",
"dependencies": {
"Microsoft.Win32.Registry": "5.0.0",
"System.Buffers": "4.5.1"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==",
"dependencies": {
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "z2fpmmt+1Jfl+ZnBki9nSP08S1/tbEOxFdsK1rSR+LBehIJz1Xv9/6qOOoGNqlwnAGGVGis1Oj6S8Kt9COEYlQ=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net472": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "0E7evZXHXaDYYiLRfpyXvCh+yzM2rNTyuZDI+ZO7UUqSc6GfjePiXTdqJGtgIKUwdI81tzQKmaWprnUiPj9hAw=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "G8DuQY8/DK5NN+3jm5wcMcd9QYD90UV7MiLmdljSJixi3U/vNaeBKmmXUqI4DJCOeWizIUEh4ALhSt58mR+5eg=="
},
"Microsoft.Win32.Registry": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
"dependencies": {
"System.Security.AccessControl": "5.0.0",
"System.Security.Principal.Windows": "5.0.0"
}
},
"MongoDB.Bson": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "3dhaZhz18B5vUoEP13o2j8A6zQfkHdZhwBvLZEjDJum4BTLLv1/Z8bt25UQEtpqvYwLgde4R6ekWZ7XAYUMxuw==",
"dependencies": {
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "5.0.0"
}
},
"SharpCompress": {
"type": "Transitive",
"resolved": "0.30.1",
"contentHash": "XqD4TpfyYGa7QTPzaGlMVbcecKnXy4YmYLDWrU+JIj7IuRNl7DH2END+Ll7ekWIY8o3dAMWLFDE1xdhfIWD1nw==",
"dependencies": {
"System.Memory": "4.5.4",
"System.Text.Encoding.CodePages": "5.0.0"
}
},
"Snappier": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "rFtK2KEI9hIe8gtx3a0YDXdHOpedIf9wYCEYtBEmtlyiWVX3XlCNV03JrmmAi/Cdfn7dxK+k0sjjcLv4fpHnqA==",
"dependencies": {
"System.Memory": "4.5.4",
"System.Runtime.CompilerServices.Unsafe": "4.7.1",
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.IO": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"System.Net.Http": {
"type": "Transitive",
"resolved": "4.3.4",
"contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==",
"dependencies": {
"System.Security.Cryptography.X509Certificates": "4.3.0"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
},
"System.Runtime": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Runtime.InteropServices.RuntimeInformation": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw=="
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
"dependencies": {
"System.Security.Principal.Windows": "5.0.0"
}
},
"System.Security.Cryptography.Algorithms": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==",
"dependencies": {
"System.IO": "4.3.0",
"System.Runtime": "4.3.0",
"System.Security.Cryptography.Encoding": "4.3.0",
"System.Security.Cryptography.Primitives": "4.3.0"
}
},
"System.Security.Cryptography.Encoding": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw=="
},
"System.Security.Cryptography.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg=="
},
"System.Security.Cryptography.X509Certificates": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==",
"dependencies": {
"System.Security.Cryptography.Algorithms": "4.3.0",
"System.Security.Cryptography.Encoding": "4.3.0"
}
},
"System.Security.Principal.Windows": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
},
"System.Text.Encoding.CodePages": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "5.0.0"
}
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Memory": "4.5.4",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"System.ValueTuple": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
},
"ZstdSharp.Port": {
"type": "Transitive",
"resolved": "0.7.3",
"contentHash": "U9Ix4l4cl58Kzz1rJzj5hoVTjmbx1qGMwzAcbv1j/d3NzrFaESIurQyg+ow4mivCgkE3S413y+U9k4WdnEIkRA==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "5.0.0",
"System.Memory": "4.5.5"
}
}
},
".NETStandard,Version=v2.1": {
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Direct",
"requested": "[6.0.0, )",
"resolved": "6.0.0",
"contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Memory": "4.5.4"
}
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.0.3, )",
"resolved": "1.0.3",
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
"contentHash": "aZyGyGg2nFSxix+xMkPmlmZSsnGQ3w+mIG23LTxJZHN+GPwTQ5FpPgDo7RMOq+Kcf5D4hFWfXkGhoGstawX13Q==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.0.0",
"Microsoft.SourceLink.Common": "1.0.0"
}
},
"MinVer": {
"type": "Direct",
"requested": "[2.5.0, )",
"resolved": "2.5.0",
"contentHash": "+vgY+COxnu93nZEVYScloRuboNRIYkElokxTdtKLt6isr/f6GllPt0oLfrHj7fzxgj7SC5xMZg5c2qvd6qyHDQ=="
},
"MongoDB.Driver": {
"type": "Direct",
"requested": "[3.1.0, )",
"resolved": "3.1.0",
"contentHash": "+O7lKaIl7VUHptE0hqTd7UY1G5KDp/o8S4upG7YL4uChMNKD/U6tz9i17nMGHaD/L2AiPLgaJcaDe2XACsegGA==",
"dependencies": {
"DnsClient": "1.6.1",
"Microsoft.Extensions.Logging.Abstractions": "2.0.0",
"MongoDB.Bson": "3.1.0",
"SharpCompress": "0.30.1",
"Snappier": "1.0.0",
"System.Buffers": "4.5.1",
"ZstdSharp.Port": "0.7.3"
}
},
"System.Text.Json": {
"type": "Direct",
"requested": "[6.0.10, )",
"resolved": "6.0.10",
"contentHash": "NSB0kDipxn2ychp88NXWfFRFlmi1bst/xynOutbnpEfRCT9JZkZ7KOmF/I/hNKo2dILiMGnqblm+j1sggdLB9g==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
"System.Buffers": "4.5.1",
"System.Memory": "4.5.4",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"System.Text.Encodings.Web": "6.0.0",
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"DnsClient": {
"type": "Transitive",
"resolved": "1.6.1",
"contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==",
"dependencies": {
"Microsoft.Win32.Registry": "5.0.0"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "z2fpmmt+1Jfl+ZnBki9nSP08S1/tbEOxFdsK1rSR+LBehIJz1Xv9/6qOOoGNqlwnAGGVGis1Oj6S8Kt9COEYlQ=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "G8DuQY8/DK5NN+3jm5wcMcd9QYD90UV7MiLmdljSJixi3U/vNaeBKmmXUqI4DJCOeWizIUEh4ALhSt58mR+5eg=="
},
"Microsoft.Win32.Registry": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Memory": "4.5.4",
"System.Security.AccessControl": "5.0.0",
"System.Security.Principal.Windows": "5.0.0"
}
},
"MongoDB.Bson": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "3dhaZhz18B5vUoEP13o2j8A6zQfkHdZhwBvLZEjDJum4BTLLv1/Z8bt25UQEtpqvYwLgde4R6ekWZ7XAYUMxuw==",
"dependencies": {
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "5.0.0"
}
},
"SharpCompress": {
"type": "Transitive",
"resolved": "0.30.1",
"contentHash": "XqD4TpfyYGa7QTPzaGlMVbcecKnXy4YmYLDWrU+JIj7IuRNl7DH2END+Ll7ekWIY8o3dAMWLFDE1xdhfIWD1nw==",
"dependencies": {
"System.Text.Encoding.CodePages": "5.0.0"
}
},
"Snappier": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "rFtK2KEI9hIe8gtx3a0YDXdHOpedIf9wYCEYtBEmtlyiWVX3XlCNV03JrmmAi/Cdfn7dxK+k0sjjcLv4fpHnqA==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "4.7.1"
}
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.4.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
"dependencies": {
"System.Security.Principal.Windows": "5.0.0"
}
},
"System.Security.Principal.Windows": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
},
"System.Text.Encoding.CodePages": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "5.0.0"
}
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Memory": "4.5.4",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"ZstdSharp.Port": {
"type": "Transitive",
"resolved": "0.7.3",
"contentHash": "U9Ix4l4cl58Kzz1rJzj5hoVTjmbx1qGMwzAcbv1j/d3NzrFaESIurQyg+ow4mivCgkE3S413y+U9k4WdnEIkRA==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
}
}
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.IO;
using System.Reflection;
using FluentAssertions;
using Machine.Specifications;
using Mongo2Go.Helper;
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
namespace Mongo2GoTests
{
[Subject("FolderSearch")]
public class when_requesting_current_executing_directory
{
public static string directory;
Because of = () => directory = FolderSearch.CurrentExecutingDirectory();
It should_contain_correct_path = () => directory.Should().Contain(Path.Combine("Mongo2GoTests", "bin"));
}
[Subject("FolderSearch")]
public class when_searching_for_folder : FolderSearchSpec
{
static string startDirectory = Path.Combine(BaseDir, "test1", "test2");
static string searchPattern = Path.Combine("packages", "Mongo2Go*", "tools", "mongodb-win32-i386*", "bin");
static string directory;
Because of = () => directory = startDirectory.FindFolder(searchPattern);
It should_find_the_path_with_the_highest_version_number = () => directory.Should().Be(MongoBinaries);
}
[Subject("FolderSearch")]
public class when_searching_for_not_existing_folder : FolderSearchSpec
{
static string startDirectory = Path.Combine(BaseDir, "test1", "test2");
static string searchPattern = Path.Combine("packages", "Mongo2Go*", "XXX", "mongodb-win32-i386*", "bin");
static string directory;
Because of = () => directory = startDirectory.FindFolder(searchPattern);
It should_return_null = () => directory.Should().BeNull();
}
[Subject("FolderSearch")]
public class when_searching_for_not_existing_start_dir : FolderSearchSpec
{
static string startDirectory = Path.Combine(Path.GetRandomFileName());
static string searchPattern = Path.Combine("packages", "Mongo2Go*", "XXX", "mongodb-win32-i386*", "bin");
static string directory;
Because of = () => directory = startDirectory.FindFolder(searchPattern);
It should_return_null = () => directory.Should().BeNull();
}
[Subject("FolderSearch")]
public class when_searching_for_folder_upwards : FolderSearchSpec
{
static string searchPattern = Path.Combine("packages", "Mongo2Go*", "tools", "mongodb-win32-i386*", "bin");
static string directory;
Because of = () => directory = LocationOfAssembly.FindFolderUpwards(searchPattern);
It should_find_the_path_with_the_highest_version_number = () => directory.Should().Be(MongoBinaries);
}
[Subject("FolderSearch")]
public class when_searching_for_not_existing_folder_upwards : FolderSearchSpec
{
static string searchPattern = Path.Combine("packages", "Mongo2Go*", "XXX", "mongodb-win32-i386*", "bin");
static string directory;
Because of = () => directory = LocationOfAssembly.FindFolderUpwards(searchPattern);
It should_return_null = () => directory.Should().BeNull();
}
[Subject("FolderSearch")]
public class when_remove_last_part_of_path
{
static string directory;
Because of = () => directory = Path.Combine("test1", "test2", "test3").RemoveLastPart();
It should_remove_the_element = () => directory.Should().Be(Path.Combine("test1", "test2"));
}
[Subject("FolderSearch")]
public class when_remove_last_part_of_single_element_path
{
static string directory;
Because of = () => directory = "test1".RemoveLastPart();
It should_return_null = () => directory.Should().BeNull();
}
[Subject("FolderSearch")]
public class when_directory_contains_multiple_versions_mongo2go
{
private readonly string[] directories;
private static string getAssemblyVersion()
{
// ReSharper disable once PossibleNullReferenceException
return typeof(FolderSearch).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
}
public when_directory_contains_multiple_versions_mongo2go()
{
// setup some directories
directories = new[]
{
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, getAssemblyVersion() + "a"),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "2.2.9"),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, getAssemblyVersion())
};
foreach (var d in directories)
Directory.CreateDirectory(d);
}
private static string path;
private Because of = () => path = FolderSearch.FindFolder(AppDomain.CurrentDomain.BaseDirectory, "*");
private It should_return_the_one_that_matches_our_own_assembly_version =
() => path.Should().Be(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, getAssemblyVersion()));
}
public class FolderSearchSpec
{
public static string BaseDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
public static string MongoBinaries = Path.Combine(BaseDir, "test1", "test2", "packages", "Mongo2Go.1.2.3", "tools", "mongodb-win32-i386-2.0.7-rc0", "bin");
public static string MongoOlderBinaries = Path.Combine(BaseDir, "test1", "test2", "packages", "Mongo2Go.1.1.1", "tools", "mongodb-win32-i386-2.0.7-rc0", "bin");
public static string LocationOfAssembly = Path.Combine(BaseDir, "test1", "test2", "Project", "bin");
Establish context = () =>
{
if (!Directory.Exists(BaseDir)) { Directory.CreateDirectory(BaseDir); }
if (!Directory.Exists(MongoBinaries)) { Directory.CreateDirectory(MongoBinaries); }
if (!Directory.Exists(MongoOlderBinaries)) { Directory.CreateDirectory(MongoOlderBinaries); }
if (!Directory.Exists(LocationOfAssembly)) { Directory.CreateDirectory(LocationOfAssembly); }
};
Cleanup stuff = () => { if (Directory.Exists(BaseDir)) { Directory.Delete(BaseDir, true); }};
}
}
// ReSharper restore UnusedMember.Local
// ReSharper restore InconsistentNaming

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Mongo2Go\Mongo2Go.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Machine.Specifications" Version="1.0.0" />
<PackageReference Include="Machine.Specifications.Runner.VisualStudio" Version="2.10.1" />
<PackageReference Include="MELT" Version="0.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,76 @@
using FluentAssertions;
using Machine.Specifications;
using Mongo2Go.Helper;
using System;
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
namespace Mongo2GoTests
{
[Subject(typeof(MongodArguments))]
public class when_null_additional_arguments_return_empty_string
{
private static string validAdditionalArguments;
Because of = () => validAdditionalArguments = MongodArguments.GetValidAdditionalArguments(string.Empty, null);
It should_be_empty_string = () => validAdditionalArguments.Should().BeEmpty();
}
[Subject(typeof(MongodArguments))]
public class when_no_additional_arguments_return_empty_string
{
private static string validAdditionalArguments;
Because of = () => validAdditionalArguments = MongodArguments.GetValidAdditionalArguments(string.Empty, string.Empty);
It should_be_empty_string = () => validAdditionalArguments.Should().BeEmpty();
}
[Subject(typeof(MongodArguments))]
public class when_additional_arguments_start_with_argument_separator_return_additional_arguments
{
private static string validAdditionalArguments;
private const string additionalArgumentsUnderTest = " --argument_1 under_test --argument_2 under test";
private const string expectedAdditionalArguments = " --argument_1 under_test --argument_2 under test";
Because of = () => validAdditionalArguments = MongodArguments.GetValidAdditionalArguments(string.Empty, additionalArgumentsUnderTest);
It should_be_expected_additional_arguments = () => validAdditionalArguments.Should().Be(expectedAdditionalArguments);
}
[Subject(typeof(MongodArguments))]
public class when_additional_arguments_does_not_start_with_argument_separator_return_additional_arguments
{
private static string validAdditionalArguments;
private const string additionalArgumentsUnderTest = "argument_1 under_test --argument_2 under test";
private const string expectedAdditionalArguments = " --argument_1 under_test --argument_2 under test";
Because of = () => validAdditionalArguments = MongodArguments.GetValidAdditionalArguments(string.Empty, additionalArgumentsUnderTest);
It should_be_expected_additional_arguments = () => validAdditionalArguments.Should().Be(expectedAdditionalArguments);
}
[Subject(typeof(MongodArguments))]
public class when_existing_arguments_and_additional_arguments_do_not_have_shared_options_return_additional_arguments
{
private static string validAdditionalArguments;
private const string existingArguments = "--existing_argument1 --existing_argument2";
private const string additionalArgumentsUnderTest = " --argument_1 under_test --argument_2 under test";
private const string expectedAdditionalArguments = " --argument_1 under_test --argument_2 under test";
Because of = () => validAdditionalArguments = MongodArguments.GetValidAdditionalArguments(existingArguments, additionalArgumentsUnderTest);
It should_be_expected_additional_arguments = () => validAdditionalArguments.Should().Be(expectedAdditionalArguments);
}
[Subject(typeof(MongodArguments))]
public class when_existing_arguments_and_additional_arguments_have_shared_options_throw_argument_exception
{
private static Exception exception;
private const string duplicateArgument = "existing_argument2";
private static readonly string existingArguments = $"--existing_argument1 --{duplicateArgument}";
private static readonly string additionalArgumentsUnderTest = $" --argument_1 under_test --{duplicateArgument} argument2_new_value --argument_2 under test";
Because of = () => exception = Catch.Exception(() => MongodArguments.GetValidAdditionalArguments(existingArguments, additionalArgumentsUnderTest));
It should_throw_argument_exception = () => exception.Should().BeOfType<ArgumentException>();
It should_contain_more_than_instance_of_the_duplicate_argument = () => exception.Message.IndexOf(duplicateArgument, StringComparison.InvariantCultureIgnoreCase).Should().NotBe(exception.Message.LastIndexOf(duplicateArgument, StringComparison.InvariantCultureIgnoreCase));
}
}
// ReSharper restore UnusedMember.Local
// ReSharper restore InconsistentNaming

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Mongo2Go;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
namespace Mongo2GoTests.Runner
{
public class MongoDebuggingTest
{
internal static MongoDbRunner _runner;
internal static IMongoCollection<TestDocument> _collection;
internal static string _databaseName = "IntegrationTest";
internal static string _collectionName = "TestCollection";
internal static IMongoDatabase _database;
internal static void CreateConnection()
{
_runner = MongoDbRunner.StartForDebugging(singleNodeReplSet: false);
MongoClient client = new MongoClient(_runner.ConnectionString);
_database = client.GetDatabase(_databaseName);
_collection = _database.GetCollection<TestDocument>(_collectionName);
}
public static IList<T> ReadBsonFile<T>(string fileName)
{
string[] content = File.ReadAllLines(fileName);
return content.Select(s => BsonSerializer.Deserialize<T>(s)).ToList();
}
}
}

View File

@@ -0,0 +1,47 @@
using Microsoft.Extensions.Logging;
using Mongo2Go;
using MongoDB.Driver;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Mongo2GoTests.Runner
{
public class MongoIntegrationTest
{
internal static MongoDbRunner _runner;
internal static IMongoCollection<TestDocument> _collection;
internal static string _databaseName = "IntegrationTest";
internal static string _collectionName = "TestCollection";
internal static void CreateConnection(ILogger logger = null)
{
_runner = MongoDbRunner.Start(singleNodeReplSet: false, logger: logger);
MongoClient client = new MongoClient(_runner.ConnectionString);
IMongoDatabase database = client.GetDatabase(_databaseName);
_collection = database.GetCollection<TestDocument>(_collectionName);
}
}
public static class TaskExtensions
{
public static async Task WithTimeout(this Task task, TimeSpan timeout)
{
using (var cancellationTokenSource = new CancellationTokenSource())
{
var completedTask = await Task.WhenAny(task, Task.Delay(timeout, cancellationTokenSource.Token));
if (completedTask == task)
{
cancellationTokenSource.Cancel();
await task;
}
else
{
throw new TimeoutException("The operation has timed out.");
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
using Mongo2Go;
using MongoDB.Driver;
namespace Mongo2GoTests.Runner
{
public class MongoTransactionTest
{
internal static MongoDbRunner _runner;
internal static IMongoCollection<TestDocument> _mainCollection;
internal static IMongoCollection<TestDocument> _dependentCollection;
internal static string _databaseName = "TransactionTest";
internal static string _mainCollectionName = "MainCollection";
internal static string _dependentCollectionName = "DependentCollection";
internal static IMongoDatabase database;
internal static IMongoClient client;
internal static void CreateConnection(ushort? singleNodeReplSetWaitTimeout = null)
{
if (singleNodeReplSetWaitTimeout.HasValue)
{
_runner = MongoDbRunner.Start(singleNodeReplSet: true, singleNodeReplSetWaitTimeout: singleNodeReplSetWaitTimeout.Value);
}
else
{
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
}
client = new MongoClient(_runner.ConnectionString);
database = client.GetDatabase(_databaseName);
_mainCollection = database.GetCollection<TestDocument>(_mainCollectionName);
_dependentCollection = database.GetCollection<TestDocument>(_dependentCollectionName);
}
}
}

View File

@@ -0,0 +1,89 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using FluentAssertions;
using Machine.Specifications;
using Mongo2Go.Helper;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using It = Machine.Specifications.It;
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
namespace Mongo2GoTests.Runner
{
[Subject("Runner Integration Test")]
public class when_using_monogoexport : MongoDebuggingTest
{
static readonly string _testFile = Path.GetTempPath() + "testExport.json";
static IList<TestDocument> parsedContent;
Establish context = () =>
{
CreateConnection();
_database.DropCollection(_collectionName);
_collection.InsertOne(TestDocument.DummyData1());
_collection.InsertOne(TestDocument.DummyData2());
_collection.InsertOne(TestDocument.DummyData3());
};
Because of = () =>
{
_runner.Export(_databaseName, _collectionName, _testFile);
Thread.Sleep(500);
parsedContent = ReadBsonFile<TestDocument>(_testFile);
};
It should_preserve_all_values1 = () => parsedContent[0].Should().BeEquivalentTo(TestDocument.DummyData1(), cfg => cfg.Excluding(d => d.Id));
It should_preserve_all_values2 = () => parsedContent[1].Should().BeEquivalentTo(TestDocument.DummyData2(), cfg => cfg.Excluding(d => d.Id));
It should_preserve_all_values3 = () => parsedContent[2].Should().BeEquivalentTo(TestDocument.DummyData3(), cfg => cfg.Excluding(d => d.Id));
Cleanup stuff = () =>
{
new FileSystem().DeleteFile(_testFile);
_runner.Dispose();
};
}
[Subject("Runner Integration Test")]
public class when_using_monogoimport : MongoDebuggingTest
{
static IQueryable<TestDocument> query;
static readonly string _testFile = Path.GetTempPath() + "testImport.json";
const string _filecontent =
@"{ ""_id"" : { ""$oid"" : ""50227b375dff9218248eadc4"" }, ""StringTest"" : ""Hello World"", ""IntTest"" : 42, ""DateTest"" : { ""$date"" : ""1984-09-30T06:06:06.171Z"" }, ""ListTest"" : [ ""I"", ""am"", ""a"", ""list"", ""of"", ""strings"" ] }" + "\r\n" +
@"{ ""_id"" : { ""$oid"" : ""50227b375dff9218248eadc5"" }, ""StringTest"" : ""Foo"", ""IntTest"" : 23, ""DateTest"" : null, ""ListTest"" : null }" + "\r\n" +
@"{ ""_id"" : { ""$oid"" : ""50227b375dff9218248eadc6"" }, ""StringTest"" : ""Bar"", ""IntTest"" : 77, ""DateTest"" : null, ""ListTest"" : null }" + "\r\n";
Establish context = () =>
{
CreateConnection();
_database.DropCollection(_collectionName);
File.WriteAllText(_testFile, _filecontent);
};
Because of = () =>
{
_runner.Import(_databaseName, _collectionName, _testFile, true);
Thread.Sleep(500);
query = _collection.AsQueryable().Select(c => c).OrderBy(c => c.Id); ;
};
It should_return_document1 = () => query.ToList().ElementAt(0).Should().BeEquivalentTo(TestDocument.DummyData1(), cfg => cfg.Excluding(d => d.Id));
It should_return_document2 = () => query.ToList().ElementAt(1).Should().BeEquivalentTo(TestDocument.DummyData2(), cfg => cfg.Excluding(d => d.Id));
It should_return_document3 = () => query.ToList().ElementAt(2).Should().BeEquivalentTo(TestDocument.DummyData3(), cfg => cfg.Excluding(d => d.Id));
Cleanup stuff = () =>
{
new FileSystem().DeleteFile(_testFile);
_runner.Dispose();
};
}
}
// ReSharper restore UnusedMember.Local
// ReSharper restore InconsistentNaming

View File

@@ -0,0 +1,118 @@
using FluentAssertions;
using Machine.Specifications;
using MELT;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using It = Machine.Specifications.It;
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
namespace Mongo2GoTests.Runner
{
[Subject("Runner Integration Test")]
public class when_using_the_inbuild_serialization : MongoIntegrationTest
{
static TestDocument findResult;
Establish context = () =>
{
CreateConnection();
_collection.InsertOne(TestDocument.DummyData1());
};
Because of = () => findResult = _collection.FindSync<TestDocument>(_ => true).First();
It should_return_a_result = () => findResult.Should().NotBeNull();
It should_hava_expected_data = () => findResult.Should().BeEquivalentTo(TestDocument.DummyData1(), cfg => cfg.Excluding(d => d.Id));
Cleanup stuff = () => _runner.Dispose();
}
[Subject("Runner Integration Test")]
public class when_using_the_new_linq_support : MongoIntegrationTest
{
static List<TestDocument> queryResult;
Establish context = () =>
{
CreateConnection();
_collection.InsertOne(TestDocument.DummyData1());
_collection.InsertOne(TestDocument.DummyData2());
_collection.InsertOne(TestDocument.DummyData3());
};
Because of = () =>
{
queryResult = (from c in _collection.AsQueryable()
where c.StringTest == TestDocument.DummyData2().StringTest || c.StringTest == TestDocument.DummyData3().StringTest
select c).ToList();
};
It should_return_two_documents = () => queryResult.Count().Should().Be(2);
It should_return_document2 = () => queryResult.ElementAt(0).IntTest = TestDocument.DummyData2().IntTest;
It should_return_document3 = () => queryResult.ElementAt(1).IntTest = TestDocument.DummyData3().IntTest;
Cleanup stuff = () => _runner.Dispose();
}
[Subject("Runner Integration Test")]
public class when_using_commands_that_create_console_output : MongoIntegrationTest
{
static List<Task> taskList = new List<Task>();
private Establish context = () =>
{
CreateConnection();
};
private Because of = () =>
{
var createIndexModel = new CreateIndexModel<TestDocument>(Builders<TestDocument>.IndexKeys.Ascending(x => x.IntTest));
taskList.Add(_collection.Indexes.CreateOneAsync(createIndexModel).WithTimeout(TimeSpan.FromMilliseconds(5000)));
taskList.Add(_collection.Indexes.DropAllAsync().WithTimeout(TimeSpan.FromMilliseconds(5000)));
};
It should_not_timeout = () => Task.WaitAll(taskList.ToArray());
Cleanup stuff = () => _runner.Dispose();
}
[Subject("Runner Integration Test")]
public class when_using_microsoft_ilogger : MongoIntegrationTest
{
static List<Task> taskList = new List<Task>();
static ITestLoggerFactory loggerFactory;
private Establish context = () =>
{
loggerFactory = TestLoggerFactory.Create();
var logger = loggerFactory.CreateLogger("MyTestLogger");
CreateConnection(logger);
};
private Because of = () =>
{
var createIndexModel = new CreateIndexModel<TestDocument>(Builders<TestDocument>.IndexKeys.Ascending(x => x.IntTest));
taskList.Add(_collection.Indexes.CreateOneAsync(createIndexModel).WithTimeout(TimeSpan.FromMilliseconds(5000)));
taskList.Add(_collection.Indexes.DropAllAsync().WithTimeout(TimeSpan.FromMilliseconds(5000)));
};
It should_not_timeout = () => Task.WaitAll(taskList.ToArray());
It should_have_received_many_logs = () =>
loggerFactory.Sink.LogEntries.Count(l => l.LogLevel == Microsoft.Extensions.Logging.LogLevel.Information)
.Should().BeGreaterThan(10);
It should_have_created_collection_statement = () => loggerFactory.Sink.LogEntries
.Count(l => l.Properties.Any(p => p.Key == "message" && (string)p.Value == "createCollection"))
.Should().BeGreaterOrEqualTo(1);
Cleanup stuff = () => _runner.Dispose();
}
}
// ReSharper restore UnusedMember.Local
// ReSharper restore InconsistentNaming

View File

@@ -0,0 +1,108 @@
using FluentAssertions;
using Machine.Specifications;
using Mongo2Go;
using Mongo2Go.Helper;
using Moq;
using System.IO;
using It = Machine.Specifications.It;
#pragma warning disable CS0618 // Type or member is obsolete
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
namespace Mongo2GoTests.Runner
{
[Subject("Runner")]
public class when_instantiating_the_runner_for_integration_test
{
static MongoDbRunner runner;
static Mock<IPortPool> portPoolMock;
static Mock<IFileSystem> fileSystemMock;
static Mock<IMongoDbProcessStarter> processStarterMock;
static Mock<IMongoBinaryLocator> binaryLocatorMock;
static string exptectedDataDirectory;
static string exptectedLogfile;
static readonly string exptectedConnectString = "mongodb://127.0.0.1:{0}/".Formatted(MongoDbDefaults.TestStartPort + 1);
Establish context = () =>
{
portPoolMock = new Mock<IPortPool>();
portPoolMock.Setup(m => m.GetNextOpenPort()).Returns(MongoDbDefaults.TestStartPort + 1);
fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(m => m.CreateFolder(Moq.It.IsAny<string>())).Callback<string>(s =>
{
exptectedDataDirectory = s;
exptectedLogfile = Path.Combine(exptectedDataDirectory, MongoDbDefaults.Lockfile);
});
var processMock = new Mock<IMongoDbProcess>();
processStarterMock = new Mock<IMongoDbProcessStarter>();
processStarterMock.Setup(m => m.Start(Moq.It.IsAny<string>(), Moq.It.IsAny<string>(), Moq.It.IsAny<int>(), false, Moq.It.IsAny<string>(), Moq.It.IsAny<ushort>(), null)).Returns(processMock.Object);
binaryLocatorMock = new Mock<IMongoBinaryLocator> ();
binaryLocatorMock.Setup(m => m.Directory).Returns(string.Empty);
};
Because of = () => runner = MongoDbRunner.StartUnitTest(portPoolMock.Object, fileSystemMock.Object, processStarterMock.Object, binaryLocatorMock.Object);
It should_create_the_data_directory = () => fileSystemMock.Verify(x => x.CreateFolder(Moq.It.Is<string>(s => s.StartsWith(Path.GetTempPath()))), Times.Exactly(1));
It should_delete_old_lock_file = () => fileSystemMock.Verify(x => x.DeleteFile(exptectedLogfile), Times.Exactly(1));
It should_start_the_process = () => processStarterMock.Verify(x => x.Start(Moq.It.IsAny<string>(), Moq.It.IsAny<string>(), Moq.It.IsAny<int>(), false, Moq.It.IsAny<string>(), Moq.It.IsAny<ushort>(), null), Times.Exactly(1));
It should_have_expected_connection_string = () => runner.ConnectionString.Should().Be(exptectedConnectString);
It should_return_an_instance_with_state_running = () => runner.State.Should().Be(State.Running);
}
[Subject("Runner")]
public class when_instantiating_the_runner_for_local_debugging
{
static MongoDbRunner runner;
static Mock<IPortWatcher> portWatcherMock;
static Mock<IProcessWatcher> processWatcherMock;
static Mock<IFileSystem> fileSystemMock;
static Mock<IMongoDbProcessStarter> processStarterMock;
static Mock<IMongoBinaryLocator> binaryLocatorMock;
static string exptectedDataDirectory;
static string exptectedLogfile;
Establish context = () =>
{
processWatcherMock = new Mock<IProcessWatcher>();
processWatcherMock.Setup(m => m.IsProcessRunning(Moq.It.IsAny<string>())).Returns(false);
portWatcherMock = new Mock<IPortWatcher>();
portWatcherMock.Setup(m => m.IsPortAvailable(Moq.It.IsAny<int>())).Returns(true);
fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(m => m.CreateFolder(Moq.It.IsAny<string>())).Callback<string>(s =>
{
exptectedDataDirectory = s;
exptectedLogfile = Path.Combine(exptectedDataDirectory, MongoDbDefaults.Lockfile);
});
var processMock = new Mock<IMongoDbProcess>();
processStarterMock = new Mock<IMongoDbProcessStarter>();
processStarterMock.Setup(m => m.Start(Moq.It.IsAny<string>(), exptectedDataDirectory, MongoDbDefaults.DefaultPort, true, false, Moq.It.IsAny<string>(), Moq.It.IsAny<ushort>(), null)).Returns(processMock.Object);
binaryLocatorMock = new Mock<IMongoBinaryLocator> ();
binaryLocatorMock.Setup(m => m.Directory).Returns(string.Empty);
};
Because of = () => runner = MongoDbRunner.StartForDebuggingUnitTest(processWatcherMock.Object, portWatcherMock.Object, fileSystemMock.Object, processStarterMock.Object, binaryLocatorMock.Object);
It should_check_for_already_running_process = () => processWatcherMock.Verify(x => x.IsProcessRunning(MongoDbDefaults.ProcessName), Times.Exactly(1));
It should_check_the_default_port = () => portWatcherMock.Verify(x => x.IsPortAvailable(MongoDbDefaults.DefaultPort), Times.Exactly(1));
It should_create_the_data_directory = () => fileSystemMock.Verify(x => x.CreateFolder(Moq.It.Is<string>(s => s.StartsWith(Path.GetTempPath()))), Times.Exactly(1));
It should_delete_old_lock_file = () => fileSystemMock.Verify(x => x.DeleteFile(exptectedLogfile), Times.Exactly(1));
It should_return_an_instance_with_state_running = () => runner.State.Should().Be(State.Running);
It should_start_the_process_without_kill = () => processStarterMock.Verify(x => x.Start(Moq.It.IsAny<string>(), exptectedDataDirectory, MongoDbDefaults.DefaultPort, true, false, Moq.It.IsAny<string>(), Moq.It.IsAny<ushort>(), null), Times.Exactly(1));
}
}
// ReSharper restore UnusedMember.Local
// ReSharper restore InconsistentNaming

View File

@@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentAssertions;
using Machine.Specifications;
using MongoDB.Driver;
namespace Mongo2GoTests.Runner
{
[Subject("Runner Transaction Test")]
public class when_transaction_completes : MongoTransactionTest
{
private static TestDocument mainDocument;
private static TestDocument dependentDocument;
Establish context = () =>
{
CreateConnection();
database.DropCollection(_mainCollectionName);
database.DropCollection(_dependentCollectionName);
_mainCollection.InsertOne(TestDocument.DummyData2());
_dependentCollection.InsertOne(TestDocument.DummyData2());
};
private Because of = () =>
{
var filter = Builders<TestDocument>.Filter.Where(x => x.IntTest == 23);
var update = Builders<TestDocument>.Update.Inc(i => i.IntTest, 10);
using (var sessionHandle = client.StartSession())
{
try
{
var i = 0;
while (i < 10)
{
try
{
i++;
sessionHandle.StartTransaction(new TransactionOptions(
readConcern: ReadConcern.Local,
writeConcern: WriteConcern.W1));
try
{
var first = _mainCollection.UpdateOne(sessionHandle, filter, update);
var second = _dependentCollection.UpdateOne(sessionHandle, filter, update);
}
catch (Exception)
{
sessionHandle.AbortTransaction();
throw;
}
var j = 0;
while (j < 10)
{
try
{
j++;
sessionHandle.CommitTransaction();
break;
}
catch (MongoException e)
{
if (e.HasErrorLabel("UnknownTransactionCommitResult"))
continue;
throw;
}
}
break;
}
catch (MongoException e)
{
if (e.HasErrorLabel("TransientTransactionError"))
continue;
throw;
}
}
}
catch (Exception)
{
}
}
mainDocument = _mainCollection.FindSync(Builders<TestDocument>.Filter.Empty).FirstOrDefault();
dependentDocument = _dependentCollection.FindSync(Builders<TestDocument>.Filter.Empty).FirstOrDefault();
};
It main_should_be_33 = () => mainDocument.IntTest.Should().Be(33);
It dependent_should_be_33 = () => dependentDocument.IntTest.Should().Be(33);
Cleanup cleanup = () => _runner.Dispose();
}
[Subject("Runner Transaction Test")]
public class when_transaction_is_aborted_before_commit : MongoTransactionTest
{
private static TestDocument mainDocument;
private static TestDocument dependentDocument;
private static TestDocument mainDocument_before_commit;
private static TestDocument dependentDocument_before_commit;
Establish context = () =>
{
CreateConnection();
database.DropCollection(_mainCollectionName);
database.DropCollection(_dependentCollectionName);
_mainCollection.InsertOne(TestDocument.DummyData2());
_dependentCollection.InsertOne(TestDocument.DummyData2());
};
private Because of = () =>
{
var filter = Builders<TestDocument>.Filter.Where(x => x.IntTest == 23);
var update = Builders<TestDocument>.Update.Inc(i => i.IntTest, 10);
using (var sessionHandle = client.StartSession())
{
try
{
var i = 0;
while (i < 2)
{
try
{
i++;
sessionHandle.StartTransaction(new TransactionOptions(
readConcern: ReadConcern.Local,
writeConcern: WriteConcern.W1));
try
{
var first = _mainCollection.UpdateOne(sessionHandle, filter, update);
var second = _dependentCollection.UpdateOne(sessionHandle, filter, update);
mainDocument_before_commit = _mainCollection.FindSync(sessionHandle, Builders<TestDocument>.Filter.Empty).ToList().FirstOrDefault();
dependentDocument_before_commit = _dependentCollection.FindSync(sessionHandle, Builders<TestDocument>.Filter.Empty).ToList().FirstOrDefault();
}
catch (Exception)
{
sessionHandle.AbortTransaction();
throw;
}
//Throw exception and do not commit
throw new ApplicationException();
}
catch (MongoException e)
{
if (e.HasErrorLabel("TransientTransactionError"))
continue;
throw;
}
}
}
catch (Exception)
{
}
}
mainDocument = _mainCollection.FindSync(Builders<TestDocument>.Filter.Empty).FirstOrDefault();
dependentDocument = _dependentCollection.FindSync(Builders<TestDocument>.Filter.Empty).FirstOrDefault();
};
It main_should_be_still_23_after_aborting = () => mainDocument.IntTest.Should().Be(23);
It dependent_should_be_still_23_after_aborting = () => dependentDocument.IntTest.Should().Be(23);
It main_should_be_33_before_aborting = () => mainDocument_before_commit.IntTest.Should().Be(33);
It dependent_should_be_33_before_aborting = () => dependentDocument_before_commit.IntTest.Should().Be(33);
Cleanup cleanup = () => _runner.Dispose();
}
[Subject("Runner Transaction Test")]
public class when_replica_set_not_ready_before_timeout_expires : MongoTransactionTest
{
private static Exception exception;
Because of = () => exception = Catch.Exception(() => CreateConnection(0));
// this passes on Windows (TimeoutException as expected)
// but breaks on my Mac (MongoDB.Driver.MongoCommandException: Command replSetInitiate failed: already initialized.)
It should_throw_timeout_exception = () => {
Console.WriteLine(exception.ToString());
exception.Should().BeOfType<TimeoutException>();
};
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Mongo2GoTests.Runner
{
public class TestDocument
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string StringTest { get; set; }
public int IntTest { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime? DateTest { get; set; }
public List<string> ListTest { get; set; }
public static TestDocument DummyData1()
{
return new TestDocument
{
StringTest = "Hello World",
IntTest = 42,
DateTest = new DateTime(1984, 09, 30, 6, 6, 6, 171, DateTimeKind.Utc).ToLocalTime(),
ListTest = new List<string> {"I", "am", "a", "list", "of", "strings"}
};
}
public static TestDocument DummyData2()
{
return new TestDocument
{
StringTest = "Foo",
IntTest = 23,
};
}
public static TestDocument DummyData3()
{
return new TestDocument
{
StringTest = "Bar",
IntTest = 77,
};
}
}
}

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ByteSizeLib;
using Espresso3389.HttpStream;
using HttpProgress;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip;
namespace MongoDownloader
{
internal class ArchiveExtractor
{
private static readonly int CachePageSize = Convert.ToInt32(ByteSize.FromMebiBytes(4).Bytes);
private readonly Options _options;
private readonly BinaryStripper? _binaryStripper;
public ArchiveExtractor(Options options, BinaryStripper? binaryStripper)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_binaryStripper = binaryStripper;
}
public async Task<IEnumerable<Task<ByteSize>>> DownloadExtractZipArchiveAsync(Download download, DirectoryInfo extractDirectory, ArchiveProgress progress, CancellationToken cancellationToken)
{
var bytesTransferred = 0L;
using var headResponse = await _options.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, download.Archive.Url), cancellationToken);
var contentLength = headResponse.Content.Headers.ContentLength ?? 0;
var cacheFile = new FileInfo(Path.Combine(_options.CacheDirectory.FullName, download.Archive.Url.Segments.Last()));
await using var cacheStream = new FileStream(cacheFile.FullName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
var stopwatch = Stopwatch.StartNew();
await using var httpStream = new HttpStream(download.Archive.Url, cacheStream, ownStream: false, CachePageSize, cached: null);
httpStream.RangeDownloaded += (_, args) =>
{
bytesTransferred += args.Length;
progress.Report(new CopyProgress(stopwatch.Elapsed, 0, bytesTransferred, contentLength));
};
using var zipFile = new ZipFile(httpStream);
var binaryRegex = _options.Binaries[(download.Product, download.Platform)];
var licenseRegex = _options.Licenses[(download.Product, download.Platform)];
var stripTasks = new List<Task<ByteSize>>();
foreach (var entry in zipFile.Cast<ZipEntry>().Where(e => e.IsFile))
{
var nameParts = entry.Name.Split('\\', '/').Skip(1).ToList();
var zipEntryPath = string.Join('/', nameParts);
var isBinaryFile = binaryRegex.IsMatch(zipEntryPath);
var isLicenseFile = licenseRegex.IsMatch(zipEntryPath);
if (isBinaryFile || isLicenseFile)
{
var destinationPathParts = isLicenseFile ? nameParts.Prepend(ProductDirectoryName(download.Product)) : nameParts;
var destinationFile = new FileInfo(Path.Combine(destinationPathParts.Prepend(extractDirectory.FullName).ToArray()));
destinationFile.Directory?.Create();
await using var destinationStream = destinationFile.OpenWrite();
await using var inputStream = zipFile.GetInputStream(entry);
await inputStream.CopyToAsync(destinationStream, cancellationToken);
if (isBinaryFile && _binaryStripper is not null)
{
stripTasks.Add(_binaryStripper.StripAsync(destinationFile, cancellationToken));
}
}
}
progress.Report(new CopyProgress(stopwatch.Elapsed, 0, bytesTransferred, bytesTransferred));
return stripTasks;
}
public IEnumerable<Task<ByteSize>> ExtractArchive(Download download, FileInfo archive, DirectoryInfo extractDirectory, CancellationToken cancellationToken)
{
switch (Path.GetExtension(archive.Name))
{
case ".tgz":
return ExtractTarGzipArchive(download, archive, extractDirectory, cancellationToken);
default:
throw new NotSupportedException($"Only .tgz archives are currently supported. \"{archive.FullName}\" can not be extracted.");
}
}
private IEnumerable<Task<ByteSize>> ExtractTarGzipArchive(Download download, FileInfo archive, DirectoryInfo extractDirectory, CancellationToken cancellationToken)
{
// See https://github.com/icsharpcode/SharpZipLib/wiki/GZip-and-Tar-Samples#-simple-full-extract-from-a-tgz-targz
using var archiveStream = archive.OpenRead();
using var gzipStream = new GZipInputStream(archiveStream);
using var tarArchive = TarArchive.CreateInputTarArchive(gzipStream, Encoding.UTF8);
var extractedFileNames = new List<string>();
tarArchive.ProgressMessageEvent += (_, entry, _) =>
{
cancellationToken.ThrowIfCancellationRequested();
extractedFileNames.Add(entry.Name);
};
tarArchive.ExtractContents(extractDirectory.FullName);
return CleanupExtractedFiles(download, extractDirectory, extractedFileNames);
}
private IEnumerable<Task<ByteSize>> CleanupExtractedFiles(Download download, DirectoryInfo extractDirectory, IEnumerable<string> extractedFileNames)
{
var rootDirectoryToDelete = new HashSet<string>();
var binaryRegex = _options.Binaries[(download.Product, download.Platform)];
var licenseRegex = _options.Licenses[(download.Product, download.Platform)];
var stripTasks = new List<Task<ByteSize>>();
foreach (var extractedFileName in extractedFileNames.Select(e => e.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar)))
{
var extractedFile = new FileInfo(Path.Combine(extractDirectory.FullName, extractedFileName));
var parts = extractedFileName.Split(Path.DirectorySeparatorChar);
var entryFileName = string.Join("/", parts.Skip(1));
rootDirectoryToDelete.Add(parts[0]);
var isBinaryFile = binaryRegex.IsMatch(entryFileName);
var isLicenseFile = licenseRegex.IsMatch(entryFileName);
if (!(isBinaryFile || isLicenseFile))
{
extractedFile.Delete();
}
else
{
var destinationPathParts = parts.Skip(1);
if (isLicenseFile)
{
destinationPathParts = destinationPathParts.Prepend(ProductDirectoryName(download.Product));
}
var destinationFile = new FileInfo(Path.Combine(destinationPathParts.Prepend(extractDirectory.FullName).ToArray()));
destinationFile.Directory?.Create();
extractedFile.MoveTo(destinationFile.FullName);
if (isBinaryFile && _binaryStripper is not null)
{
stripTasks.Add(_binaryStripper.StripAsync(destinationFile));
}
}
}
var rootArchiveDirectory = new DirectoryInfo(Path.Combine(extractDirectory.FullName, rootDirectoryToDelete.Single()));
var binDirectory = new DirectoryInfo(Path.Combine(rootArchiveDirectory.FullName, "bin"));
binDirectory.Delete(recursive: false);
rootArchiveDirectory.Delete(recursive: false);
return stripTasks;
}
private static string ProductDirectoryName(Product product)
{
return product switch
{
Product.CommunityServer => "community-server",
Product.DatabaseTools => "database-tools",
_ => throw new ArgumentOutOfRangeException(nameof(product), product, null)
};
}
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ByteSizeLib;
using HttpProgress;
using Spectre.Console;
namespace MongoDownloader
{
public class ArchiveProgress : IProgress<ICopyProgress>
{
private readonly ProgressTask _archiveProgress;
private readonly ProgressTask _globalProgress;
private readonly IEnumerable<ProgressTask> _allArchiveProgresses;
private readonly Download _download;
private readonly string _completedDescription;
public ArchiveProgress(ProgressTask archiveProgress, ProgressTask globalProgress, IEnumerable<ProgressTask> allArchiveProgresses, Download download, string completedDescription)
{
_archiveProgress = archiveProgress ?? throw new ArgumentNullException(nameof(archiveProgress));
_globalProgress = globalProgress ?? throw new ArgumentNullException(nameof(globalProgress));
_allArchiveProgresses = allArchiveProgresses ?? throw new ArgumentNullException(nameof(allArchiveProgresses));
_download = download ?? throw new ArgumentNullException(nameof(download));
_completedDescription = completedDescription ?? throw new ArgumentNullException(nameof(completedDescription));
}
public void Report(ICopyProgress progress)
{
_archiveProgress.Value = progress.BytesTransferred;
_archiveProgress.MaxValue = progress.ExpectedBytes;
string text;
bool isIndeterminate;
if (progress.BytesTransferred < progress.ExpectedBytes)
{
var speed = ByteSize.FromBytes(progress.BytesTransferred / progress.TransferTime.TotalSeconds);
text = $"Downloading {_download} from {_download.Archive.Url} at {speed:0.0}/s";
isIndeterminate = false;
}
else
{
text = $"Downloaded {_download}";
isIndeterminate = true;
// Cheat by subtracting 1 so that the progress stays at 99% in indeterminate mode for
// remaining tasks (stripping) to complete with an indeterminate progress bar
_archiveProgress.Value = progress.BytesTransferred - 1;
}
Report(text, isIndeterminate);
lock (_globalProgress)
{
_globalProgress.Value = _allArchiveProgresses.Sum(e => e.Value);
_globalProgress.MaxValue = _allArchiveProgresses.Sum(e => e.MaxValue);
}
}
public void Report(string action)
{
Report(action, isIndeterminate: true);
}
public void ReportCompleted(ByteSize strippedSize)
{
_archiveProgress.Value = _archiveProgress.MaxValue;
lock (_globalProgress)
{
if (_allArchiveProgresses.All(e => e.IsFinished))
{
_globalProgress.Description = _completedDescription;
_globalProgress.Value = _globalProgress.MaxValue;
}
}
var saved = strippedSize.Bytes > 0 ? $" (saved {strippedSize:#.#} by stripping)" : "";
Report($"Extracted {_download}{saved}", isIndeterminate: false);
}
private void Report(string description, bool isIndeterminate)
{
_archiveProgress.Description = description;
_archiveProgress.IsIndeterminate = isIndeterminate;
lock (_globalProgress)
{
_globalProgress.IsIndeterminate = _allArchiveProgresses.All(e => e.IsFinished || e.IsIndeterminate);
}
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using ByteSizeLib;
using CliWrap;
namespace MongoDownloader
{
public class BinaryStripper
{
private const string LlvmStripToolName = "llvm-strip";
private readonly string _llvmStripPath;
private BinaryStripper(string llvmStripPath)
{
_llvmStripPath = llvmStripPath ?? throw new ArgumentNullException(nameof(llvmStripPath));
}
public static async Task<BinaryStripper> CreateAsync(CancellationToken cancellationToken)
{
var llvmStripPath = await GetLlvmStripPathAsync(cancellationToken);
return new BinaryStripper(llvmStripPath);
}
public async Task<ByteSize> StripAsync(FileInfo executable, CancellationToken cancellationToken = default)
{
var sizeBefore = ByteSize.FromBytes(executable.Length);
await Cli.Wrap(_llvmStripPath).WithArguments(executable.FullName).ExecuteAsync(cancellationToken);
executable.Refresh();
var sizeAfter = ByteSize.FromBytes(executable.Length);
return sizeBefore - sizeAfter;
}
private static async Task<string> GetLlvmStripPathAsync(CancellationToken cancellationToken)
{
try
{
await Cli.Wrap(LlvmStripToolName).WithArguments("--version").ExecuteAsync(cancellationToken);
// llvm-strip is on the PATH
return LlvmStripToolName;
}
catch (Win32Exception exception) when (exception.NativeErrorCode == 2)
{
// llvm-strip is NOT in the PATH, let's search with homebrew
var llvmStripToolPath = await TryGetLlvmStripPathWithHomebrew();
if (llvmStripToolPath != null)
{
return llvmStripToolPath;
}
throw new FileNotFoundException($"The \"{LlvmStripToolName}\" tool was not found.");
}
}
private static async Task<string?> TryGetLlvmStripPathWithHomebrew()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return null;
}
string? llvmStripToolPath = null;
try
{
await Cli.Wrap("brew")
// don't validate exit code, if `brew list llvm` fails it's because the llvm formula is not installed
.WithValidation(CommandResultValidation.None)
.WithArguments(new[] {"list", "llvm"})
.WithStandardOutputPipe(PipeTarget.ToDelegate(line =>
{
if (llvmStripToolPath == null && line.EndsWith(LlvmStripToolName))
{
llvmStripToolPath = line;
}
}))
.ExecuteAsync();
}
catch (Win32Exception exception) when (exception.NativeErrorCode == 2)
{
// brew is not installed
return null;
}
return llvmStripToolPath;
}
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CollectionNeverUpdated.Global
namespace MongoDownloader
{
public enum Platform
{
Linux,
// ReSharper disable once InconsistentNaming
macOS,
Windows,
}
public enum Product
{
CommunityServer,
DatabaseTools,
}
/// <summary>
/// The root object of the JSON describing the available releases.
/// </summary>
public class Release
{
[JsonPropertyName("versions")]
public List<Version> Versions { get; set; } = new();
}
public class Version
{
[JsonPropertyName("version")]
public string Number { get; set; } = "";
[JsonPropertyName("production_release")]
public bool Production { get; set; } = false;
[JsonPropertyName("downloads")]
public List<Download> Downloads { get; set; } = new();
}
public class Download
{
/// <summary>
/// Used to identify the platform for the Community Server archives
/// </summary>
[JsonPropertyName("target")]
public string Target { get; set; } = "";
/// <summary>
/// Used to identify the platform for the Database Tools archives
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = "";
[JsonPropertyName("arch")]
public string Arch { get; set; } = "";
[JsonPropertyName("edition")]
public string Edition { get; set; } = "";
[JsonPropertyName("archive")]
public Archive Archive { get; set; } = new();
public Product Product { get; set; }
public Platform Platform { get; set; }
public Architecture Architecture { get; set; }
public override string ToString() => $"{Product} for {Platform}/{Architecture.ToString().ToLowerInvariant()}";
}
public class Archive
{
[JsonPropertyName("url")]
public Uri Url { get; set; } = default!;
}
}

View File

@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ByteSizeLib;
using HttpProgress;
using Spectre.Console;
namespace MongoDownloader
{
internal class MongoDbDownloader
{
private readonly ArchiveExtractor _extractor;
private readonly Options _options;
public MongoDbDownloader(ArchiveExtractor extractor, Options options)
{
_extractor = extractor ?? throw new ArgumentNullException(nameof(extractor));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
public async Task<ByteSize> RunAsync(DirectoryInfo toolsDirectory, CancellationToken cancellationToken)
{
var strippedSize = await AnsiConsole
.Progress()
.Columns(
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new DownloadedColumn(),
new TaskDescriptionColumn { Alignment = Justify.Left }
)
.StartAsync(async context => await RunAsync(context, toolsDirectory, cancellationToken));
return strippedSize;
}
private async Task<ByteSize> RunAsync(ProgressContext context, DirectoryInfo toolsDirectory, CancellationToken cancellationToken)
{
const double initialMaxValue = double.Epsilon;
var globalProgress = context.AddTask("Downloading MongoDB", maxValue: initialMaxValue);
var (communityServerVersion, communityServerDownloads) = await GetCommunityServerDownloadsAsync(cancellationToken);
globalProgress.Description = $"Downloading MongoDB Community Server {communityServerVersion.Number}";
var (databaseToolsVersion, databaseToolsDownloads) = await GetDatabaseToolsDownloadsAsync(cancellationToken);
globalProgress.Description = $"Downloading MongoDB Community Server {communityServerVersion.Number} and Database Tools {databaseToolsVersion.Number}";
var tasks = new List<Task<ByteSize>>();
var allArchiveProgresses = new List<ProgressTask>();
foreach (var download in communityServerDownloads.Concat(databaseToolsDownloads))
{
var archiveProgress = context.AddTask($"Downloading {download} from {download.Archive.Url}", maxValue: initialMaxValue);
var directoryName = $"mongodb-{download.Platform.ToString().ToLowerInvariant()}-{download.Architecture.ToString().ToLowerInvariant()}-{communityServerVersion.Number}-database-tools-{databaseToolsVersion.Number}";
var extractDirectory = new DirectoryInfo(Path.Combine(toolsDirectory.FullName, directoryName));
allArchiveProgresses.Add(archiveProgress);
var progress = new ArchiveProgress(archiveProgress, globalProgress, allArchiveProgresses, download, $"✅ Downloaded and extracted MongoDB Community Server {communityServerVersion.Number} and Database Tools {databaseToolsVersion.Number} into {new Uri(toolsDirectory.FullName).AbsoluteUri}");
tasks.Add(ProcessArchiveAsync(download, extractDirectory, progress, cancellationToken));
}
var strippedSizes = await Task.WhenAll(tasks);
return strippedSizes.Aggregate(new ByteSize(0), (current, strippedSize) => current + strippedSize);
}
private async Task<ByteSize> ProcessArchiveAsync(Download download, DirectoryInfo extractDirectory, ArchiveProgress progress, CancellationToken cancellationToken)
{
IEnumerable<Task<ByteSize>> stripTasks;
var archiveExtension = Path.GetExtension(download.Archive.Url.AbsolutePath);
if (archiveExtension == ".zip")
{
stripTasks = await _extractor.DownloadExtractZipArchiveAsync(download, extractDirectory, progress, cancellationToken);
}
else
{
var archiveFileInfo = await DownloadArchiveAsync(download.Archive, progress, cancellationToken);
stripTasks = _extractor.ExtractArchive(download, archiveFileInfo, extractDirectory, cancellationToken);
}
progress.Report("Stripping binaries");
var completedStripTasks = await Task.WhenAll(stripTasks);
var totalStrippedSize = completedStripTasks.Aggregate(new ByteSize(0), (current, strippedSize) => current + strippedSize);
progress.ReportCompleted(totalStrippedSize);
return totalStrippedSize;
}
private async Task<FileInfo> DownloadArchiveAsync(Archive archive, IProgress<ICopyProgress> progress, CancellationToken cancellationToken)
{
_options.CacheDirectory.Create();
var destinationFile = new FileInfo(Path.Combine(_options.CacheDirectory.FullName, archive.Url.Segments.Last()));
var useCache = bool.TryParse(Environment.GetEnvironmentVariable("MONGO2GO_DOWNLOADER_USE_CACHED_FILE") ?? "", out var useCachedFile) && useCachedFile;
if (useCache && destinationFile.Exists)
{
progress.Report(new CopyProgress(TimeSpan.Zero, 0, 1, 1));
return destinationFile;
}
await using var destinationStream = destinationFile.OpenWrite();
await _options.HttpClient.GetAsync(archive.Url.AbsoluteUri, destinationStream, progress, cancellationToken);
return destinationFile;
}
private async Task<(Version version, IEnumerable<Download> downloads)> GetCommunityServerDownloadsAsync(CancellationToken cancellationToken)
{
var release = await _options.HttpClient.GetFromJsonAsync<Release>(_options.CommunityServerUrl, cancellationToken) ?? throw new InvalidOperationException($"Failed to deserialize {nameof(Release)}");
var version = release.Versions.FirstOrDefault(e => e.Production) ?? throw new InvalidOperationException("No Community Server production version was found");
var downloads = Enum.GetValues<Platform>().SelectMany(platform => GetDownloads(platform, Product.CommunityServer, version, _options, _options.Edition));
return (version, downloads);
}
private async Task<(Version version, IEnumerable<Download> downloads)> GetDatabaseToolsDownloadsAsync(CancellationToken cancellationToken)
{
var release = await _options.HttpClient.GetFromJsonAsync<Release>(_options.DatabaseToolsUrl, cancellationToken) ?? throw new InvalidOperationException($"Failed to deserialize {nameof(Release)}");
var version = release.Versions.FirstOrDefault() ?? throw new InvalidOperationException("No Database Tools version was found");
var downloads = Enum.GetValues<Platform>().SelectMany(platform => GetDownloads(platform, Product.DatabaseTools, version, _options));
return (version, downloads);
}
private static IEnumerable<Download> GetDownloads(Platform platform, Product product, Version version, Options options, Regex? editionRegex = null)
{
var platformRegex = options.PlatformIdentifiers[platform];
Func<Download, bool> platformPredicate = product switch
{
Product.CommunityServer => download => platformRegex.IsMatch(download.Target),
Product.DatabaseTools => download => platformRegex.IsMatch(download.Name),
_ => throw new ArgumentOutOfRangeException(nameof(product), product, $"The value of argument '{nameof(product)}' ({product}) is invalid for enum type '{nameof(Product)}'.")
};
foreach (var architecture in options.Architectures[platform])
{
var architectureRegex = options.ArchitectureIdentifiers[architecture];
var matchingDownloads = version.Downloads
.Where(platformPredicate)
.Where(e => architectureRegex.IsMatch(e.Arch))
.Where(e => editionRegex?.IsMatch(e.Edition) ?? true)
.ToList();
if (matchingDownloads.Count == 0)
{
var downloads = version.Downloads.OrderBy(e => e.Target).ThenBy(e => e.Arch);
var messages = Enumerable.Empty<string>()
.Append($"Download not found for {platform}/{architecture}.")
.Append($" Available downloads for {product} {version.Number}:")
.Concat(downloads.Select(e => $" - {e.Target}/{e.Arch} ({e.Edition})"));
throw new InvalidOperationException(string.Join(Environment.NewLine, messages));
}
if (matchingDownloads.Count > 1)
{
throw new InvalidOperationException($"Found {matchingDownloads.Count} downloads for {platform}/{architecture} but expected to find only one.");
}
var download = matchingDownloads[0];
download.Platform = platform;
download.Architecture = architecture;
download.Product = product;
yield return download;
}
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="bloomtom.HttpProgress" Version="2.3.2" />
<PackageReference Include="ByteSize" Version="2.1.0" />
<PackageReference Include="CliWrap" Version="3.3.3" />
<PackageReference Include="Espresso3389.HttpStream" Version="2.0.52.3" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="Spectre.Console" Version="0.42.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace MongoDownloader
{
internal class Options
{
/// <summary>
/// The <see cref="HttpClient"/> instance used to fetch data over HTTP.
/// </summary>
public HttpClient HttpClient { get; init; } = new();
/// <summary>
/// The URL of the MongoDB Community Server download information JSON.
/// </summary>
public string CommunityServerUrl { get; init; } = "https://s3.amazonaws.com/downloads.mongodb.org/current.json";
/// <summary>
/// The URL of the MongoDB Database Tools download information JSON.
/// </summary>
public string DatabaseToolsUrl { get; init; } = "https://s3.amazonaws.com/downloads.mongodb.org/tools/db/release.json";
/// <summary>
/// The directory to store the downloaded archive files.
/// </summary>
public DirectoryInfo CacheDirectory { get; init; } = new(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.InternetCache), nameof(MongoDownloader)));
/// <summary>
/// The architectures to download for a given platform.
/// </summary>
public IReadOnlyDictionary<Platform, IReadOnlyCollection<Architecture>> Architectures { get; init; } = new Dictionary<Platform, IReadOnlyCollection<Architecture>>
{
[Platform.Linux] = new[] { Architecture.Arm64, Architecture.X64 },
[Platform.macOS] = new[] { Architecture.X64 },
[Platform.Windows] = new[] { Architecture.X64 },
};
/// <summary>
/// The edition of the archive to download.
/// </summary>
/// <remarks>macOS and Windows use <c>base</c> and Linux uses <c>targeted</c> for the community edition</remarks>
public Regex Edition { get; init; } = new(@"base|targeted");
/// <summary>
/// The regular expressions used to identify platform-specific archives to download.
/// </summary>
public IReadOnlyDictionary<Platform, Regex> PlatformIdentifiers { get; init; } = new Dictionary<Platform, Regex>
{
[Platform.Linux] = new(@"ubuntu2004", RegexOptions.IgnoreCase),
[Platform.macOS] = new(@"macOS", RegexOptions.IgnoreCase),
[Platform.Windows] = new(@"windows", RegexOptions.IgnoreCase),
};
/// <summary>
/// The regular expressions used to identify architectures to download.
/// </summary>
public IReadOnlyDictionary<Architecture, Regex> ArchitectureIdentifiers { get; init; } = new Dictionary<Architecture, Regex>
{
[Architecture.Arm64] = new("arm64|aarch64", RegexOptions.IgnoreCase),
[Architecture.X64] = new("x86_64", RegexOptions.IgnoreCase),
};
/// <summary>
/// A dictionary describing how to match MongoDB binaries inside the zip archives.
/// <para/>
/// The key is a tuple with the <see cref="Product"/>/<see cref="Platform"/> and the
/// value is a regular expressions to match against the zip file name entry.
/// </summary>
public IReadOnlyDictionary<(Product, Platform), Regex> Binaries { get; init; } = new Dictionary<(Product, Platform), Regex>
{
[(Product.CommunityServer, Platform.Linux)] = new(@"bin/mongod"),
[(Product.CommunityServer, Platform.macOS)] = new(@"bin/mongod"),
[(Product.CommunityServer, Platform.Windows)] = new(@"bin/mongod\.exe"),
[(Product.DatabaseTools, Platform.Linux)] = new(@"bin/(mongoexport|mongoimport)"),
[(Product.DatabaseTools, Platform.macOS)] = new(@"bin/(mongoexport|mongoimport)"),
[(Product.DatabaseTools, Platform.Windows)] = new(@"bin/(mongoexport|mongoimport)\.exe"),
};
/// <summary>
/// A dictionary describing how to match licence files inside the zip archives.
/// <para/>
/// The key is a tuple with the <see cref="Product"/>/<see cref="Platform"/> and the
/// value is a regular expressions to match against the zip file name entry.
/// </summary>
public IReadOnlyDictionary<(Product, Platform), Regex> Licenses { get; init; } = new Dictionary<(Product, Platform), Regex>
{
// The regular expression matches anything at the zip top level, i.e. does not contain any slash (/) character
[(Product.CommunityServer, Platform.Linux)] = new(@"^[^/]+$"),
[(Product.CommunityServer, Platform.macOS)] = new(@"^[^/]+$"),
[(Product.CommunityServer, Platform.Windows)] = new(@"^[^/]+$"),
[(Product.DatabaseTools, Platform.Linux)] = new(@"^[^/]+$"),
[(Product.DatabaseTools, Platform.macOS)] = new(@"^[^/]+$"),
[(Product.DatabaseTools, Platform.Windows)] = new(@"^[^/]+$"),
};
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console;
namespace MongoDownloader
{
internal static class Program
{
private static async Task<int> Main(string[] args)
{
try
{
var toolsDirectory = GetToolsDirectory();
foreach (DirectoryInfo dir in toolsDirectory.EnumerateDirectories())
{
dir.Delete(true);
}
var cancellationTokenSource = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
// Try to cancel gracefully the first time, then abort the process the second time Ctrl+C is pressed
eventArgs.Cancel = !cancellationTokenSource.IsCancellationRequested;
cancellationTokenSource.Cancel();
};
var options = new Options();
var performStrip = args.All(e => e != "--no-strip");
var binaryStripper = performStrip ? await GetBinaryStripperAsync(cancellationTokenSource.Token) : null;
var archiveExtractor = new ArchiveExtractor(options, binaryStripper);
var downloader = new MongoDbDownloader(archiveExtractor, options);
var strippedSize = await downloader.RunAsync(toolsDirectory, cancellationTokenSource.Token);
if (performStrip)
{
AnsiConsole.WriteLine($"Saved {strippedSize:#.#} by stripping executables");
}
return 0;
}
catch (Exception exception)
{
if (exception is not OperationCanceledException)
{
AnsiConsole.WriteException(exception, ExceptionFormats.ShortenPaths);
}
return 1;
}
}
private static DirectoryInfo GetToolsDirectory()
{
for (var directory = new DirectoryInfo("."); directory != null; directory = directory.Parent)
{
var toolsDirectory = directory.GetDirectories("tools", SearchOption.TopDirectoryOnly).SingleOrDefault();
if (toolsDirectory?.Exists ?? false)
{
return toolsDirectory;
}
}
throw new InvalidOperationException("The tools directory was not found");
}
private static async Task<BinaryStripper?> GetBinaryStripperAsync(CancellationToken cancellationToken)
{
try
{
return await BinaryStripper.CreateAsync(cancellationToken);
}
catch (FileNotFoundException exception)
{
string installCommand;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
installCommand = "brew install llvm";
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
installCommand = "scoop install llvm";
else
installCommand = "apt-get install llvm";
throw new Exception($"{exception.Message} Either install llvm with `{installCommand}` or run MongoDownloader with the --no-strip option to skip binary stripping.", exception);
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB