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"
- }
-}