From 3681e1f272ae9d82ecf7512a2890073b06c2a6cb Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 13 Aug 2018 13:29:07 -0700 Subject: [PATCH 1/8] improved layout and handling of bindings --- .gitignore | 358 +++--------------- .gitmodules | 3 - .vscode/launch.json | 14 + .vscode/tasks.json | 15 + README.md | 25 +- azure-functions-powershell-worker.sln | 71 ++++ .../PSCoreApp/MyHttpTrigger/function.json | 20 + examples/PSCoreApp/MyHttpTrigger/run.ps1 | 7 + examples/PSCoreApp/host.json | 11 + examples/PSCoreApp/local.settings.json | 8 + ...nctions.PowerShell.Worker.Messaging.csproj | 12 + .../FunctionMessagingClient.cs | 50 +++ .../FunctionRpc.cs | 0 .../FunctionRpcGrpc.cs | 0 .../Azure.Functions.PowerShell.Worker.csproj} | 15 +- .../Context/Context.cs | 68 ++++ .../Context/ContextHttpRequest.cs | 17 + .../Context/ContextHttpResponse.cs | 82 ++++ .../Function/FunctionInfo.cs | 41 ++ .../Function/FunctionLoader.cs | 32 ++ .../StartupArguments.cs | 39 ++ .../Utility/TypeConverter.cs | 162 ++++++++ .../Worker.cs | 183 +++++++++ .../worker.config.json | 8 + src/PSWorker.cs | 55 --- src/protocol/protobuf | 1 - ...re.Functions.PowerShell.Worker.Test.csproj | 16 + .../UnitTest1.cs | 14 + worker.config.json | 8 - 29 files changed, 938 insertions(+), 397 deletions(-) delete mode 100644 .gitmodules create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 azure-functions-powershell-worker.sln create mode 100644 examples/PSCoreApp/MyHttpTrigger/function.json create mode 100644 examples/PSCoreApp/MyHttpTrigger/run.ps1 create mode 100644 examples/PSCoreApp/host.json create mode 100644 examples/PSCoreApp/local.settings.json create mode 100644 src/Azure.Functions.PowerShell.Worker.Messaging/Azure.Functions.PowerShell.Worker.Messaging.csproj create mode 100644 src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs rename src/{protocol => Azure.Functions.PowerShell.Worker.Messaging}/FunctionRpc.cs (100%) rename src/{protocol => Azure.Functions.PowerShell.Worker.Messaging}/FunctionRpcGrpc.cs (100%) rename src/{psworker.csproj => Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj} (51%) create mode 100644 src/Azure.Functions.PowerShell.Worker/Context/Context.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Context/ContextHttpRequest.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Context/ContextHttpResponse.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/StartupArguments.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Worker.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/worker.config.json delete mode 100644 src/PSWorker.cs delete mode 160000 src/protocol/protobuf create mode 100644 test/Azure.Functions.PowerShell.Worker.Test/Azure.Functions.PowerShell.Worker.Test.csproj create mode 100644 test/Azure.Functions.PowerShell.Worker.Test/UnitTest1.cs delete mode 100644 worker.config.json diff --git a/.gitignore b/.gitignore index 3e759b75..e10ab2d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,330 +1,60 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +bin/ +obj/ +*-tests.xml +/debug/ +/staging/ +/Packages/ +*.nuget.props -# User-specific files +# VS auto-generated solution files for project.json solutions +*.xproj +*.xproj.user *.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ +# VS auto-generated files for csproj files +*.csproj.user -# Visual Studio 2015/2017 cache/options directory +# Visual Studio IDE directory .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* -# NUNIT -*.VisualState.xml -TestResult.xml +# Project Rider IDE files +.idea.powershell/ -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -# StyleCop -StyleCopReport.xml +# Ignore executables +*.exe +*.msi +*.appx -# Files built by Visual Studio -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.iobj -*.pch +# Ignore binaries and symbols *.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages +*.dll +*.wixpdb + +# Ignore packages +*.deb +*.tar.gz +*.zip +*.rpm +*.pkg *.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig +*.AppImage -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# default location for produced nuget packages +/nuget-artifacts -# OpenCover UI analysis results -OpenCover/ +# resgen output +gen -# Azure Stream Analytics local run output -ASALocalRun/ +# Per repo profile +.profile.ps1 -# MSBuild Binary and Structured Log -*.binlog +# macOS +.DS_Store -# NVidia Nsight GPU debugger configuration file -*.nvuser +# TestsResults +TestsResults*.xml -# MFractors (Xamarin productivity tool) working folder -.mfractor/ +# Resharper settings +PowerShell.sln.DotSettings.user +*.msp +StyleCop.Cache diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 55e696ff..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/protocol/protobuf"] - path = src/protocol/protobuf - url = https://github.com/Azure/azure-functions-language-worker-protobuf.git diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..30e8e977 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ,] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..e9bf3dde --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/psworker.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/README.md b/README.md index 72f1506a..b85aedd7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ +# PSWorkerPrototype +Prototype for Azure Functions PowerShell Language Worker -# Contributing +## Steps -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. +1. Modify `DefaultExecutablePath` in `worker.config.json` (to something like this `"C:\\Program Files\\dotnet\\dotnet.exe"`) +2. `cd path/to/PSWorkerPrototype` +3. `dotnet publish` +4. Run: -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. +```powershell +# Windows if you installed the Azure Functions Core Tools via npm +Remove-Item -Recurse -Force ~\AppData\Roaming\npm\node_modules\azure-functions-core-tools\bin\workers\powershell +Copy-Item src\Azure.Functions.PowerShell.Worker\bin\Debug\netcoreapp2.1\publish ~\AppData\Roaming\npm\node_modules\azure-functions-core-tools\bin\workers\powershell -Recurse -Force -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +# macOS if you installed the Azure Functions Core Tools via brew +Remove-Item -Recurse -Force /usr/local/Cellar/azure-functions-core-tools/2.0.1-beta.33/workers/powershell +Copy-Item src/Azure.Functions.PowerShell.Worker/bin/Debug/netcoreapp2.1/publish /usr/local/Cellar/azure-functions-core-tools/2.0.1-beta.33/workers/powershell -Recurse -Force +``` \ No newline at end of file diff --git a/azure-functions-powershell-worker.sln b/azure-functions-powershell-worker.sln new file mode 100644 index 00000000..81b95708 --- /dev/null +++ b/azure-functions-powershell-worker.sln @@ -0,0 +1,71 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8C758288-3909-4CE1-972D-1BE966628D6C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Functions.PowerShell.Worker", "src\Azure.Functions.PowerShell.Worker\Azure.Functions.PowerShell.Worker.csproj", "{939262BA-4823-405E-81CD-436C0B77D524}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Functions.PowerShell.Worker.Messaging", "src\Azure.Functions.PowerShell.Worker.Messaging\Azure.Functions.PowerShell.Worker.Messaging.csproj", "{A1581262-DE79-4C01-AD6C-88BE7C3E6322}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{12092936-4F2A-4B40-9AF2-56C840D44FEA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Functions.PowerShell.Worker.Test", "test\Azure.Functions.PowerShell.Worker.Test\Azure.Functions.PowerShell.Worker.Test.csproj", "{535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {939262BA-4823-405E-81CD-436C0B77D524}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Debug|Any CPU.Build.0 = Debug|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Debug|x64.ActiveCfg = Debug|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Debug|x64.Build.0 = Debug|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Debug|x86.ActiveCfg = Debug|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Debug|x86.Build.0 = Debug|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Release|Any CPU.ActiveCfg = Release|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Release|Any CPU.Build.0 = Release|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Release|x64.ActiveCfg = Release|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Release|x64.Build.0 = Release|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Release|x86.ActiveCfg = Release|Any CPU + {939262BA-4823-405E-81CD-436C0B77D524}.Release|x86.Build.0 = Release|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Debug|x64.Build.0 = Debug|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Debug|x86.Build.0 = Debug|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Release|Any CPU.Build.0 = Release|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Release|x64.ActiveCfg = Release|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Release|x64.Build.0 = Release|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Release|x86.ActiveCfg = Release|Any CPU + {A1581262-DE79-4C01-AD6C-88BE7C3E6322}.Release|x86.Build.0 = Release|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Debug|x64.Build.0 = Debug|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Debug|x86.Build.0 = Debug|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Release|Any CPU.Build.0 = Release|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Release|x64.ActiveCfg = Release|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Release|x64.Build.0 = Release|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Release|x86.ActiveCfg = Release|Any CPU + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {939262BA-4823-405E-81CD-436C0B77D524} = {8C758288-3909-4CE1-972D-1BE966628D6C} + {A1581262-DE79-4C01-AD6C-88BE7C3E6322} = {8C758288-3909-4CE1-972D-1BE966628D6C} + {535C8DA3-479D-42BF-B1AF-5B03ECAF67A4} = {12092936-4F2A-4B40-9AF2-56C840D44FEA} + EndGlobalSection +EndGlobal diff --git a/examples/PSCoreApp/MyHttpTrigger/function.json b/examples/PSCoreApp/MyHttpTrigger/function.json new file mode 100644 index 00000000..18ab7de9 --- /dev/null +++ b/examples/PSCoreApp/MyHttpTrigger/function.json @@ -0,0 +1,20 @@ +{ + "disabled": false, + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} \ No newline at end of file diff --git a/examples/PSCoreApp/MyHttpTrigger/run.ps1 b/examples/PSCoreApp/MyHttpTrigger/run.ps1 new file mode 100644 index 00000000..d766f17c --- /dev/null +++ b/examples/PSCoreApp/MyHttpTrigger/run.ps1 @@ -0,0 +1,7 @@ +param($req) + +$req.StatusCode = "201" +$req.Body.String = "hi" +return [PSCustomObject]@{ + res = $req +} \ No newline at end of file diff --git a/examples/PSCoreApp/host.json b/examples/PSCoreApp/host.json new file mode 100644 index 00000000..5553680f --- /dev/null +++ b/examples/PSCoreApp/host.json @@ -0,0 +1,11 @@ +{ + "logger": { + "categoryFilter": { + "defaultLevel": "Trace", + "categoryLevels": { + "Worker": "Trace" + } + }, + "fileLoggingMode": "always" + } +} diff --git a/examples/PSCoreApp/local.settings.json b/examples/PSCoreApp/local.settings.json new file mode 100644 index 00000000..df48436a --- /dev/null +++ b/examples/PSCoreApp/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "powershell", + "AzureWebJobsStorage": "UseDevelopmentStorage=true" + }, + "ConnectionStrings": {} +} diff --git a/src/Azure.Functions.PowerShell.Worker.Messaging/Azure.Functions.PowerShell.Worker.Messaging.csproj b/src/Azure.Functions.PowerShell.Worker.Messaging/Azure.Functions.PowerShell.Worker.Messaging.csproj new file mode 100644 index 00000000..28b5a7ad --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker.Messaging/Azure.Functions.PowerShell.Worker.Messaging.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs new file mode 100644 index 00000000..63ad0959 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Azure.Functions.PowerShell.Worker.Messaging +{ + public class FunctionMessagingClient : IDisposable + { + public bool isDisposed = false; + private AsyncDuplexStreamingCall _call; + + public FunctionMessagingClient(string host, int port) + { + Channel channel = new Channel(host, port, ChannelCredentials.Insecure); + _call = new FunctionRpc.FunctionRpcClient(channel).EventStream(); + } + + public async Task WriteAsync(StreamingMessage message) + { + if(isDisposed) return; + + await _call.RequestStream.WriteAsync(message); + } + + public async Task MoveNext() + { + if(isDisposed) return false; + + return await _call.ResponseStream.MoveNext(CancellationToken.None); + } + + public StreamingMessage GetCurrentMessage() + { + if(isDisposed) return null; + + return _call.ResponseStream.Current; + } + + public void Dispose() + { + if (!isDisposed) + { + isDisposed = true; + _call.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/protocol/FunctionRpc.cs b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionRpc.cs similarity index 100% rename from src/protocol/FunctionRpc.cs rename to src/Azure.Functions.PowerShell.Worker.Messaging/FunctionRpc.cs diff --git a/src/protocol/FunctionRpcGrpc.cs b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionRpcGrpc.cs similarity index 100% rename from src/protocol/FunctionRpcGrpc.cs rename to src/Azure.Functions.PowerShell.Worker.Messaging/FunctionRpcGrpc.cs diff --git a/src/psworker.csproj b/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj similarity index 51% rename from src/psworker.csproj rename to src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj index 405b0e9c..7671027a 100644 --- a/src/psworker.csproj +++ b/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj @@ -3,21 +3,24 @@ Exe netcoreapp2.1 - Microsoft.Azure.PowerShell.Worker - - - - + + + + - + PreserveNewest + + latest + + diff --git a/src/Azure.Functions.PowerShell.Worker/Context/Context.cs b/src/Azure.Functions.PowerShell.Worker/Context/Context.cs new file mode 100644 index 00000000..59b2d17f --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Context/Context.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using Google.Protobuf.Collections; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Microsoft.Azure.Functions.PowerShellWorker +{ + public class Context + { + public MapField Bindings {get; set;} + public MapField BindingData {get; set;} + public ExecutionContext ExecutionContext {get; set;} + public string InvocationId {get; set;} + public ContextHttpRequest Request {get; set;} + public ContextHttpResponse Response {get; set;} + + public Context (FunctionInfo info, InvocationRequest request) + { + InvocationId = request.InvocationId; + Bindings = new MapField(); + ExecutionContext = new ExecutionContext + { + InvocationId = request.InvocationId, + FunctionName = info.Name, + FunctionDirectory = info.Directory + }; + + + BindingData = request.TriggerMetadata; + BindingData.Add("InvocationId", new TypedData { + String = InvocationId + }); + } + + public static (Context context, List inputs) CreateContextAndInputs(FunctionInfo info, InvocationRequest request) + { + var context = new Context(info, request); + List inputs = new List(); + ContextHttpRequest httpInput = null; + foreach (var binding in request.InputData) + { + if (binding.Name != null && binding.Data != null ) + { + if (binding.Data.Http != null) + { + httpInput = TypeConverter.ToContextHttp(binding.Data.Http); + } + inputs.Add(binding.Data); + } + } + + if (httpInput != null) + { + context.Request = httpInput; + context.Response = new ContextHttpResponse(); + } + + return (context, inputs); + } + } + + public class ExecutionContext + { + public string InvocationId {get; internal set;} + public string FunctionName {get; internal set;} + public string FunctionDirectory {get; internal set;} + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpRequest.cs b/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpRequest.cs new file mode 100644 index 00000000..6b6d9c3c --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpRequest.cs @@ -0,0 +1,17 @@ +using Google.Protobuf.Collections; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Microsoft.Azure.Functions.PowerShellWorker +{ + public class ContextHttpRequest + { + public string Method {get; set;} + public string Url {get; set;} + public string OriginalUrl {get; set;} + public MapField Headers {get; set;} + public MapField Query {get; set;} + public MapField Params {get; set;} + public TypedData Body {get; set;} + public TypedData RawBody {get; set;} + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpResponse.cs b/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpResponse.cs new file mode 100644 index 00000000..67fe63d4 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpResponse.cs @@ -0,0 +1,82 @@ +using Google.Protobuf.Collections; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Microsoft.Azure.Functions.PowerShellWorker +{ + public class ContextHttpResponse + { +#region properties + public string StatusCode {get; set;} = "200"; + public MapField Headers {get; set;} = new MapField(); + public TypedData Body {get; set;} = new TypedData { String = "" }; + public bool EnableContentNegotiation {get; set;} +#endregion +#region Helper functions for user to use to set data + public ContextHttpResponse Header(string field, string value) => + SetHeader(field, value); + public ContextHttpResponse SetHeader(string field, string value) + { + Headers.Add(field, value); + return this; + } + + public string GetHeader(string field) => + Headers[field]; + + public ContextHttpResponse RemoveHeader(string field) + { + Headers.Remove(field); + return this; + } + + public ContextHttpResponse Status(int statusCode) => + SetStatus(statusCode); + public ContextHttpResponse Status(string statusCode) => + SetStatus(statusCode); + public ContextHttpResponse SetStatus(int statusCode) => + SetStatus(statusCode); + public ContextHttpResponse SetStatus(string statusCode) + { + StatusCode = statusCode; + return this; + } + + public ContextHttpResponse Type(string type) => + SetHeader("content-type", type); + public ContextHttpResponse SetContentType(string type) => + SetHeader("content-type", type); + + public ContextHttpResponse Send(int val) + { + Body = new TypedData + { + Int = val + }; + return this; + } + public ContextHttpResponse Send(double val) + { + Body = new TypedData + { + Double = val + }; + return this; + } + public ContextHttpResponse Send(string val) + { + Body = new TypedData + { + String = val + }; + return this; + } + public ContextHttpResponse Json(string val) { + Body = new TypedData + { + Json = val + }; + return Type("application/json"); + } +#endregion + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs new file mode 100644 index 00000000..d16253bc --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs @@ -0,0 +1,41 @@ +using Google.Protobuf.Collections; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; + +namespace Microsoft.Azure.Functions.PowerShellWorker +{ + public class FunctionInfo + { + public string Name {get; private set;} + public string Directory {get; private set;} + public MapField Bindings {get; private set;} + public MapField OutputBindings {get; private set;} + public string HttpOutputName {get; private set;} + + public FunctionInfo(RpcFunctionMetadata metadata) + { + Name = metadata.Name; + Directory = metadata.Directory; + Bindings = new MapField(); + OutputBindings = new MapField(); + HttpOutputName = ""; + + foreach (var binding in metadata.Bindings) + { + Bindings.Add(binding.Key, binding.Value); + + if (binding.Value.Direction != BindingInfo.Types.Direction.In) + { + if(binding.Value.Type == "http") + { + HttpOutputName = binding.Key; + }if(binding.Value.Type == "http") + { + HttpOutputName = binding.Key; + } + OutputBindings.Add(binding.Key, binding.Value); + } + } + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs new file mode 100644 index 00000000..c8bfb0ba --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs @@ -0,0 +1,32 @@ +using Google.Protobuf.Collections; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Microsoft.Azure.Functions.PowerShellWorker +{ + public class FunctionLoader + { + private readonly MapField _LoadedFunctions = new MapField(); + public void Load(string functionId, RpcFunctionMetadata metadata) + { + // TODO: catch "load" issues at "func start" time. + // ex. Script doesn't exist, entry point doesn't exist + _LoadedFunctions.Add(functionId, new Function + { + Info = new FunctionInfo(metadata), + ScriptPath = metadata.ScriptFile, + EntryPoint = metadata.EntryPoint + }); + } + + public FunctionInfo GetInfo(string functionId) => _LoadedFunctions[functionId].Info; + public (string ScriptPath, string EntryPoint) GetFunc(string functionId) => + (_LoadedFunctions[functionId].ScriptPath, _LoadedFunctions[functionId].EntryPoint); + } + + public class Function + { + public FunctionInfo Info {get; internal set;} + public string ScriptPath {get; internal set;} + public string EntryPoint {get; internal set;} + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs new file mode 100644 index 00000000..c8f99766 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs @@ -0,0 +1,39 @@ +using System; + +namespace Microsoft.Azure.Functions.PowerShellWorker +{ + public class StartupArguments + { + public int GrpcMaxMessageLength { get; set; } + public string Host {get; set;} + public int Port {get; set;} + public string RequestId {get; set;} + public string WorkerId {get; set;} + + public static StartupArguments Parse(string[] args) + { + StartupArguments arguments = new StartupArguments(); + for (int i = 1; i < 10; i+=2) + { + string currentArg = args[i]; + switch (i) + { + case 1: arguments.Host = currentArg; break; + case 3: arguments.Port = int.Parse(currentArg); break; + case 5: arguments.WorkerId = currentArg; break; + case 7: arguments.RequestId = currentArg; break; + case 9: arguments.GrpcMaxMessageLength = int.Parse(currentArg); break; + default: throw new InvalidOperationException(); + } + } + + Console.WriteLine($"host: {arguments.Host}"); + Console.WriteLine($"port: {arguments.Port}"); + Console.WriteLine($"workerId: {arguments.WorkerId}"); + Console.WriteLine($"requestId: {arguments.RequestId}"); + Console.WriteLine($"grpcMaxMessageLength: {arguments.GrpcMaxMessageLength}"); + + return arguments; + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs b/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs new file mode 100644 index 00000000..23ff3c7b --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs @@ -0,0 +1,162 @@ +using System.Management.Automation; +using Google.Protobuf; +using Microsoft.Azure.Functions.PowerShellWorker; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.TypedData; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Utility +{ + public class TypeConverter + { + public static ContextHttpRequest ToContextHttp (RpcHttp rpcHttp) + { + return new ContextHttpRequest + { + Method = rpcHttp.Method, + Url = rpcHttp.Url, + OriginalUrl = rpcHttp.Url, + Headers = rpcHttp.Headers, + Params = rpcHttp.Params, + Body = rpcHttp.Body, + RawBody = rpcHttp.RawBody, + Query = rpcHttp.Query + }; + } + + public static RpcHttp ToRpcHttp (ContextHttpResponse contextHttpResponse) + { + var rpcHttp = new RpcHttp + { + StatusCode = contextHttpResponse.StatusCode, + Body = contextHttpResponse.Body, + EnableContentNegotiation = contextHttpResponse.EnableContentNegotiation + }; + rpcHttp.Headers.Add(contextHttpResponse.Headers); + + return rpcHttp; + } + + public static object FromTypedData (TypedData data) + { + switch (data.DataCase) + { + case DataOneofCase.Json: + return data.Json; + case DataOneofCase.Bytes: + return data.Bytes; + case DataOneofCase.Double: + return data.Double; + case DataOneofCase.Http: + return data.Http; + case DataOneofCase.Int: + return data.Int; + case DataOneofCase.Stream: + return data.Stream; + case DataOneofCase.String: + return data.String; + default: + // possibly throw? + return null; + } + } + + public static TypedData ToTypedData (string bindingName, BindingInfo binding, PSObject psobject) + { + switch (binding.Type) + { + case "json": + + if(!LanguagePrimitives.TryConvertTo( + psobject.Properties[bindingName]?.Value, + out string jsonVal)) + { + throw new PSInvalidCastException(); + } + return new TypedData() + { + Json = jsonVal + }; + + case "bytes": + + if(!LanguagePrimitives.TryConvertTo( + psobject.Properties[bindingName]?.Value, + out ByteString bytesVal)) + { + throw new PSInvalidCastException(); + } + return new TypedData() + { + Bytes = bytesVal + }; + + case "double": + + if(!LanguagePrimitives.TryConvertTo( + psobject.Properties[bindingName]?.Value, + out double doubleVal)) + { + throw new PSInvalidCastException(); + } + return new TypedData() + { + Double = doubleVal + }; + + case "http": + + if(!LanguagePrimitives.TryConvertTo( + psobject.Properties[bindingName]?.Value, + out RpcHttp httpVal)) + { + throw new PSInvalidCastException(); + } + return new TypedData() + { + Http = httpVal + }; + + case "int": + + if(!LanguagePrimitives.TryConvertTo( + psobject.Properties[bindingName]?.Value, + out int intVal)) + { + throw new PSInvalidCastException(); + } + return new TypedData() + { + Int = intVal + }; + + case "stream": + + if(!LanguagePrimitives.TryConvertTo( + psobject.Properties[bindingName]?.Value, + out ByteString streamVal)) + { + throw new PSInvalidCastException(); + } + return new TypedData() + { + Stream = streamVal + }; + + case "string": + + if(!LanguagePrimitives.TryConvertTo( + psobject.Properties[bindingName]?.Value, + out string stringVal)) + { + throw new PSInvalidCastException(); + } + return new TypedData() + { + String = stringVal + }; + default: + throw new PSInvalidCastException("could not parse type"); + } + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Worker.cs b/src/Azure.Functions.PowerShell.Worker/Worker.cs new file mode 100644 index 00000000..36d4b87d --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Worker.cs @@ -0,0 +1,183 @@ +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Management.Automation; +using System.Management.Automation.Runspaces; + +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Azure.Functions.PowerShell.Worker.Messaging; +using Microsoft.PowerShell; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; + +namespace Microsoft.Azure.Functions.PowerShellWorker +{ + public class Worker + { + private static FunctionMessagingClient s_client; + private static System.Management.Automation.PowerShell s_ps; + private static FunctionLoader s_FunctionLoader = new FunctionLoader(); + public async static Task Main(string[] args) + { + if (args.Length != 10) + { + Console.WriteLine("usage --host --port --workerId --requestId --grpcMaxMessageLength "); + return; + } + StartupArguments startupArguments = StartupArguments.Parse(args); + + s_client = new FunctionMessagingClient(startupArguments.Host, startupArguments.Port); + InitPowerShell(); + + var streamingMessage = new StreamingMessage() { + RequestId = startupArguments.RequestId, + StartStream = new StartStream() { WorkerId = startupArguments.WorkerId } + }; + await s_client.WriteAsync(streamingMessage); + + await ProcessEvent(); + } + + private static void InitPowerShell() + { + s_ps = System.Management.Automation.PowerShell.Create(InitialSessionState.CreateDefault2()); + s_ps.AddScript("$PSHOME"); + //s_ps.AddCommand("Set-ExecutionPolicy").AddParameter("ExecutionPolicy", ExecutionPolicy.Unrestricted).AddParameter("Scope", ExecutionPolicyScope.Process); + var result = s_ps.Invoke(); + s_ps.Commands.Clear(); + + Console.WriteLine(result[0]); + } + + private static async Task ProcessEvent() + { + using (s_client) + { + while (await s_client.MoveNext()) + { + var message = s_client.GetCurrentMessage(); + switch (message.ContentCase) + { + case StreamingMessage.ContentOneofCase.WorkerInitRequest: + await HandleWorkerInitRequest(message.RequestId, message.WorkerInitRequest); + break; + + case StreamingMessage.ContentOneofCase.FunctionLoadRequest: + await HandleFunctionLoadRequest(message.RequestId, message.FunctionLoadRequest); + break; + + case StreamingMessage.ContentOneofCase.InvocationRequest: + await HandleInvocationRequest(message.RequestId, message.InvocationRequest); + break; + + default: + throw new InvalidOperationException($"Not supportted message type: {message.ContentCase}"); + } + } + } + } + + private static async Task HandleWorkerInitRequest(string requestId, WorkerInitRequest initRequest) + { + var response = new StreamingMessage() + { + RequestId = requestId, + WorkerInitResponse = new WorkerInitResponse() + { + Result = new StatusResult() + { + Status = StatusResult.Types.Status.Success + } + } + }; + await s_client.WriteAsync(response); + } + + private static async Task HandleFunctionLoadRequest(string requestId, FunctionLoadRequest loadRequest) + { + s_FunctionLoader.Load(loadRequest.FunctionId, loadRequest.Metadata); + var response = new StreamingMessage() + { + RequestId = requestId, + FunctionLoadResponse = new FunctionLoadResponse() + { + FunctionId = loadRequest.FunctionId, + Result = new StatusResult() + { + Status = StatusResult.Types.Status.Success + } + } + }; + await s_client.WriteAsync(response); + } + + private static async Task HandleInvocationRequest(string requestId, InvocationRequest request) + { + var status = new StatusResult() { Status = StatusResult.Types.Status.Success }; + var response = new StreamingMessage() + { + RequestId = requestId, + InvocationResponse = new InvocationResponse() + { + InvocationId = request.InvocationId, + Result = status + } + }; + + var info = s_FunctionLoader.GetInfo(request.FunctionId); + // (Context context, List inputs) = Context.CreateContextAndInputs(info, request); + (string scriptPath, string entryPoint) = s_FunctionLoader.GetFunc(request.FunctionId); + + if(entryPoint != "") + { + s_ps.AddCommand(entryPoint); + } + else + { + s_ps.AddCommand(scriptPath); + } + + foreach (ParameterBinding binding in request.InputData) + { + s_ps.AddParameter(binding.Name, TypeConverter.FromTypedData(binding.Data)); + } + + // s_ps.AddParameter("context", context); + // foreach (TypedData input in inputs) + // { + // s_ps.AddArgument(input); + // } + PSObject result = null; + try + { + result = s_ps.Invoke()[0]; + } + finally + { + s_ps.Commands.Clear(); + } + + foreach (var binding in info.OutputBindings) + { + ParameterBinding paramBinding = new ParameterBinding() + { + Name = binding.Key, + Data = TypeConverter.ToTypedData( + binding.Key, + binding.Value, + result) + }; + + // Not exactly sure which one to use for what scenario, so just set both. + response.InvocationResponse.OutputData.Add(paramBinding); + + if(binding.Key == "$return") + { + response.InvocationResponse.ReturnValue = paramBinding.Data; + } + } + + await s_client.WriteAsync(response); + } + } +} diff --git a/src/Azure.Functions.PowerShell.Worker/worker.config.json b/src/Azure.Functions.PowerShell.Worker/worker.config.json new file mode 100644 index 00000000..c144335a --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/worker.config.json @@ -0,0 +1,8 @@ +{ + "Description":{ + "Language":"powershell", + "Extension":".ps1", + "DefaultExecutablePath":"dotnet", + "DefaultWorkerPath":"Azure.Functions.PowerShell.Worker.dll" + } +} diff --git a/src/PSWorker.cs b/src/PSWorker.cs deleted file mode 100644 index b0f54302..00000000 --- a/src/PSWorker.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -using CommandLine; -using Grpc.Core; -using Microsoft.Azure.WebJobs.Script.Grpc.Messages; - -namespace Microsoft.Azure.PowerShell.Worker -{ - public class WorkerEntry - { - public static void Main(string[] args) - { - LanguageWorker worker; - Parser.Default.ParseArguments(args) - .WithParsed(ops => worker = new LanguageWorker(ops)) - .WithNotParsed(err => Environment.Exit(1)); - } - } - - public class ArgumentOptions - { - [Option("host", Required = true, HelpText = "IP Address used to connect to the Host via gRPC.")] - public string Host { get; set; } - - [Option("port", Required = true, HelpText = "Port used to connect to the Host via gRPC.")] - public int Port { get; set; } - - [Option("workerId", Required = true, HelpText = "Worker ID assigned to this language worker.")] - public string WorkerId { get; set; } - - [Option("requestId", Required = true, HelpText = "Request ID used for gRPC communication with the Host.")] - public string RequestId { get; set; } - - [Option("grpcMaxMessageLength", Required = true, HelpText = "gRPC Maximum message size.")] - public int MaxMessageLength { get; set; } - } - - internal class LanguageWorker - { - private ArgumentOptions _options; - private FunctionRpc.FunctionRpcClient _client; - private AsyncDuplexStreamingCall _streamingCall; - - internal LanguageWorker(ArgumentOptions options) - { - var channel = new Channel(options.Host, options.Port, ChannelCredentials.Insecure); - _client = new FunctionRpc.FunctionRpcClient(channel); - _streamingCall = _client.EventStream(); - _options = options; - } - } -} diff --git a/src/protocol/protobuf b/src/protocol/protobuf deleted file mode 160000 index 58b3dc0d..00000000 --- a/src/protocol/protobuf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 58b3dc0dbde065b15bd979bd2e27f36b7ee6273e diff --git a/test/Azure.Functions.PowerShell.Worker.Test/Azure.Functions.PowerShell.Worker.Test.csproj b/test/Azure.Functions.PowerShell.Worker.Test/Azure.Functions.PowerShell.Worker.Test.csproj new file mode 100644 index 00000000..061aa24f --- /dev/null +++ b/test/Azure.Functions.PowerShell.Worker.Test/Azure.Functions.PowerShell.Worker.Test.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + diff --git a/test/Azure.Functions.PowerShell.Worker.Test/UnitTest1.cs b/test/Azure.Functions.PowerShell.Worker.Test/UnitTest1.cs new file mode 100644 index 00000000..0b9576bc --- /dev/null +++ b/test/Azure.Functions.PowerShell.Worker.Test/UnitTest1.cs @@ -0,0 +1,14 @@ +using System; +using Xunit; + +namespace Azure.Functions.PowerShell.Worker.Test +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} diff --git a/worker.config.json b/worker.config.json deleted file mode 100644 index 4d8760eb..00000000 --- a/worker.config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Description":{ - "Language":"powershell", - "Extension":".ps1", - "DefaultExecutablePath":"C:\\Users\\dongbow\\AppData\\Local\\Microsoft\\dotnet\\dotnet.exe", - "DefaultWorkerPath":"psworker.dll" - } -} From f22f6ef98b1e82d1169d30149a921799b2d4cf4f Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 22 Aug 2018 12:52:19 -0700 Subject: [PATCH 2/8] magic variables go to bindings, entry point is acknowledged --- .../PSCoreApp/MyHttpTrigger/function.json | 3 +- examples/PSCoreApp/MyHttpTrigger/run.ps1 | 11 +- .../FunctionMessagingClient.cs | 12 +- .../Azure.Functions.PowerShell.Worker.csproj | 3 +- .../Context/Context.cs | 68 ------ .../HttpRequestContext.cs} | 13 +- .../HttpResponseContext.cs} | 32 +-- .../PowerShell/Host/Host.cs | 139 +++++++++++ .../PowerShell/Host/HostUserInterface.cs | 221 ++++++++++++++++++ .../PowerShell/Host/RawUserInterface.cs | 179 ++++++++++++++ .../Requests/HandleFunctionLoadRequest.cs | 34 +++ .../Requests/HandleInvocationRequest.cs | 154 ++++++++++++ .../Requests/HandleWorkerInitRequest.cs | 31 +++ .../Utility/RpcLogger.cs | 77 ++++++ .../Utility/TypeConverter.cs | 112 +++++---- .../Worker.cs | 150 ++++-------- ...re.Functions.PowerShell.Worker.Test.csproj | 6 + .../Requests/HandleWorkerInitRequestTests.cs | 45 ++++ .../UnitTest1.cs | 14 -- .../Utility/TypeConverterTests.cs | 9 + 20 files changed, 1052 insertions(+), 261 deletions(-) delete mode 100644 src/Azure.Functions.PowerShell.Worker/Context/Context.cs rename src/Azure.Functions.PowerShell.Worker/{Context/ContextHttpRequest.cs => Http/HttpRequestContext.cs} (60%) rename src/Azure.Functions.PowerShell.Worker/{Context/ContextHttpResponse.cs => Http/HttpResponseContext.cs} (63%) create mode 100644 src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs create mode 100644 src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs create mode 100644 test/Azure.Functions.PowerShell.Worker.Test/Requests/HandleWorkerInitRequestTests.cs delete mode 100644 test/Azure.Functions.PowerShell.Worker.Test/UnitTest1.cs create mode 100644 test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeConverterTests.cs diff --git a/examples/PSCoreApp/MyHttpTrigger/function.json b/examples/PSCoreApp/MyHttpTrigger/function.json index 18ab7de9..9fc9b080 100644 --- a/examples/PSCoreApp/MyHttpTrigger/function.json +++ b/examples/PSCoreApp/MyHttpTrigger/function.json @@ -1,5 +1,6 @@ { "disabled": false, + "entryPoint":"FunctionName", "bindings": [ { "authLevel": "function", @@ -14,7 +15,7 @@ { "type": "http", "direction": "out", - "name": "res" + "name": "$return" } ] } \ No newline at end of file diff --git a/examples/PSCoreApp/MyHttpTrigger/run.ps1 b/examples/PSCoreApp/MyHttpTrigger/run.ps1 index d766f17c..9271bc39 100644 --- a/examples/PSCoreApp/MyHttpTrigger/run.ps1 +++ b/examples/PSCoreApp/MyHttpTrigger/run.ps1 @@ -1,7 +1,6 @@ -param($req) - -$req.StatusCode = "201" -$req.Body.String = "hi" -return [PSCustomObject]@{ - res = $req +function FunctionName { + $global:res = $req.GetHttpResponseContext() + "hello verbose" + $res.Json('{"Hello":"World"}') + $res.SetHeader("foo", "bar") } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs index 63ad0959..4f576381 100644 --- a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs +++ b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs @@ -10,6 +10,7 @@ public class FunctionMessagingClient : IDisposable { public bool isDisposed = false; private AsyncDuplexStreamingCall _call; + private SemaphoreSlim _writeStreamHandle = new SemaphoreSlim(1, 1); public FunctionMessagingClient(string host, int port) { @@ -20,8 +21,15 @@ public FunctionMessagingClient(string host, int port) public async Task WriteAsync(StreamingMessage message) { if(isDisposed) return; - - await _call.RequestStream.WriteAsync(message); + await _writeStreamHandle.WaitAsync(); + try + { + await _call.RequestStream.WriteAsync(message); + } + finally + { + _writeStreamHandle.Release(); + } } public async Task MoveNext() diff --git a/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj b/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj index 7671027a..d7b0d5ce 100644 --- a/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj +++ b/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Azure.Functions.PowerShell.Worker/Context/Context.cs b/src/Azure.Functions.PowerShell.Worker/Context/Context.cs deleted file mode 100644 index 59b2d17f..00000000 --- a/src/Azure.Functions.PowerShell.Worker/Context/Context.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using Google.Protobuf.Collections; -using Microsoft.Azure.Functions.PowerShellWorker.Utility; -using Microsoft.Azure.WebJobs.Script.Grpc.Messages; - -namespace Microsoft.Azure.Functions.PowerShellWorker -{ - public class Context - { - public MapField Bindings {get; set;} - public MapField BindingData {get; set;} - public ExecutionContext ExecutionContext {get; set;} - public string InvocationId {get; set;} - public ContextHttpRequest Request {get; set;} - public ContextHttpResponse Response {get; set;} - - public Context (FunctionInfo info, InvocationRequest request) - { - InvocationId = request.InvocationId; - Bindings = new MapField(); - ExecutionContext = new ExecutionContext - { - InvocationId = request.InvocationId, - FunctionName = info.Name, - FunctionDirectory = info.Directory - }; - - - BindingData = request.TriggerMetadata; - BindingData.Add("InvocationId", new TypedData { - String = InvocationId - }); - } - - public static (Context context, List inputs) CreateContextAndInputs(FunctionInfo info, InvocationRequest request) - { - var context = new Context(info, request); - List inputs = new List(); - ContextHttpRequest httpInput = null; - foreach (var binding in request.InputData) - { - if (binding.Name != null && binding.Data != null ) - { - if (binding.Data.Http != null) - { - httpInput = TypeConverter.ToContextHttp(binding.Data.Http); - } - inputs.Add(binding.Data); - } - } - - if (httpInput != null) - { - context.Request = httpInput; - context.Response = new ContextHttpResponse(); - } - - return (context, inputs); - } - } - - public class ExecutionContext - { - public string InvocationId {get; internal set;} - public string FunctionName {get; internal set;} - public string FunctionDirectory {get; internal set;} - } -} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpRequest.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs similarity index 60% rename from src/Azure.Functions.PowerShell.Worker/Context/ContextHttpRequest.cs rename to src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs index 6b6d9c3c..ffa55652 100644 --- a/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs @@ -1,9 +1,9 @@ -using Google.Protobuf.Collections; +using Google.Protobuf.Collections; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; namespace Microsoft.Azure.Functions.PowerShellWorker { - public class ContextHttpRequest + public class HttpRequestContext { public string Method {get; set;} public string Url {get; set;} @@ -11,7 +11,12 @@ public class ContextHttpRequest public MapField Headers {get; set;} public MapField Query {get; set;} public MapField Params {get; set;} - public TypedData Body {get; set;} - public TypedData RawBody {get; set;} + public object Body {get; set;} + public object RawBody {get; set;} + + public HttpResponseContext GetHttpResponseContext() + { + return new HttpResponseContext(); + } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpResponse.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs similarity index 63% rename from src/Azure.Functions.PowerShell.Worker/Context/ContextHttpResponse.cs rename to src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs index 67fe63d4..60ac368c 100644 --- a/src/Azure.Functions.PowerShell.Worker/Context/ContextHttpResponse.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs @@ -1,20 +1,20 @@ -using Google.Protobuf.Collections; +using Google.Protobuf.Collections; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; namespace Microsoft.Azure.Functions.PowerShellWorker { - public class ContextHttpResponse + public class HttpResponseContext { #region properties public string StatusCode {get; set;} = "200"; public MapField Headers {get; set;} = new MapField(); public TypedData Body {get; set;} = new TypedData { String = "" }; - public bool EnableContentNegotiation {get; set;} + public bool EnableContentNegotiation {get; set;} = false; #endregion #region Helper functions for user to use to set data - public ContextHttpResponse Header(string field, string value) => + public HttpResponseContext Header(string field, string value) => SetHeader(field, value); - public ContextHttpResponse SetHeader(string field, string value) + public HttpResponseContext SetHeader(string field, string value) { Headers.Add(field, value); return this; @@ -23,30 +23,30 @@ public ContextHttpResponse SetHeader(string field, string value) public string GetHeader(string field) => Headers[field]; - public ContextHttpResponse RemoveHeader(string field) + public HttpResponseContext RemoveHeader(string field) { Headers.Remove(field); return this; } - public ContextHttpResponse Status(int statusCode) => + public HttpResponseContext Status(int statusCode) => SetStatus(statusCode); - public ContextHttpResponse Status(string statusCode) => + public HttpResponseContext Status(string statusCode) => SetStatus(statusCode); - public ContextHttpResponse SetStatus(int statusCode) => + public HttpResponseContext SetStatus(int statusCode) => SetStatus(statusCode); - public ContextHttpResponse SetStatus(string statusCode) + public HttpResponseContext SetStatus(string statusCode) { StatusCode = statusCode; return this; } - public ContextHttpResponse Type(string type) => + public HttpResponseContext Type(string type) => SetHeader("content-type", type); - public ContextHttpResponse SetContentType(string type) => + public HttpResponseContext SetContentType(string type) => SetHeader("content-type", type); - public ContextHttpResponse Send(int val) + public HttpResponseContext Send(int val) { Body = new TypedData { @@ -54,7 +54,7 @@ public ContextHttpResponse Send(int val) }; return this; } - public ContextHttpResponse Send(double val) + public HttpResponseContext Send(double val) { Body = new TypedData { @@ -62,7 +62,7 @@ public ContextHttpResponse Send(double val) }; return this; } - public ContextHttpResponse Send(string val) + public HttpResponseContext Send(string val) { Body = new TypedData { @@ -70,7 +70,7 @@ public ContextHttpResponse Send(string val) }; return this; } - public ContextHttpResponse Json(string val) { + public HttpResponseContext Json(string val) { Body = new TypedData { Json = val diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs new file mode 100644 index 00000000..829c92a7 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs @@ -0,0 +1,139 @@ +using System; +using System.Globalization; +using System.Management.Automation.Host; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; + +namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host +{ + /// + /// A sample implementation of the PSHost abstract class for console + /// applications. Not all members are implemented. Those that aren't throw a + /// NotImplementedException. + /// + internal class Host : PSHost + { + private RpcLogger _logger; + + /// + /// Creates an instance of the PSHostUserInterface object for this + /// application. + /// + private HostUserInterface HostUI; + + /// + /// The culture info of the thread that created + /// this object. + /// + private CultureInfo originalCultureInfo = System.Threading.Thread.CurrentThread.CurrentCulture; + + /// + /// The UI culture info of the thread that created + /// this object. + /// + private CultureInfo originalUICultureInfo = System.Threading.Thread.CurrentThread.CurrentUICulture; + + /// + /// The identifier of the PSHost implementation. + /// + private Guid Id = Guid.NewGuid(); + + /// + /// Initializes a new instance of the Host class. Keep + /// a reference to the hosting application object so it can + /// be informed of when to exit. + /// + /// A reference to the host application object. + + /// + /// Gets the culture info to use - this implementation just snapshots the + /// curture info of the thread that created this object. + /// + public override System.Globalization.CultureInfo CurrentCulture => originalCultureInfo; + + /// + /// Gets the UI culture info to use - this implementation just snapshots the + /// UI curture info of the thread that created this object. + /// + public override System.Globalization.CultureInfo CurrentUICulture => originalUICultureInfo; + + /// + /// Gets an identifier for this host. This implementation always returns + /// the GUID allocated at instantiation time. + /// + public override Guid InstanceId => Id; + + /// + /// Gets an appropriate string to identify you host implementation. + /// Keep in mind that this string may be used by script writers to identify + /// when your host is being used. + /// + public override string Name => "AzureFunctionsHost"; + + /// + /// Gets the implementation of the PSHostUserInterface class. + /// + public override PSHostUserInterface UI => HostUI; + + /// + /// Return the version object for this application. Typically this should match the version + /// resource in the application. + /// + public override Version Version => new Version(1, 0, 0, 0); + + public Host(RpcLogger logger) + { + _logger = logger; + + HostUI = new HostUserInterface(logger); + } + + /// + /// Not implemented by this example class. The call fails with an exception. + /// + public override void EnterNestedPrompt() + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// Not implemented by this example class. The call fails with an exception. + /// + public override void ExitNestedPrompt() + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// This API is called before an external application process is started. Typically + /// it's used to save state that the child process may alter so the parent can + /// restore that state when the child exits. In this sample, we don't need this so + /// the method simple returns. + /// + public override void NotifyBeginApplication() + { + return; // Do nothing. + } + + /// + /// This API is called after an external application process finishes. Typically + /// it's used to restore state that the child process may have altered. In this + /// sample, we don't need this so the method simple returns. + /// + public override void NotifyEndApplication() + { + return; // Do nothing. + } + + /// + /// Indicate to the host application that exit has + /// been requested. Pass the exit code that the host + /// application should use when exiting the process. + /// + /// The exit code that the host application should use. + public override void SetShouldExit(int exitCode) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + } +} + diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs new file mode 100644 index 00000000..86565cec --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Management.Automation; +using System.Management.Automation.Host; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host +{ + /// + /// An implementation of the PSHostUserInterface abstract class for console + /// applications. Few members are actually implemented. Those that aren't throw a + /// NotImplementedException. + /// + internal class HostUserInterface : PSHostUserInterface + { + private RpcLogger _logger; + + /// + /// An instance of the PSRawUserInterface object. + /// + private RawUserInterface RawUi = new RawUserInterface(); + + /// + /// Gets an instance of the PSRawUserInterface object for this host + /// application. + /// + public override PSHostRawUserInterface RawUI => RawUi; + + public HostUserInterface(RpcLogger logger) + { + _logger = logger; + } + + /// + /// Prompts the user for input. + /// + /// The caption or title of the prompt. + /// The text of the prompt. + /// A collection of FieldDescription objects that + /// describe each field of the prompt. + /// Throws a NotImplementedException exception. + public override Dictionary Prompt(string caption, string message, System.Collections.ObjectModel.Collection descriptions) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// Provides a set of choices that enable the user to choose a single option from a set of options. + /// + /// Text that proceeds (a title) the choices. + /// A message that describes the choice. + /// A collection of ChoiceDescription objects that describes + /// each choice. + /// The index of the label in the Choices parameter + /// collection. To indicate no default choice, set to -1. + /// Throws a NotImplementedException exception. + public override int PromptForChoice(string caption, string message, System.Collections.ObjectModel.Collection choices, int defaultChoice) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// Prompts the user for credentials with a specified prompt window caption, + /// prompt message, user name, and target name. + /// + /// The caption for the message window. + /// The text of the message. + /// The user name whose credential is to be prompted for. + /// The name of the target for which the credential is collected. + /// Throws a NotImplementedException exception. + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// Prompts the user for credentials by using a specified prompt window caption, + /// prompt message, user name and target name, credential types allowed to be + /// returned, and UI behavior options. + /// + /// The caption for the message window. + /// The text of the message. + /// The user name whose credential is to be prompted for. + /// The name of the target for which the credential is collected. + /// A PSCredentialTypes constant that + /// identifies the type of credentials that can be returned. + /// A PSCredentialUIOptions constant that identifies the UI + /// behavior when it gathers the credentials. + /// Throws a NotImplementedException exception. + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// Reads characters that are entered by the user until a newline + /// (carriage return) is encountered. + /// + /// The characters that are entered by the user. + public override string ReadLine() + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// Reads characters entered by the user until a newline (carriage return) + /// is encountered and returns the characters as a secure string. + /// + /// Throws a NotImplemented exception. + public override System.Security.SecureString ReadLineAsSecureString() + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// Writes a new line character (carriage return) to the output display + /// of the host. + /// + /// The characters to be written. + public override void Write(string value) + { + _logger.LogInformation(value); + } + + /// + /// Writes characters to the output display of the host with possible + /// foreground and background colors. This implementation ignores the colors. + /// + /// The color of the characters. + /// The backgound color to use. + /// The characters to be written. + public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + // Just ignore the colors. + _logger.LogInformation(value); + } + + /// + /// Writes a debug message to the output display of the host. + /// + /// The debug message that is displayed. + public override void WriteDebugLine(string message) + { + _logger.LogDebug(String.Format(CultureInfo.CurrentCulture, "DEBUG: {0}", message)); + } + + /// + /// Writes an error message to the output display of the host. + /// + /// The error message that is displayed. + public override void WriteErrorLine(string value) + { + _logger.LogError(String.Format(CultureInfo.CurrentCulture, "ERROR: {0}", value)); + } + + /// + /// Writes a newline character (carriage return) + /// to the output display of the host. + /// + public override void WriteLine() + { + //do nothing + } + + /// + /// Writes a line of characters to the output display of the host + /// and appends a newline character(carriage return). + /// + /// The line to be written. + public override void WriteLine(string value) + { + _logger.LogInformation(value); + } + + /// + /// Writes a line of characters to the output display of the host + /// with foreground and background colors and appends a newline (carriage return). + /// + /// The forground color of the display. + /// The background color of the display. + /// The line to be written. + public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + // Write to the log, ignore the colors + _logger.LogInformation(value); + } + + /// + /// Writes a progress report to the output display of the host. + /// + /// Unique identifier of the source of the record. + /// A ProgressReport object. + public override void WriteProgress(long sourceId, ProgressRecord record) + { + _logger.LogTrace(String.Format(CultureInfo.CurrentCulture, "PROGRESS: {0}", record.StatusDescription)); + } + + /// + /// Writes a verbose message to the output display of the host. + /// + /// The verbose message that is displayed. + public override void WriteVerboseLine(string message) + { + //Console.WriteLine(String.Format(CultureInfo.CurrentCulture, "VERBOSE: {0}", message)); + _logger.LogTrace(String.Format(CultureInfo.CurrentCulture, "VERBOSE: {0}", message)); + } + + /// + /// Writes a warning message to the output display of the host. + /// + /// The warning message that is displayed. + public override void WriteWarningLine(string message) + { + //Console.WriteLine(String.Format(CultureInfo.CurrentCulture, "WARNING: {0}", message)); + _logger.LogWarning(String.Format(CultureInfo.CurrentCulture, "WARNING: {0}", message)); + } + } +} + diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs new file mode 100644 index 00000000..1bbebb37 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs @@ -0,0 +1,179 @@ +using System; +using System.Management.Automation.Host; + +namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host +{ + /// + /// An implementation of the PSHostRawUserInterface for a console + /// application. Members of this class that map trivially to the .NET console + /// class are implemented. More complex methods are not implemented and will + /// throw a NotImplementedException. + /// + internal class RawUserInterface : PSHostRawUserInterface + { + /// + /// Gets or sets the background color of text to be written. + /// This maps pretty directly onto the corresponding .NET Console + /// property. + /// + public override ConsoleColor BackgroundColor + { + get { return Console.BackgroundColor; } + set { Console.BackgroundColor = value; } + } + + /// + /// Gets or sets the host buffer size adapted from on the .NET Console buffer size + /// + public override Size BufferSize + { + get { return new Size(Console.BufferWidth, Console.BufferHeight); } + set { Console.SetBufferSize(value.Width, value.Height); } + } + + /// + /// Gets or sets the cursor position. This functionality is not currently implemented. The call fails with an exception. + /// + public override Coordinates CursorPosition + { + get { throw new NotImplementedException("The method or operation is not implemented."); } + set { throw new NotImplementedException("The method or operation is not implemented."); } + } + + /// + /// Gets or sets the cursor size taken directly from the .NET Console cursor size. + /// + public override int CursorSize + { + get { return Console.CursorSize; } + set { Console.CursorSize = value; } + } + + /// + /// Gets or sets the foreground color of the text to be written. + /// This maps pretty directly onto the corresponding .NET Console + /// property. + /// + public override ConsoleColor ForegroundColor + { + get { return Console.ForegroundColor; } + set { Console.ForegroundColor = value; } + } + + /// + /// Gets a value indicating whether a key is available. This implementation + /// maps directly to the corresponding .NET Console property. + /// + public override bool KeyAvailable + { + get { return Console.KeyAvailable; } + } + + /// + /// Gets the maximum physical size of the window adapted from the + /// .NET Console LargestWindowWidth and LargestWindowHeight properties. + /// + public override Size MaxPhysicalWindowSize + { + get { return new Size(Console.LargestWindowWidth, Console.LargestWindowHeight); } + } + + /// + /// Gets the maximum window size adapted from the .NET Console + /// LargestWindowWidth and LargestWindowHeight properties. + /// + public override Size MaxWindowSize + { + get { return new Size(Console.LargestWindowWidth, Console.LargestWindowHeight); } + } + + /// + /// Gets or sets the window position adapted from the Console window position + /// information. + /// + public override Coordinates WindowPosition + { + get { return new Coordinates(Console.WindowLeft, Console.WindowTop); } + set { Console.SetWindowPosition(value.X, value.Y); } + } + + /// + /// Gets or sets the window size adapted from the corresponding .NET Console calls. + /// + public override Size WindowSize + { + get { return new Size(Console.WindowWidth, Console.WindowHeight); } + set { Console.SetWindowSize(value.Width, value.Height); } + } + + /// + /// Gets or sets the title of the window mapped to the Console.Title property. + /// + public override string WindowTitle + { + get { return Console.Title; } + set { Console.Title = value; } + } + + /// + /// This functionality is not currently implemented. The call simple returns silently. + /// + public override void FlushInputBuffer() + { + // Do nothing. + } + + /// + /// This functionality is not currently implemented. The call fails with an exception. + /// + /// This parameter is not used. + /// Throws a NotImplementedException exception. + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// This functionality is not currently implemented. The call fails with an exception. + /// + /// The parameter is not used. + /// Throws a NotImplementedException exception. + public override KeyInfo ReadKey(ReadKeyOptions options) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// This functionality is not currently implemented. The call fails with an exception. + /// + /// The parameter is not used. + /// The parameter is not used. + /// The parameter is not used. + /// The parameter is not used. + public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// This functionality is not currently implemented. The call fails with an exception. + /// + /// The parameter is not used. + /// The parameter is not used. + public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// This functionality is not currently implemented. The call fails with an exception. + /// + /// The parameter is not used. + /// The parameter is not used. + public override void SetBufferContents(Rectangle rectangle, BufferCell fill) + { + throw new NotImplementedException("The method or operation is not implemented."); + } + } +} + diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs new file mode 100644 index 00000000..5eb18f94 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs @@ -0,0 +1,34 @@ +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Requests +{ + using System.Management.Automation; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; + + public class HandleFunctionLoadRequest + { + public static StreamingMessage Invoke( + PowerShell powershell, + FunctionLoader functionLoader, + StreamingMessage request, + RpcLogger logger) + { + FunctionLoadRequest functionLoadRequest = request.FunctionLoadRequest; + functionLoader.Load(functionLoadRequest.FunctionId, functionLoadRequest.Metadata); + var response = new StreamingMessage() + { + RequestId = request.RequestId, + FunctionLoadResponse = new FunctionLoadResponse() + { + FunctionId = functionLoadRequest.FunctionId, + Result = new StatusResult() + { + Status = StatusResult.Types.Status.Success + } + } + }; + return response; + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs new file mode 100644 index 00000000..d56215a5 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Requests +{ + using System.Management.Automation; + using System.Text; + + public class HandleInvocationRequest + { + public static StreamingMessage Invoke( + PowerShell powershell, + FunctionLoader functionLoader, + StreamingMessage request, + RpcLogger logger) + { + InvocationRequest invocationRequest = request.InvocationRequest; + logger.SetContext(request.RequestId, invocationRequest.InvocationId); + + var status = new StatusResult() { Status = StatusResult.Types.Status.Success }; + var response = new StreamingMessage() + { + RequestId = request.RequestId, + InvocationResponse = new InvocationResponse() + { + InvocationId = invocationRequest.InvocationId, + Result = status + } + }; + + var info = functionLoader.GetInfo(invocationRequest.FunctionId); + + // Add $Context variable, which contains trigger metadata, to the Global scope + Hashtable triggerMetadata = new Hashtable(); + foreach (var dataItem in invocationRequest.TriggerMetadata) + { + triggerMetadata.Add(dataItem.Key, TypeConverter.FromTypedData(dataItem.Value)); + } + + if (triggerMetadata.Count > 0) + { + powershell.AddCommand("Set-Variable").AddParameters( new Hashtable { + { "Name", "Context"}, + { "Scope", "Global"}, + { "Value", triggerMetadata} + }); + powershell.Invoke(); + } + + foreach (ParameterBinding binding in invocationRequest.InputData) + { + powershell.AddCommand("Set-Variable").AddParameters( new Hashtable { + { "Name", binding.Name}, + { "Scope", "Global"}, + { "Value", TypeConverter.FromTypedData(binding.Data)} + }); + powershell.Invoke(); + } + + // foreach (KeyValuePair binding in info.OutputBindings) + // { + // powershell.AddCommand("Set-Variable").AddParameters( new Hashtable { + // { "Name", binding.Key}, + // { "Scope", "Global"}, + // { "Value", null} + // }); + // powershell.Invoke(); + // } + + (string scriptPath, string entryPoint) = functionLoader.GetFunc(invocationRequest.FunctionId); + + if(entryPoint != "") + { + powershell.AddScript($@". {scriptPath}"); + powershell.Invoke(); + powershell.AddCommand(entryPoint); + } + else + { + powershell.AddCommand(scriptPath); + } + + powershell.AddScript(@" +param([Parameter(ValueFromPipeline=$true)]$return) + +$return | Out-Default + +Set-Variable -Name '$return' -Value $return -Scope global +"); + + StringBuilder script = new StringBuilder(); + script.AppendLine("@{"); + foreach (KeyValuePair binding in info.OutputBindings) + { + script.Append("'"); + script.Append(binding.Key); + + // since $return has a dollar sign, we have to treat it differently + if (binding.Key == "$return") + { + script.Append("' = "); + } + else + { + script.Append("' = $"); + } + script.AppendLine(binding.Key); + } + script.AppendLine("}"); + + Hashtable result = null; + try + { + powershell.Invoke(); + powershell.AddScript(script.ToString()); + result = powershell.Invoke()[0]; + } + catch (Exception e) + { + status.Status = StatusResult.Types.Status.Failure; + status.Exception = TypeConverter.ToRpcException(e); + powershell.Commands.Clear(); + return response; + } + powershell.Commands.Clear(); + + foreach (KeyValuePair binding in info.OutputBindings) + { + ParameterBinding paramBinding = new ParameterBinding() + { + Name = binding.Key, + Data = TypeConverter.ToTypedData( + binding.Key, + binding.Value, + result[binding.Key]) + }; + + response.InvocationResponse.OutputData.Add(paramBinding); + + if(binding.Key == "$return") + { + response.InvocationResponse.ReturnValue = paramBinding.Data; + } + } + + return response; + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs new file mode 100644 index 00000000..b5d21cd0 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs @@ -0,0 +1,31 @@ +using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Requests +{ + using System.Management.Automation; + + public class HandleWorkerInitRequest + { + public static StreamingMessage Invoke( + PowerShell powershell, + FunctionLoader functionLoader, + StreamingMessage request, + RpcLogger logger) + { + var response = new StreamingMessage() + { + RequestId = request.RequestId, + WorkerInitResponse = new WorkerInitResponse() + { + Result = new StatusResult() + { + Status = StatusResult.Types.Status.Success + } + } + }; + return response; + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs new file mode 100644 index 00000000..497ef5b6 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs @@ -0,0 +1,77 @@ +using System; +using Azure.Functions.PowerShell.Worker.Messaging; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Microsoft.Extensions.Logging; +using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Utility +{ + public class RpcLogger : ILogger + { + private FunctionMessagingClient _Client; + private string _invocationId = ""; + private string _requestId = ""; + + public RpcLogger(FunctionMessagingClient client) + { + _Client = client; + } + + public void SetContext(string requestId, string invocationId) + { + _requestId = requestId; + _invocationId = invocationId; + } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + throw new NotImplementedException(); + } + + public async void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (_Client != null) + { + var logMessage = new StreamingMessage + { + RequestId = _requestId, + RpcLog = new RpcLog() + { + Exception = exception == null ? null : TypeConverter.ToRpcException(exception), + InvocationId = _invocationId, + Level = ConvertLogLevel(logLevel), + Message = formatter(state, exception) + } + }; + + await _Client.WriteAsync(logMessage); + } + } + + public static Level ConvertLogLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Critical: + return Level.Critical; + case LogLevel.Debug: + return Level.Debug; + case LogLevel.Error: + return Level.Error; + case LogLevel.Information: + return Level.Information; + case LogLevel.Trace: + return Level.Trace; + case LogLevel.Warning: + return Level.Warning; + default: + return Level.None; + } + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs b/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs index 23ff3c7b..a2125377 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs @@ -2,72 +2,48 @@ using Google.Protobuf; using Microsoft.Azure.Functions.PowerShellWorker; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using System.Net.Http; using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.TypedData; +using System; namespace Microsoft.Azure.Functions.PowerShellWorker.Utility { public class TypeConverter { - public static ContextHttpRequest ToContextHttp (RpcHttp rpcHttp) - { - return new ContextHttpRequest - { - Method = rpcHttp.Method, - Url = rpcHttp.Url, - OriginalUrl = rpcHttp.Url, - Headers = rpcHttp.Headers, - Params = rpcHttp.Params, - Body = rpcHttp.Body, - RawBody = rpcHttp.RawBody, - Query = rpcHttp.Query - }; - } - - public static RpcHttp ToRpcHttp (ContextHttpResponse contextHttpResponse) - { - var rpcHttp = new RpcHttp - { - StatusCode = contextHttpResponse.StatusCode, - Body = contextHttpResponse.Body, - EnableContentNegotiation = contextHttpResponse.EnableContentNegotiation - }; - rpcHttp.Headers.Add(contextHttpResponse.Headers); - - return rpcHttp; - } - public static object FromTypedData (TypedData data) { switch (data.DataCase) { case DataOneofCase.Json: + // consider doing ConvertFrom-Json return data.Json; case DataOneofCase.Bytes: return data.Bytes; case DataOneofCase.Double: return data.Double; case DataOneofCase.Http: - return data.Http; + return ToHttpContext(data.Http); case DataOneofCase.Int: return data.Int; case DataOneofCase.Stream: return data.Stream; case DataOneofCase.String: return data.String; - default: - // possibly throw? + case DataOneofCase.None: return null; + default: + return new InvalidOperationException("Data Case was not set."); } } - public static TypedData ToTypedData (string bindingName, BindingInfo binding, PSObject psobject) + public static TypedData ToTypedData (string bindingName, BindingInfo binding, object psobject) { switch (binding.Type) { case "json": if(!LanguagePrimitives.TryConvertTo( - psobject.Properties[bindingName]?.Value, + psobject, out string jsonVal)) { throw new PSInvalidCastException(); @@ -80,7 +56,7 @@ public static TypedData ToTypedData (string bindingName, BindingInfo binding, PS case "bytes": if(!LanguagePrimitives.TryConvertTo( - psobject.Properties[bindingName]?.Value, + psobject, out ByteString bytesVal)) { throw new PSInvalidCastException(); @@ -93,7 +69,7 @@ public static TypedData ToTypedData (string bindingName, BindingInfo binding, PS case "double": if(!LanguagePrimitives.TryConvertTo( - psobject.Properties[bindingName]?.Value, + psobject, out double doubleVal)) { throw new PSInvalidCastException(); @@ -105,21 +81,21 @@ public static TypedData ToTypedData (string bindingName, BindingInfo binding, PS case "http": - if(!LanguagePrimitives.TryConvertTo( - psobject.Properties[bindingName]?.Value, - out RpcHttp httpVal)) + if(!LanguagePrimitives.TryConvertTo( + psobject, + out HttpResponseContext httpVal)) { throw new PSInvalidCastException(); } return new TypedData() { - Http = httpVal + Http = ToRpcHttp(httpVal) }; case "int": if(!LanguagePrimitives.TryConvertTo( - psobject.Properties[bindingName]?.Value, + psobject, out int intVal)) { throw new PSInvalidCastException(); @@ -132,7 +108,7 @@ public static TypedData ToTypedData (string bindingName, BindingInfo binding, PS case "stream": if(!LanguagePrimitives.TryConvertTo( - psobject.Properties[bindingName]?.Value, + psobject, out ByteString streamVal)) { throw new PSInvalidCastException(); @@ -145,7 +121,7 @@ public static TypedData ToTypedData (string bindingName, BindingInfo binding, PS case "string": if(!LanguagePrimitives.TryConvertTo( - psobject.Properties[bindingName]?.Value, + psobject, out string stringVal)) { throw new PSInvalidCastException(); @@ -158,5 +134,57 @@ public static TypedData ToTypedData (string bindingName, BindingInfo binding, PS throw new PSInvalidCastException("could not parse type"); } } + + public static HttpRequestContext ToHttpContext (RpcHttp rpcHttp) + { + var httpRequestContext = new HttpRequestContext + { + Method = rpcHttp.Method, + Url = rpcHttp.Url, + OriginalUrl = rpcHttp.Url, + Headers = rpcHttp.Headers, + Params = rpcHttp.Params, + Query = rpcHttp.Query + }; + + if (rpcHttp.Body != null) + { + httpRequestContext.Body = FromTypedData(rpcHttp.Body); + } + + if (rpcHttp.RawBody != null) + { + httpRequestContext.Body = FromTypedData(rpcHttp.RawBody); + } + + return httpRequestContext; + } + + public static RpcHttp ToRpcHttp (HttpResponseContext httpResponseContext) + { + var rpcHttp = new RpcHttp + { + StatusCode = httpResponseContext.StatusCode?? "200" + }; + + if (httpResponseContext.Body != null) + { + rpcHttp.Body = httpResponseContext.Body; + } + + rpcHttp.Headers.Add(httpResponseContext.Headers); + + return rpcHttp; + } + + public static RpcException ToRpcException (Exception exception) + { + return new RpcException + { + Message = exception?.Message, + Source = exception?.Source ?? "", + StackTrace = exception?.StackTrace ?? "" + }; + } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Worker.cs b/src/Azure.Functions.PowerShell.Worker/Worker.cs index 36d4b87d..6f5ea007 100644 --- a/src/Azure.Functions.PowerShell.Worker/Worker.cs +++ b/src/Azure.Functions.PowerShell.Worker/Worker.cs @@ -9,6 +9,11 @@ using Azure.Functions.PowerShell.Worker.Messaging; using Microsoft.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using System.Collections; +using Microsoft.Azure.Functions.PowerShellWorker.Requests; +using Microsoft.Extensions.Logging; +using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; +using Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host; namespace Microsoft.Azure.Functions.PowerShellWorker { @@ -16,7 +21,9 @@ public class Worker { private static FunctionMessagingClient s_client; private static System.Management.Automation.PowerShell s_ps; + private static Runspace s_runspace; private static FunctionLoader s_FunctionLoader = new FunctionLoader(); + private static RpcLogger s_Logger; public async static Task Main(string[] args) { if (args.Length != 10) @@ -27,12 +34,14 @@ public async static Task Main(string[] args) StartupArguments startupArguments = StartupArguments.Parse(args); s_client = new FunctionMessagingClient(startupArguments.Host, startupArguments.Port); + s_Logger = new RpcLogger(s_client); InitPowerShell(); var streamingMessage = new StreamingMessage() { RequestId = startupArguments.RequestId, StartStream = new StartStream() { WorkerId = startupArguments.WorkerId } }; + await s_client.WriteAsync(streamingMessage); await ProcessEvent(); @@ -40,7 +49,22 @@ public async static Task Main(string[] args) private static void InitPowerShell() { - s_ps = System.Management.Automation.PowerShell.Create(InitialSessionState.CreateDefault2()); + // var events = new StreamEvents(s_Logger); + var host = new Host(s_Logger); + + s_runspace = RunspaceFactory.CreateRunspace(host); + s_runspace.Open(); + s_ps = System.Management.Automation.PowerShell.Create(InitialSessionState.CreateDefault()); + s_ps.Runspace = s_runspace; + + // Setup Stream event listeners + // s_ps.Streams.Debug.DataAdded += events.DebugDataAdded; + // s_ps.Streams.Error.DataAdded += events.ErrorDataAdded; + // s_ps.Streams.Information.DataAdded += events.InformationDataAdded; + // s_ps.Streams.Progress.DataAdded += events.ProgressDataAdded; + // s_ps.Streams.Verbose.DataAdded += events.VerboseDataAdded; + // s_ps.Streams.Warning.DataAdded += events.WarningDataAdded; + s_ps.AddScript("$PSHOME"); //s_ps.AddCommand("Set-ExecutionPolicy").AddParameter("ExecutionPolicy", ExecutionPolicy.Unrestricted).AddParameter("Scope", ExecutionPolicyScope.Process); var result = s_ps.Invoke(); @@ -56,128 +80,40 @@ private static async Task ProcessEvent() while (await s_client.MoveNext()) { var message = s_client.GetCurrentMessage(); + StreamingMessage response; switch (message.ContentCase) { case StreamingMessage.ContentOneofCase.WorkerInitRequest: - await HandleWorkerInitRequest(message.RequestId, message.WorkerInitRequest); + response = HandleWorkerInitRequest.Invoke( + s_ps, + s_FunctionLoader, + message, + s_Logger); break; case StreamingMessage.ContentOneofCase.FunctionLoadRequest: - await HandleFunctionLoadRequest(message.RequestId, message.FunctionLoadRequest); + response = HandleFunctionLoadRequest.Invoke( + s_ps, + s_FunctionLoader, + message, + s_Logger); break; case StreamingMessage.ContentOneofCase.InvocationRequest: - await HandleInvocationRequest(message.RequestId, message.InvocationRequest); + response = HandleInvocationRequest.Invoke( + s_ps, + s_FunctionLoader, + message, + s_Logger); break; default: throw new InvalidOperationException($"Not supportted message type: {message.ContentCase}"); } - } - } - } - - private static async Task HandleWorkerInitRequest(string requestId, WorkerInitRequest initRequest) - { - var response = new StreamingMessage() - { - RequestId = requestId, - WorkerInitResponse = new WorkerInitResponse() - { - Result = new StatusResult() - { - Status = StatusResult.Types.Status.Success - } - } - }; - await s_client.WriteAsync(response); - } - - private static async Task HandleFunctionLoadRequest(string requestId, FunctionLoadRequest loadRequest) - { - s_FunctionLoader.Load(loadRequest.FunctionId, loadRequest.Metadata); - var response = new StreamingMessage() - { - RequestId = requestId, - FunctionLoadResponse = new FunctionLoadResponse() - { - FunctionId = loadRequest.FunctionId, - Result = new StatusResult() - { - Status = StatusResult.Types.Status.Success - } - } - }; - await s_client.WriteAsync(response); - } - - private static async Task HandleInvocationRequest(string requestId, InvocationRequest request) - { - var status = new StatusResult() { Status = StatusResult.Types.Status.Success }; - var response = new StreamingMessage() - { - RequestId = requestId, - InvocationResponse = new InvocationResponse() - { - InvocationId = request.InvocationId, - Result = status - } - }; - - var info = s_FunctionLoader.GetInfo(request.FunctionId); - // (Context context, List inputs) = Context.CreateContextAndInputs(info, request); - (string scriptPath, string entryPoint) = s_FunctionLoader.GetFunc(request.FunctionId); - - if(entryPoint != "") - { - s_ps.AddCommand(entryPoint); - } - else - { - s_ps.AddCommand(scriptPath); - } - - foreach (ParameterBinding binding in request.InputData) - { - s_ps.AddParameter(binding.Name, TypeConverter.FromTypedData(binding.Data)); - } - - // s_ps.AddParameter("context", context); - // foreach (TypedData input in inputs) - // { - // s_ps.AddArgument(input); - // } - PSObject result = null; - try - { - result = s_ps.Invoke()[0]; - } - finally - { - s_ps.Commands.Clear(); - } - foreach (var binding in info.OutputBindings) - { - ParameterBinding paramBinding = new ParameterBinding() - { - Name = binding.Key, - Data = TypeConverter.ToTypedData( - binding.Key, - binding.Value, - result) - }; - - // Not exactly sure which one to use for what scenario, so just set both. - response.InvocationResponse.OutputData.Add(paramBinding); - - if(binding.Key == "$return") - { - response.InvocationResponse.ReturnValue = paramBinding.Data; + await s_client.WriteAsync(response); } } - - await s_client.WriteAsync(response); } } -} +} \ No newline at end of file diff --git a/test/Azure.Functions.PowerShell.Worker.Test/Azure.Functions.PowerShell.Worker.Test.csproj b/test/Azure.Functions.PowerShell.Worker.Test/Azure.Functions.PowerShell.Worker.Test.csproj index 061aa24f..f442707f 100644 --- a/test/Azure.Functions.PowerShell.Worker.Test/Azure.Functions.PowerShell.Worker.Test.csproj +++ b/test/Azure.Functions.PowerShell.Worker.Test/Azure.Functions.PowerShell.Worker.Test.csproj @@ -11,6 +11,12 @@ + + + + + + diff --git a/test/Azure.Functions.PowerShell.Worker.Test/Requests/HandleWorkerInitRequestTests.cs b/test/Azure.Functions.PowerShell.Worker.Test/Requests/HandleWorkerInitRequestTests.cs new file mode 100644 index 00000000..521cea05 --- /dev/null +++ b/test/Azure.Functions.PowerShell.Worker.Test/Requests/HandleWorkerInitRequestTests.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.Azure.Functions.PowerShellWorker.Requests; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Xunit; + +namespace Azure.Functions.PowerShell.Worker.Test +{ + using System.Management.Automation; + using System.Management.Automation.Runspaces; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; + + public class HandleWorkerInitRequestTests + { + [Fact] + public void HandleWorkerInitRequestSuccess() + { + var requestId = "testRequest"; + var status = StatusResult.Types.Status.Success; + var expectedResponse = new StreamingMessage() + { + RequestId = requestId, + WorkerInitResponse = new WorkerInitResponse() + { + Result = new StatusResult() + { + Status = status + } + } + }; + + StreamingMessage result = HandleWorkerInitRequest.Invoke( + null, + null, + new StreamingMessage() + { + RequestId = requestId + }, + new RpcLogger(null) + ); + + Assert.Equal(requestId, result.RequestId); + Assert.Equal(status, result.WorkerInitResponse.Result.Status); + } + } +} diff --git a/test/Azure.Functions.PowerShell.Worker.Test/UnitTest1.cs b/test/Azure.Functions.PowerShell.Worker.Test/UnitTest1.cs deleted file mode 100644 index 0b9576bc..00000000 --- a/test/Azure.Functions.PowerShell.Worker.Test/UnitTest1.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Xunit; - -namespace Azure.Functions.PowerShell.Worker.Test -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeConverterTests.cs b/test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeConverterTests.cs new file mode 100644 index 00000000..7d690934 --- /dev/null +++ b/test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeConverterTests.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace Azure.Functions.PowerShell.Worker.Test +{ + public class TypeConverterTests + { + + } +} \ No newline at end of file From 91b2bf851642c05a1d60a9e07a22294b6f0ed975 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 22 Aug 2018 13:49:42 -0700 Subject: [PATCH 3/8] Change ToTypedData logic to match Host --- .../Azure.Functions.PowerShell.Worker.csproj | 1 + .../Requests/HandleInvocationRequest.cs | 2 - .../Utility/TypeConverter.cs | 134 +++++------------- 3 files changed, 39 insertions(+), 98 deletions(-) diff --git a/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj b/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj index d7b0d5ce..bfe63885 100644 --- a/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj +++ b/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs index d56215a5..02cdbfda 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs @@ -135,8 +135,6 @@ public static StreamingMessage Invoke( { Name = binding.Key, Data = TypeConverter.ToTypedData( - binding.Key, - binding.Value, result[binding.Key]) }; diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs b/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs index a2125377..a83da137 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs @@ -5,12 +5,14 @@ using System.Net.Http; using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.TypedData; using System; +using Newtonsoft.Json; +using System.Collections; namespace Microsoft.Azure.Functions.PowerShellWorker.Utility { public class TypeConverter { - public static object FromTypedData (TypedData data) + public static object ToObject (TypedData data) { switch (data.DataCase) { @@ -36,103 +38,43 @@ public static object FromTypedData (TypedData data) } } - public static TypedData ToTypedData (string bindingName, BindingInfo binding, object psobject) + public static TypedData ToTypedData(object value) { - switch (binding.Type) - { - case "json": - - if(!LanguagePrimitives.TryConvertTo( - psobject, - out string jsonVal)) - { - throw new PSInvalidCastException(); - } - return new TypedData() - { - Json = jsonVal - }; - - case "bytes": - - if(!LanguagePrimitives.TryConvertTo( - psobject, - out ByteString bytesVal)) - { - throw new PSInvalidCastException(); - } - return new TypedData() - { - Bytes = bytesVal - }; - - case "double": - - if(!LanguagePrimitives.TryConvertTo( - psobject, - out double doubleVal)) - { - throw new PSInvalidCastException(); - } - return new TypedData() - { - Double = doubleVal - }; - - case "http": - - if(!LanguagePrimitives.TryConvertTo( - psobject, - out HttpResponseContext httpVal)) - { - throw new PSInvalidCastException(); - } - return new TypedData() - { - Http = ToRpcHttp(httpVal) - }; - - case "int": - - if(!LanguagePrimitives.TryConvertTo( - psobject, - out int intVal)) - { - throw new PSInvalidCastException(); - } - return new TypedData() - { - Int = intVal - }; - - case "stream": - - if(!LanguagePrimitives.TryConvertTo( - psobject, - out ByteString streamVal)) - { - throw new PSInvalidCastException(); - } - return new TypedData() - { - Stream = streamVal - }; + TypedData typedData = new TypedData(); - case "string": + if (value == null) + { + return typedData; + } - if(!LanguagePrimitives.TryConvertTo( - psobject, - out string stringVal)) - { - throw new PSInvalidCastException(); - } - return new TypedData() - { - String = stringVal - }; - default: - throw new PSInvalidCastException("could not parse type"); + if (LanguagePrimitives.TryConvertTo( + value, out byte[] arr)) + { + typedData.Bytes = ByteString.CopyFrom(arr); + } + else if(LanguagePrimitives.TryConvertTo( + value, out HttpResponseContext http)) + { + typedData.Http = ToRpcHttp(http); + } + else if (LanguagePrimitives.TryConvertTo( + value, out Hashtable hashtable)) + { + typedData.Json = JsonConvert.SerializeObject(hashtable); + } + else if (LanguagePrimitives.TryConvertTo( + value, out string str)) + { + try + { + typedData.Json = JsonConvert.SerializeObject(str); + } + catch + { + typedData.String = str; + } } + return typedData; } public static HttpRequestContext ToHttpContext (RpcHttp rpcHttp) @@ -149,12 +91,12 @@ public static HttpRequestContext ToHttpContext (RpcHttp rpcHttp) if (rpcHttp.Body != null) { - httpRequestContext.Body = FromTypedData(rpcHttp.Body); + httpRequestContext.Body = ToObject(rpcHttp.Body); } if (rpcHttp.RawBody != null) { - httpRequestContext.Body = FromTypedData(rpcHttp.RawBody); + httpRequestContext.Body = ToObject(rpcHttp.RawBody); } return httpRequestContext; From e395476fea7d3e9ed784b2c1314faee7b7f3bd84 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 23 Aug 2018 19:25:45 -0700 Subject: [PATCH 4/8] added a bunch of comments --- .../PSCoreApp/MyHttpTrigger/function.json | 3 +- examples/PSCoreApp/MyHttpTrigger/run.ps1 | 16 ++- .../FunctionMessagingClient.cs | 3 + .../Function/FunctionInfo.cs | 4 +- .../Http/HttpRequestContext.cs | 5 - .../Http/HttpResponseContext.cs | 75 +--------- .../Host/{Host.cs => AzureFunctionsHost.cs} | 18 +-- .../PowerShell/Host/HostUserInterface.cs | 19 +-- .../PowerShell/PowerShellWorkerExtensions.cs | 131 ++++++++++++++++++ .../Requests/HandleFunctionLoadRequest.cs | 28 +++- .../Requests/HandleInvocationRequest.cs | 114 ++++----------- .../Requests/HandleWorkerInitRequest.cs | 3 +- .../StartupArguments.cs | 6 - .../Utility/RpcLogger.cs | 2 +- .../{TypeConverter.cs => TypeExtensions.cs} | 42 ++++-- .../Worker.cs | 22 ++- 16 files changed, 254 insertions(+), 237 deletions(-) rename src/Azure.Functions.PowerShell.Worker/PowerShell/Host/{Host.cs => AzureFunctionsHost.cs} (90%) create mode 100644 src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs rename src/Azure.Functions.PowerShell.Worker/Utility/{TypeConverter.cs => TypeExtensions.cs} (69%) diff --git a/examples/PSCoreApp/MyHttpTrigger/function.json b/examples/PSCoreApp/MyHttpTrigger/function.json index 9fc9b080..18ab7de9 100644 --- a/examples/PSCoreApp/MyHttpTrigger/function.json +++ b/examples/PSCoreApp/MyHttpTrigger/function.json @@ -1,6 +1,5 @@ { "disabled": false, - "entryPoint":"FunctionName", "bindings": [ { "authLevel": "function", @@ -15,7 +14,7 @@ { "type": "http", "direction": "out", - "name": "$return" + "name": "res" } ] } \ No newline at end of file diff --git a/examples/PSCoreApp/MyHttpTrigger/run.ps1 b/examples/PSCoreApp/MyHttpTrigger/run.ps1 index 9271bc39..121371d6 100644 --- a/examples/PSCoreApp/MyHttpTrigger/run.ps1 +++ b/examples/PSCoreApp/MyHttpTrigger/run.ps1 @@ -1,6 +1,12 @@ -function FunctionName { - $global:res = $req.GetHttpResponseContext() - "hello verbose" - $res.Json('{"Hello":"World"}') - $res.SetHeader("foo", "bar") +$name = 'World' +if($req.Query.Name) { + $name = $req.Query.Name +} + +Write-Verbose "Hello $name" -Verbose +Write-Warning "Warning $name" + +$res = [HttpResponseContext]@{ + Body = @{ Hello = $name } + ContentType = 'application/json' } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs index 4f576381..3aeb5e07 100644 --- a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs +++ b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs @@ -21,6 +21,9 @@ public FunctionMessagingClient(string host, int port) public async Task WriteAsync(StreamingMessage message) { if(isDisposed) return; + + // Wait for the handle to be released because we can't have + // more than one message being sent at the same time await _writeStreamHandle.WaitAsync(); try { diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs index d16253bc..617f088f 100644 --- a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs @@ -24,12 +24,10 @@ public FunctionInfo(RpcFunctionMetadata metadata) { Bindings.Add(binding.Key, binding.Value); + // Only add Out and InOut bindings to the OutputBindings if (binding.Value.Direction != BindingInfo.Types.Direction.In) { if(binding.Value.Type == "http") - { - HttpOutputName = binding.Key; - }if(binding.Value.Type == "http") { HttpOutputName = binding.Key; } diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs index ffa55652..aef21832 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs @@ -13,10 +13,5 @@ public class HttpRequestContext public MapField Params {get; set;} public object Body {get; set;} public object RawBody {get; set;} - - public HttpResponseContext GetHttpResponseContext() - { - return new HttpResponseContext(); - } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs index 60ac368c..5eef4a3e 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs @@ -1,3 +1,4 @@ +using System.Collections; using Google.Protobuf.Collections; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; @@ -5,78 +6,10 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { public class HttpResponseContext { -#region properties public string StatusCode {get; set;} = "200"; - public MapField Headers {get; set;} = new MapField(); - public TypedData Body {get; set;} = new TypedData { String = "" }; + public Hashtable Headers {get; set;} = new Hashtable(); + public object Body {get; set;} + public string ContentType {get; set;} = "text/plain"; public bool EnableContentNegotiation {get; set;} = false; -#endregion -#region Helper functions for user to use to set data - public HttpResponseContext Header(string field, string value) => - SetHeader(field, value); - public HttpResponseContext SetHeader(string field, string value) - { - Headers.Add(field, value); - return this; - } - - public string GetHeader(string field) => - Headers[field]; - - public HttpResponseContext RemoveHeader(string field) - { - Headers.Remove(field); - return this; - } - - public HttpResponseContext Status(int statusCode) => - SetStatus(statusCode); - public HttpResponseContext Status(string statusCode) => - SetStatus(statusCode); - public HttpResponseContext SetStatus(int statusCode) => - SetStatus(statusCode); - public HttpResponseContext SetStatus(string statusCode) - { - StatusCode = statusCode; - return this; - } - - public HttpResponseContext Type(string type) => - SetHeader("content-type", type); - public HttpResponseContext SetContentType(string type) => - SetHeader("content-type", type); - - public HttpResponseContext Send(int val) - { - Body = new TypedData - { - Int = val - }; - return this; - } - public HttpResponseContext Send(double val) - { - Body = new TypedData - { - Double = val - }; - return this; - } - public HttpResponseContext Send(string val) - { - Body = new TypedData - { - String = val - }; - return this; - } - public HttpResponseContext Json(string val) { - Body = new TypedData - { - Json = val - }; - return Type("application/json"); - } -#endregion } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs similarity index 90% rename from src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs rename to src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs index 829c92a7..d09eaacb 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/Host.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs @@ -10,8 +10,11 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// applications. Not all members are implemented. Those that aren't throw a /// NotImplementedException. /// - internal class Host : PSHost + internal class AzureFunctionsHost : PSHost { + /// + /// The private reference of the logger. + /// private RpcLogger _logger; /// @@ -80,15 +83,14 @@ internal class Host : PSHost /// public override Version Version => new Version(1, 0, 0, 0); - public Host(RpcLogger logger) + public AzureFunctionsHost(RpcLogger logger) { _logger = logger; - HostUI = new HostUserInterface(logger); } /// - /// Not implemented by this example class. The call fails with an exception. + /// Not implemented by this class. The call fails with an exception. /// public override void EnterNestedPrompt() { @@ -96,7 +98,7 @@ public override void EnterNestedPrompt() } /// - /// Not implemented by this example class. The call fails with an exception. + /// Not implemented by this class. The call fails with an exception. /// public override void ExitNestedPrompt() { @@ -106,7 +108,7 @@ public override void ExitNestedPrompt() /// /// This API is called before an external application process is started. Typically /// it's used to save state that the child process may alter so the parent can - /// restore that state when the child exits. In this sample, we don't need this so + /// restore that state when the child exits. In this, we don't need this so /// the method simple returns. /// public override void NotifyBeginApplication() @@ -116,8 +118,8 @@ public override void NotifyBeginApplication() /// /// This API is called after an external application process finishes. Typically - /// it's used to restore state that the child process may have altered. In this - /// sample, we don't need this so the method simple returns. + /// it's used to restore state that the child process may have altered. In this, + /// we don't need this so the method simple returns. /// public override void NotifyEndApplication() { diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs index 86565cec..b0173481 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs @@ -15,6 +15,9 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// internal class HostUserInterface : PSHostUserInterface { + /// + /// The private reference of the logger. + /// private RpcLogger _logger; /// @@ -40,7 +43,7 @@ public HostUserInterface(RpcLogger logger) /// The text of the prompt. /// A collection of FieldDescription objects that /// describe each field of the prompt. - /// Throws a NotImplementedException exception. + /// Throws a NotImplementedException exception because we don't need a prompt. public override Dictionary Prompt(string caption, string message, System.Collections.ObjectModel.Collection descriptions) { throw new NotImplementedException("The method or operation is not implemented."); @@ -55,7 +58,7 @@ public override Dictionary Prompt(string caption, string messa /// each choice. /// The index of the label in the Choices parameter /// collection. To indicate no default choice, set to -1. - /// Throws a NotImplementedException exception. + /// Throws a NotImplementedException exception because we don't need a prompt. public override int PromptForChoice(string caption, string message, System.Collections.ObjectModel.Collection choices, int defaultChoice) { throw new NotImplementedException("The method or operation is not implemented."); @@ -69,7 +72,7 @@ public override int PromptForChoice(string caption, string message, System.Colle /// The text of the message. /// The user name whose credential is to be prompted for. /// The name of the target for which the credential is collected. - /// Throws a NotImplementedException exception. + /// Throws a NotImplementedException exception because we don't need a prompt. public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) { throw new NotImplementedException("The method or operation is not implemented."); @@ -88,7 +91,7 @@ public override PSCredential PromptForCredential(string caption, string message, /// identifies the type of credentials that can be returned. /// A PSCredentialUIOptions constant that identifies the UI /// behavior when it gathers the credentials. - /// Throws a NotImplementedException exception. + /// Throws a NotImplementedException exception because we don't need a prompt. public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) { throw new NotImplementedException("The method or operation is not implemented."); @@ -98,7 +101,7 @@ public override PSCredential PromptForCredential(string caption, string message, /// Reads characters that are entered by the user until a newline /// (carriage return) is encountered. /// - /// The characters that are entered by the user. + /// Throws a NotImplemented exception because we are in a non-interactive experience. public override string ReadLine() { throw new NotImplementedException("The method or operation is not implemented."); @@ -108,7 +111,7 @@ public override string ReadLine() /// Reads characters entered by the user until a newline (carriage return) /// is encountered and returns the characters as a secure string. /// - /// Throws a NotImplemented exception. + /// Throws a NotImplemented exception because we are in a non-interactive experience. public override System.Security.SecureString ReadLineAsSecureString() { throw new NotImplementedException("The method or operation is not implemented."); @@ -161,7 +164,7 @@ public override void WriteErrorLine(string value) /// public override void WriteLine() { - //do nothing + //do nothing because we don't need to log empty lines } /// @@ -203,7 +206,6 @@ public override void WriteProgress(long sourceId, ProgressRecord record) /// The verbose message that is displayed. public override void WriteVerboseLine(string message) { - //Console.WriteLine(String.Format(CultureInfo.CurrentCulture, "VERBOSE: {0}", message)); _logger.LogTrace(String.Format(CultureInfo.CurrentCulture, "VERBOSE: {0}", message)); } @@ -213,7 +215,6 @@ public override void WriteVerboseLine(string message) /// The warning message that is displayed. public override void WriteWarningLine(string message) { - //Console.WriteLine(String.Format(CultureInfo.CurrentCulture, "WARNING: {0}", message)); _logger.LogWarning(String.Format(CultureInfo.CurrentCulture, "WARNING: {0}", message)); } } diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs new file mode 100644 index 00000000..a1ffbcb4 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell +{ + using System.Management.Automation; + + public static class PowerShellWorkerExtensions + { + // This script handles when the user adds something to the pipeline. + // It logs the item that comes and stores it as the $return out binding. + // The last item stored as $return will be returned to the function host. + + private static string s_LogAndSetReturnValueScript = @" +param([Parameter(ValueFromPipeline=$true)]$return) + +$return | Out-Default + +Set-Variable -Name '$return' -Value $return -Scope global +"; + + public static PowerShell SetGlobalVariables(this PowerShell ps, Hashtable triggerMetadata, IList inputData) + { + try { + // Set the global $Context variable which contains trigger metadata + ps.AddCommand("Set-Variable").AddParameters( new Hashtable { + { "Name", "Context"}, + { "Scope", "Global"}, + { "Value", triggerMetadata} + }).Invoke(); + + // Sets a global variable for each input binding + foreach (ParameterBinding binding in inputData) + { + ps.AddCommand("Set-Variable").AddParameters( new Hashtable { + { "Name", binding.Name}, + { "Scope", "Global"}, + { "Value", binding.Data.ToObject()} + }).Invoke(); + } + return ps; + } + catch(Exception e) + { + ps.CleanupRunspace(); + throw e; + } + } + + public static PowerShell InvokeFunctionAndSetGlobalReturn(this PowerShell ps, string scriptPath, string entryPoint) + { + try + { + // We need to take into account if the user has an entry point. + // If it does, we invoke the command of that name + if(entryPoint != "") + { + ps.AddScript($@". {scriptPath}").Invoke(); + ps.AddScript($@". {entryPoint}"); + } + else + { + ps.AddScript($@". {scriptPath}"); + } + + // This script handles when the user adds something to the pipeline. + ps.AddScript(s_LogAndSetReturnValueScript).Invoke(); + return ps; + } + catch(Exception e) + { + ps.CleanupRunspace(); + throw e; + } + } + + public static Hashtable ReturnBindingHashtable(this PowerShell ps, IDictionary outBindings) + { + try + { + // This script returns a hashtable that contains the + // output bindings that we will return to the function host. + var result = ps.AddScript(BuildBindingHashtableScript(outBindings)).Invoke()[0]; + ps.Commands.Clear(); + return result; + } + catch(Exception e) + { + ps.CleanupRunspace(); + throw e; + } + } + + private static string BuildBindingHashtableScript(IDictionary outBindings) + { + // Since all of the out bindings are stored in variables at this point, + // we must construct a script that will return those output bindings in a hashtable + StringBuilder script = new StringBuilder(); + script.AppendLine("@{"); + foreach (KeyValuePair binding in outBindings) + { + script.Append("'"); + script.Append(binding.Key); + + // since $return has a dollar sign, we have to treat it differently + if (binding.Key == "$return") + { + script.Append("' = "); + } + else + { + script.Append("' = $"); + } + script.AppendLine(binding.Key); + } + script.AppendLine("}"); + + return script.ToString(); + } + + // TODO: make sure this completely cleans up the runspace + private static void CleanupRunspace(this PowerShell ps) + { + ps.Commands.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs index 5eb18f94..7c67b637 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs @@ -3,6 +3,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Requests { + using System; using System.Management.Automation; using Microsoft.Azure.Functions.PowerShellWorker.Utility; @@ -15,20 +16,33 @@ public static StreamingMessage Invoke( RpcLogger logger) { FunctionLoadRequest functionLoadRequest = request.FunctionLoadRequest; - functionLoader.Load(functionLoadRequest.FunctionId, functionLoadRequest.Metadata); - var response = new StreamingMessage() + + // Assume success unless something bad happens + StatusResult status = new StatusResult() + { + Status = StatusResult.Types.Status.Success + }; + + // Try to load the functions + try + { + functionLoader.Load(functionLoadRequest.FunctionId, functionLoadRequest.Metadata); + } + catch (Exception e) + { + status.Status = StatusResult.Types.Status.Failure; + status.Exception = e.ToRpcException(); + } + + return new StreamingMessage() { RequestId = request.RequestId, FunctionLoadResponse = new FunctionLoadResponse() { FunctionId = functionLoadRequest.FunctionId, - Result = new StatusResult() - { - Status = StatusResult.Types.Status.Success - } + Result = status } }; - return response; } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs index 02cdbfda..5c424b5b 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs @@ -4,6 +4,7 @@ using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Extensions.Logging; namespace Microsoft.Azure.Functions.PowerShellWorker.Requests @@ -20,8 +21,22 @@ public static StreamingMessage Invoke( RpcLogger logger) { InvocationRequest invocationRequest = request.InvocationRequest; + + // Set the RequestId and InvocationId for logging purposes logger.SetContext(request.RequestId, invocationRequest.InvocationId); + // Load information about the function + var functionInfo = functionLoader.GetInfo(invocationRequest.FunctionId); + (string scriptPath, string entryPoint) = functionLoader.GetFunc(invocationRequest.FunctionId); + + // Bundle all TriggerMetadata into Hashtable to send down to PowerShell + Hashtable triggerMetadata = new Hashtable(); + foreach (var dataItem in invocationRequest.TriggerMetadata) + { + triggerMetadata.Add(dataItem.Key, dataItem.Value.ToObject()); + } + + // Assume success unless something bad happens var status = new StatusResult() { Status = StatusResult.Types.Status.Success }; var response = new StreamingMessage() { @@ -33,113 +48,34 @@ public static StreamingMessage Invoke( } }; - var info = functionLoader.GetInfo(invocationRequest.FunctionId); - - // Add $Context variable, which contains trigger metadata, to the Global scope - Hashtable triggerMetadata = new Hashtable(); - foreach (var dataItem in invocationRequest.TriggerMetadata) - { - triggerMetadata.Add(dataItem.Key, TypeConverter.FromTypedData(dataItem.Value)); - } - - if (triggerMetadata.Count > 0) - { - powershell.AddCommand("Set-Variable").AddParameters( new Hashtable { - { "Name", "Context"}, - { "Scope", "Global"}, - { "Value", triggerMetadata} - }); - powershell.Invoke(); - } - - foreach (ParameterBinding binding in invocationRequest.InputData) - { - powershell.AddCommand("Set-Variable").AddParameters( new Hashtable { - { "Name", binding.Name}, - { "Scope", "Global"}, - { "Value", TypeConverter.FromTypedData(binding.Data)} - }); - powershell.Invoke(); - } - - // foreach (KeyValuePair binding in info.OutputBindings) - // { - // powershell.AddCommand("Set-Variable").AddParameters( new Hashtable { - // { "Name", binding.Key}, - // { "Scope", "Global"}, - // { "Value", null} - // }); - // powershell.Invoke(); - // } - - (string scriptPath, string entryPoint) = functionLoader.GetFunc(invocationRequest.FunctionId); - - if(entryPoint != "") - { - powershell.AddScript($@". {scriptPath}"); - powershell.Invoke(); - powershell.AddCommand(entryPoint); - } - else - { - powershell.AddCommand(scriptPath); - } - - powershell.AddScript(@" -param([Parameter(ValueFromPipeline=$true)]$return) - -$return | Out-Default - -Set-Variable -Name '$return' -Value $return -Scope global -"); - - StringBuilder script = new StringBuilder(); - script.AppendLine("@{"); - foreach (KeyValuePair binding in info.OutputBindings) - { - script.Append("'"); - script.Append(binding.Key); - - // since $return has a dollar sign, we have to treat it differently - if (binding.Key == "$return") - { - script.Append("' = "); - } - else - { - script.Append("' = $"); - } - script.AppendLine(binding.Key); - } - script.AppendLine("}"); - + // Invoke powershell logic and return hashtable of out binding data Hashtable result = null; try { - powershell.Invoke(); - powershell.AddScript(script.ToString()); - result = powershell.Invoke()[0]; + result = powershell + .SetGlobalVariables(triggerMetadata, invocationRequest.InputData) + .InvokeFunctionAndSetGlobalReturn(scriptPath, entryPoint) + .ReturnBindingHashtable(functionInfo.OutputBindings); } catch (Exception e) { status.Status = StatusResult.Types.Status.Failure; - status.Exception = TypeConverter.ToRpcException(e); - powershell.Commands.Clear(); + status.Exception = e.ToRpcException(); return response; } - powershell.Commands.Clear(); - foreach (KeyValuePair binding in info.OutputBindings) + // Set out binding data and return response to be sent back to host + foreach (KeyValuePair binding in functionInfo.OutputBindings) { ParameterBinding paramBinding = new ParameterBinding() { Name = binding.Key, - Data = TypeConverter.ToTypedData( - result[binding.Key]) + Data = result[binding.Key].ToTypedData() }; response.InvocationResponse.OutputData.Add(paramBinding); + // if one of the bindings is $return we need to also set the ReturnValue if(binding.Key == "$return") { response.InvocationResponse.ReturnValue = paramBinding.Data; diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs index b5d21cd0..70ddfc92 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs @@ -14,7 +14,7 @@ public static StreamingMessage Invoke( StreamingMessage request, RpcLogger logger) { - var response = new StreamingMessage() + return new StreamingMessage() { RequestId = request.RequestId, WorkerInitResponse = new WorkerInitResponse() @@ -25,7 +25,6 @@ public static StreamingMessage Invoke( } } }; - return response; } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs index c8f99766..91738ee2 100644 --- a/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs +++ b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs @@ -27,12 +27,6 @@ public static StartupArguments Parse(string[] args) } } - Console.WriteLine($"host: {arguments.Host}"); - Console.WriteLine($"port: {arguments.Port}"); - Console.WriteLine($"workerId: {arguments.WorkerId}"); - Console.WriteLine($"requestId: {arguments.RequestId}"); - Console.WriteLine($"grpcMaxMessageLength: {arguments.GrpcMaxMessageLength}"); - return arguments; } } diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs index 497ef5b6..c03941e6 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs @@ -42,7 +42,7 @@ public async void Log(LogLevel logLevel, EventId eventId, TState state, RequestId = _requestId, RpcLog = new RpcLog() { - Exception = exception == null ? null : TypeConverter.ToRpcException(exception), + Exception = exception == null ? null : exception.ToRpcException(), InvocationId = _invocationId, Level = ConvertLogLevel(logLevel), Message = formatter(state, exception) diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs similarity index 69% rename from src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs rename to src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs index a83da137..0a48cead 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/TypeConverter.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs @@ -7,24 +7,29 @@ using System; using Newtonsoft.Json; using System.Collections; +using System.Collections.Generic; namespace Microsoft.Azure.Functions.PowerShellWorker.Utility { - public class TypeConverter + public static class TypeExtensions { - public static object ToObject (TypedData data) + public static object ToObject (this TypedData data) { + if (data == null) + { + return null; + } + switch (data.DataCase) { case DataOneofCase.Json: - // consider doing ConvertFrom-Json - return data.Json; + return JsonConvert.DeserializeObject(data.Json); case DataOneofCase.Bytes: return data.Bytes; case DataOneofCase.Double: return data.Double; case DataOneofCase.Http: - return ToHttpContext(data.Http); + return data.Http.ToHttpContext(); case DataOneofCase.Int: return data.Int; case DataOneofCase.Stream: @@ -38,7 +43,7 @@ public static object ToObject (TypedData data) } } - public static TypedData ToTypedData(object value) + public static TypedData ToTypedData(this object value) { TypedData typedData = new TypedData(); @@ -55,7 +60,7 @@ public static TypedData ToTypedData(object value) else if(LanguagePrimitives.TryConvertTo( value, out HttpResponseContext http)) { - typedData.Http = ToRpcHttp(http); + typedData.Http = http.ToRpcHttp(); } else if (LanguagePrimitives.TryConvertTo( value, out Hashtable hashtable)) @@ -65,6 +70,8 @@ public static TypedData ToTypedData(object value) else if (LanguagePrimitives.TryConvertTo( value, out string str)) { + // Attempt to parse the string into json. If it fails, + // fallback to storing as a string try { typedData.Json = JsonConvert.SerializeObject(str); @@ -77,7 +84,7 @@ public static TypedData ToTypedData(object value) return typedData; } - public static HttpRequestContext ToHttpContext (RpcHttp rpcHttp) + public static HttpRequestContext ToHttpContext (this RpcHttp rpcHttp) { var httpRequestContext = new HttpRequestContext { @@ -91,35 +98,40 @@ public static HttpRequestContext ToHttpContext (RpcHttp rpcHttp) if (rpcHttp.Body != null) { - httpRequestContext.Body = ToObject(rpcHttp.Body); + httpRequestContext.Body = rpcHttp.Body.ToObject(); } if (rpcHttp.RawBody != null) { - httpRequestContext.Body = ToObject(rpcHttp.RawBody); + httpRequestContext.Body = rpcHttp.RawBody.ToObject(); } return httpRequestContext; } - public static RpcHttp ToRpcHttp (HttpResponseContext httpResponseContext) + public static RpcHttp ToRpcHttp (this HttpResponseContext httpResponseContext) { var rpcHttp = new RpcHttp { - StatusCode = httpResponseContext.StatusCode?? "200" + StatusCode = httpResponseContext.StatusCode }; if (httpResponseContext.Body != null) { - rpcHttp.Body = httpResponseContext.Body; + rpcHttp.Body = httpResponseContext.Body.ToTypedData(); } - rpcHttp.Headers.Add(httpResponseContext.Headers); + // Add all the headers. ContentType is separated for convenience + foreach (DictionaryEntry item in httpResponseContext.Headers) + { + rpcHttp.Headers.Add(item.Key.ToString(), item.Value.ToString()); + } + rpcHttp.Headers.Add("content-type", httpResponseContext.ContentType); return rpcHttp; } - public static RpcException ToRpcException (Exception exception) + public static RpcException ToRpcException (this Exception exception) { return new RpcException { diff --git a/src/Azure.Functions.PowerShell.Worker/Worker.cs b/src/Azure.Functions.PowerShell.Worker/Worker.cs index 6f5ea007..110451bd 100644 --- a/src/Azure.Functions.PowerShell.Worker/Worker.cs +++ b/src/Azure.Functions.PowerShell.Worker/Worker.cs @@ -9,7 +9,6 @@ using Azure.Functions.PowerShell.Worker.Messaging; using Microsoft.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; -using System.Collections; using Microsoft.Azure.Functions.PowerShellWorker.Requests; using Microsoft.Extensions.Logging; using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; @@ -33,10 +32,12 @@ public async static Task Main(string[] args) } StartupArguments startupArguments = StartupArguments.Parse(args); + // Initialize Rpc client, logger, and PowerShell s_client = new FunctionMessagingClient(startupArguments.Host, startupArguments.Port); s_Logger = new RpcLogger(s_client); InitPowerShell(); + // Send StartStream message var streamingMessage = new StreamingMessage() { RequestId = startupArguments.RequestId, StartStream = new StartStream() { WorkerId = startupArguments.WorkerId } @@ -49,28 +50,21 @@ public async static Task Main(string[] args) private static void InitPowerShell() { - // var events = new StreamEvents(s_Logger); - var host = new Host(s_Logger); + var host = new AzureFunctionsHost(s_Logger); s_runspace = RunspaceFactory.CreateRunspace(host); s_runspace.Open(); s_ps = System.Management.Automation.PowerShell.Create(InitialSessionState.CreateDefault()); s_ps.Runspace = s_runspace; - // Setup Stream event listeners - // s_ps.Streams.Debug.DataAdded += events.DebugDataAdded; - // s_ps.Streams.Error.DataAdded += events.ErrorDataAdded; - // s_ps.Streams.Information.DataAdded += events.InformationDataAdded; - // s_ps.Streams.Progress.DataAdded += events.ProgressDataAdded; - // s_ps.Streams.Verbose.DataAdded += events.VerboseDataAdded; - // s_ps.Streams.Warning.DataAdded += events.WarningDataAdded; - s_ps.AddScript("$PSHOME"); //s_ps.AddCommand("Set-ExecutionPolicy").AddParameter("ExecutionPolicy", ExecutionPolicy.Unrestricted).AddParameter("Scope", ExecutionPolicyScope.Process); - var result = s_ps.Invoke(); - s_ps.Commands.Clear(); + s_ps.Invoke(); - Console.WriteLine(result[0]); + // Add HttpResponseContext namespace so users can reference + // HttpResponseContext without needing to specify the full namespace + s_ps.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}").Invoke(); + s_ps.Commands.Clear(); } private static async Task ProcessEvent() From 93fdbd9062a509635eb129480ceba3361733d95a Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Fri, 24 Aug 2018 10:10:29 -0700 Subject: [PATCH 5/8] organize usings and add headers --- .../FunctionMessagingClient.cs | 6 ++++ .../Function/FunctionInfo.cs | 6 +++- .../Function/FunctionLoader.cs | 5 ++++ .../Http/HttpRequestContext.cs | 6 +++- .../Http/HttpResponseContext.cs | 7 +++-- .../PowerShell/Host/AzureFunctionsHost.cs | 6 ++++ .../PowerShell/Host/HostUserInterface.cs | 6 ++++ .../PowerShell/Host/RawUserInterface.cs | 5 ++++ .../PowerShell/PowerShellWorkerExtensions.cs | 6 ++++ .../Requests/HandleFunctionLoadRequest.cs | 11 +++++-- .../Requests/HandleInvocationRequest.cs | 11 ++++--- .../Requests/HandleWorkerInitRequest.cs | 6 +++- .../StartupArguments.cs | 5 ++++ .../Utility/RpcLogger.cs | 23 ++++++++------ .../Utility/TypeExtensions.cs | 30 ++++++++++--------- .../Worker.cs | 19 ++++++------ 16 files changed, 113 insertions(+), 45 deletions(-) diff --git a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs index 3aeb5e07..7b183241 100644 --- a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs +++ b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs @@ -1,6 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Threading; using System.Threading.Tasks; + using Grpc.Core; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs index 617f088f..16d98db1 100644 --- a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs @@ -1,6 +1,10 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using Google.Protobuf.Collections; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; -using Microsoft.Azure.Functions.PowerShellWorker.Utility; namespace Microsoft.Azure.Functions.PowerShellWorker { diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs index c8bfb0ba..4f13d6ff 100644 --- a/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs @@ -1,3 +1,8 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using Google.Protobuf.Collections; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs index aef21832..84e33d24 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs @@ -1,5 +1,9 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using Google.Protobuf.Collections; -using Microsoft.Azure.WebJobs.Script.Grpc.Messages; namespace Microsoft.Azure.Functions.PowerShellWorker { diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs index 5eef4a3e..993d0044 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs @@ -1,6 +1,9 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System.Collections; -using Google.Protobuf.Collections; -using Microsoft.Azure.WebJobs.Script.Grpc.Messages; namespace Microsoft.Azure.Functions.PowerShellWorker { diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs index d09eaacb..f19c5798 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs @@ -1,6 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Globalization; using System.Management.Automation.Host; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs index b0173481..038505b0 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs @@ -1,8 +1,14 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections.Generic; using System.Globalization; using System.Management.Automation; using System.Management.Automation.Host; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Extensions.Logging; diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs index 1bbebb37..fef2fb26 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs @@ -1,3 +1,8 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Management.Automation.Host; diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs index a1ffbcb4..77ad1073 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs @@ -1,7 +1,13 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections; using System.Collections.Generic; using System.Text; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs index 7c67b637..83ce8b0a 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs @@ -1,11 +1,16 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; -using Microsoft.Extensions.Logging; namespace Microsoft.Azure.Functions.PowerShellWorker.Requests { - using System; using System.Management.Automation; - using Microsoft.Azure.Functions.PowerShellWorker.Utility; public class HandleFunctionLoadRequest { diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs index 5c424b5b..4b3f00f6 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs @@ -1,16 +1,19 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections; using System.Collections.Generic; -using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; -using Microsoft.Extensions.Logging; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; -namespace Microsoft.Azure.Functions.PowerShellWorker.Requests +namespace Microsoft.Azure.Functions.PowerShellWorker.Requests { using System.Management.Automation; - using System.Text; public class HandleInvocationRequest { diff --git a/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs b/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs index 70ddfc92..d9cd8a28 100644 --- a/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs @@ -1,6 +1,10 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; -using Microsoft.Extensions.Logging; namespace Microsoft.Azure.Functions.PowerShellWorker.Requests { diff --git a/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs index 91738ee2..fd793f47 100644 --- a/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs +++ b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs @@ -1,3 +1,8 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; namespace Microsoft.Azure.Functions.PowerShellWorker diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs index c03941e6..bef888fc 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs @@ -1,8 +1,13 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; + using Azure.Functions.PowerShell.Worker.Messaging; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using Microsoft.Extensions.Logging; -using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types; namespace Microsoft.Azure.Functions.PowerShellWorker.Utility { @@ -53,24 +58,24 @@ public async void Log(LogLevel logLevel, EventId eventId, TState state, } } - public static Level ConvertLogLevel(LogLevel logLevel) + public static RpcLog.Types.Level ConvertLogLevel(LogLevel logLevel) { switch (logLevel) { case LogLevel.Critical: - return Level.Critical; + return RpcLog.Types.Level.Critical; case LogLevel.Debug: - return Level.Debug; + return RpcLog.Types.Level.Debug; case LogLevel.Error: - return Level.Error; + return RpcLog.Types.Level.Error; case LogLevel.Information: - return Level.Information; + return RpcLog.Types.Level.Information; case LogLevel.Trace: - return Level.Trace; + return RpcLog.Types.Level.Trace; case LogLevel.Warning: - return Level.Warning; + return RpcLog.Types.Level.Warning; default: - return Level.None; + return RpcLog.Types.Level.None; } } } diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs index 0a48cead..22044a04 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs @@ -1,13 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; using System.Management.Automation; + using Google.Protobuf; -using Microsoft.Azure.Functions.PowerShellWorker; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; -using System.Net.Http; -using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.TypedData; -using System; using Newtonsoft.Json; -using System.Collections; -using System.Collections.Generic; namespace Microsoft.Azure.Functions.PowerShellWorker.Utility { @@ -22,21 +24,21 @@ public static object ToObject (this TypedData data) switch (data.DataCase) { - case DataOneofCase.Json: + case TypedData.DataOneofCase.Json: return JsonConvert.DeserializeObject(data.Json); - case DataOneofCase.Bytes: + case TypedData.DataOneofCase.Bytes: return data.Bytes; - case DataOneofCase.Double: + case TypedData.DataOneofCase.Double: return data.Double; - case DataOneofCase.Http: + case TypedData.DataOneofCase.Http: return data.Http.ToHttpContext(); - case DataOneofCase.Int: + case TypedData.DataOneofCase.Int: return data.Int; - case DataOneofCase.Stream: + case TypedData.DataOneofCase.Stream: return data.Stream; - case DataOneofCase.String: + case TypedData.DataOneofCase.String: return data.String; - case DataOneofCase.None: + case TypedData.DataOneofCase.None: return null; default: return new InvalidOperationException("Data Case was not set."); diff --git a/src/Azure.Functions.PowerShell.Worker/Worker.cs b/src/Azure.Functions.PowerShell.Worker/Worker.cs index 110451bd..22aa0c33 100644 --- a/src/Azure.Functions.PowerShell.Worker/Worker.cs +++ b/src/Azure.Functions.PowerShell.Worker/Worker.cs @@ -1,18 +1,17 @@ -using System; -using System.Text; -using System.Threading; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Threading.Tasks; -using System.Management.Automation; using System.Management.Automation.Runspaces; -using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using Azure.Functions.PowerShell.Worker.Messaging; -using Microsoft.PowerShell; -using Microsoft.Azure.Functions.PowerShellWorker.Utility; -using Microsoft.Azure.Functions.PowerShellWorker.Requests; -using Microsoft.Extensions.Logging; -using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host; +using Microsoft.Azure.Functions.PowerShellWorker.Requests; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; namespace Microsoft.Azure.Functions.PowerShellWorker { From 615ebcdd47ddcbfe5b9d521deda6a5a7a184f855 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Fri, 24 Aug 2018 10:35:24 -0700 Subject: [PATCH 6/8] misc syntax sugar --- .../FunctionMessagingClient.cs | 22 ++---- .../Function/FunctionLoader.cs | 2 +- .../PowerShell/Host/AzureFunctionsHost.cs | 24 +++--- .../PowerShell/Host/HostUserInterface.cs | 75 +++++-------------- .../PowerShell/Host/RawUserInterface.cs | 2 +- .../PowerShell/PowerShellWorkerExtensions.cs | 6 +- .../Requests/HandleFunctionLoadRequest.cs | 2 +- .../Requests/HandleInvocationRequest.cs | 2 +- .../Requests/HandleWorkerInitRequest.cs | 2 +- .../Utility/RpcLogger.cs | 16 ++-- .../Worker.cs | 17 +++-- 11 files changed, 59 insertions(+), 111 deletions(-) diff --git a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs index 7b183241..b57b199e 100644 --- a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs +++ b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs @@ -14,9 +14,9 @@ namespace Azure.Functions.PowerShell.Worker.Messaging { public class FunctionMessagingClient : IDisposable { - public bool isDisposed = false; - private AsyncDuplexStreamingCall _call; - private SemaphoreSlim _writeStreamHandle = new SemaphoreSlim(1, 1); + public bool isDisposed; + AsyncDuplexStreamingCall _call; + SemaphoreSlim _writeStreamHandle = new SemaphoreSlim(1, 1); public FunctionMessagingClient(string host, int port) { @@ -41,19 +41,11 @@ public async Task WriteAsync(StreamingMessage message) } } - public async Task MoveNext() - { - if(isDisposed) return false; - - return await _call.ResponseStream.MoveNext(CancellationToken.None); - } + public async Task MoveNext() => + !isDisposed && await _call.ResponseStream.MoveNext(CancellationToken.None); - public StreamingMessage GetCurrentMessage() - { - if(isDisposed) return null; - - return _call.ResponseStream.Current; - } + public StreamingMessage GetCurrentMessage() => + isDisposed ? null : _call.ResponseStream.Current; public void Dispose() { diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs index 4f13d6ff..044cb342 100644 --- a/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs @@ -10,7 +10,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { public class FunctionLoader { - private readonly MapField _LoadedFunctions = new MapField(); + readonly MapField _LoadedFunctions = new MapField(); public void Load(string functionId, RpcFunctionMetadata metadata) { // TODO: catch "load" issues at "func start" time. diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs index f19c5798..18e37515 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs @@ -16,35 +16,35 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// applications. Not all members are implemented. Those that aren't throw a /// NotImplementedException. /// - internal class AzureFunctionsHost : PSHost + class AzureFunctionsHost : PSHost { /// /// The private reference of the logger. /// - private RpcLogger _logger; + RpcLogger _logger; /// /// Creates an instance of the PSHostUserInterface object for this /// application. /// - private HostUserInterface HostUI; + HostUserInterface HostUI; /// /// The culture info of the thread that created /// this object. /// - private CultureInfo originalCultureInfo = System.Threading.Thread.CurrentThread.CurrentCulture; + CultureInfo originalCultureInfo = System.Threading.Thread.CurrentThread.CurrentCulture; /// /// The UI culture info of the thread that created /// this object. /// - private CultureInfo originalUICultureInfo = System.Threading.Thread.CurrentThread.CurrentUICulture; + CultureInfo originalUICultureInfo = System.Threading.Thread.CurrentThread.CurrentUICulture; /// /// The identifier of the PSHost implementation. /// - private Guid Id = Guid.NewGuid(); + Guid Id = Guid.NewGuid(); /// /// Initializes a new instance of the Host class. Keep @@ -99,17 +99,13 @@ public AzureFunctionsHost(RpcLogger logger) /// Not implemented by this class. The call fails with an exception. /// public override void EnterNestedPrompt() - { - throw new NotImplementedException("The method or operation is not implemented."); - } + => throw new NotImplementedException("The method or operation is not implemented."); /// /// Not implemented by this class. The call fails with an exception. /// public override void ExitNestedPrompt() - { - throw new NotImplementedException("The method or operation is not implemented."); - } + => throw new NotImplementedException("The method or operation is not implemented."); /// /// This API is called before an external application process is started. Typically @@ -139,9 +135,7 @@ public override void NotifyEndApplication() /// /// The exit code that the host application should use. public override void SetShouldExit(int exitCode) - { - throw new NotImplementedException("The method or operation is not implemented."); - } + => throw new NotImplementedException("The method or operation is not implemented."); } } diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs index 038505b0..cf7ccf4c 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs @@ -19,17 +19,17 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// applications. Few members are actually implemented. Those that aren't throw a /// NotImplementedException. /// - internal class HostUserInterface : PSHostUserInterface + class HostUserInterface : PSHostUserInterface { /// /// The private reference of the logger. /// - private RpcLogger _logger; + RpcLogger _logger; /// /// An instance of the PSRawUserInterface object. /// - private RawUserInterface RawUi = new RawUserInterface(); + RawUserInterface RawUi = new RawUserInterface(); /// /// Gets an instance of the PSRawUserInterface object for this host @@ -50,10 +50,8 @@ public HostUserInterface(RpcLogger logger) /// A collection of FieldDescription objects that /// describe each field of the prompt. /// Throws a NotImplementedException exception because we don't need a prompt. - public override Dictionary Prompt(string caption, string message, System.Collections.ObjectModel.Collection descriptions) - { + public override Dictionary Prompt(string caption, string message, System.Collections.ObjectModel.Collection descriptions) => throw new NotImplementedException("The method or operation is not implemented."); - } /// /// Provides a set of choices that enable the user to choose a single option from a set of options. @@ -65,10 +63,8 @@ public override Dictionary Prompt(string caption, string messa /// The index of the label in the Choices parameter /// collection. To indicate no default choice, set to -1. /// Throws a NotImplementedException exception because we don't need a prompt. - public override int PromptForChoice(string caption, string message, System.Collections.ObjectModel.Collection choices, int defaultChoice) - { + public override int PromptForChoice(string caption, string message, System.Collections.ObjectModel.Collection choices, int defaultChoice) => throw new NotImplementedException("The method or operation is not implemented."); - } /// /// Prompts the user for credentials with a specified prompt window caption, @@ -79,10 +75,8 @@ public override int PromptForChoice(string caption, string message, System.Colle /// The user name whose credential is to be prompted for. /// The name of the target for which the credential is collected. /// Throws a NotImplementedException exception because we don't need a prompt. - public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) - { + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) => throw new NotImplementedException("The method or operation is not implemented."); - } /// /// Prompts the user for credentials by using a specified prompt window caption, @@ -98,40 +92,31 @@ public override PSCredential PromptForCredential(string caption, string message, /// A PSCredentialUIOptions constant that identifies the UI /// behavior when it gathers the credentials. /// Throws a NotImplementedException exception because we don't need a prompt. - public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) - { + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) => throw new NotImplementedException("The method or operation is not implemented."); - } /// /// Reads characters that are entered by the user until a newline /// (carriage return) is encountered. /// /// Throws a NotImplemented exception because we are in a non-interactive experience. - public override string ReadLine() - { + public override string ReadLine() => throw new NotImplementedException("The method or operation is not implemented."); - } /// /// Reads characters entered by the user until a newline (carriage return) /// is encountered and returns the characters as a secure string. /// /// Throws a NotImplemented exception because we are in a non-interactive experience. - public override System.Security.SecureString ReadLineAsSecureString() - { + public override System.Security.SecureString ReadLineAsSecureString() => throw new NotImplementedException("The method or operation is not implemented."); - } /// /// Writes a new line character (carriage return) to the output display /// of the host. /// /// The characters to be written. - public override void Write(string value) - { - _logger.LogInformation(value); - } + public override void Write(string value) => _logger.LogInformation(value); /// /// Writes characters to the output display of the host with possible @@ -140,48 +125,37 @@ public override void Write(string value) /// The color of the characters. /// The backgound color to use. /// The characters to be written. - public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) - { - // Just ignore the colors. + public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) => _logger.LogInformation(value); - } /// /// Writes a debug message to the output display of the host. /// /// The debug message that is displayed. - public override void WriteDebugLine(string message) - { + public override void WriteDebugLine(string message) => _logger.LogDebug(String.Format(CultureInfo.CurrentCulture, "DEBUG: {0}", message)); - } /// /// Writes an error message to the output display of the host. /// /// The error message that is displayed. - public override void WriteErrorLine(string value) - { + public override void WriteErrorLine(string value) => _logger.LogError(String.Format(CultureInfo.CurrentCulture, "ERROR: {0}", value)); - } /// /// Writes a newline character (carriage return) /// to the output display of the host. /// - public override void WriteLine() - { - //do nothing because we don't need to log empty lines - } + public override void WriteLine() {} //do nothing because we don't need to log empty lines /// /// Writes a line of characters to the output display of the host /// and appends a newline character(carriage return). /// /// The line to be written. - public override void WriteLine(string value) - { + public override void WriteLine(string value) => _logger.LogInformation(value); - } + /// /// Writes a line of characters to the output display of the host @@ -190,39 +164,30 @@ public override void WriteLine(string value) /// The forground color of the display. /// The background color of the display. /// The line to be written. - public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) - { - // Write to the log, ignore the colors + public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) => _logger.LogInformation(value); - } /// /// Writes a progress report to the output display of the host. /// /// Unique identifier of the source of the record. /// A ProgressReport object. - public override void WriteProgress(long sourceId, ProgressRecord record) - { + public override void WriteProgress(long sourceId, ProgressRecord record) => _logger.LogTrace(String.Format(CultureInfo.CurrentCulture, "PROGRESS: {0}", record.StatusDescription)); - } /// /// Writes a verbose message to the output display of the host. /// /// The verbose message that is displayed. - public override void WriteVerboseLine(string message) - { + public override void WriteVerboseLine(string message) => _logger.LogTrace(String.Format(CultureInfo.CurrentCulture, "VERBOSE: {0}", message)); - } /// /// Writes a warning message to the output display of the host. /// /// The warning message that is displayed. - public override void WriteWarningLine(string message) - { + public override void WriteWarningLine(string message) => _logger.LogWarning(String.Format(CultureInfo.CurrentCulture, "WARNING: {0}", message)); - } } } diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs index fef2fb26..4b5270a4 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs @@ -14,7 +14,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// class are implemented. More complex methods are not implemented and will /// throw a NotImplementedException. /// - internal class RawUserInterface : PSHostRawUserInterface + class RawUserInterface : PSHostRawUserInterface { /// /// Gets or sets the background color of text to be written. diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs index 77ad1073..bb7c0acf 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs @@ -21,7 +21,7 @@ public static class PowerShellWorkerExtensions // It logs the item that comes and stores it as the $return out binding. // The last item stored as $return will be returned to the function host. - private static string s_LogAndSetReturnValueScript = @" + static string s_LogAndSetReturnValueScript = @" param([Parameter(ValueFromPipeline=$true)]$return) $return | Out-Default @@ -101,7 +101,7 @@ public static Hashtable ReturnBindingHashtable(this PowerShell ps, IDictionary outBindings) + static string BuildBindingHashtableScript(IDictionary outBindings) { // Since all of the out bindings are stored in variables at this point, // we must construct a script that will return those output bindings in a hashtable @@ -129,7 +129,7 @@ private static string BuildBindingHashtableScript(IDictionary(TState state) - { + public IDisposable BeginScope(TState state) => throw new NotImplementedException(); - } - public bool IsEnabled(LogLevel logLevel) - { + public bool IsEnabled(LogLevel logLevel) => throw new NotImplementedException(); - } public async void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { @@ -47,7 +43,7 @@ public async void Log(LogLevel logLevel, EventId eventId, TState state, RequestId = _requestId, RpcLog = new RpcLog() { - Exception = exception == null ? null : exception.ToRpcException(), + Exception = exception?.ToRpcException(), InvocationId = _invocationId, Level = ConvertLogLevel(logLevel), Message = formatter(state, exception) diff --git a/src/Azure.Functions.PowerShell.Worker/Worker.cs b/src/Azure.Functions.PowerShell.Worker/Worker.cs index 22aa0c33..5169e197 100644 --- a/src/Azure.Functions.PowerShell.Worker/Worker.cs +++ b/src/Azure.Functions.PowerShell.Worker/Worker.cs @@ -15,13 +15,14 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { - public class Worker + public static class Worker { - private static FunctionMessagingClient s_client; - private static System.Management.Automation.PowerShell s_ps; - private static Runspace s_runspace; - private static FunctionLoader s_FunctionLoader = new FunctionLoader(); - private static RpcLogger s_Logger; + static FunctionMessagingClient s_client; + static System.Management.Automation.PowerShell s_ps; + static Runspace s_runspace; + static readonly FunctionLoader s_FunctionLoader = new FunctionLoader(); + static RpcLogger s_Logger; + public async static Task Main(string[] args) { if (args.Length != 10) @@ -47,7 +48,7 @@ public async static Task Main(string[] args) await ProcessEvent(); } - private static void InitPowerShell() + static void InitPowerShell() { var host = new AzureFunctionsHost(s_Logger); @@ -66,7 +67,7 @@ private static void InitPowerShell() s_ps.Commands.Clear(); } - private static async Task ProcessEvent() + static async Task ProcessEvent() { using (s_client) { From ada91c6fcbefd2433f9fc831857ec1828e177fe4 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Fri, 24 Aug 2018 11:07:34 -0700 Subject: [PATCH 7/8] alpha sort all code --- .../FunctionMessagingClient.cs | 34 +++--- .../Function/FunctionInfo.cs | 6 +- .../Function/FunctionLoader.cs | 12 +- .../Http/HttpRequestContext.cs | 6 +- .../Http/HttpResponseContext.cs | 4 +- .../PowerShell/Host/AzureFunctionsHost.cs | 43 +++---- .../PowerShell/Host/HostUserInterface.cs | 4 +- .../PowerShell/PowerShellWorkerExtensions.cs | 96 +++++++-------- .../Utility/RpcLogger.cs | 56 ++++----- .../Utility/TypeExtensions.cs | 114 +++++++++--------- .../Worker.cs | 56 ++++----- 11 files changed, 210 insertions(+), 221 deletions(-) diff --git a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs index b57b199e..e1257581 100644 --- a/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs +++ b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs @@ -14,9 +14,9 @@ namespace Azure.Functions.PowerShell.Worker.Messaging { public class FunctionMessagingClient : IDisposable { - public bool isDisposed; - AsyncDuplexStreamingCall _call; SemaphoreSlim _writeStreamHandle = new SemaphoreSlim(1, 1); + AsyncDuplexStreamingCall _call; + public bool isDisposed; public FunctionMessagingClient(string host, int port) { @@ -24,6 +24,21 @@ public FunctionMessagingClient(string host, int port) _call = new FunctionRpc.FunctionRpcClient(channel).EventStream(); } + public void Dispose() + { + if (!isDisposed) + { + isDisposed = true; + _call.Dispose(); + } + } + + public StreamingMessage GetCurrentMessage() => + isDisposed ? null : _call.ResponseStream.Current; + + public async Task MoveNext() => + !isDisposed && await _call.ResponseStream.MoveNext(CancellationToken.None); + public async Task WriteAsync(StreamingMessage message) { if(isDisposed) return; @@ -40,20 +55,5 @@ public async Task WriteAsync(StreamingMessage message) _writeStreamHandle.Release(); } } - - public async Task MoveNext() => - !isDisposed && await _call.ResponseStream.MoveNext(CancellationToken.None); - - public StreamingMessage GetCurrentMessage() => - isDisposed ? null : _call.ResponseStream.Current; - - public void Dispose() - { - if (!isDisposed) - { - isDisposed = true; - _call.Dispose(); - } - } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs index 16d98db1..977bf259 100644 --- a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs @@ -10,11 +10,11 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { public class FunctionInfo { - public string Name {get; private set;} - public string Directory {get; private set;} public MapField Bindings {get; private set;} - public MapField OutputBindings {get; private set;} + public string Directory {get; private set;} public string HttpOutputName {get; private set;} + public string Name {get; private set;} + public MapField OutputBindings {get; private set;} public FunctionInfo(RpcFunctionMetadata metadata) { diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs index 044cb342..f45bbf50 100644 --- a/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs @@ -11,6 +11,12 @@ namespace Microsoft.Azure.Functions.PowerShellWorker public class FunctionLoader { readonly MapField _LoadedFunctions = new MapField(); + + public (string ScriptPath, string EntryPoint) GetFunc(string functionId) => + (_LoadedFunctions[functionId].ScriptPath, _LoadedFunctions[functionId].EntryPoint); + + public FunctionInfo GetInfo(string functionId) => _LoadedFunctions[functionId].Info; + public void Load(string functionId, RpcFunctionMetadata metadata) { // TODO: catch "load" issues at "func start" time. @@ -22,16 +28,12 @@ public void Load(string functionId, RpcFunctionMetadata metadata) EntryPoint = metadata.EntryPoint }); } - - public FunctionInfo GetInfo(string functionId) => _LoadedFunctions[functionId].Info; - public (string ScriptPath, string EntryPoint) GetFunc(string functionId) => - (_LoadedFunctions[functionId].ScriptPath, _LoadedFunctions[functionId].EntryPoint); } public class Function { + public string EntryPoint {get; internal set;} public FunctionInfo Info {get; internal set;} public string ScriptPath {get; internal set;} - public string EntryPoint {get; internal set;} } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs index 84e33d24..f88e54e8 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs @@ -9,13 +9,13 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { public class HttpRequestContext { + public object Body {get; set;} + public MapField Headers {get; set;} public string Method {get; set;} public string Url {get; set;} public string OriginalUrl {get; set;} - public MapField Headers {get; set;} - public MapField Query {get; set;} public MapField Params {get; set;} - public object Body {get; set;} + public MapField Query {get; set;} public object RawBody {get; set;} } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs index 993d0044..2b722861 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs @@ -9,10 +9,10 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { public class HttpResponseContext { - public string StatusCode {get; set;} = "200"; - public Hashtable Headers {get; set;} = new Hashtable(); public object Body {get; set;} public string ContentType {get; set;} = "text/plain"; public bool EnableContentNegotiation {get; set;} = false; + public Hashtable Headers {get; set;} = new Hashtable(); + public string StatusCode {get; set;} = "200"; } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs index 18e37515..957c1e06 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs @@ -21,49 +21,42 @@ class AzureFunctionsHost : PSHost /// /// The private reference of the logger. /// - RpcLogger _logger; + RpcLogger _logger { get; set; } /// /// Creates an instance of the PSHostUserInterface object for this /// application. /// - HostUserInterface HostUI; + HostUserInterface HostUI { get; set; } /// /// The culture info of the thread that created /// this object. /// - CultureInfo originalCultureInfo = System.Threading.Thread.CurrentThread.CurrentCulture; + readonly CultureInfo originalCultureInfo = System.Threading.Thread.CurrentThread.CurrentCulture; /// /// The UI culture info of the thread that created /// this object. /// - CultureInfo originalUICultureInfo = System.Threading.Thread.CurrentThread.CurrentUICulture; + readonly CultureInfo originalUICultureInfo = System.Threading.Thread.CurrentThread.CurrentUICulture; /// /// The identifier of the PSHost implementation. /// Guid Id = Guid.NewGuid(); - /// - /// Initializes a new instance of the Host class. Keep - /// a reference to the hosting application object so it can - /// be informed of when to exit. - /// - /// A reference to the host application object. - /// /// Gets the culture info to use - this implementation just snapshots the /// curture info of the thread that created this object. /// - public override System.Globalization.CultureInfo CurrentCulture => originalCultureInfo; - + public override CultureInfo CurrentCulture => originalCultureInfo; + /// /// Gets the UI culture info to use - this implementation just snapshots the /// UI curture info of the thread that created this object. /// - public override System.Globalization.CultureInfo CurrentUICulture => originalUICultureInfo; + public override CultureInfo CurrentUICulture => originalUICultureInfo; /// /// Gets an identifier for this host. This implementation always returns @@ -98,14 +91,14 @@ public AzureFunctionsHost(RpcLogger logger) /// /// Not implemented by this class. The call fails with an exception. /// - public override void EnterNestedPrompt() - => throw new NotImplementedException("The method or operation is not implemented."); + public override void EnterNestedPrompt() => + throw new NotImplementedException("The method or operation is not implemented."); /// /// Not implemented by this class. The call fails with an exception. /// - public override void ExitNestedPrompt() - => throw new NotImplementedException("The method or operation is not implemented."); + public override void ExitNestedPrompt() => + throw new NotImplementedException("The method or operation is not implemented."); /// /// This API is called before an external application process is started. Typically @@ -113,20 +106,14 @@ public override void ExitNestedPrompt() /// restore that state when the child exits. In this, we don't need this so /// the method simple returns. /// - public override void NotifyBeginApplication() - { - return; // Do nothing. - } + public override void NotifyBeginApplication() { return; } // Do nothing. /// /// This API is called after an external application process finishes. Typically /// it's used to restore state that the child process may have altered. In this, /// we don't need this so the method simple returns. /// - public override void NotifyEndApplication() - { - return; // Do nothing. - } + public override void NotifyEndApplication() { return; } // Do nothing. /// /// Indicate to the host application that exit has @@ -134,8 +121,8 @@ public override void NotifyEndApplication() /// application should use when exiting the process. /// /// The exit code that the host application should use. - public override void SetShouldExit(int exitCode) - => throw new NotImplementedException("The method or operation is not implemented."); + public override void SetShouldExit(int exitCode) => + throw new NotImplementedException("The method or operation is not implemented."); } } diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs index cf7ccf4c..8c15472d 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs @@ -24,12 +24,12 @@ class HostUserInterface : PSHostUserInterface /// /// The private reference of the logger. /// - RpcLogger _logger; + RpcLogger _logger { get; set; } /// /// An instance of the PSRawUserInterface object. /// - RawUserInterface RawUi = new RawUserInterface(); + readonly RawUserInterface RawUi = new RawUserInterface(); /// /// Gets an instance of the PSRawUserInterface object for this host diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs index bb7c0acf..294f8f78 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs @@ -21,7 +21,7 @@ public static class PowerShellWorkerExtensions // It logs the item that comes and stores it as the $return out binding. // The last item stored as $return will be returned to the function host. - static string s_LogAndSetReturnValueScript = @" + readonly static string s_LogAndSetReturnValueScript = @" param([Parameter(ValueFromPipeline=$true)]$return) $return | Out-Default @@ -29,32 +29,37 @@ public static class PowerShellWorkerExtensions Set-Variable -Name '$return' -Value $return -Scope global "; - public static PowerShell SetGlobalVariables(this PowerShell ps, Hashtable triggerMetadata, IList inputData) + static string BuildBindingHashtableScript(IDictionary outBindings) { - try { - // Set the global $Context variable which contains trigger metadata - ps.AddCommand("Set-Variable").AddParameters( new Hashtable { - { "Name", "Context"}, - { "Scope", "Global"}, - { "Value", triggerMetadata} - }).Invoke(); + // Since all of the out bindings are stored in variables at this point, + // we must construct a script that will return those output bindings in a hashtable + StringBuilder script = new StringBuilder(); + script.AppendLine("@{"); + foreach (KeyValuePair binding in outBindings) + { + script.Append("'"); + script.Append(binding.Key); - // Sets a global variable for each input binding - foreach (ParameterBinding binding in inputData) + // since $return has a dollar sign, we have to treat it differently + if (binding.Key == "$return") { - ps.AddCommand("Set-Variable").AddParameters( new Hashtable { - { "Name", binding.Name}, - { "Scope", "Global"}, - { "Value", binding.Data.ToObject()} - }).Invoke(); + script.Append("' = "); } - return ps; - } - catch(Exception e) - { - ps.CleanupRunspace(); - throw e; + else + { + script.Append("' = $"); + } + script.AppendLine(binding.Key); } + script.AppendLine("}"); + + return script.ToString(); + } + + // TODO: make sure this completely cleans up the runspace + static void CleanupRunspace(this PowerShell ps) + { + ps.Commands.Clear(); } public static PowerShell InvokeFunctionAndSetGlobalReturn(this PowerShell ps, string scriptPath, string entryPoint) @@ -101,37 +106,32 @@ public static Hashtable ReturnBindingHashtable(this PowerShell ps, IDictionary outBindings) + public static PowerShell SetGlobalVariables(this PowerShell ps, Hashtable triggerMetadata, IList inputData) { - // Since all of the out bindings are stored in variables at this point, - // we must construct a script that will return those output bindings in a hashtable - StringBuilder script = new StringBuilder(); - script.AppendLine("@{"); - foreach (KeyValuePair binding in outBindings) - { - script.Append("'"); - script.Append(binding.Key); + try { + // Set the global $Context variable which contains trigger metadata + ps.AddCommand("Set-Variable").AddParameters( new Hashtable { + { "Name", "Context"}, + { "Scope", "Global"}, + { "Value", triggerMetadata} + }).Invoke(); - // since $return has a dollar sign, we have to treat it differently - if (binding.Key == "$return") - { - script.Append("' = "); - } - else + // Sets a global variable for each input binding + foreach (ParameterBinding binding in inputData) { - script.Append("' = $"); + ps.AddCommand("Set-Variable").AddParameters( new Hashtable { + { "Name", binding.Name}, + { "Scope", "Global"}, + { "Value", binding.Data.ToObject()} + }).Invoke(); } - script.AppendLine(binding.Key); + return ps; + } + catch(Exception e) + { + ps.CleanupRunspace(); + throw e; } - script.AppendLine("}"); - - return script.ToString(); - } - - // TODO: make sure this completely cleans up the runspace - static void CleanupRunspace(this PowerShell ps) - { - ps.Commands.Clear(); } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs index d0087597..5243a8cc 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs @@ -13,30 +13,45 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility { public class RpcLogger : ILogger { - FunctionMessagingClient _Client; + FunctionMessagingClient _client; string _invocationId = ""; string _requestId = ""; public RpcLogger(FunctionMessagingClient client) { - _Client = client; - } - - public void SetContext(string requestId, string invocationId) - { - _requestId = requestId; - _invocationId = invocationId; + _client = client; } public IDisposable BeginScope(TState state) => throw new NotImplementedException(); + public static RpcLog.Types.Level ConvertLogLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Critical: + return RpcLog.Types.Level.Critical; + case LogLevel.Debug: + return RpcLog.Types.Level.Debug; + case LogLevel.Error: + return RpcLog.Types.Level.Error; + case LogLevel.Information: + return RpcLog.Types.Level.Information; + case LogLevel.Trace: + return RpcLog.Types.Level.Trace; + case LogLevel.Warning: + return RpcLog.Types.Level.Warning; + default: + return RpcLog.Types.Level.None; + } + } + public bool IsEnabled(LogLevel logLevel) => throw new NotImplementedException(); public async void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - if (_Client != null) + if (_client != null) { var logMessage = new StreamingMessage { @@ -50,29 +65,14 @@ public async void Log(LogLevel logLevel, EventId eventId, TState state, } }; - await _Client.WriteAsync(logMessage); + await _client.WriteAsync(logMessage); } } - public static RpcLog.Types.Level ConvertLogLevel(LogLevel logLevel) + public void SetContext(string requestId, string invocationId) { - switch (logLevel) - { - case LogLevel.Critical: - return RpcLog.Types.Level.Critical; - case LogLevel.Debug: - return RpcLog.Types.Level.Debug; - case LogLevel.Error: - return RpcLog.Types.Level.Error; - case LogLevel.Information: - return RpcLog.Types.Level.Information; - case LogLevel.Trace: - return RpcLog.Types.Level.Trace; - case LogLevel.Warning: - return RpcLog.Types.Level.Warning; - default: - return RpcLog.Types.Level.None; - } + _requestId = requestId; + _invocationId = invocationId; } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs index 22044a04..b037b2b4 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs @@ -15,6 +15,31 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility { public static class TypeExtensions { + public static HttpRequestContext ToHttpContext (this RpcHttp rpcHttp) + { + var httpRequestContext = new HttpRequestContext + { + Method = rpcHttp.Method, + Url = rpcHttp.Url, + OriginalUrl = rpcHttp.Url, + Headers = rpcHttp.Headers, + Params = rpcHttp.Params, + Query = rpcHttp.Query + }; + + if (rpcHttp.Body != null) + { + httpRequestContext.Body = rpcHttp.Body.ToObject(); + } + + if (rpcHttp.RawBody != null) + { + httpRequestContext.Body = rpcHttp.RawBody.ToObject(); + } + + return httpRequestContext; + } + public static object ToObject (this TypedData data) { if (data == null) @@ -45,6 +70,38 @@ public static object ToObject (this TypedData data) } } + public static RpcException ToRpcException (this Exception exception) + { + return new RpcException + { + Message = exception?.Message, + Source = exception?.Source ?? "", + StackTrace = exception?.StackTrace ?? "" + }; + } + + public static RpcHttp ToRpcHttp (this HttpResponseContext httpResponseContext) + { + var rpcHttp = new RpcHttp + { + StatusCode = httpResponseContext.StatusCode + }; + + if (httpResponseContext.Body != null) + { + rpcHttp.Body = httpResponseContext.Body.ToTypedData(); + } + + // Add all the headers. ContentType is separated for convenience + foreach (DictionaryEntry item in httpResponseContext.Headers) + { + rpcHttp.Headers.Add(item.Key.ToString(), item.Value.ToString()); + } + rpcHttp.Headers.Add("content-type", httpResponseContext.ContentType); + + return rpcHttp; + } + public static TypedData ToTypedData(this object value) { TypedData typedData = new TypedData(); @@ -85,62 +142,5 @@ public static TypedData ToTypedData(this object value) } return typedData; } - - public static HttpRequestContext ToHttpContext (this RpcHttp rpcHttp) - { - var httpRequestContext = new HttpRequestContext - { - Method = rpcHttp.Method, - Url = rpcHttp.Url, - OriginalUrl = rpcHttp.Url, - Headers = rpcHttp.Headers, - Params = rpcHttp.Params, - Query = rpcHttp.Query - }; - - if (rpcHttp.Body != null) - { - httpRequestContext.Body = rpcHttp.Body.ToObject(); - } - - if (rpcHttp.RawBody != null) - { - httpRequestContext.Body = rpcHttp.RawBody.ToObject(); - } - - return httpRequestContext; - } - - public static RpcHttp ToRpcHttp (this HttpResponseContext httpResponseContext) - { - var rpcHttp = new RpcHttp - { - StatusCode = httpResponseContext.StatusCode - }; - - if (httpResponseContext.Body != null) - { - rpcHttp.Body = httpResponseContext.Body.ToTypedData(); - } - - // Add all the headers. ContentType is separated for convenience - foreach (DictionaryEntry item in httpResponseContext.Headers) - { - rpcHttp.Headers.Add(item.Key.ToString(), item.Value.ToString()); - } - rpcHttp.Headers.Add("content-type", httpResponseContext.ContentType); - - return rpcHttp; - } - - public static RpcException ToRpcException (this Exception exception) - { - return new RpcException - { - Message = exception?.Message, - Source = exception?.Source ?? "", - StackTrace = exception?.StackTrace ?? "" - }; - } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Worker.cs b/src/Azure.Functions.PowerShell.Worker/Worker.cs index 5169e197..76f9ae48 100644 --- a/src/Azure.Functions.PowerShell.Worker/Worker.cs +++ b/src/Azure.Functions.PowerShell.Worker/Worker.cs @@ -17,11 +17,30 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { public static class Worker { + static readonly FunctionLoader s_functionLoader = new FunctionLoader(); static FunctionMessagingClient s_client; + static RpcLogger s_logger; static System.Management.Automation.PowerShell s_ps; static Runspace s_runspace; - static readonly FunctionLoader s_FunctionLoader = new FunctionLoader(); - static RpcLogger s_Logger; + + static void InitPowerShell() + { + var host = new AzureFunctionsHost(s_logger); + + s_runspace = RunspaceFactory.CreateRunspace(host); + s_runspace.Open(); + s_ps = System.Management.Automation.PowerShell.Create(InitialSessionState.CreateDefault()); + s_ps.Runspace = s_runspace; + + s_ps.AddScript("$PSHOME"); + //s_ps.AddCommand("Set-ExecutionPolicy").AddParameter("ExecutionPolicy", ExecutionPolicy.Unrestricted).AddParameter("Scope", ExecutionPolicyScope.Process); + s_ps.Invoke(); + + // Add HttpResponseContext namespace so users can reference + // HttpResponseContext without needing to specify the full namespace + s_ps.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}").Invoke(); + s_ps.Commands.Clear(); + } public async static Task Main(string[] args) { @@ -34,7 +53,7 @@ public async static Task Main(string[] args) // Initialize Rpc client, logger, and PowerShell s_client = new FunctionMessagingClient(startupArguments.Host, startupArguments.Port); - s_Logger = new RpcLogger(s_client); + s_logger = new RpcLogger(s_client); InitPowerShell(); // Send StartStream message @@ -48,25 +67,6 @@ public async static Task Main(string[] args) await ProcessEvent(); } - static void InitPowerShell() - { - var host = new AzureFunctionsHost(s_Logger); - - s_runspace = RunspaceFactory.CreateRunspace(host); - s_runspace.Open(); - s_ps = System.Management.Automation.PowerShell.Create(InitialSessionState.CreateDefault()); - s_ps.Runspace = s_runspace; - - s_ps.AddScript("$PSHOME"); - //s_ps.AddCommand("Set-ExecutionPolicy").AddParameter("ExecutionPolicy", ExecutionPolicy.Unrestricted).AddParameter("Scope", ExecutionPolicyScope.Process); - s_ps.Invoke(); - - // Add HttpResponseContext namespace so users can reference - // HttpResponseContext without needing to specify the full namespace - s_ps.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}").Invoke(); - s_ps.Commands.Clear(); - } - static async Task ProcessEvent() { using (s_client) @@ -80,25 +80,25 @@ static async Task ProcessEvent() case StreamingMessage.ContentOneofCase.WorkerInitRequest: response = HandleWorkerInitRequest.Invoke( s_ps, - s_FunctionLoader, + s_functionLoader, message, - s_Logger); + s_logger); break; case StreamingMessage.ContentOneofCase.FunctionLoadRequest: response = HandleFunctionLoadRequest.Invoke( s_ps, - s_FunctionLoader, + s_functionLoader, message, - s_Logger); + s_logger); break; case StreamingMessage.ContentOneofCase.InvocationRequest: response = HandleInvocationRequest.Invoke( s_ps, - s_FunctionLoader, + s_functionLoader, message, - s_Logger); + s_logger); break; default: From ffd4aa378922328a668539aa003bb172a3b964e6 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Fri, 24 Aug 2018 18:08:51 -0700 Subject: [PATCH 8/8] added a bunch of tests --- .../Function/FunctionInfo.cs | 14 +- .../Http/HttpRequestContext.cs | 15 +- .../Http/HttpResponseContext.cs | 23 +- .../PowerShell/Host/AzureFunctionsHost.cs | 4 +- .../StartupArguments.cs | 6 + .../Utility/TypeExtensions.cs | 35 +- .../Worker.cs | 7 +- .../Function/FunctionLoaderTests.cs | 137 ++++++ .../Requests/HandleWorkerInitRequestTests.cs | 6 +- .../StartupArgumentsTests.cs | 40 ++ .../Utility/TypeConverterTests.cs | 9 - .../Utility/TypeExtensionsTests.cs | 412 ++++++++++++++++++ 12 files changed, 659 insertions(+), 49 deletions(-) create mode 100644 test/Azure.Functions.PowerShell.Worker.Test/Function/FunctionLoaderTests.cs create mode 100644 test/Azure.Functions.PowerShell.Worker.Test/StartupArgumentsTests.cs delete mode 100644 test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeConverterTests.cs create mode 100644 test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeExtensionsTests.cs diff --git a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs index 977bf259..14a872d4 100644 --- a/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs @@ -10,18 +10,18 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { public class FunctionInfo { - public MapField Bindings {get; private set;} - public string Directory {get; private set;} - public string HttpOutputName {get; private set;} - public string Name {get; private set;} - public MapField OutputBindings {get; private set;} + public string Directory {get; set;} + public string HttpOutputName {get; set;} + public string Name {get; set;} + public MapField Bindings { get; } = new MapField(); + public MapField OutputBindings { get; } = new MapField(); + + public FunctionInfo() { } public FunctionInfo(RpcFunctionMetadata metadata) { Name = metadata.Name; Directory = metadata.Directory; - Bindings = new MapField(); - OutputBindings = new MapField(); HttpOutputName = ""; foreach (var binding in metadata.Bindings) diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs index f88e54e8..0a1a7e02 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs @@ -3,19 +3,30 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using Google.Protobuf.Collections; namespace Microsoft.Azure.Functions.PowerShellWorker { - public class HttpRequestContext + public class HttpRequestContext : IEquatable { public object Body {get; set;} public MapField Headers {get; set;} public string Method {get; set;} public string Url {get; set;} - public string OriginalUrl {get; set;} public MapField Params {get; set;} public MapField Query {get; set;} public object RawBody {get; set;} + + public bool Equals(HttpRequestContext other) + { + return Method == other.Method + && Url == other.Url + && Headers.Equals(other.Headers) + && Params.Equals(other.Params) + && Query.Equals(other.Query) + && (Body == other.Body || Body.Equals(other.Body)) + && (RawBody == other.RawBody || RawBody.Equals(other.RawBody)); + } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs index 2b722861..b76ba180 100644 --- a/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs @@ -3,16 +3,37 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Collections; namespace Microsoft.Azure.Functions.PowerShellWorker { - public class HttpResponseContext + public class HttpResponseContext : IEquatable { public object Body {get; set;} public string ContentType {get; set;} = "text/plain"; public bool EnableContentNegotiation {get; set;} = false; public Hashtable Headers {get; set;} = new Hashtable(); public string StatusCode {get; set;} = "200"; + + public bool Equals(HttpResponseContext other) + { + bool sameHeaders = true; + foreach (DictionaryEntry dictionaryEntry in Headers) + { + if (!other.Headers.ContainsKey(dictionaryEntry.Key) + || dictionaryEntry.Value != other.Headers[dictionaryEntry.Key]) + { + sameHeaders = false; + break; + } + } + + return ContentType == other.ContentType + && EnableContentNegotiation == other.EnableContentNegotiation + && StatusCode == other.StatusCode + && sameHeaders + && (Body == other.Body || Body.Equals(other.Body)); + } } } \ No newline at end of file diff --git a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs index 957c1e06..d3faebcd 100644 --- a/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs @@ -16,7 +16,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell.Host /// applications. Not all members are implemented. Those that aren't throw a /// NotImplementedException. /// - class AzureFunctionsHost : PSHost + class AzureFunctionsPowerShellHost : PSHost { /// /// The private reference of the logger. @@ -82,7 +82,7 @@ class AzureFunctionsHost : PSHost /// public override Version Version => new Version(1, 0, 0, 0); - public AzureFunctionsHost(RpcLogger logger) + public AzureFunctionsPowerShellHost(RpcLogger logger) { _logger = logger; HostUI = new HostUserInterface(logger); diff --git a/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs index fd793f47..ee865564 100644 --- a/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs +++ b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs @@ -17,6 +17,12 @@ public class StartupArguments public static StartupArguments Parse(string[] args) { + if (args.Length != 10) + { + Console.WriteLine("usage --host --port --workerId --requestId --grpcMaxMessageLength "); + throw new InvalidOperationException("Incorrect startup arguments were given."); + } + StartupArguments arguments = new StartupArguments(); for (int i = 1; i < 10; i+=2) { diff --git a/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs index b037b2b4..5fd0f565 100644 --- a/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs +++ b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs @@ -15,13 +15,12 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility { public static class TypeExtensions { - public static HttpRequestContext ToHttpContext (this RpcHttp rpcHttp) + static HttpRequestContext ToHttpRequestContext (this RpcHttp rpcHttp) { var httpRequestContext = new HttpRequestContext { Method = rpcHttp.Method, Url = rpcHttp.Url, - OriginalUrl = rpcHttp.Url, Headers = rpcHttp.Headers, Params = rpcHttp.Params, Query = rpcHttp.Query @@ -34,7 +33,7 @@ public static HttpRequestContext ToHttpContext (this RpcHttp rpcHttp) if (rpcHttp.RawBody != null) { - httpRequestContext.Body = rpcHttp.RawBody.ToObject(); + httpRequestContext.RawBody = rpcHttp.RawBody.ToObject(); } return httpRequestContext; @@ -52,15 +51,15 @@ public static object ToObject (this TypedData data) case TypedData.DataOneofCase.Json: return JsonConvert.DeserializeObject(data.Json); case TypedData.DataOneofCase.Bytes: - return data.Bytes; + return data.Bytes.ToByteArray(); case TypedData.DataOneofCase.Double: return data.Double; case TypedData.DataOneofCase.Http: - return data.Http.ToHttpContext(); + return data.Http.ToHttpRequestContext(); case TypedData.DataOneofCase.Int: return data.Int; case TypedData.DataOneofCase.Stream: - return data.Stream; + return data.Stream.ToByteArray(); case TypedData.DataOneofCase.String: return data.String; case TypedData.DataOneofCase.None: @@ -80,7 +79,7 @@ public static RpcException ToRpcException (this Exception exception) }; } - public static RpcHttp ToRpcHttp (this HttpResponseContext httpResponseContext) + static RpcHttp ToRpcHttp (this HttpResponseContext httpResponseContext) { var rpcHttp = new RpcHttp { @@ -97,7 +96,12 @@ public static RpcHttp ToRpcHttp (this HttpResponseContext httpResponseContext) { rpcHttp.Headers.Add(item.Key.ToString(), item.Value.ToString()); } - rpcHttp.Headers.Add("content-type", httpResponseContext.ContentType); + + // Allow the user to set content-type in the Headers + if (!rpcHttp.Headers.ContainsKey("content-type")) + { + rpcHttp.Headers.Add("content-type", httpResponseContext.ContentType); + } return rpcHttp; } @@ -111,29 +115,26 @@ public static TypedData ToTypedData(this object value) return typedData; } - if (LanguagePrimitives.TryConvertTo( - value, out byte[] arr)) + if (LanguagePrimitives.TryConvertTo(value, out byte[] arr)) { typedData.Bytes = ByteString.CopyFrom(arr); } - else if(LanguagePrimitives.TryConvertTo( - value, out HttpResponseContext http)) + else if(LanguagePrimitives.TryConvertTo(value, out HttpResponseContext http)) { typedData.Http = http.ToRpcHttp(); } - else if (LanguagePrimitives.TryConvertTo( - value, out Hashtable hashtable)) + else if (LanguagePrimitives.TryConvertTo(value, out Hashtable hashtable)) { typedData.Json = JsonConvert.SerializeObject(hashtable); } - else if (LanguagePrimitives.TryConvertTo( - value, out string str)) + else if (LanguagePrimitives.TryConvertTo(value, out string str)) { // Attempt to parse the string into json. If it fails, // fallback to storing as a string try { - typedData.Json = JsonConvert.SerializeObject(str); + JsonConvert.DeserializeObject(str); + typedData.Json = str; } catch { diff --git a/src/Azure.Functions.PowerShell.Worker/Worker.cs b/src/Azure.Functions.PowerShell.Worker/Worker.cs index 76f9ae48..395cf27d 100644 --- a/src/Azure.Functions.PowerShell.Worker/Worker.cs +++ b/src/Azure.Functions.PowerShell.Worker/Worker.cs @@ -25,7 +25,7 @@ public static class Worker static void InitPowerShell() { - var host = new AzureFunctionsHost(s_logger); + var host = new AzureFunctionsPowerShellHost(s_logger); s_runspace = RunspaceFactory.CreateRunspace(host); s_runspace.Open(); @@ -44,11 +44,6 @@ static void InitPowerShell() public async static Task Main(string[] args) { - if (args.Length != 10) - { - Console.WriteLine("usage --host --port --workerId --requestId --grpcMaxMessageLength "); - return; - } StartupArguments startupArguments = StartupArguments.Parse(args); // Initialize Rpc client, logger, and PowerShell diff --git a/test/Azure.Functions.PowerShell.Worker.Test/Function/FunctionLoaderTests.cs b/test/Azure.Functions.PowerShell.Worker.Test/Function/FunctionLoaderTests.cs new file mode 100644 index 00000000..1434c1f2 --- /dev/null +++ b/test/Azure.Functions.PowerShell.Worker.Test/Function/FunctionLoaderTests.cs @@ -0,0 +1,137 @@ +using System; +using Microsoft.Azure.Functions.PowerShellWorker; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Xunit; + +namespace Azure.Functions.PowerShell.Worker.Test +{ + public class FunctionLoaderTests + { + [Fact] + public void TestFunctionLoaderGetFunc() + { + var functionId = Guid.NewGuid().ToString(); + var directory = "/Users/tylerleonhardt/Desktop/Tech/PowerShell/AzureFunctions/azure-functions-powershell-worker/examples/PSCoreApp/MyHttpTrigger"; + var scriptPathExpected = $"{directory}/run.ps1"; + var metadata = new RpcFunctionMetadata + { + Name = "MyHttpTrigger", + EntryPoint = "", + Directory = directory, + ScriptFile = scriptPathExpected + }; + metadata.Bindings.Add("req", new BindingInfo + { + Direction = BindingInfo.Types.Direction.In, + Type = "httpTrigger" + }); + metadata.Bindings.Add("res", new BindingInfo + { + Direction = BindingInfo.Types.Direction.Out, + Type = "http" + }); + + var functionLoader = new FunctionLoader(); + functionLoader.Load(functionId, metadata); + + (string scriptPathResult, string entryPointResult) = functionLoader.GetFunc(functionId); + + Assert.Equal(scriptPathExpected, scriptPathResult); + Assert.Equal("", entryPointResult); + } + + [Fact] + public void TestFunctionLoaderGetFuncWithEntryPoint() + { + var functionId = Guid.NewGuid().ToString(); + var directory = "/Users/tylerleonhardt/Desktop/Tech/PowerShell/AzureFunctions/azure-functions-powershell-worker/examples/PSCoreApp/MyHttpTrigger"; + var scriptPathExpected = $"{directory}/run.ps1"; + var entryPointExpected = "Foo"; + var metadata = new RpcFunctionMetadata + { + Name = "MyHttpTrigger", + EntryPoint = entryPointExpected, + Directory = directory, + ScriptFile = scriptPathExpected + }; + metadata.Bindings.Add("req", new BindingInfo + { + Direction = BindingInfo.Types.Direction.In, + Type = "httpTrigger" + }); + metadata.Bindings.Add("res", new BindingInfo + { + Direction = BindingInfo.Types.Direction.Out, + Type = "http" + }); + + var functionLoader = new FunctionLoader(); + functionLoader.Load(functionId, metadata); + + (string scriptPathResult, string entryPointResult) = functionLoader.GetFunc(functionId); + + Assert.Equal(scriptPathExpected, scriptPathResult); + Assert.Equal(entryPointExpected, entryPointResult); + } + + [Fact] + public void TestFunctionLoaderGetInfo() + { + var functionId = Guid.NewGuid().ToString(); + var directory = "/Users/tylerleonhardt/Desktop/Tech/PowerShell/AzureFunctions/azure-functions-powershell-worker/examples/PSCoreApp/MyHttpTrigger"; + var scriptPathExpected = $"{directory}/run.ps1"; + var name = "MyHttpTrigger"; + var metadata = new RpcFunctionMetadata + { + Name = name, + EntryPoint = "", + Directory = directory, + ScriptFile = scriptPathExpected + }; + metadata.Bindings.Add("req", new BindingInfo + { + Direction = BindingInfo.Types.Direction.In, + Type = "httpTrigger" + }); + metadata.Bindings.Add("res", new BindingInfo + { + Direction = BindingInfo.Types.Direction.Out, + Type = "http" + }); + + var infoExpected = new FunctionInfo + { + Directory = directory, + HttpOutputName = "", + Name = name + }; + infoExpected.Bindings.Add("req", new BindingInfo + { + Direction = BindingInfo.Types.Direction.In, + Type = "httpTrigger" + }); + infoExpected.Bindings.Add("res", new BindingInfo + { + Direction = BindingInfo.Types.Direction.Out, + Type = "http" + }); + + infoExpected.OutputBindings.Add("res", new BindingInfo + { + Direction = BindingInfo.Types.Direction.Out, + Type = "http" + }); + + var functionLoader = new FunctionLoader(); + functionLoader.Load(functionId, metadata); + + var infoResult = functionLoader.GetInfo(functionId); + + Assert.Equal(directory, infoResult.Directory); + Assert.Equal("res", infoResult.HttpOutputName); + Assert.Equal(name, infoResult.Name); + Assert.Equal(infoExpected.Bindings.Count, infoResult.Bindings.Count); + Assert.Equal(infoExpected.OutputBindings.Count, infoResult.OutputBindings.Count); + } + } +} diff --git a/test/Azure.Functions.PowerShell.Worker.Test/Requests/HandleWorkerInitRequestTests.cs b/test/Azure.Functions.PowerShell.Worker.Test/Requests/HandleWorkerInitRequestTests.cs index 521cea05..c2778586 100644 --- a/test/Azure.Functions.PowerShell.Worker.Test/Requests/HandleWorkerInitRequestTests.cs +++ b/test/Azure.Functions.PowerShell.Worker.Test/Requests/HandleWorkerInitRequestTests.cs @@ -1,14 +1,10 @@ -using System; using Microsoft.Azure.Functions.PowerShellWorker.Requests; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using Xunit; namespace Azure.Functions.PowerShell.Worker.Test { - using System.Management.Automation; - using System.Management.Automation.Runspaces; - using Microsoft.Azure.Functions.PowerShellWorker.Utility; - public class HandleWorkerInitRequestTests { [Fact] diff --git a/test/Azure.Functions.PowerShell.Worker.Test/StartupArgumentsTests.cs b/test/Azure.Functions.PowerShell.Worker.Test/StartupArgumentsTests.cs new file mode 100644 index 00000000..93ab0053 --- /dev/null +++ b/test/Azure.Functions.PowerShell.Worker.Test/StartupArgumentsTests.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.Azure.Functions.PowerShellWorker; +using Xunit; + +namespace Azure.Functions.PowerShell.Worker.Test +{ + public class StartupArgumentsTests + { + [Fact] + public void TestStartupArumentsParse() + { + var host = "0.0.0.0"; + var port = 1234; + var workerId = Guid.NewGuid().ToString(); + var requestId = Guid.NewGuid().ToString(); + var grpcMaxMessageLength = 100; + var args = $"--host {host} --port {port} --workerId {workerId} --requestId {requestId} --grpcMaxMessageLength {grpcMaxMessageLength}"; + + var startupArguments = StartupArguments.Parse(args.Split(' ')); + + Assert.Equal(host, startupArguments.Host); + Assert.Equal(port, startupArguments.Port); + Assert.Equal(workerId, startupArguments.WorkerId); + Assert.Equal(requestId, startupArguments.RequestId); + Assert.Equal(grpcMaxMessageLength, startupArguments.GrpcMaxMessageLength); + } + + [Fact] + public void TestStartupArumentsParseThrows() + { + var host = "0.0.0.0"; + var port = 1234; + var workerId = Guid.NewGuid().ToString(); + var requestId = Guid.NewGuid().ToString(); + var args = $"--host {host} --port {port} --workerId {workerId} --requestId {requestId} --grpcMaxMessageLength"; + + Assert.Throws(() => StartupArguments.Parse(args.Split(' '))); + } + } +} diff --git a/test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeConverterTests.cs b/test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeConverterTests.cs deleted file mode 100644 index 7d690934..00000000 --- a/test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeConverterTests.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xunit; - -namespace Azure.Functions.PowerShell.Worker.Test -{ - public class TypeConverterTests - { - - } -} \ No newline at end of file diff --git a/test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeExtensionsTests.cs b/test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeExtensionsTests.cs new file mode 100644 index 00000000..24a72fbe --- /dev/null +++ b/test/Azure.Functions.PowerShell.Worker.Test/Utility/TypeExtensionsTests.cs @@ -0,0 +1,412 @@ +using System; +using System.Collections; + +using Google.Protobuf; +using Google.Protobuf.Collections; +using Microsoft.Azure.Functions.PowerShellWorker; +using Microsoft.Azure.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Newtonsoft.Json; +using Xunit; + +namespace Azure.Functions.PowerShell.Worker.Test +{ + public class TypeExtensionsTests + { + #region TypedDataToObject + [Fact] + public void TestTypedDataToObjectHttpRequestContextBasic() + { + var method = "Get"; + var url = "https://example.com"; + + var input = new TypedData + { + Http = new RpcHttp + { + Method = method, + Url = url + } + }; + + var expected = new HttpRequestContext + { + Method = method, + Url = url, + Headers = new MapField(), + Params = new MapField(), + Query = new MapField() + }; + + Assert.Equal(expected, (HttpRequestContext)input.ToObject()); + } + + [Fact] + public void TestTypedDataToObjectHttpRequestContextWithUrlData() + { + var method = "Get"; + var url = "https://example.com"; + var key = "foo"; + var value = "bar"; + + var input = new TypedData + { + Http = new RpcHttp + { + Method = method, + Url = url + } + }; + input.Http.Headers.Add(key, value); + input.Http.Params.Add(key, value); + input.Http.Query.Add(key, value); + + var expected = new HttpRequestContext + { + Method = method, + Url = url, + Headers = new MapField + { + {key, value} + }, + Params = new MapField + { + {key, value} + }, + Query = new MapField + { + {key, value} + } + }; + + Assert.Equal(expected, (HttpRequestContext)input.ToObject()); + } + + [Fact] + public void TestTypedDataToObjectHttpRequestContextBodyData() + { + var method = "Get"; + var url = "https://example.com"; + var data = "Hello World"; + + var input = new TypedData + { + Http = new RpcHttp + { + Method = method, + Url = url, + Body = new TypedData + { + String = data + }, + RawBody = new TypedData + { + String = data + } + } + }; + + var expected = new HttpRequestContext + { + Method = method, + Url = url, + Headers = new MapField(), + Params = new MapField(), + Query = new MapField(), + Body = data, + RawBody = data + }; + + Assert.Equal(expected, (HttpRequestContext)input.ToObject()); + } + + [Fact] + public void TestTypedDataToObjectString() + { + var data = "Hello World"; + + var input = new TypedData { String = data }; + var expected = data; + + Assert.Equal(expected, (string)input.ToObject()); + } + + [Fact] + public void TestTypedDataToObjectInt() + { + long data = 2; + + var input = new TypedData { Int = data }; + var expected = data; + + Assert.Equal(expected, (long)input.ToObject()); + } + + [Fact] + public void TestTypedDataToObjectDouble() + { + var data = 2.2; + + var input = new TypedData { Double = data }; + var expected = data; + + Assert.Equal(expected, (double)input.ToObject()); + } + + [Fact] + public void TestTypedDataToObjectJson() + { + var data = "{\"Foo\":\"Bar\"}"; + + var input = new TypedData { Json = data }; + var expected = JsonConvert.DeserializeObject(data); + var actual = (Hashtable)input.ToObject(); + Assert.Equal((string)expected["Foo"], (string)actual["Foo"]); + } + + [Fact] + public void TestTypedDataToObjectBytes() + { + var data = ByteString.CopyFromUtf8("Hello World"); + + var input = new TypedData { Bytes = data }; + var expected = data.ToByteArray(); + + Assert.Equal(expected, (byte[])input.ToObject()); + } + + [Fact] + public void TestTypedDataToObjectStream() + { + var data = ByteString.CopyFromUtf8("Hello World"); + + var input = new TypedData { Stream = data }; + var expected = data.ToByteArray(); + + Assert.Equal(expected, (byte[])input.ToObject()); + } + #endregion + #region ExceptionToRpcException + [Fact] + public void TestExceptionToRpcExceptionBasic() + { + var data = "bad"; + + var input = new Exception(data); + var expected = new RpcException + { + Message = "bad" + }; + + Assert.Equal(expected, input.ToRpcException()); + } + + [Fact] + public void TestExceptionToRpcExceptionExtraData() + { + var data = "bad"; + + var input = new Exception(data); + input.Source = data; + + var expected = new RpcException + { + Message = data, + Source = data + }; + + Assert.Equal(expected, input.ToRpcException()); + } + #endregion + #region ObjectToTypedData + [Fact] + public void TestObjectToTypedDataRpcHttpBasic() + { + var data = "Hello World"; + + var input = new HttpResponseContext + { + Body = data + }; + var expected = new TypedData + { + Http = new RpcHttp + { + StatusCode = "200", + Body = new TypedData { String = data }, + Headers = { { "content-type", "text/plain" } } + } + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataRpcHttpContentTypeSet() + { + var data = ""; + + var input = new HttpResponseContext + { + Body = data, + ContentType = "text/html" + }; + var expected = new TypedData + { + Http = new RpcHttp + { + StatusCode = "200", + Body = new TypedData { String = data }, + Headers = { { "content-type", "text/html" } } + } + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataRpcHttpContentTypeInHeader() + { + var data = ""; + + var input = new HttpResponseContext + { + Body = data, + Headers = { { "content-type", "text/html" } } + }; + var expected = new TypedData + { + Http = new RpcHttp + { + StatusCode = "200", + Body = new TypedData { String = data }, + Headers = { { "content-type", "text/html" } } + } + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataRpcHttpStatusCodeString() + { + var data = "Hello World"; + + var input = new HttpResponseContext + { + Body = data, + StatusCode = "201" + }; + var expected = new TypedData + { + Http = new RpcHttp + { + StatusCode = "201", + Body = new TypedData { String = data }, + Headers = { { "content-type", "text/plain" } } + } + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataInt() + { + var data = (long)1; + + var input = (object)data; + var expected = new TypedData + { + Int = data + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataDouble() + { + var data = 1.1; + + var input = (object)data; + var expected = new TypedData + { + Double = data + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataString() + { + var data = "Hello World!"; + + var input = (object)data; + var expected = new TypedData + { + String = data + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataBytes() + { + var data = ByteString.CopyFromUtf8("Hello World!").ToByteArray(); + + var input = (object)data; + var expected = new TypedData + { + Bytes = ByteString.CopyFrom(data) + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataStream() + { + var data = ByteString.CopyFromUtf8("Hello World!").ToByteArray(); + + var input = (object)data; + var expected = new TypedData + { + Stream = ByteString.CopyFrom(data) + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataJsonString() + { + var data = "{\"foo\":\"bar\"}"; + + var input = (object)data; + var expected = new TypedData + { + Json = data + }; + + Assert.Equal(expected, input.ToTypedData()); + } + + [Fact] + public void TestObjectToTypedDataJsonHashtable() + { + var data = new Hashtable { { "foo", "bar" } }; + + var input = (object)data; + var expected = new TypedData + { + Json = "{\"foo\":\"bar\"}" + }; + + Assert.Equal(expected, input.ToTypedData()); + } + #endregion + } +} \ No newline at end of file