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..121371d6 --- /dev/null +++ b/examples/PSCoreApp/MyHttpTrigger/run.ps1 @@ -0,0 +1,12 @@ +$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/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..e1257581 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker.Messaging/FunctionMessagingClient.cs @@ -0,0 +1,59 @@ +// +// 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; + +namespace Azure.Functions.PowerShell.Worker.Messaging +{ + public class FunctionMessagingClient : IDisposable + { + SemaphoreSlim _writeStreamHandle = new SemaphoreSlim(1, 1); + AsyncDuplexStreamingCall _call; + public bool isDisposed; + + public FunctionMessagingClient(string host, int port) + { + Channel channel = new Channel(host, port, ChannelCredentials.Insecure); + _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; + + // 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 + { + await _call.RequestStream.WriteAsync(message); + } + finally + { + _writeStreamHandle.Release(); + } + } + } +} \ 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/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj b/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj new file mode 100644 index 00000000..bfe63885 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Azure.Functions.PowerShell.Worker.csproj @@ -0,0 +1,28 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + + + + + + + + PreserveNewest + + + + + latest + + + 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..14a872d4 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionInfo.cs @@ -0,0 +1,43 @@ +// +// 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 +{ + public class FunctionInfo + { + 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; + HttpOutputName = ""; + + foreach (var binding in metadata.Bindings) + { + 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; + } + 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..f45bbf50 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Function/FunctionLoader.cs @@ -0,0 +1,39 @@ +// +// 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 +{ + 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. + // 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 class Function + { + public string EntryPoint {get; internal set;} + public FunctionInfo Info {get; internal set;} + public string ScriptPath {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 new file mode 100644 index 00000000..0a1a7e02 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpRequestContext.cs @@ -0,0 +1,32 @@ +// +// 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 Google.Protobuf.Collections; + +namespace Microsoft.Azure.Functions.PowerShellWorker +{ + 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 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 new file mode 100644 index 00000000..b76ba180 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Http/HttpResponseContext.cs @@ -0,0 +1,39 @@ +// +// 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; + +namespace Microsoft.Azure.Functions.PowerShellWorker +{ + 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 new file mode 100644 index 00000000..d3faebcd --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/AzureFunctionsHost.cs @@ -0,0 +1,128 @@ +// +// 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 +{ + /// + /// A sample implementation of the PSHost abstract class for console + /// applications. Not all members are implemented. Those that aren't throw a + /// NotImplementedException. + /// + class AzureFunctionsPowerShellHost : PSHost + { + /// + /// The private reference of the logger. + /// + RpcLogger _logger { get; set; } + + /// + /// Creates an instance of the PSHostUserInterface object for this + /// application. + /// + HostUserInterface HostUI { get; set; } + + /// + /// The culture info of the thread that created + /// this object. + /// + readonly CultureInfo originalCultureInfo = System.Threading.Thread.CurrentThread.CurrentCulture; + + /// + /// The UI culture info of the thread that created + /// this object. + /// + readonly CultureInfo originalUICultureInfo = System.Threading.Thread.CurrentThread.CurrentUICulture; + + /// + /// The identifier of the PSHost implementation. + /// + Guid Id = Guid.NewGuid(); + + /// + /// Gets the culture info to use - this implementation just snapshots the + /// curture info of the thread that created this object. + /// + 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 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 AzureFunctionsPowerShellHost(RpcLogger logger) + { + _logger = logger; + HostUI = new HostUserInterface(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."); + + /// + /// 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."); + + /// + /// 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, 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, + /// 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..8c15472d --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/HostUserInterface.cs @@ -0,0 +1,193 @@ +// +// 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; + +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. + /// + class HostUserInterface : PSHostUserInterface + { + /// + /// The private reference of the logger. + /// + RpcLogger _logger { get; set; } + + /// + /// An instance of the PSRawUserInterface object. + /// + readonly 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 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."); + + /// + /// 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 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."); + + /// + /// 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 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."); + + /// + /// 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 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."); + + /// + /// 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() => + 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() => + 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) => + _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 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) => + _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) => + _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) => + _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) => + _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..4b5270a4 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/Host/RawUserInterface.cs @@ -0,0 +1,184 @@ +// +// 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; + +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. + /// + 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/PowerShell/PowerShellWorkerExtensions.cs b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs new file mode 100644 index 00000000..294f8f78 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/PowerShell/PowerShellWorkerExtensions.cs @@ -0,0 +1,137 @@ +// +// 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; + +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. + + readonly static string s_LogAndSetReturnValueScript = @" +param([Parameter(ValueFromPipeline=$true)]$return) + +$return | Out-Default + +Set-Variable -Name '$return' -Value $return -Scope global +"; + + 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 + static void CleanupRunspace(this PowerShell ps) + { + ps.Commands.Clear(); + } + + 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; + } + } + + 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; + } + } + } +} \ 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 new file mode 100644 index 00000000..8b388f16 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleFunctionLoadRequest.cs @@ -0,0 +1,53 @@ +// +// 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; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Requests +{ + using System.Management.Automation; + + public static class HandleFunctionLoadRequest + { + public static StreamingMessage Invoke( + PowerShell powershell, + FunctionLoader functionLoader, + StreamingMessage request, + RpcLogger logger) + { + FunctionLoadRequest functionLoadRequest = request.FunctionLoadRequest; + + // 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 = status + } + }; + } + } +} \ 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..505f4969 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleInvocationRequest.cs @@ -0,0 +1,91 @@ +// +// 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.Functions.PowerShellWorker.Utility; +using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Requests +{ + using System.Management.Automation; + + public static class HandleInvocationRequest + { + public static StreamingMessage Invoke( + PowerShell powershell, + FunctionLoader functionLoader, + StreamingMessage request, + 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() + { + RequestId = request.RequestId, + InvocationResponse = new InvocationResponse() + { + InvocationId = invocationRequest.InvocationId, + Result = status + } + }; + + // Invoke powershell logic and return hashtable of out binding data + Hashtable result = null; + try + { + result = powershell + .SetGlobalVariables(triggerMetadata, invocationRequest.InputData) + .InvokeFunctionAndSetGlobalReturn(scriptPath, entryPoint) + .ReturnBindingHashtable(functionInfo.OutputBindings); + } + catch (Exception e) + { + status.Status = StatusResult.Types.Status.Failure; + status.Exception = e.ToRpcException(); + return response; + } + + // 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 = 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; + } + } + + 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..1fef511d --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Requests/HandleWorkerInitRequest.cs @@ -0,0 +1,34 @@ +// +// 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; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Requests +{ + using System.Management.Automation; + + public static class HandleWorkerInitRequest + { + public static StreamingMessage Invoke( + PowerShell powershell, + FunctionLoader functionLoader, + StreamingMessage request, + RpcLogger logger) + { + return new StreamingMessage() + { + RequestId = request.RequestId, + WorkerInitResponse = new WorkerInitResponse() + { + Result = new StatusResult() + { + Status = StatusResult.Types.Status.Success + } + } + }; + } + } +} \ 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..ee865564 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/StartupArguments.cs @@ -0,0 +1,44 @@ +// +// 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 +{ + 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) + { + 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) + { + 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(); + } + } + + return arguments; + } + } +} \ 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..5243a8cc --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Utility/RpcLogger.cs @@ -0,0 +1,78 @@ +// +// 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; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Utility +{ + public class RpcLogger : ILogger + { + FunctionMessagingClient _client; + string _invocationId = ""; + string _requestId = ""; + + public RpcLogger(FunctionMessagingClient client) + { + _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) + { + var logMessage = new StreamingMessage + { + RequestId = _requestId, + RpcLog = new RpcLog() + { + Exception = exception?.ToRpcException(), + InvocationId = _invocationId, + Level = ConvertLogLevel(logLevel), + Message = formatter(state, exception) + } + }; + + await _client.WriteAsync(logMessage); + } + } + + public void SetContext(string requestId, string invocationId) + { + _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 new file mode 100644 index 00000000..5fd0f565 --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Utility/TypeExtensions.cs @@ -0,0 +1,147 @@ +// +// 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.WebJobs.Script.Grpc.Messages; +using Newtonsoft.Json; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Utility +{ + public static class TypeExtensions + { + static HttpRequestContext ToHttpRequestContext (this RpcHttp rpcHttp) + { + var httpRequestContext = new HttpRequestContext + { + Method = rpcHttp.Method, + Url = 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.RawBody = rpcHttp.RawBody.ToObject(); + } + + return httpRequestContext; + } + + public static object ToObject (this TypedData data) + { + if (data == null) + { + return null; + } + + switch (data.DataCase) + { + case TypedData.DataOneofCase.Json: + return JsonConvert.DeserializeObject(data.Json); + case TypedData.DataOneofCase.Bytes: + return data.Bytes.ToByteArray(); + case TypedData.DataOneofCase.Double: + return data.Double; + case TypedData.DataOneofCase.Http: + return data.Http.ToHttpRequestContext(); + case TypedData.DataOneofCase.Int: + return data.Int; + case TypedData.DataOneofCase.Stream: + return data.Stream.ToByteArray(); + case TypedData.DataOneofCase.String: + return data.String; + case TypedData.DataOneofCase.None: + return null; + default: + return new InvalidOperationException("Data Case was not set."); + } + } + + public static RpcException ToRpcException (this Exception exception) + { + return new RpcException + { + Message = exception?.Message, + Source = exception?.Source ?? "", + StackTrace = exception?.StackTrace ?? "" + }; + } + + 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()); + } + + // 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; + } + + public static TypedData ToTypedData(this object value) + { + TypedData typedData = new TypedData(); + + if (value == null) + { + return typedData; + } + + if (LanguagePrimitives.TryConvertTo(value, out byte[] arr)) + { + typedData.Bytes = ByteString.CopyFrom(arr); + } + else if(LanguagePrimitives.TryConvertTo(value, out HttpResponseContext http)) + { + typedData.Http = http.ToRpcHttp(); + } + else if (LanguagePrimitives.TryConvertTo(value, out Hashtable hashtable)) + { + typedData.Json = JsonConvert.SerializeObject(hashtable); + } + 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 + { + JsonConvert.DeserializeObject(str); + typedData.Json = str; + } + catch + { + typedData.String = str; + } + } + return typedData; + } + } +} \ 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..395cf27d --- /dev/null +++ b/src/Azure.Functions.PowerShell.Worker/Worker.cs @@ -0,0 +1,108 @@ +// +// 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.Runspaces; + +using Azure.Functions.PowerShell.Worker.Messaging; +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 +{ + 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 void InitPowerShell() + { + var host = new AzureFunctionsPowerShellHost(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) + { + 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 } + }; + + await s_client.WriteAsync(streamingMessage); + + await ProcessEvent(); + } + + static async Task ProcessEvent() + { + using (s_client) + { + while (await s_client.MoveNext()) + { + var message = s_client.GetCurrentMessage(); + StreamingMessage response; + switch (message.ContentCase) + { + case StreamingMessage.ContentOneofCase.WorkerInitRequest: + response = HandleWorkerInitRequest.Invoke( + s_ps, + s_functionLoader, + message, + s_logger); + break; + + case StreamingMessage.ContentOneofCase.FunctionLoadRequest: + response = HandleFunctionLoadRequest.Invoke( + s_ps, + s_functionLoader, + message, + s_logger); + break; + + case StreamingMessage.ContentOneofCase.InvocationRequest: + response = HandleInvocationRequest.Invoke( + s_ps, + s_functionLoader, + message, + s_logger); + break; + + default: + throw new InvalidOperationException($"Not supportted message type: {message.ContentCase}"); + } + + await s_client.WriteAsync(response); + } + } + } + } +} \ No newline at end of file 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/src/psworker.csproj b/src/psworker.csproj deleted file mode 100644 index 405b0e9c..00000000 --- a/src/psworker.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - Exe - netcoreapp2.1 - Microsoft.Azure.PowerShell.Worker - - - - - - - - - - - - - PreserveNewest - - - - 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..f442707f --- /dev/null +++ b/test/Azure.Functions.PowerShell.Worker.Test/Azure.Functions.PowerShell.Worker.Test.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000..c2778586 --- /dev/null +++ b/test/Azure.Functions.PowerShell.Worker.Test/Requests/HandleWorkerInitRequestTests.cs @@ -0,0 +1,41 @@ +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 +{ + 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/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/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 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" - } -}