diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7ecd38a41..9230f13ed 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,41 +2,58 @@ "version": "2.0.0", "tasks": [ { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/src/AnalysisPrograms/AnalysisPrograms.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/AnalysisPrograms/AnalysisPrograms.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } }, { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/src/AnalysisPrograms/AnalysisPrograms.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" + "label": "test", + "command": "dotnet", + "type": "process", + "args": [ + "test" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "test", + "isDefault": true + } }, { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "${workspaceFolder}/src/AnalysisPrograms/AnalysisPrograms.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/AnalysisPrograms/AnalysisPrograms.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/src/AnalysisPrograms/AnalysisPrograms.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" } ] } \ No newline at end of file diff --git a/src/Acoustics.Shared/Extensions/ProcessExtensions.cs b/src/Acoustics.Shared/Extensions/ProcessExtensions.cs new file mode 100644 index 000000000..c0d6918ac --- /dev/null +++ b/src/Acoustics.Shared/Extensions/ProcessExtensions.cs @@ -0,0 +1,110 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). +// +// +// Defines the Utilities type. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace System +{ + using System.ComponentModel; + using System.Diagnostics; + using System.Runtime.InteropServices; + using static Acoustics.Shared.AppConfigHelper; + + public static class ProcessExtensions + { + /// + /// A utility class to determine a process parent. + /// http://stackoverflow.com/a/3346055. + /// + [StructLayout(LayoutKind.Sequential)] + public struct ParentProcessUtilities + { +#if DEBUG + #pragma warning disable SA1310 + // These members must match PROCESS_BASIC_INFORMATION + internal IntPtr Reserved1; + + internal IntPtr PebBaseAddress; + + internal IntPtr Reserved2_0; + + internal IntPtr Reserved2_1; + + internal IntPtr UniqueProcessId; + + internal IntPtr InheritedFromUniqueProcessId; + #pragma warning restore + + // TODO This import is windows only, replace with .NET Core API + [DllImport("ntdll.dll")] + private static extern int NtQueryInformationProcess( + IntPtr processHandle, + int processInformationClass, + ref ParentProcessUtilities processInformation, + int processInformationLength, + out int returnLength); + + #pragma warning disable SA1202 + /// + /// !WARNING Windows OS only + /// Gets the parent process of the current process. + /// Linux support blocked by https://github.com/dotnet/runtime/issues/24423. + /// + /// An instance of the Process class. + public static Process GetParentProcess() + { + return GetParentProcess(Process.GetCurrentProcess().Handle); + } + + /// + /// !WARNING Windows OS only + /// Gets the parent process of specified process. + /// + /// The process id. + /// An instance of the Process class. + public static Process GetParentProcess(int id) + { + Process process = Process.GetProcessById(id); + return GetParentProcess(process.Handle); + } + + /// + /// !WARNING Windows OS only + /// Gets the parent process of a specified process. + /// + /// The process handle. + /// An instance of the Process class. + public static Process GetParentProcess(IntPtr handle) + { + if (!IsWindows) + { + return null; + } + + ParentProcessUtilities pbi = default; + + int status = NtQueryInformationProcess(handle, 0, ref pbi, Marshal.SizeOf(pbi), out var _); + if (status != 0) + { + throw new Win32Exception(status); + } + + try + { + return Process.GetProcessById(pbi.InheritedFromUniqueProcessId.ToInt32()); + } + catch (ArgumentException) + { + // not found + return null; + } + } + #pragma warning restore +#endif + } + } +} \ No newline at end of file diff --git a/src/Acoustics.Shared/Extensions/ExtensionsSystem.cs b/src/Acoustics.Shared/Extensions/SystemExtensions.cs similarity index 76% rename from src/Acoustics.Shared/Extensions/ExtensionsSystem.cs rename to src/Acoustics.Shared/Extensions/SystemExtensions.cs index 89c6c084a..a027abd32 100644 --- a/src/Acoustics.Shared/Extensions/ExtensionsSystem.cs +++ b/src/Acoustics.Shared/Extensions/SystemExtensions.cs @@ -1,5 +1,5 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). // // @@ -9,27 +9,30 @@ namespace System { - using Collections.Generic; - using Collections.Specialized; - using ComponentModel; - using Diagnostics; - using IO; - using Linq; - using Linq.Expressions; - using Runtime.InteropServices; - using Runtime.Serialization; - using Runtime.Serialization.Formatters.Binary; - using Text; - using Text.RegularExpressions; - - public static class ExtensionsGeneral + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.IO; + using System.Linq; + using System.Linq.Expressions; + using System.Runtime.Serialization; + using System.Runtime.Serialization.Formatters.Binary; + using System.Text; + using System.Text.RegularExpressions; + + public static class SystemExtensions { - /// Deserialise byte array to object. + /// + /// Gets ExecutingDirectory. + /// + public static string ExecutingDirectory => Path.GetDirectoryName(Reflection.Assembly.GetExecutingAssembly().Location); + + /// Deserialize byte array to object. /// /// /// The bytes. /// - /// Deserialised object. + /// Deserialized object. /// public static object BinaryDeserialize(this byte[] bytes) { @@ -46,7 +49,7 @@ public static object BinaryDeserialize(this byte[] bytes) } /// - /// Deserialise byte array to object. + /// Deserialize byte array to object. /// /// /// The bytes. @@ -55,7 +58,7 @@ public static object BinaryDeserialize(this byte[] bytes) /// The binder. /// /// - /// Deserialised object. + /// Deserialized object. /// public static object BinaryDeserialize(this byte[] bytes, SerializationBinder binder) { @@ -65,7 +68,6 @@ public static object BinaryDeserialize(this byte[] bytes, SerializationBinder bi } var formatter = new BinaryFormatter { Binder = binder }; - using (var stream = new MemoryStream(bytes)) { return formatter.Deserialize(stream); @@ -73,12 +75,12 @@ public static object BinaryDeserialize(this byte[] bytes, SerializationBinder bi } /// - /// Convert an object to it's binary serialised form. + /// Convert an object to it's binary serialized form. /// /// - /// Object to serialise. + /// Object to serialize. /// - /// Serialised object. + /// Serialized object. /// public static byte[] BinarySerialize(this object o) { @@ -219,11 +221,6 @@ public static string ToByteDisplay(this long byteCount) return size; } - /// - /// Gets ExecutingDirectory. - /// - public static string ExecutingDirectory => Path.GetDirectoryName(Reflection.Assembly.GetExecutingAssembly().Location); - /// /// Check if a dictionary has a value for a key. /// @@ -310,20 +307,20 @@ public static void AddRange(this IList list, IEnumerable values) /// IQueryable to page. /// Page number (begins at 1). /// Number of items per page. - /// + /// Paged LINQ to SQL. public static IQueryable Page(this IQueryable source, int page, int pageSize) { return source.Skip((page - 1) * pageSize).Take(pageSize); } /// - /// Paging for LINQ + /// Paging for LINQ. /// /// IEnumerable of 'object type' to page. /// IEnumerable to page. /// Page number (begins at 1). /// Number of items per page. - /// + /// Paged LINQ. public static IEnumerable Page(this IEnumerable source, int page, int pageSize) { return source.Skip((page - 1) * pageSize).Take(pageSize); @@ -336,7 +333,7 @@ public static IEnumerable Page(this IEnumerable sourc /// IQueryable to page. /// Index of first item to return. /// number of items to return. - /// + /// Paged IQueryable object. public static IQueryable PageByIndex(this IQueryable value, int? startIndex, int? length) { if (startIndex != null) @@ -402,99 +399,20 @@ public static Expression> False() return f => false; } - public static Expression> Or(this Expression> expr1, - Expression> expr2) + public static Expression> Or( + this Expression> expr1, + Expression> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast()); - return Expression.Lambda> - (Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); + return Expression.Lambda>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); } - public static Expression> And(this Expression> expr1, - Expression> expr2) + public static Expression> And( + this Expression> expr1, + Expression> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast()); return Expression.Lambda>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters); } } - - public static class ProcessExtensions - { -#if DEBUG - - /// - /// A utility class to determine a process parent. - /// http://stackoverflow.com/a/3346055 - /// - [StructLayout(LayoutKind.Sequential)] - public struct ParentProcessUtilities - { - // These members must match PROCESS_BASIC_INFORMATION - internal IntPtr Reserved1; - - internal IntPtr PebBaseAddress; - - internal IntPtr Reserved2_0; - - internal IntPtr Reserved2_1; - - internal IntPtr UniqueProcessId; - - internal IntPtr InheritedFromUniqueProcessId; - - [DllImport("ntdll.dll")] - private static extern int NtQueryInformationProcess( - IntPtr processHandle, - int processInformationClass, - ref ParentProcessUtilities processInformation, - int processInformationLength, - out int returnLength); - - /// - /// Gets the parent process of the current process. - /// - /// An instance of the Process class. - public static Process GetParentProcess() - { - return GetParentProcess(Process.GetCurrentProcess().Handle); - } - - /// - /// Gets the parent process of specified process. - /// - /// The process id. - /// An instance of the Process class. - public static Process GetParentProcess(int id) - { - Process process = Process.GetProcessById(id); - return GetParentProcess(process.Handle); - } - - /// - /// Gets the parent process of a specified process. - /// - /// The process handle. - /// An instance of the Process class. - public static Process GetParentProcess(IntPtr handle) - { - ParentProcessUtilities pbi = new ParentProcessUtilities(); - int status = NtQueryInformationProcess(handle, 0, ref pbi, Marshal.SizeOf(pbi), out var returnLength); - if (status != 0) - { - throw new Win32Exception(status); - } - - try - { - return Process.GetProcessById(pbi.InheritedFromUniqueProcessId.ToInt32()); - } - catch (ArgumentException) - { - // not found - return null; - } - } - } -#endif - } } \ No newline at end of file diff --git a/src/AnalysisPrograms/Production/MainEntryUtilities.cs b/src/AnalysisPrograms/Production/MainEntryUtilities.cs index 658994e00..d04fa8c81 100644 --- a/src/AnalysisPrograms/Production/MainEntryUtilities.cs +++ b/src/AnalysisPrograms/Production/MainEntryUtilities.cs @@ -73,8 +73,6 @@ internal enum Usages NoAction, } - public static bool AppConfigOverridden { get; private set; } - /// /// Gets the default log level set by an environment variable. /// @@ -101,6 +99,13 @@ internal enum Usages /// internal static bool ApAutoAttach { get; private set; } + // https://stackoverflow.com/questions/3167617/determine-if-code-is-running-as-part-of-a-unit-test + internal static bool IsMsTestRunningMe { get; } = + AppDomain + .CurrentDomain + .GetAssemblies() + .Any(a => a.FullName.StartsWith("Microsoft.VisualStudio.TestPlatform.TestFramework")); + public static void SetLogVerbosity(LogVerbosity logVerbosity, bool quietConsole = false) { var modifiedLevel = VerbosityToLevel(logVerbosity); @@ -279,21 +284,21 @@ internal static void HangBeforeExit() // if Michael is debugging with visual studio, this will prevent the window closing. Process parentProcess = ProcessExtensions.ParentProcessUtilities.GetParentProcess(); - if (parentProcess.ProcessName == "devenv") + if (Debugger.IsAttached) { - LoggedConsole.WriteSuccessLine("FINISHED: Press RETURN key to exit."); - Console.ReadLine(); + if (parentProcess == null) + { + LoggedConsole.WriteWarnLine("WARNING: Unable to detect parent process, this is a windows specific tool."); + } + else if (parentProcess?.ProcessName == "devenv") + { + LoggedConsole.WriteSuccessLine("FINISHED: Press RETURN key to exit."); + Console.ReadLine(); + } } #endif } - // https://stackoverflow.com/questions/3167617/determine-if-code-is-running-as-part-of-a-unit-test - internal static bool IsMsTestRunningMe { get; } = - AppDomain - .CurrentDomain - .GetAssemblies() - .Any(a => a.FullName.StartsWith("Microsoft.VisualStudio.TestPlatform.TestFramework")); - internal static void PrintUsage(string message, Usages usageStyle, string commandName = null) { //Contract.Requires(usageStyle != Usages.Single || commandName != null); @@ -380,8 +385,6 @@ private static void PrepareForErrors() { ExitCode = ExceptionLookup.UnhandledExceptionErrorCode; - AppConfigOverridden = CheckAndUpdateApplicationConfig(); - if (CheckForDataAnnotations() is string message) { Console.WriteLine(message); @@ -491,7 +494,6 @@ private static void LogProgramStats() ProcessorCount, ExecutionTime = (DateTime.Now - thisProcess.StartTime).TotalSeconds, PeakWorkingSet = thisProcess.PeakWorkingSet64, - AppConfigOverridden, }; var statsString = "Programs stats:\n" + Json.SerializeToString(stats, prettyPrint: true); diff --git a/tests/Acoustics.Test/AnalysisPrograms/Production/MainEntryUtilitiesTests.cs b/tests/Acoustics.Test/AnalysisPrograms/Production/MainEntryUtilitiesTests.cs new file mode 100644 index 000000000..3776f0108 --- /dev/null +++ b/tests/Acoustics.Test/AnalysisPrograms/Production/MainEntryUtilitiesTests.cs @@ -0,0 +1,23 @@ +// +// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). +// + +namespace Acoustics.Test.AnalysisPrograms.Production +{ + using System; + using Acoustics.Test.TestHelpers; + using global::AnalysisPrograms; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MainEntryUtilitiesTests + { +#if DEBUG + [TestMethod] + public void TestHangBeforeExitDoesNotThrowException() + { + Assertions.DoesNotThrow(() => MainEntry.HangBeforeExit()); + } +#endif + } +} \ No newline at end of file diff --git a/tests/Acoustics.Test/Shared/Extensions/ProcessExtensionsTests.cs b/tests/Acoustics.Test/Shared/Extensions/ProcessExtensionsTests.cs new file mode 100644 index 000000000..4e357d786 --- /dev/null +++ b/tests/Acoustics.Test/Shared/Extensions/ProcessExtensionsTests.cs @@ -0,0 +1,28 @@ +// +// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). +// + +namespace Acoustics.Test.Shared.Extensions +{ +#if DEBUG + using Acoustics.Test.TestHelpers; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using static System.ProcessExtensions.ParentProcessUtilities; + + [TestClass] + public class ProcessExtensionsTests + { + [PlatformSpecificTestMethod("Windows", Reason = "https://github.com/dotnet/runtime/issues/24423")] + public void TestGetParentProcessReturnsProcessOnWindows() + { + Assert.IsNotNull(GetParentProcess()); + } + + [PlatformSpecificTestMethod("!Windows", Reason = "https://github.com/dotnet/runtime/issues/24423")] + public void TestGetParentProcessReturnsNullWhenNotWindows() + { + Assert.IsNull(GetParentProcess()); + } + } +#endif +} \ No newline at end of file diff --git a/tests/Acoustics.Test/TestHelpers/Assertions.cs b/tests/Acoustics.Test/TestHelpers/Assertions.cs index 2937e9b0c..d28f30a51 100644 --- a/tests/Acoustics.Test/TestHelpers/Assertions.cs +++ b/tests/Acoustics.Test/TestHelpers/Assertions.cs @@ -15,6 +15,12 @@ namespace Acoustics.Test.TestHelpers public static class Assertions { + public enum DiffStyle + { + Full, + Minimal, + } + [DebuggerHidden] public static void AreEqual(this Assert assert, long expected, long actual, long delta, string message = null) { @@ -282,12 +288,6 @@ public static void NotContains(this StringAssert assert, string value, string su $"String\n{value}\n should not contain `{substring}`. {message}"); } - public enum DiffStyle - { - Full, - Minimal, - } - public static void StringEqualWithDiff(this Assert assert, string expectedValue, string actualValue) { ShouldEqualWithDiff(actualValue, expectedValue, DiffStyle.Full, Console.Out); @@ -308,6 +308,25 @@ public static void ShouldEqualWithDiff(this string actualValue, string expectedV ShouldEqualWithDiff(actualValue, expectedValue, diffStyle, Console.Out); } + public static void DoesNotThrow(Action expressionUnderTest, string exceptionMessage = "Expected exception was thrown by target of invocation.") + where T : Exception + { + try + { + expressionUnderTest(); + } + catch (T) + { + Assert.Fail(exceptionMessage); + } + catch (Exception) + { + Assert.IsTrue(true); + } + + Assert.IsTrue(true); + } + public static void ShouldEqualWithDiff(this string actualValue, string expectedValue, DiffStyle diffStyle, TextWriter output) { if (actualValue == null || expectedValue == null)