diff --git a/src/chocolatey.benchmark/App.config b/src/chocolatey.benchmark/App.config new file mode 100644 index 0000000000..f8e1309c75 --- /dev/null +++ b/src/chocolatey.benchmark/App.config @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/chocolatey.benchmark/BenchmarkConfig.cs b/src/chocolatey.benchmark/BenchmarkConfig.cs new file mode 100644 index 0000000000..7e9f243a7c --- /dev/null +++ b/src/chocolatey.benchmark/BenchmarkConfig.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Exporters.Csv; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; + +namespace chocolatey.benchmark +{ + internal static class BenchmarkConfig + { + public static IConfig Get() + { + var config = ManualConfig.CreateEmpty(); + + foreach (var platform in new[] { Platform.X64/*, Platform.X86*/ }) + { + config = config.AddJob(Job.Default.WithRuntime(ClrRuntime.Net48).WithPlatform(platform).WithJit(Jit.LegacyJit)); + } + + return config + .AddDiagnoser(MemoryDiagnoser.Default) + .AddColumnProvider(DefaultColumnProviders.Instance) + .AddLogger(ConsoleLogger.Default) + .AddExporter(CsvExporter.Default) + .AddExporter(HtmlExporter.Default) + .AddExporter(MarkdownExporter.Default) + .AddExporter(AsciiDocExporter.Default) + .AddAnalyser(GetAnalysers().ToArray()); + } + + private static IEnumerable GetAnalysers() + { + yield return EnvironmentAnalyser.Default; + yield return OutliersAnalyser.Default; + yield return MinIterationTimeAnalyser.Default; + yield return MultimodalDistributionAnalyzer.Default; + yield return RuntimeErrorAnalyser.Default; + yield return ZeroMeasurementAnalyser.Default; + yield return BaselineCustomAnalyzer.Default; + } + } +} diff --git a/src/chocolatey.benchmark/ParentProcessBenchmarks.cs b/src/chocolatey.benchmark/ParentProcessBenchmarks.cs new file mode 100644 index 0000000000..21486b8304 --- /dev/null +++ b/src/chocolatey.benchmark/ParentProcessBenchmarks.cs @@ -0,0 +1,69 @@ +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using chocolatey.benchmark.helpers; +using chocolatey.infrastructure.information; + +namespace chocolatey.benchmark +{ + public class ParentProcessBenchmarks + { + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public string GetParentProcessDocumentedPinvoke() + { + return PinvokeProcessHelper.GetDocumentedParent(); + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public string GetParentProcessFilteredDocumentedPinvoke() + { + return PinvokeProcessHelper.GetDocumentedParentFiltered(); + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public string GetParentProcessFilteredManaged() + { + return ManagedProcessHelper.GetParent(); + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public string GetParentProcessFilteredUndocumentedPinvoke() + { + return PinvokeProcessHelper.GetUndocumentedParentFiltered(); + } + + [Benchmark(Baseline = true), MethodImpl(MethodImplOptions.NoInlining)] + public string GetParentProcessManaged() + { + return ManagedProcessHelper.GetParent(); + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public ProcessTree GetParentProcessTreeDocumentedPinvoke() + { + return PinvokeProcessHelper.GetDocumentedProcessTree(); + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public ProcessTree GetParentProcessTreeManaged() + { + return ManagedProcessHelper.GetProcessTree(); + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public ProcessTree GetParentProcessTreeUndocumentedPinvoke() + { + return PinvokeProcessHelper.GetUndocumentedProcessTree(); + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public string GetParentProcessUndocumentedPinvoke() + { + return PinvokeProcessHelper.GetUndocumentedParent(); + } + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public ProcessTree GetParentProcessTreeImplemented() + { + return ProcessInformation.GetProcessTree(); + } + } +} \ No newline at end of file diff --git a/src/chocolatey.benchmark/ProcessTreeBenchmark.cs b/src/chocolatey.benchmark/ProcessTreeBenchmark.cs new file mode 100644 index 0000000000..b6264324e7 --- /dev/null +++ b/src/chocolatey.benchmark/ProcessTreeBenchmark.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using chocolatey.benchmark.helpers; +using chocolatey.infrastructure.information; + +namespace chocolatey.benchmark +{ + public class ProcessTreeBenchmark + { + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + [ArgumentsSource(nameof(GetProcessTree))] + public string GetFirstFilteredProcessName(ProcessTree tree) + { + return tree.FirstFilteredProcessName; + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + [ArgumentsSource(nameof(GetProcessTree))] + public string GetFirstProcessName(ProcessTree tree) + { + return tree.FirstProcessName; + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + [ArgumentsSource(nameof(GetProcessTree))] + public string GetLastFilteredProcessName(ProcessTree tree) + { + return tree.LastFilteredProcessName; + } + + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + [ArgumentsSource(nameof(GetProcessTree))] + public string GetLastProcessName(ProcessTree tree) + { + return tree.LastProcessName; + } + + [Benchmark(Baseline = true), MethodImpl(MethodImplOptions.NoInlining)] + [ArgumentsSource(nameof(GetProcessTree))] + public LinkedList GetProcessesList(ProcessTree tree) + { + return tree.Processes; + } + + public IEnumerable GetProcessTree() + { + var currentProcess = Process.GetCurrentProcess(); + + var tree = new ProcessTree(currentProcess.ProcessName); + tree.Processes.AddLast("devenv"); + tree.Processes.AddLast("cmd"); + tree.Processes.AddLast("Tabby"); + tree.Processes.AddLast("explorer"); + yield return tree; + + yield return new ProcessTree(currentProcess.ProcessName); + + tree = new ProcessTree(currentProcess.ProcessName); + tree.Processes.AddLast(currentProcess.ProcessName); + tree.Processes.AddLast("WindowsTerminal"); + yield return tree; + + yield return PinvokeProcessHelper.GetUndocumentedProcessTree(currentProcess); + } + } +} \ No newline at end of file diff --git a/src/chocolatey.benchmark/Program.cs b/src/chocolatey.benchmark/Program.cs new file mode 100644 index 0000000000..f6df03d30f --- /dev/null +++ b/src/chocolatey.benchmark/Program.cs @@ -0,0 +1,22 @@ +using System.Linq; +using BenchmarkDotNet.Running; + +namespace chocolatey.benchmark +{ + internal class Program + { + private static void Main(string[] args) + { + var switcher = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly); + + if (args.Length > 0) + { + switcher.Run(args, BenchmarkConfig.Get()).ToArray(); + } + else + { + switcher.RunAll(BenchmarkConfig.Get()).ToArray(); + } + } + } +} diff --git a/src/chocolatey.benchmark/Properties/AssemblyInfo.cs b/src/chocolatey.benchmark/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..ceb1f4def2 --- /dev/null +++ b/src/chocolatey.benchmark/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("chocolatey.benchmark")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("chocolatey.benchmark")] +[assembly: AssemblyCopyright("Copyright © 2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("2b98bb42-a7ae-4ef6-b0b8-aa7bfc1e1180")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/chocolatey.benchmark/chocolatey.benchmark.csproj b/src/chocolatey.benchmark/chocolatey.benchmark.csproj new file mode 100644 index 0000000000..f42ea66591 --- /dev/null +++ b/src/chocolatey.benchmark/chocolatey.benchmark.csproj @@ -0,0 +1,277 @@ + + + + + + Debug + AnyCPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180} + Exe + chocolatey.benchmark + chocolatey.benchmark + v4.8 + 512 + true + true + ..\ + + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + bin\ReleaseOfficial\ + TRACE + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + false + + + + ..\packages\BenchmarkDotNet.0.13.12\lib\netstandard2.0\BenchmarkDotNet.dll + + + ..\packages\BenchmarkDotNet.Annotations.0.13.12\lib\netstandard2.0\BenchmarkDotNet.Annotations.dll + + + ..\packages\BenchmarkDotNet.Diagnostics.Windows.0.13.12\lib\netstandard2.0\BenchmarkDotNet.Diagnostics.Windows.dll + + + ..\packages\CommandLineParser.2.9.1\lib\net461\CommandLine.dll + + + ..\packages\Microsoft.Diagnostics.Tracing.TraceEvent.3.1.10\lib\netstandard2.0\Dia2Lib.dll + True + + + ..\packages\Gee.External.Capstone.2.3.0\lib\netstandard2.0\Gee.External.Capstone.dll + + + ..\packages\Iced.1.21.0\lib\net45\Iced.dll + + + ..\packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\packages\Microsoft.CodeAnalysis.Common.4.9.2\lib\netstandard2.0\Microsoft.CodeAnalysis.dll + + + ..\packages\Microsoft.CodeAnalysis.CSharp.4.9.2\lib\netstandard2.0\Microsoft.CodeAnalysis.CSharp.dll + + + ..\packages\Microsoft.Diagnostics.Tracing.TraceEvent.3.1.10\lib\netstandard2.0\Microsoft.Diagnostics.FastSerialization.dll + + + ..\packages\Microsoft.Diagnostics.NETCore.Client.0.2.510501\lib\netstandard2.0\Microsoft.Diagnostics.NETCore.Client.dll + + + ..\packages\Microsoft.Diagnostics.Runtime.3.1.512801\lib\netstandard2.0\Microsoft.Diagnostics.Runtime.dll + + + ..\packages\Microsoft.Diagnostics.Tracing.TraceEvent.3.1.10\lib\netstandard2.0\Microsoft.Diagnostics.Tracing.TraceEvent.dll + + + ..\packages\Microsoft.DotNet.PlatformAbstractions.3.1.6\lib\net45\Microsoft.DotNet.PlatformAbstractions.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.8.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.8.0.1\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Logging.8.0.0\lib\net462\Microsoft.Extensions.Logging.dll + + + ..\packages\Microsoft.Extensions.Logging.Abstractions.8.0.1\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Options.8.0.2\lib\net462\Microsoft.Extensions.Options.dll + + + ..\packages\Microsoft.Extensions.Primitives.8.0.0\lib\net462\Microsoft.Extensions.Primitives.dll + + + ..\packages\Microsoft.Win32.Registry.5.0.0\lib\net461\Microsoft.Win32.Registry.dll + + + ..\packages\Perfolizer.0.2.1\lib\netstandard2.0\Perfolizer.dll + + + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + ..\packages\System.CodeDom.8.0.0\lib\net462\System.CodeDom.dll + + + ..\packages\System.Collections.Immutable.8.0.0\lib\net462\System.Collections.Immutable.dll + + + + + + ..\packages\System.Diagnostics.DiagnosticSource.8.0.1\lib\net462\System.Diagnostics.DiagnosticSource.dll + + + ..\packages\System.Diagnostics.Process.4.3.0\lib\net461\System.Diagnostics.Process.dll + True + True + + + ..\packages\System.Diagnostics.TraceSource.4.3.0\lib\net46\System.Diagnostics.TraceSource.dll + True + True + + + ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll + True + True + + + ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll + True + True + + + ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + True + True + + + ..\packages\System.IO.UnmanagedMemoryStream.4.3.0\lib\net46\System.IO.UnmanagedMemoryStream.dll + True + True + + + + ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + ..\packages\System.Net.NameResolution.4.3.0\lib\net46\System.Net.NameResolution.dll + True + True + + + ..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\System.Numerics.dll + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Reflection.Metadata.8.0.0\lib\net462\System.Reflection.Metadata.dll + + + ..\packages\System.Reflection.TypeExtensions.4.7.0\lib\net461\System.Reflection.TypeExtensions.dll + + + ..\packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll + True + True + + + ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + True + True + + + ..\packages\System.Security.AccessControl.6.0.1\lib\net461\System.Security.AccessControl.dll + + + ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll + True + True + + + ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + True + True + + + ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + True + True + + + ..\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll + + + ..\packages\System.Text.Encoding.CodePages.8.0.0\lib\net462\System.Text.Encoding.CodePages.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + ..\packages\System.Threading.Thread.4.3.0\lib\net46\System.Threading.Thread.dll + True + True + + + ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + + + + + + + + + ..\packages\Microsoft.Diagnostics.Tracing.TraceEvent.3.1.10\lib\netstandard2.0\TraceReloggerLib.dll + True + + + + + + + + + + + + + + + + + + + + + + {5563dc61-35fd-4fab-b331-9ae1fdb23f80} + chocolatey + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/chocolatey.benchmark/helpers/ManagedProcessHelper.cs b/src/chocolatey.benchmark/helpers/ManagedProcessHelper.cs new file mode 100644 index 0000000000..204728978c --- /dev/null +++ b/src/chocolatey.benchmark/helpers/ManagedProcessHelper.cs @@ -0,0 +1,180 @@ +using System; +using System.Diagnostics; +using System.Linq; +using chocolatey.infrastructure.information; + +namespace chocolatey.benchmark.helpers +{ + internal static class ManagedProcessHelper + { + private static readonly string[] _filteredParents = new[] + { + "explorer", + "powershell", + "pwsh", + "cmd", + "bash", + // The name used to launch windows services + // in the operating system. + "services", + // Known Terminal Emulators + "Tabby", + "WindowsTerminal", + "FireCMD", + "ConEmu64", + "ConEmuC64" + }; + + public static ProcessTree GetProcessTree(Process process = null) + { + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + var tree = new ProcessTree(process.ProcessName); + + Process nextProcess = null; + + while (true) + { + var processId = nextProcess?.Id ?? process.Id; + var processName = FindIndexedProcessName(processId); + + if (string.IsNullOrEmpty(processName)) + { + break; + } + + var foundProcess = FindPidFromIndexedProcessName(processName); + + if (foundProcess == null) + { + break; + } + + nextProcess = foundProcess; + tree.Processes.AddLast(nextProcess.ProcessName); + } + + return tree; + } + + public static string GetParent(Process process = null) + { + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + Process nextProcess = null; + + while (true) + { + var processId = nextProcess?.Id ?? process.Id; + + var processName = FindIndexedProcessName(processId); + + if (string.IsNullOrEmpty(processName)) + { + break; + } + + var foundProcess = FindPidFromIndexedProcessName(processName); + + if (foundProcess == null) + { + break; + } + + nextProcess = foundProcess; + } + + return nextProcess?.ProcessName; + } + + public static string GetParentFiltered(Process process = null) + { + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + Process nextProcess = null; + Process selectedProcess = null; + + while (true) + { + var processId = nextProcess?.Id ?? process.Id; + + var processName = FindIndexedProcessName(processId); + + if (string.IsNullOrEmpty(processName)) + { + break; + } + + var foundProcess = FindPidFromIndexedProcessName(processName); + + if (foundProcess == null) + { + break; + } + + nextProcess = foundProcess; + + if (!IsIgnoredParent(nextProcess.ProcessName)) + { + selectedProcess = nextProcess; + } + } + + return selectedProcess?.ProcessName; + } + + private static bool IsIgnoredParent(string processName) + { + return _filteredParents.Contains(processName, StringComparer.OrdinalIgnoreCase); + } + + private static Process FindPidFromIndexedProcessName(string indexedProcessName) + { + try + { + var parentId = new PerformanceCounter("Process", "Creating Process ID", indexedProcessName); + return Process.GetProcessById((int)parentId.NextValue()); + } + catch + { + return null; + } + } + + private static string FindIndexedProcessName(int pid) + { + var processName = Process.GetProcessById(pid).ProcessName; + var processByName = Process.GetProcessesByName(processName); + string processIndexedName = null; + + for (var i = 0; i < processByName.Length; i++) + { + try + { + processIndexedName = i == 0 ? processName : processName + "#" + i; + var processId = new PerformanceCounter("Process", "ID Process", processIndexedName); + + if ((int)processId.NextValue() == pid) + { + return processIndexedName; + } + } + catch + { + // Empty on purpose + } + } + + return processIndexedName; + } + } +} diff --git a/src/chocolatey.benchmark/helpers/PinvokeProcessHelper.cs b/src/chocolatey.benchmark/helpers/PinvokeProcessHelper.cs new file mode 100644 index 0000000000..fb4f5d063e --- /dev/null +++ b/src/chocolatey.benchmark/helpers/PinvokeProcessHelper.cs @@ -0,0 +1,394 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +using System.Security.Permissions; +using System.Security; +using chocolatey.infrastructure.information; +using chocolatey.infrastructure.platforms; + +namespace chocolatey.benchmark.helpers +{ + internal class PinvokeProcessHelper + { + private static readonly string[] _filteredParents = new[] + { + "explorer", + "powershell", + "pwsh", + "cmd", + "bash", + // The name used to launch windows services + // in the operating system. + "services", + // Known Terminal Emulators + "Tabby", + "WindowsTerminal", + "FireCMD", + "ConEmu64", + "ConEmuC64" + }; + + public static ProcessTree GetDocumentedProcessTree(Process process = null) + { + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + var tree = new ProcessTree(process.ProcessName); + + if (Platform.GetPlatform() != PlatformType.Windows) + { + return tree; + } + + Process nextProcess = null; + + while (true) + { + var foundProcess = ParentDocumentedHelper.ParentProcess(nextProcess ?? process); + + if (foundProcess == null) + { + break; + } + + nextProcess = foundProcess; + tree.Processes.AddLast(nextProcess.ProcessName); + } + + return tree; + } + + public static string GetDocumentedParent(Process process = null) + { + if (Platform.GetPlatform() != PlatformType.Windows) + { + return null; + } + + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + Process nextProcess = null; + + while (true) + { + var foundProcess = ParentDocumentedHelper.ParentProcess(nextProcess ?? process); + + if (foundProcess == null) + { + break; + } + + nextProcess = foundProcess; + } + + return nextProcess?.ProcessName; + } + + public static string GetDocumentedParentFiltered(Process process = null) + { + if (Platform.GetPlatform() != PlatformType.Windows) + { + return null; + } + + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + Process nextProcess = null; + Process selectedProcess = null; + + while (true) + { + var foundProcess = ParentDocumentedHelper.ParentProcess(nextProcess ?? process); + + if (foundProcess == null) + { + break; + } + + nextProcess = foundProcess; + + if (!IsIgnoredParent(nextProcess.ProcessName)) + { + selectedProcess = nextProcess; + } + } + + return selectedProcess?.ProcessName; + } + + public static ProcessTree GetUndocumentedProcessTree(Process process = null) + { + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + var tree = new ProcessTree(process.ProcessName); + + Process nextProcess = null; + + while (true) + { + var parentProcess = ParentProcessUtilities.GetParentProcess(nextProcess ?? process); + + if (parentProcess == null) + { + break; + } + + nextProcess = parentProcess; + tree.Processes.AddLast(nextProcess.ProcessName); + } + + return tree; + } + + public static string GetUndocumentedParent(Process process = null) + { + if (Platform.GetPlatform() != PlatformType.Windows) + { + return null; + } + + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + Process nextProcess = null; + + while (true) + { + var foundProcess = ParentProcessUtilities.GetParentProcess(nextProcess ?? process); + + if (foundProcess == null) + { + break; + } + + nextProcess = foundProcess; + } + + return nextProcess?.ProcessName; + } + + public static string GetUndocumentedParentFiltered(Process process = null) + { + if (Platform.GetPlatform() != PlatformType.Windows) + { + return null; + } + + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + Process nextProcess = null; + Process selectedProcess = null; + + while (true) + { + var foundProcess = ParentProcessUtilities.GetParentProcess(nextProcess ?? process); + + if (foundProcess == null) + { + break; + } + + nextProcess = foundProcess; + + if (!IsIgnoredParent(nextProcess.ProcessName)) + { + selectedProcess = nextProcess; + } + } + + return selectedProcess?.ProcessName; + } + + private static bool IsIgnoredParent(string processName) + { + return _filteredParents.Contains(processName, StringComparer.OrdinalIgnoreCase); + } + + private class ParentDocumentedHelper + { + public static Process ParentProcess(Process process) + { + try + { + var processId = ParentProcessId(process.Id); + + if (processId == -1) + { + return null; + } + else + { + return Process.GetProcessById(processId); + } + } + catch + { + return null; + } + } + + private static int ParentProcessId(int id) + { + var pe32 = new PROCESSENTRY32 + { + dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32)) + }; + + using (var hSnapshot = CreateToolhelp32Snapshot(SnapshotFlags.Process, (uint)id)) + { + if (hSnapshot.IsInvalid) + { + throw new Win32Exception(); + } + + if (!Process32First(hSnapshot, ref pe32)) + { + var errno = Marshal.GetLastWin32Error(); + + if (errno == ERROR_NO_MORE_FILES) + { + return -1; + } + + throw new Win32Exception(errno); + } + + do + { + if (pe32.th32ProcessID == (uint)id) + { + return (int)pe32.th32ParentProcessID; + } + } while (Process32Next(hSnapshot, ref pe32)); + } + + return -1; + } + + private const int ERROR_NO_MORE_FILES = 0x12; + [DllImport("kernel32.dll", SetLastError = true)] + private static extern SafeSnapshotHandle CreateToolhelp32Snapshot(SnapshotFlags flags, uint id); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool Process32First(SafeSnapshotHandle hSnapshot, ref PROCESSENTRY32 lppe); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool Process32Next(SafeSnapshotHandle hSnapshot, ref PROCESSENTRY32 lppe); + + [Flags] + private enum SnapshotFlags : uint + { + HeapList = 0x00000001, + Process = 0x00000002, + Thread = 0x00000004, + Module = 0x00000008, + Module32 = 0x00000010, + All = (HeapList | Process | Thread | Module), + Inherit = 0x80000000, + NoHeaps = 0x40000000 + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESSENTRY32 + { +#pragma warning disable IDE1006 // Naming Styles + public uint dwSize; + public uint cntUsage; + public uint th32ProcessID; + public IntPtr th32DefaultHeapID; + public uint th32ModuleID; + public uint cntThreads; + public uint th32ParentProcessID; + public int pcPriClassBase; + public uint dwFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string szExeFile; +#pragma warning restore IDE1006 // Naming Styles + } + + [SuppressUnmanagedCodeSecurity, HostProtection(SecurityAction.LinkDemand, MayLeakOnAbort = true)] + internal sealed class SafeSnapshotHandle : SafeHandleMinusOneIsInvalid + { + internal SafeSnapshotHandle() + : base(true) + { + } + + [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)] + internal SafeSnapshotHandle(IntPtr handle) + : base(true) + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + return CloseHandle(handle); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + private static extern bool CloseHandle(IntPtr handle); + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct ParentProcessUtilities + { + internal IntPtr Reserved1; + internal IntPtr PebBaseAddress; + internal IntPtr Reserved2_0; + internal IntPtr Reselved2_1; + internal IntPtr UniqueProcessId; + internal IntPtr InheritedFromUniqueProcessId; + + [DllImport("ntdll.dll")] + private static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformaiton, int processInformationLength, out int returnLength); + + public static Process GetParentProcess(Process process) + { + return GetParentProcess(process.Handle); + } + + public static Process GetParentProcess(IntPtr handle) + { + var processUtilities = new ParentProcessUtilities(); + var status = NtQueryInformationProcess(handle, 0, ref processUtilities, Marshal.SizeOf(processUtilities), out _); + + if (status != 0) + { + return null; + } + + try + { + return Process.GetProcessById(processUtilities.InheritedFromUniqueProcessId.ToInt32()); + } + catch (ArgumentException) + { + return null; + } + } + } + } +} diff --git a/src/chocolatey.benchmark/packages.config b/src/chocolatey.benchmark/packages.config new file mode 100644 index 0000000000..568db12c80 --- /dev/null +++ b/src/chocolatey.benchmark/packages.config @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/chocolatey.console/Program.cs b/src/chocolatey.console/Program.cs index 51c10e1535..312673a473 100644 --- a/src/chocolatey.console/Program.cs +++ b/src/chocolatey.console/Program.cs @@ -16,9 +16,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using Microsoft.Win32; using chocolatey.infrastructure.information; using chocolatey.infrastructure.app; @@ -270,6 +270,7 @@ private static void RemoveOldChocoExe(IFileSystem fileSystem) ); } + [Conditional("DEBUG")] private static void PauseIfDebug() { #if DEBUG diff --git a/src/chocolatey.sln b/src/chocolatey.sln index a528f26a0c..25811d8588 100644 --- a/src/chocolatey.sln +++ b/src/chocolatey.sln @@ -55,6 +55,10 @@ Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "chocolatey.install", "choco EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chocolatey.PowerShell", "Chocolatey.PowerShell\Chocolatey.PowerShell.csproj", "{88396C46-8089-4814-A7D1-E18777FF6083}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{46D88287-13E4-4044-89FD-B52AAE7791BF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chocolatey.benchmark", "chocolatey.benchmark\chocolatey.benchmark.csproj", "{2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -273,6 +277,36 @@ Global {88396C46-8089-4814-A7D1-E18777FF6083}.WIX|Mixed Platforms.Build.0 = Debug|Any CPU {88396C46-8089-4814-A7D1-E18777FF6083}.WIX|x86.ActiveCfg = Debug|Any CPU {88396C46-8089-4814-A7D1-E18777FF6083}.WIX|x86.Build.0 = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Debug|x86.ActiveCfg = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Debug|x86.Build.0 = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.NoResources|Any CPU.ActiveCfg = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.NoResources|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.NoResources|x86.ActiveCfg = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Release|Any CPU.Build.0 = Release|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Release|x86.ActiveCfg = Release|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.Release|x86.Build.0 = Release|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.ReleaseOfficial|Any CPU.ActiveCfg = ReleaseOfficial|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.ReleaseOfficial|Any CPU.Build.0 = ReleaseOfficial|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.ReleaseOfficial|Mixed Platforms.ActiveCfg = ReleaseOfficial|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.ReleaseOfficial|Mixed Platforms.Build.0 = ReleaseOfficial|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.ReleaseOfficial|x86.ActiveCfg = ReleaseOfficial|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.ReleaseOfficial|x86.Build.0 = ReleaseOfficial|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.ReleaseOfficialNo7zip|Any CPU.ActiveCfg = Release|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.ReleaseOfficialNo7zip|Mixed Platforms.ActiveCfg = Release|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.ReleaseOfficialNo7zip|x86.ActiveCfg = Release|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.WIX|Any CPU.ActiveCfg = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.WIX|Any CPU.Build.0 = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.WIX|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.WIX|Mixed Platforms.Build.0 = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.WIX|x86.ActiveCfg = Debug|Any CPU + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180}.WIX|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -285,6 +319,7 @@ Global {DD9689F3-1D2D-41AE-A672-063EE70C0C9A} = {9AF88603-3E34-4B68-9B69-B0F1967A86BC} {9AF88603-3E34-4B68-9B69-B0F1967A86BC} = {FB6236DD-17EF-4C36-943C-47792FBC3306} {4795798A-2F92-467A-88FC-772E66BF8E57} = {FB6236DD-17EF-4C36-943C-47792FBC3306} + {2B98BB42-A7AE-4EF6-B0B8-AA7BFC1E1180} = {46D88287-13E4-4044-89FD-B52AAE7791BF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {998CAC46-A2B8-447C-B4CC-3B11DC35ED46} diff --git a/src/chocolatey.slnf b/src/chocolatey.slnf index 3eee04a084..a491a4615a 100644 --- a/src/chocolatey.slnf +++ b/src/chocolatey.slnf @@ -2,6 +2,7 @@ "solution": { "path": "chocolatey.sln", "projects": [ + "Chocolatey.PowerShell\\Chocolatey.PowerShell.csproj", "chocolatey.console\\chocolatey.console.csproj", "chocolatey.resources\\chocolatey.resources.csproj", "chocolatey.tests.integration\\chocolatey.tests.integration.csproj", diff --git a/src/chocolatey.tests/infrastructure.app/nuget/NugetCommonSpecs.cs b/src/chocolatey.tests/infrastructure.app/nuget/NugetCommonSpecs.cs index c5c41cf74d..a35d6601ef 100644 --- a/src/chocolatey.tests/infrastructure.app/nuget/NugetCommonSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/nuget/NugetCommonSpecs.cs @@ -16,23 +16,24 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; +using System.Text.RegularExpressions; using System.Threading; -using Chocolatey.NuGet.Frameworks; +using System.Threading.Tasks; using chocolatey.infrastructure.app; using chocolatey.infrastructure.app.configuration; using chocolatey.infrastructure.app.nuget; using chocolatey.infrastructure.filesystem; +using Chocolatey.NuGet.Frameworks; +using FluentAssertions; using Moq; using NuGet.Common; using NuGet.Configuration; using NuGet.Packaging; using NuGet.Packaging.Core; -using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Versioning; -using FluentAssertions; namespace chocolatey.tests.infrastructure.app.nuget { @@ -175,14 +176,17 @@ public void Should_set_user_agent_string() Context(); var source = "https://community.chocolatey.org/api/v2/"; _configuration.Sources = source; - _configuration.Information.ChocolateyProductVersion = "vNext"; _because(); // Change this when the NuGet version is updated. - var nugetClientVersion = "6.4.1"; - var expectedUserAgentString = "{0}/{1} via NuGet Client/{2}".FormatWith(ApplicationParameters.UserAgent, _configuration.Information.ChocolateyProductVersion, nugetClientVersion); - UserAgent.UserAgentString.Should().StartWith(expectedUserAgentString); + const string nugetClientVersion = "6.4.1"; + var currentProcess = Process.GetCurrentProcess(); + var expectedUserAgentRegexString = @"^{0}\/[\d\.]+(-[A-za-z\d\.-]+)? {1}\/[\d\.]+(-[A-Za-z\d\.-]+)? (\([A-za-z\d\.-]+(, [A-Za-z\d\.-]+)?\) )?via NuGet Client\/{2}".FormatWith( + ApplicationParameters.UserAgent, + currentProcess.ProcessName, + Regex.Escape(nugetClientVersion)); + UserAgent.UserAgentString.Should().MatchRegex(expectedUserAgentRegexString); } } diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 0f243f55e1..72a74ea052 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -229,7 +229,9 @@ + + @@ -263,6 +265,7 @@ + diff --git a/src/chocolatey/infrastructure.app/nuget/NugetCommon.cs b/src/chocolatey/infrastructure.app/nuget/NugetCommon.cs index 669a702165..e2043fdd1d 100644 --- a/src/chocolatey/infrastructure.app/nuget/NugetCommon.cs +++ b/src/chocolatey/infrastructure.app/nuget/NugetCommon.cs @@ -15,40 +15,33 @@ // limitations under the License. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Net; -using System.Net.Http; -using System.Net.Security; -using System.Runtime.CompilerServices; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; using chocolatey.infrastructure.adapters; -using Alphaleonis.Win32.Filesystem; -using Chocolatey.NuGet.Frameworks; -using chocolatey.infrastructure.configuration; using chocolatey.infrastructure.app.configuration; -using chocolatey.infrastructure.app.domain; +using chocolatey.infrastructure.app.services; using chocolatey.infrastructure.filesystem; -using chocolatey.infrastructure.logging; -using NuGet; +using chocolatey.infrastructure.information; +using chocolatey.infrastructure.registration; +using chocolatey.infrastructure.results; +using Chocolatey.NuGet.Frameworks; using NuGet.Common; using NuGet.Configuration; using NuGet.Credentials; -using NuGet.PackageManagement; using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.ProjectManagement; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Versioning; -using chocolatey.infrastructure.results; using Console = chocolatey.infrastructure.adapters.Console; -using Environment = chocolatey.infrastructure.adapters.Environment; -using System.Collections.Concurrent; namespace chocolatey.infrastructure.app.nuget { @@ -100,8 +93,60 @@ public static SourceRepository GetLocalRepository() public static IEnumerable GetRemoteRepositories(ChocolateyConfiguration configuration, ILogger nugetLogger, IFileSystem filesystem) #pragma warning restore IDE0060 // unused method parameter (nugetLogger) { + // As this is a static method, we need to call the global SimpleInjector container to get a registered service. + var collectorService = SimpleInjectorContainer.Container.GetInstance(); + var processTree = collectorService.GetProcessTree(); + "chocolatey".Log().Debug("Process Tree: {0}", processTree); + + var userAgent = new StringBuilder() + .Append(ApplicationParameters.UserAgent) + .Append('/') + .Append(VersionInformation.GetCurrentInformationalVersion(Assembly.GetAssembly(typeof(NugetCommon)))); + + if (!string.IsNullOrEmpty(collectorService.UserAgentProcessName)) + { + userAgent.Append(' ').Append(collectorService.UserAgentProcessName); + var processVersion = collectorService.UserAgentProcessVersion; + + if (string.IsNullOrEmpty(processVersion)) + { + processVersion = VersionInformation.GetCurrentInformationalVersion(); + } + + if (!string.IsNullOrEmpty(processVersion)) + { + userAgent.Append('/').Append(processVersion); + } + } + else if (processTree.CurrentProcessName != "Chocolatey CLI") + { + userAgent.Append(' ').Append(processTree.CurrentProcessName); + var processVersion = VersionInformation.GetCurrentInformationalVersion(); + + if (!string.IsNullOrEmpty(processVersion)) + { + userAgent.Append('/').Append(processVersion); + } + } + + if (processTree.LastFilteredProcessName != processTree.FirstFilteredProcessName && !string.IsNullOrEmpty(processTree.LastFilteredProcessName) && !string.IsNullOrEmpty(processTree.FirstFilteredProcessName)) + { + userAgent.Append(" (").Append(processTree.LastFilteredProcessName).Append(", ").Append(processTree.FirstFilteredProcessName).Append(')'); + } + else if (!string.IsNullOrEmpty(processTree.LastFilteredProcessName)) + { + userAgent.Append(" (").Append(processTree.LastFilteredProcessName).Append(')'); + } + else if (!string.IsNullOrEmpty(processTree.FirstFilteredProcessName)) + { + userAgent.Append(" (").Append(processTree.FirstFilteredProcessName).Append(')'); + } + + userAgent.Append(" via NuGet Client"); + // Set user agent for all NuGet library calls. Should not affect any HTTP calls that Chocolatey itself would make. - UserAgent.SetUserAgentString(new UserAgentStringBuilder("{0}/{1} via NuGet Client".FormatWith(ApplicationParameters.UserAgent, configuration.Information.ChocolateyProductVersion))); + UserAgent.SetUserAgentString(new UserAgentStringBuilder(userAgent.ToString())); + "chocolatey".Log().Debug("Updating User Agent to '{0}'.", UserAgent.UserAgentString); // ensure credentials can be grabbed from configuration SetHttpHandlerCredentialService(configuration); diff --git a/src/chocolatey/infrastructure.app/registration/ChocolateyRegistrationModule.cs b/src/chocolatey/infrastructure.app/registration/ChocolateyRegistrationModule.cs index ab559a6af2..359a7c9ec1 100644 --- a/src/chocolatey/infrastructure.app/registration/ChocolateyRegistrationModule.cs +++ b/src/chocolatey/infrastructure.app/registration/ChocolateyRegistrationModule.cs @@ -94,6 +94,7 @@ public void RegisterDependencies(IContainerRegistrator registrator, ChocolateyCo .ToArray(); registrator.RegisterService(availableRules); + registrator.RegisterService(); } #pragma warning disable IDE0022, IDE1006 diff --git a/src/chocolatey/infrastructure.app/services/IProcessCollectorService.cs b/src/chocolatey/infrastructure.app/services/IProcessCollectorService.cs new file mode 100644 index 0000000000..d5640b4b31 --- /dev/null +++ b/src/chocolatey/infrastructure.app/services/IProcessCollectorService.cs @@ -0,0 +1,45 @@ +using chocolatey.infrastructure.information; + +namespace chocolatey.infrastructure.app.services +{ + /// + /// Collector service that will get information about the processes in the current execution. + /// + /// + /// This service is used to build the correct user agent we want to send to the remote servers. + /// + public interface IProcessCollectorService + { + /// + /// Gets the friendly name of the currently running process. + /// + /// + /// If no user agent process name is specified, the current process in the process tree will be used instead. + /// + string UserAgentProcessName { get; } + + /// + /// Gets the version number of the currently running process. + /// + /// + /// + /// If no user agent process version is specified, the version number of the currently + /// running proccess will be looked up. + /// + /// + /// This property will only be used if have also been specified. + /// + /// + string UserAgentProcessVersion { get; } + + /// + /// Gets the full details of the process tree that Chocolatey CLI is part of. This includes + /// the top level parent, the closest parent, the current process name and all other + /// processes between these. + /// + /// + /// The found process tree, returning null from this will throw an exception in Chocolatey CLI. + /// + ProcessTree GetProcessTree(); + } +} diff --git a/src/chocolatey/infrastructure.app/services/ProcessCollectorService.cs b/src/chocolatey/infrastructure.app/services/ProcessCollectorService.cs new file mode 100644 index 0000000000..164a4a1c30 --- /dev/null +++ b/src/chocolatey/infrastructure.app/services/ProcessCollectorService.cs @@ -0,0 +1,30 @@ +using System; +using chocolatey.infrastructure.information; + +namespace chocolatey.infrastructure.app.services +{ + public class ProcessCollectorService : IProcessCollectorService + { + private static ProcessTree _processTree = null; + + /// + public virtual string UserAgentProcessName { get; } = string.Empty; + + /// + public virtual string UserAgentProcessVersion { get; } = string.Empty; + + /// + /// + /// This method is not overridable on purpose, as once a tree is created it should not be changed. + /// + public ProcessTree GetProcessTree() + { + if (_processTree is null) + { + _processTree = ProcessInformation.GetProcessTree(); + } + + return _processTree; + } + } +} diff --git a/src/chocolatey/infrastructure/information/ProcessInformation.cs b/src/chocolatey/infrastructure/information/ProcessInformation.cs index bb6dfb9935..75c896ca8c 100644 --- a/src/chocolatey/infrastructure/information/ProcessInformation.cs +++ b/src/chocolatey/infrastructure/information/ProcessInformation.cs @@ -15,9 +15,16 @@ // limitations under the License. using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; +using System.Security.Permissions; +using System.Security; using System.Security.Principal; using chocolatey.infrastructure.platforms; +using Microsoft.Win32.SafeHandles; namespace chocolatey.infrastructure.information { @@ -158,6 +165,130 @@ public static bool UserIsSystem() return isSystem; } + public static ProcessTree GetProcessTree() + { + return GetProcessTree(null); + } + + public static ProcessTree GetProcessTree(Process process) + { + if (process == null) + { + process = Process.GetCurrentProcess(); + } + + var tree = new ProcessTree(process.ProcessName); + + if (Platform.GetPlatform() != PlatformType.Windows) + { + return tree; + } + + try + { + try + { + tree = PopulateProcessTreeInternal(tree, process); + } + catch (TypeLoadException ex) when (ex is DllNotFoundException || ex is EntryPointNotFoundException) + { + try + { + // These exceptions mean the lookup failed because the DLL is missing or the entry point is no longer present. + // Ignore these and fall back to the alternative p/invoke method if we haven't already. + tree = PopulateProcessTreeStable(tree, process); + } + catch (TypeLoadException) + { + "chocolatey".Log().Warn(logging.ChocolateyLoggers.LogFileOnly, "All available methods of querying processes from the win32 APIs are broken or critical DLLs are missing."); + } + } + } + catch (Win32Exception ex) + { + "chocolatey".Log().Warn(logging.ChocolateyLoggers.LogFileOnly, "Unhandled Win32Exception ({0}) in finding parent processes.", ex.Message); + } + + return tree; + } + + private static ProcessTree PopulateProcessTreeInternal(ProcessTree tree, Process currentProcess) + { + Process nextProcess = null; + try + { + while (true) + { + var parentProcess = ParentProcessHelperInternal.GetParentProcess(nextProcess ?? currentProcess); + + if (parentProcess is null) + { + break; + } + + nextProcess = parentProcess; + tree.Processes.AddLast(nextProcess.ProcessName); + } + } + catch (Win32Exception ex) + { + // Native error code 5 is access denied. + // This usually happens if the parent executable + // is running as a different user, in which case + // we are not able to get the necessary handle for + // the process. + if (ex.NativeErrorCode != 5) + { + throw; + } + else + { + "chocolatey".Log().Debug(logging.ChocolateyLoggers.LogFileOnly, "Unable to get parent process for '{0}'. Ignoring...", currentProcess.ProcessName); + } + } + + return tree; + } + + private static ProcessTree PopulateProcessTreeStable(ProcessTree tree, Process currentProcess) + { + Process nextProcess = null; + try + { + while (true) + { + var parentProcess = ParentProcessHelperStable.GetParentProcess(nextProcess ?? currentProcess); + + if (parentProcess is null) + { + break; + } + + nextProcess = parentProcess; + tree.Processes.AddLast(nextProcess.ProcessName); + } + } + catch (Win32Exception ex) + { + // Native error code 5 is access denied. + // This usually happens if the parent executable + // is running as a different user, in which case + // we are not able to get the necessary handle for + // the process. + if (ex.NativeErrorCode != 5) + { + throw; + } + else + { + "chocolatey".Log().Debug(logging.ChocolateyLoggers.LogFileOnly, "Unable to get parent process for '{0}'. Ignoring...", currentProcess.ProcessName); + } + } + + return tree; + } + + /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa376402.aspx BOOL WINAPI ConvertStringSidToSid( @@ -244,6 +375,174 @@ private enum TokenElevationType TokenElevationTypeLimited } + [StructLayout(LayoutKind.Sequential)] + private struct ParentProcessHelperInternal + { + internal IntPtr Reserved1; + internal IntPtr PebBaseAddress; + internal IntPtr Reserved2_0; + internal IntPtr Reselved2_1; + internal IntPtr UniqueProcessId; + internal IntPtr InheritedFromUniqueProcessId; + + [DllImport("ntdll.dll")] + private static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessHelperInternal processInformation, int processInformationLength, out int returnLength); + + public static Process GetParentProcess(Process process) + { + return GetParentProcess(process.Handle); + } + + public static Process GetParentProcess(IntPtr handle) + { + try + { + var processUtilities = new ParentProcessHelperInternal(); + + // https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess#process_basic_information + // Retrieves a pointer to a PEB structure that can be used to determine whether the specified process is being debugged, + // and a unique value used by the system to identify the specified process. + // It also includes the `InheritedFromUniqueProcessId` value which we can use to look up the parent process directly. + const int processBasicInformation = 0; + + var status = NtQueryInformationProcess(handle, processBasicInformation, ref processUtilities, Marshal.SizeOf(processUtilities), out _); + + if (status != 0) + { + return null; + } + + return Process.GetProcessById(processUtilities.InheritedFromUniqueProcessId.ToInt32()); + } + catch (ArgumentException) + { + return null; + } + } + } + + + private static class ParentProcessHelperStable + { + public static Process GetParentProcess(Process process) + { + var processId = ParentProcessId(process.Id); + + if (processId == -1) + { + return null; + } + else + { + return Process.GetProcessById(processId); + } + } + + private static int ParentProcessId(int id) + { + var pe32 = new PROCESSENTRY32 + { + dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32)) + }; + + using (var hSnapshot = CreateToolhelp32Snapshot(SnapshotFlags.Process, (uint)id)) + { + if (hSnapshot.IsInvalid) + { + throw new Win32Exception(); + } + + if (!Process32First(hSnapshot, ref pe32)) + { + var errno = Marshal.GetLastWin32Error(); + + if (errno == ERROR_NO_MORE_FILES) + { + return -1; + } + + throw new Win32Exception(errno); + } + + do + { + if (pe32.th32ProcessID == (uint)id) + { + return (int)pe32.th32ParentProcessID; + } + } while (Process32Next(hSnapshot, ref pe32)); + } + + return -1; + } + + private const int ERROR_NO_MORE_FILES = 0x12; + [DllImport("kernel32.dll", SetLastError = true)] + private static extern SafeSnapshotHandle CreateToolhelp32Snapshot(SnapshotFlags flags, uint id); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool Process32First(SafeSnapshotHandle hSnapshot, ref PROCESSENTRY32 lppe); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool Process32Next(SafeSnapshotHandle hSnapshot, ref PROCESSENTRY32 lppe); + + [Flags] + private enum SnapshotFlags : uint + { + HeapList = 0x00000001, + Process = 0x00000002, + Thread = 0x00000004, + Module = 0x00000008, + Module32 = 0x00000010, + All = (HeapList | Process | Thread | Module), + Inherit = 0x80000000, + NoHeaps = 0x40000000 + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESSENTRY32 + { +#pragma warning disable IDE1006 // Naming Styles + public uint dwSize; + public uint cntUsage; + public uint th32ProcessID; + public IntPtr th32DefaultHeapID; + public uint th32ModuleID; + public uint cntThreads; + public uint th32ParentProcessID; + public int pcPriClassBase; + public uint dwFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string szExeFile; +#pragma warning restore IDE1006 // Naming Styles + } + + [SuppressUnmanagedCodeSecurity, HostProtection(SecurityAction.LinkDemand, MayLeakOnAbort = true)] + internal sealed class SafeSnapshotHandle : SafeHandleMinusOneIsInvalid + { + internal SafeSnapshotHandle() + : base(true) + { + } + + [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)] + internal SafeSnapshotHandle(IntPtr handle) + : base(true) + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + return CloseHandle(handle); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + private static extern bool CloseHandle(IntPtr handle); + } + } + #pragma warning disable IDE0022, IDE1006 [Obsolete("This overload is deprecated and will be removed in v3.")] public static bool user_is_administrator() diff --git a/src/chocolatey/infrastructure/information/ProcessTree.cs b/src/chocolatey/infrastructure/information/ProcessTree.cs new file mode 100644 index 0000000000..fa36ab54be --- /dev/null +++ b/src/chocolatey/infrastructure/information/ProcessTree.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace chocolatey.infrastructure.information +{ + [DebuggerDisplay("{" + nameof(GetDebuggerDisplay) + "(),nq}")] + public class ProcessTree + { + // IGNORED USER AGENT PROCESSES + // Our Pester tests may need their own exclusion list in the verification + // updated when this list changes. Search the repo for the above string + // in caps if you have trouble finding the corresponding list in tests + // (should be in UserAgent.Tests.ps1). + private static readonly string[] _filteredParents = new[] + { + // Windows processes and shells + "explorer", + "winlogon", + "powershell", + "pwsh", + "cmd", + "bash", + // The name used to launch windows services + // in the operating system. + "services", + "svchost", + // Nested processes / invoked by the shim choco.exe + "Chocolatey CLI", + // Known Terminal Emulators + "alacritty", + "code", + "ConEmu64", + "ConEmuC64", + "conhost", + "c3270", + "FireCMD", + "Hyper", + "SecureCRT", + "Tabby", + "wezterm", + "wezterm-gui", + "WindowsTerminal", + }; + + public ProcessTree(string currentProcessName) + { + CurrentProcessName = ToFriendlyName(currentProcessName); + } + + public string CurrentProcessName { get; } + + public string FirstFilteredProcessName + { + get { return GetFirstProcess(includeIgnored: false); } + } + + public string FirstProcessName + { + get { return GetFirstProcess(includeIgnored: true); } + } + + public string LastFilteredProcessName + { + get { return GetLastProcess(includeIgnored: false); } + } + + public string LastProcessName + { + get { return GetLastProcess(includeIgnored: true); } + } + + public LinkedList Processes { get; } = new LinkedList(); + + private static bool IsIgnoredProcess(string value) + { + return _filteredParents.Contains(value, StringComparer.OrdinalIgnoreCase); + } + + protected virtual string ToFriendlyName(string value) + { + switch (value.ToLowerInvariant()) + { + case "choco": + return "Chocolatey CLI"; + + case "chocolateygui": + return "Chocolatey GUI"; + + case "chocolatey-agent": + return "Chocolatey Agent"; + + default: + return value; + } + } + + private string GetFirstProcess(bool includeIgnored) + { + if (Processes.Count == 0) + { + return null; + } + + if (includeIgnored) + { + return Processes.First.Value; + } + + LinkedListNode currentNode = Processes.First; + + while (currentNode != null) + { + if (!IsIgnoredProcess(currentNode.Value) && currentNode.Value != CurrentProcessName) + { + return ToFriendlyName(currentNode.Value); + } + + currentNode = currentNode.Next; + } + + return null; + } + + private string GetLastProcess(bool includeIgnored) + { + if (Processes.Count == 0) + { + return null; + } + + if (includeIgnored) + { + return Processes.Last.Value; + } + + LinkedListNode currentNode = Processes.Last; + + while (currentNode != null) + { + if (!IsIgnoredProcess(currentNode.Value) && currentNode.Value != CurrentProcessName) + { + return ToFriendlyName(currentNode.Value); + } + + currentNode = currentNode.Previous; + } + + return null; + } + + public override string ToString() + { + if (Processes.Count == 0) + { + return CurrentProcessName; + } + else + { + return CurrentProcessName + " => " + string.Join(" => ", Processes.Select(ToFriendlyName)); + } + } + + private string GetDebuggerDisplay() + { + return "ProcessTree (" + ToString() + ")"; + } + } +} diff --git a/tests/pester-tests/features/UserAgent.Tests.ps1 b/tests/pester-tests/features/UserAgent.Tests.ps1 new file mode 100644 index 0000000000..0637a8be2b --- /dev/null +++ b/tests/pester-tests/features/UserAgent.Tests.ps1 @@ -0,0 +1,91 @@ +Import-Module helpers/common-helpers + +Describe "Chocolatey User Agent" -Tag Chocolatey, UserAgent { + BeforeAll { + Initialize-ChocolateyTestInstall + New-ChocolateyInstallSnapshot + + $Output = Invoke-Choco search chocolatey --debug + $ChocolateyVersion = Get-ChocolateyVersion + + $Processes = [System.Collections.Generic.List[string]]::new() + + # IGNORED USER AGENT PROCESSES + # This list should match the one in CLI code for things we don't expect to see in the user agent + # after filtering the process tree. + # The corresponding list can be found in chocolatey.infrastructure.information.ProcessTree; + # search the repo for the above string in caps if you have trouble finding it. + $ExcludedProcesses = @( + "explorer" + "winlogon" + "powershell" + "pwsh" + "cmd" + "bash" + "services" + "svchost" + "Chocolatey CLI" + "alacritty" + "code" + "ConEmu64" + "ConEmuC64" + "conhost" + "c3270" + "FireCMD" + "Hyper" + "SecureCRT" + "Tabby" + "wezterm" + "wezterm-gui" + "WindowsTerminal" + ) + } + + AfterAll { + Remove-ChocolateyTestInstall + } + + It 'Logs the full process tree to debug' { + $logLine = $Output.Lines | Where-Object { $_ -match '^Process Tree' } + + $logLine | Should -Not -BeNullOrEmpty -Because 'choco.exe should log the process tree to debug' + + Write-Host "================== PROCESS TREE ==================" + Write-Host $logLine + + $parentProcesses = [string[]]@($logLine -replace '^Process Tree: ' -split ' => ' | Select-Object -Skip 1) + if ($parentProcesses.Count -gt 0) { + $Processes.AddRange($parentProcesses) + } + } + + It 'Logs the final user agent to debug' { + $logLine = $Output.Lines | Where-Object { $_ -match '^Updating User Agent' } + + $logLine | Should -Not -BeNullOrEmpty -Because "choco.exe should log the user agent string to debug`n$($Output.Lines)" + + Write-Host "================== USER AGENT ==================" + Write-Host $logLine + + $result = $logLine -match "'(?Chocolatey Command Line/[^']+)'" + $result | Should -BeTrue -Because "the user agent string should start with Chocolatey Command Line. $logLine" + + $userAgent = $matches['UserAgent'] + + $userAgent -match 'Chocolatey Command Line/(?[a-z0-9.-]+) ([a-z ]+/([a-z0-9.-]+) )?\((?[^,)]+)(?:, (?[^)]+))?\) via NuGet Client' | + Should -BeTrue -Because "the user agent string should contain the choco.exe version, the licensed extension version if any, and any parent processes. $logLine" + + $matches['Version'] | Should -Be $ChocolateyVersion -Because "the user agent string should contain the currently running Chocolatey version. $logLine" + $filteredProcesses = @($Processes | Where-Object { $_ -notin $ExcludedProcesses }) + + if ($filteredProcesses.Count -gt 1) { + $rootProcess = $filteredProcesses[-1] + $matches['RootProcess'] | Should -Be $rootProcess -Because "the user agent string should show the root calling process '$rootProcess'. $logLine" + } + + if ($filteredProcesses.Count -gt 0) { + $callingProcess = $filtered[0] + $matches['ParentProcess'] | Should -Be $callingProcess -Because "the user agent string should show the parent process '$callingProcess'. $logLine" + } + } +} \ No newline at end of file