diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml new file mode 100644 index 0000000..978e456 --- /dev/null +++ b/.github/workflows/native-aot.yml @@ -0,0 +1,50 @@ +name: Publish native AOT + +on: + push: + branches: [ disabled ] + +jobs: + build-aot: + name: Build Native AOT + strategy: + matrix: + # https://github.com/actions/runner-images + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + env: + # temp fix frontend build + # Treating warnings as errors because process.env.CI = true. + # Most CI servers set it automatically. + CI: 'false' + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + global-json-file: global.json + - uses: actions/setup-node@v4 + with: + # Version Spec of the version to use in SemVer notation. + # It also emits such aliases as lts, latest, nightly and canary builds + # Examples: 12.x, 10.15.1, >=10.15.0, lts/Hydrogen, 16-nightly, latest, node + node-version: 20 + - name: Publish AOT version + shell: pwsh + run: | + ./scripts/publish-native-aot.ps1 + # TODO join steps + - name: 'Upload Artifact linux-x64' + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: actions/upload-artifact@v4 + with: + name: Heartbeat-linux-x64 + path: artifacts/linux-x64/ + retention-days: 1 + - name: 'Upload Artifact win-x64' + if: ${{ matrix.os == 'windows-latest' }} + uses: actions/upload-artifact@v4 + with: + name: Heartbeat-win-x64 + path: artifacts/win-x64/ + retention-days: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dde65aa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,73 @@ +name: Create release + +on: + push: + tags: + - "v*.*.*" + +jobs: + build-aot: + name: Build Native AOT + strategy: + matrix: + # https://github.com/actions/runner-images + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + env: + # temp fix frontend build + # Treating warnings as errors because process.env.CI = true. + # Most CI servers set it automatically. + CI: 'false' + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + global-json-file: global.json + - uses: actions/setup-node@v4 + with: + # Version Spec of the version to use in SemVer notation. + # It also emits such aliases as lts, latest, nightly and canary builds + # Examples: 12.x, 10.15.1, >=10.15.0, lts/Hydrogen, 16-nightly, latest, node + node-version: 20 + - name: Publish AOT version + shell: pwsh + run: | + ./scripts/publish-native-aot.ps1 + # TODO join steps + - name: 'Upload Artifact linux-x64' + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: actions/upload-artifact@v4 + with: + name: Heartbeat-linux-x64 + path: artifacts/linux-x64/ + retention-days: 1 + - name: 'Upload Artifact win-x64' + if: ${{ matrix.os == 'windows-latest' }} + uses: actions/upload-artifact@v4 + with: + name: Heartbeat-win-x64 + path: artifacts/win-x64/ + retention-days: 1 + + release: + needs: build-aot + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + # https://github.com/actions/download-artifact + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: release-artifacts + merge-multiple: true + # https://github.com/softprops/action-gh-release + - name: Release + uses: softprops/action-gh-release@v1 + with: + draft: true + generate_release_notes: true + files: release-artifacts/** + + # TODO try https://github.com/marketplace/actions/release-drafter diff --git a/Directory.Build.props b/Directory.Build.props index 2ac07e6..f538a75 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ - 0.4.0 + 0.5.0 https://github.com/Ne4to/Heartbeat true MIT @@ -15,6 +15,10 @@ ClrMd diagnostics Diagnostics utility to analyze memory dumps of a .NET application + + + Debug;Release;DebugOpenAPI;ReleaseAOT + diff --git a/Heartbeat.sln b/Heartbeat.sln index 71d269e..9551b77 100644 --- a/Heartbeat.sln +++ b/Heartbeat.sln @@ -47,6 +47,8 @@ Global Release|arm64 = Release|arm64 Release|x64 = Release|x64 Release|x86 = Release|x86 + DebugOpenAPI|Any CPU = DebugOpenAPI|Any CPU + ReleaseAOT|Any CPU = ReleaseAOT|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9E63F5A0-7695-474C-A946-64D75F8D9617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -57,14 +59,6 @@ Global {9E63F5A0-7695-474C-A946-64D75F8D9617}.Debug|x64.Build.0 = Debug|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Debug|x86.ActiveCfg = Debug|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Debug|x86.Build.0 = Debug|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|Any CPU.ActiveCfg = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|Any CPU.Build.0 = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|arm64.ActiveCfg = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|arm64.Build.0 = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|x64.ActiveCfg = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|x64.Build.0 = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|x86.ActiveCfg = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|x86.Build.0 = DebugLocal|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|Any CPU.ActiveCfg = Release|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|Any CPU.Build.0 = Release|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|arm64.ActiveCfg = Release|Any CPU @@ -73,6 +67,10 @@ Global {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|x64.Build.0 = Release|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|x86.ActiveCfg = Release|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|x86.Build.0 = Release|Any CPU + {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugOpenAPI|Any CPU.Build.0 = DebugOpenAPI|Any CPU + {9E63F5A0-7695-474C-A946-64D75F8D9617}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU + {9E63F5A0-7695-474C-A946-64D75F8D9617}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -81,14 +79,6 @@ Global {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|x64.Build.0 = Debug|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|x86.ActiveCfg = Debug|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|x86.Build.0 = Debug|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|Any CPU.ActiveCfg = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|Any CPU.Build.0 = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|arm64.ActiveCfg = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|arm64.Build.0 = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|x64.ActiveCfg = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|x64.Build.0 = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|x86.ActiveCfg = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|x86.Build.0 = DebugLocal|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|Any CPU.Build.0 = Release|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|arm64.ActiveCfg = Release|Any CPU @@ -97,6 +87,10 @@ Global {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|x64.Build.0 = Release|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|x86.ActiveCfg = Release|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|x86.Build.0 = Release|Any CPU + {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugOpenAPI|Any CPU.Build.0 = DebugOpenAPI|Any CPU + {D4060CFE-8141-49CE-99A5-559599D0E6B4}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU + {D4060CFE-8141-49CE-99A5-559599D0E6B4}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -121,6 +115,8 @@ Global {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Release|x64.Build.0 = Release|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Release|x86.ActiveCfg = Release|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Release|x86.Build.0 = Release|Any CPU + {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Debug|Any CPU.Build.0 = Debug|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -145,6 +141,8 @@ Global {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Release|x64.Build.0 = Release|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Release|x86.ActiveCfg = Release|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Release|x86.Build.0 = Release|Any CPU + {789E65CA-B8F7-47B9-9013-B159D1E93F36}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {789E65CA-B8F7-47B9-9013-B159D1E93F36}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Debug|Any CPU.Build.0 = Debug|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -169,6 +167,8 @@ Global {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Release|x64.Build.0 = Release|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Release|x86.ActiveCfg = Release|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Release|x86.Build.0 = Release|Any CPU + {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -193,6 +193,8 @@ Global {3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x64.Build.0 = Release|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x86.ActiveCfg = Release|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x86.Build.0 = Release|Any CPU + {3D11554D-E09C-4710-B071-D90BB2447F46}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {3D11554D-E09C-4710-B071-D90BB2447F46}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.draft.md b/README.draft.md new file mode 100644 index 0000000..4194b4d --- /dev/null +++ b/README.draft.md @@ -0,0 +1,93 @@ +## Troubleshooting + +Unable to connect to [symbol server](https://github.com/microsoft/clrmd/blob/437022b361da20cf5f02d401a01c5e2c6c366097/src/Microsoft.Diagnostics.Runtime/Implementation/SymbolServer.cs#L17) +[clrmd docs](https://github.com/microsoft/clrmd/blob/437022b361da20cf5f02d401a01c5e2c6c366097/doc/GettingStarted.md?plain=1#L67) + +``` +Unhandled exception: System.AggregateException: One or more errors occurred. (The SSL connection could not be established, see inner exception.) + ---> System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. + ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot +``` + +TODO find minimum requirement + +`apt update & apt install -y curl` + +``` +Selecting previously unselected package openssl. +(Reading database ... 4393 files and directories currently installed.) +Preparing to unpack .../00-openssl_3.0.2-0ubuntu1.13_amd64.deb ... +Unpacking openssl (3.0.2-0ubuntu1.13) ... +Selecting previously unselected package ca-certificates. +Preparing to unpack .../01-ca-certificates_20230311ubuntu0.22.04.1_all.deb ... +Unpacking ca-certificates (20230311ubuntu0.22.04.1) ... +Selecting previously unselected package libnghttp2-14:amd64. +Preparing to unpack .../02-libnghttp2-14_1.43.0-1ubuntu0.1_amd64.deb ... +Unpacking libnghttp2-14:amd64 (1.43.0-1ubuntu0.1) ... +Selecting previously unselected package libpsl5:amd64. +Preparing to unpack .../03-libpsl5_0.21.0-1.2build2_amd64.deb ... +Unpacking libpsl5:amd64 (0.21.0-1.2build2) ... +Selecting previously unselected package publicsuffix. +Preparing to unpack .../04-publicsuffix_20211207.1025-1_all.deb ... +Unpacking publicsuffix (20211207.1025-1) ... +Selecting previously unselected package libbrotli1:amd64. +Preparing to unpack .../05-libbrotli1_1.0.9-2build6_amd64.deb ... +Unpacking libbrotli1:amd64 (1.0.9-2build6) ... +Selecting previously unselected package libsasl2-modules-db:amd64. +Preparing to unpack .../06-libsasl2-modules-db_2.1.27+dfsg2-3ubuntu1.2_amd64.deb ... +Unpacking libsasl2-modules-db:amd64 (2.1.27+dfsg2-3ubuntu1.2) ... +Selecting previously unselected package libsasl2-2:amd64. +Preparing to unpack .../07-libsasl2-2_2.1.27+dfsg2-3ubuntu1.2_amd64.deb ... +Unpacking libsasl2-2:amd64 (2.1.27+dfsg2-3ubuntu1.2) ... +Selecting previously unselected package libldap-2.5-0:amd64. +Preparing to unpack .../08-libldap-2.5-0_2.5.16+dfsg-0ubuntu0.22.04.1_amd64.deb ... +Unpacking libldap-2.5-0:amd64 (2.5.16+dfsg-0ubuntu0.22.04.1) ... +Selecting previously unselected package librtmp1:amd64. +Preparing to unpack .../09-librtmp1_2.4+20151223.gitfa8646d.1-2build4_amd64.deb ... +Unpacking librtmp1:amd64 (2.4+20151223.gitfa8646d.1-2build4) ... +Selecting previously unselected package libssh-4:amd64. +Preparing to unpack .../10-libssh-4_0.9.6-2ubuntu0.22.04.3_amd64.deb ... +Unpacking libssh-4:amd64 (0.9.6-2ubuntu0.22.04.3) ... +Selecting previously unselected package libcurl4:amd64. +Preparing to unpack .../11-libcurl4_7.81.0-1ubuntu1.15_amd64.deb ... +Unpacking libcurl4:amd64 (7.81.0-1ubuntu1.15) ... +Selecting previously unselected package curl. +Preparing to unpack .../12-curl_7.81.0-1ubuntu1.15_amd64.deb ... +Unpacking curl (7.81.0-1ubuntu1.15) ... +Selecting previously unselected package libldap-common. +Preparing to unpack .../13-libldap-common_2.5.16+dfsg-0ubuntu0.22.04.1_all.deb ... +Unpacking libldap-common (2.5.16+dfsg-0ubuntu0.22.04.1) ... +Selecting previously unselected package libsasl2-modules:amd64. +Preparing to unpack .../14-libsasl2-modules_2.1.27+dfsg2-3ubuntu1.2_amd64.deb ... +Unpacking libsasl2-modules:amd64 (2.1.27+dfsg2-3ubuntu1.2) ... +Setting up libpsl5:amd64 (0.21.0-1.2build2) ... +Setting up libbrotli1:amd64 (1.0.9-2build6) ... +Setting up libsasl2-modules:amd64 (2.1.27+dfsg2-3ubuntu1.2) ... +Setting up libnghttp2-14:amd64 (1.43.0-1ubuntu0.1) ... +Setting up libldap-common (2.5.16+dfsg-0ubuntu0.22.04.1) ... +Setting up libsasl2-modules-db:amd64 (2.1.27+dfsg2-3ubuntu1.2) ... +Setting up librtmp1:amd64 (2.4+20151223.gitfa8646d.1-2build4) ... +Setting up libsasl2-2:amd64 (2.1.27+dfsg2-3ubuntu1.2) ... +Setting up libssh-4:amd64 (0.9.6-2ubuntu0.22.04.3) ... +Setting up openssl (3.0.2-0ubuntu1.13) ... +Setting up publicsuffix (20211207.1025-1) ... +Setting up libldap-2.5-0:amd64 (2.5.16+dfsg-0ubuntu0.22.04.1) ... +Setting up ca-certificates (20230311ubuntu0.22.04.1) ... +debconf: unable to initialize frontend: Dialog +debconf: (No usable dialog-like program is installed, so the dialog based frontend cannot be used. at /usr/share/perl5/Debconf/FrontEnd/Dialog.pm line 78.) +debconf: falling back to frontend: Readline +debconf: unable to initialize frontend: Readline +debconf: (Can't locate Term/ReadLine.pm in @INC (you may need to install the Term::ReadLine module) (@INC contains: /etc/perl /usr/local/lib/x86_64-linux-gnu/perl/5.34.0 /usr/local/share/perl/5.34.0 /usr/lib/x86_64-linux-gnu/perl5/5.34 /usr/share/perl5 /usr/lib/x86_64-l +inux-gnu/perl-base /usr/lib/x86_64-linux-gnu/perl/5.34 /usr/share/perl/5.34 /usr/local/lib/site_perl) at /usr/share/perl5/Debconf/FrontEnd/Readline.pm line 7.) +debconf: falling back to frontend: Teletype +Updating certificates in /etc/ssl/certs... +137 added, 0 removed; done. +Setting up libcurl4:amd64 (7.81.0-1ubuntu1.15) ... +Setting up curl (7.81.0-1ubuntu1.15) ... +Processing triggers for libc-bin (2.35-0ubuntu3.6) ... +Processing triggers for ca-certificates (20230311ubuntu0.22.04.1) ... +Updating certificates in /etc/ssl/certs... +0 added, 0 removed; done. +Running hooks in /etc/ca-certificates/update.d... +done. +``` diff --git a/README.md b/README.md index 662973c..a9fd0ed 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ # Heartbeat [![NuGet Badge](https://buildstats.info/nuget/heartbeat?includePreReleases=true&dWidth=0)](https://www.nuget.org/packages/Heartbeat/) -Diagnostics utility with web UI to analyze memory dumps of a .NET application +Diagnostics utility with web UI to analyze .NET application memory dump ## Getting started +### dotnet tool + +.NET 8 SDK is required + ```shell dotnet tool install --global Heartbeat # optional @@ -12,7 +16,9 @@ export PATH=$PATH:$HOME/.dotnet/tools heartbeat --dump ``` Open `http://localhost:5000/` in web browser. -See [UI screen](https://github.com/Ne4to/Heartbeat/tree/master/assets) for examples + +### Native AOT +Heartbeat is also available in [Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) version. You can download it from the [latest release](https://github.com/Ne4to/Heartbeat/releases/latest) -## Usage +### Using Heartbeat ``` heartbeat [options] @@ -59,6 +65,9 @@ Options: --version Show version information -?, -h, --help Show help and usage information ``` - + +See [UI screen](https://github.com/Ne4to/Heartbeat/tree/master/assets) for examples. + +### Listening endpoint + +Use [ASPNETCORE_URLS](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-8.0) environment variable to change default endpoint (`export ASPNETCORE_URLS=http://0.0.0.0:5555` or `$env:ASPNETCORE_URLS='http://127.0.0.1:5555'`) diff --git a/scripts/clean-artifacts.ps1 b/scripts/clean-artifacts.ps1 new file mode 100644 index 0000000..26cb73d --- /dev/null +++ b/scripts/clean-artifacts.ps1 @@ -0,0 +1,10 @@ +$RepositoryRoot = Split-Path $PSScriptRoot +$ArtifactsRoot = Join-Path $RepositoryRoot 'artifacts' + +Remove-Item $ArtifactsRoot -Force -Recurse -ErrorAction SilentlyContinue +Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter bin | Remove-Item -Force -Recurse +Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter obj | Remove-Item -Force -Recurse +Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter app | Remove-Item -Force -Recurse +Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter nupkg | Remove-Item -Force -Recurse +Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter build | Remove-Item -Force -Recurse +Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter node_modules | Remove-Item -Force -Recurse diff --git a/scripts/publish-native-aot.ps1 b/scripts/publish-native-aot.ps1 new file mode 100644 index 0000000..6e079c5 --- /dev/null +++ b/scripts/publish-native-aot.ps1 @@ -0,0 +1,76 @@ +$ErrorActionPreference = "Stop" + +function Get-Runtime { + if ($IsWindows) { + return $env:PROCESSOR_ARCHITECTURE -eq 'AMD64' ? 'win-x64' : 'win-arm64' + } + + if ($IsLinux) { + return (uname -m) -eq 'aarch64' ? 'linux-arm64' : 'linux-x64' + } + + if ($IsMacOS) { + return (uname -m) -eq 'arm64' ? 'osx-arm64' : 'osx-x64' + } +} + +function Assert-ExitCode { + if (-not $?) { + throw 'Latest command failed' + } +} + +$Configuration = 'ReleaseAOT' +$RepositoryRoot = Split-Path $PSScriptRoot +$SpaRoot = Join-Path $RepositoryRoot 'src/Heartbeat/ClientApp' +$PublishProject = Join-Path $RepositoryRoot 'src/Heartbeat/Heartbeat.csproj' +$ArtifactsRoot = Join-Path $RepositoryRoot 'artifacts' + +Push-Location +try { + $SpaRoot + Set-Location $SpaRoot + npm install + npm run build + + Set-Location $RepositoryRoot + + # [xml]$XmlConfig = Get-Content 'Directory.Build.props' + + # $XmlElement = Select-Xml '/Project/PropertyGroup/VersionPrefix' $XmlConfig | + # Select-Object -ExpandProperty Node + + # $VersionPrefix = $XmlElement.InnerText + # $VersionSuffix = "rc.$(Get-Date -Format 'yyyy-MM-dd-HHmm')" + # $PackageVersion = "$VersionPrefix-$VersionSuffix" + + # dotnet clean --configuration $Configuration + + # https://learn.microsoft.com/en-us/dotnet/core/rid-catalog + # $Runtimes = @('win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64') + $Runtimes = @(Get-Runtime) + # TODO check `uname -m` for linux and macos + # $env:PROCESSOR_ARCHITECTURE + foreach ($Runtime in $Runtimes) { + $OutDir = Join-Path $ArtifactsRoot $Runtime + Write-Host "Publish native AOT version for $Runtime to $OutDir" + dotnet publish --configuration $Configuration --runtime $Runtime --output $OutDir $PublishProject + Assert-ExitCode + + Write-Host "Files in $OutDir" + Get-ChildItem $OutDir | Select-Object -Property Length,Name + } + + # TODO zip? +} +catch { + Write-Host 'Publish AOT - FAILED!' -ForegroundColor Red + throw +} +finally { + Pop-Location +} + +# TODO Cross-OS native compilation +# C:\Users\Ne4to\.nuget\packages\microsoft.dotnet.ilcompiler\8.0.1\build\Microsoft.NETCore.Native.Windows.targets(123,5): error : Platform linker not found. Ensure you have all the required prerequisites documented at https://aka.ms/nativeaot-prerequisites, in particular the Desktop Development for C++ workload in Visual Studio. For ARM64 development also install C++ ARM64 build tools. [C:\Users\Ne4to\projects\github.com\Ne4to\Heartbeat\src\Heartbeat\Heartbeat.csproj] +# C:\Users\Ne4to\.nuget\packages\microsoft.dotnet.ilcompiler\8.0.1\build\Microsoft.NETCore.Native.Publish.targets(60,5): error : Cross-OS native compilation is not supported. [C:\Users\Ne4to\projects\github.com\Ne4to\Heartbeat\src\Heartbeat\Heartbeat.csproj] \ No newline at end of file diff --git a/scripts/reinstall-dev-tool.ps1 b/scripts/reinstall-dev-tool.ps1 index 449f0b0..cb74db9 100644 --- a/scripts/reinstall-dev-tool.ps1 +++ b/scripts/reinstall-dev-tool.ps1 @@ -1,10 +1,23 @@ $ErrorActionPreference = "Stop" +function Assert-ExitCode { + if (-not $?) { + throw 'Latest command failed' + } +} + $RepositoryRoot = Split-Path $PSScriptRoot +$SpaRoot = Join-Path $RepositoryRoot 'src/Heartbeat/ClientApp' +$PublishProject = Join-Path $RepositoryRoot 'src/Heartbeat/Heartbeat.csproj' Push-Location try { + $SpaRoot + Set-Location $SpaRoot + npm install + npm run build + Set-Location $RepositoryRoot [xml]$XmlConfig = Get-Content 'Directory.Build.props' @@ -16,11 +29,14 @@ try dotnet tool uninstall -g Heartbeat dotnet clean --configuration Release - Get-Date -Format '' $VersionSuffix = "rc.$(Get-Date -Format 'yyyy-MM-dd-HHmm')" - dotnet pack --version-suffix $VersionSuffix + dotnet publish $PublishProject + Assert-ExitCode + dotnet pack --version-suffix $VersionSuffix $PublishProject + Assert-ExitCode $PackageVersion = "$VersionPrefix-$VersionSuffix" dotnet tool install --global --add-source ./src/Heartbeat/nupkg Heartbeat --version $PackageVersion + Assert-ExitCode } catch { Write-Host 'Install global tool - FAILED!' -ForegroundColor Red @@ -28,4 +44,7 @@ catch { } finally { Pop-Location -} \ No newline at end of file +} + + + diff --git a/scripts/update-ts-client.ps1 b/scripts/update-ts-client.ps1 index 718eaab..e2d4e9c 100644 --- a/scripts/update-ts-client.ps1 +++ b/scripts/update-ts-client.ps1 @@ -1,9 +1,10 @@ $ErrorActionPreference = "Stop" +$Configuration = 'DebugOpenAPI' $RepositoryRoot = Split-Path $PSScriptRoot $FrontendRoot = Join-Path $RepositoryRoot 'src/Heartbeat/ClientApp' $ContractPath = Join-Path $FrontendRoot 'api.yml' -$DllPath = Join-Path $RepositoryRoot 'src/Heartbeat/bin/Debug/net8.0/Heartbeat.dll' +$DllPath = Join-Path $RepositoryRoot "src/Heartbeat/bin/$Configuration/net8.0/Heartbeat.dll" Push-Location try @@ -11,10 +12,9 @@ try Set-Location $RepositoryRoot dotnet tool restore - dotnet build --configuration Debug - + dotnet build --configuration $Configuration + Set-Location $FrontendRoot - $env:HEARTBEAT_GENERATE_CONTRACTS = 'true' dotnet swagger tofile --yaml --output $ContractPath $DllPath Heartbeat dotnet kiota generate -l typescript --openapi $ContractPath -c HeartbeatClient -o ./src/client --clean-output @@ -26,5 +26,4 @@ catch { } finally { Pop-Location - $env:HEARTBEAT_GENERATE_CONTRACTS = $null } \ No newline at end of file diff --git a/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj b/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj index b77dc6e..f29ff4c 100644 --- a/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj +++ b/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj @@ -3,6 +3,10 @@ + + true + + @@ -11,8 +15,6 @@ - - all diff --git a/src/Heartbeat/ClientApp/api.yml b/src/Heartbeat/ClientApp/api.yml index 1a3c09c..97c4ce8 100644 --- a/src/Heartbeat/ClientApp/api.yml +++ b/src/Heartbeat/ClientApp/api.yml @@ -12,68 +12,64 @@ paths: get: tags: - Dump - summary: Get dump info operationId: GetInfo responses: - '500': - description: Server Error + '200': + description: OK content: application/json: schema: - $ref: '#/components/schemas/ProblemDetails' - '200': - description: Success + $ref: '#/components/schemas/DumpInfo' + '500': + description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/DumpInfo' + $ref: '#/components/schemas/ProblemDetails' /api/dump/modules: get: tags: - Dump - summary: Get modules operationId: GetModules responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Module' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/segments: get: tags: - Dump - summary: Get segments operationId: GetSegments responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/HeapSegment' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/roots: get: tags: - Dump - summary: Get heap roots operationId: GetRoots parameters: - name: kind @@ -82,25 +78,24 @@ paths: schema: $ref: '#/components/schemas/ClrRootKind' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/RootInfo' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/heap-dump-statistics: get: tags: - Dump - summary: Get heap dump statistics operationId: GetHeapDumpStat parameters: - name: gcStatus @@ -114,25 +109,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/ObjectTypeStatistics' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/strings: get: tags: - Dump - summary: Get heap dump statistics operationId: GetStrings parameters: - name: gcStatus @@ -146,25 +140,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/StringInfo' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/string-duplicates: get: tags: - Dump - summary: Get string duplicates operationId: GetStringDuplicates parameters: - name: gcStatus @@ -178,25 +171,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/StringDuplicate' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object-instances/{mt}': get: tags: - Dump - summary: Get object instances operationId: GetObjectInstances parameters: - name: mt @@ -217,23 +209,22 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error + '200': + description: OK content: application/json: schema: - $ref: '#/components/schemas/ProblemDetails' - '200': - description: Success + $ref: '#/components/schemas/GetObjectInstancesResult' + '500': + description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/GetObjectInstancesResult' + $ref: '#/components/schemas/ProblemDetails' /api/dump/arrays/sparse: get: tags: - Dump - summary: Get sparse arrays operationId: GetSparseArrays parameters: - name: gcStatus @@ -247,25 +238,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/ArrayInfo' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/arrays/sparse/stat: get: tags: - Dump - summary: Get arrays operationId: GetSparseArraysStat parameters: - name: gcStatus @@ -279,25 +269,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/SparseArrayStatistics' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object/{address}': get: tags: - Dump - summary: Get object operationId: GetClrObject parameters: - name: address @@ -308,14 +297,8 @@ paths: type: integer format: int64 responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: @@ -326,11 +309,16 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object/{address}/fields': get: tags: - Dump - summary: Get object fields operationId: GetClrObjectFields parameters: - name: address @@ -341,14 +329,8 @@ paths: type: integer format: int64 responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: @@ -361,11 +343,16 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object/{address}/roots': get: tags: - Dump - summary: Get object roots operationId: GetClrObjectRoots parameters: - name: address @@ -376,14 +363,8 @@ paths: type: integer format: int64 responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: @@ -396,6 +377,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' components: schemas: Architecture: diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts index 66a3615..de12874 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts @@ -28,7 +28,6 @@ export class SparseRequestBuilder extends BaseRequestBuilder new SparseRequestBuilder(x, y)); } /** - * Get sparse arrays * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of ArrayInfo */ @@ -42,7 +41,6 @@ export class SparseRequestBuilder extends BaseRequestBuilder(requestInfo, createArrayInfoFromDiscriminatorValue, errorMapping); } /** - * Get sparse arrays * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts index 904f308..d653a82 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts @@ -21,7 +21,6 @@ export class StatRequestBuilder extends BaseRequestBuilder { super(pathParameters, requestAdapter, "{+baseurl}/api/dump/arrays/sparse/stat{?gcStatus*,generation*}", (x, y) => new StatRequestBuilder(x, y)); } /** - * Get arrays * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of SparseArrayStatistics */ @@ -35,7 +34,6 @@ export class StatRequestBuilder extends BaseRequestBuilder { return this.requestAdapter.sendCollectionAsync(requestInfo, createSparseArrayStatisticsFromDiscriminatorValue, errorMapping); } /** - * Get arrays * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/heapDumpStatistics/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/heapDumpStatistics/index.ts index a840707..e4f268b 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/heapDumpStatistics/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/heapDumpStatistics/index.ts @@ -21,7 +21,6 @@ export class HeapDumpStatisticsRequestBuilder extends BaseRequestBuilder new HeapDumpStatisticsRequestBuilder(x, y)); } /** - * Get heap dump statistics * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of ObjectTypeStatistics */ @@ -35,7 +34,6 @@ export class HeapDumpStatisticsRequestBuilder extends BaseRequestBuilder(requestInfo, createObjectTypeStatisticsFromDiscriminatorValue, errorMapping); } /** - * Get heap dump statistics * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/info/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/info/index.ts index 145d642..39b2050 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/info/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/info/index.ts @@ -17,7 +17,6 @@ export class InfoRequestBuilder extends BaseRequestBuilder { super(pathParameters, requestAdapter, "{+baseurl}/api/dump/info", (x, y) => new InfoRequestBuilder(x, y)); } /** - * Get dump info * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of DumpInfo */ @@ -31,7 +30,6 @@ export class InfoRequestBuilder extends BaseRequestBuilder { return this.requestAdapter.sendAsync(requestInfo, createDumpInfoFromDiscriminatorValue, errorMapping); } /** - * Get dump info * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/modules/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/modules/index.ts index b42906c..e0bb107 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/modules/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/modules/index.ts @@ -17,7 +17,6 @@ export class ModulesRequestBuilder extends BaseRequestBuilder new ModulesRequestBuilder(x, y)); } /** - * Get modules * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of Module */ @@ -31,7 +30,6 @@ export class ModulesRequestBuilder extends BaseRequestBuilder(requestInfo, createModuleFromDiscriminatorValue, errorMapping); } /** - * Get modules * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts index 1d7c701..41ca43c 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts @@ -17,7 +17,6 @@ export class FieldsRequestBuilder extends BaseRequestBuilder new FieldsRequestBuilder(x, y)); } /** - * Get object fields * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of ClrObjectField */ @@ -32,7 +31,6 @@ export class FieldsRequestBuilder extends BaseRequestBuilder(requestInfo, createClrObjectFieldFromDiscriminatorValue, errorMapping); } /** - * Get object fields * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts index b274c22..547c321 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts @@ -31,7 +31,6 @@ export class WithAddressItemRequestBuilder extends BaseRequestBuilder new WithAddressItemRequestBuilder(x, y)); } /** - * Get object * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of GetClrObjectResult */ @@ -46,7 +45,6 @@ export class WithAddressItemRequestBuilder extends BaseRequestBuilder(requestInfo, createGetClrObjectResultFromDiscriminatorValue, errorMapping); } /** - * Get object * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/roots/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/roots/index.ts index c224129..d6f83af 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/roots/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/roots/index.ts @@ -17,7 +17,6 @@ export class RootsRequestBuilder extends BaseRequestBuilder super(pathParameters, requestAdapter, "{+baseurl}/api/dump/object/{address}/roots", (x, y) => new RootsRequestBuilder(x, y)); } /** - * Get object roots * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of ClrObjectRootPath */ @@ -32,7 +31,6 @@ export class RootsRequestBuilder extends BaseRequestBuilder return this.requestAdapter.sendCollectionAsync(requestInfo, createClrObjectRootPathFromDiscriminatorValue, errorMapping); } /** - * Get object roots * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/objectInstances/item/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/objectInstances/item/index.ts index 42321c2..45b0bb8 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/objectInstances/item/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/objectInstances/item/index.ts @@ -21,7 +21,6 @@ export class WithMtItemRequestBuilder extends BaseRequestBuilder new WithMtItemRequestBuilder(x, y)); } /** - * Get object instances * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of GetObjectInstancesResult */ @@ -35,7 +34,6 @@ export class WithMtItemRequestBuilder extends BaseRequestBuilder(requestInfo, createGetObjectInstancesResultFromDiscriminatorValue, errorMapping); } /** - * Get object instances * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/roots/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/roots/index.ts index e78f8d2..53001bc 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/roots/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/roots/index.ts @@ -20,7 +20,6 @@ export class RootsRequestBuilder extends BaseRequestBuilder super(pathParameters, requestAdapter, "{+baseurl}/api/dump/roots{?kind*}", (x, y) => new RootsRequestBuilder(x, y)); } /** - * Get heap roots * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of RootInfo */ @@ -34,7 +33,6 @@ export class RootsRequestBuilder extends BaseRequestBuilder return this.requestAdapter.sendCollectionAsync(requestInfo, createRootInfoFromDiscriminatorValue, errorMapping); } /** - * Get heap roots * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts index 3fcacc2..24adb7a 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts @@ -17,7 +17,6 @@ export class SegmentsRequestBuilder extends BaseRequestBuilder new SegmentsRequestBuilder(x, y)); } /** - * Get segments * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of HeapSegment */ @@ -31,7 +30,6 @@ export class SegmentsRequestBuilder extends BaseRequestBuilder(requestInfo, createHeapSegmentFromDiscriminatorValue, errorMapping); } /** - * Get segments * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/stringDuplicates/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/stringDuplicates/index.ts index 9297487..433614f 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/stringDuplicates/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/stringDuplicates/index.ts @@ -21,7 +21,6 @@ export class StringDuplicatesRequestBuilder extends BaseRequestBuilder new StringDuplicatesRequestBuilder(x, y)); } /** - * Get string duplicates * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of StringDuplicate */ @@ -35,7 +34,6 @@ export class StringDuplicatesRequestBuilder extends BaseRequestBuilder(requestInfo, createStringDuplicateFromDiscriminatorValue, errorMapping); } /** - * Get string duplicates * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/strings/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/strings/index.ts index 01bb66e..f259d74 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/strings/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/strings/index.ts @@ -21,7 +21,6 @@ export class StringsRequestBuilder extends BaseRequestBuilder new StringsRequestBuilder(x, y)); } /** - * Get heap dump statistics * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of StringInfo */ @@ -35,7 +34,6 @@ export class StringsRequestBuilder extends BaseRequestBuilder(requestInfo, createStringInfoFromDiscriminatorValue, errorMapping); } /** - * Get heap dump statistics * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/kiota-lock.json b/src/Heartbeat/ClientApp/src/client/kiota-lock.json index 1671570..4104768 100644 --- a/src/Heartbeat/ClientApp/src/client/kiota-lock.json +++ b/src/Heartbeat/ClientApp/src/client/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "E058DCE3BA746E408EADE32EA9E442AFC9D72743F2398CD19A5CCA613B92AB688674BFB60B624AA3AD06461E16CA4C2EBC6F9C5C62315B4F01C073DEF44C1C6C", + "descriptionHash": "2680CD432C0BAF2EAD8335B87589C6613B8A32E7D90AAF6EC468A74F00CDC479E9320B872E8B6E0C6AE5CDD8A0F2251B0991E07026BB56A11771AE55760D7F36", "descriptionLocation": "..\\..\\api.yml", "lockFileVersion": "1.0.0", "kiotaVersion": "1.10.1", diff --git a/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs new file mode 100644 index 0000000..2cae3ac --- /dev/null +++ b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace Heartbeat.Host.Endpoints; + +[JsonSourceGenerationOptions(UseStringEnumConverter = true)] +[JsonSerializable(typeof(DumpInfo))] +[JsonSerializable(typeof(GetObjectInstancesResult))] +[JsonSerializable(typeof(GetClrObjectResult))] +[JsonSerializable(typeof(Module[]))] +[JsonSerializable(typeof(ClrObjectField[]))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable>))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +internal partial class EndpointJsonSerializerContext : JsonSerializerContext; \ No newline at end of file diff --git a/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs b/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..376877e --- /dev/null +++ b/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Heartbeat.Host.Endpoints; + +internal static class EndpointRouteBuilderExtensions +{ + public static void MapDumpEndpoints(this IEndpointRouteBuilder app) + { + var dumpGroup = app.MapGroup("api/dump") + .CacheOutput() + .WithTags("Dump") + .WithOpenApi(); + + dumpGroup.MapGet("info", RouteHandlers.GetInfo) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetInfo"); + + dumpGroup.MapGet("modules", RouteHandlers.GetModules) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetModules"); + + dumpGroup.MapGet("segments", RouteHandlers.GetSegments) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetSegments"); + + dumpGroup.MapGet("roots", RouteHandlers.GetRoots) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetRoots"); + + dumpGroup.MapGet("heap-dump-statistics", RouteHandlers.GetHeapDumpStat) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetHeapDumpStat"); + + dumpGroup.MapGet("strings", RouteHandlers.GetStrings) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetStrings"); + + dumpGroup.MapGet("string-duplicates", RouteHandlers.GetStringDuplicates) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetStringDuplicates"); + + dumpGroup.MapGet("object-instances/{mt}", RouteHandlers.GetObjectInstances) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetObjectInstances"); + + dumpGroup.MapGet("arrays/sparse", RouteHandlers.GetSparseArrays) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetSparseArrays"); + + dumpGroup.MapGet("arrays/sparse/stat", RouteHandlers.GetSparseArraysStat) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetSparseArraysStat"); + + dumpGroup.MapGet("object/{address}", RouteHandlers.GetClrObject) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetClrObject"); + + dumpGroup.MapGet("object/{address}/fields", RouteHandlers.GetClrObjectFields) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetClrObjectFields"); + + dumpGroup.MapGet("object/{address}/roots", RouteHandlers.GetClrObjectRoots) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetClrObjectRoots"); + } +} \ No newline at end of file diff --git a/src/Heartbeat/Controllers/Models.cs b/src/Heartbeat/Endpoints/Models.cs similarity index 97% rename from src/Heartbeat/Controllers/Models.cs rename to src/Heartbeat/Endpoints/Models.cs index 06ccb75..e1dea58 100644 --- a/src/Heartbeat/Controllers/Models.cs +++ b/src/Heartbeat/Endpoints/Models.cs @@ -2,7 +2,7 @@ using System.Runtime.InteropServices; -namespace Heartbeat.Host.Controllers; +namespace Heartbeat.Host.Endpoints; // ReSharper disable NotAccessedPositionalProperty.Global public record DumpInfo( @@ -40,7 +40,7 @@ public record ClrObjectField( bool IsValueType, ulong? ObjectAddress, string Value, - string Name); + string? Name); public record Module(ulong Address, ulong Size, string? Name); @@ -56,6 +56,7 @@ public record StringDuplicate(string Value, int Count, int FullLength, ulong Was public record RootInfo(ulong Address, ClrRootKind Kind, bool IsPinned, ulong Size, ulong MethodTable, string TypeName); public record ClrObjectRootPath(RootInfo Root, IReadOnlyList PathItems); + public record RootPathItem(ulong Address, ulong MethodTable, string? TypeName, ulong Size, Generation Generation); public record ArrayInfo(ulong Address, ulong MethodTable, string? TypeName, int Length, int UnusedItemsCount, double UnusedPercent, ulong Wasted); diff --git a/src/Heartbeat/Controllers/DumpController.cs b/src/Heartbeat/Endpoints/RouteHandlers.cs similarity index 63% rename from src/Heartbeat/Controllers/DumpController.cs rename to src/Heartbeat/Endpoints/RouteHandlers.cs index fc96a50..e35eeca 100644 --- a/src/Heartbeat/Controllers/DumpController.cs +++ b/src/Heartbeat/Endpoints/RouteHandlers.cs @@ -5,62 +5,40 @@ using Heartbeat.Runtime.Extensions; using Heartbeat.Runtime.Proxies; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.OutputCaching; using Microsoft.Diagnostics.Runtime; -using Swashbuckle.AspNetCore.Annotations; - using System.Globalization; -using System.Net.Mime; - -namespace Heartbeat.Host.Controllers; - -[ApiController] -[OutputCache] -[Route("api/dump")] -[ApiExplorerSettings(GroupName = "Heartbeat")] -[Consumes(MediaTypeNames.Application.Json)] -[Produces(MediaTypeNames.Application.Json)] -[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] -public class DumpController : ControllerBase -{ - private readonly RuntimeContext _context; - public DumpController(RuntimeContext context) - { - _context = context; - } +namespace Heartbeat.Host.Endpoints; - [HttpGet] - [Route("info")] - [ProducesResponseType(typeof(DumpInfo), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get dump info")] - public DumpInfo GetInfo() +internal static class RouteHandlers +{ + public static DumpInfo GetInfo([FromServices] RuntimeContext context) { - var clrHeap = _context.Heap; - var clrInfo = _context.Runtime.ClrInfo; + var clrHeap = context.Heap; + var clrInfo = context.Runtime.ClrInfo; var dataReader = clrInfo.DataTarget.DataReader; + - return new DumpInfo( - _context.DumpPath, + var dumpInfo = new DumpInfo( + context.DumpPath, clrHeap.CanWalkHeap, clrHeap.IsServer, clrInfo.ModuleInfo.FileName, dataReader.Architecture, dataReader.ProcessId, dataReader.TargetPlatform.ToString(), - clrInfo.Version.ToString(2) + clrInfo.Version.ToString() ); + + return dumpInfo; } - [HttpGet] - [Route("modules")] - [ProducesResponseType(typeof(Module[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get modules")] - public IEnumerable GetModules() + public static Module[] GetModules([FromServices] RuntimeContext context) { - var modules = _context.Runtime + var modules = context.Runtime .EnumerateModules() .Select(m => new Module(m.Address, m.Size, m.Name)) .ToArray(); @@ -68,30 +46,23 @@ public IEnumerable GetModules() return modules; } - [HttpGet] - [Route("segments")] - [ProducesResponseType(typeof(HeapSegment[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get segments")] - public IEnumerable GetSegments() + public static IEnumerable GetSegments([FromServices] RuntimeContext context) { var segments = - from s in _context.Heap.Segments + from s in context.Heap.Segments select new HeapSegment( s.Start, s.End, - s.Kind); + s.Kind + ); return segments; } - [HttpGet] - [Route("roots")] - [ProducesResponseType(typeof(RootInfo[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get heap roots")] - public IEnumerable GetRoots([FromQuery] ClrRootKind? kind = null) + public static IEnumerable GetRoots([FromServices] RuntimeContext context, [FromQuery] ClrRootKind? kind = null) { return - from root in _context.Heap.EnumerateRoots() + from root in context.Heap.EnumerateRoots() where kind == null || root.RootKind == kind let objectType = root.Object.Type select new RootInfo( @@ -104,17 +75,14 @@ from root in _context.Heap.EnumerateRoots() ); } - [HttpGet] - [Route("heap-dump-statistics")] - [ProducesResponseType(typeof(ObjectTypeStatistics[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get heap dump statistics")] - public IEnumerable GetHeapDumpStat( + public static IEnumerable GetHeapDumpStat( + [FromServices] RuntimeContext context, [FromQuery] ObjectGCStatus? gcStatus = null, [FromQuery] Generation? generation = null) // TODO filter by just my code - how to filter Action? // TODO filter by type name { - var analyzer = new HeapDumpStatisticsAnalyzer(_context) { ObjectGcStatus = gcStatus, Generation = generation }; + var analyzer = new HeapDumpStatisticsAnalyzer(context) { ObjectGcStatus = gcStatus, Generation = generation }; var statistics = analyzer.GetObjectTypeStatistics() .OrderByDescending(s => s.TotalSize) @@ -124,17 +92,14 @@ public IEnumerable GetHeapDumpStat( return statistics; } - [HttpGet] - [Route("strings")] - [ProducesResponseType(typeof(StringInfo[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get heap dump statistics")] - public IEnumerable GetStrings( + public static IEnumerable GetStrings( + [FromServices] RuntimeContext context, [FromQuery] ObjectGCStatus? gcStatus = null, [FromQuery] Generation? generation = null) // TODO filter by min length // TODO filter by max length { - var query = from obj in _context.EnumerateStrings(gcStatus, generation) + var query = from obj in context.EnumerateStrings(gcStatus, generation) let str = obj.AsString() let length = obj.ReadField("_stringLength") select new StringInfo(obj.Address, length, obj.Size, str); @@ -143,26 +108,20 @@ public IEnumerable GetStrings( return query; } - [HttpGet] - [Route("string-duplicates")] - [ProducesResponseType(typeof(StringDuplicate[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get string duplicates")] - public IEnumerable GetStringDuplicates( + public static IEnumerable GetStringDuplicates( + [FromServices] RuntimeContext context, [FromQuery] ObjectGCStatus? gcStatus = null, [FromQuery] Generation? generation = null) // TODO filter by min length { - var analyzer = new StringDuplicateAnalyzer(_context) { ObjectGcStatus = gcStatus, Generation = generation }; + var analyzer = new StringDuplicateAnalyzer(context) { ObjectGcStatus = gcStatus, Generation = generation }; return analyzer.GetStringDuplicates() .Select(sd => new StringDuplicate(sd.Value, sd.Count, sd.FullLength, sd.WastedMemory)); } - [HttpGet] - [Route("object-instances/{mt}")] - [ProducesResponseType(typeof(GetObjectInstancesResult), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get object instances")] - public GetObjectInstancesResult GetObjectInstances( + public static GetObjectInstancesResult GetObjectInstances( + [FromServices] RuntimeContext context, ulong mt, [FromQuery] ObjectGCStatus? gcStatus = null, [FromQuery] Generation? generation = null) @@ -170,10 +129,10 @@ public GetObjectInstancesResult GetObjectInstances( { var methodTable = new MethodTable(mt); - var clrType = _context.Heap.FindTypeByMethodTable(methodTable); + var clrType = context.Heap.FindTypeByMethodTable(methodTable); var instances = ( - from obj in _context.EnumerateObjects(gcStatus, generation) + from obj in context.EnumerateObjects(gcStatus, generation) where obj.Type != null && obj.Type.MethodTable == methodTable orderby obj.Size descending @@ -187,20 +146,17 @@ orderby obj.Size descending return new GetObjectInstancesResult(methodTable, clrType?.Name, instances); } - [HttpGet] - [Route("arrays/sparse")] - [ProducesResponseType(typeof(ArrayInfo[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get sparse arrays")] // TODO add arrays // TODO add arrays/sparse // TODO add arrays/sparse/stat - public IEnumerable GetSparseArrays( + public static IEnumerable GetSparseArrays( + [FromServices] RuntimeContext context, [FromQuery] ObjectGCStatus? gcStatus = null, [FromQuery] Generation? generation = null) { - var query = from obj in _context.EnumerateObjects(gcStatus, generation) + var query = from obj in context.EnumerateObjects(gcStatus, generation) where obj.IsArray - let proxy = new ArrayProxy(_context, obj) + let proxy = new ArrayProxy(context, obj) where proxy.UnusedItemsPercent >= 0.2 orderby proxy.Wasted descending select new ArrayInfo(obj.Address, obj.Type.MethodTable, obj.Type.Name, proxy.Length, proxy.UnusedItemsCount, @@ -209,17 +165,14 @@ orderby proxy.Wasted descending return query.Take(100); } - [HttpGet] - [Route("arrays/sparse/stat")] - [ProducesResponseType(typeof(SparseArrayStatistics[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get arrays")] - public IEnumerable GetSparseArraysStat( + public static IEnumerable GetSparseArraysStat( + [FromServices] RuntimeContext context, [FromQuery] ObjectGCStatus? gcStatus = null, [FromQuery] Generation? generation = null) { - var query = from obj in _context.EnumerateObjects(gcStatus, generation) + var query = from obj in context.EnumerateObjects(gcStatus, generation) where obj.IsArray - let proxy = new ArrayProxy(_context, obj) + let proxy = new ArrayProxy(context, obj) where proxy.UnusedItemsCount != 0 group proxy by obj.Type.MethodTable into grp @@ -234,17 +187,12 @@ into grp return query; } - [HttpGet] - [Route("object/{address}")] - [ProducesResponseType(typeof(GetClrObjectResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [SwaggerOperation(summary: "Get object")] - public IActionResult GetClrObject(ulong address) + public static Results, NotFound> GetClrObject([FromServices] RuntimeContext context, ulong address) { - var clrObject = _context.Heap.GetObject(address); + var clrObject = context.Heap.GetObject(address); if (clrObject.Type == null) { - return NotFound(); + return TypedResults.NotFound(); } var result = new GetClrObjectResult( @@ -253,23 +201,18 @@ public IActionResult GetClrObject(ulong address) clrObject.Type.Name, clrObject.Type.MethodTable, clrObject.Size, - _context.Heap.GetGeneration(clrObject.Address), + context.Heap.GetGeneration(clrObject.Address), clrObject.Type.IsString ? clrObject.AsString() : null); - return Ok(result); + return TypedResults.Ok(result); } - [HttpGet] - [Route("object/{address}/fields")] - [ProducesResponseType(typeof(ClrObjectField[]), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [SwaggerOperation(summary: "Get object fields")] - public IActionResult GetClrObjectFields(ulong address) + public static Results, NotFound> GetClrObjectFields([FromServices] RuntimeContext context, ulong address) { - var clrObject = _context.Heap.GetObject(address); + var clrObject = context.Heap.GetObject(address); if (clrObject.Type == null) { - return NotFound(); + return TypedResults.NotFound(); } var fields = ( @@ -287,24 +230,19 @@ from field in clrObject.Type.Fields field.Name) ).ToArray(); - return Ok(fields); + return TypedResults.Ok(fields); } - [HttpGet] - [Route("object/{address}/roots")] - [ProducesResponseType(typeof(ClrObjectRootPath[]), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [SwaggerOperation(summary: "Get object roots")] - public IActionResult GetClrObjectRoots(ulong address, CancellationToken ct) + public static Results>, NotFound> GetClrObjectRoots([FromServices] RuntimeContext context, ulong address, CancellationToken ct) { - var clrObject = _context.Heap.GetObject(address); + var clrObject = context.Heap.GetObject(address); if (clrObject.Type == null) { - return NotFound(); + return TypedResults.NotFound(); } var result = new List(); - GCRoot gcRoot = new(_context.Heap, new[] { address }); + GCRoot gcRoot = new(context.Heap, new[] { address }); foreach ((ClrRoot root, GCRoot.ChainLink path) in gcRoot.EnumerateRootPaths(ct)) { var rootType = root.Object.Type!; @@ -323,14 +261,14 @@ public IActionResult GetClrObjectRoots(ulong address, CancellationToken ct) GCRoot.ChainLink? current = path; while (current != null) { - var obj = _context.Heap.GetObject(current.Object); + var obj = context.Heap.GetObject(current.Object); var item = new RootPathItem( obj.Address, obj.Type!.MethodTable, obj.Type.Name, obj.Size, - _context.Heap.GetGeneration(obj.Address)); + context.Heap.GetGeneration(obj.Address)); pathItems.Add(item); @@ -342,7 +280,7 @@ public IActionResult GetClrObjectRoots(ulong address, CancellationToken ct) break; } - return Ok(result); + return TypedResults.Ok(result); } private static Address? GetFieldObjectAddress(ClrInstanceField field, ulong address) @@ -386,12 +324,12 @@ string GetAddress() if (field.Type?.IsEnum ?? false) { ClrEnum enumField = field.Type.AsEnum(); - // TODO handle other types + // TODO handle other enum base types if (enumField.ElementType == ClrElementType.Int32) { var fieldValue = field.Read(address, false); var name = enumField.EnumerateValues() - .FirstOrDefault(v => (int)v.Value == fieldValue) + .FirstOrDefault(v => v.Value != null && (int)v.Value == fieldValue) .Name; return !string.IsNullOrEmpty(name) @@ -410,7 +348,7 @@ string GetAddress() if (clrObject.Type?.IsString ?? false) { - return clrObject.AsString(100); + return clrObject.AsString(100) ?? string.Empty; } if (clrObject is { IsNull: false, Type.Name: "System.Version" }) diff --git a/src/Heartbeat/Extensions/SwaggerExtensions.cs b/src/Heartbeat/Extensions/SwaggerExtensions.cs index 70cbff7..95b4f4b 100644 --- a/src/Heartbeat/Extensions/SwaggerExtensions.cs +++ b/src/Heartbeat/Extensions/SwaggerExtensions.cs @@ -1,9 +1,11 @@ +#if OPENAPI using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.SwaggerGen; +using System.Diagnostics; using System.Reflection; namespace Heartbeat.Host.Extensions; @@ -88,6 +90,7 @@ public void Apply(OpenApiSchema model, SchemaFilterContext context) } } + [Conditional("DEBUG")] private static void FixNullableProperties(OpenApiSchema schema, SchemaFilterContext context) { foreach (var property in schema.Properties) @@ -115,4 +118,5 @@ private static void FixNullableProperties(OpenApiSchema schema, SchemaFilterCont } } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/Heartbeat/Heartbeat.csproj b/src/Heartbeat/Heartbeat.csproj index 8abf4e5..8071128 100644 --- a/src/Heartbeat/Heartbeat.csproj +++ b/src/Heartbeat/Heartbeat.csproj @@ -14,6 +14,21 @@ npm start + + DEBUG;OPENAPI + + true + + + + true + true + RELEASE;AOT + true + false + true + + true true @@ -24,62 +39,59 @@ + - - - - - + + + + + + + + + + + - + + + + - - - - - + + + + - - - - - + + - + - - - - - - - - - - - + - - + - + wwwroot\%(RecursiveDir)%(FileName)%(Extension) PreserveNewest @@ -88,4 +100,4 @@ - + \ No newline at end of file diff --git a/src/Heartbeat/Program.cs b/src/Heartbeat/Program.cs index 11c08de..2b8c055 100644 --- a/src/Heartbeat/Program.cs +++ b/src/Heartbeat/Program.cs @@ -1,106 +1,108 @@ using Heartbeat.Host.CommandLine; -using Heartbeat.Host.Extensions; +using Heartbeat.Host.Endpoints; using Heartbeat.Runtime; -using Microsoft.AspNetCore.Diagnostics; +using Microsoft.Extensions.FileProviders; using System.CommandLine; -using System.Net.Mime; +using System.Diagnostics; + +#if OPENAPI +using Heartbeat.Host.Extensions; using System.Text.Json.Serialization; +#endif -if (Environment.GetEnvironmentVariable("HEARTBEAT_GENERATE_CONTRACTS") == "true") +#if OPENAPI +Console.WriteLine("Generating OpenAPI contract"); +var builder = WebApplication.CreateSlimBuilder(args); + +// workaround for https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2550 +builder.Services.AddControllers().AddJsonOptions(options => { - var builder = WebApplication.CreateBuilder(args); + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); - builder.Services - .AddControllers() - .AddJsonOptions( - options => - { - var enumConverter = new JsonStringEnumConverter(); - options.JsonSerializerOptions.Converters.Add(enumConverter); - }); - - builder.Services.AddSwagger(); - var app = builder.Build(); - app.UseSwagger(); - app.UseSwaggerUI(options => - { - options.SwaggerEndpoint("Heartbeat/swagger.yaml", "Heartbeat"); - }); - app.MapControllers(); - app.Run(); - return; -} +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, EndpointJsonSerializerContext.Default); +}); + +builder.Services.AddSwagger(); + +var app = builder.Build(); +app.UseSwagger(); +app.MapDumpEndpoints(); +app.Run(); +return; +#endif var (rootCommand, binder) = WebCommandOptions.RootCommand(); rootCommand.SetHandler((WebCommandOptions options) => MainWeb(options, args), binder); -//rootCommand.Add(AnalyzeCommandOptions.Command("analyze")); rootCommand.Invoke(args); -// TODO try native AOT - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot?view=aspnetcore-8.0 static void MainWeb(WebCommandOptions options, string[] args) { -#if !DEBUG +#if !DEBUG && !AOT // fix for static files when running as dotnet tool string rootDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!; Directory.SetCurrentDirectory(rootDir); #endif - var builder = WebApplication.CreateBuilder(args); - - builder.Services - .AddControllers() - .AddJsonOptions( - options => - { - var enumConverter = new JsonStringEnumConverter(); - options.JsonSerializerOptions.Converters.Add(enumConverter); - }); + var builder = WebApplication.CreateSlimBuilder(args); + + builder.Services.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.TypeInfoResolverChain.Insert(0, EndpointJsonSerializerContext.Default); + }); builder.Services.AddProblemDetails(); - builder.Services.AddSwagger(); builder.Services.AddOutputCache(); // TODO support auth -// TODO setup listening port var runtimeContext = new RuntimeContext(options.Dump.FullName, options.DacPath?.FullName, options.IgnoreDacMismatch ?? false); builder.Services.AddSingleton(runtimeContext); var app = builder.Build(); - app.UseDefaultFiles(); - app.UseStaticFiles(); - app.UseSwagger(); - app.UseSwaggerUI(options => - { - options.EnableTryItOutByDefault(); - options.SwaggerEndpoint("Heartbeat/swagger.yaml", "Heartbeat"); - }); - app.UseExceptionHandler(exceptionHandlerApp => - { - exceptionHandlerApp.Run(async context => - { - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - context.Response.ContentType = MediaTypeNames.Application.Json; - if (context.RequestServices.GetService() is { } problemDetailsService) - { - var exceptionHandlerFeature = context.Features.Get(); - var exceptionType = exceptionHandlerFeature?.Error; - await problemDetailsService.WriteAsync(new ProblemDetailsContext - { - HttpContext = context, - ProblemDetails = - { - Title = "An error occurred while processing your request.", - Detail = exceptionType?.Message, - Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1", - Status = 500 - } - }); - } - }); +#if AOT + var fileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly, "ClientApp/build"); + app.UseDefaultFiles(new DefaultFilesOptions + { + FileProvider = fileProvider }); + app.UseStaticFiles(new StaticFileOptions { FileProvider = fileProvider }); +#else + app.UseDefaultFiles(); + app.UseStaticFiles(); +#endif + app.UseStatusCodePages(async statusCodeContext + => await Results.Problem(statusCode: statusCodeContext.HttpContext.Response.StatusCode) + .ExecuteAsync(statusCodeContext.HttpContext)); + // app.UseExceptionHandler(exceptionHandlerApp => + // { + // exceptionHandlerApp.Run(async context => + // { + // context.Response.StatusCode = StatusCodes.Status500InternalServerError; + // context.Response.ContentType = MediaTypeNames.Application.Json; + // + // if (context.RequestServices.GetService() is { } problemDetailsService) + // { + // var exceptionHandlerFeature = context.Features.Get(); + // var exceptionType = exceptionHandlerFeature?.Error; + // await problemDetailsService.WriteAsync(new ProblemDetailsContext + // { + // HttpContext = context, + // ProblemDetails = + // { + // Title = "An error occurred while processing your request.", + // Detail = exceptionType?.Message, + // Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1", + // Status = 500 + // } + // }); + // } + // }); + // }); app.UseOutputCache(); - app.MapControllers(); + app.MapDumpEndpoints(); app.Run(); -} \ No newline at end of file +} diff --git a/src/Heartbeat/temp.props b/src/Heartbeat/temp.props new file mode 100644 index 0000000..25eecce --- /dev/null +++ b/src/Heartbeat/temp.props @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wwwroot\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + true + + + \ No newline at end of file