From aa216b403eb2686538799455521bad34e620a507 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 31 Jan 2025 10:23:03 +0000 Subject: [PATCH 01/37] Change to process group --- src/MonitoringDemo/DemoLauncher.cs | 18 +- src/MonitoringDemo/Job.cs | 209 ------------------ src/MonitoringDemo/ProcessGroup.cs | 336 +++++++++++++++++++++++++++++ 3 files changed, 345 insertions(+), 218 deletions(-) delete mode 100644 src/MonitoringDemo/Job.cs create mode 100644 src/MonitoringDemo/ProcessGroup.cs diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index 6716868b..affde75e 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -4,7 +4,7 @@ sealed class DemoLauncher : IDisposable { public DemoLauncher() { - demoJob = new Job("Particular.MonitoringDemo"); + demoProcessGroup = new ProcessGroup("Particular.MonitoringDemo"); File.WriteAllText(@".\Marker.sln", string.Empty); } @@ -13,7 +13,7 @@ public void Dispose() { disposed = true; - demoJob.Dispose(); + demoProcessGroup.Dispose(); File.Delete(@".\Marker.sln"); @@ -35,7 +35,7 @@ public void Platform() return; } - demoJob.AddProcess(Path.Combine("Platform", $"Platform.exe")); + demoProcessGroup.AddProcess(Path.Combine("Platform", $"Platform.exe")); } public void Billing() @@ -45,7 +45,7 @@ public void Billing() return; } - demoJob.AddProcess(Path.Combine("Billing", "Billing.exe")); + demoProcessGroup.AddProcess(Path.Combine("Billing", "Billing.exe")); } public void Shipping() @@ -55,7 +55,7 @@ public void Shipping() return; } - demoJob.AddProcess(Path.Combine("Shipping", "Shipping.exe")); + demoProcessGroup.AddProcess(Path.Combine("Shipping", "Shipping.exe")); } public void ScaleOutSales() @@ -65,7 +65,7 @@ public void ScaleOutSales() return; } - demoJob.AddProcess(Path.Combine("Sales", "Sales.exe")); + demoProcessGroup.AddProcess(Path.Combine("Sales", "Sales.exe")); } public void ScaleInSales() @@ -75,7 +75,7 @@ public void ScaleInSales() return; } - demoJob.KillProcess(Path.Combine("Sales", "Sales.exe")); + demoProcessGroup.KillProcess(Path.Combine("Sales", "Sales.exe")); } public void ClientUI() @@ -85,9 +85,9 @@ public void ClientUI() return; } - demoJob.AddProcess(Path.Combine("ClientUI", "ClientUI.exe")); + demoProcessGroup.AddProcess(Path.Combine("ClientUI", "ClientUI.exe")); } - readonly Job demoJob; + readonly ProcessGroup demoProcessGroup; private bool disposed; } \ No newline at end of file diff --git a/src/MonitoringDemo/Job.cs b/src/MonitoringDemo/Job.cs deleted file mode 100644 index 9ae5f42b..00000000 --- a/src/MonitoringDemo/Job.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace MonitoringDemo; - -partial class Job : IDisposable -{ - public Job(string jobName) - { - handle = CreateJobObject(nint.Zero, jobName); - - var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION - { - LimitFlags = 0x2000 - }; - - var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION - { - BasicLimitInformation = info - }; - - var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); - var extendedInfoPtr = Marshal.AllocHGlobal(length); - Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); - - if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length)) - { - throw new Exception($"Unable to set information. Error: {Marshal.GetLastWin32Error()}"); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public bool AddProcess(string relativeExePath) - { - if (!processesByExec.TryGetValue(relativeExePath, out var processes)) - { - processes = []; - processesByExec[relativeExePath] = processes; - } - - var processesCount = processes.Count; - var instanceId = processesCount == 0 ? null : $"instance-{processesCount}"; - - var process = StartProcess(relativeExePath, instanceId); - - if (process is null) - { - return false; - } - - processes.Push(process); - - return AddProcess(process); - } - - public void KillProcess(string relativeExePath) - { - if (!processesByExec.TryGetValue(relativeExePath, out var processes)) - { - return; - } - - while (processes.TryPop(out var victim)) - { - try - { - victim.Kill(true); - return; - } - catch (Exception) - { - //The process has died or has been killed by the user. Let's try to kill another one by doing at - // least another iteration - } - finally - { - victim.Dispose(); - } - } - } - - bool AddProcess(Process process) => AddProcess(process.Handle); - - bool AddProcess(nint processHandle) => AssignProcessToJobObject(handle, processHandle); - - static Process? StartProcess(string relativeExePath, string? arguments = null) - { - var fullExePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, relativeExePath)); - var workingDirectory = Path.GetDirectoryName(fullExePath); - - var startInfo = new ProcessStartInfo(fullExePath) - { - WorkingDirectory = workingDirectory, - UseShellExecute = true - }; - - if (arguments is not null) - { - startInfo.Arguments = arguments; - } - - return Process.Start(startInfo); - } - - void Dispose(bool disposing) - { - if (disposed) - { - return; - } - - if (!disposing) - { - return; - } - - CloseHandle(handle); - handle = nint.Zero; - processesByExec.Clear(); - disposed = true; - } - - [LibraryImport("kernel32.dll", EntryPoint = "CreateJobObjectW", StringMarshalling = StringMarshalling.Utf16)] - private static partial nint CreateJobObject(nint a, string lpName); - - [LibraryImport("kernel32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool SetInformationJobObject(nint hJob, JobObjectInfoType infoType, nint lpJobObjectInfo, uint cbJobObjectInfoLength); - - [LibraryImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool AssignProcessToJobObject(nint job, nint process); - - [LibraryImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool CloseHandle(nint hObject); - - readonly Dictionary> processesByExec = []; - - nint handle; - bool disposed; -} - -#region Helper classes - -[StructLayout(LayoutKind.Sequential)] -#pragma warning disable PS0024 // A non-interface type should not be prefixed with I -struct IO_COUNTERS -#pragma warning restore PS0024 // A non-interface type should not be prefixed with I -{ - public ulong ReadOperationCount; - public ulong WriteOperationCount; - public ulong OtherOperationCount; - public ulong ReadTransferCount; - public ulong WriteTransferCount; - public ulong OtherTransferCount; -} - - -[StructLayout(LayoutKind.Sequential)] -struct JOBOBJECT_BASIC_LIMIT_INFORMATION -{ - public long PerProcessUserTimeLimit; - public long PerJobUserTimeLimit; - public uint LimitFlags; - public nuint MinimumWorkingSetSize; - public nuint MaximumWorkingSetSize; - public uint ActiveProcessLimit; - public nuint Affinity; - public uint PriorityClass; - public uint SchedulingClass; -} - -[StructLayout(LayoutKind.Sequential)] -struct SECURITY_ATTRIBUTES -{ - public uint nLength; - public nint lpSecurityDescriptor; - public int bInheritHandle; -} - -[StructLayout(LayoutKind.Sequential)] -struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION -{ - public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; - public IO_COUNTERS IoInfo; - public nuint ProcessMemoryLimit; - public nuint JobMemoryLimit; - public nuint PeakProcessMemoryUsed; - public nuint PeakJobMemoryUsed; -} - -enum JobObjectInfoType -{ - AssociateCompletionPortInformation = 7, - BasicLimitInformation = 2, - BasicUIRestrictions = 4, - EndOfJobTimeInformation = 6, - ExtendedLimitInformation = 9, - SecurityLimitInformation = 5, - GroupInformation = 11 -} - -#endregion diff --git a/src/MonitoringDemo/ProcessGroup.cs b/src/MonitoringDemo/ProcessGroup.cs new file mode 100644 index 00000000..cdf531e6 --- /dev/null +++ b/src/MonitoringDemo/ProcessGroup.cs @@ -0,0 +1,336 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace MonitoringDemo; + +/// +/// Provides process group management functionality across Windows, Linux, and macOS. +/// Uses Windows Job Objects on Windows and Process Groups on Unix-like systems. +/// +partial class ProcessGroup : IDisposable +{ + readonly Dictionary> processesByExec = []; + readonly List managedProcessIds = []; + bool disposed; + + // Windows-specific fields + nint jobHandle; + + public ProcessGroup(string groupName) + { + if (OperatingSystem.IsWindows()) + { + InitializeWindowsJob(groupName); + } + } + + public bool AddProcess(string relativeExePath) + { + if (!processesByExec.TryGetValue(relativeExePath, out var processes)) + { + processes = []; + processesByExec[relativeExePath] = processes; + } + + var processesCount = processes.Count; + var instanceId = processesCount == 0 ? null : $"instance-{processesCount}"; + + var process = StartProcess(relativeExePath, instanceId); + + if (process is null) + { + return false; + } + + processes.Push(process); + managedProcessIds.Add(process.Id); + + return OperatingSystem.IsWindows() ? AddProcessToWindowsJob(process) + : (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) && AddProcessToUnixGroup(process); + } + + public void KillProcess(string relativeExePath) + { + if (!processesByExec.TryGetValue(relativeExePath, out var processes)) + { + return; + } + + while (processes.TryPop(out var victim)) + { + try + { + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + KillProcessGroupUnix(victim.Id); + } + else + { + victim.Kill(true); + } + return; + } + catch (Exception) + { + // Process already terminated + } + finally + { + victim.Dispose(); + } + } + } + + #region Unix-specific implementations + + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + private static bool AddProcessToUnixGroup(Process process) + { + try + { + // Set process group ID to child process ID (creating new group) + return setpgid(process.Id, process.Id) == 0; + } + catch + { + return false; + } + } + + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + private void KillProcessGroupUnix(int processId) + { + // Send SIGTERM to the entire process group + kill(-processId, 15); // 15 is SIGTERM + } + + // P/Invoke declarations for Unix systems + [LibraryImport("libc", SetLastError = true)] + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + private static partial int setpgid(int pid, int pgid); + + [LibraryImport("libc", SetLastError = true)] + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + private static partial int kill(int pid, int sig); + + [LibraryImport("libc", SetLastError = true, StringMarshalling = StringMarshalling.Utf8)] + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + private static partial int chmod(string path, int mode); + + #endregion + + #region Windows-specific implementations + + [SupportedOSPlatform("windows")] + private void InitializeWindowsJob(string jobName) + { + jobHandle = CreateJobObject(nint.Zero, jobName); + + var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION + { + LimitFlags = 0x2000 // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + }; + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + BasicLimitInformation = info + }; + + var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + var extendedInfoPtr = Marshal.AllocHGlobal(length); + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + try + { + if (!SetInformationJobObject(jobHandle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length)) + { + throw new Exception($"Unable to set information. Error: {Marshal.GetLastWin32Error()}"); + } + } + finally + { + Marshal.FreeHGlobal(extendedInfoPtr); + } + } + + [SupportedOSPlatform("windows")] + private bool AddProcessToWindowsJob(Process process) => + AssignProcessToJobObject(jobHandle, process.Handle); + + [SupportedOSPlatform("windows")] + private void DisposeWindowsJob() + { + if (jobHandle != nint.Zero) + { + CloseHandle(jobHandle); + jobHandle = nint.Zero; + } + } + + // P/Invoke declarations for Windows + [LibraryImport("kernel32.dll", EntryPoint = "CreateJobObjectW", StringMarshalling = StringMarshalling.Utf16)] + [SupportedOSPlatform("windows")] + private static partial nint CreateJobObject(nint a, string lpName); + + [LibraryImport("kernel32.dll")] + [SupportedOSPlatform("windows")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SetInformationJobObject(nint hJob, JobObjectInfoType infoType, nint lpJobObjectInfo, uint cbJobObjectInfoLength); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [SupportedOSPlatform("windows")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool AssignProcessToJobObject(nint job, nint process); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [SupportedOSPlatform("windows")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool CloseHandle(nint hObject); + + #endregion + + private static Process? StartProcess(string relativeExePath, string? arguments = null) + { + // Handle platform-specific executable names + var adjustedPath = relativeExePath; + if (!OperatingSystem.IsWindows() && relativeExePath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + { + // Remove .exe extension for non-Windows platforms + adjustedPath = relativeExePath[..^4]; + } + + var fullExePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, adjustedPath)); + var workingDirectory = Path.GetDirectoryName(fullExePath); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + // Ensure the file has execute permissions on Unix systems + try + { + chmod(fullExePath, 0x755); // rwxr-xr-x permissions + } + catch + { + // If chmod fails, the Process.Start will likely fail too + } + } + + var startInfo = new ProcessStartInfo(fullExePath) + { + WorkingDirectory = workingDirectory, + UseShellExecute = OperatingSystem.IsWindows(), + CreateNoWindow = !OperatingSystem.IsWindows(), + }; + + if (arguments is not null) + { + startInfo.Arguments = arguments; + } + + return Process.Start(startInfo); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + if (disposing) + { + if (OperatingSystem.IsWindows()) + { + DisposeWindowsJob(); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + // Clean up any remaining processes on Unix systems + foreach (var pid in managedProcessIds) + { + try + { + KillProcessGroupUnix(pid); + } + catch + { + // Process might already be terminated + } + } + } + else + { + throw new PlatformNotSupportedException("Process management is not supported on this platform."); + } + + processesByExec.Clear(); + managedProcessIds.Clear(); + } + + disposed = true; + } +} + +#region Windows-specific structs and enums +#pragma warning disable PS0024 +[StructLayout(LayoutKind.Sequential)] +file struct IO_COUNTERS +#pragma warning restore PS0024 +{ + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; +} + +[StructLayout(LayoutKind.Sequential)] +file struct JOBOBJECT_BASIC_LIMIT_INFORMATION +{ + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public uint LimitFlags; + public nuint MinimumWorkingSetSize; + public nuint MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public nuint Affinity; + public uint PriorityClass; + public uint SchedulingClass; +} + +[StructLayout(LayoutKind.Sequential)] +file struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION +{ + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public nuint ProcessMemoryLimit; + public nuint JobMemoryLimit; + public nuint PeakProcessMemoryUsed; + public nuint PeakJobMemoryUsed; +} + +internal enum JobObjectInfoType +{ + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 +} +#pragma warning restore PS0024 +#endregion \ No newline at end of file From 77953987b94dc65060816e206d9dec9bed8eec12 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 14 Feb 2025 18:54:17 +0100 Subject: [PATCH 02/37] Rename to platformlauncher to not clash with casing with platform --- src/MonitoringDemo/DemoLauncher.cs | 2 +- src/Platform/Platform.csproj | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index affde75e..960d0815 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -35,7 +35,7 @@ public void Platform() return; } - demoProcessGroup.AddProcess(Path.Combine("Platform", $"Platform.exe")); + demoProcessGroup.AddProcess(Path.Combine("Platform", "PlatformLauncher.exe")); } public void Billing() diff --git a/src/Platform/Platform.csproj b/src/Platform/Platform.csproj index 49e8bb17..033f64b0 100644 --- a/src/Platform/Platform.csproj +++ b/src/Platform/Platform.csproj @@ -6,6 +6,7 @@ enable enable ..\binaries\Platform\ + PlatformLauncher From eef6e6ba389876d2dafe3cb85d98c35fdf72e46e Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 13 Mar 2025 16:53:47 +0100 Subject: [PATCH 03/37] Remove runtime --- src/Directory.Build.props | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 61546c64..e214f941 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -8,8 +8,6 @@ true low all - win-x64 - false false @@ -21,4 +19,4 @@ - \ No newline at end of file + From 5c8bcf2615c1b37b38796020f3917649bdb15673 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 13 Mar 2025 16:59:56 +0100 Subject: [PATCH 04/37] Scripts --- src/MonitoringDemo/MonitoringDemo.csproj | 5 +++++ src/MonitoringDemo/launch.ps1 | 1 + src/MonitoringDemo/launch.sh | 1 + 3 files changed, 7 insertions(+) create mode 100644 src/MonitoringDemo/launch.ps1 create mode 100644 src/MonitoringDemo/launch.sh diff --git a/src/MonitoringDemo/MonitoringDemo.csproj b/src/MonitoringDemo/MonitoringDemo.csproj index 54c12dcb..064a107a 100644 --- a/src/MonitoringDemo/MonitoringDemo.csproj +++ b/src/MonitoringDemo/MonitoringDemo.csproj @@ -9,4 +9,9 @@ true + + + + + \ No newline at end of file diff --git a/src/MonitoringDemo/launch.ps1 b/src/MonitoringDemo/launch.ps1 new file mode 100644 index 00000000..8f621f2e --- /dev/null +++ b/src/MonitoringDemo/launch.ps1 @@ -0,0 +1 @@ +dotnet MonitoringDemo.dll \ No newline at end of file diff --git a/src/MonitoringDemo/launch.sh b/src/MonitoringDemo/launch.sh new file mode 100644 index 00000000..8f621f2e --- /dev/null +++ b/src/MonitoringDemo/launch.sh @@ -0,0 +1 @@ +dotnet MonitoringDemo.dll \ No newline at end of file From 35a7b018489a7c61286eb8d23e7faef820d899b5 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 13 Mar 2025 17:02:59 +0100 Subject: [PATCH 05/37] Enable linux builds --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a300562b..e02db97a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,8 @@ jobs: include: - os: windows-latest name: Windows - #- os: ubuntu-latest - # name: Linux + - os: ubuntu-latest + name: Linux fail-fast: false steps: - name: Checkout From 3a37cbe694374c38b41e87d5e1bf097385e1aeed Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 13 Mar 2025 17:06:29 +0100 Subject: [PATCH 06/37] Macos --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e02db97a..63be3377 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: name: Windows - os: ubuntu-latest name: Linux + - os: macos-14 + name: Macos fail-fast: false steps: - name: Checkout From 021c4aa735bee62d3277ca4bd3408d1bd7fbb6b4 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 13 Mar 2025 17:10:51 +0100 Subject: [PATCH 07/37] Readme tweaks --- README.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b1aba7dd..27257dd1 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,18 @@ Self-contained demo showing all of the monitoring components working together. T - https://docs.particular.net/tutorials/monitoring/demo/ -# Prerequisites - -Running the demo requires .Net Framework 4.6.1 or newer. +## Prerequisites In order to run the downloaded sample you will need the following prerequisites. - -- Window operating system, the Particular Service Platform requires the Windows operating system - - Desktop: Windows 8 or higher - - Server: Windows Server 2016 or higher -- Powershell 3.0 or higher -- .NET Framework 4.6.1 (check version) -# Running +- .NET 8.0 or higher + +## Running - Compile `src\MonitoringDemo.sln` -- Execute `src\binaries\MonitoringDemo.exe` +- Execute `src\binaries\launch.sh` or `src\binaries\launch.ps1` -# Deploying +## Deploying 1. Go to the [Release action page](https://github.com/Particular/MonitoringDemo/actions/workflows/release.yml). 2. Click the **Run workflow** button. From 012303a361e781fda2021ff439090c4ac0cb2e84 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 14:38:00 -0400 Subject: [PATCH 08/37] Rename Platform project --- src/MonitoringDemo.sln | 2 +- src/MonitoringDemo/DemoLauncher.cs | 2 +- .../PlatformLauncher.csproj} | 35 +++++++++---------- src/{Platform => PlatformLauncher}/Program.cs | 24 ++++++------- 4 files changed, 31 insertions(+), 32 deletions(-) rename src/{Platform/Platform.csproj => PlatformLauncher/PlatformLauncher.csproj} (77%) rename src/{Platform => PlatformLauncher}/Program.cs (84%) diff --git a/src/MonitoringDemo.sln b/src/MonitoringDemo.sln index 90aea022..baaa8891 100644 --- a/src/MonitoringDemo.sln +++ b/src/MonitoringDemo.sln @@ -14,7 +14,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shipping", "Shipping\Shippi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "Shared\Shared.csproj", "{13B5577A-6635-4964-9B3C-7EA59E4978F4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Platform", "Platform\Platform.csproj", "{D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformLauncher", "PlatformLauncher\PlatformLauncher.csproj", "{D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonitoringDemo", "MonitoringDemo\MonitoringDemo.csproj", "{55C64607-52E9-4E85-A547-50B191856A93}" EndProject diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index 960d0815..53df75ec 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -35,7 +35,7 @@ public void Platform() return; } - demoProcessGroup.AddProcess(Path.Combine("Platform", "PlatformLauncher.exe")); + demoProcessGroup.AddProcess(Path.Combine("PlatformLauncher", "PlatformLauncher.exe")); } public void Billing() diff --git a/src/Platform/Platform.csproj b/src/PlatformLauncher/PlatformLauncher.csproj similarity index 77% rename from src/Platform/Platform.csproj rename to src/PlatformLauncher/PlatformLauncher.csproj index 033f64b0..8031d606 100644 --- a/src/Platform/Platform.csproj +++ b/src/PlatformLauncher/PlatformLauncher.csproj @@ -1,18 +1,17 @@ - - - - net8.0 - Exe - enable - enable - ..\binaries\Platform\ - PlatformLauncher - - - - - - - - - + + + + net8.0 + Exe + enable + enable + ..\binaries\PlatformLauncher\ + + + + + + + + + diff --git a/src/Platform/Program.cs b/src/PlatformLauncher/Program.cs similarity index 84% rename from src/Platform/Program.cs rename to src/PlatformLauncher/Program.cs index b7b367f3..eb7015db 100644 --- a/src/Platform/Program.cs +++ b/src/PlatformLauncher/Program.cs @@ -1,12 +1,12 @@ -using Particular; - -Console.Title = "Platform"; -try -{ - await PlatformLauncher.Launch(showPlatformToolConsoleOutput: false, servicePulseDefaultRoute: "/monitoring"); -} -catch (Exception e) -{ - Console.WriteLine(e); - Console.ReadLine(); -} +using Particular; + +Console.Title = "PlatformLauncher"; +try +{ + await PlatformLauncher.Launch(showPlatformToolConsoleOutput: false, servicePulseDefaultRoute: "/monitoring"); +} +catch (Exception e) +{ + Console.WriteLine(e); + Console.ReadLine(); +} From 53cacf4f3a989de42294329769ed294cd27b0651 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 15:13:29 -0400 Subject: [PATCH 09/37] Launch processes with dotnet host --- src/MonitoringDemo/DemoLauncher.cs | 12 +++---- src/MonitoringDemo/ProcessGroup.cs | 52 ++++++++---------------------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index 53df75ec..bbaf44fc 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -35,7 +35,7 @@ public void Platform() return; } - demoProcessGroup.AddProcess(Path.Combine("PlatformLauncher", "PlatformLauncher.exe")); + demoProcessGroup.AddProcess(Path.Combine("PlatformLauncher", "PlatformLauncher.dll")); } public void Billing() @@ -45,7 +45,7 @@ public void Billing() return; } - demoProcessGroup.AddProcess(Path.Combine("Billing", "Billing.exe")); + demoProcessGroup.AddProcess(Path.Combine("Billing", "Billing.dll")); } public void Shipping() @@ -55,7 +55,7 @@ public void Shipping() return; } - demoProcessGroup.AddProcess(Path.Combine("Shipping", "Shipping.exe")); + demoProcessGroup.AddProcess(Path.Combine("Shipping", "Shipping.dll")); } public void ScaleOutSales() @@ -65,7 +65,7 @@ public void ScaleOutSales() return; } - demoProcessGroup.AddProcess(Path.Combine("Sales", "Sales.exe")); + demoProcessGroup.AddProcess(Path.Combine("Sales", "Sales.dll")); } public void ScaleInSales() @@ -75,7 +75,7 @@ public void ScaleInSales() return; } - demoProcessGroup.KillProcess(Path.Combine("Sales", "Sales.exe")); + demoProcessGroup.KillProcess(Path.Combine("Sales", "Sales.dll")); } public void ClientUI() @@ -85,7 +85,7 @@ public void ClientUI() return; } - demoProcessGroup.AddProcess(Path.Combine("ClientUI", "ClientUI.exe")); + demoProcessGroup.AddProcess(Path.Combine("ClientUI", "ClientUI.dll")); } readonly ProcessGroup demoProcessGroup; diff --git a/src/MonitoringDemo/ProcessGroup.cs b/src/MonitoringDemo/ProcessGroup.cs index cdf531e6..2dcb5601 100644 --- a/src/MonitoringDemo/ProcessGroup.cs +++ b/src/MonitoringDemo/ProcessGroup.cs @@ -10,7 +10,7 @@ namespace MonitoringDemo; /// partial class ProcessGroup : IDisposable { - readonly Dictionary> processesByExec = []; + readonly Dictionary> processesByAssemblyPath = []; readonly List managedProcessIds = []; bool disposed; @@ -25,18 +25,18 @@ public ProcessGroup(string groupName) } } - public bool AddProcess(string relativeExePath) + public bool AddProcess(string relativeAssemblyPath) { - if (!processesByExec.TryGetValue(relativeExePath, out var processes)) + if (!processesByAssemblyPath.TryGetValue(relativeAssemblyPath, out var processes)) { processes = []; - processesByExec[relativeExePath] = processes; + processesByAssemblyPath[relativeAssemblyPath] = processes; } var processesCount = processes.Count; var instanceId = processesCount == 0 ? null : $"instance-{processesCount}"; - var process = StartProcess(relativeExePath, instanceId); + var process = StartProcess(relativeAssemblyPath, instanceId); if (process is null) { @@ -50,9 +50,9 @@ public bool AddProcess(string relativeExePath) : (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) && AddProcessToUnixGroup(process); } - public void KillProcess(string relativeExePath) + public void KillProcess(string relativeAssemblyPath) { - if (!processesByExec.TryGetValue(relativeExePath, out var processes)) + if (!processesByAssemblyPath.TryGetValue(relativeAssemblyPath, out var processes)) { return; } @@ -118,11 +118,6 @@ private void KillProcessGroupUnix(int processId) [SupportedOSPlatform("macos")] private static partial int kill(int pid, int sig); - [LibraryImport("libc", SetLastError = true, StringMarshalling = StringMarshalling.Utf8)] - [SupportedOSPlatform("linux")] - [SupportedOSPlatform("macos")] - private static partial int chmod(string path, int mode); - #endregion #region Windows-specific implementations @@ -195,33 +190,12 @@ private void DisposeWindowsJob() #endregion - private static Process? StartProcess(string relativeExePath, string? arguments = null) + private static Process? StartProcess(string relativeAssemblyPath, string? arguments = null) { - // Handle platform-specific executable names - var adjustedPath = relativeExePath; - if (!OperatingSystem.IsWindows() && relativeExePath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) - { - // Remove .exe extension for non-Windows platforms - adjustedPath = relativeExePath[..^4]; - } - - var fullExePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, adjustedPath)); - var workingDirectory = Path.GetDirectoryName(fullExePath); - - if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) - { - // Ensure the file has execute permissions on Unix systems - try - { - chmod(fullExePath, 0x755); // rwxr-xr-x permissions - } - catch - { - // If chmod fails, the Process.Start will likely fail too - } - } + var fullAssemblyPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, relativeAssemblyPath)); + var workingDirectory = Path.GetDirectoryName(fullAssemblyPath); - var startInfo = new ProcessStartInfo(fullExePath) + var startInfo = new ProcessStartInfo("dotnet", fullAssemblyPath) { WorkingDirectory = workingDirectory, UseShellExecute = OperatingSystem.IsWindows(), @@ -230,7 +204,7 @@ private void DisposeWindowsJob() if (arguments is not null) { - startInfo.Arguments = arguments; + startInfo.Arguments += $" {arguments}"; } return Process.Start(startInfo); @@ -275,7 +249,7 @@ protected virtual void Dispose(bool disposing) throw new PlatformNotSupportedException("Process management is not supported on this platform."); } - processesByExec.Clear(); + processesByAssemblyPath.Clear(); managedProcessIds.Clear(); } From 957a7494c10fe5195c9ced8005b959dfa8a18a24 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 15:19:04 -0400 Subject: [PATCH 10/37] Remove icons that won't be seen anyway --- src/Billing/Billing.csproj | 1 - src/Billing/failures.ico | Bin 24362 -> 0 bytes src/ClientUI/ClientUI.csproj | 1 - src/ClientUI/traffic.ico | Bin 23462 -> 0 bytes src/Sales/Sales.csproj | 1 - src/Sales/processing-time-alternate.ico | Bin 24362 -> 0 bytes src/Shipping/Shipping.csproj | 1 - src/Shipping/processing-time.ico | Bin 24362 -> 0 bytes 8 files changed, 4 deletions(-) delete mode 100644 src/Billing/failures.ico delete mode 100644 src/ClientUI/traffic.ico delete mode 100644 src/Sales/processing-time-alternate.ico delete mode 100644 src/Shipping/processing-time.ico diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index b0bb2f24..eafacbd9 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -6,7 +6,6 @@ enable enable ..\binaries\Billing\ - failures.ico diff --git a/src/Billing/failures.ico b/src/Billing/failures.ico deleted file mode 100644 index d302303d3260e6cbde8644836877bc3836ab2374..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24362 zcmdsuw?Pz<?~v!Vuv_?%Ft$Sabp`Wa~wMxF>?$%8#1#!yC5@Dcc+4aX5pW% zGAGB>{p@OPu5XOROIVEhoSbX(&79B5$~yV6V?BKIXnia;Z1D3vdjdQ+S6};Zo3gW; z7RMNFafKOf#H_EcPsvA*_VB_&Z5Eq0`9tl^uyX^q@#C9$Q`7Ab2jX}5a5tB|vsR#FGNm;RyFd)PEn9};OfXxy^12~rq|Ytf$d}?`A8%=i zjJrdJx_N%Sx>tn1%Yxa86~>vljT`wheY%ggwMDW!a3H|9ZHs(2Z{FW=1d>&=!j+4)6>hBEHNKvZs$hl*@FkWLop^| zjm~#w&TE2~DBZL#2%m`A_U&CU%mh0#=QT05DBZ>_FRzL3-5ZEt*4^F5J3D(L>UQqz z;wJA{uG<%g*}i>|%(}XI`JzSbeD2(~7N zyjXU-cA1MiH*hN~Z01LfL^8W^qgRPBu^x$X;lg$v2n?S|h_9Jx<7pm=LTN8 zYHMq`)w#U&_~BkVCoM4UZ)&WCwD)0-K)&tM`%rv4R=tbQrgaQ>RXi66*@=pNiaR1v`gu8-ktX zobwF9OqBKY^<%;9yL7X20Jl3}@}A%nudJ)9%R-ya)5*@}+}e%Uhky523wHmegP#$* z#9X6KI{tm39vljVPQ4}i%mKICsb*(GZpvC#QBjd8V?{jUt#zV3e4_pKN-EjejN3gh z!EX`E6Ih5nG-7X!?g8#OXnQnO>}yFhmc+1Lr*c{|73&y|&x6J>rSX)W);dpS#{ zgX%GF%fbC`3HyZn(j0GJ!_I7(KZu1K*mKORFH!vlFxH1MT~uFPUHuyR_@nwIWJc}N z#BB(=yjo{ftUZ&^<|^5g%nbkbU%q_#AsIjDeH{84jOt7Sw>!|G`8#oV?t$ICMr}9H zzLEII*wKEDu|6{}UaI4*4eQ9uGJB~W&(n1m?|`n!$+9Jd@m7cRzd&Xu=AXX%IcQ{# zlhF1q*e#LSMD@@y8~IiuT`AzE+{?XQ?*lUXupU1RyDlcOEk(~0+J`wE2QEKo=}$7Z zdl>Hn8L!}cz5#n~YT1;gXSLvyC$qJoq2UqOaY$xM61fEsN7H3?P(9v}m9XQU%oYpk ze}PTU$!yj-OD2iiIWT)trXO?nAoP@5z(voQ<~Oi=OJ+OO!}b!`{ztg|Bjtwvn)mYK z$B#b-ecy)bw4po*cADP{z~&c_n}zKo;70fI^;+|U7%YaK8$y=@%CEG(7l1#x@S&Rg zFv+b9edlR01Jh+_^B3uGSpE_APLTOVYaX6^tE#GAk+qT4Yu%UV`~ha$FgD$-oW@Mp z+k<#|UFJX4YcbI37w=gxjw>C^OK_ud0Q3J*JU^3J2WHPh*EN?g3-P0UPwRVukub!( zECjFL-NsD#19oR{E};LaH)#pX=w5zb=x}k_1>dHQ6kPBg@`Z~zh2GULAMYaemYQXU zah^fG6gFNw9d4~l_`QIPEB0W{2N>HrsO-m__cP`ZvY)XcAvZFXFJv!cSwhCZnL&97 zaz;=_!B|kn@QiI%$H(xD%~oYU$>UVHk>nib$YidPU~?$RWcV0L@I90^ha`oS_(Tjv zBDNwCbCGB+M4~woiRMluBXT1;1>0x%hvh7quVB5RGAI|(TnA;3XILI8@{0P5!Td%c zXZF)TYWpmICIUj0iyA3LRN3QYWkM#w8>~kIG*jASpktO_m1)4tepSx*D}kr9Ap_Wu zT2F>Ck1CVFtjJ4kRKEF*Bx4AqtbUTS{192@0Lf5EX^>=?LaB^oSVqay02zNN6>$hk bjAICrl_4<@$$q5?%OKgS%8X=@Jdpnf52)r* diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj index ef78e1f7..d9a06066 100644 --- a/src/ClientUI/ClientUI.csproj +++ b/src/ClientUI/ClientUI.csproj @@ -6,7 +6,6 @@ enable enable ..\binaries\ClientUI\ - traffic.ico diff --git a/src/ClientUI/traffic.ico b/src/ClientUI/traffic.ico deleted file mode 100644 index d5dc024e952c03e41e336d63a397dd1362596168..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23462 zcmeI4Yitx%6vt;RT2yS6fEp9c|oFvP*1wzjrB zh?)rEHm)Qq`7@AYm1HGPK;APsu9B?e3CMdU$5oP*JOO#nL89j56D+FJf04-I@{XX`Z#5(s;UREy^uktm<=ZL+%F-rl}M%(bPZB@@?R3UBY|=$MG{ z<+H~avZ2U6eF)paMofB*jdW3WHpDla~k8@d%%7#~M5H%;dA zrMVpT<%l=Hu>j7Fhj~BJvHt@|ALl1^zZBz)bQ}5RnaLUB;>+&t?pyghPo6wE4xGM; z5a$Z)uk!w+<9_)x>=z>RF=Qvk80j{0m1WgW<3`L6<7PVeHOHuVkgh$9y_@*{=ralO zLyW!}H@>eMI`WiZbs2uoi}`_5HtZK=YXz^$AxtFEr@Q$9A@D=;>i zn2*J+rEzMOjp)B;&z^k{)v6pmd{|?{*L~#35fzMdaqH{rRd6!&=le?PrsqGgcC&4# zS>gVqVjZMoMCYNgu~Eeu(p4I*%SQBff|b$SMBIp2i* zu=*C)+g*%}(H85{G_b0%$t%%WZKS!0=K~%SFvdQkEpI13*f!eWm8h()V=OJ^S&+Va z_wHF>@mq+;S@;A#L+1Tpdkg!oT40ultj?j%C;9qG2ZJ|Z|3_dzOy3rLF^A2DpI`7b zn$LM#La{>KhfwFeeC_mE73|U02Z42j*Xz}Nt^iIGAZFfYOc+*}gNiZd+`@ChHFpQ< z-6=Uf>UQqj8TQ#I`W8y|PXt!|G;VlCq=VA~u%DEylU+;g=<|>m3$~?*(-$LFytBH5 zI_8PB;W~T?b^R5iHpWNXUcnfBMT`md&mi7~C^2ob!aQ)=j#N}sTrv(3cO@jPrln-o9uu@TvW!b)cdsv<2<>l(CRjYI{sa;l9 z7UmnFE?K{?(f>A{7v7^A3tO2%+_+-P)h{HZ)I(nE7t zKgTR?Snb-iOD!lU2;;PA(iw{BG( zn44s|MZDjF*9nO0tcr??!se!xD_5$erKPepI_>wl(K-(2Y?(Oj%(m^!>qdRotXZQP z&G+2A;WQhgc;AW72WGh#`5V=1)wW^72DQ4n+9-E7Y=+OBMhs+aR=J4%3JVKWtOwm- z6785baP2LTWf99ytl3Jtq@*NTzOL^Buc+VYj>U-Wx59?+Lu&$BQ!xRz^H_^EqCLQD zPHZVA)=ZYa=LKG2-&tl1WVd|X4(L0;F7O>gtkbg*@1g@S18Wi7Tk~XV8ZJMFY8gZv zFLKm9pRHcKdOFO_P8?&Du)2ae!k+utxdo?jkZQZUMgglN;|MM;tGxdp4viLjA>f>3**UzwwZU2>yob5{y+hYI{QD zgc=sWel4hHON80)IW!hK{^;Lggq8( zH9=SzT|;T8Z?|~vCEC5H`E`*O?gNjZuRmJ7H-Epce0?rt`;u&n=J=1K)VP27gAbQ}`O7dvIQt!#-eC zhYedB`qF;^r+dM+6w=4Xjl*&E)#lBcb-($fXJO=A#pms?{x>+htb>IHj*$iVogFKV zSp7%iM$8xQ2Tw+>O)TGG+_K~Ap1pCr49HJ(+&E$NC-Qhf7og@8f_IV@Bip!zd}cvI{xQfzWqaSf25;Ek<6t@3^@F2?xbkUxZw)#VIFt z&JH0z-FGM5;DzrnX}`C0R}cPNFPKAcIUm$By~HJ`f8ra`d;FU2@#SiIiZ@Hs)0H$$ zpQ5B{dX|#nLnBnV$3LViJ)BM-{4hEhoKD#>d#ZOx_xOf%#n0(WIDIOoE1I6EhWwGR zre|_`ws*+B%%|xde~+f8fCB5As$^(-8YnP(mXhjadM+pgbfkfopVL3)^ej%F!s+Rp zo>EHr2VWj_NCzg(uH?pNX>^@DET3>3Dq9>??>u$bQDJ=^h_Z2-;J8=OT3Se_ftDK5bD@Jmz{d|A U6au;rIw%BmFX`Dqom7_3|26J{`v3p{ diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj index 8d6caaa1..4b7bbb80 100644 --- a/src/Sales/Sales.csproj +++ b/src/Sales/Sales.csproj @@ -6,7 +6,6 @@ enable enable ..\binaries\Sales\ - processing-time-alternate.ico diff --git a/src/Sales/processing-time-alternate.ico b/src/Sales/processing-time-alternate.ico deleted file mode 100644 index 776266f2cbf2f5bafd8d4fb51a4ba68c719191ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24362 zcmd^{du$X%9LJ{=DF~&ANCd&tAZmDsMxz*!CKwY0At0g{qC!Yi5+U&~VnDb*G(rFH zL4pxP6A@8Rpj_`-N`brbvd5K|1mvY03M#Kqo}PW&`2Fqetk=uV-d^waZWgEcb$>TI zJM;T}<~OsmbK5djgrD(a8GhfzdcDe6Z^qdDknxaDA(T()jj4SvZR0CX`tOnZ()7o;Qf=8N0#R!A@W?4N?b*Lx_=GX+l{a z7>?(R7iHYI3ojr)5kE8F_sI-6S@HY{{KmKtg>92Ls~-qfJ95M;mE{`F8hY6_#w7(4 z^U|(M4lxr9e@EX8cL|cTW@22<08~4#z+M}z{Y3PVy4Y?SOe|8M?qYMNno~0 zL2ih-u-7nBF@>{WXE3|2Cb!5T#c&zspif7s$xN(EcHUtatB?Y2b92J2V}!?h?6xmm zB4W*v<`{WiAzX$jVD@z`xHXMDz|WmKXIrkm_e&aHBHqGt4PzBjz>M~4ZTcWD=Nz|P zpDTpRFa^x^Db8(`VXQ(5)^3NK;P&_Q!#pMp|A_3PL7_4#~DR1m+}^*z`IS`OWnYY*d3Xg{XA!9E=*r0JUNw_s_{nL{1uh_!r&d* z#7PdW4S!+AOl5AwuJ)@ce!cgPIq<(dV?JDu@9S@NBR66f{H&VCderBe-NAJ4bg6hh zC0@z0d07>|+Gl6J*zLkQ7PrabOys+P8!@c;Ccw`;vdv+3;pJ9iQ!t~WTexZQ=d0ug zW^U$9U2{8bn@a2P4t01!GM99^tGMaocjJlxKmJ^hH+KnV6@N5u9-6k9*Q^XIPRA+r zyc@ZRxv%;=Ki@xl6F)a>yX|!{vHo-THr_C2BM+=G=FyK+xupB-W^RHX9f`q?_iOn6 zH#hNPF9i9CF(H0(bR9o2u8tp_66AZ|s^wchtk%|r$=B(+Te)2^@ukf@)#3AQ;}!#t z=@k_f{c&t?n2l9GxQW~8Kp^m#m`7@AYPy2k_XT3-7H&H?Y}n98FcU}4c@gH@IbheC zXMS$rwqo__)y6)(V3vFYvoVmyJhD@nTQkK{G9N1)WB6WhJCrAOs&YFI|DUqrWOlBq zs;WD#^W}k^irn_$yRy5@xKN%xr`;kuk1<^gZXYcgrmf{<w&M(WrpW`TR`S(3~AN-R+ow8@Gf^eg;MKK_YGm0Jf7HWroS5WsP zS-q5xI!8eMYDeA44D-z`2EV3R`Gxm(9)eFNv+yhJJqG>K4|YVdXd|E8PQlJct(d`= zmr&+<7XBp1guk}7woKL@;=ME4S>DdJ^2Kcj)&#f9e4w=|m@RCl56P@j^}~qY`(*7V z?F+PI*mgd+Ed`fOGTX7w(G&Ca#tiLF1nt@v1-YeC;w?h(L-fe^d<0#0!|k8x-DZY%s43p?|<_ruZVN~=97^kXT?RM_Z8 zC%Ekgvvkkph1hEp@wf+E?v#yNn)7hp3yE9s%PBYX)hc@Smf3{9?gm?au&_t?=JfF_ z`nFtVC*{M&7a_8Jfn0IZ)=bn_GFz}_8UQ;EK%8b`w|-hn&XC!wvm1t3IALOQF1TGr zy%Vh3$Ng#~%A7T6->#32l><9F`vO~1Y|ob4G3@UQvSNn*O8Z=%Vv}QxEZW(+cI{gF z_kpHK-xo+;U$&bY`l))|x^+EF8$u>Mm&494Eo?0WAFDQhLmxjZ^M~^F7@&QD_Hz4# z*7&kEa6j*hy6auUOz^x4n=Svg8@@e(GH=U#m*rc{{d5iLd>rxzMA+iCV;phyuGQXi zItDU!vV5?b3dz-Td7*g+to8+D3^K_#nKujVbFSJyf;gamHJkH<`_QeZdlMvAwyIXo zCHOWjm*B828;UxAy@8pDK1{nWV3#ZP0fhQO)ct=rehYu4f1_ZywLr# z7vs!h#mMX&xFLCqd5iYW0jJ{vp*J#CF7zm4WkL@zRw8sSW5q&814X(Xht6~z2gc?l z>QB~Vls_VokEYt{{gU!^JxXPgb^HfKvR=#2vreXH3HBICrjIa6Xg(UB<{6sC%J|e` zXc|xPi8zZy{6+F4>C|5$Wuk|0kzT$i`k!7*eWd48_Y2)a{j2Af(}s$!&-a83y`?NF z^x{~t9!rRv#v)XjC|?$0s3@U(yljEcq2Oa89|cfM)9J#JXhNq7VV#yw4I%I}oeW^} z6FM2j$`ks0(mf%qjK-)eO1kDCD{dscL<_>A80lz|Qk-;{LTLf%u#A!?2%Q)pm2v3A b38{p1tr}EKdQ{71MWlxkx;DOaRkQ71%;cV0 diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj index 39b65477..4ca1517d 100644 --- a/src/Shipping/Shipping.csproj +++ b/src/Shipping/Shipping.csproj @@ -6,7 +6,6 @@ enable enable ..\binaries\Shipping\ - processing-time.ico diff --git a/src/Shipping/processing-time.ico b/src/Shipping/processing-time.ico deleted file mode 100644 index afd6abae889887de4254a03dcbce6346509348c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24362 zcmd^{e{2**6vw9&DG0TQNCd%Z5H{i@p*05Fh+PBPA7M9k zusa!YF-FWp?EQlEPA@yL1FzB;GxM-ZW1rfE8^*_EEtq-M(JefK4cDEWxvs6zh#SRS zp7z`v>{-p25w|uxlijN`JB-mS8ge6cPF=b2Ox7LtHEPMt!H#LmEsc%Ul9||Nna=Li zksa3P??r@LMpy2BrFZBuqlahBA^6CS4)%hU+#KwiNN{U*j^TB6bs@{u{H39BL%(Xt z&9hevEwTEOjs|N9KO#T&)_nMGrCyIlL{STf5n{%^SoU z*U$Hgdsbq*>q1y@6CXP|;%g+Yo$Aj_*LWjtB}Ik&;#3{fjrLivq zG1HkFv8%}Ynm4}MFMNL$drYeNjGhtY*PNMn1>xM@9l42n-_C-~yk^yWUbkRm$m?Wc{nxvrc=eivytJ@D+(%dXa|!m^&fEk)Iue5e z>p$T&E9ddrr7!YR+2i@?ta1F*2NU_PA5G(jS1;iEzgl6e3w^IMb-QwF2;hr|g=ie@ z#;pmwW)>C}4#KfIhV1Ob?QCgj>Eq%)QeIx(6WqRw5j(qZ`@XEKtiNC;j-2y2+;10x zU0am-*@4@}yu3W`KD}V(djzxbkoqXH)0tZf#gZ={m5wodAGjTh5<6YFU4Z{jt2hNZ zw`0eSTX0=*6xiv=?Fim0yC)DA$}{h2x600AOvi)Ua*NsN#;urQM#jWv4!v$}4OAXu zd>YtYvxuKA+}wzpMF-__OCH_5KYHGLtKfv)X9UaYy*!tY@+pD7>A=D@#Zm5-=nAI7(I@36A6vMb80 zR@D*axma>50~6`}9UKyHeq9*;oJ4u$_j}Af_&1C?WzV`X;YMSNVn7yW6g%*(svZ6{ zpzbNMdMO`uj)wf*j=FsrMw?p`{K{1Mh0pCg44+PSz^|Zl4Ekjt>^RhcHloSx4D5VN z#SFfrp-f{3{PB&6l8TCoBw2fi_wHzCLp$4w7PkXf6Wk&5f!3;Ewyd2#__FfXk0XBX zm$mD&FVGrh+tJ{b11{ZUwqu{85AN5y!nE6;kLvYBuzN<5r?up4nY|{vk%)y;0c?&0 zw=1Z3lB#{oS7T7-e1P^t_0h6&U>C~1z+OMwJIbvV`#VEb%+Oy!pUeGhvW$_Uo&8(4 zZl#|O44BM)0pImyySbsCc5d6Yt#`nNkV((wuyeh_)>!aSwfQ^x_z{^ulyAlW?F+P* z+bUY)%i6$vJ^*zev=KAG^D1msezzOGJ&7`lWWLMtRrh|n26a9G`4b{+vD?v&xLTsx zdk)4x#!i+GRt`v{p37s+yHvF=AY%|tet`RCtbLAD`zH_w^i#7DPnd^pL*2U}k+M~{ zdM?Aai2j}+)@375=Wm^undrly`vRc`B=rG=`a;xwBOEuuU+H%gRP1Oj!F&_T=ki$h z&tZ(SWEG=8=fDlg8P8jd&m3?%W(&QZu@s@Z7)ug*6=MlPcQO_)bTklW>Tc-F)Nx>J zv8R4ty@~Qid-BoL_4+`cd{cK(8DAa0ph(sm`BB!%6eGbNZ<6Vwy(J7Ey{{H~HG`G+ zRjXGsc#2oVStQ~wQnHUu{UuU3dKE4*%g4F?*W;;=%zWy8p(j)Sn)xZTpmJm7haj4W&KB4t+WzMk|1BM9S~NJo>D+@!-4O4+2tGD^vn(1`(3 g5{FKlkP=8YszKGHyNqo7RyXu2k8X@FQ#EY+58S$O^Z)<= From 1be28e62be76c68e00e9aae059ad57446ec5985f Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 15:23:32 -0400 Subject: [PATCH 11/37] Clean up package references --- src/Billing/Billing.csproj | 8 +++----- src/ClientUI/ClientUI.csproj | 8 +++----- src/Messages/Messages.csproj | 2 +- src/PlatformLauncher/PlatformLauncher.csproj | 2 -- src/Sales/Sales.csproj | 8 +++----- src/Shared/Shared.csproj | 8 +++----- src/Shipping/Shipping.csproj | 8 +++----- 7 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index eafacbd9..fd03189e 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -14,11 +14,9 @@ - - - - - + + + \ No newline at end of file diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj index d9a06066..4646af90 100644 --- a/src/ClientUI/ClientUI.csproj +++ b/src/ClientUI/ClientUI.csproj @@ -14,11 +14,9 @@ - - - - - + + + \ No newline at end of file diff --git a/src/Messages/Messages.csproj b/src/Messages/Messages.csproj index 72b74b03..285dd21e 100644 --- a/src/Messages/Messages.csproj +++ b/src/Messages/Messages.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/PlatformLauncher/PlatformLauncher.csproj b/src/PlatformLauncher/PlatformLauncher.csproj index 8031d606..0e9d3a31 100644 --- a/src/PlatformLauncher/PlatformLauncher.csproj +++ b/src/PlatformLauncher/PlatformLauncher.csproj @@ -10,8 +10,6 @@ - - diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj index 4b7bbb80..996b5ab2 100644 --- a/src/Sales/Sales.csproj +++ b/src/Sales/Sales.csproj @@ -14,11 +14,9 @@ - - - - - + + + \ No newline at end of file diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index da29cc5c..8ceecf0c 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -7,11 +7,9 @@ - - - - - + + + diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj index 4ca1517d..f15c576d 100644 --- a/src/Shipping/Shipping.csproj +++ b/src/Shipping/Shipping.csproj @@ -14,11 +14,9 @@ - - - - - + + + \ No newline at end of file From d99d9b9cc93ea943c36a641c5e7609c96fe92388 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 15:27:41 -0400 Subject: [PATCH 12/37] Update Particular.Analyzers --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e214f941..2fd19a9c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -16,7 +16,7 @@ - + From 8bb0f002cac9afd1237d6c3b528e23d68a3aeca1 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 15:35:27 -0400 Subject: [PATCH 13/37] Remove Serilog and Shared project --- src/Billing/Billing.csproj | 1 - src/Billing/Program.cs | 3 -- src/ClientUI/ClientUI.csproj | 1 - src/ClientUI/Program.cs | 3 -- src/MonitoringDemo.sln | 6 ---- src/Sales/Program.cs | 3 -- src/Sales/Sales.csproj | 1 - src/Shared/LoggingUtils.cs | 56 ------------------------------------ src/Shared/Shared.csproj | 15 ---------- src/Shipping/Program.cs | 3 -- src/Shipping/Shipping.csproj | 1 - 11 files changed, 93 deletions(-) delete mode 100644 src/Shared/LoggingUtils.cs delete mode 100644 src/Shared/Shared.csproj diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index fd03189e..191ca454 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index e6fcaddc..363b3778 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -2,13 +2,10 @@ using Billing; using Messages; using Microsoft.Extensions.DependencyInjection; -using Shared; Console.Title = "Failure rate (Billing)"; Console.SetWindowSize(65, 15); -LoggingUtils.ConfigureLogging("Billing"); - var endpointConfiguration = new EndpointConfiguration("Billing"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj index 4646af90..b0d0018c 100644 --- a/src/ClientUI/ClientUI.csproj +++ b/src/ClientUI/ClientUI.csproj @@ -10,7 +10,6 @@ - diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs index 06830cbf..16e5f866 100644 --- a/src/ClientUI/Program.cs +++ b/src/ClientUI/Program.cs @@ -1,13 +1,10 @@ using System.Text.Json; using ClientUI; using Messages; -using Shared; Console.Title = "Load (ClientUI)"; Console.SetWindowSize(65, 15); -LoggingUtils.ConfigureLogging("ClientUI"); - var endpointConfiguration = new EndpointConfiguration("ClientUI"); var serializer = endpointConfiguration.UseSerialization(); diff --git a/src/MonitoringDemo.sln b/src/MonitoringDemo.sln index baaa8891..fa157857 100644 --- a/src/MonitoringDemo.sln +++ b/src/MonitoringDemo.sln @@ -12,8 +12,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Billing", "Billing\Billing. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shipping", "Shipping\Shipping.csproj", "{41F0D809-8FA4-4139-9131-09441D69AFB1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "Shared\Shared.csproj", "{13B5577A-6635-4964-9B3C-7EA59E4978F4}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformLauncher", "PlatformLauncher\PlatformLauncher.csproj", "{D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonitoringDemo", "MonitoringDemo\MonitoringDemo.csproj", "{55C64607-52E9-4E85-A547-50B191856A93}" @@ -49,10 +47,6 @@ Global {41F0D809-8FA4-4139-9131-09441D69AFB1}.Debug|Any CPU.Build.0 = Debug|Any CPU {41F0D809-8FA4-4139-9131-09441D69AFB1}.Release|Any CPU.ActiveCfg = Release|Any CPU {41F0D809-8FA4-4139-9131-09441D69AFB1}.Release|Any CPU.Build.0 = Release|Any CPU - {13B5577A-6635-4964-9B3C-7EA59E4978F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13B5577A-6635-4964-9B3C-7EA59E4978F4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13B5577A-6635-4964-9B3C-7EA59E4978F4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13B5577A-6635-4964-9B3C-7EA59E4978F4}.Release|Any CPU.Build.0 = Release|Any CPU {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs index 5472c887..63cf7076 100644 --- a/src/Sales/Program.cs +++ b/src/Sales/Program.cs @@ -4,12 +4,9 @@ using Messages; using Microsoft.Extensions.DependencyInjection; using Sales; -using Shared; Console.SetWindowSize(65, 15); -LoggingUtils.ConfigureLogging("Sales"); - var instanceName = args.FirstOrDefault(); if (string.IsNullOrEmpty(instanceName)) diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj index 996b5ab2..3d04ca70 100644 --- a/src/Sales/Sales.csproj +++ b/src/Sales/Sales.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Shared/LoggingUtils.cs b/src/Shared/LoggingUtils.cs deleted file mode 100644 index 42be799f..00000000 --- a/src/Shared/LoggingUtils.cs +++ /dev/null @@ -1,56 +0,0 @@ - -using System.Reflection; -using NServiceBus.Extensions.Logging; -using NServiceBus.Logging; -using Serilog; -using Serilog.Extensions.Logging; - -namespace Shared; - -public static class LoggingUtils -{ - public static void ConfigureLogging(string endpointName) - { - var logsFolder = GetLogLocation(); - - if (logsFolder is null) - { - return; - } - - var logPath = Path.Combine(logsFolder, $"{endpointName}.txt"); - - Log.Logger = new LoggerConfiguration() - .WriteTo.File(logPath) - .CreateLogger(); - - LogManager.UseFactory(new ExtensionsLoggerFactory(new SerilogLoggerFactory())); - } - - static string? GetLogLocation() - { - var assemblyPath = new Uri(Assembly.GetExecutingAssembly().Location).LocalPath; - var assemblyFolder = Path.GetDirectoryName(assemblyPath); - - if (string.IsNullOrEmpty(assemblyFolder)) - { - return null; - } - - var workingDir = new DirectoryInfo(assemblyFolder); - var logLocation = FindLogFolder(workingDir); - return (logLocation ?? workingDir).FullName; - } - - static DirectoryInfo? FindLogFolder(DirectoryInfo? currentDir) - { - if (currentDir is null) - { - return null; - } - - var logsFolders = currentDir.GetDirectories("logs", SearchOption.TopDirectoryOnly); - - return logsFolders.FirstOrDefault() ?? FindLogFolder(currentDir.Parent); - } -} \ No newline at end of file diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj deleted file mode 100644 index 8ceecf0c..00000000 --- a/src/Shared/Shared.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs index a8ac51ff..d90e79d1 100644 --- a/src/Shipping/Program.cs +++ b/src/Shipping/Program.cs @@ -1,14 +1,11 @@ using System.Text.Json; using Messages; using Microsoft.Extensions.DependencyInjection; -using Shared; using Shipping; Console.Title = "Processing (Shipping)"; Console.SetWindowSize(65, 15); -LoggingUtils.ConfigureLogging("Shipping"); - var endpointConfiguration = new EndpointConfiguration("Shipping"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj index f15c576d..e6cd3ad3 100644 --- a/src/Shipping/Shipping.csproj +++ b/src/Shipping/Shipping.csproj @@ -10,7 +10,6 @@ - From c073264ded5a657ba49b87c2d209dec9d1b52fff Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 15:46:58 -0400 Subject: [PATCH 14/37] Move binaries out of src folder --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 14 +++++++------- .gitignore | 2 +- src/Billing/Billing.csproj | 2 +- src/ClientUI/ClientUI.csproj | 2 +- src/MonitoringDemo/MonitoringDemo.csproj | 2 +- src/PlatformLauncher/PlatformLauncher.csproj | 2 +- src/Sales/Sales.csproj | 2 +- src/Shipping/Shipping.csproj | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63be3377..cf524b49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,5 +35,5 @@ jobs: uses: actions/upload-artifact@v4.6.2 with: name: binaries - path: src/binaries/* + path: binaries/* retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cce9954..3a59254e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,12 +27,12 @@ jobs: --azure-key-vault-tenant-id ${{ secrets.AZURE_KEY_VAULT_TENANT_ID }} ` --azure-key-vault-client-secret ${{ secrets.AZURE_KEY_VAULT_CLIENT_SECRET }} ` --azure-key-vault-certificate ${{ secrets.AZURE_KEY_VAULT_CERTIFICATE_NAME }} ` - src/binaries/MonitoringDemo.exe ` - src/binaries/Billing/Billing.exe ` - src/binaries/ClientUI/ClientUI.exe ` - src/binaries/Platform/Platform.exe ` - src/binaries/Sales/Sales.exe ` - src/binaries/Shipping/Shipping.exe + binaries/MonitoringDemo.exe ` + binaries/Billing/Billing.exe ` + binaries/ClientUI/ClientUI.exe ` + binaries/Platform/Platform.exe ` + binaries/Sales/Sales.exe ` + binaries/Shipping/Shipping.exe shell: pwsh - name: Setup AWS Credentials uses: aws-actions/configure-aws-credentials@v4.1.0 @@ -44,7 +44,7 @@ jobs: shell: pwsh run: | echo "Creating Particular.MonitoringDemo.zip archive" - Compress-Archive -Path ./src/binaries/* -DestinationPath ./Particular.MonitoringDemo.zip + Compress-Archive -Path ./binaries/* -DestinationPath ./Particular.MonitoringDemo.zip echo "Uploading zip file to AWS" aws s3 cp ./Particular.MonitoringDemo.zip s3://particular.downloads/MonitoringDemo/Particular.MonitoringDemo.zip --content-type application/zip --acl public-read diff --git a/.gitignore b/.gitignore index 3abcba8d..3540ccab 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ **/Platform/**/db **/bin/ **/obj/ -src/binaries/**/* +binaries/**/* **/.vs/ MonitoringDemo.Sql/support/*.log MonitoringDemo.Sql/transport/ diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 191ca454..166e7075 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -5,7 +5,7 @@ Exe enable enable - ..\binaries\Billing\ + ..\..\binaries\Billing\ diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj index b0d0018c..fc7479b4 100644 --- a/src/ClientUI/ClientUI.csproj +++ b/src/ClientUI/ClientUI.csproj @@ -5,7 +5,7 @@ Exe enable enable - ..\binaries\ClientUI\ + ..\..\binaries\ClientUI\ diff --git a/src/MonitoringDemo/MonitoringDemo.csproj b/src/MonitoringDemo/MonitoringDemo.csproj index 064a107a..919f9108 100644 --- a/src/MonitoringDemo/MonitoringDemo.csproj +++ b/src/MonitoringDemo/MonitoringDemo.csproj @@ -5,7 +5,7 @@ Exe enable enable - ..\binaries\ + ..\..\binaries\ true diff --git a/src/PlatformLauncher/PlatformLauncher.csproj b/src/PlatformLauncher/PlatformLauncher.csproj index 0e9d3a31..2a144997 100644 --- a/src/PlatformLauncher/PlatformLauncher.csproj +++ b/src/PlatformLauncher/PlatformLauncher.csproj @@ -5,7 +5,7 @@ Exe enable enable - ..\binaries\PlatformLauncher\ + ..\..\binaries\PlatformLauncher\ diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj index 3d04ca70..21c60d8b 100644 --- a/src/Sales/Sales.csproj +++ b/src/Sales/Sales.csproj @@ -5,7 +5,7 @@ Exe enable enable - ..\binaries\Sales\ + ..\..\binaries\Sales\ diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj index e6cd3ad3..7fe96fbb 100644 --- a/src/Shipping/Shipping.csproj +++ b/src/Shipping/Shipping.csproj @@ -5,7 +5,7 @@ Exe enable enable - ..\binaries\Shipping\ + ..\..\binaries\Shipping\ From 90423379245e2156da367f9fdf25b43a33f46ab2 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 16:05:19 -0400 Subject: [PATCH 15/37] Update workflows --- .github/workflows/ci.yml | 26 +++++++++++++------------- .github/workflows/release.yml | 35 +++++++++++++---------------------- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf524b49..5369dd63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,20 +7,13 @@ on: workflow_dispatch: env: DOTNET_NOLOGO: true +defaults: + run: + shell: pwsh jobs: build: - name: ${{ matrix.name }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: windows-latest - name: Windows - - os: ubuntu-latest - name: Linux - - os: macos-14 - name: Macos - fail-fast: false + name: Linux + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v5.0.0 @@ -30,8 +23,15 @@ jobs: global-json-file: global.json - name: Build run: dotnet build src --configuration Release + - name: Remove executables + run: | + Remove-Item binaries/MonitoringDemo + Remove-Item binaries/Billing/Billing + Remove-Item binaries/ClientUI/ClientUI + Remove-Item binaries/PlatformLauncher/PlatformLauncher + Remove-Item binaries/Sales/Sales + Remove-Item binaries/Shipping/Shipping - name: Publish artifacts - if: matrix.name == 'Windows' uses: actions/upload-artifact@v4.6.2 with: name: binaries diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a59254e..16782c6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,9 +3,12 @@ on: workflow_dispatch: env: DOTNET_NOLOGO: true +defaults: + run: + shell: pwsh jobs: release: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v5.0.0 @@ -15,25 +18,14 @@ jobs: global-json-file: global.json - name: Build run: dotnet build src --configuration Release - - name: Install AzureSignTool - run: dotnet tool install --global azuresigntool - - name: Sign binaries + - name: Remove executables run: | - AzureSignTool sign ` - --file-digest sha256 ` - --timestamp-rfc3161 http://timestamp.digicert.com ` - --azure-key-vault-url https://particularcodesigning.vault.azure.net ` - --azure-key-vault-client-id ${{ secrets.AZURE_KEY_VAULT_CLIENT_ID }} ` - --azure-key-vault-tenant-id ${{ secrets.AZURE_KEY_VAULT_TENANT_ID }} ` - --azure-key-vault-client-secret ${{ secrets.AZURE_KEY_VAULT_CLIENT_SECRET }} ` - --azure-key-vault-certificate ${{ secrets.AZURE_KEY_VAULT_CERTIFICATE_NAME }} ` - binaries/MonitoringDemo.exe ` - binaries/Billing/Billing.exe ` - binaries/ClientUI/ClientUI.exe ` - binaries/Platform/Platform.exe ` - binaries/Sales/Sales.exe ` - binaries/Shipping/Shipping.exe - shell: pwsh + Remove-Item binaries/MonitoringDemo + Remove-Item binaries/Billing/Billing + Remove-Item binaries/ClientUI/ClientUI + Remove-Item binaries/PlatformLauncher/PlatformLauncher + Remove-Item binaries/Sales/Sales + Remove-Item binaries/Shipping/Shipping - name: Setup AWS Credentials uses: aws-actions/configure-aws-credentials@v4.1.0 with: @@ -41,17 +33,16 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRETKEY }} aws-region: us-east-1 - name: Deploy to S3 - shell: pwsh + shell: bash run: | echo "Creating Particular.MonitoringDemo.zip archive" - Compress-Archive -Path ./binaries/* -DestinationPath ./Particular.MonitoringDemo.zip + (cd binaries && zip -r "$OLDPWD/Particular.MonitoringDemo.zip" .) echo "Uploading zip file to AWS" aws s3 cp ./Particular.MonitoringDemo.zip s3://particular.downloads/MonitoringDemo/Particular.MonitoringDemo.zip --content-type application/zip --acl public-read echo "Complete" - name: Upload dependency file to AWS - shell: pwsh run: | $dotnetPackages = dotnet list src/Platform package --include-transitive --format json | ConvertFrom-Json $firstProject = $dotnetPackages.projects[0] From 7021998afaf0f5b4bbdfa711cf099920ee730bf1 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 17:16:00 -0400 Subject: [PATCH 16/37] Make script executable --- src/MonitoringDemo/launch.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 src/MonitoringDemo/launch.sh diff --git a/src/MonitoringDemo/launch.sh b/src/MonitoringDemo/launch.sh old mode 100644 new mode 100755 From 2602dbac333d487790a47576d7e86b3a9211e04b Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 19 Mar 2025 18:57:53 -0400 Subject: [PATCH 17/37] Ensure line endings are right --- .gitattributes | 3 + src/MonitoringDemo.sln | 130 +++++++------- src/MonitoringDemo/ColoredConsole.cs | 38 ++--- src/MonitoringDemo/DemoLauncher.cs | 184 ++++++++++---------- src/MonitoringDemo/DirectoryEx.cs | 112 ++++++------ src/MonitoringDemo/MonitoringDemo.csproj | 32 ++-- src/MonitoringDemo/Program.cs | 208 +++++++++++------------ src/MonitoringDemo/launch.sh | 2 + 8 files changed, 357 insertions(+), 352 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..31bb7740 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Auto detect text files and perform LF normalization +* text=auto +*.sh text eol=lf diff --git a/src/MonitoringDemo.sln b/src/MonitoringDemo.sln index fa157857..7e7d9f90 100644 --- a/src/MonitoringDemo.sln +++ b/src/MonitoringDemo.sln @@ -1,65 +1,65 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34902.65 -MinimumVisualStudioVersion = 15.0.26730.12 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientUI", "ClientUI\ClientUI.csproj", "{D4DCF868-A625-4B0B-BB20-C150553A6548}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messages", "Messages\Messages.csproj", "{F6DE8266-1F56-4241-965C-2DDBB76CEC1E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sales", "Sales\Sales.csproj", "{CD42E5DF-D4A7-4933-8017-1398B2E9560F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Billing", "Billing\Billing.csproj", "{9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shipping", "Shipping\Shipping.csproj", "{41F0D809-8FA4-4139-9131-09441D69AFB1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformLauncher", "PlatformLauncher\PlatformLauncher.csproj", "{D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonitoringDemo", "MonitoringDemo\MonitoringDemo.csproj", "{55C64607-52E9-4E85-A547-50B191856A93}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D29C905-CE3A-4D93-8271-7BA09CEE1631}" - ProjectSection(SolutionItems) = preProject - Directory.Build.props = Directory.Build.props - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D4DCF868-A625-4B0B-BB20-C150553A6548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4DCF868-A625-4B0B-BB20-C150553A6548}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4DCF868-A625-4B0B-BB20-C150553A6548}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4DCF868-A625-4B0B-BB20-C150553A6548}.Release|Any CPU.Build.0 = Release|Any CPU - {F6DE8266-1F56-4241-965C-2DDBB76CEC1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6DE8266-1F56-4241-965C-2DDBB76CEC1E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6DE8266-1F56-4241-965C-2DDBB76CEC1E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6DE8266-1F56-4241-965C-2DDBB76CEC1E}.Release|Any CPU.Build.0 = Release|Any CPU - {CD42E5DF-D4A7-4933-8017-1398B2E9560F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD42E5DF-D4A7-4933-8017-1398B2E9560F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD42E5DF-D4A7-4933-8017-1398B2E9560F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD42E5DF-D4A7-4933-8017-1398B2E9560F}.Release|Any CPU.Build.0 = Release|Any CPU - {9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}.Release|Any CPU.Build.0 = Release|Any CPU - {41F0D809-8FA4-4139-9131-09441D69AFB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {41F0D809-8FA4-4139-9131-09441D69AFB1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {41F0D809-8FA4-4139-9131-09441D69AFB1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {41F0D809-8FA4-4139-9131-09441D69AFB1}.Release|Any CPU.Build.0 = Release|Any CPU - {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Release|Any CPU.Build.0 = Release|Any CPU - {55C64607-52E9-4E85-A547-50B191856A93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {55C64607-52E9-4E85-A547-50B191856A93}.Debug|Any CPU.Build.0 = Debug|Any CPU - {55C64607-52E9-4E85-A547-50B191856A93}.Release|Any CPU.ActiveCfg = Release|Any CPU - {55C64607-52E9-4E85-A547-50B191856A93}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {081FC59E-04F4-4FB2-88A6-64A7C18BAA10} - EndGlobalSection -EndGlobal +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34902.65 +MinimumVisualStudioVersion = 15.0.26730.12 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientUI", "ClientUI\ClientUI.csproj", "{D4DCF868-A625-4B0B-BB20-C150553A6548}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messages", "Messages\Messages.csproj", "{F6DE8266-1F56-4241-965C-2DDBB76CEC1E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sales", "Sales\Sales.csproj", "{CD42E5DF-D4A7-4933-8017-1398B2E9560F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Billing", "Billing\Billing.csproj", "{9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shipping", "Shipping\Shipping.csproj", "{41F0D809-8FA4-4139-9131-09441D69AFB1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformLauncher", "PlatformLauncher\PlatformLauncher.csproj", "{D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonitoringDemo", "MonitoringDemo\MonitoringDemo.csproj", "{55C64607-52E9-4E85-A547-50B191856A93}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D29C905-CE3A-4D93-8271-7BA09CEE1631}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D4DCF868-A625-4B0B-BB20-C150553A6548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4DCF868-A625-4B0B-BB20-C150553A6548}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4DCF868-A625-4B0B-BB20-C150553A6548}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4DCF868-A625-4B0B-BB20-C150553A6548}.Release|Any CPU.Build.0 = Release|Any CPU + {F6DE8266-1F56-4241-965C-2DDBB76CEC1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6DE8266-1F56-4241-965C-2DDBB76CEC1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6DE8266-1F56-4241-965C-2DDBB76CEC1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6DE8266-1F56-4241-965C-2DDBB76CEC1E}.Release|Any CPU.Build.0 = Release|Any CPU + {CD42E5DF-D4A7-4933-8017-1398B2E9560F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD42E5DF-D4A7-4933-8017-1398B2E9560F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD42E5DF-D4A7-4933-8017-1398B2E9560F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD42E5DF-D4A7-4933-8017-1398B2E9560F}.Release|Any CPU.Build.0 = Release|Any CPU + {9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BF02A43-6D9D-4B49-A06D-603A66C6BCB5}.Release|Any CPU.Build.0 = Release|Any CPU + {41F0D809-8FA4-4139-9131-09441D69AFB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41F0D809-8FA4-4139-9131-09441D69AFB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41F0D809-8FA4-4139-9131-09441D69AFB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41F0D809-8FA4-4139-9131-09441D69AFB1}.Release|Any CPU.Build.0 = Release|Any CPU + {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D98DF31F-6B4B-42E1-BEE5-8CAB949638C2}.Release|Any CPU.Build.0 = Release|Any CPU + {55C64607-52E9-4E85-A547-50B191856A93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55C64607-52E9-4E85-A547-50B191856A93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55C64607-52E9-4E85-A547-50B191856A93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55C64607-52E9-4E85-A547-50B191856A93}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {081FC59E-04F4-4FB2-88A6-64A7C18BAA10} + EndGlobalSection +EndGlobal diff --git a/src/MonitoringDemo/ColoredConsole.cs b/src/MonitoringDemo/ColoredConsole.cs index b85e9191..0db64820 100644 --- a/src/MonitoringDemo/ColoredConsole.cs +++ b/src/MonitoringDemo/ColoredConsole.cs @@ -1,20 +1,20 @@ -namespace MonitoringDemo; - -static class ColoredConsole -{ - public static IDisposable Use(ConsoleColor color) - { - var previousColor = Console.ForegroundColor; - Console.ForegroundColor = color; - - return new Restorer(previousColor); - } - - class Restorer(ConsoleColor previousColor) : IDisposable - { - public void Dispose() - { - Console.ForegroundColor = previousColor; - } - } +namespace MonitoringDemo; + +static class ColoredConsole +{ + public static IDisposable Use(ConsoleColor color) + { + var previousColor = Console.ForegroundColor; + Console.ForegroundColor = color; + + return new Restorer(previousColor); + } + + class Restorer(ConsoleColor previousColor) : IDisposable + { + public void Dispose() + { + Console.ForegroundColor = previousColor; + } + } } \ No newline at end of file diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index bbaf44fc..c2963d5d 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -1,93 +1,93 @@ -namespace MonitoringDemo; - -sealed class DemoLauncher : IDisposable -{ - public DemoLauncher() - { - demoProcessGroup = new ProcessGroup("Particular.MonitoringDemo"); - - File.WriteAllText(@".\Marker.sln", string.Empty); - } - - public void Dispose() - { - disposed = true; - - demoProcessGroup.Dispose(); - - File.Delete(@".\Marker.sln"); - - Console.WriteLine("Removing Transport Files"); - DirectoryEx.Delete(".learningtransport"); - - Console.WriteLine("Deleting log folders"); - DirectoryEx.Delete(".logs"); - - Console.WriteLine("Deleting db folders"); - DirectoryEx.ForceDeleteReadonly(".db"); - DirectoryEx.ForceDeleteReadonly(".audit-db"); - } - - public void Platform() - { - if (disposed) - { - return; - } - - demoProcessGroup.AddProcess(Path.Combine("PlatformLauncher", "PlatformLauncher.dll")); - } - - public void Billing() - { - if (disposed) - { - return; - } - - demoProcessGroup.AddProcess(Path.Combine("Billing", "Billing.dll")); - } - - public void Shipping() - { - if (disposed) - { - return; - } - - demoProcessGroup.AddProcess(Path.Combine("Shipping", "Shipping.dll")); - } - - public void ScaleOutSales() - { - if (disposed) - { - return; - } - - demoProcessGroup.AddProcess(Path.Combine("Sales", "Sales.dll")); - } - - public void ScaleInSales() - { - if (disposed) - { - return; - } - - demoProcessGroup.KillProcess(Path.Combine("Sales", "Sales.dll")); - } - - public void ClientUI() - { - if (disposed) - { - return; - } - - demoProcessGroup.AddProcess(Path.Combine("ClientUI", "ClientUI.dll")); - } - - readonly ProcessGroup demoProcessGroup; - private bool disposed; +namespace MonitoringDemo; + +sealed class DemoLauncher : IDisposable +{ + public DemoLauncher() + { + demoProcessGroup = new ProcessGroup("Particular.MonitoringDemo"); + + File.WriteAllText(@".\Marker.sln", string.Empty); + } + + public void Dispose() + { + disposed = true; + + demoProcessGroup.Dispose(); + + File.Delete(@".\Marker.sln"); + + Console.WriteLine("Removing Transport Files"); + DirectoryEx.Delete(".learningtransport"); + + Console.WriteLine("Deleting log folders"); + DirectoryEx.Delete(".logs"); + + Console.WriteLine("Deleting db folders"); + DirectoryEx.ForceDeleteReadonly(".db"); + DirectoryEx.ForceDeleteReadonly(".audit-db"); + } + + public void Platform() + { + if (disposed) + { + return; + } + + demoProcessGroup.AddProcess(Path.Combine("PlatformLauncher", "PlatformLauncher.dll")); + } + + public void Billing() + { + if (disposed) + { + return; + } + + demoProcessGroup.AddProcess(Path.Combine("Billing", "Billing.dll")); + } + + public void Shipping() + { + if (disposed) + { + return; + } + + demoProcessGroup.AddProcess(Path.Combine("Shipping", "Shipping.dll")); + } + + public void ScaleOutSales() + { + if (disposed) + { + return; + } + + demoProcessGroup.AddProcess(Path.Combine("Sales", "Sales.dll")); + } + + public void ScaleInSales() + { + if (disposed) + { + return; + } + + demoProcessGroup.KillProcess(Path.Combine("Sales", "Sales.dll")); + } + + public void ClientUI() + { + if (disposed) + { + return; + } + + demoProcessGroup.AddProcess(Path.Combine("ClientUI", "ClientUI.dll")); + } + + readonly ProcessGroup demoProcessGroup; + private bool disposed; } \ No newline at end of file diff --git a/src/MonitoringDemo/DirectoryEx.cs b/src/MonitoringDemo/DirectoryEx.cs index eeedacab..dff5d6a9 100644 --- a/src/MonitoringDemo/DirectoryEx.cs +++ b/src/MonitoringDemo/DirectoryEx.cs @@ -1,57 +1,57 @@ -namespace MonitoringDemo; - -static class DirectoryEx -{ - public static void Delete(string directoryPath) - { - for (var i = 0; i < 3; i++) - { - try - { - Directory.Delete(directoryPath, true); - return; - } - catch (DirectoryNotFoundException) - { - return; - } - catch (Exception) - { - // ignored - Thread.Sleep(2000); - } - } - } - - public static void ForceDeleteReadonly(string directoryPath) - { - for (var i = 0; i < 3; i++) - { - try - { - // necessary because ravendb creates some folders read-only - var directory = new DirectoryInfo(directoryPath) { Attributes = FileAttributes.Normal }; - - foreach (var info in directory.GetFileSystemInfos("*", SearchOption.AllDirectories)) - { - if (info.Attributes != FileAttributes.Normal) - { - info.Attributes = FileAttributes.Normal; - } - } - - directory.Delete(true); - return; - } - catch (DirectoryNotFoundException) - { - return; - } - catch (Exception) - { - // ignored - Thread.Sleep(2000); - } - } - } +namespace MonitoringDemo; + +static class DirectoryEx +{ + public static void Delete(string directoryPath) + { + for (var i = 0; i < 3; i++) + { + try + { + Directory.Delete(directoryPath, true); + return; + } + catch (DirectoryNotFoundException) + { + return; + } + catch (Exception) + { + // ignored + Thread.Sleep(2000); + } + } + } + + public static void ForceDeleteReadonly(string directoryPath) + { + for (var i = 0; i < 3; i++) + { + try + { + // necessary because ravendb creates some folders read-only + var directory = new DirectoryInfo(directoryPath) { Attributes = FileAttributes.Normal }; + + foreach (var info in directory.GetFileSystemInfos("*", SearchOption.AllDirectories)) + { + if (info.Attributes != FileAttributes.Normal) + { + info.Attributes = FileAttributes.Normal; + } + } + + directory.Delete(true); + return; + } + catch (DirectoryNotFoundException) + { + return; + } + catch (Exception) + { + // ignored + Thread.Sleep(2000); + } + } + } } \ No newline at end of file diff --git a/src/MonitoringDemo/MonitoringDemo.csproj b/src/MonitoringDemo/MonitoringDemo.csproj index 919f9108..c70c36f5 100644 --- a/src/MonitoringDemo/MonitoringDemo.csproj +++ b/src/MonitoringDemo/MonitoringDemo.csproj @@ -1,17 +1,17 @@ - - - - net8.0 - Exe - enable - enable - ..\..\binaries\ - true - - - - - - - + + + + net8.0 + Exe + enable + enable + ..\..\binaries\ + true + + + + + + + \ No newline at end of file diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index 937cb738..2e16b5e8 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -1,104 +1,104 @@ -using MonitoringDemo; - -CancellationTokenSource tokenSource = new(); -Console.Title = "MonitoringDemo"; -var syncEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - -Console.CancelKeyPress += (sender, eventArgs) => -{ - eventArgs.Cancel = true; - tokenSource.Cancel(); - syncEvent.TrySetResult(true); -}; - -try -{ - using var launcher = new DemoLauncher(); - Console.WriteLine("Starting the Particular Platform"); - - launcher.Platform(); - - using (ColoredConsole.Use(ConsoleColor.Yellow)) - { - Console.WriteLine( - "Once ServiceControl has finished starting a browser window will pop up showing the ServicePulse monitoring tab"); - } - - Console.WriteLine("Starting Demo Solution"); - - if (!tokenSource.IsCancellationRequested) - { - Console.WriteLine("Starting Billing endpoint."); - launcher.Billing(); - - Console.WriteLine("Starting Sales endpoint."); - launcher.ScaleOutSales(); - - Console.WriteLine("Starting Shipping endpoint."); - launcher.Shipping(); - - Console.WriteLine("Starting ClientUI endpoint."); - launcher.ClientUI(); - - using (ColoredConsole.Use(ConsoleColor.Yellow)) - { - ScaleSalesEndpointIfRequired(launcher, syncEvent); - - await syncEvent.Task; - - Console.WriteLine("Shutting down"); - } - } -} -catch (Exception e) -{ - using (ColoredConsole.Use(ConsoleColor.Red)) - { - Console.WriteLine("Error starting setting up demo."); - Console.WriteLine($"{e.Message}{Environment.NewLine}{e.StackTrace}"); - } -} - -using (ColoredConsole.Use(ConsoleColor.Yellow)) -{ - Console.WriteLine("Done, press ENTER."); - Console.ReadLine(); -} - -void ScaleSalesEndpointIfRequired(DemoLauncher launcher, TaskCompletionSource syncEvent) -{ - _ = Task.Run(() => - { - try - { - Console.WriteLine(); - Console.WriteLine("Press [up arrow] to scale out the Sales service or [down arrow] to scale in"); - Console.WriteLine("Press Ctrl+C stop Particular Monitoring Demo."); - Console.WriteLine(); - - while (!tokenSource.IsCancellationRequested) - { - var input = Console.ReadKey(true); - - switch (input.Key) - { - case ConsoleKey.DownArrow: - launcher.ScaleInSales(); - break; - case ConsoleKey.UpArrow: - launcher.ScaleOutSales(); - break; - } - } - } - catch (OperationCanceledException) - { - // ignore - } - catch (Exception e) - { - // surface any other exception - syncEvent.TrySetException(e); - } - }); -} +using MonitoringDemo; + +CancellationTokenSource tokenSource = new(); +Console.Title = "MonitoringDemo"; +var syncEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + +Console.CancelKeyPress += (sender, eventArgs) => +{ + eventArgs.Cancel = true; + tokenSource.Cancel(); + syncEvent.TrySetResult(true); +}; + +try +{ + using var launcher = new DemoLauncher(); + Console.WriteLine("Starting the Particular Platform"); + + launcher.Platform(); + + using (ColoredConsole.Use(ConsoleColor.Yellow)) + { + Console.WriteLine( + "Once ServiceControl has finished starting a browser window will pop up showing the ServicePulse monitoring tab"); + } + + Console.WriteLine("Starting Demo Solution"); + + if (!tokenSource.IsCancellationRequested) + { + Console.WriteLine("Starting Billing endpoint."); + launcher.Billing(); + + Console.WriteLine("Starting Sales endpoint."); + launcher.ScaleOutSales(); + + Console.WriteLine("Starting Shipping endpoint."); + launcher.Shipping(); + + Console.WriteLine("Starting ClientUI endpoint."); + launcher.ClientUI(); + + using (ColoredConsole.Use(ConsoleColor.Yellow)) + { + ScaleSalesEndpointIfRequired(launcher, syncEvent); + + await syncEvent.Task; + + Console.WriteLine("Shutting down"); + } + } +} +catch (Exception e) +{ + using (ColoredConsole.Use(ConsoleColor.Red)) + { + Console.WriteLine("Error starting setting up demo."); + Console.WriteLine($"{e.Message}{Environment.NewLine}{e.StackTrace}"); + } +} + +using (ColoredConsole.Use(ConsoleColor.Yellow)) +{ + Console.WriteLine("Done, press ENTER."); + Console.ReadLine(); +} + +void ScaleSalesEndpointIfRequired(DemoLauncher launcher, TaskCompletionSource syncEvent) +{ + _ = Task.Run(() => + { + try + { + Console.WriteLine(); + Console.WriteLine("Press [up arrow] to scale out the Sales service or [down arrow] to scale in"); + Console.WriteLine("Press Ctrl+C stop Particular Monitoring Demo."); + Console.WriteLine(); + + while (!tokenSource.IsCancellationRequested) + { + var input = Console.ReadKey(true); + + switch (input.Key) + { + case ConsoleKey.DownArrow: + launcher.ScaleInSales(); + break; + case ConsoleKey.UpArrow: + launcher.ScaleOutSales(); + break; + } + } + } + catch (OperationCanceledException) + { + // ignore + } + catch (Exception e) + { + // surface any other exception + syncEvent.TrySetException(e); + } + }); +} diff --git a/src/MonitoringDemo/launch.sh b/src/MonitoringDemo/launch.sh index 8f621f2e..5dcca52c 100755 --- a/src/MonitoringDemo/launch.sh +++ b/src/MonitoringDemo/launch.sh @@ -1 +1,3 @@ +#!/bin/bash + dotnet MonitoringDemo.dll \ No newline at end of file From 3339a6be0698df5bfdcec87197740eb47ec92572 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 21 Mar 2025 14:18:32 -0400 Subject: [PATCH 18/37] Restore exe entry point for Windows --- .github/workflows/ci.yml | 1 - .github/workflows/release.yml | 1 - src/MonitoringDemo/MonitoringDemo.csproj | 5 +++-- src/MonitoringDemo/{launch.sh => MonitoringDemo.sh} | 0 src/MonitoringDemo/launch.ps1 | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) rename src/MonitoringDemo/{launch.sh => MonitoringDemo.sh} (100%) delete mode 100644 src/MonitoringDemo/launch.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5369dd63..889f1ee0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,6 @@ jobs: run: dotnet build src --configuration Release - name: Remove executables run: | - Remove-Item binaries/MonitoringDemo Remove-Item binaries/Billing/Billing Remove-Item binaries/ClientUI/ClientUI Remove-Item binaries/PlatformLauncher/PlatformLauncher diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16782c6b..99546408 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,6 @@ jobs: run: dotnet build src --configuration Release - name: Remove executables run: | - Remove-Item binaries/MonitoringDemo Remove-Item binaries/Billing/Billing Remove-Item binaries/ClientUI/ClientUI Remove-Item binaries/PlatformLauncher/PlatformLauncher diff --git a/src/MonitoringDemo/MonitoringDemo.csproj b/src/MonitoringDemo/MonitoringDemo.csproj index c70c36f5..4eb148fc 100644 --- a/src/MonitoringDemo/MonitoringDemo.csproj +++ b/src/MonitoringDemo/MonitoringDemo.csproj @@ -7,11 +7,12 @@ enable ..\..\binaries\ true + win-x64 + false - - + \ No newline at end of file diff --git a/src/MonitoringDemo/launch.sh b/src/MonitoringDemo/MonitoringDemo.sh similarity index 100% rename from src/MonitoringDemo/launch.sh rename to src/MonitoringDemo/MonitoringDemo.sh diff --git a/src/MonitoringDemo/launch.ps1 b/src/MonitoringDemo/launch.ps1 deleted file mode 100644 index 8f621f2e..00000000 --- a/src/MonitoringDemo/launch.ps1 +++ /dev/null @@ -1 +0,0 @@ -dotnet MonitoringDemo.dll \ No newline at end of file From a9eb9ca24d306126830db9bb398ed4089ca841c2 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 28 Mar 2025 16:58:14 +0700 Subject: [PATCH 19/37] Redirect mode Co-authored-by: SzymonPobiega --- src/Billing/Billing.csproj | 6 +++ src/Billing/Program.cs | 40 ++++------------- src/ClientUI/ClientUI.csproj | 6 +++ src/ClientUI/Program.cs | 39 ++++------------ src/MonitoringDemo/DemoLauncher.cs | 27 ++++++++--- src/MonitoringDemo/ProcessGroup.cs | 35 ++++++++++++--- src/MonitoringDemo/Program.cs | 12 +++-- src/MonitoringDemo/UserInterface.cs | 70 +++++++++++++++++++++++++++++ src/Sales/Program.cs | 53 +++++++--------------- src/Sales/Sales.csproj | 6 +++ src/Shipping/Program.cs | 45 ++++--------------- src/Shipping/Shipping.csproj | 6 +++ 12 files changed, 194 insertions(+), 151 deletions(-) create mode 100644 src/MonitoringDemo/UserInterface.cs diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 166e7075..fd61d422 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -18,4 +18,10 @@ + + + UserInterface.cs + + + \ No newline at end of file diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index 363b3778..2c6b97bd 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -2,9 +2,7 @@ using Billing; using Messages; using Microsoft.Extensions.DependencyInjection; - -Console.Title = "Failure rate (Billing)"; -Console.SetWindowSize(65, 15); +using Shared; var endpointConfiguration = new EndpointConfiguration("Billing"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); @@ -41,35 +39,13 @@ var endpointInstance = await Endpoint.Start(endpointConfiguration); -RunUserInterfaceLoop(simulationEffects); - -await endpointInstance.Stop(); +var nonInteractive = args.Length > 1 && bool.TryParse(args[1], out var isInteractive) && !isInteractive; +var interactive = !nonInteractive; -void RunUserInterfaceLoop(SimulationEffects state) +UserInterface.RunLoop("Failure rate (Billing)", new Dictionary { - while (true) - { - Console.Clear(); - Console.WriteLine("Billing Endpoint"); - Console.WriteLine("Press F to increase the simulated failure rate"); - Console.WriteLine("Press S to decrease the simulated failure rate"); - Console.WriteLine("Press ESC to quit"); - Console.WriteLine(); - - state.WriteState(Console.Out); + ['w'] = ("increase the simulated failure rate", () => simulationEffects.IncreaseFailureRate()), + ['s'] = ("decrease the simulated failure rate", () => simulationEffects.DecreaseFailureRate()) +}, writer => simulationEffects.WriteState(writer), interactive); - var input = Console.ReadKey(true); - - switch (input.Key) - { - case ConsoleKey.F: - state.IncreaseFailureRate(); - break; - case ConsoleKey.S: - state.DecreaseFailureRate(); - break; - case ConsoleKey.Escape: - return; - } - } -} +await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj index fc7479b4..375782e3 100644 --- a/src/ClientUI/ClientUI.csproj +++ b/src/ClientUI/ClientUI.csproj @@ -18,4 +18,10 @@ + + + UserInterface.cs + + + \ No newline at end of file diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs index 16e5f866..31fe9ba2 100644 --- a/src/ClientUI/Program.cs +++ b/src/ClientUI/Program.cs @@ -1,9 +1,7 @@ using System.Text.Json; using ClientUI; using Messages; - -Console.Title = "Load (ClientUI)"; -Console.SetWindowSize(65, 15); +using Shared; var endpointConfiguration = new EndpointConfiguration("ClientUI"); @@ -40,35 +38,16 @@ var cancellation = new CancellationTokenSource(); var simulatedWork = simulatedCustomers.Run(cancellation.Token); -RunUserInterfaceLoop(simulatedCustomers); - -cancellation.Cancel(); - -await simulatedWork; - -await endpointInstance.Stop(); +var nonInteractive = args.Length > 1 && bool.TryParse(args[1], out var isInteractive) && !isInteractive; +var interactive = !nonInteractive; -void RunUserInterfaceLoop(SimulatedCustomers simulatedCustomers) +UserInterface.RunLoop("Load (ClientUI)", new Dictionary { - while (true) - { - Console.Clear(); - Console.WriteLine("Simulating customers placing orders on a website"); - Console.WriteLine("Press T to toggle High/Low traffic mode"); - Console.WriteLine("Press ESC to quit"); - Console.WriteLine(); + ['c'] = ("toggle High/Low traffic mode", () => simulatedCustomers.ToggleTrafficMode()), +}, writer => simulatedCustomers.WriteState(writer), interactive); - simulatedCustomers.WriteState(Console.Out); +cancellation.Cancel(); - var input = Console.ReadKey(true); +await simulatedWork; - switch (input.Key) - { - case ConsoleKey.T: - simulatedCustomers.ToggleTrafficMode(); - break; - case ConsoleKey.Escape: - return; - } - } -} +await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index c2963d5d..5d4b394c 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -2,13 +2,21 @@ sealed class DemoLauncher : IDisposable { - public DemoLauncher() + public DemoLauncher(bool remoteControlMode) { - demoProcessGroup = new ProcessGroup("Particular.MonitoringDemo"); + demoProcessGroup = new ProcessGroup("Particular.MonitoringDemo", remoteControlMode); File.WriteAllText(@".\Marker.sln", string.Empty); } + public void Send(string value) + { + demoProcessGroup.Send(BillingPath, 0, value); + demoProcessGroup.Send(ShippingPath, 0, value); + demoProcessGroup.Send(ClientPath, 0, value); + demoProcessGroup.Send(SalesPath, 0, value); + } + public void Dispose() { disposed = true; @@ -45,7 +53,7 @@ public void Billing() return; } - demoProcessGroup.AddProcess(Path.Combine("Billing", "Billing.dll")); + demoProcessGroup.AddProcess(BillingPath); } public void Shipping() @@ -55,7 +63,7 @@ public void Shipping() return; } - demoProcessGroup.AddProcess(Path.Combine("Shipping", "Shipping.dll")); + demoProcessGroup.AddProcess(ShippingPath); } public void ScaleOutSales() @@ -65,7 +73,7 @@ public void ScaleOutSales() return; } - demoProcessGroup.AddProcess(Path.Combine("Sales", "Sales.dll")); + demoProcessGroup.AddProcess(SalesPath); } public void ScaleInSales() @@ -75,7 +83,7 @@ public void ScaleInSales() return; } - demoProcessGroup.KillProcess(Path.Combine("Sales", "Sales.dll")); + demoProcessGroup.KillProcess(SalesPath); } public void ClientUI() @@ -85,9 +93,14 @@ public void ClientUI() return; } - demoProcessGroup.AddProcess(Path.Combine("ClientUI", "ClientUI.dll")); + demoProcessGroup.AddProcess(ClientPath); } readonly ProcessGroup demoProcessGroup; private bool disposed; + + private static readonly string BillingPath = Path.Combine("Billing", "Billing.dll"); + private static readonly string ShippingPath = Path.Combine("Shipping", "Shipping.dll"); + private static readonly string SalesPath = Path.Combine("Sales", "Sales.dll"); + private static readonly string ClientPath = Path.Combine("ClientUI", "ClientUI.dll"); } \ No newline at end of file diff --git a/src/MonitoringDemo/ProcessGroup.cs b/src/MonitoringDemo/ProcessGroup.cs index 2dcb5601..01f647f1 100644 --- a/src/MonitoringDemo/ProcessGroup.cs +++ b/src/MonitoringDemo/ProcessGroup.cs @@ -10,6 +10,7 @@ namespace MonitoringDemo; /// partial class ProcessGroup : IDisposable { + readonly bool redirectInputAndOutput; readonly Dictionary> processesByAssemblyPath = []; readonly List managedProcessIds = []; bool disposed; @@ -17,14 +18,31 @@ partial class ProcessGroup : IDisposable // Windows-specific fields nint jobHandle; - public ProcessGroup(string groupName) + public ProcessGroup(string groupName,bool redirectInputAndOutput) { + this.redirectInputAndOutput = redirectInputAndOutput; if (OperatingSystem.IsWindows()) { InitializeWindowsJob(groupName); } } + public void Send(string relativeAssemblyPath, int index, string value) + { + if (!redirectInputAndOutput) + { + return; + } + + if (processesByAssemblyPath.TryGetValue(relativeAssemblyPath, out var processes)) + { + if (processes.Count > index) + { + processes.ElementAt(index).StandardInput.WriteLine(value); + } + } + } + public bool AddProcess(string relativeAssemblyPath) { if (!processesByAssemblyPath.TryGetValue(relativeAssemblyPath, out var processes)) @@ -43,6 +61,12 @@ public bool AddProcess(string relativeAssemblyPath) return false; } + if (redirectInputAndOutput) + { + process.OutputDataReceived += (sender, args) => Console.WriteLine(args.Data); + process.BeginOutputReadLine(); + } + processes.Push(process); managedProcessIds.Add(process.Id); @@ -190,7 +214,7 @@ private void DisposeWindowsJob() #endregion - private static Process? StartProcess(string relativeAssemblyPath, string? arguments = null) + private Process? StartProcess(string relativeAssemblyPath, string? arguments = null) { var fullAssemblyPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, relativeAssemblyPath)); var workingDirectory = Path.GetDirectoryName(fullAssemblyPath); @@ -198,13 +222,14 @@ private void DisposeWindowsJob() var startInfo = new ProcessStartInfo("dotnet", fullAssemblyPath) { WorkingDirectory = workingDirectory, - UseShellExecute = OperatingSystem.IsWindows(), - CreateNoWindow = !OperatingSystem.IsWindows(), + UseShellExecute = !redirectInputAndOutput, + RedirectStandardInput = redirectInputAndOutput, + RedirectStandardOutput = redirectInputAndOutput }; if (arguments is not null) { - startInfo.Arguments += $" {arguments}"; + startInfo.Arguments += $" {arguments} {!redirectInputAndOutput}"; } return Process.Start(startInfo); diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index 2e16b5e8..b790b4af 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -2,6 +2,8 @@ CancellationTokenSource tokenSource = new(); Console.Title = "MonitoringDemo"; + +var remoteControlMode = args.Length > 0 && string.Equals(args[0], bool.TrueString, StringComparison.InvariantCultureIgnoreCase); var syncEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); Console.CancelKeyPress += (sender, eventArgs) => @@ -13,7 +15,7 @@ try { - using var launcher = new DemoLauncher(); + using var launcher = new DemoLauncher(remoteControlMode); Console.WriteLine("Starting the Particular Platform"); launcher.Platform(); @@ -79,15 +81,17 @@ void ScaleSalesEndpointIfRequired(DemoLauncher launcher, TaskCompletionSource controls, Action reportState, bool interactive) + { + if (interactive) + { + RunInteractiveLoop(title, controls, reportState); + } + else + { + RunNonInteractiveLoop(title, controls, reportState); + } + } + + static void RunInteractiveLoop(string title, Dictionary controls, Action reportState) + { + Console.Title = title; + Console.SetWindowSize(65, 15); + + while (true) + { + Console.Clear(); + foreach (var kvp in controls) + { + Console.WriteLine($"Press {char.ToUpperInvariant(kvp.Key)} to {kvp.Value.Message}"); + } + Console.WriteLine("Press ESC to quit"); + Console.WriteLine(); + + reportState(Console.Out); + + var input = Console.ReadKey(true); + + if (controls.TryGetValue(char.ToLowerInvariant(input.KeyChar), out var control)) + { + control.Action(); + } + else if (input.Key == ConsoleKey.Escape) + { + return; + } + } + } + + static void RunNonInteractiveLoop(string title, Dictionary controls, Action reportState) + { + Console.Title = title; + + reportState(Console.Out); + + while (true) + { + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + { + return; + } + + var key = input[0]; + + if (controls.TryGetValue(char.ToLowerInvariant(key), out var control)) + { + control.Action(); + reportState(Console.Out); + } + } + } +} \ No newline at end of file diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs index 63cf7076..79660680 100644 --- a/src/Sales/Program.cs +++ b/src/Sales/Program.cs @@ -4,23 +4,25 @@ using Messages; using Microsoft.Extensions.DependencyInjection; using Sales; - -Console.SetWindowSize(65, 15); +using Shared; var instanceName = args.FirstOrDefault(); -if (string.IsNullOrEmpty(instanceName)) +var instanceNumber = args.FirstOrDefault(); +string title; + +if (string.IsNullOrEmpty(instanceNumber)) { - Console.Title = "Processing (Sales)"; + title = "Processing (Sales)"; - instanceName = "original-instance"; + instanceNumber = "original-instance"; } else { - Console.Title = $"Sales - {instanceName}"; + title = $"Sales - {instanceNumber}"; } -var instanceId = DeterministicGuid.Create("Sales", instanceName); +var instanceId = DeterministicGuid.Create("Sales", instanceNumber); var endpointConfiguration = new EndpointConfiguration("Sales"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); @@ -54,39 +56,16 @@ var endpointInstance = await Endpoint.Start(endpointConfiguration); -RunUserInterfaceLoop(simulationEffects, instanceName); - -await endpointInstance.Stop(); +var nonInteractive = args.Length > 1 && bool.TryParse(args[1], out var isInteractive) && !isInteractive; +var interactive = !nonInteractive; -void RunUserInterfaceLoop(SimulationEffects state, string instanceName) +UserInterface.RunLoop(title, new Dictionary { - while (true) - { - Console.Clear(); - Console.WriteLine($"Sales Endpoint - {instanceName}"); - Console.WriteLine("Press F to process messages faster"); - Console.WriteLine("Press S to process messages slower"); - - Console.WriteLine("Press ESC to quit"); - Console.WriteLine(); - - state.WriteState(Console.Out); + ['r'] = ("process messages faster", () => simulationEffects.ProcessMessagesFaster()), + ['f'] = ("process messages slower", () => simulationEffects.ProcessMessagesSlower()) +}, writer => simulationEffects.WriteState(writer), interactive); - var input = Console.ReadKey(true); - - switch (input.Key) - { - case ConsoleKey.F: - state.ProcessMessagesFaster(); - break; - case ConsoleKey.S: - state.ProcessMessagesSlower(); - break; - case ConsoleKey.Escape: - return; - } - } -} +await endpointInstance.Stop(); static class DeterministicGuid { diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj index 21c60d8b..67bc7d3a 100644 --- a/src/Sales/Sales.csproj +++ b/src/Sales/Sales.csproj @@ -18,4 +18,10 @@ + + + UserInterface.cs + + + \ No newline at end of file diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs index d90e79d1..02cb5e38 100644 --- a/src/Shipping/Program.cs +++ b/src/Shipping/Program.cs @@ -1,11 +1,9 @@ using System.Text.Json; using Messages; using Microsoft.Extensions.DependencyInjection; +using Shared; using Shipping; -Console.Title = "Processing (Shipping)"; -Console.SetWindowSize(65, 15); - var endpointConfiguration = new EndpointConfiguration("Shipping"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); @@ -38,39 +36,14 @@ var endpointInstance = await Endpoint.Start(endpointConfiguration); -RunUserInterfaceLoop(simulationEffects); - -await endpointInstance.Stop(); +var nonInteractive = args.Length > 1 && args[1] == bool.FalseString; +var interactive = !nonInteractive; -void RunUserInterfaceLoop(SimulationEffects state) +UserInterface.RunLoop("Processing (Shipping)", new Dictionary { - while (true) - { - Console.Clear(); - Console.WriteLine("Shipping Endpoint"); - Console.WriteLine("Press D to toggle resource degradation simulation"); - Console.WriteLine("Press F to process OrderBilled events faster"); - Console.WriteLine("Press S to process OrderBilled events slower"); - Console.WriteLine("Press ESC to quit"); - Console.WriteLine(); - - state.WriteState(Console.Out); + ['z'] = ("toggle resource degradation simulation", () => simulationEffects.ToggleDegradationSimulation()), + ['q'] = ("process OrderBilled events faster", () => simulationEffects.ProcessMessagesFaster()), + ['a'] = ("process OrderBilled events slower", () => simulationEffects.ProcessMessagesSlower()) +}, writer => simulationEffects.WriteState(writer), interactive); - var input = Console.ReadKey(true); - - switch (input.Key) - { - case ConsoleKey.D: - state.ToggleDegradationSimulation(); - break; - case ConsoleKey.F: - state.ProcessMessagesFaster(); - break; - case ConsoleKey.S: - state.ProcessMessagesSlower(); - break; - case ConsoleKey.Escape: - return; - } - } -} +await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj index 7fe96fbb..7bb828dd 100644 --- a/src/Shipping/Shipping.csproj +++ b/src/Shipping/Shipping.csproj @@ -18,4 +18,10 @@ + + + UserInterface.cs + + + \ No newline at end of file From 3acc812604049f03ca69d9b1a8b0481996e9995b Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Tue, 1 Apr 2025 12:30:10 -0400 Subject: [PATCH 20/37] Revert "Restore exe entry point for Windows" This reverts commit c6a4e3b3379e39a0320bce51f6b40b3deda13dc7. --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 1 + src/MonitoringDemo/MonitoringDemo.csproj | 5 ++--- src/MonitoringDemo/launch.ps1 | 1 + src/MonitoringDemo/{MonitoringDemo.sh => launch.sh} | 0 5 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 src/MonitoringDemo/launch.ps1 rename src/MonitoringDemo/{MonitoringDemo.sh => launch.sh} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 889f1ee0..5369dd63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: run: dotnet build src --configuration Release - name: Remove executables run: | + Remove-Item binaries/MonitoringDemo Remove-Item binaries/Billing/Billing Remove-Item binaries/ClientUI/ClientUI Remove-Item binaries/PlatformLauncher/PlatformLauncher diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99546408..16782c6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,6 +20,7 @@ jobs: run: dotnet build src --configuration Release - name: Remove executables run: | + Remove-Item binaries/MonitoringDemo Remove-Item binaries/Billing/Billing Remove-Item binaries/ClientUI/ClientUI Remove-Item binaries/PlatformLauncher/PlatformLauncher diff --git a/src/MonitoringDemo/MonitoringDemo.csproj b/src/MonitoringDemo/MonitoringDemo.csproj index 4eb148fc..c70c36f5 100644 --- a/src/MonitoringDemo/MonitoringDemo.csproj +++ b/src/MonitoringDemo/MonitoringDemo.csproj @@ -7,12 +7,11 @@ enable ..\..\binaries\ true - win-x64 - false - + + \ No newline at end of file diff --git a/src/MonitoringDemo/launch.ps1 b/src/MonitoringDemo/launch.ps1 new file mode 100644 index 00000000..8f621f2e --- /dev/null +++ b/src/MonitoringDemo/launch.ps1 @@ -0,0 +1 @@ +dotnet MonitoringDemo.dll \ No newline at end of file diff --git a/src/MonitoringDemo/MonitoringDemo.sh b/src/MonitoringDemo/launch.sh similarity index 100% rename from src/MonitoringDemo/MonitoringDemo.sh rename to src/MonitoringDemo/launch.sh From ba61f24d7c3ef017189e81c948792f91e6e90cef Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 15 May 2025 15:32:35 +0200 Subject: [PATCH 21/37] UI (#206) * Spike UI * Fix rendering * Delete src/MonitoringDemo/.\Marker.sln * Use storage directory * Multi-instance windows * Small tweaks while looking at the code * ProcessHandle * Recognized commands based on help messages * Minor todo * Cosmetics * Fix recognition of commands * Regex * Forward to all instance when uppercase * Remove obsolete comment * Widgets? Gadgets? * Bring in more chaos! * Outbox! * Remove unused line * Hide instances in the platform window * Use explicit root folder for the transport * Indent properly * Upgrade to alpha because v1 should no longer be used (there might still be rough edges) * Fix keys, I hope * Fix lowercase vs uppercase keys * Simplify process view slightly * Command binding * Hopefully slightly better approach for visibility handling * Simplify process window state * Shitty scale out and in * More robust process handling * Dynamic scale out of all instances except clientui --------- Co-authored-by: Daniel Marbach Co-authored-by: Szymon Pobiega --- src/.editorconfig | 4 + src/Billing/Billing.csproj | 4 + src/Billing/DispatchingProgressBehavior.cs | 26 ++ src/Billing/FailureSimulation.cs | 32 ++ src/Billing/FailureSimulator.cs | 30 ++ .../ProcessingMessageProgressBehavior.cs | 24 ++ src/Billing/Program.cs | 34 +- src/Billing/ProgressBar.cs | 21 ++ .../RetrievingMessageProgressBehavior.cs | 25 ++ src/ClientUI/Program.cs | 16 +- src/ClientUI/SimulatedCustomers.cs | 37 ++- src/MonitoringDemo/ColoredConsole.cs | 20 -- src/MonitoringDemo/DemoLauncher.cs | 82 +---- src/MonitoringDemo/DeterministicGuid.cs | 15 + src/MonitoringDemo/IWidget.cs | 6 + src/MonitoringDemo/MonitoringDemo.csproj | 4 + src/MonitoringDemo/ProcessGroup.cs | 112 +++---- src/MonitoringDemo/ProcessHandle.cs | 26 ++ src/MonitoringDemo/ProcessWindow.cs | 298 ++++++++++++++++++ src/MonitoringDemo/Program.cs | 197 +++++++----- src/MonitoringDemo/ProgressBarWidget.cs | 13 + src/MonitoringDemo/UserInterface.cs | 61 +--- src/PlatformLauncher/Program.cs | 6 +- src/Sales/DispatchingProgressBehavior.cs | 26 ++ src/Sales/FailureSimulation.cs | 32 ++ src/Sales/FailureSimulator.cs | 30 ++ src/Sales/PlaceOrderHandler.cs | 9 +- .../ProcessingMessageProgressBehavior.cs | 24 ++ src/Sales/Program.cs | 60 ++-- src/Sales/ProgressBar.cs | 21 ++ .../RetrievingMessageProgressBehavior.cs | 25 ++ src/Sales/Sales.csproj | 4 + src/Shipping/Program.cs | 27 +- src/Shipping/Shipping.csproj | 3 + 34 files changed, 1010 insertions(+), 344 deletions(-) create mode 100644 src/.editorconfig create mode 100644 src/Billing/DispatchingProgressBehavior.cs create mode 100644 src/Billing/FailureSimulation.cs create mode 100644 src/Billing/FailureSimulator.cs create mode 100644 src/Billing/ProcessingMessageProgressBehavior.cs create mode 100644 src/Billing/ProgressBar.cs create mode 100644 src/Billing/RetrievingMessageProgressBehavior.cs delete mode 100644 src/MonitoringDemo/ColoredConsole.cs create mode 100644 src/MonitoringDemo/DeterministicGuid.cs create mode 100644 src/MonitoringDemo/IWidget.cs create mode 100644 src/MonitoringDemo/ProcessHandle.cs create mode 100644 src/MonitoringDemo/ProcessWindow.cs create mode 100644 src/MonitoringDemo/ProgressBarWidget.cs create mode 100644 src/Sales/DispatchingProgressBehavior.cs create mode 100644 src/Sales/FailureSimulation.cs create mode 100644 src/Sales/FailureSimulator.cs create mode 100644 src/Sales/ProcessingMessageProgressBehavior.cs create mode 100644 src/Sales/ProgressBar.cs create mode 100644 src/Sales/RetrievingMessageProgressBehavior.cs diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 00000000..84a8142f --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,4 @@ +[*.{csproj,props,targets,xml}] +indent_style = space +indent_size = 2 +xml_space_inside_empty_tag = true \ No newline at end of file diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index fd61d422..28b39554 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -14,6 +14,7 @@ + @@ -22,6 +23,9 @@ UserInterface.cs + + DeterministicGuid.cs + \ No newline at end of file diff --git a/src/Billing/DispatchingProgressBehavior.cs b/src/Billing/DispatchingProgressBehavior.cs new file mode 100644 index 00000000..63a72d5e --- /dev/null +++ b/src/Billing/DispatchingProgressBehavior.cs @@ -0,0 +1,26 @@ +using NServiceBus.Pipeline; +using NServiceBus.Transport; + +namespace Billing; + +public class DispatchingProgressBehavior : Behavior +{ + private FailureSimulator failureSimulator = new(); + + public override async Task Invoke(IBatchDispatchContext context, Func next) + { + var incomingMessage = context.Extensions.Get(); + if (incomingMessage.Headers.ContainsKey("MonitoringDemo.SlowMotion")) + { + Console.WriteLine($"Dispatching outgoing messages {incomingMessage.MessageId}..."); + await failureSimulator.RunInteractive(context.CancellationToken); + } + + await next().ConfigureAwait(false); + } + + public void Failure() + { + failureSimulator.Trigger(); + } +} \ No newline at end of file diff --git a/src/Billing/FailureSimulation.cs b/src/Billing/FailureSimulation.cs new file mode 100644 index 00000000..8e2d3952 --- /dev/null +++ b/src/Billing/FailureSimulation.cs @@ -0,0 +1,32 @@ +namespace Billing; + +public class FailureSimulation +{ + private RetrievingMessageProgressBehavior retrievingMessageProgressBehavior = new RetrievingMessageProgressBehavior(); + private ProcessingMessageProgressBehavior processingMessageProgressBehavior = new ProcessingMessageProgressBehavior(); + private DispatchingProgressBehavior dispatchingMessageProgressBehavior = new DispatchingProgressBehavior(); + + public void Register(EndpointConfiguration endpointConfiguration) + { + endpointConfiguration.Pipeline.Register(retrievingMessageProgressBehavior, "Shows progress of retrieving messages"); + + endpointConfiguration.Pipeline.Register(processingMessageProgressBehavior, "Shows progress of processing messages"); + + endpointConfiguration.Pipeline.Register(dispatchingMessageProgressBehavior, "Shows progress of dispatching messages"); + } + + public void TriggerFailureReceiving() + { + retrievingMessageProgressBehavior.Failure(); + } + + public void TriggerFailureProcessing() + { + processingMessageProgressBehavior.Failure(); + } + + public void TriggerFailureDispatching() + { + dispatchingMessageProgressBehavior.Failure(); + } +} \ No newline at end of file diff --git a/src/Billing/FailureSimulator.cs b/src/Billing/FailureSimulator.cs new file mode 100644 index 00000000..8643dd06 --- /dev/null +++ b/src/Billing/FailureSimulator.cs @@ -0,0 +1,30 @@ +namespace Billing; + +public class FailureSimulator +{ + private bool failureTriggered = false; + +#pragma warning disable PS0003 + public async Task RunInteractive(CancellationToken cancellationToken) +#pragma warning restore PS0003 + { + using var progressBar = new ProgressBar(); + + for (var i = 0; i <= 100; i++) + { + if (failureTriggered) + { + failureTriggered = false; + throw new Exception("Simulated failure"); + } + progressBar.Update(i); + await Task.Delay(25, cancellationToken).ConfigureAwait(false); + } + } + + public void Trigger() + { + //TODO: Use Interlocked + failureTriggered = true; + } +} \ No newline at end of file diff --git a/src/Billing/ProcessingMessageProgressBehavior.cs b/src/Billing/ProcessingMessageProgressBehavior.cs new file mode 100644 index 00000000..756f5362 --- /dev/null +++ b/src/Billing/ProcessingMessageProgressBehavior.cs @@ -0,0 +1,24 @@ +using NServiceBus.Pipeline; + +namespace Billing; + +public class ProcessingMessageProgressBehavior : Behavior +{ + private FailureSimulator failureSimulator = new(); + + public override async Task Invoke(IIncomingLogicalMessageContext context, Func next) + { + if (context.Headers.ContainsKey("MonitoringDemo.SlowMotion")) + { + Console.WriteLine($"Processing message {context.MessageId}..."); + await failureSimulator.RunInteractive(context.CancellationToken); + } + + await next().ConfigureAwait(false); + } + + public void Failure() + { + failureSimulator.Trigger(); + } +} \ No newline at end of file diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index 2c6b97bd..d24de2b7 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -1,9 +1,17 @@ -using System.Text.Json; +using System.Reflection; +using System.Text.Json; using Billing; using Messages; using Microsoft.Extensions.DependencyInjection; using Shared; +var instancePostfix = args.FirstOrDefault(); + +var title = string.IsNullOrEmpty(instancePostfix) ? "Failure rate (Billing)" : $"Billing - {instancePostfix}"; +var instanceName = string.IsNullOrEmpty(instancePostfix) ? "billing" : $"billing-{instancePostfix}"; + +var instanceId = DeterministicGuid.Create("Billing", instanceName); + var endpointConfiguration = new EndpointConfiguration("Billing"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); @@ -16,7 +24,12 @@ } }); -endpointConfiguration.UseTransport(); +var transport = new LearningTransport +{ + StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport"), + TransportTransactionMode = TransportTransactionMode.ReceiveOnly +}; +endpointConfiguration.UseTransport(transport); endpointConfiguration.Recoverability() .Delayed(delayed => delayed.NumberOfRetries(0)); @@ -25,8 +38,8 @@ endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); endpointConfiguration.UniquelyIdentifyRunningInstance() - .UsingCustomIdentifier(new Guid("1C62248E-2681-45A4-B44D-5CF93584BAD6")) - .UsingCustomDisplayName("original-instance"); + .UsingCustomIdentifier(instanceId) + .UsingCustomDisplayName(instanceName); var metrics = endpointConfiguration.EnableMetrics(); metrics.SendMetricDataToServiceControl( @@ -34,18 +47,21 @@ TimeSpan.FromMilliseconds(500) ); +endpointConfiguration.UsePersistence(); +endpointConfiguration.EnableOutbox(); + +var failureSimulation = new FailureSimulation(); +failureSimulation.Register(endpointConfiguration); + var simulationEffects = new SimulationEffects(); endpointConfiguration.RegisterComponents(cc => cc.AddSingleton(simulationEffects)); var endpointInstance = await Endpoint.Start(endpointConfiguration); -var nonInteractive = args.Length > 1 && bool.TryParse(args[1], out var isInteractive) && !isInteractive; -var interactive = !nonInteractive; - -UserInterface.RunLoop("Failure rate (Billing)", new Dictionary +UserInterface.RunLoop(title, new Dictionary { ['w'] = ("increase the simulated failure rate", () => simulationEffects.IncreaseFailureRate()), ['s'] = ("decrease the simulated failure rate", () => simulationEffects.DecreaseFailureRate()) -}, writer => simulationEffects.WriteState(writer), interactive); +}, writer => simulationEffects.WriteState(writer)); await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/Billing/ProgressBar.cs b/src/Billing/ProgressBar.cs new file mode 100644 index 00000000..f04d484f --- /dev/null +++ b/src/Billing/ProgressBar.cs @@ -0,0 +1,21 @@ +namespace Billing; + +public class ProgressBar : IDisposable +{ + private readonly string widgetId = Guid.NewGuid().ToString("N"); + + public ProgressBar() + { + Console.WriteLine($"!BeginWidget Progress {widgetId}"); + } + + public void Update(int percent) + { + Console.WriteLine($"!Widget {widgetId} {percent}"); + } + + public void Dispose() + { + Console.WriteLine($"!EndWidget {widgetId}"); + } +} \ No newline at end of file diff --git a/src/Billing/RetrievingMessageProgressBehavior.cs b/src/Billing/RetrievingMessageProgressBehavior.cs new file mode 100644 index 00000000..2c56cc33 --- /dev/null +++ b/src/Billing/RetrievingMessageProgressBehavior.cs @@ -0,0 +1,25 @@ +using NServiceBus.Pipeline; +using Shared; + +namespace Billing; + +public class RetrievingMessageProgressBehavior : Behavior +{ + private FailureSimulator failureSimulator = new(); + + public override async Task Invoke(ITransportReceiveContext context, Func next) + { + if (context.Message.Headers.ContainsKey("MonitoringDemo.SlowMotion")) + { + Console.WriteLine($"Retrieving message {context.Message.MessageId}..."); + await failureSimulator.RunInteractive(context.CancellationToken); + } + + await next().ConfigureAwait(false); + } + + public void Failure() + { + failureSimulator.Trigger(); + } +} \ No newline at end of file diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs index 31fe9ba2..6a6c0bca 100644 --- a/src/ClientUI/Program.cs +++ b/src/ClientUI/Program.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Reflection; +using System.Text.Json; using ClientUI; using Messages; using Shared; @@ -14,7 +15,11 @@ } }); -var transport = endpointConfiguration.UseTransport(); +var transport = new LearningTransport +{ + StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport") +}; +var routing = endpointConfiguration.UseTransport(transport); endpointConfiguration.AuditProcessedMessagesTo("audit"); endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); @@ -29,7 +34,6 @@ TimeSpan.FromMilliseconds(500) ); -var routing = transport.Routing(); routing.RouteToEndpoint(typeof(PlaceOrder), "Sales"); var endpointInstance = await Endpoint.Start(endpointConfiguration); @@ -38,13 +42,13 @@ var cancellation = new CancellationTokenSource(); var simulatedWork = simulatedCustomers.Run(cancellation.Token); -var nonInteractive = args.Length > 1 && bool.TryParse(args[1], out var isInteractive) && !isInteractive; -var interactive = !nonInteractive; UserInterface.RunLoop("Load (ClientUI)", new Dictionary { ['c'] = ("toggle High/Low traffic mode", () => simulatedCustomers.ToggleTrafficMode()), -}, writer => simulatedCustomers.WriteState(writer), interactive); + ['v'] = ("toggle manual mode", () => simulatedCustomers.ToggleManualMode()), + ['b'] = ("send message manually", () => simulatedCustomers.SendManually()), +}, writer => simulatedCustomers.WriteState(writer)); cancellation.Cancel(); diff --git a/src/ClientUI/SimulatedCustomers.cs b/src/ClientUI/SimulatedCustomers.cs index 368d73e8..dcb4923e 100644 --- a/src/ClientUI/SimulatedCustomers.cs +++ b/src/ClientUI/SimulatedCustomers.cs @@ -4,10 +4,14 @@ namespace ClientUI; class SimulatedCustomers(IEndpointInstance endpointInstance) { + private const string Letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public void WriteState(TextWriter output) { - var trafficMode = highTrafficMode ? "High" : "Low"; - output.WriteLine($"{trafficMode} traffic mode - sending {rate} orders / second"); + var trafficMode = manualMode + ? "Manual sending mode" + : highTrafficMode ? $"High traffic mode - sending {rate} orders / second" : $"Low traffic mode - sending {rate} orders / second"; + output.WriteLine(trafficMode); } public void ToggleTrafficMode() @@ -16,6 +20,16 @@ public void ToggleTrafficMode() rate = highTrafficMode ? 8 : 1; } + public void ToggleManualMode() + { + manualMode = !manualMode; + } + + public void SendManually() + { + manualModeSemaphore.Release(); + } + public async Task Run(CancellationToken cancellationToken = default) { nextReset = DateTime.UtcNow.AddSeconds(1); @@ -30,6 +44,11 @@ public async Task Run(CancellationToken cancellationToken = default) nextReset = now.AddSeconds(1); } + if (manualMode) + { + await manualModeSemaphore.WaitAsync(); + } + await PlaceSingleOrder(cancellationToken); currentIntervalCount++; @@ -53,12 +72,22 @@ public async Task Run(CancellationToken cancellationToken = default) Task PlaceSingleOrder(CancellationToken cancellationToken) { + var messageId = new string(Enumerable.Range(0, 4).Select(x => Letters[Random.Shared.Next(Letters.Length)]).ToArray()); + var placeOrderCommand = new PlaceOrder { OrderId = Guid.NewGuid().ToString() }; - return endpointInstance.Send(placeOrderCommand, cancellationToken); + var sendOptions = new SendOptions(); + + if (manualMode) + { + sendOptions.SetHeader("MonitoringDemo.SlowMotion", "True"); + } + + sendOptions.SetMessageId(messageId); + return endpointInstance.Send(placeOrderCommand, sendOptions, cancellationToken); } bool highTrafficMode; @@ -66,4 +95,6 @@ Task PlaceSingleOrder(CancellationToken cancellationToken) DateTime nextReset; int currentIntervalCount; int rate = 1; + private bool manualMode; + private SemaphoreSlim manualModeSemaphore = new SemaphoreSlim(0); } \ No newline at end of file diff --git a/src/MonitoringDemo/ColoredConsole.cs b/src/MonitoringDemo/ColoredConsole.cs deleted file mode 100644 index 0db64820..00000000 --- a/src/MonitoringDemo/ColoredConsole.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MonitoringDemo; - -static class ColoredConsole -{ - public static IDisposable Use(ConsoleColor color) - { - var previousColor = Console.ForegroundColor; - Console.ForegroundColor = color; - - return new Restorer(previousColor); - } - - class Restorer(ConsoleColor previousColor) : IDisposable - { - public void Dispose() - { - Console.ForegroundColor = previousColor; - } - } -} \ No newline at end of file diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index 5d4b394c..4eda516c 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -2,19 +2,9 @@ sealed class DemoLauncher : IDisposable { - public DemoLauncher(bool remoteControlMode) + public DemoLauncher() { - demoProcessGroup = new ProcessGroup("Particular.MonitoringDemo", remoteControlMode); - - File.WriteAllText(@".\Marker.sln", string.Empty); - } - - public void Send(string value) - { - demoProcessGroup.Send(BillingPath, 0, value); - demoProcessGroup.Send(ShippingPath, 0, value); - demoProcessGroup.Send(ClientPath, 0, value); - demoProcessGroup.Send(SalesPath, 0, value); + demoProcessGroup = new ProcessGroup("Particular.MonitoringDemo"); } public void Dispose() @@ -23,77 +13,26 @@ public void Dispose() demoProcessGroup.Dispose(); - File.Delete(@".\Marker.sln"); - - Console.WriteLine("Removing Transport Files"); + //Console.WriteLine("Removing Transport Files"); DirectoryEx.Delete(".learningtransport"); - Console.WriteLine("Deleting log folders"); + //Console.WriteLine("Deleting log folders"); DirectoryEx.Delete(".logs"); - Console.WriteLine("Deleting db folders"); + //Console.WriteLine("Deleting db folders"); DirectoryEx.ForceDeleteReadonly(".db"); DirectoryEx.ForceDeleteReadonly(".audit-db"); } - public void Platform() + public ProcessHandle AddProcess(string name, string instanceId) { if (disposed) { - return; + return ProcessHandle.Empty; } - demoProcessGroup.AddProcess(Path.Combine("PlatformLauncher", "PlatformLauncher.dll")); - } - - public void Billing() - { - if (disposed) - { - return; - } - - demoProcessGroup.AddProcess(BillingPath); - } - - public void Shipping() - { - if (disposed) - { - return; - } - - demoProcessGroup.AddProcess(ShippingPath); - } - - public void ScaleOutSales() - { - if (disposed) - { - return; - } - - demoProcessGroup.AddProcess(SalesPath); - } - - public void ScaleInSales() - { - if (disposed) - { - return; - } - - demoProcessGroup.KillProcess(SalesPath); - } - - public void ClientUI() - { - if (disposed) - { - return; - } - - demoProcessGroup.AddProcess(ClientPath); + var path = Path.Combine(name, $"{name}.dll"); //TODO: Hard-coded convention + return demoProcessGroup.AddProcess(path, instanceId); } readonly ProcessGroup demoProcessGroup; @@ -103,4 +42,7 @@ public void ClientUI() private static readonly string ShippingPath = Path.Combine("Shipping", "Shipping.dll"); private static readonly string SalesPath = Path.Combine("Sales", "Sales.dll"); private static readonly string ClientPath = Path.Combine("ClientUI", "ClientUI.dll"); + private static readonly string PlatformPath = Path.Combine("PlatformLauncher", "PlatformLauncher.dll"); + + } \ No newline at end of file diff --git a/src/MonitoringDemo/DeterministicGuid.cs b/src/MonitoringDemo/DeterministicGuid.cs new file mode 100644 index 00000000..bf91fa63 --- /dev/null +++ b/src/MonitoringDemo/DeterministicGuid.cs @@ -0,0 +1,15 @@ +using System.Security.Cryptography; +using System.Text; + +static class DeterministicGuid +{ + public static Guid Create(params object[] data) + { + // use MD5 hash to get a 16-byte hash of the string + using var provider = MD5.Create(); + var inputBytes = Encoding.Default.GetBytes(string.Concat(data)); + var hashBytes = provider.ComputeHash(inputBytes); + // generate a guid from the hash: + return new Guid(hashBytes); + } +} \ No newline at end of file diff --git a/src/MonitoringDemo/IWidget.cs b/src/MonitoringDemo/IWidget.cs new file mode 100644 index 00000000..fc9c7a7f --- /dev/null +++ b/src/MonitoringDemo/IWidget.cs @@ -0,0 +1,6 @@ +namespace MonitoringDemo; + +public interface IWidget +{ + string ProcessInput(string line); +} \ No newline at end of file diff --git a/src/MonitoringDemo/MonitoringDemo.csproj b/src/MonitoringDemo/MonitoringDemo.csproj index c70c36f5..7638f642 100644 --- a/src/MonitoringDemo/MonitoringDemo.csproj +++ b/src/MonitoringDemo/MonitoringDemo.csproj @@ -14,4 +14,8 @@ + + + + \ No newline at end of file diff --git a/src/MonitoringDemo/ProcessGroup.cs b/src/MonitoringDemo/ProcessGroup.cs index 01f647f1..be0a4f62 100644 --- a/src/MonitoringDemo/ProcessGroup.cs +++ b/src/MonitoringDemo/ProcessGroup.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using System.Threading.Channels; namespace MonitoringDemo; @@ -10,40 +11,18 @@ namespace MonitoringDemo; /// partial class ProcessGroup : IDisposable { - readonly bool redirectInputAndOutput; - readonly Dictionary> processesByAssemblyPath = []; - readonly List managedProcessIds = []; + readonly Dictionary> processesByAssemblyPath = []; bool disposed; - // Windows-specific fields - nint jobHandle; - - public ProcessGroup(string groupName,bool redirectInputAndOutput) + public ProcessGroup(string groupName) { - this.redirectInputAndOutput = redirectInputAndOutput; if (OperatingSystem.IsWindows()) { InitializeWindowsJob(groupName); } } - public void Send(string relativeAssemblyPath, int index, string value) - { - if (!redirectInputAndOutput) - { - return; - } - - if (processesByAssemblyPath.TryGetValue(relativeAssemblyPath, out var processes)) - { - if (processes.Count > index) - { - processes.ElementAt(index).StandardInput.WriteLine(value); - } - } - } - - public bool AddProcess(string relativeAssemblyPath) + public ProcessHandle AddProcess(string relativeAssemblyPath, string instanceId) { if (!processesByAssemblyPath.TryGetValue(relativeAssemblyPath, out var processes)) { @@ -51,58 +30,66 @@ public bool AddProcess(string relativeAssemblyPath) processesByAssemblyPath[relativeAssemblyPath] = processes; } - var processesCount = processes.Count; - var instanceId = processesCount == 0 ? null : $"instance-{processesCount}"; - var process = StartProcess(relativeAssemblyPath, instanceId); if (process is null) { - return false; + return ProcessHandle.Empty; } - if (redirectInputAndOutput) + var outputChannel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true }); + process.OutputDataReceived += (sender, args) => outputChannel.Writer.TryWrite(args.Data); + process.BeginOutputReadLine(); + + processes.Add(process.Id, process); + + if (OperatingSystem.IsWindows()) { - process.OutputDataReceived += (sender, args) => Console.WriteLine(args.Data); - process.BeginOutputReadLine(); + AddProcessToWindowsJob(process); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + AddProcessToUnixGroup(process); } - processes.Push(process); - managedProcessIds.Add(process.Id); - - return OperatingSystem.IsWindows() ? AddProcessToWindowsJob(process) - : (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) && AddProcessToUnixGroup(process); + return new ProcessHandle(outputChannel, input => process.StandardInput.WriteLine(input), () => + { + KillProcess(relativeAssemblyPath, process.Id); + }); } - public void KillProcess(string relativeAssemblyPath) + private void KillProcess(string relativeAssemblyPath, int id) { if (!processesByAssemblyPath.TryGetValue(relativeAssemblyPath, out var processes)) { return; } - while (processes.TryPop(out var victim)) + if (!processes.TryGetValue(id, out var victim)) { - try - { - if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) - { - KillProcessGroupUnix(victim.Id); - } - else - { - victim.Kill(true); - } - return; - } - catch (Exception) + return; + } + + try + { + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { - // Process already terminated + KillProcessGroupUnix(victim.Id); } - finally + else { - victim.Dispose(); + victim.Kill(true); } + + processes.Remove(id); + } + catch (Exception) + { + // Process already terminated + } + finally + { + victim.Dispose(); } } @@ -146,6 +133,8 @@ private void KillProcessGroupUnix(int processId) #region Windows-specific implementations + nint jobHandle; + [SupportedOSPlatform("windows")] private void InitializeWindowsJob(string jobName) { @@ -214,7 +203,7 @@ private void DisposeWindowsJob() #endregion - private Process? StartProcess(string relativeAssemblyPath, string? arguments = null) + private static Process? StartProcess(string relativeAssemblyPath, string? arguments = null) { var fullAssemblyPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, relativeAssemblyPath)); var workingDirectory = Path.GetDirectoryName(fullAssemblyPath); @@ -222,14 +211,14 @@ private void DisposeWindowsJob() var startInfo = new ProcessStartInfo("dotnet", fullAssemblyPath) { WorkingDirectory = workingDirectory, - UseShellExecute = !redirectInputAndOutput, - RedirectStandardInput = redirectInputAndOutput, - RedirectStandardOutput = redirectInputAndOutput + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true }; if (arguments is not null) { - startInfo.Arguments += $" {arguments} {!redirectInputAndOutput}"; + startInfo.Arguments += $" {arguments}"; } return Process.Start(startInfo); @@ -257,7 +246,7 @@ protected virtual void Dispose(bool disposing) else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { // Clean up any remaining processes on Unix systems - foreach (var pid in managedProcessIds) + foreach (var pid in processesByAssemblyPath.Values.SelectMany(x => x.Keys)) { try { @@ -275,7 +264,6 @@ protected virtual void Dispose(bool disposing) } processesByAssemblyPath.Clear(); - managedProcessIds.Clear(); } disposed = true; diff --git a/src/MonitoringDemo/ProcessHandle.cs b/src/MonitoringDemo/ProcessHandle.cs new file mode 100644 index 00000000..482edcd3 --- /dev/null +++ b/src/MonitoringDemo/ProcessHandle.cs @@ -0,0 +1,26 @@ +using System.Threading.Channels; + +namespace MonitoringDemo; + +sealed class ProcessHandle(Channel outputChannel, Action sendAction, Action closeAction) + : IDisposable +{ + public ChannelReader Reader { get; } = outputChannel.Reader; + + public void Send(string value) + { + sendAction(value); + } + + public IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken = default) { + return outputChannel.Reader.ReadAllAsync(cancellationToken); + } + + public void Dispose() + { + outputChannel.Writer.TryComplete(); + closeAction(); + } + + public static readonly ProcessHandle Empty = new(Channel.CreateBounded(0), _ => { }, () => { }); +} \ No newline at end of file diff --git a/src/MonitoringDemo/ProcessWindow.cs b/src/MonitoringDemo/ProcessWindow.cs new file mode 100644 index 00000000..1880268d --- /dev/null +++ b/src/MonitoringDemo/ProcessWindow.cs @@ -0,0 +1,298 @@ +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using System.Text.RegularExpressions; +using Terminal.Gui; +using Window = Terminal.Gui.Window; + +namespace MonitoringDemo; + +sealed partial class ProcessWindow : Window +{ + private const string Letters = "abcdefghijklmnopqrstuvwxyz"; + + private readonly string name; + private readonly DemoLauncher launcher; + + private readonly ConcurrentDictionary> linesPerInstance = new(); + private readonly HashSet recognizedKeys = new(); + public ListView? InstanceView { get; } + public ListView LogView { get; } + private ObservableCollection Instances { get; } = new(); + private Dictionary Processes { get; } = new(); + + [GeneratedRegex(@"Press (\w) to")] + private static partial Regex PressKeyRegex(); + + [GeneratedRegex(@"!BeginWidget (\w+) (\w+)")] + private static partial Regex WidgetStartRegex(); + + [GeneratedRegex(@"!EndWidget (\w+)")] + private static partial Regex WidgetEndRegex(); + + [GeneratedRegex(@"!Widget (\w+) (\w+)")] + private static partial Regex WidgetUpdateRegex(); + + public ProcessWindow(string title, string name, bool singleInstance, DemoLauncher launcher, CancellationToken cancellationToken) + { + this.name = name; + this.launcher = launcher; + + Title = title; + X = 0; + Y = 1; + Width = Dim.Fill(); + Height = Dim.Fill(); + + if (!singleInstance) + { + InstanceView = new ListView + { + X = 0, + Y = 0, + Width = Dim.Fill(), + Height = Dim.Fill(), + Source = new ListWrapper(Instances) + }; + InstanceView.SetSource(Instances); + InstanceView.SelectedItemChanged += InstanceView_SelectedItemChanged; + var instanceViewFrame = new FrameView + { + X = 0, + Y = 0, + Width = 15, + Height = Dim.Fill(), + Title = "Instances" + }; + instanceViewFrame.Add(InstanceView); + + Add(instanceViewFrame); + } + + LogView = new ListView + { + X = 0, + Y = 0, + Width = Dim.Fill(), + Height = Dim.Fill(), + Source = new ListWrapper([]) + }; + LogView.AllowsMarking = false; + LogView.AllowsMultipleSelection = false; + var logViewFrame = new FrameView + { + X = InstanceView != null ? 15 : 0, + Y = 0, + Width = Dim.Fill(), + Height = Dim.Fill(), + Title = "Output" + }; + logViewFrame.Add(LogView); + + Add(logViewFrame); + + AddCommand(Command.DeleteAll, () => + { + var instance = Instances[SelectedInstance]; + var lines = linesPerInstance.GetOrAdd(instance, _ => []); + lines.Clear(); + LogView.SetSource(lines); + return true; + }); + AddCommand(Command.HotKey, () => + { + var instance = Instances[SelectedInstance]; + Processes[instance].Send("?"); + return true; + }); + AddCommand(Command.Up, () => + { + var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + StartNewProcess(cancellationTokenSource); + return true; + }); + AddCommand(Command.Down, () => + { + var instance = Instances[SelectedInstance]; + Processes.Remove(instance, out var process); + process!.Dispose(); + Instances.Remove(instance); + linesPerInstance.TryRemove(instance, out _); + return true; + }); + + KeyBindings.Add(Key.C.WithCtrl, Command.DeleteAll); + KeyBindings.Add(Key.F1, Command.HotKey); + + if (!singleInstance) + { + KeyBindings.Add(Key.F2, Command.Up); + KeyBindings.Add(Key.F3, Command.Down); + } + + StartNewProcess(CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)); + } + + int SelectedInstance => Math.Max(InstanceView?.SelectedItem ?? 0, 0); + + private void InstanceView_SelectedItemChanged(object? sender, ListViewItemEventArgs args) + { + SelectInstance(Instances[args.Item]); + } + + void StartNewProcess(CancellationTokenSource cancellationTokenSource) + { + string instanceId; + do + { + instanceId = new string(Enumerable.Range(0, 4).Select(x => Letters[Random.Shared.Next(Letters.Length)]).ToArray()); + } while (Instances.Contains(instanceId)); + + var process = new Process(launcher.AddProcess(name, instanceId), cancellationTokenSource); + Processes[instanceId] = process; + Instances.Add(instanceId); + + PrintOutput(instanceId, process, cancellationTokenSource.Token); + + SelectInstance(instanceId); + } + + void SelectInstance(string instance) + { + LogView.SetSource(linesPerInstance.GetOrAdd(instance, _ => [])); + LogView.MoveEnd(); + } + + void PrintOutput(string instance, Process process, CancellationToken cancellationToken) + { + var lines = linesPerInstance.GetOrAdd(instance, _ => []); + + _ = Task.Run(async () => + { + try + { + var activeWidgets = new Dictionary(); + var activeWidgetPositions = new Dictionary(); + + await foreach (var output in process.ReadAllAsync(cancellationToken)) + { + if (string.IsNullOrWhiteSpace(output)) + { + continue; + } + + Application.Invoke(() => + { + var startWidgetMatch = WidgetStartRegex().Match(output); + if (startWidgetMatch.Success) + { + var widgetName = startWidgetMatch.Groups[1].Value; + var widgetId = startWidgetMatch.Groups[2].Value; + + var widget = CreateWidget(widgetName); + if (widget != null) + { + activeWidgets[widgetId] = widget; + } + return; + } + var endWidgetMatch = WidgetEndRegex().Match(output); + if (endWidgetMatch.Success) + { + var widgetId = endWidgetMatch.Groups[1].Value; + activeWidgets.Remove(widgetId); + return; + } + + var updateWidgetMatch = WidgetUpdateRegex().Match(output); + if (updateWidgetMatch.Success) + { + var widgetId = updateWidgetMatch.Groups[1].Value; + var widgetData = updateWidgetMatch.Groups[2].Value; + + var widgetLine = activeWidgets[widgetId].ProcessInput(widgetData); + + if (!activeWidgetPositions.TryGetValue(widgetId, out var position)) + { + activeWidgetPositions[widgetId] = lines.Count; + lines.Add(widgetLine); + + } + else + { + lines[position] = widgetLine; + } + LogView.MoveEnd(); // Scroll to end + return; + } + + var pressKeyMatch = PressKeyRegex().Match(output); + if (pressKeyMatch.Success) + { + var groupValue = pressKeyMatch.Groups[1].Value[0]; + recognizedKeys.Add(groupValue); + recognizedKeys.Add(char.ToLowerInvariant(groupValue)); + } + + lines.Add(output); + LogView.MoveEnd(); // Scroll to end + }); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Ignore cancellation + } + }); + } + + private static IWidget? CreateWidget(string widgetName) + { + return widgetName == "Progress" ? new ProgressBarWidget() : null; + } + + public void HandleKey(Key e) + { + var instance = Instances[SelectedInstance]; + + var keyChar = (char)e.KeyCode; + if (!recognizedKeys.Contains(keyChar)) + { + return; + } + + //If uppercase, send to all instances. If lowercase, send to selected instance + if (e.IsShift) + { + foreach (var handle in Processes.Values) + { + handle.Send(new string(keyChar, 1)); + } + } + else + { + Processes[instance].Send(new string(keyChar, 1)); + } + + e.Handled = true; + } + + sealed class Process(ProcessHandle handle, CancellationTokenSource cancellationTokenSource) + : IDisposable + { + public void Send(string value) + { + handle.Send(value); + } + + public IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken = default) { + return handle.ReadAllAsync(cancellationToken); + } + + public void Dispose() + { + cancellationTokenSource.Cancel(); + handle.Dispose(); + cancellationTokenSource.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index b790b4af..f455e193 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -1,108 +1,141 @@ using MonitoringDemo; +using System.Reflection.Metadata; +using Terminal.Gui; CancellationTokenSource tokenSource = new(); -Console.Title = "MonitoringDemo"; +var cancellationToken = tokenSource.Token; -var remoteControlMode = args.Length > 0 && string.Equals(args[0], bool.TrueString, StringComparison.InvariantCultureIgnoreCase); -var syncEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); +Application.Init(); -Console.CancelKeyPress += (sender, eventArgs) => -{ - eventArgs.Cancel = true; - tokenSource.Cancel(); - syncEvent.TrySetResult(true); -}; - -try -{ - using var launcher = new DemoLauncher(remoteControlMode); - Console.WriteLine("Starting the Particular Platform"); +using var launcher = new DemoLauncher(); - launcher.Platform(); +using var top = new Window(); +top.Title = "Particular Monitoring Demo"; +top.X = 0; +top.Y = 1; +top.Width = Dim.Fill(); +top.Height = Dim.Fill(); - using (ColoredConsole.Use(ConsoleColor.Yellow)) - { - Console.WriteLine( - "Once ServiceControl has finished starting a browser window will pop up showing the ServicePulse monitoring tab"); - } +var menuBarItems = new List(); - Console.WriteLine("Starting Demo Solution"); +ProcessWindow[] windows = []; +windows = [ + CreateWindow("Platform", "PlatformLauncher", "_Platform", true, cancellationToken), + CreateWindow("Billing", "Billing", "_Billing", false, cancellationToken), + CreateWindow("Shipping", "Shipping", "S_hipping", false, cancellationToken), + CreateWindow("ClientUI", "ClientUI", "_ClientUI", true, cancellationToken), + CreateWindow("Sales", "Sales", "_Sales", false, cancellationToken) +]; - if (!tokenSource.IsCancellationRequested) +menuBarItems.Add( + new MenuBarItem("_Quit", "", () => { - Console.WriteLine("Starting Billing endpoint."); - launcher.Billing(); + tokenSource.Cancel(); + Application.RequestStop(); + })); - Console.WriteLine("Starting Sales endpoint."); - launcher.ScaleOutSales(); - - Console.WriteLine("Starting Shipping endpoint."); - launcher.Shipping(); +top.Add(new MenuBar +{ + Menus = menuBarItems.ToArray() +}); +foreach (var window in windows) +{ + top.Add(window); +} - Console.WriteLine("Starting ClientUI endpoint."); - launcher.ClientUI(); +foreach (var window in windows.Skip(1)) +{ + window.Visible = false; +} - using (ColoredConsole.Use(ConsoleColor.Yellow)) - { - ScaleSalesEndpointIfRequired(launcher, syncEvent); +Application.KeyDown += ApplicationKeyDown; - await syncEvent.Task; +void ApplicationKeyDown(object? sender, Key e) +{ + if (!e.IsKeyCodeAtoZ) + { + return; + } - Console.WriteLine("Shutting down"); + foreach (var processWindow in windows) + { + processWindow.HandleKey(e); + if (e.Handled) + { + break; } } } -catch (Exception e) + +Application.Run(top); + +Application.Shutdown(); +return; + + +static void SwitchWindow(IReadOnlyCollection windowsToHide, View windowToShow, View focusTarget) { - using (ColoredConsole.Use(ConsoleColor.Red)) + // Hide all other windows windows + foreach (var window in windowsToHide) { - Console.WriteLine("Error starting setting up demo."); - Console.WriteLine($"{e.Message}{Environment.NewLine}{e.StackTrace}"); + window.Visible = false; } -} -using (ColoredConsole.Use(ConsoleColor.Yellow)) -{ - Console.WriteLine("Done, press ENTER."); - Console.ReadLine(); + windowToShow.Visible = true; + focusTarget.SetFocus(); + windowToShow.SetNeedsDraw(); } -void ScaleSalesEndpointIfRequired(DemoLauncher launcher, TaskCompletionSource syncEvent) +ProcessWindow CreateWindow(string title, string name, string menuItemText, bool singleInstance, CancellationToken cancellationToken) { - _ = Task.Run(() => - { - try - { - Console.WriteLine(); - Console.WriteLine("Press [up arrow] to scale out the Sales service or [down arrow] to scale in"); - Console.WriteLine("Press Ctrl+C stop Particular Monitoring Demo."); - Console.WriteLine(); - - while (!tokenSource.IsCancellationRequested) - { - var input = Console.ReadKey(true); - switch (input.Key) - { - case ConsoleKey.LeftArrow: - launcher.ScaleInSales(); - break; - case ConsoleKey.RightArrow: - launcher.ScaleOutSales(); - break; - default: - launcher.Send(new string(input.KeyChar, 1)); - break; - } - } - } - catch (OperationCanceledException) - { - // ignore - } - catch (Exception e) - { - // surface any other exception - syncEvent.TrySetException(e); - } - }); + var processWindow = new ProcessWindow(title, name, singleInstance, launcher, cancellationToken); + var windowsToHide = windows.Except([processWindow]).ToArray(); + + var menuItem = new MenuBarItem(menuItemText, "", + () => SwitchWindow(windowsToHide, processWindow, processWindow.LogView)); + + menuBarItems.Add(menuItem); + + return processWindow; } + + +// void ScaleSalesEndpointIfRequired(DemoLauncher launcher, TaskCompletionSource syncEvent) +// { +// _ = Task.Run(() => +// { +// try +// { +// Console.WriteLine(); +// Console.WriteLine("Press [up arrow] to scale out the Sales service or [down arrow] to scale in"); +// Console.WriteLine("Press Ctrl+C stop Particular Monitoring Demo."); +// Console.WriteLine(); +// +// while (!tokenSource.IsCancellationRequested) +// { +// var input = Console.ReadKey(true); +// switch (input.Key) +// { +// case ConsoleKey.LeftArrow: +// launcher.ScaleInSales(); +// break; +// case ConsoleKey.RightArrow: +// launcher.ScaleOutSales(); +// break; +// default: +// launcher.Send(new string(input.KeyChar, 1)); +// break; +// } +// } +// } +// catch (OperationCanceledException) +// { +// // ignore +// } +// catch (Exception e) +// { +// // surface any other exception +// syncEvent.TrySetException(e); +// } +// }); +// } \ No newline at end of file diff --git a/src/MonitoringDemo/ProgressBarWidget.cs b/src/MonitoringDemo/ProgressBarWidget.cs new file mode 100644 index 00000000..9286c179 --- /dev/null +++ b/src/MonitoringDemo/ProgressBarWidget.cs @@ -0,0 +1,13 @@ +namespace MonitoringDemo; + +public class ProgressBarWidget : IWidget +{ + public string ProcessInput(string line) + { + var progressPercent = int.Parse(line); + var barsFilled = progressPercent / 10; + var bars = new string('\u2588', barsFilled); + var spaces = new string(' ', 10 - barsFilled); + return $"[{bars}{spaces}] {progressPercent}%"; + } +} \ No newline at end of file diff --git a/src/MonitoringDemo/UserInterface.cs b/src/MonitoringDemo/UserInterface.cs index 39628519..349b1acf 100644 --- a/src/MonitoringDemo/UserInterface.cs +++ b/src/MonitoringDemo/UserInterface.cs @@ -2,53 +2,14 @@ namespace Shared; public static class UserInterface { - public static void RunLoop(string title, Dictionary controls, Action reportState, bool interactive) - { - if (interactive) - { - RunInteractiveLoop(title, controls, reportState); - } - else - { - RunNonInteractiveLoop(title, controls, reportState); - } - } - - static void RunInteractiveLoop(string title, Dictionary controls, Action reportState) - { - Console.Title = title; - Console.SetWindowSize(65, 15); - - while (true) - { - Console.Clear(); - foreach (var kvp in controls) - { - Console.WriteLine($"Press {char.ToUpperInvariant(kvp.Key)} to {kvp.Value.Message}"); - } - Console.WriteLine("Press ESC to quit"); - Console.WriteLine(); - - reportState(Console.Out); - - var input = Console.ReadKey(true); - - if (controls.TryGetValue(char.ToLowerInvariant(input.KeyChar), out var control)) - { - control.Action(); - } - else if (input.Key == ConsoleKey.Escape) - { - return; - } - } - } - - static void RunNonInteractiveLoop(string title, Dictionary controls, Action reportState) +#pragma warning disable PS0018 + public static void RunLoop(string title, Dictionary controls, Action reportState) +#pragma warning restore PS0018 { Console.Title = title; reportState(Console.Out); + PrintControls(controls); while (true) { @@ -65,6 +26,18 @@ static void RunNonInteractiveLoop(string title, Dictionary controls) + { + foreach (var kvp in controls) + { + Console.WriteLine($"Press {char.ToUpperInvariant(kvp.Key)} to {kvp.Value.Message}"); } + Console.WriteLine("Press ? for help"); } -} \ No newline at end of file +} diff --git a/src/PlatformLauncher/Program.cs b/src/PlatformLauncher/Program.cs index eb7015db..62bda151 100644 --- a/src/PlatformLauncher/Program.cs +++ b/src/PlatformLauncher/Program.cs @@ -1,9 +1,13 @@ using Particular; +using System.Reflection; Console.Title = "PlatformLauncher"; + +var rootFolder = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName); + try { - await PlatformLauncher.Launch(showPlatformToolConsoleOutput: false, servicePulseDefaultRoute: "/monitoring"); + await PlatformLauncher.Launch(showPlatformToolConsoleOutput: false, servicePulseDefaultRoute: "/monitoring", rootFolder); } catch (Exception e) { diff --git a/src/Sales/DispatchingProgressBehavior.cs b/src/Sales/DispatchingProgressBehavior.cs new file mode 100644 index 00000000..3bae23ac --- /dev/null +++ b/src/Sales/DispatchingProgressBehavior.cs @@ -0,0 +1,26 @@ +using NServiceBus.Pipeline; +using NServiceBus.Transport; + +namespace Sales; + +public class DispatchingProgressBehavior : Behavior +{ + private FailureSimulator failureSimulator = new(); + + public override async Task Invoke(IBatchDispatchContext context, Func next) + { + await next().ConfigureAwait(false); + + var incomingMessage = context.Extensions.Get(); + if (incomingMessage.Headers.ContainsKey("MonitoringDemo.SlowMotion")) + { + Console.WriteLine($"Dispatching outgoing messages {incomingMessage.MessageId}..."); + await failureSimulator.RunInteractive(context.CancellationToken); + } + } + + public void Failure() + { + failureSimulator.Trigger(); + } +} \ No newline at end of file diff --git a/src/Sales/FailureSimulation.cs b/src/Sales/FailureSimulation.cs new file mode 100644 index 00000000..634a145e --- /dev/null +++ b/src/Sales/FailureSimulation.cs @@ -0,0 +1,32 @@ +namespace Sales; + +public class FailureSimulation +{ + private RetrievingMessageProgressBehavior retrievingMessageProgressBehavior = new RetrievingMessageProgressBehavior(); + private ProcessingMessageProgressBehavior processingMessageProgressBehavior = new ProcessingMessageProgressBehavior(); + private DispatchingProgressBehavior dispatchingMessageProgressBehavior = new DispatchingProgressBehavior(); + + public void Register(EndpointConfiguration endpointConfiguration) + { + endpointConfiguration.Pipeline.Register(retrievingMessageProgressBehavior, "Shows progress of retrieving messages"); + + endpointConfiguration.Pipeline.Register(processingMessageProgressBehavior, "Shows progress of processing messages"); + + endpointConfiguration.Pipeline.Register(dispatchingMessageProgressBehavior, "Shows progress of dispatching messages"); + } + + public void TriggerFailureReceiving() + { + retrievingMessageProgressBehavior.Failure(); + } + + public void TriggerFailureProcessing() + { + processingMessageProgressBehavior.Failure(); + } + + public void TriggerFailureDispatching() + { + dispatchingMessageProgressBehavior.Failure(); + } +} \ No newline at end of file diff --git a/src/Sales/FailureSimulator.cs b/src/Sales/FailureSimulator.cs new file mode 100644 index 00000000..04f088bd --- /dev/null +++ b/src/Sales/FailureSimulator.cs @@ -0,0 +1,30 @@ +namespace Sales; + +public class FailureSimulator +{ + private bool failureTriggered = false; + +#pragma warning disable PS0003 + public async Task RunInteractive(CancellationToken cancellationToken) +#pragma warning restore PS0003 + { + using var progressBar = new ProgressBar(); + + for (var i = 0; i <= 100; i++) + { + if (failureTriggered) + { + failureTriggered = false; + throw new Exception("Simulated failure"); + } + progressBar.Update(i); + await Task.Delay(25, cancellationToken).ConfigureAwait(false); + } + } + + public void Trigger() + { + //TODO: Use Interlocked + failureTriggered = true; + } +} \ No newline at end of file diff --git a/src/Sales/PlaceOrderHandler.cs b/src/Sales/PlaceOrderHandler.cs index 5bad2144..276699aa 100644 --- a/src/Sales/PlaceOrderHandler.cs +++ b/src/Sales/PlaceOrderHandler.cs @@ -14,6 +14,13 @@ public async Task Handle(PlaceOrder message, IMessageHandlerContext context) OrderId = message.OrderId }; - await context.Publish(orderPlaced); + var publishOptions = new PublishOptions(); + + if (context.MessageHeaders.ContainsKey("MonitoringDemo.SlowMotion")) + { + publishOptions.SetHeader("MonitoringDemo.SlowMotion", "True"); + } + + await context.Publish(orderPlaced, publishOptions); } } \ No newline at end of file diff --git a/src/Sales/ProcessingMessageProgressBehavior.cs b/src/Sales/ProcessingMessageProgressBehavior.cs new file mode 100644 index 00000000..59f34eee --- /dev/null +++ b/src/Sales/ProcessingMessageProgressBehavior.cs @@ -0,0 +1,24 @@ +using NServiceBus.Pipeline; + +namespace Sales; + +public class ProcessingMessageProgressBehavior : Behavior +{ + private FailureSimulator failureSimulator = new(); + + public override async Task Invoke(IIncomingLogicalMessageContext context, Func next) + { + if (context.Headers.ContainsKey("MonitoringDemo.SlowMotion")) + { + Console.WriteLine($"Processing message {context.MessageId}..."); + await failureSimulator.RunInteractive(context.CancellationToken); + } + + await next().ConfigureAwait(false); + } + + public void Failure() + { + failureSimulator.Trigger(); + } +} \ No newline at end of file diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs index 79660680..273d4f62 100644 --- a/src/Sales/Program.cs +++ b/src/Sales/Program.cs @@ -1,28 +1,16 @@ -using System.Security.Cryptography; -using System.Text; +using System.Reflection; using System.Text.Json; using Messages; using Microsoft.Extensions.DependencyInjection; using Sales; using Shared; -var instanceName = args.FirstOrDefault(); +var instancePostfix = args.FirstOrDefault(); -var instanceNumber = args.FirstOrDefault(); -string title; +var title = string.IsNullOrEmpty(instancePostfix) ? "Processing (Sales)" : $"Sales - {instancePostfix}"; +var instanceName = string.IsNullOrEmpty(instancePostfix) ? "sales" : $"sales-{instancePostfix}"; -if (string.IsNullOrEmpty(instanceNumber)) -{ - title = "Processing (Sales)"; - - instanceNumber = "original-instance"; -} -else -{ - title = $"Sales - {instanceNumber}"; -} - -var instanceId = DeterministicGuid.Create("Sales", instanceNumber); +var instanceId = DeterministicGuid.Create("Sales", instanceName); var endpointConfiguration = new EndpointConfiguration("Sales"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); @@ -36,7 +24,12 @@ } }); -endpointConfiguration.UseTransport(); +var transport = new LearningTransport +{ + StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport"), + TransportTransactionMode = TransportTransactionMode.ReceiveOnly +}; +endpointConfiguration.UseTransport(transport); endpointConfiguration.AuditProcessedMessagesTo("audit"); endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); @@ -51,31 +44,24 @@ TimeSpan.FromMilliseconds(500) ); +var failureSimulation = new FailureSimulation(); +failureSimulation.Register(endpointConfiguration); + var simulationEffects = new SimulationEffects(); endpointConfiguration.RegisterComponents(cc => cc.AddSingleton(simulationEffects)); -var endpointInstance = await Endpoint.Start(endpointConfiguration); +endpointConfiguration.UsePersistence(); +endpointConfiguration.EnableOutbox(); -var nonInteractive = args.Length > 1 && bool.TryParse(args[1], out var isInteractive) && !isInteractive; -var interactive = !nonInteractive; +var endpointInstance = await Endpoint.Start(endpointConfiguration); UserInterface.RunLoop(title, new Dictionary { ['r'] = ("process messages faster", () => simulationEffects.ProcessMessagesFaster()), - ['f'] = ("process messages slower", () => simulationEffects.ProcessMessagesSlower()) -}, writer => simulationEffects.WriteState(writer), interactive); + ['f'] = ("process messages slower", () => simulationEffects.ProcessMessagesSlower()), + ['t'] = ("simulate failure in retrieving", () => failureSimulation.TriggerFailureReceiving()), + ['y'] = ("simulate failure in processing", () => failureSimulation.TriggerFailureProcessing()), + ['u'] = ("simulate failure in dispatching", () => failureSimulation.TriggerFailureDispatching()) +}, writer => simulationEffects.WriteState(writer)); -await endpointInstance.Stop(); - -static class DeterministicGuid -{ - public static Guid Create(params object[] data) - { - // use MD5 hash to get a 16-byte hash of the string - using var provider = MD5.Create(); - var inputBytes = Encoding.Default.GetBytes(string.Concat(data)); - var hashBytes = provider.ComputeHash(inputBytes); - // generate a guid from the hash: - return new Guid(hashBytes); - } -} \ No newline at end of file +await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/Sales/ProgressBar.cs b/src/Sales/ProgressBar.cs new file mode 100644 index 00000000..a76b0f9d --- /dev/null +++ b/src/Sales/ProgressBar.cs @@ -0,0 +1,21 @@ +namespace Sales; + +public class ProgressBar : IDisposable +{ + private readonly string widgetId = Guid.NewGuid().ToString("N"); + + public ProgressBar() + { + Console.WriteLine($"!BeginWidget Progress {widgetId}"); + } + + public void Update(int percent) + { + Console.WriteLine($"!Widget {widgetId} {percent}"); + } + + public void Dispose() + { + Console.WriteLine($"!EndWidget {widgetId}"); + } +} \ No newline at end of file diff --git a/src/Sales/RetrievingMessageProgressBehavior.cs b/src/Sales/RetrievingMessageProgressBehavior.cs new file mode 100644 index 00000000..fcfea357 --- /dev/null +++ b/src/Sales/RetrievingMessageProgressBehavior.cs @@ -0,0 +1,25 @@ +using NServiceBus.Pipeline; +using Shared; + +namespace Sales; + +public class RetrievingMessageProgressBehavior : Behavior +{ + private FailureSimulator failureSimulator = new(); + + public override async Task Invoke(ITransportReceiveContext context, Func next) + { + if (context.Message.Headers.ContainsKey("MonitoringDemo.SlowMotion")) + { + Console.WriteLine($"Retrieving message {context.Message.MessageId}..."); + await failureSimulator.RunInteractive(context.CancellationToken); + } + + await next().ConfigureAwait(false); + } + + public void Failure() + { + failureSimulator.Trigger(); + } +} \ No newline at end of file diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj index 67bc7d3a..70b34974 100644 --- a/src/Sales/Sales.csproj +++ b/src/Sales/Sales.csproj @@ -14,6 +14,7 @@ + @@ -22,6 +23,9 @@ UserInterface.cs + + DeterministicGuid.cs + \ No newline at end of file diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs index 02cb5e38..e9f8e195 100644 --- a/src/Shipping/Program.cs +++ b/src/Shipping/Program.cs @@ -1,9 +1,17 @@ -using System.Text.Json; +using System.Reflection; +using System.Text.Json; using Messages; using Microsoft.Extensions.DependencyInjection; using Shared; using Shipping; +var instancePostfix = args.FirstOrDefault(); + +var title = string.IsNullOrEmpty(instancePostfix) ? "Processing (Shipping)" : $"Shipping - {instancePostfix}"; +var instanceName = string.IsNullOrEmpty(instancePostfix) ? "shipping" : $"shipping-{instancePostfix}"; + +var instanceId = DeterministicGuid.Create("Shipping", instanceName); + var endpointConfiguration = new EndpointConfiguration("Shipping"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); @@ -16,14 +24,18 @@ } }); -endpointConfiguration.UseTransport(); +var transport = new LearningTransport +{ + StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport") +}; +endpointConfiguration.UseTransport(transport); endpointConfiguration.AuditProcessedMessagesTo("audit"); endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); endpointConfiguration.UniquelyIdentifyRunningInstance() - .UsingCustomIdentifier(new Guid("BB8A8BAF-4187-455E-AAD2-211CD43267CB")) - .UsingCustomDisplayName("original-instance"); + .UsingCustomIdentifier(instanceId) + .UsingCustomDisplayName(instanceName); var metrics = endpointConfiguration.EnableMetrics(); metrics.SendMetricDataToServiceControl( @@ -36,14 +48,11 @@ var endpointInstance = await Endpoint.Start(endpointConfiguration); -var nonInteractive = args.Length > 1 && args[1] == bool.FalseString; -var interactive = !nonInteractive; - -UserInterface.RunLoop("Processing (Shipping)", new Dictionary +UserInterface.RunLoop(title, new Dictionary { ['z'] = ("toggle resource degradation simulation", () => simulationEffects.ToggleDegradationSimulation()), ['q'] = ("process OrderBilled events faster", () => simulationEffects.ProcessMessagesFaster()), ['a'] = ("process OrderBilled events slower", () => simulationEffects.ProcessMessagesSlower()) -}, writer => simulationEffects.WriteState(writer), interactive); +}, writer => simulationEffects.WriteState(writer)); await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj index 7bb828dd..49d226c2 100644 --- a/src/Shipping/Shipping.csproj +++ b/src/Shipping/Shipping.csproj @@ -22,6 +22,9 @@ UserInterface.cs + + DeterministicGuid.cs + \ No newline at end of file From b24a6c547b7d359a75a94617d1f859df9e672690 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Thu, 15 May 2025 17:19:06 -0400 Subject: [PATCH 22/37] Tweaks --- src/Billing/Billing.csproj | 10 +++------- src/ClientUI/ClientUI.csproj | 4 +--- src/MonitoringDemo/DeterministicGuid.cs | 6 ++---- src/MonitoringDemo/MonitoringDemo.csproj | 6 +++--- src/Sales/Sales.csproj | 10 +++------- src/Shipping/Shipping.csproj | 8 ++------ 6 files changed, 14 insertions(+), 30 deletions(-) diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 28b39554..98031c7c 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -14,18 +14,14 @@ - + - - UserInterface.cs - - - DeterministicGuid.cs - + + \ No newline at end of file diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj index 375782e3..ce5b83c8 100644 --- a/src/ClientUI/ClientUI.csproj +++ b/src/ClientUI/ClientUI.csproj @@ -19,9 +19,7 @@ - - UserInterface.cs - + \ No newline at end of file diff --git a/src/MonitoringDemo/DeterministicGuid.cs b/src/MonitoringDemo/DeterministicGuid.cs index bf91fa63..0ff0c309 100644 --- a/src/MonitoringDemo/DeterministicGuid.cs +++ b/src/MonitoringDemo/DeterministicGuid.cs @@ -5,11 +5,9 @@ static class DeterministicGuid { public static Guid Create(params object[] data) { - // use MD5 hash to get a 16-byte hash of the string - using var provider = MD5.Create(); var inputBytes = Encoding.Default.GetBytes(string.Concat(data)); - var hashBytes = provider.ComputeHash(inputBytes); - // generate a guid from the hash: + var hashBytes = MD5.HashData(inputBytes); + return new Guid(hashBytes); } } \ No newline at end of file diff --git a/src/MonitoringDemo/MonitoringDemo.csproj b/src/MonitoringDemo/MonitoringDemo.csproj index 7638f642..8adf54d9 100644 --- a/src/MonitoringDemo/MonitoringDemo.csproj +++ b/src/MonitoringDemo/MonitoringDemo.csproj @@ -10,12 +10,12 @@ - - + - + + \ No newline at end of file diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj index 70b34974..16d1af2e 100644 --- a/src/Sales/Sales.csproj +++ b/src/Sales/Sales.csproj @@ -14,18 +14,14 @@ - + - - UserInterface.cs - - - DeterministicGuid.cs - + + \ No newline at end of file diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj index 49d226c2..0a403d0b 100644 --- a/src/Shipping/Shipping.csproj +++ b/src/Shipping/Shipping.csproj @@ -19,12 +19,8 @@ - - UserInterface.cs - - - DeterministicGuid.cs - + + \ No newline at end of file From 51d02dd09da19573e4066725884299e7ab76fad5 Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Fri, 16 May 2025 13:04:30 +0200 Subject: [PATCH 23/37] Add back shared project and redesign command mapping # Conflicts: # src/Billing/Billing.csproj # src/Billing/Program.cs # src/ClientUI/ClientUI.csproj # src/MonitoringDemo/Program.cs # src/Sales/Program.cs # src/Sales/Sales.csproj # src/Shipping/Program.cs # src/Shipping/Shipping.csproj --- src/Billing/Billing.csproj | 1 + src/Billing/DispatchingProgressBehavior.cs | 26 ------- src/Billing/FailureSimulation.cs | 32 -------- src/Billing/FailureSimulator.cs | 30 -------- src/Billing/OrderPlacedHandler.cs | 10 ++- .../ProcessingMessageProgressBehavior.cs | 24 ------ src/Billing/Program.cs | 16 ++-- src/Billing/ProgressBar.cs | 21 ----- .../RetrievingMessageProgressBehavior.cs | 25 ------ src/Billing/SimulationEffect.cs | 32 -------- src/ClientUI/ClientUI.csproj | 5 +- src/ClientUI/Program.cs | 15 ++-- src/ClientUI/SimulatedCustomers.cs | 64 +++++++++------ src/MonitoringDemo.sln | 6 ++ src/MonitoringDemo/KeyboardController.cs | 11 +++ src/MonitoringDemo/Program.cs | 47 +---------- src/MonitoringDemo/UserInterface.cs | 43 ----------- src/Sales/FailureSimulation.cs | 32 -------- src/Sales/PlaceOrderHandler.cs | 14 +--- src/Sales/Program.cs | 45 ++++++----- src/Sales/Sales.csproj | 6 +- src/Sales/SimulationEffects.cs | 30 -------- src/Shared/ButtonControl.cs | 49 ++++++++++++ .../DatabaseFailureSimulationBehavior.cs | 27 +++++++ src/Shared/DialControl.cs | 69 +++++++++++++++++ .../DispatchingProgressBehavior.cs | 4 +- src/{Sales => Shared}/FailureSimulator.cs | 2 +- src/Shared/IControl.cs | 8 ++ src/Shared/MessageIdHelper.cs | 14 ++++ src/Shared/ProcessingEndpointControls.cs | 72 +++++++++++++++++ .../ProcessingMessageProgressBehavior.cs | 4 +- src/{Sales => Shared}/ProgressBar.cs | 2 +- src/Shared/PropagateManualModeBehavior.cs | 16 ++++ .../RetrievingMessageProgressBehavior.cs | 4 +- src/Shared/SendingEndpointControls.cs | 24 ++++++ src/Shared/Shared.csproj | 12 +++ .../SlowProcessingSimulationBehavior.cs | 28 +++++++ src/Shared/ToggleControl.cs | 67 ++++++++++++++++ src/Shared/UserInterface.cs | 77 +++++++++++++++++++ src/Shipping/OrderBilledHandler.cs | 4 +- src/Shipping/OrderPlacedHandler.cs | 4 +- src/Shipping/Program.cs | 18 +++-- src/Shipping/Shipping.csproj | 6 +- src/Shipping/SimulationEffects.cs | 58 -------------- 44 files changed, 604 insertions(+), 500 deletions(-) delete mode 100644 src/Billing/DispatchingProgressBehavior.cs delete mode 100644 src/Billing/FailureSimulation.cs delete mode 100644 src/Billing/FailureSimulator.cs delete mode 100644 src/Billing/ProcessingMessageProgressBehavior.cs delete mode 100644 src/Billing/ProgressBar.cs delete mode 100644 src/Billing/RetrievingMessageProgressBehavior.cs delete mode 100644 src/Billing/SimulationEffect.cs create mode 100644 src/MonitoringDemo/KeyboardController.cs delete mode 100644 src/MonitoringDemo/UserInterface.cs delete mode 100644 src/Sales/FailureSimulation.cs delete mode 100644 src/Sales/SimulationEffects.cs create mode 100644 src/Shared/ButtonControl.cs create mode 100644 src/Shared/DatabaseFailureSimulationBehavior.cs create mode 100644 src/Shared/DialControl.cs rename src/{Sales => Shared}/DispatchingProgressBehavior.cs (95%) rename src/{Sales => Shared}/FailureSimulator.cs (97%) create mode 100644 src/Shared/IControl.cs create mode 100644 src/Shared/MessageIdHelper.cs create mode 100644 src/Shared/ProcessingEndpointControls.cs rename src/{Sales => Shared}/ProcessingMessageProgressBehavior.cs (86%) rename src/{Sales => Shared}/ProgressBar.cs (95%) create mode 100644 src/Shared/PropagateManualModeBehavior.cs rename src/{Sales => Shared}/RetrievingMessageProgressBehavior.cs (95%) create mode 100644 src/Shared/SendingEndpointControls.cs create mode 100644 src/Shared/Shared.csproj create mode 100644 src/Shared/SlowProcessingSimulationBehavior.cs create mode 100644 src/Shared/ToggleControl.cs create mode 100644 src/Shared/UserInterface.cs delete mode 100644 src/Shipping/SimulationEffects.cs diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 98031c7c..311e4ecc 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Billing/DispatchingProgressBehavior.cs b/src/Billing/DispatchingProgressBehavior.cs deleted file mode 100644 index 63a72d5e..00000000 --- a/src/Billing/DispatchingProgressBehavior.cs +++ /dev/null @@ -1,26 +0,0 @@ -using NServiceBus.Pipeline; -using NServiceBus.Transport; - -namespace Billing; - -public class DispatchingProgressBehavior : Behavior -{ - private FailureSimulator failureSimulator = new(); - - public override async Task Invoke(IBatchDispatchContext context, Func next) - { - var incomingMessage = context.Extensions.Get(); - if (incomingMessage.Headers.ContainsKey("MonitoringDemo.SlowMotion")) - { - Console.WriteLine($"Dispatching outgoing messages {incomingMessage.MessageId}..."); - await failureSimulator.RunInteractive(context.CancellationToken); - } - - await next().ConfigureAwait(false); - } - - public void Failure() - { - failureSimulator.Trigger(); - } -} \ No newline at end of file diff --git a/src/Billing/FailureSimulation.cs b/src/Billing/FailureSimulation.cs deleted file mode 100644 index 8e2d3952..00000000 --- a/src/Billing/FailureSimulation.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Billing; - -public class FailureSimulation -{ - private RetrievingMessageProgressBehavior retrievingMessageProgressBehavior = new RetrievingMessageProgressBehavior(); - private ProcessingMessageProgressBehavior processingMessageProgressBehavior = new ProcessingMessageProgressBehavior(); - private DispatchingProgressBehavior dispatchingMessageProgressBehavior = new DispatchingProgressBehavior(); - - public void Register(EndpointConfiguration endpointConfiguration) - { - endpointConfiguration.Pipeline.Register(retrievingMessageProgressBehavior, "Shows progress of retrieving messages"); - - endpointConfiguration.Pipeline.Register(processingMessageProgressBehavior, "Shows progress of processing messages"); - - endpointConfiguration.Pipeline.Register(dispatchingMessageProgressBehavior, "Shows progress of dispatching messages"); - } - - public void TriggerFailureReceiving() - { - retrievingMessageProgressBehavior.Failure(); - } - - public void TriggerFailureProcessing() - { - processingMessageProgressBehavior.Failure(); - } - - public void TriggerFailureDispatching() - { - dispatchingMessageProgressBehavior.Failure(); - } -} \ No newline at end of file diff --git a/src/Billing/FailureSimulator.cs b/src/Billing/FailureSimulator.cs deleted file mode 100644 index 8643dd06..00000000 --- a/src/Billing/FailureSimulator.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Billing; - -public class FailureSimulator -{ - private bool failureTriggered = false; - -#pragma warning disable PS0003 - public async Task RunInteractive(CancellationToken cancellationToken) -#pragma warning restore PS0003 - { - using var progressBar = new ProgressBar(); - - for (var i = 0; i <= 100; i++) - { - if (failureTriggered) - { - failureTriggered = false; - throw new Exception("Simulated failure"); - } - progressBar.Update(i); - await Task.Delay(25, cancellationToken).ConfigureAwait(false); - } - } - - public void Trigger() - { - //TODO: Use Interlocked - failureTriggered = true; - } -} \ No newline at end of file diff --git a/src/Billing/OrderPlacedHandler.cs b/src/Billing/OrderPlacedHandler.cs index 058bbcc4..edfb94af 100644 --- a/src/Billing/OrderPlacedHandler.cs +++ b/src/Billing/OrderPlacedHandler.cs @@ -1,18 +1,20 @@ using Messages; +using NServiceBus; +using Shared; namespace Billing; -public class OrderPlacedHandler(SimulationEffects simulationEffects) : IHandleMessages +public class OrderPlacedHandler : IHandleMessages { public async Task Handle(OrderPlaced message, IMessageHandlerContext context) { - await simulationEffects.SimulatedMessageProcessing(context.CancellationToken); - var orderBilled = new OrderBilled { OrderId = message.OrderId }; - await context.Publish(orderBilled); + var publishOptions = new PublishOptions(); + publishOptions.SetHumanReadableMessageId(); + await context.Publish(orderBilled, publishOptions); } } \ No newline at end of file diff --git a/src/Billing/ProcessingMessageProgressBehavior.cs b/src/Billing/ProcessingMessageProgressBehavior.cs deleted file mode 100644 index 756f5362..00000000 --- a/src/Billing/ProcessingMessageProgressBehavior.cs +++ /dev/null @@ -1,24 +0,0 @@ -using NServiceBus.Pipeline; - -namespace Billing; - -public class ProcessingMessageProgressBehavior : Behavior -{ - private FailureSimulator failureSimulator = new(); - - public override async Task Invoke(IIncomingLogicalMessageContext context, Func next) - { - if (context.Headers.ContainsKey("MonitoringDemo.SlowMotion")) - { - Console.WriteLine($"Processing message {context.MessageId}..."); - await failureSimulator.RunInteractive(context.CancellationToken); - } - - await next().ConfigureAwait(false); - } - - public void Failure() - { - failureSimulator.Trigger(); - } -} \ No newline at end of file diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index d24de2b7..c15d8b4e 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -50,18 +50,18 @@ endpointConfiguration.UsePersistence(); endpointConfiguration.EnableOutbox(); -var failureSimulation = new FailureSimulation(); +var failureSimulation = new ProcessingEndpointControls(); failureSimulation.Register(endpointConfiguration); -var simulationEffects = new SimulationEffects(); -endpointConfiguration.RegisterComponents(cc => cc.AddSingleton(simulationEffects)); +var ui = new UserInterface(); +failureSimulation.BindSlowProcessingDial(ui, '5', 't'); +failureSimulation.BindDatabaseFailuresDial(ui, '6', 'y'); +failureSimulation.BindFailureReceivingButton(ui, 'v'); +failureSimulation.BindFailureProcessingButton(ui, 'b'); +failureSimulation.BindFailureDispatchingButton(ui, 'n'); var endpointInstance = await Endpoint.Start(endpointConfiguration); -UserInterface.RunLoop(title, new Dictionary -{ - ['w'] = ("increase the simulated failure rate", () => simulationEffects.IncreaseFailureRate()), - ['s'] = ("decrease the simulated failure rate", () => simulationEffects.DecreaseFailureRate()) -}, writer => simulationEffects.WriteState(writer)); +ui.RunLoop("Billing"); await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/Billing/ProgressBar.cs b/src/Billing/ProgressBar.cs deleted file mode 100644 index f04d484f..00000000 --- a/src/Billing/ProgressBar.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Billing; - -public class ProgressBar : IDisposable -{ - private readonly string widgetId = Guid.NewGuid().ToString("N"); - - public ProgressBar() - { - Console.WriteLine($"!BeginWidget Progress {widgetId}"); - } - - public void Update(int percent) - { - Console.WriteLine($"!Widget {widgetId} {percent}"); - } - - public void Dispose() - { - Console.WriteLine($"!EndWidget {widgetId}"); - } -} \ No newline at end of file diff --git a/src/Billing/RetrievingMessageProgressBehavior.cs b/src/Billing/RetrievingMessageProgressBehavior.cs deleted file mode 100644 index 2c56cc33..00000000 --- a/src/Billing/RetrievingMessageProgressBehavior.cs +++ /dev/null @@ -1,25 +0,0 @@ -using NServiceBus.Pipeline; -using Shared; - -namespace Billing; - -public class RetrievingMessageProgressBehavior : Behavior -{ - private FailureSimulator failureSimulator = new(); - - public override async Task Invoke(ITransportReceiveContext context, Func next) - { - if (context.Message.Headers.ContainsKey("MonitoringDemo.SlowMotion")) - { - Console.WriteLine($"Retrieving message {context.Message.MessageId}..."); - await failureSimulator.RunInteractive(context.CancellationToken); - } - - await next().ConfigureAwait(false); - } - - public void Failure() - { - failureSimulator.Trigger(); - } -} \ No newline at end of file diff --git a/src/Billing/SimulationEffect.cs b/src/Billing/SimulationEffect.cs deleted file mode 100644 index 199c518e..00000000 --- a/src/Billing/SimulationEffect.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Billing; - -public class SimulationEffects -{ - public void IncreaseFailureRate() - { - failureRate = Math.Min(1, failureRate + failureRateIncrement); - } - - public void DecreaseFailureRate() - { - failureRate = Math.Max(0, failureRate - failureRateIncrement); - } - - public void WriteState(TextWriter output) - { - output.WriteLine("Failure rate: {0:P0}", failureRate); - } - - public async Task SimulatedMessageProcessing(CancellationToken cancellationToken = default) - { - await Task.Delay(200, cancellationToken); - - if (Random.Shared.NextDouble() < failureRate) - { - throw new Exception("BOOM! A failure occurred"); - } - } - - double failureRate; - const double failureRateIncrement = 0.1; -} \ No newline at end of file diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj index ce5b83c8..cc5be964 100644 --- a/src/ClientUI/ClientUI.csproj +++ b/src/ClientUI/ClientUI.csproj @@ -10,6 +10,7 @@ + @@ -18,8 +19,4 @@ - - - - \ No newline at end of file diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs index 6a6c0bca..f169a532 100644 --- a/src/ClientUI/Program.cs +++ b/src/ClientUI/Program.cs @@ -40,15 +40,16 @@ var simulatedCustomers = new SimulatedCustomers(endpointInstance); var cancellation = new CancellationTokenSource(); -var simulatedWork = simulatedCustomers.Run(cancellation.Token); +var ui = new UserInterface(); +simulatedCustomers.BindSendingRateDial(ui, '-', '['); +simulatedCustomers.BindDuplicateLikelihoodDial(ui, '=', ']'); +simulatedCustomers.BindManualModeToggle(ui, ';'); +simulatedCustomers.BindManualSendButton(ui, '/'); -UserInterface.RunLoop("Load (ClientUI)", new Dictionary -{ - ['c'] = ("toggle High/Low traffic mode", () => simulatedCustomers.ToggleTrafficMode()), - ['v'] = ("toggle manual mode", () => simulatedCustomers.ToggleManualMode()), - ['b'] = ("send message manually", () => simulatedCustomers.SendManually()), -}, writer => simulatedCustomers.WriteState(writer)); +var simulatedWork = simulatedCustomers.Run(cancellation.Token); + +ui.RunLoop("Client UI"); cancellation.Cancel(); diff --git a/src/ClientUI/SimulatedCustomers.cs b/src/ClientUI/SimulatedCustomers.cs index dcb4923e..fef67591 100644 --- a/src/ClientUI/SimulatedCustomers.cs +++ b/src/ClientUI/SimulatedCustomers.cs @@ -1,33 +1,34 @@ using Messages; +using Shared; namespace ClientUI; class SimulatedCustomers(IEndpointInstance endpointInstance) { - private const string Letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - public void WriteState(TextWriter output) + public void BindSendingRateDial(UserInterface userInterface, char upKey, char downKey) { - var trafficMode = manualMode - ? "Manual sending mode" - : highTrafficMode ? $"High traffic mode - sending {rate} orders / second" : $"Low traffic mode - sending {rate} orders / second"; - output.WriteLine(trafficMode); + userInterface.BindDial('B', upKey, downKey, + $"Press {upKey} to increase sending rate or {downKey} to decrease it.", + () => $"Sending rate: {rate}", x => rate = x + 1); //Rate is from 1 to 10 } - public void ToggleTrafficMode() + public void BindDuplicateLikelihoodDial(UserInterface userInterface, char upKey, char downKey) { - highTrafficMode = !highTrafficMode; - rate = highTrafficMode ? 8 : 1; + userInterface.BindDial('C', upKey, downKey, + $"Press {upKey} to increase duplicate message rate or {downKey} to decrease it.", + () => $"Duplicate rate: {duplicateLikelihood * 10}%", x => duplicateLikelihood = x); } - public void ToggleManualMode() + public void BindManualModeToggle(UserInterface userInterface, char toggleKey) { - manualMode = !manualMode; + userInterface.BindToggle('D', toggleKey, $"Press {toggleKey} to toggle manual send mode", + () => manualMode ? "Manual sending mode" : "Automatic sending mode", + () => manualMode = true, () => manualMode = false); } - public void SendManually() + public void BindManualSendButton(UserInterface userInterface, char key) { - manualModeSemaphore.Release(); + userInterface.BindButton('G', key, $"Press {key} to send a message", null, () => manualModeSemaphore.Release()); } public async Task Run(CancellationToken cancellationToken = default) @@ -70,31 +71,50 @@ public async Task Run(CancellationToken cancellationToken = default) } } - Task PlaceSingleOrder(CancellationToken cancellationToken) + async Task PlaceSingleOrder(CancellationToken cancellationToken) { - var messageId = new string(Enumerable.Range(0, 4).Select(x => Letters[Random.Shared.Next(Letters.Length)]).ToArray()); - var placeOrderCommand = new PlaceOrder { OrderId = Guid.NewGuid().ToString() }; - var sendOptions = new SendOptions(); + var sendOptions = await SendOneMessage(cancellationToken, placeOrderCommand); if (manualMode) { - sendOptions.SetHeader("MonitoringDemo.SlowMotion", "True"); + Console.WriteLine($"Message {sendOptions.GetMessageId()} sent."); } - sendOptions.SetMessageId(messageId); - return endpointInstance.Send(placeOrderCommand, sendOptions, cancellationToken); + if (Random.Shared.Next(10) < duplicateLikelihood) + { + //Send a duplicate + await SendOneMessage(cancellationToken, placeOrderCommand); + + if (manualMode) + { + Console.WriteLine($"Duplicate message {sendOptions.GetMessageId()} sent."); + } + } } - bool highTrafficMode; + private async Task SendOneMessage(CancellationToken cancellationToken, PlaceOrder placeOrderCommand) + { + var sendOptions = new SendOptions(); + + if (manualMode) + { + sendOptions.SetHeader("MonitoringDemo.ManualMode", "True"); + } + + sendOptions.SetHumanReadableMessageId(); + await endpointInstance.Send(placeOrderCommand, sendOptions, cancellationToken); + return sendOptions; + } DateTime nextReset; int currentIntervalCount; int rate = 1; + private int duplicateLikelihood; private bool manualMode; private SemaphoreSlim manualModeSemaphore = new SemaphoreSlim(0); } \ No newline at end of file diff --git a/src/MonitoringDemo.sln b/src/MonitoringDemo.sln index 7e7d9f90..b95e5d9b 100644 --- a/src/MonitoringDemo.sln +++ b/src/MonitoringDemo.sln @@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{5EB4D9AD-4AF7-4CED-9F69-36763A31FBE1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +57,10 @@ Global {55C64607-52E9-4E85-A547-50B191856A93}.Debug|Any CPU.Build.0 = Debug|Any CPU {55C64607-52E9-4E85-A547-50B191856A93}.Release|Any CPU.ActiveCfg = Release|Any CPU {55C64607-52E9-4E85-A547-50B191856A93}.Release|Any CPU.Build.0 = Release|Any CPU + {5EB4D9AD-4AF7-4CED-9F69-36763A31FBE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EB4D9AD-4AF7-4CED-9F69-36763A31FBE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EB4D9AD-4AF7-4CED-9F69-36763A31FBE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EB4D9AD-4AF7-4CED-9F69-36763A31FBE1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MonitoringDemo/KeyboardController.cs b/src/MonitoringDemo/KeyboardController.cs new file mode 100644 index 00000000..19ecea36 --- /dev/null +++ b/src/MonitoringDemo/KeyboardController.cs @@ -0,0 +1,11 @@ +namespace MonitoringDemo; + +public class KeyboardDriver +{ + //Controls a process via key bindings. Level controls are bound to up/down key combinations +} + +public class ControllerDriver +{ + //Controls a process via a dedicated USB controller. +} \ No newline at end of file diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index f455e193..f8db01ff 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -21,9 +21,9 @@ ProcessWindow[] windows = []; windows = [ CreateWindow("Platform", "PlatformLauncher", "_Platform", true, cancellationToken), - CreateWindow("Billing", "Billing", "_Billing", false, cancellationToken), - CreateWindow("Shipping", "Shipping", "S_hipping", false, cancellationToken), - CreateWindow("ClientUI", "ClientUI", "_ClientUI", true, cancellationToken), + //CreateWindow("Billing", "Billing", "_Billing", false, cancellationToken), + //CreateWindow("Shipping", "Shipping", "S_hipping", false, cancellationToken), + //CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, cancellationToken), CreateWindow("Sales", "Sales", "_Sales", false, cancellationToken) ]; @@ -98,44 +98,3 @@ ProcessWindow CreateWindow(string title, string name, string menuItemText, bool return processWindow; } - - -// void ScaleSalesEndpointIfRequired(DemoLauncher launcher, TaskCompletionSource syncEvent) -// { -// _ = Task.Run(() => -// { -// try -// { -// Console.WriteLine(); -// Console.WriteLine("Press [up arrow] to scale out the Sales service or [down arrow] to scale in"); -// Console.WriteLine("Press Ctrl+C stop Particular Monitoring Demo."); -// Console.WriteLine(); -// -// while (!tokenSource.IsCancellationRequested) -// { -// var input = Console.ReadKey(true); -// switch (input.Key) -// { -// case ConsoleKey.LeftArrow: -// launcher.ScaleInSales(); -// break; -// case ConsoleKey.RightArrow: -// launcher.ScaleOutSales(); -// break; -// default: -// launcher.Send(new string(input.KeyChar, 1)); -// break; -// } -// } -// } -// catch (OperationCanceledException) -// { -// // ignore -// } -// catch (Exception e) -// { -// // surface any other exception -// syncEvent.TrySetException(e); -// } -// }); -// } \ No newline at end of file diff --git a/src/MonitoringDemo/UserInterface.cs b/src/MonitoringDemo/UserInterface.cs deleted file mode 100644 index 349b1acf..00000000 --- a/src/MonitoringDemo/UserInterface.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Shared; - -public static class UserInterface -{ -#pragma warning disable PS0018 - public static void RunLoop(string title, Dictionary controls, Action reportState) -#pragma warning restore PS0018 - { - Console.Title = title; - - reportState(Console.Out); - PrintControls(controls); - - while (true) - { - var input = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(input)) - { - return; - } - - var key = input[0]; - - if (controls.TryGetValue(char.ToLowerInvariant(key), out var control)) - { - control.Action(); - reportState(Console.Out); - } - else if (key == '?') - { - PrintControls(controls); - } - } - } - private static void PrintControls(Dictionary controls) - { - foreach (var kvp in controls) - { - Console.WriteLine($"Press {char.ToUpperInvariant(kvp.Key)} to {kvp.Value.Message}"); - } - Console.WriteLine("Press ? for help"); - } -} diff --git a/src/Sales/FailureSimulation.cs b/src/Sales/FailureSimulation.cs deleted file mode 100644 index 634a145e..00000000 --- a/src/Sales/FailureSimulation.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Sales; - -public class FailureSimulation -{ - private RetrievingMessageProgressBehavior retrievingMessageProgressBehavior = new RetrievingMessageProgressBehavior(); - private ProcessingMessageProgressBehavior processingMessageProgressBehavior = new ProcessingMessageProgressBehavior(); - private DispatchingProgressBehavior dispatchingMessageProgressBehavior = new DispatchingProgressBehavior(); - - public void Register(EndpointConfiguration endpointConfiguration) - { - endpointConfiguration.Pipeline.Register(retrievingMessageProgressBehavior, "Shows progress of retrieving messages"); - - endpointConfiguration.Pipeline.Register(processingMessageProgressBehavior, "Shows progress of processing messages"); - - endpointConfiguration.Pipeline.Register(dispatchingMessageProgressBehavior, "Shows progress of dispatching messages"); - } - - public void TriggerFailureReceiving() - { - retrievingMessageProgressBehavior.Failure(); - } - - public void TriggerFailureProcessing() - { - processingMessageProgressBehavior.Failure(); - } - - public void TriggerFailureDispatching() - { - dispatchingMessageProgressBehavior.Failure(); - } -} \ No newline at end of file diff --git a/src/Sales/PlaceOrderHandler.cs b/src/Sales/PlaceOrderHandler.cs index 276699aa..23de1041 100644 --- a/src/Sales/PlaceOrderHandler.cs +++ b/src/Sales/PlaceOrderHandler.cs @@ -1,26 +1,20 @@ using Messages; +using NServiceBus; +using Shared; namespace Sales; -public class PlaceOrderHandler(SimulationEffects simulationEffects) : IHandleMessages +public class PlaceOrderHandler : IHandleMessages { public async Task Handle(PlaceOrder message, IMessageHandlerContext context) { - // Simulate the time taken to process a message - await simulationEffects.SimulateMessageProcessing(context.CancellationToken); - var orderPlaced = new OrderPlaced { OrderId = message.OrderId }; var publishOptions = new PublishOptions(); - - if (context.MessageHeaders.ContainsKey("MonitoringDemo.SlowMotion")) - { - publishOptions.SetHeader("MonitoringDemo.SlowMotion", "True"); - } - + publishOptions.SetHumanReadableMessageId(); await context.Publish(orderPlaced, publishOptions); } } \ No newline at end of file diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs index 273d4f62..0132c9dc 100644 --- a/src/Sales/Program.cs +++ b/src/Sales/Program.cs @@ -1,14 +1,24 @@ -using System.Reflection; +using System.Diagnostics; +using System.Reflection; using System.Text.Json; using Messages; -using Microsoft.Extensions.DependencyInjection; -using Sales; using Shared; -var instancePostfix = args.FirstOrDefault(); +var instanceName = args.FirstOrDefault(); -var title = string.IsNullOrEmpty(instancePostfix) ? "Processing (Sales)" : $"Sales - {instancePostfix}"; -var instanceName = string.IsNullOrEmpty(instancePostfix) ? "sales" : $"sales-{instancePostfix}"; +var instanceNumber = args.FirstOrDefault(); +string title; + +if (string.IsNullOrEmpty(instanceNumber)) +{ + title = "Sales"; + + instanceNumber = "original-instance"; +} +else +{ + title = $"Sales - {instanceNumber}"; +} var instanceId = DeterministicGuid.Create("Sales", instanceName); @@ -44,24 +54,23 @@ TimeSpan.FromMilliseconds(500) ); -var failureSimulation = new FailureSimulation(); +var failureSimulation = new ProcessingEndpointControls(); failureSimulation.Register(endpointConfiguration); -var simulationEffects = new SimulationEffects(); -endpointConfiguration.RegisterComponents(cc => cc.AddSingleton(simulationEffects)); - endpointConfiguration.UsePersistence(); endpointConfiguration.EnableOutbox(); +Debugger.Launch(); + +var ui = new UserInterface(); +failureSimulation.BindSlowProcessingDial(ui, '2', 'w'); +failureSimulation.BindDatabaseFailuresDial(ui, '3', 'e'); +failureSimulation.BindFailureReceivingButton(ui, 'z'); +failureSimulation.BindFailureProcessingButton(ui, 'x'); +failureSimulation.BindFailureDispatchingButton(ui, 'c'); + var endpointInstance = await Endpoint.Start(endpointConfiguration); -UserInterface.RunLoop(title, new Dictionary -{ - ['r'] = ("process messages faster", () => simulationEffects.ProcessMessagesFaster()), - ['f'] = ("process messages slower", () => simulationEffects.ProcessMessagesSlower()), - ['t'] = ("simulate failure in retrieving", () => failureSimulation.TriggerFailureReceiving()), - ['y'] = ("simulate failure in processing", () => failureSimulation.TriggerFailureProcessing()), - ['u'] = ("simulate failure in dispatching", () => failureSimulation.TriggerFailureDispatching()) -}, writer => simulationEffects.WriteState(writer)); +ui.RunLoop(title); await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj index 16d1af2e..afce7ebc 100644 --- a/src/Sales/Sales.csproj +++ b/src/Sales/Sales.csproj @@ -10,6 +10,7 @@ + @@ -19,9 +20,4 @@ - - - - - \ No newline at end of file diff --git a/src/Sales/SimulationEffects.cs b/src/Sales/SimulationEffects.cs deleted file mode 100644 index 4dfea0cc..00000000 --- a/src/Sales/SimulationEffects.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Sales; - -public class SimulationEffects -{ - public void WriteState(TextWriter output) - { - output.WriteLine("Base time to handle each order: {0} seconds", baseProcessingTime.TotalSeconds); - } - - public Task SimulateMessageProcessing(CancellationToken cancellationToken = default) - { - return Task.Delay(baseProcessingTime, cancellationToken); - } - - public void ProcessMessagesFaster() - { - if (baseProcessingTime > TimeSpan.Zero) - { - baseProcessingTime -= increment; - } - } - - public void ProcessMessagesSlower() - { - baseProcessingTime += increment; - } - - TimeSpan baseProcessingTime = TimeSpan.FromMilliseconds(1300); - TimeSpan increment = TimeSpan.FromMilliseconds(100); -} \ No newline at end of file diff --git a/src/Shared/ButtonControl.cs b/src/Shared/ButtonControl.cs new file mode 100644 index 00000000..04ea82f2 --- /dev/null +++ b/src/Shared/ButtonControl.cs @@ -0,0 +1,49 @@ +namespace Shared; + +class ButtonControl : IControl +{ + private readonly char inputId; + private readonly char buttonKey; + private readonly string helpMessage; + private readonly string? pressedMessage; + private readonly Action pressedAction; + + public ButtonControl(char inputId, char buttonKey, string helpMessage, string? pressedMessage, Action pressedAction) + { + this.inputId = inputId; + this.buttonKey = buttonKey; + this.helpMessage = helpMessage; + this.pressedMessage = pressedMessage; + this.pressedAction = pressedAction; + } + + public bool Match(string input) + { + if (input[0] == buttonKey) + { + pressedAction(); + return true; + } + + if (input[0] == '~' && input.Length >= 2 && input[1] == inputId) + { + pressedAction(); + return true; + } + + return false; + } + + public void Help(TextWriter textWriter) + { + textWriter.WriteLine(helpMessage); + } + + public void ReportState(TextWriter textWriter) + { + if (pressedMessage != null) + { + textWriter.WriteLine(pressedMessage); + } + } +} \ No newline at end of file diff --git a/src/Shared/DatabaseFailureSimulationBehavior.cs b/src/Shared/DatabaseFailureSimulationBehavior.cs new file mode 100644 index 00000000..cadbfc00 --- /dev/null +++ b/src/Shared/DatabaseFailureSimulationBehavior.cs @@ -0,0 +1,27 @@ +using NServiceBus.Pipeline; + +namespace Shared; + +public class DatabaseFailureSimulationBehavior : Behavior +{ + private int failureLevel = 0; + + public override Task Invoke(IInvokeHandlerContext context, Func next) + { + if (Random.Shared.Next(10) < failureLevel) + { + throw new Exception("Simulated"); + } + return next(); + } + + public void SetFailureLevel(int randomFailureLevel) + { + failureLevel = randomFailureLevel; + } + + public string ReportState() + { + return $"Likelihood of random database failure: {failureLevel * 10}%"; + } +} \ No newline at end of file diff --git a/src/Shared/DialControl.cs b/src/Shared/DialControl.cs new file mode 100644 index 00000000..5b259820 --- /dev/null +++ b/src/Shared/DialControl.cs @@ -0,0 +1,69 @@ +namespace Shared; + +class DialControl : IControl +{ + private int value; + private readonly char inputId; + private readonly char upKey; + private readonly char downKey; + private readonly string helpMessage; + private readonly Func getState; + private readonly Action setAction; + + public DialControl(char inputId, char upKey, char downKey, string helpMessage, Func getState, + Action setAction) + { + this.inputId = inputId; + this.upKey = upKey; + this.downKey = downKey; + this.helpMessage = helpMessage; + this.getState = getState; + this.setAction = setAction; + } + + public bool Match(string input) + { + if (input[0] == upKey) + { + //Increase + if (value < 9) + { + value++; + } + + setAction(value); + return true; + } + + if (input[0] == downKey) + { + //Decrease + if (value > 0) + { + value--; + } + + setAction(value); + return true; + } + + if (input[0] == '~' && input.Length >= 3 && input[1] == inputId) + { + value = int.Parse(input[2].ToString()); + setAction(value); + return true; + } + + return false; + } + + public void Help(TextWriter textWriter) + { + textWriter.WriteLine(helpMessage); + } + + public void ReportState(TextWriter textWriter) + { + textWriter.WriteLine(getState()); + } +} \ No newline at end of file diff --git a/src/Sales/DispatchingProgressBehavior.cs b/src/Shared/DispatchingProgressBehavior.cs similarity index 95% rename from src/Sales/DispatchingProgressBehavior.cs rename to src/Shared/DispatchingProgressBehavior.cs index 3bae23ac..f7ace051 100644 --- a/src/Sales/DispatchingProgressBehavior.cs +++ b/src/Shared/DispatchingProgressBehavior.cs @@ -1,7 +1,7 @@ using NServiceBus.Pipeline; using NServiceBus.Transport; -namespace Sales; +namespace Shared; public class DispatchingProgressBehavior : Behavior { @@ -12,7 +12,7 @@ public override async Task Invoke(IBatchDispatchContext context, Func next await next().ConfigureAwait(false); var incomingMessage = context.Extensions.Get(); - if (incomingMessage.Headers.ContainsKey("MonitoringDemo.SlowMotion")) + if (incomingMessage.Headers.ContainsKey("MonitoringDemo.ManualMode")) { Console.WriteLine($"Dispatching outgoing messages {incomingMessage.MessageId}..."); await failureSimulator.RunInteractive(context.CancellationToken); diff --git a/src/Sales/FailureSimulator.cs b/src/Shared/FailureSimulator.cs similarity index 97% rename from src/Sales/FailureSimulator.cs rename to src/Shared/FailureSimulator.cs index 04f088bd..a100c162 100644 --- a/src/Sales/FailureSimulator.cs +++ b/src/Shared/FailureSimulator.cs @@ -1,4 +1,4 @@ -namespace Sales; +namespace Shared; public class FailureSimulator { diff --git a/src/Shared/IControl.cs b/src/Shared/IControl.cs new file mode 100644 index 00000000..eca11d29 --- /dev/null +++ b/src/Shared/IControl.cs @@ -0,0 +1,8 @@ +namespace Shared; + +public interface IControl +{ + bool Match(string input); + void Help(TextWriter textWriter); + void ReportState(TextWriter textWriter); +} \ No newline at end of file diff --git a/src/Shared/MessageIdHelper.cs b/src/Shared/MessageIdHelper.cs new file mode 100644 index 00000000..42724945 --- /dev/null +++ b/src/Shared/MessageIdHelper.cs @@ -0,0 +1,14 @@ +using NServiceBus.Extensibility; + +namespace Shared; + +public static class MessageIdHelper +{ + private const string Letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + public static void SetHumanReadableMessageId(this ExtendableOptions opts) + { + var messageId = new string(Enumerable.Range(0, 4).Select(x => Letters[Random.Shared.Next(Letters.Length)]).ToArray()); + opts.SetMessageId(messageId); + } +} \ No newline at end of file diff --git a/src/Shared/ProcessingEndpointControls.cs b/src/Shared/ProcessingEndpointControls.cs new file mode 100644 index 00000000..fff0673a --- /dev/null +++ b/src/Shared/ProcessingEndpointControls.cs @@ -0,0 +1,72 @@ +namespace Shared; + +public class ProcessingEndpointControls +{ + private readonly RetrievingMessageProgressBehavior retrievingMessageProgressBehavior = new RetrievingMessageProgressBehavior(); + private readonly ProcessingMessageProgressBehavior processingMessageProgressBehavior = new ProcessingMessageProgressBehavior(); + private readonly DispatchingProgressBehavior dispatchingMessageProgressBehavior = new DispatchingProgressBehavior(); + private readonly SlowProcessingSimulationBehavior slowProcessingSimulationBehavior = new SlowProcessingSimulationBehavior(); + private readonly DatabaseFailureSimulationBehavior databaseFailureSimulationBehavior = new DatabaseFailureSimulationBehavior(); + + public void Register(EndpointConfiguration endpointConfiguration) + { + endpointConfiguration.Pipeline.Register(retrievingMessageProgressBehavior, "Shows progress of retrieving messages"); + endpointConfiguration.Pipeline.Register(processingMessageProgressBehavior, "Shows progress of processing messages"); + endpointConfiguration.Pipeline.Register(dispatchingMessageProgressBehavior, "Shows progress of dispatching messages"); + endpointConfiguration.Pipeline.Register(slowProcessingSimulationBehavior, "Simulates slow processing"); + endpointConfiguration.Pipeline.Register(databaseFailureSimulationBehavior, "Simulates faulty database"); + endpointConfiguration.Pipeline.Register(new PropagateManualModeBehavior(), "Propagates manual mode settings"); + } + + public void BindSlowProcessingDial(UserInterface userInterface, char upKey, char downKey) + { + userInterface.BindDial( + 'B', upKey, downKey, $"Press {upKey} to increase processing delay or {downKey} to decrease it.", + () => slowProcessingSimulationBehavior.ReportState(), + x => slowProcessingSimulationBehavior.SetProcessingDelay(x)); + } + + public void BindDatabaseFailuresDial(UserInterface userInterface, char upKey, char downKey) + { + userInterface.BindDial( + 'C', upKey, downKey, $"Press {upKey} to increase database failure rate or {downKey} to decrease it.", + () => databaseFailureSimulationBehavior.ReportState(), + x => databaseFailureSimulationBehavior.SetFailureLevel(x)); + } + + public void BindDatabaseDownToggle(UserInterface userInterface, char toggleKey) + { + + } + + public void BindOutboxToggle(UserInterface userInterface, char toggleKey) + { + + } + + public void BindAutoThrottleToggle(UserInterface userInterface, char toggleKey) + { + + } + + public void BindFailureReceivingButton(UserInterface userInterface, char key) + { + userInterface.BindButton('G', key, $"Press {key} to trigger failure while receiving a message", + null, + () => retrievingMessageProgressBehavior.Failure()); + } + + public void BindFailureProcessingButton(UserInterface userInterface, char key) + { + userInterface.BindButton('H', key, $"Press {key} to trigger failure while processing a message", + null, + () => processingMessageProgressBehavior.Failure()); + } + + public void BindFailureDispatchingButton(UserInterface userInterface, char key) + { + userInterface.BindButton('I', key, $"Press {key} to trigger failure while dispatching follow-up messages", + null, + () => dispatchingMessageProgressBehavior.Failure()); + } +} \ No newline at end of file diff --git a/src/Sales/ProcessingMessageProgressBehavior.cs b/src/Shared/ProcessingMessageProgressBehavior.cs similarity index 86% rename from src/Sales/ProcessingMessageProgressBehavior.cs rename to src/Shared/ProcessingMessageProgressBehavior.cs index 59f34eee..55cb70b1 100644 --- a/src/Sales/ProcessingMessageProgressBehavior.cs +++ b/src/Shared/ProcessingMessageProgressBehavior.cs @@ -1,6 +1,6 @@ using NServiceBus.Pipeline; -namespace Sales; +namespace Shared; public class ProcessingMessageProgressBehavior : Behavior { @@ -8,7 +8,7 @@ public class ProcessingMessageProgressBehavior : Behavior next) { - if (context.Headers.ContainsKey("MonitoringDemo.SlowMotion")) + if (context.Headers.ContainsKey("MonitoringDemo.ManualMode")) { Console.WriteLine($"Processing message {context.MessageId}..."); await failureSimulator.RunInteractive(context.CancellationToken); diff --git a/src/Sales/ProgressBar.cs b/src/Shared/ProgressBar.cs similarity index 95% rename from src/Sales/ProgressBar.cs rename to src/Shared/ProgressBar.cs index a76b0f9d..ec6d4123 100644 --- a/src/Sales/ProgressBar.cs +++ b/src/Shared/ProgressBar.cs @@ -1,4 +1,4 @@ -namespace Sales; +namespace Shared; public class ProgressBar : IDisposable { diff --git a/src/Shared/PropagateManualModeBehavior.cs b/src/Shared/PropagateManualModeBehavior.cs new file mode 100644 index 00000000..d9aac46a --- /dev/null +++ b/src/Shared/PropagateManualModeBehavior.cs @@ -0,0 +1,16 @@ +using NServiceBus.Pipeline; + +namespace Shared; + +public class PropagateManualModeBehavior : Behavior +{ + public override Task Invoke(IOutgoingLogicalMessageContext context, Func next) + { + if (context.TryGetIncomingPhysicalMessage(out var incomingMessage) + && incomingMessage.Headers.ContainsKey("MonitoringDemo.ManualMode")) + { + context.Headers["MonitoringDemo.ManualMode"] = "True"; + } + return next(); + } +} \ No newline at end of file diff --git a/src/Sales/RetrievingMessageProgressBehavior.cs b/src/Shared/RetrievingMessageProgressBehavior.cs similarity index 95% rename from src/Sales/RetrievingMessageProgressBehavior.cs rename to src/Shared/RetrievingMessageProgressBehavior.cs index fcfea357..64bf27ac 100644 --- a/src/Sales/RetrievingMessageProgressBehavior.cs +++ b/src/Shared/RetrievingMessageProgressBehavior.cs @@ -1,7 +1,7 @@ using NServiceBus.Pipeline; using Shared; -namespace Sales; +namespace Shared; public class RetrievingMessageProgressBehavior : Behavior { @@ -9,7 +9,7 @@ public class RetrievingMessageProgressBehavior : Behavior next) { - if (context.Message.Headers.ContainsKey("MonitoringDemo.SlowMotion")) + if (context.Message.Headers.ContainsKey("MonitoringDemo.ManualMode")) { Console.WriteLine($"Retrieving message {context.Message.MessageId}..."); await failureSimulator.RunInteractive(context.CancellationToken); diff --git a/src/Shared/SendingEndpointControls.cs b/src/Shared/SendingEndpointControls.cs new file mode 100644 index 00000000..12eca6e3 --- /dev/null +++ b/src/Shared/SendingEndpointControls.cs @@ -0,0 +1,24 @@ +namespace Shared; + +public class SendingEndpointControls +{ + public void BindSendingDelayDial(UserInterface userInterface, char upKey, char downKey) + { + + } + + public void BindDuplicateLikelihoodDial(UserInterface userInterface, char upKey, char downKey) + { + + } + + public void BindManualModeToggle(UserInterface userInterface, char toggleKey) + { + + } + + public void BindManualSendButton(UserInterface userInterface, char key) + { + + } +} \ No newline at end of file diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj new file mode 100644 index 00000000..de6902d7 --- /dev/null +++ b/src/Shared/Shared.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + enable + enable + + + + + + diff --git a/src/Shared/SlowProcessingSimulationBehavior.cs b/src/Shared/SlowProcessingSimulationBehavior.cs new file mode 100644 index 00000000..e0cd1088 --- /dev/null +++ b/src/Shared/SlowProcessingSimulationBehavior.cs @@ -0,0 +1,28 @@ +using NServiceBus.Pipeline; + +namespace Shared; + +public class SlowProcessingSimulationBehavior : Behavior +{ + readonly int baseProcessingTime = 1000; + readonly int increment = 100; + private int delayLevel = 0; + + public override async Task Invoke(IInvokeHandlerContext context, Func next) + { + await Task.Delay(Delay, context.CancellationToken); + await next().ConfigureAwait(false); + } + + private TimeSpan Delay => TimeSpan.FromMilliseconds(baseProcessingTime + delayLevel * increment); + + public void SetProcessingDelay(int delayLevel) + { + this.delayLevel = delayLevel; + } + + public string ReportState() + { + return $"Time to process each message: {Delay.TotalSeconds} seconds"; + } +} \ No newline at end of file diff --git a/src/Shared/ToggleControl.cs b/src/Shared/ToggleControl.cs new file mode 100644 index 00000000..23ae6a28 --- /dev/null +++ b/src/Shared/ToggleControl.cs @@ -0,0 +1,67 @@ +namespace Shared; + +class ToggleControl : IControl +{ + private readonly char inputId; + private readonly char toggleKey; + private readonly string helpMessage; + private readonly Func getState; + private readonly Action enableAction; + private readonly Action disableAction; + private bool enabled; + + public ToggleControl(char inputId, char toggleKey, string helpMessage, Func getState, Action enableAction, + Action disableAction) + { + this.inputId = inputId; + this.toggleKey = toggleKey; + this.helpMessage = helpMessage; + this.getState = getState; + this.enableAction = enableAction; + this.disableAction = disableAction; + } + + public bool Match(string input) + { + if (input[0] == toggleKey) + { + enabled = !enabled; + if (enabled) + { + enableAction(); + } + else + { + disableAction(); + } + return true; + } + + if (input[0] == '~' && input.Length >= 3 && input[1] == inputId) + { + var value = int.Parse(input[2].ToString()); + enabled = value == 1; + if (enabled) + { + enableAction(); + } + else + { + disableAction(); + } + return true; + } + + return false; + } + + public void Help(TextWriter textWriter) + { + textWriter.WriteLine(helpMessage); + } + + public void ReportState(TextWriter textWriter) + { + textWriter.WriteLine(getState()); + } +} \ No newline at end of file diff --git a/src/Shared/UserInterface.cs b/src/Shared/UserInterface.cs new file mode 100644 index 00000000..04cc66f3 --- /dev/null +++ b/src/Shared/UserInterface.cs @@ -0,0 +1,77 @@ +namespace Shared; + +public class UserInterface +{ + List controls = []; + + public void BindDial(char inputId, char upKey, char downKey, string helpMessage, Func getState, Action action) + { + controls.Add(new DialControl(inputId, upKey, downKey, helpMessage, getState, action)); + } + + public void BindToggle(char inputId, char toggleKey, string helpMessage, Func getState, Action enableAction, Action disableAction) + { + controls.Add(new ToggleControl(inputId, toggleKey, helpMessage, getState, enableAction, disableAction)); + } + + public void BindButton(char inputId, char buttonKey, string helpMessage, string? pressedMessage, Action pressedAction) + { + controls.Add(new ButtonControl(inputId, buttonKey, helpMessage, pressedMessage, pressedAction)); + } + + +#pragma warning disable PS0018 + public void RunLoop(string title) +#pragma warning restore PS0018 + { + if (!Console.IsInputRedirected) + { + Console.Title = title; + } + + PrintControls(); + + while (true) + { + var input = ReadKeyOrLine(); + if (string.IsNullOrWhiteSpace(input)) + { + return; + } + + if (input == "?") + { + foreach (var ctrl in controls) + { + ctrl.Help(Console.Out); + } + } + + var matchedControl = controls.FirstOrDefault(x => x.Match(input)); + if (matchedControl != null) + { + matchedControl.ReportState(Console.Out); + } + } + } + + private static string? ReadKeyOrLine() + { + if (Console.IsInputRedirected) + { + return Console.ReadLine(); + } + + var key = Console.ReadKey(true); + return new string(key.KeyChar, 1); + } + + private static void PrintControls() + { + //foreach (var kvp in controls) + //{ + // Console.WriteLine($"Press {char.ToUpperInvariant(kvp.Key)} to {kvp.Value.Message}"); + //} + Console.WriteLine("Press ? for help"); + } +} diff --git a/src/Shipping/OrderBilledHandler.cs b/src/Shipping/OrderBilledHandler.cs index 4e8f17d6..0a001419 100644 --- a/src/Shipping/OrderBilledHandler.cs +++ b/src/Shipping/OrderBilledHandler.cs @@ -2,10 +2,10 @@ namespace Shipping; -public class OrderBilledHandler(SimulationEffects simulationEffects) : IHandleMessages +public class OrderBilledHandler : IHandleMessages { public Task Handle(OrderBilled message, IMessageHandlerContext context) { - return simulationEffects.SimulateOrderBilledMessageProcessing(context.CancellationToken); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Shipping/OrderPlacedHandler.cs b/src/Shipping/OrderPlacedHandler.cs index dc65d2f7..60f55861 100644 --- a/src/Shipping/OrderPlacedHandler.cs +++ b/src/Shipping/OrderPlacedHandler.cs @@ -2,10 +2,10 @@ namespace Shipping; -public class OrderPlacedHandler(SimulationEffects simulationEffects) : IHandleMessages +public class OrderPlacedHandler : IHandleMessages { public Task Handle(OrderPlaced message, IMessageHandlerContext context) { - return simulationEffects.SimulateOrderPlacedMessageProcessing(context.CancellationToken); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs index e9f8e195..fd4eb445 100644 --- a/src/Shipping/Program.cs +++ b/src/Shipping/Program.cs @@ -43,16 +43,18 @@ TimeSpan.FromMilliseconds(500) ); -var simulationEffects = new SimulationEffects(); -endpointConfiguration.RegisterComponents(cc => cc.AddSingleton(simulationEffects)); +var failureSimulation = new ProcessingEndpointControls(); +failureSimulation.Register(endpointConfiguration); + +var ui = new UserInterface(); +failureSimulation.BindSlowProcessingDial(ui, '8', 'i'); +failureSimulation.BindDatabaseFailuresDial(ui, '9', 'o'); +failureSimulation.BindFailureReceivingButton(ui, ','); +failureSimulation.BindFailureProcessingButton(ui, '.'); +failureSimulation.BindFailureDispatchingButton(ui, '/'); var endpointInstance = await Endpoint.Start(endpointConfiguration); -UserInterface.RunLoop(title, new Dictionary -{ - ['z'] = ("toggle resource degradation simulation", () => simulationEffects.ToggleDegradationSimulation()), - ['q'] = ("process OrderBilled events faster", () => simulationEffects.ProcessMessagesFaster()), - ['a'] = ("process OrderBilled events slower", () => simulationEffects.ProcessMessagesSlower()) -}, writer => simulationEffects.WriteState(writer)); +ui.RunLoop("Shipping"); await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj index 0a403d0b..ab7eda15 100644 --- a/src/Shipping/Shipping.csproj +++ b/src/Shipping/Shipping.csproj @@ -10,6 +10,7 @@ + @@ -18,9 +19,4 @@ - - - - - \ No newline at end of file diff --git a/src/Shipping/SimulationEffects.cs b/src/Shipping/SimulationEffects.cs deleted file mode 100644 index 5ecc7f25..00000000 --- a/src/Shipping/SimulationEffects.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Shipping; - -public class SimulationEffects -{ - public void WriteState(TextWriter output) - { - output.WriteLine("Base time to handle each OrderBilled event: {0} seconds", baseProcessingTime.TotalSeconds); - - output.Write("Simulated degrading resource: "); - output.WriteLine(degradingResourceSimulationStarted.HasValue ? "ON" : "OFF"); - } - - public Task SimulateOrderBilledMessageProcessing(CancellationToken cancellationToken = default) - { - return Task.Delay(baseProcessingTime, cancellationToken); - } - - public void ProcessMessagesFaster() - { - if (baseProcessingTime > TimeSpan.Zero) - { - baseProcessingTime -= increment; - } - } - - public void ProcessMessagesSlower() - { - baseProcessingTime += increment; - } - - public Task SimulateOrderPlacedMessageProcessing(CancellationToken cancellationToken = default) - { - var delay = TimeSpan.FromMilliseconds(200) + Degradation(); - return Task.Delay(delay, cancellationToken); - } - - public void ToggleDegradationSimulation() - { - degradingResourceSimulationStarted = degradingResourceSimulationStarted.HasValue ? default(DateTime?) : DateTime.UtcNow; - } - - TimeSpan Degradation() - { - var timeSinceDegradationStarted = DateTime.UtcNow - (degradingResourceSimulationStarted ?? DateTime.MaxValue); - if (timeSinceDegradationStarted < TimeSpan.Zero) - { - return TimeSpan.Zero; - } - - return new TimeSpan(timeSinceDegradationStarted.Ticks / degradationRate); - } - - TimeSpan baseProcessingTime = TimeSpan.FromMilliseconds(700); - TimeSpan increment = TimeSpan.FromMilliseconds(100); - - DateTime? degradingResourceSimulationStarted; - const int degradationRate = 5; -} \ No newline at end of file From 75eb50938a91982e25d644a0487401296857d994 Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Fri, 16 May 2025 13:05:33 +0200 Subject: [PATCH 24/37] Fix command wiring and make UI work also when not in the redirected mode # Conflicts: # src/MonitoringDemo/ProcessWindow.cs --- src/ClientUI/SimulatedCustomers.cs | 4 ++-- src/MonitoringDemo/KeyHelper.cs | 16 +++++++++++++ src/MonitoringDemo/ProcessWindow.cs | 19 +++++++-------- src/MonitoringDemo/Program.cs | 8 +++---- src/Sales/Program.cs | 10 +++++--- src/Shared/DispatchingProgressBehavior.cs | 3 +-- src/Shared/FailureSimulator.cs | 4 ++-- src/Shared/ProcessingEndpointControls.cs | 5 ++-- .../ProcessingMessageProgressBehavior.cs | 3 +-- src/Shared/ProgressBar.cs | 24 +++++++++++++++---- .../RetrievingMessageProgressBehavior.cs | 3 +-- src/Shared/UserInterface.cs | 19 ++++++++++----- src/Shipping/Program.cs | 6 ++--- 13 files changed, 82 insertions(+), 42 deletions(-) create mode 100644 src/MonitoringDemo/KeyHelper.cs diff --git a/src/ClientUI/SimulatedCustomers.cs b/src/ClientUI/SimulatedCustomers.cs index fef67591..f8ffa2dd 100644 --- a/src/ClientUI/SimulatedCustomers.cs +++ b/src/ClientUI/SimulatedCustomers.cs @@ -8,14 +8,14 @@ class SimulatedCustomers(IEndpointInstance endpointInstance) public void BindSendingRateDial(UserInterface userInterface, char upKey, char downKey) { userInterface.BindDial('B', upKey, downKey, - $"Press {upKey} to increase sending rate or {downKey} to decrease it.", + $"Press {upKey} to increase sending rate.{Environment.NewLine}Press {downKey} to decrease it.", () => $"Sending rate: {rate}", x => rate = x + 1); //Rate is from 1 to 10 } public void BindDuplicateLikelihoodDial(UserInterface userInterface, char upKey, char downKey) { userInterface.BindDial('C', upKey, downKey, - $"Press {upKey} to increase duplicate message rate or {downKey} to decrease it.", + $"Press {upKey} to increase duplicate message rate.{Environment.NewLine}Press {downKey} to decrease it.", () => $"Duplicate rate: {duplicateLikelihood * 10}%", x => duplicateLikelihood = x); } diff --git a/src/MonitoringDemo/KeyHelper.cs b/src/MonitoringDemo/KeyHelper.cs new file mode 100644 index 00000000..b28a9f86 --- /dev/null +++ b/src/MonitoringDemo/KeyHelper.cs @@ -0,0 +1,16 @@ +using System.Text; +using Terminal.Gui; + +namespace MonitoringDemo; + +static class KeyHelper +{ + private static readonly string AllRecognizedKeys = "1234567890-=qwertyuiop[]asdfghjkl;'zxcvbnm,./"; + private static readonly Rune[] Runes = AllRecognizedKeys.Select(x => (Rune)x).ToArray(); + + public static bool IsRecognized(this Key k) + { + var rune = k.AsRune; + return Runes.Contains(rune); + } +} \ No newline at end of file diff --git a/src/MonitoringDemo/ProcessWindow.cs b/src/MonitoringDemo/ProcessWindow.cs index 1880268d..3899946c 100644 --- a/src/MonitoringDemo/ProcessWindow.cs +++ b/src/MonitoringDemo/ProcessWindow.cs @@ -20,7 +20,7 @@ sealed partial class ProcessWindow : Window private ObservableCollection Instances { get; } = new(); private Dictionary Processes { get; } = new(); - [GeneratedRegex(@"Press (\w) to")] + [GeneratedRegex(@"Press (.) to")] private static partial Regex PressKeyRegex(); [GeneratedRegex(@"!BeginWidget (\w+) (\w+)")] @@ -225,13 +225,12 @@ void PrintOutput(string instance, Process process, CancellationToken cancellatio return; } - var pressKeyMatch = PressKeyRegex().Match(output); - if (pressKeyMatch.Success) - { - var groupValue = pressKeyMatch.Groups[1].Value[0]; - recognizedKeys.Add(groupValue); - recognizedKeys.Add(char.ToLowerInvariant(groupValue)); - } + var pressKeyMatch = PressKeyRegex().Match(output); + if (pressKeyMatch.Success) + { + var groupValue = pressKeyMatch.Groups[1].Value[0]; + recognizedKeys.Add(char.ToLowerInvariant(groupValue)); + } lines.Add(output); LogView.MoveEnd(); // Scroll to end @@ -254,8 +253,8 @@ public void HandleKey(Key e) { var instance = Instances[SelectedInstance]; - var keyChar = (char)e.KeyCode; - if (!recognizedKeys.Contains(keyChar)) + var keyChar = char.ToLower((char)e.KeyCode); + if (recognizedKeys.Contains(keyChar)) { return; } diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index f8db01ff..1f276612 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -21,9 +21,9 @@ ProcessWindow[] windows = []; windows = [ CreateWindow("Platform", "PlatformLauncher", "_Platform", true, cancellationToken), - //CreateWindow("Billing", "Billing", "_Billing", false, cancellationToken), - //CreateWindow("Shipping", "Shipping", "S_hipping", false, cancellationToken), - //CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, cancellationToken), + CreateWindow("Billing", "Billing", "_Billing", false, cancellationToken), + CreateWindow("Shipping", "Shipping", "S_hipping", false, cancellationToken), + CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, cancellationToken), CreateWindow("Sales", "Sales", "_Sales", false, cancellationToken) ]; @@ -52,7 +52,7 @@ void ApplicationKeyDown(object? sender, Key e) { - if (!e.IsKeyCodeAtoZ) + if (!e.IsRecognized()) { return; } diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs index 0132c9dc..5a37369e 100644 --- a/src/Sales/Program.cs +++ b/src/Sales/Program.cs @@ -45,10 +45,16 @@ endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); endpointConfiguration.UniquelyIdentifyRunningInstance() - .UsingCustomDisplayName(instanceName) .UsingCustomIdentifier(instanceId); +if (instanceName != null) +{ + endpointConfiguration.UniquelyIdentifyRunningInstance() + .UsingCustomDisplayName(instanceName); +} + var metrics = endpointConfiguration.EnableMetrics(); + metrics.SendMetricDataToServiceControl( "Particular.Monitoring", TimeSpan.FromMilliseconds(500) @@ -60,8 +66,6 @@ endpointConfiguration.UsePersistence(); endpointConfiguration.EnableOutbox(); -Debugger.Launch(); - var ui = new UserInterface(); failureSimulation.BindSlowProcessingDial(ui, '2', 'w'); failureSimulation.BindDatabaseFailuresDial(ui, '3', 'e'); diff --git a/src/Shared/DispatchingProgressBehavior.cs b/src/Shared/DispatchingProgressBehavior.cs index f7ace051..e76ce37c 100644 --- a/src/Shared/DispatchingProgressBehavior.cs +++ b/src/Shared/DispatchingProgressBehavior.cs @@ -14,8 +14,7 @@ public override async Task Invoke(IBatchDispatchContext context, Func next var incomingMessage = context.Extensions.Get(); if (incomingMessage.Headers.ContainsKey("MonitoringDemo.ManualMode")) { - Console.WriteLine($"Dispatching outgoing messages {incomingMessage.MessageId}..."); - await failureSimulator.RunInteractive(context.CancellationToken); + await failureSimulator.RunInteractive($"Dispatching outgoing messages {incomingMessage.MessageId}...", context.CancellationToken); } } diff --git a/src/Shared/FailureSimulator.cs b/src/Shared/FailureSimulator.cs index a100c162..a4d9deb4 100644 --- a/src/Shared/FailureSimulator.cs +++ b/src/Shared/FailureSimulator.cs @@ -5,10 +5,10 @@ public class FailureSimulator private bool failureTriggered = false; #pragma warning disable PS0003 - public async Task RunInteractive(CancellationToken cancellationToken) + public async Task RunInteractive(string taskDescription, CancellationToken cancellationToken) #pragma warning restore PS0003 { - using var progressBar = new ProgressBar(); + using var progressBar = new ProgressBar(taskDescription); for (var i = 0; i <= 100; i++) { diff --git a/src/Shared/ProcessingEndpointControls.cs b/src/Shared/ProcessingEndpointControls.cs index fff0673a..4a9510a4 100644 --- a/src/Shared/ProcessingEndpointControls.cs +++ b/src/Shared/ProcessingEndpointControls.cs @@ -21,7 +21,8 @@ public void Register(EndpointConfiguration endpointConfiguration) public void BindSlowProcessingDial(UserInterface userInterface, char upKey, char downKey) { userInterface.BindDial( - 'B', upKey, downKey, $"Press {upKey} to increase processing delay or {downKey} to decrease it.", + 'B', upKey, downKey, + $"Press {upKey} to increase processing delay.{Environment.NewLine}Press {downKey} to increase it.", () => slowProcessingSimulationBehavior.ReportState(), x => slowProcessingSimulationBehavior.SetProcessingDelay(x)); } @@ -29,7 +30,7 @@ public void BindSlowProcessingDial(UserInterface userInterface, char upKey, char public void BindDatabaseFailuresDial(UserInterface userInterface, char upKey, char downKey) { userInterface.BindDial( - 'C', upKey, downKey, $"Press {upKey} to increase database failure rate or {downKey} to decrease it.", + 'C', upKey, downKey, $"Press {upKey} to increase database failure rate.{Environment.NewLine}Press {downKey} to decrease it.", () => databaseFailureSimulationBehavior.ReportState(), x => databaseFailureSimulationBehavior.SetFailureLevel(x)); } diff --git a/src/Shared/ProcessingMessageProgressBehavior.cs b/src/Shared/ProcessingMessageProgressBehavior.cs index 55cb70b1..853a327e 100644 --- a/src/Shared/ProcessingMessageProgressBehavior.cs +++ b/src/Shared/ProcessingMessageProgressBehavior.cs @@ -10,8 +10,7 @@ public override async Task Invoke(IIncomingLogicalMessageContext context, Func n { if (context.Message.Headers.ContainsKey("MonitoringDemo.ManualMode")) { - Console.WriteLine($"Retrieving message {context.Message.MessageId}..."); - await failureSimulator.RunInteractive(context.CancellationToken); + await failureSimulator.RunInteractive($"Retrieving message {context.Message.MessageId}...", context.CancellationToken); } await next().ConfigureAwait(false); diff --git a/src/Shared/UserInterface.cs b/src/Shared/UserInterface.cs index 04cc66f3..fe3bacc5 100644 --- a/src/Shared/UserInterface.cs +++ b/src/Shared/UserInterface.cs @@ -66,12 +66,19 @@ public void RunLoop(string title) return new string(key.KeyChar, 1); } - private static void PrintControls() + private void PrintControls() { - //foreach (var kvp in controls) - //{ - // Console.WriteLine($"Press {char.ToUpperInvariant(kvp.Key)} to {kvp.Value.Message}"); - //} - Console.WriteLine("Press ? for help"); + foreach (var ctrl in controls) + { + ctrl.Help(Console.Out); + } + if (!Console.IsInputRedirected) + { + Console.WriteLine("Press ? for help"); + } + else + { + Console.WriteLine("Press F1 for help"); + } } } diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs index fd4eb445..cb38a643 100644 --- a/src/Shipping/Program.cs +++ b/src/Shipping/Program.cs @@ -49,9 +49,9 @@ var ui = new UserInterface(); failureSimulation.BindSlowProcessingDial(ui, '8', 'i'); failureSimulation.BindDatabaseFailuresDial(ui, '9', 'o'); -failureSimulation.BindFailureReceivingButton(ui, ','); -failureSimulation.BindFailureProcessingButton(ui, '.'); -failureSimulation.BindFailureDispatchingButton(ui, '/'); +failureSimulation.BindFailureReceivingButton(ui, 'm'); +failureSimulation.BindFailureProcessingButton(ui, ','); +failureSimulation.BindFailureDispatchingButton(ui, '.'); var endpointInstance = await Endpoint.Start(endpointConfiguration); From cf2bb8f215c23c0ca0898db37b3bb7eec05408c3 Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Fri, 16 May 2025 13:06:34 +0200 Subject: [PATCH 25/37] Improvements # Conflicts: # src/MonitoringDemo/ProcessWindow.cs --- src/ClientUI/Program.cs | 5 +++- src/MonitoringDemo/KeyHelper.cs | 40 +++++++++++++++++++++++++++++ src/MonitoringDemo/ProcessWindow.cs | 23 ++++++++++++----- src/MonitoringDemo/Program.cs | 38 ++++++++++++++++++++++----- src/Shared/ButtonControl.cs | 2 +- src/Shared/DialControl.cs | 2 +- src/Shared/ToggleControl.cs | 2 +- 7 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs index f169a532..dd21a2e7 100644 --- a/src/ClientUI/Program.cs +++ b/src/ClientUI/Program.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics; +using System.Reflection; using System.Text.Json; using ClientUI; using Messages; @@ -36,6 +37,8 @@ routing.RouteToEndpoint(typeof(PlaceOrder), "Sales"); +//Debugger.Launch(); + var endpointInstance = await Endpoint.Start(endpointConfiguration); var simulatedCustomers = new SimulatedCustomers(endpointInstance); diff --git a/src/MonitoringDemo/KeyHelper.cs b/src/MonitoringDemo/KeyHelper.cs index b28a9f86..bf557ac1 100644 --- a/src/MonitoringDemo/KeyHelper.cs +++ b/src/MonitoringDemo/KeyHelper.cs @@ -7,10 +7,50 @@ static class KeyHelper { private static readonly string AllRecognizedKeys = "1234567890-=qwertyuiop[]asdfghjkl;'zxcvbnm,./"; private static readonly Rune[] Runes = AllRecognizedKeys.Select(x => (Rune)x).ToArray(); + private static readonly Rune dollarRune = (Rune)'$'; public static bool IsRecognized(this Key k) { var rune = k.AsRune; return Runes.Contains(rune); } + + public static bool IsPartOfControllerSequence(this Key k, out string? sequence) + { + var r = k.AsRune; + if (r.Value == 0) + { + sequence = null; + return false; + } + if (r == dollarRune) + { + //Begin a new sequence regardless + currentSequence = "$"; + sequence = null; + return true; + } + if (currentSequence != null) + { + currentSequence += r.ToString(); + + if (currentSequence.Length == 4) + { + //Sequence is complete + sequence = currentSequence; + currentSequence = null; + } + else + { + sequence = null; + } + return true; + } + + sequence = null; + return false; + } + + + private static string? currentSequence; } \ No newline at end of file diff --git a/src/MonitoringDemo/ProcessWindow.cs b/src/MonitoringDemo/ProcessWindow.cs index 3899946c..0d347192 100644 --- a/src/MonitoringDemo/ProcessWindow.cs +++ b/src/MonitoringDemo/ProcessWindow.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.ObjectModel; +using System.Text; using System.Text.RegularExpressions; using Terminal.Gui; using Window = Terminal.Gui.Window; @@ -14,7 +15,7 @@ sealed partial class ProcessWindow : Window private readonly DemoLauncher launcher; private readonly ConcurrentDictionary> linesPerInstance = new(); - private readonly HashSet recognizedKeys = new(); + private readonly Dictionary recognizedKeys = new(); public ListView? InstanceView { get; } public ListView LogView { get; } private ObservableCollection Instances { get; } = new(); @@ -229,7 +230,8 @@ void PrintOutput(string instance, Process process, CancellationToken cancellatio if (pressKeyMatch.Success) { var groupValue = pressKeyMatch.Groups[1].Value[0]; - recognizedKeys.Add(char.ToLowerInvariant(groupValue)); + var c = char.ToLowerInvariant(groupValue); + recognizedKeys[(Rune)c] = c; } lines.Add(output); @@ -249,12 +251,19 @@ void PrintOutput(string instance, Process process, CancellationToken cancellatio return widgetName == "Progress" ? new ProgressBarWidget() : null; } + public void HandleSequence(string sequenceWithoutDollar) + { + foreach (var handle in Handles.Values) + { + handle.Send($"${sequenceWithoutDollar}"); + } + } + public void HandleKey(Key e) { var instance = Instances[SelectedInstance]; - - var keyChar = char.ToLower((char)e.KeyCode); - if (recognizedKeys.Contains(keyChar)) + var r = e.AsRune; + if (recognizedKeys.TryGetValue(r, out var c)) { return; } @@ -264,12 +273,12 @@ public void HandleKey(Key e) { foreach (var handle in Processes.Values) { - handle.Send(new string(keyChar, 1)); + handle.Send(new string(c, 1)); } } else { - Processes[instance].Send(new string(keyChar, 1)); + Processes[instance].Send(new string(c, 1)); } e.Handled = true; diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index 1f276612..c5ba1798 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -1,4 +1,5 @@ -using MonitoringDemo; +using System.Diagnostics; +using MonitoringDemo; using System.Reflection.Metadata; using Terminal.Gui; @@ -19,11 +20,12 @@ var menuBarItems = new List(); ProcessWindow[] windows = []; +var clientWindow = CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, cancellationToken); windows = [ CreateWindow("Platform", "PlatformLauncher", "_Platform", true, cancellationToken), CreateWindow("Billing", "Billing", "_Billing", false, cancellationToken), CreateWindow("Shipping", "Shipping", "S_hipping", false, cancellationToken), - CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, cancellationToken), + clientWindow, CreateWindow("Sales", "Sales", "_Sales", false, cancellationToken) ]; @@ -52,17 +54,39 @@ void ApplicationKeyDown(object? sender, Key e) { - if (!e.IsRecognized()) + if (e.IsCtrl) { + //Do not forward ctrl return; } - foreach (var processWindow in windows) + if (e.IsPartOfControllerSequence(out var seq)) { - processWindow.HandleKey(e); - if (e.Handled) + e.Handled = true; + if (seq != null) { - break; + Debug.WriteLine(seq); + if (seq[1] == '1') + { + //First controller is always wired to Client + clientWindow.HandleSequence(seq.Substring(2)); + } + else + { + var visibleWindow = windows.FirstOrDefault(x => x.Visible); + visibleWindow?.HandleSequence(seq.Substring(2)); + } + } + } + else + { + foreach (var processWindow in windows) + { + processWindow.HandleKey(e); + if (e.Handled) + { + break; + } } } } diff --git a/src/Shared/ButtonControl.cs b/src/Shared/ButtonControl.cs index 04ea82f2..3045119b 100644 --- a/src/Shared/ButtonControl.cs +++ b/src/Shared/ButtonControl.cs @@ -25,7 +25,7 @@ public bool Match(string input) return true; } - if (input[0] == '~' && input.Length >= 2 && input[1] == inputId) + if (input[0] == '$' && input.Length >= 2 && input[1] == inputId) { pressedAction(); return true; diff --git a/src/Shared/DialControl.cs b/src/Shared/DialControl.cs index 5b259820..d4523b0b 100644 --- a/src/Shared/DialControl.cs +++ b/src/Shared/DialControl.cs @@ -47,7 +47,7 @@ public bool Match(string input) return true; } - if (input[0] == '~' && input.Length >= 3 && input[1] == inputId) + if (input[0] == '$' && input.Length >= 3 && input[1] == inputId) { value = int.Parse(input[2].ToString()); setAction(value); diff --git a/src/Shared/ToggleControl.cs b/src/Shared/ToggleControl.cs index 23ae6a28..836658b8 100644 --- a/src/Shared/ToggleControl.cs +++ b/src/Shared/ToggleControl.cs @@ -37,7 +37,7 @@ public bool Match(string input) return true; } - if (input[0] == '~' && input.Length >= 3 && input[1] == inputId) + if (input[0] == '$' && input.Length >= 3 && input[1] == inputId) { var value = int.Parse(input[2].ToString()); enabled = value == 1; From b803a024b09cfce283a657913363ea752df903f2 Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Fri, 16 May 2025 13:23:41 +0200 Subject: [PATCH 26/37] Fix rebase --- src/Billing/Billing.csproj | 5 ---- src/MonitoringDemo/ProcessWindow.cs | 4 +-- src/Sales/Program.cs | 27 ++++--------------- .../DeterministicGuid.cs | 2 +- 4 files changed, 8 insertions(+), 30 deletions(-) rename src/{MonitoringDemo => Shared}/DeterministicGuid.cs (88%) diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 311e4ecc..650f7f46 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -20,9 +20,4 @@ - - - - - \ No newline at end of file diff --git a/src/MonitoringDemo/ProcessWindow.cs b/src/MonitoringDemo/ProcessWindow.cs index 0d347192..e4cfec36 100644 --- a/src/MonitoringDemo/ProcessWindow.cs +++ b/src/MonitoringDemo/ProcessWindow.cs @@ -253,7 +253,7 @@ void PrintOutput(string instance, Process process, CancellationToken cancellatio public void HandleSequence(string sequenceWithoutDollar) { - foreach (var handle in Handles.Values) + foreach (var handle in Processes.Values) { handle.Send($"${sequenceWithoutDollar}"); } @@ -263,7 +263,7 @@ public void HandleKey(Key e) { var instance = Instances[SelectedInstance]; var r = e.AsRune; - if (recognizedKeys.TryGetValue(r, out var c)) + if (!recognizedKeys.TryGetValue(r, out var c)) { return; } diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs index 5a37369e..28185514 100644 --- a/src/Sales/Program.cs +++ b/src/Sales/Program.cs @@ -4,21 +4,9 @@ using Messages; using Shared; -var instanceName = args.FirstOrDefault(); - -var instanceNumber = args.FirstOrDefault(); -string title; - -if (string.IsNullOrEmpty(instanceNumber)) -{ - title = "Sales"; - - instanceNumber = "original-instance"; -} -else -{ - title = $"Sales - {instanceNumber}"; -} +var instancePostfix = args.FirstOrDefault(); +var title = string.IsNullOrEmpty(instancePostfix) ? "Processing (Sales)" : $"Sales - {instancePostfix}"; +var instanceName = string.IsNullOrEmpty(instancePostfix) ? "sales" : $"sales-{instancePostfix}"; var instanceId = DeterministicGuid.Create("Sales", instanceName); @@ -45,13 +33,8 @@ endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); endpointConfiguration.UniquelyIdentifyRunningInstance() - .UsingCustomIdentifier(instanceId); - -if (instanceName != null) -{ - endpointConfiguration.UniquelyIdentifyRunningInstance() - .UsingCustomDisplayName(instanceName); -} + .UsingCustomIdentifier(instanceId) + .UsingCustomDisplayName(instanceName); var metrics = endpointConfiguration.EnableMetrics(); diff --git a/src/MonitoringDemo/DeterministicGuid.cs b/src/Shared/DeterministicGuid.cs similarity index 88% rename from src/MonitoringDemo/DeterministicGuid.cs rename to src/Shared/DeterministicGuid.cs index 0ff0c309..92226822 100644 --- a/src/MonitoringDemo/DeterministicGuid.cs +++ b/src/Shared/DeterministicGuid.cs @@ -1,7 +1,7 @@ using System.Security.Cryptography; using System.Text; -static class DeterministicGuid +public static class DeterministicGuid { public static Guid Create(params object[] data) { From 217905f95f269be708924b704e0f05a92f31d40c Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Mon, 19 May 2025 12:24:21 +0200 Subject: [PATCH 27/37] Scale out client UI --- src/ClientUI/Program.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs index dd21a2e7..5d50e5d4 100644 --- a/src/ClientUI/Program.cs +++ b/src/ClientUI/Program.cs @@ -5,6 +5,12 @@ using Messages; using Shared; +var instancePostfix = args.FirstOrDefault(); + +var title = string.IsNullOrEmpty(instancePostfix) ? "ClientUI" : $"ClientUI - {instancePostfix}"; +var instanceName = string.IsNullOrEmpty(instancePostfix) ? "clientui" : $"clientui-{instancePostfix}"; +var instanceId = DeterministicGuid.Create("ClientUI", instanceName); + var endpointConfiguration = new EndpointConfiguration("ClientUI"); var serializer = endpointConfiguration.UseSerialization(); @@ -26,8 +32,8 @@ endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); endpointConfiguration.UniquelyIdentifyRunningInstance() - .UsingCustomIdentifier(new Guid("EA3E7D1B-8171-4098-B160-1FEA975CCB2C")) - .UsingCustomDisplayName("original-instance"); + .UsingCustomIdentifier(instanceId) + .UsingCustomDisplayName(instanceName); var metrics = endpointConfiguration.EnableMetrics(); metrics.SendMetricDataToServiceControl( From 0f4633dba18e35ac0cf92b2570a6f6e0e268fdd2 Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Mon, 19 May 2025 12:25:02 +0200 Subject: [PATCH 28/37] Bind scale out to controller box --- src/MonitoringDemo/ProcessWindow.cs | 95 +++++++++++++++++++++++------ src/MonitoringDemo/Program.cs | 15 +++-- src/Shipping/Program.cs | 3 +- 3 files changed, 89 insertions(+), 24 deletions(-) diff --git a/src/MonitoringDemo/ProcessWindow.cs b/src/MonitoringDemo/ProcessWindow.cs index e4cfec36..b1708364 100644 --- a/src/MonitoringDemo/ProcessWindow.cs +++ b/src/MonitoringDemo/ProcessWindow.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; using Terminal.Gui; @@ -12,7 +13,9 @@ sealed partial class ProcessWindow : Window private const string Letters = "abcdefghijklmnopqrstuvwxyz"; private readonly string name; + private readonly bool singleInstance; private readonly DemoLauncher launcher; + private readonly CancellationToken cancellationToken; private readonly ConcurrentDictionary> linesPerInstance = new(); private readonly Dictionary recognizedKeys = new(); @@ -36,7 +39,9 @@ sealed partial class ProcessWindow : Window public ProcessWindow(string title, string name, bool singleInstance, DemoLauncher launcher, CancellationToken cancellationToken) { this.name = name; + this.singleInstance = singleInstance; this.launcher = launcher; + this.cancellationToken = cancellationToken; Title = title; X = 0; @@ -107,17 +112,12 @@ public ProcessWindow(string title, string name, bool singleInstance, DemoLaunche }); AddCommand(Command.Up, () => { - var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - StartNewProcess(cancellationTokenSource); + ScaleOut(); return true; }); AddCommand(Command.Down, () => { - var instance = Instances[SelectedInstance]; - Processes.Remove(instance, out var process); - process!.Dispose(); - Instances.Remove(instance); - linesPerInstance.TryRemove(instance, out _); + ScaleIn(); return true; }); @@ -133,6 +133,54 @@ public ProcessWindow(string title, string name, bool singleInstance, DemoLaunche StartNewProcess(CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)); } + private void ScaleIn() + { + var instance = Instances[SelectedInstance]; + DoScaleIn(instance); + } + + private void ScaleInLast() + { + var instance = Instances.LastOrDefault(); + if (instance == null) + { + return; + } + DoScaleIn(instance); + } + + private void DoScaleIn(string instance) + { + Debug.WriteLine($"Stopping instance {instance}"); + + Processes.Remove(instance, out var process); + process!.Dispose(); + Instances.Remove(instance); + linesPerInstance.TryRemove(instance, out _); + } + + private void ScaleOut() + { + Debug.WriteLine($"Starting new instance."); + + var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + StartNewProcess(cancellationTokenSource); + } + + private void ScaleTo(int value) + { + var numberOfInstances = (value / 2) + 1; //Value is 0-9. Make it min one instance, max 5 instances + Debug.WriteLine($"Scaling to {numberOfInstances}."); + while (Instances.Count < numberOfInstances) + { + ScaleOut(); + } + while (Instances.Count > numberOfInstances) + { + ScaleInLast(); + } + } + int SelectedInstance => Math.Max(InstanceView?.SelectedItem ?? 0, 0); private void InstanceView_SelectedItemChanged(object? sender, ListViewItemEventArgs args) @@ -226,13 +274,13 @@ void PrintOutput(string instance, Process process, CancellationToken cancellatio return; } - var pressKeyMatch = PressKeyRegex().Match(output); - if (pressKeyMatch.Success) - { - var groupValue = pressKeyMatch.Groups[1].Value[0]; - var c = char.ToLowerInvariant(groupValue); - recognizedKeys[(Rune)c] = c; - } + var pressKeyMatch = PressKeyRegex().Match(output); + if (pressKeyMatch.Success) + { + var groupValue = pressKeyMatch.Groups[1].Value[0]; + var c = char.ToLowerInvariant(groupValue); + recognizedKeys[(Rune)c] = c; + } lines.Add(output); LogView.MoveEnd(); // Scroll to end @@ -253,9 +301,21 @@ void PrintOutput(string instance, Process process, CancellationToken cancellatio public void HandleSequence(string sequenceWithoutDollar) { - foreach (var handle in Processes.Values) + if (sequenceWithoutDollar is ['A', _, ..]) { - handle.Send($"${sequenceWithoutDollar}"); + if (!singleInstance) + { + //First dial is scale out + var scaleFactor = int.Parse(sequenceWithoutDollar.Substring(1, 1)); + ScaleTo(scaleFactor); + } + } + else + { + foreach (var handle in Processes.Values) + { + handle.Send($"${sequenceWithoutDollar}"); + } } } @@ -292,7 +352,8 @@ public void Send(string value) handle.Send(value); } - public IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken = default) { + public IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken = default) + { return handle.ReadAllAsync(cancellationToken); } diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index c5ba1798..4b1b5342 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -21,12 +21,17 @@ ProcessWindow[] windows = []; var clientWindow = CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, cancellationToken); +var platformWindow = CreateWindow("Platform", "PlatformLauncher", "_Platform", true, cancellationToken); +var billingWindow = CreateWindow("Billing", "Billing", "_Billing", false, cancellationToken); +var shippingWindow = CreateWindow("Shipping", "Shipping", "S_hipping", false, cancellationToken); +var salesWindow = CreateWindow("Sales", "Sales", "_Sales", false, cancellationToken); + windows = [ - CreateWindow("Platform", "PlatformLauncher", "_Platform", true, cancellationToken), - CreateWindow("Billing", "Billing", "_Billing", false, cancellationToken), - CreateWindow("Shipping", "Shipping", "S_hipping", false, cancellationToken), + platformWindow, + billingWindow, + shippingWindow, clientWindow, - CreateWindow("Sales", "Sales", "_Sales", false, cancellationToken) + salesWindow ]; menuBarItems.Add( @@ -73,7 +78,7 @@ void ApplicationKeyDown(object? sender, Key e) } else { - var visibleWindow = windows.FirstOrDefault(x => x.Visible); + var visibleWindow = windows.FirstOrDefault(x => x.Focused != null); visibleWindow?.HandleSequence(seq.Substring(2)); } } diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs index cb38a643..47785580 100644 --- a/src/Shipping/Program.cs +++ b/src/Shipping/Program.cs @@ -9,7 +9,6 @@ var title = string.IsNullOrEmpty(instancePostfix) ? "Processing (Shipping)" : $"Shipping - {instancePostfix}"; var instanceName = string.IsNullOrEmpty(instancePostfix) ? "shipping" : $"shipping-{instancePostfix}"; - var instanceId = DeterministicGuid.Create("Shipping", instanceName); var endpointConfiguration = new EndpointConfiguration("Shipping"); @@ -55,6 +54,6 @@ var endpointInstance = await Endpoint.Start(endpointConfiguration); -ui.RunLoop("Shipping"); +ui.RunLoop(title); await endpointInstance.Stop(); \ No newline at end of file From 9c1faaea1444fb3a060d20506493810d8bc1e62d Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Mon, 19 May 2025 19:07:32 +0200 Subject: [PATCH 29/37] Some more fixes and controller bindings --- Box1.joystick.amgp | 345 ++++++++++++++++++++++++++++ Box2.joystick.amgp | 345 ++++++++++++++++++++++++++++ src/Billing/Program.cs | 9 +- src/ClientUI/Program.cs | 8 +- src/MonitoringDemo/DemoLauncher.cs | 4 +- src/MonitoringDemo/ProcessGroup.cs | 10 +- src/MonitoringDemo/ProcessWindow.cs | 43 +++- src/MonitoringDemo/Program.cs | 14 +- src/Sales/Program.cs | 6 + src/Shared/OpenTelemetryUtils.cs | 31 +++ src/Shared/Shared.csproj | 1 + src/Shipping/Program.cs | 9 +- 12 files changed, 804 insertions(+), 21 deletions(-) create mode 100644 Box1.joystick.amgp create mode 100644 Box2.joystick.amgp create mode 100644 src/Shared/OpenTelemetryUtils.cs diff --git a/Box1.joystick.amgp b/Box1.joystick.amgp new file mode 100644 index 00000000..eae5caa8 --- /dev/null +++ b/Box1.joystick.amgp @@ -0,0 +1,345 @@ + + + + Button box + + 0300ac06c0160000dc27000000000000582410204 + + + + 1000 + positive + + + + $1B0 + textentry + + + 10 + distance + + + $1B1 + textentry + + + 10 + distance + + + $1B2 + textentry + + + 10 + distance + + + $1B3 + textentry + + + 10 + distance + + + $1B4 + textentry + + + 10 + distance + + + $1B5 + textentry + + + 10 + distance + + + $1B6 + textentry + + + 10 + distance + + + $1B7 + textentry + + + 10 + distance + + + $1B8 + textentry + + + 10 + distance + + + $1B9 + textentry + + + + + + 1000 + positive + + + + $1A0 + textentry + + + 10 + distance + + + $1A1 + textentry + + + 10 + distance + + + $1A2 + textentry + + + 10 + distance + + + $1A3 + textentry + + + 10 + distance + + + $1A4 + textentry + + + 10 + distance + + + $1A5 + textentry + + + 10 + distance + + + $1A6 + textentry + + + 10 + distance + + + $1A7 + textentry + + + 10 + distance + + + $1A8 + textentry + + + 10 + distance + + + $1A9 + textentry + + + + + + normal + + + normal + + + normal + + + 1000 + positive + + + + $1C0 + textentry + + + 10 + distance + + + $1C1 + textentry + + + 10 + distance + + + $1C2 + textentry + + + 10 + distance + + + $1C3 + textentry + + + 10 + distance + + + $1C4 + textentry + + + 10 + distance + + + $1C5 + textentry + + + 10 + distance + + + $1C6 + textentry + + + 10 + distance + + + $1C7 + textentry + + + 10 + distance + + + $1C8 + textentry + + + 10 + distance + + + $1C9 + textentry + + + + + + + + + + + + + diff --git a/Box2.joystick.amgp b/Box2.joystick.amgp new file mode 100644 index 00000000..c8784cdc --- /dev/null +++ b/Box2.joystick.amgp @@ -0,0 +1,345 @@ + + + + Button box + + 0300ac06c0160000dc27000000000000582410204 + + + + 0 + positive + + + + $2B0 + textentry + + + 2 + distance + + + $2B1 + textentry + + + 3 + distance + + + $2B2 + textentry + + + 3 + distance + + + $2B3 + textentry + + + 3 + distance + + + $2B4 + textentry + + + 4 + distance + + + $2B5 + textentry + + + 10 + distance + + + $2B6 + textentry + + + 15 + distance + + + $2B7 + textentry + + + 20 + distance + + + $2B8 + textentry + + + 20 + distance + + + $2B9 + textentry + + + + + + 1000 + positive + + + + $2A0 + textentry + + + 10 + distance + + + $2A1 + textentry + + + 10 + distance + + + $2A2 + textentry + + + 10 + distance + + + $2A3 + textentry + + + 10 + distance + + + $2A4 + textentry + + + 10 + distance + + + $2A5 + textentry + + + 10 + distance + + + $2A6 + textentry + + + 10 + distance + + + $2A7 + textentry + + + 10 + distance + + + $2A8 + textentry + + + 10 + distance + + + $2A9 + textentry + + + + + + normal + + + normal + + + normal + + + 1000 + positive + + + + $2C0 + textentry + + + 10 + distance + + + $2C1 + textentry + + + 10 + distance + + + $2C2 + textentry + + + 10 + distance + + + $2C3 + textentry + + + 10 + distance + + + $2C4 + textentry + + + 10 + distance + + + $2C5 + textentry + + + 10 + distance + + + $2C6 + textentry + + + 10 + distance + + + $2C7 + textentry + + + 10 + distance + + + $2C8 + textentry + + + 10 + distance + + + $2C9 + textentry + + + + + + + + + + + + + diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index c15d8b4e..b7ac454b 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -9,8 +9,8 @@ var title = string.IsNullOrEmpty(instancePostfix) ? "Failure rate (Billing)" : $"Billing - {instancePostfix}"; var instanceName = string.IsNullOrEmpty(instancePostfix) ? "billing" : $"billing-{instancePostfix}"; - var instanceId = DeterministicGuid.Create("Billing", instanceName); +var prometheusPortString = args.Skip(1).FirstOrDefault(); var endpointConfiguration = new EndpointConfiguration("Billing"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); @@ -60,8 +60,13 @@ failureSimulation.BindFailureProcessingButton(ui, 'b'); failureSimulation.BindFailureDispatchingButton(ui, 'n'); +if (prometheusPortString != null) +{ + endpointConfiguration.ConfigureOpenTelemetry("Billing", instanceId.ToString(), int.Parse(prometheusPortString)); +} + var endpointInstance = await Endpoint.Start(endpointConfiguration); -ui.RunLoop("Billing"); +ui.RunLoop(title); await endpointInstance.Stop(); \ No newline at end of file diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs index 5d50e5d4..cd9198ae 100644 --- a/src/ClientUI/Program.cs +++ b/src/ClientUI/Program.cs @@ -10,6 +10,7 @@ var title = string.IsNullOrEmpty(instancePostfix) ? "ClientUI" : $"ClientUI - {instancePostfix}"; var instanceName = string.IsNullOrEmpty(instancePostfix) ? "clientui" : $"clientui-{instancePostfix}"; var instanceId = DeterministicGuid.Create("ClientUI", instanceName); +var prometheusPortString = args.Skip(1).FirstOrDefault(); var endpointConfiguration = new EndpointConfiguration("ClientUI"); @@ -43,7 +44,10 @@ routing.RouteToEndpoint(typeof(PlaceOrder), "Sales"); -//Debugger.Launch(); +if (prometheusPortString != null) +{ + endpointConfiguration.ConfigureOpenTelemetry("ClientUI", instanceId.ToString(), int.Parse(prometheusPortString)); +} var endpointInstance = await Endpoint.Start(endpointConfiguration); @@ -58,7 +62,7 @@ var simulatedWork = simulatedCustomers.Run(cancellation.Token); -ui.RunLoop("Client UI"); +ui.RunLoop(title); cancellation.Cancel(); diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index 4eda516c..331243d9 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -24,7 +24,7 @@ public void Dispose() DirectoryEx.ForceDeleteReadonly(".audit-db"); } - public ProcessHandle AddProcess(string name, string instanceId) + public ProcessHandle AddProcess(string name, string instanceId, int port) { if (disposed) { @@ -32,7 +32,7 @@ public ProcessHandle AddProcess(string name, string instanceId) } var path = Path.Combine(name, $"{name}.dll"); //TODO: Hard-coded convention - return demoProcessGroup.AddProcess(path, instanceId); + return demoProcessGroup.AddProcess(path, instanceId, port); } readonly ProcessGroup demoProcessGroup; diff --git a/src/MonitoringDemo/ProcessGroup.cs b/src/MonitoringDemo/ProcessGroup.cs index be0a4f62..82925415 100644 --- a/src/MonitoringDemo/ProcessGroup.cs +++ b/src/MonitoringDemo/ProcessGroup.cs @@ -22,7 +22,7 @@ public ProcessGroup(string groupName) } } - public ProcessHandle AddProcess(string relativeAssemblyPath, string instanceId) + public ProcessHandle AddProcess(string relativeAssemblyPath, string instanceId, int port) { if (!processesByAssemblyPath.TryGetValue(relativeAssemblyPath, out var processes)) { @@ -30,7 +30,7 @@ public ProcessHandle AddProcess(string relativeAssemblyPath, string instanceId) processesByAssemblyPath[relativeAssemblyPath] = processes; } - var process = StartProcess(relativeAssemblyPath, instanceId); + var process = StartProcess(relativeAssemblyPath, instanceId, port.ToString()); if (process is null) { @@ -203,7 +203,7 @@ private void DisposeWindowsJob() #endregion - private static Process? StartProcess(string relativeAssemblyPath, string? arguments = null) + private static Process? StartProcess(string relativeAssemblyPath, params string[] arguments) { var fullAssemblyPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, relativeAssemblyPath)); var workingDirectory = Path.GetDirectoryName(fullAssemblyPath); @@ -216,9 +216,9 @@ private void DisposeWindowsJob() RedirectStandardOutput = true }; - if (arguments is not null) + foreach (var a in arguments) { - startInfo.Arguments += $" {arguments}"; + startInfo.Arguments += $" {a}"; } return Process.Start(startInfo); diff --git a/src/MonitoringDemo/ProcessWindow.cs b/src/MonitoringDemo/ProcessWindow.cs index b1708364..4ae96d1f 100644 --- a/src/MonitoringDemo/ProcessWindow.cs +++ b/src/MonitoringDemo/ProcessWindow.cs @@ -14,6 +14,7 @@ sealed partial class ProcessWindow : Window private readonly string name; private readonly bool singleInstance; + private readonly int basePort; private readonly DemoLauncher launcher; private readonly CancellationToken cancellationToken; @@ -22,6 +23,9 @@ sealed partial class ProcessWindow : Window public ListView? InstanceView { get; } public ListView LogView { get; } private ObservableCollection Instances { get; } = new(); + + private string?[] PrometheusPorts = new string[10]; + private Dictionary Processes { get; } = new(); [GeneratedRegex(@"Press (.) to")] @@ -36,10 +40,11 @@ sealed partial class ProcessWindow : Window [GeneratedRegex(@"!Widget (\w+) (\w+)")] private static partial Regex WidgetUpdateRegex(); - public ProcessWindow(string title, string name, bool singleInstance, DemoLauncher launcher, CancellationToken cancellationToken) + public ProcessWindow(string title, string name, bool singleInstance, int basePort, DemoLauncher launcher, CancellationToken cancellationToken) { this.name = name; this.singleInstance = singleInstance; + this.basePort = basePort; this.launcher = launcher; this.cancellationToken = cancellationToken; @@ -157,6 +162,8 @@ private void DoScaleIn(string instance) process!.Dispose(); Instances.Remove(instance); linesPerInstance.TryRemove(instance, out _); + + FreePort(instance); } private void ScaleOut() @@ -190,13 +197,21 @@ private void InstanceView_SelectedItemChanged(object? sender, ListViewItemEventA void StartNewProcess(CancellationTokenSource cancellationTokenSource) { + string instanceId; do { instanceId = new string(Enumerable.Range(0, 4).Select(x => Letters[Random.Shared.Next(Letters.Length)]).ToArray()); } while (Instances.Contains(instanceId)); - var process = new Process(launcher.AddProcess(name, instanceId), cancellationTokenSource); + var port = FindPort(instanceId); + if (port == null) + { + //No more free ports + return; + } + + var process = new Process(launcher.AddProcess(name, instanceId, basePort + port.Value), cancellationTokenSource); Processes[instanceId] = process; Instances.Add(instanceId); @@ -205,6 +220,30 @@ void StartNewProcess(CancellationTokenSource cancellationTokenSource) SelectInstance(instanceId); } + int? FindPort(string instance) + { + for (var i = 0; i < PrometheusPorts.Length; i++) + { + if (PrometheusPorts[i] == null) + { + PrometheusPorts[i] = instance; + return i; + } + } + return null; + } + + void FreePort(string instance) + { + for (var i = 0; i < PrometheusPorts.Length; i++) + { + if (PrometheusPorts[i] == instance) + { + PrometheusPorts[i] = null; + } + } + } + void SelectInstance(string instance) { LogView.SetSource(linesPerInstance.GetOrAdd(instance, _ => [])); diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index 4b1b5342..06cd8433 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -20,11 +20,11 @@ var menuBarItems = new List(); ProcessWindow[] windows = []; -var clientWindow = CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, cancellationToken); -var platformWindow = CreateWindow("Platform", "PlatformLauncher", "_Platform", true, cancellationToken); -var billingWindow = CreateWindow("Billing", "Billing", "_Billing", false, cancellationToken); -var shippingWindow = CreateWindow("Shipping", "Shipping", "S_hipping", false, cancellationToken); -var salesWindow = CreateWindow("Sales", "Sales", "_Sales", false, cancellationToken); +var clientWindow = CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, 10000, cancellationToken); +var platformWindow = CreateWindow("Platform", "PlatformLauncher", "_Platform", true, 10010, cancellationToken); +var billingWindow = CreateWindow("Billing", "Billing", "_Billing", false, 10020, cancellationToken); +var shippingWindow = CreateWindow("Shipping", "Shipping", "S_hipping", false, 10030, cancellationToken); +var salesWindow = CreateWindow("Sales", "Sales", "_Sales", false, 10040, cancellationToken); windows = [ platformWindow, @@ -115,9 +115,9 @@ static void SwitchWindow(IReadOnlyCollection windowsToHide, View windowToShow.SetNeedsDraw(); } -ProcessWindow CreateWindow(string title, string name, string menuItemText, bool singleInstance, CancellationToken cancellationToken) +ProcessWindow CreateWindow(string title, string name, string menuItemText, bool singleInstance, int basePort, CancellationToken cancellationToken) { - var processWindow = new ProcessWindow(title, name, singleInstance, launcher, cancellationToken); + var processWindow = new ProcessWindow(title, name, singleInstance, basePort, launcher, cancellationToken); var windowsToHide = windows.Except([processWindow]).ToArray(); var menuItem = new MenuBarItem(menuItemText, "", diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs index 28185514..1f7bf979 100644 --- a/src/Sales/Program.cs +++ b/src/Sales/Program.cs @@ -7,6 +7,7 @@ var instancePostfix = args.FirstOrDefault(); var title = string.IsNullOrEmpty(instancePostfix) ? "Processing (Sales)" : $"Sales - {instancePostfix}"; var instanceName = string.IsNullOrEmpty(instancePostfix) ? "sales" : $"sales-{instancePostfix}"; +var prometheusPortString = args.Skip(1).FirstOrDefault(); var instanceId = DeterministicGuid.Create("Sales", instanceName); @@ -56,6 +57,11 @@ failureSimulation.BindFailureProcessingButton(ui, 'x'); failureSimulation.BindFailureDispatchingButton(ui, 'c'); +if (prometheusPortString != null) +{ + endpointConfiguration.ConfigureOpenTelemetry("Sales", instanceId.ToString(), int.Parse(prometheusPortString)); +} + var endpointInstance = await Endpoint.Start(endpointConfiguration); ui.RunLoop(title); diff --git a/src/Shared/OpenTelemetryUtils.cs b/src/Shared/OpenTelemetryUtils.cs new file mode 100644 index 00000000..3611923b --- /dev/null +++ b/src/Shared/OpenTelemetryUtils.cs @@ -0,0 +1,31 @@ +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; + +namespace Shared; + +public static class OpenTelemetryUtils +{ + public static IDisposable ConfigureOpenTelemetry(this EndpointConfiguration endpointConfig, string name, string id, int port) + { + var attributes = new Dictionary + { + ["service.name"] = name, + ["service.instance.id"] = id, + }; + + var resourceBuilder = ResourceBuilder.CreateDefault().AddAttributes(attributes); + + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .SetResourceBuilder(resourceBuilder) + .AddMeter("NServiceBus.Core*"); + + meterProviderBuilder.AddPrometheusHttpListener(options => options.UriPrefixes = [$"http://127.0.0.1:{port}"]); + + var meterProvider = meterProviderBuilder.Build(); + + endpointConfig.EnableOpenTelemetry(); + + return meterProvider; + } +} \ No newline at end of file diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index de6902d7..37dcfe0f 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -8,5 +8,6 @@ + diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs index 47785580..cfd623ac 100644 --- a/src/Shipping/Program.cs +++ b/src/Shipping/Program.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics; +using System.Reflection; using System.Text.Json; using Messages; using Microsoft.Extensions.DependencyInjection; @@ -10,6 +11,7 @@ var title = string.IsNullOrEmpty(instancePostfix) ? "Processing (Shipping)" : $"Shipping - {instancePostfix}"; var instanceName = string.IsNullOrEmpty(instancePostfix) ? "shipping" : $"shipping-{instancePostfix}"; var instanceId = DeterministicGuid.Create("Shipping", instanceName); +var prometheusPortString = args.Skip(1).FirstOrDefault(); var endpointConfiguration = new EndpointConfiguration("Shipping"); endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); @@ -52,6 +54,11 @@ failureSimulation.BindFailureProcessingButton(ui, ','); failureSimulation.BindFailureDispatchingButton(ui, '.'); +if (prometheusPortString != null) +{ + endpointConfiguration.ConfigureOpenTelemetry("Shipping", instanceId.ToString(), int.Parse(prometheusPortString)); +} + var endpointInstance = await Endpoint.Start(endpointConfiguration); ui.RunLoop(title); From a597086827b1059dd28bcb6ae0fd749bcada1618 Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Mon, 23 Jun 2025 13:18:31 +0200 Subject: [PATCH 30/37] Fixes to simulation # Conflicts: # otel/docker-compose.yml --- .gitignore | 3 +- src/Billing/OrderPlacedHandler.cs | 2 +- src/Billing/Program.cs | 99 +++++++++------- src/ClientUI/Program.cs | 6 +- src/ClientUI/SimulatedCustomers.cs | 58 ++++++++-- src/MonitoringDemo/ProcessWindow.cs | 6 +- src/Sales/PlaceOrderHandler.cs | 2 +- src/Sales/Program.cs | 95 ++++++++------- src/Shared/DatabaseDownSimulationBehavior.cs | 32 +++++ src/Shared/MessageIdHelper.cs | 4 +- src/Shared/OpenTelemetryUtils.cs | 4 +- src/Shared/ProcessingEndpointControls.cs | 116 ++++++++++++++++++- src/Shipping/Program.cs | 91 ++++++++------- 13 files changed, 367 insertions(+), 151 deletions(-) create mode 100644 src/Shared/DatabaseDownSimulationBehavior.cs diff --git a/.gitignore b/.gitignore index 3540ccab..3f5466ee 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ MonitoringDemo.Sql/transport/ **/.diagnostics/* .logs -.idea \ No newline at end of file +.idea +/otel/ diff --git a/src/Billing/OrderPlacedHandler.cs b/src/Billing/OrderPlacedHandler.cs index edfb94af..d344d17b 100644 --- a/src/Billing/OrderPlacedHandler.cs +++ b/src/Billing/OrderPlacedHandler.cs @@ -14,7 +14,7 @@ public async Task Handle(OrderPlaced message, IMessageHandlerContext context) }; var publishOptions = new PublishOptions(); - publishOptions.SetHumanReadableMessageId(); + publishOptions.SetMessageId(MessageIdHelper.GetHumanReadableMessageId()); await context.Publish(orderBilled, publishOptions); } } \ No newline at end of file diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index b7ac454b..47d1b36b 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -12,61 +12,72 @@ var instanceId = DeterministicGuid.Create("Billing", instanceName); var prometheusPortString = args.Skip(1).FirstOrDefault(); -var endpointConfiguration = new EndpointConfiguration("Billing"); -endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); +var endpointControls = new ProcessingEndpointControls(() => PrepareEndpointConfiguration(instanceId, instanceName, prometheusPortString)); -var serializer = endpointConfiguration.UseSerialization(); -serializer.Options(new JsonSerializerOptions -{ - TypeInfoResolverChain = - { - MessagesSerializationContext.Default - } -}); +var ui = new UserInterface(); +endpointControls.BindSlowProcessingDial(ui, '5', 't'); +endpointControls.BindDatabaseFailuresDial(ui, '6', 'y'); + +endpointControls.BindDatabaseDownToggle(ui, 'f'); +endpointControls.BindDelayedRetriesToggle(ui, 'g'); +endpointControls.BindAutoThrottleToggle(ui, 'h'); -var transport = new LearningTransport +endpointControls.BindFailureReceivingButton(ui, 'v'); +endpointControls.BindFailureProcessingButton(ui, 'b'); +endpointControls.BindFailureDispatchingButton(ui, 'n'); + +if (prometheusPortString != null) { - StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport"), - TransportTransactionMode = TransportTransactionMode.ReceiveOnly -}; -endpointConfiguration.UseTransport(transport); + OpenTelemetryUtils.ConfigureOpenTelemetry("Billing", instanceId.ToString(), int.Parse(prometheusPortString)); +} -endpointConfiguration.Recoverability() - .Delayed(delayed => delayed.NumberOfRetries(0)); +endpointControls.Start(); -endpointConfiguration.AuditProcessedMessagesTo("audit"); -endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); +ui.RunLoop(title); -endpointConfiguration.UniquelyIdentifyRunningInstance() - .UsingCustomIdentifier(instanceId) - .UsingCustomDisplayName(instanceName); +await endpointControls.StopEndpoint(); -var metrics = endpointConfiguration.EnableMetrics(); -metrics.SendMetricDataToServiceControl( - "Particular.Monitoring", - TimeSpan.FromMilliseconds(500) -); +EndpointConfiguration PrepareEndpointConfiguration(Guid guid, string s, string? prometheusPortString1) +{ + var endpointConfiguration1 = new EndpointConfiguration("Billing"); + endpointConfiguration1.LimitMessageProcessingConcurrencyTo(4); -endpointConfiguration.UsePersistence(); -endpointConfiguration.EnableOutbox(); + var serializer = endpointConfiguration1.UseSerialization(); + serializer.Options(new JsonSerializerOptions + { + TypeInfoResolverChain = + { + MessagesSerializationContext.Default + } + }); -var failureSimulation = new ProcessingEndpointControls(); -failureSimulation.Register(endpointConfiguration); + var transport = new LearningTransport + { + StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport"), + TransportTransactionMode = TransportTransactionMode.ReceiveOnly + }; + endpointConfiguration1.UseTransport(transport); -var ui = new UserInterface(); -failureSimulation.BindSlowProcessingDial(ui, '5', 't'); -failureSimulation.BindDatabaseFailuresDial(ui, '6', 'y'); -failureSimulation.BindFailureReceivingButton(ui, 'v'); -failureSimulation.BindFailureProcessingButton(ui, 'b'); -failureSimulation.BindFailureDispatchingButton(ui, 'n'); + endpointConfiguration1.Recoverability() + .Delayed(delayed => delayed.NumberOfRetries(0)); -if (prometheusPortString != null) -{ - endpointConfiguration.ConfigureOpenTelemetry("Billing", instanceId.ToString(), int.Parse(prometheusPortString)); -} + endpointConfiguration1.AuditProcessedMessagesTo("audit"); + endpointConfiguration1.SendHeartbeatTo("Particular.ServiceControl"); -var endpointInstance = await Endpoint.Start(endpointConfiguration); + endpointConfiguration1.UniquelyIdentifyRunningInstance() + .UsingCustomIdentifier(guid) + .UsingCustomDisplayName(s); -ui.RunLoop(title); + var metrics = endpointConfiguration1.EnableMetrics(); + metrics.SendMetricDataToServiceControl( + "Particular.Monitoring", + TimeSpan.FromMilliseconds(500) + ); + + endpointConfiguration1.UsePersistence(); + endpointConfiguration1.EnableOutbox(); + + endpointConfiguration1.EnableOpenTelemetry(); -await endpointInstance.Stop(); \ No newline at end of file + return endpointConfiguration1; +} \ No newline at end of file diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs index cd9198ae..cdcd2805 100644 --- a/src/ClientUI/Program.cs +++ b/src/ClientUI/Program.cs @@ -36,6 +36,8 @@ .UsingCustomIdentifier(instanceId) .UsingCustomDisplayName(instanceName); +endpointConfiguration.EnableOpenTelemetry(); + var metrics = endpointConfiguration.EnableMetrics(); metrics.SendMetricDataToServiceControl( "Particular.Monitoring", @@ -46,7 +48,7 @@ if (prometheusPortString != null) { - endpointConfiguration.ConfigureOpenTelemetry("ClientUI", instanceId.ToString(), int.Parse(prometheusPortString)); + OpenTelemetryUtils.ConfigureOpenTelemetry("ClientUI", instanceId.ToString(), int.Parse(prometheusPortString)); } var endpointInstance = await Endpoint.Start(endpointConfiguration); @@ -59,6 +61,8 @@ simulatedCustomers.BindDuplicateLikelihoodDial(ui, '=', ']'); simulatedCustomers.BindManualModeToggle(ui, ';'); simulatedCustomers.BindManualSendButton(ui, '/'); +simulatedCustomers.BindNoiseToggle(ui, '`'); +simulatedCustomers.BindBlackFridayToggle(ui, '\''); var simulatedWork = simulatedCustomers.Run(cancellation.Token); diff --git a/src/ClientUI/SimulatedCustomers.cs b/src/ClientUI/SimulatedCustomers.cs index f8ffa2dd..41289f1d 100644 --- a/src/ClientUI/SimulatedCustomers.cs +++ b/src/ClientUI/SimulatedCustomers.cs @@ -23,7 +23,25 @@ public void BindManualModeToggle(UserInterface userInterface, char toggleKey) { userInterface.BindToggle('D', toggleKey, $"Press {toggleKey} to toggle manual send mode", () => manualMode ? "Manual sending mode" : "Automatic sending mode", - () => manualMode = true, () => manualMode = false); + () => manualMode = true, () => + { + manualMode = false; + manualModeSemaphore.Release(); + }); + } + + public void BindNoiseToggle(UserInterface userInterface, char toggleKey) + { + userInterface.BindToggle('E', toggleKey, $"Press {toggleKey} to toggle random noise", + () => enableRandomNoise ? "Random noise" : "No random noise", + () => enableRandomNoise = true, () => enableRandomNoise = false); + } + + public void BindBlackFridayToggle(UserInterface userInterface, char toggleKey) + { + userInterface.BindToggle('F', toggleKey, $"Press {toggleKey} to toggle Black Friday mode", + () => blackFriday ? "Black Friday!" : "Business as usual", + () => blackFriday = true, () => blackFriday = false); } public void BindManualSendButton(UserInterface userInterface, char key) @@ -31,6 +49,9 @@ public void BindManualSendButton(UserInterface userInterface, char key) userInterface.BindButton('G', key, $"Press {key} to send a message", null, () => manualModeSemaphore.Release()); } + private int EffectiveRate => Math.Max(blackFriday ? 32 : NoiseModifiedRate, 0); + private int NoiseModifiedRate => enableRandomNoise ? rate + noiseComponent : rate; + public async Task Run(CancellationToken cancellationToken = default) { nextReset = DateTime.UtcNow.AddSeconds(1); @@ -42,6 +63,21 @@ public async Task Run(CancellationToken cancellationToken = default) if (now > nextReset) { currentIntervalCount = 0; + + var noiseIncrease = Random.Shared.Next(Math.Abs(noiseComponent) + 1) == 0; + if (noiseComponent == 0) + { + //Randomly go up or down + } + else if (noiseIncrease) + { + noiseComponent += Math.Sign(noiseComponent); + } + else + { + noiseComponent -= Math.Sign(noiseComponent); + } + nextReset = now.AddSeconds(1); } @@ -55,7 +91,7 @@ public async Task Run(CancellationToken cancellationToken = default) try { - if (currentIntervalCount >= rate) + if (currentIntervalCount >= EffectiveRate) { var delay = nextReset - DateTime.UtcNow; if (delay > TimeSpan.Zero) @@ -78,26 +114,28 @@ async Task PlaceSingleOrder(CancellationToken cancellationToken) OrderId = Guid.NewGuid().ToString() }; - var sendOptions = await SendOneMessage(cancellationToken, placeOrderCommand); + var messageId = MessageIdHelper.GetHumanReadableMessageId(); + + await SendOneMessage(messageId, cancellationToken, placeOrderCommand); if (manualMode) { - Console.WriteLine($"Message {sendOptions.GetMessageId()} sent."); + Console.WriteLine($"Message {messageId} sent."); } if (Random.Shared.Next(10) < duplicateLikelihood) { //Send a duplicate - await SendOneMessage(cancellationToken, placeOrderCommand); + await SendOneMessage(messageId, cancellationToken, placeOrderCommand); if (manualMode) { - Console.WriteLine($"Duplicate message {sendOptions.GetMessageId()} sent."); + Console.WriteLine($"Duplicate message {messageId} sent."); } } } - private async Task SendOneMessage(CancellationToken cancellationToken, PlaceOrder placeOrderCommand) + private async Task SendOneMessage(string messageId, CancellationToken cancellationToken, PlaceOrder placeOrderCommand) { var sendOptions = new SendOptions(); @@ -106,15 +144,17 @@ private async Task SendOneMessage(CancellationToken cancellationTok sendOptions.SetHeader("MonitoringDemo.ManualMode", "True"); } - sendOptions.SetHumanReadableMessageId(); + sendOptions.SetMessageId(messageId); await endpointInstance.Send(placeOrderCommand, sendOptions, cancellationToken); - return sendOptions; } DateTime nextReset; int currentIntervalCount; int rate = 1; + private int noiseComponent = 0; + private bool enableRandomNoise; private int duplicateLikelihood; private bool manualMode; + private bool blackFriday; private SemaphoreSlim manualModeSemaphore = new SemaphoreSlim(0); } \ No newline at end of file diff --git a/src/MonitoringDemo/ProcessWindow.cs b/src/MonitoringDemo/ProcessWindow.cs index 4ae96d1f..7b5b6c54 100644 --- a/src/MonitoringDemo/ProcessWindow.cs +++ b/src/MonitoringDemo/ProcessWindow.cs @@ -197,7 +197,6 @@ private void InstanceView_SelectedItemChanged(object? sender, ListViewItemEventA void StartNewProcess(CancellationTokenSource cancellationTokenSource) { - string instanceId; do { @@ -322,6 +321,11 @@ void PrintOutput(string instance, Process process, CancellationToken cancellatio } lines.Add(output); + //Simple hacky way to not store all the data in the world + if (lines.Count > 100) + { + lines.RemoveAt(0); + } LogView.MoveEnd(); // Scroll to end }); } diff --git a/src/Sales/PlaceOrderHandler.cs b/src/Sales/PlaceOrderHandler.cs index 23de1041..bc0fda6b 100644 --- a/src/Sales/PlaceOrderHandler.cs +++ b/src/Sales/PlaceOrderHandler.cs @@ -14,7 +14,7 @@ public async Task Handle(PlaceOrder message, IMessageHandlerContext context) }; var publishOptions = new PublishOptions(); - publishOptions.SetHumanReadableMessageId(); + publishOptions.SetMessageId(MessageIdHelper.GetHumanReadableMessageId()); await context.Publish(orderPlaced, publishOptions); } } \ No newline at end of file diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs index 1f7bf979..6ccda497 100644 --- a/src/Sales/Program.cs +++ b/src/Sales/Program.cs @@ -11,59 +11,70 @@ var instanceId = DeterministicGuid.Create("Sales", instanceName); -var endpointConfiguration = new EndpointConfiguration("Sales"); -endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); +var endpointControls = new ProcessingEndpointControls(() => PrepareEndpointConfiguration(instanceId, instanceName, prometheusPortString)); -var serializer = endpointConfiguration.UseSerialization(); -serializer.Options(new JsonSerializerOptions -{ - TypeInfoResolverChain = - { - MessagesSerializationContext.Default - } -}); +var ui = new UserInterface(); +endpointControls.BindSlowProcessingDial(ui, '2', 'w'); +endpointControls.BindDatabaseFailuresDial(ui, '3', 'e'); + +endpointControls.BindDatabaseDownToggle(ui, 'a'); +endpointControls.BindDelayedRetriesToggle(ui, 's'); +endpointControls.BindAutoThrottleToggle(ui, 'd'); -var transport = new LearningTransport +endpointControls.BindFailureReceivingButton(ui, 'z'); +endpointControls.BindFailureProcessingButton(ui, 'x'); +endpointControls.BindFailureDispatchingButton(ui, 'c'); + +if (prometheusPortString != null) { - StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport"), - TransportTransactionMode = TransportTransactionMode.ReceiveOnly -}; -endpointConfiguration.UseTransport(transport); + OpenTelemetryUtils.ConfigureOpenTelemetry("Sales", instanceId.ToString(), int.Parse(prometheusPortString)); +} -endpointConfiguration.AuditProcessedMessagesTo("audit"); -endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); +endpointControls.Start(); -endpointConfiguration.UniquelyIdentifyRunningInstance() - .UsingCustomIdentifier(instanceId) - .UsingCustomDisplayName(instanceName); +ui.RunLoop(title); -var metrics = endpointConfiguration.EnableMetrics(); +await endpointControls.StopEndpoint(); -metrics.SendMetricDataToServiceControl( - "Particular.Monitoring", - TimeSpan.FromMilliseconds(500) -); +EndpointConfiguration PrepareEndpointConfiguration(Guid guid, string displayName, string? prometheusPortString1) +{ + var endpointConfiguration1 = new EndpointConfiguration("Sales"); + endpointConfiguration1.LimitMessageProcessingConcurrencyTo(4); -var failureSimulation = new ProcessingEndpointControls(); -failureSimulation.Register(endpointConfiguration); + var serializer = endpointConfiguration1.UseSerialization(); + serializer.Options(new JsonSerializerOptions + { + TypeInfoResolverChain = + { + MessagesSerializationContext.Default + } + }); -endpointConfiguration.UsePersistence(); -endpointConfiguration.EnableOutbox(); + var transport = new LearningTransport + { + StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport"), + TransportTransactionMode = TransportTransactionMode.ReceiveOnly + }; + endpointConfiguration1.UseTransport(transport); -var ui = new UserInterface(); -failureSimulation.BindSlowProcessingDial(ui, '2', 'w'); -failureSimulation.BindDatabaseFailuresDial(ui, '3', 'e'); -failureSimulation.BindFailureReceivingButton(ui, 'z'); -failureSimulation.BindFailureProcessingButton(ui, 'x'); -failureSimulation.BindFailureDispatchingButton(ui, 'c'); + endpointConfiguration1.AuditProcessedMessagesTo("audit"); + endpointConfiguration1.SendHeartbeatTo("Particular.ServiceControl"); -if (prometheusPortString != null) -{ - endpointConfiguration.ConfigureOpenTelemetry("Sales", instanceId.ToString(), int.Parse(prometheusPortString)); -} + endpointConfiguration1.UniquelyIdentifyRunningInstance() + .UsingCustomIdentifier(guid) + .UsingCustomDisplayName(displayName); -var endpointInstance = await Endpoint.Start(endpointConfiguration); + var metrics = endpointConfiguration1.EnableMetrics(); -ui.RunLoop(title); + metrics.SendMetricDataToServiceControl( + "Particular.Monitoring", + TimeSpan.FromMilliseconds(500) + ); + + endpointConfiguration1.UsePersistence(); + endpointConfiguration1.EnableOutbox(); + + endpointConfiguration1.EnableOpenTelemetry(); -await endpointInstance.Stop(); \ No newline at end of file + return endpointConfiguration1; +} \ No newline at end of file diff --git a/src/Shared/DatabaseDownSimulationBehavior.cs b/src/Shared/DatabaseDownSimulationBehavior.cs new file mode 100644 index 00000000..8fbb1c81 --- /dev/null +++ b/src/Shared/DatabaseDownSimulationBehavior.cs @@ -0,0 +1,32 @@ +using NServiceBus.Pipeline; + +namespace Shared; + +public class DatabaseDownSimulationBehavior : Behavior +{ + private bool databaseDown; + + public override Task Invoke(IInvokeHandlerContext context, Func next) + { + if (databaseDown) + { + throw new Exception("Simulated"); + } + return next(); + } + + public string ReportState() + { + return databaseDown ? "Database down" : "Database up"; + } + + public void Down() + { + databaseDown = true; + } + + public void Up() + { + databaseDown = false; + } +} \ No newline at end of file diff --git a/src/Shared/MessageIdHelper.cs b/src/Shared/MessageIdHelper.cs index 42724945..23136f5c 100644 --- a/src/Shared/MessageIdHelper.cs +++ b/src/Shared/MessageIdHelper.cs @@ -6,9 +6,9 @@ public static class MessageIdHelper { private const string Letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - public static void SetHumanReadableMessageId(this ExtendableOptions opts) + public static string GetHumanReadableMessageId() { var messageId = new string(Enumerable.Range(0, 4).Select(x => Letters[Random.Shared.Next(Letters.Length)]).ToArray()); - opts.SetMessageId(messageId); + return messageId; } } \ No newline at end of file diff --git a/src/Shared/OpenTelemetryUtils.cs b/src/Shared/OpenTelemetryUtils.cs index 3611923b..b1bac821 100644 --- a/src/Shared/OpenTelemetryUtils.cs +++ b/src/Shared/OpenTelemetryUtils.cs @@ -6,7 +6,7 @@ namespace Shared; public static class OpenTelemetryUtils { - public static IDisposable ConfigureOpenTelemetry(this EndpointConfiguration endpointConfig, string name, string id, int port) + public static IDisposable ConfigureOpenTelemetry(string name, string id, int port) { var attributes = new Dictionary { @@ -24,8 +24,6 @@ public static IDisposable ConfigureOpenTelemetry(this EndpointConfiguration endp var meterProvider = meterProviderBuilder.Build(); - endpointConfig.EnableOpenTelemetry(); - return meterProvider; } } \ No newline at end of file diff --git a/src/Shared/ProcessingEndpointControls.cs b/src/Shared/ProcessingEndpointControls.cs index 4a9510a4..237d82b3 100644 --- a/src/Shared/ProcessingEndpointControls.cs +++ b/src/Shared/ProcessingEndpointControls.cs @@ -1,23 +1,102 @@ namespace Shared; -public class ProcessingEndpointControls +public class ProcessingEndpointControls(Func endpointConfigProvider) { + private IEndpointInstance? runningEndpoint; + + private bool delayedRetries; + private bool autoThrottle; + private readonly RetrievingMessageProgressBehavior retrievingMessageProgressBehavior = new RetrievingMessageProgressBehavior(); private readonly ProcessingMessageProgressBehavior processingMessageProgressBehavior = new ProcessingMessageProgressBehavior(); private readonly DispatchingProgressBehavior dispatchingMessageProgressBehavior = new DispatchingProgressBehavior(); private readonly SlowProcessingSimulationBehavior slowProcessingSimulationBehavior = new SlowProcessingSimulationBehavior(); private readonly DatabaseFailureSimulationBehavior databaseFailureSimulationBehavior = new DatabaseFailureSimulationBehavior(); + private readonly DatabaseDownSimulationBehavior databaseDownSimulationBehavior = new DatabaseDownSimulationBehavior(); + private CancellationTokenSource? stopTokenSource; + private readonly SemaphoreSlim restartSemaphore = new SemaphoreSlim(1); + private Task? restartTask; - public void Register(EndpointConfiguration endpointConfiguration) + void Register(EndpointConfiguration endpointConfiguration) { endpointConfiguration.Pipeline.Register(retrievingMessageProgressBehavior, "Shows progress of retrieving messages"); endpointConfiguration.Pipeline.Register(processingMessageProgressBehavior, "Shows progress of processing messages"); endpointConfiguration.Pipeline.Register(dispatchingMessageProgressBehavior, "Shows progress of dispatching messages"); endpointConfiguration.Pipeline.Register(slowProcessingSimulationBehavior, "Simulates slow processing"); endpointConfiguration.Pipeline.Register(databaseFailureSimulationBehavior, "Simulates faulty database"); + endpointConfiguration.Pipeline.Register(databaseDownSimulationBehavior, "Simulates down database"); endpointConfiguration.Pipeline.Register(new PropagateManualModeBehavior(), "Propagates manual mode settings"); } + public void Start() + { + stopTokenSource = new CancellationTokenSource(); + restartTask = Task.Run(async () => + { + var stopToken = stopTokenSource.Token; + while (!stopToken.IsCancellationRequested) + { + try + { + await restartSemaphore.WaitAsync(stopToken); + //await Task.Delay(5000); + await RestartEndpoint(); + } +#pragma warning disable PS0019 + catch (Exception e) +#pragma warning restore PS0019 + { + Console.WriteLine(e); + } + + } + }); + } + +#pragma warning disable PS0018 + async Task RestartEndpoint() +#pragma warning restore PS0018 + { + if (runningEndpoint != null) + { + await runningEndpoint.Stop(); + } + + var config = endpointConfigProvider(); + + if (!delayedRetries) + { + config.Recoverability().Delayed(settings => settings.NumberOfRetries(0)); + } + + if (autoThrottle) + { + var rateLimitSettings = new RateLimitSettings + { + }; + config.Recoverability().OnConsecutiveFailures(5, rateLimitSettings); + } + + Register(config); + + runningEndpoint = await Endpoint.Start(config); + } + +#pragma warning disable PS0018 + public async Task StopEndpoint() +#pragma warning restore PS0018 + { + stopTokenSource?.Cancel(); + if (restartTask != null) + { + await restartTask; + } + if (runningEndpoint != null) + { + await runningEndpoint.Stop(); + } + } + public void BindSlowProcessingDial(UserInterface userInterface, char upKey, char downKey) { userInterface.BindDial( @@ -37,17 +116,42 @@ public void BindDatabaseFailuresDial(UserInterface userInterface, char upKey, ch public void BindDatabaseDownToggle(UserInterface userInterface, char toggleKey) { - + userInterface.BindToggle('D', toggleKey, $"Press {toggleKey} to toggle database down simulation.", + () => databaseDownSimulationBehavior.ReportState(), + () => databaseDownSimulationBehavior.Down(), + () => databaseDownSimulationBehavior.Up()); } - public void BindOutboxToggle(UserInterface userInterface, char toggleKey) + public void BindDelayedRetriesToggle(UserInterface userInterface, char toggleKey) { - + userInterface.BindToggle('E', toggleKey, $"Press {toggleKey} to toggle delayed retries.", + () => delayedRetries ? "Delayed retries enabled" : "Delayed retries disabled", + () => + { + delayedRetries = true; + restartSemaphore.Release(); + }, + () => + { + delayedRetries = false; + restartSemaphore.Release(); + }); } public void BindAutoThrottleToggle(UserInterface userInterface, char toggleKey) { - + userInterface.BindToggle('F', toggleKey, $"Press {toggleKey} to toggle auto throttle.", + () => autoThrottle ? "Auto throttle enabled" : "Auto throttle disabled", + () => + { + autoThrottle = true; + restartSemaphore.Release(); + }, + () => + { + autoThrottle = false; + restartSemaphore.Release(); + }); } public void BindFailureReceivingButton(UserInterface userInterface, char key) diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs index cfd623ac..2dc81c78 100644 --- a/src/Shipping/Program.cs +++ b/src/Shipping/Program.cs @@ -1,8 +1,10 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.Reflection; using System.Text.Json; using Messages; using Microsoft.Extensions.DependencyInjection; +using NServiceBus; using Shared; using Shipping; @@ -13,54 +15,63 @@ var instanceId = DeterministicGuid.Create("Shipping", instanceName); var prometheusPortString = args.Skip(1).FirstOrDefault(); -var endpointConfiguration = new EndpointConfiguration("Shipping"); -endpointConfiguration.LimitMessageProcessingConcurrencyTo(4); +var endpointControls = new ProcessingEndpointControls(() => PrepareEndpointConfiguration(instanceId, instanceName, prometheusPortString)); -var serializer = endpointConfiguration.UseSerialization(); -serializer.Options(new JsonSerializerOptions -{ - TypeInfoResolverChain = - { - MessagesSerializationContext.Default - } -}); +var ui = new UserInterface(); +endpointControls.BindSlowProcessingDial(ui, '8', 'i'); +endpointControls.BindDatabaseFailuresDial(ui, '9', 'o'); -var transport = new LearningTransport +endpointControls.BindDatabaseDownToggle(ui, 'j'); +endpointControls.BindDelayedRetriesToggle(ui, 'k'); +endpointControls.BindAutoThrottleToggle(ui, 'l'); + +endpointControls.BindFailureReceivingButton(ui, 'm'); +endpointControls.BindFailureProcessingButton(ui, ','); +endpointControls.BindFailureDispatchingButton(ui, '.'); + +if (prometheusPortString != null) { - StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport") -}; -endpointConfiguration.UseTransport(transport); + OpenTelemetryUtils.ConfigureOpenTelemetry("Shipping", instanceId.ToString(), int.Parse(prometheusPortString)); +} +endpointControls.Start(); +ui.RunLoop(title); -endpointConfiguration.AuditProcessedMessagesTo("audit"); -endpointConfiguration.SendHeartbeatTo("Particular.ServiceControl"); +await endpointControls.StopEndpoint(); -endpointConfiguration.UniquelyIdentifyRunningInstance() - .UsingCustomIdentifier(instanceId) - .UsingCustomDisplayName(instanceName); +EndpointConfiguration PrepareEndpointConfiguration(Guid guid, string s, string? prometheusPortString1) +{ + var endpointConfiguration1 = new EndpointConfiguration("Shipping"); + endpointConfiguration1.LimitMessageProcessingConcurrencyTo(4); -var metrics = endpointConfiguration.EnableMetrics(); -metrics.SendMetricDataToServiceControl( - "Particular.Monitoring", - TimeSpan.FromMilliseconds(500) -); + var serializer = endpointConfiguration1.UseSerialization(); + serializer.Options(new JsonSerializerOptions + { + TypeInfoResolverChain = + { + MessagesSerializationContext.Default + } + }); -var failureSimulation = new ProcessingEndpointControls(); -failureSimulation.Register(endpointConfiguration); + var transport = new LearningTransport + { + StorageDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.Parent!.FullName, ".learningtransport") + }; + endpointConfiguration1.UseTransport(transport); -var ui = new UserInterface(); -failureSimulation.BindSlowProcessingDial(ui, '8', 'i'); -failureSimulation.BindDatabaseFailuresDial(ui, '9', 'o'); -failureSimulation.BindFailureReceivingButton(ui, 'm'); -failureSimulation.BindFailureProcessingButton(ui, ','); -failureSimulation.BindFailureDispatchingButton(ui, '.'); + endpointConfiguration1.AuditProcessedMessagesTo("audit"); + endpointConfiguration1.SendHeartbeatTo("Particular.ServiceControl"); -if (prometheusPortString != null) -{ - endpointConfiguration.ConfigureOpenTelemetry("Shipping", instanceId.ToString(), int.Parse(prometheusPortString)); -} + endpointConfiguration1.UniquelyIdentifyRunningInstance() + .UsingCustomIdentifier(guid) + .UsingCustomDisplayName(s); -var endpointInstance = await Endpoint.Start(endpointConfiguration); + var metrics = endpointConfiguration1.EnableMetrics(); + metrics.SendMetricDataToServiceControl( + "Particular.Monitoring", + TimeSpan.FromMilliseconds(500) + ); -ui.RunLoop(title); + endpointConfiguration1.EnableOpenTelemetry(); -await endpointInstance.Stop(); \ No newline at end of file + return endpointConfiguration1; +} \ No newline at end of file From 1ad97703cde0986fd087a1f237e5269a75b6915d Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Mon, 23 Jun 2025 13:19:22 +0200 Subject: [PATCH 31/37] Delete joystick files --- Box1.joystick.amgp | 345 --------------------------------------------- Box2.joystick.amgp | 345 --------------------------------------------- 2 files changed, 690 deletions(-) delete mode 100644 Box1.joystick.amgp delete mode 100644 Box2.joystick.amgp diff --git a/Box1.joystick.amgp b/Box1.joystick.amgp deleted file mode 100644 index eae5caa8..00000000 --- a/Box1.joystick.amgp +++ /dev/null @@ -1,345 +0,0 @@ - - - - Button box - - 0300ac06c0160000dc27000000000000582410204 - - - - 1000 - positive - - - - $1B0 - textentry - - - 10 - distance - - - $1B1 - textentry - - - 10 - distance - - - $1B2 - textentry - - - 10 - distance - - - $1B3 - textentry - - - 10 - distance - - - $1B4 - textentry - - - 10 - distance - - - $1B5 - textentry - - - 10 - distance - - - $1B6 - textentry - - - 10 - distance - - - $1B7 - textentry - - - 10 - distance - - - $1B8 - textentry - - - 10 - distance - - - $1B9 - textentry - - - - - - 1000 - positive - - - - $1A0 - textentry - - - 10 - distance - - - $1A1 - textentry - - - 10 - distance - - - $1A2 - textentry - - - 10 - distance - - - $1A3 - textentry - - - 10 - distance - - - $1A4 - textentry - - - 10 - distance - - - $1A5 - textentry - - - 10 - distance - - - $1A6 - textentry - - - 10 - distance - - - $1A7 - textentry - - - 10 - distance - - - $1A8 - textentry - - - 10 - distance - - - $1A9 - textentry - - - - - - normal - - - normal - - - normal - - - 1000 - positive - - - - $1C0 - textentry - - - 10 - distance - - - $1C1 - textentry - - - 10 - distance - - - $1C2 - textentry - - - 10 - distance - - - $1C3 - textentry - - - 10 - distance - - - $1C4 - textentry - - - 10 - distance - - - $1C5 - textentry - - - 10 - distance - - - $1C6 - textentry - - - 10 - distance - - - $1C7 - textentry - - - 10 - distance - - - $1C8 - textentry - - - 10 - distance - - - $1C9 - textentry - - - - - - - - - - - - - diff --git a/Box2.joystick.amgp b/Box2.joystick.amgp deleted file mode 100644 index c8784cdc..00000000 --- a/Box2.joystick.amgp +++ /dev/null @@ -1,345 +0,0 @@ - - - - Button box - - 0300ac06c0160000dc27000000000000582410204 - - - - 0 - positive - - - - $2B0 - textentry - - - 2 - distance - - - $2B1 - textentry - - - 3 - distance - - - $2B2 - textentry - - - 3 - distance - - - $2B3 - textentry - - - 3 - distance - - - $2B4 - textentry - - - 4 - distance - - - $2B5 - textentry - - - 10 - distance - - - $2B6 - textentry - - - 15 - distance - - - $2B7 - textentry - - - 20 - distance - - - $2B8 - textentry - - - 20 - distance - - - $2B9 - textentry - - - - - - 1000 - positive - - - - $2A0 - textentry - - - 10 - distance - - - $2A1 - textentry - - - 10 - distance - - - $2A2 - textentry - - - 10 - distance - - - $2A3 - textentry - - - 10 - distance - - - $2A4 - textentry - - - 10 - distance - - - $2A5 - textentry - - - 10 - distance - - - $2A6 - textentry - - - 10 - distance - - - $2A7 - textentry - - - 10 - distance - - - $2A8 - textentry - - - 10 - distance - - - $2A9 - textentry - - - - - - normal - - - normal - - - normal - - - 1000 - positive - - - - $2C0 - textentry - - - 10 - distance - - - $2C1 - textentry - - - 10 - distance - - - $2C2 - textentry - - - 10 - distance - - - $2C3 - textentry - - - 10 - distance - - - $2C4 - textentry - - - 10 - distance - - - $2C5 - textentry - - - 10 - distance - - - $2C6 - textentry - - - 10 - distance - - - $2C7 - textentry - - - 10 - distance - - - $2C8 - textentry - - - 10 - distance - - - $2C9 - textentry - - - - - - - - - - - - - From 6ecac97fcd7730c366c3e67d794d349ee4d6ad09 Mon Sep 17 00:00:00 2001 From: SzymonPobiega Date: Tue, 24 Jun 2025 09:10:50 +0200 Subject: [PATCH 32/37] Removed unused class --- src/Shared/SendingEndpointControls.cs | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 src/Shared/SendingEndpointControls.cs diff --git a/src/Shared/SendingEndpointControls.cs b/src/Shared/SendingEndpointControls.cs deleted file mode 100644 index 12eca6e3..00000000 --- a/src/Shared/SendingEndpointControls.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Shared; - -public class SendingEndpointControls -{ - public void BindSendingDelayDial(UserInterface userInterface, char upKey, char downKey) - { - - } - - public void BindDuplicateLikelihoodDial(UserInterface userInterface, char upKey, char downKey) - { - - } - - public void BindManualModeToggle(UserInterface userInterface, char toggleKey) - { - - } - - public void BindManualSendButton(UserInterface userInterface, char key) - { - - } -} \ No newline at end of file From fe10b7b01a7fa006bd99136f7652a4ae8e969ef3 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Tue, 24 Jun 2025 12:44:22 -0400 Subject: [PATCH 33/37] Cleanup --- src/Billing/Billing.csproj | 1 - src/Billing/OrderPlacedHandler.cs | 1 - src/Billing/Program.cs | 2 -- src/ClientUI/ClientUI.csproj | 1 - src/ClientUI/Program.cs | 3 +-- src/ClientUI/SimulatedCustomers.cs | 4 ++-- src/MonitoringDemo/DemoLauncher.cs | 2 -- src/MonitoringDemo/KeyHelper.cs | 1 - src/MonitoringDemo/Program.cs | 2 -- src/Sales/PlaceOrderHandler.cs | 1 - src/Sales/Program.cs | 1 - src/Sales/Sales.csproj | 1 - src/Shared/DeterministicGuid.cs | 2 ++ src/Shared/IControl.cs | 2 ++ src/Shared/MessageIdHelper.cs | 4 +--- src/Shared/ProcessingEndpointControls.cs | 1 - src/Shared/RetrievingMessageProgressBehavior.cs | 1 - src/Shared/Shared.csproj | 1 + src/Shipping/Program.cs | 7 +------ src/Shipping/Shipping.csproj | 1 - 20 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 650f7f46..eb33ab31 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Billing/OrderPlacedHandler.cs b/src/Billing/OrderPlacedHandler.cs index d344d17b..b043d6de 100644 --- a/src/Billing/OrderPlacedHandler.cs +++ b/src/Billing/OrderPlacedHandler.cs @@ -1,5 +1,4 @@ using Messages; -using NServiceBus; using Shared; namespace Billing; diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index 47d1b36b..30200765 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -1,8 +1,6 @@ using System.Reflection; using System.Text.Json; -using Billing; using Messages; -using Microsoft.Extensions.DependencyInjection; using Shared; var instancePostfix = args.FirstOrDefault(); diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj index cc5be964..c29b83c3 100644 --- a/src/ClientUI/ClientUI.csproj +++ b/src/ClientUI/ClientUI.csproj @@ -14,7 +14,6 @@ - diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs index cdcd2805..595ec121 100644 --- a/src/ClientUI/Program.cs +++ b/src/ClientUI/Program.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using System.Reflection; +using System.Reflection; using System.Text.Json; using ClientUI; using Messages; diff --git a/src/ClientUI/SimulatedCustomers.cs b/src/ClientUI/SimulatedCustomers.cs index 41289f1d..94785ac1 100644 --- a/src/ClientUI/SimulatedCustomers.cs +++ b/src/ClientUI/SimulatedCustomers.cs @@ -7,14 +7,14 @@ class SimulatedCustomers(IEndpointInstance endpointInstance) { public void BindSendingRateDial(UserInterface userInterface, char upKey, char downKey) { - userInterface.BindDial('B', upKey, downKey, + userInterface.BindDial('B', upKey, downKey, $"Press {upKey} to increase sending rate.{Environment.NewLine}Press {downKey} to decrease it.", () => $"Sending rate: {rate}", x => rate = x + 1); //Rate is from 1 to 10 } public void BindDuplicateLikelihoodDial(UserInterface userInterface, char upKey, char downKey) { - userInterface.BindDial('C', upKey, downKey, + userInterface.BindDial('C', upKey, downKey, $"Press {upKey} to increase duplicate message rate.{Environment.NewLine}Press {downKey} to decrease it.", () => $"Duplicate rate: {duplicateLikelihood * 10}%", x => duplicateLikelihood = x); } diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index 331243d9..1d2ba812 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -43,6 +43,4 @@ public ProcessHandle AddProcess(string name, string instanceId, int port) private static readonly string SalesPath = Path.Combine("Sales", "Sales.dll"); private static readonly string ClientPath = Path.Combine("ClientUI", "ClientUI.dll"); private static readonly string PlatformPath = Path.Combine("PlatformLauncher", "PlatformLauncher.dll"); - - } \ No newline at end of file diff --git a/src/MonitoringDemo/KeyHelper.cs b/src/MonitoringDemo/KeyHelper.cs index bf557ac1..ebf60c3e 100644 --- a/src/MonitoringDemo/KeyHelper.cs +++ b/src/MonitoringDemo/KeyHelper.cs @@ -50,7 +50,6 @@ public static bool IsPartOfControllerSequence(this Key k, out string? sequence) sequence = null; return false; } - private static string? currentSequence; } \ No newline at end of file diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index 06cd8433..6257cd7c 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using MonitoringDemo; -using System.Reflection.Metadata; using Terminal.Gui; CancellationTokenSource tokenSource = new(); @@ -101,7 +100,6 @@ void ApplicationKeyDown(object? sender, Key e) Application.Shutdown(); return; - static void SwitchWindow(IReadOnlyCollection windowsToHide, View windowToShow, View focusTarget) { // Hide all other windows windows diff --git a/src/Sales/PlaceOrderHandler.cs b/src/Sales/PlaceOrderHandler.cs index bc0fda6b..dec04a74 100644 --- a/src/Sales/PlaceOrderHandler.cs +++ b/src/Sales/PlaceOrderHandler.cs @@ -1,5 +1,4 @@ using Messages; -using NServiceBus; using Shared; namespace Sales; diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs index 6ccda497..d487488d 100644 --- a/src/Sales/Program.cs +++ b/src/Sales/Program.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Reflection; using System.Text.Json; using Messages; diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj index afce7ebc..32277bba 100644 --- a/src/Sales/Sales.csproj +++ b/src/Sales/Sales.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Shared/DeterministicGuid.cs b/src/Shared/DeterministicGuid.cs index 92226822..1be178d5 100644 --- a/src/Shared/DeterministicGuid.cs +++ b/src/Shared/DeterministicGuid.cs @@ -1,6 +1,8 @@ using System.Security.Cryptography; using System.Text; +namespace Shared; + public static class DeterministicGuid { public static Guid Create(params object[] data) diff --git a/src/Shared/IControl.cs b/src/Shared/IControl.cs index eca11d29..5a50207d 100644 --- a/src/Shared/IControl.cs +++ b/src/Shared/IControl.cs @@ -3,6 +3,8 @@ namespace Shared; public interface IControl { bool Match(string input); + void Help(TextWriter textWriter); + void ReportState(TextWriter textWriter); } \ No newline at end of file diff --git a/src/Shared/MessageIdHelper.cs b/src/Shared/MessageIdHelper.cs index 23136f5c..84d11b94 100644 --- a/src/Shared/MessageIdHelper.cs +++ b/src/Shared/MessageIdHelper.cs @@ -1,5 +1,3 @@ -using NServiceBus.Extensibility; - namespace Shared; public static class MessageIdHelper @@ -8,7 +6,7 @@ public static class MessageIdHelper public static string GetHumanReadableMessageId() { - var messageId = new string(Enumerable.Range(0, 4).Select(x => Letters[Random.Shared.Next(Letters.Length)]).ToArray()); + var messageId = new string([.. Enumerable.Range(0, 4).Select(x => Letters[Random.Shared.Next(Letters.Length)])]); return messageId; } } \ No newline at end of file diff --git a/src/Shared/ProcessingEndpointControls.cs b/src/Shared/ProcessingEndpointControls.cs index 237d82b3..b6cb3837 100644 --- a/src/Shared/ProcessingEndpointControls.cs +++ b/src/Shared/ProcessingEndpointControls.cs @@ -48,7 +48,6 @@ public void Start() { Console.WriteLine(e); } - } }); } diff --git a/src/Shared/RetrievingMessageProgressBehavior.cs b/src/Shared/RetrievingMessageProgressBehavior.cs index 6a673252..360e0f63 100644 --- a/src/Shared/RetrievingMessageProgressBehavior.cs +++ b/src/Shared/RetrievingMessageProgressBehavior.cs @@ -1,5 +1,4 @@ using NServiceBus.Pipeline; -using Shared; namespace Shared; diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index 37dcfe0f..e7b6aaf4 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -10,4 +10,5 @@ + diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs index 2dc81c78..2041264c 100644 --- a/src/Shipping/Program.cs +++ b/src/Shipping/Program.cs @@ -1,12 +1,7 @@ -using System; -using System.Diagnostics; -using System.Reflection; +using System.Reflection; using System.Text.Json; using Messages; -using Microsoft.Extensions.DependencyInjection; -using NServiceBus; using Shared; -using Shipping; var instancePostfix = args.FirstOrDefault(); diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj index ab7eda15..09689433 100644 --- a/src/Shipping/Shipping.csproj +++ b/src/Shipping/Shipping.csproj @@ -14,7 +14,6 @@ - From 58c1acca65b3ddcbd552402b20591addfd8be3be Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 8 Aug 2025 16:26:42 -0400 Subject: [PATCH 34/37] Move MonitoringDemo into subfolder --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- src/MonitoringDemo/DemoLauncher.cs | 8 +------- src/MonitoringDemo/MonitoringDemo.bat | 1 + src/MonitoringDemo/MonitoringDemo.csproj | 13 ++++++++----- src/MonitoringDemo/MonitoringDemo.sh | 3 +++ src/MonitoringDemo/launch.ps1 | 1 - src/MonitoringDemo/launch.sh | 3 --- 8 files changed, 15 insertions(+), 18 deletions(-) create mode 100644 src/MonitoringDemo/MonitoringDemo.bat create mode 100755 src/MonitoringDemo/MonitoringDemo.sh delete mode 100644 src/MonitoringDemo/launch.ps1 delete mode 100755 src/MonitoringDemo/launch.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5369dd63..d4dadfd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: run: dotnet build src --configuration Release - name: Remove executables run: | - Remove-Item binaries/MonitoringDemo + Remove-Item binaries/MonitoringDemo/MonitoringDemo Remove-Item binaries/Billing/Billing Remove-Item binaries/ClientUI/ClientUI Remove-Item binaries/PlatformLauncher/PlatformLauncher diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16782c6b..f9bf9894 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet build src --configuration Release - name: Remove executables run: | - Remove-Item binaries/MonitoringDemo + Remove-Item binaries/MonitoringDemo/MonitoringDemo Remove-Item binaries/Billing/Billing Remove-Item binaries/ClientUI/ClientUI Remove-Item binaries/PlatformLauncher/PlatformLauncher diff --git a/src/MonitoringDemo/DemoLauncher.cs b/src/MonitoringDemo/DemoLauncher.cs index 1d2ba812..b6bdd30f 100644 --- a/src/MonitoringDemo/DemoLauncher.cs +++ b/src/MonitoringDemo/DemoLauncher.cs @@ -31,16 +31,10 @@ public ProcessHandle AddProcess(string name, string instanceId, int port) return ProcessHandle.Empty; } - var path = Path.Combine(name, $"{name}.dll"); //TODO: Hard-coded convention + var path = Path.Combine("..", name, $"{name}.dll"); //TODO: Hard-coded convention return demoProcessGroup.AddProcess(path, instanceId, port); } readonly ProcessGroup demoProcessGroup; private bool disposed; - - private static readonly string BillingPath = Path.Combine("Billing", "Billing.dll"); - private static readonly string ShippingPath = Path.Combine("Shipping", "Shipping.dll"); - private static readonly string SalesPath = Path.Combine("Sales", "Sales.dll"); - private static readonly string ClientPath = Path.Combine("ClientUI", "ClientUI.dll"); - private static readonly string PlatformPath = Path.Combine("PlatformLauncher", "PlatformLauncher.dll"); } \ No newline at end of file diff --git a/src/MonitoringDemo/MonitoringDemo.bat b/src/MonitoringDemo/MonitoringDemo.bat new file mode 100644 index 00000000..cb97b05f --- /dev/null +++ b/src/MonitoringDemo/MonitoringDemo.bat @@ -0,0 +1 @@ +dotnet MonitoringDemo\MonitoringDemo.dll \ No newline at end of file diff --git a/src/MonitoringDemo/MonitoringDemo.csproj b/src/MonitoringDemo/MonitoringDemo.csproj index 8adf54d9..1d9b9fc8 100644 --- a/src/MonitoringDemo/MonitoringDemo.csproj +++ b/src/MonitoringDemo/MonitoringDemo.csproj @@ -5,7 +5,7 @@ Exe enable enable - ..\..\binaries\ + ..\..\binaries\MonitoringDemo true @@ -13,9 +13,12 @@ - - - - + + + + + + + \ No newline at end of file diff --git a/src/MonitoringDemo/MonitoringDemo.sh b/src/MonitoringDemo/MonitoringDemo.sh new file mode 100755 index 00000000..d94a6e0a --- /dev/null +++ b/src/MonitoringDemo/MonitoringDemo.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +dotnet MonitoringDemo/MonitoringDemo.dll \ No newline at end of file diff --git a/src/MonitoringDemo/launch.ps1 b/src/MonitoringDemo/launch.ps1 deleted file mode 100644 index 8f621f2e..00000000 --- a/src/MonitoringDemo/launch.ps1 +++ /dev/null @@ -1 +0,0 @@ -dotnet MonitoringDemo.dll \ No newline at end of file diff --git a/src/MonitoringDemo/launch.sh b/src/MonitoringDemo/launch.sh deleted file mode 100755 index 5dcca52c..00000000 --- a/src/MonitoringDemo/launch.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -dotnet MonitoringDemo.dll \ No newline at end of file From c38086a9a4838241e8ea779a52412a7f73e064be Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 8 Aug 2025 16:33:39 -0400 Subject: [PATCH 35/37] Change window ordering to put Platform first --- src/MonitoringDemo/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index 6257cd7c..adc10a8a 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -19,17 +19,17 @@ var menuBarItems = new List(); ProcessWindow[] windows = []; -var clientWindow = CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, 10000, cancellationToken); var platformWindow = CreateWindow("Platform", "PlatformLauncher", "_Platform", true, 10010, cancellationToken); +var clientWindow = CreateWindow("ClientUI", "ClientUI", "_ClientUI", false, 10000, cancellationToken); var billingWindow = CreateWindow("Billing", "Billing", "_Billing", false, 10020, cancellationToken); var shippingWindow = CreateWindow("Shipping", "Shipping", "S_hipping", false, 10030, cancellationToken); var salesWindow = CreateWindow("Sales", "Sales", "_Sales", false, 10040, cancellationToken); windows = [ platformWindow, + clientWindow, billingWindow, shippingWindow, - clientWindow, salesWindow ]; From 2a35f517c2c7a3102b4a7009325c21a2b120d73a Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 5 Sep 2025 22:19:15 +0200 Subject: [PATCH 36/37] Update Terminal.Gui --- src/MonitoringDemo/KeyHelper.cs | 2 +- src/MonitoringDemo/MonitoringDemo.csproj | 2 +- src/MonitoringDemo/ProcessWindow.cs | 7 +++-- src/MonitoringDemo/Program.cs | 40 ++++++++++++++++-------- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/MonitoringDemo/KeyHelper.cs b/src/MonitoringDemo/KeyHelper.cs index ebf60c3e..8e7130d0 100644 --- a/src/MonitoringDemo/KeyHelper.cs +++ b/src/MonitoringDemo/KeyHelper.cs @@ -1,5 +1,5 @@ using System.Text; -using Terminal.Gui; +using Terminal.Gui.Input; namespace MonitoringDemo; diff --git a/src/MonitoringDemo/MonitoringDemo.csproj b/src/MonitoringDemo/MonitoringDemo.csproj index 1d9b9fc8..5af58f0d 100644 --- a/src/MonitoringDemo/MonitoringDemo.csproj +++ b/src/MonitoringDemo/MonitoringDemo.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/MonitoringDemo/ProcessWindow.cs b/src/MonitoringDemo/ProcessWindow.cs index 7b5b6c54..f8cf3fdd 100644 --- a/src/MonitoringDemo/ProcessWindow.cs +++ b/src/MonitoringDemo/ProcessWindow.cs @@ -3,8 +3,11 @@ using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; -using Terminal.Gui; -using Window = Terminal.Gui.Window; +using Terminal.Gui.App; +using Terminal.Gui.Input; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; + namespace MonitoringDemo; diff --git a/src/MonitoringDemo/Program.cs b/src/MonitoringDemo/Program.cs index adc10a8a..5e57ef34 100644 --- a/src/MonitoringDemo/Program.cs +++ b/src/MonitoringDemo/Program.cs @@ -1,6 +1,9 @@ using System.Diagnostics; using MonitoringDemo; -using Terminal.Gui; +using Terminal.Gui.App; +using Terminal.Gui.Input; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; CancellationTokenSource tokenSource = new(); var cancellationToken = tokenSource.Token; @@ -16,7 +19,7 @@ top.Width = Dim.Fill(); top.Height = Dim.Fill(); -var menuBarItems = new List(); +var menuBarItems = new List(); ProcessWindow[] windows = []; var platformWindow = CreateWindow("Platform", "PlatformLauncher", "_Platform", true, 10010, cancellationToken); @@ -33,17 +36,20 @@ salesWindow ]; -menuBarItems.Add( - new MenuBarItem("_Quit", "", () => - { - tokenSource.Cancel(); - Application.RequestStop(); - })); +var quitMenuBarItem = new MenuBarItemv2("_Quit"); +quitMenuBarItem.Accepting += (_, eventArgs) => +{ + tokenSource.Cancel(); + eventArgs.Handled = true; + Application.RequestStop(); +}; +menuBarItems.Add(quitMenuBarItem); -top.Add(new MenuBar +top.Add(new MenuBarv2 { - Menus = menuBarItems.ToArray() + Menus = [.. menuBarItems] }); + foreach (var window in windows) { top.Add(window); @@ -118,10 +124,18 @@ ProcessWindow CreateWindow(string title, string name, string menuItemText, bool var processWindow = new ProcessWindow(title, name, singleInstance, basePort, launcher, cancellationToken); var windowsToHide = windows.Except([processWindow]).ToArray(); - var menuItem = new MenuBarItem(menuItemText, "", - () => SwitchWindow(windowsToHide, processWindow, processWindow.LogView)); + var menuBarItem = new MenuBarItemv2(menuItemText) + { + Id = name, + Title = menuItemText, + }; + menuBarItem.Accepting += (_, eventArgs) => + { + SwitchWindow(windowsToHide, processWindow, processWindow.LogView); + eventArgs.Handled = true; + }; - menuBarItems.Add(menuItem); + menuBarItems.Add(menuBarItem); return processWindow; } From 1271db2d1dc330f1bca5950c58bdbcdb27db7a3e Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 20 Sep 2025 12:52:36 +0200 Subject: [PATCH 37/37] Treat scale out and in similar to the help standard input --- src/Shared/UserInterface.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Shared/UserInterface.cs b/src/Shared/UserInterface.cs index fe3bacc5..86381714 100644 --- a/src/Shared/UserInterface.cs +++ b/src/Shared/UserInterface.cs @@ -72,6 +72,7 @@ private void PrintControls() { ctrl.Help(Console.Out); } + Console.WriteLine("Page F2/F3 to scale out/in instances."); if (!Console.IsInputRedirected) { Console.WriteLine("Press ? for help");