From 2d16152a9c9cad8ed71e8aae1f660d7025a9c498 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Wed, 26 Jul 2023 14:12:54 -0700 Subject: [PATCH] Linux support for FunctionsNetHost (#1704) * FunctionsNetHost in managed code (#1551) First iteration of Azure Functions Custom .NET Host(Validated in Windows) * Adding support for linux cold start (#1594) * Adding support for linux cold start. * Using native "get_hostfxr_path" method from "nethost". Removed PathResolver code (which was our version to do the same thing) * Update build YAML to copy all files instead of just .exe. * Linted yaml file. fixed the copy step to copy only needed dependencies * YAML * Get the NativeApplication instance from pointer and call the methods instead of static method call. * Revert "Get the NativeApplication instance from pointer and call the methods instead of static method call." This reverts commit 2f54a1cdff58d2e47388063f4b029d3b874f36ed. * Using NET8 Preview4. * NET6 support on Debian * Version bump for DotNetIsolatedNativeHost package * Add net6.0;net7.0; to TargetFrameworks. * Removing the shim for Windows. * Rebase on main + used csproj from origin main. Updated dotnetworker.csprok to include net6.0;net7.0; * Removed preivew tag(VersionSuffix) for now. Will add as needed when ready to release the packages. * Nit fixes to address PR feedback(indentation, copyright notice) * Added accidently deleted using statement block * Updating release notes. * Added a global.json for the FunctionsNetHost src directory to use .NET8 since our root level one uses 7.0. We use .NET AOT compiler to publish the FunctionsNetHost. * Fixed the version to 8.0.100-preview * Added "includePreviewVersions: true" to "Install current .NET SDK" build step. * Switching version to 8.0.100-preview.6 and "rollForward" value to "latestMinor" in FunctionsNetHost/src/global.json * Specifically set 7.0.306 in Install current .NET SDK step * Add a new UseDotnet task to install .net8 preview 6 in install-dotnet.yml * set includePreviewVersions true for new step * usesGlobalJson false for new UseDotnet task * use specific version 8.0.100-preview.6.23330.14 --- host/azure-pipelines.yml | 162 ++++++++++---- host/src/.clang-format | 2 - host/src/CMakeLists.txt | 28 --- host/src/CMakePresets.json | 64 ------ host/src/FunctionsNetHost.sln | 25 +++ .../FunctionsNetHost/AppLoader/AppLoader.cs | 107 ++++++++++ .../src/FunctionsNetHost/AppLoader/HostFxr.cs | 50 +++++ .../src/FunctionsNetHost/AppLoader/NetHost.cs | 31 +++ .../WorkerLoadStatusSignalManager.cs | 19 ++ host/src/FunctionsNetHost/CMakeLists.txt | 20 -- .../Environment/EnvironmentSettingNames.cs | 12 ++ .../Environment/EnvironmentUtils.cs | 37 ++++ .../FunctionsNetHost/FunctionsNetHost.csproj | 39 ++++ host/src/FunctionsNetHost/Grpc/GrpcClient.cs | 134 ++++++++++++ .../Grpc/GrpcWorkerStartupOptions.cs | 18 ++ .../Grpc/IncomingGrpcMessageHandler.cs | 101 +++++++++ .../FunctionsNetHost/Grpc/MessageChannel.cs | 61 ++++++ host/src/FunctionsNetHost/Grpc/PathUtils.cs | 53 +++++ host/src/FunctionsNetHost/Logger.cs | 46 ++++ .../FunctionsNetHost/Native/NativeExports.cs | 71 +++++++ .../Native/NativeHostApplication.cs | 32 +++ .../FunctionsNetHost/Native/NativeHostData.cs | 9 + host/src/FunctionsNetHost/Program.cs | 66 ++++++ .../Properties/launchSettings.json | 8 + host/src/FunctionsNetHost/exports.def | 4 + host/src/FunctionsNetHost/global.json | 7 + host/src/FunctionsNetHost/main.cpp | 89 -------- host/src/FunctionsNetHost/managedexports.cpp | 58 ----- host/src/Protos/CMakeLists.txt | 44 ---- host/src/README.md | 12 +- host/src/funcgrpc/CMakeLists.txt | 41 ---- host/src/funcgrpc/byte_buffer_helper.cpp | 49 ----- host/src/funcgrpc/byte_buffer_helper.h | 23 -- host/src/funcgrpc/func_bidi_reactor.cpp | 201 ------------------ host/src/funcgrpc/func_bidi_reactor.h | 110 ---------- host/src/funcgrpc/func_log.cpp | 22 -- host/src/funcgrpc/func_log.h | 35 --- host/src/funcgrpc/func_perf_marker.h | 35 --- host/src/funcgrpc/funcgrpc.h | 29 --- host/src/funcgrpc/funcgrpc_handlers.h | 28 --- .../funcgrpc_worker_config_handle.cpp | 52 ----- .../funcgrpc/funcgrpc_worker_config_handle.h | 27 --- .../handlers/funcgrpc_native_handler.cpp | 114 ---------- .../handlers/funcgrpc_native_handler.h | 38 ---- host/src/funcgrpc/messaging_channel.cpp | 27 --- host/src/funcgrpc/messaging_channel.h | 51 ----- host/src/funcgrpc/nativehostapplication.cpp | 119 ----------- host/src/funcgrpc/nativehostapplication.h | 70 ------ host/src/vcpkg.json | 14 -- ....Functions.DotnetIsolatedNativeHost.nuspec | 5 +- host/tools/build/worker.config.json | 18 +- release_notes.md | 2 +- .../DotNetWorker.Grpc.csproj | 7 +- .../NativeHostIntegration/NativeMethods.cs | 46 +++- .../Shim/NativeLibrary.Linux.cs | 49 +++++ src/DotNetWorker/DotNetWorker.csproj | 2 +- 56 files changed, 1172 insertions(+), 1451 deletions(-) delete mode 100644 host/src/.clang-format delete mode 100644 host/src/CMakeLists.txt delete mode 100644 host/src/CMakePresets.json create mode 100644 host/src/FunctionsNetHost.sln create mode 100644 host/src/FunctionsNetHost/AppLoader/AppLoader.cs create mode 100644 host/src/FunctionsNetHost/AppLoader/HostFxr.cs create mode 100644 host/src/FunctionsNetHost/AppLoader/NetHost.cs create mode 100644 host/src/FunctionsNetHost/AppLoader/WorkerLoadStatusSignalManager.cs delete mode 100644 host/src/FunctionsNetHost/CMakeLists.txt create mode 100644 host/src/FunctionsNetHost/Environment/EnvironmentSettingNames.cs create mode 100644 host/src/FunctionsNetHost/Environment/EnvironmentUtils.cs create mode 100644 host/src/FunctionsNetHost/FunctionsNetHost.csproj create mode 100644 host/src/FunctionsNetHost/Grpc/GrpcClient.cs create mode 100644 host/src/FunctionsNetHost/Grpc/GrpcWorkerStartupOptions.cs create mode 100644 host/src/FunctionsNetHost/Grpc/IncomingGrpcMessageHandler.cs create mode 100644 host/src/FunctionsNetHost/Grpc/MessageChannel.cs create mode 100644 host/src/FunctionsNetHost/Grpc/PathUtils.cs create mode 100644 host/src/FunctionsNetHost/Logger.cs create mode 100644 host/src/FunctionsNetHost/Native/NativeExports.cs create mode 100644 host/src/FunctionsNetHost/Native/NativeHostApplication.cs create mode 100644 host/src/FunctionsNetHost/Native/NativeHostData.cs create mode 100644 host/src/FunctionsNetHost/Program.cs create mode 100644 host/src/FunctionsNetHost/Properties/launchSettings.json create mode 100644 host/src/FunctionsNetHost/exports.def create mode 100644 host/src/FunctionsNetHost/global.json delete mode 100644 host/src/FunctionsNetHost/main.cpp delete mode 100644 host/src/FunctionsNetHost/managedexports.cpp delete mode 100644 host/src/Protos/CMakeLists.txt delete mode 100644 host/src/funcgrpc/CMakeLists.txt delete mode 100644 host/src/funcgrpc/byte_buffer_helper.cpp delete mode 100644 host/src/funcgrpc/byte_buffer_helper.h delete mode 100644 host/src/funcgrpc/func_bidi_reactor.cpp delete mode 100644 host/src/funcgrpc/func_bidi_reactor.h delete mode 100644 host/src/funcgrpc/func_log.cpp delete mode 100644 host/src/funcgrpc/func_log.h delete mode 100644 host/src/funcgrpc/func_perf_marker.h delete mode 100644 host/src/funcgrpc/funcgrpc.h delete mode 100644 host/src/funcgrpc/funcgrpc_handlers.h delete mode 100644 host/src/funcgrpc/funcgrpc_worker_config_handle.cpp delete mode 100644 host/src/funcgrpc/funcgrpc_worker_config_handle.h delete mode 100644 host/src/funcgrpc/handlers/funcgrpc_native_handler.cpp delete mode 100644 host/src/funcgrpc/handlers/funcgrpc_native_handler.h delete mode 100644 host/src/funcgrpc/messaging_channel.cpp delete mode 100644 host/src/funcgrpc/messaging_channel.h delete mode 100644 host/src/funcgrpc/nativehostapplication.cpp delete mode 100644 host/src/funcgrpc/nativehostapplication.h delete mode 100644 host/src/vcpkg.json create mode 100644 src/DotNetWorker.Grpc/NativeHostIntegration/Shim/NativeLibrary.Linux.cs diff --git a/host/azure-pipelines.yml b/host/azure-pipelines.yml index 2eb7fec2b..deb7d5476 100644 --- a/host/azure-pipelines.yml +++ b/host/azure-pipelines.yml @@ -1,14 +1,9 @@ -variables: - - name: VCPKG_BINARY_SOURCES - value: "clear;nuget,https://azfunc.pkgs.visualstudio.com/e6a70c92-4128-439f-8012-382fe78d6396/_packaging/FunctionsNetHostBinaryCache/nuget/v3/index.json,readwrite" - - name: VCPKG_USE_NUGET_CACHE - value: 1 - trigger: branches: include: - main - release/* + - feature/* paths: include: - host/src/ @@ -17,38 +12,131 @@ pr: include: - main - release/* + - feature/* paths: include: - host/src/ +stages: + - stage: BuildAndPublish + displayName: "Dotnet Publish(W+L)" + jobs: + - job: BuildAndPublishLinux + displayName: "Publish on Linux" + pool: + vmImage: "ubuntu-20.04" + steps: + - task: UseDotNet@2 + inputs: + version: "8.x" + includePreviewVersions: true + + - script: | + sudo apt-get install clang zlib1g-dev + + - task: DotnetCoreCLI@2 + displayName: "Dotnet Publish" + inputs: + command: "publish" + publishWebProjects: false + zipAfterPublish: false + arguments: "-c Release -r linux-x64 -o $(Build.ArtifactStagingDirectory)/output/linux" + workingDirectory: $(Build.SourcesDirectory)/host/src/FunctionsNetHost + + - task: CopyFiles@2 + displayName: "Copy needed files" + inputs: + SourceFolder: "$(Build.ArtifactStagingDirectory)/output/linux" + # Publish output will include many other files. We only need the FunctionsNetHost & libnethost.so + Contents: | + FunctionsNetHost + libnethost.so + TargetFolder: "$(Build.ArtifactStagingDirectory)/output/linux_filtered" + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: "$(Build.ArtifactStagingDirectory)/output/linux_filtered" + artifact: "linux_publish_output" + + - job: BuildAndPublishWindows + displayName: "Publish on Windows" + pool: + vmImage: "windows-latest" + steps: + - task: UseDotNet@2 + inputs: + version: "8.x" + includePreviewVersions: true + + - task: DotnetCoreCLI@2 + displayName: "Dotnet Publish" + inputs: + command: "publish" + publishWebProjects: false + zipAfterPublish: false + arguments: "-c Release -r win-x64 -o $(Build.ArtifactStagingDirectory)/output/windows" + workingDirectory: $(Build.SourcesDirectory)/host/src/FunctionsNetHost + + - task: CopyFiles@2 + displayName: "Copy needed files" + inputs: + SourceFolder: "$(Build.ArtifactStagingDirectory)/output/windows" + # Publish output will include many other files. We only need FunctionsNetHost.exe & nethost.dll + Contents: | + FunctionsNetHost.exe + nethost.dll + TargetFolder: "$(Build.ArtifactStagingDirectory)/output/windows_filtered" + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: "$(Build.ArtifactStagingDirectory)/output/windows_filtered" + artifact: "windows_publish_output" + + - stage: ConsolidateArtifacts + displayName: "Nuget Publish" + dependsOn: BuildAndPublish + jobs: + - job: ConsolidateArtifacts + displayName: "Consolidate Artifacts" + pool: + vmImage: "windows-latest" + steps: + - task: UseDotNet@2 + inputs: + version: "7.x" + + - task: DownloadPipelineArtifact@2 + displayName: "Download Artifacts from Linux build" + inputs: + artifactName: "linux_publish_output" + path: "$(Build.ArtifactStagingDirectory)/linux_output" + + - task: DownloadPipelineArtifact@2 + displayName: "Download Artifacts from Windows build" + inputs: + artifactName: "windows_publish_output" + path: "$(Build.ArtifactStagingDirectory)/windows_output" + + - task: CopyFiles@2 + displayName: "Copy files from linux artifacts to dist dir" + inputs: + SourceFolder: "$(Build.ArtifactStagingDirectory)/linux_output" + TargetFolder: "$(Build.SourcesDirectory)/host/dist/linux" + + - task: CopyFiles@2 + displayName: "Copy files from Windows artifacts to dist dir" + inputs: + SourceFolder: "$(Build.ArtifactStagingDirectory)/windows_output" + TargetFolder: "$(Build.SourcesDirectory)/host/dist/windows" + + - task: NuGetCommand@2 + displayName: "Nuget pack" + inputs: + command: "pack" + packagesToPack: "$(Build.SourcesDirectory)/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec" + versioningScheme: "off" + packDestination: "$(Build.ArtifactStagingDirectory)/host/dist/nuget" + basePath: "$(Build.SourcesDirectory)/host/tools/build" -pool: - vmImage: windows-latest - -steps: - # Remember to add this task to allow vcpkg to upload archives via NuGet - - task: NuGetAuthenticate@1 - - # Run Cmake and output goes to /build dir. - - task: CMake@1 - displayName: "CMake generation" - inputs: - cmakeArgs: "-S $(Build.SourcesDirectory)/host/src -B $(Build.SourcesDirectory)/host/build/win-x64" - - # Run Cmake --build which produces the native binaries. - - task: CMake@1 - displayName: "CMake build" - inputs: - cmakeArgs: "--build $(Build.SourcesDirectory)/host/build/win-x64 --config Release" - - - task: NuGetCommand@2 - displayName: "Nuget pack" - inputs: - command: "pack" - packagesToPack: "$(Build.SourcesDirectory)/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec" - versioningScheme: "off" - packDestination: "$(Build.ArtifactStagingDirectory)/host/build/nuget" - basePath: "$(Build.SourcesDirectory)/host/tools/build" - - # Publish artifacts. - - publish: $(Build.ArtifactStagingDirectory)/host/build/nuget - artifact: drop + # Publish artifacts. + - publish: $(Build.ArtifactStagingDirectory)/host/dist/nuget + artifact: drop diff --git a/host/src/.clang-format b/host/src/.clang-format deleted file mode 100644 index 2df4c36a4..000000000 --- a/host/src/.clang-format +++ /dev/null @@ -1,2 +0,0 @@ -Language: Cpp -BasedOnStyle: Microsoft \ No newline at end of file diff --git a/host/src/CMakeLists.txt b/host/src/CMakeLists.txt deleted file mode 100644 index 4612ba95c..000000000 --- a/host/src/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -# CMakeList.txt : Top-level CMake project file, do global configuration -# and include sub-projects here. -# -cmake_minimum_required (VERSION 3.23) - -include(FetchContent) -FetchContent_Declare(vcpkg - GIT_REPOSITORY https://github.com/microsoft/vcpkg/ - GIT_TAG 2022.11.14 -) -FetchContent_MakeAvailable(vcpkg) - -# NOTE: This must be defined before the first project call -set(CMAKE_TOOLCHAIN_FILE "${vcpkg_SOURCE_DIR}/scripts/buildsystems/vcpkg.cmake" CACHE FILEPATH "") - -set(VCPKG_INSTALL_OPTIONS "--debug") - -project ("FunctionsNetHost") -set(CMAKE_CXX_STANDARD 20) - -# Include sub-projects. -add_subdirectory("Protos") -add_subdirectory ("FunctionsNetHost") -add_subdirectory("funcgrpc") - -target_include_directories(FunctionsNetHost PUBLIC - "${PROJECT_BINARY_DIR}" - ) \ No newline at end of file diff --git a/host/src/CMakePresets.json b/host/src/CMakePresets.json deleted file mode 100644 index 08bb7a625..000000000 --- a/host/src/CMakePresets.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "version": 3, - "configurePresets": [ - { - "name": "windows-base", - "hidden": true, - "generator": "Ninja", - "binaryDir": "${sourceDir}/out/build/${presetName}", - "installDir": "${sourceDir}/out/install/${presetName}", - "cacheVariables": { - "CMAKE_C_COMPILER": "cl.exe", - "CMAKE_CXX_COMPILER": "cl.exe" - }, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - } - }, - { - "name": "x64-debug", - "displayName": "x64 Debug", - "inherits": "windows-base", - "architecture": { - "value": "x64", - "strategy": "external" - }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - }, - "environment": { - "Test": "test" - } - }, - { - "name": "x64-release", - "displayName": "x64 Release", - "inherits": "x64-debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "x86-debug", - "displayName": "x86 Debug", - "inherits": "windows-base", - "architecture": { - "value": "x86", - "strategy": "external" - }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "x86-release", - "displayName": "x86 Release", - "inherits": "x86-debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - } - ] -} diff --git a/host/src/FunctionsNetHost.sln b/host/src/FunctionsNetHost.sln new file mode 100644 index 000000000..4e8b85966 --- /dev/null +++ b/host/src/FunctionsNetHost.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33627.172 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionsNetHost", "FunctionsNetHost\FunctionsNetHost.csproj", "{6C05D0AC-F6AC-45FB-8A73-A3F44DF131BC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6C05D0AC-F6AC-45FB-8A73-A3F44DF131BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C05D0AC-F6AC-45FB-8A73-A3F44DF131BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C05D0AC-F6AC-45FB-8A73-A3F44DF131BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C05D0AC-F6AC-45FB-8A73-A3F44DF131BC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {32F98336-4B56-47A5-806E-0D1CC5F9F48B} + EndGlobalSection +EndGlobal diff --git a/host/src/FunctionsNetHost/AppLoader/AppLoader.cs b/host/src/FunctionsNetHost/AppLoader/AppLoader.cs new file mode 100644 index 000000000..fd3005688 --- /dev/null +++ b/host/src/FunctionsNetHost/AppLoader/AppLoader.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Runtime.InteropServices; + +namespace FunctionsNetHost +{ + /// + /// Manages loading hostfxr & worker assembly. + /// + internal sealed class AppLoader : IDisposable + { + private IntPtr _hostfxrHandle = IntPtr.Zero; + private IntPtr _hostContextHandle = IntPtr.Zero; + private bool _disposed; + + internal AppLoader() + { + LoadHostfxrLibrary(); + } + + private void LoadHostfxrLibrary() + { + // If having problems with the managed host, enable the following: + // Environment.SetEnvironmentVariable("COREHOST_TRACE", "1"); + // In Unix environment, you need to run the below command in the terminal to set the environment variable. + // export COREHOST_TRACE=1 + + var hostfxrFullPath = NetHost.GetHostFxrPath(); + Logger.LogTrace($"hostfxr path:{hostfxrFullPath}"); + + _hostfxrHandle = NativeLibrary.Load(hostfxrFullPath); + if (_hostfxrHandle == IntPtr.Zero) + { + Logger.Log($"Failed to load hostfxr. hostfxr path:{hostfxrFullPath}"); + return; + } + + Logger.LogTrace($"hostfxr library loaded successfully."); + } + + internal int RunApplication(string? assemblyPath) + { + ArgumentNullException.ThrowIfNull(assemblyPath, nameof(assemblyPath)); + + unsafe + { + var parameters = new HostFxr.hostfxr_initialize_parameters + { + size = sizeof(HostFxr.hostfxr_initialize_parameters) + }; + + var error = HostFxr.Initialize(1, new[] { assemblyPath }, ref parameters, out _hostContextHandle); + + if (_hostContextHandle == IntPtr.Zero) + { + Logger.Log( + $"Failed to initialize the .NET Core runtime. Assembly path:{assemblyPath}"); + return -1; + } + + if (error < 0) + { + return error; + } + + Logger.LogTrace($"hostfxr initialized with {assemblyPath}"); + HostFxr.SetAppContextData(_hostContextHandle, "AZURE_FUNCTIONS_NATIVE_HOST", "1"); + + return HostFxr.Run(_hostContextHandle); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (!disposing) + { + return; + } + + if (_hostfxrHandle != IntPtr.Zero) + { + NativeLibrary.Free(_hostfxrHandle); + Logger.LogTrace($"Freed hostfxr library handle"); + _hostfxrHandle = IntPtr.Zero; + } + + if (_hostContextHandle != IntPtr.Zero) + { + NativeLibrary.Free(_hostContextHandle); + Logger.LogTrace($"Freed hostcontext handle"); + _hostContextHandle = IntPtr.Zero; + } + + _disposed = true; + } + } + } +} diff --git a/host/src/FunctionsNetHost/AppLoader/HostFxr.cs b/host/src/FunctionsNetHost/AppLoader/HostFxr.cs new file mode 100644 index 000000000..7655ca919 --- /dev/null +++ b/host/src/FunctionsNetHost/AppLoader/HostFxr.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Runtime.InteropServices; + +namespace FunctionsNetHost +{ + static partial class HostFxr + { + public unsafe struct hostfxr_initialize_parameters + { + public nint size; + public char* host_path; + public char* dotnet_root; + }; + + [LibraryImport("hostfxr", EntryPoint = "hostfxr_initialize_for_dotnet_command_line")] + public unsafe static partial int Initialize( + int argc, + [MarshalAs(UnmanagedType.LPArray, ArraySubType = +#if OS_LINUX + UnmanagedType.LPStr +#else + UnmanagedType.LPWStr +#endif + )] string[] argv, + ref hostfxr_initialize_parameters parameters, + out IntPtr host_context_handle + ); + + [LibraryImport("hostfxr", EntryPoint = "hostfxr_run_app")] + public static partial int Run(IntPtr host_context_handle); + + [LibraryImport("hostfxr", EntryPoint = "hostfxr_set_runtime_property_value")] + public static partial int SetAppContextData(IntPtr host_context_handle, [MarshalAs( +#if OS_LINUX + UnmanagedType.LPStr +#else + UnmanagedType.LPWStr +#endif + )] string name, [MarshalAs( +#if OS_LINUX + UnmanagedType.LPStr +#else + UnmanagedType.LPWStr +#endif + )] string value); + + } +} diff --git a/host/src/FunctionsNetHost/AppLoader/NetHost.cs b/host/src/FunctionsNetHost/AppLoader/NetHost.cs new file mode 100644 index 000000000..61397c29f --- /dev/null +++ b/host/src/FunctionsNetHost/AppLoader/NetHost.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Runtime.InteropServices; + +namespace FunctionsNetHost +{ + internal class NetHost + { + [DllImport("nethost", CharSet = CharSet.Auto)] + private static extern int get_hostfxr_path( + [Out] char[] buffer, + [In] ref int buffer_size, + IntPtr reserved); + + internal static string GetHostFxrPath() + { + char[] buffer = new char[200]; + int bufferSize = buffer.Length; + + int rc = get_hostfxr_path(buffer, ref bufferSize, IntPtr.Zero); + + if (rc != 0) + { + throw new InvalidOperationException("Failed to get the hostfxr path."); + } + + return new string(buffer, 0, bufferSize - 1); + } + } +} diff --git a/host/src/FunctionsNetHost/AppLoader/WorkerLoadStatusSignalManager.cs b/host/src/FunctionsNetHost/AppLoader/WorkerLoadStatusSignalManager.cs new file mode 100644 index 000000000..6bd3f9e51 --- /dev/null +++ b/host/src/FunctionsNetHost/AppLoader/WorkerLoadStatusSignalManager.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace FunctionsNetHost; + +/// +/// Provides a signaling mechanism to wait and get notified about successful load of worker assembly. +/// +public class WorkerLoadStatusSignalManager +{ + private WorkerLoadStatusSignalManager() + { + Signal = new ManualResetEvent(false); + } + + public static WorkerLoadStatusSignalManager Instance { get; } = new(); + + public readonly ManualResetEvent Signal; +} diff --git a/host/src/FunctionsNetHost/CMakeLists.txt b/host/src/FunctionsNetHost/CMakeLists.txt deleted file mode 100644 index 6ada4967d..000000000 --- a/host/src/FunctionsNetHost/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -# CMakeList.txt : CMake project for FunctionsNetHost, include source and define -# project specific logic here. -# -add_executable (FunctionsNetHost "main.cpp" "managedexports.cpp") -target_link_libraries(FunctionsNetHost PRIVATE funcgrpc) - -set_target_properties(FunctionsNetHost PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE) - -if (CMAKE_VERSION VERSION_GREATER 3.12) - set_property(TARGET FunctionsNetHost PROPERTY CXX_STANDARD 20) -endif() - -find_package(Boost REQUIRED COMPONENTS program_options) -target_link_libraries(FunctionsNetHost PRIVATE Boost::program_options) - -if(NOT TARGET spdlog) - # Stand-alone build - find_package(spdlog REQUIRED) -endif() -target_link_libraries(FunctionsNetHost PRIVATE spdlog::spdlog) \ No newline at end of file diff --git a/host/src/FunctionsNetHost/Environment/EnvironmentSettingNames.cs b/host/src/FunctionsNetHost/Environment/EnvironmentSettingNames.cs new file mode 100644 index 000000000..f23da721b --- /dev/null +++ b/host/src/FunctionsNetHost/Environment/EnvironmentSettingNames.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace FunctionsNetHost; + +internal static class EnvironmentSettingNames +{ + /// + /// Set value to "1" for enabling extra trace logs in FunctionsNetHost. + /// + internal const string FunctionsNetHostTrace = "AZURE_FUNCTIONS_FUNCTIONSNETHOST_TRACE"; +} diff --git a/host/src/FunctionsNetHost/Environment/EnvironmentUtils.cs b/host/src/FunctionsNetHost/Environment/EnvironmentUtils.cs new file mode 100644 index 000000000..024a99315 --- /dev/null +++ b/host/src/FunctionsNetHost/Environment/EnvironmentUtils.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace FunctionsNetHost +{ + internal static class EnvironmentUtils + { +#if OS_LINUX + [System.Runtime.InteropServices.DllImport("libc")] + private static extern int setenv(string name, string value, int overwrite); +#endif + + /// + /// Gets the environment variable value. + /// + internal static string? GetValue(string environmentVariableName) + { + return Environment.GetEnvironmentVariable(environmentVariableName); + } + + /// + /// Sets the environment variable value. + /// + internal static void SetValue(string name, string value) + { + /* + * Environment.SetEnvironmentVariable is not setting the value of the parent process in Unix. + * So using the native method directly here. + * */ +#if OS_LINUX + setenv(name, value, 1); +#else + Environment.SetEnvironmentVariable(name, value); +#endif + } + } +} diff --git a/host/src/FunctionsNetHost/FunctionsNetHost.csproj b/host/src/FunctionsNetHost/FunctionsNetHost.csproj new file mode 100644 index 000000000..9a761b186 --- /dev/null +++ b/host/src/FunctionsNetHost/FunctionsNetHost.csproj @@ -0,0 +1,39 @@ + + + + Exe + net8.0 + enable + enable + True + true + Speed + true + + + + OS_LINUX + + + + $(MSBuildThisFileDirectory)exports.def + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/host/src/FunctionsNetHost/Grpc/GrpcClient.cs b/host/src/FunctionsNetHost/Grpc/GrpcClient.cs new file mode 100644 index 000000000..bbc1f7167 --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/GrpcClient.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Threading.Channels; +using Google.Protobuf; +using Grpc.Core; +using Grpc.Net.Client; +using Microsoft.Azure.Functions.Worker.Grpc.Messages; +using static Microsoft.Azure.Functions.Worker.Grpc.Messages.FunctionRpc; + +namespace FunctionsNetHost.Grpc +{ + internal sealed class GrpcClient + { + private readonly Channel _outgoingMessageChannel; + private readonly IncomingGrpcMessageHandler _messageHandler; + private readonly GrpcWorkerStartupOptions _grpcWorkerStartupOptions; + + internal GrpcClient(GrpcWorkerStartupOptions grpcWorkerStartupOptions, AppLoader appLoader) + { + _grpcWorkerStartupOptions = grpcWorkerStartupOptions; + var channelOptions = new UnboundedChannelOptions + { + SingleWriter = false, + SingleReader = false, + AllowSynchronousContinuations = true + }; + + _outgoingMessageChannel = Channel.CreateUnbounded(channelOptions); + + _messageHandler = new IncomingGrpcMessageHandler(appLoader); + } + + internal async Task InitAsync() + { + var endpoint = $"http://{_grpcWorkerStartupOptions.Host}:{_grpcWorkerStartupOptions.Port}"; + Logger.LogTrace($"Grpc service endpoint:{endpoint}"); + + var functionRpcClient = CreateFunctionRpcClient(endpoint); + var eventStream = functionRpcClient.EventStream(); + + await SendStartStreamMessageAsync(eventStream.RequestStream); + + var readerTask = StartReaderAsync(eventStream.ResponseStream); + var writerTask = StartWriterAsync(eventStream.RequestStream); + _ = StartInboundMessageForwarding(); + _ = StartOutboundMessageForwarding(); + + await Task.WhenAll(readerTask, writerTask); + } + + private async Task StartReaderAsync(IAsyncStreamReader responseStream) + { + while (await responseStream.MoveNext()) + { + await _messageHandler.ProcessMessageAsync(responseStream.Current); + } + } + + private async Task StartWriterAsync(IClientStreamWriter requestStream) + { + await foreach (var rpcWriteMsg in _outgoingMessageChannel.Reader.ReadAllAsync()) + { + await requestStream.WriteAsync(rpcWriteMsg); + } + } + + private async Task SendStartStreamMessageAsync(IClientStreamWriter requestStream) + { + var startStreamMsg = new StartStream() + { + WorkerId = _grpcWorkerStartupOptions.WorkerId + }; + + var startStream = new StreamingMessage() + { + StartStream = startStreamMsg + }; + + await requestStream.WriteAsync(startStream); + } + + private FunctionRpcClient CreateFunctionRpcClient(string endpoint) + { + if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var grpcUri)) + { + throw new InvalidOperationException($"The gRPC channel URI '{endpoint}' could not be parsed."); + } + + var grpcChannel = GrpcChannel.ForAddress(grpcUri, new GrpcChannelOptions() + { + MaxReceiveMessageSize = _grpcWorkerStartupOptions.GrpcMaxMessageLength, + MaxSendMessageSize = _grpcWorkerStartupOptions.GrpcMaxMessageLength, + Credentials = ChannelCredentials.Insecure + }); + + return new FunctionRpcClient(grpcChannel); + } + + /// + /// Listens to messages in the inbound message channel and forward them the customer payload via interop layer. + /// + private async Task StartInboundMessageForwarding() + { + await foreach (var inboundMessage in MessageChannel.Instance.InboundChannel.Reader.ReadAllAsync()) + { + await HandleIncomingMessage(inboundMessage); + } + } + + /// + /// Listens to messages in the inbound message channel and forward them the customer payload via interop layer. + /// + private async Task StartOutboundMessageForwarding() + { + await foreach (var outboundMessage in MessageChannel.Instance.OutboundChannel.Reader.ReadAllAsync()) + { + await _outgoingMessageChannel.Writer.WriteAsync(outboundMessage); + } + } + + private static Task HandleIncomingMessage(StreamingMessage inboundMessage) + { + // Queue the work to another thread. + Task.Run(() => + { + byte[] inboundMessageBytes = inboundMessage.ToByteArray(); + NativeHostApplication.Instance.HandleInboundMessage(inboundMessageBytes, inboundMessageBytes.Length); + }); + + return Task.CompletedTask; + } + } +} diff --git a/host/src/FunctionsNetHost/Grpc/GrpcWorkerStartupOptions.cs b/host/src/FunctionsNetHost/Grpc/GrpcWorkerStartupOptions.cs new file mode 100644 index 000000000..3ecf513b8 --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/GrpcWorkerStartupOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace FunctionsNetHost.Grpc +{ + internal sealed class GrpcWorkerStartupOptions + { + public string? Host { get; set; } + + public int Port { get; set; } + + public string? WorkerId { get; set; } + + public string? RequestId { get; set; } + + public int GrpcMaxMessageLength { get; set; } + } +} diff --git a/host/src/FunctionsNetHost/Grpc/IncomingGrpcMessageHandler.cs b/host/src/FunctionsNetHost/Grpc/IncomingGrpcMessageHandler.cs new file mode 100644 index 000000000..4beb947ad --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/IncomingGrpcMessageHandler.cs @@ -0,0 +1,101 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Azure.Functions.Worker.Grpc.Messages; + +namespace FunctionsNetHost.Grpc +{ + internal sealed class IncomingGrpcMessageHandler + { + private bool _specializationDone; + private readonly AppLoader _appLoader; + + internal IncomingGrpcMessageHandler(AppLoader appLoader) + { + _appLoader = appLoader; + } + + internal Task ProcessMessageAsync(StreamingMessage message) + { + Task.Run(() => Process(message)); + + return Task.CompletedTask; + } + + private async Task Process(StreamingMessage msg) + { + if (_specializationDone) + { + // Specialization done. So forward all messages to customer payload. + await MessageChannel.Instance.SendInboundAsync(msg); + return; + } + + var responseMessage = new StreamingMessage(); + + switch (msg.ContentCase) + { + case StreamingMessage.ContentOneofCase.WorkerInitRequest: + { + responseMessage.WorkerInitResponse = BuildWorkerInitResponse(); + break; + } + case StreamingMessage.ContentOneofCase.FunctionsMetadataRequest: + { + responseMessage.FunctionMetadataResponse = BuildFunctionMetadataResponse(); + break; + } + case StreamingMessage.ContentOneofCase.FunctionEnvironmentReloadRequest: + + Logger.LogTrace("Specialization request received."); + + var envReloadRequest = msg.FunctionEnvironmentReloadRequest; + foreach (var kv in envReloadRequest.EnvironmentVariables) + { + EnvironmentUtils.SetValue(kv.Key, kv.Value); + } + + var applicationExePath = PathUtils.GetApplicationExePath(envReloadRequest.FunctionAppDirectory); + Logger.LogTrace($"application path {applicationExePath}"); + +#pragma warning disable CS4014 + Task.Run(() => +#pragma warning restore CS4014 + { + _ = _appLoader.RunApplication(applicationExePath); + }); + + Logger.LogTrace($"Will wait for worker loaded signal."); + WorkerLoadStatusSignalManager.Instance.Signal.WaitOne(); + Logger.LogTrace($"Received worker loaded signal. Forwarding environment reload request to worker."); + + await MessageChannel.Instance.SendInboundAsync(msg); + _specializationDone = true; + break; + } + + await MessageChannel.Instance.SendOutboundAsync(responseMessage); + } + + private static FunctionMetadataResponse BuildFunctionMetadataResponse() + { + var metadataResponse = new FunctionMetadataResponse + { + UseDefaultMetadataIndexing = true, + Result = new StatusResult { Status = StatusResult.Types.Status.Success } + }; + + return metadataResponse; + } + + private static WorkerInitResponse BuildWorkerInitResponse() + { + var response = new WorkerInitResponse + { + Result = new StatusResult { Status = StatusResult.Types.Status.Success } + }; + + return response; + } + } +} diff --git a/host/src/FunctionsNetHost/Grpc/MessageChannel.cs b/host/src/FunctionsNetHost/Grpc/MessageChannel.cs new file mode 100644 index 000000000..bf2234c1c --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/MessageChannel.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Threading.Channels; +using Microsoft.Azure.Functions.Worker.Grpc.Messages; + +namespace FunctionsNetHost.Grpc +{ + /// + /// Bidirectional message channel meant to store inbound(to worker) and outbound(to host) messages. + /// + internal sealed class MessageChannel + { + private MessageChannel() + { + InboundChannel = Channel.CreateUnbounded(CreateUnboundedChannelOptions()); + OutboundChannel = Channel.CreateUnbounded(CreateUnboundedChannelOptions()); + } + + /// + /// Gets the instances of the messaging channel. + /// + internal static MessageChannel Instance { get; } = new(); + + /// + /// Messages which needs to go to worker payload gets pushed to this channel. + /// + internal Channel InboundChannel { get; } + + /// + /// Messages which needs to go to host gets pushed to this channel. + /// + internal Channel OutboundChannel { get; } + + /// + /// Pushes a message to the inbound channel(to worker). + /// + internal async Task SendInboundAsync(StreamingMessage inboundMessage) + { + await InboundChannel.Writer.WriteAsync(inboundMessage); + } + + /// + /// Pushes a messages to the outbound channel(to host) + /// + internal async Task SendOutboundAsync(StreamingMessage outboundMessage) + { + await OutboundChannel.Writer.WriteAsync(outboundMessage); + } + + private static UnboundedChannelOptions CreateUnboundedChannelOptions() + { + return new UnboundedChannelOptions + { + SingleWriter = false, + SingleReader = false, + AllowSynchronousContinuations = true + }; + } + } +} diff --git a/host/src/FunctionsNetHost/Grpc/PathUtils.cs b/host/src/FunctionsNetHost/Grpc/PathUtils.cs new file mode 100644 index 000000000..839d79d0d --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/PathUtils.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace FunctionsNetHost.Grpc +{ + internal static class PathUtils + { + /// + /// Gets the absolute path to worker application executable. + /// Builds the path by reading the worker.config.json + /// + /// The FunctionAppDirectory value from environment reload request. + internal static string? GetApplicationExePath(string applicationDirectory) + { + string jsonString = string.Empty; + string workerConfigPath = string.Empty; + try + { + workerConfigPath = Path.Combine(applicationDirectory, "worker.config.json"); + + jsonString = File.ReadAllText(workerConfigPath); + var workerConfigJsonNode = JsonNode.Parse(jsonString)!; + var executableName = workerConfigJsonNode["description"]?["defaultWorkerPath"]?.ToString(); + + if (executableName == null) + { + Logger.Log($"Invalid worker configuration. description > defaultWorkerPath property value is null. jsonString:{jsonString}"); + return null; + } + + return Path.Combine(applicationDirectory, executableName); + } + catch (FileNotFoundException ex) + { + Logger.Log($"{workerConfigPath} file not found.{ex}"); + return null; + } + catch (JsonException ex) + { + Logger.Log($"Error parsing JSON in GetApplicationExePath.{ex}. jsonString:{jsonString}"); + return null; + } + catch (Exception ex) + { + Logger.Log($"Error in GetApplicationExePath.{ex}. jsonString:{jsonString}"); + return null; + } + } + } +} diff --git a/host/src/FunctionsNetHost/Logger.cs b/host/src/FunctionsNetHost/Logger.cs new file mode 100644 index 000000000..5731bf7f4 --- /dev/null +++ b/host/src/FunctionsNetHost/Logger.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Globalization; + +namespace FunctionsNetHost +{ + internal static class Logger + { + private static readonly string LogPrefix; + + static Logger() + { +#if !DEBUG + LogPrefix = "LanguageWorkerConsoleLog"; +#else + LogPrefix = ""; +#endif + } + + internal static bool IsTraceLogEnabled + { + get + { + return string.Equals(EnvironmentUtils.GetValue(EnvironmentSettingNames.FunctionsNetHostTrace), "1"); + } + } + + /// + /// Logs a trace message if "AZURE_FUNCTIONS_FUNCTIONSNETHOST_TRACE" environment variable value is set to "1" + /// + internal static void LogTrace(string message) + { + if (IsTraceLogEnabled) + { + Log(message); + } + } + + internal static void Log(string message) + { + var ts = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); + Console.WriteLine($"{LogPrefix}[{ts}] [FunctionsNetHost] {message}"); + } + } +} diff --git a/host/src/FunctionsNetHost/Native/NativeExports.cs b/host/src/FunctionsNetHost/Native/NativeExports.cs new file mode 100644 index 000000000..2d44f3015 --- /dev/null +++ b/host/src/FunctionsNetHost/Native/NativeExports.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Runtime.InteropServices; +using FunctionsNetHost.Grpc; +using Microsoft.Azure.Functions.Worker.Grpc.Messages; + +namespace FunctionsNetHost +{ + public static class NativeExports + { + [UnmanagedCallersOnly(EntryPoint = "get_application_properties")] + public static int GetApplicationProperties(NativeHostData nativeHostData) + { + Logger.LogTrace("NativeExports.GetApplicationProperties method invoked."); + + try + { + var nativeHostApplication = NativeHostApplication.Instance; + GCHandle gch = GCHandle.Alloc(nativeHostApplication, GCHandleType.Pinned); + IntPtr pNativeApplication = gch.AddrOfPinnedObject(); + nativeHostData.PNativeApplication = pNativeApplication; + + return 1; + } + catch (Exception ex) + { + Logger.Log($"Error in NativeExports.GetApplicationProperties: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly(EntryPoint = "register_callbacks")] + public static unsafe int RegisterCallbacks(IntPtr pInProcessApplication, + delegate* unmanaged requestCallback, + IntPtr grpcHandler) + { + Logger.LogTrace("NativeExports.RegisterCallbacks method invoked."); + + try + { + NativeHostApplication.Instance.SetCallbackHandles(requestCallback, grpcHandler); + return 1; + } + catch (Exception ex) + { + Logger.Log($"Error in RegisterCallbacks: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly(EntryPoint = "send_streaming_message")] + public static unsafe int SendStreamingMessage(IntPtr pInProcessApplication, byte* streamingMessage, int streamingMessageSize) + { + try + { + var span = new ReadOnlySpan(streamingMessage, streamingMessageSize); + var outboundMessageToHost = StreamingMessage.Parser.ParseFrom(span); + + _ = MessageChannel.Instance.SendOutboundAsync(outboundMessageToHost); + + return 1; + } + catch (Exception ex) + { + Logger.Log($"Error in SendStreamingMessage: {ex}"); + return 0; + } + } + } +} diff --git a/host/src/FunctionsNetHost/Native/NativeHostApplication.cs b/host/src/FunctionsNetHost/Native/NativeHostApplication.cs new file mode 100644 index 000000000..8bcee10ed --- /dev/null +++ b/host/src/FunctionsNetHost/Native/NativeHostApplication.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace FunctionsNetHost +{ + public sealed class NativeHostApplication + { + private IntPtr _workerHandle; + unsafe delegate* unmanaged _requestHandlerCallback; + public static NativeHostApplication Instance { get; } = new(); + + private NativeHostApplication() + { + } + + public unsafe void HandleInboundMessage(byte[] buffer, int size) + { + fixed (byte* pBuffer = buffer) + { + _requestHandlerCallback(&pBuffer, size, _workerHandle); + } + } + + public unsafe void SetCallbackHandles(delegate* unmanaged callback, IntPtr grpcHandle) + { + _requestHandlerCallback = callback; + _workerHandle = grpcHandle; + + WorkerLoadStatusSignalManager.Instance.Signal.Set(); + } + } +} diff --git a/host/src/FunctionsNetHost/Native/NativeHostData.cs b/host/src/FunctionsNetHost/Native/NativeHostData.cs new file mode 100644 index 000000000..07162c18a --- /dev/null +++ b/host/src/FunctionsNetHost/Native/NativeHostData.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace FunctionsNetHost; + +public struct NativeHostData +{ + public IntPtr PNativeApplication; +} diff --git a/host/src/FunctionsNetHost/Program.cs b/host/src/FunctionsNetHost/Program.cs new file mode 100644 index 000000000..3f8def5eb --- /dev/null +++ b/host/src/FunctionsNetHost/Program.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.CommandLine; +using FunctionsNetHost.Grpc; + +namespace FunctionsNetHost +{ + internal class Program + { + static async Task Main(string[] args) + { + try + { + Logger.Log("Starting FunctionsNetHost"); + + var workerStartupOptions = await GetStartupOptionsFromCmdLineArgs(args); + + + using var appLoader = new AppLoader(); + var grpcClient = new GrpcClient(workerStartupOptions, appLoader); + + await grpcClient.InitAsync(); + } + catch (Exception exception) + { + Logger.Log($"An error occurred while running FunctionsNetHost.{exception}"); + } + } + + private static async Task GetStartupOptionsFromCmdLineArgs(string[] args) + { + var hostOption = new Option("--host"); + var portOption = new Option("--port"); + var workerOption = new Option("--workerId"); + var grpcMsgLengthOption = new Option("--grpcMaxMessageLength"); + var requestIdOption = new Option("--requestId"); + + var rootCommand = new RootCommand(); + rootCommand.AddOption(portOption); + rootCommand.AddOption(hostOption); + rootCommand.AddOption(workerOption); + rootCommand.AddOption(grpcMsgLengthOption); + rootCommand.AddOption(requestIdOption); + + var workerStartupOptions = new GrpcWorkerStartupOptions(); + + rootCommand.SetHandler((host, port, workerId, grpcMsgLength, requestId) => + { + workerStartupOptions.Host = host; + workerStartupOptions.Port = port; + workerStartupOptions.WorkerId = workerId; + workerStartupOptions.GrpcMaxMessageLength = grpcMsgLength; + workerStartupOptions.RequestId = requestId; + }, + hostOption, portOption, workerOption, grpcMsgLengthOption, requestIdOption); + + Logger.LogTrace($"raw args:{string.Join(" ", args)}"); + + var argsWithoutExecutableName = args.Skip(1).ToArray(); + await rootCommand.InvokeAsync(argsWithoutExecutableName); + + return workerStartupOptions; + } + } +} diff --git a/host/src/FunctionsNetHost/Properties/launchSettings.json b/host/src/FunctionsNetHost/Properties/launchSettings.json new file mode 100644 index 000000000..c6b3919b9 --- /dev/null +++ b/host/src/FunctionsNetHost/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "FunctionsNetHost": { + "commandName": "Project", + "commandLineArgs": "FunctionsNetHost.exe --host 127.0.0.1 --port 503 --workerId 13a2c943-ee61-449b-97ea-7c2577cbb1db --requestId 78522dbc-3bef-4ced-8988-bb3761c94e00 --grpcMaxMessageLength 2147483647" + } + } +} \ No newline at end of file diff --git a/host/src/FunctionsNetHost/exports.def b/host/src/FunctionsNetHost/exports.def new file mode 100644 index 000000000..b350d24ca --- /dev/null +++ b/host/src/FunctionsNetHost/exports.def @@ -0,0 +1,4 @@ +EXPORTS + get_application_properties + register_callbacks + send_streaming_message diff --git a/host/src/FunctionsNetHost/global.json b/host/src/FunctionsNetHost/global.json new file mode 100644 index 000000000..e90889403 --- /dev/null +++ b/host/src/FunctionsNetHost/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.100-preview.6.23330.14", + "allowPrerelease": true, + "rollForward": "latestMinor" + } +} \ No newline at end of file diff --git a/host/src/FunctionsNetHost/main.cpp b/host/src/FunctionsNetHost/main.cpp deleted file mode 100644 index a0d22d109..000000000 --- a/host/src/FunctionsNetHost/main.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#include "../funcgrpc/func_bidi_reactor.h" -#include "../funcgrpc/func_perf_marker.h" -#include -#include -#include -#include -#include - -using namespace std; -using namespace funcgrpc; -namespace po = boost::program_options; - -unique_ptr getWorkerStartupOptions(int argc, char *const *argv); - -int main(int argc, char *argv[]) -{ - funcgrpc::Log::Init(); - FUNC_LOG_INFO("Starting FunctionsNetHost."); - - try - { - auto pOptions = getWorkerStartupOptions(argc, argv); - auto pApplication = std::make_unique(); - auto worker = std::make_unique(pOptions.get(), pApplication.get()); - Status status = worker->Await(); - - if (!status.ok()) - { - FUNC_LOG_ERROR("Rpc failed. error_message:{}", status.error_message()); - } - } - catch (const std::exception &ex) - { - FUNC_LOG_ERROR("Caught unknown exception.{}", ex.what()); - } - catch (...) - { - FUNC_LOG_ERROR("Caught unknown exception."); - } - - return 0; -} - -unique_ptr getWorkerStartupOptions(int argc, char *const *argv) -{ - FuncPerfMarker marker("BuildWorkerStartupOptions"); - - po::options_description desc("Allowed options"); - desc.add_options()("help", "sample usage: FunctionsNetHost --host --port --workerid " - "--requestid --grpcmaxrequestlength ")( - "host", boost::program_options::value(), - "Address of grpc server")("port", po::value(), "Port number of grpc server connection")( - "workerId", boost::program_options::value(), - "Worker id")("requestId", boost::program_options::value(), - "Request id")("grpcMaxMessageLength", po::value()->default_value(INT_MAX), - "Max length for grpc messages. Default is INT_MAX"); - - po::variables_map vm; - po::store(po::parse_command_line(argc, argv, desc), vm); - po::notify(vm); - - auto options = make_unique(); - - if (vm.count("host")) - { - options->host = vm["host"].as(); - } - if (vm.count("port")) - { - options->port = vm["port"].as(); - } - if (vm.count("workerId")) - { - options->workerId = vm["workerId"].as(); - } - if (vm.count("requestId")) - { - options->requestId = vm["requestId"].as(); - } - if (vm.count("grpcMaxMessageLength")) - { - options->grpcMaxMessageLength = vm["grpcMaxMessageLength"].as(); - } - - return options; -} diff --git a/host/src/FunctionsNetHost/managedexports.cpp b/host/src/FunctionsNetHost/managedexports.cpp deleted file mode 100644 index 457fab678..000000000 --- a/host/src/FunctionsNetHost/managedexports.cpp +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#include "../funcgrpc/byte_buffer_helper.h" -#include "../funcgrpc/func_log.h" -#include "../funcgrpc/nativehostapplication.h" -#include -#include -struct NativeHostData -{ - NativeHostApplication *pNativeApplication; -}; - -extern "C" __declspec(dllexport) HRESULT get_application_properties(_In_ NativeHostData *pNativeHostData) -{ - auto pInProcessApplication = NativeHostApplication::GetInstance(); - - if (pInProcessApplication == nullptr) - { - return E_FAIL; - } - - pNativeHostData->pNativeApplication = pInProcessApplication; - - return S_OK; -} - -extern "C" __declspec(dllexport) HRESULT send_streaming_message(_In_ NativeHostApplication *pInProcessApplication, - _In_ char *managedMessage, _In_ int managedMessageSize) -{ - FUNC_LOG_DEBUG("Calling send_streaming_message. managedMessageSize:{}", managedMessageSize); - - if (managedMessageSize == 0) - { - FUNC_LOG_WARN("send_streaming_message. size 0"); - return S_OK; - } - - auto bbUPtr = funcgrpc::SerializeToByteBufferFromChar(managedMessage, managedMessageSize); - auto byteBuffer = bbUPtr.get(); - pInProcessApplication->SendOutgoingMessage(byteBuffer); - - return S_OK; -} - -extern "C" __declspec(dllexport) HRESULT - register_callbacks(_In_ NativeHostApplication *pInProcessApplication, _In_ PFN_REQUEST_HANDLER request_handler, - _In_ VOID *grpcHandler) -{ - if (pInProcessApplication == nullptr) - { - return E_INVALIDARG; - } - - pInProcessApplication->SetCallbackHandles(request_handler, grpcHandler); - - return S_OK; -} \ No newline at end of file diff --git a/host/src/Protos/CMakeLists.txt b/host/src/Protos/CMakeLists.txt deleted file mode 100644 index e118f2806..000000000 --- a/host/src/Protos/CMakeLists.txt +++ /dev/null @@ -1,44 +0,0 @@ -find_package(protobuf CONFIG REQUIRED) -find_package(gRPC CONFIG REQUIRED) -find_package(Threads) - -# We cannot directly refer the proto files in the repo root as that dir is not a CMAKE Source dir. -# So we will copy protos files to a relative location. -get_filename_component(HOST_DIR ${CMAKE_SOURCE_DIR} DIRECTORY) -get_filename_component(REPO_ROOT ${HOST_DIR} DIRECTORY) -SET (PROTOS_TARGET_DIR ${CMAKE_SOURCE_DIR}/Protos) -SET (PROTOS_SRC_DIR ${REPO_ROOT}/protos/azure-functions-language-worker-protobuf/src/proto) - -# Copy proto files to the "Protos" directory(where this CMakeList.txt file is present). -file(COPY ${PROTOS_SRC_DIR}/shared/NullableTypes.proto DESTINATION ${PROTOS_TARGET_DIR}/shared) -file(COPY ${PROTOS_SRC_DIR}/identity/ClaimsIdentityRpc.proto DESTINATION ${PROTOS_TARGET_DIR}/identity) -file(COPY ${PROTOS_SRC_DIR}/FunctionRpc.proto DESTINATION ${PROTOS_TARGET_DIR}) - -# -# Protobuf/Grpc source files for function RPC -# -set(PROTO_FILES - FunctionRpc.proto - shared/NullableTypes.proto - identity/ClaimsIdentityRpc.proto -) - -# -# Add Library target with protobuf sources -# -add_library(func_protos ${PROTO_FILES}) -target_link_libraries(func_protos - PUBLIC - protobuf::libprotobuf - gRPC::grpc - gRPC::grpc++ -) - -target_include_directories(func_protos PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) - -# -# Compile protobuf and grpc files -# -get_target_property(grpc_cpp_plugin_location gRPC::grpc_cpp_plugin LOCATION) -protobuf_generate(TARGET func_protos LANGUAGE cpp) -protobuf_generate(TARGET func_protos LANGUAGE grpc GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc PLUGIN "protoc-gen-grpc=${grpc_cpp_plugin_location}") \ No newline at end of file diff --git a/host/src/README.md b/host/src/README.md index 03d672106..e9ce2556d 100644 --- a/host/src/README.md +++ b/host/src/README.md @@ -1,11 +1,7 @@ +# FunctionsNetHost -This is the project root where we have our root level CMakeLists.txt. -### Load & Build. +Managed code version of FunctionsNetHost. -Open this directory in Visual studio(Open a local folder). -VS will read the CMakeLists.txt and start executing CMake. -We use [vcpkg](https://vcpkg.io) for dependency management. When CMake execution starts, it will start downloading the dependencies to the build output directory.This may take a while to finish the first time. +## Publish -Dependencies are listed in the vcpkg.json file. - -Once CMake generation is done, build the code by Build-> Build All (or F6 key). It will do compilation and linking. +Open a terminal here and run `dotnet publish -c release -r win-x64`. This will produce the native exe in `FunctionsNetHost\bin\Release\net7.0\win-x64\publish\` directory. diff --git a/host/src/funcgrpc/CMakeLists.txt b/host/src/funcgrpc/CMakeLists.txt deleted file mode 100644 index d11dfd336..000000000 --- a/host/src/funcgrpc/CMakeLists.txt +++ /dev/null @@ -1,41 +0,0 @@ -# Add source to this project's executable. -add_library(funcgrpc - "funcgrpc_handlers.h" - "funcgrpc.h" - "func_bidi_reactor.h" - "func_bidi_reactor.cpp" - "handlers/funcgrpc_native_handler.h" - "handlers/funcgrpc_native_handler.cpp" - "messaging_channel.h" - "messaging_channel.cpp" - "nativehostapplication.cpp" - "nativehostapplication.h" - func_perf_marker.h - func_log.cpp - func_log.h - funcgrpc_worker_config_handle.cpp - funcgrpc_worker_config_handle.h - byte_buffer_helper.cpp - byte_buffer_helper.h) - -# set_target_properties(funcgrpc PROPERTIES ENABLE_EXPORTS TRUE) -# set_target_properties(funcgrpc PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE) - -target_link_libraries(funcgrpc PUBLIC func_protos) - -find_package(Boost REQUIRED COMPONENTS fiber) -target_link_libraries(funcgrpc PUBLIC Boost::fiber) - -#find_package(Boost REQUIRED COMPONENTS json) -#target_link_libraries(funcgrpc PUBLIC Boost::json) - -#find_package(nlohmann_json 3.2.0 REQUIRED) -#target_link_libraries(funcgrpc PRIVATE nlohmann_json::nlohmann_json) - -find_package(unofficial-nethost CONFIG REQUIRED) -target_link_libraries(funcgrpc PRIVATE unofficial::nethost::nethost) - -if (NOT TARGET spdlog) - find_package(spdlog REQUIRED) -endif () -target_link_libraries(funcgrpc PRIVATE spdlog::spdlog) \ No newline at end of file diff --git a/host/src/funcgrpc/byte_buffer_helper.cpp b/host/src/funcgrpc/byte_buffer_helper.cpp deleted file mode 100644 index dfa6add15..000000000 --- a/host/src/funcgrpc/byte_buffer_helper.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "byte_buffer_helper.h" -#include - -// these bytebuffer helpers were copied from e2e tests of https://github.com/grpc/grpc - -std::unique_ptr funcgrpc::SerializeToByteBuffer(protobuf::Message *message) -{ - grpc::string buf; - message->SerializePartialToString(&buf); - Slice slice(buf); - - return std::make_unique(&slice, 1); -} - -bool funcgrpc::ParseFromByteBuffer(ByteBuffer *buffer, protobuf::Message *message) -{ - std::vector slices; - (void)buffer->Dump(&slices); - grpc::string buf; - buf.reserve(buffer->Length()); - for (auto s = slices.begin(); s != slices.end(); s++) - { - buf.append(reinterpret_cast(s->begin()), s->size()); - } - - return message->ParseFromString(buf); -} - -std::unique_ptr funcgrpc::SerializeToByteBufferFromChar(char *managedMessage, int managedMessageSize) -{ - grpc::string buf(managedMessage, managedMessageSize); - Slice slice(buf); - - return std::make_unique(&slice, 1); -} - -string funcgrpc::ParseFromByteBufferToString(ByteBuffer *buffer) -{ - std::vector slices; - (void)buffer->Dump(&slices); - grpc::string buf; - buf.reserve(buffer->Length()); - for (auto s = slices.begin(); s != slices.end(); s++) - { - buf.append(reinterpret_cast(s->begin()), s->size()); - } - - return buf; -} diff --git a/host/src/funcgrpc/byte_buffer_helper.h b/host/src/funcgrpc/byte_buffer_helper.h deleted file mode 100644 index b14c74f2f..000000000 --- a/host/src/funcgrpc/byte_buffer_helper.h +++ /dev/null @@ -1,23 +0,0 @@ - -#ifndef FUNCTIONSNETHOST_BYTE_BUFFER_HELPER_H -#define FUNCTIONSNETHOST_BYTE_BUFFER_HELPER_H - -#include "grpcpp/impl/codegen/config_protobuf.h" -#include "grpcpp/support/byte_buffer.h" -#include "iostream" -#include -#include - -using namespace std; -using namespace grpc; -namespace funcgrpc -{ -std::unique_ptr SerializeToByteBuffer(grpc::protobuf::Message *message); - -bool ParseFromByteBuffer(grpc::ByteBuffer *buffer, grpc::protobuf::Message *message); - -string ParseFromByteBufferToString(ByteBuffer *buffer); - -std::unique_ptr SerializeToByteBufferFromChar(char *managedMessage, int managedMessageSize); -} -#endif diff --git a/host/src/funcgrpc/func_bidi_reactor.cpp b/host/src/funcgrpc/func_bidi_reactor.cpp deleted file mode 100644 index 9c272ecab..000000000 --- a/host/src/funcgrpc/func_bidi_reactor.cpp +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#include "func_bidi_reactor.h" -#include "byte_buffer_helper.h" -#include "func_log.h" -#include "handlers/funcgrpc_native_handler.h" -#include "messaging_channel.h" - -funcgrpc::FunctionBidiReactor::FunctionBidiReactor(GrpcWorkerStartupOptions *pOptions, - NativeHostApplication *pApplication) -{ - pOptions_ = pOptions; - pApplication_ = pApplication; - - std::string endpoint = pOptions->host + ":" + std::to_string(pOptions->port); - grpc::ChannelArguments channelArgs; - channelArgs.SetInt(GRPC_ARG_MAX_SEND_MESSAGE_LENGTH, pOptions->grpcMaxMessageLength); - channelArgs.SetInt(GRPC_ARG_MAX_RECEIVE_MESSAGE_LENGTH, pOptions->grpcMaxMessageLength); - auto channel = grpc::CreateCustomChannel(endpoint, grpc::InsecureChannelCredentials(), channelArgs); - - auto generic_stub_ = make_unique(channel); - const char *suffix_for_stats = nullptr; - grpc::StubOptions options(suffix_for_stats); - generic_stub_->PrepareBidiStreamingCall(&client_context_, "/AzureFunctionsRpcMessages.FunctionRpc/EventStream", - options, this); - handler_ = std::unique_ptr(new NativeHostMessageHandler(pApplication)); - - sendStartStream(); - StartRead(&read_); - StartCall(); - - auto outboundWriterTask = std::async(std::launch::async, [this]() { startOutboundWriter(); }); - auto inboundMsgHandlingTask = std::async(std::launch::async, [this]() { handleInboundMessagesForApplication(); }); -} - -void funcgrpc::FunctionBidiReactor::OnWriteDone(bool ok) -{ - FUNC_LOG_TRACE("OnWriteDone. ok:{}", ok); - - { - bool expect = true; - if (!write_inprogress_.compare_exchange_strong(expect, false, std::memory_order_relaxed)) - { - FUNC_LOG_WARN("Illegal write_inprogress_ state"); - } - } - - fireWrite(); -} - -void funcgrpc::FunctionBidiReactor::OnReadDone(bool ok) -{ - if (!ok) - { - FUNC_LOG_WARN("Failed to read response."); - return; - } - - grpc::ByteBuffer outboundMessage(read_); - - auto handleMsgTask = std::async(std::launch::async, [this, &outboundMessage]() { - auto outboundStreamingMsg = StreamingMessage(); - handler_->HandleMessage(&outboundMessage); - }); - - fireRead(); -} - -void funcgrpc::FunctionBidiReactor::OnDone(const grpc::Status &status) -{ - if (status.ok()) - { - FUNC_LOG_DEBUG("Bi-directional stream ended. status.code={}, status.message={}", status.error_code(), - status.error_message()); - } - - std::unique_lock l(mu_); - status_ = status; - done_ = true; - cv_.notify_one(); -} - -Status funcgrpc::FunctionBidiReactor::Await() -{ - std::unique_lock l(mu_); - cv_.wait(l, [this] { return done_; }); - return std::move(status_); -} - -/// -/// Manages outbound(to host) write operations. -/// Listening to the outbound channel and when a new message arrives, -/// we will push that entry to the write buffer. -/// - -void funcgrpc::FunctionBidiReactor::startOutboundWriter() -{ - FUNC_LOG_DEBUG("startOutboundWriter started"); - auto &outboundChannel = funcgrpc::MessageChannel::GetInstance().GetOutboundChannel(); - - grpc::ByteBuffer messagetoSend; - while (channel_pop_status_t::success == outboundChannel.pop(messagetoSend)) - { - FUNC_LOG_DEBUG("Popped new message received in outbound channel"); - writeToOutboundBuffer(messagetoSend); - } - FUNC_LOG_WARN("exiting startOutboundWriter."); -} - -/// -/// Sends the startStream message to host. -/// This will initiate the GRPC communication with host. -/// -void funcgrpc::FunctionBidiReactor::sendStartStream() -{ - StreamingMessage startStream; - startStream.mutable_start_stream()->set_worker_id(pOptions_->workerId); - FUNC_LOG_INFO("Sending StartStream message."); - - auto bbUniqPtr = funcgrpc::SerializeToByteBuffer(&startStream); - auto bbPtr = bbUniqPtr.get(); - writeToOutboundBuffer(*bbPtr); -} - -/// -/// Pushes an outgoing message(to host) to the buffer. -/// -void funcgrpc::FunctionBidiReactor::writeToOutboundBuffer(const grpc::ByteBuffer &outgoingMessage) -{ - { - absl::MutexLock lk(&writes_mtx_); - writes_.push_back(outgoingMessage); - FUNC_LOG_TRACE("Pushed entry to writes_ buffer"); - } - fireWrite(); -} - -/// -/// Pull the message from buffer and writes to GRPC outbound stream. -/// -void funcgrpc::FunctionBidiReactor::fireWrite() -{ - { - absl::MutexLock lk(&writes_mtx_); - if (writes_.empty()) - { - return; - } - - bool expect = false; - if (write_inprogress_.compare_exchange_strong(expect, true, std::memory_order_relaxed)) - { - write_ = *writes_.begin(); - writes_.erase(writes_.begin()); - } - else - { - FUNC_LOG_DEBUG("Another write operation is in progress."); - return; - } - } - - StartWrite(&write_); -} - -void funcgrpc::FunctionBidiReactor::fireRead() -{ - FUNC_LOG_TRACE("fireRead called"); - StartRead(&read_); -} - -/// -/// Handles messages meant for the application/dotnet worker. -/// Listening to the inbound channel and when a new message arrives, -/// we will send that to the application_. -/// -/// TO DO: I think we can move this to the funcgrpc_native_handler.cpp. -/// -/// -void funcgrpc::FunctionBidiReactor::handleInboundMessagesForApplication() -{ - FUNC_LOG_DEBUG("handleInboundMessagesForApplication started"); - - auto &inboundChannel = funcgrpc::MessageChannel::GetInstance().GetInboundChannel(); - grpc::ByteBuffer ibByteBuffer; - - while (channel_pop_status_t::success == inboundChannel.pop(ibByteBuffer)) - { - FUNC_LOG_DEBUG("Popped new message received in inbound channel"); - - auto size = ibByteBuffer.Length(); - std::string t = funcgrpc::ParseFromByteBufferToString(&ibByteBuffer); - auto charArr = t.c_str(); - auto *unsignedCharArr = (unsigned char *)charArr; - - pApplication_->HandleIncomingMessage(unsignedCharArr, size); - } - - FUNC_LOG_WARN("exiting handleInboundMessagesForApplication"); -} \ No newline at end of file diff --git a/host/src/funcgrpc/func_bidi_reactor.h b/host/src/funcgrpc/func_bidi_reactor.h deleted file mode 100644 index d153cade7..000000000 --- a/host/src/funcgrpc/func_bidi_reactor.h +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "funcgrpc.h" -#include "funcgrpc_handlers.h" -#include "nativehostapplication.h" -#include -#include - -using AzureFunctionsRpcMessages::FunctionLoadResponse; -using AzureFunctionsRpcMessages::FunctionRpc; -using AzureFunctionsRpcMessages::StartStream; -using AzureFunctionsRpcMessages::StatusResult; -using AzureFunctionsRpcMessages::StreamingMessage; -using AzureFunctionsRpcMessages::WorkerInitResponse; -using grpc::ByteBuffer; -using grpc::Channel; -using grpc::ClientContext; -using grpc::Status; - -using namespace AzureFunctionsRpc; -using namespace grpc; -namespace funcgrpc -{ - -/// -/// BidiReactor implementation which reads and writes messages from the GRPC stream asynchronously. -/// See https://github.com/grpc/proposal/blob/master/L67-cpp-callback-api.md for details. -/// -class FunctionBidiReactor : public grpc::ClientBidiReactor -{ - public: - FunctionBidiReactor(GrpcWorkerStartupOptions *options, NativeHostApplication *application); - - void OnWriteDone(bool ok) override; - - void OnReadDone(bool ok) override; - - void OnDone(const Status &status) override; - - Status Await(); - - void startOutboundWriter(); - - void sendStartStream(); - - void handleInboundMessagesForApplication(); - - /// - /// Push a new message to the buffer. - /// - /// - void writeToOutboundBuffer(const ByteBuffer &outgoingMessage); - - /// - /// Writes the next message in buffer to the outbound GRPC stream. - /// - void fireWrite(); - - /// - /// Reads from GRPC stream. - /// - void fireRead(); - - private: - GrpcWorkerStartupOptions *pOptions_; - std::unique_ptr handler_; - NativeHostApplication *pApplication_; - - std::mutex mu_; - std::condition_variable cv_; - Status status_; - bool done_ = false; - - grpc::ClientContext client_context_; - - /// - /// Message to read from server. - /// This should not be modified while a read operation( StartRead(&read_) ) is in progress. - /// - ByteBuffer read_; - - /// - /// Message to write to server. - /// This should not be modified while a write operation( StartWrite(&write_) ) is in progress. - /// - ByteBuffer write_; - - std::atomic_bool write_inprogress_{false}; - - // Buffer for writing operations. - std::vector writes_ GUARDED_BY(writes_mtx_); - absl::Mutex writes_mtx_; -}; -} // namespace funcgrpc \ No newline at end of file diff --git a/host/src/funcgrpc/func_log.cpp b/host/src/funcgrpc/func_log.cpp deleted file mode 100644 index 683cb5390..000000000 --- a/host/src/funcgrpc/func_log.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#include "func_log.h" - -namespace funcgrpc -{ -std::shared_ptr Log::funcLogger; - -void Log::Init() -{ - funcLogger = spdlog::stdout_color_mt("FunctionsNetHost"); - spdlog::set_pattern("LanguageWorkerConsoleLog%^[%H:%M:%S.%e] [%l] %n: %v%$"); - funcLogger->set_level(spdlog::level::info); - -#if defined(_DEBUG) || defined(DEBUG) - funcLogger->flush_on(spdlog::level::trace); -#elif defined(NDEBUG) - funcLogger->flush_on(spdlog::level::warn); -#endif -} -} // namespace funcgrpc \ No newline at end of file diff --git a/host/src/funcgrpc/func_log.h b/host/src/funcgrpc/func_log.h deleted file mode 100644 index 366b7e25e..000000000 --- a/host/src/funcgrpc/func_log.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#pragma once - -#include "spdlog/sinks/stdout_color_sinks.h" -#include "spdlog/spdlog.h" -#include -#include - -namespace funcgrpc -{ -class Log -{ - public: - static void Init(); - - private: - static std::shared_ptr funcLogger; -}; -} // namespace funcgrpc - -#if defined(_DEBUG) || defined(DEBUG) -#define FUNC_LOG_TRACE(...) spdlog::get("FunctionsNetHost")->trace(__VA_ARGS__) -#define FUNC_LOG_DEBUG(...) spdlog::get("FunctionsNetHost")->debug(__VA_ARGS__) -#define FUNC_LOG_INFO(...) spdlog::get("FunctionsNetHost")->info(__VA_ARGS__) -#define FUNC_LOG_WARN(...) spdlog::get("FunctionsNetHost")->warn(__VA_ARGS__) -#define FUNC_LOG_ERROR(...) spdlog::get("FunctionsNetHost")->error(__VA_ARGS__) -#elif defined(NDEBUG) -#define FUNC_LOG_TRACE(...) (void)0 -#define FUNC_LOG_DEBUG(...) (void)0 -#define FUNC_LOG_INFO(...) spdlog::get("FunctionsNetHost")->info(__VA_ARGS__) -#define FUNC_LOG_WARN(...) spdlog::get("FunctionsNetHost")->warn(__VA_ARGS__) -#define FUNC_LOG_ERROR(...) spdlog::get("FunctionsNetHost")->error(__VA_ARGS__) -#endif \ No newline at end of file diff --git a/host/src/funcgrpc/func_perf_marker.h b/host/src/funcgrpc/func_perf_marker.h deleted file mode 100644 index 92abc4f4e..000000000 --- a/host/src/funcgrpc/func_perf_marker.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#ifndef FUNCTIONSNETHOST_FUNC_PERF_MARKER_H -#define FUNCTIONSNETHOST_FUNC_PERF_MARKER_H - -#include "func_log.h" -#include -#include -#include -#include -namespace funcgrpc -{ -class FuncPerfMarker -{ - public: - explicit FuncPerfMarker(const std::string &name) - { - _name = name; - _start = std::chrono::high_resolution_clock::now(); - } - - ~FuncPerfMarker() - { - auto stop = std::chrono::high_resolution_clock::now(); - auto durationMs = duration_cast(stop - _start); - FUNC_LOG_INFO("{} elapsed: {}ms", _name, durationMs.count()); - } - - private: - std::chrono::time_point _start; - std::string _name; -}; -} // namespace funcgrpc -#endif diff --git a/host/src/funcgrpc/funcgrpc.h b/host/src/funcgrpc/funcgrpc.h deleted file mode 100644 index affe1281a..000000000 --- a/host/src/funcgrpc/funcgrpc.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#ifndef FUNC_WORKER -#define FUNC_WORKER - -#include "funcgrpc_handlers.h" -#include -#include - -using namespace AzureFunctionsRpc; - -namespace funcgrpc -{ - -class GrpcWorkerStartupOptions -{ - public: - GrpcWorkerStartupOptions() = default; - ; - std::string host; - int port; - std::string workerId; - std::string requestId; - int grpcMaxMessageLength; -}; -} // namespace funcgrpc - -#endif \ No newline at end of file diff --git a/host/src/funcgrpc/funcgrpc_handlers.h b/host/src/funcgrpc/funcgrpc_handlers.h deleted file mode 100644 index b7b463bf7..000000000 --- a/host/src/funcgrpc/funcgrpc_handlers.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#ifndef FUNC_HANDLERS -#define FUNC_HANDLERS - -#include -#include -#include -#include -#include -#include -#include - -using AzureFunctionsRpcMessages::StreamingMessage; -using grpc::ByteBuffer; -namespace AzureFunctionsRpc -{ - -class MessageHandler -{ - public: - virtual void HandleMessage(ByteBuffer *receivedMessage){}; -}; - -} // namespace AzureFunctionsRpc - -#endif \ No newline at end of file diff --git a/host/src/funcgrpc/funcgrpc_worker_config_handle.cpp b/host/src/funcgrpc/funcgrpc_worker_config_handle.cpp deleted file mode 100644 index 4124eccc8..000000000 --- a/host/src/funcgrpc/funcgrpc_worker_config_handle.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#include "funcgrpc_worker_config_handle.h" -#include "func_log.h" -#include "func_perf_marker.h" -#include "rapidjson/document.h" -#include "rapidjson/filereadstream.h" -std::string funcgrpc::WorkerConfigHandle::GetApplicationExePath(const std::string &dir) -{ - std::string fullPath; - try - { - funcgrpc::FuncPerfMarker mark("WorkerConfigHandle->GetApplicationExePath"); - - fullPath = dir + "/worker.config.json"; - - fp = fopen(fullPath.c_str(), "r"); - char readBuffer[65536]; - rapidjson::FileReadStream is(fp, readBuffer, sizeof(readBuffer)); - - rapidjson::Document doc; - doc.ParseStream(is); - closeFileHandle(); - - if (doc.HasParseError()) - { - FUNC_LOG_ERROR("Error parsing {} to rapidJson::Document.", fullPath); - } - std::string workerPath = doc["description"]["defaultWorkerPath"].GetString(); - std::string exePath = dir + "/" + workerPath; - - return exePath; - } - catch (std::exception &ex) - { - FUNC_LOG_ERROR("Error parsing {} to rapidJson::Document.", fullPath); - throw; - } -} -funcgrpc::WorkerConfigHandle::~WorkerConfigHandle() -{ - closeFileHandle(); -} -void funcgrpc::WorkerConfigHandle::closeFileHandle() -{ - if (fp != nullptr) - { - fclose(fp); - fp = nullptr; - } -} diff --git a/host/src/funcgrpc/funcgrpc_worker_config_handle.h b/host/src/funcgrpc/funcgrpc_worker_config_handle.h deleted file mode 100644 index cba86abbf..000000000 --- a/host/src/funcgrpc/funcgrpc_worker_config_handle.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#pragma once - -#include - -namespace funcgrpc -{ -class WorkerConfigHandle -{ - public: - WorkerConfigHandle() = default; - - ~WorkerConfigHandle(); - - /** - * Gets the full path to the function app executable. - * @param dir Path to function app directory. - */ - std::string GetApplicationExePath(const std::string &dir); - - private: - FILE *fp; - void closeFileHandle(); -}; -} // namespace funcgrpc diff --git a/host/src/funcgrpc/handlers/funcgrpc_native_handler.cpp b/host/src/funcgrpc/handlers/funcgrpc_native_handler.cpp deleted file mode 100644 index 8406c651d..000000000 --- a/host/src/funcgrpc/handlers/funcgrpc_native_handler.cpp +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#include "funcgrpc_native_handler.h" -#include "../byte_buffer_helper.h" -#include "../func_log.h" -#include "../func_perf_marker.h" -#include "../funcgrpc_worker_config_handle.h" - -using namespace AzureFunctionsRpc; -using AzureFunctionsRpcMessages::FunctionEnvironmentReloadResponse; -using AzureFunctionsRpcMessages::FunctionLoadResponse; -using AzureFunctionsRpcMessages::FunctionMetadataResponse; -using AzureFunctionsRpcMessages::StartStream; -using AzureFunctionsRpcMessages::StatusResult; -using AzureFunctionsRpcMessages::WorkerInitResponse; - -using namespace std; - -AzureFunctionsRpc::NativeHostMessageHandler::NativeHostMessageHandler(NativeHostApplication *application) - : MessageHandler() -{ - application_ = application; -} - -void AzureFunctionsRpc::NativeHostMessageHandler::HandleMessage(ByteBuffer *receivedMessageBb) -{ - if (specializationRequestReceived) - { - // Once we received specialization request & returned a response for that, - // We do not need to deserialize the byte buffer version of message. - FUNC_LOG_DEBUG("New message received in handler. Pushing to InboundChannel."); - - // Forward to inbound channel(managed code wrapper is listening to that channel). - funcgrpc::MessageChannel::GetInstance().GetInboundChannel().push(*receivedMessageBb); - } - else - { - // We will deserialize the bytebuffer version as we need some property values. - StreamingMessage receivedMessage; - funcgrpc::ParseFromByteBuffer(receivedMessageBb, &receivedMessage); - StreamingMessage::ContentCase contentCase = receivedMessage.content_case(); - FUNC_LOG_DEBUG("New message received. contentCase: {}", contentCase); - - if (contentCase == StreamingMessage::ContentCase::kWorkerInitRequest) - { - StreamingMessage streamingMsg; - streamingMsg.mutable_worker_init_response()->mutable_result()->set_status( - AzureFunctionsRpcMessages::StatusResult::Success); - streamingMsg.mutable_worker_init_response()->set_worker_version("1.0.0.2"); - auto uPtrBb = funcgrpc::SerializeToByteBuffer(&streamingMsg); - auto byteBuffer = uPtrBb.get(); - - FUNC_LOG_DEBUG("Pushing response to OutboundChannel.contentCase: {}", streamingMsg.content_case()); - funcgrpc::MessageChannel::GetInstance().GetOutboundChannel().push(*byteBuffer); - } - else if (contentCase == StreamingMessage::ContentCase::kFunctionsMetadataRequest) - { - StreamingMessage streamingMsg; - streamingMsg.mutable_function_metadata_response()->mutable_result()->set_status( - AzureFunctionsRpcMessages::StatusResult::Success); - streamingMsg.mutable_function_metadata_response()->set_use_default_metadata_indexing(true); - auto uPtrBb = funcgrpc::SerializeToByteBuffer(&streamingMsg); - auto byteBuffer = uPtrBb.get(); - - FUNC_LOG_DEBUG("Pushing response to outbound channel.contentCase: {}", streamingMsg.content_case()); - funcgrpc::MessageChannel::GetInstance().GetOutboundChannel().push(*byteBuffer); - } - else if (contentCase == StreamingMessage::ContentCase::kFunctionEnvironmentReloadRequest) - { - try - { - string dir(receivedMessage.function_environment_reload_request().function_app_directory()); - - { - funcgrpc::FuncPerfMarker mark1("Setting environment variables"); - - google::protobuf::Map envVars = - receivedMessage.function_environment_reload_request().environment_variables(); - for (auto &envVar : envVars) - { - string envString = envVar.first; // key - string value = envVar.second; // value - envString.append("=").append(value); - - _putenv(envString.c_str()); - } - } - - string exePath = funcgrpc::WorkerConfigHandle().GetApplicationExePath(dir); - { - funcgrpc::FuncPerfMarker mark2("application_->ExecuteApplication"); - application_->ExecuteApplication(exePath); - } - - FUNC_LOG_INFO("Waiting for worker initialization."); - std::unique_lock lk(application_->mtx_workerLoaded); - application_->cv_workerLoaded.wait(lk, [this] { return application_->hasWorkerLoaded; }); - FUNC_LOG_INFO("Worker payload loaded. Forwarding env reload request to worker."); - - funcgrpc::MessageChannel::GetInstance().GetInboundChannel().push(*receivedMessageBb); - specializationRequestReceived = true; - } - catch (const std::exception &ex) - { - FUNC_LOG_ERROR("Caught unknown exception inside handler.{}", ex.what()); - } - catch (...) - { - FUNC_LOG_ERROR("Caught unknown exception in handler."); - } - } - } -} \ No newline at end of file diff --git a/host/src/funcgrpc/handlers/funcgrpc_native_handler.h b/host/src/funcgrpc/handlers/funcgrpc_native_handler.h deleted file mode 100644 index d8e120ad6..000000000 --- a/host/src/funcgrpc/handlers/funcgrpc_native_handler.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#ifndef FUNC_NATIVEHANDLER -#define FUNC_NATIVEHANDLER - -#include "../funcgrpc.h" -#include "../funcgrpc_handlers.h" -#include "../nativehostapplication.h" -#include "grpcpp/support/byte_buffer.h" -#include -#include -#include -#include -#include -#include - -using namespace AzureFunctionsRpc; - -using grpc::ByteBuffer; -namespace AzureFunctionsRpc -{ - -class NativeHostMessageHandler : public MessageHandler -{ - - public: - explicit NativeHostMessageHandler(NativeHostApplication *application); - - void HandleMessage(ByteBuffer *receivedMessage) override; - - private: - NativeHostApplication *application_; - bool specializationRequestReceived = false; -}; -} // namespace AzureFunctionsRpc - -#endif \ No newline at end of file diff --git a/host/src/funcgrpc/messaging_channel.cpp b/host/src/funcgrpc/messaging_channel.cpp deleted file mode 100644 index 77d91cde5..000000000 --- a/host/src/funcgrpc/messaging_channel.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#include "messaging_channel.h" -#include - -namespace funcgrpc -{ -std::unique_ptr outboundChannelPtr = std::make_unique(); -std::unique_ptr inboundChannelPtr = std::make_unique(); - -MessageChannel &MessageChannel::GetInstance() -{ - static MessageChannel single; - return single; -} - -channel_t &MessageChannel::GetOutboundChannel() -{ - return *outboundChannelPtr; -} - -channel_t &MessageChannel::GetInboundChannel() -{ - return *inboundChannelPtr; -} -} // namespace funcgrpc \ No newline at end of file diff --git a/host/src/funcgrpc/messaging_channel.h b/host/src/funcgrpc/messaging_channel.h deleted file mode 100644 index 5c31849f7..000000000 --- a/host/src/funcgrpc/messaging_channel.h +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#pragma once -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -using grpc::ByteBuffer; - -namespace funcgrpc -{ -typedef boost::fibers::unbuffered_channel channel_t; -typedef boost::fibers::channel_op_status channel_pop_status_t; - -class MessageChannel -{ - private: - MessageChannel() = default; - - public: - static MessageChannel &GetInstance(); - - /// - /// Gets the outbound channel. Any messages which needs to go out (to the host) - /// should be pushed to this channel. - /// - /// - channel_t &GetOutboundChannel(); - - /// - /// Gets the inbound channel. - /// Call pop() on this channel to get the messages coming to the worker from host. - /// Invocation request is an example message coming through this channel. - /// Example use: - /// StreamingMessage message; - /// while (channel_pop_status_t::success == outboundChannel.pop(message)) { - /// // Do something with the message - /// } - /// - /// - channel_t &GetInboundChannel(); -}; -} // namespace funcgrpc \ No newline at end of file diff --git a/host/src/funcgrpc/nativehostapplication.cpp b/host/src/funcgrpc/nativehostapplication.cpp deleted file mode 100644 index 2d6ae0a5c..000000000 --- a/host/src/funcgrpc/nativehostapplication.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#include "nativehostapplication.h" -#include "func_log.h" -#include - -using namespace std; - -NativeHostApplication *NativeHostApplication::s_Application = nullptr; - -NativeHostApplication::NativeHostApplication() -{ - initMutex_ = CreateMutex(nullptr, FALSE, nullptr); - load_hostfxr(); -} - -NativeHostApplication::~NativeHostApplication() -{ -} - -void NativeHostApplication::ExecuteApplication(string dllPath) -{ - s_Application = this; - - hostfxr_handle cxt = nullptr; - - wstring wdllPath(dllPath.begin(), dllPath.end()); - - const char_t *dotnet_app = wdllPath.c_str(); - int rc = init_fptr(1, &dotnet_app, nullptr, &cxt); - - if (rc != 0 || cxt == nullptr) - { - std::cerr << "Init failed: " << std::hex << std::showbase << rc << '\n'; - close_fptr(cxt); - } - - set_runtime_prop(cxt, L"AZURE_FUNCTIONS_NATIVE_HOST", L"1"); - - clrThread_ = thread( - [](hostfxr_run_app_fn r, hostfxr_handle h, HANDLE m) { - WaitForSingleObject(m, INFINITE); - - int rc = r(h); - - if (rc != 0 || h == nullptr) - { - std::cerr << "Init failed2: " << std::hex << std::showbase << rc << '\n'; - // close_fptr(cxt); - } - }, - run_app_fptr, cxt, initMutex_); -} - -void NativeHostApplication::HandleIncomingMessage(unsigned char *buffer, int size) -{ - WaitForSingleObject(initMutex_, INFINITE); - - callback(&buffer, size, handle); -} - -void NativeHostApplication::SendOutgoingMessage(_In_ ByteBuffer *msg) -{ - FUNC_LOG_DEBUG("NativeHostApplication::SendOutgoingMessage > Pushing message to outbound channel."); - auto &outboundChannel = funcgrpc::MessageChannel::GetInstance().GetOutboundChannel(); - outboundChannel.push(*msg); -} - -void NativeHostApplication::SetCallbackHandles(_In_ PFN_REQUEST_HANDLER request_callback, _In_ void *grpcHandle) -{ - callback = request_callback; - handle = grpcHandle; - - ReleaseMutex(initMutex_); - - { - std::lock_guard lk(mtx_workerLoaded); - hasWorkerLoaded = true; - } - - cv_workerLoaded.notify_one(); -} - -bool NativeHostApplication::load_hostfxr() -{ - // Pre-allocate a large buffer for the path to hostfxr - char_t buffer[MAX_PATH]; - size_t buffer_size = sizeof(buffer) / sizeof(char_t); - int rc = get_hostfxr_path(buffer, &buffer_size, nullptr); - if (rc != 0) - return false; - - // Load hostfxr and get desired exports - void *lib = load_library(buffer); - - init_fptr = - (hostfxr_initialize_for_dotnet_command_line_fn)get_export(lib, "hostfxr_initialize_for_dotnet_command_line"); - get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate"); - set_runtime_prop = (hostfxr_set_runtime_property_value_fn)get_export(lib, "hostfxr_set_runtime_property_value"); - run_app_fptr = (hostfxr_run_app_fn)get_export(lib, "hostfxr_run_app"); - close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close"); - - return (init_fptr && get_delegate_fptr && close_fptr); -} - -void *NativeHostApplication::load_library(const char_t *path) -{ - HMODULE h = ::LoadLibraryW(path); - assert(h != nullptr); - return (void *)h; -} - -void *NativeHostApplication::get_export(void *h, const char *name) -{ - void *f = ::GetProcAddress((HMODULE)h, name); - assert(f != nullptr); - return f; -} \ No newline at end of file diff --git a/host/src/funcgrpc/nativehostapplication.h b/host/src/funcgrpc/nativehostapplication.h deleted file mode 100644 index 68b052036..000000000 --- a/host/src/funcgrpc/nativehostapplication.h +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#pragma once -#include "messaging_channel.h" -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -using grpc::ByteBuffer; -using namespace std; - -// delegate for requests -typedef int(__stdcall *PFN_REQUEST_HANDLER)(unsigned char **msg, int size, void *grpcHandle); - -class NativeHostApplication -{ - public: - NativeHostApplication(); - - ~NativeHostApplication(); - - void ExecuteApplication(string dllPath); - - void SetCallbackHandles(_In_ PFN_REQUEST_HANDLER request_callback, _In_ void *grpcHandle); - - void HandleIncomingMessage(_In_ unsigned char *buffer, _In_ int size); - - void SendOutgoingMessage(_In_ ByteBuffer *message); - - static NativeHostApplication *GetInstance() - { - return s_Application; - } - - // Indicates whether the worker payload(managed code) loaded. - bool hasWorkerLoaded = false; - - std::condition_variable cv_workerLoaded; - - std::mutex mtx_workerLoaded; - private: - static NativeHostApplication *s_Application; - - // Globals to hold hostfxr exports - hostfxr_initialize_for_dotnet_command_line_fn init_fptr; - hostfxr_get_runtime_delegate_fn get_delegate_fptr; - hostfxr_set_runtime_property_value_fn set_runtime_prop; - hostfxr_run_app_fn run_app_fptr; - hostfxr_close_fn close_fptr; - - bool load_hostfxr(); - void *load_library(const char_t *); - void *get_export(void *h, const char *name); - - PFN_REQUEST_HANDLER callback; - void *handle; - - thread clrThread_; - HANDLE initMutex_; -}; \ No newline at end of file diff --git a/host/src/vcpkg.json b/host/src/vcpkg.json deleted file mode 100644 index bbd0d3c30..000000000 --- a/host/src/vcpkg.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", - "name": "func-net-host", - "version": "0.0.1", - "builtin-baseline": "6f7ffeb18f99796233b958aaaf14ec7bd4fb64b2", - "dependencies": [ - "grpc", - "boost-program-options", - "boost-fiber", - "spdlog", - "nethost", - "rapidjson" - ] -} \ No newline at end of file diff --git a/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec b/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec index 76e84eac7..2fe6ae20c 100644 --- a/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec +++ b/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec @@ -4,7 +4,7 @@ Microsoft.Azure.Functions.DotNetIsolatedNativeHost Microsoft Azure Functions dotnet-isolated native host dotnet-isolated azure-functions azure - 1.0.0-preview8 + 1.0.0-preview805 Microsoft Microsoft https://github.com/Azure/azure-functions-dotnet-worker @@ -17,7 +17,8 @@ - + + \ No newline at end of file diff --git a/host/tools/build/worker.config.json b/host/tools/build/worker.config.json index 3b248f8cd..7dfab24f9 100644 --- a/host/tools/build/worker.config.json +++ b/host/tools/build/worker.config.json @@ -5,5 +5,21 @@ "defaultExecutablePath": "%FUNCTIONS_WORKER_DIRECTORY%/bin/FunctionsNetHost.exe", "defaultWorkerPath": "bin/FunctionsNetHost.exe", "workerIndexing": "true" - } + }, + "profiles": [ + { + "profileName": "DotnetIsolatedLinux", + "conditions": [ + { + "conditionType": "hostProperty", + "conditionName": "platform", + "conditionExpression": "LINUX" + } + ], + "description": { + "defaultExecutablePath": "%FUNCTIONS_WORKER_DIRECTORY%/bin/FunctionsNetHost", + "defaultWorkerPath": "bin/FunctionsNetHost" + } + } + ] } \ No newline at end of file diff --git a/release_notes.md b/release_notes.md index c96b86559..540e00977 100644 --- a/release_notes.md +++ b/release_notes.md @@ -14,4 +14,4 @@ ### Microsoft.Azure.Functions.Worker.Grpc -- +- Add placeholder support for linux platform. (#1704) \ No newline at end of file diff --git a/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj b/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj index 59cf3970d..df6d0db63 100644 --- a/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj +++ b/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj @@ -2,7 +2,7 @@ Library - net5.0;netstandard2.0 + net5.0;net6.0;net7.0;netstandard2.0 Microsoft.Azure.Functions.Worker.Grpc This library provides gRPC support for Azure Functions .NET Worker communication with the Azure Functions Host. Microsoft.Azure.Functions.Worker.Grpc @@ -45,10 +45,7 @@ - + diff --git a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeMethods.cs b/src/DotNetWorker.Grpc/NativeHostIntegration/NativeMethods.cs index 7d0e03b5c..390d45efe 100644 --- a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeMethods.cs +++ b/src/DotNetWorker.Grpc/NativeHostIntegration/NativeMethods.cs @@ -8,14 +8,24 @@ namespace Microsoft.Azure.Functions.Worker.Grpc.NativeHostIntegration { - internal static unsafe partial class NativeMethods + internal static unsafe class NativeMethods { private const string NativeWorkerDll = "FunctionsNetHost.exe"; + static NativeMethods() + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, ImportResolver); + } + public static NativeHost GetNativeHostData() { - _ = get_application_properties(out var hostData); - return hostData; + var result = get_application_properties(out var hostData); + if (result == 1) + { + return hostData; + } + + throw new InvalidOperationException($"Invalid result returned from get_application_properties: {result}"); } public static void RegisterCallbacks(NativeSafeHandle nativeApplication, @@ -31,15 +41,39 @@ public static void SendStreamingMessage(NativeSafeHandle nativeApplication, Stre _ = send_streaming_message(nativeApplication, bytes, bytes.Length); } - [DllImport(NativeWorkerDll)] + [DllImport(NativeWorkerDll, CharSet = CharSet.Auto)] private static extern int get_application_properties(out NativeHost hostData); - [DllImport(NativeWorkerDll)] + [DllImport(NativeWorkerDll, CharSet = CharSet.Auto)] private static extern int send_streaming_message(NativeSafeHandle pInProcessApplication, byte[] streamingMessage, int streamingMessageSize); - [DllImport(NativeWorkerDll)] + [DllImport(NativeWorkerDll, CharSet = CharSet.Auto)] private static extern unsafe int register_callbacks(NativeSafeHandle pInProcessApplication, delegate* unmanaged requestCallback, IntPtr grpcHandler); + + /// + /// Custom import resolve callback. + /// When trying to resolve "FunctionsNetHost", we return the handle using GetMainProgramHandle API in this callback. + /// + private static IntPtr ImportResolver(string libraryName, System.Reflection.Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName == NativeWorkerDll) + { +#if NET6_0 + if (OperatingSystem.IsLinux()) + { + return NativeLibraryLinux.GetMainProgramHandle(); + } +#elif NET7_0_OR_GREATER + return NativeLibrary.GetMainProgramHandle(); +#else + throw new PlatformNotSupportedException("Interop communication with FunctionsNetHost is not supported in the current platform. Consider upgrading your project to .NET 7.0 or later."); +#endif + } + + // Return 0 so that built-in resolving code will be executed. + return IntPtr.Zero; + } } } diff --git a/src/DotNetWorker.Grpc/NativeHostIntegration/Shim/NativeLibrary.Linux.cs b/src/DotNetWorker.Grpc/NativeHostIntegration/Shim/NativeLibrary.Linux.cs new file mode 100644 index 000000000..4166785fa --- /dev/null +++ b/src/DotNetWorker.Grpc/NativeHostIntegration/Shim/NativeLibrary.Linux.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Azure.Functions.Worker.Grpc.NativeHostIntegration +{ + /// + /// NativeLibrary.GetMainProgramHandle is only available from NET7. + /// This shim calls the native API on Linux to get the main program handle + /// + internal class NativeLibraryLinux + { + // Value 1 loads the library lazily, resolving symbols only as they are used + private const int RTLD_LAZY = 1; + + [DllImport("libdl.so", CharSet = CharSet.Auto)] + private static extern IntPtr dlerror(); + + [DllImport("libdl.so", CharSet = CharSet.Auto)] + private static extern IntPtr dlclose(nint handle); + + [DllImport("libdl.so", CharSet = CharSet.Auto)] + private static extern IntPtr dlopen(string filename, int flags); + + internal static IntPtr GetMainProgramHandle() + { +#pragma warning disable CS8625 // Passing null will return main program handle. + var handle = dlopen(filename: null, RTLD_LAZY); +#pragma warning restore CS8625 + + if (handle == IntPtr.Zero) + { + var error = Marshal.PtrToStringAnsi(dlerror()); + throw new InvalidOperationException($"Failed to get main program handle.{error}"); + } + + var result = dlclose(handle); + if (result != IntPtr.Zero) + { + var error = Marshal.PtrToStringAnsi(dlerror()); + throw new InvalidOperationException($"Failed to close main program handle: {error}"); + } + + return handle; + } + } +} diff --git a/src/DotNetWorker/DotNetWorker.csproj b/src/DotNetWorker/DotNetWorker.csproj index 36bc0e0ee..a4d0fc1dc 100644 --- a/src/DotNetWorker/DotNetWorker.csproj +++ b/src/DotNetWorker/DotNetWorker.csproj @@ -2,7 +2,7 @@ Library - net5.0;netstandard2.0 + net5.0;net6.0;net7.0;netstandard2.0 Microsoft.Azure.Functions.Worker This library enables you to create an Azure Functions .NET Worker, adding support for the isolated, out-of-process execution model. Microsoft.Azure.Functions.Worker